Skip to content
/ verbs Public

A reasonably simple API to build arbitrary complex CLIs. Command line parser and help text generator.

License

Notifications You must be signed in to change notification settings

revl/verbs

Repository files navigation

verbs

The POSIX-compatible command line parser verbs was built on three postulates:

  1. The order and cardinality of options as they appear on the command line matters and must be preserved.
  2. Word wrapping when displaying help text is essential for good usability and accessibility.
  3. Simplicity is key.

verbs.Parser.Parse returns a ParseResult containing options and arguments in a slice (OptsAndArgs), maintaining their exact relative command-line order and cardinality.

When option order matters

  • Opposing options cancel each other's effect: rm -f -i FILE is not the same as rm -i -f FILE; ls -t -S is not the same as ls -S -t.
  • Options override their previous values: gcc -O2 -O3 vs gcc -O3 -O2.
  • Options enable or change modes of operation: ld -Bstatic -lfoo -Bdynamic -lbar.
  • When pipeline and transformer tools process options sequentially: for example, the effect of -ss depends on whether it appears before or after -i in the command lines ffmpeg -ss 00:01:00 -i video.mp4 and ffmpeg -i video.mp4 -ss 00:01:00 final.mp4.
  • When options connect positional arguments: test "$e1" -a "$e2" -o "$e3" is not the same as test "$e1" -o "$e2" -a "$e3".

When option cardinality matters

  • Options have additive effect when specified multiple times: ssh -v -v -v.
  • Options are used to specify lists of values: grep -e pattern1 -e pattern2 and docker run -v /l1:/c1 -v /l2:/c2.

Why choose verbs?

Key Features

  • Order-preserving: Options and arguments are returned in command-line order
  • POSIX-compatible: Supports -f, --flag, -oVALUE, -o VALUE, --opt=VALUE, --opt VALUE, and grouped short options -abc.
  • Two modes: Command-driven (like git) or argument-only (like ls)
  • Built-in help: Automatic help screen generation with customizable formatting
  • Command groups: Organize commands hierarchically (e.g., git remote add)
  • Flexible API: Use any type (strings, constants, functions) to identify options and commands
  • Additional help topics: Add general help content accessible via help <topic>

Simple but Flexible API

This library allows for easy identification of command and option values in the parsed results by tagging them with the values that the caller provided in the CLI definitions.

After parsing, the tags can be used to filter and group the parsed results:

parseResult := verbs.NewParser(&verbs.CLI{
    Options: []*verbs.Option{{Name: "p|path", Param: "PATH", Tag: "paths"}},
    Args: []*verbs.Arg{{Name: "FILE", Tag: "file"}},
}).Parse(os.Args)

args := make(map[string][]string)
for _, arg := range parseResult.OptsAndArgs {
    args[arg.Tag.(string)] = append(args[arg.Tag.(string)], arg.Value)
}

fmt.Println("Paths:", args["paths"])
fmt.Println("File:", args["file"][0])

In the following example, the RESOURCE argument and FORCE option are tagged with pointers to the resource and force variables, respectively. When the parser returns the parsed results, those pointers are used to save the parsed values to the variables in one sweep.

var resource, force string

resourceArg := &verbs.Arg{Name: "RESOURCE", Tag: &resource}

parser := verbs.NewParser(&verbs.CLI{
    Commands: []*verbs.Command{
        {Name: "create", Args: []*verbs.Arg{resourceArg}},
        {Name: "delete", Args: []*verbs.Arg{resourceArg}},
    },
    Options: []*verbs.Option{{Name: "f|force", Tag: &force}},
})

parseResult := parser.Parse(os.Args)

for _, opt := range parseResult.OptsAndArgs {
    *(opt.Tag.(*string)) = opt.Value
}

switch parseResult.Command.Tag {
case "create":
    fmt.Println("Creating resource", resource)
case "delete":
    fmt.Println("Deleting resource", resource)
}
if force == "true" {
    fmt.Println("(with force option)")
}

The types of the receiver variables can be used for data validation as in the following example:

var names []string
var verbose bool
var count int

parseResult := verbs.NewParser(&verbs.CLI{
    Options: []*verbs.Option{
        {Name: "v|verbose", Tag: &verbose},
        {Name: "n|name", Param: "NAME", Tag: &names},
        {Name: "c|count", Param: "INT", Tag: &count},
    },
}).Parse(os.Args)

for _, opt := range parseResult.OptsAndArgs {
    switch opt.Tag.(type) {
    case *bool:
        *opt.Tag.(*bool) = true
    case *[]string:
        *opt.Tag.(*[]string) =
            append(*opt.Tag.(*[]string), opt.Value)
    case *int:
        count, err := strconv.Atoi(opt.Value)
        if err != nil {
            parseResult.HandleError(fmt.Errorf(
                "invalid value '%s' specified "+
                    "for integer option '%s'",
                opt.Value, opt.Token))
        }
        *opt.Tag.(*int) = count
    }
}

Because the tags can be of any type, they can also be functions:

result := verbs.NewParser(&verbs.CLI{
    Commands: []*verbs.Command{
        {
            Name: "create",
            Tag: func(optsAndArgs []*verbs.ParsedArg) {
                fmt.Println("Creating resources")
            },
        },
        {
            Name: "delete",
            Tag: func(optsAndArgs []*verbs.ParsedArg) {
                fmt.Println("Deleting resources")
            },
        },
    },
}).Parse(os.Args)

// In command-driven mode, result.Command is never nil.
(result.Command.Tag).(func([]*verbs.ParsedArg))(result.OptsAndArgs)

Documentation

Full API documentation is available at pkg.go.dev. The library also includes a number of usage examples.

About

A reasonably simple API to build arbitrary complex CLIs. Command line parser and help text generator.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages