charli

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2024 License: ISC Imports: 7 Imported by: 1

README

charli

A small, pure(-ish) CLI parser and help formatting library.

  • Reads your CLI configuration (options, flags etc.) from structs.
  • Checks input syntax and lists errors.
  • Captures valid option & positional argument values.
  • Renders usage help (to a string, if you'd like).
  • Only provides very basic validation. Bring your own!

See the code for the below screenshot.

Screenshot

Quickstart

Install

To install:

go get github.com/starriver/charli

To import:

import cli "github.com/starriver/charli"
Configuration

Declare an App with Commands to configure your CLI.

var app = cli.App{
	Commands: []cli.Command{
		get,
		put,
	},
}

var get = cli.Command{
	Name: "get",
	Headline: "Download some stuff",
	Options: []cli.Option{
		Short: 'o'
		Long: "output",
		Metavar: "PATH",
		Headline: "Download to {PATH}",
	},
	Run: func(r *cli.Result) bool {
		return len(r.Errs) == 0 // TODO
	},
}

var put = cli.Command{
	Name: "put",
	Headline: "Upload some stuff",
	Args: cli.Args{
		Count: 1,
		Metavars: []string{"FILE"},
	},
	Run: func(r *cli.Result) bool {
		return len(r.Errs) == 0 // TODO
	},
}
Implement Run functions

Command.Run(...) functions are where you do your own validations, and – if they pass – proceed to actually do that command's work.

Here's an example for the get command above, which wants to download a file to the path specified by the --output flag.

Run: func(r* cli.Result) bool {
	v := r.Options["output"].Value
	if v == "" {
		r.Error("blank path supplied")
	} else if _, err := os.Stat(v); err == nil {
		r.Error("file already exists")
	}

	if len(r.Errs) != 0 {
		return false
	}

	// TODO: actually download some stuff.

	return true
}
Parsing

Parse your args, then handle the result however you'd like:

package main

import (
	"fmt"
	"os"

	cli "github.com/starriver/charli"
)

func main() {
	r := app.Parse(os.Args)

	ok := false
	switch r.Action {
	case cli.Proceed:
		ok = r.RunCommand()
	case cli.HelpOK:
		r.PrintHelp()
		ok = true
	case cli.HelpError:
		r.PrintHelp()
	case cli.Fatal:
		// Nothing to do.
	}

	for _, err := range r.Errs {
		fmt.Fprintf(os.Stderr, "%s\n", err)
	}
	if !ok {
		os.Exit(1)
	}
}

See the examples and the docs for more.

Design

charli provides a very lightweight, 'toolkit' approach to CLI development.

If you've used urfave/cli, you might notice the config structs (App, Command etc.) are similar, but that's about it. charli is functionally pure first and foremost: its workhorse functions, App.Parse(...) and App.Help(...), have no side-effects.

Some I/O helpers are provided (eg. Result.PrintHelp()), but aside from parsing and help generation, the way your app responds to a parse Result – or a help string – is entirely up to you.

Why did we make this? Well, 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.

Goals
  • Provide only basic 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.
    • Buffer 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 our preferred help format.

License

ISC

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Action

type Action int

Suggested action after parsing.

const (
	// Proceed to Run(...) the command. Exit 0 if Run(...) returns true, nonzero
	// otherwise.
	Proceed Action = iota

	// Display help. The user explicitly requested it, so exit 0.
	HelpOK

	// Display help. The user didn't request it (or requested it wonkily) so
	// exit nonzero.
	HelpError

	// Nothing more to do. Exit nonzero.
	Fatal
)

type App

type App struct {
	// Headline of the app, displayed at the top of help output.
	//
	// Note that unlike Command.Headline, this is displayed above *everything*,
	// and in all help output (regardless of command). It also won't be
	// formatted in the same way ({} won't be highlighted).
	Headline string

	// Long description of the app, displayed below usage in help output. Should
	// both start and end with a newline \n.
	//
	// This is usually a long, multiline string. Rather the inlining this string
	// to an App literal, it's recommended to create a raw const string
	// elsewhere and use that instead, putting each backtick on its own line.
	// See the examples.
	//
	// Text inside {curly braces} will be highlighted.
	Description string

	// The app's commands, in order of display in help output.
	Commands []Command

	// Name of the command to run if none is supplied. Leave blank to require
	// one.
	DefaultCommand string

	// Highlight color in help output.
	HighlightColor color.Attribute
}

CLI application configuration.

func (*App) Help

func (app *App) Help(program string, cmd *Command) string

func (*App) Parse

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

Parse argv and populate the user-specified command's values. argv must be the full list of args, including the executable name (you probably want os.Args).

Returns a Result struct, populated with its findings.

type Args

type Args struct {
	// Number of expected positional args. If Varadic is set, this is the
	// *minimum* number of positional args.
	Count int

	// Whether to accept more args than specified in Count.
	Varadic bool

	// Sets term(s) for the args. These are displayed in the usage line at the
	// top of help. They should be uppercase. '[--]' will be prepended, and
	// names will be [bracketed] appropriately if Varadic is set.
	Metavars []string
}

Configuration for positional arguments.

type Command

type Command struct {
	// Name of the command that the user will need to specify.
	Name string

	// Summary of the command. Displayed in help, below 'Usage:'.
	//
	// Text inside {curly braces} will be highlighted.
	Headline string

	// Long description of the command, displayed below usage in help output.
	// Should both start and end with a newline \n.
	//
	// This is usually a long, multiline string. Rather the inlining this string
	// to a Command literal, it's recommended to create a raw const string
	// elsewhere and use that instead, putting each backtick on its own line.
	// See the examples.
	//
	// Text inside {curly braces} will be highlighted.
	Description string

	// This command's options, in order of display in help output.
	Options []Option

	// This command's trailing arguments. It's safe to leave this blank if you
	// don't have any.
	Args Args

	// Function to run if this Command is chosen.
	//
	// This is expected to further validate each option's values, *appending* to
	// r.Errs. If r.Errs has any members by the end of validation, it should
	// return false before proceeding with its business. At this point, it can
	// stop appending to r.Errs, but should only return true if the operation
	// succeeded overall.
	//
	// The caller should inspect r.Errs after this function returns, and exit
	// the program nonzero if it returned false.
	Run func(r *Result) bool
}

Configuration for a single CLI (sub-)command.

type Option

type Option struct {
	// Short option name, used for single-dashed args (like '-a'). Don't include
	// the hyphen. This is optional, but one of either Short of Long must be
	// specified.
	Short rune

	// Long option name, used for double-dashed args (like '--all'). Don't
	// include the hyphens. This is optional, but one of either Short of Long
	// must be specified.
	Long string

	// Set to true if this option is a flag, ie. it takes no value.
	Flag bool

	// If set, adds a simple from-a-list constraint to this option. The
	// available choices will be appended to the option's headline. Has no
	// effect on flags.
	Choices []string

	// Sets a term for this option's value - usually uppercase (like
	// '-p/--person NAME'). Optional but recommended.
	Metavar string

	// Summary of the option. Displayed in the command help, and at the top of
	// the command's help output. Text inside {curly braces} will be
	// highlighted.
	Headline string
}

Configuration for a single option, providing a value or a flag.

type OptionResult

type OptionResult struct {
	// The original Option that this OptionResult provides for.
	Option *Option

	// The option's string value. This may be blank if an empty value was
	// supplied (eg. --opt ”) - check IsSet to be sure.
	//
	// If the option is a flag, this will always be blank.
	Value string

	// Whether the option was supplied.
	IsSet bool
}

Parsing results for a single option.

type Result

type Result struct {
	// Suggested action. Note that regardless of this value, Errs may be
	// populated.
	Action Action

	// All errors encountered so far. You may append your own during your own
	// validations.
	Errs []error

	// The app you called Parse(...) on.
	App *App

	// The chosen command. May be nil if Action != Proceed.
	Command *Command

	// Map of option values.
	//
	// You can access values with either the short or long option names. Don't
	// include hyphens - eg. use "o" or "opt" rather than "-o" or "--opt".
	Options map[string]*OptionResult

	// List of positional args.
	//
	// In the case of too many args being supplied, len(Args) won't be more than
	// Command.Args.Count - that is, the extraneous args will be dropped. This
	// obviously doesn't apply if Command.Args.Varadic is set.
	Args []string
}

Parsing results. Returned by App.Parse(...).

func (*Result) Error

func (r *Result) Error(str string)

Append an error to Result.Errs.

func (*Result) Errorf

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

Append an error to Result.Errs.

func (*Result) PrintHelp

func (r *Result) PrintHelp()

Print app/command help to stderr.

func (*Result) RunCommand

func (r *Result) RunCommand() bool

Shorthand for r.Command.Run(&r).

Directories

Path Synopsis
examples
args 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