cli

package module
v0.0.0-...-8be729e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 1, 2026 License: MIT Imports: 12 Imported by: 0

README

cli

Go Reference

cli routes command-line arguments to handlers.

A Mux maps command names to Runner values, similar to how net/http.ServeMux maps URL patterns to http.Handler values. A *Command adds typed input declarations (flags, options, and positional arguments) to a Runner. A Program ties a root runner to the process environment and handles signal, I/O, and exit-code normalization.

Example

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/mzattahri/cli"
)

func main() {
	cmd := &cli.Command{
		Description: "Greet someone by name.",
		Run: func(out *cli.Output, call *cli.Call) error {
			name := call.Args["name"]
			if call.Flags["verbose"] {
				fmt.Fprintln(out.Stdout, "verbose mode")
			}
			_, err := fmt.Fprintf(out.Stdout, "hello %s\n", name)
			return err
		},
	}
	cmd.Flag("verbose", "v", false, "Enable verbose output")
	cmd.Arg("name", "Name to greet")

	mux := cli.NewMux("app")
	mux.Handle("greet", "Print a greeting", cmd)

	if err := (&cli.Program{Runner: mux}).Invoke(context.Background(), os.Args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(err.Code)
	}
}

Input model

The package distinguishes three kinds of command-line input:

Term CLI shape Go type Example
Flag Presence-based boolean bool --verbose
Option Named with value string --host localhost
Arg Positional, required string <image>

Flags and options must appear before positional arguments. The parser stops consuming flags at the first non-flag token or after --.

In POSIX terminology flags and options are both "options." This package separates them because the two forms have different signatures.

Mux-level flags and options

Flags and options declared on a Mux are parsed before subcommand routing. Each mux in a hierarchy can declare its own, and parsed values accumulate in Call.GlobalFlags and Call.GlobalOptions as routing descends:

root := cli.NewMux("app")
root.Flag("verbose", "v", false, "Enable verbose output")

repo := cli.NewMux("repo")
repo.Option("repository", "r", ".", "Repo path")
repo.Handle("init", "Initialize", initCmd)

root.Handle("repo", "Repository commands", repo)

// app --verbose repo --repository /tmp init
// → call.GlobalFlags["verbose"] == true
// → call.GlobalOptions["repository"] == "/tmp"

Core types

  • Program — process-level invocation environment: I/O streams, signal handling, exit-code normalization.
  • Mux — routes command paths to Runner values. Declares mux-level flags and options via Flag and Option. Implements Runner and Completer, so muxes compose.
  • Command — combines a Runner with per-command flags, options, and positional arguments declared via Flag, Option, and Arg. Implements Completer for tab completion of command-level flags.
  • Call — per-invocation input: flags, options, args, argv, stdin, context, and environment. The CLI equivalent of http.Request.
  • Output — carries Stdout and Stderr writers.
  • RunnerFunc — adapts a plain function to Runner.
  • Completer — interface for tab completion. Implemented by Mux and Command.
  • clitest.Recorder — captures stdout/stderr for test assertions.

Testing

go test ./...

The clitest sub-package provides in-memory helpers:

recorder := clitest.NewRecorder()
call := clitest.NewCall("greet gopher", nil)
err := mux.RunCLI(recorder.Output(), call)
// recorder.Stdout.String() == "hello gopher"

Shell completion

CompletionRunner returns a Runner that outputs tab completions. Register it on your mux under any name you like:

mux.Handle("complete", "Output completions", cli.CompletionRunner(mux))

The runner expects the current command line as positional arguments after -- and prints one value\tdescription pair per line.

