Go by Example

Command-Line Subcommands

flag.NewFlagSet creates a per-subcommand flag set, enabling tools with subcommands like "git commit" or "docker run" where each subcommand has its own flags.

flag.NewFlagSet creates an independent flag parser. Dispatch on os.Args[1] to select the subcommand, then call Parse on the matching FlagSet with the remaining arguments.

Create a FlagSet for each subcommand. Each set parses only its own flags, so --output on build does not conflict with --output on deploy.

package main
 
import (
    "flag"
    "fmt"
    "os"
)
 
func main() {
    buildCmd := flag.NewFlagSet("build", flag.ExitOnError)
    buildOutput := buildCmd.String("output", "app", "output binary name")
 
    deployCmd := flag.NewFlagSet("deploy", flag.ExitOnError)
    deployEnv := deployCmd.String("env", "staging", "target environment")
 
    if len(os.Args) < 2 {
        fmt.Println("usage: tool <build|deploy> [flags]")
        os.Exit(1)
    }
 
    switch os.Args[1] {
    case "build":
        buildCmd.Parse(os.Args[2:])
        fmt.Printf("building binary: %s\n", *buildOutput)
    case "deploy":
        deployCmd.Parse(os.Args[2:])
        fmt.Printf("deploying to: %s\n", *deployEnv)
    default:
        fmt.Printf("unknown subcommand: %s\n", os.Args[1])
        os.Exit(1)
    }
}

Each FlagSet exposes its own Args() method returning positional arguments that follow the flags. Subcommand positional arguments do not mix with the top-level os.Args.

package main
 
import (
    "flag"
    "fmt"
    "os"
)
 
func main() {
    addCmd := flag.NewFlagSet("add", flag.ExitOnError)
    force := addCmd.Bool("force", false, "overwrite if exists")
 
    if len(os.Args) < 2 {
        fmt.Fprintln(os.Stderr, "usage: store <add> [flags] <files...>")
        os.Exit(1)
    }
 
    switch os.Args[1] {
    case "add":
        addCmd.Parse(os.Args[2:])
        files := addCmd.Args() // positional args after flags
        fmt.Printf("force: %v, files: %v\n", *force, files)
    default:
        fmt.Fprintf(os.Stderr, "unknown: %s\n", os.Args[1])
        os.Exit(1)
    }
}

In production

The standard library flag.FlagSet approach is sufficient for tools with two or three subcommands. For larger CLIs (nested subcommands, shell completion, manpage generation, rich help formatting), cobra or urfave/cli are the production standard - they are what the Go, Docker, and Kubernetes CLIs use. Structure subcommands as separate packages to keep main thin: each subcommand package exports a Run(args []string) error function, and main dispatches to it. This makes subcommands independently testable without going through os.Args. Always handle the case where os.Args has fewer than two elements before indexing into os.Args[1] - a bare invocation without a subcommand should print usage and exit cleanly, not panic.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free