Giter Site home page Giter Site logo

mow.cli's Introduction

mow.cli

CI GoDoc Coverage Status

Package cli provides a framework to build command line applications in Go with most of the burden of arguments parsing and validation placed on the framework instead of the user.

Getting Started

The following examples demonstrate basic usage the package.

Simple Application

In this simple application, we mimic the argument parsing of the standard UNIX cp command. Our application requires the user to specify one or more source files followed by a destination. An optional recursive flag may be provided.

package main

import (
	"fmt"
	"os"

	"github.com/jawher/mow.cli"
)

func main() {
	// create an app
	app := cli.App("cp", "Copy files around")

	// Here's what differentiates mow.cli from other CLI libraries:
	// This line is not just for help message generation.
	// It also validates the call to reject calls with less than 2 arguments
	// and split the arguments between SRC or DST
	app.Spec = "[-r] SRC... DST"

	var (
		// declare the -r flag as a boolean flag
		recursive = app.BoolOpt("r recursive", false, "Copy files recursively")
		// declare the SRC argument as a multi-string argument
		src = app.StringsArg("SRC", nil, "Source files to copy")
		// declare the DST argument as a single string (string slice) arguments
		dst = app.StringArg("DST", "", "Destination where to copy files to")
	)

	// Specify the action to execute when the app is invoked correctly
	app.Action = func() {
		fmt.Printf("Copying %v to %s [recursively: %v]\n", *src, *dst, *recursive)
	}

	// Invoke the app passing in os.Args
	app.Run(os.Args)
}

Pointers to existing variables

This variant of the cp command uses the Ptr variants, where you can pass pointers to existing variables instead of declaring new ones for the options/arguments:

package main

import (
	"fmt"
	"os"

	cli "github.com/jawher/mow.cli"
)

type Config struct {
	Recursive bool
	Src       []string
	Dst       string
}

func main() {
	var (
		app = cli.App("cp", "Copy files around")
		cfg Config
	)
	// Here's what differentiates mow.cli from other CLI libraries:
	// This line is not just for help message generation.
	// It also validates the call to reject calls with less than 2 arguments
	// and split the arguments between SRC or DST
	app.Spec = "[-r] SRC... DST"

	// declare the -r flag as a boolean flag
	app.BoolOptPtr(&cfg.Recursive, "r recursive", false, "Copy files recursively")
	// declare the SRC argument as a multi-string argument
	app.StringsArgPtr(&cfg.Src, "SRC", nil, "Source files to copy")
	// declare the DST argument as a single string (string slice) arguments
	app.StringArgPtr(&cfg.Dst, "DST", "", "Destination where to copy files to")

	// Specify the action to execute when the app is invoked correctly
	app.Action = func() {
		fmt.Printf("Copying using config: %+v\n", cfg)
	}
	// Invoke the app passing in os.Args
	app.Run(os.Args)
}

Multi-Command Application

In the next example, we create a multi-command application in the same style as familiar commands such as git and docker. We build a fictional utility called uman to manage users in a system. It provides two commands that can be invoked: list and get. The list command takes an optional flag to specify all users including disabled ones. The get command requires one argument, the user ID, and takes an optional flag to specify a detailed listing.

package main

import (
	"fmt"
	"os"

	"github.com/jawher/mow.cli"
)

func main() {
	app := cli.App("uman", "User Manager")

	app.Spec = "[-v]"

	var (
		verbose = app.BoolOpt("v verbose", false, "Verbose debug mode")
	)

	app.Before = func() {
		if *verbose {
			// Here we can enable debug output in our logger for example
			fmt.Println("Verbose mode enabled")
		}
	}

	// Declare our first command, which is invocable with "uman list"
	app.Command("list", "list the users", func(cmd *cli.Cmd) {
		// These are the command-specific options and args, nicely scoped
		// inside a func so they don't pollute the namespace
		var (
			all = cmd.BoolOpt("all", false, "List all users, including disabled")
		)

		// Run this function when the command is invoked
		cmd.Action = func() {
			// Inside the action, and only inside, we can safely access the
			// values of the options and arguments
			fmt.Printf("user list (including disabled ones: %v)\n", *all)
		}
	})

	// Declare our second command, which is invocable with "uman get"
	app.Command("get", "get a user details", func(cmd *cli.Cmd) {
		var (
			detailed = cmd.BoolOpt("detailed", false, "Display detailed info")
			id       = cmd.StringArg("ID", "", "The user id to display")
		)

		cmd.Action = func() {
			fmt.Printf("user %q details (detailed mode: %v)\n", *id, *detailed)
		}
	})

	// With the app configured, execute it, passing in the os.Args array
	app.Run(os.Args)
}

A Larger Multi-Command Example

This example shows an alternate way of organizing our code when dealing with a larger number of commands and subcommands. This layout emphasizes the command structure and defers the details of each command to subsequent functions. Like the prior examples, options and arguments are still scoped to their respective functions and don't pollute the global namespace.

package main

import (
	"fmt"
	"os"

	"github.com/jawher/mow.cli"
)

// Global options available to any of the commands
var filename *string

func main() {
	app := cli.App("vault", "Password Keeper")

	// Define our top-level global options
	filename = app.StringOpt("f file", os.Getenv("HOME")+"/.safe", "Path to safe")

	// Define our command structure for usage like this:
	app.Command("list", "list accounts", cmdList)
	app.Command("creds", "display account credentials", cmdCreds)
	app.Command("config", "manage accounts", func(config *cli.Cmd) {
		config.Command("list", "list accounts", cmdList)
		config.Command("add", "add an account", cmdAdd)
		config.Command("remove", "remove an account(s)", cmdRemove)
	})

	app.Run(os.Args)
}

// Sample use: vault list OR vault config list
func cmdList(cmd *cli.Cmd) {
	cmd.Action = func() {
		fmt.Printf("list the contents of the safe here")
	}
}

// Sample use: vault creds reddit.com
func cmdCreds(cmd *cli.Cmd) {
	cmd.Spec = "ACCOUNT"
	account := cmd.StringArg("ACCOUNT", "", "Name of account")
	cmd.Action = func() {
		fmt.Printf("display account info for %s\n", *account)
	}
}

// Sample use: vault config add reddit.com -u username -p password
func cmdAdd(cmd *cli.Cmd) {
	cmd.Spec = "ACCOUNT [ -u=<username> ] [ -p=<password> ]"
	var (
		account  = cmd.StringArg("ACCOUNT", "", "Account name")
		username = cmd.StringOpt("u username", "admin", "Account username")
		password = cmd.StringOpt("p password", "admin", "Account password")
	)
	cmd.Action = func() {
		fmt.Printf("Adding account %s:%s@%s", *username, *password, *account)
	}
}

// Sample use: vault config remove reddit.com twitter.com
func cmdRemove(cmd *cli.Cmd) {
	cmd.Spec = "ACCOUNT..."
	var (
		accounts = cmd.StringsArg("ACCOUNT", nil, "Account names to remove")
	)
	cmd.Action = func() {
		fmt.Printf("Deleting accounts: %v", *accounts)
	}
}

Comparison to Other Tools

There are several tools in the Go ecosystem to facilitate the creation of command line tools. The following is a comparison to the built-in flag package as well as the popular urfave/cli (formerly known as codegangsta/cli):

mow.cli urfave/cli flag
Contextual help
Commands
Option folding -xyz
Option value folding -fValue
Option exclusion --start ❘ --stop
Option dependency [-a -b] or [-a [-b]]
Arguments validation SRC DST
Argument optionality SRC [DST]
Argument repetition SRC... DST
Option/argument dependency SRC [-f DST]
Any combination of the above [-d ❘ --rm] IMAGE [COMMAND [ARG...]]

Unlike the simple packages above, docopt is another library that supports rich set of flag and argument validation. It does, however, fall short for many use cases including:

mow.cli docopt
Contextual help
Backtracking SRC... DST
Backtracking [SRC] DST
Branching (SRC ❘ -f DST)

Installation

To install this package, run the following:

go get github.com/jawher/mow.cli

Package Documentation

Package cli provides a framework to build command line applications in Go with most of the burden of arguments parsing and validation placed on the framework instead of the user.

Basics

To create a new application, initialize an app with cli.App. Specify a name and a brief description for the application:

cp := cli.App("cp", "Copy files around")

To attach code to execute when the app is launched, assign a function to the Action field:

cp.Action = func() {
    fmt.Printf("Hello world\n")
}

To assign a version to the application, use Version method and specify the flags that will be used to invoke the version command:

