cli

package module
v0.18.3 Latest Latest
Warning

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

Go to latest
Published: Dec 14, 2025 License: MIT Imports: 14 Imported by: 0

README

CLI

License Go Reference Go Report Card GitHub CI codecov

Tiny, simple, but powerful CLI framework for modern Go 🚀

demo

[!WARNING] CLI is still in development and is not yet stable

Project Description

cli is a simple, minimalist, yet functional and powerful CLI framework for Go. Inspired by things like spf13/cobra and urfave/cli, but building on lessons learned and using modern Go techniques and idioms.

Installation

go get go.followtheprocess.codes/cli@latest

Quickstart

package main

import (
    "context"
    "fmt"
    "os"

    "go.followtheprocess.codes/cli"
)

func main() {
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}

func run() error {
    var count int

    cmd, err := cli.New(
        "quickstart",
        cli.Short("Short description of your command"),
        cli.Long("Much longer text..."),
        cli.Version("v1.2.3"),
        cli.Commit("7bcac896d5ab67edc5b58632c821ec67251da3b8"),
        cli.BuildDate("2024-08-17T10:37:30Z"),
        cli.Stdout(os.Stdout),
        cli.Example("Do a thing", "quickstart something"),
        cli.Example("Count the things", "quickstart something --count 3"),
        cli.Flag(&count, "count", 'c', "Count the things"),
        cli.Run(func(ctx context.Context, cmd *cli.Command) error {
            fmt.Fprintf(cmd.Stdout(), "Hello from quickstart!, my args were: %v, count was %d\n", cmd.Args(), count)
            return nil
        }),
    )
    if err != nil {
        return err
    }

    return cmd.Execute(context.Background())
}

Will get you the following:

quickstart

[!TIP] See usage section below and more examples under ./examples

Usage

Commands

To create CLI commands, you simply call cli.New:

cmd, err := cli.New(
    "name", // The name of your command
    cli.Short("A new command") // Shown in the help
    cli.Run(func(ctx context.Context, cmd *cli.Command) error {
        // This function is what your command does
        fmt.Printf("name called with args: %v\n", cmd.Args())
        return nil
    })
)

[!TIP] The command can be customised by applying any number of functional options for setting the help text, describing the arguments or flags it takes, adding subcommands etc. see https://pkg.go.dev/github.com/FollowTheProcess/cli#Option

Sub Commands

To add a subcommand underneath the command you've just created, it's again cli.New:

// Best to abstract it into a function
func buildSubcommand() (*cli.Command, error) {
    return cli.New(
        "sub", // Name of the sub command e.g. 'clone' for 'git clone'
        cli.Short("A sub command"),
        // etc..
    )
}

And add it to your parent command:

ctx := context.Background()

// From the example above
cmd, err := cli.New(
    "name", // The name of your command
    // ...
    cli.SubCommands(buildSubcommand),
)

This pattern can be repeated recursively to create complex command structures.

Flags

Flags in cli are generic, that is, there is one way to add a flag to your command, and that's with the cli.Flag option to cli.New

// These will get set at command line parse time
// based on your flags
type options struct {
    name string
    force bool
    size uint
    items []string
}

func buildCmd() (*cli.Command, error) {
    var opts options
    return cli.New(
        // ...
        // Signature is cli.Flag(*T, name, shorthand, description)
        cli.Flag(&options.name, "name", 'n', "The name of something"),
        cli.Flag(&options.force, "force", cli.NoShortHand, "Force delete without confirmation"),
        cli.Flag(&options.size, "size", 's', "Size of something"),
        cli.Flag(&options.items, "items", 'i', "Items to include"),
        // ...
    )
}

[!TIP] Default values can be provided with the cli.FlagDefault Option

The types are all inferred automatically! No more BoolSliceVarP

The types you can use for flags currently are:

  • int
  • int8
  • int16
  • int32
  • int64
  • uint
  • uint8
  • uint16
  • uint32
  • uint64
  • uintptr
  • float32
  • float64
  • string
  • bool
  • []byte (interpreted as a hex string)
  • Count (special type for flags that count things e.g. a --verbosity flag may be used like -vvv to increase verbosity to 3)
  • time.Time
  • time.Duration
  • net.IP
  • []int
  • []int8
  • []int16
  • []int32
  • []int64
  • []uint
  • []uint16
  • []uint32
  • []uint64
  • []float32
  • []float64
  • []string