Bash
_myapp() {
  local cur="${COMP_WORDS[COMP_CWORD]}"
  local words=("${COMP_WORDS[@]:1}")
  COMPREPLY=()
  while IFS=$'\t' read -r val _; do
    COMPREPLY+=("$val")
  done < <(myapp complete -- "${words[@]}")
}
complete -F _myapp myapp
Zsh
_myapp() {
  local -a completions
  while IFS=$'\t' read -r val desc; do
    completions+=("${val}:${desc}")
  done < <(myapp complete -- "${words[@]:1}")
  _describe 'command' completions
}
compdef _myapp myapp
Fish
complete -c myapp -f -a '(myapp complete -- (commandline -cop))'

Documentation

Overview

Package cli routes command-line arguments to handlers.

A Mux maps command names to Runner values, similar to how net/http.ServeMux maps URL patterns to net/http.Handler values. A *Command adds typed input declarations (flags, options, and positional arguments) to a Runner. A Program ties a root runner to the process environment and handles signal, I/O, and exit-code normalization.

cmd := &cli.Command{
	Run: func(out *cli.Output, call *cli.Call) error {
		_, err := fmt.Fprintf(out.Stdout, "hello %s\n", call.Args["name"])
		return err
	},
}
cmd.Arg("name", "Person to greet")

mux := cli.NewMux("app")
mux.Handle("greet", "Print a greeting", cmd)

Flags are booleans toggled by presence (--verbose). Options carry a string value (--host localhost). Positional arguments are required and ordered. Flags and options must appear before positional arguments; the parser stops at the first non-flag token or after "--".

In POSIX terminology flags and options are both "options." This package separates them because the two forms have different signatures.

Index

Examples

Constants

View Source
const (
	ExitOK      = 0
	ExitFailure = 1
	ExitHelp    = 2
)

Standard process exit codes used by Program.Invoke.

Variables

View Source
var DefaultProgram = &Program{
	Stdout:    os.Stdout,
	Stderr:    os.Stderr,
	Stdin:     os.Stdin,
	LookupEnv: os.LookupEnv,
}

DefaultProgram is the default Program used by the package-level Handle, HandleFunc, and Invoke functions.

View Source
var ErrHelp = errors.New("cli: help requested")

ErrHelp is returned when help output was displayed instead of executing a command. It is a sentinel distinct from ExitError.

Functions

func DefaultHelpFunc

func DefaultHelpFunc(w io.Writer, help *Help)

DefaultHelpFunc is the built-in HelpFunc used when no override is set. It renders a tabular summary to w.

func Flag

func Flag(name, short string, value bool, usage string)

Flag declares a boolean flag on the DefaultProgram mux. The mux is created lazily on first use.

func Handle

func Handle(pattern string, usage string, runner Runner)

Handle registers runner on the DefaultProgram mux. The mux is created lazily on first use.

func HandleFunc

func HandleFunc(pattern string, usage string, fn func(*Output, *Call) error)

HandleFunc registers fn on the DefaultProgram mux. The mux is created lazily on first use.

func Option

func Option(name, short, value, usage string)

Option declares a value option on the DefaultProgram mux. The mux is created lazily on first use.

Types

type Call

type Call struct {

	// Pattern is the matched command path (e.g. "app repo init").
	Pattern string

	// Argv is the remaining argument tail after command routing.
	Argv []string

	// Stdin is the standard input stream.
	Stdin io.Reader

	// Env resolves environment variables. It follows the signature of
	// [os.LookupEnv].
	Env func(string) (string, bool)

	// GlobalFlags holds mux-level boolean flags accumulated during routing.
	GlobalFlags map[string]bool

	// GlobalOptions holds mux-level option values accumulated during routing.
	GlobalOptions map[string]string

	// Flags holds command-level boolean flags.
	Flags map[string]bool

	// Options holds command-level option values.
	Options map[string]string

	// Args holds bound positional arguments.
	Args map[string]string

	// Rest holds unmatched trailing positional arguments when
	// [Command.CaptureRest] is set.
	Rest []string
	// contains filtered or unexported fields
}

A Call carries the parsed input for a single command invocation. It is the CLI equivalent of an net/http.Request.

func NewCall

func NewCall(ctx context.Context, pattern string, argv []string) *Call

