cli

package module
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Jan 29, 2025 License: MIT Imports: 10 Imported by: 5

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 early development and is not yet stable

Project Description

cli is a simple, minimalist, zero-dependency 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 github.com/FollowTheProcess/cli@latest

Quickstart

package main

import (
    "fmt"
    "os"

    "github.com/FollowTheProcess/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.Allow(cli.MinArgs(1)), // Must have at least one argument
        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', 0, "Count the things"),
        cli.Run(runQuickstart(&count)),
    )
    if err != nil {
        return err
    }

    return cmd.Execute()
}

func runQuickstart(count *int) func(cmd *cli.Command, args []string) error {
    return func(cmd *cli.Command, args []string) error {
        fmt.Fprintf(cmd.Stdout(), "Hello from quickstart!, my args were: %v, count was %d\n", args, *count)
        return nil
    }
}

Will get you the following:

quickstart

[!TIP] See more examples under ./examples

Core Principles
😱 Well behaved libraries don't panic

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

  • Duplicate subcommand added
  • Command adding itself as a subcommand
  • Duplicate flag added
  • 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 tiny public interface and gives you only what you need to build amazing CLI apps, no more confusing options and hundreds of struct fields.

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.Allow(cli.MinArgs(1)),
    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", false, "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', false, "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, false, "Delete something"))

Documentation ΒΆ

Overview ΒΆ

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

Index ΒΆ

Constants ΒΆ

View Source
const NoShortHand = flag.NoShortHand

NoShortHand should be passed as the "short" argument to Flag if the desired flag should be the long hand version only e.g. --count, not -c/--count.

Variables ΒΆ

This section is empty.

Functions ΒΆ

This section is empty.

Types ΒΆ

type ArgValidator ΒΆ

type ArgValidator func(cmd *Command, args []string) error

ArgValidator is a function responsible for validating the provided positional arguments to a Command.

An ArgValidator should return an error if it thinks the arguments are not valid.

func AnyArgs ΒΆ

func AnyArgs() ArgValidator

AnyArgs is a positional argument validator that allows any arbitrary args, it never returns an error.

This is the default argument validator on a Command instantiated with cli.New.

func BetweenArgs ΒΆ

func BetweenArgs(min, max int) ArgValidator

BetweenArgs is a positional argument validator that allows between min and max arguments (inclusive), any outside that range will return an error.

func Combine ΒΆ

func Combine(validators ...ArgValidator) ArgValidator

Combine allows multiple positional argument validators to be composed together.

The first validator to fail will be the one that returns the error.

func ExactArgs ΒΆ

func ExactArgs(n int) ArgValidator

ExactArgs is a positional argument validator that allows exactly n args, any more or less will return an error.

func MaxArgs ΒΆ

func MaxArgs(n int) ArgValidator

MaxArgs is a positional argument validator that returns an error if there are more than n arguments.

func MinArgs ΒΆ

func MinArgs(n int) ArgValidator

MinArgs is a positional argument validator that requires at least n arguments.

func NoArgs ΒΆ

func NoArgs() ArgValidator

NoArgs is a positional argument validator that does not allow any arguments, it returns an error if there are any arguments.

func ValidArgs ΒΆ

func ValidArgs(valid []string) ArgValidator

ValidArgs is a positional argument validator that only allows arguments that are contained in the valid slice. If any non-valid arguments are seen, an error will be returned.

type Builder ΒΆ added in v0.3.0

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. In terms of an example, in the line git commit -m <msg>; 'commit' is the command. It can have any number of subcommands which themselves can have subcommands etc. The root command in this example is 'git'.

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) Arg ΒΆ added in v0.6.0

func (cmd *Command) Arg(name string) string

Arg looks up a named positional argument by name.

If the argument was defined with a default, and it was not provided on the command line then the value returned will be the default value.

If no named argument exists with the given name, it will return "".

func (*Command) Execute ΒΆ

