pressly/cli
A small Go library for building CLIs. Extends the standard library's flag package
with nested subcommands, flag inheritance, and flags-anywhere parsing.
Intentionally minimal.
Go 1.27+ · standard library only · MIT
go get github.com/pressly/cli@latest
Quick start
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
"github.com/pressly/cli"
)
func main() {
root := &cli.Command{
Name: "echo",
Usage: "echo [flags] <text>...",
Summary: "Print text",
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
f.Bool("capitalize", false, "capitalize the input")
}),
FlagConfigs: []cli.FlagConfig{
{Name: "capitalize", Short: "c"},
},
Exec: func(ctx context.Context, state *cli.State) error {
text := strings.Join(state.Args, " ")
if state.GetFlag[bool]("capitalize") {
text = strings.ToUpper(text)
}
fmt.Fprintln(state.Stdout, text)
return nil
},
}
if err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
$ echo hello world
hello world
$ echo -c hello world
HELLO WORLD
$ echo --help
Print text
Usage:
echo [flags] <text>...
Flags:
-c, --capitalize capitalize the input
At a glance
The entire API fits on one screen. Each name links to godoc.
FUNCTIONS
func FlagsFunc(fn func(f *flag.FlagSet)) *flag.FlagSet
func Parse(root *Command, args []string) error
func ParseAndRun(ctx context.Context, root *Command, args []string, options *RunOptions) error
func Run(ctx context.Context, root *Command, options *RunOptions) error
func UsageErrorf(format string, args ...any) error
TYPES
type Command
func (c *Command) Path() []*Command
type FlagConfig
type FlagName[T any] string
type RunOptions
type State
func (s *State) GetFlag[T any](name FlagName[T]) T
Examples
Required flags
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
f.String("output", "", "output file")
}),
FlagConfigs: []cli.FlagConfig{
// The command will not run without --output.
{Name: "output", Short: "o", Required: true},
},
$ build
error: command "build": required flag "-output" not set
$ build --help
Usage:
build [flags]
Flags:
-o, --output string output file (required)
Nested subcommands with inherited flags
root := &cli.Command{
Name: "todo",
Usage: "todo <command> [flags]",
// Root flags are inherited.
Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
f.Bool("verbose", false, "enable verbose output")
}),
SubCommands: []*cli.Command{
{
Name: "list",
Summary: "List all tasks",
Exec: func(ctx context.Context, state *cli.State) error {
if state.GetFlag[bool]("verbose") {
fmt.Fprintln(state.Stderr, "listing tasks...")
}
return nil
},
},
{
Name: "add",
Usage: "todo add <text>",
Summary: "Add a task",
Exec: func(ctx context.Context, state *cli.State) error {
// state.Args is everything left after flag parsing.
fmt.Fprintf(state.Stdout, "added: %s\n",
strings.Join(state.Args, " "))
return nil
},
},
},
}
$ todo --help
Usage:
todo <command> [flags]
Available Commands:
add Add a task
list List all tasks
Flags:
--verbose enable verbose output
Use "todo [command] --help" for more information about a command.
$ todo list --help
List all tasks
Usage:
todo list [flags]
Inherited Flags:
--verbose enable verbose output
Subpackages
Small helpers that can also be used on their own.
flagtype
Common flag types for slices, enums, maps, URLs, and regular expressions.
f.Var(flagtype.Enum("json", "yaml", "table"), "format", "output format")
format := state.GetFlag[string]("format")
graceful
Signal-aware shutdown for servers, workers, and batch jobs.
graceful.Run(
// Drain requests for up to 15 seconds.
graceful.ListenAndServe(server, 15*time.Second),
// Give the whole shutdown 30 seconds.
graceful.WithTerminationTimeout(30*time.Second),
)
xflag
Flags-anywhere parsing for the standard library. Used by cli internally, but also works on its own.
err := xflag.ParseToEnd(f, os.Args[1:])
But why?
Compared to cobra, urfave, and kong
cli stays close to the standard library: command tree, flag parsing, and subcommands. Bring your own color, configuration, prompts, and everything else.
cobra, urfave/cli, and kong include more features out of the box. Choose one if that is what you want.
Built on the standard library
f.Bool("verbose", false, "enable verbose output")
f.String("output", "", "output file")
f.Duration("timeout", 5*time.Second, "request timeout")
There is no second flag system to learn.
Custom flag types follow the same pattern
flagtype includes slices, enums, maps, URLs, and regular expressions.
// Custom types use the same FlagSet.
f.Var(flagtype.StringSlice(), "tag", "add a tag (repeatable)")
f.Var(flagtype.URL(), "endpoint", "API endpoint")
If a type belongs in flagtype, open an issue.
Flags can appear anywhere
The standard library stops at the first positional argument. cli keeps parsing, and xflag provides the same behavior on its own.
# All three are equivalent.
todo --verbose add buy milk
todo add --verbose buy milk
todo add buy milk --verbose
Parent flags inherit by default
FlagConfigs: []cli.FlagConfig{
// Keep --force on this command instead of inheriting it.
{Name: "force", Local: true},
},
Type-safe flag access
Go 1.27 lets the flag type live directly on the method call:
verbose := state.GetFlag[bool]("verbose")
output := state.GetFlag[string]("output")
tags := state.GetFlag[[]string]("tag")
A missing flag or wrong type is a programming error. cli reports it cleanly.
// Keep the name and type together when a flag is shared.
const verbose cli.FlagName[bool] = "verbose"
f.Bool(string(verbose), false, "enable verbose output")
enabled := state.GetFlag(verbose) // bool is inferred
// A plain string needs a type when passed to GetFlag.
name := "verbose"
enabled = state.GetFlag(cli.FlagName[bool](name))
No dependencies
The library only imports the standard library. The requirements in go.mod are for tests.
Color, prompts, configuration, and logging stay in your application. Pick the packages you like.
Small packages stay separate
Common helpers live beside cli instead of growing its core API. Each works on its own; xflag is also used internally.
A package belongs here if it only needs the standard library and solves a common CLI problem.
Inspired by ff/v3
Inspired by Peter Bourgon's ff library, especially its v3 branch, which was close to what I wanted. v4 took a different direction, but I wanted to keep the simplicity of v3. This library carries that idea forward.