NewCall returns a new Call with the given context, pattern, and argv. All map fields are initialized to non-nil empty maps.

It panics if ctx is nil.

func NewCallWithContext

func NewCallWithContext(ctx context.Context, call *Call) *Call

NewCallWithContext returns a shallow copy of call with ctx replacing the original context. Exported maps and slices are deep-copied.

It panics if ctx or call is nil.

Example
package main

import (
	"context"
	"fmt"

	"github.com/mzattahri/cli"
	"github.com/mzattahri/cli/clitest"
)

type authContextKey struct{}

func main() {
	call := clitest.NewCall("whoami", nil)
	ctx := context.WithValue(context.Background(), authContextKey{}, "alice")
	derived := cli.NewCallWithContext(ctx, call)

	fmt.Print(derived.Context().Value(authContextKey{}))
}
Output:
alice

func (*Call) Context

func (c *Call) Context() context.Context

Context returns the call's context, defaulting to context.Background if the call or its context is nil.

func (*Call) String

func (c *Call) String() string

String returns Argv joined as a shell-like string, quoting tokens that contain special characters.

type Command

type Command struct {
	// Description is the longer help text shown by [HelpFunc].
	Description string

	// CaptureRest preserves unmatched trailing positional arguments
	// in [Call.Rest].
	CaptureRest bool

	// Run handles the command invocation.
	Run RunnerFunc
	// contains filtered or unexported fields
}

A Command combines a handler function with per-command input declarations.

Flags, options, and positional arguments are declared with the Command.Flag, Command.Option, and Command.Arg methods. All declarations must be made before the command is registered with Mux.Handle.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/mzattahri/cli"
)

func main() {
	cmd := &cli.Command{
		CaptureRest: true,
		Run: func(out *cli.Output, call *cli.Call) error {
			detach := call.Flags["detach"]
			_, err := fmt.Fprintf(out.Stdout, "image=%s detach=%t command=%v", call.Args["image"], detach, call.Rest)
			return err
		},
	}
	cmd.Flag("detach", "", false, "Run in background")
	cmd.Arg("image", "Image reference")

	mux := cli.NewMux("app")
	mux.Handle("run", "Run a container", cmd)

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	program := &cli.Program{Stdout: &stdout, Stderr: &stderr, Runner: mux}
	_ = program.Invoke(context.Background(), []string{"app", "run", "--detach", "alpine", "sh", "-c", "echo hi"})
	fmt.Print(stdout.String())
}
Output:
image=alpine detach=true command=[sh -c echo hi]

func (*Command) Arg

func (c *Command) Arg(name, usage string)

Arg declares a required positional argument. It panics if name is empty or duplicated.

func (*Command) Complete

func (c *Command) Complete(w io.Writer, tokens []string)

Complete writes tab-completion candidates for command-level flags and options to w, implementing the Completer interface.

func (*Command) Flag

func (c *Command) Flag(name, short string, value bool, usage string)

Flag declares a boolean flag toggled by presence.

short is an optional one-character short form (e.g. "v" for -v). An empty string means the flag has no short form. It panics on duplicate or reserved names.

func (*Command) Option

func (c *Command) Option(name, short, value, usage string)

Option declares a named value option with a default.

short is an optional one-character short form (e.g. "o" for -o). An empty string means the option has no short form. It panics on duplicate or reserved names.

func (*Command) RunCLI

func (c *Command) RunCLI(out *Output, call *Call) error

RunCLI calls c.Run.

type Completer

type Completer interface {
	Complete(w io.Writer, tokens []string)
}

A Completer writes tab completions for a partial command line to w. The tokens parameter contains the tokens currently on the command line; the last element is the partial word being typed (possibly empty). Each completion is written as a tab-separated line: value\tdescription.

*Mux and *Command both implement Completer. A Mux completes subcommands and mux-level flags; a Command completes command-level flags and options.

type Completion

type Completion struct {
	Value       string
	Description string
}

