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.
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".