全局标志和子命令

huangapple go评论145阅读模式
英文:

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()没有捕获到命令行中的-required标志,然后fooCmd报错说我给了一个它不认识的标志。

在Golang中,实现具有全局标志的子命令的最简单方法是什么?

英文:

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?

答案1

得分: 15

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

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

答案2

得分: 3

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

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() // -required标志之后的所有内容,例如[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",
    )
}

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

英文:

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.

答案3

得分: 1

也许你会对使用https://github.com/spf13/cobra感兴趣-它支持这种用例和许多其他用例。

英文:

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

huangapple
  • 本文由 发表于 2021年6月9日 21:05:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/67904765.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定