Go option parser inspired on the flexibility of Perl’s GetOpt::Long.
- Quick overview
- Examples
- DAG Build System
- Features
- How to install it
- Dependencies
- Introduction
- Boolean options
- Options with String arguments
- Options with Integer arguments
- Options with Floating point arguments
- Options with array arguments
- Options with Key Value arguments
- Options with array arguments and multiple entries
- Options with key value arguments and multiple entries
- Stop parsing options when
--
is passed - Stop parsing options when a command is passed
- Allow passing options and non-options in any order
- Allow pass through
- Fail on unknown
- Warn on unknown
- Option aliases
- Required options
- Incremental option
- Additional types
- Options with optional arguments
- Option flags that call a method internally
- Operation Modes
- Biggest option parser misfeature - Automatically generate help
- Command behaviour
- Environment Variables Support
- ROADMAP
- License
-
Define your command line specification:
package main import ( "fmt" "io/ioutil" "log" "os" "github.com/DavidGamba/go-getoptions" ) var logger = log.New(ioutil.Discard, "DEBUG: ", log.LstdFlags) func main() { var debug bool var greetCount int var list map[string]string opt := getoptions.New() opt.Bool("help", false, opt.Alias("h", "?")) opt.BoolVar(&debug, "debug", false) opt.IntVar(&greetCount, "greet", 0, opt.Required(), opt.Description("Number of times to greet.")) opt.StringMapVar(&list, "list", 1, 99, opt.Description("Greeting list by language.")) remaining, err := opt.Parse(os.Args[1:]) if opt.Called("help") { fmt.Fprintf(os.Stderr, opt.Help()) os.Exit(1) } if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", err) fmt.Fprintf(os.Stderr, opt.Help(getoptions.HelpSynopsis)) os.Exit(1) } // Use the passed command line options... Enjoy! if debug { logger.SetOutput(os.Stderr) } logger.Printf("Unhandled CLI args: %v\n", remaining) // Use the int variable for i := 0; i < greetCount; i++ { fmt.Println("Hello World, from go-getoptions!") } // Use the map[string]string variable if len(list) > 0 { fmt.Printf("Greeting List:\n") for k, v := range list { fmt.Printf("\t%s=%s\n", k, v) } } }
-
Call it:
Show help$ ./myscript --help SYNOPSIS: myscript --greet <int> [--debug] [--help|-h|-?] [--list <key=value>...]... REQUIRED PARAMETERS: --greet <int> Number of times to greet. OPTIONS: --debug (default: false) --help|-h|-? (default: false) --list <key=value>... Greeting list by language. (default: {})
Show errors$ ./myscript ERROR: Missing required option 'greet'! SYNOPSIS: myscript --greet <int> [--debug] [--help|-h|-?] [--list <key=value>...]...
Show errors$ ./myscript -g ERROR: Missing argument for option 'greet'! SYNOPSIS: myscript --greet <int> [--debug] [--help|-h|-?] [--list <key=value>...]...
Use of int option$ ./myscript -g 3 Hello World, from go-getoptions! Hello World, from go-getoptions! Hello World, from go-getoptions!
Use of bool option$ ./myscript --debug -g 1 other stuff DEBUG: 2019/07/14 23:20:22 Unhandled CLI args: [other stuff] Hello World, from go-getoptions!
Use of map option./myscript -g 0 -l en='Hello World' es='Hola Mundo' Greeting List: en=Hello World es=Hola Mundo
A simple script ./examples/myscript/main.go
To use the autocompletion, cd to the ./examples/myscript dir and run: source sourceme.bash
The run go build
and ./myscript
.
Tab completion for this script is triggered for options only, so you need to have a dash -
to trigger it: ./myscript -<tab><tab>
This is the other extreme, a large program that can separate each command in a separate go package.
The base is located at ./examples/mygit/main.go
The commands are located at:
To use the autocompletion, cd to the ./examples/mygit dir and run: source sourceme.bash
The run go build
and ./mygit
.
Tab completion without arguments triggers completion for commands, for option completion add a dash -
and trigger it: ./mygit -<tab><tab>
The slow command shows an example of an slow command that can be cancelled with Ctrl+C
.
The cancellation is passed to the command through context.Context
and it is handled at the command to stop taking new work and trigger a cleanup routine.
Running Ctrl+C
twice cancels the cancellation routine and fully cancels the program.
This example shows task dependency orchestration and parallelization ./examples/dag/main.go.
To use the autocompletion, cd to the ./examples/dag dir and run: source sourceme.bash
The run go build
and ./dag
.
Tab completion without arguments triggers completion for commands, for option completion add a dash -
and trigger it: ./dag -<tab><tab>
For an overview of the Directed Acyclic Graph Build System see ./dag/README.adoc
-
Built in auto completion. A single line of bash is all it takes.
-
Allow passing options and non-options in any order.
-
Support for
--long
options. -
Support for short (
-s
) options with flexible behaviour (see the Operation Modes section for details):-
Normal (default)
-
Bundling
-
SingleDash
-
-
Called()
method indicates if the option was passed on the command line. -
Multiple aliases for the same option. e.g.
help
,man
. -
CalledAs()
method indicates what alias was used to call the option on the command line. -
Simple synopsis and option list automated help.
-
Boolean, String, Int, Float64, Slice and Map type options.
-
Negatable Boolean options.
For example:
--verbose
,--no-verbose
or--noverbose
. -
Options with Array arguments. The same option can be used multiple times with different arguments. The list of arguments will be saved into an Slice.
-
Options with array arguments and multiple entries.
For example, instead of writing:
color --r 10 --g 20 --b 30 --next-option
orcolor --rgb 10 --rgb 20 --rgb 30 --next-option
the input could be:color --rgb 10 20 30 --next-option
-
When using integer array options with multiple arguments, positive integer ranges are allowed.
For example, Instead of writing:
csv --columns 1 2 3
orcsv --columns 1 --columns 2 --columns 3
The input could be:csv --columns 1..3
-
Options with Key Value arguments. This allows the same option to be used multiple times with arguments of key value type.
For example:
rpmbuild --define name=myrpm --define version=123
-
Options with key value arguments and multiple entries.
For example, instead of writing:
connection --server hostname=serverIP --server port=123 --client hostname=localhost --client port=456
the input could be:connection --server hostname=serverIP port=123 --client hostname=localhost port=456
-
Supports command line options with '='.
For example: You can use
--string=mystring
and--string mystring
. -
Allows passing arguments to options that start with dash
-
when passed after equal.For example:
--string=--hello
and--int=-123
. -
Supports passing
--
to stop parsing arguments (everything after will be left in theremaining []string
). -
Options with optional arguments. If the default argument is not passed the default is set.
For example: You can call
--int 123
which yields123
or--int
which yields the given default. -
Allows abbreviations when the provided option is not ambiguous.
For example: An option called
build
can be called with--b
,--bu
,--bui
,--buil
and--build
as long as there is no ambiguity. In the case of ambiguity, the shortest non ambiguous combination is required. -
Support for the lonesome dash "-". To indicate, for example, when to read input from STDIO.
-
Incremental options. Allows the same option to be called multiple times to increment a counter.
-
Supports case sensitive options. For example, you can use
v
to defineverbose
andV
to defineVersion
. -
Support indicating if an option is required and allows overriding default error message.
-
Errors exposed as public variables to allow overriding them for internationalization.
-
Supports program commands (when a command is passed a command function is triggered to handle the command logic).
-
Built in
opt.Dispatch
function calls commands and propagates context, options, arguments and cancellation signals. -
Multiple ways of managing unknown options:
-
Fail on unknown (default).
-
Warn on unknown.
-
Pass through, allows for commands and can be combined with Require Order.
-
-
Require order: Allows for commands. Stop parsing arguments when the first non-option is found. When mixed with Pass through, it also stops parsing arguments when the first unmatched option is found.
-
Set options by reading Environment Variables.
-
Get it from github:
go get github.com/DavidGamba/go-getoptions
-
Then import it:
import "github.com/DavidGamba/go-getoptions" // As getoptions
-
Enjoy!
Note
|
For a Quick overview, jump to that section in the TOC or review the GoDoc Documentation. |
Option parsing is the act of taking command line arguments and converting them into meaningful structures within the program.
An option parser should support, at least, the following:
True
when passed on the command line.
For example:
ls --all
In go-getoptions
this is accomplished with:
-
ptr := opt.Bool(name, default_value)
. -
opt.BoolVar(&ptr, name, default_value)
. -
Additionally, if all you want to know is if the option was passed you can use:
opt.Bool(name, default_value)
(without capturing its return value) and then checkopt.Called(name)
. -
Also, you can get the value with
v, ok := opt.Value(name).(bool)
.
The option will accept a string argument. For example:
grepp --ignore .txt
Additionally, arguments to options can be passed with the =
symbol.
grepp --ignore=.txt
In go-getoptions
this is accomplished with:
-
ptr := opt.String(name, default_value)
. -
opt.StringVar(&ptr, name, default_value)
.
The features listed above are enough to create basic programs but an option parser should do better:
Parse an option string argument into an Integer and provide an user error if the string provided is not an integer. For example:
grepp --contex-lines 3
and:
grepp --context-lines string
Error: 'string' is not a valid integer.
In go-getoptions
this is accomplished with:
-
ptr := opt.Int(name, default_value)
. -
opt.IntVar(&ptr, name, default_value)
.
Parse an option string argument into a Floating point value and provide an user error if the string provided is not a valid floating point. For example:
program --approximation 3.5
and:
$ program --approximation string Error: 'string' is not a valid floating point value.
In go-getoptions
this is accomplished with:
-
ptr := opt.Float64(name, default_value)
. -
opt.Float64Var(&ptr, name, default_value)
.
The features listed above relieve the programmer from the cumbersome task of converting the option argument into the expected type.
That covers the most basic set of features, but still it is not enough to get past a basic program. The following features will allow for a more complete interface.
This allows the same option to be used multiple times with different arguments. The list of arguments will be saved into a Slice inside the program. For example:
list-files --exclude .txt --exclude .html --exclude .pdf
In go-getoptions
this is accomplished with:
-
ptr := opt.StringSlice(name, 1, 1)
. -
opt.StringSliceVar(&ptr, name, 1, 1)
. -
ptr := opt.IntSlice(name, 1, 1)
. -
opt.IntSliceVar(&ptr, name, 1, 1)
.
go-getoptions
has only implemented this feature for string and int.
This allows the same option to be used multiple times with arguments of key value type. For example:
rpmbuild --define name=myrpm --define version=123
In go-getoptions
this is accomplished with:
-
strMap := opt.StringMap(name, 1, 1)
. -
opt.StringMapVar(&ptr, name, 1, 1)
.
go-getoptions
has only implemented this feature for string.
The features above are useful when you have a variable amount of arguments, but it becomes cumbersome for the user when the number of entries is always the same. The features described below are meant to handle the cases when each option has a known number of multiple entries.
This allows the user to save typing. For example:
Instead of writing: color --r 10 --g 20 --b 30 --next-option
or color --rgb 10 --rgb 20 --rgb 30 --next-option
The input could be: color --rgb 10 20 30 --next-option
.
The setup for this feature should allow for the user to continue using both versions of the input, that is passing one argument at a time or passing the 3 arguments at once, or allow the setup to force the user to have to use the 3 arguments at once version. This is accomplished with the minimum and maximum setup parameters.
The minimum setup parameter indicates the minimum amount of parameters the user can pass at a time. For the example above, the parameter could be set to 3 to force the user to have to pass the 3 arguments at once. When set to 1, the user will be able to pass a single parameter per option call.
The maximum setup parameter indicates the maximum amount of parameters the user can pass at a time.
The option parser will leave any non option argument after the maximum in the remaining
slice.
In go-getoptions
this is accomplished with:
-
strSlice := opt.StringSlice(name, minArgs, maxArgs)
. -
opt.StringSliceVar(&ptr, name, minArgs, maxArgs)
. -
intSlice := opt.IntSlice(name, minArgs, maxArgs)
. -
opt.IntSliceVar(&ptr, name, minArgs, maxArgs)
.
Additionally, in the case of integers, positive integer ranges are allowed. For example:
Instead of writing: csv --columns 1 2 3
or csv --columns 1 --columns 2 --columns 3
The input could be: csv --columns 1..3
.
In go-getoptions
this is currently enabled by default when using:
-
intSlice := opt.IntSlice(name, minArgs, maxArgs)
-
opt.IntSliceVar(&ptr, name, minArgs, maxArgs)
.
This allows the user to save typing. For example:
Instead of writing: connection --server hostname=serverIP --server port=123 --client hostname=localhost --client port=456
The input could be: connection --server hostname=serverIP port=123 --client hostname=localhost port=456
In go-getoptions
this is accomplished with:
-
strMap := opt.StringMap(name, minArgs, maxArgs)
. -
opt.StringMapVar(&ptr, name, minArgs, maxArgs)
.
That covers a complete user interface that is flexible enough to accommodate most programs. The following are advanced features:
Useful when arguments start with dash -
and you don’t want them interpreted as options.
In go-getoptions
this is the default behaviour.
A command is assumed to be the first argument that is not an option or an argument to an option. When a command is found, stop parsing arguments and let a command handler handle the remaining arguments. For example:
program --opt arg command --subopt subarg
In the example above, --opt
is an option and arg
is an argument to an option, making command
the first non option argument.
Additionally, when mixed with pass through, it will also stop parsing arguments when it finds the first unmatched option.
In go-getoptions
this is accomplished with:
-
opt.SetUnknownMode(getoptions.Pass)
.
And can be combined with:
-
opt.SetRequireOrder()
.
Some option parsers force you to put the options before or after the arguments. That is really annoying!
In go-getoptions
this is the default behaviour.
Have an option to pass through unmatched options. Useful when writing programs with multiple options depending on the main arguments. The initial parser will only capture the help or global options and pass through everything else. Additional argument parsing calls are invoked on the remaining arguments based on the initial input.
In go-getoptions
this is accomplished with:
-
opt.SetUnknownMode(getoptions.Pass)
.
The opposite of the above option. Useful if you want to ensure there are no input mistakes and force the application to stop.
In go-getoptions
this is the default behaviour.
It can be explicitly set with:
opt.SetUnknownMode(getoptions.Fail)
.
Less strict parsing of options. This will warn the user that the option used is not a valid option but it will not stop the rest of the program.
In go-getoptions
this is accomplished with:
-
opt.SetUnknownMode(getoptions.Warn)
.
Options should be allowed to have different aliases.
For example, the same option could be invoked with --address
or --hostname
.
In go-getoptions
, pass opt.Alias("my-alias")
to any option.
For example:
opt.BoolVar(&flag, "flag", false, opt.Alias("alias", "alias-2"))
Finally, to know with what alias an option was called with used opt.CalledAs(<name>)
.
Mark an option as required. Return an error if the option is not called.
In go-getoptions
, pass opt.Required()
to any option.
For example:
opt.BoolVar(&flag, "flag", false, opt.Required())
Optionally, override the default error message with opt.Required(msg)
.
For example:
opt.BoolVar(&flag, "flag", false, opt.Required("Missing --flag!"))
Some options can be passed more than once to increment an internal counter. For example:
command --v --v --v
Could increase the verbosity level each time the option is passed.
In go-getoptions
this is accomplished with:
-
ptr := opt.Increment(name, default_value)
. -
opt.IncrementVar(&ptr, name, default_value)
.
The option parser could provide converters to additional types. The disadvantage of providing non basic types is that the option parser grows in size.
Not yet implemented in go-getoptions
.
With regular options, when the argument is not passed (for example: --level
instead of --level=debug
) you will get a Missing argument error.
When using options with optional arguments, If the argument is not passed, the option will set the default value for the option type.
For this feature to be fully effective in strong typed languages where types have defaults, there must be a means to query the option parser to determine whether or not the option was called or not.
In go-getoptions
this is accomplished with:
-
ptr := opt.StringOptional(name, default_value)
. -
ptr := opt.IntOptional(name, default_value)
. -
ptr := opt.Float64Optional(name, default_value)
. -
The above should be used in combination with
opt.Called(name)
.
For example, for the following definition:
ptr := opt.StringOptional("level", "info")
-
If the option
level
is called with just--level
, the value of*ptr
is the default"info"
and queryingopt.Called("level")
returnstrue
. -
If the option
level
is called with--level=debug
, the value of*ptr
is"debug"
and queryingopt.Called("level")
returnstrue
. -
Finally, If the option
level
is not called, the value of*ptr
is the default"info"
and queryingopt.Called("level")
returnsfalse
.
Notice how so far only long options (options starting with double dash --
) have been mentioned.
There are 3 main ways to handle short options (options starting with only one dash -
).
The behaviour for long options (options starting with double dash --
) is consistent across operation modes.
The behaviour for short options (options starting with only one dash -
) depends on the operation mode.
The sections below show the different operation modes.
Given argument | Interpretation |
---|---|
--opt |
option: |
--opt=arg |
|
-opt |
option: |
-opt=arg |
Set by defining opt.SetMode(getoptions.Bundling)
.
Given option | Interpretation |
---|---|
--opt |
option: |
--opt=arg |
|
-opt |
option: |
-opt=arg |
Set by defining opt.SetMode(getoptions.SingleDash)
.
Given option | Interpretation |
---|---|
--opt |
option: |
--opt=arg |
|
-opt |
|
-opt=arg |
The biggest misfeature an option parser can have is to automatically generate the help message for the programmer. This seemingly helpful feature has caused most tools not to have proper man pages anymore and to have all verbose descriptions mixed in the help synopsis.
If you are writing a mid to large tool, don’t be lazy, write a man page for your program! If you are looking for options, asciidoctor has a manpage backend that can generate manpages written in the Asciidoc markup.
For the help synopsis, however, use the automated help. It even shows when an option can be set with environment variables.
For example, the following is a script using the built in help:
$ ./aws-configure -h NAME: aws-configure - Generate default ~/.aws/config and ~/.aws/credentials configuration. When a role is passed, it allows the use of the role in the default profile. NOTE: Remember to unset AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY after use. SYNOPSIS: aws-configure --access-key-id <string> --region <string> --secret-access-key <string> [--debug] [--help|-?] [--output-dir <string>] [--role-arn <string>] [--version|-V] [<args>] REQUIRED PARAMETERS: --access-key-id <string> AWS Access Key ID. (env: AWS_ACCESS_KEY_ID) --region <string> Default Region. (env: AWS_DEFAULT_REGION) --secret-access-key <string> AWS Secret Access Key. (env: AWS_SECRET_ACCESS_KEY) OPTIONS: --debug (default: false) --help|-? (default: false) --output-dir <string> Where to place the config and credentials file. (default: "/home/david/.aws") --role-arn <string> Role ARN. (default: "", env: AWS_ROLE_ARN) --version|-V (default: false)
And below is the output of the automated help of a program with multiple commands:
$ menu SYNOPSIS: menu [--config <string>] [--debug] [--help|-?] [--profile <string>] [--region <string>] [--role <string>] [--version|-V] <command> [<args>] COMMANDS: docker docker tasks help Use 'menu help <command>' for extra details. instance Actions on your deployed instances terraform Run terraform commands from inside the container OPTIONS: --config <string> (default: "config.yml") --debug (default: false) --help|-? (default: false) --profile <string> (default: "default") --region <string> (default: "us-west-2") --role <string> (default: "") --version|-V (default: false) Use 'menu help <command>' for extra details.
This section describes how the parser resolves ambiguities between the program and the command.
Given a definition like:
func main() { var profile, password string opt := New() opt.SetUnknownMode(Pass) opt.StringVar(&profile, "profile", "") command := NewCommand() command.StringVar(&password, "password", "") opt.Command(command.Self("command", "").SetCommandFn(commandFn)) remaining, err := opt.Parse(os.Args[1:]) ... err = opt.Dispatch("help", remaining) ... }
func commandFn(opt *getoptions.GetOpt, args []string) error { args, err := opt.Parse(remaining) ... }
There is an option at the parent, profile
and one at the command, password
.
Passing --p <arg>
is ambiguous and results in an error.
At minimum, --pr <arg>
and --pa <arg>
are required.
Given a definition like:
func main() { var profile, password string opt := New() opt.SetUnknownMode(Pass) opt.StringVar(&profile, "profile", "") command := NewCommand() command.StringVar(&password, "password", "", opt.Alias("p")) opt.Command(command.Self("command", "").SetCommandFn(commandFn)) remaining, err := opt.Parse(os.Args[1:]) ... err = opt.Dispatch("help", remaining) ... }
func commandFn(opt *getoptions.GetOpt, args []string) error { args, err := opt.Parse(remaining) ... }
There is an option at the parent, profile
and one at the command, password
with alias p
.
Passing --p <arg>
at the parent results in the parent opt.Parse
call to leave the --p <arg>
option unhandled and leave it in the remaining slice.
The opt.Dispatch
call gets the -p <arg>
option and throws an error.
At minimum, --pr <arg>
is required to call profile
at the parent and command options must be passed after the command declaration.
For example, the calls below is correct:
$ ./program -pr <profile> command -p <password>
$ ./program command -pr <profile> -p <password>
But the following one is incorrect:
./program -pr <profile> -p <password> command
Initial support for environment variables has been added.
Currently, only:
- opt.Bool
and opt.BoolVar
- opt.String
, opt.StringVar
, opt.StringOptional
, and opt.StringVarOptional
- opt.Int
, opt.IntVar
, opt.IntOptional
, and opt.IntVarOptional
- opt.Float64
, opt.Float64Var
, opt.Float64Optional
, and opt.Float64VarOptional
To use it, set the option modify function to opt.GetEnv. For example:
var profile string
opt.StringVar(&profile, "profile", "default", opt.GetEnv("AWS_PROFILE"))
Or:
profile := opt.String("profile", "default", opt.GetEnv("AWS_PROFILE"))
Note
|
Non supported option types behave with a No-Op when opt.GetEnv is defined.
|
When using opt.GetEnv
with opt.Bool
or opt.BoolVar
, only the words "true" or "false" are valid.
They can be provided in any casing, for example: "true", "True" or "TRUE".
Note
|
For numeric values, opt.Int and opt.Float64 and their derivatives, environment variable string conversion errors are ignored and the default value is assigned.
|
The Roadmap isn’t clear given that there might not be enough value in implementing all of them.
-
Handle
opt.Int
andopt.Float64
errors.- StringSlice and StringSliceVar
-
Comma separated? ← Most likely
Comma space separated? Proper CSV parsing to allow comma escaping?
- IntSlice and IntSliceVar
-
Comma separated?
- StringMap and StringMapVar
-
Comma separated key=value?
-
Create new error description for errors when parsing integer ranges (
1..3
). -
Option that runs a function?
-
Case insensitive matching.
-
Option values in the bundle:
-h1024w800
→-h 1024 -w 800
-
prefix and prefix_pattern. The string that starts options. Defaults to "--" and "-" but could include "/" to support Win32 style argument handling.
-
Supports argument dividers other than '='. For example: You could define ':' and use
--string=mystring
,--string:mystring
and--string mystring
. -
All other Perl’s Getopt::Long goodies that seem reasonable to add!
-
Remove need for
opt.HelpCommand("")
when dispatch is defined.Currently we have to define the
opt.Bool("help", false)
flag, thenopt.HelpCommand("")
, and finallyopt.Dispatch(ctx, "help", remaining)
.opt.HelpCommand
is redundant. Additionallyopt.HelpCommand
has thehelp
command name hardcoded in it.
This file is part of go-getoptions.
Copyright © 2015-2021 David Gamba Rios
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.