Expert · CLI tools and Go in cloud and DevOps · topic 39 of 46 · 35 min
Command-line tools with flag and os
Explanation
Why Go is great for CLIs
A CLI is a program you run in a terminal: git, docker, kubectl, terraform, gh. Go is a favourite for CLIs because the result is one binary with no runtime to install, it starts instantly, and you can build it for every operating system from one machine. Teams write internal CLIs to deploy services, rotate keys, or generate reports.
Standard streams and exit codes
- Standard output (os.Stdout): the real result, which other programs may read through a pipe (|).
- Standard error (os.Stderr): messages, warnings and errors for the human. Keeping them separate means piping the output does not mix in error text.
- Exit code: 0 means success; anything else means failure. Scripts and CI pipelines check it. By convention 1 is a general error and 2 is wrong usage (bad flags).
os.Args: the raw arguments
// go run . hello 42
fmt.Println(os.Args) // [/tmp/go-build.../exe/app hello 42]
fmt.Println(os.Args[1:]) // [hello 42] (os.Args[0] is the program itself)The flag package
package main
import (
"flag"
"fmt"
"os"
)
func main() {
n := flag.Int("n", 10, "number of lines to show")
verbose := flag.Bool("v", false, "verbose output")
var city string
flag.StringVar(&city, "city", "Pune", "city name")
flag.Parse()
files := flag.Args() // what is left after the flags
if len(files) == 0 {
fmt.Fprintln(os.Stderr, "usage: report [-n N] [-v] [-city NAME] FILE...")
os.Exit(2)
}
fmt.Println(*n, *verbose, city, files)
}report -n 5 -city Mumbai sales.csv
report --n=5 -v sales.csv # -flag and --flag both work
report -h # prints the generated help text
report sales.csv -n 5 # careful: parsing stops at sales.csv, so -n 5 is treated as a file nameThe run() pattern
os.Exit stops the program immediately and skips deferred calls, so files might not be flushed. A common pattern keeps main tiny: all logic goes into run, which returns an error, and only main calls os.Exit. run is also easy to test, because you can pass it arguments and a buffer instead of the real terminal.
func main() {
if err := run(os.Args[1:], os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func run(args []string, out io.Writer) error {
fs := flag.NewFlagSet("gst", flag.ContinueOnError)
rate := fs.Int("rate", 18, "GST rate in percent")
if err := fs.Parse(args); err != nil {
return err
}
for _, a := range fs.Args() {
paise, err := strconv.ParseInt(a, 10, 64)
if err != nil {
return fmt.Errorf("bad amount %q: %w", a, err)
}
total := paise + paise*int64(*rate)/100
fmt.Fprintf(out, "%d -> %d\n", paise, total)
}
return nil
}Subcommands
Tools like git and kubectl have subcommands: git commit, kubectl get pods. With the standard library you switch on os.Args[1] and give each subcommand its own flag.NewFlagSet. For large tools, the cobra library (used by kubectl, Hugo and GitHub's gh CLI) adds nested commands, help text, and shell completion.
switch os.Args[1] {
case "add":
fs := flag.NewFlagSet("add", flag.ExitOnError)
amount := fs.Int64("paise", 0, "amount in paise")
fs.Parse(os.Args[2:])
fmt.Println("adding", *amount)
case "report":
// ...
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
os.Exit(2)
}Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Command-line tools with flag and os · 1/7
Go is a favourite language for command-line tools like kubectl, docker and terraform.
Prefer a video by a teacher? Find free videos on “golang command line tool flag package os.Args tutorial” on YouTube ↗
In real life: A GST calculator CLI for an accounts team
An accounts team pastes invoice amounts into a terminal tool every day. The Go CLI takes amounts in paise as arguments and a -rate flag, prints one line per amount to stdout so it can be piped into a file, writes errors to stderr, and exits with code 1 on a bad amount so their script stops.
go build -o gst .
./gst -rate 12 10000 25050
# 10000 -> 11200
# 25050 -> 28056
./gst 12abc ; echo "exit code: $?"
# error: bad amount "12abc": strconv.ParseInt: parsing "12abc": invalid syntax
# exit code: 1Practice (0/4 done)
What is os.Args[0]?
n := flag.Int("n", 3, "count") followed by flag.Parse(). The user runs tool -n 7. What is *n?
Why should a CLI print errors to os.Stderr instead of os.Stdout?
Build a CLI called wordcount that reads text from a file given as an argument (or from standard input if no file is given), and prints the number of lines, words and characters. Add a -top N flag that also prints the N most common words. Exit with code 1 and a message on stderr if the file cannot be opened.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Cross-compiling and build flagsCLI tools and Go in cloud and DevOps · same module
- Docker images for Go servicesCLI tools and Go in cloud and DevOps · same module
- Go in cloud and DevOps: Kubernetes, Terraform and PrometheusCLI tools and Go in cloud and DevOps · same module
- Building a CLI toolRust Programming · Real projects: CLI, web and WebAssembly
- Bash scripts: variables, quoting, arguments and exit codesLinux and Red Hat (RHCSA to RHCE) · Automation basics: scheduling, archives and shell scripts
- Node.js basicsJavaScript Programming (language in depth) · Performance and Node.js
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What exit code means success?
Answer: 0
Zero is success; non-zero is failure.
Why keep os.Exit only in main?
Answer: os.Exit skips deferred calls, and code that returns errors is easier to test
The run() pattern keeps clean-up and testing simple.
Which popular library do kubectl and Hugo use for commands?
Answer: cobra
cobra provides subcommands, help and completions.
Where should a CLI take an API token from?
Answer: An environment variable or an interactive prompt
Flags can leak through shell history and process lists.
Which crate is the common choice for parsing command-line arguments?Related
What exit code means success?Related
Which file should be committed to Git?Related
Answer: package-lock.json
The lock file guarantees the same versions everywhere; node_modules and secrets are not committed.
From Node.js basics
Which AWS CLI option filters the output on your machine using JMESPath?Related
Answer: --query
--query takes a JMESPath expression and filters the JSON before it is printed.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “CLI tools and Go in cloud and DevOps” before its exam: Cross-compiling and build flags, Docker images for Go services, Go in cloud and DevOps: Kubernetes, Terraform and Prometheus.
- Module 9 exam: CLI tools and Go in cloud and DevOps · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
- Then the next module, Careers and interviews. 46 topics left in the course before the final exam.
← Previous: Configuration, project structure and graceful shutdown