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! โจ
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
- Install Cobra:
1
go get -u github.com/spf13/cobra@latest - 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.", }
- 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]) }, }
- Define Flags:
1
greetCmd.Flags().StringP("name", "n", "World", "Name to greet")
- 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:
- Command-line flags
- Environment variables
- 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
.debpackage for Debian-based systems.
Example for Homebrew:
1
brew install your-tool
3. Cross-Compilation ๐
- Build for Multiple Platforms: Use tools like
Goto 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
What is the recommended way to structure a Cobra CLI application?
Cobra best practice is to use a separate cmd/ directory with individual files for each command (root.go, subcommand1.go, etc.) for better organization and maintainability.
In Viper, which configuration source has the highest precedence?
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?
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?
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?
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 calledsysmon that monitors system resources (CPU, memory, disk) with Cobra commands, Viper configuration, colored output, and progress bars.๐ฏ Requirements
- Cobra Commands: Implement the following commands:
sysmon status- Show current system statussysmon watch- Continuously monitor (with refresh interval flag)sysmon report- Generate a detailed report
- 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)
- System Metrics: Collect and display:
- CPU usage percentage
- Memory usage (used/total)
- Disk usage for root partition
- Number of running processes
- Rich Output:
- Use
tablewriterfor status display - Use
fatih/colorfor warnings (orange) and errors (red) - Use
progressbarfor resource usage bars
- Use
- 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
- Use
github.com/shirou/gopsutil/v3library 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) - Create a Cobra root command with version info and global flags
- Use Viper to load default config from
~/.sysmon/config.yaml - Implement continuous monitoring with
time.Tickerin watch mode - 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 historycommand that logs metrics to a SQLite database and displays trends - Level 3: Implement export to JSON/CSV with
--exportflag - 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!