Skip to main content
eLearner.app
Module 10 · Lesson 4 of 549/50 in the course~18 min
Module lessons (4/5)

Mini-project: a CLI with flags

Let's put the building blocks of the previous modules together to build a small idiomatic CLI: flag parsing with the flag package, handling of positional arguments, errors on os.Stderr, and the right exit code.

The flag package

Go
package main

import (
    "flag"
    "fmt"
    "os"
)

func main() {
    n := flag.Int("n", 1, "number of repetitions")
    upper := flag.Bool("u", false, "print in uppercase")
    flag.Parse()

    args := flag.Args() // remaining positional arguments
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "usage: echo [-n N] [-u] <text>")
        os.Exit(2)
    }

    text := args[0]
    if *upper {
        text = strings.ToUpper(text)
    }
    for i := 0; i < *n; i++ {
        fmt.Println(text)
    }
}

Key points:

  • flag.Int, flag.String, flag.Bool, flag.Duration (etc.) return a pointer: you use *n, *upper.
  • flag.Parse() must be called ONCE, before reading the values and before flag.Args().
  • flag.Args() returns the positional arguments (those after -- or after the last flag).
  • Accepted forms: -n 3, -n=3, --n=3. Booleans: -u (true), -u=false.

Conventional exit codes

Go
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)

Shells and scripts expect these codes: use them consistently. Never exit from a non-main goroutine with os.Exit: it skips every defer, including log flushing.

Stderr vs Stdout

Old and sacred Unix rule:

  • os.Stdout → useful output, pipe-able (cli | grep ...).
  • os.Stderr → diagnostics, errors, progress bars, usage.
Go
fmt.Println("result: 42")                   // stdout
fmt.Fprintln(os.Stderr, "error: ...")       // stderr

This way cli 2>/dev/null shows only the results, cli >out.txt redirects only the result and errors stay visible.

log vs fmt

  • fmt for user output.
  • log for diagnostics: adds a timestamp, writes to os.Stderr by default, has log.Fatal (logs + os.Exit(1)) and log.Panic.
Go
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1

For modern apps (Go 1.21+) use log/slog for structured logging (JSON or text, levels, typed fields).

Architecture: separate main from logic

Go
func main() {
    if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(args []string, stdout, stderr io.Writer) error {
    // here use a new flag.FlagSet instead of the global flag.CommandLine
    fs := flag.NewFlagSet("echo", flag.ContinueOnError)
    fs.SetOutput(stderr)
    n := fs.Int("n", 1, "repetitions")
    if err := fs.Parse(args); err != nil {
        return err
    }
    // ...
    return nil
}

Advantages:

  • Testable: you pass bytes.Buffer as stdout/stderr and verify the output.
  • No global state: each FlagSet is isolated.
  • main stays tiny: it prepares IO + delegates.

Subcommands

For CLIs with subcommands (mycli serve, mycli migrate), a scheme with no external dependencies:

Go
func main() {
    if len(os.Args) < 2 {
        usage()
        os.Exit(2)
    }
    switch os.Args[1] {
    case "serve":
        serveCmd(os.Args[2:])
    case "migrate":
        migrateCmd(os.Args[2:])
    default:
        usage()
        os.Exit(2)
    }
}

Each subcommand has its own flag.NewFlagSet. For larger projects, libraries like cobra or urfave/cli structure this scheme with auto-generated help.

Exercises

Exercise#go.m10.l4.e1
Attempts: 0Loading…

Definisci il flag -n di tipo int con valore di default 1 e descrizione 'ripetizioni', poi chiama flag.Parse() e stampa il valore.

Loading editor…

Solution available after 3 attempts

Exercise#go.m10.l4.e2
Attempts: 0Loading…

Se non c'è almeno un argomento posizionale dopo flag.Parse, stampa 'manca argomento' su Stderr e termina con os.Exit(1).

Loading editor…

Solution available after 3 attempts

Quiz#go.m10.l4.e3
Ready

Su quale stream stampi messaggi di errore e usage in un CLI Unix-friendly?

Go
fmt.Fprintln(???, "uso: prog ...")
Answer options