cp.Version("v version", "cp 1.2.3")

Finally, in the main func, call Run passing in the arguments for parsing:

cp.Run(os.Args)

Options

To add one or more command line options (also known as flags), use one of the short-form StringOpt, StringsOpt, IntOpt, IntsOpt, Float64Opt, Floats64Opt, or BoolOpt methods on App (or Cmd if adding flags to a command or a subcommand). For example, to add a boolean flag to the cp command that specifies recursive mode, use the following:

recursive := cp.BoolOpt("R recursive", false, "recursively copy the src to dst")

or:

cp.BoolOptPtr(&cfg.recursive, "R recursive", false, "recursively copy the src to dst")

The first version returns a new pointer to a bool value which will be populated when the app is run, whereas the second version will populate a pointer to an existing variable you specify.

The option name(s) is a space separated list of names (without the dashes). The one letter names can then be called with a single dash (short option, -R), the others with two dashes (long options, --recursive).

You also specify the default value for the option if it is not supplied by the user.

The last parameter is the description to be shown in help messages.

There is also a second set of methods on App called String, Strings, Int, Ints, and Bool, which accept a long-form struct of the type: cli.StringOpt, cli.StringsOpt, cli.IntOpt, cli.IntsOpt, cli.Float64Opt, cli.Floats64Opt, cli.BoolOpt. The struct describes the option and allows the use of additional features not available in the short-form methods described above:

recursive = cp.Bool(cli.BoolOpt{
    Name:       "R recursive",
    Value:      false,
    Desc:       "copy src files recursively",
    EnvVar:     "VAR_RECURSIVE",
    SetByUser:  &recursiveSetByUser,
})

Or:

recursive = cp.BoolPtr(&recursive, cli.BoolOpt{
    Name:       "R recursive",
    Value:      false,
    Desc:       "copy src files recursively",
    EnvVar:     "VAR_RECURSIVE",
    SetByUser:  &recursiveSetByUser,
})

The first version returns a new pointer to a value which will be populated when the app is run, whereas the second version will populate a pointer to an existing variable you specify.

Two features, EnvVar and SetByUser, can be defined in the long-form struct method. EnvVar is a space separated list of environment variables used to initialize the option if a value is not provided by the user. When help messages are shown, the value of any environment variables will be displayed. SetByUser is a pointer to a boolean variable that is set to true if the user specified the value on the command line. This can be useful to determine if the value of the option was explicitly set by the user or set via the default value.

You can only access the values stored in the pointers in the Action func, which is invoked after argument parsing has been completed. This precludes using the value of one option as the default value of another.

On the command line, the following syntaxes are supported when specifying options.

Boolean options:

-f         single dash one letter name
-f=false   single dash one letter name, equal sign followed by true or false
--force    double dash for longer option names
-it        single dash for multiple one letter names (option folding), this is equivalent to: -i -t

String, int and float options:

-e=value       single dash one letter name, equal sign, followed by the value
-e value       single dash one letter name, space followed by the value
-Ivalue        single dash one letter name, immediately followed by the value
--extra=value  double dash for longer option names, equal sign followed by the value
--extra value  double dash for longer option names, space followed by the value

Slice options (StringsOpt, IntsOpt, Floats64Opt) where option is repeated to accumulate values in a slice:

-e PATH:/bin    -e PATH:/usr/bin     resulting slice contains ["/bin", "/usr/bin"]
-ePATH:/bin     -ePATH:/usr/bin      resulting slice contains ["/bin", "/usr/bin"]
-e=PATH:/bin    -e=PATH:/usr/bin     resulting slice contains ["/bin", "/usr/bin"]
--env PATH:/bin --env PATH:/usr/bin  resulting slice contains ["/bin", "/usr/bin"]
--env=PATH:/bin --env=PATH:/usr/bin  resulting slice contains ["/bin", "/usr/bin"]

Arguments

To add one or more command line arguments (not prefixed by dashes), use one of the short-form StringArg, StringsArg, IntArg, IntsArg, Float64Arg, Floats64Arg, or BoolArg methods on App (or Cmd if adding arguments to a command or subcommand). For example, to add two string arguments to our cp command, use the following calls:

src := cp.StringArg("SRC", "", "the file to copy")
dst := cp.StringArg("DST", "", "the destination")

Or:

cp.StringArgPtr(&src, "SRC", "", "the file to copy")
cp.StringArgPtr(&dst, "DST", "", "the destination")

The first version returns a new pointer to a value which will be populated when the app is run, whereas the second version will populate a pointer to an existing variable you specify.

You then specify the argument as will be displayed in help messages. Argument names must be specified as all uppercase. The next parameter is the default value for the argument if it is not supplied. And the last is the description to be shown in help messages.

There is also a second set of methods on App called String, Strings, Int, Ints, Float64, Floats64 and Bool, which accept a long-form struct of the type: cli.StringArg, cli.StringsArg, cli.IntArg, cli.IntsArg, cli.BoolArg. The struct describes the arguments and allows the use of additional features not available in the short-form methods described above:

src = cp.Strings(StringsArg{
    Name:      "SRC",
    Desc:      "The source files to copy",
    Value:     "default value",
    EnvVar:    "VAR1 VAR2",
    SetByUser: &srcSetByUser,
})

Or:

src = cp.StringsPtr(&src, StringsArg{
    Name:      "SRC",
    Desc:      "The source files to copy",
    Value:     "default value",
    EnvVar:    "VAR1 VAR2",
    SetByUser: &srcSetByUser,
})

The first version returns a new pointer to a value which will be populated when the app is run, whereas the second version will populate a pointer to an existing variable you specify.

Two features, EnvVar and SetByUser, can be defined in the long-form struct method. EnvVar is a space separated list of environment variables used to initialize the argument if a value is not provided by the user. When help messages are shown, the value of any environment variables will be displayed. SetByUser is a pointer to a boolean variable that is set to true if the user specified the value on the command line. This can be useful to determine if the value of the argument was explicitly set by the user or set via the default value.

You can only access the values stored in the pointers in the Action func, which is invoked after argument parsing has been completed. This precludes using the value of one argument as the default value of another.

Operators

The -- operator marks the end of command line options. Everything that follows will be treated as an argument, even if starts with a dash. For example, the standard POSIX touch command, which takes a filename as an argument (and possibly other options that we'll ignore here), could be defined as:

file := cp.StringArg("FILE", "", "the file to create")

If we try to create a file named "-f" via our touch command:

$ touch -f

It will fail because the -f will be parsed as an option, not as an argument. The fix is to insert -- after all flags have been specified, so the remaining arguments are parsed as arguments instead of options as follows:

$ touch -- -f

This ensures the -f is parsed as an argument instead of a flag named f.

Commands

This package supports nesting of commands and subcommands. Declare a top-level command by calling the Command func on the top-level App struct. For example, the following creates an application called docker that will have one command called run:

docker := cli.App("docker", "A self-sufficient runtime for linux containers")

docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
    // initialize the run command here
})

The first argument is the name of the command the user will specify on the command line to invoke this command. The second argument is the description of the command shown in help messages. And, the last argument is a CmdInitializer, which is a function that receives a pointer to a Cmd struct representing the command.

Within this function, define the options and arguments for the command by calling the same methods as you would with top-level App struct (BoolOpt, StringArg, ...). To execute code when the command is invoked, assign a function to the Action field of the Cmd struct. Within that function, you can safely refer to the options and arguments as command line parsing will be completed at the time the function is invoked:

docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
    var (
        detached = cmd.BoolOpt("d detach", false, "Run container in background")
        memory   = cmd.StringOpt("m memory", "", "Set memory limit")
        image    = cmd.StringArg("IMAGE", "", "The image to run")
    )

    cmd.Action = func() {
        if *detached {
            // do something
        }
        runContainer(*image, *detached, *memory)
    }
})

Optionally, to provide a more extensive description of the command, assign a string to LongDesc, which is displayed when a user invokes --help. A LongDesc can be provided for Cmds as well as the top-level App:

cmd.LongDesc = `Run a command in a new container

With the docker run command, an operator can add to or override the
image defaults set by a developer. And, additionally, operators can
override nearly all the defaults set by the Docker runtime itself.
The operator’s ability to override image and Docker runtime defaults
is why run has more options than any other docker command.`

Subcommands can be added by calling Command on the Cmd struct. They can by defined to any depth if needed:

