CLI Cookbook: Stdlib and Cobra Examples

Updated

September 8, 2026

CLI Cookbook: Stdlib and Cobra Examples

Overview

Runnable sketches you can paste into a module. §A–§F are stdlib-only. §G–§H use Cobra. Trim imports and split packages as needed.

mkdir -p /tmp/cookbook && cd /tmp/cookbook
go mod init example.com/cookbook

A. hello — flags + exit codes (stdlib)

package main

import (
    "errors"
    "flag"
    "fmt"
    "io"
    "os"
)

var errUsage = errors.New("usage")

func main() {
    os.Exit(realMain(os.Args[1:], os.Stdout, os.Stderr))
}

func realMain(args []string, stdout, stderr io.Writer) int {
    if err := run(args, stdout, stderr); err != nil {
        fmt.Fprintln(stderr, err)
        if errors.Is(err, errUsage) {
            return 2
        }
        return 1
    }
    return 0
}

func run(args []string, stdout, stderr io.Writer) error {
    fs := flag.NewFlagSet("hello", flag.ContinueOnError)
    fs.SetOutput(stderr)
    n := fs.Int("n", 1, "times")
    name := fs.String("name", "world", "name")
    if err := fs.Parse(args); err != nil {
        return errUsage
    }
    for i := 0; i < *n; i++ {
        fmt.Fprintf(stdout, "hello, %s\n", *name)
    }
    return nil
}
go run . -name Ada -n 2

B. catn — files or stdin (stdlib)

package main

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

func main() {
    flag.Parse()
    args := flag.Args()
    if len(args) == 0 {
        if _, err := io.Copy(os.Stdout, os.Stdin); err != nil {
            fail(err)
        }
        return
    }
    for _, p := range args {
        if p == "-" {
            if _, err := io.Copy(os.Stdout, os.Stdin); err != nil {
                fail(err)
            }
            continue
        }
        f, err := os.Open(p)
        if err != nil {
            fail(err)
        }
        _, err = io.Copy(os.Stdout, f)
        f.Close()
        if err != nil {
            fail(err)
        }
    }
}

func fail(err error) {
    fmt.Fprintln(os.Stderr, "catn:", err)
    os.Exit(1)
}
echo hi | go run .
go run . /etc/hosts

C. ffind — walk files by suffix (stdlib)

package main

import (
    "flag"
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
    "strings"
)

func main() {
    root := flag.String("root", ".", "start directory")
    suffix := flag.String("ext", ".go", "file suffix")
    flag.Parse()

    err := filepath.WalkDir(*root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if d.IsDir() {
            if d.Name() == ".git" || d.Name() == "node_modules" {
                return fs.SkipDir
            }
            return nil
        }
        if strings.HasSuffix(path, *suffix) {
            fmt.Println(path)
        }
        return nil
    })
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

D. jqq — extract JSON field from NDJSON stdin (stdlib)

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "os"
)

func main() {
    field := flag.String("f", "", "field name")
    flag.Parse()
    if *field == "" {
        fmt.Fprintln(os.Stderr, "usage: jqq -f field < ndjson")
        os.Exit(2)
    }
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var m map[string]any
        if err := dec.Decode(&m); err != nil {
            break
        }
        if v, ok := m[*field]; ok {
            _ = enc.Encode(v)
        }
    }
}
printf '%s\n' '{"user":"a"}' '{"user":"b"}' | go run . -f user

E. httpc — GET with timeout (stdlib)

package main

import (
    "context"
    "flag"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func main() {
    url := flag.String("url", "", "URL")
    timeout := flag.Duration("timeout", 10*time.Second, "timeout")
    flag.Parse()
    if *url == "" {
        fmt.Fprintln(os.Stderr, "usage: httpc -url URL")
        os.Exit(2)
    }
    ctx, cancel := context.WithTimeout(context.Background(), *timeout)
    defer cancel()
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, *url, nil)
    if err != nil {
        fail(err)
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fail(err)
    }
    defer resp.Body.Close()
    fmt.Fprintln(os.Stderr, resp.Status)
    _, _ = io.Copy(os.Stdout, io.LimitReader(resp.Body, 2<<20))
}

func fail(err error) { fmt.Fprintln(os.Stderr, err); os.Exit(1) }

F. kv — subcommands with FlagSet (stdlib)

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "os"
    "sync"
)

type DB struct {
    mu   sync.Mutex
    Data map[string]string `json:"data"`
    path string
}

func openDB(path string) (*DB, error) {
    db := &DB{Data: map[string]string{}, path: path}
    b, err := os.ReadFile(path)
    if err != nil {
        if os.IsNotExist(err) {
            return db, nil
        }
        return nil, err
    }
    if err := json.Unmarshal(b, db); err != nil {
        return nil, err
    }
    if db.Data == nil {
        db.Data = map[string]string{}
    }
    return db, nil
}

