If you are using cobra cmd line library for golang applications and it’s PersistentFlags
and if you have a use case where you are adding same kind of flag in multiple places. You might burn your fingers in that case, if you keep adding it in multiple sub-commands without giving it a second thought. To understand what is really happening and why it is happening follow along.
All the code referenced here can be found here https://github.com/surajssd/cobrademo.
The cmd line tool built with cobra has following structure. The main tool is called cobrademo
. And the sub-commands are alpha
and num
. Sub-command num
has one more sub-command called one
.
cobrademo
├── alpha
└── num
└── one
Now I want a persistent flag --config
to be availabe under sub-command one
and alpha
both. So I created a func that allows me to add this flag under any command, which looked like following:
func addConfig(cmd *cobra.Command) {
// add config flag
cmd.PersistentFlags().String(
"config",
os.ExpandEnv("$HOME/.config"),
"Path to config file")
viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config"))
}
Above code is here.
Now this is called from one.go
and alpha.go
to add the flag under those sub-command.
Now the command structure for alpha
sub-command looks like following:
$ go run main.go alpha -h
All the alphabet related commands
Usage:
cobrademo alpha [flags]
Flags:
--config string Path to config file (default "/home/hummer/.config")
-h, --help help for alpha
For sub-command one
it looks like following:
$ go run main.go num -h
All the numeric related commands
Usage:
cobrademo num [command]
Available Commands:
one first subcommand in numerics
Flags:
-h, --help help for num
Use "cobrademo num [command] --help" for more information about a command.
$ go run main.go num one -h
first subcommand in numerics
Usage:
cobrademo num one [flags]
Flags:
--config string Path to config file (default "/home/hummer/.config")
-h, --help help for one
But if you look at the functionality it does not work as expected.
$ go run main.go num one --config=foobar
inside one, config value: foobar
$ go run main.go alpha --config=foobar
inside alpha, config value: /home/hummer/.config
If you see the output of both the commands it is different. While it should have been same i.e. foobar
. What made it work in case of sub-command one
and it did not work in case of sub-command alpha
?
Now we are registering a persistent flag twice once for sub-command one
and again for alpha
. And these calls happen from the init
func of those files. If you look at the order of the evaluation of those init
functions then it happens in alphabetical order.
$ tree cmd/
cmd/
├── alpha.go
├── num.go
├── one.go
└── root.go
Hence the init
func of alpha
is called first and the flag config
is registered there first and again it is registered for one
. So the final flag is just registered for one
. Hence the functionality works correctly for one
and not for alpha
.
So the right way to work with persistent flags is to register them only once. If any particular sub-command tree needs that flag then only register at it’s root. In our case most of the sub-commands will need it, so the right way to use it is to add it to the rootCmd
.
In above code I removed the function addConfig
and all it’s references(see the changes here). And added following code snippet to the init
func of root.go
.
// add config flag
rootCmd.PersistentFlags().String(
"config",
os.ExpandEnv("$HOME/.config"),
"Path to config file")
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
And now after running the code again with above changes it works absolutely fine:
$ go run main.go num one --config=foobar
inside one, config value: foobar
$ go run main.go alpha --config=foobar
inside alpha, config value: foobar
There are other cobra gotchas that exist but then that is for another post.