commando

package module
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Apr 9, 2020 License: MIT Imports: 6 Imported by: 72

README

Commando

logo

go-version   Build

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.

logo

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() {

	// set CLI executable, version and description
	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) {
			//fmt.Println("event-name: ", eventName)
		})

	// configure the root-command
	// $ reactor <category>  --verbose|-V  --version|-v  --help|-h
	commando.
		Register(nil).
		AddArgument("category", "category of the information to look for", ""). // required
		AddFlag("verbose,V", "display log information ", commando.Bool, nil).   // optional
		SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
			// print arguments
			for k, v := range args {
				fmt.Printf("arg -> %v: %v(%T)\n", k, v.Value, v.Value)
			}

			// print flags
			for k, v := range flags {
				fmt.Printf("flag -> %v: %v(%T)\n", k, v.Value, v.Value)
			}
		})

	// register `create` sub-command
	// $ reactor create <name> [type]  --dir|-d <dir>  --type|-t [type]  --timeout [timeout]  --verbose|-v  --help|-h
	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", "").                                  // required
		AddArgument("version", "version of the component", "1.0.0").                                 // optional
		AddArgument("files...", "files to remove once component is created", "").                    // variadic, optional
		AddFlag("dir, d", "output directory for the component files", commando.String, nil).         // required
		AddFlag("type, t", "type of the component to create", commando.String, "simple_type").       // optional
		AddFlag("timeout", "operation timeout in seconds", commando.Int, 60).                        // optional
		AddFlag("verbose,v", "display logs while creating the component files", commando.Bool, nil). // optional
		AddFlag("no-clean", "avoid cleanup of the component directory", commando.Bool, nil).         // optional, inverted flag
		SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
			// print arguments
			for k, v := range args {
				fmt.Printf("arg -> %v: %v(%T)\n", k, v.Value, v.Value)
			}

			// print flags
			for k, v := range flags {
				fmt.Printf("flag -> %v: %v(%T)\n", k, v.Value, v.Value)
			}
		})

	// register `serve` sub-command
	// $ reactor serve --verbose|-v  --help|-h
	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). // optional
		SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
			// print arguments
			for k, v := range args {
				fmt.Printf("arg -> %v: %v(%T)\n", k, v.Value, v.Value)
			}

			// print flags
			for k, v := range flags {
				fmt.Printf("flag -> %v: %v(%T)\n", k, v.Value, v.Value)
			}
		})

	// register `build` sub-command
	// $ reactor build  --dir|-d <dir>  --verbose|-v  --help|-h
	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).      // required
		AddFlag("verbose,v", "display logs while serving the project", commando.Bool, nil). // optional
		SetAction(func(args map[string]commando.ArgValue, flags map[string]commando.FlagValue) {
			// print arguments
			for k, v := range args {
				fmt.Printf("arg -> %v: %v(%T)\n", k, v.Value, v.Value)
			}

			// print flags
			for k, v := range flags {
				fmt.Printf("flag -> %v: %v(%T)\n", k, v.Value, v.Value)
			}
		})

	// parse command-line arguments from the STDIN
	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.

Documentation

Overview

Package commando helps you create 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 and optional arguments.

Index

Constants

View Source
const (
	// bool data type
	Bool = iota

	// int data type
	Int

	// string data type
	String
)

data type declarations for the flag values

View Source
const (
	EventVersion = "version"
	EventHelp    = "help"
)

event names

Variables

View Source
var DefaultCommandRegistry = NewCommandRegistry()

DefaultCommandRegistry holds the default registry.

Functions

func Parse

func Parse(osArgs []string)

Parse parses the command-line arguments for the `DefaultCommandRegistry` registry.

Types

type Arg

type Arg struct {

	// clapper argument config
	ClpArg *clapper.Arg

	// argument description
	Desc string

	// is argument required to be provided by the user
	IsRequired bool
}

Arg defines the configuration of an argument.

type ArgValue

type ArgValue struct {
	Arg

	// value of the argument
	Value string
}

ArgValue represents an argument value to pass as an argument in action function.

type Command

type Command struct {

	// description of the command
	Desc string

	// short-description of the command
	ShortDesc string

	// is root-command
	IsRoot bool

	// specific arguments to parse from the command-line arguments
	Args map[string]*Arg

	// flags to parse from the command-line arguments
	Flags map[string]*Flag

	// Action function
	Action func(map[string]ArgValue, map[string]FlagValue)
	// contains filtered or unexported fields
}

Command holds the configuration of a command.

func Register

func Register(name interface{}) *Command

Register registers a command in the `DefaultCommandRegistry` registry.

func (*Command) AddArgument

