claptrap

package module
v3.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2022 License: BSD-3-Clause Imports: 8 Imported by: 1

README

claptrap

logo

A simple but powerful Go package to parse command-line arguments getopt(3) style. Designed especially for making CLI based libraries with ease. It has built-in support for sub-commands, long and short flag name combination (for example --version <==> -v), --flag=<value> syntax, inverted flag (for example --no-clean), variadic arguments, global flags, and partial command matching.

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

Installation

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

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

Usage

package main

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

func main() {

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

	reg.AddFlag("!count", "c", []int{1, 2, 3}, "count flag")
	reg.AddFlag("prob...", "p", []float64{1.5, 2.5, 0.5}, "probability flag")
	reg.AddFlag("delay...", "d", []time.Duration{10 * time.Second, 20 * time.Minute, time.Hour}, "duration flag")
	reg.AddFlag("verbose", "v", false, "verbosity flag")
	reg.AddFlag("fuzzy", "f", true, "fuzzy match flag") // A flag with a default true value
	reg.AddFlag("name", "", "Mr. Biggles", "your name")
	reg.AddArg("commestibles...", []string{"carrots", "cabbage", "ceyanne"}, "food to eat")
	reg.AddCommand("make-man", "make a manpage")

	info := reg.AddCommand("info", "the info command about information")
	info.AddArg("first", true, "the first argument")                            // The first optional argument must be a boolean (default: true)
	info.AddArg("second", 99, "the second argument")                            // The second optional argument is an int (default: 99)
	info.AddArg("third...", []string{"a", "b", "c", "d"}, "the third argument") // The rest of the arguments must each be a letter, a-d
	info.AddFlag("verbose", "v", 0, "info verbosity flag")                      // A completely different verbose, defaulting to true
	info.AddFlag("fuzzy", "f", true, "fuzzilischiousity!")                      // A completely different verbose, defaulting to true

	subinfo := info.AddCommand("subcommand", "a subcommand for the info command")
	subinfo.AddFlag("verbose", "V", []string{"quiet", "loud"}, "how verbose the subcommand is")

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

	e := reg.Parse(os.Args[1:]) // exclude the command!

	if e != nil {
		fmt.Printf("%+v\n", e)
	} else {
		// 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.Args["commestibles"].Strings())
				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.Args["count"].Ints())
				fmt.Printf("%s %+v\n", "prob", c.Args["prob"].Floats())
				fmt.Printf("%s %+v\n", "delay", c.Args["delay"].Durations())
			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.Args["third"].Strings())
				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":
				fmt.Printf("%+v\n", claptrap.RootCommand)
				claptrap.Usage()
				os.Exit(0)
			}
		}
	}
}

Rules

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

Commands

