cli

package module
v0.2.1 Latest Latest
Warning

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

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

README

cli

GoDoc CI

A Go package for building CLI applications. Extends the standard library's flag package to support flags anywhere in command arguments.

Features

The bare minimum to build a CLI application while leveraging the standard library's flag package.

  • Nested subcommands for organizing complex CLIs
  • Flexible flag parsing, allowing flags anywhere
  • Subcommands inherit flags from parent commands
  • Type-safe flag access
  • Automatic generation of help text and usage information
  • Suggestions for misspelled or incomplete commands
But why?

This package is intentionally minimal. It aims to be a building block for CLI applications that want to leverage the standard library's flag package while providing a bit more structure and flexibility.

  • Build maintainable command-line tools quickly
  • Focus on application logic rather than framework complexity
  • Extend functionality only when needed

Sometimes less is more. While other frameworks offer extensive features, this package focuses on core functionality.

Installation

go get github.com/mfridman/cli@latest

Required go version: 1.21 or higher

Quick Start

Here's a simple example of a CLI application that echoes back the input with a required -c flag to capitalize the output:

root := &cli.Command{
	Name:      "echo",
	Usage:     "echo [flags] <text>...",
	ShortHelp: "echo is a simple command that prints the provided text",
	Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
		// Add a flag to capitalize the input
		f.Bool("c", false, "capitalize the input")
	}),
	FlagsMetadata: []cli.FlagMetadata{
		{Name: "c", Required: true},
	},
	Exec: func(ctx context.Context, s *cli.State) error {
		if len(s.Args) == 0 {
			return errors.New("must provide text to echo, see --help")
		}
		output := strings.Join(s.Args, " ")
		// If -c flag is set, capitalize the output
		if cli.GetFlag[bool](s, "c") {
			output = strings.ToUpper(output)
		}
		fmt.Fprintln(s.Stdout, output)
		return nil
	},
}
if err := cli.Parse(root, os.Args[1:]); err != nil {
	if errors.Is(err, flag.ErrHelp) {
		fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root))
		return
	}
	fmt.Fprintf(os.Stderr, "error: %v\n", err)
	os.Exit(1)
}
if err := cli.Run(context.Background(), root, nil); err != nil {
	fmt.Fprintf(os.Stderr, "error: %v\n", err)
	os.Exit(1)
}

Command Structure

Each command is represented by a Command struct:

type Command struct {
	Name          string // Required
	Usage         string
	ShortHelp     string
	UsageFunc     func(*Command) string
	Flags         *flag.FlagSet
	FlagsMetadata []FlagMetadata
	SubCommands   []*Command
	Exec          func(ctx context.Context, s *State) error
}

The Name field is the command's name and is required.

The Usage and ShortHelp fields are used to generate help text. Nice-to-have but not required.

The Flags field is a *flag.FlagSet that defines the command's flags.

[!TIP]

There's a convenience function FlagsFunc that allows you to define flags inline:

root := &cli.Command{
	Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
		fs.Bool("verbose", false, "enable verbose output")
		fs.String("output", "", "output file")
		fs.Int("count", 0, "number of items")
	}),
	FlagsMetadata: []cli.FlagMetadata{
		{Name: "c", Required: true},
	},
}

The optional FlagsMetadata field is a way to extend defined flags. The flag package alone is a bit limiting, so we add this to provide the most common features, such as handling of required flags.

The SubCommands field is a list of *Command structs that represent subcommands. This allows you to organize CLI applications into a hierarchy of commands. Each subcommand can have its own flags and business logic.

The Exec field is a function that is called when the command is executed. This is where you put business logic.

Flag Access

Flags can be accessed using the type-safe GetFlag function, called inside the Exec function:

// Access boolean flag
verbose := cli.GetFlag[bool](state, "verbose")
// Access string flag
output := cli.GetFlag[string](state, "output")
// Access integer flag
count := cli.GetFlag[int](state, "count")
State Inheritance

Child commands automatically inherit their parent command's flags:

// Parent command with a verbose flag
root := cli.Command{
	Name: "root",
	Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
		f.Bool("verbose", false, "enable verbose mode")
	}),
}

// Child command that can access parent's verbose flag
sub := cli.Command{
	Name: "sub",
	Exec: func(ctx context.Context, s *cli.State) error {
		verbose := cli.GetFlag[bool](s, "verbose")
		if verbose {
			fmt.Println("Verbose mode enabled")
		}
		return nil
	},
}

Help System

Help text is automatically generated, but you can customize it by setting the UsageFunc field.

There is a DefaultUsage function that generates a default help text for a command, which is useful to display when flag.ErrHelp is returned from Parse:

if err := cli.Parse(root, os.Args[1:]); err != nil {
	if errors.Is(err, flag.ErrHelp) {
		fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root)) // Display help text and exit
		return
	}
	fmt.Fprintf(os.Stderr, "error: %v\n", err)
	os.Exit(1)
}

Usage Syntax Conventions

When reading command usage strings, the following syntax is used:

Syntax Description
<required> Required argument
[optional] Optional argument
<arg>... One or more arguments
[arg]... Zero or more arguments
(a|b) Must choose one of a or b
[-f <file>] Flag with value (optional)
-f <file> Flag with value (required)

Examples:

# Multiple source files, one destination
mv <source>... <dest>

# Required flag with value, optional config
build -t <tag> [config]...

# Subcommands with own flags
docker (run|build) [--file <dockerfile>] <image>

# Multiple flag values
find [--exclude <pattern>]... <path>

# Choice between options, required path
chmod (u+x|a+r) <file>...

# Flag groups with value
kubectl [-n <namespace>] (get|delete) (pod|service) <name>

Status

This project is in active development and undergoing changes as the API gets refined. Please open an issue if you encounter any problems or have suggestions for improvement.

