package cli
import (
"flag"
"fmt"
"io"
"strings"
"text/tabwriter"
"github.com/jawher/mow.cli/internal/container"
"github.com/jawher/mow.cli/internal/flow"
"github.com/jawher/mow.cli/internal/fsm"
"github.com/jawher/mow.cli/internal/lexer"
"github.com/jawher/mow.cli/internal/parser"
)
/*
Cmd represents a command (or sub command) in a CLI application. It should be constructed
by calling Command() on an app to create a top level command or by calling Command() on another
command to create a sub command
*/
type Cmd struct {
// The code to execute when this command is matched
Action func()
// The code to execute before this command or any of its children is matched
Before func()
// The code to execute after this command or any of its children is matched
After func()
// The command options and arguments
Spec string
// The command long description to be shown when help is requested
LongDesc string
// Hide this command in the help messages
Hidden bool
// The command error handling strategy
ErrorHandling flag.ErrorHandling
init CmdInitializer
name string
aliases []string
desc string
commands []*Cmd
options []*container.Container
optionsIdx map[string]*container.Container
args []*container.Container
argsIdx map[string]*container.Container
parents []string
fsm *fsm.State
}
/*
BoolParam represents a Bool option or argument
*/
type BoolParam interface {
value(into *bool) (flag.Value, *bool)
}
/*
StringParam represents a String option or argument
*/
type StringParam interface {
value(into *string) (flag.Value, *string)
}
/*
IntParam represents an Int option or argument
*/
type IntParam interface {
value(into *int) (flag.Value, *int)
}
/*
Float64Param represents an Float64 option or argument
*/
type Float64Param interface {
value(into *float64) (flag.Value, *float64)
}
/*
StringsParam represents a string slice option or argument
*/
type StringsParam interface {
value(into *[]string) (flag.Value, *[]string)
}
/*
IntsParam represents an float64 slice option or argument
*/
type IntsParam interface {
value(into *[]int) (flag.Value, *[]int)
}
/*
Floats64Param represents an float64 slice option or argument
*/
type Floats64Param interface {
value(into *[]float64) (flag.Value, *[]float64)
}
/*
VarParam represents an custom option or argument where the type and format are controlled by the developer
*/
type VarParam interface {
value() flag.Value
}
/*
CmdInitializer is a function that configures a command by adding options, arguments, a spec, sub commands and the code
to execute when the command is called
*/
type CmdInitializer func(*Cmd)
/*
Command adds a new (sub) command to c where name is the command name (what you type in the console),
description is what would be shown in the help messages, e.g.:
Usage: git [OPTIONS] COMMAND [arg...]
Commands:
$name $desc
the last argument, init, is a function that will be called by mow.cli to further configure the created
(sub) command, e.g. to add options, arguments and the code to execute
*/
func (c *Cmd) Command(name, desc string, init CmdInitializer) {
aliases := strings.Fields(name)
c.commands = append(c.commands, &Cmd{
ErrorHandling: c.ErrorHandling,
name: aliases[0],
aliases: aliases,
desc: desc,
init: init,
commands: []*Cmd{},
options: []*container.Container{},
optionsIdx: map[string]*container.Container{},
args: []*container.Container{},
argsIdx: map[string]*container.Container{},
})
}
/*
Bool can be used to add a bool option or argument to a command.
It accepts either a BoolOpt or a BoolArg struct.
The result should be stored in a variable (a pointer to a bool) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) Bool(p BoolParam) *bool {
value, into := p.value(nil)
switch x := p.(type) {
case BoolOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case BoolArg:
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
}
/*
BoolPtr can be used to add a bool option or argument to a command.
It accepts either a pointer to a bool var and a BoolOpt or a BoolArg struct.
The into parameter points to a variable (a pointer to a bool) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) BoolPtr(into *bool, p BoolParam) {
value, _ := p.value(into)
switch x := p.(type) {
case BoolOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case BoolArg:
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))
}
}
/*
String can be used to add a string option or argument to a command.
It accepts either a StringOpt or a StringArg struct.
The result should be stored in a variable (a pointer to a string) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) String(p StringParam) *string {
value, into := p.value(nil)
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
}
/*
StringPtr can be used to add a string option or argument to a command.
It accepts either a pointer to a string var and a StringOpt or a StringArg struct.
The into parameter points to a variable (a pointer to a string) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) StringPtr(into *string, p StringParam) {
value, _ := p.value(into)
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))
}
}
/*
Int can be used to add an int option or argument to a command.
It accepts either a IntOpt or a IntArg struct.
The result should be stored in a variable (a pointer to an int) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) Int(p IntParam) *int {
value, into := p.value(nil)
switch x := p.(type) {
case IntOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case IntArg:
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
}
/*
IntPtr can be used to add a int option or argument to a command.
It accepts either a pointer to a int var and a IntOpt or a IntArg struct.
The into parameter points to a variable (a pointer to a int) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) IntPtr(into *int, p IntParam) {
value, _ := p.value(into)
switch x := p.(type) {
case IntOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case IntArg:
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))
}
}
/*
Float64 can be used to add a float64 option or argument to a command.
It accepts either a Float64Opt or a Float64Arg struct.
The result should be stored in a variable (a pointer to a float64) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) Float64(p Float64Param) *float64 {
value, into := p.value(nil)
switch x := p.(type) {
case Float64Opt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case Float64Arg:
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
}
/*
Float64Ptr can be used to add a float64 option or argument to a command.
It accepts either a pointer to a float64 var and a Float64Opt or a Float64Arg struct.
The into parameter points to a variable (a pointer to a float64) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) Float64Ptr(into *float64, p Float64Param) {
value, _ := p.value(into)
switch x := p.(type) {
case Float64Opt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case Float64Arg:
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))
}
}
/*
Strings can be used to add a string slice option or argument to a command.
It accepts either a StringsOpt or a StringsArg struct.
The result should be stored in a variable (a pointer to a string slice) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) Strings(p StringsParam) *[]string {
value, into := p.value(nil)
switch x := p.(type) {
case StringsOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case StringsArg:
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
}
/*
StringsPtr can be used to add a string slice option or argument to a command.
It accepts either a pointer to a string slice var and a StringsOpt or a StringsArg struct.
The into parameter points to a variable (a pointer to a string slice) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) StringsPtr(into *[]string, p StringsParam) {
value, _ := p.value(into)
switch x := p.(type) {
case StringsOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case StringsArg:
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))
}
}
/*
Ints can be used to add an int slice option or argument to a command.
It accepts either a IntsOpt or a IntsArg struct.
The result should be stored in a variable (a pointer to an int slice) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) Ints(p IntsParam) *[]int {
value, into := p.value(nil)
switch x := p.(type) {
case IntsOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case IntsArg:
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
}
/*
IntsPtr can be used to add a int slice option or argument to a command.
It accepts either a pointer to a int slice var and a IntsOpt or a IntsArg struct.
The into parameter points to a variable (a pointer to a int slice) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) IntsPtr(into *[]int, p IntsParam) {
value, _ := p.value(into)
switch x := p.(type) {
case IntsOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case IntsArg:
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))
}
}
/*
Floats64 can be used to add an float64 slice option or argument to a command.
It accepts either a Floats64Opt or a Floats64Arg struct.
The result should be stored in a variable (a pointer to an float64 slice) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) Floats64(p Floats64Param) *[]float64 {
value, into := p.value(nil)
switch x := p.(type) {
case Floats64Opt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case Floats64Arg:
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
}
/*
Floats64Ptr can be used to add a float64 slice option or argument to a command.
It accepts either a pointer to a float64 slice var and a Floats64Opt or a Floats64Arg struct.
The into parameter points to a variable (a pointer to a float64 slice) which will be populated when the app is run and the call arguments get parsed
*/
func (c *Cmd) Floats64Ptr(into *[]float64, p Floats64Param) {
value, _ := p.value(into)
switch x := p.(type) {
case Floats64Opt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: value, ValueSetByUser: x.SetByUser})
case Floats64Arg:
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))
}
}
/*
Var can be used to add a custom option or argument to a command.
It accepts either a VarOpt or a VarArg struct.
As opposed to the other built-in types, this function does not return a pointer the the value.
Instead, the VarOpt or VarOptArg structs hold the said value.
*/
func (c *Cmd) Var(p VarParam) {
switch x := p.(type) {
case VarOpt:
c.mkOpt(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: p.value(), ValueSetByUser: x.SetByUser})
case VarArg:
c.mkArg(container.Container{Name: x.Name, Desc: x.Desc, EnvVar: x.EnvVar, HideValue: x.HideValue, Value: p.value(), ValueSetByUser: x.SetByUser})
default:
panic(fmt.Sprintf("Unhandled param %v", p))
}
}
func (c *Cmd) doInit() error {
if c.init != nil {
c.init(c)
}
parents := append(c.parents, c.name)
for _, sub := range c.commands {
sub.parents = parents
}
if len(c.Spec) == 0 {
if len(c.options) > 0 {
c.Spec = "[OPTIONS] "
}
for _, arg := range c.args {
c.Spec += arg.Name + " "
}
}
tokens, err := lexer.Tokenize(c.Spec)
if err != nil {
return err
}
params := parser.Params{
Spec: c.Spec,
Options: c.options,
OptionsIdx: c.optionsIdx,
Args: c.args,
ArgsIdx: c.argsIdx,
}
s, err := parser.Parse(tokens, params)
if err != nil {
return err
}
c.fsm = s
return nil
}
func (c *Cmd) onError(err error) {
if err == errHelpRequested || err == errVersionRequested {
if c.ErrorHandling == flag.ExitOnError {
exiter(0)
}
return
}
switch c.ErrorHandling {
case flag.ExitOnError:
exiter(2)
case flag.PanicOnError:
panic(err)
}
}
/*
PrintHelp prints the command's help message.
In most cases the library users won't need to call this method, unless
a more complex validation is needed
*/
func (c *Cmd) PrintHelp() {
c.printHelp(false)
}
/*
PrintLongHelp prints the command's help message using the command long description if specified.
In most cases the library users won't need to call this method, unless
a more complex validation is needed
*/
func (c *Cmd) PrintLongHelp() {
c.printHelp(true)
}
func (c *Cmd) printHelp(longDesc bool) {
full := append(c.parents, c.name)
path := strings.Join(full, " ")
fmt.Fprintf(stdErr, "\nUsage: %s", path)
spec := strings.TrimSpace(c.Spec)
if len(spec) > 0 {
fmt.Fprintf(stdErr, " %s", spec)
}
if len(c.commands) > 0 {
fmt.Fprint(stdErr, " COMMAND [arg...]")
}
fmt.Fprint(stdErr, "\n\n")
desc := c.desc
if longDesc && len(c.LongDesc) > 0 {
desc = c.LongDesc
}
if len(desc) > 0 {
fmt.Fprintf(stdErr, "%s\n", desc)
}
w := tabwriter.NewWriter(stdErr, 15, 1, 3, ' ', 0)
if len(c.args) > 0 {
fmt.Fprint(w, "\t\nArguments:\t\n")
for _, arg := range c.args {
var (
env = formatEnvVarsForHelp(arg.EnvVar)
value = formatValueForHelp(arg.HideValue, arg.DefaultValue)
)
printTabbedRow(w, arg.Name, joinStrings(arg.Desc, env, value))
}
}
if len(c.options) > 0 {
fmt.Fprint(w, "\t\nOptions:\t\n")
for _, opt := range c.options {
var (
optNames = formatOptNamesForHelp(opt)
env = formatEnvVarsForHelp(opt.EnvVar)
value = formatValueForHelp(opt.HideValue, opt.DefaultValue)
)
printTabbedRow(w, optNames, joinStrings(opt.Desc, env, value))
}
}
commands := make([]*Cmd, 0, len(c.commands))
for _, c := range c.commands {
if err := c.doInit(); err != nil {
panic(err)
}
if c.Hidden {
continue
}
commands = append(commands, c)
}
if len(commands) > 0 {
fmt.Fprint(w, "\t\nCommands:\t\n")
for _, c := range commands {
fmt.Fprintf(w, " %s\t%s\n", strings.Join(c.aliases, ", "), c.desc)
}
}
if len(commands) > 0 {
fmt.Fprintf(w, "\t\nRun '%s COMMAND --help' for more information on a command.\n", path)
}
w.Flush()
}
func formatOptNamesForHelp(o *container.Container) string {
short, long := "", ""
for _, n := range o.Names {
if len(n) == 2 && short == "" {
short = n
}
if len(n) > 2 && long == "" {
long = n
}
}
switch {
case short != "" && long != "":
return fmt.Sprintf("%s, %s", short, long)
case short != "":
return short
case long != "":
// 2 spaces instead of the short option (-x), one space for the comma (,) and one space for the after comma blank
return fmt.Sprintf(" %s", long)
default:
return ""
}
}
func formatValueForHelp(hide bool, v string) string {
if hide {
return ""
}
if v == "" {
return ""
}
return fmt.Sprintf("(default %s)", v)
}
func formatEnvVarsForHelp(envVars string) string {
if strings.TrimSpace(envVars) == "" {
return ""
}
vars := strings.Fields(envVars)
res := "(env"
sep := " "
for i, v := range vars {
if i > 0 {
sep = ", "
}
res += fmt.Sprintf("%s$%s", sep, v)
}
res += ")"
return res
}
func (c *Cmd) parse(args []string, entry, inFlow, outFlow *flow.Step) error {
if c.helpRequested(args) {
c.PrintLongHelp()
c.onError(errHelpRequested)
return nil
}
nargsLen := c.getOptsAndArgs(args)
if err := c.fsm.Parse(args[:nargsLen]); err != nil {
fmt.Fprintf(stdErr, "Error: %s\n", err.Error())
c.PrintHelp()
c.onError(err)
return err
}
newInFlow := &flow.Step{
Do: c.Before,
Error: outFlow,
Desc: fmt.Sprintf("%s.Before", c.name),
Exiter: exiter,
}
inFlow.Success = newInFlow
newOutFlow := &flow.Step{
Do: c.After,
Success: outFlow,
Error: outFlow,
Desc: fmt.Sprintf("%s.After", c.name),
Exiter: exiter,
}
args = args[nargsLen:]
if len(args) == 0 {
if c.Action != nil {
newInFlow.Success = &flow.Step{
Do: c.Action,
Success: newOutFlow,
Error: newOutFlow,
Desc: fmt.Sprintf("%s.Action", c.name),
Exiter: exiter,
}
entry.Run(nil)
return nil
}
c.PrintHelp()
c.onError(nil)
return nil
}
arg := args[0]
for _, sub := range c.commands {
if sub.isAlias(arg) {
if err := sub.doInit(); err != nil {
panic(err)
}
return sub.parse(args[1:], entry, newInFlow, newOutFlow)
}
}
var err error
switch {
case strings.HasPrefix(arg, "-"):
err = fmt.Errorf("Error: illegal option %s", arg)
fmt.Fprintln(stdErr, err.Error())
default:
err = fmt.Errorf("Error: illegal input %s", arg)
fmt.Fprintln(stdErr, err.Error())
}
c.PrintHelp()
c.onError(err)
return err
}
func (c *Cmd) helpRequested(args []string) bool {
return c.isFlagSet(args, []string{"-h", "--help"})
}
func (c *Cmd) isFlagSet(args []string, searchArgs []string) bool {
if len(args) == 0 {
return false
}
arg := args[0]
for _, searchArg := range searchArgs {
if arg == searchArg {
return true
}
}
return false
}
func (c *Cmd) getOptsAndArgs(args []string) int {
consumed := 0
for _, arg := range args {
for _, sub := range c.commands {
if sub.isAlias(arg) {
return consumed
}
}
consumed++
}
return consumed
}
func (c *Cmd) isAlias(arg string) bool {
for _, alias := range c.aliases {
if arg == alias {
return true
}
}
return false
}
func joinStrings(parts ...string) string {
res := ""
for _, part := range parts {
s := strings.TrimSpace(part)
if s == "" {
continue
}
if res != "" {
res += " "
}
res += part
}
return res
}
func printTabbedRow(w io.Writer, s1 string, s2 string) {
lines := strings.Split(s2, "\n")
fmt.Fprintf(w, " %s\t%s\n", s1, strings.TrimSpace(lines[0]))
if len(lines) == 1 {
return
}
for _, line := range lines[1:] {
fmt.Fprintf(w, " %s\t%s\n", "", strings.TrimSpace(line))
}
}