Commands are specified without dashes, and are like flag sets: each command can have its own set of flags and args. Commands are registered with the `Registry.Register()`` function. 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.AddArg(); 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. In the above example, the program would take up to three arguments; it'd place the first into first, the second into second, and the third into third.

More arguments would result in a parse error. The last Arg registered can be made variadic by adding ... to the name.

Now, the program will take any number of arguments, with the fourth and greater being placed in fourth. 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. In:

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

claptrap will validate the input data and return a parse error if the argument is not of the required type.

Choices are also supported. Choices limit the allowed arguments, and are set by passing an array of the choices to default value parameter of AddArg().

This works 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:

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.

  • Bool() (the only getter that provides a single value)
  • Strings()
  • Ints()
  • Floats()
  • Durations()
package main
import "ser1.net/claptrap/v3"
import "os"
import "fmt"
func main() {
  reg := claptrap.Command("test", "description")
  reg.AddArg("!first...", []string{"carrots", "cabbage", "ceyanne"}, "")
  c, e := reg.Parse([]string{"a"})  // exclude the command!
  if e != nil {
    fmt.Printf("%+v\n", e)
  } else {
    fmt.Printf("%+v\n", c.Args["first"].Strings())
  }

  c, e = reg.Parse([]string{"carrots"})
  if e != nil {
    fmt.Printf("%+v\n", e)
  } else {
    fmt.Printf("%+v\n", c.Args["first"].Strings())
  }

  c, e = reg.Parse([]string{"carrots", "cabbage"})
  if e != nil {
    fmt.Printf("%+v\n", e)
  } else {
    fmt.Printf("%+v\n", c.Args["first"].Strings())
  }
}
$ ./claptest a
first: illegal value a, must be [carrots cabbage ceyanne]
$ ./claptest carrots
[carrots]
$ ./claptest cabbage carrots
[cabbage carrots]

Durations can be any string parsable by Go time.ParseDuration(). Date/times can be in any one of the following formats: time.RFC3339, 2006-01-02T03:04:05, 2006-01-02T03:04, 2006-01-02, 01-02, 03:04:05 (full time), 03:04 (hour and minute), :04:05 (minute and second), 03: (just an hour), 04 (just the minute), :05 (just the second), 2006 (year only), 01- (month only), -02 (day only).

Flags

Flags are prefixed with dashes. Unlike Args, flags are always provided on input. 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. Flags and Args can be combined, and do not interfere with each other.
  • Mandatory and variadic syntax is specified on the long form.

The biggest differences are in boolean flags:

  • 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.

For demonstration, if the example code at the top of this readme is run, it will produce the following outputs for the given inputs.

# Flag can be required
$ go run .
claptest -- missing mandatory option: count

# Flag values can be constrained
$ go run . -c 5
count -- illegal value 5, must be [1 2 3]

# Illegal variadic
$ go run . -c 1 --count 2
count -- 2 values provided for non-variadic argument count

# Defaults for booleans can be true or false, and multiple flags can be variadic
$ go run . -c 1 -p 1.5 -p 2.5 --delay 10s -d 1h
commestibles []
verbose false
fuzzy true
count [1]
prob [1.5 2.5]
delay [10s 1h0m0s]

# Supplying a boolean Flag is always true
$ go run . -c 1 -v -f
commestibles []
verbose true
fuzzy true
count [1]
prob []
delay []

# Automatic --no-longflag is always false, and args can be anywhere
$ go run . carrots -c 2 --no-fuzzy --no-verbose ceyanne
commestibles [ceyanne]
verbose false
fuzzy false
count [2]
prob []
delay []

# Short boolean arguments can be combined. A non-boolean argument can be included, but
# it must be at the end:
$ go run . -vfc 3
commestibles []
verbose true
fuzzy true
count [3]
prob []
delay []

# Flags with the same name can be given for different commands, and have different types and defaults
$ go run ./main.go -c 1 -v --fuzzy info -v 5 --no-fuzzy
commestibles []
verbose true
fuzzy true
count [1]
prob []
delay []
info: first true
info: second 99
info: third []
info: verbose 5
info: fuzzy false

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.

UIs that have global flags should limit themselves to using Flags and not Args in the root command.

Commands can have subcommands. You can easily construct interfaces that have unexpected results if you mix arguments and subcommands. claptrap will process arguments as subcommands first, then as arguments.

Args and flags must not have the same names.

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

  • list, less -- ls bad
  • config, configs -- config *good
  • list, config, help -- l, c, h good

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

Convenience Rules

The short version is that the convenience functions only work at the command level; they do not reach into the global parameters, for instance. To get to those, you need to get the global command from the Registry and inspect it. The code will look in both Args and Flags, and return either a value, a default value, or a default value for the type, based on the rules below.

The long version is:

  1. If neither an Arg nor a Flag is 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.
  3. If the value is a choice, the default value of the type is returned. Choices do not have default values.
  4. If the type is wrong, the default value for the type is returned

Style

The examples so far use a style of setting everything within and with reference to a registry. An alternative style is more Go stdlib flags-like, creating pointers to value containers:

  reg := claptrap.NewRegistry()
  root := reg.Register("", "")
  username := root.AddFlag("user", "u", "", "")
  password := root.AddFlag("password", "p", "", "")
  server := root.AddFlag("server", "s", "", "")
  item := root.AddFlag("!get", "g", 0, "")
  reg.Parse(os.Args[1:])
  
  service.Login(server.Strings()[0], username.Strings()[0], password.Strings()[0])
  i := service.Get(item.Ints()[0])

Help Text

claptrap generates help text output.

The program at the top of this readme will generate the following Usage output:

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 / -d     <Duration>...    duration flag  ([10s 20m0s 1h0m0s])
   --fuzzy / -f                      fuzzy match flag  (true)
   --name / -       <string>         your name  (Mr. Biggles)
   --prob / -p      <float64>...     probability flag  ([1.5 2.5 0.5])
   --verbose / -v                    verbosity flag  (false)

help [args]
 print the help
 Commands:

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

 Flags:

info [args] [commands]
 the info command about information
 Commands:
   subcommand                        a subcommand for the info command

 Arguments:
   first                             the first argument  (true)
   second           <int>            the second argument  (99)
   third            <string>...      the third argument  ([a b c d])

 Flags:
   --fuzzy / -f                      fuzzilischiousity!  (true)
   --verbose / -v   <int>            info verbosity flag  (0)

make-man
 make a manpage
 Commands:

 Arguments:

 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.

To generate a man page, change the help template to the provided manpage template, and call the default Usage function:

  claptrap.HelpTemplate = claptrap.ManTempl
  claptrap.Usage()

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

The primary feature is general POSIX getopt compatability, so that the tools built with the library have a familiar interface for the largest popular command-line-using population in the world (hint: it isn't Plan 9). On top of that, I wanted the library to support some validation, to reduce runtime errors easily caused by repetitive validation code. The objective of claptrap is to be small but provide general getopt compatability and validation features. The most popular libraries are large, either directly or indirectly through dependencies. claptrap has 0 dependencies outside of the Go stdlib.

lib short long combined inverted env vars types choices commands varargs good api mand/opt files usage
claptrap Y Y Y Y N Y Y Y Y Y Y N Y
clapper Y Y N Y N N N Y Y Y N N
droundy Y Y Y N Y Y Y N Y
sircmpwn Y N Y N N N N N Y Y Y N Y
opflag Y Y Y N N Y N N Y Y N N Y
namsral Y Y Y Y Y Y Y Y
integrii Y Y y Y Y Y Y Y
jessevdk Y Y Y N N Y Y Y Y N Y N Y

claptrap supports the following data types:

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

Env vars would be nice, to be able to support 12-Factor apps, but that starts to feel like feature envy to me, and it's something that is easily added by a library such as GoBike. The biggest missing feature is synchronization with configuration files -- this often accounts for a significant amount of error-prone code; I'm hoping to provide (or find) support through a seperate library, such as go-ini.

Compared to other flags libraries, claptrap is among the smallest, and it pulls in no other dependencies.

Library LOC Deps ABC Score Complexity
clapper 303 0 119 76
sircmpwn-getopt 501 0 154 60
cosiner-argv 462 0 155 94
claptrap 494 0 243 167
droundy-goopt 596 0 243 162
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
moogar0880-venom 2029 2 604 303
stevenroose-gonfig 1169 5 540 375
peterbourgon-ff 1060 6 308 231
cobra 4529 6 1507 808
dc0d-argify 348 14 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

ABC is a better prediction of the amount of binary size a library will add to any give program using it. By that metric, claptrap is third in this list.

Related projects I'd like to provide for claptrap:

  • GoBike's envflag project is fantastic, and works with the core Go library.
  • subpop's go-ini works similarly, for integration with config files.
  • Man page generation, but as a separate program

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 455 lines of code, and 0 dependencies.

Documentation

Overview

Package claptrap processes the command-line arguments of getopt(3) syntax. This package provides the ability to process the root command, sub commands, command-line arguments and command-line flags.

Index

Constants

This section is empty.

Variables

View Source
var HelpTempl string
View Source
var HelpTemplate string = HelpTempl
View Source
var ManTempl string
View Source
var Usage func() = func() {
	t := template.New("help")
	t.Funcs(template.FuncMap{"PadOut": func(i int, v string) string {
		v = strings.ReplaceAll(strings.ReplaceAll(v, "[]", ""), "time.", "")
		if len(v) > i {
			i = len(v)
		}
		return v + strings.Repeat(" ", i-len(v))
	}})
	if mp, err := t.Parse(HelpTemplate); err != nil {
		panic(err)
	} else {
		mp.Execute(os.Stdout, RootCommand)
	}
}

Functions

This section is empty.

Types

type Arg

type Arg struct {
	// name of the argument
	Name         string
	ShortName    string
	IsVariadic   bool
	IsMandatory  bool
	DefaultValue interface{}

	Description string
	IsFlag      bool
	// contains filtered or unexported fields
}

Arg holds the structured information about an argument.

func (Arg) Bool

func (a Arg) Bool() bool

Bool returns the Arg value as a bool, or the default if no argument was given. It returns false anif the Arg was declared as a different type.

func (Arg) Durations

func (a Arg) Durations() []time.Duration

Durations returns the Arg value as an array of type duration, or the defaults if no argument was given. It returns an empty array if the Arg was declared as a different type.

func (Arg) Floats

func (a Arg) Floats() []float64

Floats returns the Arg value as an array of type float, or the defaults if no argument was given. It returns an empty array if the Arg was declared as a different type.

func (Arg) Ints

func (a Arg) Ints() []int

Ints returns the Arg value as an array of type int, or the defaults if no argument was given. It returns an empty array if the Arg was declared as a different type.

func (Arg) IsSet

func (a Arg) IsSet() bool

IsSet returns true if the Argument was provided with a value in the Parsed data.

func (Arg) String

func (c Arg) String() string

func (Arg) Strings

func (a Arg) Strings() []string

Strings returns the Arg value as an array of type string, or the defaults if no argument was given. It returns an empty array if the Arg was declared as a different type.

type CommandConfig

type CommandConfig struct {

	// name of the sub-command ("" for the root command)
	Name string

	// Defined subcommands
	Commands map[string]*CommandConfig

	// The parsed subcommand, if any
	Command *CommandConfig

	// registered command argument values
	Args map[string]*Arg

	Description string
	// contains filtered or unexported fields
}
var RootCommand *CommandConfig

func Command added in v3.4.0

func Command(name, description string) *CommandConfig

func (*CommandConfig) AddArg

func (commandConfig *CommandConfig) AddArg(name string, defaultValue interface{}, description string) *Arg

CommandConfig type holds the structure and values of the command-line arguments of command. AddArg creates a named argument. Args are placeholders for non-flagged arguments; command-line elements are consumed into Args, in the order they are defined. The very last Arg created (on a command) can be variadic, which means it consumes all of the remaining arguments.

If more elements are provided than there are Args, and the last Arg is *not* variadic, a parse error will result.

Arguments can be made mandatory with a ! prefix in the name. Because Args are consumed in order, this has the effect of making _all_ arguments before the mandatory argument required.

Arguments are strongly typed. The type of an argument is derived from the default value. The supported type

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

If the provided defaultValue is an array of one of the above types, then that array defines a set of legal values; any provided parameter that doesn't match a value in the array will result in a parse error.

Boolean arguments differ from other types in that:

- boolean Args may *not* be variadic, and will produce an error - the getter, `Bool()`, always returns a single value -- not an array, like the rest - boolean Args may not have array defaults; the options are always `true` and `false`

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.

func (*CommandConfig) AddCommand

func (command *CommandConfig) AddCommand(name, description string) *CommandConfig

AddCommand creates a command. If the command name is an empty string, the command created is the root command. Example ``` r := Newcommand() a := r.Command("").AddArg("X", "") c := r.Parse([]string{"abc"}) // No explicit command, just an arg if c.Args["X"].Strings() != []string{"abc"} { panic() } ````

