dbzuk
dbzuk

Reputation: 255

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?

Upvotes: 14

Views: 8978

Answers (3)

skaurus
skaurus

Reputation: 1709

Maybe you would be interested in using https://github.com/spf13/cobra - it supports exactly this usecase and many others.

Upvotes: 3

Peter
Peter

Reputation: 31701

Put the global flags before the subcommand:

go run . -required=x foo.

Use flag.Args() instead of os.Args:

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")
    }   

    args := flag.Args() // everything after the -required flag, e.g. [foo, -foo-flag-1, -foo-flag-2, ...]
    switch args[0] {
    case "foo":
        fooCmd.Parse(args[1:])
        fmt.Println("foo")
    case "bar":
        barCmd.Parse(args[1:])
        fmt.Println("bar")
    default:
        log.Fatalf("[ERROR] unknown subcommand '%s', see help for more details.", args[0])
    }   
}

If you want to keep all flags together, after the subcommand, write a helper function that adds common flags to each FlagSet:

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

type globalOpts struct {
    required string
}

func main() {
    var opts globalOpts

    addGlobalFlags(fooCmd, &opts)
    addGlobalFlags(barCmd, &opts)

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

    // ...
}

func addGlobalFlags(fs *flag.FlagSet, opts *globalOpts) {
    fs.StringVar(
        &opts.required,
        "required",
        "",
        "required for all commands",
    )
}

Perhaps you can also combine the two approaches to make the global flags work in any position.

Upvotes: 4

icza
icza

Reputation: 417777

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")
    }
}

Upvotes: 20

Related Questions