CLI Tools in Go
Build command-line tools with the flag package, cobra CLI framework, and work with os.Args, stdin, stdout, and stderr.
The flag Package
The standard library flag package handles command-line flags:
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Define flags
host := flag.String("host", "localhost", "server hostname")
port := flag.Int("port", 8080, "server port")
verbose := flag.Bool("verbose", false, "enable verbose output")
timeout := flag.Duration("timeout", 30*time.Second, "request timeout")
flag.Parse() // parse os.Args
// Remaining non-flag arguments
args := flag.Args()
if *verbose {
fmt.Printf("connecting to %s:%d (timeout: %v)\n", *host, *port, *timeout)
}
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "error: at least one argument required")
flag.Usage()
os.Exit(1)
}
fmt.Printf("host=%s port=%d args=%v\n", *host, *port, args)
}
./mytool -host=api.example.com -port=443 -verbose file.txt
# connecting to api.example.com:443 (timeout: 30s)
# host=api.example.com port=443 args=[file.txt]
Custom flag.Usage
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] <files...>\n\n", os.Args[0])
fmt.Fprintln(os.Stderr, "Options:")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, "\nExamples:")
fmt.Fprintln(os.Stderr, " mytool -port 9090 file.txt")
}
os.Args — Raw Argument Access
// os.Args[0] = program name
// os.Args[1:] = arguments
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: greet <name>")
os.Exit(1)
}
name := os.Args[1]
fmt.Printf("Hello, %s!\n", name)
Reading stdin/stdout/stderr
// Write to stderr (errors, logs)
fmt.Fprintln(os.Stderr, "error: something went wrong")
// Read all stdin (for pipe input)
data, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("read %d bytes\n", len(data))
// Read stdin line by line
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
// process line
fmt.Println(strings.ToUpper(line))
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading stdin:", err)
os.Exit(1)
}
Detect if stdin is a pipe
func stdinIsPipe() bool {
stat, err := os.Stdin.Stat()
if err != nil {
return false
}
return (stat.Mode() & os.ModeCharDevice) == 0
}
func main() {
if stdinIsPipe() {
// process piped input
data, _ := io.ReadAll(os.Stdin)
process(data)
} else {
// interactive — prompt for input
fmt.Print("Enter input: ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
process([]byte(scanner.Text()))
}
}
Environment Variables
// Read
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
dbURL = "postgres://localhost/mydb" // default
}
// Read with a helper
func getEnv(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return fallback
}
// Set (for the current process)
os.Setenv("DEBUG", "true")
// All environment variables
for _, env := range os.Environ() {
fmt.Println(env) // KEY=VALUE
}
Cobra CLI Framework
Cobra is the standard library for multi-command CLIs (used by kubectl, gh, Hugo):
go get github.com/spf13/cobra@latest
Project structure
myapp/
├── main.go
└── cmd/
├── root.go
├── serve.go
└── migrate.go
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
Long: `A longer description of my application.`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
}
// cmd/serve.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the HTTP server",
RunE: func(cmd *cobra.Command, args []string) error {
port, _ := cmd.Flags().GetInt("port")
verbose, _ := cmd.Root().PersistentFlags().GetBool("verbose")
if verbose {
fmt.Printf("starting server on port %d\n", port)
}
return startServer(port)
},
}
func init() {
rootCmd.AddCommand(serveCmd)
serveCmd.Flags().IntP("port", "p", 8080, "HTTP port to listen on")
}
// cmd/migrate.go
package cmd
var migrateCmd = &cobra.Command{
Use: "migrate [up|down]",
Short: "Run database migrations",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
direction := args[0]
if direction != "up" && direction != "down" {
return fmt.Errorf("direction must be 'up' or 'down', got %q", direction)
}
return runMigrations(direction)
},
}
func init() {
rootCmd.AddCommand(migrateCmd)
}
// main.go
package main
import "myapp/cmd"
func main() {
cmd.Execute()
}
./myapp serve --port 9090 -v
./myapp migrate up
./myapp --help
./myapp serve --help
Colored Output
// Using ANSI codes directly (no dependency)
const (
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorBlue = "\033[34m"
colorReset = "\033[0m"
)
func success(msg string) { fmt.Printf("%s✓%s %s\n", colorGreen, colorReset, msg) }
func failure(msg string) { fmt.Fprintf(os.Stderr, "%s✗%s %s\n", colorRed, colorReset, msg) }
func info(msg string) { fmt.Printf("%s→%s %s\n", colorBlue, colorReset, msg) }
Progress Indicators
func showProgress(total int, fn func(i int)) {
for i := 0; i < total; i++ {
fn(i)
pct := float64(i+1) / float64(total) * 100
filled := int(pct / 5)
bar := strings.Repeat("█", filled) + strings.Repeat("░", 20-filled)
fmt.Printf("\r[%s] %.0f%%", bar, pct)
}
fmt.Println()
}
showProgress(50, func(i int) {
time.Sleep(50 * time.Millisecond) // simulate work
})
Practical Example — File Line Counter
package main
import (
"bufio"
"flag"
"fmt"
"os"
)
func countLines(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
return 0, err
}
defer f.Close()
count := 0
scanner := bufio.NewScanner(f)
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
func main() {
total := flag.Bool("total", false, "print only the total")
flag.Parse()
files := flag.Args()
if len(files) == 0 {
// read from stdin
scanner := bufio.NewScanner(os.Stdin)
n := 0
for scanner.Scan() {
n++
}
fmt.Printf("%8d\n", n)
return
}
grand := 0
for _, f := range files {
n, err := countLines(f)
if err != nil {
fmt.Fprintf(os.Stderr, "wc: %v\n", err)
continue
}
grand += n
if !*total {
fmt.Printf("%8d %s\n", n, f)
}
}
if *total || len(files) > 1 {
fmt.Printf("%8d total\n", grand)
}
} Frequently Asked Questions
Should I use the flag package or cobra for my CLI?
Use the standard flag package for simple tools with a few flags. Use cobra when you need subcommands (like git commit, git push), nested commands, shell completion, or a more polished user experience.
How do I read from stdin in Go?
Use bufio.NewScanner(os.Stdin) to read line by line. Use io.ReadAll(os.Stdin) to read everything at once. This enables your tool to work in Unix pipelines.
How do I exit a Go program with a specific exit code?
Use os.Exit(code). Exit code 0 means success, non-zero means failure. Note that os.Exit does not run deferred functions, so call it only at the top level of main after cleanup.