Commando
Commando helps you create beautiful CLI applications with ease. It parses "getopt(3)" style command-line arguments, supports sub-command architecture, allows a short-name alias for flags and captures required & optional arguments. The motivation behind creating this library is to provide easy-to-use APIs to create simple command-line tools.
This library uses clapper
package to parse command-line arguments. Visit the documentation of this package to understand supported arguments and flag patterns.
Documentation
pkg.go.dev
Tutorial
Medium
Installation
$ go get -u "github.com/thatisuday/commando"
Terminology
Let's imagine we are building a CLI tool reactor
to generate and manage React front-end projects.
Root command
$ reactor --version
$ reactor -v
$ reactor --help
$ reactor -h
In the example above, the reactor
alone is the root-command. Here, the --version
and -v
are flags. The --version
is a long flag name and -v
is a short flag name. The -v
is an alias for --version
. Similarly --help
and -h
are flags.
Commando adds --version
and --help
flags (along with their aliases) automatically for the root-command.
Sub command
$ reactor build --dir ./out/dir
In the above example, build
is a sub-command. This sub-command has --dir
flag, and unlike --help
flag, this flag takes a user-input value like ./out/dir
we have provided above.
Commando adds --help
flag automatically for a sub-command. Also, it registers help
and version
sub-commands automatically to display CLI usage and version number respectively.
Arguments
$ reactor create <name> --dir <dir>
In the above example, create
is a sub-command. The <name>
placeholder is for the name
argument value that the user is supposed to provide. For example, $ reactor create form --dir ./components/form
. Here, form
is the value for the name
argument.
The root-command can also be configured to take arguments. For example, $ reactor <category>
command is valid. If the category
argument value doesn't match with any registered sub-command, then the root-command is executed, else the sub-command is executed.
How to configure?
Step 1: executable, version and description setup
First, we need to specify the name of the executable file using commando.SetExecutableName
function. This value is same as your root-command name. You can use os.Executable
function to get the name of the executable. Normally, you just specify the name of your package because when the user install your module using go get
command, Go creates an executable file with the name same as your package name.
Then we need to set the version and the description of our CLI application using CommandRegistry.SetVersion
and CommandRegistry.SetDescription
function respectively. This version will be displayed with the --version
flag (or version
sub-command) and description will be displayed with --help
flag (or help
sub-command).
import "github.com/thatisuday/commando"
func main() {
commando.
SetExecutableName("reactor").
SetVersion("v1.0.0").
SetDescription("This CLI tool helps you create and manage React projects.")
}
Step 2: Register a sub-command
A sub-command is registered using commando.Register("<sub-command>")
function. This function returns the *commando.Command
object. If the sub-command is already registered, it returns the registered *commando.Command
object.
If nil
is passed as an argument, then the root-command is registered. However, Commando automatically registers the root-command and commando.Register(nil)
returns the *commando.Command
object of the root-command.
Commando automatically registers the root-command to provide built-in support for --help
and --version
flags, hence you do not need to manually register the root-command unless you want to configure it.
The *commando.Command
object is a struct and it provides SetDescription
, AddArgument
, AddFlag
, and SetAction
methods.
- The
SetDescription
method sets the description of the command. - The
AddArgument
method registers an argument with the command. - The
AddFlag
method registers a flag with the command. - The
SetAction
method registers a function that will be executed with argument values and flag values provided by the user when the root-command or a sub-command is executed by the user.
Step 3: Set a description of a sub-command
commando.
Register("<sub-command>").
SetDescription("<sub-command-description>")
SetShortDescription("<sub-command-short-description>")
The SetDescription
and SetShortDescription
method takes a string
argument tp set the long and a short description of the sub-command and returns the *commando.Command
object of the same command. These descriptions are printed when user executes the $ reactor <sub-command> --help
command.
You can set a description of the root-command by passing nil
as an argument to the Register()
function.
Step 4: Add an argument
commando.
Register("<sub-command>").
AddArgument("<name>", "<description>", "<default-value>")
The AddArgument
method registers an argument with the command. The first argument is the name of the argument and the second argument is the description of that argument.
The third argument is a string
value which is the default-value of the argument. If user doesn't provide the value of this argument, this argument will get the value from the default-value. If the default-value is an empty string (""), then it becomes a required argument. If the value of a required argument is not provided by the user, an error message is displayed.
Ideally, you should register all required arguments before the optional arguments. Since these are positional values, it is mandatory to do so, else you would get inappropriate results.
If the argument name ends with ...
suffix, then it is considered as a variadic argument. A variadic argument stores all the leftover argument values and concatenate them using command comma (,
). Hence a command should only contain one variadic argument and it should be registered after all arguments are registered.
If the argument is already registered, then registration of the argument is skipped without returning an error. You can configure arguments of the root-command by passing nil
as an argument to the Register()
function.
Step 5: Add a flag
commando.
Register("<sub-command>").
AddFlag("<long-name>,<short-name>", "<description>", <dataType>, <defaultValue>).
The AddFlag
method registers a flag with the command. The first argument is string
value containing the long-name and the short-name of the flag separated by a comma. For example, dir,d
is a valid argument value for --dir
and -d
flags. You can skip the short-name registration if you do not need one by providing only long-name value, like dir
.
The second argument sets the description of the flag. This will be displayed with the usage of the command (--help
flag).
The third argument is the data-type of the value that will be provided by the user for this flag. The value of this argument could be either commando.Bool
, commando.Int
or commando.String
. If the data-type is commando.Bool
, then the flag does not take any user input (like --version
flag).
The last argument is the default-value of the flag. The value of this argument must be of the data-type provided in the previous argument. If nil
value is provided, then the flag doesn't have any default-value and it becomes required to be provided by the user, except if the data-type is commando.Bool
in which case the default-value is false
automatically.
If the flag name starts with no-
prefix, for example no-clean
, then it is considered as an inverted flag. Default value of an inverted flag is true
. When --no-clean
flag is provided, value of this flag becomes false
. This flag is stored without no-
prefix, like clean
here, however, --clean
is not a valid flag.
If the flag is already registered, then registration of the flag is skipped without returning an error. You should avoid using the same short-name for multiple flags. You can configure flags of the root-command by passing nil
as an argument to the Register()
function.
Step 6: Register an action
commando.
Register("<sub-command>").
SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
})
The SetAction
function registers a callback function that will be executed when the root-command or the sub-command is executed by the user. This function is called with the argument values and the flag values provided by the user. If the required argument or a required flag is not provided by the user, this function won't be executed and an error message is shown to the user.
The first argument is a map
that contains the argument values. The keys of this map are names of the arguments and values are the values of the arguments provided by the user. The values of this map are structs of type commando.ArgValue
that contains the argument value and other meta-data provided by you during the registration of the argument.
The second argument is a map
that contains the flag values. The keys of this map are long-names of the flags and values are the values of the flags provided by the user (or the default-values). The values of this map are structs of type commando.FlagValue
that contains the flag value and other meta-data provided by you during the registration of the flag.
The data-type of the Value
field of the commando.ArgValue
type is string
. However, the data-type of the commando.FlagValue
type is an empty interface interface{}
. The concrete value of this field can be a bool
, an int
or a string
based on the data-type specified in the flag registration. You should manually extract the concrete value using type-assertion. The commando.FlagValue
also provides GetBool
, GetInt
and GetString
methods to return the flag-value in the correct format.
Step 7: Parse the command-line arguments
commando.Parse(nil)
The Parse
function parses the command-line arguments provided by the user. Commando executes a sub-command or the root-command based on these values. If the sub-command is not registered or arguments/flags values provided by the user are invalid, then an error message is shown to the user. Else, the action function registered with the command is executed.
commando.Parse([]string{})
You can also pass a custom list of command-line arguments. In this case, Commando won't read command-line arguments from the standard-input (terminal).
Example
package main
import (
"fmt"
"github.com/thatisuday/commando"
)
func main() {
commando.
SetExecutableName("reactor").
SetVersion("v1.0.0").
SetDescription("Reactor is a command-line tool to generate React projects.\nIt helps you create components, write test cases, start a development server and much more.").
SetEventListener(func(eventName string) {
})
commando.
Register(nil).
AddArgument("category", "category of the information to look for", "").
AddFlag("verbose,V", "display log information ", commando.Bool, nil).
SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
for k, v := range args {
fmt.Printf("arg -> %v: %v(%T)\n", k, v.Value, v.Value)
}
for k, v := range flags {
fmt.Printf("flag -> %v: %v(%T)\n", k, v.Value, v.Value)
}
})
commando.
Register("create").
SetDescription("This command creates a component of a given type and outputs component files in the project directory.").
SetShortDescription("creates a component").
AddArgument("name", "name of the component to create", "").
AddArgument("version", "version of the component", "1.0.0").
AddArgument("files...", "files to remove once component is created", "").
AddFlag("dir, d", "output directory for the component files", commando.String, nil).
AddFlag("type, t", "type of the component to create", commando.String, "simple_type").
AddFlag("timeout", "operation timeout in seconds", commando.Int, 60).
AddFlag("verbose,v", "display logs while creating the component files", commando.Bool, nil).
AddFlag("no-clean", "avoid cleanup of the component directory", commando.Bool, nil).
SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
for k, v := range args {
fmt.Printf("arg -> %v: %v(%T)\n", k, v.Value, v.Value)
}
for k, v := range flags {
fmt.Printf("flag -> %v: %v(%T)\n", k, v.Value, v.Value)
}
})
commando.
Register("serve").
SetDescription("This command starts the Webpack dev-server on an available port.").
SetShortDescription("starts a development server").
AddFlag("verbose,v", "display logs while serving the project", commando.Bool, nil).
SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
for k, v := range args {
fmt.Printf("arg -> %v: %v(%T)\n", k, v.Value, v.Value)
}
for k, v := range flags {
fmt.Printf("flag -> %v: %v(%T)\n", k, v.Value, v.Value)
}
})
commando.
Register("build").
SetDescription("This command builds the project with Webpack and outputs the build files in the given directory.").
SetShortDescription("creates build artifacts").
AddFlag("dir,d", "output directory of the build files", commando.String, nil).
AddFlag("verbose,v", "display logs while serving the project", commando.Bool, nil).
SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
for k, v := range args {
fmt.Printf("arg -> %v: %v(%T)\n", k, v.Value, v.Value)
}
for k, v := range flags {
fmt.Printf("flag -> %v: %v(%T)\n", k, v.Value, v.Value)
}
})
commando.Parse(nil)
}
Usage of the root-command
$ reactor --help
$ reactor -h
$ reactor help
Reactor is a command-line tool to generate React projects.
It helps you create components, write test cases, start a development server and much more.
Usage:
reactor <category> {flags}
reactor <command> {flags}
Commands:
build creates build artifacts
create creates a component
help displays usage informationn
serve starts a development server
version displays version number
Arguments:
category category of the information to look for
Flags:
-h, --help displays usage information of the application or a command (default: false)
-V, --verbose display log information (default: false)
-v, --version displays version number (default: false)
You can use CommandRegistry.SetEventListener
function to add a callback function when usage information is displayed.
Version of the CLI application
$ reactor version
$ reactor --version
$ reactor -v
Version: v1.0.0
You can use CommandRegistry.SetEventListener
function to add a callback function when version information is displayed.
Usage of the sub-command
$ reactor create --help
$ reactor create -h
This command creates a component of a given type and outputs component files in the project directory.
Usage:
reactor <name> [version] [files] {flags}
Arguments:
name name of the component to create
version version of the component (default: 1.0.0)
files files to remove once component is created {variadic}
Flags:
--no-clean avoid cleanup of the component directory (default: true)
-d, --dir output directory for the component files
-h, --help displays usage information of the application or a command (default: false)
--timeout operation timeout in seconds (default: 60)
-t, --type type of the component to create (default: simple_type)
-v, --verbose display logs while creating the component files (default: false)
Executing the root-command
$ reactor --verbose
Error: value of the category argument can not be empty.
Oops! A user needs to provide the value of the category argument since it is a required argument.
$ reactor service --verbose
$ reactor service -V
arg -> category: service(string)
flag -> version: false(bool)
flag -> help: false(bool)
flag -> verbose: true(bool)
Executing a sub-command
$ reactor create
Error: value of the name argument can not be empty.
Oops! A user needs to provide the value of the name argument since it is a required argument.
$ reactor create my-service
Error: value of the --dir flag can not be empty.
Oops! A user needs to provide the value of the dir flag since it is a required argument (no default-value).
$ reactor create my-service -t service --dir ./service/my-service --timeout 10sec
Error: value of the --timeout flag must be an integer.
Oops! Since we have specified that the value of the --timeout
flag must be an integer, a user needs to provide an integer value.
$ reactor create my-service -t service --dir ./service/my-service --timeout 10
arg -> name: my-service(string)
arg -> version: 1.0.0(string)
arg -> files: (string)
flag -> dir: ./service/my-service(string)
flag -> type: service(string)
flag -> timeout: 10(int)
flag -> verbose: false(bool)
flag -> clean: true(bool)
flag -> help: false(bool)
Here, the value of the version
argument the default value we provided earlier since the user did not provide any value for it. Also, since it is an optional argument, Commando did not print any errors.
$ reactor create my-service 2.0.5 file1.txt file2.txt file3.txt -t service --dir=./service/my-service --timeout 10 -v --no-clean
arg -> name: my-service(string)
arg -> version: 2.0.5(string)
arg -> files: file1.txt,file2.txt,file3.txt(string)
flag -> help: false(bool)
flag -> dir: ./service/my-service(string)
flag -> type: service(string)
flag -> timeout: 10(int)
flag -> verbose: true(bool)
flag -> clean: false(bool)
How to create a CLI application?
The example above is a clear demonstration of how a CLI application can be created, however, you can follow this tutorial on Medium. Here are a few things you should be concerned about.
- Keep your argument names and flag names as simple as possible.
- Try not to override the
--help
or --version
flags and their short-names. - Do not configure the root-command unless necessary. You do not need to set an action function for the root-command. If the action function is missing for the root-command, it won't generate any output or an error.
- Register all optional arguments of a command before the required arguments.
- Do not modify commands after
commando.Parse()
is called.
Your code must be part of the main
package like we have seen in the previous example. It's better if your work with the [Go modules] so that a user can install your application from anywhere on the system.
A user can install the CLI application using GO111MODULE=on go get "github.com/<username>/<module-name>"
command. Since your code is part of the main
package, Go creates <module-name>
binary executable file inside GOBIN
directory that is supposed to be in the PATH
of the system.