A Completion is a single tab-completion candidate.

type ExitError

type ExitError struct {
	Code int
	Err  error
}

An ExitError is an error with an explicit process exit code.

func Invoke

func Invoke(ctx context.Context, args []string) *ExitError

Invoke calls DefaultProgram.Invoke(ctx, args).

func (*ExitError) Error

func (e *ExitError) Error() string

Error returns the underlying error message, or the empty string if the underlying error is nil.

func (*ExitError) Unwrap

func (e *ExitError) Unwrap() error

Unwrap returns the underlying error.

type Flusher

type Flusher interface {
	Flush() error
}

Flusher is implemented by writers that support flushing buffered output.

type Help

type Help struct {
	// Name is the final segment of the command path.
	Name string
	// FullPath is the complete command path (e.g. "app repo init").
	FullPath string
	// Usage is a short one-line summary.
	Usage string
	// Description is longer free-form help text.
	Description string

	// GlobalFlags lists program-level boolean flags in scope.
	GlobalFlags []HelpFlag
	// GlobalOptions lists program-level value options in scope.
	GlobalOptions []HelpOption

	// Commands lists the immediate child commands.
	Commands []HelpCommand
	// Arguments lists positional arguments accepted by this command.
	Arguments []HelpArg
	// Flags lists command-level boolean flags.
	Flags []HelpFlag
	// Options lists command-level value options.
	Options []HelpOption
}

Help holds the data passed to a HelpFunc when rendering help output.

type HelpArg

type HelpArg struct {
	Name  string
	Usage string
}

A HelpArg describes a positional argument in rendered help.

type HelpCommand

type HelpCommand struct {
	Name        string
	Usage       string
	Description string
}

A HelpCommand describes an immediate child command in rendered help.

type HelpFlag

type HelpFlag struct {
	Name    string
	Short   string
	Usage   string
	Default bool
}

A HelpFlag describes a boolean flag in rendered help.

type HelpFunc

type HelpFunc func(w io.Writer, help *Help)

A HelpFunc renders help output to w for a resolved command path.

type HelpOption

type HelpOption struct {
	Name    string
	Short   string
	Usage   string
	Default string
}

A HelpOption describes a value option in rendered help.

type Mux

type Mux struct {
	Name string
	// contains filtered or unexported fields
}

A Mux is a command multiplexer. It routes argv tokens to Runner values registered with Mux.Handle, in the same spirit as net/http.ServeMux.

func NewMux

func NewMux(name string) *Mux

NewMux returns a new Mux with the given program name. It panics if name is empty.

func (*Mux) Complete

func (m *Mux) Complete(w io.Writer, tokens []string)

Complete writes tab-completion candidates for the given command line tokens to w, implementing the Completer interface.

The trie walk matches completed tokens against registered subcommands. When a child node implements Completer (a mounted *Mux or a *Command), the remaining tokens are delegated to that node's Complete method. Otherwise, the mux completes its own subcommands and mux-level flags.

func (*Mux) Flag

func (m *Mux) Flag(name, short string, value bool, usage string)

Flag declares a mux-level boolean flag that is parsed before subcommand routing. Parsed values accumulate in [Call.GlobalFlags].

short is an optional one-character short form (e.g. "v" for -v). An empty string means the flag has no short form. It panics on duplicate or reserved names.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/mzattahri/cli"
)

func main() {
	mux := cli.NewMux("app")
	mux.Flag("verbose", "v", false, "Enable verbose output")
	mux.Handle("status", "Print status", cli.RunnerFunc(func(out *cli.Output, call *cli.Call) error {
		_, err := fmt.Fprintf(out.Stdout, "verbose=%t", call.GlobalFlags["verbose"])
		return err
	}))

	var stdout bytes.Buffer
	program := &cli.Program{Stdout: &stdout, Stderr: &bytes.Buffer{}, Runner: mux}
	_ = program.Invoke(context.Background(), []string{"app", "--verbose", "status"})
	fmt.Print(stdout.String())
}
Output:
verbose=true

