CLI Tool Development — cobra & flag
Go is an optimal language for CLI tool development. With single binary deployment, fast startup time, and cross-compilation support, it is widely used in DevOps tools.
Standard Library flag Package
For simple CLIs, the standard flag package is sufficient.
// main.go
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Define flags
host := flag.String("host", "localhost", "Server host")
port := flag.Int("port", 8080, "Server port")
verbose := flag.Bool("verbose", false, "Verbose output")
timeout := flag.Duration("timeout", 30*time.Second, "Connection timeout")
// Custom usage message
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\nOptions:\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
// Remaining arguments after parsing (non-flag arguments)
args := flag.Args()
if *verbose {
fmt.Printf("Server: %s:%d, Timeout: %s\n", *host, *port, *timeout)
}
fmt.Printf("Remaining args: %v\n", args)
}
go run main.go -host=example.com -port=9090 -verbose file1.txt file2.txt
# Server: example.com:9090, Timeout: 30s
# Remaining args: [file1.txt file2.txt]
cobra — Production CLI Framework
go get github.com/spf13/cobra@latest
go install github.com/spf13/cobra-cli@latest # Code generator
Project Structure
mycli/
├── cmd/
│ ├── root.go ← Root command
│ ├── serve.go ← serve subcommand
│ ├── user.go ← user subcommand group
│ └── user_list.go ← user list subcommand
├── internal/
│ └── config/
│ └── config.go
└── main.go
Root Command (root.go)
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
verbose bool
)
var rootCmd = &cobra.Command{
Use: "mycli",
Short: "My CLI — A great tool",
Long: `mycli is a feature-complete CLI tool that
automates server management and deployments.`,
// Called before all subcommands
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initConfig()
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
// Persistent flags: applied to root + all subcommands
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Config file path (default: $HOME/.mycli.yaml)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output")
// Local flags: applied to this command only
rootCmd.Flags().BoolP("version", "V", false, "Print version")
}
func initConfig() error {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, _ := os.UserHomeDir()
viper.AddConfigPath(home)
viper.SetConfigName(".mycli")
}
viper.AutomaticEnv() // Automatic environment variable binding
return viper.ReadInConfig()
}
Subcommand (serve.go)
package cmd
import (
"fmt"
"net/http"
"github.com/spf13/cobra"
)
var (
servePort int
serveTLS bool
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start HTTP server",
Long: `Start an HTTP server on the specified port.`,
Example: ` mycli serve
mycli serve --port 9090
mycli serve --port 443 --tls`,
RunE: func(cmd *cobra.Command, args []string) error {
addr := fmt.Sprintf(":%d", servePort)
fmt.Printf("Starting server: %s (TLS: %v)\n", addr, serveTLS)
return http.ListenAndServe(addr, nil)
},
}
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().IntVarP(&servePort, "port", "p", 8080, "Port number")
serveCmd.Flags().BoolVar(&serveTLS, "tls", false, "Enable TLS")
// Required flags
// serveCmd.MarkFlagRequired("port")
}
Nested Subcommand (user.go)
package cmd
import "github.com/spf13/cobra"
// user command group
var userCmd = &cobra.Command{
Use: "user",
Short: "User management",
Long: "Commands for creating, viewing, and deleting users",
}
var userListCmd = &cobra.Command{
Use: "list",
Short: "List users",
RunE: func(cmd *cobra.Command, args []string) error {
format, _ := cmd.Flags().GetString("format")
limit, _ := cmd.Flags().GetInt("limit")
return listUsers(format, limit)
},
}
var userCreateCmd = &cobra.Command{
Use: "create <name> <email>",
Short: "Create a user",
Args: cobra.ExactArgs(2), // Exactly 2 arguments required
RunE: func(cmd *cobra.Command, args []string) error {
name, email := args[0], args[1]
return createUser(name, email)
},
}
func init() {
rootCmd.AddCommand(userCmd)
userCmd.AddCommand(userListCmd)
userCmd.AddCommand(userCreateCmd)
userListCmd.Flags().String("format", "table", "Output format (table|json|yaml)")
userListCmd.Flags().Int("limit", 20, "Maximum output")
}
main.go
package main
import "mycli/cmd"
func main() {
cmd.Execute()
}
Output Formatter Implementation
package cmd
import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func printUsers(users []User, format string) error {
switch format {
case "json":
return json.NewEncoder(os.Stdout).Encode(users)
case "table":
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tNAME\tEMAIL")
fmt.Fprintln(w, "--\t----\t-----")
for _, u := range users {
fmt.Fprintf(w, "%d\t%s\t%s\n", u.ID, u.Name, u.Email)
}
return w.Flush()
default:
return fmt.Errorf("unknown format: %s", format)
}
}
Progress Bars & Color Output
go get github.com/fatih/color
go get github.com/schollz/progressbar/v3
import (
"github.com/fatih/color"
"github.com/schollz/progressbar/v3"
)
func deployWithProgress(items []string) {
// Color output
green := color.New(color.FgGreen, color.Bold)
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
// Progress bar
bar := progressbar.NewOptions(len(items),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionSetDescription("[cyan]Deploying...[reset]"),
progressbar.OptionShowCount(),
)
for _, item := range items {
if err := deploy(item); err != nil {
red.Printf("✗ %s: %v\n", item, err)
} else {
green.Printf("✓ %s\n", item)
}
bar.Add(1)
}
yellow.Println("\nDeployment complete!")
}
Cross Compilation
# Build Linux/Windows binaries from macOS
GOOS=linux GOARCH=amd64 go build -o dist/mycli-linux-amd64 .
GOOS=windows GOARCH=amd64 go build -o dist/mycli-windows-amd64.exe .
GOOS=darwin GOARCH=arm64 go build -o dist/mycli-darwin-arm64 .
# Reduce binary size and inject version info with ldflags
VERSION=$(git describe --tags --always)
go build -ldflags="-s -w -X main.version=${VERSION}" -o mycli .
// main.go
var version = "dev" // Injected via -ldflags
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("mycli %s\n", version)
},
}
Configuration File Integration — viper
import "github.com/spf13/viper"
// Configuration hierarchy (higher priority wins)
// 1. Command-line flags
// 2. Environment variables (MYCLI_PORT)
// 3. Configuration file (.mycli.yaml)
// 4. Default values
viper.SetDefault("server.port", 8080)
viper.SetEnvPrefix("MYCLI") // MYCLI_SERVER_PORT
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Bind flags with viper
rootCmd.PersistentFlags().Int("port", 8080, "Port")
viper.BindPFlag("server.port", rootCmd.PersistentFlags().Lookup("port"))
// Read configuration value
port := viper.GetInt("server.port")
Key Takeaways
| Tool | Purpose |
|---|---|
flag | Simple flags, no external dependencies |
cobra | Subcommands, auto help, auto completion |
viper | Unified config file + environment variables + flags |
tabwriter | Aligned table output |
fatih/color | Colored terminal output |
- cobra + viper are built by the same team, making integration seamless
- Use
RunEto return errors, preferred overRun - Use
PersistentPreRunEfor common auth/config initialization across all commands