~ser/claptrap

Aflags library for Go: very small, much features, getoptish
~ser/clapenv

New hg repository added

2 years ago

#3 Add a non-verifying Parse() and expose Verify()

~ser filed ticket on claptrap todo

2 years ago

#claptrap

logo

Claptrap is a small but feature-rich Go flags library. Claptrap features: getopt-ish flags; sub-commands; long and short flags; --flag=<value> syntax; invertable boolean flags; short-flag combination; variadic arguments; typed arguments/flags (int, bool, string, float, Duration); global flags; mandatory arguments; positional arguments; Levenshtein(ish) command matching; Usage text; and no external dependencies. Claptrap is a single, stand-alone Go source file.

Build statusAPI DocumentationGo Report Card

The project home is here. File bugs here. Send patches, or any other comments or questions, to ~ser/claptrap@lists.sr.ht. For help, join #Claptrap:matrix.org with a (Matrix client).

Why? You'd consider this if you want POSIX getopt flags in a small (ca 500 LOC, among the smallest) and yet feature-rich library.

There are a growing number of external, optional expansions for Claptrap:

  • claphelp adds color to usage and error text
  • Claphelp also provides a command-line tool that will generate manpages from your program.
  • clapconf provides TOML config file read/write syncing to a claptrap.CommandConfig with 1 line of code.
  • clapenv adds environment variable support to claptrap

Note: Claptrap is unrelated to the args library go-clap. The first public release of Claptrap was March 29, 2020; go-clap's first commit was Apr 27, 2023. go-clap has a significantly different design, and I believe the naming is sheer coincidence. The two projects share a couple of features: like Claptrap, go-clap also has no dependencies; and go-clap also prefers double-dash long format arguments. go-clap is of the style that parses args into a struct, so if that design is your cup of tea, check it out!

#Installation

claptrap is a library, and is added to your Go modules like any other:

$ go get ser1.net/claptrap/v4@latest

#Usage

module ser1.net/claptest
require ser1.net/claptrap/v4 v4.1.7
go 1.16
package main

import (
  "fmt"
  "os"
  "ser1.net/claptrap/v4"
  "time"
)

