【问题标题】:Global flags and subcommands全局标志和子命令
【发布时间】:2021-08-26 11:51:49
【问题描述】:

我正在实现一个带有多个子命令的小 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 中使用带全局标志的子命令最简单的方法是什么?

【问题讨论】:

  • 永远不要使用go run main.go
  • @Flimzy 不仅Eli说了什么,而且,没有解释?没有理由?这与手头的问题有关吗? :D
  • @EliBendersky:如果你认为我的评论是“火焰”,我很抱歉。建议很中肯。
  • @dbzuk:这与您的问题没有直接关系,这就是为什么它是评论而不是答案的原因。为什么不这样做的简短解释:因为 Go 中的编译单元是一个包,而不是一个文件,这通常意味着一个目录中的所有 .go 文件。它还忽略构建标签和其他极端情况。 go run main.go 有时会起作用……直到不起作用。然后它令人困惑。所以最好不要养成这个习惯。

标签: go command-line-interface command-line-arguments


【解决方案1】:

如果你打算实现子命令,你不应该调用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")
    }
}

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

      猜你喜欢
      • 2014-06-11
      • 1970-01-01
      • 1970-01-01
      • 2018-07-22
      • 1970-01-01
      • 1970-01-01
      • 2012-07-26
      • 1970-01-01
      • 2014-01-08
      相关资源
      最近更新 更多