1package flags
2
3import (
4	"fmt"
5	"os"
6	"reflect"
7	"runtime"
8	"strconv"
9	"strings"
10	"testing"
11	"time"
12)
13
14type defaultOptions struct {
15	Int        int `long:"i"`
16	IntDefault int `long:"id" default:"1"`
17
18	Float64        float64 `long:"f"`
19	Float64Default float64 `long:"fd" default:"-3.14"`
20
21	NumericFlag bool `short:"3"`
22
23	String            string `long:"str"`
24	StringDefault     string `long:"strd" default:"abc"`
25	StringNotUnquoted string `long:"strnot" unquote:"false"`
26
27	Time        time.Duration `long:"t"`
28	TimeDefault time.Duration `long:"td" default:"1m"`
29
30	Map        map[string]int `long:"m"`
31	MapDefault map[string]int `long:"md" default:"a:1"`
32
33	Slice        []int `long:"s"`
34	SliceDefault []int `long:"sd" default:"1" default:"2"`
35}
36
37func TestDefaults(t *testing.T) {
38	var tests = []struct {
39		msg      string
40		args     []string
41		expected defaultOptions
42	}{
43		{
44			msg:  "no arguments, expecting default values",
45			args: []string{},
46			expected: defaultOptions{
47				Int:        0,
48				IntDefault: 1,
49
50				Float64:        0.0,
51				Float64Default: -3.14,
52
53				NumericFlag: false,
54
55				String:        "",
56				StringDefault: "abc",
57
58				Time:        0,
59				TimeDefault: time.Minute,
60
61				Map:        map[string]int{},
62				MapDefault: map[string]int{"a": 1},
63
64				Slice:        []int{},
65				SliceDefault: []int{1, 2},
66			},
67		},
68		{
69			msg:  "non-zero value arguments, expecting overwritten arguments",
70			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"},
71			expected: defaultOptions{
72				Int:        3,
73				IntDefault: 3,
74
75				Float64:        -2.71,
76				Float64Default: 2.71,
77
78				NumericFlag: true,
79
80				String:        "def",
81				StringDefault: "def",
82
83				Time:        3 * time.Millisecond,
84				TimeDefault: 3 * time.Millisecond,
85
86				Map:        map[string]int{"c": 3},
87				MapDefault: map[string]int{"c": 3},
88
89				Slice:        []int{3},
90				SliceDefault: []int{3},
91			},
92		},
93		{
94			msg:  "zero value arguments, expecting overwritten arguments",
95			args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"},
96			expected: defaultOptions{
97				Int:        0,
98				IntDefault: 0,
99
100				Float64:        0,
101				Float64Default: 0,
102
103				String:        "",
104				StringDefault: "",
105
106				Time:        0,
107				TimeDefault: 0,
108
109				Map:        map[string]int{"": 0},
110				MapDefault: map[string]int{"": 0},
111
112				Slice:        []int{0},
113				SliceDefault: []int{0},
114			},
115		},
116	}
117
118	for _, test := range tests {
119		var opts defaultOptions
120
121		_, err := ParseArgs(&opts, test.args)
122		if err != nil {
123			t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
124		}
125
126		if opts.Slice == nil {
127			opts.Slice = []int{}
128		}
129
130		if !reflect.DeepEqual(opts, test.expected) {
131			t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
132		}
133	}
134}
135
136func TestNoDefaultsForBools(t *testing.T) {
137	var opts struct {
138		DefaultBool bool `short:"d" default:"true"`
139	}
140
141	if runtime.GOOS == "windows" {
142		assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned on", &opts)
143	} else {
144		assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned on", &opts)
145	}
146}
147
148func TestUnquoting(t *testing.T) {
149	var tests = []struct {
150		arg   string
151		err   error
152		value string
153	}{
154		{
155			arg:   "\"abc",
156			err:   strconv.ErrSyntax,
157			value: "",
158		},
159		{
160			arg:   "\"\"abc\"",
161			err:   strconv.ErrSyntax,
162			value: "",
163		},
164		{
165			arg:   "\"abc\"",
166			err:   nil,
167			value: "abc",
168		},
169		{
170			arg:   "\"\\\"abc\\\"\"",
171			err:   nil,
172			value: "\"abc\"",
173		},
174		{
175			arg:   "\"\\\"abc\"",
176			err:   nil,
177			value: "\"abc",
178		},
179	}
180
181	for _, test := range tests {
182		var opts defaultOptions
183
184		for _, delimiter := range []bool{false, true} {
185			p := NewParser(&opts, None)
186
187			var err error
188			if delimiter {
189				_, err = p.ParseArgs([]string{"--str=" + test.arg, "--strnot=" + test.arg})
190			} else {
191				_, err = p.ParseArgs([]string{"--str", test.arg, "--strnot", test.arg})
192			}
193
194			if test.err == nil {
195				if err != nil {
196					t.Fatalf("Expected no error but got: %v", err)
197				}
198
199				if test.value != opts.String {
200					t.Fatalf("Expected String to be %q but got %q", test.value, opts.String)
201				}
202				if q := strconv.Quote(test.value); q != opts.StringNotUnquoted {
203					t.Fatalf("Expected StringDefault to be %q but got %q", q, opts.StringNotUnquoted)
204				}
205			} else {
206				if err == nil {
207					t.Fatalf("Expected error")
208				} else if e, ok := err.(*Error); ok {
209					if strings.HasPrefix(e.Message, test.err.Error()) {
210						t.Fatalf("Expected error message to end with %q but got %v", test.err.Error(), e.Message)
211					}
212				}
213			}
214		}
215	}
216}
217
218// EnvRestorer keeps a copy of a set of env variables and can restore the env from them
219type EnvRestorer struct {
220	env map[string]string
221}
222
223func (r *EnvRestorer) Restore() {
224	os.Clearenv()
225
226	for k, v := range r.env {
227		os.Setenv(k, v)
228	}
229}
230
231// EnvSnapshot returns a snapshot of the currently set env variables
232func EnvSnapshot() *EnvRestorer {
233	r := EnvRestorer{make(map[string]string)}
234
235	for _, kv := range os.Environ() {
236		parts := strings.SplitN(kv, "=", 2)
237
238		if len(parts) != 2 {
239			panic("got a weird env variable: " + kv)
240		}
241
242		r.env[parts[0]] = parts[1]
243	}
244
245	return &r
246}
247
248type envDefaultOptions struct {
249	Int   int            `long:"i" default:"1" env:"TEST_I"`
250	Time  time.Duration  `long:"t" default:"1m" env:"TEST_T"`
251	Map   map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"`
252	Slice []int          `long:"s" default:"1" default:"2" env:"TEST_S"  env-delim:","`
253}
254
255func TestEnvDefaults(t *testing.T) {
256	var tests = []struct {
257		msg      string
258		args     []string
259		expected envDefaultOptions
260		env      map[string]string
261	}{
262		{
263			msg:  "no arguments, no env, expecting default values",
264			args: []string{},
265			expected: envDefaultOptions{
266				Int:   1,
267				Time:  time.Minute,
268				Map:   map[string]int{"a": 1},
269				Slice: []int{1, 2},
270			},
271		},
272		{
273			msg:  "no arguments, env defaults, expecting env default values",
274			args: []string{},
275			expected: envDefaultOptions{
276				Int:   2,
277				Time:  2 * time.Minute,
278				Map:   map[string]int{"a": 2, "b": 3},
279				Slice: []int{4, 5, 6},
280			},
281			env: map[string]string{
282				"TEST_I": "2",
283				"TEST_T": "2m",
284				"TEST_M": "a:2;b:3",
285				"TEST_S": "4,5,6",
286			},
287		},
288		{
289			msg:  "non-zero value arguments, expecting overwritten arguments",
290			args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3"},
291			expected: envDefaultOptions{
292				Int:   3,
293				Time:  3 * time.Millisecond,
294				Map:   map[string]int{"c": 3},
295				Slice: []int{3},
296			},
297			env: map[string]string{
298				"TEST_I": "2",
299				"TEST_T": "2m",
300				"TEST_M": "a:2;b:3",
301				"TEST_S": "4,5,6",
302			},
303		},
304		{
305			msg:  "zero value arguments, expecting overwritten arguments",
306			args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0"},
307			expected: envDefaultOptions{
308				Int:   0,
309				Time:  0,
310				Map:   map[string]int{"": 0},
311				Slice: []int{0},
312			},
313			env: map[string]string{
314				"TEST_I": "2",
315				"TEST_T": "2m",
316				"TEST_M": "a:2;b:3",
317				"TEST_S": "4,5,6",
318			},
319		},
320	}
321
322	oldEnv := EnvSnapshot()
323	defer oldEnv.Restore()
324
325	for _, test := range tests {
326		var opts envDefaultOptions
327		oldEnv.Restore()
328		for envKey, envValue := range test.env {
329			os.Setenv(envKey, envValue)
330		}
331		_, err := ParseArgs(&opts, test.args)
332		if err != nil {
333			t.Fatalf("%s:\nUnexpected error: %v", test.msg, err)
334		}
335
336		if opts.Slice == nil {
337			opts.Slice = []int{}
338		}
339
340		if !reflect.DeepEqual(opts, test.expected) {
341			t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts)
342		}
343	}
344}
345
346func TestOptionAsArgument(t *testing.T) {
347	var tests = []struct {
348		args        []string
349		expectError bool
350		errType     ErrorType
351		errMsg      string
352		rest        []string
353	}{
354		{
355			// short option must not be accepted as argument
356			args:        []string{"--string-slice", "foobar", "--string-slice", "-o"},
357			expectError: true,
358			errType:     ErrExpectedArgument,
359			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-o'",
360		},
361		{
362			// long option must not be accepted as argument
363			args:        []string{"--string-slice", "foobar", "--string-slice", "--other-option"},
364			expectError: true,
365			errType:     ErrExpectedArgument,
366			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `--other-option'",
367		},
368		{
369			// long option must not be accepted as argument
370			args:        []string{"--string-slice", "--"},
371			expectError: true,
372			errType:     ErrExpectedArgument,
373			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got double dash `--'",
374		},
375		{
376			// quoted and appended option should be accepted as argument (even if it looks like an option)
377			args: []string{"--string-slice", "foobar", "--string-slice=\"--other-option\""},
378		},
379		{
380			// Accept any single character arguments including '-'
381			args: []string{"--string-slice", "-"},
382		},
383		{
384			// Do not accept arguments which start with '-' even if the next character is a digit
385			args:        []string{"--string-slice", "-3.14"},
386			expectError: true,
387			errType:     ErrExpectedArgument,
388			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-3.14'",
389		},
390		{
391			// Do not accept arguments which start with '-' if the next character is not a digit
392			args:        []string{"--string-slice", "-character"},
393			expectError: true,
394			errType:     ErrExpectedArgument,
395			errMsg:      "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-character'",
396		},
397		{
398			args: []string{"-o", "-", "-"},
399			rest: []string{"-", "-"},
400		},
401		{
402			// Accept arguments which start with '-' if the next character is a digit, for number options only
403			args: []string{"--int-slice", "-3"},
404		},
405		{
406			// Accept arguments which start with '-' if the next character is a digit, for number options only
407			args: []string{"--int16", "-3"},
408		},
409		{
410			// Accept arguments which start with '-' if the next character is a digit, for number options only
411			args: []string{"--float32", "-3.2"},
412		},
413		{
414			// Accept arguments which start with '-' if the next character is a digit, for number options only
415			args: []string{"--float32ptr", "-3.2"},
416		},
417	}
418
419	var opts struct {
420		StringSlice []string `long:"string-slice"`
421		IntSlice    []int    `long:"int-slice"`
422		Int16       int16    `long:"int16"`
423		Float32     float32  `long:"float32"`
424		Float32Ptr  *float32 `long:"float32ptr"`
425		OtherOption bool     `long:"other-option" short:"o"`
426	}
427
428	for _, test := range tests {
429		if test.expectError {
430			assertParseFail(t, test.errType, test.errMsg, &opts, test.args...)
431		} else {
432			args := assertParseSuccess(t, &opts, test.args...)
433
434			assertStringArray(t, args, test.rest)
435		}
436	}
437}
438
439func TestUnknownFlagHandler(t *testing.T) {
440
441	var opts struct {
442		Flag1 string `long:"flag1"`
443		Flag2 string `long:"flag2"`
444	}
445
446	p := NewParser(&opts, None)
447
448	var unknownFlag1 string
449	var unknownFlag2 bool
450	var unknownFlag3 string
451
452	// Set up a callback to intercept unknown options during parsing
453	p.UnknownOptionHandler = func(option string, arg SplitArgument, args []string) ([]string, error) {
454		if option == "unknownFlag1" {
455			if argValue, ok := arg.Value(); ok {
456				unknownFlag1 = argValue
457				return args, nil
458			}
459			// consume a value from remaining args list
460			unknownFlag1 = args[0]
461			return args[1:], nil
462		} else if option == "unknownFlag2" {
463			// treat this one as a bool switch, don't consume any args
464			unknownFlag2 = true
465			return args, nil
466		} else if option == "unknownFlag3" {
467			if argValue, ok := arg.Value(); ok {
468				unknownFlag3 = argValue
469				return args, nil
470			}
471			// consume a value from remaining args list
472			unknownFlag3 = args[0]
473			return args[1:], nil
474		}
475
476		return args, fmt.Errorf("Unknown flag: %v", option)
477	}
478
479	// Parse args containing some unknown flags, verify that
480	// our callback can handle all of them
481	_, err := p.ParseArgs([]string{"--flag1=stuff", "--unknownFlag1", "blah", "--unknownFlag2", "--unknownFlag3=baz", "--flag2=foo"})
482
483	if err != nil {
484		assertErrorf(t, "Parser returned unexpected error %v", err)
485	}
486
487	assertString(t, opts.Flag1, "stuff")
488	assertString(t, opts.Flag2, "foo")
489	assertString(t, unknownFlag1, "blah")
490	assertString(t, unknownFlag3, "baz")
491
492	if !unknownFlag2 {
493		assertErrorf(t, "Flag should have been set by unknown handler, but had value: %v", unknownFlag2)
494	}
495
496	// Parse args with unknown flags that callback doesn't handle, verify it returns error
497	_, err = p.ParseArgs([]string{"--flag1=stuff", "--unknownFlagX", "blah", "--flag2=foo"})
498
499	if err == nil {
500		assertErrorf(t, "Parser should have returned error, but returned nil")
501	}
502}
503
504func TestChoices(t *testing.T) {
505	var opts struct {
506		Choice string `long:"choose" choice:"v1" choice:"v2"`
507	}
508
509	assertParseFail(t, ErrInvalidChoice, "Invalid value `invalid' for option `"+defaultLongOptDelimiter+"choose'. Allowed values are: v1 or v2", &opts, "--choose", "invalid")
510	assertParseSuccess(t, &opts, "--choose", "v2")
511	assertString(t, opts.Choice, "v2")
512}
513
514func TestEmbedded(t *testing.T) {
515	type embedded struct {
516		V bool `short:"v"`
517	}
518	var opts struct {
519		embedded
520	}
521
522	assertParseSuccess(t, &opts, "-v")
523
524	if !opts.V {
525		t.Errorf("Expected V to be true")
526	}
527}
528
529type command struct {
530}
531
532func (c *command) Execute(args []string) error {
533	return nil
534}
535
536func TestCommandHandlerNoCommand(t *testing.T) {
537	var opts = struct {
538		Value bool `short:"v"`
539	}{}
540
541	parser := NewParser(&opts, Default&^PrintErrors)
542
543	var executedCommand Commander
544	var executedArgs []string
545
546	executed := false
547
548	parser.CommandHandler = func(command Commander, args []string) error {
549		executed = true
550
551		executedCommand = command
552		executedArgs = args
553
554		return nil
555	}
556
557	_, err := parser.ParseArgs([]string{"arg1", "arg2"})
558
559	if err != nil {
560		t.Fatalf("Unexpected parse error: %s", err)
561	}
562
563	if !executed {
564		t.Errorf("Expected command handler to be executed")
565	}
566
567	if executedCommand != nil {
568		t.Errorf("Did not exect an executed command")
569	}
570
571	assertStringArray(t, executedArgs, []string{"arg1", "arg2"})
572}
573
574func TestCommandHandler(t *testing.T) {
575	var opts = struct {
576		Value bool `short:"v"`
577
578		Command command `command:"cmd"`
579	}{}
580
581	parser := NewParser(&opts, Default&^PrintErrors)
582
583	var executedCommand Commander
584	var executedArgs []string
585
586	executed := false
587
588	parser.CommandHandler = func(command Commander, args []string) error {
589		executed = true
590
591		executedCommand = command
592		executedArgs = args
593
594		return nil
595	}
596
597	_, err := parser.ParseArgs([]string{"cmd", "arg1", "arg2"})
598
599	if err != nil {
600		t.Fatalf("Unexpected parse error: %s", err)
601	}
602
603	if !executed {
604		t.Errorf("Expected command handler to be executed")
605	}
606
607	if executedCommand == nil {
608		t.Errorf("Expected command handler to be executed")
609	}
610
611	assertStringArray(t, executedArgs, []string{"arg1", "arg2"})
612}
613