func (*CommandConfig) AddFlag

func (commandConfig *CommandConfig) AddFlag(name string, shortName string, defaultValue interface{}, desc string) *Arg

AddFlag creates a flag of a given type and defaults. Do not provide the dashes in the names.

The rules for flags are the same as for args, but in addition:

  • The `name` argument is the long-name of the flag. The long name can be prefixed with `!`, in which case it is mandatory. It can also be suffixed with `...`, in which case it is variadic.
  • Unlike Args, any flag can be variadic.
  • The `shortName` argument should be a single character.
  • Boolean Flags don't accept arguments; if the flagis provided on the command line, it is true; otherwise, it is the default.
  • Boolean flags automatically get a `no-<longname>` option, which if provided, returns `false' for the flag.
  • Regardless of the default, boolean flags are true if provided, and false if inverted

On the command line, Flags are prefixed with dashes. Unlike Args, Flag names are always provided on input. With the exceptions above, Flags follow all the rules and features of arguments: mandatory, variadic, defaults, choices, and types.

func (CommandConfig) Bool

func (c CommandConfig) Bool(n string) bool

Bool finds a set Arg or Flag by name and returns its first value, or the value of the default, as an bool. See documentation for Int() for the rules

func (CommandConfig) Duration

func (c CommandConfig) Duration(n string) time.Duration

Duration finds a set Arg or Flag by name and returns its first value, or the value of the default, as an duration. See documentation for Int() for the rules

func (CommandConfig) Float

func (c CommandConfig) Float(n string) float64

Float finds a set Arg or Flag by name and returns its first value, or the value of the default, as an float. See documentation for Int() for the rules

func (CommandConfig) Int

func (c CommandConfig) Int(n string) int

Ints finds a set Arg or Flag by name and returns its first value, or the value of the default, as an int.

func (*CommandConfig) Parse

func (command *CommandConfig) Parse(values []string) error

Parse parses an array of arguments and returns a command object, or an error if any of the parameter validations fail. If no root command is registered, the first array item must be a registered command.

See the rules for `AddArg()` and `AddFlag()` for specific parse rules.

func (CommandConfig) String

func (c CommandConfig) String(n string) string

String finds a set Arg or Flag by name and returns its first value, or the value of the default, as an string. See documentation for Int() for the rules

Jump to

Keyboard shortcuts

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