Post

33. Building Command-Line Tools

๐Ÿš€ Master CLI development in Go! Learn Cobra framework, Viper configuration, interactive prompts, progress bars, and distribution strategies. Build professional command-line tools! โœจ

33. Building Command-Line Tools

What we will learn in this post?

  • ๐Ÿ‘‰ CLI Design Principles
  • ๐Ÿ‘‰ Using Cobra Framework
  • ๐Ÿ‘‰ Configuration Management
  • ๐Ÿ‘‰ Interactive Prompts and Input
  • ๐Ÿ‘‰ Progress Bars and Output Formatting
  • ๐Ÿ‘‰ Distribution and Updates

Principles for Good CLI Design ๐Ÿš€

Creating a Command Line Interface (CLI) that users love is all about simplicity and clarity. Professional tools like Docker, Git, and kubectl all follow these core design principles.

1. Clear Command Structure ๐Ÿ—‚๏ธ

A clear command structure helps users understand how to use your CLI. For example:

1
2
# Correct usage
mycli list --all

2. Helpful Error Messages โŒ

When something goes wrong, provide clear feedback:

1
2
# Error message example
Error: Invalid command 'xyz'. Use 'mycli help' for a list of commands.

3. Consistent Flag Naming ๐Ÿท๏ธ

Use consistent naming for flags to avoid confusion:

1
2
3
# Consistent flag usage
mycli --verbose
mycli --quiet

4. UNIX Philosophy ๐Ÿ’ก

Follow the UNIX philosophy: do one thing well. Each command should focus on a single task.

5. stdin/stdout Usage ๐Ÿ”„

Utilize standard input and output for flexibility:

1
2
# Using stdin and stdout
cat file.txt | mycli process

6. Exit Codes โœ…

Use exit codes to indicate success or failure:

1
2
3
# Exit code example
exit 0  # Success
exit 1  # Error

User Experience Considerations ๐Ÿ˜Š

  • Intuitive Commands: Make commands easy to remember.
  • Documentation: Provide clear help and usage instructions.
  • Feedback: Give users feedback on their actions.

For more in-depth reading, check out CLI Design Best Practices.

graph LR
  START["User Input"]:::style1 --> PARSE["Parse Command"]:::style2
  PARSE --> VALIDATE{"Valid?"}:::style3
  VALIDATE -- "Yes" --> EXECUTE["Execute Action"]:::style4
  VALIDATE -- "No" --> ERROR["Show Error"]:::style5
  EXECUTE --> SUCCESS["Success Output"]:::style6
  ERROR --> HELP["Display Help"]:::style7

  classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style6 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style7 fill:#9e9e9e,stroke:#616161,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

  class START style1;
  class PARSE style2;
  class VALIDATE style3;
  class EXECUTE style4;
  class ERROR style5;
  class SUCCESS style6;
  class HELP style7;

  linkStyle default stroke:#e67e22,stroke-width:3px;

By following these principles, you can create a CLI that is not only functional but also enjoyable to use!

Building CLI Applications with Cobra Framework ๐Ÿš€

Creating a Command Line Interface (CLI) app with the Cobra framework is fun and straightforward! Cobra powers major tools like kubectl, Hugo, and GitHub CLI.

graph TD
  ROOT["Root Command<br/>mycli"]:::style1 --> CMD1["Subcommand: greet"]:::style2
  ROOT --> CMD2["Subcommand: version"]:::style3
  ROOT --> CMD3["Subcommand: config"]:::style4
  CMD1 --> FLAG1["--name flag"]:::style5
  CMD1 --> FLAG2["--verbose flag"]:::style5
  CMD2 --> OUTPUT1["Display version"]:::style6
  CMD3 --> OUTPUT2["Show config"]:::style6

  classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style3 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style4 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style5 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style6 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

  class ROOT style1;
  class CMD1 style2;
  class CMD2 style3;
  class CMD3 style4;
  class FLAG1,FLAG2 style5;
  class OUTPUT1,OUTPUT2 style6;

  linkStyle default stroke:#e67e22,stroke-width:3px;

Basic Structure

Hereโ€™s a simple structure for your CLI app:

1
2
3
4
5
6
myapp/
โ”œโ”€โ”€ main.go
โ””โ”€โ”€ cmd/
    โ”œโ”€โ”€ root.go
    โ”œโ”€โ”€ greet.go
    โ””โ”€โ”€ version.go

Setting Up Your App

  1. Install Cobra:
    1
    
    go get -u github.com/spf13/cobra@latest
    
  2. Create the Root Command: In root.go:
    1
    2
    3
    4
    5
    
    var rootCmd = &cobra.Command{
        Use:   "myapp",
        Short: "My CLI Application",
        Long:  "This is a simple CLI application built with Cobra.",
    }
    
  3. Add Subcommands: In greet.go:
    1
    2
    3
    4
    5
    6
    7
    
    var greetCmd = &cobra.Command{
        Use:   "greet",
        Short: "Greet someone",
        Run: func(cmd *cobra.Command, args []string) {
            fmt.Println("Hello, " + args[0])
        },
    }
    
  4. Define Flags:
    1
    
    greetCmd.Flags().StringP("name", "n", "World", "Name to greet")
    
  5. Group Commands: You can group commands logically for better organization.

Real-World Example: Task Management CLI

Hereโ€™s a production-ready task management tool using Cobra:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
    "fmt"
    "github.com/spf13/cobra"
    "os"
)

var rootCmd = &cobra.Command{
    Use:   "taskmgr",
    Short: "Task Management CLI",
    Long:  "A production-ready task management tool built with Cobra",
}

var addCmd = &cobra.Command{
    Use:   "add [task description]",
    Short: "Add a new task",
    Args:  cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        task := args[0]
        priority, _ := cmd.Flags().GetString("priority")
        fmt.Printf("โœ“ Added task: %s (Priority: %s)\n", task, priority)
    },
}

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "List all tasks",
    Run: func(cmd *cobra.Command, args []string) {
        completed, _ := cmd.Flags().GetBool("completed")
        if completed {
            fmt.Println("Completed tasks:")
        } else {
            fmt.Println("Active tasks:")
        }
        fmt.Println("1. Write documentation")
        fmt.Println("2. Fix bug #123")
    },
}

func init() {
    addCmd.Flags().StringP("priority", "p", "medium", "Task priority (low/medium/high)")
    listCmd.Flags().BoolP("completed", "c", false, "Show completed tasks")
    
    rootCmd.AddCommand(addCmd)
    rootCmd.AddCommand(listCmd)
}

func main() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

Help Text

Cobra automatically generates help text! Just run:

1
myapp help

Resources

With this setup, you can create powerful CLI applications easily! Happy coding! ๐ŸŽ‰

Managing Configuration with Viper ๐ŸŒŸ

Viper is a powerful library in Go for managing configuration. Itโ€™s the same library used by Hugo and Cobra for configuration management.

graph LR
  CONFIG["Config Sources"]:::style1 --> FILE["Config Files<br/>(YAML/JSON/TOML)"]:::style2
  CONFIG --> ENV["Environment<br/>Variables"]:::style3
  CONFIG --> FLAGS["Command-Line<br/>Flags"]:::style4
  FILE --> VIPER["Viper<br/>Processor"]:::style5
  ENV --> VIPER
  FLAGS --> VIPER
  VIPER --> APP["Application<br/>Configuration"]:::style6

  classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style3 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style4 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style5 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style6 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

  class CONFIG style1;
  class FILE style2;
  class ENV style3;
  class FLAGS style4;
  class VIPER style5;
  class APP style6;

  linkStyle default stroke:#e67e22,stroke-width:3px;

Reading Config Files ๐Ÿ“„

Viper supports multiple formats such as JSON, YAML, and TOML. Hereโ€™s how to read a YAML file:

1
2
3
4
5
6
7
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("yaml")    // or "json", "toml"
viper.AddConfigPath(".")       // path to look for the config file
err := viper.ReadInConfig()     // read the config
if err != nil {
    log.Fatal(err)
}

Environment Variables ๐ŸŒ

You can also set configurations using environment variables. Viper automatically reads them if you set them up correctly. For example, if you have a variable MYAPP_PORT, you can access it like this:

1
port := viper.GetString("port")

Command-Line Flags ๐ŸŽ›๏ธ

Viper works well with command-line flags. You can bind flags to Viper like this:

1
2
flag.String("port", "8080", "Port to run the application")
viper.BindPFlag("port", flag.Lookup("port"))

Configuration Precedence โš–๏ธ

Viper follows a specific order of precedence:

  1. Command-line flags
  2. Environment variables
  3. Config files

This means if a value is set in multiple places, the last one wins!

Watching for Changes ๐Ÿ‘€

You can watch for changes in your config file and reload automatically:

1
2
3
4
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
    fmt.Println("Config file changed:", e.Name)
})

Real-World Example: API Client Configuration

Hereโ€™s a production-ready configuration setup for an API client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
    "fmt"
    "github.com/spf13/viper"
    "log"
)

type APIConfig struct {
    Host     string
    Port     int
    APIKey   string
    Timeout  int
    Debug    bool
}

func LoadConfig() (*APIConfig, error) {
    // Set defaults
    viper.SetDefault("host", "api.example.com")
    viper.SetDefault("port", 443)
    viper.SetDefault("timeout", 30)
    viper.SetDefault("debug", false)

    // Config file
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
    viper.AddConfigPath("$HOME/.myapp")

    // Environment variables
    viper.SetEnvPrefix("MYAPP")
    viper.AutomaticEnv()

    // Read config
    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
            return nil, err
        }
    }

    // Watch for changes
    viper.WatchConfig()

    config := &APIConfig{
        Host:    viper.GetString("host"),
        Port:    viper.GetInt("port"),
        APIKey:  viper.GetString("apikey"),
        Timeout: viper.GetInt("timeout"),
        Debug:   viper.GetBool("debug"),
    }

    return config, nil
}

func main() {
    config, err := LoadConfig()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("API Host: %s:%d\n", config.Host, config.Port)
    fmt.Printf("Debug Mode: %v\n", config.Debug)
}

Resources ๐Ÿ“š

With Viper, managing your appโ€™s configuration becomes a breeze! Happy coding! ๐ŸŽ‰

Creating Interactive CLI Experiences ๐ŸŽ‰

Creating interactive command-line interfaces (CLI) can be fun and engaging! Libraries like PromptUI bring the same user experience as tools like npm init and create-react-app.

%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#ff4f81','primaryTextColor':'#fff','primaryBorderColor':'#c43e3e','lineColor':'#e67e22','secondaryColor':'#6b5bff','tertiaryColor':'#ffd700'}}}%%
sequenceDiagram
    participant U as ๐Ÿ‘ค User
    participant P as ๐ŸŽฏ PromptUI
    participant A as โš™๏ธ Application
    
    Note over U,A: Interactive Input Flow
    U->>+P: Start prompt
    P->>P: Display options/input field
    P-->>U: Show interface
    U->>P: Provide input
    P->>P: Validate input
    alt Valid Input
        P->>-A: Return value โœ“
        A->>U: Process & continue
    else Invalid Input
        P->>U: Show error โŒ
        P->>P: Retry prompt
    end
