1 package ffcli_test
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "flag"
8 "fmt"
9 "io/ioutil"
10 "log"
11 "reflect"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/peterbourgon/ff/v3/ffcli"
17 "github.com/peterbourgon/ff/v3/fftest"
18 )
19
20 func TestCommandRun(t *testing.T) {
21 t.Parallel()
22
23 for _, testcase := range []struct {
24 name string
25 args []string
26 rootvars fftest.Vars
27 rootran bool
28 rootargs []string
29 foovars fftest.Vars
30 fooran bool
31 fooargs []string
32 barvars fftest.Vars
33 barran bool
34 barargs []string
35 }{
36 {
37 name: "root",
38 rootran: true,
39 },
40 {
41 name: "root flags",
42 args: []string{"-s", "123", "-b"},
43 rootvars: fftest.Vars{S: "123", B: true},
44 rootran: true,
45 },
46 {
47 name: "root args",
48 args: []string{"hello"},
49 rootran: true,
50 rootargs: []string{"hello"},
51 },
52 {
53 name: "root flags args",
54 args: []string{"-i=123", "hello world"},
55 rootvars: fftest.Vars{I: 123},
56 rootran: true,
57 rootargs: []string{"hello world"},
58 },
59 {
60 name: "root flags -- args",
61 args: []string{"-f", "1.23", "--", "hello", "world"},
62 rootvars: fftest.Vars{F: 1.23},
63 rootran: true,
64 rootargs: []string{"hello", "world"},
65 },
66 {
67 name: "root foo",
68 args: []string{"foo"},
69 fooran: true,
70 },
71 {
72 name: "root flags foo",
73 args: []string{"-s", "OK", "-d", "10m", "foo"},
74 rootvars: fftest.Vars{S: "OK", D: 10 * time.Minute},
75 fooran: true,
76 },
77 {
78 name: "root flags foo flags",
79 args: []string{"-s", "OK", "-d", "10m", "foo", "-s", "Yup"},
80 rootvars: fftest.Vars{S: "OK", D: 10 * time.Minute},
81 foovars: fftest.Vars{S: "Yup"},
82 fooran: true,
83 },
84 {
85 name: "root flags foo flags args",
86 args: []string{"-f=0.99", "foo", "-f", "1.01", "verb", "noun", "adjective adjective"},
87 rootvars: fftest.Vars{F: 0.99},
88 foovars: fftest.Vars{F: 1.01},
89 fooran: true,
90 fooargs: []string{"verb", "noun", "adjective adjective"},
91 },
92 {
93 name: "root flags foo args",
94 args: []string{"-f=0.99", "foo", "abc", "def", "ghi"},
95 rootvars: fftest.Vars{F: 0.99},
96 fooran: true,
97 fooargs: []string{"abc", "def", "ghi"},
98 },
99 {
100 name: "root bar -- args",
101 args: []string{"bar", "--", "argument", "list"},
102 barran: true,
103 barargs: []string{"argument", "list"},
104 },
105 } {
106 t.Run(testcase.name, func(t *testing.T) {
107 foofs, foovars := fftest.Pair()
108 var fooargs []string
109 var fooran bool
110 foo := &ffcli.Command{
111 Name: "foo",
112 FlagSet: foofs,
113 Exec: func(_ context.Context, args []string) error { fooran, fooargs = true, args; return nil },
114 }
115
116 barfs, barvars := fftest.Pair()
117 var barargs []string
118 var barran bool
119 bar := &ffcli.Command{
120 Name: "bar",
121 FlagSet: barfs,
122 Exec: func(_ context.Context, args []string) error { barran, barargs = true, args; return nil },
123 }
124
125 rootfs, rootvars := fftest.Pair()
126 var rootargs []string
127 var rootran bool
128 root := &ffcli.Command{
129 FlagSet: rootfs,
130 Subcommands: []*ffcli.Command{foo, bar},
131 Exec: func(_ context.Context, args []string) error { rootran, rootargs = true, args; return nil },
132 }
133
134 err := root.ParseAndRun(context.Background(), testcase.args)
135 assertNoError(t, err)
136 fftest.Compare(t, &testcase.rootvars, rootvars)
137 assertBool(t, testcase.rootran, rootran)
138 assertStringSlice(t, testcase.rootargs, rootargs)
139 fftest.Compare(t, &testcase.foovars, foovars)
140 assertBool(t, testcase.fooran, fooran)
141 assertStringSlice(t, testcase.fooargs, fooargs)
142 fftest.Compare(t, &testcase.barvars, barvars)
143 assertBool(t, testcase.barran, barran)
144 assertStringSlice(t, testcase.barargs, barargs)
145 })
146 }
147 }
148
149 func TestHelpUsage(t *testing.T) {
150 t.Parallel()
151
152 for _, testcase := range []struct {
153 name string
154 usageFunc func(*ffcli.Command) string
155 exec func(context.Context, []string) error
156 args []string
157 output string
158 }{
159 {
160 name: "nil",
161 args: []string{"-h"},
162 output: defaultUsageFuncOutput,
163 },
164 {
165 name: "DefaultUsageFunc",
166 usageFunc: ffcli.DefaultUsageFunc,
167 args: []string{"-h"},
168 output: defaultUsageFuncOutput,
169 },
170 {
171 name: "custom usage",
172 usageFunc: func(*ffcli.Command) string { return "๐ฐ" },
173 args: []string{"-h"},
174 output: "๐ฐ\n",
175 },
176 {
177 name: "ErrHelp",
178 usageFunc: func(*ffcli.Command) string { return "๐น" },
179 exec: func(context.Context, []string) error { return flag.ErrHelp },
180 output: "๐น\n",
181 },
182 } {
183 t.Run(testcase.name, func(t *testing.T) {
184 fs, _ := fftest.Pair()
185 var buf bytes.Buffer
186 fs.SetOutput(&buf)
187
188 command := &ffcli.Command{
189 Name: "TestHelpUsage",
190 ShortUsage: "TestHelpUsage [flags] <args>",
191 ShortHelp: "Some short help.",
192 LongHelp: "Some long help.",
193 FlagSet: fs,
194 UsageFunc: testcase.usageFunc,
195 Exec: testcase.exec,
196 }
197
198 err := command.ParseAndRun(context.Background(), testcase.args)
199 assertErrorIs(t, flag.ErrHelp, err)
200 assertMultilineString(t, testcase.output, buf.String())
201 })
202 }
203 }
204
205 func TestNestedOutput(t *testing.T) {
206 t.Parallel()
207
208 for _, testcase := range []struct {
209 name string
210 args []string
211 wantErr error
212 wantOutput string
213 }{
214 {
215 name: "root without args",
216 args: []string{},
217 wantErr: flag.ErrHelp,
218 wantOutput: "root usage func\n",
219 },
220 {
221 name: "root with args",
222 args: []string{"abc", "def ghi"},
223 wantErr: flag.ErrHelp,
224 wantOutput: "root usage func\n",
225 },
226 {
227 name: "root help",
228 args: []string{"-h"},
229 wantErr: flag.ErrHelp,
230 wantOutput: "root usage func\n",
231 },
232 {
233 name: "foo without args",
234 args: []string{"foo"},
235 wantOutput: "foo: ''\n",
236 },
237 {
238 name: "foo with args",
239 args: []string{"foo", "alpha", "beta"},
240 wantOutput: "foo: 'alpha beta'\n",
241 },
242 {
243 name: "foo help",
244 args: []string{"foo", "-h"},
245 wantErr: flag.ErrHelp,
246 wantOutput: "foo usage func\n",
247 },
248 {
249 name: "foo bar without args",
250 args: []string{"foo", "bar"},
251 wantErr: flag.ErrHelp,
252 wantOutput: "bar usage func\n",
253 },
254 {
255 name: "foo bar with args",
256 args: []string{"foo", "bar", "--", "baz quux"},
257 wantErr: flag.ErrHelp,
258 wantOutput: "bar usage func\n",
259 },
260 {
261 name: "foo bar help",
262 args: []string{"foo", "bar", "--help"},
263 wantErr: flag.ErrHelp,
264 wantOutput: "bar usage func\n",
265 },
266 } {
267 t.Run(testcase.name, func(t *testing.T) {
268 var (
269 rootfs = flag.NewFlagSet("root", flag.ContinueOnError)
270 foofs = flag.NewFlagSet("foo", flag.ContinueOnError)
271 barfs = flag.NewFlagSet("bar", flag.ContinueOnError)
272 buf bytes.Buffer
273 )
274 rootfs.SetOutput(&buf)
275 foofs.SetOutput(&buf)
276 barfs.SetOutput(&buf)
277
278 barExec := func(_ context.Context, args []string) error {
279 return flag.ErrHelp
280 }
281
282 bar := &ffcli.Command{
283 Name: "bar",
284 FlagSet: barfs,
285 UsageFunc: func(*ffcli.Command) string { return "bar usage func" },
286 Exec: barExec,
287 }
288
289 fooExec := func(_ context.Context, args []string) error {
290 fmt.Fprintf(&buf, "foo: '%s'\n", strings.Join(args, " "))
291 return nil
292 }
293
294 foo := &ffcli.Command{
295 Name: "foo",
296 FlagSet: foofs,
297 UsageFunc: func(*ffcli.Command) string { return "foo usage func" },
298 Subcommands: []*ffcli.Command{bar},
299 Exec: fooExec,
300 }
301
302 rootExec := func(_ context.Context, args []string) error {
303 return flag.ErrHelp
304 }
305
306 root := &ffcli.Command{
307 FlagSet: rootfs,
308 UsageFunc: func(*ffcli.Command) string { return "root usage func" },
309 Subcommands: []*ffcli.Command{foo},
310 Exec: rootExec,
311 }
312
313 err := root.ParseAndRun(context.Background(), testcase.args)
314 if want, have := testcase.wantErr, err; !errors.Is(have, want) {
315 t.Errorf("error: want %v, have %v", want, have)
316 }
317 if want, have := testcase.wantOutput, buf.String(); want != have {
318 t.Errorf("output: want %q, have %q", want, have)
319 }
320 })
321 }
322 }
323
324 func TestIssue57(t *testing.T) {
325 t.Parallel()
326
327 for _, testcase := range []struct {
328 args []string
329 parseErrAs error
330 parseErrIs error
331 parseErrStr string
332 runErrAs error
333 runErrIs error
334 runErrStr string
335 }{
336 {
337 args: []string{},
338 parseErrAs: &ffcli.NoExecError{},
339 runErrAs: &ffcli.NoExecError{},
340 },
341 {
342 args: []string{"-h"},
343 parseErrIs: flag.ErrHelp,
344 runErrIs: ffcli.ErrUnparsed,
345 },
346 {
347 args: []string{"bar"},
348 parseErrAs: &ffcli.NoExecError{},
349 runErrAs: &ffcli.NoExecError{},
350 },
351 {
352 args: []string{"bar", "-h"},
353 parseErrAs: flag.ErrHelp,
354 runErrAs: ffcli.ErrUnparsed,
355 },
356 {
357 args: []string{"bar", "-undefined"},
358 parseErrStr: "error parsing commandline args: flag provided but not defined: -undefined",
359 runErrIs: ffcli.ErrUnparsed,
360 },
361 {
362 args: []string{"bar", "baz"},
363 },
364 {
365 args: []string{"bar", "baz", "-h"},
366 parseErrIs: flag.ErrHelp,
367 runErrIs: ffcli.ErrUnparsed,
368 },
369 {
370 args: []string{"bar", "baz", "-also.undefined"},
371 parseErrStr: "error parsing commandline args: flag provided but not defined: -also.undefined",
372 runErrIs: ffcli.ErrUnparsed,
373 },
374 } {
375 t.Run(strings.Join(append([]string{"foo"}, testcase.args...), " "), func(t *testing.T) {
376 fs := flag.NewFlagSet("ยท", flag.ContinueOnError)
377 fs.SetOutput(ioutil.Discard)
378
379 var (
380 baz = &ffcli.Command{Name: "baz", FlagSet: fs, Exec: func(_ context.Context, args []string) error { return nil }}
381 bar = &ffcli.Command{Name: "bar", FlagSet: fs, Subcommands: []*ffcli.Command{baz}}
382 foo = &ffcli.Command{Name: "foo", FlagSet: fs, Subcommands: []*ffcli.Command{bar}}
383 )
384
385 var (
386 parseErr = foo.Parse(testcase.args)
387 runErr = foo.Run(context.Background())
388 )
389
390 if testcase.parseErrAs != nil {
391 if want, have := &testcase.parseErrAs, parseErr; !errors.As(have, want) {
392 t.Errorf("Parse: want %v, have %v", want, have)
393 }
394 }
395
396 if testcase.parseErrIs != nil {
397 if want, have := testcase.parseErrIs, parseErr; !errors.Is(have, want) {
398 t.Errorf("Parse: want %v, have %v", want, have)
399 }
400 }
401
402 if testcase.parseErrStr != "" {
403 if want, have := testcase.parseErrStr, parseErr.Error(); want != have {
404 t.Errorf("Parse: want %q, have %q", want, have)
405 }
406 }
407
408 if testcase.runErrAs != nil {
409 if want, have := &testcase.runErrAs, runErr; !errors.As(have, want) {
410 t.Errorf("Run: want %v, have %v", want, have)
411 }
412 }
413
414 if testcase.runErrIs != nil {
415 if want, have := testcase.runErrIs, runErr; !errors.Is(have, want) {
416 t.Errorf("Run: want %v, have %v", want, have)
417 }
418 }
419
420 if testcase.runErrStr != "" {
421 if want, have := testcase.runErrStr, runErr.Error(); want != have {
422 t.Errorf("Run: want %q, have %q", want, have)
423 }
424 }
425
426 var (
427 noParseErr = testcase.parseErrAs == nil && testcase.parseErrIs == nil && testcase.parseErrStr == ""
428 noRunErr = testcase.runErrAs == nil && testcase.runErrIs == nil && testcase.runErrStr == ""
429 )
430 if noParseErr && noRunErr {
431 if parseErr != nil {
432 t.Errorf("Parse: unexpected error: %v", parseErr)
433 }
434 if runErr != nil {
435 t.Errorf("Run: unexpected error: %v", runErr)
436 }
437 }
438 })
439 }
440 }
441
442 func ExampleCommand_Parse_then_Run() {
443
444 type FooClient struct {
445 token string
446 }
447
448
449 NewFooClient := func(token string) (*FooClient, error) {
450 if token == "" {
451 return nil, fmt.Errorf("token required")
452 }
453 return &FooClient{token: token}, nil
454 }
455
456
457 var (
458 rootFlagSet = flag.NewFlagSet("mycommand", flag.ExitOnError)
459 token = rootFlagSet.String("token", "", "API token")
460 )
461
462
463 var client *FooClient
464
465
466
467 foo := &ffcli.Command{
468 Name: "foo",
469 Exec: func(context.Context, []string) error {
470 fmt.Printf("subcommand foo can use the client: %v", client)
471 return nil
472 },
473 }
474
475 root := &ffcli.Command{
476 FlagSet: rootFlagSet,
477 Subcommands: []*ffcli.Command{foo},
478 }
479
480
481 if err := root.Parse([]string{"-token", "SECRETKEY", "foo"}); err != nil {
482 log.Fatalf("Parse failure: %v", err)
483 }
484
485
486 var err error
487 client, err = NewFooClient(*token)
488 if err != nil {
489 log.Fatalf("error constructing FooClient: %v", err)
490 }
491
492
493 if err := root.Run(context.Background()); err != nil {
494 log.Fatalf("Run failure: %v", err)
495 }
496
497
498
499 }
500
501 func assertNoError(t *testing.T, err error) {
502 t.Helper()
503 if err != nil {
504 t.Fatal(err)
505 }
506 }
507
508 func assertErrorIs(t *testing.T, want, have error) {
509 t.Helper()
510 if !errors.Is(have, want) {
511 t.Fatalf("want %v, have %v", want, have)
512 }
513 }
514
515 func assertMultilineString(t *testing.T, want, have string) {
516 t.Helper()
517 if want != have {
518 t.Fatalf("\nwant:\n%s\n\nhave:\n%s\n", want, have)
519 }
520 }
521
522 func assertBool(t *testing.T, want, have bool) {
523 t.Helper()
524 if want != have {
525 t.Fatalf("want %v, have %v", want, have)
526 }
527 }
528
529 func assertStringSlice(t *testing.T, want, have []string) {
530 t.Helper()
531 if len(want) == 0 && len(have) == 0 {
532 return
533 }
534 if !reflect.DeepEqual(want, have) {
535 t.Fatalf("want %#+v, have %#+v", want, have)
536 }
537 }
538
539 var defaultUsageFuncOutput = strings.TrimSpace(`
540 USAGE
541 TestHelpUsage [flags] <args>
542
543 Some long help.
544
545 FLAGS
546 -b=false bool
547 -d 0s time.Duration
548 -f 0 float64
549 -i 0 int
550 -s ... string
551 -x ... collection of strings (repeatable)
552 `) + "\n\n"
553
View as plain text