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

Why this over cobra or urfave? →


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.