1package zli_test
2
3import (
4	"fmt"
5	"regexp"
6	"strings"
7	"testing"
8
9	"zgo.at/zli"
10)
11
12func ExampleFlags() {
13	// Create new flags from os.Args.
14	f := zli.NewFlags([]string{"example", "-vv", "-f=csv", "-a", "xx", "yy"})
15
16	// Add a string, bool, and "counter" flag.
17	var (
18		verbose = f.IntCounter(0, "v", "verbose")
19		all     = f.Bool(false, "a", "all")
20		format  = f.String("", "f", "format")
21	)
22
23	// Shift the first argument (i.e. os.Args[1], if any, empty string if there
24	// isn't). Useful to get the "subcommand" name. This works before and after
25	// Parse().
26	switch f.Shift() {
27	case "help":
28		// Run help
29	case "install":
30		// Run install
31	case "":
32		// Error: need a command (or just print the usage)
33	default:
34		// Error: Unknown command
35	}
36
37	// Parse the shebang!
38	err := f.Parse()
39	if err != nil {
40		// Print error, usage.
41	}
42
43	// You can check if the flag was present on the CLI with Set(). This way you
44	// can distinguish between "was an empty value passed" // (-format '') and
45	// "this flag wasn't on the CLI".
46	if format.Set() {
47		fmt.Println("Format was set to", format.String())
48	}
49
50	// The IntCounter adds 1 for every time the -v flag is on the CLI.
51	if verbose.Int() > 1 {
52		// ...Print very verbose info.
53	} else if verbose.Int() > 0 {
54		// ...Print less verbose info.
55	}
56
57	// Just a bool!
58	fmt.Println("All:", all.Bool())
59
60	// f.Args is set to everything that's not a flag or argument.
61	fmt.Println("Remaining:", f.Args)
62
63	// Output:
64	// Format was set to csv
65	// All: true
66	// Remaining: [xx yy]
67}
68
69func TestFlags(t *testing.T) {
70	tests := []struct {
71		name    string
72		args    []string
73		flags   func(*zli.Flags) []interface{}
74		want    string
75		wantErr string
76	}{
77		// No arguments, no problem.
78		{"nil args", nil,
79			func(f *zli.Flags) []interface{} {
80				return []interface{}{f.Bool(false, "b")}
81			}, `
82			bool 1false
83			args0 []
84			`, ""},
85		{"empty args", []string{},
86			func(f *zli.Flags) []interface{} {
87				return []interface{}{f.Bool(false, "b")}
88			}, `
89			bool 1false
90			args0 []
91			`, ""},
92		{"prog name", []string{"progname"},
93			func(f *zli.Flags) []interface{} {
94				return []interface{}{f.Bool(false, "b")}
95			}, `
96			bool 1false
97			args0 []
98			`, ""},
99
100		// Get positional arguments
101		{"1 arg", []string{"progname", "pos1"},
102			func(f *zli.Flags) []interface{} {
103				return []interface{}{f.String("", "s")}
104			}, `
105			string 1""
106			args1 [pos1]
107			`, ""},
108		{"args with space", []string{"progname", "pos1", "pos 2", "pos\n3"},
109			func(f *zli.Flags) []interface{} {
110				return []interface{}{f.String("", "s")}
111			}, `
112			string 1""
113			args3 [pos1 pos 2 pos
114							3]
115			`, ""},
116		{"after flag", []string{"progname", "-s", "arg", "pos 1", "pos 2"},
117			func(f *zli.Flags) []interface{} {
118				return []interface{}{f.String("", "s")}
119			}, `
120			string 1"arg"
121			args2 [pos 1 pos 2]
122			`, ""},
123		{"before flag", []string{"progname", "pos 1", "pos 2", "-s", "arg"},
124			func(f *zli.Flags) []interface{} {
125				return []interface{}{f.String("", "s")}
126			}, `
127			string 1"arg"
128			args2 [pos 1 pos 2]
129			`, ""},
130		{"before and after flag", []string{"progname", "pos 1", "-s", "arg", "pos 2"},
131			func(f *zli.Flags) []interface{} {
132				return []interface{}{f.String("", "s")}
133			}, `
134			string 1"arg"
135			args2 [pos 1 pos 2]
136			`, ""},
137
138		{"single - is a valid argument", []string{"progname", "-s", "-", "-"},
139			func(f *zli.Flags) []interface{} {
140				return []interface{}{f.String("", "s")}
141			}, `
142			string 1"-"
143			args1 [-]
144			`, ""},
145		// Make sure parsing is stopped after --
146		{
147			"-- bool", []string{"prog", "-b", "--"},
148			func(f *zli.Flags) []interface{} {
149				return []interface{}{
150					f.Bool(false, "b"),
151				}
152			}, `
153				bool 1true
154				args0 []
155			`, ""},
156		//
157		/*
158			{[]string{"prog", "-b", "--", "-str"}, "", `
159					str   | false | "default"
160					bool  | true  | true
161					args  | 1     | [-str]
162				`},
163			{[]string{"prog", "-b", "--", ""}, "", `
164					str   | false | "default"
165					bool  | true  | true
166					args  | 1     | []
167				`},
168			// Various --
169		*/
170
171		// Basic test for all the different flag types.
172		{"bool", []string{"prog", "-b"},
173			func(f *zli.Flags) []interface{} {
174				return []interface{}{f.Bool(false, "b")}
175			}, `
176			bool 1true
177			args0 []
178			`, ""},
179		{"string", []string{"prog", "-s", "val"},
180			func(f *zli.Flags) []interface{} {
181				return []interface{}{f.String("", "s")}
182			}, `
183				string 1"val"
184				args0 []
185			`, ""},
186		{"int", []string{"prog", "-i", "42"},
187			func(f *zli.Flags) []interface{} {
188				return []interface{}{f.Int(0, "i")}
189			}, `
190				int 142
191				args0 []
192			`, ""},
193		{"int64", []string{"prog", "-i", "42"},
194			func(f *zli.Flags) []interface{} {
195				return []interface{}{f.Int64(0, "i")}
196			}, `
197				int64 142
198				args0 []
199			`, ""},
200		{"int64", []string{"prog", "-i", "1_000_000"},
201			func(f *zli.Flags) []interface{} {
202				return []interface{}{f.Int64(0, "i")}
203			}, `
204				int64 11000000
205				args0 []
206			`, ""},
207		{"int64", []string{"prog", "-i", "0x10"},
208			func(f *zli.Flags) []interface{} {
209				return []interface{}{f.Int64(0, "i")}
210			}, `
211				int64 116
212				args0 []
213			`, ""},
214		{"float64", []string{"prog", "-i", "42.666"},
215			func(f *zli.Flags) []interface{} {
216				return []interface{}{f.Float64(0, "i")}
217			}, `
218				float64 142.666000
219				args0 []
220			`, ""},
221		{"intcounter", []string{"prog", "-i", "-i", "-i"},
222			func(f *zli.Flags) []interface{} {
223				return []interface{}{f.IntCounter(0, "i")}
224			}, `
225				int 13
226				args0 []
227			`, ""},
228		{"stringlist", []string{"prog", "-s", "a", "-s", "b", "-s", "c"},
229			func(f *zli.Flags) []interface{} {
230				return []interface{}{f.StringList(nil, "s")}
231			}, `
232				list 1 → [a b c]
233				args0 []
234			`, ""},
235		{"intlist", []string{"prog", "-s", "1", "-s", "3", "-s", "5"},
236			func(f *zli.Flags) []interface{} {
237				return []interface{}{f.IntList(nil, "s")}
238			}, `
239				list 1 → [1 3 5]
240				args0 []
241			`, ""},
242
243		// Various kinds of wrong input.
244		{"unknown", []string{"prog", "-x"},
245			func(f *zli.Flags) []interface{} {
246				return []interface{}{f.String("", "s")}
247			}, `
248				string 1""
249				args1 [-x]
250			`, `unknown flag: "-x"`},
251		{"no argument", []string{"prog", "-s"},
252			func(f *zli.Flags) []interface{} {
253				return []interface{}{f.String("", "s")}
254			}, `
255				string 1""
256				args1 [-s]
257			`, "-s: needs an argument"},
258		{"multiple", []string{"prog", "-s=a", "-s=b"},
259			func(f *zli.Flags) []interface{} {
260				return []interface{}{f.String("", "s")}
261			}, `
262				string 1"a"
263				args2 [-s=a -s=b]
264			`, `flag given more than once: "-s=b"`},
265		{"not an int", []string{"prog", "-i=no"},
266			func(f *zli.Flags) []interface{} {
267				return []interface{}{f.Int(42, "i")}
268			}, `
269				int 142
270				args1 [-i=no]
271		`, `-i=no: invalid syntax (must be a number)`},
272		{"not an int64", []string{"prog", "-i=no"},
273			func(f *zli.Flags) []interface{} {
274				return []interface{}{f.Int64(42, "i")}
275			}, `
276				int64 142
277				args1 [-i=no]
278		`, `-i=no: invalid syntax (must be a number)`},
279		{"not a float", []string{"prog", "-i=no"},
280			func(f *zli.Flags) []interface{} {
281				return []interface{}{f.Float64(42, "i")}
282			}, `
283				float64 142.000000
284				args1 [-i=no]
285		`, `-i=no: invalid syntax (must be a number)`},
286
287		// Argument parsing
288		{"-s=arg", []string{"prog", "-s=xx"},
289			func(f *zli.Flags) []interface{} {
290				return []interface{}{
291					f.String("default", "s"),
292				}
293			}, `
294				string 1"xx"
295				args0 []
296			`, ""},
297		{"--s=arg", []string{"prog", "--s=xx"},
298			func(f *zli.Flags) []interface{} {
299				return []interface{}{
300					f.String("default", "s"),
301				}
302			}, `
303				string 1"xx"
304				args0 []
305			`, ""},
306		{"--s=-arg", []string{"prog", "--s=-xx"},
307			func(f *zli.Flags) []interface{} {
308				return []interface{}{
309					f.String("default", "s"),
310				}
311			}, `
312				string 1"-xx"
313				args0 []
314			`, ""},
315		{"--s=-o", []string{"prog", "--s=-o"},
316			func(f *zli.Flags) []interface{} {
317				return []interface{}{
318					f.String("default", "s"),
319					f.String("default", "o"),
320				}
321			}, `
322				string 1"-o"
323				string 2"default"
324				args0 []
325			`, ""},
326		// TODO: this should probably be an error?
327		{"--s -o", []string{"prog", "-s", "-o"},
328			func(f *zli.Flags) []interface{} {
329				return []interface{}{
330					f.String("", "s"),
331					f.String("", "o"),
332				}
333			}, `
334				string 1"-o"
335				string 2""
336				args0 []
337			`, ""},
338		{"--s arg", []string{"prog", "--s", "xx"},
339			func(f *zli.Flags) []interface{} {
340				return []interface{}{
341					f.String("default", "s"),
342				}
343			}, `
344				string 1"xx"
345				args0 []
346			`, ""},
347		{"blank =", []string{"prog", "-s="},
348			func(f *zli.Flags) []interface{} {
349				return []interface{}{
350					f.String("default", "s"),
351				}
352			}, `
353				string 1""
354				args0 []
355			`, ""},
356		{"blank space", []string{"prog", "-s", ""},
357			func(f *zli.Flags) []interface{} {
358				return []interface{}{
359					f.String("default", "s"),
360				}
361			}, `
362				string 1""
363				args0 []
364			`, ""},
365
366		// Okay for booleans to have multiple flags, as it doesn't really
367		// matter.
368		{"multiple bool", []string{"prog", "-b", "-b"},
369			func(f *zli.Flags) []interface{} {
370				return []interface{}{f.Bool(false, "b")}
371			}, `
372				bool 1true
373				args0 []
374			`, ""},
375
376		// Group -ab as -a -b if they're booleans.
377		{"group bool", []string{"prog", "-a", "-b"},
378			func(f *zli.Flags) []interface{} {
379				return []interface{}{
380					f.Bool(false, "a"),
381					f.Bool(false, "b"),
382				}
383			}, `
384				bool 1true
385				bool 2true
386				args0 []
387			`, ""},
388		{"group bool", []string{"prog", "-ab"},
389			func(f *zli.Flags) []interface{} {
390				return []interface{}{
391					f.Bool(false, "a"),
392					f.Bool(false, "b"),
393				}
394			}, `
395				bool 1true
396				bool 2true
397				args0 []
398			`, ""},
399		{"group bool only with single -", []string{"prog", "--ab"},
400			func(f *zli.Flags) []interface{} {
401				return []interface{}{
402					f.Bool(false, "a"),
403					f.Bool(false, "b"),
404				}
405			}, `
406				bool 1false
407				bool 2false
408				args1 [--ab]
409			`, `unknown flag: "--ab"`},
410		{"long flag overrides grouped bool", []string{"prog", "--ab", "x"},
411			func(f *zli.Flags) []interface{} {
412				return []interface{}{
413					f.String("", "ab"),
414					f.Bool(false, "a"),
415					f.Bool(false, "b"),
416				}
417			}, `
418				string 1"x"
419				bool 2false
420				bool 3false
421				args0 []
422			`, ""},
423
424		{"arguments starting with - work", []string{"prog", "-b", "-arg", "--long", "--long"},
425			func(f *zli.Flags) []interface{} {
426				return []interface{}{
427					f.String("", "b"),
428					f.String("", "l", "long"),
429				}
430			}, `
431				string 1"-arg"
432				string 2"--long"
433				args0 []
434			`, ""},
435
436		{"prefer_long", []string{"prog", "-long"},
437			func(f *zli.Flags) []interface{} {
438				return []interface{}{
439					f.Bool(false, "long"),
440					f.Bool(false, "l"),
441					f.Bool(false, "o"),
442					f.Bool(false, "n"),
443					f.Bool(false, "g"),
444				}
445			}, `
446				bool 1true
447				bool 2false
448				bool 3false
449				bool 4false
450				bool 5false
451				args0 []
452			`, ""},
453
454		{"prefer_long", []string{"prog", "-long"},
455			func(f *zli.Flags) []interface{} {
456				return []interface{}{
457					f.Bool(false, "l"),
458					f.Bool(false, "o"),
459					f.Bool(false, "long"),
460					f.Bool(false, "n"),
461					f.Bool(false, "g"),
462				}
463			}, `
464				bool 1false
465				bool 2false
466				bool 3true
467				bool 4false
468				bool 5false
469				args0 []
470			`, ""},
471	}
472
473	type (
474		booler       interface{ Bool() bool }
475		stringer     interface{ String() string }
476		inter        interface{ Int() int }
477		int64er      interface{ Int64() int64 }
478		floater      interface{ Float64() float64 }
479		stringlister interface{ Strings() []string }
480		intlister    interface{ Ints() []int }
481	)
482
483	for _, tt := range tests {
484		t.Run(tt.name, func(t *testing.T) {
485			flag := zli.NewFlags(tt.args)
486			setFlags := tt.flags(&flag)
487			err := flag.Parse()
488			if !errorContains(err, tt.wantErr) {
489				t.Fatalf("wrong error\nout:  %v\nwant: %v", err, tt.wantErr)
490			}
491
492			var out string
493			for i, f := range setFlags {
494				switch ff := f.(type) {
495				case booler:
496					out += fmt.Sprintf("bool %d → %t\n", i+1, ff.Bool())
497				case stringer:
498					out += fmt.Sprintf("string %d → %q\n", i+1, ff.String())
499				case inter:
500					out += fmt.Sprintf("int %d → %d\n", i+1, ff.Int())
501				case int64er:
502					out += fmt.Sprintf("int64 %d → %d\n", i+1, ff.Int64())
503				case floater:
504					out += fmt.Sprintf("float64 %d → %f\n", i+1, ff.Float64())
505				case stringlister:
506					out += fmt.Sprintf("list %d → %v\n", i+1, ff.Strings())
507				case intlister:
508					out += fmt.Sprintf("list %d → %v\n", i+1, ff.Ints())
509				default:
510					t.Fatalf("unknown type: %T", f)
511				}
512			}
513			out += fmt.Sprintf("args → %d %v", len(flag.Args), flag.Args)
514
515			want := strings.TrimSpace(strings.ReplaceAll(tt.want, "\t", ""))
516			want = regexp.MustCompile(`\s+→\s+`).ReplaceAllString(want, " → ")
517
518			// Indent so it looks nicer.
519			out = "        " + strings.ReplaceAll(out, "\n", "\n        ")
520			want = "        " + strings.ReplaceAll(want, "\n", "\n        ")
521
522			if out != want {
523				t.Errorf("\nout:\n%s\nwant:\n%s\n", out, want)
524			}
525		})
526	}
527}
528
529func TestShiftCommand(t *testing.T) {
530	tests := []struct {
531		in       []string
532		commands []string
533		want     string
534	}{
535		{[]string{""}, nil, zli.CommandNoneGiven},
536		{[]string{"-a"}, nil, zli.CommandNoneGiven},
537
538		{[]string{"help"}, []string{"asd"}, zli.CommandUnknown},
539
540		{[]string{"help"}, []string{"help", "heee"}, "help"},
541		{[]string{"hel"}, []string{"help", "heee"}, "help"},
542		{[]string{"he"}, []string{"help", "heee"}, zli.CommandAmbiguous},
543
544		{[]string{"usage"}, []string{"help", "usage=help"}, "help"},
545
546		{[]string{"create", "-db=x"}, []string{"create"}, "create"},
547		{[]string{"-flag", "create", "-db=x"}, []string{"create"}, "create"},
548
549		{[]string{"-flag", "create", "-db=x"}, nil, "create"},
550	}
551
552	for _, tt := range tests {
553		t.Run("", func(t *testing.T) {
554			f := zli.NewFlags(append([]string{"prog"}, tt.in...))
555			got := f.ShiftCommand(tt.commands...)
556			f.Bool(false, "a")
557			f.Parse()
558
559			if got != tt.want {
560				t.Errorf("\ngot:  %q\nwant: %q", got, tt.want)
561			}
562		})
563	}
564}
565
566/*
567func TestDoubleParse(t *testing.T) {
568	f := zli.NewFlags([]string{"prog", "-global", "cmd", "-other"})
569	f.IgnoreUnknown(true)
570
571	var global = f.Bool(false, "global")
572	{
573		err := f.Parse()
574		if err != nil {
575			t.Fatal(err)
576		}
577		if !global.Set() {
578			t.Fatal("global not set")
579		}
580	}
581
582	t.Log(f.Args)
583	f.IgnoreUnknown(false)
584	var other = f.Bool(false, "other")
585	err := f.Parse()
586	if err != nil {
587		t.Fatal(err)
588	}
589
590	if other.Set() {
591		t.Error("other not set", f.Args)
592	}
593	if len(f.Args) != 1 && f.Args[1] != "cmd" {
594		t.Error(f.Args)
595	}
596}
597*/
598
599// Just to make sure it's not ridiculously slow or anything.
600func BenchmarkFlag(b *testing.B) {
601	b.ReportAllocs()
602	var err error
603	for n := 0; n < b.N; n++ {
604		flag := zli.NewFlags([]string{"prog", "cmd", "-vv", "-V", "str foo"})
605		flag.Shift()
606		flag.String("", "s", "str")
607		flag.Bool(false, "V", "version")
608		flag.IntCounter(0, "v", "verbose")
609		err = flag.Parse()
610	}
611	_ = err
612}
613
614func errorContains(out error, want string) bool {
615	if out == nil {
616		return want == ""
617	}
618	if want == "" {
619		return false
620	}
621	return strings.Contains(out.Error(), want)
622}
623