flag, os/exec, and os/signal

Updated

September 8, 2026

flag, os/exec, and os/signal

Overview

Most Go programs start as tools: parse flags, run work, maybe spawn subprocesses, and shut down cleanly on SIGINT/SIGTERM.

Package Role
flag CLI flags and usage
os/exec Subprocesses
os/signal Graceful shutdown signals

flag

var (
    addr = flag.String("addr", ":8080", "listen address")
    n    = flag.Int("n", 1, "repeat count")
    v    = flag.Bool("v", false, "verbose")
)
flag.Parse()
args := flag.Args() // non-flag args

Custom FlagSet (subcommands)

fs := flag.NewFlagSet("greet", flag.ExitOnError)
name := fs.String("name", "world", "who to greet")
_ = fs.Parse(os.Args[2:])

Good CLI hygiene

  • Provide defaults and help text.
  • Exit non-zero on misuse (flag.ExitOnError or manual os.Exit(2)).
  • Prefer explicit flags over positional soup for optional config.
flag.Usage = func() {
    fmt.Fprintf(os.Stderr, "usage: %s [flags] <path>\n", filepath.Base(os.Args[0]))
    flag.PrintDefaults()
}

os/exec

cmd := exec.CommandContext(ctx, "git", "status", "--short")
cmd.Dir = repo
cmd.Env = append(os.Environ(), "LC_ALL=C")
out, err := cmd.Output() // stdout; err may be *exec.ExitError

Stream pipes

cmd := exec.Command("grep", "TODO")
cmd.Stdin = strings.NewReader(src)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()

Rules

  1. Prefer CommandContext so cancel kills the process tree (platform nuances apply).
  2. Never build shell command strings with untrusted input — pass argv slices.
  3. Check exit codes via errors.As(err, &exitErr).
if err := cmd.Run(); err != nil {
    var ee *exec.ExitError
    if errors.As(err, &ee) {
        return fmt.Errorf("exit %d: %w", ee.ExitCode(), err)
    }
    return err
}

os/signal and graceful shutdown

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

go func() {
    <-ctx.Done()
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    _ = srv.Shutdown(shutdownCtx)
}()

signal.NotifyContext (Go 1.16+) is the clean default for servers and long CLIs.

Runnable example

go mod init example
go run . -n 2 hello
package main

import (
    "bytes"
    "context"
    "flag"
    "fmt"
    "os"
    "os/exec"
    "time"
)

func main() {
    n := flag.Int("n", 1, "repeat")
    flag.Parse()
    msg := "world"
    if flag.NArg() > 0 {
        msg = flag.Arg(0)
    }
    for i := 0; i < *n; i++ {
        fmt.Println("hello", msg)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    cmd := exec.CommandContext(ctx, "echo", "from-subprocess")
    var buf bytes.Buffer
    cmd.Stdout = &buf
    if err := cmd.Run(); err != nil {
        fmt.Fprintln(os.Stderr, "exec:", err)
        os.Exit(1)
    }
    fmt.Print("exec out:", buf.String())
}

Expected output:

hello hello
hello hello
exec out:from-subprocess

(With -n 2 hello you get two greeting lines.)

What to notice: - flag.Arg / Args are only valid after Parse. - CommandContext ties subprocess lifetime to a deadline. - Real services combine NotifyContext with http.Server.Shutdown.

Try next: Add a timeout duration flag and pass it into context.WithTimeout around a long exec.Command.