func (c *Command) AddArgument(name string, desc string, defaultValue string) *Command

AddArgument registers an argument for a command. When the defaultValue is an empty string, a user needs to provide a value for this argument. If an argument name ends with `...`, it is an variadic argument.

func (*Command) AddFlag

func (c *Command) AddFlag(flagNames string, desc string, dataType int, defaultValue interface{}) *Command

AddFlag registers a flag for the command. The flagNames argument should contain "long,short" flag names (e.g. "version,v"). If dataType argument is `commando.Bool` (boolean), then the defaultValue argument is ignored. For non-boolean flags, if the defaultValue argument is `nil`, then the flag is required.

func (*Command) SetAction

func (c *Command) SetAction(action func(map[string]ArgValue, map[string]FlagValue)) *Command

SetAction registers a callback function with a command configuration that will execute after command-line arguments are parsed. If an action function is already registered with a command, it won't get registered again.

func (*Command) SetDescription

func (c *Command) SetDescription(desc string) *Command

SetDescription sets the description for a command.

func (*Command) SetShortDescription

func (c *Command) SetShortDescription(shortDesc string) *Command

SetShortDescription sets the short-description for a command.

type CommandRegistry

type CommandRegistry struct {

	// executable name of the command registry
	Executable string

	// version of the command-line interface
	Version string

	// description of the command-line interface
	Desc string

	// registered command configurations
	Commands map[string]*Command

	// event listener for version, help etc. events
	EventListener func(string)
	// contains filtered or unexported fields
}

CommandRegistry holds the registered command configurations. It also stores the version of the CLI application and its description.

func NewCommandRegistry

func NewCommandRegistry() *CommandRegistry

NewCommandRegistry returns a new value of registry and registers the root-command with a bare-minimum configuration to enable `--help` and `--version` command.

func SetExecutableName

func SetExecutableName(name string) *CommandRegistry

SetExecutableName sets the executable name of the `DefaultCommandRegistry` registry.

func (*CommandRegistry) Parse

func (cr *CommandRegistry) Parse(osArgs []string)

Parse parses the command-line arguments and executes the action function registered with the command. If there is an usage-error while parsing the command-line arguments, it will display a message in the console without returning an error. If osArgs is `nil`, Parse uses arguments received from `os.Args[1:]`.

func (*CommandRegistry) PrintHelp

func (cr *CommandRegistry) PrintHelp(c *Command)

PrintHelp prints the usage of the command or CLI application.

func (*CommandRegistry) PrintVersion

func (cr *CommandRegistry) PrintVersion()

PrintVersion prints version of the CLI application.

func (*CommandRegistry) Register

func (cr *CommandRegistry) Register(name interface{}) *Command

Register registers a command in the registry and adds `--help` flag automatically. If the root-command is registered, it adds the `--version` flags to display the version. The "name" argument must be a string. If `nil` is passed, the root-command is registered.

func (*CommandRegistry) SetDescription

func (cr *CommandRegistry) SetDescription(desc string) *CommandRegistry

SetDescription sets the description for the CLI application. This description will be printed with `--help` flag on the root-command.

func (*CommandRegistry) SetEventListener added in v1.0.3

func (cr *CommandRegistry) SetEventListener(listener func(string)) *CommandRegistry

SetEventListener registers a callback function with the registry. This function is executed with an event name when the user uses `--help` or `--version` flag. If this function is already registered, it won't get registered again.

func (*CommandRegistry) SetExecutableName

func (cr *CommandRegistry) SetExecutableName(name string) *CommandRegistry

SetExecutableName sets the executable name of the registry.

func (*CommandRegistry) SetVersion

func (cr *CommandRegistry) SetVersion(version string) *CommandRegistry

SetVersion sets the version for the CLI application. This version will be printed with the `--version` flag on the root-command.

type Flag

type Flag struct {

	// clapper flag config
	ClpFlag *clapper.Flag

	// flag description
	Desc string

	// data type of the flag value
	DataType int

	// default value of the flag
	DefaultValue interface{}

	// is flag required to be provided by the user
	IsRequired bool
}

Flag defines the configuration of a flag.

type FlagValue

type FlagValue struct {
	Flag
	Value interface{}
}

FlagValue represents a flag value to pass as an argument in action function. It also provides an easy interface to get value in an appropriate format.

func (FlagValue) GetBool

func (fv FlagValue) GetBool() (bool, error)

GetBool returns `bool` value of a flag.

func (FlagValue) GetInt

func (fv FlagValue) GetInt() (int, error)

GetInt returns `int` value of a flag.

func (FlagValue) GetString

func (fv FlagValue) GetString() (string, error)

GetString returns `string` value of a flag.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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