func main() {

  reg := claptrap.Command("claptest", "This is the claptest")

  // Flags start with a -- for long flags, and a - for short flags. The long name must preceed the short name.
  // Boolean flags may default to true or false.
  reg.Add("--fuzzy", "-f", true, "fuzzy match flag") // A flag with a default true value
  // If no default value is provided, the argument has a boolean false default. 
  reg.Add("-v", "verbosity flag")
  // Either the long or short name may be omitted.
  reg.Add("--name", "Mr. Biggles", "your name")
  // Mandatory arguments start with a `!`. Commands may not be mandatory.
  // If the default value is an array, only the values in the array are legal values.
  reg.Add("!--count", "-c", []int{1, 2, 3}, "count flag")
  // Variadic arguments end in `...`. Variadic flags may appear anywhere, any number of times (minimum 1 if they are also mandatory).
  reg.Add("--prob...", "-p", 0.5, "probability flag")
  // time.Durations are parsed by the `time` package rules: "10s", "20m", "1h"
  reg.Add("--delay...", []time.Duration{10 * time.Second, 20 * time.Minute, time.Hour}, "duration flag")
  // Named, positional arguments consume any non-flag values. They may also be mandatory or variadic (or both), have types, and choices.
  reg.Add("commestibles...", []string{"carrots", "cabbage", "ceyanne"}, "food to eat")
  // Sub-commands are added with `AddCommand`, and may have flags, arguments, and sub-sub-commands.
  // Ad-nauseum. You're free to make as hellish a UI as you like
  reg.AddCommand("make-man", "make a manpage")
  // Any arguments added to the root command are global, and may be combined with sub-commands -- *before* the subcommand appears
  // in the arguments.

  info := reg.AddCommand("info", "the info command about information")
  info.Add("first", "the first argument")                                    // default: false
  info.Add("second", 99, "the second argument")                              // an int argument
  info.Add("third...", []string{"a", "b", "c", "d"}, "the third argument")   // any combination of these values may appear
  info.Add("--verbose", "-v", 0, "info verbosity flag")                      // this does not conflict with the root "-v" flag -- different type
  info.Add("--fuzzy", "-f", true, "fuzzilischiousity!")                      // A completely different "fuzzy", defaulting to true

  subinfo := info.AddCommand("subcommand", "a subcommand for the info command")   // Sub-commands may be nested indefinitely, guaranteeing chaos
  subinfo.Add("--verbose", "-V", []string{"quiet", "loud"}, "how verbose the subcommand is") // yet another verbosity with a different short flag and values

  help := reg.AddCommand("help", "print the help")
  help.Add("subcommand...", []string{"help", "info"}, "Get help about a subcommand")

  reg.Parse(os.Args[1:]) // exclude the command when parsing, or pass nil

  // Looping like this is probably only good if the commands are unique
  for c := reg; c != nil; c = c.Command {
    switch c.Name {
    case "make-man":
      claptrap.HelpTemplate = claptrap.ManTempl
      claptrap.Usage()
      os.Exit(0)
    case "claptest":
      fmt.Printf("%s %v\n", "commestibles", c.Strings("commestibles"))
      fmt.Printf("%s %t\n", "verbose", c.Bool("verbose"))
      fmt.Printf("%s %t\n", "fuzzy", c.Bool("fuzzy"))
      fmt.Printf("%s %+v\n", "count", c.Ints("count"))
      fmt.Printf("%s %+v\n", "prob", c.Floats("prob"))
      fmt.Printf("%s %+v\n", "delay", c.Durations("delay"))
    case "info":
      fmt.Printf("info: %s %t\n", "first", c.Bool("first"))
      fmt.Printf("info: %s %+v\n", "second", c.Int("second"))
      fmt.Printf("info: %s %+v\n", "third", c.Strings("third"))
      fmt.Printf("info: %s %d\n", "verbose", c.Int("verbose"))
      fmt.Printf("info: %s %t\n", "fuzzy", c.Bool("fuzzy"))
    case "subcommand":
      fmt.Printf("Heeeyaw, subcommand, with verbose %q\n", c.String("verbose"))
    case "help":
	    claptrap.Usage()
	    os.Exit(0)
    }
  }
}

Flags and arguments are added with Add(). The format of the command is:

Add( <name>           // string, mandatory argument.
        <shortname>,     // string, optional.
        <default value>, // optional. If not provided, defaults to boolean false
        <description>    // textual description of command, mandatory
      )

The format of <name> determines characteristics of the argument. If <name>

  • starts with !, then the argument is mandatory. The parser will error out if the argument is missing,
  • ends with ..., then the argument is variadic, and may occur many times,
  • starts with -- (after !), then the argument is a long flag,
  • starts with - (after !), then the argument is a short flag (single character)
  • has no dashes, then it is a positional argument

The default value, if provided, may be a string, int, float64, bool, or time.Duration. The default type of arguments are strings, and the default types of flags are bools; this is validated by the Parse() function. The default value may also be an array of any of these types, in which case the argument is a choice -- users may provide any of the arguments in the array, and no others. The default value for choices is the first choice.

#Rules

There are commands, flags, and args. A parse scope is created with claptrap.Command().

#Commands

Commands are specified without dashes, and are like flag sets: each command can have its own set of flags and args. The top-level command is registered with the claptrap.Command() function; subcommands with the CommandConfig.AddCommand() method. Commands usually have a name, but there is a special root command specified by the empty string; args and flags created on this root command don't require a command.

The parser will match abbreviated strings to commands, so "lis", "ls", and "l" will match "list", as long as it is a unique match. If both "less" and "list" are registered, the user could supply "lss", "le", or "lt" for example; "ls" is ambiguous and would be a parse error.

#Args

Args are placeholders for non-flagged arguments, and are created with CommandConfig.Add(); they consume variables in the order they're created. The very last Arg created (on a command) can be variadic, which means it consumes all of the remaining arguments.

Arg names are just that: references to use in your program. They do not appear in the command line. If a varargs argument is not provided, then input is restricted to no more than the number of positional arguments.

See the example code for more info.

