Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 970
larsks Avatar asked May 28 '21 16:05

larsks


1 Answers

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"))
like image 176
Dean Avatar answered Sep 28 '22 02:09

Dean