6

我最近才开始使用 Go,并且在使用 Cobra 和 Viper 时遇到了一些我不确定我是否理解的行为。

这是您通过运行获得的示例代码的略微修改版本cobra init。在main.go我有:

package main

import (
    "github.com/larsks/example/cmd"
    "github.com/spf13/cobra"
)

func main() {
    rootCmd := cmd.NewCmdRoot()
    cobra.CheckErr(rootCmd.Execute())
}

cmd/root.go我有:

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"

    "github.com/spf13/viper"
)

var cfgFile string

func NewCmdRoot() *cobra.Command {
    config := viper.New()

    var cmd = &cobra.Command{
        Use:   "example",
        Short: "A brief description of your application",
        PersistentPreRun: func(cmd *cobra.Command, args []string) {
            initConfig(cmd, config)
        },
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Printf("This is a test\n")
        },
    }

    cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.example.yaml)")
    cmd.PersistentFlags().String("name", "", "a name")

  // *** If I move this to the top of initConfig
  // *** the code runs correctly.
    config.BindPFlag("name", cmd.Flags().Lookup("name"))

    return cmd
}

func initConfig(cmd *cobra.Command, config *viper.Viper) {
    if cfgFile != "" {
        // Use config file from the flag.
        config.SetConfigFile(cfgFile)
    } else {
        config.AddConfigPath(".")
        config.SetConfigName(".example")
    }

    config.AutomaticEnv() // read in environment variables that match

    // If a config file is found, read it in.
    if err := config.ReadInConfig(); err == nil {
        fmt.Fprintln(os.Stderr, "Using config file:", config.ConfigFileUsed())
    }

  // *** This line triggers a nil pointer reference.
    fmt.Printf("name is %s\n", config.GetString("name"))
}

此代码将在最终调用时出现 nil 指针引用恐慌 fmt.Printf

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x50 pc=0x6a90e5]

如果我将调用config.BindPFlagNewCmdRoot 函数移到命令的顶部initConfig,一切运行都没有问题。

这里发生了什么?根据有关使用的 Viper 文档 BindPFlags

和 BindEnv 一样,值不是在调用绑定方法时设置,而是在访问时设置。这意味着您可以尽可能早地绑定,即使在 init() 函数中也是如此。

这几乎正​​是我在这里所做的。在我打电话的时候 config.BindPflagconfig是非零,cmd是非零,并且 name参数已经被注册。

config我认为我在 in 的闭包中使用了一些事情PersistentPreRun,但我不知道为什么会导致这个失败。

4

3 回答 3

3

我认为这很有趣,所以我进行了一些挖掘,发现您的确切问题记录在 issue中。有问题的行是这样的:

config.BindPFlag("name", cmd.Flags().Lookup("name"))
//                           ^^^^^^^

您创建了一个持久标志,但将该标志绑定到该Flags属性。如果您将代码更改为绑定到PersistentFlags,即使在以下行中,一切都会按预期工作NewCmdRoot

config.BindPFlag("name", cmd.PersistentFlags().Lookup("name"))
于 2021-06-03T02:17:56.913 回答
1

如果我使用cmd.PersistentFlags().Lookup("name").

    // *** If I move this to the top of initConfig
    // *** the code runs correctly.
    pflag := cmd.PersistentFlags().Lookup("name")
    config.BindPFlag("name", pflag)

考虑到您刚刚注册了持久标志(标志将可用于分配给它的命令以及该命令下的每个命令),调用cmd.PersistentFlags().Lookup("name"). 而不是cmd.Flags().Lookup("name").

后者返回nil,因为PersistentPreRun仅在调用时才rootCmd.Execute()调用 ,即after cmd.NewCmdRoot()
cmd.NewCmdRoot()级别上,标志尚未初始化,即使在一些被声明为“持久”之后也是如此。

于 2021-05-31T08:28:18.040 回答
1

这最终比乍一看要复杂一些,所以虽然这里的其他答案帮助我解决了这个问题,但我想添加一些细节。

如果您刚开始使用 Cobra,文档中的一些细微差别并不是特别清楚。让我们从该PersistentFlags方法的文档开始:

PersistentFlags 返回当前命令中专门设置的持久化标志集。

关键在...在当前命令中。在我的NewCmdRootroot 方法中,我们可以使用cmd.PersistentFlags(),因为 root 命令是当前命令。我们甚至可以cmd.PersistentFlags()PersistentPreRun方法中使用,只要我们不处理 subcommand

