···2525 - [Sub Commands](#sub-commands)
2626 - [Flags](#flags)
2727 - [Arguments](#arguments)
2828- - [Shell Completion](#shell-completion)
2928 - [Core Principles](#core-principles)
3029 - [😱 Well behaved libraries don't panic](#-well-behaved-libraries-dont-panic)
3130 - [🧘🏻 Keep it Simple](#-keep-it-simple)
···331330> Slice types are not supported (yet), for those you need to use the `cmd.Args()` method to get the arguments manually. I plan to address this but it can be tricky
332331> as slice types will eat up the remainder of the arguments so I need to figure out a good DevEx for this as it could lead to confusing outcomes
333332334334-### Shell Completion
335335-336336-`cli` has built-in support for shell completions via [carapace-bin]. Wire in `cli.CompletionSubCommand()` alongside your other subcommands:
337337-338338-```go
339339-cmd, err := cli.New(
340340- "mytool",
341341- // ...
342342- cli.SubCommands(
343343- buildServeCommand,
344344- buildDeployCommand,
345345- cli.CompletionSubCommand(), // add this
346346- ),
347347-)
348348-```
349349-350350-Running `mytool completion` outputs a [carapace-spec] YAML document describing your full command tree — all subcommands, flags, and descriptions. Redirect it once to register completions with [carapace-bin]:
351351-352352-```shell
353353-mytool completion > ~/.config/carapace/specs/mytool.yaml
354354-```
355355-356356-carapace-bin then provides completions across bash, zsh, fish, nushell, PowerShell, and more — no shell-specific scripts required.
357357-358358-> [!TIP]
359359-> See the [`./examples/completion`](https://github.com/FollowTheProcess/cli/tree/main/examples/completion) example for a working demonstration, and the [carapace-spec] docs for how to extend the generated YAML with semantic completion hints (file paths, environment variables, etc.)
360360-361333## Core Principles
362334363335When designing and implementing `cli`, I had some core goals and guiding principles for implementation.
···455427cli.New("demo", cli.Flag(&delete, "delete", cli.NoShortHand, "Delete something"))
456428```
457429430430+## Gaps
431431+432432+`cli` is new and under active development, so naturally there are some gaps compared to more mature frameworks:
433433+434434+- No shell completion (yet): I'm thinking about some smart ways to achieve this and haven't made up my mind yet
435435+- Man page generation: Similar to shell completion
436436+- Text wrapping and terminal width detection for `--help` (currently this is on you to format when writing)
437437+458438## In the Wild
459439460440I built `cli` for my own uses really, so I've quickly adopted it across a number of tools. See the following projects for some working examples in real code:
···468448[spf13/pflag]: https://github.com/spf13/pflag
469449[urfave/cli]: https://github.com/urfave/cli
470450[functional options]: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
471471-[carapace-bin]: https://github.com/carapace-sh/carapace-bin
472472-[carapace-spec]: https://github.com/carapace-sh/carapace-spec
-182
completion.go
···11-package cli
22-33-import (
44- "context"
55- "fmt"
66- "slices"
77- "strings"
88-99- publicflag "go.followtheprocess.codes/cli/flag"
1010- "go.yaml.in/yaml/v4"
1111-)
1212-1313-const completionLong = `
1414-Outputs a carapace-spec YAML document describing this command's flags and subcommands.
1515-1616-Redirect the output to register completions with carapace-bin:
1717-1818- mytool completion > ~/.config/carapace/specs/mytool.yaml
1919-`
2020-2121-// CompletionSubCommand returns a [Builder] that constructs a "completion" subcommand.
2222-//
2323-// When run, it outputs a carapace-spec YAML document to stdout describing the full
2424-// command tree, all flags, and all subcommands.
2525-//
2626-// Wire it into your root command:
2727-//
2828-// cli.New("mytool",
2929-// cli.SubCommands(cli.CompletionSubCommand()),
3030-// ...
3131-// )
3232-//
3333-// Users register completions by running:
3434-//
3535-// mytool completion > ~/.config/carapace/specs/mytool.yaml
3636-//
3737-// carapace-bin then provides completions across bash, zsh, fish, nushell,
3838-// powershell, and more. See https://github.com/carapace-sh/carapace-spec
3939-// for how to extend the generated YAML with semantic completion hints.
4040-func CompletionSubCommand() Builder {
4141- return func() (*Command, error) {
4242- return New(
4343- "completion",
4444- Short("Output a carapace-spec YAML document to stdout"),
4545- Long(completionLong),
4646- Run(func(_ context.Context, cmd *Command) error {
4747- data, err := marshalCompletionSpec(cmd.root())
4848- if err != nil {
4949- return fmt.Errorf("generating carapace spec: %w", err)
5050- }
5151-5252- _, err = cmd.Stdout().Write(data)
5353-5454- return err
5555- }),
5656- )
5757- }
5858-}
5959-6060-// specCommand is the internal representation of a carapace-spec YAML command node.
6161-//
6262-// See https://github.com/carapace-sh/carapace-spec for the full schema.
6363-type specCommand struct {
6464- Name string `yaml:"name"`
6565- Description string `yaml:"description,omitempty"`
6666- Flags map[string]string `yaml:"flags,omitempty"`
6767- PersistentFlags map[string]string `yaml:"persistentflags,omitempty"`
6868- Commands []specCommand `yaml:"commands,omitempty"`
6969-}
7070-7171-// marshalCompletionSpec generates a carapace-spec YAML document for cmd and its
7272-// full subcommand tree.
7373-func marshalCompletionSpec(cmd *Command) ([]byte, error) {
7474- spec := buildSpecTree(cmd)
7575-7676- // Help and version are on every command; declare them once as persistentflags.
7777- spec.PersistentFlags = persistentFlagsFrom(cmd)
7878-7979- data, err := yaml.Marshal(spec)
8080- if err != nil {
8181- return nil, fmt.Errorf("marshalling carapace spec: %w", err)
8282- }
8383-8484- return data, nil
8585-}
8686-8787-// isSystemFlag reports whether name is an automatically-injected flag.
8888-// These flags appear on every command and are emitted as persistentflags
8989-// on the root rather than repeated in every command's flags map.
9090-func isSystemFlag(name string) bool {
9191- return name == "help" || name == "version"
9292-}
9393-9494-// buildSpecTree recursively builds a specCommand tree from cmd.
9595-func buildSpecTree(cmd *Command) specCommand {
9696- spec := specCommand{
9797- Name: cmd.name,
9898- Description: cmd.short,
9999- Flags: specFlagsFrom(cmd),
100100- }
101101-102102- if len(cmd.subcommands) > 0 {
103103- sorted := make([]*Command, len(cmd.subcommands))
104104- copy(sorted, cmd.subcommands)
105105- slices.SortFunc(sorted, func(a, b *Command) int {
106106- return strings.Compare(a.name, b.name)
107107- })
108108-109109- spec.Commands = make([]specCommand, len(sorted))
110110- for i, sub := range sorted {
111111- spec.Commands[i] = buildSpecTree(sub)
112112- }
113113- }
114114-115115- return spec
116116-}
117117-118118-// specFlagsFrom builds the carapace-spec flags map for cmd, excluding system
119119-// flags (help, version) which are emitted as persistentflags on the root.
120120-func specFlagsFrom(cmd *Command) map[string]string {
121121- flags := make(map[string]string)
122122-123123- for name, fl := range cmd.flagSet().All() {
124124- if isSystemFlag(name) {
125125- continue
126126- }
127127-128128- key := specFlagKey(name, fl.Short(), fl.Type(), fl.NoArgValue())
129129- flags[key] = fl.Usage()
130130- }
131131-132132- if len(flags) == 0 {
133133- return nil
134134- }
135135-136136- return flags
137137-}
138138-139139-// persistentFlagsFrom builds the carapace-spec persistentflags map from the
140140-// system flags (help, version) that are automatically added to every command.
141141-func persistentFlagsFrom(cmd *Command) map[string]string {
142142- flags := make(map[string]string)
143143-144144- for name, fl := range cmd.flagSet().All() {
145145- if !isSystemFlag(name) {
146146- continue
147147- }
148148-149149- key := specFlagKey(name, fl.Short(), fl.Type(), fl.NoArgValue())
150150- flags[key] = fl.Usage()
151151- }
152152-153153- if len(flags) == 0 {
154154- return nil
155155- }
156156-157157- return flags
158158-}
159159-160160-// specFlagKey encodes a flag name, shorthand, and type into a carapace-spec
161161-// flag map key.
162162-func specFlagKey(name string, short rune, typ, noArgValue string) string {
163163- var base string
164164- if short != publicflag.NoShortHand {
165165- base = fmt.Sprintf("-%s, --%s", string(short), name)
166166- } else {
167167- base = "--" + name
168168- }
169169-170170- switch {
171171- case noArgValue == "":
172172- // Value is required
173173- return base + "="
174174- case typ == "bool":
175175- return base
176176- case typ == "count":
177177- return base + "*"
178178- default:
179179- // Optional value
180180- return base + "?"
181181- }
182182-}
···88 - [`./subcommands`](#subcommands)
99 - [`./namedargs`](#namedargs)
1010 - [`./cancel`](#cancel)
1111- - [`./completion`](#completion)
1211 - [TODO](#todo)
13121413## `./cover`
···39384039
41404242-## `./completion`
4343-4444-Demonstrates how to add shell completion support to any CLI by wiring in `cli.CompletionSubCommand()`. Running `mytool completion` outputs a [carapace-spec] YAML document describing the full command tree. Redirect it once to register completions with [carapace-bin]:
4545-4646-```shell
4747-mytool completion > ~/.config/carapace/specs/mytool.yaml
4848-```
4949-5050-carapace-bin then provides completions across bash, zsh, fish, nushell, PowerShell, and more — no shell-specific scripts required.
5151-5241### TODO
53425443- Replicate one or two well known CLI tools as an example
···57465847[quickstart]: <https://github.com/FollowTheProcess/cli#quickstart>
5948[freeze]: <https://github.com/charmbracelet/freeze>
6060-[carapace-spec]: <https://github.com/carapace-sh/carapace-spec>
6161-[carapace-bin]: <https://github.com/carapace-sh/carapace-bin>
-92
examples/completion/cli.go
···11-package main
22-33-import (
44- "context"
55- "fmt"
66- "strings"
77-88- "go.followtheprocess.codes/cli"
99- "go.followtheprocess.codes/cli/flag"
1010-)
1111-1212-// BuildCLI constructs the root command with two subcommands and a completion
1313-// subcommand wired in via [cli.CompletionSubCommand].
1414-//
1515-// Running "mytool completion" outputs a carapace-spec YAML document that
1616-// describes the full command tree. Redirect it once to register completions
1717-// with carapace-bin:
1818-//
1919-// mytool completion > ~/.config/carapace/specs/mytool.yaml
2020-//
2121-// carapace-bin then provides completions across bash, zsh, fish, nushell,
2222-// PowerShell, and more — no shell-specific scripts required.
2323-func BuildCLI() (*cli.Command, error) {
2424- return cli.New(
2525- "mytool",
2626- cli.Short("An example tool with shell completion support"),
2727- cli.Version("dev"),
2828- cli.Example("Say hello", "mytool hello --name Alice"),
2929- cli.Example("Say goodbye loudly", "mytool goodbye --name Bob --shout"),
3030- cli.Example("Register completions (run once)", "mytool completion > ~/.config/carapace/specs/mytool.yaml"),
3131- cli.SubCommands(
3232- buildHelloCommand,
3333- buildGoodbyeCommand,
3434- cli.CompletionSubCommand(), // wire in completion support
3535- ),
3636- )
3737-}
3838-3939-type helloOptions struct {
4040- name string
4141- verbosity flag.Count
4242-}
4343-4444-func buildHelloCommand() (*cli.Command, error) {
4545- var opts helloOptions
4646-4747- return cli.New(
4848- "hello",
4949- cli.Short("Greet someone"),
5050- cli.Example("Greet a person", "mytool hello --name Alice"),
5151- cli.Example("Greet verbosely", "mytool hello --name Alice -vvv"),
5252- cli.Flag(&opts.name, "name", 'n', "Name of the person to greet", cli.FlagDefault("World")),
5353- cli.Flag(&opts.verbosity, "verbose", 'v', "Increase output verbosity"),
5454- cli.Run(func(_ context.Context, cmd *cli.Command) error {
5555- fmt.Fprintf(cmd.Stdout(), "Hello, %s!\n", opts.name)
5656-5757- if opts.verbosity > 0 {
5858- fmt.Fprintf(cmd.Stdout(), "(verbosity level: %d)\n", opts.verbosity)
5959- }
6060-6161- return nil
6262- }),
6363- )
6464-}
6565-6666-type goodbyeOptions struct {
6767- name string
6868- shout bool
6969-}
7070-7171-func buildGoodbyeCommand() (*cli.Command, error) {
7272- var opts goodbyeOptions
7373-7474- return cli.New(
7575- "goodbye",
7676- cli.Short("Bid someone farewell"),
7777- cli.Example("Say goodbye", "mytool goodbye --name Bob"),
7878- cli.Example("Say it louder", "mytool goodbye --name Bob --shout"),
7979- cli.Flag(&opts.name, "name", 'n', "Name of the person to farewell", cli.FlagDefault("World")),
8080- cli.Flag(&opts.shout, "shout", 's', "Say the farewell in uppercase"),
8181- cli.Run(func(_ context.Context, cmd *cli.Command) error {
8282- msg := fmt.Sprintf("Goodbye, %s!", opts.name)
8383- if opts.shout {
8484- msg = strings.ToUpper(msg)
8585- }
8686-8787- fmt.Fprintln(cmd.Stdout(), msg)
8888-8989- return nil
9090- }),
9191- )
9292-}
-29
examples/completion/main.go
···11-// Package completion demonstrates how to add shell completion support to a CLI
22-// using [cli.CompletionSubCommand].
33-package main
44-55-import (
66- "context"
77- "fmt"
88- "os"
99-)
1010-1111-func main() {
1212- if err := run(); err != nil {
1313- fmt.Fprintf(os.Stderr, "Error: %s\n", err)
1414- os.Exit(1)
1515- }
1616-}
1717-1818-func run() error {
1919- cmd, err := BuildCLI()
2020- if err != nil {
2121- return fmt.Errorf("could not build root command: %w", err)
2222- }
2323-2424- if err := cmd.Execute(context.Background()); err != nil {
2525- return fmt.Errorf("could not execute root command: %w", err)
2626- }
2727-2828- return nil
2929-}
···11-source: completion_test.go
22-expression: stdout.String()
33----
44-|
55- name: mytool
66- description: My tool
77- flags:
88- -f, --force: Force deletion
99- persistentflags:
1010- -V, --version: Show version info for mytool
1111- -h, --help: Show help for mytool
1212- commands:
1313- - name: completion
1414- description: Output a carapace-spec YAML document to stdout
···11-source: completion_test.go
22-expression: stdout.String()
33----
44-|
55- name: mytool
66- description: My tool
77- flags:
88- -b, --bind=: Address to bind
99- persistentflags:
1010- -V, --version: Show version info for mytool
1111- -h, --help: Show help for mytool
1212- commands:
1313- - name: completion
1414- description: Output a carapace-spec YAML document to stdout
···11-source: completion_test.go
22-expression: stdout.String()
33----
44-|
55- name: mytool
66- description: My tool
77- flags:
88- --dry-run: Dry run mode
99- persistentflags:
1010- -V, --version: Show version info for mytool
1111- -h, --help: Show help for mytool
1212- commands:
1313- - name: completion
1414- description: Output a carapace-spec YAML document to stdout
···11-source: completion_test.go
22-expression: stdout.String()
33----
44-|
55- name: mytool
66- description: My tool
77- flags:
88- --dry-run: Dry run, make no changes
99- --tags=: Tags to apply
1010- --timeout=: Timeout in seconds
1111- -f, --force: Force the operation
1212- -o, --output=: Output file
1313- -v, --verbose*: Verbosity level
1414- persistentflags:
1515- -V, --version: Show version info for mytool
1616- -h, --help: Show help for mytool
1717- commands:
1818- - name: completion
1919- description: Output a carapace-spec YAML document to stdout
···11-source: completion_test.go
22-expression: stdout.String()
33----
44-|
55- name: mytool
66- description: My tool
77- persistentflags:
88- -V, --version: Show version info for mytool
99- -h, --help: Show help for mytool
1010- commands:
1111- - name: completion
1212- description: Output a carapace-spec YAML document to stdout
···11-source: completion_test.go
22-expression: stdout.String()
33----
44-|
55- name: mytool
66- description: My tool
77- flags:
88- -t, --tags=: Tags to apply
99- persistentflags:
1010- -V, --version: Show version info for mytool
1111- -h, --help: Show help for mytool
1212- commands:
1313- - name: completion
1414- description: Output a carapace-spec YAML document to stdout
···11-source: completion_test.go
22-expression: stdout.String()
33----
44-|
55- name: mytool
66- description: My tool
77- persistentflags:
88- -V, --version: Show version info for mytool
99- -h, --help: Show help for mytool
1010- commands:
1111- - name: completion
1212- description: Output a carapace-spec YAML document to stdout
1313- - name: frobnicate
1414- description: A placeholder for something cool
···11-source: completion_test.go
22-expression: stdout.String()
33----
44-|
55- name: mytool
66- description: My tool
77- persistentflags:
88- -V, --version: Show version info for mytool
99- -h, --help: Show help for mytool
1010- commands:
1111- - name: completion
1212- description: Output a carapace-spec YAML document to stdout
1313- - name: delete
1414- description: Delete a resource
1515- - name: get
1616- description: Get a resource