This package provides an easy to use command line parser. By describing your command line with string descriptors, you get:
- Execution of a command handler function, selected according to the process arguments
- Ability to describe the command values close to the way the command is used
- Auto-generated help
- Parsing of options available to all command handlers (global options)
- Conversion into the basic types:
string
,bool
,int
,float64
,path
- Support for simple position-oriented parameters
- Support for optional parameters
- Support for repeated parameters
- Ability to extend the supported types with your own conversion code
- Detailed descriptor errors to speed fixing development mistakes
Let's look at a process that we name myexample
that has no command line arguments:
Code
import (
"github.com/jimsnab/go-cmdline"
"fmt"
"os"
)
func main() {
cl := cmdline.NewCommandLine()
cl.RegisterCommand(singleCommand, "~")
args := os.Args[1:] // exclude executable name in os.Args[0]
err := cl.Process(args)
if err != nil {
cl.Help(err, "myexample", args)
}
}
func singleCommand(args cmdline.Values) error {
fmt.Println("Hello, world!")
return nil
}
In the code above, after creating the command line struct, the command handler function singleCommand
is registered as command named ~
, which means "unnamed". Then the command line arguments are sent in to be parsed. If something goes wrong, help is printed.
Let's build and run it.
Build and Run
$ # build myexample
$ go build
$ # run it
$ ./myexample
Hello, world!
$ ./myexample myarg
Usage: myexample
This command has no options.
$ ./myexample --help
This command has no options.
Fine. Let's add some help. Add ?This is an example
to the code above.
Code
cl.RegisterCommand(singleCommand, "~?This is an example")
Build and Run
$ go build
$ ./myexample --help
Description: This is an example
$ ./myexample foo
Usage: myexample
Description: This is an example
The main use of this package is to parse command line options, so let's add
one. Update the same line and change the singleCommand
handler to use the argument, as follows:
Code
cl.RegisterCommand(singleCommand, "~ <string-name>?This is an example")
...
fmt.Printf("Hello, %s!\n", args["name"].(string))
Now the program requires an argument, called name
.
Build and Run
$ go build
$ ./myexample
Usage: myexample <command>
Command Options:
<name> This is an example
$ ./myexample fido
Hello, fido!
One of the major use cases supported by the cmdline
package is a tool that
provides several commands. Here's an example.
Code
func main() {
cl := cmdline.NewCommandLine()
cl.RegisterCommand(
func (args cmdline.Values) error {
fmt.Println("74 degrees F and partly cloudy with 6 MPH winds")
return nil
},
"now?Provides the current weather",
)
cl.RegisterCommand(
func (args cmdline.Values) error {
fmt.Println("Today will be sunny and 82 degrees F")
return nil
},
"forecast?Provides a forecast of today's weather",
)
cl.RegisterCommand(
func (args cmdline.Values) error {
fmt.Println("0.2 in of precipitation so far this month")
return nil
},
"precip?Print the precipitation statistics",
)
args := os.Args[1:] // exclude executable name in os.Args[0]
err := cl.Process(args)
if err != nil {
cl.Help(err, "myexample", args)
}
}
Build and Run
$ go build
$ ./myexample
Usage: myexample <command>
All Commands:
forecast Provides a forecast of today's weather
now Provides the current weather
precip Print the precipitation statistics
$ ./myexample now
74 degrees F and partly cloudy with 6 MPH winds
A command handler receives a map of command line arguments and returns an error
.
func myHandler (args cmdline.Values) error {
// your code
return err
}
The args
is filled with:
- A
bool
true
for the primary command, exactly as the command is named. - A
bool
for each option of the command,true
if the option was specified. - Each value for parameters of each switch, if any.
For example, consider the following command.
Code
package main
import (
"fmt"
"os"
"github.com/jimsnab/go-cmdline"
)
func main() {
cl := cmdline.NewCommandLine()
cl.RegisterCommand(
myHandler,
"format?Formats the storage",
"-i <path-initFile>?Specifies the path to the initialization descriptor file",
"[--force]?Performs the format even if the storage has been formatted",
"[--dynamic[ <int-blockSize>]]?Formats for dynamic sizing, with optional blockSize",
)
args := os.Args[1:] // exclude executable name in os.Args[0]
err := cl.Process(args)
if err != nil {
cl.Help(err, "myexample", args)
}
}
func myHandler(args cmdline.Values) error {
fmt.Println(args)
return nil
}
Build and Run
$ go build
$ ./myexample
Usage: myexample <command> <options>
Command Options:
format Formats the storage
-i <initFile> Specifies the path to the initialization descriptor file
[--dynamic [<blockSize>]] Formats for dynamic sizing, with optional blockSize
[--force] Performs the format even if the storage has been formatted
$ ./myexample format -i /tmp/example.cfg
map[--dynamic:false --force:false -i:true blockSize:0 format:true initFile:/tmp/example.cfg]
The optional values that are not specified on the command line will have the default value for the type.
Values in the map are typed, so the handler code can use type assertions.
Code
func myHandler(args cmdline.Values) error {
blockSize := args["blockSize"].(int)
fmt.Println(blockSize)
return nil
}
Build and Run
$ go build
$ ./myexample format -i /tmp/example.cfg
0
As shown above, the standard pattern for showing help is:
args := os.Args[1:] // exclude executable name in os.Args[0]
err := cl.Process(args)
if err != nil {
cl.Help(err, "myexample", args)
}
The Process()
function will parse the command line arguments and invoke
the corresponding handler, or, return an error
if something went wrong. A
command line error is of type cmdline.CommandLineError
. This type can be used
to distinguish between command line syntax errors and runtime errors.
The Help()
function generates help according to the command line definition.
It also handles help
and --help
switches.
Help()
will show help for a command when the command argument ends in a question mark.
In the "format" example above:
Help Example
$ ./myexample format?
Command Options:
format Formats the storage
-i <initFile> Specifies the path to the initialization descriptor file
[--dynamic [<blockSize>]] Formats for dynamic sizing, with optional blockSize
[--force] Performs the format even if the storage has been formatted
Help()
supports a filter
argument. The filter can be specified with --help <filter>
,
or simply <filter>
:
Help Example
$ ./myexample form
Usage: myexample <command> <options>
Command Options:
format Formats the storage
-i <initFile> Specifies the path to the initialization descriptor file
[--dynamic [<blockSize>]] Formats for dynamic sizing, with optional blockSize
[--force] Performs the format even if the storage has been formatted
If the filter text is found somewhere in the command help, the help for the entire
command will be printed. This is better than piping help to grep
.
Your code can print a specific command with cl.PrintCommand()
, or print the help
without "Usage" or filter help text by using cl.PrintCommands()
.
A program with several commands can benefit from global options that are available across all of the commands. When a global option is specified, an option handler is invoked, with a map containing the global options.
Global options must be provided on the command line before the command.
Code
package main
import (
"fmt"
"os"
"github.com/jimsnab/go-cmdline"
)
func main() {
cl := cmdline.NewCommandLine()
cl.RegisterCommand(
exampleHandler,
"users?Performs operations on a user",
"[--create <string-createUser>]?Creates a user",
"[--delete <string-deleteUser>]?Deletes a user",
"[--list]?List users",
)
cl.RegisterCommand(
exampleHandler,
"groups?Performs operations on user groups",
"[--create <string-createGroup>]?Creates a user group",
"[--delete <string-deleteGroup>]?Deletes a user group",
"[--list]?List user groups",
"[--describe <string-describeGroup>]?Describe details of a user group",
)
cl.RegisterGlobalOption(
func(values cmdline.Values) error {
fmt.Println("global", values)
return nil
},
"--env:<string-env>",
)
args := os.Args[1:] // exclude executable name in os.Args[0]
err := cl.Process(args)
if err != nil {
cl.Help(err, "myexample", args)
}
}
func exampleHandler(args cmdline.Values) error {
fmt.Println("command", args)
return nil
}
Build and Run
$ go build
$ ./myexample
Usage: myexample <global options> <command> <options>
Global Options:
--env:<env>
All Commands:
groups Performs operations on user groups
[--create <createGroup>] Creates a user group
[--delete <deleteGroup>] Deletes a user group
[--describe <describeGroup>]
Describe details of a user group
[--list] List user groups
users Performs operations on a user
[--create <createUser>] Creates a user
[--delete <deleteUser>] Deletes a user
[--list] List users
Search help with myexample --help <filter text>. Example: myexample --help users
Or, put a question mark on the end. Example: myexample users?
$ ./myexample --env:prod users --list
global map[--env:true env:prod]
command map[--create:false --delete:false --list:true createUser: deleteUser: users:true]
NOTE: The example above needs improvement. Adding mutually exclusive secondary arguments is in the backlog.
An argument value is specified as <
type -
variable name >
, where type can be one of:
string
- an ordinary text stringbool
-true
orfalse
textint
- a 32-bit integerfloat64
- a floating point valuepath
- a string holding a path in its canonical (absolute) form
A command can have optional arguments based on their position. Only a single list of
position-based arguments can be specified. A list of multiple values can be specified
at the last position using an asterisk *
.
Syntax
// unnamed single command
cl.RegisterCommand(
myHandler,
"~ <string-posarg1> <string-posarg2> <string-posarg3>",
)
or
// named command (multiple named commands supported)
cl.RegisterCommand(
myHandler,
"mycommand <string-posarg1> <string-posarg2> <string-posarg3>",
)
or
// one or more values combined into an array
cl.RegisterCommand(
myHandler,
"mycommand *<string-multiPosArg>",
)
The right side arguments can be optional.
Syntax
cl.RegisterCommand(
myHandler,
"~ <string-posarg1> [<string-posarg2>] [<string-posarg3>]",
)
Position-oriented parameters cannot have values that start with a dash, as that is used to match named parameters.
It is possible to register named command handlers along with a position-oriented handler. Named command handlers have priority.
For example, it is possible to add a catch-all handler as shown here:
Syntax
cl.RegisterCommand(
namedHandler,
"named <string-arg>",
)
cl.RegisterCommand(
defaultHandler,
"~ *<string-args>",
)
A single argument can be divided into values by using a colon to delimit the first value, and commas to delimit subsequent values.
Code
package main
import (
"fmt"
"os"
"github.com/jimsnab/go-cmdline"
)
func main() {
cl := cmdline.NewCommandLine()
cl.RegisterCommand(
exampleHandler,
"--first:<int-begin>,<int-end>",
)
cl.RegisterCommand(
exampleHandler,
"rangeA",
"--second:<int-begin>,<int-end>",
)
cl.RegisterCommand(
exampleHandler,
"rangeB",
"--third:<int-begin>[,<int-end>]",
)
args := os.Args[1:] // exclude executable name in os.Args[0]
err := cl.Process(args)
if err != nil {
cl.Help(err, "myexample", args)
}
}
func exampleHandler(args cmdline.Values) error {
fmt.Println("command", args)
return nil
}
In the code above, the --third
switch has an optional end
argument. When using comma-separated optional arguments, the last value specified on the command line is
used as the default for each missing optional value.
Example Run
$ ./myexample
Usage: myexample <command> <options>
All Commands:
--first:<begin>,<end>
rangeA
--second:<begin>,<end>
rangeB
--third:<begin>[,<end>]
$ ./myexample rangeB --third:10
command map[--third:true begin:10 end:10 rangeB:true]
It is possible to register two or more tokens as the "primary command".
Example: suppose you wanted a command view
with different handlers for each kind of view, such as table
row
and cell
. Easy! Just use +
to indicate a space in the primary command. Expand below for more details.
Code
package main
import (
"fmt"
"os"
"github.com/jimsnab/go-cmdline"
)
func main() {
cl := cmdline.NewCommandLine()
cl.RegisterCommand(
func(args cmdline.Values) error {
fmt.Println("view table...")
},
"view+table",
)
cl.RegisterCommand(
func(args cmdline.Values) error {
fmt.Println("view row...")
},
"view+row",
)
cl.RegisterCommand(
func(args cmdline.Values) error {
fmt.Println("view cell...")
},
"view+cell",
)
args := os.Args[1:] // exclude executable name in os.Args[0]
err := cl.Process(args)
if err != nil {
cl.Help(err, "myexample", args)
}
}
Example Run
$ ./myexample view table
view table...
To allow a command line switch to be used more than once, it can be marked
with an asterisk (*
), and the same switch can be specified more than once.
The command handler will get an array value.
Code
package main
import ( "fmt" "os" "github.com/jimsnab/go-cmdline" )
func main() { cl := cmdline.NewCommandLine()
cl.RegisterCommand(
exampleHandler,
"~",
"*-f:<string-text>",
)
args := os.Args[1:] // exclude executable name in os.Args[0]
err := cl.Process(args)
if err != nil {
cl.Help(err, "myexample", args)
}
}
func exampleHandler(args cmdline.Values) error { fmt.Println("command", args) return nil }
Output
$ ./myexample
Usage: myexample <options>
Command Options:
*-f:<text>
$ ./myexample -f:one -f:two -f:three
command map[-f:true text:[one two three] ~:true]
To support zero or more multiple switches, make the argument optional with the asterisk first, e.g., *[-f:<string-text>]
.
Your program can use the parser to extract the primary command.
primaryCmd := cl.PrimaryCommand(os.Args[1:])
This might be useful to find the primary command specified when global options are possible.
An empty string is returned if the command line arguments do not map to a command.
You can write your own cmdline.OptionTypes
interface to convert arguments to your own
types and structs. It takes a moment to understand this interface, but it ultimately
pretty simple.
Construct the command line object with:
cl := cmdline.NewCustomTypesCommandLine(myType)
where myType
fulfills the following interface:
type OptionTypes interface {
StringToAttributes(typeName string, spec string) *OptionTypeAttributes
MakeValue(typeIndex int, inputValue string) (any, error)
NewList(typeIndex int) (any, error)
AppendList(typeIndex int, list any, inputValue string) (any, error)
}
Your implementation determines valid values for typeIndex
. Typically it is an integer
enumeration.
StringToAttributes
converts type stringspec
to the corresponding index and typed default value (the two members ofcmdline.OptionTypeAttributes
)MakeValue
converts command line inputinputValue
into the corresponding typed valueNewList
allocates a new typed array (see repeated values above)AppendList
appends a value to the typed array provided byNewList
A custom types handler owns supporting all the spec
types used in your command line.
It is often desired to retain the default types (bool
, string
, int
, float64
,
path
). This can be achieved via NewDefaultOptionTypes()
, which provides the
default interface, so myType
can fall back to the default implementation on unknown
spec
types or unknown typeIndex
values.
Example
type (
cmdLineTypes struct {
dot *cmdline.DefaultOptionTypes
index int
}
)
func newCmdLineTypes() *cmdLineTypes {
ut := cmdLineTypes{}
ut.dot, ut.index = cmdline.NewDefaultOptionTypes()
return &ut
}
func (t *cmdLineTypes) StringToAttributes(typeName string, spec string) (a *cmdline.OptionTypeAttributes) {
if typeName == "uint64" {
a = &cmdline.OptionTypeAttributes{
DefaultValue: uint64(0),
Index: t.index,
}
} else {
a = t.dot.StringToAttributes(typeName, spec)
}
return
}
func (t *cmdLineTypes) MakeValue(typeIndex int, inputValue string) (v any, err error) {
if typeIndex == t.index {
var n uint64
if inputValue != "" {
n, err = strconv.ParseUint(inputValue, 10, 64)
if err != nil {
return
}
}
v = n
} else {
v, err = t.dot.MakeValue(typeIndex, inputValue)
}
return
}
func (t *cmdLineTypes) NewList(typeIndex int) (v any, err error) {
if typeIndex == t.index {
v = []uint64{}
} else {
v, err = t.dot.NewList(typeIndex)
}
return
}
func (t *cmdLineTypes) AppendList(typeIndex int, list any, inputValue string) (v any, err error) {
if typeIndex == t.index {
var n any
n, err = t.MakeValue(typeIndex, inputValue)
if err != nil {
return
}
l := list.([]uint64)
v = append(l, n.(uint64))
} else {
v, err = t.dot.AppendList(typeIndex, list, inputValue)
}
return
}
If your command or global option registration is malformed, the registration API will
invoke panic
with a message explaining the error. This helps quickly spot typos and
unsupported syntax.
It is not advised to try to recover
from a registration api panic.
The command line parser uses toolprinter to print to stdout.
You can provide your own implementation of this interface by calling SetPrinter()
, if you want
to render help on something other than a shell terminal.