Cobra: Providing context to subcommands without using package globals?

4.7k views Asked by At

I've written a simple CLI tool using cobra and viper. I've recently been refactoring it to avoid package global variables, largely because it turned out to be difficult to test using the layout suggested by e.g. cobra init.

Instead of...

var rootCmd = &cobra.Command{
  ...
}

func main() {
  rootCmd.Execute()
}

I've got something more like:

func NewCmdRoot() *cobra.Command {
  cmd := &cobra.Command{
    ...
  }

  return cmd
}

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

This has actually worked out great, and makes it much easier for tests to start with a clean set of cli options. I'm running into some difficulties integrating Viper into the new scheme. If I only care about the root command, I can set things up in a PersistentPreRun command, like this:

func initConfig(cmd *cobra.Command) {
  config := viper.New()
  rootCmd := cmd.Root()

  config.BindPFlag("my-nifty-option", rootCmd.Flag("my-nifty-option")); err != nil {

  // ...stuff happens here...

  config.ReadInConfig()

  // What happens next?
}

func NewCmdRoot() *cobra.Command {
  cmd := &cobra.Command{
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
      initConfig(cmd)
    },
  }

This sort of works: as long as I'm only interested in config options that correspond to Cobra command line options, things works as expected. But what if I want access to the config variable itself?

I'm unsure how to expose the config variable outside of the initConfig method without turning it into a package global. I'd like the possibility of instantiating multiple command trees, each with it's own isolated Viper config object, but I'm unclear where to put it.

1

There are 1 answers

0
Christian González Di Antonio On

The cobra team did that recently, see https://github.com/spf13/cobra/pull/1551

func TestFoo(t *testing.T){
    root := &Command{
        Use: "root",
        PreRun: func(cmd *Command, args []string) {
            ctx := context.WithValue(cmd.Context(), key{}, val)
            cmd.SetContext(ctx)
        },
        Run: func(cmd *Command, args []string) {
            fmt.Println(cmd.Context().Value(key{}))
        },
    }
    ctx := context.WithValue(cmd.Context(), "key", "default")
    root.ExecuteContext(ctx)
}