I was playing around writing a simple command-line tool and I struggled to make flags work together with environment variables.

I choose spf13/cobra since is the one that we are currently using at work, it’s widely adopted and well documented, but the solution to this problem is not simple as the one offered by urfave/cli, where you just need to set the EnvVars field of your Flag

&cli.StringFlag{
    EnvVars: []string{"MY_VAR"},
}

Unfortunately with cobra is not that easy and googling around I’ve stumbled on this awesome blog post by @carolynvs:

Her solution was simple and effective but since I had a different use case (with subcommands) I had to adapt it a bit. I was not familiar with cobra and it took me a while, so I thought about forking her repository and modifying his sample with a subcommand: https://github.com/enrichman/stingoftheviper.

In this example I’ve createad a cli with a generic stingoftheviper command and a stingoftheviper sting subcommand. The first has a couple of flags, --name and --number, while the sting subcommand has only a --name flag. Just be aware that the --number flag is global, so it will be available also from the subcommand.

While it’s possible to use simple variables I find convenient to have a Config struct to share across the commands

type Config struct {
	Name   string
	Number int

	StingConfig StingConfig
}

type StingConfig struct {
	Name string
}

I’ve defined a constructor to initialize the Config with default values

func NewConfig() Config {
	return Config{
		Number: 42,
		Name:   "default_name",

		StingConfig: StingConfig{
			Name: "default_sting_name",
		},
	}
}

This Config is created at the beginning of the initialization of our rootCmd, and then shared across all the other commands

func NewRootCommand() *cobra.Command {
	config := NewConfig()
}

Before sharing it we need to initialize Cobra with a PersistentPreRunE func, to tell Cobra how to fetch the environment variables (you can find a detailed explanation in the Carolyn’s blog post).

func NewRootCommand() *cobra.Command {
	config := NewConfig()

	rootCmd := &cobra.Command{
		Use:   "stingoftheviper",
		Short: "Cobra and Viper together at last",
		Long:  `Demonstrate how to get cobra flags to bind to viper properly`,
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return initializeConfig(cmd)
		},
		Run: func(cmd *cobra.Command, args []string) {
			// ...
		},
	}
}

To bind the command flags we will use a dedicated bindRootFlags func

func NewRootCommand() *cobra.Command {
	config := NewConfig()

	rootCmd := &cobra.Command{
		Use:   "stingoftheviper",
		Short: "Cobra and Viper together at last",
		Long:  `Demonstrate how to get cobra flags to bind to viper properly`,
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return initializeConfig(cmd)
		},
		Run: func(cmd *cobra.Command, args []string) {
			// ...
		},
	}

	bindRootFlags(rootCmd.Flags(), rootCmd.PersistentFlags(), &config)
}

// bindRootFlags will bind the Config struct with the flags and environment variables
func bindRootFlags(flags, persistentFlags *pflag.FlagSet, config *Config) {
	flags.StringVarP(&config.Name, "name", "n", config.Name, "What's your name?")
	persistentFlags.IntVar(&config.Number, "number", config.Number, "Which is your favorite number?")
}

And finally we can create and add the subcommand, sharing the Config with it

func NewRootCommand() *cobra.Command {
	config := NewConfig()

	rootCmd := &cobra.Command{
		Use:   "stingoftheviper",
		Short: "Cobra and Viper together at last",
		Long:  `Demonstrate how to get cobra flags to bind to viper properly`,
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return initializeConfig(cmd)
		},
		Run: func(cmd *cobra.Command, args []string) {
			// ...
		},
	}

	bindRootFlags(rootCmd.Flags(), rootCmd.PersistentFlags(), &config)

	rootCmd.AddCommand(NewStingCommand(&config))

	return rootCmd
}

Then the subcommand will be pretty much the same

func NewStingCommand(config *Config) *cobra.Command {
	stingConfig := &config.StingConfig

	stingCmd := &cobra.Command{
		Use:   "sting",
		Short: "a small subcommand",

		Run: func(cmd *cobra.Command, args []string) {
			// ...
		},
	}

	bindStingFlags(stingCmd.Flags(), stingCmd.PersistentFlags(), stingConfig)

	return stingCmd
}

func bindStingFlags(flags, persistentFlags *pflag.FlagSet, config *StingConfig) {
	flags.StringVarP(&config.Name, "name", "n", config.Name, "Who do you want to sting?")
}

Hopefully, this will help someone else!