Adding a Configuration File to Multi-git

Let's continue refactoring Multi-git and utilize Viper to read a configuration file. We'll also discuss backward compatibility in general and in the context of multi-git.

Adding a configuration file

The first question to ask is where to put the configuration file. I recommend following the XDG guidelines and using $HOME/.config as the configuration directory. However, to support other use cases you should be able to provide the path to the configuration file as an optional command-line argument.

Let’s set it up first in cmd/root.go. We define a configFilename variable and then bind the "config" command-line flag to it, so you can pass it on the command-line as --config <filename>. If it’s not provided the default configuration file is $HOME/.config/multi-git.toml

var configFilename string


...

func init() {
	...
	// Find home directory.
	home, err := homedir.Dir()
	check(err)

	defaultConfigFilename := path.Join(home, ".config/multi-git.toml")
	rootCmd.Flags().StringVar(&configFilename,
		"config",
		defaultConfigFilename,
		"config file path (default is $HOME/multi-git.toml)")
	...

The next step is to tell Viper to read the configuration file. First, in the init() function we pass an initConfig() function to cobra.OnInitialize:

func init() {
	cobra.OnInitialize(initConfig)
	...
}

The initConfig() function will be called by Cobra when it’s ready for initialization, as opposed to the init() function that may be called too early.

It checks that the configuration file exists, it sets Viper to use the configuration filename and reads it.

func initConfig() {
	_, err := os.Stat(configFilename)
	if os.IsNotExist(err) {
		check(err)
	}

	viper.SetConfigFile(configFilename)
	err = viper.ReadInConfig()
	check(err)

	...
}

Here is a sample configuration file that matches our testing conditions:

$ cat ~/.config/multi-git.toml
root = "/tmp/multi-git"
repos = "repo-1,repo-2"

We can add the ignore-errors key to the configuration file if we want to override the default of false:

root = "/tmp/multi-git"
repos = "repo-1,repo-2"
ignore-errors = true

Of course, if we provide --ignore-errors as a command-line flag it will override whatever is in the configuration file.

Now, with the root directory and repos configured via the configuration file, there is no need to set the MG_ROOT and MG_REPOS environment variables every time we need to run multi-git in a new terminal window.

Considering backwards-compatibility

Existing multi-git users are used to the MG_ROOT and MG_REPOS environment variables. Now that we have a better solution with the configuration file, should we still keep the environment variables for backward compatibility purposes?

Let’s weigh the pros and cons

Pros:

  • Users that are fine using the environment variables can keep on doing what they always did.
  • Scripts that use multi-git with the environment variables will not break.
  • Define a configuration file once, but override it with environment variables in some special cases.

Cons:

  • Multi-git is more complicated because we have to consider the interaction between the configuration file and environment variables.
  • Environment variables might unintentionally override the configuration file.
  • The “MG_” prefix is not that unique and might conflict with another application.

For multi-git the decision is super-easy: backward compatibility is not needed. Multi-git probably has no users, and I don’t use it myself. It’s just developed for learning and teaching purposes.

For your programs, you should make sure the impact of making a breaking change is tolerable. In the industry, there are horror stories of large systems, including operating systems, that had to maintain known bugs in newer versions because users depended on the buggy behavior.

So, even if there is no “business justification”, let’s keep the environment variables as a way to override the configuration file as well as provide a backward-compatible command-line interface.

Going back to the initConfig() function in cmd/root.go, we can set the environment prefix to MG and bind to the keys "root" and "repos" to the environment:

func initConfig() {
	...
	viper.SetEnvPrefix("MG")
	err = viper.BindEnv("root")
	check(err)

	err = viper.BindEnv("repos")
	check(err)

As you recall, this means that the environment variables MG_ROOT and MG_REPOS, if defined, will override the “root” and “repos” keys defined in the configuration file because environment variables have precedence over the configuration file.

Conclusion

Multi-git is a pretty simple Cobra application. It has just one root command and no sub-commands, but, even such a simple program can benefit tremendously from combining Cobra and Viper for a sophisticated configuration story. Finally, we demonstrated how to bind command-line flags to Viper, how to add a configuration file and where to place it, and how to maintain backward compatibility by preserving the MG_ROOT and MG_REPOS environment variables.

Quiz

Get hands-on with 1200+ tech skills courses.