kevinschoon-pomo/vendor/github.com/jawher/mow.cli/commands.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
}