docker.Command("job", "actions on jobs", func(job *cli.Cmd) {
    job.Command("list", "list jobs", listJobs)
    job.Command("start", "start a new job", startJob)
    job.Command("log", "log commands", func(log *cli.Cmd) {
        log.Command("show", "show logs", showLog)
        log.Command("clear", "clear logs", clearLog)
    })
})

Command and subcommand aliases are also supported. To define one or more aliases, specify a space-separated list of strings to the first argument of Command:

job.Command("start run r", "start a new job", startJob)

With the command structure defined above, users can invoke the app in a variety of ways:

$ docker job list
$ docker job start
$ docker job run   # using the alias we defined
$ docker job r     # using the alias we defined
$ docker job log show
$ docker job log clear

Commands can be hidden in the help messages. This can prove useful to deprecate a command so that it does not appear to new users in the help, but still exists to not break existing scripts. To hide a command, set the Hidden field to true:

app.Command("login", "login to the backend (DEPRECATED: please use auth instead)", func(cmd *cli.Cmd)) {
    cmd.Hidden = true
}

As a convenience, to assign an Action to a func with no arguments, use ActionCommand when defining the Command. For example, the following two statements are equivalent:

app.Command("list", "list all configs", cli.ActionCommand(list))

// Exactly the same as above, just more verbose
app.Command("list", "list all configs", func(cmd *cli.Cmd)) {
    cmd.Action = func() {
        list()
    }
}

Please note that options, arguments, specs, and long descriptions cannot be provided when using ActionCommand. This is intended for very simple command invocations that take no arguments.

Finally, as a side-note, it may seem a bit weird that this package uses a function to initialize a command instead of simply returning a command struct. The motivation behind this API decision is scoping: as with the standard flag package, adding an option or an argument returns a pointer to a value which will be populated when the app is run. Since you'll want to store these pointers in variables, and to avoid having dozens of them in the same scope (the main func for example or as global variables), this API was specifically tailored to take a func parameter (called CmdInitializer), which accepts the command struct. With this design, the command's specific variables are limited in scope to this function.

Interceptors

Interceptors, or hooks, can be defined to be executed before and after a command or when any of its subcommands are executed. For example, the following app defines multiple commands as well as a global flag which toggles verbosity:

app := cli.App("app", "bla bla")
verbose := app.BoolOpt("verbose v", false, "Enable debug logs")

app.Command("command1", "...", func(cmd *cli.Cmd) {
    if (*verbose) {
        logrus.SetLevel(logrus.DebugLevel)
    }
})

app.Command("command2", "...", func(cmd *cli.Cmd) {
    if (*verbose) {
        logrus.SetLevel(logrus.DebugLevel)
    }
})

Instead of duplicating the check for the verbose flag and setting the debug level in every command (and its sub-commands), a Before interceptor can be set on the top-level App instead:

app.Before = func() {
    if (*verbose) {
        logrus.SetLevel(logrus.DebugLevel)
    }
}

Whenever a valid command is called by the user, all the Before interceptors defined on the app and the intermediate commands will be called, in order from the root to the leaf.

Similarly, to execute a hook after a command has been called, e.g. to cleanup resources allocated in Before interceptors, simply set the After field of the App struct or any other Command. After interceptors will be called, in order, from the leaf up to the root (the opposite order of the Before interceptors).

The following diagram shows when and in which order multiple Before and After interceptors are executed:

+------------+    success    +------------+   success   +----------------+     success
| app.Before +---------------> cmd.Before +-------------> sub_cmd.Before +---------+
+------------+               +-+----------+             +--+-------------+         |
                               |                           |                     +-v-------+
                 error         |           error           |                     | sub_cmd |
       +-----------------------+   +-----------------------+                     | Action  |
       |                           |                                             +-+-------+
+------v-----+               +-----v------+             +----------------+         |
| app.After  <---------------+ cmd.After  <-------------+  sub_cmd.After <---------+
+------------+    always     +------------+    always   +----------------+      always

Exiting

To exit the application, use cli.Exit function, which accepts an exit code and exits the app with the provided code. It is important to use cli.Exit instead of os.Exit as the former ensures that all of the After interceptors are executed before exiting.

cli.Exit(1)

Spec Strings

An App or Command's invocation syntax can be customized using spec strings. This can be useful to indicate that an argument is optional or that two options are mutually exclusive. The spec string is one of the key differentiators between this package and other CLI packages as it allows the developer to express usage in a simple, familiar, yet concise grammar.

To define option and argument usage for the top-level App, assign a spec string to the App's Spec field:

cp := cli.App("cp", "Copy files around")
cp.Spec = "[-R [-H | -L | -P]]"

Likewise, to define option and argument usage for a command or subcommand, assign a spec string to the Command's Spec field:

docker := cli.App("docker", "A self-sufficient runtime for linux containers")
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
    cmd.Spec = "[-d|--rm] IMAGE [COMMAND [ARG...]]"
    :
    :
}

The spec syntax is mostly based on the conventions used in POSIX command line applications (help messages and man pages). This syntax is described in full below. If a user invokes the app or command with the incorrect syntax, the app terminates with a help message showing the proper invocation. The remainder of this section describes the many features and capabilities of the spec string grammar.

Options can use both short and long option names in spec strings. In the example below, the option is mandatory and must be provided. Any options referenced in a spec string MUST be explicitly declared, otherwise this package will panic. I.e. for each item in the spec string, a corresponding *Opt or *Arg is required:

x.Spec = "-f"  // or x.Spec = "--force"
forceFlag := x.BoolOpt("f force", ...)

Arguments are specified with all-uppercased words. In the example below, both SRC and DST must be provided by the user (two arguments). Like options, any argument referenced in a spec string MUST be explicitly declared, otherwise this package will panic:

x.Spec="SRC DST"
src := x.StringArg("SRC", ...)
dst := x.StringArg("DST", ...)

With the exception of options, the order of the elements in a spec string is respected and enforced when command line arguments are parsed. In the example below, consecutive options (-f and -g) are parsed regardless of the order they are specified (both "-f=5 -g=6" and "-g=6 -f=5" are valid). Order between options and arguments is significant (-f and -g must appear before the SRC argument). The same holds true for arguments, where SRC must appear before DST:

x.Spec = "-f -g SRC -h DST"
var (
    factor = x.IntOpt("f", 1, "Fun factor (1-5)")
    games  = x.IntOpt("g", 1, "# of games")
    health = x.IntOpt("h", 1, "# of hosts")
    src    = x.StringArg("SRC", ...)
    dst    = x.StringArg("DST", ...)
)

Optionality of options and arguments is specified in a spec string by enclosing the item in square brackets []. If the user does not provide an optional value, the app will use the default value specified when the argument was defined. In the example below, if -x is not provided, heapSize will default to 1024:

x.Spec = "[-x]"
heapSize := x.IntOpt("x", 1024, "Heap size in MB")

Choice between two or more items is specified in a spec string by separating each choice with the | operator. Choices are mutually exclusive. In the examples below, only a single choice can be provided by the user otherwise the app will terminate displaying a help message on proper usage:

x.Spec = "--rm | --daemon"
x.Spec = "-H | -L | -P"
x.Spec = "-t | DST"

Repetition of options and arguments is specified in a spec string with the ... postfix operator to mark an item as repeatable. Both options and arguments support repitition. In the example below, users may invoke the command with multiple -e options and multiple SRC arguments:

x.Spec = "-e... SRC..."

// Allows parsing of the following shell command:
//   $ app -eeeee file1 file2
//   $ app -e -e -e -e file1 file2

Grouping of options and arguments is specified in a spec string with parenthesis. When combined with the choice | and repetition ... operators, complex syntaxes can be created. The parenthesis in the example below indicate a repeatable sequence of a -e option followed by an argument, and that is mutually exclusive to a choice between -x and -y options.

x.Spec = "(-e COMMAND)... | (-x|-y)"

// Allows parsing of the following shell command:
//   $ app -e show -e add
//   $ app -y
// But not the following:
//   $ app -e show -x

Option groups, or option folding, are a shorthand method to declaring a choice between multiple options. I.e. any combination of the listed options in any order with at least one option selected. The following two statements are equivalent:

x.Spec = "-abcd"
x.Spec = "(-a | -b | -c | -d)..."

Option groups are typically used in conjunction with optionality [] operators. I.e. any combination of the listed options in any order or none at all. The following two statements are equivalent:

x.Spec = "[-abcd]"
x.Spec = "[-a | -b | -c | -d]..."

All of the options can be specified using a special syntax: [OPTIONS]. This is a special token in the spec string (not optionality and not an argument called OPTIONS). It is equivalent to an optional repeatable choice between all the available options. For example, if an app or a command declares 4 options a, b, c and d, then the following two statements are equivalent:

