Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Global flags and subcommands

I'm implementing a little CLI with multiple subcommands. I'd like to support global flags, that is flags that apply to all subcommands to avoid repeating them.

For example, in the example below I'm trying to have -required flag that is required for all subcommands.

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
)

var (
    required = flag.String(
        "required",
        "",
        "required for all commands",
    )
    fooCmd = flag.NewFlagSet("foo", flag.ExitOnError)
    barCmd = flag.NewFlagSet("bar", flag.ExitOnError)
)

func main() {
    flag.Parse()

    if *required == "" {
        fmt.Println("-required is required for all commands")
    }

    switch os.Args[1] {
    case "foo":
        fooCmd.Parse(os.Args[2:])
        fmt.Println("foo")
    case "bar":
        barCmd.Parse(os.Args[2:])
        fmt.Println("bar")
    default:
        log.Fatalf("[ERROR] unknown subcommand '%s', see help for more details.", os.Args[1])
    }
}

I would expect usage to be like:

$ go run main.go foo -required helloworld

but if I ran that with the above code I get:

$ go run main.go foo -required hello
-required is required for all commands
flag provided but not defined: -required
Usage of foo:
exit status 2

It looks like flag.Parse() is not capturing -required from the CLI, and then the fooCmd is complaining that I've given it a flag it doesn't recognize.

What's the easiest way to have subcommands with global flags in Golang?

like image 286
dbzuk Avatar asked Dec 23 '22 15:12

dbzuk


1 Answers

If you intend to implement subcommands, you shouldn't call flag.Parse().

Instead decide which subcommand to use (as you did with os.Args[1]), and call only its FlagSet.Parse() method.

Yes, for this to work, all flag sets should contain the common flags. But it's easy to register them once (in one place). Create a package level variable:

var (
    required string

    fooCmd = flag.NewFlagSet("foo", flag.ExitOnError)
    barCmd = flag.NewFlagSet("bar", flag.ExitOnError)
)

And use a loop to iterate over all flagsets, and register the common flags, pointing to your variable using FlagSet.StringVar():

func setupCommonFlags() {
    for _, fs := range []*flag.FlagSet{fooCmd, barCmd} {
        fs.StringVar(
            &required,
            "required",
            "",
            "required for all commands",
        )
    }
}

And in main() call Parse() of the appropriate flag set, and test required afterwards:

func main() {
    setupCommonFlags()

    switch os.Args[1] {
    case "foo":
        fooCmd.Parse(os.Args[2:])
        fmt.Println("foo")
    case "bar":
        barCmd.Parse(os.Args[2:])
        fmt.Println("bar")
    default:
        log.Fatalf("[ERROR] unknown subcommand '%s', see help for more details.", os.Args[1])
    }

    if required == "" {
        fmt.Println("-required is required for all commands")
    }
}

You can improve the above solution by creating a map of flag sets, so you can use that map to register common flags, and also to do the parsing.

Full app:

var (
    required string

    fooCmd = flag.NewFlagSet("foo", flag.ExitOnError)
    barCmd = flag.NewFlagSet("bar", flag.ExitOnError)
)

var subcommands = map[string]*flag.FlagSet{
    fooCmd.Name(): fooCmd,
    barCmd.Name(): barCmd,
}

func setupCommonFlags() {
    for _, fs := range subcommands {
        fs.StringVar(
            &required,
            "required",
            "",
            "required for all commands",
        )
    }
}

func main() {
    setupCommonFlags()

    cmd := subcommands[os.Args[1]]
    if cmd == nil {
        log.Fatalf("[ERROR] unknown subcommand '%s', see help for more details.", os.Args[1])
    }

    cmd.Parse(os.Args[2:])
    fmt.Println(cmd.Name())

    if required == "" {
        fmt.Println("-required is required for all commands")
    }
}
like image 146
icza Avatar answered Dec 25 '22 04:12

icza