Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"flag"
"fmt"

"github.com/jawher/mow.cli/internal/container"
"github.com/jawher/mow.cli/internal/values"
Expand All @@ -27,6 +28,29 @@ func (a BoolArg) value() bool {
return a.Value
}

// EnumArg describes a string option
type EnumArg struct {
// A space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
// The one letter names will then be called with a single dash (short option), the others with two (long options).
Name string
// The option description as will be shown in help messages
Desc string
// A space separated list of environment variables names to be used to initialize this option
EnvVar string
// The option's initial value
Value string
// A boolean to display or not the current value of the option in the help message
HideValue bool
// Set to true if this option was set by the user (as opposed to being set from env or not set at all)
SetByUser *bool
// Enums contains the enum values
Validation []EnumValidator
}

func (a EnumArg) value() string {
return a.Value
}

// StringArg describes a string argument
type StringArg struct {
// The argument name as will be shown in help messages
Expand Down Expand Up @@ -143,6 +167,24 @@ func (c *Cmd) BoolArg(name string, value bool, desc string) *bool {
})
}

/*
EnumArg defines an enum argument on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.

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) EnumArg(name, value, desc string, validation []EnumValidator) *string {
if validation == nil || len(validation) == 0 {
panic(fmt.Sprintf("Enums require validation %s %s", name, value))
}

return c.Enum(EnumArg{
Name: name,
Value: value,
Desc: desc,
Validation: validation,
})
}

/*
StringArg defines a string argument on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.

Expand Down Expand Up @@ -205,6 +247,10 @@ func (c *Cmd) VarArg(name string, value flag.Value, desc string) {
}

func (c *Cmd) mkArg(arg container.Container) {
arg.DefaultValue = arg.Value.String()
if dv, ok := arg.Value.(values.DefaultValued); ok {
arg.DefaultDisplay = dv.IsDefault()
}
arg.ValueSetFromEnv = values.SetFromEnv(arg.Value, arg.EnvVar)

c.args = append(c.args, &arg)
Expand Down
241 changes: 240 additions & 1 deletion cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cli
import (
"bytes"
"flag"
"strings"

"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -454,7 +455,7 @@ func TestHelpMessage(t *testing.T) {
defer exitShouldBeCalledWith(t, 0, &exitCalled)()

app := App("app", "App Desc")
app.Spec = "[-bdsuikqs] BOOL1 [STR1] INT3..."
app.Spec = "[-bdesuikqs] BOOL1 [STR1] INT3..."

// Options
app.Bool(BoolOpt{Name: "b bool1 u uuu", Value: false, EnvVar: "BOOL1", Desc: "Bool Option 1"})
Expand All @@ -465,6 +466,24 @@ func TestHelpMessage(t *testing.T) {
app.String(StringOpt{Name: "str2", Value: "a value", Desc: "String Option 2"})
app.String(StringOpt{Name: "u", Value: "another value", EnvVar: "STR3", Desc: "String Option 3", HideValue: true})

app.Enum(EnumOpt{Name: "e enum1", Value: "", EnvVar: "ENUM1", Desc: "Enum Option 1", Validation: []EnumValidator{
{User: "value1", Value: "v1", Help: "Option 1 value 1"},
{User: "value2", Value: "v2", Help: "Option 1 value 2"},
{User: "value3", Value: "v3", Help: "Option 1 value 3"},
}})

app.Enum(EnumOpt{Name: "enum2", Value: "a value", Desc: "Enum Option 2", Validation: []EnumValidator{
{User: "value1", Value: "v1", Help: "Option 2 value 1"},
{User: "value2", Value: "v2", Help: "Option 2 value 2"},
{User: "value3", Value: "v3", Help: "Option 2 value 3"},
}})

app.Enum(EnumOpt{Name: "f", Value: "another value", EnvVar: "ENUM3", Desc: "Enum Option 3", HideValue: true, Validation: []EnumValidator{
{User: "value1", Value: "v1", Help: "Option 3 value 1"},
{User: "value2", Value: "v2", Help: "Option 3 value 2"},
{User: "value3", Value: "v3", Help: "Option 3 value 3"},
}})

app.Int(IntOpt{Name: "i int1", Value: 0, EnvVar: "INT1 ALIAS_INT1"})
app.Int(IntOpt{Name: "int2", Value: 1, EnvVar: "INT2", Desc: "Int Option 2"})
app.Int(IntOpt{Name: "k", Value: 1, EnvVar: "INT3", Desc: "Int Option 3", HideValue: true})
Expand All @@ -486,6 +505,24 @@ func TestHelpMessage(t *testing.T) {
app.String(StringArg{Name: "STR2", Value: "a value", EnvVar: "STR2", Desc: "String Argument 2"})
app.String(StringArg{Name: "STR3", Value: "another value", EnvVar: "STR3", Desc: "String Argument 3", HideValue: true})

app.Enum(EnumArg{Name: "ENUM1", Value: "", EnvVar: "ENUM1", Desc: "Enum Argument 1", Validation: []EnumValidator{
{User: "value1", Value: "v1", Help: "Argument 1 value 1"},
{User: "value2", Value: "v2", Help: "Argument 1 value 2"},
{User: "value3", Value: "v3", Help: "Argument 1 value 3"},
}})

app.Enum(EnumArg{Name: "ENUM2", Value: "a value", Desc: "Enum Argument 2", Validation: []EnumValidator{
{User: "value1", Value: "v1", Help: "Argument 2 value 1"},
{User: "value2", Value: "v2", Help: "Argument 2 value 2"},
{User: "value3", Value: "v3", Help: "Argument 2 value 3"},
}})

app.Enum(EnumArg{Name: "ENUM3", Value: "another value", EnvVar: "ENUM3", Desc: "Enum Argument 3", HideValue: true, Validation: []EnumValidator{
{User: "value1", Value: "v1", Help: "Argument 3 value 1"},
{User: "value2", Value: "v2", Help: "Argument 3 value 2"},
{User: "value3", Value: "v3", Help: "Argument 3 value 3"},
}})

app.Int(IntArg{Name: "INT1", Value: 0, EnvVar: "INT1", Desc: "Int Argument 1"})
app.Int(IntArg{Name: "INT2", Value: 1, EnvVar: "INT2", Desc: "Int Argument 2"})
app.Int(IntArg{Name: "INT3", Value: 1, EnvVar: "INT3", Desc: "Int Argument 3", HideValue: true})
Expand Down Expand Up @@ -737,6 +774,45 @@ func TestOptSetByUser(t *testing.T) {
expected: true,
},

// Enum
{
desc: "Enum Opt, not set by user, default value",
config: func(c *Cli, s *bool) {
c.Enum(EnumOpt{Name: "f", Value: "v1", SetByUser: s,
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test"},
expected: false,
},
{
desc: "Enum Opt, not set by user, env value",
config: func(c *Cli, s *bool) {
os.Setenv("MOW_VALUE", "v2")
c.Enum(EnumOpt{Name: "f", EnvVar: "MOW_VALUE", SetByUser: s,
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test"},
expected: false,
},
{
desc: "Enum Opt, set by user",
config: func(c *Cli, s *bool) {
c.Enum(EnumOpt{Name: "f", Value: "a", SetByUser: s,
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test", "-f=v2"},
expected: true,
},

// Bool
{
desc: "Bool Opt, not set by user, default value",
Expand Down Expand Up @@ -904,6 +980,48 @@ func TestArgSetByUser(t *testing.T) {
expected: true,
},

// Enum
{
desc: "Enum Arg, not set by user, default value",
config: func(c *Cli, s *bool) {
c.Spec = "[ARG]"
c.Enum(EnumArg{Name: "ARG", Value: "v1", SetByUser: s,
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test"},
expected: false,
},
{
desc: "Enum Arg, not set by user, env value",
config: func(c *Cli, s *bool) {
c.Spec = "[ARG]"
os.Setenv("MOW_VALUE", "v2")
c.Enum(EnumArg{Name: "ARG", EnvVar: "MOW_VALUE", SetByUser: s,
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test"},
expected: false,
},
{
desc: "Enum Arg, set by user",
config: func(c *Cli, s *bool) {
c.Spec = "[ARG]"
c.Enum(EnumArg{Name: "ARG", Value: "a", SetByUser: s,
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test", "v2"},
expected: true,
},

// Bool
{
desc: "Bool Arg, not set by user, default value",
Expand Down Expand Up @@ -1083,6 +1201,48 @@ func TestOptSetByEnv(t *testing.T) {
expected: "user",
},

// Enum
{
desc: "Enum Opt, empty env var",
config: func(c *Cli) interface{} {
os.Setenv("MOW_VALUE", "")
return c.Enum(EnumOpt{Name: "f", Value: "v1", EnvVar: "MOW_VALUE",
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test"},
expected: "1",
},
{
desc: "Enum Opt, env set, not set by user",
config: func(c *Cli) interface{} {
os.Setenv("MOW_VALUE", "v2")
return c.Enum(EnumOpt{Name: "f", Value: "v1", EnvVar: "MOW_VALUE",
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test"},
expected: "2",
},
{
desc: "Enum Opt, env set, set by user",
config: func(c *Cli) interface{} {
os.Setenv("MOW_VALUE", "v2")
return c.Enum(EnumOpt{Name: "f", Value: "v1", EnvVar: "MOW_VALUE",
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
{User: "v3", Value: "3", Help: "v3"},
}})
},
args: []string{"test", "-f=v3"},
expected: "3",
},

// Bool
{
desc: "Bool Opt, empty env var",
Expand Down Expand Up @@ -1334,6 +1494,51 @@ func TestArgSetByEnv(t *testing.T) {
expected: "user",
},

// Enum
{
desc: "Enum Arg, empty env var",
config: func(c *Cli) interface{} {
c.Spec = "[ARG]"
os.Setenv("MOW_VALUE", "")
return c.Enum(EnumArg{Name: "ARG", Value: "v1", EnvVar: "MOW_VALUE",
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test"},
expected: "1",
},
{
desc: "Enum Arg, env set, not set by user",
config: func(c *Cli) interface{} {
c.Spec = "[ARG]"
os.Setenv("MOW_VALUE", "v2")
return c.Enum(EnumArg{Name: "ARG", Value: "default", EnvVar: "MOW_VALUE",
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
}})
},
args: []string{"test"},
expected: "2",
},
{
desc: "Enum Arg, env set, set by user",
config: func(c *Cli) interface{} {
c.Spec = "[ARG]"
os.Setenv("MOW_VALUE", "v2")
return c.Enum(EnumArg{Name: "ARG", Value: "v1", EnvVar: "MOW_VALUE",
Validation: []EnumValidator{
{User: "v1", Value: "1", Help: "v1"},
{User: "v2", Value: "2", Help: "v2"},
{User: "v3", Value: "3", Help: "v3"},
}})
},
args: []string{"test", "v3"},
expected: "3",
},

// Bool
{
desc: "Bool Arg, empty env var",
Expand Down Expand Up @@ -1781,6 +1986,40 @@ func TestBeforeAndAfterFlowOrderWhenMultipleAftersPanic(t *testing.T) {
require.Equal(t, 7, counter)
}

// In some cases depending on where the validation failure occurs, the
// assignment to variables could have been done before the help is printed,
// making the defaults wrong (would print the assigned value instead of the
// actual default value).
//
// TODO: Also note in one case it would print 'Error: Incorrect usage' while
// in the other it wouldn't, this should probably be fixed, the testcase below
// ignores that by looking for the common portions.
func TestDefaultValueShadowing(t *testing.T) {
var out, err string
defer captureAndRestoreOutput(&out, &err)()

app := App("test", "")
app.Spec = "[-t] ARG"
app.String(StringOpt{Name: "t tcomm", Value: "tdefault", Desc: "somedesc"})
app.String(StringArg{Name: "ARG", Value: "argdefault", Desc: "somedesc"})
app.Command("command1", "command1 description", func(cmd *Cmd) {})

app.ErrorHandling = flag.ContinueOnError

// First run, arg ok, bad command
app.Run([]string{"test", "-t", "tvalue", "somearg", "badcommand"})
startfirst := strings.Index(err, "Usage: test [-t]")
errfirst := err[startfirst:]

// Second run, missing arg, still bad command
app.Run([]string{"test", "-t", "tvalue", "badcommand"})
errsecond := err[startfirst+len(errfirst):]
errsecond = errsecond[strings.Index(errsecond, "Usage: test [-t]"):]

require.Equal(t, errfirst, errsecond)

}

func exitShouldBeCalledWith(t *testing.T, wantedExitCode int, called *bool) func() {
oldExiter := exiter
exiter = func(code int) {
Expand Down
Loading