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.
We'll cover the following
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 1400+ tech skills courses.