If a varargs argument appears (an argument with "..." at the end of the name), then it gets any other input; note there can be at most one vararg argument, while there can be many vararg flags.

Arguments are optional by default, but can be made mandatory by prefixing the name with !.

Note that this has the effect of also making any previous arguments also mandatory, because claptrap fills arguments in order.

claptrap supports strong typing; types are derived from the default value.

Choices are also supported. Choices limit the allowed arguments, and are set by passing an array of the choices to default value parameter of Add(). Choices with all types except bools: booleans are always only ever true or false, and boolean arguments can not be variadic. With the exception of booleans, all of these features can be combined.

See the documentation examples for a complete list of the rules in action.

Values are retrieved using one of the type getters. If the wrong type is retrieved, an empty array is provided. If the argument was not provided by the user, the default is provided. An argument can be tested whether it is a default, or was provided by the user, with the Arg.IsSet() function. Getters are available for both single values and vararg arrays. The convenience functions get the first value of any argument; if the argument type is different than what is asked for, then the zero-value for that type is returned.

Durations can be any string parsable by Go time.ParseDuration().

#Flags

Flags are prefixed with dashes. Flags can have long and short forms; like getopt, long forms are specified with a double-dash -- and short forms with a single dash -. With a couple of exceptions, Flags follow all the rules and features of arguments: mandatory, variadic, defaults, choices, and types.

  • All flags may be variadic, not only the last.
  • Mandatory and variadic syntax is specified on the long form (the first argument to Add()).
  • Boolean flags take no arguments. If they're supplied, they're true; if they are not supplied, they get the default.
  • Boolean long Flags automatically get a no- version that returns false if supplied.
package main
import "ser1.net/claptrap/v4"
import "fmt"
func main() {
  rt := claptrap.Command("flags", "demonstrating arguments (vs args)")
	rt.Add("!--required", "-r", "this is a very silly flag")
	rt.Add("--choice", []string{"connor", "kurgen"}, "there can be only one")
	rt.Add("--poke...", "", "there can be many")
	rt.Add("--semi...", "-s", "", "there can be many more")
	rt.Add("--true", "-t", true, "boolean defaults only matter if they're not provided")
	sc := rt.AddCommand("sub", "this is a sub-command")
	sc.Add("--choice", []string{"go", "rust", "c"}, "ha ha! No way this could be confusing!")
	rt.Parse(nil) // If you pass nil to Parse, os.Args[1:] are used
	for _, k := range []string{"choice", "poke", "semi", "true"} {
	  fmt.Printf("%s: %v\n", k, rt.Args[k].Value)
	}
	if rt.Command != nil && rt.Command.Name == "sub" {
		fmt.Printf("sub choice: %s\n", rt.Command.String("choice"))
	}
}

Important


Root arguments and flags are global: they can be provided on the command line before other arguments. However, if the root command has any arguments, they will consume commands before the commands are identified.

If args and flags are created with the same name, the one that's added last is the one that wins.

#Shell completion

There is an autocompletion function in assets/clapcomplete.sh. Currently, as my autocomplete-fu is rather basic, it only completes the first word. It depends on jq, as well. To use it:

$ source assets/clapcomplete.sh
$ complete -F _clapcomplete_ <yourprogram>

I will happily accept patches to improve the autocompletion.

#Best practices

UIs that have subcommands should limit themselves to using Flags and not Args in the parent command. It'll work, but it's easy to get unexpected results. For example, what would you expect to happen here?

root := Command("","")
	root.Add("arg1", []string{"info", "man"}, "first argument")
	info := root.AddCommand("info", "more information")

What should happen? I can tell you what will happen, but it's confusing for everyone and could change in the future, so it's best avoided. It gets worse with command matching; consider:

root := Command("","")
	root.Add("arg1", "first argument")
	info := root.AddCommand("category", "the category of the thing")
	root.Parse([]string{"cat"})

There isn't much logic trying to make this work a particular way, because no matter what, it's going to be unexpected for somebody. It is far more clear to simply avoid mixing Arguments and Commands, except in commands which do not have subcommands.

Args and flags should not have the same names; this is merely because of an implementation decision to simplify the code, rather than adding extra code to separate things out.

For shorthand commands ("ls" for "list"), the first letter must match, and the match must be unique. For example:

  • list, less -- ls bad, it matches both
  • config, configs -- config good, it matches both, but one is an exact match
  • list, config, help -- l, c, h good, completely unambiguous

But -- again -- if you have arguments defined claptrap will choose commands over arguments. Keep this in mind when creating your UI.

#Convenience Rules

The convenience functions return a single argument of the requested type; what gets returned is based on these rules:

  1. If an argument(/flag) is not found found; or if the found item is not the correct type; or the default is a choice; then the default value of the type requested is returned.
  2. If the value is variadic, the first element of the user values is returned. It's best to not use the convenience functions with variadic args, because you throw away user input.

If you need to inspect the argument, acces them by name with CommandConfig.Args[]; with the result, you can check Arg.IsSet() to see if the user provided a value, or if you're getting a default (among other things, but this is probably the most useful).

#Help Text

claptrap generates help text output; it looks like this:

USAGE: claptest [args] [commands]
 This is the claptest
 Commands:
   help                              print the help
   info                              the info command about information
   make-man                          make a manpage

 Arguments:
   commestibles     <string>...      food to eat  ([carrots cabbage ceyanne])

 Flags:
   --count / -c     <int>            count flag  (![1 2 3])
   --delay / -      <Duration>...    duration flag  ([10s 20m0s 1h0m0s])
   --fuzzy / -f                      fuzzy match flag  (true)
   --name / -       <string>         your name  (Mr. Biggles)
   --prob / -p      <float64>...     probability flag  (0.5)
   -- / -v                           verbosity flag  (false)

help [args]
 print the help
 Commands:

 Arguments:
   subcommand       <string>...      Get help about a subcommand  ([help info])

 Flags:

You can replace the Usage function with your own. The default usage function uses templates, so you can also write your own templates and use the default function. Again, see the API documentation example.

claptrap responds to a super-secret environment variable, CLAPTRAP_USAGE_JSON. If set (to any value), claptrap's Usage function will dump the configuration as a JSON document. This document can be fed to the other tools (such as claphelp) to, e.g., generate man pages.

#Credits

This library was based initially on clapper; it adds several features missing from clapper, but the main reason for the fork was that the several behaviors were changed in ways that differ fundamentally with how clapper treats arguments.

#Features

Briefly:

  • Positional (named) arguments: myprog server.com server.net could be parsed into from and to args
  • Subcommands: myprog tcp connect myhost.net, myprog tcp list, myprog tcp scan bing.com
  • getopt-like long and short flag names, and = syntax: --count 5, -c 5, --count=5
  • Automatic boolean flag inversion: --clean gives you also --no-clean
  • Short-flag combination: -c -v -p == -cvp
  • Variadic arguments: -a 1 -a 2 -a 3
  • Typed arguments/flags, supporting int, bool, string, float, & Duration
  • Global flags: myprog -v tcp list -a, -v would be global, -a is a flag on the list command
  • Mandatory arguments: flags and arguments can be made mandatory
  • Command matching: list will be matched by ls and lt, if they the match is unique
  • Easy Usage templating and overriding
  • No external dependencies
  • Ca 500 LOC in a single, stand-alone claptrap.go file. You could vendor it by copying the file into your project, change the package name, and go, change the package name, and go.

Tools built with the Claptrap have an opts interface that is familiar to largest population of command-line users in the world: POSIX getop. Claptrap provides a fair amount of user input validation: variadics, mandatory, and argument types.

Claptrap strives to remain light, while providing these features. The most popular Go flags libraries are large, either themselves or through their dependencies; Claptrap has 0 non-stdlib dependencies.