func (cmd *Command) Execute() 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 FlagCount ΒΆ

type FlagCount = flag.Count

FlagCount is a type used for a flag who's job is to increment a counter, e.g. a "verbosity" flag may be passed "-vvv" which should increase the verbosity level to 3.

type Flaggable ΒΆ

type Flaggable flag.Flaggable

Flaggable is a type constraint that defines any type capable of being parsed as a command line flag.

type Option ΒΆ

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

Option is a functional option for configuring a Command.

func Allow ΒΆ

func Allow(validator ArgValidator) Option

Allow is an Option that allows for validating positional arguments to a Command.

You provide a validator function that returns an error if it encounters invalid arguments, and it will be run for you, passing in the non-flag arguments to the Command that was called.

Successive calls overwrite previous ones, use Combine to compose multiple validators.

// No positional arguments allowed
cli.New("test", cli.Allow(cli.NoArgs()))

func BuildDate ΒΆ added in v0.2.0

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 ΒΆ added in v0.2.0

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 Flaggable](p *T, name string, short rune, value T, usage string) Option

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

The variable is set when the flag is parsed during command execution. The value provided by the 'value' argument to Flag is used as the default value, which will be used if the flag value was not given via the command line.

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".

// Add a force flag
var force bool
cli.New("rm", cli.Flag(&force, "force", 'f', false, "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 ΒΆ added in v0.10.0

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 OptionalArg ΒΆ added in v0.10.0

func OptionalArg(name, description, value string) Option

OptionalArg is an Option that adds a named positional argument, with a default value, to a Command.

An optional named argument is given a name, a description, and a default value that will be shown in the help text. If the argument isn't given when the command is invoke, the default value is used in it's place.

The order of calls matters, each call to OptionalArg effectively appends an optional, named positional argument to the command so the following:

cli.New(
    "cp",
    cli.OptionalArg("src", "The file to copy", "./default-src.txt"),
    cli.OptionalArg("dest", "Where to copy to", "./default-dest.txt"),
)

results in a command that will expect the following args *in order*

cp src.txt dest.txt

If the argument should be required (e.g. no sensible default), use RequiredArg.

Arguments added to the command may be retrieved by name from within command logic with Command.Arg.

func OverrideArgs ΒΆ added in v0.6.0

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 RequiredArg ΒΆ added in v0.10.0

func RequiredArg(name, description string) Option

RequiredArg is an Option that adds a required named positional argument to a Command.

A required named argument is given a name, and a description that will be shown in the help text. Failure to provide this argument on the command line when the command is invoked will result in an error from Command.Execute.

The order of calls matters, each call to RequiredArg effectively appends a required, named positional argument to the command so the following:

cli.New(
    "cp",
    cli.RequiredArg("src", "The file to copy"),
    cli.RequiredArg("dest", "Where to copy to"),
)

results in a command that will expect the following args *in order*

cp src.txt dest.txt

If the argument should have a default value if not specified on the command line, use OptionalArg.

Arguments added to the command may be retrieved by name from within command logic with Command.Arg.

func Run ΒΆ

func Run(run func(cmd *Command, args []string) 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"))

func VersionFunc ΒΆ

func VersionFunc(fn func(cmd *Command) error) Option

VersionFunc is an Option that allows for a custom implementation of the -v/--version flag.

A Command will have a default implementation of this function that prints a default format of the version info to os.Stderr.

This option is particularly useful if you want to inject ldflags in at build time for e.g commit hash.

Directories ΒΆ

Path Synopsis
examples
internal
colour
Package colour implements basic text colouring for cli's limited needs.
Package colour implements basic text colouring for cli's limited needs.
flag
Package flag provides a command line flag definition and parsing library.
Package flag provides a command line flag definition and parsing library.
table
Package table implements a thin wrapper around text/tabwriter to keep formatting consistent across cli.
Package table implements a thin wrapper around text/tabwriter to keep formatting consistent across cli.

Jump to

Keyboard shortcuts

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