Golang is the one of the most useful technologies I’ve recently learned. Golang has a pretty nice support for networking, command line or logging out of the box, you don’t need any dependency. But there are libraries making developers' life even easier.
I’ve already talked about creating REST service in go, today I’d like to focus on creating a command line tool.
Flags
package main
import (
"flag"
"fmt"
)
func main() {
var nFlag = flag.Int("lines", 1234, "number of lines")
flag.Parse()
fmt.Printf("Lines %d\n", *nFlag)
}
With previous simple code we have already some useful capabilities
$ cmd --help
> Usage of cmd:
-lines int
number of lines (default 1234)
$ cmd
> Lines 1234
$ cmd --lines=2
> Lines 2
$ cmd --lines=asdf
> invalid value "asdf" for flag -lines: parse error
Usage of cmd:
-lines int
number of lines (default 1234)
exit status 2
For a simple command line tool, in most of the cases, we will have enough with the default language support. But if we want to bring more features to the combo like reading configuration from environment variables or from files, then a library like the awesome Viper will come really handy.
Viper
Viper is very powerful and well documented library widely used from many projects, I will not get into the details or how to use it, because I’ve created an abstraction to simplify its usage, I named it Meta-Viper.
Meta-Viper
Meta-Viper abstracts you of the details of reading configuration from files, environment or flags. This extra simplicity comes with a tradeoff, we are missing some flexibility.
Let’s see some features with an example.
go mod init example.com/meta-viper (1)
go get github.com/carlosvin/meta-viper (2)
-
Create a go modules project
-
Install the meta-viper dependency
Now let’s create a program that is configurable from files, command line params and environment variables.
package main
import (
"log"
"os"
config "github.com/carlosvin/meta-viper"
)
// Here is where you define the struct that will hold the configuration values
// cfg_name is the parameter name
// cfg_desc is the parameter description that will be shown in the command line help
type appConfig struct {
Host string `cfg_name:"host" cfg_desc:"Server host"`
Port int `cfg_name:"port" cfg_desc:"Server port"`
SearchAPI string `cfg_name:"apis.search" cfg_desc:"Search API endpoint"`
}
func main() {
// Instantiate the structure with default values
cfg := &appConfig{
Host: "localhost",
Port: 6000,
SearchAPI: "google",
}
// Meta-Viper instance is loading the configuration from wherever is available: files, env, or input params
_, err := config.New(cfg, os.Args)
if err != nil {
panic(err)
}
log.Printf("Loaded Configuration %v...", cfg)
}
Now let’s see some examples how this command line tool is able to load configuration.
$ cmd --help
Usage of flagsConfig:
--apis.search string Search API endpoint (default "google")
--config string Configuration name
--config-dirs strings Configuration directories search paths (default [.,config,configs,cfg])
--host string Server host (default "localhost")
--port int Server port (default 6000)
pflag: help requested
exit status 2
$ cmd
2021/02/15 23:12:48 No configuration name has been specified, so no configuration file will be loaded. Using flags and environment variables.
2021/02/15 23:12:48 Loaded Configuration &{localhost 6000 google}...
$ PORT=9999 cmd --host=myhost
2021/02/15 23:15:47 No configuration name has been specified, so no configuration file will be loaded. Using flags and environment variables.
2021/02/15 23:15:47 Loaded Configuration &{myhost 9999 google}...
The last example will aggregate all the possible sources of configuration, it will extend the previous one adding configuration from a file. So let’s create a configuration file:
{
"apis": {
"search": "duckduckgo"
}
}
$ PORT=9999 cmd --host=myhost --config=the-config
2021/02/15 23:22:17 Loaded Configuration &{myhost 9999 duckduckgo}...
Tip
|
Here you can find a multi-environment example a little bit more complete. |