func (db *DB) save() error {
    db.mu.Lock()
    defer db.mu.Unlock()
    b, err := json.MarshalIndent(db, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(db.path, b, 0o600)
}

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintln(os.Stderr, "usage: kv <get|set|list> ...")
        os.Exit(2)
    }
    path := os.Getenv("KV_DB")
    if path == "" {
        path = "kv.json"
    }
    db, err := openDB(path)
    if err != nil {
        fail(err)
    }
    switch os.Args[1] {
    case "get":
        err = cmdGet(db, os.Args[2:])
    case "set":
        err = cmdSet(db, os.Args[2:])
    case "list":
        err = cmdList(db, os.Args[2:])
    default:
        fmt.Fprintln(os.Stderr, "unknown command")
        os.Exit(2)
    }
    if err != nil {
        fail(err)
    }
}

func cmdGet(db *DB, args []string) error {
    fs := flag.NewFlagSet("get", flag.ContinueOnError)
    if err := fs.Parse(args); err != nil {
        return err
    }
    if fs.NArg() != 1 {
        return fmt.Errorf("usage: kv get KEY")
    }
    db.mu.Lock()
    v, ok := db.Data[fs.Arg(0)]
    db.mu.Unlock()
    if !ok {
        return fmt.Errorf("not found")
    }
    fmt.Println(v)
    return nil
}

func cmdSet(db *DB, args []string) error {
    fs := flag.NewFlagSet("set", flag.ContinueOnError)
    if err := fs.Parse(args); err != nil {
        return err
    }
    if fs.NArg() != 2 {
        return fmt.Errorf("usage: kv set KEY VALUE")
    }
    db.mu.Lock()
    db.Data[fs.Arg(0)] = fs.Arg(1)
    db.mu.Unlock()
    return db.save()
}

func cmdList(db *DB, args []string) error {
    db.mu.Lock()
    defer db.mu.Unlock()
    for k, v := range db.Data {
        fmt.Printf("%s\t%s\n", k, v)
    }
    return nil
}

func fail(err error) { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
go run . set hello world
go run . get hello
go run . list

G. Cobra todo — add / list (library)

go get github.com/spf13/cobra@latest
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"

    "github.com/spf13/cobra"
)

type Task struct {
    Text string    `json:"text"`
    At   time.Time `json:"at"`
}

func load(path string) ([]Task, error) {
    b, err := os.ReadFile(path)
    if err != nil {
        if os.IsNotExist(err) {
            return nil, nil
        }
        return nil, err
    }
    var t []Task
    return t, json.Unmarshal(b, &t)
}

func save(path string, tasks []Task) error {
    b, err := json.MarshalIndent(tasks, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(path, b, 0o600)
}

func main() {
    path := "tasks.json"
    root := &cobra.Command{Use: "todo", Short: "Tiny todo list"}

    add := &cobra.Command{
        Use:  "add TEXT...",
        Args: cobra.MinimumNArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            tasks, err := load(path)
            if err != nil {
                return err
            }
            text := args[0]
            for _, a := range args[1:] {
                text += " " + a
            }
            tasks = append(tasks, Task{Text: text, At: time.Now().UTC()})
            return save(path, tasks)
        },
    }

    list := &cobra.Command{
        Use: "list",
        RunE: func(cmd *cobra.Command, args []string) error {
            tasks, err := load(path)
            if err != nil {
                return err
            }
            for i, t := range tasks {
                fmt.Printf("%d.\t%s\t(%s)\n", i+1, t.Text, t.At.Format(time.RFC3339))
            }
            return nil
        },
    }

    root.AddCommand(add, list)
    if err := root.Execute(); err != nil {
        os.Exit(1)
    }
}
go run . add write docs
go run . list

H. Cobra parent + persistent verbose

package main

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

func main() {
    var verbose bool
    root := &cobra.Command{Use: "tool"}
    root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose")

    ping := &cobra.Command{
        Use: "ping",
        RunE: func(cmd *cobra.Command, args []string) error {
            if verbose {
                fmt.Fprintln(os.Stderr, "pinging...")
            }
            fmt.Println("pong")
            return nil
        },
    }
    root.AddCommand(ping)
    _ = root.Execute()
}
go run . -v ping

I. Ideas to implement yourself

Tool Skills
sha1dir walk + crypto/sha256 + parallel with errgroup
portwait dial loop + timeout + signals
envshow filter os.Environ with regex
jwtlook base64 decode payload (no verify) for debugging
modgraph go mod graph via exec + format
tailf poll file size + print deltas
csv2json encoding/csv + json encoder
watchrun poll mtime + exec command

J. Full project: network diagnostics CLI

For a multi-command ping / dns / scan / probe toolkit with layout, stats, workers, and tests, follow the dedicated project chapter:

317 Project: Network CLI Tools

K. Full project: network security mini-tools

For minimal security helpers (TLS cert, headers, banner, redirects, CIDR, cookies, hash, secret scan, rand, listen):

318 Project: Simple Network Security Mini-Tools

Use stdlib until the command tree aches—then port the same domain to Cobra.


Cookbook checklist

  • stdout data / stderr logs
  • non-zero exit on failure
  • ContinueOnError FlagSet or Cobra RunE
  • context on network/exec
  • at least one table test on run / realMain

When those hold, your CLI is ready to grow.