charli

package module
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2025 License: ISC Imports: 9 Imported by: 0

README

charli

Go Reference Go Report Card Fantano Rating

A small CLI toolkit. It includes a CLI parser, help formatter, and completer for bash & fish.

Screenshot

See the code for the above screenshot.

Quickstart

To install:

go get codeberg.org/starriver/charli

Who's this for?

Use charli if you want to:

  • Configure your CLI with struct data. It doesn't use the builder pattern, struct tags or reflection.
  • Have complete control over your app's I/O. Expect no magic or surprises! None of the core functions have any side-effects.
  • Bring your own input validation. The parser outputs a map of options & positional args according to your config. It aggregates errors caused by unknown args and bad syntax. Nothing else is transformed: values are strings, flags are bools.

Design

We made charli because we're very picky about how we want our CLIs to look and behave – in particular, we want to engineer complex, imperative flows for validation. The amount of hacking required on other libraries wasn't worth it for us, so we made this instead.

Comparisons
  • urfave/cli is a great library. Before using charli, try this. It's far more beginner-friendly, yet is probably this library's closest relative. Like charli, it features procedural operation and is configured via struct data – yet provides far more functionality out of the box.
  • mitchellh/cli (now archived) also provides a nice degree of control with a procedural style, but instead provides flexibility through its interface types and factories (which you won't find here).
Parser syntax
Options

charli supports both short and (GNU-style) long options.

Options normally take a string value. Flags are options without a value – ie. they have a boolean output.

Provided that -o/--option and -f/--flag are configured, all of these are valid:

program --option value
program --option=value
program -o value
program --flag
program -f

If a value starts with -, usage of the --option=value format is required to prevent ambiguity:

program --option -f    # Error!
program --option=-f    # Valid

Additionally, combined short flags are supported, but not combined short options. If -j and -k are also configured as flags:

program -fjk           # Valid
program -fjko value    # Error!
program -fjk -o value  # Valid
Positional arguments

Any number of positional arguments can be configured. Args can be mixed in with options, and -- can be used to stop parsing options.

Let's say 2 args are configured:

program a b                 # Valid: []string{"a", "b"}
program a -- -b             # Valid: []string{"a", "-b"}
program a --option value b  # Valid: []string{"a", "b"}
program --option a b        # Error! ('a' is the value for --option)
program a b c               # Error! (too many args)

Regarding the last line above, varadic args are also supported. If enabled, it would become valid.

Commands

In all of the above examples, the program has only had a single command. Instead, we can add multiple named commands, which should be supplied as the first argument.

Options can be global or command-specific. Positional arguments are always command-specific.

With pull and push commands configured, all of these are valid:

program pull
program push
program pull --option value
program push -f

A default command can also be configured, allowing the first argument to be omitted. Note that this introduces some ambiguity should the first argument not be an option (ie. not starting with -).

Requesting help

By default, the special -h/--help options can be used to request help – either for the whole program or a single command.

This flag can be supplied anywhere on the command line, but the parser will suggest that only certain forms exit with an OK status (it's up to you how to react to this suggestion, though).

program -h              # OK - display global help
program -h pull         # OK - display help for pull
program pull -h         # OK - display help for pull
program pull --flag -h  # Error - display help for pull anyway
program                 # Error - display global help anyway

help can also be optionally enabled as a pseudo-command, which will make all of these valid:

program help
program help pull
program pull help
Goals
  • Provide only necessary validation.
    • Syntax checking only.
    • No transformation for values – only strings (and bools, in the case of flags).
    • This is to provide full control over the validation process downstream.
  • Produce as many errors as possible.
    • Aggregate errors. Downstream can decide how to deal with them.
    • Don't give up after encountering one parse error. Keep going!
    • Allow downstream validations to continue even with parse errors.
    • However: make downstream validations aware of previous errors, so that expensive operations can be short-circuited.
  • Render a relatively sane help format.
    • Allow arbitrary highlighting using a set color.
    • Prefer using raw strings for long description blocks (example).
    • Make color optional. We use fatih/color, which allows turning them off (and automatically disables them when not in a tty).
    • More than anything else, we just made it look the way we wanted it to.
  • Idiomatic Go.
    • Leverage the flexibility of structs and zero values.
    • Aim for a procedural style.
    • io.Writer galore.

License

ISC

Documentation

Overview

Package charli is a small CLI toolkit.

It includes a CLI parser (App.Parse), help formatter (App.Help) and shell completions (App.Complete).

Configure your CLI with an App. Apps have one or several Command structs, each with several Option structs and Args.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateBashCompletions

func GenerateBashCompletions(w io.Writer, program, flag string)

GenerateBashCompletions writes a bash completion script to w.

program should be the program name (which will presumably be in the user's PATH). flag should be a special trigger flag, *including* hyphen prefixes, which your program should use to bypass normal execution and generate completions instead (using App.Complete).

flag can be anything you want, but don't use anything ambiguous to your CLI. If in doubt, use "--_complete".

func GenerateFishCompletions

func GenerateFishCompletions(w io.Writer, program, flag string)

GenerateFishCompletions writes a fish completion script to w.

program should be the program name (which will presumably be in the user's PATH). flag should be a special trigger flag, *including* hyphen prefixes, which your program should use to bypass normal execution and generate completions instead (using App.Complete).

flag can be anything you want, but don't use anything ambiguous to your CLI. If in doubt, use "--_complete".

Types

type Action

type Action int

Action indicates what the parser suggests should happen next.

const (
	Proceed Action = iota // proceed to call [Command.Run]
	Help                  // display help
	Fatal                 // nothing else to do; [Result.Fail] will always be true
)

type AmbiguousValueError

type AmbiguousValueError struct {
	Option    *Option // the [Option] in question
	OptionArg string  // the first argument (which triggered the [Option])
	Value     string  // the second argument (which is ambiguous)
}

AmbiguousValueError indicates that the user has supplied a value for an option that looks like another option itself (that is, the value starts with '-').

func (AmbiguousValueError) Error

func (err AmbiguousValueError) Error() string

type App

type App struct {
	// Headline is the text displayed at the top of help output,
	// above the 'Usage:' line.
	//
	// Text surrounded by {curly braces} will be highlighted.
	Headline string

	// Description is a longer description of the app,
	// which may be several paragraphs long.
	//
	// If using only a single command, this won't be displayed.
	// Instead, set [Command.Description].
	//
	// In global help output, this is displayed below the 'Usage:' line.
	// Each line will be indented by 2 spaces.
	//
	// It should start and end with a newline `\n`.
	// This is because it's designed to be written using a raw string literal,
	// with each backtick on a separate line, like this:
	//
	//  const description = `
	//  This is the first paragraph.
	//
	//  This is the second paragraph.
	//  `
	//
	// The text won't be automatically justified when printed.
	// Generally, it should be pre-justified at 78 characters
	// (to make up for the 2-space indent).
	//
	// Text surrounded by {curly braces} will be highlighted.
	Description string

	// Commands configures the app's [Command]s,
	// in the order they should be displayed in help output.
	//
	// If only one command is configured,
	// the parser won't require a command name will be supplied at all,
	// and [Command.Name] should be omitted.
	// Otherwise, all commands must have a name.
	//
	// Commands must not have duplicate names.
	Commands []Command

	// GlobalOptions configures [Option]s that apply to every command in
	// [App.Commands].
	// They are effectively prepended to every [Command.Options] slice.
	//
	// Note that unless [App.HelpAccess] explicitly removes it,
	// `-h/--help` is always available.
	// The help flags override any flag with `-h` or `--help` that you may
	// configure.
	GlobalOptions []Option

	// DefaultCommand is the name of the command to run if none is supplied.
	// If blank, the parser will require a command.
	//
	// Note that setting a default can introduce some ambiguity
	// where the first supplied argument isn't an option
	// (ie. it doesn't start with `-`).
	//
	// If only one command is configured,
	// this must be blank.
	DefaultCommand string

	// HelpAccess specifies how users can access help.
	//
	// This is a bitmask.
	// It should be [HelpFlag], [HelpCommand], or both.
	//
	// If nothing is supplied, it will default to [HelpFlag].
	HelpAccess HelpAccess

	// ErrorHandler is a callback which, if set,
	// will handle [App.Parse] errors as they happen.
	//
	// If this *isn't* set,
	// errors will be aggregated in [Result.Errs].
	ErrorHandler func(error)

	// HighlightColor is the color used for highlighting in help output.
	//
	// To disable color, don't use this.
	// Instead, set [github.com/fatih/color.NoColor] to true.
	HighlightColor color.Attribute
}

An App contains configuration for your CLI.

func (*App) Complete

func (app *App) Complete(w io.Writer, argv []string)

Complete writes shell completions to w for argv. argv should be a slice of the arguments currently on the command line, including the program name, and truncated at the cursor position. The last element of argv may be an empty string, in which case all relevant completions will be written.

Completions are line-separated. Each line contains a potential completion, followed by '\t', then a description of the completion.

Regarding shell completion functions that use this function's output:

  • The bash function should truncate each line at the '\t'.
  • The fish function doesn't need to truncate each line, as it will display the characters after the '\t' as a description for each completion.

func (*App) Help

func (app *App) Help(w io.Writer, program string, cmd *Command)

Help writes CLI help to w.

program should be the name of the program, usually the first element of os.Args.

If cmd is nil, global help will be written. Otherwise, command help will be written.

The App Headline is written first, followed by a usage line. For global help, the following are then written:

  • The App Description
  • A list of commands
  • A list of options

For command help, the following are instead written:

  • The Command Description
  • A list of options

func (*App) Parse

func (app *App) Parse(argv []string) (r Result)

Parse parses CLI arguments, returning a Result. argv should be the full list of arguments, including the executable name. (if in doubt, use os.Args.)

See the readme for a complete description of the syntax supported by Parse.

type Args

type Args struct {
	// Count is the number of required positional arguments.
	//
	// When parsing, if more positional arguments than Count are supplied,
	// those arguments are excluded from [Result.Args].
	//
	// If [Args.Varadic] is set, this becomes the *minimum* number of arguments.
	Count int

	// Varadic indicates whether to allow more positional arguments
	// than are specified in [Args.Count].
	Varadic bool

	// Metavars is a list of names for the positional arguments.
	//
	// Omitted metavars will default to `ARG`.
	//
	// They are displayed in the 'Usage:' line at the top of help output,
	// like `Usage: program ARG1 ARG2 ARG3`.
	//
	// They should be uppercase, but this is not a requirement.
	//
	// In help output, if [Args.Varadic] is true:
	//
	//   - `[--]` will be prepended.
	//   - Metavars with an index higher than [Args.Count] will be
	//     `[bracketed]`.
	//   - The last metavar will be ellipsized, like `ARG...`.
	Metavars []string
}

An Args contains configuration for positional arguments.

type CombinedEqualsError

type CombinedEqualsError struct {
	Arg string // the combined argument in question
}

CombinedEqualsError indicates that the user attempted to use '=' in a combined option.

Combined options may only contain flags, meaning that '=' can't be used to set an option's value.

func (CombinedEqualsError) Error

func (err CombinedEqualsError) Error() string

type CombinedValueError

type CombinedValueError struct {
	Option      *Option // the [Option] in question
	Arg         string  // the option's name (as used in the combined argument)
	CombinedArg string  // the combined argument it is part of
}

CombinedValueError indicates that the user attempted to use a non-flag option as part of a combined option.

Combined options may only contain flags.

func (CombinedValueError) Error

func (err CombinedValueError) Error() string

type Command

type Command struct {
	// Name is the command name.
	//
	// It should be short (ideally a single word, or kebab-case if not).
	Name string

	// Headline is a one-line summary of the command.
	//
	// In global help output,
	// it is displayed alongside the command name in the 'Commands:' listing.
	// In command help output,
	// it is also displayed below the 'Usage:' line.
	//
	// Text surrounded by {curly braces} will be highlighted.
	Headline string

	// Description is a longer description of the command,
	// which may be several paragraphs long.
	// In command help output, it is displayed below the 'Usage:' line.
	// Each line will be indented by 2 spaces.
	//
	// It should start and end with a newline `\n`.
	// This is because it's designed to be written using a raw string literal,
	// with each backtick on a separate line, like this:
	//
	//  const description = `
	//  This is the first paragraph.
	//
	//  This is the second paragraph.
	//  `
	//
	// The text won't be automatically justified when printed.
	// Generally, it should be pre-justified at 78 characters
	// (to make up for the 2-space indent).
	//
	// Text surrounded by {curly braces} will be highlighted.
	Description string

	// Options is a slice of this command's unique [Option]s,
	// in order of display in command help output.
	// If [App.GlobalOptions] is set, those options will effectively be
	// prepended to this slice.
	//
	// Note that unless [App.HelpAccess] explicitly removes it,
	// `-h/--help` is always available.
	// The help flags override any flag with `-h` or `--help` that you may
	// configure.
	Options []Option

	// Args is the configuration for this command's positional arguments.
	//
	// If left blank, no positional arguments will be allowed.
	Args Args

	// Run is the function to execute if this Command is chosen.
	//
	// Supplying this function is actually entirely optional,
	// as it's up to you whether to call it.
	// You may wish to have an entirely different way of reacting to the
	// command the user has chosen.
	//
	// If you are supplying a Run function, it should:
	//
	//   - Validate the option values in the passed [Result].
	//   - Call [Result.Error] (or its other `Error*` functions) in the case
	//     of errors).
	//   - Return before doing any meaningful work if [Result.Fail] is true.
	//
	// [Result.Fail] may additionally be set by this function for any reason.
	//
	// Responsibility for processing and displaying [Result.Errs] should
	// generally be with the caller,
	// or you may wish to set [App.ErrorHandler].
	Run func(r *Result)
}

A Command contains configuration for a single CLI command.

type DuplicateOptionError

type DuplicateOptionError struct {
	Option      *OptionResult // the [OptionResult] set in the first instance
	Arg         string        // the argument in question
	CombinedArg string        // the combined argument it is part of (if applicable)

}

DuplicateOptionError indicates that the user supplied the same option more than once.

func (DuplicateOptionError) Error

func (err DuplicateOptionError) Error() string

type HelpAccess

type HelpAccess uint8

HelpAccess indicates how help output should be accessed by the CLI user. This is a bitmask.

const (
	HelpFlag    HelpAccess = 1 << iota // access via the `-h/--help` flags
	HelpCommand                        // access via a `help` pseudo-command
)

type InvalidChoiceError

type InvalidChoiceError struct {
	Option    *Option // the [Option] in question
	JoinedArg string  // the argument(s) in question, which may be concatenated
	Value     string  // the invalid value
}

InvalidChoiceError indicates that the user has supplied an invalid choice as the value for an option which has Choices set.

func (InvalidChoiceError) Error

func (err InvalidChoiceError) Error() string

type InvalidCommandError

type InvalidCommandError struct {
	Program     string     // the name of the program
	Name        string     // the name of the invalid command
	SuggestHelp HelpAccess // how to suggest CLI help is accessed
}

InvalidCommandError indicates the user has selected a command that doesn't exist.

func (InvalidCommandError) Error

func (err InvalidCommandError) Error() string

type InvalidOptionError

type InvalidOptionError struct {
	Arg         string // the invalid option's argument
	CombinedArg string // the combined argument it is part of (if applicable)
}

InvalidOptionError indicates that the user supplied an option which doesn't exist.

func (InvalidOptionError) Error

func (err InvalidOptionError) Error() string

type MissingArgsError

type MissingArgsError struct {
	Metavars []string // the metavars for the missing arguments
}

MissingArgsError indicates that the user didn't supply enough positional arguments, as specified by the Args Count.

func (MissingArgsError) Error

func (err MissingArgsError) Error() string

type MissingCommandError

type MissingCommandError struct {
	Program    string     // the name of the program
	HelpAccess HelpAccess // how to suggest CLI help is accessed
}

MissingCommandError indicates that the CLI requires the user to supply a command, yet they didn't.

This error only occurs when multiple Command structs are configured and DefaultCommand is blank.

func (MissingCommandError) Error

func (err MissingCommandError) Error() string

type MissingValueError

type MissingValueError struct {
	Option  *Option // the [Option] in question
	Arg     string  // the argument that triggered the option
	Metavar string  // the option's metavar
}

MissingValueError indicates that the user omitted the value for an option from the end of the command line.

func (MissingValueError) Error

func (err MissingValueError) Error() string

type Option

type Option struct {
	// Short is the short option name. This and/or [Option.Long] must be set.
	//
	// Short options have a single hyphen followed by a character (like `-a`).
	// Omit the hyphen, as this is a rune.
	//
	// `-` isn't a valid value for this,
	// because `--` is a special argument used to disable option parsing.
	Short rune

	// Long is the long option name. This and/or [Option.Short] must be set.
	//
	// Long options have a double hyphen followed by a string (like `--option`).
	// Don't include the hyphens.
	Long string

	// Flag indicates whether this option should take no value.
	// Flags are effectively boolean.
	//
	// If true, the option will be listed without [Option.Metavar]
	// in help output (like `--option` rather than `--option ARG`).
	//
	// After parsing, in this option's [OptionResult],
	// [OptionResult.IsSet] can be used to check whether the flag was supplied.
	Flag bool

	// Choices constrains this option's values to a list.
	//
	// Available choices will be appended to the option's headline in help
	// output.
	// This should only be used for simple, short sets of strings.
	//
	// This is invalid if set on flags.
	Choices []string

	// Metavar is the term for this option's value. It is shown in help output
	// after the option name(s), like `VALUE` in `-o/--option VALUE`.
	//
	// If omitted, it will default to `ARG`.
	//
	// It should be uppercase, but this is not a requirement.
	//
	// It is invalid if set with [Option.Flag].
	Metavar string

	// Headline is a one-line summary of the option,
	// shown in the 'Options:' section of help output.
	//
	// Text surrounded by {curly braces} will be highlighted.
	Headline string
}

An Option contains configuration for a single CLI option.

type OptionResult

type OptionResult struct {
	// Option is the [Option] that this refers to.
	Option *Option

	// Value is the option's string value.
	// This may be blank if an empty value was supplied (like `--opt ”`) -
	// [OptionResult.IsSet] is preferable when checking whether an option
	// was supplied at all.
	//
	// If the option is a flag, this will always be blank.
	Value string

	// IsSet indicates whether the option was supplied.
	IsSet bool
}

An OptionResult contains parsing results for a single option.

type Result

type Result struct {
	// Action indicates what the parser suggests should happen next.
	// See [Action] for details.
	Action Action

	// Errs is a slice of all errors encountered during parsing.
	// You may choose to append errors from your own validations
	// using the [Result.Error] functions.
	//
	// Note that if [App.ErrorHandler] is set, this slice will not be appended
	// to automatically.
	// [App.ErrorHandler] may re-implement this behavior, if desired.
	Errs []error

	// Fail is true if any error has occurred during parsing.
	// If you are using the [Result.Error] functions in your own validations,
	// any call to those functions will set this to true.
	//
	// If true, it is suggested that the program exits with a failure code.
	//
	// Note also that it is valid for Fail to be true when [Result.Errs] is
	// empty - for example, when [Result.Action] is [Help], but extraneous
	// arguments were supplied.
	Fail bool

	// App is the [App] that [App.Parse] was called on.
	App *App

	// Command is the [Command] chosen by the user.
	// This may be nil when [Result.Action] != [Proceed].
	Command *Command

	// Options is a map of [Option] names to [OptionResult]s.
	//
	// Both [Option.Short] and [Option.Long] will be set as keys for the
	// [OptionResult] for a given [Option].
	// Don't prepend the hyphens in either case (ie. use `opt` as a key,
	// rather than `--opt`).
	Options map[string]*OptionResult

	// Args is a slice of the positional arguments.
	//
	// In the case of too many arguments being supplied,
	// `len(Args)` won't be more than [Args.Count] for the given [Command].
	// In other words, the extraneous args will be dropped.
	Args []string
}

A Result is a collection of results returned by App.Parse.

func (*Result) Error

func (r *Result) Error(err error)

Error reports a pre-made error and sets Fail to true. This is called by App.Parse, and you can use it in your own validations. Unless ErrorHandler is set, the error will be appended to Errs. Otherwise, the handler will be called.

func (*Result) ErrorString

func (r *Result) ErrorString(str string)

ErrorString reports an error described by str and sets Fail to true. This is called by App.Parse, and you can use it in your own validations. Unless ErrorHandler is set, the error will be appended to Errs. Otherwise, the handler will be called.

func (*Result) Errorf

func (r *Result) Errorf(format string, a ...any)

Errorf reports an error using the specified format and sets Fail to true. This is called by App.Parse, and you can use it in your own validations. Unless ErrorHandler is set, the error will be appended to Errs. Otherwise, the handler will be called.

func (*Result) PrintHelp

func (r *Result) PrintHelp()

PrintHelp writes global or command help to stderr, depending on whether the user selected a valid command.

func (*Result) RunCommand

func (r *Result) RunCommand()

RunCommand calls [Command.Run] for the command the user chose. This is shorthand for:

r.Command.Run(&r)

type TooManyArgsError

type TooManyArgsError struct {
	Args []string // the extraneous arguments
}

TooManyArgsError indicates that the user supplied more positional arguments than were allowed by the Args Count.

This error only occurs when Varadic is false.

func (TooManyArgsError) Error

func (err TooManyArgsError) Error() string

Directories

Path Synopsis
examples
args command
completions command
options command
readme command
single-command command

Jump to

Keyboard shortcuts

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