[!NOTE] You basically can't get this wrong, if you try and use an unsupported type, the Go compiler will yell at you

Arguments

There are two approaches to positional arguments in cli, you can either just get the raw arguments yourself with cmd.Args() and do whatever you want with them:

cli.New(
    "my-command",
    // ...
    cli.Run(func(ctx context.Context, cmd *cli.Command) error {
        fmt.Fprintf(cmd.Stdout(), "Hello! My arguments were: %v\n", cmd.Args())
        return nil
    })
)

This will return a []string containing all the positional arguments to your command (not flags, they've already been parsed out!)

Or, if you want to get smarter 🧠 cli allows you to define type safe representations of your arguments, with or without default values! This follows a similar idea to Flags

That works like this:

// Define a struct to hold your arguments
type myArgs struct {
    name     string
    age      int
    employed bool
}

// Instantiate it
var args myArgs

// Tell cli about your arguments with the Arg option
cli.New(
    "my-command",
    // ... other options here
    cli.Arg(&args.name, "name", "The name of a person"),
    cli.Arg(&args.age, "age", "How old the person is"),
    cli.Arg(&args.employed, "employed", "Whether they are employed", cli.ArgDefault(true))
)

And just like Flags, your argument types are all inferred and parsed automatically ✨, and you get nicer --help output too!

Have a look at the ./examples to see more!

Other CLI libraries that do this sort of thing rely heavily on reflection and struct tags, cli does things differently! We prefer Go code to be 100% type safe and statically checkable, so all this is implemented using generics and compile time checks 🚀

[!NOTE] Just like flags, you can't really get this wrong. The types you can use for arguments are part of a generic constraint so using the wrong type results in a compiler error

The types you can currently use for positional args are:

  • int
  • int8
  • int16
  • int32
  • int64
  • uint
  • uint8
  • uint16
  • uint32
  • uint64
  • uintptr
  • float32
  • float64
  • string
  • *url.URL
  • bool
  • []byte (interpreted as a hex string)
  • time.Time
  • time.Duration
  • net.IP

[!WARNING] 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 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

Core Principles

When designing and implementing cli, I had some core goals and guiding principles for implementation.

😱 Well behaved libraries don't panic

cli validates heavily and returns errors for you to handle. By contrast spf13/cobra (and by extension spf13/pflag) panic in a number of (IMO unnecessary) conditions including:

  • Duplicate subcommand
  • Command adding itself as a subcommand
  • Duplicate flag
  • Invalid shorthand flag letter

The design of cli is such that commands are instantiated with cli.New and a number of functional options. These options are in charge of configuring your command and each will perform validation prior to applying the setting.

These errors are joined and bubbled up to you in one go via cli.New so you don't have to play error whack-a-mole, and more importantly your application won't panic!

🧘🏻 Keep it Simple

cli has an intentionally small public interface and gives you only what you need to build amazing CLI apps:

  • No huge structs with hundreds of fields
  • No confusing or conflicting options
  • Customisation in areas where it makes sense, sensible opinionated defaults everywhere else
  • No reflection or struct tags

There is one and only one way to do things (and that is usually to use an option in cli.New)

👨🏻‍🔬 Use Modern Techniques

The dominant Go CLI toolkits were mostly built many years (and many versions of Go) ago. They are reliable and battle hardened but because of their high number of users, they have had to be very conservative with changes.

cli has none of these constraints and can use bang up to date Go techniques and idioms.

One example is generics, consider how you define a flag:

var force bool
cli.New("demo", cli.Flag(&force, "force", 'f', false, "Force something"))

Note the type bool is inferred by cli.Flag. This will work with any type allowed by the Flaggable generic constraint so you'll get compile time feedback if you've got it wrong. No more flag.BoolStringSliceVarP 🎉

🥹 A Beautiful API

cli heavily leverages the functional options pattern to create a delightful experience building a CLI tool. It almost reads like plain english:

var count int
cmd, err := cli.New(
    "test",
    cli.Short("Short description of your command"),
    cli.Long("Much longer text..."),
    cli.Version("v1.2.3"),
    cli.Stdout(os.Stdout),
    cli.Example("Do a thing", "test run thing --now"),
    cli.Flag(&count, "count", 'c', 0, "Count the things"),
)
🔐 Immutable State

Typically, commands are implemented as a big struct with lots of fields. cli is no different in this regard.

What is different though is that this large struct can only be configured with cli.New. Once you've built your command, it can't be modified.

This eliminates a whole class of bugs and prevents misconfiguration and footguns 🔫

🚧 Good Libraries are Hard to Misuse

Everything in cli is (hopefully) clear, intuitive, and well-documented. There's a tonne of strict validation in a bunch of places and wherever possible, misuse results in a compilation error.

Consider the following example of a bad shorthand value:

var delete bool

// Note: "de" is a bad shorthand, it's two letters
cli.New("demo", cli.Flag(&delete, "delete", "de", "Delete something"))

In cli this is impossible as we use rune as the type for a flag shorthand, so the above example would not compile. Instead you must specify a valid rune:

var delete bool

// Ahhh, that's better
cli.New("demo", cli.Flag(&delete, "delete", 'd', "Delete something"))

And if you don't want a shorthand? i.e. just --delete with no -d option:

var delete bool
cli.New("demo", cli.Flag(&delete, "delete", cli.NoShortHand, "Delete something"))

In the Wild

I 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:

Documentation

Overview

Package cli provides a clean, minimal and simple mechanism for constructing CLI commands.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ArgOption added in v0.17.0

type ArgOption[T arg.Argable] interface {
	// contains filtered or unexported methods
}

ArgOption is a functional option for configuring an Arg.

func ArgDefault added in v0.17.0

func ArgDefault[T arg.Argable](value T) ArgOption[T]

ArgDefault is a cli.ArgOption that sets the default value for a positional argument.

By default, positional arguments are required, but by providing a default value via this option, you mark the argument as not required.

If a default is given and the argument is not provided via the command line, the default is used in its place.

type Builder

type Builder func() (*Command, error)

Builder is a function that constructs and returns a Command, it makes constructing complex command trees easier as they can be passed directly to the SubCommands option.

type Command

type Command struct {
	// contains filtered or unexported fields
}

Command represents a CLI command.

Commands in cli are recursive, that means that the root command is not different or special compared to any of its subcommands, this structure makes defining complex command trees as simple as creating a single command.

In the command line 'git commit -m "Message"' both 'git' and 'commit' would be a clio command, '-m' would be a Flag taking a string argument.

Commands are constructed with the New function and customised by providing a number of functional options to layer different settings and functionality.

func New

func New(name string, options ...Option) (*Command, error)

New builds and returns a new Command.

The command can be customised by passing in a number of options enabling you to do things like configure stderr and stdout, add or customise help or version output add subcommands and run functions etc.

Without any options passed, the default implementation returns a Command with no subcommands, a -v/--version and a -h/--help flag, hooked up to os.Stdin, os.Stdout and os.Stderr and accepting arbitrary positional arguments from os.Args (with the command path stripped, equivalent to os.Args[1:]).

Options will validate their inputs where possible and return errors which will be bubbled up through New to aid debugging invalid configuration.

func (*Command) Args added in v0.17.0

func (cmd *Command) Args() []string

Args returns the positional arguments passed to the command.

func (*Command) Execute

func (cmd *Command) Execute(ctx context.Context) error

Execute parses the flags and arguments, and invokes the Command's Run function, returning any error.

If the flags fail to parse, an error will be returned and the Run function will not be called.

func (*Command) ExtraArgs

func (cmd *Command) ExtraArgs() (args []string, ok bool)

ExtraArgs returns any additional arguments following a "--", and a boolean indicating whether or not they were present. This is useful for when you want to implement argument pass through in your commands.

If there were no extra arguments, it will return nil, false.

func (*Command) Stderr

func (cmd *Command) Stderr() io.Writer

Stderr returns the configured Stderr for the Command.

func (*Command) Stdin

func (cmd *Command) Stdin() io.Reader

Stdin returns the configured Stdin for the Command.

func (*Command) Stdout

func (cmd *Command) Stdout() io.Writer

Stdout returns the configured Stdout for the Command.

type FlagOption added in v0.18.0

type FlagOption[T flag.Flaggable] interface {
	// contains filtered or unexported methods
}

FlagOption is a functional option for configuring a Flag.

func FlagDefault added in v0.18.0

func FlagDefault[T flag.Flaggable](value T) FlagOption[T]

FlagDefault is a cli.FlagOption that sets the default value for command line flag.

By default, a flag's default value is the zero value for its type. But using this option, you may set a non-zero default value that the flag should inherit if not provided on the command line.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option is a functional option for configuring a Command.

func Arg added in v0.17.0

func Arg[T arg.Argable](p *T, name, usage string, options ...ArgOption[T]) Option

Arg is an Option that adds a typed argument to a Command, storing its value in a variable via its pointer 'target'.

The variable is set when the argument is parsed during command execution.

Args linked to slice values (e.g. []string) must be defined last as they eagerly consume all remaining command line arguments.

The argument may be given a default value with the ArgDefault option. Without this option the argument will be required, i.e. failing to provide it on the command line is an error, but when a default is given and the value omitted on the command line, the default is used in its place.

// Add an int arg that defaults to 1
var number int
cli.New("add", cli.Arg(&number, "number", "Add a number", cli.ArgDefault(1)))

func BuildDate

func BuildDate(date string) Option

BuildDate is an Option that sets the build date for a binary built with CLI. It is particularly useful for embedding rich version info into a binary using ldflags

Without this option, the build date is simply omitted from the version info shown when -v/--version is called.

If set to a non empty string, the build date will be shown.

cli.New("test", cli.BuildDate("2024-07-06T10:37:30Z"))

func Commit

func Commit(commit string) Option

Commit is an Option that sets the commit hash for a binary built with CLI. It is particularly useful for embedding rich version info into a binary using ldflags.

Without this option, the commit hash is simply omitted from the version info shown when -v/--version is called.

If set to a non empty string, the commit hash will be shown.

cli.New("test", cli.Commit("b43fd2c"))

func Example

func Example(comment, command string) Option

Example is an Option that adds an example to a Command.

Examples take the form of an explanatory comment and a command showing the command to the CLI, these will show up in the help text.

For example, a program called "myrm" that deletes files and directories might have an example declared as follows:

cli.Example("Delete a folder recursively without confirmation", "myrm ./dir --recursive --force")

Which would show up in the help text like so:

Examples:
# Delete a folder recursively without confirmation
$ myrm ./dir --recursive --force

An arbitrary number of examples can be added to a Command, and calls to Example are additive.

func Flag

func Flag[T flag.Flaggable](target *T, name string, short rune, usage string, options ...FlagOption[T]) Option

Flag is an Option that adds a typed flag to a Command, storing its value in a variable via its pointer 'target'.

The variable is set when the flag is parsed during command execution. By default, the flag will assume the zero value for its type, the default may be provided explicitly using the FlagDefault option.

If the default value is not the zero value for the type T, the flags usage message will show the default value in the commands help text.

To add a long flag only (e.g. --delete with no -d option), pass [NoShortHand] for "short".

Flags linked to slice values (e.g. []string) work by appending the passed values to the slice so multiple values may be given by repeat usage of the flag e.g. --items "one" --items "two".

// Add a force flag
var force bool
cli.New("rm", cli.Flag(&force, "force", 'f', "Force deletion without confirmation"))

func Long

func Long(long string) Option

Long is an Option that sets the full description for a Command.

The long description will appear in the help text for a command. Users are responsible for wrapping the text at a sensible width.

For consistency of formatting, all leading and trailing whitespace is stripped.

Successive calls will simply overwrite any previous calls.

cli.New("rm", cli.Long("... lots of text here"))

func NoColour

func NoColour(noColour bool) Option

NoColour is an Option that disables all colour output from the Command.

CLI respects the values of $NO_COLOR and $FORCE_COLOR automatically so this need not be set for most applications.

Setting this option takes precedence over all other colour configuration.

func OverrideArgs

func OverrideArgs(args []string) Option

OverrideArgs is an Option that sets the arguments for a Command, overriding any arguments parsed from the command line.

Without this option, the command will default to os.Args[1:], this option is particularly useful for testing.

Successive calls override previous ones.

// Override arguments for testing
cli.New("test", cli.OverrideArgs([]string{"test", "me"}))

func Run

func Run(run func(ctx context.Context, cmd *Command) error) Option

Run is an Option that sets the run function for a Command.

The run function is the actual implementation of your command i.e. what you want it to do when invoked.

Successive calls overwrite previous ones.

func Short

func Short(short string) Option

Short is an Option that sets the one line usage summary for a Command.

The one line usage will appear in the help text as well as alongside subcommands when they are listed.

For consistency of formatting, all leading and trailing whitespace is stripped.

Successive calls will simply overwrite any previous calls.

cli.New("rm", cli.Short("Delete files and directories"))

func Stderr

func Stderr(stderr io.Writer) Option

Stderr is an Option that sets the Stderr for a Command.

Successive calls will simply overwrite any previous calls. Without this option the command will default to os.Stderr.

// Set stderr to a temporary buffer
buf := &bytes.Buffer{}
cli.New("test", cli.Stderr(buf))

func Stdin

func Stdin(stdin io.Reader) Option

Stdin is an Option that sets the Stdin for a Command.

Successive calls will simply overwrite any previous calls. Without this option the command will default to os.Stdin.

// Set stdin to os.Stdin (the default anyway)
cli.New("test", cli.Stdin(os.Stdin))

func Stdout

func Stdout(stdout io.Writer) Option

Stdout is an Option that sets the Stdout for a Command.

Successive calls will simply overwrite any previous calls. Without this option the command will default to os.Stdout.

// Set stdout to a temporary buffer
buf := &bytes.Buffer{}
cli.New("test", cli.Stdout(buf))

func SubCommands

func SubCommands(builders ...Builder) Option

SubCommands is an Option that attaches 1 or more subcommands to the command being configured.

Sub commands must have unique names, any duplicates will result in an error.

This option is additive and can be called as many times as desired, subcommands are effectively appended on every call.

func Version

func Version(version string) Option

Version is an Option that sets the version for a Command.

Without this option, the command defaults to a version of "dev".

cli.New("test", cli.Version("v1.2.3"))

Directories

Path Synopsis
Package arg provides mechanisms for defining and configuring command line arguments.
Package arg provides mechanisms for defining and configuring command line arguments.
examples
cancel command
Package cancel demonstrates how to handle CTRL+C and cancellation/timeouts easily with cli.
Package cancel demonstrates how to handle CTRL+C and cancellation/timeouts easily with cli.
cover command
Package cover is a simple CLI demonstrating the core features of this library.
Package cover is a simple CLI demonstrating the core features of this library.
namedargs command
Package namedargs demonstrates how to use named positional arguments in cli.
Package namedargs demonstrates how to use named positional arguments in cli.
quickstart command
Package quickstart demonstrates a quickstart example for cli.
Package quickstart demonstrates a quickstart example for cli.
subcommands command
Package subcommands demonstrates how to build a CLI with a full command structure.
Package subcommands demonstrates how to build a CLI with a full command structure.
Package flag provides mechanisms for defining and configuring command line flags.
Package flag provides mechanisms for defining and configuring command line flags.
internal
arg
Package arg provides a command line arg definition and parsing library.
Package arg provides a command line arg definition and parsing library.
constraints
Package constraints provides generic constraints for cli.
Package constraints provides generic constraints for cli.
flag
Package flag provides a command line flag definition and parsing library.
Package flag provides a command line flag definition and parsing library.
format
Package format is the inverse of parse.
Package format is the inverse of parse.
parse
Package parse provides functions to parse strings into Go types and produce detailed, consistent errors.
Package parse provides functions to parse strings into Go types and produce detailed, consistent errors.
style
Package style simply provides a uniform terminal printing style via hue for use across the library.
Package style simply provides a uniform terminal printing style via hue for use across the library.

Jump to

Keyboard shortcuts

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