x.Spec = "[OPTIONS]"
x.Spec = "[-a | -b | -c | -d]..."

Inline option values are specified in the spec string with the = notation immediately following an option (long or short form) to provide users with an inline description or value. The actual inline values are ignored by the spec parser as they exist only to provide a contextual hint to the user. In the example below, "absolute-path" and "in seconds" are ignored by the parser:

x.Spec = "[ -a=<absolute-path> | --timeout=<in seconds> ] ARG"

The -- operator can be used to automatically treat everything following it as arguments. In other words, placing a -- in the spec string automatically inserts a -- in the same position in the program call arguments. This lets you write programs such as the POSIX time utility for example:

x.Spec = "-lp [-- CMD [ARG...]]"

// Allows parsing of the following shell command:
//   $ app -p ps -aux

Spec Grammar

Below is the full EBNF grammar for the Specs language:

spec         -> sequence
sequence     -> choice*
req_sequence -> choice+
choice       -> atom ('|' atom)*
atom         -> (shortOpt | longOpt | optSeq | allOpts | group | optional) rep?
shortOp      -> '-' [A-Za-z]
longOpt      -> '--' [A-Za-z][A-Za-z0-9]*
optSeq       -> '-' [A-Za-z]+
allOpts      -> '[OPTIONS]'
group        -> '(' req_sequence ')'
optional     -> '[' req_sequence ']'
rep          -> '...'

By combining a few of these building blocks together (while respecting the grammar above), powerful and sophisticated validation constraints can be created in a simple and concise manner without having to define in code. This is one of the key differentiators between this package and other CLI packages. Validation of usage is handled entirely by the package through the spec string.

Behind the scenes, this package parses the spec string and constructs a finite state machine used to parse the command line arguments. It also handles backtracking, which allows it to handle tricky cases, or what I like to call "the cp test":

cp SRC... DST

Without backtracking, this deceptively simple spec string cannot be parsed correctly. For instance, docopt can't handle this case, whereas this package does.

Default Spec

By default an auto-generated spec string is created for the app and every command unless a spec string has been set by the user. This can simplify use of the package even further for simple syntaxes.

The following logic is used to create an auto-generated spec string: 1) start with an empty spec string, 2) if at least one option was declared, append "[OPTIONS]" to the spec string, and 3) for each declared argument, append it, in the order of declaration, to the spec string. For example, given this command declaration:

docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
    var (
        detached = cmd.BoolOpt("d detach", false, "Run container in background")
        memory   = cmd.StringOpt("m memory", "", "Set memory limit")
        image    = cmd.StringArg("IMAGE", "", "The image to run")
        args     = cmd.StringsArg("ARG", nil, "Arguments")
    )
})

The auto-generated spec string, which should suffice for simple cases, would be:

[OPTIONS] IMAGE ARG

If additional constraints are required, the spec string must be set explicitly using the grammar documented above.

Custom Types

By default, the following types are supported for options and arguments: bool, string, int, float64, strings (slice of strings), ints (slice of ints) and floats64 (slice of float64). You can, however, extend this package to handle other types, e.g. time.Duration, float64, or even your own struct types.

To define your own custom type, you must implement the flag.Value interface for your custom type, and then declare the option or argument using VarOpt or VarArg respectively if using the short-form methods. If using the long-form struct, then use Var instead.

The following example defines a custom type for a duration. It defines a duration argument that users will be able to invoke with strings in the form of "1h31m42s":

// Declare your type
type Duration time.Duration

// Make it implement flag.Value
func (d *Duration) Set(v string) error {
    parsed, err := time.ParseDuration(v)
    if err != nil {
        return err
    }
    *d = Duration(parsed)
    return nil
}

func (d *Duration) String() string {
    duration := time.Duration(*d)
    return duration.String()
}

func main() {
    duration := Duration(0)
    app := App("var", "")
    app.VarArg("DURATION", &duration, "")
    app.Run([]string{"cp", "1h31m42s"})
}

To make a custom type to behave as a boolean option, i.e. doesn't take a value, it must implement the IsBoolFlag method that returns true:

type BoolLike int

func (d *BoolLike) IsBoolFlag() bool {
    return true
}

To make a custom type behave as a multi-valued option or argument, i.e. takes multiple values, it must implement the Clear method, which is called whenever the values list needs to be cleared, e.g. when the value was initially populated from an environment variable, and then explicitly set from the CLI:

type Durations []time.Duration

// Make it implement flag.Value
func (d *Durations) Set(v string) error {
    parsed, err := time.ParseDuration(v)
    if err != nil {
        return err
    }
    *d = append(*d, Duration(parsed))
    return nil
}

func (d *Durations) String() string {
    return fmt.Sprintf("%v", *d)
}

// Make it multi-valued
func (d *Durations) Clear() {
    *d = []Duration{}
}

To hide the default value of a custom type, it must implement the IsDefault method that returns a boolean. The help message generator will use the return value to decide whether or not to display the default value to users:

type Action string

func (a *Action) IsDefault() bool {
    return (*a) == "nop"
}

License

This work is published under the MIT license.

Please see the LICENSE file for details.


Automatically generated by autoreadme on 2020.08.08

mow.cli's People

Contributors

andrzejressel avatar aurkenb avatar bmoyles avatar codykrieger avatar ggiamarchi avatar gwatts avatar jawher avatar jbuberel avatar jorisroovers avatar julienvey avatar pascalbourdier avatar pkazmier avatar r6m avatar sakateka avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mow.cli's Issues

Command Usage strings do not include external arguments

I'm working on a whitelist application. Whitelists have a name, WL. Once you name a whitelist, you can manipulate and query them in various ways.

This shows up correctly in the global usage string:

$ ndwhitelist -h

Usage: ndwhitelist WL COMMAND [arg...]

However, WL is omitted from subcommand usage strings:

$ ndwhitelist wl add -h

Usage: ndwhitelist add KEY (-v | -f | -d | -D)

The actual parser parses correctly, but WL is omitted from the usage string. The usage string which should appear is:

Usage: ndwhitelist WL add KEY (-v | -f | -d | -D)

Typo in Basics section of the README

This is really a nitpick, but the README states

cp = cli.App("cp", "Copy files around")

There is a colon missing in this assignment. Code needs to be:

