123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612 |
- 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"})
- }
|