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!