Skip to content

Expert · CLI tools and Go in cloud and DevOps · topic 39 of 46 · 35 min

Command-line tools with flag and os

Explanation

A command-line tool (CLI) reads arguments and flags, does one job, prints results to standard output, errors to standard error, and exits with code 0 for success or non-zero for failure. Go's flag package parses flags like -n 5 or --verbose, os.Args holds the raw arguments, and big CLIs such as kubectl use the cobra library for subcommands.

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
// 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)
Fine for tiny tools; for anything with options, use flag.

The flag package

go
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)
}
flag.Int returns a *int, so you read it with *n. flag.Parse must run before you read any flag.
bash
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 name
Flags must come before the positional arguments with the standard flag package.
Tip: Boolean flags are set with -v or -v=true. Writing -v false does not work: "false" becomes a positional argument.

The 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.

go
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
}
A FlagSet with ContinueOnError returns errors instead of exiting, which makes run testable.

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.

go
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)
}
Check len(os.Args) >= 2 first, or os.Args[1] panics.
Careful: Read secrets (API tokens) from environment variables or a prompt, not from flags. Flags appear in shell history and in process lists that other users on the machine can see.

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

CLI tools in Go

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.

bash
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: 1
Using the run() function above. 25050 × 12 / 100 = 3006 (integer division), so 25050 + 3006 = 28056.

Practice (0/4 done)

Task 1 · Question

What is os.Args[0]?

Task 2 · Question

n := flag.Int("n", 3, "count") followed by flag.Parse(). The user runs tool -n 7. What is *n?

Task 3 · Question

Why should a CLI print errors to os.Stderr instead of os.Stdout?

Task 4 · Practice

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

Question 1

1.What exit code means success?

Question 2

2.Why keep os.Exit only in main?

Question 3

3.Which popular library do kubectl and Hugo use for commands?

Question 4

4.Where should a CLI take an API token from?

Read more (free)

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

Answer: clap

clap generates parsing and help from a struct.

From Building a CLI tool

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.

From The command line on all three clouds

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

  1. 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.
  2. 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.
  3. Then the next module, Careers and interviews. 46 topics left in the course before the final exam.

← Previous: Configuration, project structure and graceful shutdown