546 lines
13 KiB
Go
546 lines
13 KiB
Go
|
package cli
|
||
|
|
||
|
import (
|
||
|
"flag"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"text/tabwriter"
|
||
|
)
|
||
|
|
||
|
/*
|
||
|
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
|
||
|
// The command error handling strategy
|
||
|
ErrorHandling flag.ErrorHandling
|
||
|
|
||
|
init CmdInitializer
|
||
|
name string
|
||
|
aliases []string
|
||
|
desc string
|
||
|
|
||
|
commands []*Cmd
|
||
|
options []*opt
|
||
|
optionsIdx map[string]*opt
|
||
|
args []*arg
|
||
|
argsIdx map[string]*arg
|
||
|
|
||
|
parents []string
|
||
|
|
||
|
fsm *state
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
BoolParam represents a Bool option or argument
|
||
|
*/
|
||
|
type BoolParam interface {
|
||
|
value() bool
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
StringParam represents a String option or argument
|
||
|
*/
|
||
|
type StringParam interface {
|
||
|
value() string
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
IntParam represents an Int option or argument
|
||
|
*/
|
||
|
type IntParam interface {
|
||
|
value() int
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
StringsParam represents a string slice option or argument
|
||
|
*/
|
||
|
type StringsParam interface {
|
||
|
value() []string
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
IntsParam represents an int slice option or argument
|
||
|
*/
|
||
|
type IntsParam interface {
|
||
|
value() []int
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
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: []*opt{},
|
||
|
optionsIdx: map[string]*opt{},
|
||
|
args: []*arg{},
|
||
|
argsIdx: map[string]*arg{},
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
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 {
|
||
|
into := new(bool)
|
||
|
value := newBoolValue(into, p.value())
|
||
|
|
||
|
switch x := p.(type) {
|
||
|
case BoolOpt:
|
||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||
|
case BoolArg:
|
||
|
c.mkArg(arg{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
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
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 {
|
||
|
into := new(string)
|
||
|
value := newStringValue(into, p.value())
|
||
|
|
||
|
switch x := p.(type) {
|
||
|
case StringOpt:
|
||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||
|
case StringArg:
|
||
|
c.mkArg(arg{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
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
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 {
|
||
|
into := new(int)
|
||
|
value := newIntValue(into, p.value())
|
||
|
|
||
|
switch x := p.(type) {
|
||
|
case IntOpt:
|
||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||
|
case IntArg:
|
||
|
c.mkArg(arg{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
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
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 {
|
||
|
into := new([]string)
|
||
|
value := newStringsValue(into, p.value())
|
||
|
|
||
|
switch x := p.(type) {
|
||
|
case StringsOpt:
|
||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||
|
case StringsArg:
|
||
|
c.mkArg(arg{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
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
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 {
|
||
|
into := new([]int)
|
||
|
value := newIntsValue(into, p.value())
|
||
|
|
||
|
switch x := p.(type) {
|
||
|
case IntsOpt:
|
||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||
|
case IntsArg:
|
||
|
c.mkArg(arg{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
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
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(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: p.value(), valueSetByUser: x.SetByUser})
|
||
|
case VarArg:
|
||
|
c.mkArg(arg{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 + " "
|
||
|
}
|
||
|
}
|
||
|
fsm, err := uParse(c)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
c.fsm = fsm
|
||
|
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.value)
|
||
|
)
|
||
|
fmt.Fprintf(w, " %s\t%s\n", 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.value)
|
||
|
)
|
||
|
fmt.Fprintf(w, " %s\t%s\n", optNames, joinStrings(opt.desc, env, value))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(c.commands) > 0 {
|
||
|
fmt.Fprint(w, "\t\nCommands:\t\n")
|
||
|
|
||
|
for _, c := range c.commands {
|
||
|
fmt.Fprintf(w, " %s\t%s\n", strings.Join(c.aliases, ", "), c.desc)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(c.commands) > 0 {
|
||
|
fmt.Fprintf(w, "\t\nRun '%s COMMAND --help' for more information on a command.\n", path)
|
||
|
}
|
||
|
|
||
|
w.Flush()
|
||
|
}
|
||
|
|
||
|
func formatOptNamesForHelp(o *opt) 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 fmt.Sprintf("%s", 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 flag.Value) string {
|
||
|
if hide {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
if dv, ok := v.(defaultValued); ok {
|
||
|
if dv.IsDefault() {
|
||
|
return ""
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return fmt.Sprintf("(default %s)", v.String())
|
||
|
}
|
||
|
|
||
|
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 *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 := &step{
|
||
|
do: c.Before,
|
||
|
error: outFlow,
|
||
|
desc: fmt.Sprintf("%s.Before", c.name),
|
||
|
}
|
||
|
inFlow.success = newInFlow
|
||
|
|
||
|
newOutFlow := &step{
|
||
|
do: c.After,
|
||
|
success: outFlow,
|
||
|
error: outFlow,
|
||
|
desc: fmt.Sprintf("%s.After", c.name),
|
||
|
}
|
||
|
|
||
|
args = args[nargsLen:]
|
||
|
if len(args) == 0 {
|
||
|
if c.Action != nil {
|
||
|
newInFlow.success = &step{
|
||
|
do: c.Action,
|
||
|
success: newOutFlow,
|
||
|
error: newOutFlow,
|
||
|
desc: fmt.Sprintf("%s.Action", c.name),
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|