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?
go
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:
Subcommands:
Key Functions and Fields of Cobra Commands:
Adding Flags to Commands:
go
helloCmd.Flags().StringVarP(&name, "name", "n", "World", "name to greet")
go
goodbyeCmd.Flags().BoolVarP(&excited, "excited", "e", false, "say goodbye with excitement")
Adding Subcommands to the Root Command:
go
rootCmd.AddCommand(helloCmd)
rootCmd.AddCommand(goodbyeCmd)
How Cobra Processes Commands Internally
bash
go build -o cobracli
cmds:
bash
./cobracli
./cobracli --help
./cobracli hello
./cobracli hello --name=keploy
./cobracli hello -n keploy
./cobracli goodbye
./cobracli goodbye --excited
./cobracli goodbye -e
Difficulties with cobra
Lets see if these problems can be solved by viper
Recommended by LinkedIn
How Viper Works?
config.yaml
yaml
app:
name: "MyApp"
version: "1.0"
database:
host: "localhost"
port: 5432
user: "admin"
password: "secret"
go
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
bash
go build -o configreader
./configreader
Define the Config Struct:
Set Configuration File Name and Path:
Read Configuration File:
Unmarshal Configuration into Struct:
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
go
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)
}
}
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.
This Article is sourced from Keploy.io