英文:
Why am I getting a nil pointer error depending on where I call BindPFlag?
问题
我刚刚开始使用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")
// *** 如果我将此行移到initConfig的顶部
// *** 代码将正确运行。
config.BindPFlag("name", cmd.Flags().Lookup("name"))
return cmd
}
func initConfig(cmd *cobra.Command, config *viper.Viper) {
if cfgFile != "" {
// 使用命令行标志中的配置文件。
config.SetConfigFile(cfgFile)
} else {
config.AddConfigPath(".")
config.SetConfigName(".example")
}
config.AutomaticEnv() // 读取匹配的环境变量
// 如果找到配置文件,则读取它。
if err := config.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", config.ConfigFileUsed())
}
// *** 此行触发了空指针引用。
fmt.Printf("name is %s\n", config.GetString("name"))
}
在最后一次调用fmt.Printf
时,此代码将引发空指针引用的恐慌:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x50 pc=0x6a90e5]
如果我将对config.BindPFlag
的调用从NewCmdRoot
函数移动到initConfig
函数的顶部,一切都可以正常运行。
这是怎么回事?根据Viper文档关于使用BindPFlags
的说明:
> 类似于BindEnv,当绑定方法被调用时,值并不会被设置,而是在访问时设置。这意味着您可以尽早进行绑定,甚至在init()函数中。
这几乎与我在这里所做的完全相同。在调用config.BindPflag
时,config
是非空的,cmd
是非空的,并且已经注册了name
参数。
我猜测在PersistentPreRun
闭包中使用config
时可能出了问题,但我不确定为什么会导致此错误。
英文:
I've just recently started working with Go, and I've run into some
behavior working with Cobra and Viper that I'm not sure I understand.
This is a slightly modified version of the sample code you get by
running cobra init
. In main.go
I have:
package main
import (
"github.com/larsks/example/cmd"
"github.com/spf13/cobra"
)
func main() {
rootCmd := cmd.NewCmdRoot()
cobra.CheckErr(rootCmd.Execute())
}
In cmd/root.go
I have:
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"))
}
This code will panic with a nil pointer reference at the final call to
fmt.Printf
:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x50 pc=0x6a90e5]
If I move the call to config.BindPFlag
from the NewCmdRoot
function to the top of the initConfig
command, everything runs
without a problem.
What's going on here? According to the Viper docs regarding the use of
BindPFlags
:
> Like BindEnv, the value is not set when the binding method is
> called, but when it is accessed. This means you can bind as early as
> you want, even in an init() function.
That's almost exactly what I'm doing here. At the time I call
config.BindPflag
, config
is non-nil, cmd
is non-nil, and the
name
argument has been registered.
I assume there's something going on with my use of config
in a
closure in PersistentPreRun
, but I don't know exactly why that is
causing this failure.
答案1
得分: 3
我觉得这很有趣,所以我进行了一些调查,并在一个问题中找到了你的确切问题的记录。问题出在这一行代码上:
config.BindPFlag("name", cmd.Flags().Lookup("name"))
// ^^^^^^^
你创建了一个持久标志,但将标志绑定到了Flags
属性上。如果你将代码更改为绑定到PersistentFlags
,即使在NewCmdRoot
中有这一行代码,一切也会按预期工作:
config.BindPFlag("name", cmd.PersistentFlags().Lookup("name"))
英文:
I thought this was interesting so I did some digging and found your exact problem documented in an issue. The problematic line is this:
config.BindPFlag("name", cmd.Flags().Lookup("name"))
// ^^^^^^^
You created a persistent flag, but bound the flag to the Flags
property. If you change your code to bind to PersistentFlags
, everything will work as intended even with this line in NewCmdRoot
:
config.BindPFlag("name", cmd.PersistentFlags().Lookup("name"))
答案2
得分: 2
这比起初看起来要复杂一些,所以虽然这里的其他答案帮助我解决了问题,但我想添加一些细节。
如果你刚开始使用Cobra,文档中有一些细微之处可能不太清楚。让我们从PersistentFlags
方法的文档开始:
> PersistentFlags返回在当前命令中明确设置的持久FlagSet。
关键在于*...在当前命令中*。在我的NewCmdRoot
根方法中,我们可以使用cmd.PersistentFlags()
,因为根命令是当前命令。我们甚至可以在PersistentPreRun
方法中使用cmd.PersistentFlags()
,只要我们不处理子命令。
如果我们重新编写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"))
}
...我们会发现当执行根命令时它可以工作:
$ ./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
部分的含义),但是当这个方法运行时,传递给PersistentPreRun
的cmd
参数不再是根命令;它是subcommand
命令。当我们尝试调用cmd.PersistentFlags()
时,它会失败,因为当前命令没有与之关联的任何持久标志。
在这种情况下,我们需要使用Flags
方法:
> Flags返回适用于此命令的完整FlagSet(在此命令和所有父命令中声明的本地和持久标志)。
这使我们可以访问由父命令声明的持久标志。
另一个问题,文档中似乎没有明确指出的是,Flags()
只有在命令处理运行后才可用(也就是说,在命令或父命令上调用cmd.Execute()
之后)。这意味着我们可以在PersistentPreRun
中使用它,但是我们不能在NewCmdRoot
中使用它(因为该方法在处理命令行之前就完成了)。
总结:
- 我们必须在
NewCmdRoot
中使用cmd.PersistentFlags()
,因为我们正在寻找应用于当前命令的持久标志,而Flags()
的值尚不可用。 - 我们需要在
PersistentPreRun
(和其他持久命令方法)中使用cmd.Flags()
,因为在处理子命令时,PersistentFlags
只会查找当前命令上的持久标志,而不会遍历父命令。我们需要使用cmd.Flags()
,它会汇总父命令声明的持久标志。
英文:
This ends up being a little more complex than it might appear at first glance, so while the other answers here helped me resolve the problem I'd like to add a little detail.
There are some nuances in the documentation that aren't particularly clear if you're just starting to work with Cobra. Let's start with the documentation for the PersistentFlags
method:
> PersistentFlags returns the persistent FlagSet specifically set in the current command.
The key is in ...in the current command. In my NewCmdRoot
root method, we can use cmd.PersistentFlags()
because the root command is the current command. We can even use cmd.PersistentFlags()
in the PersistentPreRun
method, as long as we're not processing a subcommand.
If we were to re-write cmd/root.go
from the example so that it includes a subcommand, like this...
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"))
}
...we would find that it works when executing the root command:
$ ./example
name =
name is
Hello, world
But it fails when we run the subcommand:
[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
This is because the subcommand inherits the PersistentPreRun
command from the root (this is what the Persistent
part means), but when this method runs, the cmd
argument passwd to PersistentPreRun
is no longer the root command; it's the subcommand
command. When we try to call cmd.PersistentFlags()
, it fails because the current command doesn't have any persistent flags associated with it.
In this case, we need to instead use the Flags
method:
> Flags returns the complete FlagSet that applies to this command (local and persistent declared here and by all parents).
This gives us access to persistent flags declared by parents.
An additional issue, that doesn't appear to be called out explicitly in the documentation, is that Flags()
is only available after command processing has been run (that is, after you call cmd.Execute()
on the command or a parent). That means we can use it in PersistentPreRun
, but we can't use it in NewCmdRoot
(because that method finishes before we process the command line).
TL;DR
- We have to use
cmd.PersistentFlags()
inNewCmdRoot
because we're looking for persistent flags applied to the current command, and the value fromFlags()
won't be available yet. - We need to use
cmd.Flags()
inPersistentPreRun
(and other persistent commands methods) because when processing a subcommand,PersistentFlags
will only look for persistent flags on the current command, but won't traverse parents. We need to usecmd.Flags()
instead, which will roll up persistent flags declared by parents.
答案3
得分: 1
如果我使用cmd.PersistentFlags().Lookup("name")
,我没有任何问题。
// *** 如果我将这段代码移到initConfig的顶部
// *** 代码将会正确运行。
pflag := cmd.PersistentFlags().Lookup("name")
config.BindPFlag("name", pflag)
考虑到你刚刚注册了持久标志(该标志将对分配给它的命令以及该命令下的每个命令都可用),调用cmd.PersistentFlags().Lookup("name")
比调用cmd.Flags().Lookup("name")
更安全。
后者返回nil
,因为只有在调用rootCmd.Execute()
时才会调用PersistentPreRun
,而这是在cmd.NewCmdRoot()
之后进行的。
在cmd.NewCmdRoot()
级别,标志尚未初始化,即使其中一些被声明为“persistent”。
英文:
I don't have any issue if I use 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)
Considering you just registered persistent flags (flag will be available to the command it's assigned to as well as every command under that command), it is safer to call cmd.PersistentFlags().Lookup("name")
, rather than cmd.Flags().Lookup("name")
.
The latter returns nil
, since the PersistentPreRun
is only called when rootCmd.Execute()
is called, which is after cmd.NewCmdRoot()
.
At cmd.NewCmdRoot()
levels, flags have not yet been initialized, even after some were declared "persistent".
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论