Subcommands with flag.FlagSet
Subcommands with flag.FlagSet
Overview
Real tools grow verbs: git status, docker compose up, kubectl get pods. You can implement subcommands with only the standard library via flag.NewFlagSet—no Cobra required.
Dispatch pattern
os.Args[0] program
os.Args[1] subcommand (get|set|list)
os.Args[2:] flags + args for that subcommand
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
var err error
switch os.Args[1] {
case "get":
err = cmdGet(os.Args[2:])
case "set":
err = cmdSet(os.Args[2:])
case "list":
err = cmdList(os.Args[2:])
case "help", "-h", "--help":
usage()
return
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
usage()
os.Exit(2)
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func usage() {
fmt.Fprintf(os.Stderr, `Usage: kv <command> [flags]
Commands:
get get a key
set set a key
list list keys
`)
}Per-command FlagSet
func cmdGet(args []string) error {
fs := flag.NewFlagSet("get", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
raw := fs.Bool("raw", false, "print raw value only")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() != 1 {
return fmt.Errorf("usage: kv get [-raw] <key>")
}
key := fs.Arg(0)
val, ok := store[key]
if !ok {
return fmt.Errorf("key not found: %s", key)
}
if *raw {
fmt.Print(val)
return nil
}
fmt.Printf("%s=%s\n", key, val)
return nil
}
func cmdSet(args []string) error {
fs := flag.NewFlagSet("set", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() != 2 {
return fmt.Errorf("usage: kv set <key> <value>")
}
store[fs.Arg(0)] = fs.Arg(1)
return nil
}Nested subcommands
// tool remote add
// tool remote list
func cmdRemote(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: tool remote <add|list>")
}
switch args[0] {
case "add":
return cmdRemoteAdd(args[1:])
case "list":
return cmdRemoteList(args[1:])
default:
return fmt.Errorf("unknown remote command %q", args[0])
}
}Example: mini todo CLI
// commands: add, list, done
// storage: JSON file in OS user config dir
func cmdAdd(args []string) error {
fs := flag.NewFlagSet("add", flag.ContinueOnError)
pri := fs.Int("pri", 0, "priority")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() < 1 {
return fmt.Errorf("usage: todo add [-pri N] <text>")
}
text := strings.Join(fs.Args(), " ")
return saveNewTask(text, *pri)
}Help that lists commands
var commands = map[string]string{
"get": "get a key",
"set": "set a key",
"list": "list all keys",
}
func usage() {
fmt.Fprintln(os.Stderr, "Usage: kv <command> [flags]")
fmt.Fprintln(os.Stderr, "\nCommands:")
keys := slices.Sorted(maps.Keys(commands))
for _, k := range keys {
fmt.Fprintf(os.Stderr, " %-8s %s\n", k, commands[k])
}
}Alias map
aliases := map[string]string{"ls": "list", "rm": "delete"}
cmd := os.Args[1]
if a, ok := aliases[cmd]; ok {
cmd = a
}When FlagSet is enough
| Enough | Grow to Cobra |
|---|---|
| ≤ ~10 commands, flat or shallow | Deep trees, many shared flags |
| Internal tooling | Public CLI with completion |
| You want zero dependencies | Man pages, docs generators |
Rules of thumb
| Do | Don’t |
|---|---|
One FlagSet per command |
One global flag.Parse for all verbs |
ContinueOnError in libraries/tests |
ExitOnError inside run used by tests |
| Document command list in usage | Silent unknown commands |
Try next
- Implement
kv get|set|listwith an in-memory map (then JSON file). - Add
kv help getthat prints only the get FlagSet defaults. - Add nested
config get/config set.