package cli import ( "bytes" "errors" "flag" "fmt" "io" "io/ioutil" "os" "reflect" "strings" "testing" ) var ( lastExitCode = 0 fakeOsExiter = func(rc int) { lastExitCode = rc } fakeErrWriter = &bytes.Buffer{} ) func init() { OsExiter = fakeOsExiter ErrWriter = fakeErrWriter } type opCounts struct { Total, BashComplete, OnUsageError, Before, CommandNotFound, Action, After, SubCommand int } func ExampleApp_Run() { // set args for examples sake os.Args = []string{"greet", "--name", "Jeremy"} app := NewApp() app.Name = "greet" app.Flags = []Flag{ StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, } app.Action = func(c *Context) error { fmt.Printf("Hello %v\n", c.String("name")) return nil } app.UsageText = "app [first_arg] [second_arg]" app.Author = "Harrison" app.Email = "harrison@lolwut.com" app.Authors = []Author{{Name: "Oliver Allen", Email: "oliver@toyshop.com"}} app.Run(os.Args) // Output: // Hello Jeremy } func ExampleApp_Run_subcommand() { // set args for examples sake os.Args = []string{"say", "hi", "english", "--name", "Jeremy"} app := NewApp() app.Name = "say" app.Commands = []Command{ { Name: "hello", Aliases: []string{"hi"}, Usage: "use it to see a description", Description: "This is how we describe hello the function", Subcommands: []Command{ { Name: "english", Aliases: []string{"en"}, Usage: "sends a greeting in english", Description: "greets someone in english", Flags: []Flag{ StringFlag{ Name: "name", Value: "Bob", Usage: "Name of the person to greet", }, }, Action: func(c *Context) error { fmt.Println("Hello,", c.String("name")) return nil }, }, }, }, } app.Run(os.Args) // Output: // Hello, Jeremy } func ExampleApp_Run_appHelp() { // set args for examples sake os.Args = []string{"greet", "help"} app := NewApp() app.Name = "greet" app.Version = "0.1.0" app.Description = "This is how we describe greet the app" app.Authors = []Author{ {Name: "Harrison", Email: "harrison@lolwut.com"}, {Name: "Oliver Allen", Email: "oliver@toyshop.com"}, } app.Flags = []Flag{ StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, } app.Commands = []Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(c *Context) error { fmt.Printf("i like to describe things") return nil }, }, } app.Run(os.Args) // Output: // NAME: // greet - A new cli application // // USAGE: // greet [global options] command [command options] [arguments...] // // VERSION: // 0.1.0 // // DESCRIPTION: // This is how we describe greet the app // // AUTHORS: // Harrison // Oliver Allen // // COMMANDS: // describeit, d use it to see a description // help, h Shows a list of commands or help for one command // // GLOBAL OPTIONS: // --name value a name to say (default: "bob") // --help, -h show help // --version, -v print the version } func ExampleApp_Run_commandHelp() { // set args for examples sake os.Args = []string{"greet", "h", "describeit"} app := NewApp() app.Name = "greet" app.Flags = []Flag{ StringFlag{Name: "name", Value: "bob", Usage: "a name to say"}, } app.Commands = []Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(c *Context) error { fmt.Printf("i like to describe things") return nil }, }, } app.Run(os.Args) // Output: // NAME: // greet describeit - use it to see a description // // USAGE: // greet describeit [arguments...] // // DESCRIPTION: // This is how we describe describeit the function } func ExampleApp_Run_noAction() { app := App{} app.Name = "greet" app.Run([]string{"greet"}) // Output: // NAME: // greet // // USAGE: // [global options] command [command options] [arguments...] // // COMMANDS: // help, h Shows a list of commands or help for one command // // GLOBAL OPTIONS: // --help, -h show help // --version, -v print the version } func ExampleApp_Run_subcommandNoAction() { app := App{} app.Name = "greet" app.Commands = []Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", }, } app.Run([]string{"greet", "describeit"}) // Output: // NAME: // describeit - use it to see a description // // USAGE: // describeit [arguments...] // // DESCRIPTION: // This is how we describe describeit the function } func ExampleApp_Run_bashComplete() { // set args for examples sake os.Args = []string{"greet", "--generate-bash-completion"} app := NewApp() app.Name = "greet" app.EnableBashCompletion = true app.Commands = []Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(c *Context) error { fmt.Printf("i like to describe things") return nil }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", Action: func(c *Context) error { fmt.Printf("the next example") return nil }, }, } app.Run(os.Args) // Output: // describeit // d // next // help // h } func ExampleApp_Run_zshComplete() { // set args for examples sake os.Args = []string{"greet", "--generate-bash-completion"} os.Setenv("_CLI_ZSH_AUTOCOMPLETE_HACK", "1") app := NewApp() app.Name = "greet" app.EnableBashCompletion = true app.Commands = []Command{ { Name: "describeit", Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(c *Context) error { fmt.Printf("i like to describe things") return nil }, }, { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", Action: func(c *Context) error { fmt.Printf("the next example") return nil }, }, } app.Run(os.Args) // Output: // describeit:use it to see a description // d:use it to see a description // next:next example // help:Shows a list of commands or help for one command // h:Shows a list of commands or help for one command } func TestApp_Run(t *testing.T) { s := "" app := NewApp() app.Action = func(c *Context) error { s = s + c.Args().First() return nil } err := app.Run([]string{"command", "foo"}) expect(t, err, nil) err = app.Run([]string{"command", "bar"}) expect(t, err, nil) expect(t, s, "foobar") } var commandAppTests = []struct { name string expected bool }{ {"foobar", true}, {"batbaz", true}, {"b", true}, {"f", true}, {"bat", false}, {"nothing", false}, } func TestApp_Command(t *testing.T) { app := NewApp() fooCommand := Command{Name: "foobar", Aliases: []string{"f"}} batCommand := Command{Name: "batbaz", Aliases: []string{"b"}} app.Commands = []Command{ fooCommand, batCommand, } for _, test := range commandAppTests { expect(t, app.Command(test.name) != nil, test.expected) } } func TestApp_Setup_defaultsWriter(t *testing.T) { app := &App{} app.Setup() expect(t, app.Writer, os.Stdout) } func TestApp_CommandWithArgBeforeFlags(t *testing.T) { var parsedOption, firstArg string app := NewApp() command := Command{ Name: "cmd", Flags: []Flag{ StringFlag{Name: "option", Value: "", Usage: "some option"}, }, Action: func(c *Context) error { parsedOption = c.String("option") firstArg = c.Args().First() return nil }, } app.Commands = []Command{command} app.Run([]string{"", "cmd", "my-arg", "--option", "my-option"}) expect(t, parsedOption, "my-option") expect(t, firstArg, "my-arg") } func TestApp_CommandWithArgBeforeBoolFlags(t *testing.T) { var parsedOption, parsedSecondOption, firstArg string var parsedBool, parsedSecondBool bool app := NewApp() command := Command{ Name: "cmd", Flags: []Flag{ StringFlag{Name: "option", Value: "", Usage: "some option"}, StringFlag{Name: "secondOption", Value: "", Usage: "another option"}, BoolFlag{Name: "boolflag", Usage: "some bool"}, BoolFlag{Name: "b", Usage: "another bool"}, }, Action: func(c *Context) error { parsedOption = c.String("option") parsedSecondOption = c.String("secondOption") parsedBool = c.Bool("boolflag") parsedSecondBool = c.Bool("b") firstArg = c.Args().First() return nil }, } app.Commands = []Command{command} app.Run([]string{"", "cmd", "my-arg", "--boolflag", "--option", "my-option", "-b", "--secondOption", "fancy-option"}) expect(t, parsedOption, "my-option") expect(t, parsedSecondOption, "fancy-option") expect(t, parsedBool, true) expect(t, parsedSecondBool, true) expect(t, firstArg, "my-arg") } func TestApp_RunAsSubcommandParseFlags(t *testing.T) { var context *Context a := NewApp() a.Commands = []Command{ { Name: "foo", Action: func(c *Context) error { context = c return nil }, Flags: []Flag{ StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", }, }, Before: func(_ *Context) error { return nil }, }, } a.Run([]string{"", "foo", "--lang", "spanish", "abcd"}) expect(t, context.Args().Get(0), "abcd") expect(t, context.String("lang"), "spanish") } func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) { a := App{ Flags: []Flag{ StringFlag{Name: "--foo"}, }, Writer: bytes.NewBufferString(""), } set := flag.NewFlagSet("", flag.ContinueOnError) set.Parse([]string{"", "---foo"}) c := &Context{flagSet: set} err := a.RunAsSubcommand(c) expect(t, err, errors.New("bad flag syntax: ---foo")) } func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { var parsedOption string var args []string app := NewApp() command := Command{ Name: "cmd", Flags: []Flag{ StringFlag{Name: "option", Value: "", Usage: "some option"}, }, Action: func(c *Context) error { parsedOption = c.String("option") args = c.Args() return nil }, } app.Commands = []Command{command} app.Run([]string{"", "cmd", "my-arg", "--option", "my-option", "--", "--notARealFlag"}) expect(t, parsedOption, "my-option") expect(t, args[0], "my-arg") expect(t, args[1], "--") expect(t, args[2], "--notARealFlag") } func TestApp_CommandWithDash(t *testing.T) { var args []string app := NewApp() command := Command{ Name: "cmd", Action: func(c *Context) error { args = c.Args() return nil }, } app.Commands = []Command{command} app.Run([]string{"", "cmd", "my-arg", "-"}) expect(t, args[0], "my-arg") expect(t, args[1], "-") } func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { var args []string app := NewApp() command := Command{ Name: "cmd", Action: func(c *Context) error { args = c.Args() return nil }, } app.Commands = []Command{command} app.Run([]string{"", "cmd", "my-arg", "--", "notAFlagAtAll"}) expect(t, args[0], "my-arg") expect(t, args[1], "--") expect(t, args[2], "notAFlagAtAll") } func TestApp_VisibleCommands(t *testing.T) { app := NewApp() app.Commands = []Command{ { Name: "frob", HelpName: "foo frob", Action: func(_ *Context) error { return nil }, }, { Name: "frib", HelpName: "foo frib", Hidden: true, Action: func(_ *Context) error { return nil }, }, } app.Setup() expected := []Command{ app.Commands[0], app.Commands[2], // help } actual := app.VisibleCommands() expect(t, len(expected), len(actual)) for i, actualCommand := range actual { expectedCommand := expected[i] if expectedCommand.Action != nil { // comparing func addresses is OK! expect(t, fmt.Sprintf("%p", expectedCommand.Action), fmt.Sprintf("%p", actualCommand.Action)) } // nil out funcs, as they cannot be compared // (https://github.com/golang/go/issues/8554) expectedCommand.Action = nil actualCommand.Action = nil if !reflect.DeepEqual(expectedCommand, actualCommand) { t.Errorf("expected\n%#v\n!=\n%#v", expectedCommand, actualCommand) } } } func TestApp_Float64Flag(t *testing.T) { var meters float64 app := NewApp() app.Flags = []Flag{ Float64Flag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"}, } app.Action = func(c *Context) error { meters = c.Float64("height") return nil } app.Run([]string{"", "--height", "1.93"}) expect(t, meters, 1.93) } func TestApp_ParseSliceFlags(t *testing.T) { var parsedIntSlice []int var parsedStringSlice []string app := NewApp() command := Command{ Name: "cmd", Flags: []Flag{ IntSliceFlag{Name: "p", Value: &IntSlice{}, Usage: "set one or more ip addr"}, StringSliceFlag{Name: "ip", Value: &StringSlice{}, Usage: "set one or more ports to open"}, }, Action: func(c *Context) error { parsedIntSlice = c.IntSlice("p") parsedStringSlice = c.StringSlice("ip") return nil }, } app.Commands = []Command{command} app.Run([]string{"", "cmd", "my-arg", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4"}) IntsEquals := func(a, b []int) bool { if len(a) != len(b) { return false } for i, v := range a { if v != b[i] { return false } } return true } StrsEquals := func(a, b []string) bool { if len(a) != len(b) { return false } for i, v := range a { if v != b[i] { return false } } return true } var expectedIntSlice = []int{22, 80} var expectedStringSlice = []string{"8.8.8.8", "8.8.4.4"} if !IntsEquals(parsedIntSlice, expectedIntSlice) { t.Errorf("%v does not match %v", parsedIntSlice, expectedIntSlice) } if !StrsEquals(parsedStringSlice, expectedStringSlice) { t.Errorf("%v does not match %v", parsedStringSlice, expectedStringSlice) } } func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) { var parsedIntSlice []int var parsedStringSlice []string app := NewApp() command := Command{ Name: "cmd", Flags: []Flag{ IntSliceFlag{Name: "a", Usage: "set numbers"}, StringSliceFlag{Name: "str", Usage: "set strings"}, }, Action: func(c *Context) error { parsedIntSlice = c.IntSlice("a") parsedStringSlice = c.StringSlice("str") return nil }, } app.Commands = []Command{command} app.Run([]string{"", "cmd", "my-arg", "-a", "2", "-str", "A"}) var expectedIntSlice = []int{2} var expectedStringSlice = []string{"A"} if parsedIntSlice[0] != expectedIntSlice[0] { t.Errorf("%v does not match %v", parsedIntSlice[0], expectedIntSlice[0]) } if parsedStringSlice[0] != expectedStringSlice[0] { t.Errorf("%v does not match %v", parsedIntSlice[0], expectedIntSlice[0]) } } func TestApp_DefaultStdout(t *testing.T) { app := NewApp() if app.Writer != os.Stdout { t.Error("Default output writer not set.") } } type mockWriter struct { written []byte } func (fw *mockWriter) Write(p []byte) (n int, err error) { if fw.written == nil { fw.written = p } else { fw.written = append(fw.written, p...) } return len(p), nil } func (fw *mockWriter) GetWritten() (b []byte) { return fw.written } func TestApp_SetStdout(t *testing.T) { w := &mockWriter{} app := NewApp() app.Name = "test" app.Writer = w err := app.Run([]string{"help"}) if err != nil { t.Fatalf("Run error: %s", err) } if len(w.written) == 0 { t.Error("App did not write output to desired writer.") } } func TestApp_BeforeFunc(t *testing.T) { counts := &opCounts{} beforeError := fmt.Errorf("fail") var err error app := NewApp() app.Before = func(c *Context) error { counts.Total++ counts.Before = counts.Total s := c.String("opt") if s == "fail" { return beforeError } return nil } app.Commands = []Command{ { Name: "sub", Action: func(c *Context) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, } app.Flags = []Flag{ StringFlag{Name: "opt"}, } // run with the Before() func succeeding err = app.Run([]string{"command", "--opt", "succeed", "sub"}) if err != nil { t.Fatalf("Run error: %s", err) } if counts.Before != 1 { t.Errorf("Before() not executed when expected") } if counts.SubCommand != 2 { t.Errorf("Subcommand not executed when expected") } // reset counts = &opCounts{} // run with the Before() func failing err = app.Run([]string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func if err != beforeError { t.Errorf("Run error expected, but not received") } if counts.Before != 1 { t.Errorf("Before() not executed when expected") } if counts.SubCommand != 0 { t.Errorf("Subcommand executed when NOT expected") } // reset counts = &opCounts{} afterError := errors.New("fail again") app.After = func(_ *Context) error { return afterError } // run with the Before() func failing, wrapped by After() err = app.Run([]string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func if _, ok := err.(MultiError); !ok { t.Errorf("MultiError expected, but not received") } if counts.Before != 1 { t.Errorf("Before() not executed when expected") } if counts.SubCommand != 0 { t.Errorf("Subcommand executed when NOT expected") } } func TestApp_AfterFunc(t *testing.T) { counts := &opCounts{} afterError := fmt.Errorf("fail") var err error app := NewApp() app.After = func(c *Context) error { counts.Total++ counts.After = counts.Total s := c.String("opt") if s == "fail" { return afterError } return nil } app.Commands = []Command{ { Name: "sub", Action: func(c *Context) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, } app.Flags = []Flag{ StringFlag{Name: "opt"}, } // run with the After() func succeeding err = app.Run([]string{"command", "--opt", "succeed", "sub"}) if err != nil { t.Fatalf("Run error: %s", err) } if counts.After != 2 { t.Errorf("After() not executed when expected") } if counts.SubCommand != 1 { t.Errorf("Subcommand not executed when expected") } // reset counts = &opCounts{} // run with the Before() func failing err = app.Run([]string{"command", "--opt", "fail", "sub"}) // should be the same error produced by the Before func if err != afterError { t.Errorf("Run error expected, but not received") } if counts.After != 2 { t.Errorf("After() not executed when expected") } if counts.SubCommand != 1 { t.Errorf("Subcommand not executed when expected") } } func TestAppNoHelpFlag(t *testing.T) { oldFlag := HelpFlag defer func() { HelpFlag = oldFlag }() HelpFlag = BoolFlag{} app := NewApp() app.Writer = ioutil.Discard err := app.Run([]string{"test", "-h"}) if err != flag.ErrHelp { t.Errorf("expected error about missing help flag, but got: %s (%T)", err, err) } } func TestRequiredFlagAppRunBehavior(t *testing.T) { tdata := []struct { testCase string appFlags []Flag appRunInput []string appCommands []Command expectedAnError bool }{ // assertion: empty input, when a required flag is present, errors { testCase: "error_case_empty_input_with_required_flag_on_app", appRunInput: []string{"myCLI"}, appFlags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, expectedAnError: true, }, { testCase: "error_case_empty_input_with_required_flag_on_command", appRunInput: []string{"myCLI", "myCommand"}, appCommands: []Command{Command{ Name: "myCommand", Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, }}, expectedAnError: true, }, { testCase: "error_case_empty_input_with_required_flag_on_subcommand", appRunInput: []string{"myCLI", "myCommand", "mySubCommand"}, appCommands: []Command{Command{ Name: "myCommand", Subcommands: []Command{Command{ Name: "mySubCommand", Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, }}, }}, expectedAnError: true, }, // assertion: inputing --help, when a required flag is present, does not error { testCase: "valid_case_help_input_with_required_flag_on_app", appRunInput: []string{"myCLI", "--help"}, appFlags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, }, { testCase: "valid_case_help_input_with_required_flag_on_command", appRunInput: []string{"myCLI", "myCommand", "--help"}, appCommands: []Command{Command{ Name: "myCommand", Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, }}, }, { testCase: "valid_case_help_input_with_required_flag_on_subcommand", appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--help"}, appCommands: []Command{Command{ Name: "myCommand", Subcommands: []Command{Command{ Name: "mySubCommand", Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, }}, }}, }, // assertion: giving optional input, when a required flag is present, errors { testCase: "error_case_optional_input_with_required_flag_on_app", appRunInput: []string{"myCLI", "--optional", "cats"}, appFlags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}}, expectedAnError: true, }, { testCase: "error_case_optional_input_with_required_flag_on_command", appRunInput: []string{"myCLI", "myCommand", "--optional", "cats"}, appCommands: []Command{Command{ Name: "myCommand", Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}}, }}, expectedAnError: true, }, { testCase: "error_case_optional_input_with_required_flag_on_subcommand", appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--optional", "cats"}, appCommands: []Command{Command{ Name: "myCommand", Subcommands: []Command{Command{ Name: "mySubCommand", Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}, StringFlag{Name: "optional"}}, }}, }}, expectedAnError: true, }, // assertion: when a required flag is present, inputting that required flag does not error { testCase: "valid_case_required_flag_input_on_app", appRunInput: []string{"myCLI", "--requiredFlag", "cats"}, appFlags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, }, { testCase: "valid_case_required_flag_input_on_command", appRunInput: []string{"myCLI", "myCommand", "--requiredFlag", "cats"}, appCommands: []Command{Command{ Name: "myCommand", Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, }}, }, { testCase: "valid_case_required_flag_input_on_subcommand", appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--requiredFlag", "cats"}, appCommands: []Command{Command{ Name: "myCommand", Subcommands: []Command{Command{ Name: "mySubCommand", Flags: []Flag{StringFlag{Name: "requiredFlag", Required: true}}, }}, }}, }, } for _, test := range tdata { t.Run(test.testCase, func(t *testing.T) { // setup - undo HelpPrinter mock when finished oldPrinter := HelpPrinter defer func() { HelpPrinter = oldPrinter }() // setup - mock HelpPrinter var helpPrinterWasCalled = false HelpPrinter = func(w io.Writer, template string, data interface{}) { helpPrinterWasCalled = true } // setup - app app := NewApp() app.Flags = test.appFlags app.Commands = test.appCommands // logic under test err := app.Run(test.appRunInput) // assertions if helpPrinterWasCalled == false { t.Errorf("HelpPrinter expected to be called, but was not") } if test.expectedAnError && err == nil { t.Errorf("expected an error, but there was none") } if _, ok := err.(requiredFlagsErr); test.expectedAnError && !ok { t.Errorf("expected a requiredFlagsErr, but got: %s", err) } if !test.expectedAnError && err != nil { t.Errorf("did not expected an error, but there was one: %s", err) } }) } } func TestAppHelpPrinter(t *testing.T) { oldPrinter := HelpPrinter defer func() { HelpPrinter = oldPrinter }() var wasCalled = false HelpPrinter = func(w io.Writer, template string, data interface{}) { wasCalled = true } app := NewApp() app.Run([]string{"-h"}) if wasCalled == false { t.Errorf("Help printer expected to be called, but was not") } } func TestApp_VersionPrinter(t *testing.T) { oldPrinter := VersionPrinter defer func() { VersionPrinter = oldPrinter }() var wasCalled = false VersionPrinter = func(c *Context) { wasCalled = true } app := NewApp() ctx := NewContext(app, nil, nil) ShowVersion(ctx) if wasCalled == false { t.Errorf("Version printer expected to be called, but was not") } } func TestApp_CommandNotFound(t *testing.T) { counts := &opCounts{} app := NewApp() app.CommandNotFound = func(c *Context, command string) { counts.Total++ counts.CommandNotFound = counts.Total } app.Commands = []Command{ { Name: "bar", Action: func(c *Context) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, } app.Run([]string{"command", "foo"}) expect(t, counts.CommandNotFound, 1) expect(t, counts.SubCommand, 0) expect(t, counts.Total, 1) } func TestApp_OrderOfOperations(t *testing.T) { counts := &opCounts{} resetCounts := func() { counts = &opCounts{} } app := NewApp() app.EnableBashCompletion = true app.BashComplete = func(c *Context) { counts.Total++ counts.BashComplete = counts.Total } app.OnUsageError = func(c *Context, err error, isSubcommand bool) error { counts.Total++ counts.OnUsageError = counts.Total return errors.New("hay OnUsageError") } beforeNoError := func(c *Context) error { counts.Total++ counts.Before = counts.Total return nil } beforeError := func(c *Context) error { counts.Total++ counts.Before = counts.Total return errors.New("hay Before") } app.Before = beforeNoError app.CommandNotFound = func(c *Context, command string) { counts.Total++ counts.CommandNotFound = counts.Total } afterNoError := func(c *Context) error { counts.Total++ counts.After = counts.Total return nil } afterError := func(c *Context) error { counts.Total++ counts.After = counts.Total return errors.New("hay After") } app.After = afterNoError app.Commands = []Command{ { Name: "bar", Action: func(c *Context) error { counts.Total++ counts.SubCommand = counts.Total return nil }, }, } app.Action = func(c *Context) error { counts.Total++ counts.Action = counts.Total return nil } _ = app.Run([]string{"command", "--nope"}) expect(t, counts.OnUsageError, 1) expect(t, counts.Total, 1) resetCounts() _ = app.Run([]string{"command", "--generate-bash-completion"}) expect(t, counts.BashComplete, 1) expect(t, counts.Total, 1) resetCounts() oldOnUsageError := app.OnUsageError app.OnUsageError = nil _ = app.Run([]string{"command", "--nope"}) expect(t, counts.Total, 0) app.OnUsageError = oldOnUsageError resetCounts() _ = app.Run([]string{"command", "foo"}) expect(t, counts.OnUsageError, 0) expect(t, counts.Before, 1) expect(t, counts.CommandNotFound, 0) expect(t, counts.Action, 2) expect(t, counts.After, 3) expect(t, counts.Total, 3) resetCounts() app.Before = beforeError _ = app.Run([]string{"command", "bar"}) expect(t, counts.OnUsageError, 0) expect(t, counts.Before, 1) expect(t, counts.After, 2) expect(t, counts.Total, 2) app.Before = beforeNoError resetCounts() app.After = nil _ = app.Run([]string{"command", "bar"}) expect(t, counts.OnUsageError, 0) expect(t, counts.Before, 1) expect(t, counts.SubCommand, 2) expect(t, counts.Total, 2) app.After = afterNoError resetCounts() app.After = afterError err := app.Run([]string{"command", "bar"}) if err == nil { t.Fatalf("expected a non-nil error") } expect(t, counts.OnUsageError, 0) expect(t, counts.Before, 1) expect(t, counts.SubCommand, 2) expect(t, counts.After, 3) expect(t, counts.Total, 3) app.After = afterNoError resetCounts() oldCommands := app.Commands app.Commands = nil _ = app.Run([]string{"command"}) expect(t, counts.OnUsageError, 0) expect(t, counts.Before, 1) expect(t, counts.Action, 2) expect(t, counts.After, 3) expect(t, counts.Total, 3) app.Commands = oldCommands } func TestApp_Run_CommandWithSubcommandHasHelpTopic(t *testing.T) { var subcommandHelpTopics = [][]string{ {"command", "foo", "--help"}, {"command", "foo", "-h"}, {"command", "foo", "help"}, } for _, flagSet := range subcommandHelpTopics { t.Logf("==> checking with flags %v", flagSet) app := NewApp() buf := new(bytes.Buffer) app.Writer = buf subCmdBar := Command{ Name: "bar", Usage: "does bar things", } subCmdBaz := Command{ Name: "baz", Usage: "does baz things", } cmd := Command{ Name: "foo", Description: "descriptive wall of text about how it does foo things", Subcommands: []Command{subCmdBar, subCmdBaz}, Action: func(c *Context) error { return nil }, } app.Commands = []Command{cmd} err := app.Run(flagSet) if err != nil { t.Error(err) } output := buf.String() t.Logf("output: %q\n", buf.Bytes()) if strings.Contains(output, "No help topic for") { t.Errorf("expect a help topic, got none: \n%q", output) } for _, shouldContain := range []string{ cmd.Name, cmd.Description, subCmdBar.Name, subCmdBar.Usage, subCmdBaz.Name, subCmdBaz.Usage, } { if !strings.Contains(output, shouldContain) { t.Errorf("want help to contain %q, did not: \n%q", shouldContain, output) } } } } func TestApp_Run_SubcommandFullPath(t *testing.T) { app := NewApp() buf := new(bytes.Buffer) app.Writer = buf app.Name = "command" subCmd := Command{ Name: "bar", Usage: "does bar things", } cmd := Command{ Name: "foo", Description: "foo commands", Subcommands: []Command{subCmd}, } app.Commands = []Command{cmd} err := app.Run([]string{"command", "foo", "bar", "--help"}) if err != nil { t.Error(err) } output := buf.String() if !strings.Contains(output, "command foo bar - does bar things") { t.Errorf("expected full path to subcommand: %s", output) } if !strings.Contains(output, "command foo bar [arguments...]") { t.Errorf("expected full path to subcommand: %s", output) } } func TestApp_Run_SubcommandHelpName(t *testing.T) { app := NewApp() buf := new(bytes.Buffer) app.Writer = buf app.Name = "command" subCmd := Command{ Name: "bar", HelpName: "custom", Usage: "does bar things", } cmd := Command{ Name: "foo", Description: "foo commands", Subcommands: []Command{subCmd}, } app.Commands = []Command{cmd} err := app.Run([]string{"command", "foo", "bar", "--help"}) if err != nil { t.Error(err) } output := buf.String() if !strings.Contains(output, "custom - does bar things") { t.Errorf("expected HelpName for subcommand: %s", output) } if !strings.Contains(output, "custom [arguments...]") { t.Errorf("expected HelpName to subcommand: %s", output) } } func TestApp_Run_CommandHelpName(t *testing.T) { app := NewApp() buf := new(bytes.Buffer) app.Writer = buf app.Name = "command" subCmd := Command{ Name: "bar", Usage: "does bar things", } cmd := Command{ Name: "foo", HelpName: "custom", Description: "foo commands", Subcommands: []Command{subCmd}, } app.Commands = []Command{cmd} err := app.Run([]string{"command", "foo", "bar", "--help"}) if err != nil { t.Error(err) } output := buf.String() if !strings.Contains(output, "command foo bar - does bar things") { t.Errorf("expected full path to subcommand: %s", output) } if !strings.Contains(output, "command foo bar [arguments...]") { t.Errorf("expected full path to subcommand: %s", output) } } func TestApp_Run_CommandSubcommandHelpName(t *testing.T) { app := NewApp() buf := new(bytes.Buffer) app.Writer = buf app.Name = "base" subCmd := Command{ Name: "bar", HelpName: "custom", Usage: "does bar things", } cmd := Command{ Name: "foo", Description: "foo commands", Subcommands: []Command{subCmd}, } app.Commands = []Command{cmd} err := app.Run([]string{"command", "foo", "--help"}) if err != nil { t.Error(err) } output := buf.String() if !strings.Contains(output, "base foo - foo commands") { t.Errorf("expected full path to subcommand: %s", output) } if !strings.Contains(output, "base foo command [command options] [arguments...]") { t.Errorf("expected full path to subcommand: %s", output) } } func TestApp_Run_Help(t *testing.T) { var helpArguments = [][]string{{"boom", "--help"}, {"boom", "-h"}, {"boom", "help"}} for _, args := range helpArguments { buf := new(bytes.Buffer) t.Logf("==> checking with arguments %v", args) app := NewApp() app.Name = "boom" app.Usage = "make an explosive entrance" app.Writer = buf app.Action = func(c *Context) error { buf.WriteString("boom I say!") return nil } err := app.Run(args) if err != nil { t.Error(err) } output := buf.String() t.Logf("output: %q\n", buf.Bytes()) if !strings.Contains(output, "boom - make an explosive entrance") { t.Errorf("want help to contain %q, did not: \n%q", "boom - make an explosive entrance", output) } } } func TestApp_Run_Version(t *testing.T) { var versionArguments = [][]string{{"boom", "--version"}, {"boom", "-v"}} for _, args := range versionArguments { buf := new(bytes.Buffer) t.Logf("==> checking with arguments %v", args) app := NewApp() app.Name = "boom" app.Usage = "make an explosive entrance" app.Version = "0.1.0" app.Writer = buf app.Action = func(c *Context) error { buf.WriteString("boom I say!") return nil } err := app.Run(args) if err != nil { t.Error(err) } output := buf.String() t.Logf("output: %q\n", buf.Bytes()) if !strings.Contains(output, "0.1.0") { t.Errorf("want version to contain %q, did not: \n%q", "0.1.0", output) } } } func TestApp_Run_Categories(t *testing.T) { app := NewApp() app.Name = "categories" app.HideHelp = true app.Commands = []Command{ { Name: "command1", Category: "1", }, { Name: "command2", Category: "1", }, { Name: "command3", Category: "2", }, } buf := new(bytes.Buffer) app.Writer = buf app.Run([]string{"categories"}) expect := CommandCategories{ &CommandCategory{ Name: "1", Commands: []Command{ app.Commands[0], app.Commands[1], }, }, &CommandCategory{ Name: "2", Commands: []Command{ app.Commands[2], }, }, } if !reflect.DeepEqual(app.Categories(), expect) { t.Fatalf("expected categories %#v, to equal %#v", app.Categories(), expect) } output := buf.String() t.Logf("output: %q\n", buf.Bytes()) if !strings.Contains(output, "1:\n command1") { t.Errorf("want buffer to include category %q, did not: \n%q", "1:\n command1", output) } } func TestApp_VisibleCategories(t *testing.T) { app := NewApp() app.Name = "visible-categories" app.HideHelp = true app.Commands = []Command{ { Name: "command1", Category: "1", HelpName: "foo command1", Hidden: true, }, { Name: "command2", Category: "2", HelpName: "foo command2", }, { Name: "command3", Category: "3", HelpName: "foo command3", }, } expected := []*CommandCategory{ { Name: "2", Commands: []Command{ app.Commands[1], }, }, { Name: "3", Commands: []Command{ app.Commands[2], }, }, } app.Setup() expect(t, expected, app.VisibleCategories()) app = NewApp() app.Name = "visible-categories" app.HideHelp = true app.Commands = []Command{ { Name: "command1", Category: "1", HelpName: "foo command1", Hidden: true, }, { Name: "command2", Category: "2", HelpName: "foo command2", Hidden: true, }, { Name: "command3", Category: "3", HelpName: "foo command3", }, } expected = []*CommandCategory{ { Name: "3", Commands: []Command{ app.Commands[2], }, }, } app.Setup() expect(t, expected, app.VisibleCategories()) app = NewApp() app.Name = "visible-categories" app.HideHelp = true app.Commands = []Command{ { Name: "command1", Category: "1", HelpName: "foo command1", Hidden: true, }, { Name: "command2", Category: "2", HelpName: "foo command2", Hidden: true, }, { Name: "command3", Category: "3", HelpName: "foo command3", Hidden: true, }, } expected = []*CommandCategory{} app.Setup() expect(t, expected, app.VisibleCategories()) } func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() app.Action = func(c *Context) error { return nil } app.Before = func(c *Context) error { return fmt.Errorf("before error") } app.After = func(c *Context) error { return fmt.Errorf("after error") } err := app.Run([]string{"foo"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } if !strings.Contains(err.Error(), "before error") { t.Errorf("expected text of error from Before method, but got none in \"%v\"", err) } if !strings.Contains(err.Error(), "after error") { t.Errorf("expected text of error from After method, but got none in \"%v\"", err) } } func TestApp_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() app.Commands = []Command{ { Subcommands: []Command{ { Name: "sub", }, }, Name: "bar", Before: func(c *Context) error { return fmt.Errorf("before error") }, After: func(c *Context) error { return fmt.Errorf("after error") }, }, } err := app.Run([]string{"foo", "bar"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } if !strings.Contains(err.Error(), "before error") { t.Errorf("expected text of error from Before method, but got none in \"%v\"", err) } if !strings.Contains(err.Error(), "after error") { t.Errorf("expected text of error from After method, but got none in \"%v\"", err) } } func TestApp_OnUsageError_WithWrongFlagValue(t *testing.T) { app := NewApp() app.Flags = []Flag{ IntFlag{Name: "flag"}, } app.OnUsageError = func(c *Context, err error, isSubcommand bool) error { if isSubcommand { t.Errorf("Expect no subcommand") } if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { t.Errorf("Expect an invalid value error, but got \"%v\"", err) } return errors.New("intercepted: " + err.Error()) } app.Commands = []Command{ { Name: "bar", }, } err := app.Run([]string{"foo", "--flag=wrong"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } if !strings.HasPrefix(err.Error(), "intercepted: invalid value") { t.Errorf("Expect an intercepted error, but got \"%v\"", err) } } func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) { app := NewApp() app.Flags = []Flag{ IntFlag{Name: "flag"}, } app.OnUsageError = func(c *Context, err error, isSubcommand bool) error { if isSubcommand { t.Errorf("Expect subcommand") } if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { t.Errorf("Expect an invalid value error, but got \"%v\"", err) } return errors.New("intercepted: " + err.Error()) } app.Commands = []Command{ { Name: "bar", }, } err := app.Run([]string{"foo", "--flag=wrong", "bar"}) if err == nil { t.Fatalf("expected to receive error from Run, got none") } if !strings.HasPrefix(err.Error(), "intercepted: invalid value") { t.Errorf("Expect an intercepted error, but got \"%v\"", err) } } // A custom flag that conforms to the relevant interfaces, but has none of the // fields that the other flag types do. type customBoolFlag struct { Nombre string } // Don't use the normal FlagStringer func (c *customBoolFlag) String() string { return "***" + c.Nombre + "***" } func (c *customBoolFlag) GetName() string { return c.Nombre } func (c *customBoolFlag) Apply(set *flag.FlagSet) { set.String(c.Nombre, c.Nombre, "") } func TestCustomFlagsUnused(t *testing.T) { app := NewApp() app.Flags = []Flag{&customBoolFlag{"custom"}} err := app.Run([]string{"foo"}) if err != nil { t.Errorf("Run returned unexpected error: %v", err) } } func TestCustomFlagsUsed(t *testing.T) { app := NewApp() app.Flags = []Flag{&customBoolFlag{"custom"}} err := app.Run([]string{"foo", "--custom=bar"}) if err != nil { t.Errorf("Run returned unexpected error: %v", err) } } func TestCustomHelpVersionFlags(t *testing.T) { app := NewApp() // Be sure to reset the global flags defer func(helpFlag Flag, versionFlag Flag) { HelpFlag = helpFlag VersionFlag = versionFlag }(HelpFlag, VersionFlag) HelpFlag = &customBoolFlag{"help-custom"} VersionFlag = &customBoolFlag{"version-custom"} err := app.Run([]string{"foo", "--help-custom=bar"}) if err != nil { t.Errorf("Run returned unexpected error: %v", err) } } func TestHandleAction_WithNonFuncAction(t *testing.T) { app := NewApp() app.Action = 42 fs, err := flagSet(app.Name, app.Flags) if err != nil { t.Errorf("error creating FlagSet: %s", err) } err = HandleAction(app.Action, NewContext(app, fs, nil)) if err == nil { t.Fatalf("expected to receive error from Run, got none") } exitErr, ok := err.(*ExitError) if !ok { t.Fatalf("expected to receive a *ExitError") } if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type.") { t.Fatalf("expected an unknown Action error, but got: %v", exitErr.Error()) } if exitErr.ExitCode() != 2 { t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) } } func TestHandleAction_WithInvalidFuncSignature(t *testing.T) { app := NewApp() app.Action = func() string { return "" } fs, err := flagSet(app.Name, app.Flags) if err != nil { t.Errorf("error creating FlagSet: %s", err) } err = HandleAction(app.Action, NewContext(app, fs, nil)) if err == nil { t.Fatalf("expected to receive error from Run, got none") } exitErr, ok := err.(*ExitError) if !ok { t.Fatalf("expected to receive a *ExitError") } if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type") { t.Fatalf("expected an unknown Action error, but got: %v", exitErr.Error()) } if exitErr.ExitCode() != 2 { t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) } } func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) { app := NewApp() app.Action = func(_ *Context) (int, error) { return 0, nil } fs, err := flagSet(app.Name, app.Flags) if err != nil { t.Errorf("error creating FlagSet: %s", err) } err = HandleAction(app.Action, NewContext(app, fs, nil)) if err == nil { t.Fatalf("expected to receive error from Run, got none") } exitErr, ok := err.(*ExitError) if !ok { t.Fatalf("expected to receive a *ExitError") } if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type") { t.Fatalf("expected an invalid Action signature error, but got: %v", exitErr.Error()) } if exitErr.ExitCode() != 2 { t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) } } func TestHandleExitCoder_Default(t *testing.T) { app := NewApp() fs, err := flagSet(app.Name, app.Flags) if err != nil { t.Errorf("error creating FlagSet: %s", err) } ctx := NewContext(app, fs, nil) app.handleExitCoder(ctx, NewExitError("Default Behavior Error", 42)) output := fakeErrWriter.String() if !strings.Contains(output, "Default") { t.Fatalf("Expected Default Behavior from Error Handler but got: %s", output) } } func TestHandleExitCoder_Custom(t *testing.T) { app := NewApp() fs, err := flagSet(app.Name, app.Flags) if err != nil { t.Errorf("error creating FlagSet: %s", err) } app.ExitErrHandler = func(_ *Context, _ error) { fmt.Fprintln(ErrWriter, "I'm a Custom error handler, I print what I want!") } ctx := NewContext(app, fs, nil) app.handleExitCoder(ctx, NewExitError("Default Behavior Error", 42)) output := fakeErrWriter.String() if !strings.Contains(output, "Custom") { t.Fatalf("Expected Custom Behavior from Error Handler but got: %s", output) } } func TestHandleAction_WithUnknownPanic(t *testing.T) { defer func() { refute(t, recover(), nil) }() var fn ActionFunc app := NewApp() app.Action = func(ctx *Context) error { fn(ctx) return nil } fs, err := flagSet(app.Name, app.Flags) if err != nil { t.Errorf("error creating FlagSet: %s", err) } HandleAction(app.Action, NewContext(app, fs, nil)) } func TestShellCompletionForIncompleteFlags(t *testing.T) { app := NewApp() app.Flags = []Flag{ IntFlag{ Name: "test-completion", }, } app.EnableBashCompletion = true app.BashComplete = func(ctx *Context) { for _, command := range ctx.App.Commands { if command.Hidden { continue } for _, name := range command.Names() { fmt.Fprintln(ctx.App.Writer, name) } } for _, flag := range ctx.App.Flags { for _, name := range strings.Split(flag.GetName(), ",") { if name == BashCompletionFlag.GetName() { continue } switch name = strings.TrimSpace(name); len(name) { case 0: case 1: fmt.Fprintln(ctx.App.Writer, "-"+name) default: fmt.Fprintln(ctx.App.Writer, "--"+name) } } } } app.Action = func(ctx *Context) error { return fmt.Errorf("should not get here") } err := app.Run([]string{"", "--test-completion", "--" + BashCompletionFlag.GetName()}) if err != nil { t.Errorf("app should not return an error: %s", err) } } func TestHandleActionActuallyWorksWithActions(t *testing.T) { var f ActionFunc called := false f = func(c *Context) error { called = true return nil } err := HandleAction(f, nil) if err != nil { t.Errorf("Should not have errored: %v", err) } if !called { t.Errorf("Function was not called") } }