Building CLI Tools with Cobra in Go
Introduction
Cobra is a powerful Go library for building command-line applications. It provides a clean structure for commands, flags, and arguments, making it easy to create professional CLI tools. This guide covers building CLI applications with Cobra.
Cobra powers popular tools like Docker, Kubernetes, and Hugo, demonstrating its capability for building complex CLI applications.
Cobra Fundamentals
Basic Command Structure
package main
import (
"fmt"
"log"
"github.com/spf13/cobra"
)
// rootCmd represents the base command
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello from myapp")
},
}
// Execute adds all child commands to the root command
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
func main() {
Execute()
}
Good: Proper Cobra Implementation
package main
import (
"fmt"
"log"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Config holds application configuration
type Config struct {
Verbose bool
Output string
Format string
}
var config Config
// rootCmd represents the base command
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A powerful CLI application",
Long: `MyApp is a CLI tool for managing resources.
It provides commands for creating, updating, and deleting resources.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if config.Verbose {
fmt.Println("Verbose mode enabled")
}
},
}
// serveCmd represents the serve command
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the server",
Long: "Start the application server on the specified port",
RunE: func(cmd *cobra.Command, args []string) error {
port, err := cmd.Flags().GetString("port")
if err != nil {
return err
}
fmt.Printf("Starting server on port %s\n", port)
return nil
},
}
// createCmd represents the create command
var createCmd = &cobra.Command{
Use: "create [resource-type]",
Short: "Create a new resource",
Long: "Create a new resource of the specified type",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
resourceType := args[0]
name, err := cmd.Flags().GetString("name")
if err != nil {
return err
}
fmt.Printf("Creating %s with name: %s\n", resourceType, name)
return nil
},
}
// listCmd represents the list command
var listCmd = &cobra.Command{
Use: "list [resource-type]",
Short: "List resources",
Long: "List all resources of the specified type",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
resourceType := args[0]
format, err := cmd.Flags().GetString("format")
if err != nil {
return err
}
fmt.Printf("Listing %s in %s format\n", resourceType, format)
return nil
},
}
// deleteCmd represents the delete command
var deleteCmd = &cobra.Command{
Use: "delete [resource-type] [resource-id]",
Short: "Delete a resource",
Long: "Delete a resource by ID",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
resourceType := args[0]
resourceID := args[1]
force, err := cmd.Flags().GetBool("force")
if err != nil {
return err
}
if force {
fmt.Printf("Force deleting %s: %s\n", resourceType, resourceID)
} else {
fmt.Printf("Deleting %s: %s\n", resourceType, resourceID)
}
return nil
},
}
// init initializes the CLI
func init() {
// Root command flags
rootCmd.PersistentFlags().BoolVarP(&config.Verbose, "verbose", "v", false, "Enable verbose output")
rootCmd.PersistentFlags().StringVarP(&config.Output, "output", "o", "", "Output file")
// Serve command flags
serveCmd.Flags().StringP("port", "p", "8080", "Port to listen on")
serveCmd.Flags().StringP("host", "h", "localhost", "Host to listen on")
// Create command flags
createCmd.Flags().StringP("name", "n", "", "Name of the resource")
createCmd.MarkFlagRequired("name")
// List command flags
listCmd.Flags().StringP("format", "f", "table", "Output format (table, json, yaml)")
listCmd.Flags().IntP("limit", "l", 10, "Limit number of results")
// Delete command flags
deleteCmd.Flags().BoolP("force", "f", false, "Force deletion without confirmation")
// Add commands to root
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(createCmd)
rootCmd.AddCommand(listCmd)
rootCmd.AddCommand(deleteCmd)
}
// Execute runs the CLI
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func main() {
Execute()
}
Bad: Improper Cobra Implementation
package main
import (
"fmt"
"github.com/spf13/cobra"
)
// BAD: No error handling
var badCmd = &cobra.Command{
Use: "bad",
Run: func(cmd *cobra.Command, args []string) {
// No error handling
// No validation
fmt.Println("Done")
},
}
// BAD: No flag validation
var badCreateCmd = &cobra.Command{
Use: "create",
Run: func(cmd *cobra.Command, args []string) {
// No required flag validation
// No argument validation
fmt.Println("Created")
},
}
Problems:
- No error handling
- No flag validation
- No argument validation
- No help text
Advanced Cobra Features
Command Groups
package main
import (
"fmt"
"github.com/spf13/cobra"
)
// userCmd represents user management commands
var userCmd = &cobra.Command{
Use: "user",
Short: "Manage users",
Long: "Commands for managing users",
}
// userCreateCmd creates a new user
var userCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a new user",
RunE: func(cmd *cobra.Command, args []string) error {
username, _ := cmd.Flags().GetString("username")
email, _ := cmd.Flags().GetString("email")
fmt.Printf("Creating user: %s (%s)\n", username, email)
return nil
},
}
// userListCmd lists users
var userListCmd = &cobra.Command{
Use: "list",
Short: "List all users",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("Listing users...")
return nil
},
}
// userDeleteCmd deletes a user
var userDeleteCmd = &cobra.Command{
Use: "delete [user-id]",
Short: "Delete a user",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
userID := args[0]
fmt.Printf("Deleting user: %s\n", userID)
return nil
},
}
func initUserCommands() {
userCreateCmd.Flags().StringP("username", "u", "", "Username")
userCreateCmd.Flags().StringP("email", "e", "", "Email")
userCreateCmd.MarkFlagRequired("username")
userCreateCmd.MarkFlagRequired("email")
userCmd.AddCommand(userCreateCmd)
userCmd.AddCommand(userListCmd)
userCmd.AddCommand(userDeleteCmd)
}
Custom Validators
package main
import (
"fmt"
"regexp"
"github.com/spf13/cobra"
)
// ValidateEmail validates an email address
func ValidateEmail(email string) error {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, err := regexp.MatchString(pattern, email)
if err != nil {
return err
}
if !matched {
return fmt.Errorf("invalid email format: %s", email)
}
return nil
}
// ValidatePort validates a port number
func ValidatePort(port string) error {
var p int
_, err := fmt.Sscanf(port, "%d", &p)
if err != nil {
return fmt.Errorf("invalid port: %s", port)
}
if p < 1 || p > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
return nil
}
// userCreateWithValidation creates a user with validation
var userCreateWithValidation = &cobra.Command{
Use: "create",
Short: "Create a new user",
RunE: func(cmd *cobra.Command, args []string) error {
email, _ := cmd.Flags().GetString("email")
if err := ValidateEmail(email); err != nil {
return err
}
fmt.Printf("Creating user with email: %s\n", email)
return nil
},
}
Configuration with Viper
package main
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Short: "Manage configuration",
}
// configSetCmd sets a configuration value
var configSetCmd = &cobra.Command{
Use: "set [key] [value]",
Short: "Set a configuration value",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
key := args[0]
value := args[1]
viper.Set(key, value)
if err := viper.WriteConfig(); err != nil {
return err
}
fmt.Printf("Set %s = %s\n", key, value)
return nil
},
}
// configGetCmd gets a configuration value
var configGetCmd = &cobra.Command{
Use: "get [key]",
Short: "Get a configuration value",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
key := args[0]
value := viper.Get(key)
fmt.Printf("%s = %v\n", key, value)
return nil
},
}
func initConfigCommands() {
configCmd.AddCommand(configSetCmd)
configCmd.AddCommand(configGetCmd)
}
Best Practices
1. Use RunE for Error Handling
// Good: Use RunE to handle errors
var cmd = &cobra.Command{
Use: "example",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
2. Validate Arguments
// Validate arguments
var cmd = &cobra.Command{
Use: "create [name]",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
3. Mark Required Flags
// Mark required flags
cmd.Flags().StringP("name", "n", "", "Name")
cmd.MarkFlagRequired("name")
4. Provide Help Text
// Always provide help text
var cmd = &cobra.Command{
Use: "command",
Short: "Short description",
Long: "Long description with examples",
}
Common Pitfalls
1. No Error Handling
Always use RunE and handle errors properly.
2. Missing Validation
Validate all arguments and flags.
3. Poor Help Text
Provide clear, concise help text.
4. No Configuration Support
Use Viper for configuration management.
Resources
Summary
Cobra makes building CLI applications straightforward. Key takeaways:
- Use command structure for organization
- Implement proper error handling with RunE
- Validate arguments and flags
- Provide clear help text
- Use Viper for configuration
- Mark required flags
- Test commands thoroughly
By mastering Cobra, you can build professional CLI tools efficiently.
Comments