Cobra and Persistentflags gotchas

How wrong usage of persistent flags can burn you

Suraj Deshmukh

3 minute read

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.

comments powered by Disqus