lib short long combined inverted env vars types choices commands varargs mand/opt files usage
claptrap Y Y Y Y N Y Y Y Y Y N Y
go-clap Y Y Y N N Y ? N Y Y N Y
clapper Y Y N Y N N N Y Y N N
droundy Y Y Y N Y Y N Y
sircmpwn Y N Y N N N N N Y Y N Y
opflag Y Y Y N N Y N N Y N N Y
namsral Y Y Y Y Y Y Y
integrii Y Y y Y Y Y Y
jessevdk Y Y Y N N Y Y Y Y Y N Y
  • short means getopt short-form syntax, e.g. "-x"
  • long means getopt long-form syntax, e.g. "--long"
  • combined means that arguments may have both long and short forms
  • inverted means automatic support for inverted boolean arguments, e.g. "--true" gives you "--no-true"
  • env vars is support for parsing environment variables into parameters, e.g. "--var" gets the value of "$PROG_VAR" (or similar)
  • types is support for typed arguments, beyond boolean and strings
  • choices is whether the library supports limited values, e.g. a paramenter "--type=" allowing only "file" and "dir" as legal arguments
  • commands are dash-less subsets, such as "backup" in the command "restic backup". Implicit here is that the tool allows flags to have distinct settings per command
  • varargs is when users can provide flags multiple times, e.g. "command --val one --val two" provides ["one", "two"]
  • mand/opt is whether the library allows setting flags as mandatory (the user must provide it) or optional (the user may provide it)
  • files is whether flags can be loaded from a configuration file
  • usage is library-generated help-text

claptrap supports the following data types:

  • bool
  • string
  • int
  • float64
  • time.Duration

Some features are provided through external add-ons; for example, clapenv adds environment variable support to claptrap, for 12-factor apps; and clapconf provides TOML config file parsing into flags.

The following size table is sorted by ABC[^1].

Library LOC Deps ABC Score Complexity
clapper 303 0 119 76
cosiner-argv 462 0 155 94
go-clap 326 0 176 104
droundy-goopt 596 0 243 162
claptrap 487 0 251 176
namsral-flag 764 0 299 162
ogier-pflag 1112 0 438 97
opflag 1161 0 461 118
integrii-flaggy 1732 0 659 303
spf13-pflag 3856 0 1464 583
jessevdk-go-flags 3529 0 1604 1045
dmulholland-args 437 1 199 97
thatisuday-commando 640 1 213 110
mwitkow-go-flagz 1461 1 487 265
cosiner-flag 1821 1 713 463
sircmpwn-getopt 501 2 154 60
moogar0880-venom 2029 2 604 303
stevenroose-gonfig 1169 4 540 375
peterbourgon-ff 1060 5 308 231
cobra 4529 5 1507 808
dc0d-argify 348 12 139 96

LOC -- no tests, no comments scc -i go -M _test --no-cocomo
ABC Score -- ABC complexity for the project (excluding dependencies) abcgo -format summary -path . -no-test

  • ~Man page generation, but as a separate program~ Man pages can be generated by a tool in the claphelp project.

Inspiration for some parts of the Claptrap ecosystem came from working with several of the other flags libraries, as well as:

As I mention above, if you prefer the parse-into-struct model, at the moment I'd recommend looking at go-clap, which is lightweight, also has no dependencies, and supports sane flag syntax.

#Elevator Pitch (toot-sized)

claptrap opts lib: very small, much features, getoptish https://sr.ht/~ser/claptrap/

  • getopt long & short flags (--bool, -b)
  • combined short (-b -e == -be)
  • inverted bools (--bool => --no-bool)
  • typed params (int, bool, Duration, string, float)
  • mandatory flags and arguments
  • positional arguments
  • global flags
  • subcommands
  • variadic flags (-a 1 -a 2 -a 3)
  • partial command matching (list =~ ls)
  • usage()

And it's under 500 lines of code, and 0 dependencies.

[^1] ABC is a better prediction of the amount of binary size a library will add to any give program using it, so I'm no longer sorting by LOC; striving for low LOC is a perverse incentive, when what we really should be striving for is simplicity and weight. Claptrap code isn't simple; it uses introspection quite a bit, and introspection is almost never easy to reason about. It does keep the end user API more simple, and Go's (new-ish) Generic system wouldn't help here because of the restriction on type parameters. Claptrap cheats on the LOC metric, as there's a fair bit of logic in the text and manpage Usage templates, and code in text/template isn't counted by LOC tools. With that code, it's closer to 560 LOC with the help text.