Building a CLI Tool in Go with Cobra and Viper


"The elegance of the command line lies in its ability to combine simple tools to accomplish complex tasks effortlessly." - Co-author of Unix
Go, with its simplicity and strong concurrency model, is a popular choice for building CLI tools. Cobra and Viper are two powerful libraries in Go for building command-line interfaces and managing configuration, respectively. They are designed to work seamlessly together, offering a robust solution for developing feature-rich CLI applications. To understand how they work internally and complement each other, let's dive deeper into their architecture and how they interact.
What are Cobra and Viper?
Cobra is a library for creating powerful modern CLI applications in Go. It's easy to use and provides a simple interface for creating commands, subcommands, and flags.
Viper is a complete configuration solution for Go applications. It can read configuration from different file formats (JSON, TOML, YAML, etc.), environment variables, command-line flags**(cobra)**, and more.
Why Use Cobra and Viper Together?
Using Cobra and Viper together gives you the flexibility to handle both command-line flags and configuration files seamlessly. This combination is ideal for building CLI tools where you want to provide a rich set of features and options to the user. For example you know that you are going to use some flag every time with same value, It will be very inconvenient to mention that flag always in the command, If we are using viper along with cobra then these flags can be mentioned in the config file and the application reads out of that configuration file. There are many such usages.
How Cobra Works?
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func main() {
// Root command
var rootCmd = &cobra.Command{
Use: "cobracli",
Short: "CobraCLI is a simple example CLI tool using Cobra",
Long: `CobraCLI is a simple example CLI tool to demonstrate how Cobra works with commands and subcommands.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Welcome to CobraCLI! Use --help to see available commands.")
},
}
// Variables to store flag values
var name string
var excited bool
// Subcommand: hello
var helloCmd = &cobra.Command{
Use: "hello",
Short: "Prints Hello with a name",
Long: `A simple subcommand that prints Hello followed by a name.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Hello %s!\n", name)
},
}
// Adding a flag to the hello command
helloCmd.Flags().StringVarP(&name, "name", "n", "World", "name to greet")
// Subcommand: goodbye
var goodbyeCmd = &cobra.Command{
Use: "goodbye",
Short: "Prints Goodbye with or without excitement",
Long: `A simple subcommand that prints Goodbye with optional excitement.`,
Run: func(cmd *cobra.Command, args []string) {
if excited {
fmt.Println("Goodbye!")
} else {
fmt.Println("Goodbye.")
}
},
}
// Adding a flag to the goodbye command
goodbyeCmd.Flags().BoolVarP(&excited, "excited", "e", false, "say goodbye with excitement")
// Add subcommands to the root command
rootCmd.AddCommand(helloCmd)
rootCmd.AddCommand(goodbyeCmd)
// Execute the root command
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
Root Command:
The root command is the base command of a CLI application. It's the entry point from which all subcommands are invoked. When you run the CLI tool without specifying any subcommands, the root command's functionality is executed.
In our example,
rootCmd
is the root command.
Subcommands:
Subcommands are commands that fall under a root command (or another subcommand) and provide additional functionality. Each subcommand has its own set of flags and can also have its own subcommands, forming a tree-like structure of commands.
In the example,
helloCmd
andgoodbyeCmd
are subcommands ofrootCmd
.
Key Functions and Fields of Cobra Commands:
Use
: A string that specifies how the command should be used in the CLI. This is essentially the name of the command and how it's invoked from the command line. For example,Use: "hello"
means you can invoke the command by typinghello
.Short
: A brief description of the command. This is used in the auto-generated help message to provide a quick summary of what the command does.Long
: A more detailed description of the command. This is useful for providing a more comprehensive explanation in the help message.Run
: A function that defines what happens when the command is executed. This is where you put the main logic of the command. TheRun
function is executed when the command is called from the command line.Flags()
: A method that returns a flag set for the command. This is used to define flags that the command can accept.
Adding Flags to Commands:
Flags for
helloCmd
:helloCmd.Flags().StringVarP(&name, "name", "n", "World", "name to greet")
Flags()
returns the flag set associated with thehelloCmd
command.StringVarP
defines a string flag (--name
or-n
) for the command, storing the value in thename
variable. The default value is"World"
, and the description is"name to greet"
.
Flags for
goodbyeCmd
:goodbyeCmd.Flags().BoolVarP(&excited, "excited", "e", false, "say goodbye with excitement")
BoolVarP
defines a boolean flag (--excited
or-e
) for the command, storing the value in theexcited
variable. The default value isfalse
, and the description is"say goodbye with excitement"
.
Adding Subcommands to the Root Command:
rootCmd.AddCommand(helloCmd)
rootCmd.AddCommand(goodbyeCmd)
AddCommand()
attaches subcommands (helloCmd
andgoodbyeCmd
) to the root command (rootCmd
). This sets up the command hierarchy and ensures that Cobra knows how to handle and dispatch the subcommands when the CLI is invoked.
How Cobra Processes Commands Internally
Initialisation:
- When the program starts, Cobra initialises the root command (
rootCmd
) and its associated subcommands (helloCmd
andgoodbyeCmd
). Each command's flags and metadata are also initialised.
- When the program starts, Cobra initialises the root command (
Argument Parsing:
When
Execute()
is called, Cobra starts parsing the command-line arguments provided by the user. It matches these arguments against the defined commands (rootCmd
,helloCmd
,goodbyeCmd
) and their flags.For example, if the user runs
cobracli hello --name=keploy
, Cobra recogniseshello
as a subcommand ofcobracli
and then parses the--name
flag.
Command Execution:
After parsing, Cobra executes the corresponding
Run
function for the matched command.If the user types
cobracli hello --name=keploy
, Cobra executes theRun
function ofhelloCmd
, which uses the flag value to printHello keploy!
.
Help and Error Handling:
- Cobra automatically generates help messages and handles errors. If the user inputs an invalid command or requests help (
--help
), Cobra displays the appropriate help or error message based on the command hierarchy and flag definitions.
- Cobra automatically generates help messages and handles errors. If the user inputs an invalid command or requests help (
go build -o cobracli
cmds:
./cobracli
./cobracli --help
./cobracli hello
./cobracli hello --name=keploy
./cobracli hello -n keploy
./cobracli goodbye
./cobracli goodbye --excited
./cobracli goodbye -e
Difficulties with cobra
Maintaining flag variables in code
Repetitive flag usage
Too many flags in the cli command
Lets see if these problems can be solved by viper
How Viper Works?
config.yaml
app:
name: "MyApp"
version: "1.0"
database:
host: "localhost"
port: 5432
user: "admin"
password: "secret"
package main
import (
"fmt"
"github.com/spf13/viper"
)
// Config struct to hold the configuration values
type Config struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
}
func main() {
var config Config
// Set the file name of the configurations file
viper.SetConfigName("config")
// Set the path to look for the configurations file
viper.AddConfigPath(".") // optionally look for config in the working directory
// Read the configuration file
if err := viper.ReadInConfig(); err != nil {
fmt.Printf("Error reading config file, %s", err)
return
}
// Unmarshal the configuration file into the Config struct
if err := viper.Unmarshal(&config); err != nil {
fmt.Printf("Unable to decode into struct, %v", err)
return
}
// Print the configuration values
fmt.Println("Database Host:", config.Host)
fmt.Println("Database Port:", config.Port)
fmt.Println("Database User:", config.User)
fmt.Println("Database Password:", config.Password)
}
run the binary
go build -o configreader
./configreader
Define the Config Struct:
A
Config
struct is defined to hold all the configuration data. It includes nested structs for different sections (e.g.,App
,Database
) of the configuration file.mapstructure
Tags: These struct tags tell Viper how to map the configuration file keys to struct fields. The tags ensure that the configuration values are correctly assigned to the appropriate fields in the struct.
Set Configuration File Name and Path:
viper.SetConfigName("config")
: Specifies the base name of the configuration file without the extension.viper.AddConfigPath(".")
: Adds the current directory as a path to look for the configuration file.
Read Configuration File:
viper.ReadInConfig()
: Reads the configuration file specified. If the file is not found or there’s an error reading it, the program prints an error message and exits.
Unmarshal Configuration into Struct:
viper.Unmarshal(&config)
: Viper unmarshals (i.e., decodes) the configuration data into theConfig
struct. If there’s an error during unmarshalling, the program prints an error message and exits.
Print Configuration Values: The program prints the configuration values stored in the struct.
what else can viper do..?
It can take input from environment variables, flags etc and combine them into one struct and later that struct can be used all over the application.
Lets see how that is done
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Config struct to hold the configuration values
type Config struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
}
var config Config
func main() {
// Initialize Cobra root command
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "MyApp is an example application to demonstrate Viper and Cobra integration",
Run: func(cmd *cobra.Command, args []string) {
// Unmarshal the configuration into the Config struct
if err := viper.Unmarshal(&config); err != nil {
fmt.Printf("Unable to decode into struct, %v\n", err)
return
}
// Print the configuration values
fmt.Println("Database Host:", config.Host)
fmt.Println("Database Port:", config.Port)
fmt.Println("Database User:", config.User)
fmt.Println("Database Password:", config.Password)
},
}
// Define flags
rootCmd.Flags().String("host", "localhost", "Database host")
rootCmd.Flags().Int("port", 5432, "Database port")
rootCmd.Flags().String("user", "admin", "Database user")
rootCmd.Flags().String("password", "secret", "Database password")
// Bind flags with Viper
viper.BindPFlags(rootCmd.Flags())
// Set environment variable prefix to avoid conflicts with other applications
viper.SetEnvPrefix("myapp")
viper.AutomaticEnv()
// Bind environment variables
viper.BindEnv("host")
viper.BindEnv("port")
viper.BindEnv("user")
viper.BindEnv("password")
// Set the configuration file name and path
viper.SetConfigName("config")
viper.AddConfigPath(".") // Search in the working directory
// Read the configuration file
if err := viper.ReadInConfig(); err != nil {
fmt.Printf("Error reading config file, %s\n", err)
}
// Execute the root command
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
Define the Configuration Struct: Create a struct (
Config
) to hold your configuration values. This struct will map to your configuration file and environment variables.Initialize Cobra Commands: Use Cobra to define your CLI's root command and any subcommands, along with their respective flags.
Bind Cobra Flags with Viper: Use
viper.BindPFlags()
to bind Cobra command flags to Viper. This allows Viper to recognize the flags defined by Cobra, so you can manage them centrally.Set Up Environment Variables: Use
viper.SetEnvPrefix()
to set a prefix for environment variables related to your application. Then, useviper.BindEnv()
to bind specific environment variables to configuration keys.Read Configuration Files: Use
viper.SetConfigName()
andviper.AddConfigPath()
to specify the name and location of your configuration file. Callviper.ReadInConfig()
to read the configuration file into memory.Unmarshal Configuration Data: Use
viper.Unmarshal()
to decode the configuration data into your predefined struct (Config
).
Conclusion
Cobra and Viper are powerful libraries that create a strong base to build an amazing command line tool in Go. Cobra takes the pain out of organizing CLI applications through the commander and create commands and flags very easily. Viper creates flexibility in your applications as it allows you to manage configurations from files, environment variables, and flags.
The combination of the two libraries helps you solve some of the most common CLI pain points, such as many flags and configuration entry points, and helps get your CLI clean and easy to maintain, while providing a great user experience if default configurations are supported. Building utilities may seem simple, but building robust and extensible CLI applications is not. You easily add features without worrying about never-ending code maintainability.
FAQ’s
1. What is Cobra and why is it used for CLI development in Go?
Cobra is a powerful Go library for creating modern CLI applications. It supports subcommands, flags, and intelligent help documentation. It’s used by tools like Kubernetes kubectl for its structure and flexibility.
Cobra makes it easy to build user-friendly and extensible CLI tools.
2. What role does Viper play alongside Cobra in a CLI tool?
Viper handles configuration management in Go applications. It supports JSON, YAML, ENV variables, flags, and more. When paired with Cobra, Viper simplifies config loading and overrides. This makes it ideal for dynamic and portable CLI tools.
3. How do Cobra and Viper work together in a CLI tool?
Cobra manages the CLI structure and arguments, while Viper handles configuration values. You can bind Cobra flags directly to Viper variables. This allows users to configure your tool via flags, config files, or env vars. Together, they create a seamless command and config experience.
4. What are the key steps to building a CLI with Cobra and Viper?
First, initialize your Go project and install Cobra and Viper packages. Define your root command and add subcommands with Cobra. Use Viper to read config files and bind them to flags. Then, test and build your CLI binary for distribution.
5. How can you structure a scalable CLI project with Cobra?
Organize commands into separate files and directories for modularity. Use a cmd/ folder with one file per subcommand for clean code. Maintain a config/ package if needed to manage Viper settings. Follow Go module conventions for easy testing and maintenance.
Subscribe to my newsletter
Read articles from Charan Kamarapu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
