CLI#

Go exposes command-line arguments through os.Args. The stdlib ships flag for basic parsing; for subcommands and richer UX reach for cobra (Kubernetes, Hugo, GitHub CLI) or urfave/cli (lighter, classic). For options declared as struct tags, kong is the modern alternative.

For the I/O surface scripts read and write through, see I/O. For packaging a Go binary for release, see Runtime.

os.Args#

package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println("program:", os.Args[0])
    for i, a := range os.Args[1:] {
        fmt.Println(i, a)
    }
}
$ go run main.go -v --port 8080 host
program: /tmp/go-build123/main
0 -v
1 --port
2 8080
3 host

flag (stdlib)#

flag is the stdlib argument parser; good for one or two flags, no subcommands.

import "flag"

func main() {
    verbose := flag.Bool("v", false, "verbose output")
    port    := flag.Int("port", 80, "port to probe")
    flag.Parse()                            // parses os.Args[1:]

    host := flag.Arg(0)
    if host == "" {
        fmt.Fprintln(os.Stderr, "usage: scan [-v] [-port N] <host>")
        os.Exit(2)
    }
    fmt.Println(host, *port, *verbose)
}
$ go run main.go -v -port 8080 example.com
example.com 8080 true

For multi-value flags or richer types, implement flag.Value on a custom type.

cobra#

cobra is the default for non-trivial CLIs. Subcommands, --help generation, persistent flags, command groups.

$ go get github.com/spf13/cobra@latest
import "github.com/spf13/cobra"

var (
    port    int
    verbose bool
)

var root = &cobra.Command{
    Use:   "scan <host>",
    Short: "Quick port scanner.",
    Args:  cobra.ExactArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        host := args[0]
        fmt.Println(host, port, verbose)
        return nil
    },
}

func init() {
    root.Flags().IntVarP(&port, "port", "p", 80, "port to probe")
    root.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
}

func main() {
    if err := root.Execute(); err != nil { os.Exit(1) }
}

For subcommands.

var upCmd = &cobra.Command{
    Use:  "up",
    RunE: func(cmd *cobra.Command, args []string) error { return start() },
}
var downCmd = &cobra.Command{
    Use:  "down",
    RunE: func(cmd *cobra.Command, args []string) error { return stop() },
}

func init() { root.AddCommand(upCmd, downCmd) }

kong (declarative)#

kong parses a struct of fields tagged with command-line metadata. Less boilerplate when the options are stable.

$ go get github.com/alecthomas/kong
import "github.com/alecthomas/kong"

var cli struct {
    Verbose bool `short:"v" help:"Verbose output."`
    Port    int  `short:"p" default:"80" help:"Port to probe."`
    Host    string `arg:"" help:"Target host."`
}

func main() {
    _ = kong.Parse(&cli)
    fmt.Println(cli.Host, cli.Port, cli.Verbose)
}

Exit codes#

os.Exit(n) exits with code n. Convention: 0 on success, small positive integer on failure. os.Exit does not run deferred functions; the operator returns from main when possible.

func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1)
    }
}

Streaming#

Filter-style scripts read stdin, transform, write stdout.

sc := bufio.NewScanner(os.Stdin)
w := bufio.NewWriter(os.Stdout)
defer w.Flush()

for sc.Scan() {
    fmt.Fprintln(w, strings.ToUpper(sc.Text()))
}
$ echo -e "one\ntwo" | ./upper
ONE
TWO

Signals and shutdown#

os/signal lets the operator catch SIGINT / SIGTERM and shut down cleanly.

import (
    "context"
    "os/signal"
    "syscall"
)

ctx, stop := signal.NotifyContext(context.Background(),
    syscall.SIGINT, syscall.SIGTERM)
defer stop()

if err := serve(ctx); err != nil { return err }

The signal cancels the context; thread the context into every long call so it tears down on Ctrl-C.

References#