func (*Mux) Handle

func (m *Mux) Handle(pattern string, usage string, runner Runner)

Handle registers runner for the given command pattern with a short usage summary shown in help output.

Pattern segments are split on whitespace. Multi-segment patterns create nested command paths (e.g. "repo init"). If runner is a *Mux, it is mounted as a sub-mux at pattern. It panics on conflicting registrations or a nil runner.

Example (OptionsOnly)
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/mzattahri/cli"
)

func main() {
	cmd := &cli.Command{
		Run: func(out *cli.Output, call *cli.Call) error {
			host := call.Options["host"]
			_, err := fmt.Fprint(out.Stdout, host)
			return err
		},
	}
	cmd.Option("host", "", "", "daemon socket")

	mux := cli.NewMux("app")
	mux.Handle("status", "Print status", cmd)

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	program := &cli.Program{Stdout: &stdout, Stderr: &stderr, Runner: mux}
	_ = program.Invoke(context.Background(), []string{"app", "status", "--host", "unix:///tmp/docker.sock"})
	fmt.Print(stdout.String())
}
Output:
unix:///tmp/docker.sock
Example (Subtree)
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/mzattahri/cli"
)

func main() {
	repo := cli.NewMux("repo")
	repo.Handle("init", "Initialize a repository", cli.RunnerFunc(func(out *cli.Output, call *cli.Call) error {
		_, err := fmt.Fprint(out.Stdout, "initialized")
		return err
	}))

	mux := cli.NewMux("app")
	mux.Handle("repo", "Manage repositories", repo)

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	program := &cli.Program{Stdout: &stdout, Stderr: &stderr, Runner: mux}
	_ = program.Invoke(context.Background(), []string{"app", "repo", "init"})
	fmt.Print(stdout.String())
}
Output:
initialized

func (*Mux) HandleFunc

func (m *Mux) HandleFunc(pattern string, usage string, fn func(*Output, *Call) error)

HandleFunc registers fn as the handler for pattern. It is a shorthand for Handle(pattern, usage, RunnerFunc(fn)).

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/mzattahri/cli"
)

func main() {
	mux := cli.NewMux("app")
	mux.HandleFunc("version", "Print version", func(out *cli.Output, call *cli.Call) error {
		_, err := fmt.Fprint(out.Stdout, "v1.0.0")
		return err
	})

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	program := &cli.Program{Stdout: &stdout, Stderr: &stderr, Runner: mux}
	_ = program.Invoke(context.Background(), []string{"app", "version"})
	fmt.Print(stdout.String())
}
Output:
v1.0.0

func (*Mux) Option

func (m *Mux) Option(name, short, value, usage string)

Option declares a mux-level named value option that is parsed before subcommand routing. Parsed values accumulate in [Call.GlobalOptions].

short is an optional one-character short form (e.g. "c" for -c). An empty string means the option has no short form. It panics on duplicate or reserved names.

func (*Mux) RunCLI

func (m *Mux) RunCLI(out *Output, call *Call) error

RunCLI routes call.Argv through the command trie and dispatches to the matched handler. It panics if call is nil.

type Output

type Output struct {
	// Stdout is the standard output stream. If it implements [Flusher],
	// [Output.Flush] flushes it after the runner returns.
	Stdout io.Writer

	// Stderr is the standard error stream. If it implements [Flusher],
	// [Output.Flush] flushes it after the runner returns.
	Stderr io.Writer
}

Output carries the output streams for a command invocation.

func (*Output) Flush

func (o *Output) Flush() error

Flush flushes Stdout and Stderr if either implements Flusher.

type Program