Acknowledgements

There are many great CLI libraries out there, but I always felt they were too heavy for my needs.

I was inspired by Peter Bourgon's ff library, specifically the v3 branch, which was soooo close to what I wanted. But the v4 branch took a different direction and I wanted to keep the simplicity of v3. This library aims to pick up where v3 left off.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package cli provides a lightweight library for building command-line applications using Go's standard library flag package. It extends flag functionality to support flags anywhere in command arguments.

Key features:

  • Nested subcommands for organizing complex CLIs
  • Flexible flag parsing, allowing flags anywhere in arguments
  • Parent-to-child flag inheritance
  • Type-safe flag access
  • Automatic help text generation
  • Command suggestions for misspelled inputs

Quick example:

root := &cli.Command{
    Name:      "echo",
    Usage:     "echo [flags] <text>...",
    ShortHelp: "prints the provided text",
    Flags: cli.FlagsFunc(func(f *flag.FlagSet) {
        f.Bool("c", false, "capitalize the input")
    }),
    Exec: func(ctx context.Context, s *cli.State) error {
        output := strings.Join(s.Args, " ")
        if cli.GetFlag[bool](s, "c") {
            output = strings.ToUpper(output)
        }
        fmt.Fprintln(s.Stdout, output)
        return nil
    },
}

The package intentionally maintains a minimal API surface to serve as a building block for CLI applications while leveraging the standard library's flag package. This approach enables developers to build maintainable command-line tools quickly while focusing on application logic rather than framework complexity.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultUsage

func DefaultUsage(root *Command) string

DefaultUsage returns the default usage string for the command hierarchy. It is used when the command does not provide a custom usage function. The usage string includes the command's short help, usage pattern, available subcommands, and flags.

func FlagsFunc

func FlagsFunc(fn func(f *flag.FlagSet)) *flag.FlagSet

FlagsFunc is a helper function that creates a new flag.FlagSet and applies the given function to it. Intended for use in command definitions to simplify flag setup. Example usage:

cmd.Flags = cli.FlagsFunc(func(f *flag.FlagSet) {
    f.Bool("verbose", false, "enable verbose output")
    f.String("output", "", "output file")
    f.Int("count", 0, "number of items")
})

func GetFlag

func GetFlag[T any](s *State, name string) T

GetFlag retrieves a flag value by name from the command hierarchy. It first checks the current command's flags, then walks up through parent commands.

If the flag doesn't exist or if the type doesn't match the requested type T an error will be raised in the Run function. This is an internal error and should never happen in normal usage. This ensures flag-related programming errors are caught early during development.

verbose := GetFlag[bool](state, "verbose")
count := GetFlag[int](state, "count")
path := GetFlag[string](state, "path")

func Parse

func Parse(root *Command, args []string) error

Parse traverses the command hierarchy and parses arguments. It returns an error if parsing fails at any point.

This function is the main entry point for parsing command-line arguments and should be called with the root command and the arguments to parse, typically os.Args[1:]. Once parsing is complete, the root command is ready to be executed with the Run function.

func Run

func Run(ctx context.Context, root *Command, options *RunOptions) error

Run executes the current command. It returns an error if the command has not been parsed or if the command has no execution function.

The options parameter may be nil, in which case default values are used. See RunOptions for more details.

Types

type Command

type Command struct {
	// Name is always a single word representing the command's name. It is used to identify the
	// command in the command hierarchy and in help text.
	Name string

	// Usage provides the command's full usage pattern.
	//
	// Example: "cli todo list [flags]"
	Usage string

	// ShortHelp is a brief description of the command's purpose. It is displayed in the help text
	// when the command is shown.
	ShortHelp string

	// UsageFunc is an optional function that can be used to generate a custom usage string for the
	// command. It receives the current command and should return a string with the full usage
	// pattern.
	UsageFunc func(*Command) string

	// Flags holds the command-specific flag definitions. Each command maintains its own flag set
	// for parsing arguments.
	Flags *flag.FlagSet
	// FlagsMetadata is an optional list of flag information to extend the FlagSet with additional
	// metadata. This is useful for tracking required flags.
	FlagsMetadata []FlagMetadata

	// SubCommands is a list of nested commands that exist under this command.
	SubCommands []*Command

	// Exec defines the command's execution logic. It receives the current application [State] and
	// returns an error if execution fails. This function is called when [Run] is invoked on the
	// command.
	Exec func(ctx context.Context, s *State) error
	// contains filtered or unexported fields
}

Command represents a CLI command or subcommand within the application's command hierarchy.

func (*Command) Path

func (c *Command) Path() []*Command

Path returns the command chain from root to current command. It can only be called after the root command has been parsed and the command hierarchy has been established.

type FlagMetadata

type FlagMetadata struct {
	// Name is the flag's name. Must match the flag name in the flag set.
	Name string

	// Required indicates whether the flag is required.
	Required bool
}

FlagMetadata holds additional metadata for a flag, such as whether it is required.

type RunOptions

type RunOptions struct {
	// Stdin, Stdout, and Stderr are the standard input, output, and error streams for the command.
	// If any of these are nil, the command will use the default streams ([os.Stdin], [os.Stdout],
	// and [os.Stderr], respectively).
	Stdin          io.Reader
	Stdout, Stderr io.Writer
}

RunOptions specifies options for running a command.

type State

type State struct {
	// Args contains the remaining arguments after flag parsing.
	Args []string

	// Standard I/O streams.
	Stdin          io.Reader
	Stdout, Stderr io.Writer
	// contains filtered or unexported fields
}

State holds command information during Exec function execution, allowing child commands to access parent flags. Use GetFlag to get flag values across the command hierarchy.

Directories

Path Synopsis
examples
pkg

Jump to

Keyboard shortcuts

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