cp := cli.App("cp", "Copy files around"

Unexpected space in main command usage

func main() {
    app := cli.App("aws-vault", "A vault for storing and accessing AWS credentials")
    app.Command("add", "Add credentials to the vault", addCredentials)
    app.Command("rm", "Remove credentials from the vault", removeCredentials)
    app.Command("exec", "Execute a sub-process with credentials", addCredentials)
    app.Run(os.Args)
}

Note double space after aws-vault and COMMAND.

Output:

Usage: aws-vault  COMMAND [arg...]

A vault for storing and accessing AWS credentials

Commands:
  add          Add credentials to the vault
  rm           Remove credentials from the vault
  exec         Execute a sub-process with credentials

Run 'aws-vault COMMAND --help' for more information on a command.
exit status 2

Help semantics are a bit confusing...

Under arguments the generated help says things like: FILE="" - which puts you under the impression you need that exact text like this FILE="c:\some\path\to\a\file.txt", when in reality you substitute the parameter value for the entire string FILE as it appears in the "Usage:" pattern.

I have been caught on this for parameters I don't use much and I observed colleagues who are newer to a given parameter do the same thing - even when they don't do it on familiar parameters.

It seems pretty natural for technical folks to make this assumption - would be nice to align the documentation semantics with the natural perceptions rather than fight them with more documentation.

Catch duplicated command line flags

It would appear to be possible to register the same short/long options multiple times for a command. It would be better if this was caught and threw an error as early as possible. I thought it would be caught in Cmd but it was not, so then I thought maybe it was caught in cmd.Spec but it appears not to be caught there either.

In a large CLI particularly if modular its hard to see which arguments have been used.

Negative values for VarArg() Bug?

I noticed that i cannot get a positional VarArg with a negative value to work properly...
Here is an example:

...
type Float32Arg float32

func (fla *Float32Arg) String() string {
    return strconv.FormatFloat(float64(*fla), 'f', -1, 32)
}

func (fla *Float32Arg) Set(s string) error {
    val, err := strconv.ParseFloat(s, 32)
    if err != nil {
        return err
    }
    *fla = Float32Arg(float32(val))
    return err
}
...

When i then use it in a VarArg

...
            val := Float32Arg(0)
            cmd.VarArg("VAL", &val, "value")
...

i cannot input a negative value because it interprets it as a flag argument:

$ ./cmd 10.0    <= works
$ ./cmd -10.0   <= does not work (prints help message because '-10.0' is not a declared flag

I am temporarily using VarOpt because that woks since its already a flag: (--val=-10.0 and -v-10.0 works fine)

Im not very familiar with the internals of this project, but to me it seems that it is a matter of flag parsing/checks preceding positional argument parsing/checks

Is this behaviour a bug?

PS.: Thank u for the beautiful api 👌

Application Hang

If we have environment variables set, and try to run the application with custom flags, app hangs.

app := cli.App("sample", "sample application")
addr := app.String(cli.StringOpt{
	Name:   "l",
	Value:  "listen",
	EnvVar: "LISTEN",
	Desc:   "listen to address.",
})
err := app.Run(os.Args)
if err != nil {
	log.Println(err)
	cli.Exit(1)
}

and in terminal

$ export LISTEN=test
$ ./app -l=test2

The app will hang, Not even app.Before or app.Action is called..

Required environment variable

The spec doesn't seem to provide for an option to be required either by supplying an environment variable and/or supplying a command line option.

eg.

package main                                                                                                                                                                                                                                                 

import (                                                                                                                                                                                                                                                     
    "fmt"                                                                                                                                                                                                                                                    
    "os"                                                                                                                                                                                                                                                     

    "github.com/jawher/mow.cli"                                                                                                                                                                                                                              
)                                                                                                                                                                                                                                                            

func main() {                                                                                                                                                                                                                                                
    app := cli.App("test", "test")                                                                                                                                                                                                                           

    app.Command("cmd", "command", func(cmd *cli.Cmd) {                                                                                                                                                                                                       
        param := cmd.String(cli.StringOpt{                                                                                                                                                                                                                   
            Name:   "param",                                                                                                                                                                                                                                 
            EnvVar: "PARAM",                                                                                                                                                                                                                                 
        })                                                                                                                                                                                                                                                   
        cmd.Spec = "--param"                                                                                                                                                                                                                                 
        cmd.Action = func() {                                                                                                                                                                                                                                
            fmt.Println("param", *param)                                                                                                                                                                                                                     
        }                                                                                                                                                                                                                                                    
    })                                                                                                                                                                                                                                                       

    app.Run(os.Args)                                                                                                                                                                                                                                         
}                                                                                                                                                                                                                                                            

i'd like this program to succeed if the program is run as either myapp --param foobar or PARAM=foobar myapp but print the usage info otherwise if both are omitted.

I guess I can make the argument optional in the Spec and then check it from within the action, but it seems like a common enough use case that mow.cli would have a better way of specifying it.

unsuccessful parsing in case of nested command

I am facing a wierd issue. Considering the following code

package main

import (
    "log"
    "os"

    "github.com/jawher/mow.cli"
)

func main() {
    mycli := cli.App("mycli", "")

    mycli.Command("labels", "Work with labels in this project", func(labelsCmd *cli.Cmd) {

        labelsCmd.Command("create", "Create label", func(createCmd *cli.Cmd) {
            createCmd.Spec = "(--title | TITLE) (--color)"

            var titleOpt = createCmd.StringOpt("t title", "", "title of the label")
            var titleArg = createCmd.StringArg("TITLE", "", "title of the label")
            var color = createCmd.StringOpt("c color", "", "color of the label, in hex notation")

            log.Println("color:", *color)
            log.Println("titleOpt:", *titleOpt)
            log.Println("titleArg:", *titleArg)

            createCmd.Action = func() {
                log.Println("executing create command")
            }
        })
    })

    mycli.Run(os.Args)
}

I then compile using go build .
Then I run the command ./mycli labels create -t test -c red
The output that I am getting is

2016/10/24 22:13:20 color: 
2016/10/24 22:13:20 titleOpt: 
2016/10/24 22:13:20 titleArg: 
2016/10/24 22:13:20 executing create command

So basically it is failing to parse the arguments that I am providing. I am unable to locate the issue. Any help would be much appreciated.

BTW, thanks for this amazing library !

display format of bool flags

Quoting a suggestion from @bmoyles in #26:

Anecdotally (I don't have data to back this up, unfortunately), bool options by convention are assumed false if not specified on the command line and are true if present. If one wants a bool that defaults to true and is toggled false via an option, the conventions I've seen most often are "--no-option" or "--without-option." I do like the option of being explicit and showing the default true/false if the situation or interface makes sense, but prefer "--option" to imply false by default and true if present and "--no-option" to imply true by default, false if not present. Do you agree and is this something you'd be interested in a PR for?

Provide more specific parsing error feedback

Currently if the FSM cannot navigate the command line options you just get an Error: incorrect usage message printed followed by the usage. With a large command this is not very helpful. It seems like mow.cli with its FSM is in a good position to give a helpful message.

It would also be useful to just echo back the verbatim command args in case on an error in case it is obvious the mistake you have made such as an $UNSET_VARIABLE in a script.

I would be interested in tackling this if you think it is a good idea and can give some guidance on how you think it would be best implemented.

What I have in mind is an output that attempts to draw a text cursor at the point of the parsing error.

Request: support enum flags

This library is off to a great start! Support for enumerations is a common feature of flag parsing libraries and it would be great if mow.cli could support this too.

Proposed design:

cmd.EnumOpt("format", "printable", []string{"printable", "binary"}, "help text")

Per-option help text could be set via the struct variants of the Enum methods.

My specific use case is that I need to add a flag that will support many different values over time but that only currently supports two. For example, the --format flag supports only printable and binary today but it might later need base64. I could use multiple boolean flags but this would require me to implement my own default-value and validation logic (undesirable).

Another use case is the --net flag for docker run:

--net="bridge"   : Set the Network mode for the container
                              'bridge': creates a new network stack for the container on the docker bridge
                              'none': no networking for this container
                              'container:<name|id>': reuses another container network stack
                              'host': use the host network stack inside the container

Thanks.

Consider tagging versions

I know there is a tradition in golang of not tagging versions and just running off master, but I am managing my dependencies with a manifest file and it would be convenient to be able to specify a version tag. I can do the same with a commit hash but isn't quite as nice. Would you consider cutting some tagged releases?

Thanks for the library.

Aliasing commands

It doesn't look like it is possible to have aliases for [sub-] commands?

For example:

some-app cp|copy SRC DEST

Should this be supported?

Synchronize README.md and doc.go

As I was submitting PRs for some README.md enhancements, I noticed that doc.go has drifted in numerous spots. I'm more than happy to update doc.go, so it matches README.md more closely. This would give me an opportunity to learn about godoc as I'm new to Go.

Ideally, it would be great if one could be derived from the other, but I'm not familiar with the documentation tools available in Go-land yet. Is there a best practice with this regards?

Float Options

There should be some kind of float type support built-in.

How to write option's argument in Spec?

hi, jawher

here is my code:

package main

import (
    "fmt"
    "github.com/jawher/mow.cli"
    "os"
)

func main() {
    app := cli.App("greet", "Greet")
    app.Spec = "-c [NAME]"
    // how to write the "c" option's argument?

    name := app.StringArg("NAME", "stranger", "Your name")
    count := app.IntOpt("c count", 1, "the count")

    app.Action = func() {
        for i := 0; i < *count; i++ {
            fmt.Printf("Hello %s\n", *name)
        }
    }
    app.Run(os.Args)
}

then run:

$ go run greet.go -h

Usage: greet -c [NAME]

Greet

Arguments:
  NAME="stranger"   Your name

Options:
  -c, --count=1   the count

meaning it is not possible to tell whether [NAME] is option's argument or positional argument

I want to get the help "Usage: greet -c COUNT [NAME]", the COUNT is "c" option's argument, and the "[NAME]" is positional argument.

Thank you!

Support environment variable as a *Opt parameter

For a more concise declaration, it would be very nice to be able to make a call such as :

recursive := cp.BoolOpt("R recursive", false, "recursively copy the src to dst", "VAR_RECURSIVE")

Instead of :

recursive = cp.Bool(cli.BoolOpt{
    Name:  "R recursive",
    Value: false,
    Desc:  "copy src files recursively",
    EnvVar: "VAR_RECURSIVE"
})

SetByUser would not be set to anything.

I can make a PR if you want me to.

-h and --help do not honour the -- operator

Given an app or a command with the following spec CMD -- ARG..., calling the command with go -h for example would still print the command help instead of parsing -h as ARG.

Provide possibility of passing pointer to value for options or argument

I was recently porting an command from Cobra. It looked like:

var cfg = config.DefaultFlags()

func init() {
	ventCmd.Flags().StringVar(&cfg.DBAdapter, "db-adapter", cfg.DBAdapter, "Database adapter, 'postgres' or 'sqlite' are fully supported")
	ventCmd.Flags().StringVar(&cfg.DBURL, "db-url", cfg.DBURL, "PostgreSQL database URL or SQLite db file path")
	ventCmd.Flags().StringVar(&cfg.DBSchema, "db-schema", cfg.DBSchema, "PostgreSQL database schema (empty for SQLite)")
	ventCmd.Flags().StringVar(&cfg.GRPCAddr, "grpc-addr", cfg.GRPCAddr, "Address to connect to the Hyperledger Burrow gRPC server")
	ventCmd.Flags().StringVar(&cfg.HTTPAddr, "http-addr", cfg.HTTPAddr, "Address to bind the HTTP server")
	ventCmd.Flags().StringVar(&cfg.LogLevel, "log-level", cfg.LogLevel, "Logging level (error, warn, info, debug)")
	ventCmd.Flags().StringVar(&cfg.SpecFile, "spec-file", cfg.SpecFile, "SQLSol json specification file full path")
	ventCmd.Flags().StringVar(&cfg.AbiFile, "abi-file", cfg.AbiFile, "Event Abi specification file full path")
	ventCmd.Flags().StringVar(&cfg.AbiDir, "abi-dir", cfg.AbiDir, "Path of a folder to look for event Abi specification files")
	ventCmd.Flags().StringVar(&cfg.SpecDir, "spec-dir", cfg.SpecDir, "Path of a folder to look for SQLSol json specification files")
	ventCmd.Flags().BoolVar(&cfg.DBBlockTx, "db-block", cfg.DBBlockTx, "Create block & transaction tables and persist related data (true/false)")
	ventCmd.Flags().BoolVarP(&printVersion, "version", "v", false, "Print the full Vent version (note this version matches the version of Burrow from the same source tree)")
}

In mow.cli since I have no way of providing it with a pointer to fill I had to do this:

		cfg := config.DefaultFlags()

		dbAdapterOpt := cmd.StringOpt("db-adapter", cfg.DBAdapter, "Database adapter, 'postgres' or 'sqlite' are fully supported")
		dbURLOpt := cmd.StringOpt("db-url", cfg.DBURL, "PostgreSQL database URL or SQLite db file path")
		dbSchemaOpt := cmd.StringOpt("db-schema", cfg.DBSchema, "PostgreSQL database schema (empty for SQLite)")
		grpcAddrOpt := cmd.StringOpt("grpc-addr", cfg.GRPCAddr, "Address to connect to the Hyperledger Burrow gRPC server")
		httpAddrOpt := cmd.StringOpt("http-addr", cfg.HTTPAddr, "Address to bind the HTTP server")
		logLevelOpt := cmd.StringOpt("log-level", cfg.LogLevel, "Logging level (error, warn, info, debug)")
		specFileOpt := cmd.StringOpt("spec-file", cfg.SpecFile, "SQLSol json specification file full path")
		abiFileOpt := cmd.StringOpt("abi-file", cfg.AbiFile, "Event Abi specification file full path")
		abiDirOpt := cmd.StringOpt("abi-dir", cfg.AbiDir, "Path of a folder to look for event Abi specification files")
		specDirOpt := cmd.StringOpt("spec-dir", cfg.SpecDir, "Path of a folder to look for SQLSol json specification files")
		dbBlockTxOpt := cmd.BoolOpt("db-block", cfg.DBBlockTx, "Create block & transaction tables and persist related data (true/false)")

		cmd.Before = func() {
			// Rather annoying boilerplate here... but there is no way to pass mow.cli a pointer for it to fill your values for you
			cfg.DBAdapter = *dbAdapterOpt
			cfg.DBURL = *dbURLOpt
			cfg.DBSchema = *dbSchemaOpt
			cfg.GRPCAddr = *grpcAddrOpt
			cfg.HTTPAddr = *httpAddrOpt
			cfg.LogLevel = *logLevelOpt
			cfg.SpecFile = *specFileOpt
			cfg.AbiFile = *abiFileOpt
			cfg.AbiDir = *abiDirOpt
			cfg.SpecDir = *specDirOpt
			cfg.DBBlockTx = *dbBlockTxOpt
		}

Not the end of the world but rather annoying boilerplate. I wonder if mow.cli could support something similar to Cobra and provide the option to pass in a pointer. I note that the base functions exist in the internal package.

Perhaps refactoring (in commands.go) to the following wouldn't be too bad:

func (c *Cmd) String(p StringParam) *string {
	into := new(string)
	return c.StringPtr(p, into)
}

func (c *Cmd) StringPtr(p StringParam, into *string) *string {
	value := values.NewString(into, p.value())

	switch x := p.(type) {
	case StringOpt:
		c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
	case StringArg:
		c.mkArg(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
	default:
		panic(fmt.Sprintf("Unhandled param %v", p))
	}

	return into
}

Support for printing the version of the application

This is a feature request for providing some syntactic sugar to easily print the version of a cli application. A --version option is a widely used by commandline programes.

Currently, I use the following workaround:

cp := cli.App("mycli", "My cli description")
cp.Command("version", "Prints the version", func(cmd *cli.Cmd) {
    cmd.Action = func() {
        fmt.Println("mycli, version", "0.1.0")
    }
})

It would however be nice to just be able to do:

// notice the extra "0.1.0" parameter for cli.App
cp := cli.App("mycli", "My cli description", "0.1.0")

This version string would then be printed automatically when users invoke: mycli version or the option variants such as as mycli --version, mycli -v.

Ideally these commands are automatically disabled for cli apps that don't specify a version.

You could conceivable also print the version number as part of the help output, but as far as I'm concerned that'd be a choice for the implementer.

Request: Long option only.

Hello,

This library is neat, however, it would be perfect if its was possible to declare an option in long format
only.

For example using StringOpt:

host := app.String(cli.StringOpt{
    Name:  "host",
    Value: "127.0.0.1",
    Desc:  "Hostname",
    LongOptOnly: true,
})

Thank you in advance.

CLI parsing chokes when optional value contains `=` char

I'm writing an application that needs to be able to receive base64-encoded data on the command line. However, mow.cli apparently doesn't like that base64 data uses equal signs for padding. Example (slightly modified to preserve NDA'd info):

$ ex --ns='9bu+xgWuYTq+5kYjWvGNKWC4kF3oFQDLcugyvomXTZk='
Error: incorrect usage
...

I believe that this is an issue with equal padding and not other some misconfiguration because trimming the last quartet causes a logic error farther into the program:

$ ex --ns='9bu+xgWuYTq+5kYjWvGNKWC4kF3oFQDLcugyvomX'
namespace must have size 32; found 30

The workaround is clear enough in my case: document that the CLI expects its values stripped of padding, and then insert any necessary padding in the logical portion of the program. However, it would be good if mow.cli were capable of handling this input in the future.

How to enable profiling via flag

Hi, how would I integrate profiling into my your app?

https://github.com/pkg/profile

I have to call something right when the app starts and right when it exits (or use defer as in the example). But I only want it enabled when I pass a flag via the command line.

Would I have to use a global flag and copy/paste the same code in each command?

Thanks!

Add global panic interceptor

In some apps, panics carry meaningful information and should be nicely reported on exit. User, of course, always has the option to defer a recovery block in the Action call back. However, in applications with many sub-commands and, correspondingly, Action call backs this results in excessive proliferation of the boilerplate code.

Considering that mov.cli already has some panic handling mechanics, may it be possible to add a global call back to an App object, which will receive, as its argument, a panic value propagating from whatever invoked subcommand and will have an option to either handle the panic or let it propagate further (by returning a bool value, perhaps)?

Additional documentation / examples

It would be nice to be able to provide extra documentation and/or example commands when --help is used, but not on an incorrect usage error.

Poor performance with multiple nested options

In the application below, running ward add --help or any other variation of add is very slow (>30s). I'm guessing it's because of the complicated spec with multiple nested options. I haven't done any profiling yet, so I can't tell you where it's spending time.

For context, this is for a command-line password manager. Specifically, the add command lets you add a new credential (e.g. ward add --login "[email protected]"), but it also has a built-in password generator with its own sub-options, so ward add --login "[email protected]" --gen --length 15 --no-symbol is valid as well.

I'm running on Windows 10 64-bit with Go 1.5.1.

// ward.go
package main

import (
  "github.com/jawher/mow.cli"
  "fmt"
  "os"
)

func run(args []string) {
  ward := cli.App("ward", "Secure password manager - https://github.com/schmich/ward")

  ward.Command("add", "Add a new credential.", func(cmd *cli.Cmd) {
    cmd.Spec = "[--login] [--realm] [--note] [--no-copy] [--gen [--length] [--min-length] [--max-length] [--no-upper] [--no-lower] [--no-digit] [--no-symbol] [--no-similar] [--min-upper] [--max-upper] [--min-lower] [--max-lower] [--min-digit] [--max-digit] [--min-symbol] [--max-symbol] [--exclude]]"

    _ = cmd.StringOpt("login", "", "Login for credential, e.g. username or email.")
    _ = cmd.StringOpt("realm", "", "Realm for credential, e.g. website or WiFi AP name.")
    _ = cmd.StringOpt("note", "", "Note for credential.")
    _ = cmd.BoolOpt("no-copy", false, "Do not copy generated password to the clipboard.")
    _ = cmd.BoolOpt("gen", false, "Generate a password.")
    _ = cmd.IntOpt("length", 0, "Password length.")
    _ = cmd.IntOpt("min-length", 30, "Minimum length password.")
    _ = cmd.IntOpt("max-length", 40, "Maximum length password.")
    _ = cmd.BoolOpt("no-upper", false, "Exclude uppercase characters in password.")
    _ = cmd.BoolOpt("no-lower", false, "Exclude lowercase characters in password.")
    _ = cmd.BoolOpt("no-digit", false, "Exclude digit characters in password.")
    _ = cmd.BoolOpt("no-symbol", false, "Exclude symbol characters in password.")
    _ = cmd.BoolOpt("no-similar", false, "Exclude similar characters in password.")
    _ = cmd.IntOpt("min-upper", 0, "Minimum number of uppercase characters in password.")
    _ = cmd.IntOpt("max-upper", -1, "Maximum number of uppercase characters in password.")
    _ = cmd.IntOpt("min-lower", 0, "Minimum number of lowercase characters in password.")
    _ = cmd.IntOpt("max-lower", -1, "Maximum number of lowercase characters in password.")
    _ = cmd.IntOpt("min-digit", 0, "Minimum number of digit characters in password.")
    _ = cmd.IntOpt("max-digit", -1, "Maximum number of digit characters in password.")
    _ = cmd.IntOpt("min-symbol", 0, "Minimum number of symbol characters in password.")
    _ = cmd.IntOpt("max-symbol", -1, "Maximum number of symbol characters in password.")
    _ = cmd.StringOpt("exclude", "", "Exclude specific characters from password.")
  })

  fmt.Println("Run.")
  ward.Run(args)
}

func main() {
  fmt.Println("Begin.")
  run(os.Args)
  fmt.Println("Fin.")
}

Inter-option ordering

I'm writing a command that needs to filter a file list.

Some filters will cause files to be selected, others to be deselected. A file can match several filters so filtering order matters:
<something that selects the file> followed by <something that deselects the file>
does not give the same result that
<something that deselects the file> followed by <something that selects the file>

I was planning to use a different option for each type of filter (basically --filtertype filterexpression) but mow.cli does not seem to preserve inter-option ordering. So I'm stuck in hardcoding the filter application ordering by type (all filters of type A, then all filters of type B, etc)

Would it be possible to enhance mow.cli so related options generate a slice of {option, value} and not separate per-option value slices?

feature: support long descriptions for subcommands

I have a subcommand where several of the options take a string, which are interpreted the same way for all of these options. Instead of giving details of possible string values for each options, I'd like to add this to the description of the subcommand.

Currently I can use a multi-line string for the description of the subcommand, and this works well if I do cmd subcmd --help. However, if I just do cmd or cmd --help it shows the full description for that subcommand (instead of just a one-line summary) making the output quite hard to read.

It would be nice if there was a way to specify the "long" and "short" description for the subcommand. The short (one-line) description would only be shown when that subcommand is listed with other subcommands. The long description would be shown when help is given only for that subcommand.

customize usage / help display

Is there currently a facility to customize the usage/help? For example I would like to include the version number, and add a line or two of text showing example invocations. If there isn't, could you point me towards where this might be implemented? While fairly new to golang I would take a crack at it.

thanks!

howto: optional longOpt before a repetition

For a reverse proxy I've created intuitively a spec like this:

func record(cmd *cli.Cmd) {
    cmd.Spec = "[--listen] HOSTNAME..."
    var (
        listenOn  = cmd.String(cli.StringArg{Name: "listen", Value: "127.0.0.7:7000", EnvVar: "LISTEN"})
        hostnames = cmd.StringsArg("HOSTNAME", nil, "")
    )

    cmd.Action = func() {
        log.Printf("Ignored for now: %v", hostnames)
        startReverseProxy(listenOn)
    }
}

… with the application record being invoked like this:

recorder record www.target.tld
recorder record www.target.tld example2.com
recorder record --listen=8.8.8.8:2700 www.target.tld example2.com example3.com

Well, the above spec leads to this error:

panic: Parse error at position 9:
[--listen] HOSTNAME...
         ^ Undeclared option --listen
  • What would be the correct cmd.Spec?
  • How can I introduce option --listen-on (including the dash)?
  • cmd.Spec = "[--listen] HOSTNAME... DIRECTORY?

Allow arbitrary args and flags

Hi Jawher,

Is there is a way to allow arbitrary arguments and flags in a command? I have a script which wraps another tool and want to give users the ability to pass any args/flags to the underlying tool as if they were using it directly. I'm able to pass arbitrary string args using:

app := cli.App(...)
...
app.Command("tool", "Send a command directly to the tool using the INSTANCE configuration", func(cmd *cli.Cmd) {
    instance := cmd.String(InstanceArg)
    args := cmd.StringsArg("ARGS", nil, "tool arguments")
    cmd.Spec = "INSTANCE [ARGS...]"

    cmd.Action = func() {
        // pass *args along to tool
    }
})

However, I haven't figured out a clean way to allow arbitrary flags as well. Ideally, I would be able to run the command like:

> main.go tool INSTANCE [args...] [flags...]

I don't want to clutter up my code with all of the potential args/flags for underlying tool, and would prefer not to manually parse os.Args before calling app.Run. Any suggestions?

Regards,
ZPatrick

Position-independent options

From what I can tell, there is no way to have a single-declaration global option that applies across all subcommands (i.e. its position on the command-line doesn't matter).

For example, let's say I have a command-line program foo which has subcommand bar which has subcommand baz. I can invoke this as foo bar baz.

Now, let's say I wanted to add a verbose logging flag -v which applies regardless of command/subcommand. Ideally, then, the following would be equivalent (I could specify the flag anywhere on the command-line):

  • foo -v bar baz
  • foo bar -v baz
  • foo bar baz -v

I'd like to do this without having to define a BoolOpt on foo, bar, and baz. Is there currently a way to do this by just defining a single global BoolOpt?

In my actual use case, I have four levels of commands (i.e. foo bar baz quux --opt waldo). I would like to be able to specify verbosity (-v, -vv, -vvv) as well as remote connection strings (--connection tcp://192.168.1.100:9000) without needing to worry about their position.

Thanks for your work on this library. It's a boon for the community, and I find myself reaching for it often.

CLI hangs when EnvVar is set

I am having an issue where the CLI will hang when I pass in an invalid flag. I have created a simplified version of my project to recreate the issue here:

https://github.com/jdonboch/mow-hangs

If I have the environment variable "FARO_HOME" set and I run the above project with an invalid flag argument, mow.cli will hang:

mow-hangs --invalid-tag api

If I unset the environment variable, the CLI will return properly with an incorrect usage (expected).

I attached a debugger and it seems to be looping forever in this area of the code:
https://github.com/jawher/mow.cli/blob/master/matchers.go#L250

I haven't had time to debug much as I am not too familiar with the code. Any work-arounds would also be appreciated.

confusion... maybe explain it in documentation?

    gocdn.Command(`kill`, `terminate the ssh connection`, func(cmd *cli.Cmd) {

        _all := cmd.BoolOpt(`a all`, false, `kill all ssh connections`)

        fmt.Println(*_all)
        cmd.Action = func() {
            fmt.Println(*_all)
        }
    })
false
true

What exactly is the reason why the flag is only available within Action? and why Action command at all?

Cannot use func literal (type func(*cli.Cli)) as type cli.CmdInitializer

Hello,

I'm currently working on an application in a Mac. When trying to do something like my_app.Command("test", "lets see if this works", func(cm *cli.Cli){}) Im getting the following compilation error:

./main.go:12: cannot use func literal (type func(*cli.Cli)) as type cli.CmdInitializer in argument to my_app.Cmd.Command

I have the following version of go installed :go version go1.8.1 darwin/amd64 Reading on the internet some people solved a similar problem with running go get -u, but this doesn't seem to work for me.

Question: Is it possible to signify nothing is parsed after argument?

I'm writing a command line utility that is designed to wrap the execution of other commands (it will have arguments of it's own). It will be common that a user would want to do something like this: mycmd their_command -c -F. I know this is currently possible using -- to say mycmd -- bash -c 'some code'.

The problem I expect to happen will be that if the user wraps a command without arguments it will work fine (e.g. mycmd echo foo) but the behavior would be different when using a command with a flag (e.g. mycmd bash -c 'echo foo').

Would it be possible to have an argument that nothing is parsed after this or to require "--" be used? I'm guessing I could precheck os.Args to ensure "--" exists before moving on to further parsing.

Handle sharing of options and arguments between subcommands

I'd like to use mow cli to implement sub commands that share a core of common options and arguments

My understanding is that it is not possible right now in mow cli without redeclaring options and args in every sub (otherwise why would the reddit example duplicate the account declaration).

Please add a way to reuse option/arg between subs to avoid the usual drift when cut and parting

If I'm wrong and it is possible today, please clarify the documentation on that point.

Lastly thanks a lot for a nicely documented project. Good documentation is the main reason I chose mow cli over alternatives.

The app panics while parsing args

The content of the main file:

func main() {
	swarm := &swarmdb.App{}
	app := cli.App("swarm", "0.1.0.draft")
	app.Command("init", "Initialize a new databse", func(cmd *cli.Cmd) {
		r := cmd.StringArg("REPLICA", "default", "replica UUID")
		cmd.Action = func() {
			err := swarm.Init(r)
			if err != nil {
				swarm.Logger.WithError(err).Error("initialize a new database")
				os.Exit(1)
			}
		}
	})

	app.Run(os.Args)
}

I ran the tool with swarm init test and have got a panic:

panic: runtime error: invalid memory address or nil pointer dereference [recovered]
	panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x40dd1e6]

goroutine 1 [running]:
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).run(0xc420010ab0, 0x429a760, 0x447e640)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:29 +0x146
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).run(0xc420010ba0, 0x429a760, 0x447e640)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:21 +0x12e
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).run(0xc420010e70, 0x429a760, 0x447e640)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:21 +0x12e
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).callDo.func1(0xc420010ea0, 0x0, 0x0)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:42 +0x4c
panic(0x429a760, 0x447e640)
	/usr/local/Cellar/go/1.9.2/libexec/src/runtime/panic.go:491 +0x283
github.com/olebedev/swarmdb/vendor/github.com/apex/log.(*Logger).log(0x0, 0x3, 0xc4200aa310, 0x42e6f4c, 0x19)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/apex/log/logger.go:142 +0x26
github.com/olebedev/swarmdb/vendor/github.com/apex/log.(*Entry).Error(0xc4200aa310, 0x42e6f4c, 0x19)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/apex/log/entry.go:96 +0x50
main.main.func1.1()
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/cmd/swarm/main.go:31 +0xc0
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).callDo(0xc420010ea0, 0x0, 0x0)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:45 +0x70
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).run(0xc420010ea0, 0x0, 0x0)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:17 +0xb3
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).run(0xc420010e40, 0x0, 0x0)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:21 +0x12e
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).run(0xc420010b70, 0x0, 0x0)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:21 +0x12e
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*step).run(0xc420051ee0, 0x0, 0x0)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/flow.go:21 +0x12e
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*Cmd).parse(0xc4200b2200, 0xc4200100e0, 0x1, 0x1, 0xc420051ee0, 0xc420010b70, 0xc420010ba0, 0xc42006e168, 0xc42006e120)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/commands.go:473 +0x529
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*Cmd).parse(0xc4200b2100, 0xc4200100d0, 0x2, 0x2, 0xc420051ee0, 0xc420051ee0, 0xc420010ab0, 0xc4200b2101, 0xc420010ab0)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/commands.go:487 +0x7ad
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*Cli).parse(0xc420051f60, 0xc4200100d0, 0x2, 0x2, 0xc420051ee0, 0xc420051ee0, 0xc420010ab0, 0x1, 0x1)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/cli.go:73 +0x102
github.com/olebedev/swarmdb/vendor/github.com/jawher/mow%2ecli.(*Cli).Run(0xc420051f60, 0xc4200100c0, 0x3, 0x3, 0x18, 0xc4200426c0)
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/vendor/github.com/jawher/mow.cli/cli.go:102 +0x134
main.main()
	/usr/local/Cellar/go/workspace/src/github.com/olebedev/swarmdb/cmd/swarm/main.go:37 +0x20b

