package flags import ( "fmt" "os" "reflect" "runtime" "strconv" "strings" "testing" "time" ) type defaultOptions struct { Int int `long:"i"` IntDefault int `long:"id" default:"1"` Float64 float64 `long:"f"` Float64Default float64 `long:"fd" default:"-3.14"` NumericFlag bool `short:"3"` String string `long:"str"` StringDefault string `long:"strd" default:"abc"` StringNotUnquoted string `long:"strnot" unquote:"false"` Time time.Duration `long:"t"` TimeDefault time.Duration `long:"td" default:"1m"` Map map[string]int `long:"m"` MapDefault map[string]int `long:"md" default:"a:1"` Slice []int `long:"s"` SliceDefault []int `long:"sd" default:"1" default:"2"` } func TestDefaults(t *testing.T) { var tests = []struct { msg string args []string expected defaultOptions }{ { msg: "no arguments, expecting default values", args: []string{}, expected: defaultOptions{ Int: 0, IntDefault: 1, Float64: 0.0, Float64Default: -3.14, NumericFlag: false, String: "", StringDefault: "abc", Time: 0, TimeDefault: time.Minute, Map: map[string]int{}, MapDefault: map[string]int{"a": 1}, Slice: []int{}, SliceDefault: []int{1, 2}, }, }, { msg: "non-zero value arguments, expecting overwritten arguments", args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "-3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, expected: defaultOptions{ Int: 3, IntDefault: 3, Float64: -2.71, Float64Default: 2.71, NumericFlag: true, String: "def", StringDefault: "def", Time: 3 * time.Millisecond, TimeDefault: 3 * time.Millisecond, Map: map[string]int{"c": 3}, MapDefault: map[string]int{"c": 3}, Slice: []int{3}, SliceDefault: []int{3}, }, }, { msg: "zero value arguments, expecting overwritten arguments", args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, expected: defaultOptions{ Int: 0, IntDefault: 0, Float64: 0, Float64Default: 0, String: "", StringDefault: "", Time: 0, TimeDefault: 0, Map: map[string]int{"": 0}, MapDefault: map[string]int{"": 0}, Slice: []int{0}, SliceDefault: []int{0}, }, }, } for _, test := range tests { var opts defaultOptions _, err := ParseArgs(&opts, test.args) if err != nil { t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) } if opts.Slice == nil { opts.Slice = []int{} } if !reflect.DeepEqual(opts, test.expected) { t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) } } } func TestNoDefaultsForBools(t *testing.T) { var opts struct { DefaultBool bool `short:"d" default:"true"` } if runtime.GOOS == "windows" { assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned on", &opts) } else { assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned on", &opts) } } func TestUnquoting(t *testing.T) { var tests = []struct { arg string err error value string }{ { arg: "\"abc", err: strconv.ErrSyntax, value: "", }, { arg: "\"\"abc\"", err: strconv.ErrSyntax, value: "", }, { arg: "\"abc\"", err: nil, value: "abc", }, { arg: "\"\\\"abc\\\"\"", err: nil, value: "\"abc\"", }, { arg: "\"\\\"abc\"", err: nil, value: "\"abc", }, } for _, test := range tests { var opts defaultOptions for _, delimiter := range []bool{false, true} { p := NewParser(&opts, None) var err error if delimiter { _, err = p.ParseArgs([]string{"--str=" + test.arg, "--strnot=" + test.arg}) } else { _, err = p.ParseArgs([]string{"--str", test.arg, "--strnot", test.arg}) } if test.err == nil { if err != nil { t.Fatalf("Expected no error but got: %v", err) } if test.value != opts.String { t.Fatalf("Expected String to be %q but got %q", test.value, opts.String) } if q := strconv.Quote(test.value); q != opts.StringNotUnquoted { t.Fatalf("Expected StringDefault to be %q but got %q", q, opts.StringNotUnquoted) } } else { if err == nil { t.Fatalf("Expected error") } else if e, ok := err.(*Error); ok { if strings.HasPrefix(e.Message, test.err.Error()) { t.Fatalf("Expected error message to end with %q but got %v", test.err.Error(), e.Message) } } } } } } // EnvRestorer keeps a copy of a set of env variables and can restore the env from them type EnvRestorer struct { env map[string]string } func (r *EnvRestorer) Restore() { os.Clearenv() for k, v := range r.env { os.Setenv(k, v) } } // EnvSnapshot returns a snapshot of the currently set env variables func EnvSnapshot() *EnvRestorer { r := EnvRestorer{make(map[string]string)} for _, kv := range os.Environ() { parts := strings.SplitN(kv, "=", 2) if len(parts) != 2 { panic("got a weird env variable: " + kv) } r.env[parts[0]] = parts[1] } return &r } type envDefaultOptions struct { Int int `long:"i" default:"1" env:"TEST_I"` Time time.Duration `long:"t" default:"1m" env:"TEST_T"` Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"` Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","` } func TestEnvDefaults(t *testing.T) { var tests = []struct { msg string args []string expected envDefaultOptions env map[string]string }{ { msg: "no arguments, no env, expecting default values", args: []string{}, expected: envDefaultOptions{ Int: 1, Time: time.Minute, Map: map[string]int{"a": 1}, Slice: []int{1, 2}, }, }, { msg: "no arguments, env defaults, expecting env default values", args: []string{}, expected: envDefaultOptions{ Int: 2, Time: 2 * time.Minute, Map: map[string]int{"a": 2, "b": 3}, Slice: []int{4, 5, 6}, }, env: map[string]string{ "TEST_I": "2", "TEST_T": "2m", "TEST_M": "a:2;b:3", "TEST_S": "4,5,6", }, }, { msg: "non-zero value arguments, expecting overwritten arguments", args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3"}, expected: envDefaultOptions{ Int: 3, Time: 3 * time.Millisecond, Map: map[string]int{"c": 3}, Slice: []int{3}, }, env: map[string]string{ "TEST_I": "2", "TEST_T": "2m", "TEST_M": "a:2;b:3", "TEST_S": "4,5,6", }, }, { msg: "zero value arguments, expecting overwritten arguments", args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0"}, expected: envDefaultOptions{ Int: 0, Time: 0, Map: map[string]int{"": 0}, Slice: []int{0}, }, env: map[string]string{ "TEST_I": "2", "TEST_T": "2m", "TEST_M": "a:2;b:3", "TEST_S": "4,5,6", }, }, } oldEnv := EnvSnapshot() defer oldEnv.Restore() for _, test := range tests { var opts envDefaultOptions oldEnv.Restore() for envKey, envValue := range test.env { os.Setenv(envKey, envValue) } _, err := ParseArgs(&opts, test.args) if err != nil { t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) } if opts.Slice == nil { opts.Slice = []int{} } if !reflect.DeepEqual(opts, test.expected) { t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) } } } func TestOptionAsArgument(t *testing.T) { var tests = []struct { args []string expectError bool errType ErrorType errMsg string rest []string }{ { // short option must not be accepted as argument args: []string{"--string-slice", "foobar", "--string-slice", "-o"}, expectError: true, errType: ErrExpectedArgument, errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-o'", }, { // long option must not be accepted as argument args: []string{"--string-slice", "foobar", "--string-slice", "--other-option"}, expectError: true, errType: ErrExpectedArgument, errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `--other-option'", }, { // long option must not be accepted as argument args: []string{"--string-slice", "--"}, expectError: true, errType: ErrExpectedArgument, errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got double dash `--'", }, { // quoted and appended option should be accepted as argument (even if it looks like an option) args: []string{"--string-slice", "foobar", "--string-slice=\"--other-option\""}, }, { // Accept any single character arguments including '-' args: []string{"--string-slice", "-"}, }, { // Do not accept arguments which start with '-' even if the next character is a digit args: []string{"--string-slice", "-3.14"}, expectError: true, errType: ErrExpectedArgument, errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-3.14'", }, { // Do not accept arguments which start with '-' if the next character is not a digit args: []string{"--string-slice", "-character"}, expectError: true, errType: ErrExpectedArgument, errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-character'", }, { args: []string{"-o", "-", "-"}, rest: []string{"-", "-"}, }, { // Accept arguments which start with '-' if the next character is a digit, for number options only args: []string{"--int-slice", "-3"}, }, { // Accept arguments which start with '-' if the next character is a digit, for number options only args: []string{"--int16", "-3"}, }, { // Accept arguments which start with '-' if the next character is a digit, for number options only args: []string{"--float32", "-3.2"}, }, { // Accept arguments which start with '-' if the next character is a digit, for number options only args: []string{"--float32ptr", "-3.2"}, }, } var opts struct { StringSlice []string `long:"string-slice"` IntSlice []int `long:"int-slice"` Int16 int16 `long:"int16"` Float32 float32 `long:"float32"` Float32Ptr *float32 `long:"float32ptr"` OtherOption bool `long:"other-option" short:"o"` } for _, test := range tests { if test.expectError { assertParseFail(t, test.errType, test.errMsg, &opts, test.args...) } else { args := assertParseSuccess(t, &opts, test.args...) assertStringArray(t, args, test.rest) } } } func TestUnknownFlagHandler(t *testing.T) { var opts struct { Flag1 string `long:"flag1"` Flag2 string `long:"flag2"` } p := NewParser(&opts, None) var unknownFlag1 string var unknownFlag2 bool var unknownFlag3 string // Set up a callback to intercept unknown options during parsing p.UnknownOptionHandler = func(option string, arg SplitArgument, args []string) ([]string, error) { if option == "unknownFlag1" { if argValue, ok := arg.Value(); ok { unknownFlag1 = argValue return args, nil } // consume a value from remaining args list unknownFlag1 = args[0] return args[1:], nil } else if option == "unknownFlag2" { // treat this one as a bool switch, don't consume any args unknownFlag2 = true return args, nil } else if option == "unknownFlag3" { if argValue, ok := arg.Value(); ok { unknownFlag3 = argValue return args, nil } // consume a value from remaining args list unknownFlag3 = args[0] return args[1:], nil } return args, fmt.Errorf("Unknown flag: %v", option) } // Parse args containing some unknown flags, verify that // our callback can handle all of them _, err := p.ParseArgs([]string{"--flag1=stuff", "--unknownFlag1", "blah", "--unknownFlag2", "--unknownFlag3=baz", "--flag2=foo"}) if err != nil { assertErrorf(t, "Parser returned unexpected error %v", err) } assertString(t, opts.Flag1, "stuff") assertString(t, opts.Flag2, "foo") assertString(t, unknownFlag1, "blah") assertString(t, unknownFlag3, "baz") if !unknownFlag2 { assertErrorf(t, "Flag should have been set by unknown handler, but had value: %v", unknownFlag2) } // Parse args with unknown flags that callback doesn't handle, verify it returns error _, err = p.ParseArgs([]string{"--flag1=stuff", "--unknownFlagX", "blah", "--flag2=foo"}) if err == nil { assertErrorf(t, "Parser should have returned error, but returned nil") } } func TestChoices(t *testing.T) { var opts struct { Choice string `long:"choose" choice:"v1" choice:"v2"` } assertParseFail(t, ErrInvalidChoice, "Invalid value `invalid' for option `"+defaultLongOptDelimiter+"choose'. Allowed values are: v1 or v2", &opts, "--choose", "invalid") assertParseSuccess(t, &opts, "--choose", "v2") assertString(t, opts.Choice, "v2") } func TestEmbedded(t *testing.T) { type embedded struct { V bool `short:"v"` } var opts struct { embedded } assertParseSuccess(t, &opts, "-v") if !opts.V { t.Errorf("Expected V to be true") } } type command struct { } func (c *command) Execute(args []string) error { return nil } func TestCommandHandlerNoCommand(t *testing.T) { var opts = struct { Value bool `short:"v"` }{} parser := NewParser(&opts, Default&^PrintErrors) var executedCommand Commander var executedArgs []string executed := false parser.CommandHandler = func(command Commander, args []string) error { executed = true executedCommand = command executedArgs = args return nil } _, err := parser.ParseArgs([]string{"arg1", "arg2"}) if err != nil { t.Fatalf("Unexpected parse error: %s", err) } if !executed { t.Errorf("Expected command handler to be executed") } if executedCommand != nil { t.Errorf("Did not exect an executed command") } assertStringArray(t, executedArgs, []string{"arg1", "arg2"}) } func TestCommandHandler(t *testing.T) { var opts = struct { Value bool `short:"v"` Command command `command:"cmd"` }{} parser := NewParser(&opts, Default&^PrintErrors) var executedCommand Commander var executedArgs []string executed := false parser.CommandHandler = func(command Commander, args []string) error { executed = true executedCommand = command executedArgs = args return nil } _, err := parser.ParseArgs([]string{"cmd", "arg1", "arg2"}) if err != nil { t.Fatalf("Unexpected parse error: %s", err) } if !executed { t.Errorf("Expected command handler to be executed") } if executedCommand == nil { t.Errorf("Expected command handler to be executed") } assertStringArray(t, executedArgs, []string{"arg1", "arg2"}) }