1
end ```

Using PromptUI Library

Input Types

You can implement various input types:

  • Text Input: For general text.
  • Select: For choosing from options.
  • Confirm: For yes/no questions.
  • Password: For sensitive information.

Example Code

Hereโ€™s a simple example using PromptUI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
    "fmt"
    "github.com/manifoldco/promptui"
)

func main() {
    // Text Input
    prompt := promptui.Prompt{
        Label: "Enter your name",
    }
    name, _ := prompt.Run()

    // Select Input
    selectPrompt := promptui.Select{
        Label: "Choose a color",
        Items: []string{"Red", "Green", "Blue"},
    }
    _, color, _ := selectPrompt.Run()

    // Confirm Input
    confirmPrompt := promptui.Prompt{
        Label:     "Continue?",
        IsConfirm: true,
    }
    _, err := confirmPrompt.Run()
    if err != nil {
        fmt.Println("Cancelled")
        return
    }

    fmt.Printf("Hello %s! You chose %s.\n", name, color)
}

Real-World Example: Project Initializer

Hereโ€™s a complete project setup wizard using PromptUI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
    "fmt"
    "github.com/manifoldco/promptui"
    "strings"
)

type ProjectConfig struct {
    Name        string
    Type        string
    Language    string
    UseDocker   bool
    UseCI       bool
}

func main() {
    config := &ProjectConfig{}

    // Project name with validation
    namePrompt := promptui.Prompt{
        Label: "Project Name",
        Validate: func(input string) error {
            if len(input) < 3 {
                return fmt.Errorf("name must be at least 3 characters")
            }
            return nil
        },
    }
    name, _ := namePrompt.Run()
    config.Name = name

    // Project type selection
    typePrompt := promptui.Select{
        Label: "Select Project Type",
        Items: []string{"Web Application", "CLI Tool", "Library", "Microservice"},
    }
    _, projectType, _ := typePrompt.Run()
    config.Type = projectType

    // Language selection
    langPrompt := promptui.Select{
        Label: "Select Programming Language",
        Items: []string{"Go", "Python", "JavaScript", "Rust"},
    }
    _, lang, _ := langPrompt.Run()
    config.Language = lang

    // Docker confirmation
    dockerPrompt := promptui.Prompt{
        Label:     "Add Docker support",
        IsConfirm: true,
    }
    _, err := dockerPrompt.Run()
    config.UseDocker = (err == nil)

    // CI/CD confirmation
    ciPrompt := promptui.Prompt{
        Label:     "Setup CI/CD",
        IsConfirm: true,
    }
    _, err = ciPrompt.Run()
    config.UseCI = (err == nil)

    // Display summary
    fmt.Println("\nโœ“ Project Configuration:")
    fmt.Printf("  Name: %s\n", config.Name)
    fmt.Printf("  Type: %s\n", config.Type)
    fmt.Printf("  Language: %s\n", config.Language)
    fmt.Printf("  Docker: %v\n", config.UseDocker)
    fmt.Printf("  CI/CD: %v\n", config.UseCI)
}

Input Validation

You can add validation to ensure correct input. For example, check if the name is not empty.

Handling Interrupts

Use os.Interrupt to gracefully handle user interrupts.

Resources

Creating interactive CLI applications can enhance user experience. Happy coding! ๐Ÿš€

Creating Polished CLI Output ๐ŸŽจ

Creating a great Command Line Interface (CLI) can be fun! Professional tools like Docker, Terraform, and npm use these libraries to provide rich visual feedback.

graph TD
  APP["CLI Application"]:::style1 --> PROGRESS["Progress Tracking"]:::style2
  APP --> COLOR["Colored Output"]:::style3
  APP --> TABLE["Table Display"]:::style4
  APP --> SPINNER["Loading Indicators"]:::style5
  
  PROGRESS --> LIB1["progressbar lib"]:::style6
  SPINNER --> LIB2["uiprogress lib"]:::style6
  COLOR --> LIB3["fatih/color lib"]:::style6
  TABLE --> LIB4["tablewriter lib"]:::style6
  
  LIB1 --> OUTPUT["Enhanced<br/>User Experience"]:::style7
  LIB2 --> OUTPUT
  LIB3 --> OUTPUT
  LIB4 --> OUTPUT

  classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style3 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style4 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style5 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style6 fill:#9e9e9e,stroke:#616161,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style7 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

  class APP style1;
  class PROGRESS style2;
  class COLOR style3;
  class TABLE style4;
  class SPINNER style5;
  class LIB1,LIB2,LIB3,LIB4 style6;
  class OUTPUT style7;

  linkStyle default stroke:#e67e22,stroke-width:3px;

Adding Progress Bars โณ

Using progress bars helps users see how much work is done. Hereโ€™s a simple example using the progressbar library:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
    "github.com/schollz/progressbar/v3"
    "time"
)

func main() {
    bar := progressbar.New(100)
    for i := 0; i < 100; i++ {
        time.Sleep(50 * time.Millisecond)
        bar.Add(1)
    }
}

Using Spinner Indicators ๐Ÿ”„

Spinners keep users engaged while waiting. You can use the uiprogress library for this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
    "github.com/gosuri/uiprogress"
    "time"
)

func main() {
    uiprogress.Start()
    spinner := uiprogress.AddSpinner()
    spinner.Start()
    time.Sleep(3 * time.Second)
    spinner.Stop()
}

Colored Output ๐ŸŽจ

With fatih/color, you can add colors to your text:

1
2
3
4
5
6
7
8
9
10
package main

import (
    "github.com/fatih/color"
)

func main() {
    color.Red("This is red text!")
    color.Green("This is green text!")
}

Table Formatting ๐Ÿ“Š

Tables make data easy to read. Use libraries like tablewriter:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
    "github.com/olekukonko/tablewriter"
    "os"
)

func main() {
    table := tablewriter.NewWriter(os.Stdout)
    table.SetHeader([]string{"Name", "Age"})
    table.Append([]string{"Alice", "30"})
    table.Render()
}

Real-World Example: File Processor with Rich Output

Hereโ€™s a production-ready file processor with progress bars, colors, and tables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package main

import (
    "fmt"
    "github.com/fatih/color"
    "github.com/olekukonko/tablewriter"
    "github.com/schollz/progressbar/v3"
    "os"
    "time"
)

type FileStats struct {
    Name      string
    Size      int64
    Processed bool
    Duration  time.Duration
}

func main() {
    color.Cyan("๐Ÿš€ Starting File Processor...\n\n")

    files := []string{"data1.csv", "data2.csv", "data3.csv", "data4.csv", "data5.csv"}
    stats := []FileStats{}

    // Progress bar
    bar := progressbar.NewOptions(len(files),
        progressbar.OptionEnableColorCodes(true),
        progressbar.OptionShowCount(),
        progressbar.OptionSetWidth(40),
        progressbar.OptionSetDescription("[cyan]Processing files...[reset]"),
        progressbar.OptionSetTheme(progressbar.Theme{
            Saucer:        "[green]=[reset]",
            SaucerHead:    "[green]>[reset]",
            SaucerPadding: " ",
            BarStart:      "[",
            BarEnd:        "]",
        }))

    for _, file := range files {
        start := time.Now()
        
        // Simulate processing
        time.Sleep(500 * time.Millisecond)
        
        stats = append(stats, FileStats{
            Name:      file,
            Size:      1024 * (int64(len(file)) * 100),
            Processed: true,
            Duration:  time.Since(start),
        })
        
        bar.Add(1)
    }

    fmt.Println("\n")
    color.Green("โœ“ Processing Complete!\n\n")

    // Display results in table
    table := tablewriter.NewWriter(os.Stdout)
    table.SetHeader([]string{"File", "Size (KB)", "Status", "Duration"})
    table.SetBorder(true)
    table.SetRowLine(true)
    table.SetHeaderColor(
        tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor},
        tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor},
        tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor},
        tablewriter.Colors{tablewriter.Bold, tablewriter.FgCyanColor},
    )

    for _, stat := range stats {
        status := "โœ“ Done"
        table.Append([]string{
            stat.Name,
            fmt.Sprintf("%d", stat.Size/1024),
            status,
            fmt.Sprintf("%dms", stat.Duration.Milliseconds()),
        })
    }

    table.Render()

    // Summary with colors
    color.Yellow("\n๐Ÿ“Š Summary:\n")
    fmt.Printf("Total Files: %s\n", color.CyanString("%d", len(stats)))
    fmt.Printf("Success Rate: %s\n", color.GreenString("100%%"))
    fmt.Printf("Total Size: %s\n", color.MagentaString("%.2f MB", float64(sumSizes(stats))/1024/1024))
}

func sumSizes(stats []FileStats) int64 {
    var total int64
    for _, s := range stats {
        total += s.Size
    }
    return total
}

Conclusion ๐ŸŽ‰

By combining these tools, you can create a polished CLI that is user-friendly and visually appealing. For more details, check out the following resources:

Happy coding! ๐Ÿ˜Š

Distributing CLI Tools Made Easy ๐Ÿš€

Distributing your Command Line Interface (CLI) tools can be simple and effective! Tools like kubectl, gh, and terraform all use these strategies for cross-platform distribution.

graph TB
  BUILD["Build Binary"]:::style1 --> CROSS["Cross-Compile<br/>for Platforms"]:::style2
  CROSS --> WIN["Windows .exe"]:::style3
  CROSS --> MAC["macOS binary"]:::style3
  CROSS --> LIN["Linux binary"]:::style3
  
  WIN --> CHECK1["Generate<br/>Checksum"]:::style4
  MAC --> CHECK2["Generate<br/>Checksum"]:::style4
  LIN --> CHECK3["Generate<br/>Checksum"]:::style4
  
  CHECK1 --> GH["GitHub Release"]:::style5
  CHECK2 --> GH
  CHECK3 --> GH
  
  GH --> PKG1["Homebrew"]:::style6
  GH --> PKG2["APT"]:::style6
  GH --> PKG3["Chocolatey"]:::style6
  
  PKG1 --> USER["Users"]:::style7
  PKG2 --> USER
  PKG3 --> USER
  GH --> USER

  classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style3 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style4 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style6 fill:#9e9e9e,stroke:#616161,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
  classDef style7 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;

  class BUILD style1;
  class CROSS style2;
  class WIN,MAC,LIN style3;
  class CHECK1,CHECK2,CHECK3 style4;
  class GH style5;
  class PKG1,PKG2,PKG3 style6;
  class USER style7;

  linkStyle default stroke:#e67e22,stroke-width:3px;

1. GitHub Releases ๐Ÿ“ฆ

  • Create a Release: Use GitHub to package your tool.
  • Upload Binaries: Add your compiled binaries for different platforms.
  • Tag Your Release: This helps users find the right version easily.

2. Package Managers ๐Ÿ› ๏ธ

  • Homebrew (macOS): Create a formula for easy installation.
  • APT (Linux): Make a .deb package for Debian-based systems.

Example for Homebrew:

1
brew install your-tool

3. Cross-Compilation ๐ŸŒ

  • Build for Multiple Platforms: Use tools like Go to compile your tool for Windows, macOS, and Linux from one codebase.

4. Checksums for Verification ๐Ÿ”’

  • Generate Checksums: Provide checksums for your binaries to ensure integrity.
  • Example Command:
    1
    
    shasum -a 256 your-tool
    

5. Self-Update Functionality ๐Ÿ”„

  • Use go-update Library: Implement self-update features in your CLI tool.
  • Keep Users Updated: Automatically check for new versions and update seamlessly.

Real-World Example: Cross-Platform Build Script

Hereโ€™s a production-ready Makefile for cross-compilation and distribution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
APP_NAME := mytool
VERSION := 1.0.0
BUILD_DIR := dist
PLATFORMS := darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64

.PHONY: build
build:
	@echo "Building $(APP_NAME) v$(VERSION)..."
	@mkdir -p $(BUILD_DIR)
	@for platform in $(PLATFORMS); do \
		GOOS=$${platform%/*} GOARCH=$${platform#*/} \
		go build -ldflags="-s -w -X main.version=$(VERSION)" \
		-o $(BUILD_DIR)/$(APP_NAME)-$${platform%/*}-$${platform#*/} main.go; \
		echo "โœ“ Built $${platform%/*}-$${platform#*/}"; \
	done

.PHONY: checksums
checksums:
	@echo "Generating checksums..."
	@cd $(BUILD_DIR) && sha256sum * > checksums.txt
	@echo "โœ“ Checksums generated"

.PHONY: release
release: build checksums
	@echo "Creating GitHub release v$(VERSION)..."
	@gh release create v$(VERSION) $(BUILD_DIR)/* \
		--title "Release v$(VERSION)" \
		--notes "Release notes here"
	@echo "โœ“ Release created"

.PHONY: clean
clean:
	@rm -rf $(BUILD_DIR)
	@echo "โœ“ Cleaned build directory"

Hereโ€™s the Go code with self-update functionality:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
    "fmt"
    "github.com/inconshreveable/go-update"
    "io"
    "net/http"
)

var version = "1.0.0"

func checkForUpdates() error {
    resp, err := http.Get("https://api.github.com/repos/user/mytool/releases/latest")
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    // Parse response and check version
    // If new version available, download and update
    
    return nil
}

func performUpdate(downloadURL string) error {
    resp, err := http.Get(downloadURL)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    err = update.Apply(resp.Body, update.Options{})
    if err != nil {
        return err
    }

    fmt.Println("โœ“ Update successful! Restart the application.")
    return nil
}

func main() {
    fmt.Printf("mytool v%s\n", version)
    
    // Check for updates
    if err := checkForUpdates(); err != nil {
        fmt.Printf("Update check failed: %v\n", err)
    }
}

Resources

For more detailed guides, check out GitHub Docs and Go Documentation.


๐Ÿง  Test Your Knowledge

In Viper, which configuration source has the highest precedence?
Explanation

Command-line flags have the highest precedence in Viper, followed by environment variables, then config files, and finally default values. This allows users to override any setting at runtime.

What library would you use to create interactive selection prompts in Go?
Explanation

PromptUI is specifically designed for creating interactive selection prompts, text inputs, and confirmations in CLI applications. It provides a rich user interface similar to tools like npm init.

Which Go build flag is commonly used to reduce binary size for distribution?
Explanation

The flags -ldflags="-s -w" strip debugging information and symbol tables from the binary, significantly reducing its size. -s removes the symbol table and -w removes DWARF debugging information.

What is the purpose of exit codes in CLI applications?
Explanation

Exit codes (exit 0 for success, non-zero for errors) allow CLI tools to communicate their execution status to the calling process, enabling scripts and automation tools to handle success and failure appropriately.


๐ŸŽฏ Hands-On Assignment: Build a System Monitoring CLI Tool ๐Ÿš€

๐Ÿ“ Your Mission

Create a production-ready CLI tool called sysmon that monitors system resources (CPU, memory, disk) with Cobra commands, Viper configuration, colored output, and progress bars.

๐ŸŽฏ Requirements

  1. Cobra Commands: Implement the following commands:
    • sysmon status - Show current system status
    • sysmon watch - Continuously monitor (with refresh interval flag)
    • sysmon report - Generate a detailed report
  2. Viper Configuration: Support config file (YAML) with:
    • Refresh interval (default: 2 seconds)
    • Alert thresholds (CPU: 80%, Memory: 85%, Disk: 90%)
    • Output format (table, json, or colored)
  3. System Metrics: Collect and display:
    • CPU usage percentage
    • Memory usage (used/total)
    • Disk usage for root partition
    • Number of running processes
  4. Rich Output:
    • Use tablewriter for status display
    • Use fatih/color for warnings (orange) and errors (red)
    • Use progressbar for resource usage bars
  5. Flags: Add command-line flags:
    • --interval, -i - Refresh interval for watch mode
    • --format, -f - Output format (table/json/colored)
    • --alert - Enable alert mode (show warnings when thresholds exceeded)

๐Ÿ’ก Implementation Hints

  1. Use github.com/shirou/gopsutil/v3 library for system metrics:
    import (
        "github.com/shirou/gopsutil/v3/cpu"
        "github.com/shirou/gopsutil/v3/mem"
        "github.com/shirou/gopsutil/v3/disk"
    )
    
    // Get CPU percentage
    cpuPercent, _ := cpu.Percent(time.Second, false)
    
    // Get memory info
    memInfo, _ := mem.VirtualMemory()
    fmt.Printf("Memory: %.2f%%\n", memInfo.UsedPercent)
    
    // Get disk usage
    diskInfo, _ := disk.Usage("/")
    fmt.Printf("Disk: %.2f%%\n", diskInfo.UsedPercent)
    
  2. Create a Cobra root command with version info and global flags
  3. Use Viper to load default config from ~/.sysmon/config.yaml
  4. Implement continuous monitoring with time.Ticker in watch mode
  5. Color-code output: green (0-60%), yellow (60-80%), red (>80%)

๐Ÿš€ Example Input/Output

$ sysmon status
โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚      System Status Report            โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Resource    โ”‚ Usage      โ”‚ Status   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ CPU         โ”‚ 45.2%      โ”‚ โœ“ Normal โ”‚
โ”‚ Memory      โ”‚ 78.5%      โ”‚ โš  High   โ”‚
โ”‚ Disk        โ”‚ 65.0%      โ”‚ โœ“ Normal โ”‚
โ”‚ Processes   โ”‚ 312        โ”‚ -        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

$ sysmon watch --interval 5 --alert
๐Ÿ”„ Monitoring system (refresh: 5s, press Ctrl+C to stop)

CPU    [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘] 42%
Memory [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘] 68%
Disk   [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘] 61%

โš  Warning: Memory usage above 65%!
โœ“ All other resources within normal range

๐Ÿ† Bonus Challenges

  • Level 2: Add sysmon history command that logs metrics to a SQLite database and displays trends
  • Level 3: Implement export to JSON/CSV with --export flag
  • Level 4: Add interactive mode using PromptUI to select which metrics to display
  • Level 5: Implement self-update functionality to check and install new versions from GitHub releases
  • Level 6: Cross-compile for multiple platforms and generate checksums automatically

๐Ÿ“š Learning Goals

  • Master Cobra framework for building professional CLI tools ๐ŸŽฏ
  • Implement Viper configuration with multiple sources (file, env, flags) โš™๏ธ
  • Create rich terminal output with colors, tables, and progress bars โœจ
  • Collect and display real-time system metrics ๐Ÿ“Š
  • Handle continuous monitoring with graceful shutdown ๐Ÿ”„

๐Ÿ’ก Pro Tip: This pattern is used in popular tools like kubectl (Kubernetes), gh (GitHub CLI), and terraform! Production CLI tools prioritize user experience with clear output, helpful errors, and configurable behavior.

Share Your Solution! ๐Ÿ’ฌ

Completed the project? Post your code in the comments below! Show us your Go CLI mastery! ๐Ÿš€โœจ


Conclusion ๐ŸŽฏ

Building command-line tools in Go with Cobra, Viper, and output libraries creates professional, user-friendly applications. Apply these patterns to build your next CLI tool!

This post is licensed under CC BY 4.0 by the author.