Search code examples
gogo-cobraviper-go

Why am I getting a nil pointer error depending on where I call BindPFlag?


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.


Solution

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