全局标志和子命令

Global flags and subcommands

我正在实现一个带有多个子命令的小 CLI。我想支持全局标志,即适用于所有子命令的标志,以避免重复它们。

例如,在下面的示例中,我试图让所有子命令都需要 -required 标志。

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

我希望用法如下:

$ go run main.go foo -required helloworld

但是如果我 运行 使用上面的代码我得到:

$ 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

看起来 flag.Parse() 没有从 CLI 捕获 -required,然后 fooCmd 抱怨我给了它一个它无法识别的标志。

在 Golang 中使用带有全局标志的子命令的最简单方法是什么?

如果你打算实现子命令,你不应该调用 flag.Parse()

而是决定使用哪个子命令(就像您对 os.Args[1] 所做的那样),并且只调用它的 FlagSet.Parse() 方法。

是的,要使其正常工作,所有标志集都应包含通用标志。但是很容易注册一次(在一个地方)。创建包级变量:

var (
    required string

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

并使用循环遍历所有标志集,并注册通用标志,使用 FlagSet.StringVar():

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

然后在 main() 中调用 Parse() 相应的标志集,然后测试 required

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

您可以通过创建标志集映射来改进上述解决方案,这样您就可以使用该映射来注册通用标志,并进行解析。

完整应用:

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

将全局标志放在子命令之前:

go run . -required=x foo.

使用flag.Args()代替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])
    }   
}

如果你想把所有的标志放在一起,在子命令之后,写一个辅助函数,为每个 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",
    )
}

也许你也可以结合这两种方法,让全局标志在任何位置都起作用。