如果我们要从示例中重写cmd/root.go,以便它包含一个子命令,就像这样......

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string

func NewCmdSubcommand() *cobra.Command {
    var cmd = &cobra.Command{
        Use:   "subcommand",
        Short: "An example subcommand",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Printf("This is an example subcommand\n")
        },
    }

    return cmd
}

func NewCmdRoot() *cobra.Command {
    config := viper.New()

    var cmd = &cobra.Command{
        Use:   "example",
        Short: "A brief description of your application",
        PersistentPreRun: func(cmd *cobra.Command, args []string) {
            initConfig(cmd, config)
        },
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Printf("Hello, world\n")
        },
    }

    cmd.PersistentFlags().StringVar(
    &cfgFile, "config", "", "config file (default is $HOME/.example.yaml)")
    cmd.PersistentFlags().String("name", "", "a name")

    cmd.AddCommand(NewCmdSubcommand())

    err := config.BindPFlag("name", cmd.PersistentFlags().Lookup("name"))
    if err != nil {
        panic(err)
    }

    return cmd
}

func initConfig(cmd *cobra.Command, config *viper.Viper) {
    name, err := cmd.PersistentFlags().GetString("name")
    if err != nil {
        panic(err)
    }
    fmt.Printf("name = %s\n", name)

    if cfgFile != "" {
        // Use config file from the flag.
        config.SetConfigFile(cfgFile)
    } else {
        config.AddConfigPath(".")
        config.SetConfigName(".example")
    }

    config.AutomaticEnv() // read in environment variables that match

    // If a config file is found, read it in.
    if err := config.ReadInConfig(); err == nil {
        fmt.Fprintln(os.Stderr, "Using config file:", config.ConfigFileUsed())
    }

    // *** This line triggers a nil pointer reference.
    fmt.Printf("name is %s\n", config.GetString("name"))
}

...我们会发现它在执行 root 命令时有效:

$ ./example
name =
name is
Hello, world

但是当我们运行子命令时它失败了:

[lars@madhatter go]$ ./example subcommand
panic: flag accessed but not defined: name

goroutine 1 [running]:
example/cmd.initConfig(0xc000172000, 0xc0001227e0)
        /home/lars/tmp/go/cmd/root.go:55 +0x368
example/cmd.NewCmdRoot.func1(0xc000172000, 0x96eca0, 0x0, 0x0)
        /home/lars/tmp/go/cmd/root.go:32 +0x34
github.com/spf13/cobra.(*Command).execute(0xc000172000, 0x96eca0, 0x0, 0x0, 0xc000172000, 0x96eca0)
        /home/lars/go/pkg/mod/github.com/spf13/cobra@v1.1.3/command.go:836 +0x231
github.com/spf13/cobra.(*Command).ExecuteC(0xc00011db80, 0x0, 0xffffffff, 0xc0000240b8)
        /home/lars/go/pkg/mod/github.com/spf13/cobra@v1.1.3/command.go:960 +0x375
github.com/spf13/cobra.(*Command).Execute(...)
        /home/lars/go/pkg/mod/github.com/spf13/cobra@v1.1.3/command.go:897
main.main()
        /home/lars/tmp/go/main.go:11 +0x2a

这是因为子命令继承了PersistentPreRun根命令(这就是Persistent部分的意思),但是当这个方法运行时,cmd参数 passwdPersistentPreRun不再是根命令;这是subcommand命令。当我们尝试调用cmd.PersistentFlags()时,它会失败,因为当前命令没有与之关联的任何持久标志。

在这种情况下,我们需要改用以下Flags方法:

Flags 返回适用于此命令的完整 FlagSet(在此处和所有父级声明的本地和持久性)。

这使我们可以访问父母声明的持久标志。

文档中似乎没有明确指出的另一个问题是,Flags()它仅在命令处理运行后可用(即,在您调用cmd.Execute()命令或父级之后)。这意味着我们可以在中使用它PersistentPreRun,但我们不能在中使用它NewCmdRoot(因为该方法在我们处理命令行之前完成)。


TL;博士

  • 我们必须使用cmd.PersistentFlags()in ,NewCmdRoot因为我们正在寻找应用于当前 command的持久标志,并且 from 的值Flags()尚不可用。
  • 我们需要使用cmd.Flags()in PersistentPreRun(和其他持久化命令方法),因为在处理子命令时,PersistentFlags只会在当前命令上查找持久化标志,而不会遍历父命令。我们需要使用cmd.Flags()它,它将汇总父母声明的持久标志。
于 2021-06-04T23:43:55.213 回答