Why this could happened?

Thanks

Undeclared arg panic when using dynamic arg names

I'm building a moderately complicated app for which there are a bunch of argument groups which we want to reuse in various subcommands. We ended up with an idiom in which for each argument group, we write two functions: one to get its spec string, and one to get the actual value. It's easier to demonstrate than to explain:

func getDurationSpec() string {
	return "DURATION"
}

func getDurationClosure(cmd *cli.Cmd) func() math.Duration {
	duration := cmd.StringOpt("DURATION", "", "duration")

	return func() math.Duration {
		...
	}
}

A particular subcommand just needs to link together the appropriate arguments, like this:

app.Command("transfer", "transfer from one address to another", func(cmd *cli.Cmd) {
	cmd.Spec = fmt.Sprintf(
		"%s %s",
		getAddressSpec("FROM"),
		getAddressSpec("TO"),
	)

	getFrom := getAddressClosure(cmd, "FROM")
	getTo := getAddressClosure(cmd, "TO")

	cmd.Action = func() {
		from := getFrom()
		to := getTo()

            ...

However, there are occasional panics which I believe to be a bug in this library. getAddress* is vulnerable:

func nameFor(id string) string {
	if id == "" {
		return "NAME"
	}
	return fmt.Sprintf("%s_NAME", id)
}

func argFor(id string) string {
	if id == "" {
		return "a address"
	}
	return strings.ToLower(fmt.Sprintf("%s_address", id))
}

func getAddressSpec(id string) string {
	if id == "" {
		return "(NAME | -a=<ADDRESS>)"
	}
	return fmt.Sprintf(
		"(%s | --%s=<%s>)",
		nameFor(id), argFor(id), strings.ToUpper(argFor(id)),
	)
}

func getAddressClosure(cmd *cli.Cmd, id string) func() address.Address {
	fmt.Printf("declaring arg for %s\n", nameFor(id))
	name := cmd.StringArg(nameFor(id), "", fmt.Sprintf("Name of %s account", id))
	fmt.Printf("declaring opt for %s\n", argFor(id))
	addr := cmd.StringOpt(argFor(id), "", fmt.Sprintf("%s Address", id))

	return func() address.Address {
        ...

generates the following output when id is blank:

Tue Jun 26 17:07:20 CEST 2018
declaring arg for NAME
declaring opt for a address
panic: Parse error at position 1:
(NAME | -a=<ADDRESS>) DURATION
 ^ Undeclared arg NAME

goroutine 1 [running]:
<SNIP>

I'm not sure what's actually causing this panic, because I have duly registered the NAME arg, as shown by the debug printlns. I'm implementing a workaround now, but it would be nice if the library were capable of handling this without error.

Request: parse expected flags with flag.ContinueOnError

I'd like my app to ignore any options that I didn't specify in advance still parsing as much of the expected ones as possible.

Given this:

package main

import (
        "flag"
        "fmt"
        "github.com/jawher/mow.cli"
        "os"
)

func main() {
        app := cli.App("cli", "mow.cli test")
        app.ErrorHandling = flag.ContinueOnError
        s := app.String(cli.StringOpt{
                Name:   "s string",
                Value:  "",
                EnvVar: "NEEDED_S",
                Desc:   "somestring",
        })
        app.Action = func() {
                fmt.Println(*s)
        }
        app.Run(os.Args)
}

Currently this fails to read the option -s due to an unexpected option -t having been provided:

$ go run cli.go -s DISPLAYME -t
Error…

This is what I'd expect:

$ go run cli.go -s DISPLAYME -t
Error…
DISPLAYME

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.