type Program struct {
	// Name is shown in usage output. When empty, [Program.Invoke]
	// uses args[0].
	Name string

	// Runner is the root runner. When nil on [DefaultProgram], a
	// default [Mux] is created lazily.
	Runner Runner

	// Stdout is the standard output writer. When nil, [Program.Invoke]
	// uses [os.Stdout].
	Stdout io.Writer

	// Stderr is the standard error writer. When nil, [Program.Invoke]
	// uses [os.Stderr].
	Stderr io.Writer

	// Stdin is the standard input reader. When nil, [Program.Invoke]
	// uses [os.Stdin].
	Stdin io.Reader

	// LookupEnv resolves environment variables. When nil,
	// [Program.Invoke] uses [os.LookupEnv].
	LookupEnv func(string) (string, bool)

	// Usage is the short summary shown in top-level help output.
	Usage string

	// Description is longer free-form help text.
	Description string

	// HelpFunc overrides the default help renderer.
	HelpFunc HelpFunc

	// IgnoreSignals disables the default SIGINT context wrapping.
	IgnoreSignals bool
}

A Program describes the process-level invocation environment for a root Runner.

func (*Program) Flag

func (p *Program) Flag(name, short string, value bool, usage string)

Flag declares a boolean flag on the underlying *Mux runner. It panics if the program's Runner is not a *Mux. See Mux.Flag for details.

func (*Program) Invoke

func (p *Program) Invoke(ctx context.Context, args []string) *ExitError

Invoke runs the program's root Runner and normalizes the result to an *ExitError. An explicit --help request returns nil after rendering help. It panics if ctx is nil.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/mzattahri/cli"
)

func main() {
	cmd := &cli.Command{
		Run: func(out *cli.Output, call *cli.Call) error {
			_, err := fmt.Fprintf(out.Stdout, "hello %s", call.Args["name"])
			return err
		},
	}
	cmd.Arg("name", "Person to greet")

	mux := cli.NewMux("app")
	mux.Handle("greet", "Print a greeting", cmd)

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	program := &cli.Program{
		Stdout: &stdout,
		Stderr: &stderr,
		Runner: mux,
	}
	_ = program.Invoke(context.Background(), []string{"app", "greet", "gopher"})
	fmt.Print(stdout.String())
}
Output:
hello gopher

func (*Program) Option

func (p *Program) Option(name, short, value, usage string)

Option declares a named value option on the underlying *Mux runner. It panics if the program's Runner is not a *Mux. See Mux.Option for details.

type Runner

type Runner interface {
	RunCLI(out *Output, call *Call) error
}

A Runner executes a CLI command. It receives an *Output for writing and a *Call carrying the parsed input.

func CompletionRunner

func CompletionRunner(c Completer) Runner

CompletionRunner returns a Runner that outputs tab completions for the current command line.

Shell integration scripts invoke this runner on each TAB press, passing the current tokens (shell tokens) as positional arguments after "--":

myapp complete -- repo init --f

It panics if c is nil.

Example
package main

import (
	"bytes"
	"context"
	"fmt"

	"github.com/mzattahri/cli"
)

func main() {
	mux := cli.NewMux("app")
	mux.Handle("greet", "Print a greeting", cli.RunnerFunc(func(out *cli.Output, call *cli.Call) error {
		_, err := fmt.Fprint(out.Stdout, "hello")
		return err
	}))
	mux.Handle("complete", "Output completions", cli.CompletionRunner(mux))
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	program := &cli.Program{Stdout: &stdout, Stderr: &stderr, Runner: mux}
	_ = program.Invoke(context.Background(), []string{"app", "complete", "--", "gr"})
	fmt.Print(stdout.String())
}
Output:
greet	Print a greeting

type RunnerFunc

type RunnerFunc func(out *Output, call *Call) error

RunnerFunc adapts a plain function to the Runner interface.

func (RunnerFunc) RunCLI

func (f RunnerFunc) RunCLI(out *Output, call *Call) error

RunCLI calls f(out, call).

Directories

Path Synopsis
Package clitest provides test helpers for cli runners and muxes.
Package clitest provides test helpers for cli runners and muxes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL