From 50bfd2f6e733a02e894e1e46212136d70b1dbe66 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 25 Jun 2023 19:40:11 -0400 Subject: [PATCH 1/8] First pass collapsing `Context` into `Command` --- command.go | 359 +++++++++--- command_test.go | 980 ++++++++++++++++++++++++++------- completion.go | 11 +- context.go | 257 --------- context_test.go | 642 --------------------- examples_test.go | 56 +- flag.go | 3 +- flag_bool.go | 4 +- flag_bool_with_inverse.go | 7 +- flag_bool_with_inverse_test.go | 9 +- flag_duration.go | 4 +- flag_float.go | 4 +- flag_float_slice.go | 4 +- flag_impl.go | 11 +- flag_int.go | 4 +- flag_int_slice.go | 4 +- flag_mutex.go | 4 +- flag_string.go | 4 +- flag_string_map.go | 4 +- flag_string_slice.go | 4 +- flag_test.go | 442 ++++++++------- flag_timestamp.go | 4 +- flag_uint.go | 4 +- flag_uint_slice.go | 4 +- funcs.go | 18 +- help.go | 127 ++--- help_test.go | 153 +++-- internal/build/build.go | 125 +++-- 28 files changed, 1551 insertions(+), 1701 deletions(-) delete mode 100644 context.go delete mode 100644 context_test.go diff --git a/command.go b/command.go index e2a0e212c5..bf61078cd1 100644 --- a/command.go +++ b/command.go @@ -12,8 +12,14 @@ import ( "strings" ) -// ignoreFlagPrefix is to ignore test flags when adding flags from other packages -const ignoreFlagPrefix = "test." +const ( + // ignoreFlagPrefix is to ignore test flags when adding flags from other packages + ignoreFlagPrefix = "test." + + commandContextKey = contextKey("cli.context") +) + +type contextKey string // Command contains everything needed to run an application that // accepts a string slice of arguments such as os.Args. A given @@ -127,6 +133,8 @@ type Command struct { // The parent of this command. This value will be nil for the // command at the root of the graph. parent *Command + // the flag.FlagSet for this command + flagSet *flag.FlagSet // track state of error handling isInError bool // track state of defaults @@ -275,21 +283,17 @@ func (cmd *Command) setupDefaults(arguments []string) { disableSliceFlagSeparator = cmd.DisableSliceFlagSeparator } -func (cmd *Command) setupCommandGraph(cCtx *Context) { +func (cmd *Command) setupCommandGraph() { for _, subCmd := range cmd.Commands { subCmd.parent = cmd - subCmd.setupSubcommand(cCtx) - subCmd.setupCommandGraph(cCtx) + subCmd.setupSubcommand() + subCmd.setupCommandGraph() } } -func (cmd *Command) setupSubcommand(cCtx *Context) { +func (cmd *Command) setupSubcommand() { cmd.ensureHelp() - if cCtx.Command.UseShortOptionHandling { - cmd.UseShortOptionHandling = true - } - tracef("setting command categories") cmd.categories = newCommandCategories() @@ -326,9 +330,8 @@ func (cmd *Command) ensureHelp() { func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error) { cmd.setupDefaults(arguments) - parentContext := &Context{Context: ctx} - if v, ok := ctx.Value(contextContextKey).(*Context); ok { - parentContext = v + if v, ok := ctx.Value(commandContextKey).(*Command); ok { + cmd.parent = v } // handle the completion flag separately from the flagset since @@ -337,24 +340,20 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error // flag name as the value of the flag before it which is undesirable // note that we can only do this because the shell autocomplete function // always appends the completion flag at the end of the command - shellComplete, arguments := checkShellCompleteFlag(cmd, arguments) - - cCtx := NewContext(cmd, nil, parentContext) - cCtx.shellComplete = shellComplete - - cCtx.Command = cmd + enableShellCompletion, arguments := checkShellCompleteFlag(cmd, arguments) + cmd.EnableShellCompletion = enableShellCompletion - ctx = context.WithValue(ctx, contextContextKey, cCtx) + ctx = context.WithValue(ctx, commandContextKey, cmd) if cmd.parent == nil { - cmd.setupCommandGraph(cCtx) + cmd.setupCommandGraph() } - a := args(arguments) - set, err := cmd.parseFlags(&a, cCtx) - cCtx.flagSet = set + argsArguments := args(arguments) + set, err := cmd.parseFlags(&argsArguments) + cmd.flagSet = set - if checkCompletions(cCtx) { + if checkCompletions(ctx, cmd) { return nil } @@ -362,27 +361,27 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error tracef("setting deferErr from %[1]v", err) deferErr = err - cCtx.Command.isInError = true + cmd.isInError = true if cmd.OnUsageError != nil { - err = cmd.OnUsageError(cCtx, err, cmd.parent != nil) - err = cCtx.Command.handleExitCoder(cCtx, err) + err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) + err = cmd.handleExitCoder(ctx, err) return err } - _, _ = fmt.Fprintf(cCtx.Command.Root().ErrWriter, "%s %s\n\n", "Incorrect Usage:", err.Error()) - if cCtx.Command.Suggest { + fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error()) + if cmd.Suggest { if suggestion, err := cmd.suggestFlagFromError(err, ""); err == nil { - fmt.Fprintf(cCtx.Command.Root().ErrWriter, "%s", suggestion) + fmt.Fprintf(cmd.Root().ErrWriter, "%s", suggestion) } } if !cmd.HideHelp { if cmd.parent == nil { tracef("running ShowAppHelp") - if err := ShowAppHelp(cCtx); err != nil { + if err := ShowAppHelp(cmd); err != nil { tracef("SILENTLY IGNORING ERROR running ShowAppHelp %[1]v", err) } } else { tracef("running ShowCommandHelp with %[1]q", cmd.Name) - if err := ShowCommandHelp(cCtx.parent, cmd.Name); err != nil { + if err := ShowCommandHelp(ctx, cmd, cmd.Name); err != nil { tracef("SILENTLY IGNORING ERROR running ShowCommandHelp with %[1]q %[2]v", cmd.Name, err) } } @@ -391,19 +390,19 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error return err } - if checkHelp(cCtx) { - return helpCommandAction(cCtx) + if checkHelp(cmd) { + return helpCommandAction(ctx, cmd) } - if cmd.parent == nil && !cCtx.Command.HideVersion && checkVersion(cCtx) { - ShowVersion(cCtx) + if cmd.parent == nil && !cmd.HideVersion && checkVersion(cmd) { + ShowVersion(cmd) return nil } - if cmd.After != nil && !cCtx.shellComplete { + if cmd.After != nil && !cmd.EnableShellCompletion { defer func() { - if err := cmd.After(cCtx); err != nil { - err = cCtx.Command.handleExitCoder(cCtx, err) + if err := cmd.After(ctx, cmd); err != nil { + err = cmd.handleExitCoder(ctx, err) if deferErr != nil { deferErr = newMultiError(deferErr, err) @@ -414,41 +413,40 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error }() } - if err := cCtx.checkRequiredFlags(cmd.Flags); err != nil { - cCtx.Command.isInError = true - _ = ShowSubcommandHelp(cCtx) + if err := cmd.checkRequiredFlags(); err != nil { + cmd.isInError = true + _ = ShowSubcommandHelp(cmd) return err } for _, grp := range cmd.MutuallyExclusiveFlags { - if err := grp.check(cCtx); err != nil { - _ = ShowSubcommandHelp(cCtx) + if err := grp.check(cmd); err != nil { + _ = ShowSubcommandHelp(cmd) return err } } - if cmd.Before != nil && !cCtx.shellComplete { - if err := cmd.Before(cCtx); err != nil { - deferErr = cCtx.Command.handleExitCoder(cCtx, err) + if cmd.Before != nil && !cmd.EnableShellCompletion { + if err := cmd.Before(ctx, cmd); err != nil { + deferErr = cmd.handleExitCoder(ctx, err) return deferErr } } - if err := runFlagActions(cCtx, cmd.appliedFlags); err != nil { + if err := runFlagActions(ctx, cmd, cmd.appliedFlags); err != nil { return err } var subCmd *Command - args := cCtx.Args() - if args.Present() { - name := args.First() - if cCtx.Command.SuggestCommandFunc != nil { - name = cCtx.Command.SuggestCommandFunc(cmd.Commands, name) + if argsArguments.Present() { + name := argsArguments.First() + if cmd.SuggestCommandFunc != nil { + name = cmd.SuggestCommandFunc(cmd.Commands, name) } subCmd = cmd.Command(name) if subCmd == nil { - hasDefault := cCtx.Command.DefaultCommand != "" - isFlagName := checkStringSliceIncludes(name, cCtx.FlagNames()) + hasDefault := cmd.DefaultCommand != "" + isFlagName := checkStringSliceIncludes(name, cmd.FlagNames()) var ( isDefaultSubcommand = false @@ -456,7 +454,7 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error ) if hasDefault { - dc := cCtx.Command.Command(cCtx.Command.DefaultCommand) + dc := cmd.Command(cmd.DefaultCommand) defaultHasSubcommands = len(dc.Commands) > 0 for _, dcSub := range dc.Commands { if checkStringSliceIncludes(name, dcSub.Names()) { @@ -467,33 +465,29 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error } if isFlagName || (hasDefault && (defaultHasSubcommands && isDefaultSubcommand)) { - argsWithDefault := cCtx.Command.argsWithDefaultCommand(args) - if !reflect.DeepEqual(args, argsWithDefault) { - subCmd = cCtx.Command.Command(argsWithDefault.First()) + argsWithDefault := cmd.argsWithDefaultCommand(&argsArguments) + if !reflect.DeepEqual(argsArguments, argsWithDefault) { + subCmd = cmd.Command(argsWithDefault.First()) } } } - } else if cmd.parent == nil && cCtx.Command.DefaultCommand != "" { - if dc := cCtx.Command.Command(cCtx.Command.DefaultCommand); dc != cmd { + } else if cmd.parent == nil && cmd.DefaultCommand != "" { + if dc := cmd.Command(cmd.DefaultCommand); dc != cmd { subCmd = dc } } if subCmd != nil { - /* - newcCtx := NewContext(cCtx.Command, nil, cCtx) - newcCtx.Command = cmd - */ - return subCmd.Run(ctx, cCtx.Args().Slice()) + return subCmd.Run(ctx, cmd.Args().Slice()) } if cmd.Action == nil { cmd.Action = helpCommandAction } - if err := cmd.Action(cCtx); err != nil { + if err := cmd.Action(ctx, cmd); err != nil { tracef("calling handleExitCoder with %[1]v", err) - deferErr = cCtx.Command.handleExitCoder(cCtx, err) + deferErr = cmd.handleExitCoder(ctx, err) } tracef("returning deferErr") @@ -546,22 +540,18 @@ func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil } -func (cmd *Command) parseFlags(args Args, ctx *Context) (*flag.FlagSet, error) { +func (cmd *Command) parseFlags(argsArguments Args) (*flag.FlagSet, error) { set, err := cmd.newFlagSet() if err != nil { return nil, err } if cmd.SkipFlagParsing { - return set, set.Parse(append([]string{"--"}, args.Tail()...)) + return set, set.Parse(append([]string{"--"}, argsArguments.Tail()...)) } - for pCtx := ctx.parent; pCtx != nil; pCtx = pCtx.parent { - if pCtx.Command == nil { - continue - } - - for _, fl := range pCtx.Command.Flags { + for pCmd := cmd.parent; pCmd != nil; pCmd = pCmd.parent { + for _, fl := range pCmd.Flags { pfl, ok := fl.(PersistentFlag) if !ok || !pfl.IsPersistent() { continue @@ -589,7 +579,7 @@ func (cmd *Command) parseFlags(args Args, ctx *Context) (*flag.FlagSet, error) { } } - if err := parseIter(set, cmd, args.Tail(), ctx.shellComplete); err != nil { + if err := parseIter(set, cmd, argsArguments.Tail(), cmd.EnableShellCompletion); err != nil { return nil, err } @@ -670,13 +660,13 @@ func (cmd *Command) appendCommand(aCmd *Command) { } } -func (cmd *Command) handleExitCoder(cCtx *Context, err error) error { +func (cmd *Command) handleExitCoder(ctx context.Context, err error) error { if cmd.parent != nil { - return cmd.parent.handleExitCoder(cCtx, err) + return cmd.parent.handleExitCoder(ctx, err) } if cmd.ExitErrHandler != nil { - cmd.ExitErrHandler(cCtx, err) + cmd.ExitErrHandler(ctx, cmd, err) return err } @@ -704,6 +694,189 @@ func (cmd *Command) Root() *Command { return cmd.parent.Root() } +func (cmd *Command) lookupFlag(name string) Flag { + for _, pCmd := range cmd.Lineage() { + for _, f := range pCmd.Flags { + for _, n := range f.Names() { + if n == name { + return f + } + } + } + } + + return nil +} + +func (cmd *Command) lookupFlagSet(name string) *flag.FlagSet { + for _, pCmd := range cmd.Lineage() { + if pCmd.flagSet == nil { + continue + } + if f := pCmd.flagSet.Lookup(name); f != nil { + return pCmd.flagSet + } + } + cmd.onInvalidFlag(context.TODO(), name) + return nil +} + +func (cmd *Command) checkRequiredFlags() requiredFlagsErr { + var missingFlags []string + for _, f := range cmd.Flags { + if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { + var flagPresent bool + var flagName string + + for _, key := range f.Names() { + flagName = key + + if cmd.IsSet(strings.TrimSpace(key)) { + flagPresent = true + } + } + + if !flagPresent && flagName != "" { + missingFlags = append(missingFlags, flagName) + } + } + } + + if len(missingFlags) != 0 { + return &errRequiredFlags{missingFlags: missingFlags} + } + + return nil +} + +func (cmd *Command) onInvalidFlag(ctx context.Context, name string) { + for cmd != nil { + if cmd.InvalidFlagAccessHandler != nil { + cmd.InvalidFlagAccessHandler(ctx, cmd, name) + break + } + cmd = cmd.parent + } +} + +// NumFlags returns the number of flags set +func (cmd *Command) NumFlags() int { + return cmd.flagSet.NFlag() +} + +// Set sets a context flag to a value. +func (cmd *Command) Set(name, value string) error { + if fs := cmd.lookupFlagSet(name); fs != nil { + return fs.Set(name, value) + } + + return fmt.Errorf("no such flag -%s", name) +} + +// IsSet determines if the flag was actually set +func (cmd *Command) IsSet(name string) bool { + if fs := cmd.lookupFlagSet(name); fs != nil { + isSet := false + fs.Visit(func(f *flag.Flag) { + if f.Name == name { + isSet = true + } + }) + if isSet { + return true + } + + f := cmd.lookupFlag(name) + if f == nil { + return false + } + + return f.IsSet() + } + + return false +} + +// LocalFlagNames returns a slice of flag names used in this +// command. +func (cmd *Command) LocalFlagNames() []string { + var names []string + cmd.flagSet.Visit(makeFlagNameVisitor(&names)) + // Check the flags which have been set via env or file + if cmd.Flags != nil { + for _, f := range cmd.Flags { + if f.IsSet() { + names = append(names, f.Names()...) + } + } + } + + // Sort out the duplicates since flag could be set via multiple + // paths + m := map[string]struct{}{} + var unames []string + for _, name := range names { + if _, ok := m[name]; !ok { + m[name] = struct{}{} + unames = append(unames, name) + } + } + + return unames +} + +// FlagNames returns a slice of flag names used by the this command +// and all of its parent commands. +func (cmd *Command) FlagNames() []string { + var names []string + for _, pCmd := range cmd.Lineage() { + names = append(names, pCmd.LocalFlagNames()...) + } + return names +} + +// Lineage returns *this* command and all of its ancestor commands +// in order from child to parent +func (cmd *Command) Lineage() []*Command { + var lineage []*Command + + for cur := cmd; cur != nil; cur = cur.parent { + lineage = append(lineage, cur) + } + + return lineage +} + +// Count returns the num of occurrences of this flag +func (cmd *Command) Count(name string) int { + if fs := cmd.lookupFlagSet(name); fs != nil { + if cf, ok := fs.Lookup(name).Value.(Countable); ok { + return cf.Count() + } + } + return 0 +} + +// Value returns the value of the flag corresponding to `name` +func (cmd *Command) Value(name string) interface{} { + if fs := cmd.lookupFlagSet(name); fs != nil { + return fs.Lookup(name).Value.(flag.Getter).Get() + } + return nil +} + +// Args returns the command line arguments associated with the +// command. +func (cmd *Command) Args() Args { + ret := args(cmd.flagSet.Args()) + return &ret +} + +// NArg returns the number of the command line arguments. +func (cmd *Command) NArg() int { + return cmd.Args().Len() +} + func hasCommand(commands []*Command, command *Command) bool { for _, existing := range commands { if command == existing { @@ -714,12 +887,12 @@ func hasCommand(commands []*Command, command *Command) bool { return false } -func runFlagActions(cCtx *Context, flags []Flag) error { +func runFlagActions(ctx context.Context, cmd *Command, flags []Flag) error { for _, fl := range flags { isSet := false for _, name := range fl.Names() { - if cCtx.IsSet(name) { + if cmd.IsSet(name) { isSet = true break } @@ -730,7 +903,7 @@ func runFlagActions(cCtx *Context, flags []Flag) error { } if af, ok := fl.(ActionableFlag); ok { - if err := af.RunAction(cCtx); err != nil { + if err := af.RunAction(ctx, cmd); err != nil { return err } } @@ -750,3 +923,21 @@ func checkStringSliceIncludes(want string, sSlice []string) bool { return found } + +func makeFlagNameVisitor(names *[]string) func(*flag.Flag) { + return func(f *flag.Flag) { + nameParts := strings.Split(f.Name, ",") + name := strings.TrimSpace(nameParts[0]) + + for _, part := range nameParts { + part = strings.TrimSpace(part) + if len(part) > len(name) { + name = part + } + } + + if name != "" { + *names = append(*names, name) + } + } +} diff --git a/command_test.go b/command_test.go index 88338738a9..f84040f5a8 100644 --- a/command_test.go +++ b/command_test.go @@ -10,6 +10,7 @@ import ( "net/mail" "os" "reflect" + "sort" "strconv" "strings" "testing" @@ -154,40 +155,39 @@ func TestCommandFlagParsing(t *testing.T) { testArgs []string skipFlagParsing bool useShortOptionHandling bool - expectedErr error + expectedErr string }{ // Test normal "not ignoring flags" flow - {testArgs: []string{"test-cmd", "-break", "blah", "blah"}, skipFlagParsing: false, useShortOptionHandling: false, expectedErr: errors.New("flag provided but not defined: -break")}, - {testArgs: []string{"test-cmd", "blah", "blah"}, skipFlagParsing: true, useShortOptionHandling: false, expectedErr: nil}, // Test SkipFlagParsing without any args that look like flags - {testArgs: []string{"test-cmd", "blah", "-break"}, skipFlagParsing: true, useShortOptionHandling: false, expectedErr: nil}, // Test SkipFlagParsing with random flag arg - {testArgs: []string{"test-cmd", "blah", "-help"}, skipFlagParsing: true, useShortOptionHandling: false, expectedErr: nil}, // Test SkipFlagParsing with "special" help flag arg - {testArgs: []string{"test-cmd", "blah", "-h"}, skipFlagParsing: false, useShortOptionHandling: true, expectedErr: nil}, // Test UseShortOptionHandling + {testArgs: []string{"test-cmd", "-break", "blah", "blah"}, skipFlagParsing: false, useShortOptionHandling: false, expectedErr: "flag provided but not defined: -break"}, + {testArgs: []string{"test-cmd", "blah", "blah"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing without any args that look like flags + {testArgs: []string{"test-cmd", "blah", "-break"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing with random flag arg + {testArgs: []string{"test-cmd", "blah", "-help"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing with "special" help flag arg + {testArgs: []string{"test-cmd", "blah", "-h"}, skipFlagParsing: false, useShortOptionHandling: true}, // Test UseShortOptionHandling } for _, c := range cases { t.Run(strings.Join(c.testArgs, " "), func(t *testing.T) { - cmd := &Command{Writer: io.Discard} - set := flag.NewFlagSet("test", 0) - _ = set.Parse(c.testArgs) - - cCtx := NewContext(cmd, set, nil) - - subCmd := &Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(_ *Context) error { return nil }, - SkipFlagParsing: c.skipFlagParsing, + cmd := &Command{ + Writer: io.Discard, + Commands: []*Command{ + { + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(context.Context, *Command) error { return nil }, + SkipFlagParsing: c.skipFlagParsing, + + flagSet: flag.NewFlagSet("test", 0), + }, + }, } - ctx, cancel := context.WithTimeout(cCtx.Context, 100*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) t.Cleanup(cancel) - err := subCmd.Run(ctx, c.testArgs) - - expect(t, err, c.expectedErr) - // expect(t, cCtx.Args().Slice(), c.testArgs) + err := cmd.Run(ctx, c.testArgs) + require.EqualError(t, err, c.expectedErr) }) } } @@ -226,8 +226,8 @@ func TestParseAndRunShortOpts(t *testing.T) { Name: "test", Usage: "this is for testing", Description: "testing", - Action: func(c *Context) error { - state["args"] = c.Args() + Action: func(_ context.Context, cmd *Command) error { + state["args"] = cmd.Args() return nil }, UseShortOptionHandling: true, @@ -266,10 +266,10 @@ func TestParseAndRunShortOpts(t *testing.T) { func TestCommand_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { cmd := &Command{ Name: "bar", - Before: func(*Context) error { + Before: func(context.Context, *Command) error { return fmt.Errorf("before error") }, - After: func(*Context) error { + After: func(context.Context, *Command) error { return fmt.Errorf("after error") }, Writer: io.Discard, @@ -288,20 +288,20 @@ func TestCommand_Run_BeforeSavesMetadata(t *testing.T) { cmd := &Command{ Name: "bar", - Before: func(c *Context) error { - c.Command.Metadata["msg"] = "hello world" + Before: func(_ context.Context, cmd *Command) error { + cmd.Metadata["msg"] = "hello world" return nil }, - Action: func(c *Context) error { - msg, ok := c.Command.Metadata["msg"] + Action: func(_ context.Context, cmd *Command) error { + msg, ok := cmd.Metadata["msg"] if !ok { return errors.New("msg not found") } receivedMsgFromAction = msg.(string) return nil }, - After: func(c *Context) error { - msg, ok := c.Command.Metadata["msg"] + After: func(_ context.Context, cmd *Command) error { + msg, ok := cmd.Metadata["msg"] if !ok { return errors.New("msg not found") } @@ -310,21 +310,11 @@ func TestCommand_Run_BeforeSavesMetadata(t *testing.T) { }, } - err := cmd.Run(buildTestContext(t), []string{"foo", "bar"}) - if err != nil { - t.Fatalf("expected no error from Run, got %s", err) - } - - expectedMsg := "hello world" + r := require.New(t) - if receivedMsgFromAction != expectedMsg { - t.Fatalf("expected msg from Action to match. Given: %q\nExpected: %q", - receivedMsgFromAction, expectedMsg) - } - if receivedMsgFromAfter != expectedMsg { - t.Fatalf("expected msg from After to match. Given: %q\nExpected: %q", - receivedMsgFromAction, expectedMsg) - } + r.NoError(cmd.Run(buildTestContext(t), []string{"foo", "bar"})) + r.Equal("hello world", receivedMsgFromAction) + r.Equal("hello world", receivedMsgFromAfter) } func TestCommand_OnUsageError_hasCommandContext(t *testing.T) { @@ -333,8 +323,8 @@ func TestCommand_OnUsageError_hasCommandContext(t *testing.T) { Flags: []Flag{ &IntFlag{Name: "flag"}, }, - OnUsageError: func(c *Context, err error, _ bool) error { - return fmt.Errorf("intercepted in %s: %s", c.Command.Name, err.Error()) + OnUsageError: func(_ context.Context, cmd *Command, err error, _ bool) error { + return fmt.Errorf("intercepted in %s: %s", cmd.Name, err.Error()) }, } @@ -354,7 +344,7 @@ func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { Flags: []Flag{ &IntFlag{Name: "flag"}, }, - OnUsageError: func(_ *Context, err error, _ bool) error { + OnUsageError: func(_ context.Context, _ *Command, err error, _ bool) error { if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { t.Errorf("Expect an invalid value error, but got \"%v\"", err) } @@ -383,7 +373,7 @@ func TestCommand_OnUsageError_WithSubcommand(t *testing.T) { Flags: []Flag{ &IntFlag{Name: "flag"}, }, - OnUsageError: func(_ *Context, err error, _ bool) error { + OnUsageError: func(_ context.Context, _ *Command, err error, _ bool) error { if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { t.Errorf("Expect an invalid value error, but got \"%v\"", err) } @@ -403,8 +393,8 @@ func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { { Name: "baz", Usage: "this is for testing", - Action: func(cCtx *Context) error { - require.Equal(t, io.Discard, cCtx.Command.Root().ErrWriter) + Action: func(_ context.Context, cmd *Command) error { + require.Equal(t, io.Discard, cmd.Root().ErrWriter) return nil }, @@ -433,8 +423,8 @@ func TestCommandSkipFlagParsing(t *testing.T) { Flags: []Flag{ &StringFlag{Name: "flag"}, }, - Action: func(c *Context) error { - args = c.Args() + Action: func(_ context.Context, cmd *Command) error { + args = cmd.Args() return nil }, Writer: io.Discard, @@ -472,8 +462,8 @@ func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) { Usage: "A number to parse", }, }, - ShellComplete: func(cCtx *Context) { - fmt.Fprintf(cCtx.Command.Root().Writer, "found %[1]d args", cCtx.NArg()) + ShellComplete: func(_ context.Context, cmd *Command) { + fmt.Fprintf(cmd.Root().Writer, "found %[1]d args", cmd.NArg()) }, } @@ -576,7 +566,7 @@ func TestCommand_RunSubcommandWithDefault(t *testing.T) { Commands: []*Command{ { Name: "foo", - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return errors.New("should not run this subcommand") }, }, @@ -584,7 +574,7 @@ func TestCommand_RunSubcommandWithDefault(t *testing.T) { Name: "bar", Usage: "this is for testing", Commands: []*Command{{}}, // some subcommand - Action: func(*Context) error { + Action: func(context.Context, *Command) error { return nil }, }, @@ -602,8 +592,8 @@ func TestCommand_Run(t *testing.T) { s := "" cmd := &Command{ - Action: func(c *Context) error { - s = s + c.Args().First() + Action: func(_ context.Context, cmd *Command) error { + s = s + cmd.Args().First() return nil }, } @@ -880,14 +870,11 @@ func TestCommand_Setup_defaultsWriter(t *testing.T) { } func TestCommand_RunAsSubcommandParseFlags(t *testing.T) { - var cCtx *Context - cmd := &Command{ Commands: []*Command{ { Name: "foo", - Action: func(c *Context) error { - cCtx = c + Action: func(context.Context, *Command) error { return nil }, Flags: []Flag{ @@ -897,15 +884,17 @@ func TestCommand_RunAsSubcommandParseFlags(t *testing.T) { Usage: "language for the greeting", }, }, - Before: func(_ *Context) error { return nil }, + Before: func(context.Context, *Command) error { return nil }, }, }, } - _ = cmd.Run(buildTestContext(t), []string{"", "foo", "--lang", "spanish", "abcd"}) + r := require.New(t) + + r.NoError(cmd.Run(buildTestContext(t), []string{"", "foo", "--lang", "spanish", "abcd"})) - expect(t, cCtx.Args().Get(0), "abcd") - expect(t, cCtx.String("lang"), "spanish") + r.Equal("abcd", cmd.Args().Get(0)) + r.Equal("spanish", cmd.String("lang")) } func TestCommand_CommandWithFlagBeforeTerminator(t *testing.T) { @@ -919,21 +908,23 @@ func TestCommand_CommandWithFlagBeforeTerminator(t *testing.T) { Flags: []Flag{ &StringFlag{Name: "option", Value: "", Usage: "some option"}, }, - Action: func(c *Context) error { - parsedOption = c.String("option") - args = c.Args() + Action: func(_ context.Context, cmd *Command) error { + parsedOption = cmd.String("option") + args = cmd.Args() return nil }, }, }, } - _ = cmd.Run(buildTestContext(t), []string{"", "cmd", "--option", "my-option", "my-arg", "--", "--notARealFlag"}) + r := require.New(t) + + r.NoError(cmd.Run(buildTestContext(t), []string{"", "cmd", "--option", "my-option", "my-arg", "--", "--notARealFlag"})) - expect(t, parsedOption, "my-option") - expect(t, args.Get(0), "my-arg") - expect(t, args.Get(1), "--") - expect(t, args.Get(2), "--notARealFlag") + r.Equal("my-option", parsedOption) + r.Equal("my-arg", args.Get(0)) + r.Equal("--", args.Get(1)) + r.Equal("--notARealFlag", args.Get(2)) } func TestCommand_CommandWithDash(t *testing.T) { @@ -943,18 +934,20 @@ func TestCommand_CommandWithDash(t *testing.T) { Commands: []*Command{ { Name: "cmd", - Action: func(c *Context) error { - args = c.Args() + Action: func(_ context.Context, cmd *Command) error { + args = cmd.Args() return nil }, }, }, } - _ = cmd.Run(buildTestContext(t), []string{"", "cmd", "my-arg", "-"}) + r := require.New(t) - expect(t, args.Get(0), "my-arg") - expect(t, args.Get(1), "-") + r.NoError(cmd.Run(buildTestContext(t), []string{"", "cmd", "my-arg", "-"})) + r.NotNil(args) + r.Equal("my-arg", args.Get(0)) + r.Equal("-", args.Get(1)) } func TestCommand_CommandWithNoFlagBeforeTerminator(t *testing.T) { @@ -964,19 +957,22 @@ func TestCommand_CommandWithNoFlagBeforeTerminator(t *testing.T) { Commands: []*Command{ { Name: "cmd", - Action: func(c *Context) error { - args = c.Args() + Action: func(_ context.Context, cmd *Command) error { + args = cmd.Args() return nil }, }, }, } - _ = cmd.Run(buildTestContext(t), []string{"", "cmd", "my-arg", "--", "notAFlagAtAll"}) + r := require.New(t) + + r.NoError(cmd.Run(buildTestContext(t), []string{"", "cmd", "my-arg", "--", "notAFlagAtAll"})) - expect(t, args.Get(0), "my-arg") - expect(t, args.Get(1), "--") - expect(t, args.Get(2), "notAFlagAtAll") + r.NotNil(args) + r.Equal("my-arg", args.Get(0)) + r.Equal("--", args.Get(1)) + r.Equal("notAFlagAtAll", args.Get(2)) } func TestCommand_SkipFlagParsing(t *testing.T) { @@ -984,8 +980,8 @@ func TestCommand_SkipFlagParsing(t *testing.T) { cmd := &Command{ SkipFlagParsing: true, - Action: func(c *Context) error { - args = c.Args() + Action: func(_ context.Context, cmd *Command) error { + args = cmd.Args() return nil }, } @@ -1002,12 +998,12 @@ func TestCommand_VisibleCommands(t *testing.T) { Commands: []*Command{ { Name: "frob", - Action: func(_ *Context) error { return nil }, + Action: func(context.Context, *Command) error { return nil }, }, { Name: "frib", Hidden: true, - Action: func(_ *Context) error { return nil }, + Action: func(context.Context, *Command) error { return nil }, }, }, } @@ -1058,10 +1054,10 @@ func TestCommand_UseShortOptionHandling(t *testing.T) { &BoolFlag{Name: "two", Aliases: []string{"t"}}, &StringFlag{Name: "name", Aliases: []string{"n"}}, } - cmd.Action = func(c *Context) error { - one = c.Bool("one") - two = c.Bool("two") - name = c.String("name") + cmd.Action = func(_ context.Context, cmd *Command) error { + one = cmd.Bool("one") + two = cmd.Bool("two") + name = cmd.String("name") return nil } @@ -1096,10 +1092,10 @@ func TestCommand_UseShortOptionHandlingCommand(t *testing.T) { &BoolFlag{Name: "two", Aliases: []string{"t"}}, &StringFlag{Name: "name", Aliases: []string{"n"}}, }, - Action: func(c *Context) error { - one = c.Bool("one") - two = c.Bool("two") - name = c.String("name") + Action: func(_ context.Context, cmd *Command) error { + one = cmd.Bool("one") + two = cmd.Bool("two") + name = cmd.String("name") return nil }, UseShortOptionHandling: true, @@ -1148,10 +1144,10 @@ func TestCommand_UseShortOptionHandlingSubCommand(t *testing.T) { &BoolFlag{Name: "two", Aliases: []string{"t"}}, &StringFlag{Name: "name", Aliases: []string{"n"}}, }, - Action: func(c *Context) error { - one = c.Bool("one") - two = c.Bool("two") - name = c.String("name") + Action: func(_ context.Context, cmd *Command) error { + one = cmd.Bool("one") + two = cmd.Bool("two") + name = cmd.String("name") return nil }, } @@ -1199,11 +1195,11 @@ func TestCommand_UseShortOptionAfterSliceFlag(t *testing.T) { &BoolFlag{Name: "two", Aliases: []string{"t"}}, &StringFlag{Name: "name", Aliases: []string{"n"}}, } - cmd.Action = func(c *Context) error { - sliceVal = c.StringSlice("env") - one = c.Bool("one") - two = c.Bool("two") - name = c.String("name") + cmd.Action = func(_ context.Context, cmd *Command) error { + sliceVal = cmd.StringSlice("env") + one = cmd.Bool("one") + two = cmd.Bool("two") + name = cmd.String("name") return nil } @@ -1222,8 +1218,8 @@ func TestCommand_Float64Flag(t *testing.T) { Flags: []Flag{ &FloatFlag{Name: "height", Value: 1.5, Usage: "Set the height, in meters"}, }, - Action: func(c *Context) error { - meters = c.Float("height") + Action: func(_ context.Context, cmd *Command) error { + meters = cmd.Float("height") return nil }, } @@ -1244,9 +1240,9 @@ func TestCommand_ParseSliceFlags(t *testing.T) { &IntSliceFlag{Name: "p", Value: []int64{}, Usage: "set one or more ip addr"}, &StringSliceFlag{Name: "ip", Value: []string{}, Usage: "set one or more ports to open"}, }, - Action: func(c *Context) error { - parsedIntSlice = c.IntSlice("p") - parsedStringSlice = c.StringSlice("ip") + Action: func(_ context.Context, cmd *Command) error { + parsedIntSlice = cmd.IntSlice("p") + parsedStringSlice = cmd.StringSlice("ip") return nil }, }, @@ -1272,9 +1268,9 @@ func TestCommand_ParseSliceFlagsWithMissingValue(t *testing.T) { &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") + Action: func(_ context.Context, cmd *Command) error { + parsedIntSlice = cmd.IntSlice("a") + parsedStringSlice = cmd.StringSlice("str") return nil }, }, @@ -1312,8 +1308,8 @@ func TestCommand_SetStdin(t *testing.T) { cmd := &Command{ Name: "test", Reader: strings.NewReader("Hello World!"), - Action: func(c *Context) error { - _, err := c.Command.Reader.Read(buf) + Action: func(_ context.Context, cmd *Command) error { + _, err := cmd.Reader.Read(buf) return err }, } @@ -1340,8 +1336,8 @@ func TestCommand_SetStdin_Subcommand(t *testing.T) { Commands: []*Command{ { Name: "subcommand", - Action: func(c *Context) error { - _, err := c.Command.Root().Reader.Read(buf) + Action: func(_ context.Context, cmd *Command) error { + _, err := cmd.Root().Reader.Read(buf) return err }, }, @@ -1384,10 +1380,10 @@ func TestCommand_BeforeFunc(t *testing.T) { var err error cmd := &Command{ - Before: func(c *Context) error { + Before: func(_ context.Context, cmd *Command) error { counts.Total++ counts.Before = counts.Total - s := c.String("opt") + s := cmd.String("opt") if s == "fail" { return beforeError } @@ -1397,7 +1393,7 @@ func TestCommand_BeforeFunc(t *testing.T) { Commands: []*Command{ { Name: "sub", - Action: func(*Context) error { + Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil @@ -1448,7 +1444,7 @@ func TestCommand_BeforeFunc(t *testing.T) { counts = &opCounts{} afterError := errors.New("fail again") - cmd.After = func(_ *Context) error { + cmd.After = func(context.Context, *Command) error { return afterError } @@ -1476,12 +1472,12 @@ func TestCommand_BeforeAfterFuncShellCompletion(t *testing.T) { cmd := &Command{ EnableShellCompletion: true, - Before: func(*Context) error { + Before: func(context.Context, *Command) error { counts.Total++ counts.Before = counts.Total return nil }, - After: func(*Context) error { + After: func(context.Context, *Command) error { counts.Total++ counts.After = counts.Total return nil @@ -1489,7 +1485,7 @@ func TestCommand_BeforeAfterFuncShellCompletion(t *testing.T) { Commands: []*Command{ { Name: "sub", - Action: func(*Context) error { + Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil @@ -1527,10 +1523,10 @@ func TestCommand_AfterFunc(t *testing.T) { var err error cmd := &Command{ - After: func(c *Context) error { + After: func(_ context.Context, cmd *Command) error { counts.Total++ counts.After = counts.Total - s := c.String("opt") + s := cmd.String("opt") if s == "fail" { return afterError } @@ -1540,7 +1536,7 @@ func TestCommand_AfterFunc(t *testing.T) { Commands: []*Command{ { Name: "sub", - Action: func(*Context) error { + Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil @@ -1740,7 +1736,7 @@ func TestRequiredFlagCommandRunBehavior(t *testing.T) { Commands: []*Command{{ Name: "mySubCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, - Action: func(c *Context) error { + Action: func(context.Context, *Command) error { return nil }, }}, @@ -1798,13 +1794,12 @@ func TestCommand_VersionPrinter(t *testing.T) { }() wasCalled := false - VersionPrinter = func(*Context) { + VersionPrinter = func(*Command) { wasCalled = true } cmd := &Command{} - cCtx := NewContext(cmd, nil, nil) - ShowVersion(cCtx) + ShowVersion(cmd) if wasCalled == false { t.Errorf("Version printer expected to be called, but was not") @@ -1814,14 +1809,14 @@ func TestCommand_VersionPrinter(t *testing.T) { func TestCommand_CommandNotFound(t *testing.T) { counts := &opCounts{} cmd := &Command{ - CommandNotFound: func(*Context, string) { + CommandNotFound: func(context.Context, *Command, string) { counts.Total++ counts.CommandNotFound = counts.Total }, Commands: []*Command{ { Name: "bar", - Action: func(*Context) error { + Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil @@ -1844,11 +1839,11 @@ func TestCommand_OrderOfOperations(t *testing.T) { cmd := &Command{ EnableShellCompletion: true, - ShellComplete: func(*Context) { + ShellComplete: func(context.Context, *Command) { counts.Total++ counts.ShellComplete = counts.Total }, - OnUsageError: func(*Context, error, bool) error { + OnUsageError: func(context.Context, *Command, error, bool) error { counts.Total++ counts.OnUsageError = counts.Total return errors.New("hay OnUsageError") @@ -1856,31 +1851,31 @@ func TestCommand_OrderOfOperations(t *testing.T) { Writer: io.Discard, } - beforeNoError := func(*Context) error { + beforeNoError := func(context.Context, *Command) error { counts.Total++ counts.Before = counts.Total return nil } - beforeError := func(*Context) error { + beforeError := func(context.Context, *Command) error { counts.Total++ counts.Before = counts.Total return errors.New("hay Before") } cmd.Before = beforeNoError - cmd.CommandNotFound = func(*Context, string) { + cmd.CommandNotFound = func(context.Context, *Command, string) { counts.Total++ counts.CommandNotFound = counts.Total } - afterNoError := func(*Context) error { + afterNoError := func(context.Context, *Command) error { counts.Total++ counts.After = counts.Total return nil } - afterError := func(*Context) error { + afterError := func(context.Context, *Command) error { counts.Total++ counts.After = counts.Total return errors.New("hay After") @@ -1890,7 +1885,7 @@ func TestCommand_OrderOfOperations(t *testing.T) { cmd.Commands = []*Command{ { Name: "bar", - Action: func(*Context) error { + Action: func(context.Context, *Command) error { counts.Total++ counts.SubCommand = counts.Total return nil @@ -1898,7 +1893,7 @@ func TestCommand_OrderOfOperations(t *testing.T) { }, } - cmd.Action = func(*Context) error { + cmd.Action = func(context.Context, *Command) error { counts.Total++ counts.Action = counts.Total return nil @@ -2002,7 +1997,7 @@ func TestCommand_Run_CommandWithSubcommandHasHelpTopic(t *testing.T) { Name: "foo", Description: "descriptive wall of text about how it does foo things", Commands: []*Command{subCmdBar, subCmdBaz}, - Action: func(c *Context) error { return nil }, + Action: func(context.Context, *Command) error { return nil }, Writer: buf, } @@ -2102,7 +2097,7 @@ func TestCommand_Run_Help(t *testing.T) { Usage: "make an explosive entrance", Writer: buf, HideHelp: tt.hideHelp, - Action: func(*Context) error { + Action: func(context.Context, *Command) error { buf.WriteString("boom I say!") return nil }, @@ -2134,7 +2129,7 @@ func TestCommand_Run_Version(t *testing.T) { Usage: "make an explosive entrance", Version: "0.1.0", Writer: buf, - Action: func(*Context) error { + Action: func(context.Context, *Command) error { buf.WriteString("boom I say!") return nil }, @@ -2314,8 +2309,8 @@ func TestCommand_Run_SubcommandDoesNotOverwriteErrorFromBefore(t *testing.T) { }, }, Name: "bar", - Before: func(c *Context) error { return fmt.Errorf("before error") }, - After: func(c *Context) error { return fmt.Errorf("after error") }, + Before: func(context.Context, *Command) error { return fmt.Errorf("before error") }, + After: func(context.Context, *Command) error { return fmt.Errorf("after error") }, }, }, } @@ -2338,7 +2333,7 @@ func TestCommand_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) { Flags: []Flag{ &IntFlag{Name: "flag"}, }, - OnUsageError: func(_ *Context, err error, isSubcommand bool) error { + OnUsageError: func(_ context.Context, _ *Command, err error, isSubcommand bool) error { if isSubcommand { t.Errorf("Expect subcommand") } @@ -2396,7 +2391,7 @@ func (c *customBoolFlag) Apply(set *flag.FlagSet) error { return nil } -func (c *customBoolFlag) RunAction(*Context) error { +func (c *customBoolFlag) RunAction(context.Context, *Command) error { return nil } @@ -2475,8 +2470,9 @@ func TestHandleExitCoder_Default(t *testing.T) { t.Errorf("error creating FlagSet: %s", err) } - cCtx := NewContext(app, fs, nil) - _ = app.handleExitCoder(cCtx, Exit("Default Behavior Error", 42)) + app.flagSet = fs + + _ = app.handleExitCoder(context.Background(), Exit("Default Behavior Error", 42)) output := fakeErrWriter.String() if !strings.Contains(output, "Default") { @@ -2486,17 +2482,12 @@ func TestHandleExitCoder_Default(t *testing.T) { func TestHandleExitCoder_Custom(t *testing.T) { cmd := buildMinimalTestCommand() - fs, err := flagSet(cmd.Name, cmd.Flags) - if err != nil { - t.Errorf("error creating FlagSet: %s", err) - } - cmd.ExitErrHandler = func(_ *Context, _ error) { + cmd.ExitErrHandler = func(context.Context, *Command, error) { _, _ = fmt.Fprintln(ErrWriter, "I'm a Custom error handler, I print what I want!") } - ctx := NewContext(cmd, fs, nil) - _ = cmd.handleExitCoder(ctx, Exit("Default Behavior Error", 42)) + _ = cmd.handleExitCoder(context.Background(), Exit("Default Behavior Error", 42)) output := fakeErrWriter.String() if !strings.Contains(output, "Custom") { @@ -2512,18 +2503,18 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) { }, }, EnableShellCompletion: true, - ShellComplete: func(ctx *Context) { - for _, command := range ctx.Command.Commands { + ShellComplete: func(_ context.Context, cmd *Command) { + for _, command := range cmd.Commands { if command.Hidden { continue } for _, name := range command.Names() { - _, _ = fmt.Fprintln(ctx.Command.Writer, name) + _, _ = fmt.Fprintln(cmd.Writer, name) } } - for _, fl := range ctx.Command.Flags { + for _, fl := range cmd.Flags { for _, name := range fl.Names() { if name == GenerateShellCompletionFlag.Names()[0] { continue @@ -2532,14 +2523,14 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) { switch name = strings.TrimSpace(name); len(name) { case 0: case 1: - _, _ = fmt.Fprintln(ctx.Command.Writer, "-"+name) + _, _ = fmt.Fprintln(cmd.Writer, "-"+name) default: - _, _ = fmt.Fprintln(ctx.Command.Writer, "--"+name) + _, _ = fmt.Fprintln(cmd.Writer, "--"+name) } } } }, - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return fmt.Errorf("should not get here") }, Writer: io.Discard, @@ -2561,7 +2552,7 @@ func TestWhenExitSubCommandWithCodeThenCommandQuitUnexpectedly(t *testing.T) { Commands: []*Command{ { Name: "subcmd", - Action: func(c *Context) error { + Action: func(context.Context, *Command) error { return Exit("exit error", testCode) }, }, @@ -2571,7 +2562,7 @@ func TestWhenExitSubCommandWithCodeThenCommandQuitUnexpectedly(t *testing.T) { // set user function as ExitErrHandler exitCodeFromExitErrHandler := int(0) - cmd.ExitErrHandler = func(_ *Context, err error) { + cmd.ExitErrHandler = func(_ context.Context, _ *Command, err error) { if exitErr, ok := err.(ExitCoder); ok { exitCodeFromExitErrHandler = exitErr.ExitCode() } @@ -2779,11 +2770,11 @@ func TestFlagAction(t *testing.T) { stringFlag := &StringFlag{ Name: "f_string", - Action: func(cCtx *Context, v string) error { + Action: func(_ context.Context, cmd *Command, v string) error { if v == "" { return fmt.Errorf("empty string") } - _, err := cCtx.Command.Root().Writer.Write([]byte(v + " ")) + _, err := cmd.Root().Writer.Write([]byte(v + " ")) return err }, } @@ -2795,11 +2786,11 @@ func TestFlagAction(t *testing.T) { { Name: "c1", Flags: []Flag{stringFlag}, - Action: func(cCtx *Context) error { return nil }, + Action: func(_ context.Context, cmd *Command) error { return nil }, Commands: []*Command{ { Name: "sub1", - Action: func(cCtx *Context) error { return nil }, + Action: func(context.Context, *Command) error { return nil }, Flags: []Flag{stringFlag}, }, }, @@ -2812,71 +2803,71 @@ func TestFlagAction(t *testing.T) { }, &StringSliceFlag{ Name: "f_string_slice", - Action: func(cCtx *Context, v []string) error { + Action: func(_ context.Context, cmd *Command, v []string) error { if v[0] == "err" { return fmt.Errorf("error string slice") } - _, err := cCtx.Command.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) + _, err := cmd.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) return err }, }, &BoolFlag{ Name: "f_bool", - Action: func(cCtx *Context, v bool) error { + Action: func(_ context.Context, cmd *Command, v bool) error { if !v { return fmt.Errorf("value is false") } - _, err := cCtx.Command.Root().Writer.Write([]byte(fmt.Sprintf("%t ", v))) + _, err := cmd.Root().Writer.Write([]byte(fmt.Sprintf("%t ", v))) return err }, }, &DurationFlag{ Name: "f_duration", - Action: func(cCtx *Context, v time.Duration) error { + Action: func(_ context.Context, cmd *Command, v time.Duration) error { if v == 0 { return fmt.Errorf("empty duration") } - _, err := cCtx.Command.Root().Writer.Write([]byte(v.String() + " ")) + _, err := cmd.Root().Writer.Write([]byte(v.String() + " ")) return err }, }, &FloatFlag{ Name: "f_float64", - Action: func(cCtx *Context, v float64) error { + Action: func(_ context.Context, cmd *Command, v float64) error { if v < 0 { return fmt.Errorf("negative float64") } - _, err := cCtx.Command.Root().Writer.Write([]byte(strconv.FormatFloat(v, 'f', -1, 64) + " ")) + _, err := cmd.Root().Writer.Write([]byte(strconv.FormatFloat(v, 'f', -1, 64) + " ")) return err }, }, &FloatSliceFlag{ Name: "f_float64_slice", - Action: func(cCtx *Context, v []float64) error { + Action: func(_ context.Context, cmd *Command, v []float64) error { if len(v) > 0 && v[0] < 0 { return fmt.Errorf("invalid float64 slice") } - _, err := cCtx.Command.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) + _, err := cmd.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) return err }, }, &IntFlag{ Name: "f_int", - Action: func(cCtx *Context, v int64) error { + Action: func(_ context.Context, cmd *Command, v int64) error { if v < 0 { return fmt.Errorf("negative int") } - _, err := cCtx.Command.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) + _, err := cmd.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) return err }, }, &IntSliceFlag{ Name: "f_int_slice", - Action: func(cCtx *Context, v []int64) error { + Action: func(_ context.Context, cmd *Command, v []int64) error { if len(v) > 0 && v[0] < 0 { return fmt.Errorf("invalid int slice") } - _, err := cCtx.Command.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) + _, err := cmd.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) return err }, }, @@ -2885,36 +2876,36 @@ func TestFlagAction(t *testing.T) { Config: TimestampConfig{ Layout: "2006-01-02 15:04:05", }, - Action: func(cCtx *Context, v time.Time) error { + Action: func(_ context.Context, cmd *Command, v time.Time) error { if v.IsZero() { return fmt.Errorf("zero timestamp") } - _, err := cCtx.Command.Root().Writer.Write([]byte(v.Format(time.RFC3339) + " ")) + _, err := cmd.Root().Writer.Write([]byte(v.Format(time.RFC3339) + " ")) return err }, }, &UintFlag{ Name: "f_uint", - Action: func(cCtx *Context, v uint64) error { + Action: func(_ context.Context, cmd *Command, v uint64) error { if v == 0 { return fmt.Errorf("zero uint64") } - _, err := cCtx.Command.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) + _, err := cmd.Root().Writer.Write([]byte(fmt.Sprintf("%v ", v))) return err }, }, &StringMapFlag{ Name: "f_string_map", - Action: func(cCtx *Context, v map[string]string) error { + Action: func(_ context.Context, cmd *Command, v map[string]string) error { if _, ok := v["err"]; ok { return fmt.Errorf("error string map") } - _, err := cCtx.Command.Root().Writer.Write([]byte(fmt.Sprintf("%v", v))) + _, err := cmd.Root().Writer.Write([]byte(fmt.Sprintf("%v", v))) return err }, }, }, - Action: func(cCtx *Context) error { return nil }, + Action: func(context.Context, *Command) error { return nil }, } err := cmd.Run(buildTestContext(t), test.args) @@ -2946,7 +2937,7 @@ func TestPersistentFlag(t *testing.T) { Name: "persistentCommandFlag", Persistent: true, Destination: &appFlag, - Action: func(ctx *Context, s string) error { + Action: func(context.Context, *Command, string) error { persistentFlagActionCount++ return nil }, @@ -2995,8 +2986,8 @@ func TestPersistentFlag(t *testing.T) { Destination: &subCommandInt, }, }, - Action: func(ctx *Context) error { - appSliceFloat64 = ctx.FloatSlice("persistentCommandFloatSliceFlag") + Action: func(_ context.Context, cmd *Command) error { + appSliceFloat64 = cmd.FloatSlice("persistentCommandFloatSliceFlag") return nil }, }, @@ -3085,7 +3076,7 @@ func TestFlagDuplicates(t *testing.T) { Name: "iflag", }, }, - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, } @@ -3129,7 +3120,7 @@ func TestFlagDuplicates(t *testing.T) { func TestShorthandCommand(t *testing.T) { af := func(p *int) ActionFunc { - return func(ctx *Context) error { + return func(context.Context, *Command) error { *p = *p + 1 return nil } @@ -3210,3 +3201,588 @@ func TestShorthandCommand(t *testing.T) { t.Errorf("Expected command1 to be trigerred once but didnt %d %d", cmd1, cmd2) } } + +func TestCommand_Int(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Int64("myflag", 12, "doc") + + parentSet := flag.NewFlagSet("test", 0) + parentSet.Int64("top-flag", 13, "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + + r := require.New(t) + r.Equal(int64(12), cmd.Int("myflag")) + r.Equal(int64(13), cmd.Int("top-flag")) +} + +func TestCommand_Uint(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Uint64("myflagUint", uint64(13), "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Uint64("top-flag", uint64(14), "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + + r := require.New(t) + r.Equal(uint64(13), cmd.Uint("myflagUint")) + r.Equal(uint64(14), cmd.Uint("top-flag")) +} + +func TestCommand_Float64(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Float64("myflag", float64(17), "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Float64("top-flag", float64(18), "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + + r := require.New(t) + r.Equal(float64(17), cmd.Float("myflag")) + r.Equal(float64(18), cmd.Float("top-flag")) +} + +func TestCommand_Duration(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Duration("myflag", 12*time.Second, "doc") + + parentSet := flag.NewFlagSet("test", 0) + parentSet.Duration("top-flag", 13*time.Second, "doc") + pCmd := &Command{flagSet: parentSet} + + cmd := &Command{flagSet: set, parent: pCmd} + + r := require.New(t) + r.Equal(12*time.Second, cmd.Duration("myflag")) + r.Equal(13*time.Second, cmd.Duration("top-flag")) +} + +func TestCommand_String(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.String("myflag", "hello world", "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.String("top-flag", "hai veld", "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + + r := require.New(t) + r.Equal("hello world", cmd.String("myflag")) + r.Equal("hai veld", cmd.String("top-flag")) + + cmd = &Command{parent: pCmd} + r.Equal("hai veld", cmd.String("top-flag")) +} + +func TestCommand_Bool(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", false, "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Bool("top-flag", true, "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + + r := require.New(t) + r.False(cmd.Bool("myflag")) + r.True(cmd.Bool("top-flag")) +} + +func TestCommand_Value(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Int("myflag", 12, "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Int("top-flag", 13, "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + + r := require.New(t) + r.Equal(12, cmd.Value("myflag")) + r.Equal(13, cmd.Value("top-flag")) + r.Equal(nil, cmd.Value("unknown-flag")) +} + +func TestCommand_Value_InvalidFlagAccessHandler(t *testing.T) { + var flagName string + cmd := &Command{ + InvalidFlagAccessHandler: func(_ context.Context, _ *Command, name string) { + flagName = name + }, + Commands: []*Command{ + { + Name: "command", + Commands: []*Command{ + { + Name: "subcommand", + Action: func(_ context.Context, cmd *Command) error { + cmd.Value("missing") + return nil + }, + }, + }, + }, + }, + } + + r := require.New(t) + + r.NoError(cmd.Run(buildTestContext(t), []string{"run", "command", "subcommand"})) + r.Equal("missing", flagName) +} + +func TestCommand_Args(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", false, "doc") + cmd := &Command{flagSet: set} + _ = set.Parse([]string{"--myflag", "bat", "baz"}) + + r := require.New(t) + r.Equal(2, cmd.Args().Len()) + r.True(cmd.Bool("myflag")) +} + +func TestCommand_NArg(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", false, "doc") + cmd := &Command{flagSet: set} + _ = set.Parse([]string{"--myflag", "bat", "baz"}) + + require.Equal(t, 2, cmd.NArg()) +} + +func TestCommand_IsSet(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("one-flag", false, "doc") + set.Bool("two-flag", false, "doc") + set.String("three-flag", "hello world", "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Bool("top-flag", true, "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + + _ = set.Parse([]string{"--one-flag", "--two-flag", "--three-flag", "frob"}) + _ = parentSet.Parse([]string{"--top-flag"}) + + r := require.New(t) + + r.True(cmd.IsSet("one-flag")) + r.True(cmd.IsSet("two-flag")) + r.True(cmd.IsSet("three-flag")) + r.True(cmd.IsSet("top-flag")) + r.False(cmd.IsSet("bogus")) +} + +// XXX Corresponds to hack in context.IsSet for flags with EnvVar field +// Should be moved to `flag_test` in v2 +func TestCommand_IsSet_fromEnv(t *testing.T) { + var ( + timeoutIsSet, tIsSet bool + noEnvVarIsSet, nIsSet bool + passwordIsSet, pIsSet bool + unparsableIsSet, uIsSet bool + ) + + t.Setenv("APP_TIMEOUT_SECONDS", "15.5") + t.Setenv("APP_PASSWORD", "") + + cmd := &Command{ + Flags: []Flag{ + &FloatFlag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("APP_TIMEOUT_SECONDS")}, + &StringFlag{Name: "password", Aliases: []string{"p"}, Sources: EnvVars("APP_PASSWORD")}, + &FloatFlag{Name: "unparsable", Aliases: []string{"u"}, Sources: EnvVars("APP_UNPARSABLE")}, + &FloatFlag{Name: "no-env-var", Aliases: []string{"n"}}, + }, + Action: func(_ context.Context, cmd *Command) error { + timeoutIsSet = cmd.IsSet("timeout") + tIsSet = cmd.IsSet("t") + passwordIsSet = cmd.IsSet("password") + pIsSet = cmd.IsSet("p") + unparsableIsSet = cmd.IsSet("unparsable") + uIsSet = cmd.IsSet("u") + noEnvVarIsSet = cmd.IsSet("no-env-var") + nIsSet = cmd.IsSet("n") + return nil + }, + } + + r := require.New(t) + + r.NoError(cmd.Run(buildTestContext(t), []string{"run"})) + r.True(timeoutIsSet) + r.True(tIsSet) + r.True(passwordIsSet) + r.True(pIsSet) + r.False(noEnvVarIsSet) + r.False(nIsSet) + + t.Setenv("APP_UNPARSABLE", "foobar") + + r.Error(cmd.Run(buildTestContext(t), []string{"run"})) + r.False(unparsableIsSet) + r.False(uIsSet) +} + +func TestCommand_NumFlags(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", false, "doc") + set.String("otherflag", "hello world", "doc") + globalSet := flag.NewFlagSet("test", 0) + globalSet.Bool("myflagGlobal", true, "doc") + globalCmd := &Command{flagSet: globalSet} + cmd := &Command{flagSet: set, parent: globalCmd} + _ = set.Parse([]string{"--myflag", "--otherflag=foo"}) + _ = globalSet.Parse([]string{"--myflagGlobal"}) + require.Equal(t, 2, cmd.NumFlags()) +} + +func TestCommand_Set(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Int64("int", int64(5), "an int") + cmd := &Command{flagSet: set} + + r := require.New(t) + + r.False(cmd.IsSet("int")) + r.NoError(cmd.Set("int", "1")) + r.Equal(int64(1), cmd.Int("int")) + r.True(cmd.IsSet("int")) +} + +func TestCommand_Set_InvalidFlagAccessHandler(t *testing.T) { + set := flag.NewFlagSet("test", 0) + var flagName string + cmd := &Command{ + InvalidFlagAccessHandler: func(_ context.Context, _ *Command, name string) { + flagName = name + }, + flagSet: set, + } + + r := require.New(t) + + r.True(cmd.Set("missing", "") != nil) + r.Equal("missing", flagName) +} + +func TestCommand_LocalFlagNames(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("one-flag", false, "doc") + set.String("two-flag", "hello world", "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Bool("top-flag", true, "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + _ = set.Parse([]string{"--one-flag", "--two-flag=foo"}) + _ = parentSet.Parse([]string{"--top-flag"}) + + actualFlags := cmd.LocalFlagNames() + sort.Strings(actualFlags) + + require.Equal(t, []string{"one-flag", "two-flag"}, actualFlags) +} + +func TestCommand_FlagNames(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("one-flag", false, "doc") + set.String("two-flag", "hello world", "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Bool("top-flag", true, "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + _ = set.Parse([]string{"--one-flag", "--two-flag=foo"}) + _ = parentSet.Parse([]string{"--top-flag"}) + + actualFlags := cmd.FlagNames() + sort.Strings(actualFlags) + + require.Equal(t, []string{"one-flag", "top-flag", "two-flag"}, actualFlags) +} + +func TestCommand_Lineage(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("local-flag", false, "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Bool("top-flag", true, "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + _ = set.Parse([]string{"--local-flag"}) + _ = parentSet.Parse([]string{"--top-flag"}) + + lineage := cmd.Lineage() + + r := require.New(t) + r.Equal(2, len(lineage)) + r.Equal(cmd, lineage[0]) + r.Equal(pCmd, lineage[1]) +} + +func TestCommand_lookupFlagSet(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("local-flag", false, "doc") + parentSet := flag.NewFlagSet("test", 0) + parentSet.Bool("top-flag", true, "doc") + pCmd := &Command{flagSet: parentSet} + cmd := &Command{flagSet: set, parent: pCmd} + _ = set.Parse([]string{"--local-flag"}) + _ = parentSet.Parse([]string{"--top-flag"}) + + r := require.New(t) + + fs := cmd.lookupFlagSet("top-flag") + r.Equal(pCmd.flagSet, fs) + + fs = cmd.lookupFlagSet("local-flag") + r.Equal(cmd.flagSet, fs) + r.Nil(cmd.lookupFlagSet("frob")) +} + +func TestCommandAttributeAccessing(t *testing.T) { + tdata := []struct { + testCase string + setBoolInput string + ctxBoolInput string + parent *Command + }{ + { + testCase: "empty", + setBoolInput: "", + ctxBoolInput: "", + }, + { + testCase: "empty_with_background_context", + setBoolInput: "", + ctxBoolInput: "", + parent: &Command{}, + }, + { + testCase: "empty_set_bool_and_present_ctx_bool", + setBoolInput: "", + ctxBoolInput: "ctx-bool", + }, + { + testCase: "present_set_bool_and_present_ctx_bool_with_background_context", + setBoolInput: "", + ctxBoolInput: "ctx-bool", + parent: &Command{}, + }, + { + testCase: "present_set_bool_and_present_ctx_bool", + setBoolInput: "ctx-bool", + ctxBoolInput: "ctx-bool", + }, + { + testCase: "present_set_bool_and_present_ctx_bool_with_background_context", + setBoolInput: "ctx-bool", + ctxBoolInput: "ctx-bool", + parent: &Command{}, + }, + { + testCase: "present_set_bool_and_different_ctx_bool", + setBoolInput: "ctx-bool", + ctxBoolInput: "not-ctx-bool", + }, + { + testCase: "present_set_bool_and_different_ctx_bool_with_background_context", + setBoolInput: "ctx-bool", + ctxBoolInput: "not-ctx-bool", + parent: &Command{}, + }, + } + + for _, test := range tdata { + t.Run(test.testCase, func(t *testing.T) { + set := flag.NewFlagSet("some-flag-set-name", 0) + set.Bool(test.setBoolInput, false, "usage documentation") + cmd := &Command{flagSet: set, parent: test.parent} + + require.False(t, cmd.Bool(test.ctxBoolInput)) + }) + } +} + +func TestCheckRequiredFlags(t *testing.T) { + tdata := []struct { + testCase string + parseInput []string + envVarInput [2]string + flags []Flag + expectedAnError bool + expectedErrorContents []string + }{ + { + testCase: "empty", + }, + { + testCase: "optional", + flags: []Flag{ + &StringFlag{Name: "optionalFlag"}, + }, + }, + { + testCase: "required", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + }, + expectedAnError: true, + expectedErrorContents: []string{"requiredFlag"}, + }, + { + testCase: "required_and_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + }, + parseInput: []string{"--requiredFlag", "myinput"}, + }, + { + testCase: "required_and_present_via_env_var", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true, Sources: EnvVars("REQUIRED_FLAG")}, + }, + envVarInput: [2]string{"REQUIRED_FLAG", "true"}, + }, + { + testCase: "required_and_optional", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "optionalFlag"}, + }, + expectedAnError: true, + }, + { + testCase: "required_and_optional_and_optional_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "optionalFlag"}, + }, + parseInput: []string{"--optionalFlag", "myinput"}, + expectedAnError: true, + }, + { + testCase: "required_and_optional_and_optional_present_via_env_var", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "optionalFlag", Sources: EnvVars("OPTIONAL_FLAG")}, + }, + envVarInput: [2]string{"OPTIONAL_FLAG", "true"}, + expectedAnError: true, + }, + { + testCase: "required_and_optional_and_required_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "optionalFlag"}, + }, + parseInput: []string{"--requiredFlag", "myinput"}, + }, + { + testCase: "two_required", + flags: []Flag{ + &StringFlag{Name: "requiredFlagOne", Required: true}, + &StringFlag{Name: "requiredFlagTwo", Required: true}, + }, + expectedAnError: true, + expectedErrorContents: []string{"requiredFlagOne", "requiredFlagTwo"}, + }, + { + testCase: "two_required_and_one_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "requiredFlagTwo", Required: true}, + }, + parseInput: []string{"--requiredFlag", "myinput"}, + expectedAnError: true, + }, + { + testCase: "two_required_and_both_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "requiredFlagTwo", Required: true}, + }, + parseInput: []string{"--requiredFlag", "myinput", "--requiredFlagTwo", "myinput"}, + }, + { + testCase: "required_flag_with_short_name", + flags: []Flag{ + &StringSliceFlag{Name: "names", Aliases: []string{"N"}, Required: true}, + }, + parseInput: []string{"-N", "asd", "-N", "qwe"}, + }, + { + testCase: "required_flag_with_multiple_short_names", + flags: []Flag{ + &StringSliceFlag{Name: "names", Aliases: []string{"N", "n"}, Required: true}, + }, + parseInput: []string{"-n", "asd", "-n", "qwe"}, + }, + { + testCase: "required_flag_with_short_alias_not_printed_on_error", + expectedAnError: true, + expectedErrorContents: []string{"Required flag \"names\" not set"}, + flags: []Flag{ + &StringSliceFlag{Name: "names, n", Required: true}, + }, + }, + { + testCase: "required_flag_with_one_character", + expectedAnError: true, + expectedErrorContents: []string{"Required flag \"n\" not set"}, + flags: []Flag{ + &StringFlag{Name: "n", Required: true}, + }, + }, + } + + for _, test := range tdata { + t.Run(test.testCase, func(t *testing.T) { + // setup + if test.envVarInput[0] != "" { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv(test.envVarInput[0], test.envVarInput[1]) + } + + set := flag.NewFlagSet("test", 0) + for _, flags := range test.flags { + _ = flags.Apply(set) + } + _ = set.Parse(test.parseInput) + + cmd := &Command{ + Flags: test.flags, + flagSet: set, + } + + err := cmd.checkRequiredFlags() + + // assertions + if test.expectedAnError && err == nil { + t.Errorf("expected an error, but there was none") + } + if !test.expectedAnError && err != nil { + t.Errorf("did not expected an error, but there was one: %s", err) + } + for _, errString := range test.expectedErrorContents { + if err != nil { + if !strings.Contains(err.Error(), errString) { + t.Errorf("expected error %q to contain %q, but it didn't!", err.Error(), errString) + } + } + } + }) + } +} + +func TestCommand_ParentCommand_Set(t *testing.T) { + parentSet := flag.NewFlagSet("parent", flag.ContinueOnError) + parentSet.String("Name", "", "") + + cmd := &Command{ + flagSet: flag.NewFlagSet("child", flag.ContinueOnError), + parent: &Command{ + flagSet: parentSet, + }, + } + + err := cmd.Set("Name", "aaa") + if err != nil { + t.Errorf("expect nil. set parent context flag return err: %s", err) + } +} diff --git a/completion.go b/completion.go index 06998b9b46..2319a815c9 100644 --- a/completion.go +++ b/completion.go @@ -1,6 +1,7 @@ package cli import ( + "context" "embed" "fmt" "sort" @@ -41,7 +42,7 @@ func buildCompletionCommand() *Command { } } -func completionCommandAction(cCtx *Context) error { +func completionCommandAction(ctx context.Context, cmd *Command) error { var shells []string for k := range shellCompletions { shells = append(shells, k) @@ -49,17 +50,17 @@ func completionCommandAction(cCtx *Context) error { sort.Strings(shells) - if cCtx.Args().Len() == 0 { + if cmd.Args().Len() == 0 { return Exit(fmt.Sprintf("no shell provided for completion command. available shells are %+v", shells), 1) } - s := cCtx.Args().First() + s := cmd.Args().First() if rc, ok := shellCompletions[s]; !ok { return Exit(fmt.Sprintf("unknown shell %s, available shells are %+v", s, shells), 1) - } else if c, err := rc(cCtx.Command); err != nil { + } else if c, err := rc(cmd); err != nil { return Exit(err, 1) } else { - if _, err = cCtx.Command.Writer.Write([]byte(c)); err != nil { + if _, err = cmd.Writer.Write([]byte(c)); err != nil { return Exit(err, 1) } } diff --git a/context.go b/context.go deleted file mode 100644 index da1965d8a8..0000000000 --- a/context.go +++ /dev/null @@ -1,257 +0,0 @@ -package cli - -import ( - "context" - "flag" - "fmt" - "strings" -) - -const ( - contextContextKey = contextKey("cli.context") -) - -type contextKey string - -// Context is a type that is passed through to -// each Handler action in a cli application. Context -// can be used to retrieve context-specific args and -// parsed command-line options. -type Context struct { - context.Context - Command *Command - shellComplete bool - flagSet *flag.FlagSet - parent *Context -} - -// NewContext creates a new context. For use in when invoking a Command action. -func NewContext(cmd *Command, set *flag.FlagSet, parent *Context) *Context { - cCtx := &Context{ - Command: cmd, - flagSet: set, - parent: parent, - } - - if parent != nil { - cCtx.Context = parent.Context - cCtx.shellComplete = parent.shellComplete - - if parent.flagSet == nil { - parent.flagSet = &flag.FlagSet{} - } - } - - if cCtx.Command == nil { - cCtx.Command = &Command{} - } - - if cCtx.Context == nil { - cCtx.Context = context.Background() - } - - return cCtx -} - -// NumFlags returns the number of flags set -func (cCtx *Context) NumFlags() int { - return cCtx.flagSet.NFlag() -} - -// Set sets a context flag to a value. -func (cCtx *Context) Set(name, value string) error { - if fs := cCtx.lookupFlagSet(name); fs != nil { - return fs.Set(name, value) - } - - return fmt.Errorf("no such flag -%s", name) -} - -// IsSet determines if the flag was actually set -func (cCtx *Context) IsSet(name string) bool { - if fs := cCtx.lookupFlagSet(name); fs != nil { - isSet := false - fs.Visit(func(f *flag.Flag) { - if f.Name == name { - isSet = true - } - }) - if isSet { - return true - } - - f := cCtx.lookupFlag(name) - if f == nil { - return false - } - - return f.IsSet() - } - - return false -} - -// LocalFlagNames returns a slice of flag names used in this context. -func (cCtx *Context) LocalFlagNames() []string { - var names []string - cCtx.flagSet.Visit(makeFlagNameVisitor(&names)) - // Check the flags which have been set via env or file - if cCtx.Command != nil && cCtx.Command.Flags != nil { - for _, f := range cCtx.Command.Flags { - if f.IsSet() { - names = append(names, f.Names()...) - } - } - } - - // Sort out the duplicates since flag could be set via multiple - // paths - m := map[string]struct{}{} - var unames []string - for _, name := range names { - if _, ok := m[name]; !ok { - m[name] = struct{}{} - unames = append(unames, name) - } - } - - return unames -} - -// FlagNames returns a slice of flag names used by the this context and all of -// its parent contexts. -func (cCtx *Context) FlagNames() []string { - var names []string - for _, pCtx := range cCtx.Lineage() { - names = append(names, pCtx.LocalFlagNames()...) - } - return names -} - -// Lineage returns *this* context and all of its ancestor contexts in order from -// child to parent -func (cCtx *Context) Lineage() []*Context { - var lineage []*Context - - for cur := cCtx; cur != nil; cur = cur.parent { - lineage = append(lineage, cur) - } - - return lineage -} - -// Count returns the num of occurrences of this flag -func (cCtx *Context) Count(name string) int { - if fs := cCtx.lookupFlagSet(name); fs != nil { - if cf, ok := fs.Lookup(name).Value.(Countable); ok { - return cf.Count() - } - } - return 0 -} - -// Value returns the value of the flag corresponding to `name` -func (cCtx *Context) Value(name string) interface{} { - if fs := cCtx.lookupFlagSet(name); fs != nil { - return fs.Lookup(name).Value.(flag.Getter).Get() - } - return nil -} - -// Args returns the command line arguments associated with the context. -func (cCtx *Context) Args() Args { - ret := args(cCtx.flagSet.Args()) - return &ret -} - -// NArg returns the number of the command line arguments. -func (cCtx *Context) NArg() int { - return cCtx.Args().Len() -} - -func (cCtx *Context) lookupFlag(name string) Flag { - for _, c := range cCtx.Lineage() { - if c.Command == nil { - continue - } - - for _, f := range c.Command.Flags { - for _, n := range f.Names() { - if n == name { - return f - } - } - } - } - - return nil -} - -func (cCtx *Context) lookupFlagSet(name string) *flag.FlagSet { - for _, c := range cCtx.Lineage() { - if c.flagSet == nil { - continue - } - if f := c.flagSet.Lookup(name); f != nil { - return c.flagSet - } - } - cCtx.onInvalidFlag(name) - return nil -} - -func (cCtx *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr { - var missingFlags []string - for _, f := range flags { - if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { - var flagPresent bool - var flagName string - - for _, key := range f.Names() { - flagName = key - - if cCtx.IsSet(strings.TrimSpace(key)) { - flagPresent = true - } - } - - if !flagPresent && flagName != "" { - missingFlags = append(missingFlags, flagName) - } - } - } - - if len(missingFlags) != 0 { - return &errRequiredFlags{missingFlags: missingFlags} - } - - return nil -} - -func (cCtx *Context) onInvalidFlag(name string) { - for cCtx != nil { - if cCtx.Command != nil && cCtx.Command.InvalidFlagAccessHandler != nil { - cCtx.Command.InvalidFlagAccessHandler(cCtx, name) - break - } - cCtx = cCtx.parent - } -} - -func makeFlagNameVisitor(names *[]string) func(*flag.Flag) { - return func(f *flag.Flag) { - nameParts := strings.Split(f.Name, ",") - name := strings.TrimSpace(nameParts[0]) - - for _, part := range nameParts { - part = strings.TrimSpace(part) - if len(part) > len(name) { - name = part - } - } - - if name != "" { - *names = append(*names, name) - } - } -} diff --git a/context_test.go b/context_test.go deleted file mode 100644 index 315b91b46c..0000000000 --- a/context_test.go +++ /dev/null @@ -1,642 +0,0 @@ -package cli - -import ( - "context" - "flag" - "os" - "sort" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestNewContext(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Int64("myflag", 12, "doc") - set.Uint64("myflagUint", uint64(93), "doc") - set.Float64("myflag64", float64(17), "doc") - - globalSet := flag.NewFlagSet("test", 0) - globalSet.Int64("myflag", 42, "doc") - globalSet.Uint64("myflagUint", uint64(33), "doc") - globalSet.Float64("myflag64", float64(47), "doc") - - globalCtx := NewContext(nil, globalSet, nil) - - command := &Command{Name: "mycommand"} - cCtx := NewContext(nil, set, globalCtx) - cCtx.Command = command - - r := require.New(t) - r.Equal(int64(12), cCtx.Int("myflag")) - r.Equal(uint64(93), cCtx.Uint("myflagUint")) - r.Equal(float64(17), cCtx.Float("myflag64")) - r.Equal("mycommand", cCtx.Command.Name) -} - -func TestContext_Int(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Int64("myflag", 12, "doc") - - parentSet := flag.NewFlagSet("test", 0) - parentSet.Int64("top-flag", 13, "doc") - parentCctx := NewContext(nil, parentSet, nil) - - cCtx := NewContext(nil, set, parentCctx) - - r := require.New(t) - r.Equal(int64(12), cCtx.Int("myflag")) - r.Equal(int64(13), cCtx.Int("top-flag")) -} - -func TestContext_Uint(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Uint64("myflagUint", uint64(13), "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Uint64("top-flag", uint64(14), "doc") - parentCtx := NewContext(nil, parentSet, nil) - cCtx := NewContext(nil, set, parentCtx) - - r := require.New(t) - r.Equal(uint64(13), cCtx.Uint("myflagUint")) - r.Equal(uint64(14), cCtx.Uint("top-flag")) -} - -func TestContext_Float64(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Float64("myflag", float64(17), "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Float64("top-flag", float64(18), "doc") - parentCtx := NewContext(nil, parentSet, nil) - c := NewContext(nil, set, parentCtx) - expect(t, c.Float("myflag"), float64(17)) - expect(t, c.Float("top-flag"), float64(18)) -} - -func TestContext_Duration(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Duration("myflag", 12*time.Second, "doc") - - parentSet := flag.NewFlagSet("test", 0) - parentSet.Duration("top-flag", 13*time.Second, "doc") - parentCtx := NewContext(nil, parentSet, nil) - - c := NewContext(nil, set, parentCtx) - expect(t, c.Duration("myflag"), 12*time.Second) - expect(t, c.Duration("top-flag"), 13*time.Second) -} - -func TestContext_String(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.String("myflag", "hello world", "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.String("top-flag", "hai veld", "doc") - parentCtx := NewContext(nil, parentSet, nil) - c := NewContext(nil, set, parentCtx) - expect(t, c.String("myflag"), "hello world") - expect(t, c.String("top-flag"), "hai veld") - c = NewContext(nil, nil, parentCtx) - expect(t, c.String("top-flag"), "hai veld") -} - -func TestContext_Bool(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("myflag", false, "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Bool("top-flag", true, "doc") - parentCtx := NewContext(nil, parentSet, nil) - c := NewContext(nil, set, parentCtx) - expect(t, c.Bool("myflag"), false) - expect(t, c.Bool("top-flag"), true) -} - -func TestContext_Value(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Int("myflag", 12, "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Int("top-flag", 13, "doc") - parentCtx := NewContext(nil, parentSet, nil) - c := NewContext(nil, set, parentCtx) - expect(t, c.Value("myflag"), 12) - expect(t, c.Value("top-flag"), 13) - expect(t, c.Value("unknown-flag"), nil) -} - -func TestContext_Value_InvalidFlagAccessHandler(t *testing.T) { - var flagName string - cmd := &Command{ - InvalidFlagAccessHandler: func(_ *Context, name string) { - flagName = name - }, - Commands: []*Command{ - { - Name: "command", - Commands: []*Command{ - { - Name: "subcommand", - Action: func(ctx *Context) error { - ctx.Value("missing") - return nil - }, - }, - }, - }, - }, - } - - expect(t, cmd.Run(buildTestContext(t), []string{"run", "command", "subcommand"}), nil) - expect(t, flagName, "missing") -} - -func TestContext_Args(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("myflag", false, "doc") - c := NewContext(nil, set, nil) - _ = set.Parse([]string{"--myflag", "bat", "baz"}) - expect(t, c.Args().Len(), 2) - expect(t, c.Bool("myflag"), true) -} - -func TestContext_NArg(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("myflag", false, "doc") - c := NewContext(nil, set, nil) - _ = set.Parse([]string{"--myflag", "bat", "baz"}) - expect(t, c.NArg(), 2) -} - -func TestContext_IsSet(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("one-flag", false, "doc") - set.Bool("two-flag", false, "doc") - set.String("three-flag", "hello world", "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Bool("top-flag", true, "doc") - parentCtx := NewContext(nil, parentSet, nil) - ctx := NewContext(nil, set, parentCtx) - - _ = set.Parse([]string{"--one-flag", "--two-flag", "--three-flag", "frob"}) - _ = parentSet.Parse([]string{"--top-flag"}) - - expect(t, ctx.IsSet("one-flag"), true) - expect(t, ctx.IsSet("two-flag"), true) - expect(t, ctx.IsSet("three-flag"), true) - expect(t, ctx.IsSet("top-flag"), true) - expect(t, ctx.IsSet("bogus"), false) -} - -// XXX Corresponds to hack in context.IsSet for flags with EnvVar field -// Should be moved to `flag_test` in v2 -func TestContext_IsSet_fromEnv(t *testing.T) { - var ( - timeoutIsSet, tIsSet bool - noEnvVarIsSet, nIsSet bool - passwordIsSet, pIsSet bool - unparsableIsSet, uIsSet bool - ) - - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") - _ = os.Setenv("APP_PASSWORD", "") - cmd := &Command{ - Flags: []Flag{ - &FloatFlag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("APP_TIMEOUT_SECONDS")}, - &StringFlag{Name: "password", Aliases: []string{"p"}, Sources: EnvVars("APP_PASSWORD")}, - &FloatFlag{Name: "unparsable", Aliases: []string{"u"}, Sources: EnvVars("APP_UNPARSABLE")}, - &FloatFlag{Name: "no-env-var", Aliases: []string{"n"}}, - }, - Action: func(ctx *Context) error { - timeoutIsSet = ctx.IsSet("timeout") - tIsSet = ctx.IsSet("t") - passwordIsSet = ctx.IsSet("password") - pIsSet = ctx.IsSet("p") - unparsableIsSet = ctx.IsSet("unparsable") - uIsSet = ctx.IsSet("u") - noEnvVarIsSet = ctx.IsSet("no-env-var") - nIsSet = ctx.IsSet("n") - return nil - }, - } - - _ = cmd.Run(buildTestContext(t), []string{"run"}) - expect(t, timeoutIsSet, true) - expect(t, tIsSet, true) - expect(t, passwordIsSet, true) - expect(t, pIsSet, true) - expect(t, noEnvVarIsSet, false) - expect(t, nIsSet, false) - - t.Setenv("APP_UNPARSABLE", "foobar") - _ = cmd.Run(buildTestContext(t), []string{"run"}) - expect(t, unparsableIsSet, false) - expect(t, uIsSet, false) -} - -func TestContext_NumFlags(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("myflag", false, "doc") - set.String("otherflag", "hello world", "doc") - globalSet := flag.NewFlagSet("test", 0) - globalSet.Bool("myflagGlobal", true, "doc") - globalCtx := NewContext(nil, globalSet, nil) - c := NewContext(nil, set, globalCtx) - _ = set.Parse([]string{"--myflag", "--otherflag=foo"}) - _ = globalSet.Parse([]string{"--myflagGlobal"}) - expect(t, c.NumFlags(), 2) -} - -func TestContext_Set(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Int64("int", int64(5), "an int") - cCtx := NewContext(nil, set, nil) - - r := require.New(t) - - r.False(cCtx.IsSet("int")) - r.NoError(cCtx.Set("int", "1")) - r.Equal(int64(1), cCtx.Int("int")) - r.True(cCtx.IsSet("int")) -} - -func TestContext_Set_InvalidFlagAccessHandler(t *testing.T) { - set := flag.NewFlagSet("test", 0) - var flagName string - cmd := &Command{ - InvalidFlagAccessHandler: func(_ *Context, name string) { - flagName = name - }, - } - - c := NewContext(cmd, set, nil) - expect(t, c.Set("missing", "") != nil, true) - expect(t, flagName, "missing") -} - -func TestContext_LocalFlagNames(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("one-flag", false, "doc") - set.String("two-flag", "hello world", "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Bool("top-flag", true, "doc") - parentCtx := NewContext(nil, parentSet, nil) - ctx := NewContext(nil, set, parentCtx) - _ = set.Parse([]string{"--one-flag", "--two-flag=foo"}) - _ = parentSet.Parse([]string{"--top-flag"}) - - actualFlags := ctx.LocalFlagNames() - sort.Strings(actualFlags) - - expect(t, actualFlags, []string{"one-flag", "two-flag"}) -} - -func TestContext_FlagNames(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("one-flag", false, "doc") - set.String("two-flag", "hello world", "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Bool("top-flag", true, "doc") - parentCtx := NewContext(nil, parentSet, nil) - ctx := NewContext(nil, set, parentCtx) - _ = set.Parse([]string{"--one-flag", "--two-flag=foo"}) - _ = parentSet.Parse([]string{"--top-flag"}) - - actualFlags := ctx.FlagNames() - sort.Strings(actualFlags) - - expect(t, actualFlags, []string{"one-flag", "top-flag", "two-flag"}) -} - -func TestContext_Lineage(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("local-flag", false, "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Bool("top-flag", true, "doc") - parentCtx := NewContext(nil, parentSet, nil) - ctx := NewContext(nil, set, parentCtx) - _ = set.Parse([]string{"--local-flag"}) - _ = parentSet.Parse([]string{"--top-flag"}) - - lineage := ctx.Lineage() - expect(t, len(lineage), 2) - expect(t, lineage[0], ctx) - expect(t, lineage[1], parentCtx) -} - -func TestContext_lookupFlagSet(t *testing.T) { - set := flag.NewFlagSet("test", 0) - set.Bool("local-flag", false, "doc") - parentSet := flag.NewFlagSet("test", 0) - parentSet.Bool("top-flag", true, "doc") - parentCtx := NewContext(nil, parentSet, nil) - ctx := NewContext(nil, set, parentCtx) - _ = set.Parse([]string{"--local-flag"}) - _ = parentSet.Parse([]string{"--top-flag"}) - - fs := ctx.lookupFlagSet("top-flag") - expect(t, fs, parentCtx.flagSet) - - fs = ctx.lookupFlagSet("local-flag") - expect(t, fs, ctx.flagSet) - - if fs := ctx.lookupFlagSet("frob"); fs != nil { - t.Fail() - } -} - -func TestNonNilContext(t *testing.T) { - ctx := NewContext(nil, nil, nil) - if ctx.Context == nil { - t.Fatal("expected a non nil context when no parent is present") - } -} - -type testKey struct{} - -// TestContextPropagation tests that -// *cli.Context always has a valid -// context.Context -func TestContextPropagation(t *testing.T) { - parent := NewContext(nil, nil, nil) - parent.Context = context.WithValue(context.Background(), testKey{}, "val") - ctx := NewContext(nil, nil, parent) - val := ctx.Context.Value(testKey{}) - if val == nil { - t.Fatal("expected a parent context to be inherited but got nil") - } - valstr, _ := val.(string) - if valstr != "val" { - t.Fatalf("expected the context value to be %q but got %q", "val", valstr) - } - parent = NewContext(nil, nil, nil) - parent.Context = nil - ctx = NewContext(nil, nil, parent) - if ctx.Context == nil { - t.Fatal("expected context to not be nil even if the parent's context is nil") - } -} - -func TestContextAttributeAccessing(t *testing.T) { - tdata := []struct { - testCase string - setBoolInput string - ctxBoolInput string - newContextInput *Context - }{ - { - testCase: "empty", - setBoolInput: "", - ctxBoolInput: "", - newContextInput: nil, - }, - { - testCase: "empty_with_background_context", - setBoolInput: "", - ctxBoolInput: "", - newContextInput: &Context{Context: context.Background()}, - }, - { - testCase: "empty_set_bool_and_present_ctx_bool", - setBoolInput: "", - ctxBoolInput: "ctx-bool", - newContextInput: nil, - }, - { - testCase: "present_set_bool_and_present_ctx_bool_with_background_context", - setBoolInput: "", - ctxBoolInput: "ctx-bool", - newContextInput: &Context{Context: context.Background()}, - }, - { - testCase: "present_set_bool_and_present_ctx_bool", - setBoolInput: "ctx-bool", - ctxBoolInput: "ctx-bool", - newContextInput: nil, - }, - { - testCase: "present_set_bool_and_present_ctx_bool_with_background_context", - setBoolInput: "ctx-bool", - ctxBoolInput: "ctx-bool", - newContextInput: &Context{Context: context.Background()}, - }, - { - testCase: "present_set_bool_and_different_ctx_bool", - setBoolInput: "ctx-bool", - ctxBoolInput: "not-ctx-bool", - newContextInput: nil, - }, - { - testCase: "present_set_bool_and_different_ctx_bool_with_background_context", - setBoolInput: "ctx-bool", - ctxBoolInput: "not-ctx-bool", - newContextInput: &Context{Context: context.Background()}, - }, - } - - for _, test := range tdata { - t.Run(test.testCase, func(t *testing.T) { - // setup - set := flag.NewFlagSet("some-flag-set-name", 0) - set.Bool(test.setBoolInput, false, "usage documentation") - ctx := NewContext(nil, set, test.newContextInput) - - // logic under test - value := ctx.Bool(test.ctxBoolInput) - - // assertions - if value != false { - t.Errorf("expected \"value\" to be false, but it was not") - } - }) - } -} - -func TestCheckRequiredFlags(t *testing.T) { - tdata := []struct { - testCase string - parseInput []string - envVarInput [2]string - flags []Flag - expectedAnError bool - expectedErrorContents []string - }{ - { - testCase: "empty", - }, - { - testCase: "optional", - flags: []Flag{ - &StringFlag{Name: "optionalFlag"}, - }, - }, - { - testCase: "required", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true}, - }, - expectedAnError: true, - expectedErrorContents: []string{"requiredFlag"}, - }, - { - testCase: "required_and_present", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true}, - }, - parseInput: []string{"--requiredFlag", "myinput"}, - }, - { - testCase: "required_and_present_via_env_var", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true, Sources: EnvVars("REQUIRED_FLAG")}, - }, - envVarInput: [2]string{"REQUIRED_FLAG", "true"}, - }, - { - testCase: "required_and_optional", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true}, - &StringFlag{Name: "optionalFlag"}, - }, - expectedAnError: true, - }, - { - testCase: "required_and_optional_and_optional_present", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true}, - &StringFlag{Name: "optionalFlag"}, - }, - parseInput: []string{"--optionalFlag", "myinput"}, - expectedAnError: true, - }, - { - testCase: "required_and_optional_and_optional_present_via_env_var", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true}, - &StringFlag{Name: "optionalFlag", Sources: EnvVars("OPTIONAL_FLAG")}, - }, - envVarInput: [2]string{"OPTIONAL_FLAG", "true"}, - expectedAnError: true, - }, - { - testCase: "required_and_optional_and_required_present", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true}, - &StringFlag{Name: "optionalFlag"}, - }, - parseInput: []string{"--requiredFlag", "myinput"}, - }, - { - testCase: "two_required", - flags: []Flag{ - &StringFlag{Name: "requiredFlagOne", Required: true}, - &StringFlag{Name: "requiredFlagTwo", Required: true}, - }, - expectedAnError: true, - expectedErrorContents: []string{"requiredFlagOne", "requiredFlagTwo"}, - }, - { - testCase: "two_required_and_one_present", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true}, - &StringFlag{Name: "requiredFlagTwo", Required: true}, - }, - parseInput: []string{"--requiredFlag", "myinput"}, - expectedAnError: true, - }, - { - testCase: "two_required_and_both_present", - flags: []Flag{ - &StringFlag{Name: "requiredFlag", Required: true}, - &StringFlag{Name: "requiredFlagTwo", Required: true}, - }, - parseInput: []string{"--requiredFlag", "myinput", "--requiredFlagTwo", "myinput"}, - }, - { - testCase: "required_flag_with_short_name", - flags: []Flag{ - &StringSliceFlag{Name: "names", Aliases: []string{"N"}, Required: true}, - }, - parseInput: []string{"-N", "asd", "-N", "qwe"}, - }, - { - testCase: "required_flag_with_multiple_short_names", - flags: []Flag{ - &StringSliceFlag{Name: "names", Aliases: []string{"N", "n"}, Required: true}, - }, - parseInput: []string{"-n", "asd", "-n", "qwe"}, - }, - { - testCase: "required_flag_with_short_alias_not_printed_on_error", - expectedAnError: true, - expectedErrorContents: []string{"Required flag \"names\" not set"}, - flags: []Flag{ - &StringSliceFlag{Name: "names, n", Required: true}, - }, - }, - { - testCase: "required_flag_with_one_character", - expectedAnError: true, - expectedErrorContents: []string{"Required flag \"n\" not set"}, - flags: []Flag{ - &StringFlag{Name: "n", Required: true}, - }, - }, - } - - for _, test := range tdata { - t.Run(test.testCase, func(t *testing.T) { - // setup - if test.envVarInput[0] != "" { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv(test.envVarInput[0], test.envVarInput[1]) - } - - set := flag.NewFlagSet("test", 0) - for _, flags := range test.flags { - _ = flags.Apply(set) - } - _ = set.Parse(test.parseInput) - - c := &Context{} - ctx := NewContext(c.Command, set, c) - ctx.Command.Flags = test.flags - - // logic under test - err := ctx.checkRequiredFlags(test.flags) - - // assertions - if test.expectedAnError && err == nil { - t.Errorf("expected an error, but there was none") - } - if !test.expectedAnError && err != nil { - t.Errorf("did not expected an error, but there was one: %s", err) - } - for _, errString := range test.expectedErrorContents { - if err != nil { - if !strings.Contains(err.Error(), errString) { - t.Errorf("expected error %q to contain %q, but it didn't!", err.Error(), errString) - } - } - } - }) - } -} - -func TestContext_ParentContext_Set(t *testing.T) { - parentSet := flag.NewFlagSet("parent", flag.ContinueOnError) - parentSet.String("Name", "", "") - - context := NewContext( - nil, - flag.NewFlagSet("child", flag.ContinueOnError), - NewContext(nil, parentSet, nil), - ) - - err := context.Set("Name", "aaa") - if err != nil { - t.Errorf("expect nil. set parent context flag return err: %s", err) - } -} diff --git a/examples_test.go b/examples_test.go index 8503d99a89..7d668af774 100644 --- a/examples_test.go +++ b/examples_test.go @@ -17,8 +17,8 @@ func ExampleCommand_Run() { Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "pat", Usage: "a name to say"}, }, - Action: func(cCtx *cli.Context) error { - fmt.Printf("Hello %[1]v\n", cCtx.String("name")) + Action: func(_ context.Context, cmd *cli.Command) error { + fmt.Printf("Hello %[1]v\n", cmd.String("name")) return nil }, Authors: []any{ @@ -62,8 +62,8 @@ func ExampleCommand_Run_subcommand() { Usage: "Name of the person to greet", }, }, - Action: func(cCtx *cli.Context) error { - fmt.Println("Hello,", cCtx.String("name")) + Action: func(_ context.Context, cmd *cli.Command) error { + fmt.Println("Hello,", cmd.String("name")) return nil }, }, @@ -101,7 +101,7 @@ func ExampleCommand_Run_appHelp() { Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", - Action: func(*cli.Context) error { + Action: func(context.Context, *cli.Command) error { fmt.Printf("i like to describe things") return nil }, @@ -149,8 +149,8 @@ func ExampleCommand_Run_commandHelp() { Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "pat", Usage: "a name to say"}, }, - Action: func(cCtx *cli.Context) error { - fmt.Fprintf(cCtx.Command.Root().Writer, "hello to %[1]q\n", cCtx.String("name")) + Action: func(_ context.Context, cmd *cli.Command) error { + fmt.Fprintf(cmd.Root().Writer, "hello to %[1]q\n", cmd.String("name")) return nil }, Commands: []*cli.Command{ @@ -159,7 +159,7 @@ func ExampleCommand_Run_commandHelp() { Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", - Action: func(*cli.Context) error { + Action: func(context.Context, *cli.Command) error { fmt.Println("i like to describe things") return nil }, @@ -347,7 +347,7 @@ func ExampleCommand_Run_shellComplete_bash() { Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", - Action: func(*cli.Context) error { + Action: func(context.Context, *cli.Command) error { fmt.Printf("i like to describe things") return nil }, @@ -355,7 +355,7 @@ func ExampleCommand_Run_shellComplete_bash() { Name: "next", Usage: "next example", Description: "more stuff to see when generating shell completion", - Action: func(*cli.Context) error { + Action: func(context.Context, *cli.Command) error { fmt.Printf("the next example") return nil }, @@ -386,7 +386,7 @@ func ExampleCommand_Run_shellComplete_zsh() { Aliases: []string{"d"}, Usage: "use it to see a description", Description: "This is how we describe describeit the function", - Action: func(*cli.Context) error { + Action: func(context.Context, *cli.Command) error { fmt.Printf("i like to describe things") return nil }, @@ -394,7 +394,7 @@ func ExampleCommand_Run_shellComplete_zsh() { Name: "next", Usage: "next example", Description: "more stuff to see when generating bash completion", - Action: func(*cli.Context) error { + Action: func(context.Context, *cli.Command) error { fmt.Printf("the next example") return nil }, @@ -423,11 +423,11 @@ func ExampleCommand_Run_sliceValues() { &cli.FloatSliceFlag{Name: "float64Slice"}, &cli.IntSliceFlag{Name: "intSlice"}, }, - Action: func(cCtx *cli.Context) error { - for i, v := range cCtx.FlagNames() { - fmt.Printf("%d-%s %#v\n", i, v, cCtx.Value(v)) + Action: func(ctx context.Context, cmd *cli.Command) error { + for i, v := range cmd.FlagNames() { + fmt.Printf("%d-%s %#v\n", i, v, cmd.Value(v)) } - err := cCtx.Err() + err := ctx.Err() fmt.Println("error:", err) return err }, @@ -455,12 +455,12 @@ func ExampleCommand_Run_mapValues() { Flags: []cli.Flag{ &cli.StringMapFlag{Name: "stringMap"}, }, - Action: func(cCtx *cli.Context) error { - for i, v := range cCtx.FlagNames() { - fmt.Printf("%d-%s %#v\n", i, v, cCtx.StringMap(v)) + Action: func(ctx context.Context, cmd *cli.Command) error { + for i, v := range cmd.FlagNames() { + fmt.Printf("%d-%s %#v\n", i, v, cmd.StringMap(v)) } - fmt.Printf("notfound %#v\n", cCtx.StringMap("notfound")) - err := cCtx.Err() + fmt.Printf("notfound %#v\n", cmd.StringMap("notfound")) + err := ctx.Err() fmt.Println("error:", err) return err }, @@ -490,7 +490,7 @@ func ExampleBoolWithInverseFlag() { Flags: []cli.Flag{ flagWithInverse, }, - Action: func(ctx *cli.Context) error { + Action: func(_ context.Context, cmd *cli.Command) error { if flagWithInverse.IsSet() { if flagWithInverse.Value() { fmt.Println("env is set") @@ -525,8 +525,8 @@ func ExampleCommand_Suggest() { Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "squirrel", Usage: "a name to say"}, }, - Action: func(cCtx *cli.Context) error { - fmt.Printf("Hello %v\n", cCtx.String("name")) + Action: func(_ context.Context, cmd *cli.Command) error { + fmt.Printf("Hello %v\n", cmd.String("name")) return nil }, } @@ -549,8 +549,8 @@ func ExampleCommand_Suggest_command() { Flags: []cli.Flag{ &cli.StringFlag{Name: "name", Value: "squirrel", Usage: "a name to say"}, }, - Action: func(cCtx *cli.Context) error { - fmt.Printf("Hello %v\n", cCtx.String("name")) + Action: func(_ context.Context, cmd *cli.Command) error { + fmt.Printf("Hello %v\n", cmd.String("name")) return nil }, Commands: []*cli.Command{ @@ -563,8 +563,8 @@ func ExampleCommand_Suggest_command() { Flags: []cli.Flag{ &cli.BoolFlag{Name: "smiling"}, }, - Action: func(cCtx *cli.Context) error { - if cCtx.Bool("smiling") { + Action: func(_ context.Context, cmd *cli.Command) error { + if cmd.Bool("smiling") { fmt.Println("😀") } fmt.Println("Hello, neighbors") diff --git a/flag.go b/flag.go index 1ef0c10b03..0447851b16 100644 --- a/flag.go +++ b/flag.go @@ -1,6 +1,7 @@ package cli import ( + "context" "errors" "flag" "fmt" @@ -91,7 +92,7 @@ func (f FlagsByName) Swap(i, j int) { // ActionableFlag is an interface that wraps Flag interface and RunAction operation. type ActionableFlag interface { - RunAction(*Context) error + RunAction(context.Context, *Command) error } // Flag is a common interface related to parsing flags in cli. diff --git a/flag_bool.go b/flag_bool.go index b54aaccdb8..f2e6bbeacf 100644 --- a/flag_bool.go +++ b/flag_bool.go @@ -25,8 +25,8 @@ type boolValue struct { count *int } -func (cCtx *Context) Bool(name string) bool { - if v, ok := cCtx.Value(name).(bool); ok { +func (cmd *Command) Bool(name string) bool { + if v, ok := cmd.Value(name).(bool); ok { return v } return false diff --git a/flag_bool_with_inverse.go b/flag_bool_with_inverse.go index f19c021ed3..7b6e18c7a7 100644 --- a/flag_bool_with_inverse.go +++ b/flag_bool_with_inverse.go @@ -1,6 +1,7 @@ package cli import ( + "context" "flag" "fmt" "strings" @@ -40,20 +41,20 @@ func (s *BoolWithInverseFlag) Value() bool { return *s.posDest } -func (s *BoolWithInverseFlag) RunAction(ctx *Context) error { +func (s *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error { if *s.negDest && *s.posDest { return fmt.Errorf("cannot set both flags `--%s` and `--%s`", s.positiveFlag.Name, s.negativeFlag.Name) } if *s.negDest { - err := ctx.Set(s.positiveFlag.Name, "false") + err := cmd.Set(s.positiveFlag.Name, "false") if err != nil { return err } } if s.BoolFlag.Action != nil { - return s.BoolFlag.Action(ctx, s.Value()) + return s.BoolFlag.Action(ctx, cmd, s.Value()) } return nil diff --git a/flag_bool_with_inverse_test.go b/flag_bool_with_inverse_test.go index 12853e5589..8f6b94af1f 100644 --- a/flag_bool_with_inverse_test.go +++ b/flag_bool_with_inverse_test.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "strings" "testing" @@ -23,7 +24,7 @@ type boolWithInverseTestCase struct { func (tc *boolWithInverseTestCase) Run(t *testing.T, flagWithInverse *BoolWithInverseFlag) error { cmd := &Command{ Flags: []Flag{flagWithInverse}, - Action: func(ctx *Context) error { return nil }, + Action: func(context.Context, *Command) error { return nil }, } for key, val := range tc.envVars { @@ -115,12 +116,12 @@ func TestBoolWithInverseAction(t *testing.T) { Name: "env", // Setting env to the opposite to test flag Action is working as intended - Action: func(ctx *Context, value bool) error { + Action: func(_ context.Context, cmd *Command, value bool) error { if value { - return ctx.Set("env", "false") + return cmd.Set("env", "false") } - return ctx.Set("env", "true") + return cmd.Set("env", "true") }, }, } diff --git a/flag_duration.go b/flag_duration.go index ebcc86b896..b487cf94b6 100644 --- a/flag_duration.go +++ b/flag_duration.go @@ -36,8 +36,8 @@ func (d *durationValue) Get() any { return time.Duration(*d) } func (d *durationValue) String() string { return (*time.Duration)(d).String() } -func (cCtx *Context) Duration(name string) time.Duration { - if v, ok := cCtx.Value(name).(time.Duration); ok { +func (cmd *Command) Duration(name string) time.Duration { + if v, ok := cmd.Value(name).(time.Duration); ok { return v } return 0 diff --git a/flag_float.go b/flag_float.go index 4160b75f3d..413fdb9b8f 100644 --- a/flag_float.go +++ b/flag_float.go @@ -38,8 +38,8 @@ func (f *floatValue) String() string { return strconv.FormatFloat(float64(*f), ' // Int looks up the value of a local IntFlag, returns // 0 if not found -func (cCtx *Context) Float(name string) float64 { - if v, ok := cCtx.Value(name).(float64); ok { +func (cmd *Command) Float(name string) float64 { + if v, ok := cmd.Value(name).(float64); ok { return v } return 0 diff --git a/flag_float_slice.go b/flag_float_slice.go index 521da504fb..ac89bee2c4 100644 --- a/flag_float_slice.go +++ b/flag_float_slice.go @@ -11,8 +11,8 @@ var NewFloatSlice = NewSliceBase[float64, NoConfig, floatValue] // FloatSlice looks up the value of a local FloatSliceFlag, returns // nil if not found -func (cCtx *Context) FloatSlice(name string) []float64 { - if fs := cCtx.lookupFlagSet(name); fs != nil { +func (cmd *Command) FloatSlice(name string) []float64 { + if fs := cmd.lookupFlagSet(name); fs != nil { return lookupFloatSlice(name, fs) } return nil diff --git a/flag_impl.go b/flag_impl.go index b1663d4c13..74070e30a6 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -1,6 +1,7 @@ package cli import ( + "context" "flag" "fmt" "reflect" @@ -99,7 +100,7 @@ type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { TakesFile bool // whether this flag takes a file argument, mainly for shell completion purposes - Action func(*Context, T) error // Action callback to be called when flag is set + Action func(context.Context, *Command, T) error // Action callback to be called when flag is set Config C // Additional/Custom configuration associated with this flag type @@ -239,8 +240,8 @@ func (f *FlagBase[T, C, V]) GetDefaultText() string { } // Get returns the flag’s value in the given Context. -func (f *FlagBase[T, C, V]) Get(ctx *Context) T { - if v, ok := ctx.Value(f.Name).(T); ok { +func (f *FlagBase[T, C, V]) Get(cmd *Command) T { + if v, ok := cmd.Value(f.Name).(T); ok { return v } var t T @@ -248,9 +249,9 @@ func (f *FlagBase[T, C, V]) Get(ctx *Context) T { } // RunAction executes flag action if set -func (f *FlagBase[T, C, V]) RunAction(ctx *Context) error { +func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error { if f.Action != nil { - return f.Action(ctx, f.Get(ctx)) + return f.Action(ctx, cmd, f.Get(cmd)) } return nil diff --git a/flag_int.go b/flag_int.go index 585084d1bb..fa43a843e4 100644 --- a/flag_int.go +++ b/flag_int.go @@ -49,8 +49,8 @@ func (i *intValue) String() string { return strconv.FormatInt(int64(*i.val), 10) // Int64 looks up the value of a local Int64Flag, returns // 0 if not found -func (cCtx *Context) Int(name string) int64 { - if v, ok := cCtx.Value(name).(int64); ok { +func (cmd *Command) Int(name string) int64 { + if v, ok := cmd.Value(name).(int64); ok { return v } return 0 diff --git a/flag_int_slice.go b/flag_int_slice.go index bea6b8724c..fd4df82de6 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -9,8 +9,8 @@ var NewIntSlice = NewSliceBase[int64, IntegerConfig, intValue] // IntSlice looks up the value of a local IntSliceFlag, returns // nil if not found -func (cCtx *Context) IntSlice(name string) []int64 { - if fs := cCtx.lookupFlagSet(name); fs != nil { +func (cmd *Command) IntSlice(name string) []int64 { + if fs := cmd.lookupFlagSet(name); fs != nil { return lookupIntSlice(name, fs) } return nil diff --git a/flag_mutex.go b/flag_mutex.go index 9417da85d4..2ca38e71ce 100644 --- a/flag_mutex.go +++ b/flag_mutex.go @@ -13,14 +13,14 @@ type MutuallyExclusiveFlags struct { Required bool } -func (grp MutuallyExclusiveFlags) check(ctx *Context) error { +func (grp MutuallyExclusiveFlags) check(cmd *Command) error { oneSet := false e := &mutuallyExclusiveGroup{} for _, grpf := range grp.Flags { for _, f := range grpf { for _, name := range f.Names() { - if ctx.IsSet(name) { + if cmd.IsSet(name) { if oneSet { e.flag2Name = name return e diff --git a/flag_string.go b/flag_string.go index 1bc205cfb1..809063a9b3 100644 --- a/flag_string.go +++ b/flag_string.go @@ -55,8 +55,8 @@ func (s *stringValue) String() string { return "" } -func (cCtx *Context) String(name string) string { - if v, ok := cCtx.Value(name).(string); ok { +func (cmd *Command) String(name string) string { + if v, ok := cmd.Value(name).(string); ok { return v } return "" diff --git a/flag_string_map.go b/flag_string_map.go index f75ed37201..a2b3c9ce23 100644 --- a/flag_string_map.go +++ b/flag_string_map.go @@ -9,8 +9,8 @@ var NewStringMap = NewMapBase[string, StringConfig, stringValue] // StringMap looks up the value of a local StringMapFlag, returns // nil if not found -func (cCtx *Context) StringMap(name string) map[string]string { - if fs := cCtx.lookupFlagSet(name); fs != nil { +func (cmd *Command) StringMap(name string) map[string]string { + if fs := cmd.lookupFlagSet(name); fs != nil { return lookupStringMap(name, fs) } return nil diff --git a/flag_string_slice.go b/flag_string_slice.go index 19e159320b..9a80cb4fce 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -11,8 +11,8 @@ var NewStringSlice = NewSliceBase[string, StringConfig, stringValue] // StringSlice looks up the value of a local StringSliceFlag, returns // nil if not found -func (cCtx *Context) StringSlice(name string) []string { - if fs := cCtx.lookupFlagSet(name); fs != nil { +func (cmd *Command) StringSlice(name string) []string { + if fs := cmd.lookupFlagSet(name); fs != nil { return lookupStringSlice(name, fs) } return nil diff --git a/flag_test.go b/flag_test.go index 1f15b40bc3..2c62edf5ce 100644 --- a/flag_test.go +++ b/flag_test.go @@ -1,6 +1,7 @@ package cli import ( + "context" "flag" "fmt" "io" @@ -50,15 +51,17 @@ func TestBoolFlagApply_SetsAllNames(t *testing.T) { expect(t, v, true) } -func TestBoolFlagValueFromContext(t *testing.T) { +func TestBoolFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("trueflag", true, "doc") set.Bool("falseflag", false, "doc") - cCtx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} tf := &BoolFlag{Name: "trueflag"} ff := &BoolFlag{Name: "falseflag"} - expect(t, tf.Get(cCtx), true) - expect(t, ff.Get(cCtx), false) + + r := require.New(t) + r.True(tf.Get(cmd)) + r.False(ff.Get(cmd)) } func TestBoolFlagApply_SetsCount(t *testing.T) { @@ -75,7 +78,7 @@ func TestBoolFlagApply_SetsCount(t *testing.T) { expect(t, count, 3) } -func TestBoolFlagCountFromContext(t *testing.T) { +func TestBoolFlagCountFromCommand(t *testing.T) { boolCountTests := []struct { input []string expectedVal bool @@ -95,15 +98,15 @@ func TestBoolFlagCountFromContext(t *testing.T) { for _, bct := range boolCountTests { set := flag.NewFlagSet("test", 0) - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} tf := &BoolFlag{Name: "tf", Aliases: []string{"w", "huh"}} - err := tf.Apply(set) - expect(t, err, nil) + r := require.New(t) - err = set.Parse(bct.input) - expect(t, err, nil) - expect(t, tf.Get(ctx), bct.expectedVal) - expect(t, ctx.Count("tf"), bct.expectedCount) + r.NoError(tf.Apply(set)) + r.NoError(set.Parse(bct.input)) + + r.Equal(bct.expectedVal, tf.Get(cmd)) + r.Equal(bct.expectedCount, cmd.Count("tf")) } } @@ -371,10 +374,10 @@ func TestFlagsFromEnv(t *testing.T) { cmd := &Command{ Flags: []Flag{tc.fl}, - Action: func(ctx *Context) error { - r.Equal(ctx.Value(tc.fl.Names()[0]), tc.output) + Action: func(_ context.Context, cmd *Command) error { + r.Equal(cmd.Value(tc.fl.Names()[0]), tc.output) r.True(tc.fl.IsSet()) - r.Equal(ctx.FlagNames(), tc.fl.Names()) + r.Equal(cmd.FlagNames(), tc.fl.Names()) return nil }, @@ -658,12 +661,12 @@ func TestStringFlagApply_SetsAllNames(t *testing.T) { expect(t, v, "YUUUU") } -func TestStringFlagValueFromContext(t *testing.T) { +func TestStringFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.String("myflag", "foobar", "doc") - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &StringFlag{Name: "myflag"} - expect(t, f.Get(ctx), "foobar") + require.Equal(t, "foobar", f.Get(cmd)) } var _ = []struct { @@ -789,12 +792,12 @@ func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) { expect(t, defValue, dest) } -func TestStringSliceFlagValueFromContext(t *testing.T) { +func TestStringSliceFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Var(NewStringSlice("a", "b", "c"), "myflag", "doc") - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &StringSliceFlag{Name: "myflag"} - expect(t, f.Get(ctx), []string{"a", "b", "c"}) + require.Equal(t, []string{"a", "b", "c"}, f.Get(cmd)) } var intFlagTests = []struct { @@ -851,12 +854,12 @@ func TestIntFlagApply_SetsAllNames(t *testing.T) { r.Equal(int64(5), v) } -func TestIntFlagValueFromContext(t *testing.T) { +func TestIntFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Int64("myflag", int64(42), "doc") - cCtx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} fl := &IntFlag{Name: "myflag"} - require.Equal(t, int64(42), fl.Get(cCtx)) + require.Equal(t, int64(42), fl.Get(cmd)) } var uintFlagTests = []struct { @@ -902,12 +905,12 @@ func TestUintFlagWithEnvVarHelpOutput(t *testing.T) { } } -func TestUintFlagValueFromContext(t *testing.T) { +func TestUintFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Uint64("myflag", 42, "doc") - cCtx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} fl := &UintFlag{Name: "myflag"} - require.Equal(t, uint64(42), fl.Get(cCtx)) + require.Equal(t, uint64(42), fl.Get(cmd)) } var uint64FlagTests = []struct { @@ -953,12 +956,12 @@ func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) { } } -func TestUint64FlagValueFromContext(t *testing.T) { +func TestUint64FlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Uint64("myflag", 42, "doc") - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &UintFlag{Name: "myflag"} - expect(t, f.Get(ctx), uint64(42)) + require.Equal(t, uint64(42), f.Get(cmd)) } var durationFlagTests = []struct { @@ -1015,12 +1018,12 @@ func TestDurationFlagApply_SetsAllNames(t *testing.T) { expect(t, v, time.Hour*30) } -func TestDurationFlagValueFromContext(t *testing.T) { +func TestDurationFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Duration("myflag", 42*time.Second, "doc") - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &DurationFlag{Name: "myflag"} - expect(t, f.Get(ctx), 42*time.Second) + require.Equal(t, 42*time.Second, f.Get(cmd)) } var intSliceFlagTests = []struct { @@ -1116,8 +1119,8 @@ func TestIntSliceFlagApply_ParentContext(t *testing.T) { Commands: []*Command{ { Name: "child", - Action: func(ctx *Context) error { - require.Equalf(t, []int64{1, 2, 3}, ctx.IntSlice("numbers"), "child context unable to view parent flag") + Action: func(_ context.Context, cmd *Command) error { + require.Equalf(t, []int64{1, 2, 3}, cmd.IntSlice("numbers"), "child context unable to view parent flag") return nil }, @@ -1126,26 +1129,26 @@ func TestIntSliceFlagApply_ParentContext(t *testing.T) { }).Run(buildTestContext(t), []string{"run", "child"}) } -func TestIntSliceFlag_SetFromParentContext(t *testing.T) { +func TestIntSliceFlag_SetFromParentCommand(t *testing.T) { fl := &IntSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []int64{1, 2, 3, 4}} set := flag.NewFlagSet("test", 0) _ = fl.Apply(set) - cCtx := &Context{ - parent: &Context{ + cmd := &Command{ + parent: &Command{ flagSet: set, }, flagSet: flag.NewFlagSet("empty", 0), } - require.Equalf(t, []int64{1, 2, 3, 4}, cCtx.IntSlice("numbers"), "child context unable to view parent flag") + require.Equalf(t, []int64{1, 2, 3, 4}, cmd.IntSlice("numbers"), "child context unable to view parent flag") } -func TestIntSliceFlagValueFromContext(t *testing.T) { +func TestIntSliceFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Var(NewIntSlice(1, 2, 3), "myflag", "doc") - cCtx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &IntSliceFlag{Name: "myflag"} - require.Equal(t, f.Get(cCtx), []int64{1, 2, 3}) + require.Equal(t, []int64{1, 2, 3}, f.Get(cmd)) } var uintSliceFlagTests = []struct { @@ -1243,9 +1246,9 @@ func TestUintSliceFlagApply_ParentContext(t *testing.T) { Commands: []*Command{ { Name: "child", - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { require.Equalf( - t, []uint64{1, 2, 3}, ctx.UintSlice("numbers"), + t, []uint64{1, 2, 3}, cmd.UintSlice("numbers"), "child context unable to view parent flag", ) return nil @@ -1255,14 +1258,14 @@ func TestUintSliceFlagApply_ParentContext(t *testing.T) { }).Run(buildTestContext(t), []string{"run", "child"}) } -func TestUintSliceFlag_SetFromParentContext(t *testing.T) { +func TestUintSliceFlag_SetFromParentCommand(t *testing.T) { fl := &UintSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []uint64{1, 2, 3, 4}} set := flag.NewFlagSet("test", 0) r := require.New(t) r.NoError(fl.Apply(set)) - cCtx := &Context{ - parent: &Context{ + cmd := &Command{ + parent: &Command{ flagSet: set, }, flagSet: flag.NewFlagSet("empty", 0), @@ -1270,7 +1273,7 @@ func TestUintSliceFlag_SetFromParentContext(t *testing.T) { r.Equalf( []uint64{1, 2, 3, 4}, - cCtx.UintSlice("numbers"), + cmd.UintSlice("numbers"), "child context unable to view parent flag", ) } @@ -1280,14 +1283,15 @@ func TestUintSliceFlag_ReturnNil(t *testing.T) { set := flag.NewFlagSet("test", 0) r := require.New(t) r.NoError(fl.Apply(set)) - cCtx := &Context{ - parent: &Context{ + cmd := &Command{ + parent: &Command{ flagSet: set, }, flagSet: flag.NewFlagSet("empty", 0), } r.Equalf( - []uint64(nil), cCtx.UintSlice("numbers"), + []uint64(nil), + cmd.UintSlice("numbers"), "child context unable to view parent flag", ) } @@ -1383,7 +1387,7 @@ func TestUint64SliceFlagApply_DefaultValueWithDestination(t *testing.T) { expect(t, defValue, dest) } -func TestUint64SliceFlagApply_ParentContext(t *testing.T) { +func TestUint64SliceFlagApply_ParentCommand(t *testing.T) { _ = (&Command{ Flags: []Flag{ &UintSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []uint64{1, 2, 3}}, @@ -1391,9 +1395,9 @@ func TestUint64SliceFlagApply_ParentContext(t *testing.T) { Commands: []*Command{ { Name: "child", - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { require.Equalf( - t, []uint64{1, 2, 3}, ctx.UintSlice("numbers"), + t, []uint64{1, 2, 3}, cmd.UintSlice("numbers"), "child context unable to view parent flag", ) return nil @@ -1403,19 +1407,19 @@ func TestUint64SliceFlagApply_ParentContext(t *testing.T) { }).Run(buildTestContext(t), []string{"run", "child"}) } -func TestUint64SliceFlag_SetFromParentContext(t *testing.T) { +func TestUint64SliceFlag_SetFromParentCommand(t *testing.T) { fl := &UintSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []uint64{1, 2, 3, 4}} set := flag.NewFlagSet("test", 0) r := require.New(t) r.NoError(fl.Apply(set)) - cCtx := &Context{ - parent: &Context{ + cmd := &Command{ + parent: &Command{ flagSet: set, }, flagSet: flag.NewFlagSet("empty", 0), } r.Equalf( - []uint64{1, 2, 3, 4}, cCtx.UintSlice("numbers"), + []uint64{1, 2, 3, 4}, cmd.UintSlice("numbers"), "child context unable to view parent flag", ) } @@ -1425,14 +1429,14 @@ func TestUint64SliceFlag_ReturnNil(t *testing.T) { set := flag.NewFlagSet("test", 0) r := require.New(t) r.NoError(fl.Apply(set)) - cCtx := &Context{ - parent: &Context{ + cmd := &Command{ + parent: &Command{ flagSet: set, }, flagSet: flag.NewFlagSet("empty", 0), } r.Equalf( - []uint64(nil), cCtx.UintSlice("numbers"), + []uint64(nil), cmd.UintSlice("numbers"), "child context unable to view parent flag", ) } @@ -1483,12 +1487,12 @@ func TestFloat64FlagApply_SetsAllNames(t *testing.T) { expect(t, v, float64(43.33333)) } -func TestFloat64FlagValueFromContext(t *testing.T) { +func TestFloat64FlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Float64("myflag", 1.23, "doc") - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &FloatFlag{Name: "myflag"} - expect(t, f.Get(ctx), 1.23) + require.Equal(t, 1.23, f.Get(cmd)) } var float64SliceFlagTests = []struct { @@ -1582,15 +1586,15 @@ func TestFloat64SliceFlagApply_DefaultValueWithDestination(t *testing.T) { expect(t, defValue, dest) } -func TestFloat64SliceFlagValueFromContext(t *testing.T) { +func TestFloat64SliceFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Var(NewFloatSlice(1.23, 4.56), "myflag", "doc") - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &FloatSliceFlag{Name: "myflag"} - expect(t, f.Get(ctx), []float64{1.23, 4.56}) + require.Equal(t, []float64{1.23, 4.56}, f.Get(cmd)) } -func TestFloat64SliceFlagApply_ParentContext(t *testing.T) { +func TestFloat64SliceFlagApply_ParentCommand(t *testing.T) { _ = (&Command{ Flags: []Flag{ &FloatSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: []float64{1.0, 2.0, 3.0}}, @@ -1598,8 +1602,8 @@ func TestFloat64SliceFlagApply_ParentContext(t *testing.T) { Commands: []*Command{ { Name: "child", - Action: func(ctx *Context) error { - require.Equalf(t, []float64{1.0, 2.0, 3.0}, ctx.FloatSlice("numbers"), "child context unable to view parent flag") + Action: func(_ context.Context, cmd *Command) error { + require.Equalf(t, []float64{1.0, 2.0, 3.0}, cmd.FloatSlice("numbers"), "child context unable to view parent flag") return nil }, }, @@ -1612,11 +1616,11 @@ func TestParseMultiString(t *testing.T) { Flags: []Flag{ &StringFlag{Name: "serve", Aliases: []string{"s"}}, }, - Action: func(ctx *Context) error { - if ctx.String("serve") != "10" { + Action: func(_ context.Context, cmd *Command) error { + if cmd.String("serve") != "10" { t.Errorf("main name not set") } - if ctx.String("s") != "10" { + if cmd.String("s") != "10" { t.Errorf("short name not set") } return nil @@ -1633,7 +1637,7 @@ func TestParseDestinationString(t *testing.T) { Destination: &dest, }, }, - Action: func(*Context) error { + Action: func(context.Context, *Command) error { if dest != "10" { t.Errorf("expected destination String 10") } @@ -1649,11 +1653,11 @@ func TestParseMultiStringFromEnv(t *testing.T) { Flags: []Flag{ &StringFlag{Name: "count", Aliases: []string{"c"}, Sources: EnvVars("APP_COUNT")}, }, - Action: func(ctx *Context) error { - if ctx.String("count") != "20" { + Action: func(_ context.Context, cmd *Command) error { + if cmd.String("count") != "20" { t.Errorf("main name not set") } - if ctx.String("c") != "20" { + if cmd.String("c") != "20" { t.Errorf("short name not set") } return nil @@ -1662,19 +1666,17 @@ func TestParseMultiStringFromEnv(t *testing.T) { } func TestParseMultiStringFromEnvCascade(t *testing.T) { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_COUNT", "20") + t.Setenv("APP_COUNT", "20") _ = (&Command{ Flags: []Flag{ &StringFlag{Name: "count", Aliases: []string{"c"}, Sources: EnvVars("COMPAT_COUNT", "APP_COUNT")}, }, - Action: func(ctx *Context) error { - if ctx.String("count") != "20" { + Action: func(_ context.Context, cmd *Command) error { + if cmd.String("count") != "20" { t.Errorf("main name not set") } - if ctx.String("c") != "20" { + if cmd.String("c") != "20" { t.Errorf("short name not set") } return nil @@ -1687,13 +1689,13 @@ func TestParseMultiStringSlice(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []string{}}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { expected := []string{"10", "20"} - if !reflect.DeepEqual(ctx.StringSlice("serve"), expected) { - t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) + if !reflect.DeepEqual(cmd.StringSlice("serve"), expected) { + t.Errorf("main name not set: %v != %v", expected, cmd.StringSlice("serve")) } - if !reflect.DeepEqual(ctx.StringSlice("s"), expected) { - t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) + if !reflect.DeepEqual(cmd.StringSlice("s"), expected) { + t.Errorf("short name not set: %v != %v", expected, cmd.StringSlice("s")) } return nil }, @@ -1705,13 +1707,13 @@ func TestParseMultiStringSliceWithDefaults(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []string{"9", "2"}}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { expected := []string{"10", "20"} - if !reflect.DeepEqual(ctx.StringSlice("serve"), expected) { - t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) + if !reflect.DeepEqual(cmd.StringSlice("serve"), expected) { + t.Errorf("main name not set: %v != %v", expected, cmd.StringSlice("serve")) } - if !reflect.DeepEqual(ctx.StringSlice("s"), expected) { - t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) + if !reflect.DeepEqual(cmd.StringSlice("s"), expected) { + t.Errorf("short name not set: %v != %v", expected, cmd.StringSlice("s")) } return nil }, @@ -1725,13 +1727,13 @@ func TestParseMultiStringSliceWithDestination(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: &dest}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { expected := []string{"10", "20"} if !reflect.DeepEqual(dest, expected) { - t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) + t.Errorf("main name not set: %v != %v", expected, cmd.StringSlice("serve")) } if !reflect.DeepEqual(dest, expected) { - t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) + t.Errorf("short name not set: %v != %v", expected, cmd.StringSlice("s")) } return nil }, @@ -1746,13 +1748,13 @@ func TestParseMultiStringSliceWithDestinationAndEnv(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: &dest, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { expected := []string{"10", "20"} if !reflect.DeepEqual(dest, expected) { - t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) + t.Errorf("main name not set: %v != %v", expected, cmd.StringSlice("serve")) } if !reflect.DeepEqual(dest, expected) { - t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) + t.Errorf("short name not set: %v != %v", expected, cmd.StringSlice("s")) } return nil }, @@ -1769,13 +1771,13 @@ func TestParseMultiFloat64SliceWithDestinationAndEnv(t *testing.T) { Flags: []Flag{ &FloatSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: &dest, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { expected := []float64{10, 20} if !reflect.DeepEqual(dest, expected) { - t.Errorf("main name not set: %v != %v", expected, ctx.StringSlice("serve")) + t.Errorf("main name not set: %v != %v", expected, cmd.StringSlice("serve")) } if !reflect.DeepEqual(dest, expected) { - t.Errorf("short name not set: %v != %v", expected, ctx.StringSlice("s")) + t.Errorf("short name not set: %v != %v", expected, cmd.StringSlice("s")) } return nil }, @@ -1790,7 +1792,7 @@ func TestParseMultiIntSliceWithDestinationAndEnv(t *testing.T) { Flags: []Flag{ &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Destination: &dest, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { require.Equalf(t, []int64{10, 20}, dest, "main name not set") return nil @@ -1803,12 +1805,12 @@ func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []string{"9", "2"}}, }, - Action: func(ctx *Context) error { - if !reflect.DeepEqual(ctx.StringSlice("serve"), []string{"9", "2"}) { - t.Errorf("main name not set: %v", ctx.StringSlice("serve")) + Action: func(_ context.Context, cmd *Command) error { + if !reflect.DeepEqual(cmd.StringSlice("serve"), []string{"9", "2"}) { + t.Errorf("main name not set: %v", cmd.StringSlice("serve")) } - if !reflect.DeepEqual(ctx.StringSlice("s"), []string{"9", "2"}) { - t.Errorf("short name not set: %v", ctx.StringSlice("s")) + if !reflect.DeepEqual(cmd.StringSlice("s"), []string{"9", "2"}) { + t.Errorf("short name not set: %v", cmd.StringSlice("s")) } return nil }, @@ -1824,11 +1826,11 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []string{}, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(ctx *Context) error { - if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { + Action: func(_ context.Context, cmd *Command) error { + if !reflect.DeepEqual(cmd.StringSlice("intervals"), []string{"20", "30", "40"}) { t.Errorf("main name not set from env") } - if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { + if !reflect.DeepEqual(cmd.StringSlice("i"), []string{"20", "30", "40"}) { t.Errorf("short name not set from env") } return nil @@ -1845,11 +1847,11 @@ func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []string{"1", "2", "5"}, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(ctx *Context) error { - if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { + Action: func(_ context.Context, cmd *Command) error { + if !reflect.DeepEqual(cmd.StringSlice("intervals"), []string{"20", "30", "40"}) { t.Errorf("main name not set from env") } - if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { + if !reflect.DeepEqual(cmd.StringSlice("i"), []string{"20", "30", "40"}) { t.Errorf("short name not set from env") } return nil @@ -1866,11 +1868,11 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []string{}, Sources: EnvVars("COMPAT_INTERVALS", "APP_INTERVALS")}, }, - Action: func(ctx *Context) error { - if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { + Action: func(_ context.Context, cmd *Command) error { + if !reflect.DeepEqual(cmd.StringSlice("intervals"), []string{"20", "30", "40"}) { t.Errorf("main name not set from env") } - if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { + if !reflect.DeepEqual(cmd.StringSlice("i"), []string{"20", "30", "40"}) { t.Errorf("short name not set from env") } return nil @@ -1887,11 +1889,11 @@ func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []string{"1", "2", "5"}, Sources: EnvVars("COMPAT_INTERVALS", "APP_INTERVALS")}, }, - Action: func(ctx *Context) error { - if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { + Action: func(_ context.Context, cmd *Command) error { + if !reflect.DeepEqual(cmd.StringSlice("intervals"), []string{"20", "30", "40"}) { t.Errorf("main name not set from env") } - if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { + if !reflect.DeepEqual(cmd.StringSlice("i"), []string{"20", "30", "40"}) { t.Errorf("short name not set from env") } return nil @@ -1909,7 +1911,7 @@ func TestParseMultiStringSliceFromEnvWithDestination(t *testing.T) { Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Destination: &dest, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(*Context) error { + Action: func(context.Context, *Command) error { if !reflect.DeepEqual(dest, []string{"20", "30", "40"}) { t.Errorf("main name not set from env") } @@ -1926,11 +1928,11 @@ func TestParseMultiInt(t *testing.T) { Flags: []Flag{ &IntFlag{Name: "serve", Aliases: []string{"s"}}, }, - Action: func(ctx *Context) error { - if ctx.Int("serve") != 10 { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Int("serve") != 10 { t.Errorf("main name not set") } - if ctx.Int("s") != 10 { + if cmd.Int("s") != 10 { t.Errorf("short name not set") } return nil @@ -1947,7 +1949,7 @@ func TestParseDestinationInt(t *testing.T) { Destination: &dest, }, }, - Action: func(*Context) error { + Action: func(context.Context, *Command) error { if dest != 10 { t.Errorf("expected destination Int 10") } @@ -1964,11 +1966,11 @@ func TestParseMultiIntFromEnv(t *testing.T) { Flags: []Flag{ &IntFlag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("APP_TIMEOUT_SECONDS")}, }, - Action: func(ctx *Context) error { - if ctx.Int("timeout") != 10 { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Int("timeout") != 10 { t.Errorf("main name not set") } - if ctx.Int("t") != 10 { + if cmd.Int("t") != 10 { t.Errorf("short name not set") } return nil @@ -1977,18 +1979,16 @@ func TestParseMultiIntFromEnv(t *testing.T) { } func TestParseMultiIntFromEnvCascade(t *testing.T) { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_TIMEOUT_SECONDS", "10") + t.Setenv("APP_TIMEOUT_SECONDS", "10") _ = (&Command{ Flags: []Flag{ &IntFlag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("COMPAT_TIMEOUT_SECONDS", "APP_TIMEOUT_SECONDS")}, }, - Action: func(ctx *Context) error { - if ctx.Int("timeout") != 10 { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Int("timeout") != 10 { t.Errorf("main name not set") } - if ctx.Int("t") != 10 { + if cmd.Int("t") != 10 { t.Errorf("short name not set") } return nil @@ -2001,11 +2001,11 @@ func TestParseMultiIntSlice(t *testing.T) { Flags: []Flag{ &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []int64{}}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { r := require.New(t) - r.Equalf([]int64{10, 20}, ctx.IntSlice("serve"), "main name not set") - r.Equalf([]int64{10, 20}, ctx.IntSlice("s"), "short name not set") + r.Equalf([]int64{10, 20}, cmd.IntSlice("serve"), "main name not set") + r.Equalf([]int64{10, 20}, cmd.IntSlice("s"), "short name not set") return nil }, @@ -2017,11 +2017,11 @@ func TestParseMultiIntSliceWithDefaults(t *testing.T) { Flags: []Flag{ &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []int64{9, 2}}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { r := require.New(t) - r.Equalf([]int64{10, 20}, ctx.IntSlice("serve"), "main name not set") - r.Equalf([]int64{10, 20}, ctx.IntSlice("s"), "short name not set") + r.Equalf([]int64{10, 20}, cmd.IntSlice("serve"), "main name not set") + r.Equalf([]int64{10, 20}, cmd.IntSlice("s"), "short name not set") return nil }, @@ -2033,11 +2033,11 @@ func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) { Flags: []Flag{ &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: []int64{9, 2}}, }, - Action: func(ctx *Context) error { - if !reflect.DeepEqual(ctx.IntSlice("serve"), []int64{9, 2}) { + Action: func(_ context.Context, cmd *Command) error { + if !reflect.DeepEqual(cmd.IntSlice("serve"), []int64{9, 2}) { t.Errorf("main name not set") } - if !reflect.DeepEqual(ctx.IntSlice("s"), []int64{9, 2}) { + if !reflect.DeepEqual(cmd.IntSlice("s"), []int64{9, 2}) { t.Errorf("short name not set") } return nil @@ -2052,11 +2052,11 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) { Flags: []Flag{ &IntSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []int64{}, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { r := require.New(t) - r.Equalf([]int64{20, 30, 40}, ctx.IntSlice("intervals"), "main name not set from env") - r.Equalf([]int64{20, 30, 40}, ctx.IntSlice("i"), "short name not set from env") + r.Equalf([]int64{20, 30, 40}, cmd.IntSlice("intervals"), "main name not set from env") + r.Equalf([]int64{20, 30, 40}, cmd.IntSlice("i"), "short name not set from env") return nil }, @@ -2072,11 +2072,11 @@ func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) { Flags: []Flag{ &IntSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []int64{1, 2, 5}, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(ctx *Context) error { - if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int64{20, 30, 40}) { + Action: func(_ context.Context, cmd *Command) error { + if !reflect.DeepEqual(cmd.IntSlice("intervals"), []int64{20, 30, 40}) { t.Errorf("main name not set from env") } - if !reflect.DeepEqual(ctx.IntSlice("i"), []int64{20, 30, 40}) { + if !reflect.DeepEqual(cmd.IntSlice("i"), []int64{20, 30, 40}) { t.Errorf("short name not set from env") } return nil @@ -2091,11 +2091,11 @@ func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { Flags: []Flag{ &IntSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []int64{}, Sources: EnvVars("COMPAT_INTERVALS", "APP_INTERVALS")}, }, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { r := require.New(t) - r.Equalf([]int64{20, 30, 40}, ctx.IntSlice("intervals"), "main name not set from env") - r.Equalf([]int64{20, 30, 40}, ctx.IntSlice("i"), "short name not set from env") + r.Equalf([]int64{20, 30, 40}, cmd.IntSlice("intervals"), "main name not set from env") + r.Equalf([]int64{20, 30, 40}, cmd.IntSlice("i"), "short name not set from env") return nil }, @@ -2107,11 +2107,11 @@ func TestParseMultiFloat64(t *testing.T) { Flags: []Flag{ &FloatFlag{Name: "serve", Aliases: []string{"s"}}, }, - Action: func(ctx *Context) error { - if ctx.Float("serve") != 10.2 { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Float("serve") != 10.2 { t.Errorf("main name not set") } - if ctx.Float("s") != 10.2 { + if cmd.Float("s") != 10.2 { t.Errorf("short name not set") } return nil @@ -2128,7 +2128,7 @@ func TestParseDestinationFloat64(t *testing.T) { Destination: &dest, }, }, - Action: func(*Context) error { + Action: func(context.Context, *Command) error { if dest != 10.2 { t.Errorf("expected destination Float64 10.2") } @@ -2138,18 +2138,16 @@ func TestParseDestinationFloat64(t *testing.T) { } func TestParseMultiFloat64FromEnv(t *testing.T) { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + t.Setenv("APP_TIMEOUT_SECONDS", "15.5") _ = (&Command{ Flags: []Flag{ &FloatFlag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("APP_TIMEOUT_SECONDS")}, }, - Action: func(ctx *Context) error { - if ctx.Float("timeout") != 15.5 { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Float("timeout") != 15.5 { t.Errorf("main name not set") } - if ctx.Float("t") != 15.5 { + if cmd.Float("t") != 15.5 { t.Errorf("short name not set") } return nil @@ -2158,19 +2156,17 @@ func TestParseMultiFloat64FromEnv(t *testing.T) { } func TestParseMultiFloat64FromEnvCascade(t *testing.T) { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + t.Setenv("APP_TIMEOUT_SECONDS", "15.5") _ = (&Command{ Flags: []Flag{ &FloatFlag{Name: "timeout", Aliases: []string{"t"}, Sources: EnvVars("COMPAT_TIMEOUT_SECONDS", "APP_TIMEOUT_SECONDS")}, }, - Action: func(ctx *Context) error { - if ctx.Float("timeout") != 15.5 { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Float("timeout") != 15.5 { t.Errorf("main name not set") } - if ctx.Float("t") != 15.5 { + if cmd.Float("t") != 15.5 { t.Errorf("short name not set") } return nil @@ -2179,32 +2175,28 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) { } func TestParseMultiFloat64SliceFromEnv(t *testing.T) { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_INTERVALS", "0.1,-10.5") + t.Setenv("APP_INTERVALS", "0.1,-10.5") _ = (&Command{ Flags: []Flag{ &FloatSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []float64{}, Sources: EnvVars("APP_INTERVALS")}, }, - Action: func(cCtx *Context) error { - require.Equalf(t, []float64{0.1, -10.5}, cCtx.FloatSlice("intervals"), "main name not set from env") + Action: func(_ context.Context, cmd *Command) error { + require.Equalf(t, []float64{0.1, -10.5}, cmd.FloatSlice("intervals"), "main name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) } func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_INTERVALS", "0.1234,-10.5") + t.Setenv("APP_INTERVALS", "0.1234,-10.5") _ = (&Command{ Flags: []Flag{ &FloatSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: []float64{}, Sources: EnvVars("COMPAT_INTERVALS", "APP_INTERVALS")}, }, - Action: func(cCtx *Context) error { - require.Equalf(t, []float64{0.1234, -10.5}, cCtx.FloatSlice("intervals"), "main name not set from env") + Action: func(_ context.Context, cmd *Command) error { + require.Equalf(t, []float64{0.1234, -10.5}, cmd.FloatSlice("intervals"), "main name not set from env") return nil }, }).Run(buildTestContext(t), []string{"run"}) @@ -2215,11 +2207,11 @@ func TestParseMultiBool(t *testing.T) { Flags: []Flag{ &BoolFlag{Name: "serve", Aliases: []string{"s"}}, }, - Action: func(ctx *Context) error { - if ctx.Bool("serve") != true { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Bool("serve") != true { t.Errorf("main name not set") } - if ctx.Bool("s") != true { + if cmd.Bool("s") != true { t.Errorf("short name not set") } return nil @@ -2233,11 +2225,11 @@ func TestParseBoolShortOptionHandle(t *testing.T) { { Name: "foobar", UseShortOptionHandling: true, - Action: func(ctx *Context) error { - if ctx.Bool("serve") != true { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Bool("serve") != true { t.Errorf("main name not set") } - if ctx.Bool("option") != true { + if cmd.Bool("option") != true { t.Errorf("short name not set") } return nil @@ -2260,7 +2252,7 @@ func TestParseDestinationBool(t *testing.T) { Destination: &dest, }, }, - Action: func(*Context) error { + Action: func(context.Context, *Command) error { if dest != true { t.Errorf("expected destination Bool true") } @@ -2270,18 +2262,16 @@ func TestParseDestinationBool(t *testing.T) { } func TestParseMultiBoolFromEnv(t *testing.T) { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_DEBUG", "1") + t.Setenv("APP_DEBUG", "1") _ = (&Command{ Flags: []Flag{ &BoolFlag{Name: "debug", Aliases: []string{"d"}, Sources: EnvVars("APP_DEBUG")}, }, - Action: func(ctx *Context) error { - if ctx.Bool("debug") != true { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Bool("debug") != true { t.Errorf("main name not set from env") } - if ctx.Bool("d") != true { + if cmd.Bool("d") != true { t.Errorf("short name not set from env") } return nil @@ -2290,18 +2280,16 @@ func TestParseMultiBoolFromEnv(t *testing.T) { } func TestParseMultiBoolFromEnvCascade(t *testing.T) { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("APP_DEBUG", "1") + t.Setenv("APP_DEBUG", "1") _ = (&Command{ Flags: []Flag{ &BoolFlag{Name: "debug", Aliases: []string{"d"}, Sources: EnvVars("COMPAT_DEBUG", "APP_DEBUG")}, }, - Action: func(ctx *Context) error { - if ctx.Bool("debug") != true { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Bool("debug") != true { t.Errorf("main name not set from env") } - if ctx.Bool("d") != true { + if cmd.Bool("d") != true { t.Errorf("short name not set from env") } return nil @@ -2321,23 +2309,23 @@ func TestParseBoolFromEnv(t *testing.T) { } for _, test := range boolFlagTests { - defer resetEnv(os.Environ()) - os.Clearenv() - _ = os.Setenv("DEBUG", test.input) - _ = (&Command{ - Flags: []Flag{ - &BoolFlag{Name: "debug", Aliases: []string{"d"}, Sources: EnvVars("DEBUG")}, - }, - Action: func(ctx *Context) error { - if ctx.Bool("debug") != test.output { - t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, ctx.Bool("debug")) - } - if ctx.Bool("d") != test.output { - t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, ctx.Bool("d")) - } - return nil - }, - }).Run(buildTestContext(t), []string{"run"}) + t.Run(fmt.Sprintf("%[1]q %[2]v", test.input, test.output), func(t *testing.T) { + t.Setenv("DEBUG", test.input) + _ = (&Command{ + Flags: []Flag{ + &BoolFlag{Name: "debug", Aliases: []string{"d"}, Sources: EnvVars("DEBUG")}, + }, + Action: func(_ context.Context, cmd *Command) error { + if cmd.Bool("debug") != test.output { + t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, cmd.Bool("debug")) + } + if cmd.Bool("d") != test.output { + t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, cmd.Bool("d")) + } + return nil + }, + }).Run(buildTestContext(t), []string{"run"}) + }) } } @@ -2346,11 +2334,11 @@ func TestParseMultiBoolT(t *testing.T) { Flags: []Flag{ &BoolFlag{Name: "implode", Aliases: []string{"i"}, Value: true}, }, - Action: func(ctx *Context) error { - if ctx.Bool("implode") { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Bool("implode") { t.Errorf("main name not set") } - if ctx.Bool("i") { + if cmd.Bool("i") { t.Errorf("short name not set") } return nil @@ -2540,13 +2528,13 @@ func TestTimestampFlagApply_Timezoned(t *testing.T) { expect(t, set.Lookup("time").Value.(flag.Getter).Get(), expectedResult.In(pdt)) } -func TestTimestampFlagValueFromContext(t *testing.T) { +func TestTimestampFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) now := time.Now() set.Var(newTimestamp(now), "myflag", "doc") - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &TimestampFlag{Name: "myflag"} - expect(t, f.Get(ctx), now) + require.Equal(t, now, f.Get(cmd)) } type flagDefaultTestCase struct { @@ -2848,15 +2836,15 @@ func TestSliceShortOptionHandle(t *testing.T) { { Name: "foobar", UseShortOptionHandling: true, - Action: func(ctx *Context) error { + Action: func(_ context.Context, cmd *Command) error { wasCalled = true - if ctx.Bool("i") != true { + if cmd.Bool("i") != true { t.Error("bool i not set") } - if ctx.Bool("t") != true { + if cmd.Bool("t") != true { t.Error("bool i not set") } - ss := ctx.StringSlice("net") + ss := cmd.StringSlice("net") if !reflect.DeepEqual(ss, []string{"foo"}) { t.Errorf("Got different slice(%v) than expected", ss) } @@ -3003,12 +2991,12 @@ func TestStringMapFlagApply_DefaultValueWithDestination(t *testing.T) { expect(t, defValue, *fl.Destination) } -func TestStringMapFlagValueFromContext(t *testing.T) { +func TestStringMapFlagValueFromCommand(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Var(NewStringMap(map[string]string{"a": "b", "c": ""}), "myflag", "doc") - ctx := NewContext(nil, set, nil) + cmd := &Command{flagSet: set} f := &StringMapFlag{Name: "myflag"} - expect(t, f.Get(ctx), map[string]string{"a": "b", "c": ""}) + require.Equal(t, map[string]string{"a": "b", "c": ""}, f.Get(cmd)) } func TestStringMapFlagApply_Error(t *testing.T) { diff --git a/flag_timestamp.go b/flag_timestamp.go index fa67513a46..18e811c4cd 100644 --- a/flag_timestamp.go +++ b/flag_timestamp.go @@ -85,8 +85,8 @@ func (t *timestampValue) Get() any { } // Timestamp gets the timestamp from a flag name -func (cCtx *Context) Timestamp(name string) *time.Time { - if fs := cCtx.lookupFlagSet(name); fs != nil { +func (cmd *Command) Timestamp(name string) *time.Time { + if fs := cmd.lookupFlagSet(name); fs != nil { return lookupTimestamp(name, fs) } return nil diff --git a/flag_uint.go b/flag_uint.go index d5e46b4927..c1ddefdca8 100644 --- a/flag_uint.go +++ b/flag_uint.go @@ -44,8 +44,8 @@ func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i.val), // Uint looks up the value of a local Uint64Flag, returns // 0 if not found -func (cCtx *Context) Uint(name string) uint64 { - if v, ok := cCtx.Value(name).(uint64); ok { +func (cmd *Command) Uint(name string) uint64 { + if v, ok := cmd.Value(name).(uint64); ok { return v } return 0 diff --git a/flag_uint_slice.go b/flag_uint_slice.go index d36ef62572..817da8c32b 100644 --- a/flag_uint_slice.go +++ b/flag_uint_slice.go @@ -11,8 +11,8 @@ var NewUintSlice = NewSliceBase[uint64, IntegerConfig, uintValue] // UintSlice looks up the value of a local UintSliceFlag, returns // nil if not found -func (cCtx *Context) UintSlice(name string) []uint64 { - if fs := cCtx.lookupFlagSet(name); fs != nil { +func (cmd *Command) UintSlice(name string) []uint64 { + if fs := cmd.lookupFlagSet(name); fs != nil { return lookupUintSlice(name, fs) } return nil diff --git a/funcs.go b/funcs.go index 5fba4d8f46..9a7361d81c 100644 --- a/funcs.go +++ b/funcs.go @@ -1,35 +1,37 @@ package cli +import "context" + // ShellCompleteFunc is an action to execute when the shell completion flag is set -type ShellCompleteFunc func(*Context) +type ShellCompleteFunc func(context.Context, *Command) // BeforeFunc is an action that executes prior to any subcommands being run once // the context is ready. If a non-nil error is returned, no subcommands are // run. -type BeforeFunc func(*Context) error +type BeforeFunc func(context.Context, *Command) error // AfterFunc is an action that executes after any subcommands are run and have // finished. The AfterFunc is run even if Action() panics. -type AfterFunc func(*Context) error +type AfterFunc func(context.Context, *Command) error // ActionFunc is the action to execute when no subcommands are specified -type ActionFunc func(*Context) error +type ActionFunc func(context.Context, *Command) error // CommandNotFoundFunc is executed if the proper command cannot be found -type CommandNotFoundFunc func(*Context, string) +type CommandNotFoundFunc func(context.Context, *Command, string) // OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying // customized usage error messages. This function is able to replace the // original error messages. If this function is not set, the "Incorrect usage" // is displayed and the execution is interrupted. -type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error +type OnUsageErrorFunc func(ctx context.Context, cmd *Command, err error, isSubcommand bool) error // InvalidFlagAccessFunc is executed when an invalid flag is accessed from the context. -type InvalidFlagAccessFunc func(*Context, string) +type InvalidFlagAccessFunc func(context.Context, *Command, string) // ExitErrHandlerFunc is executed if provided in order to handle exitError values // returned by Actions and Before/After functions. -type ExitErrHandlerFunc func(cCtx *Context, err error) +type ExitErrHandlerFunc func(context.Context, *Command, error) // FlagStringFunc is used by the help generation to display a flag, which is // expected to be a single line. diff --git a/help.go b/help.go index e26cb02b00..9a6dc1629d 100644 --- a/help.go +++ b/help.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "io" "os" @@ -59,8 +60,8 @@ func buildHelpCommand(withAction bool) *Command { return cmd } -func helpCommandAction(cCtx *Context) error { - args := cCtx.Args() +func helpCommandAction(ctx context.Context, cmd *Command) error { + args := cmd.Args() firstArg := args.First() // This action can be triggered by a "default" action of a command @@ -78,79 +79,79 @@ func helpCommandAction(cCtx *Context) error { // to // $ app foo // which will then be handled as case 3 - if cCtx.parent != nil && (cCtx.Command.HasName(helpName) || cCtx.Command.HasName(helpAlias)) { - tracef("setting cCtx to cCtx.parentContext") - cCtx = cCtx.parent + if cmd.parent != nil && (cmd.HasName(helpName) || cmd.HasName(helpAlias)) { + tracef("setting cmd to cmd.parent") + cmd = cmd.parent } // Case 4. $ app help foo // foo is the command for which help needs to be shown if firstArg != "" { tracef("returning ShowCommandHelp with %[1]q", firstArg) - return ShowCommandHelp(cCtx, firstArg) + return ShowCommandHelp(ctx, cmd, firstArg) } // Case 1 & 2 // Special case when running help on main app itself as opposed to individual // commands/subcommands - if cCtx.parent.Command == nil { + if cmd.parent == nil { tracef("returning ShowAppHelp") - _ = ShowAppHelp(cCtx) + _ = ShowAppHelp(cmd) return nil } // Case 3, 5 - if (len(cCtx.Command.Commands) == 1 && !cCtx.Command.HideHelp) || - (len(cCtx.Command.Commands) == 0 && cCtx.Command.HideHelp) { + if (len(cmd.Commands) == 1 && !cmd.HideHelp) || + (len(cmd.Commands) == 0 && cmd.HideHelp) { - tmpl := cCtx.Command.CustomHelpTemplate + tmpl := cmd.CustomHelpTemplate if tmpl == "" { tmpl = CommandHelpTemplate } - tracef("running HelpPrinter with command %[1]q", cCtx.Command.Name) - HelpPrinter(cCtx.Command.Root().Writer, tmpl, cCtx.Command) + tracef("running HelpPrinter with command %[1]q", cmd.Name) + HelpPrinter(cmd.Root().Writer, tmpl, cmd.Command) return nil } tracef("running ShowSubcommandHelp") - return ShowSubcommandHelp(cCtx) + return ShowSubcommandHelp(cmd) } // ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. -func ShowAppHelpAndExit(c *Context, exitCode int) { - _ = ShowAppHelp(c) +func ShowAppHelpAndExit(cmd *Command, exitCode int) { + _ = ShowAppHelp(cmd) os.Exit(exitCode) } // ShowAppHelp is an action that displays the help. -func ShowAppHelp(cCtx *Context) error { - tmpl := cCtx.Command.CustomRootCommandHelpTemplate +func ShowAppHelp(cmd *Command) error { + tmpl := cmd.CustomRootCommandHelpTemplate if tmpl == "" { tracef("using RootCommandHelpTemplate") tmpl = RootCommandHelpTemplate } - if cCtx.Command.ExtraInfo == nil { - HelpPrinter(cCtx.Command.Root().Writer, tmpl, cCtx.Command.Root()) + if cmd.ExtraInfo == nil { + HelpPrinter(cmd.Root().Writer, tmpl, cmd.Root()) return nil } tracef("setting ExtraInfo in customAppData") customAppData := func() map[string]any { return map[string]any{ - "ExtraInfo": cCtx.Command.ExtraInfo, + "ExtraInfo": cmd.ExtraInfo, } } - HelpPrinterCustom(cCtx.Command.Root().Writer, tmpl, cCtx.Command.Root(), customAppData()) + HelpPrinterCustom(cmd.Root().Writer, tmpl, cmd.Root(), customAppData()) return nil } // DefaultAppComplete prints the list of subcommands as the default app completion method -func DefaultAppComplete(cCtx *Context) { - DefaultCompleteWithFlags(nil)(cCtx) +func DefaultAppComplete(ctx context.Context, cmd *Command) { + DefaultCompleteWithFlags(nil)(ctx, cmd) } func printCommandSuggestions(commands []*Command, writer io.Writer) { @@ -209,55 +210,55 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { // match if last argument matches this flag and it is not repeated if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) { flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) - _, _ = fmt.Fprintln(writer, flagCompletion) + fmt.Fprintln(writer, flagCompletion) } } } } -func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) { - return func(cCtx *Context) { +func DefaultCompleteWithFlags(cmd *Command) func(ctx context.Context, cmd *Command) { + return func(_ context.Context, cmd *Command) { if len(os.Args) > 2 { lastArg := os.Args[len(os.Args)-2] if strings.HasPrefix(lastArg, "-") { if cmd != nil { - printFlagSuggestions(lastArg, cmd.Flags, cCtx.Command.Root().Writer) + printFlagSuggestions(lastArg, cmd.Flags, cmd.Root().Writer) return } - printFlagSuggestions(lastArg, cCtx.Command.Flags, cCtx.Command.Root().Writer) + printFlagSuggestions(lastArg, cmd.Flags, cmd.Root().Writer) return } } if cmd != nil { - printCommandSuggestions(cmd.Commands, cCtx.Command.Root().Writer) + printCommandSuggestions(cmd.Commands, cmd.Root().Writer) return } - printCommandSuggestions(cCtx.Command.Commands, cCtx.Command.Root().Writer) + printCommandSuggestions(cmd.Commands, cmd.Root().Writer) } } // ShowCommandHelpAndExit - exits with code after showing help -func ShowCommandHelpAndExit(c *Context, command string, code int) { - _ = ShowCommandHelp(c, command) +func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) { + _ = ShowCommandHelp(ctx, cmd, command) os.Exit(code) } // ShowCommandHelp prints help for the given command -func ShowCommandHelp(cCtx *Context, commandName string) error { - for _, cmd := range cCtx.Command.Commands { - if !cmd.HasName(commandName) { +func ShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error { + for _, subCmd := range cmd.Commands { + if !subCmd.HasName(commandName) { continue } - tmpl := cmd.CustomHelpTemplate + tmpl := subCmd.CustomHelpTemplate if tmpl == "" { - if len(cmd.Commands) == 0 { + if len(subCmd.Commands) == 0 { tracef("using CommandHelpTemplate") tmpl = CommandHelpTemplate } else { @@ -267,7 +268,7 @@ func ShowCommandHelp(cCtx *Context, commandName string) error { } tracef("running HelpPrinter") - HelpPrinter(cCtx.Command.Root().Writer, tmpl, cmd) + HelpPrinter(cmd.Root().Writer, tmpl, subCmd) tracef("returning nil after printing help") return nil @@ -275,11 +276,11 @@ func ShowCommandHelp(cCtx *Context, commandName string) error { tracef("no matching command found") - if cCtx.Command.CommandNotFound == nil { + if cmd.CommandNotFound == nil { errMsg := fmt.Sprintf("No help topic for '%v'", commandName) - if cCtx.Command.Suggest { - if suggestion := SuggestCommand(cCtx.Command.Commands, commandName); suggestion != "" { + if cmd.Suggest { + if suggestion := SuggestCommand(cmd.Commands, commandName); suggestion != "" { errMsg += ". " + suggestion } } @@ -289,34 +290,34 @@ func ShowCommandHelp(cCtx *Context, commandName string) error { } tracef("running CommandNotFound func for %[1]q", commandName) - cCtx.Command.CommandNotFound(cCtx, commandName) + cmd.CommandNotFound(ctx, cmd, commandName) return nil } // ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code. -func ShowSubcommandHelpAndExit(c *Context, exitCode int) { - _ = ShowSubcommandHelp(c) +func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) { + _ = ShowSubcommandHelp(cmd) os.Exit(exitCode) } // ShowSubcommandHelp prints help for the given subcommand -func ShowSubcommandHelp(cCtx *Context) error { - if cCtx == nil { +func ShowSubcommandHelp(cmd *Command) error { + if cmd == nil { return nil } - HelpPrinter(cCtx.Command.Root().Writer, SubcommandHelpTemplate, cCtx.Command) + HelpPrinter(cmd.Root().Writer, SubcommandHelpTemplate, cmd) return nil } // ShowVersion prints the version number of the App -func ShowVersion(cCtx *Context) { - VersionPrinter(cCtx) +func ShowVersion(cmd *Command) { + VersionPrinter(cmd) } -func printVersion(cCtx *Context) { - _, _ = fmt.Fprintf(cCtx.Command.Root().Writer, "%v version %v\n", cCtx.Command.Name, cCtx.Command.Version) +func printVersion(cmd *Command) { + _, _ = fmt.Fprintf(cmd.Root().Writer, "%v version %v\n", cmd.Name, cmd.Version) } func handleTemplateError(err error) { @@ -418,20 +419,20 @@ func printHelp(out io.Writer, templ string, data interface{}) { HelpPrinterCustom(out, templ, data, nil) } -func checkVersion(cCtx *Context) bool { +func checkVersion(cmd *Command) bool { found := false for _, name := range VersionFlag.Names() { - if cCtx.Bool(name) { + if cmd.Bool(name) { found = true } } return found } -func checkHelp(cCtx *Context) bool { +func checkHelp(cmd *Command) bool { found := false for _, name := range HelpFlag.Names() { - if cCtx.Bool(name) { + if cmd.Bool(name) { found = true break } @@ -455,21 +456,21 @@ func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) { return true, arguments[:pos] } -func checkCompletions(cCtx *Context) bool { - if !cCtx.shellComplete { +func checkCompletions(ctx context.Context, cmd *Command) bool { + if !cmd.EnableShellCompletion { return false } - if args := cCtx.Args(); args.Present() { - name := args.First() - if cmd := cCtx.Command.Command(name); cmd != nil { + if argsArguments := cmd.Args(); argsArguments.Present() { + name := argsArguments.First() + if cmd := cmd.Command(name); cmd != nil { // let the command handle the completion return false } } - if cCtx.Command != nil && cCtx.Command.ShellComplete != nil { - cCtx.Command.ShellComplete(cCtx) + if cmd.ShellComplete != nil { + cmd.ShellComplete(ctx, cmd) } return true diff --git a/help_test.go b/help_test.go index 915bd63bd0..d3bee346e0 100644 --- a/help_test.go +++ b/help_test.go @@ -2,6 +2,7 @@ package cli import ( "bytes" + "context" "flag" "fmt" "io" @@ -16,10 +17,7 @@ import ( func Test_ShowAppHelp_NoAuthor(t *testing.T) { output := new(bytes.Buffer) cmd := &Command{Writer: output} - - c := NewContext(cmd, nil, nil) - - _ = ShowAppHelp(c) + _ = ShowAppHelp(cmd) if bytes.Contains(output.Bytes(), []byte("AUTHOR(S):")) { t.Errorf("expected\n%snot to include %s", output.String(), "AUTHOR(S):") @@ -32,9 +30,7 @@ func Test_ShowAppHelp_NoVersion(t *testing.T) { cmd.Version = "" - c := NewContext(cmd, nil, nil) - - _ = ShowAppHelp(c) + _ = ShowAppHelp(cmd) if bytes.Contains(output.Bytes(), []byte("VERSION:")) { t.Errorf("expected\n%snot to include %s", output.String(), "VERSION:") @@ -47,9 +43,7 @@ func Test_ShowAppHelp_HideVersion(t *testing.T) { cmd.HideVersion = true - c := NewContext(cmd, nil, nil) - - _ = ShowAppHelp(c) + _ = ShowAppHelp(cmd) if bytes.Contains(output.Bytes(), []byte("VERSION:")) { t.Errorf("expected\n%snot to include %s", output.String(), "VERSION:") @@ -63,9 +57,7 @@ func Test_ShowAppHelp_MultiLineDescription(t *testing.T) { cmd.HideVersion = true cmd.Description = "multi\n line" - c := NewContext(cmd, nil, nil) - - _ = ShowAppHelp(c) + _ = ShowAppHelp(cmd) if !bytes.Contains(output.Bytes(), []byte("DESCRIPTION:\n multi\n line")) { t.Errorf("expected\n%s\nto include\n%s", output.String(), "DESCRIPTION:\n multi\n line") @@ -90,8 +82,8 @@ func Test_Help_Custom_Flags(t *testing.T) { Flags: []Flag{ &BoolFlag{Name: "foo", Aliases: []string{"h"}}, }, - Action: func(ctx *Context) error { - if ctx.Bool("h") != true { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Bool("h") != true { t.Errorf("custom help flag not set") } return nil @@ -120,8 +112,8 @@ func Test_Version_Custom_Flags(t *testing.T) { Flags: []Flag{ &BoolFlag{Name: "foo", Aliases: []string{"v"}}, }, - Action: func(ctx *Context) error { - if ctx.Bool("v") != true { + Action: func(_ context.Context, cmd *Command) error { + if cmd.Bool("v") != true { t.Errorf("custom version flag not set") } return nil @@ -134,14 +126,13 @@ func Test_Version_Custom_Flags(t *testing.T) { } func Test_helpCommand_Action_ErrorIfNoTopic(t *testing.T) { - cmd := &Command{} - - set := flag.NewFlagSet("test", 0) - _ = set.Parse([]string{"foo"}) + cmd := &Command{ + flagSet: flag.NewFlagSet("test", 0), + } - c := NewContext(cmd, set, nil) + _ = cmd.flagSet.Parse([]string{"foo"}) - err := helpCommandAction(c) + err := helpCommandAction(context.Background(), cmd) if err == nil { t.Fatalf("expected error from helpCommandAction(), but got nil") @@ -220,9 +211,9 @@ func TestHelpCommand_FullName(t *testing.T) { }, } for _, tc := range testCases { - out := &bytes.Buffer{} - t.Run(tc.name, func(t *testing.T) { + out := &bytes.Buffer{} + if tc.skip { t.SkipNow() } @@ -275,14 +266,12 @@ func Test_helpCommand_HideHelpFlag(t *testing.T) { } func Test_helpSubcommand_Action_ErrorIfNoTopic(t *testing.T) { - cmd := &Command{} - - set := flag.NewFlagSet("test", 0) - _ = set.Parse([]string{"foo"}) - - c := NewContext(cmd, set, nil) + cmd := &Command{ + flagSet: flag.NewFlagSet("test", 0), + } + _ = cmd.flagSet.Parse([]string{"foo"}) - err := helpCommandAction(c) + err := helpCommandAction(context.Background(), cmd) if err == nil { t.Fatalf("expected error from helpCommandAction(), but got nil") @@ -310,7 +299,7 @@ func TestShowAppHelp_CommandAliases(t *testing.T) { { Name: "frobbly", Aliases: []string{"fr", "frob"}, - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, @@ -568,7 +557,7 @@ func TestShowCommandHelp_CommandAliases(t *testing.T) { { Name: "frobbly", Aliases: []string{"fr", "frob", "bork"}, - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, @@ -586,7 +575,7 @@ func TestShowSubcommandHelp_CommandAliases(t *testing.T) { { Name: "frobbly", Aliases: []string{"fr", "frob", "bork"}, - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, @@ -609,7 +598,7 @@ func TestShowCommandHelp_Customtemplate(t *testing.T) { Commands: []*Command{ { Name: "frobbly", - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, CustomHelpTemplate: `NAME: @@ -761,14 +750,14 @@ func TestShowAppHelp_HiddenCommand(t *testing.T) { Commands: []*Command{ { Name: "frobbly", - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, { Name: "secretfrob", Hidden: true, - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, @@ -932,14 +921,14 @@ func TestShowAppHelp_CustomAppTemplate(t *testing.T) { Commands: []*Command{ { Name: "frobbly", - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, { Name: "secretfrob", Hidden: true, - Action: func(ctx *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, @@ -1172,60 +1161,46 @@ func TestHideHelpCommand_WithSubcommands(t *testing.T) { } func TestDefaultCompleteWithFlags(t *testing.T) { - origEnv := os.Environ() origArgv := os.Args + t.Cleanup(func() { os.Args = origArgv }) - t.Cleanup(func() { - os.Args = origArgv - resetEnv(origEnv) - }) - - os.Setenv("SHELL", "bash") + t.Setenv("SHELL", "bash") for _, tc := range []struct { name string - c *Context cmd *Command argv []string expected string }{ { name: "empty", - c: &Context{Command: &Command{}}, cmd: &Command{}, argv: []string{"prog", "cmd"}, expected: "", }, { name: "typical-flag-suggestion", - c: &Context{Command: &Command{ - Name: "cmd", - Flags: []Flag{ - &BoolFlag{Name: "happiness"}, - &IntFlag{Name: "everybody-jump-on"}, - }, - Commands: []*Command{ - {Name: "putz"}, - }, - }}, cmd: &Command{ Flags: []Flag{ &BoolFlag{Name: "excitement"}, &StringFlag{Name: "hat-shape"}, }, + parent: &Command{ + Name: "cmd", + Flags: []Flag{ + &BoolFlag{Name: "happiness"}, + &IntFlag{Name: "everybody-jump-on"}, + }, + Commands: []*Command{ + {Name: "putz"}, + }, + }, }, argv: []string{"cmd", "--e", "--generate-shell-completion"}, expected: "--excitement\n", }, { name: "typical-command-suggestion", - c: &Context{Command: &Command{ - Name: "cmd", - Flags: []Flag{ - &BoolFlag{Name: "happiness"}, - &IntFlag{Name: "everybody-jump-on"}, - }, - }}, cmd: &Command{ Name: "putz", Commands: []*Command{ @@ -1235,19 +1210,19 @@ func TestDefaultCompleteWithFlags(t *testing.T) { &BoolFlag{Name: "excitement"}, &StringFlag{Name: "hat-shape"}, }, + parent: &Command{ + Name: "cmd", + Flags: []Flag{ + &BoolFlag{Name: "happiness"}, + &IntFlag{Name: "everybody-jump-on"}, + }, + }, }, argv: []string{"cmd", "--generate-shell-completion"}, expected: "futz\n", }, { name: "autocomplete-with-spaces", - c: &Context{Command: &Command{ - Name: "cmd", - Flags: []Flag{ - &BoolFlag{Name: "happiness"}, - &IntFlag{Name: "everybody-jump-on"}, - }, - }}, cmd: &Command{ Name: "putz", Commands: []*Command{ @@ -1257,6 +1232,13 @@ func TestDefaultCompleteWithFlags(t *testing.T) { &BoolFlag{Name: "excitement"}, &StringFlag{Name: "hat-shape"}, }, + parent: &Command{ + Name: "cmd", + Flags: []Flag{ + &BoolFlag{Name: "happiness"}, + &IntFlag{Name: "everybody-jump-on"}, + }, + }, }, argv: []string{"cmd", "--url", "http://localhost:8000", "h", "--generate-shell-completion"}, expected: "help\n", @@ -1264,11 +1246,12 @@ func TestDefaultCompleteWithFlags(t *testing.T) { } { t.Run(tc.name, func(ct *testing.T) { writer := &bytes.Buffer{} - tc.c.Command.Writer = writer + rootCmd := tc.cmd.Root() + rootCmd.Writer = writer os.Args = tc.argv f := DefaultCompleteWithFlags(tc.cmd) - f(tc.c) + f(context.Background(), tc.cmd) written := writer.String() @@ -1317,8 +1300,6 @@ Including newlines. And then another long line. Blah blah blah does anybody ever read these things?`, } - c := NewContext(cmd, nil, nil) - HelpPrinter = func(w io.Writer, templ string, data interface{}) { funcMap := map[string]interface{}{ "wrapAt": func() int { @@ -1329,7 +1310,7 @@ And then another long line. Blah blah blah does anybody ever read these things?` HelpPrinterCustom(w, templ, data, funcMap) } - _ = ShowAppHelp(c) + _ = ShowAppHelp(cmd) expected := `NAME: - here's a sample @@ -1397,16 +1378,14 @@ func TestWrappedCommandHelp(t *testing.T) { Usage: "add a task to the list", UsageText: "this is an even longer way of describing adding a task to the list", Description: "and a description long enough to wrap in this test case", - Action: func(c *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, }, } cmd.setupDefaults([]string{"cli.test"}) - - cCtx := NewContext(cmd, nil, nil) - cmd.setupCommandGraph(cCtx) + cmd.setupCommandGraph() HelpPrinter = func(w io.Writer, templ string, data interface{}) { funcMap := map[string]interface{}{ @@ -1420,7 +1399,7 @@ func TestWrappedCommandHelp(t *testing.T) { r := require.New(t) - r.NoError(ShowCommandHelp(cCtx, "add")) + r.NoError(ShowCommandHelp(context.Background(), cmd, "add")) r.Equal(`NAME: cli.test add - add a task to the list @@ -1465,7 +1444,7 @@ func TestWrappedSubcommandHelp(t *testing.T) { Usage: "add a task to the list", UsageText: "this is an even longer way of describing adding a task to the list", Description: "and a description long enough to wrap in this test case", - Action: func(c *Context) error { + Action: func(context.Context, *Command) error { return nil }, Commands: []*Command{ @@ -1473,7 +1452,7 @@ func TestWrappedSubcommandHelp(t *testing.T) { Name: "grok", Usage: "remove an existing template", UsageText: "longer usage text goes here, la la la, hopefully this is long enough to wrap even more", - Action: func(c *Context) error { + Action: func(context.Context, *Command) error { return nil }, }, @@ -1535,7 +1514,7 @@ func TestWrappedHelpSubcommand(t *testing.T) { Usage: "add a task to the list", UsageText: "this is an even longer way of describing adding a task to the list", Description: "and a description long enough to wrap in this test case", - Action: func(c *Context) error { + Action: func(context.Context, *Command) error { return nil }, Commands: []*Command{ @@ -1543,7 +1522,7 @@ func TestWrappedHelpSubcommand(t *testing.T) { Name: "grok", Usage: "remove an existing template", UsageText: "longer usage text goes here, la la la, hopefully this is long enough to wrap even more", - Action: func(c *Context) error { + Action: func(context.Context, *Command) error { return nil }, Flags: []Flag{ diff --git a/internal/build/build.go b/internal/build/build.go index 60017ca7e8..48b809be00 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -18,6 +18,7 @@ import ( "path/filepath" "runtime" "strings" + "time" "github.com/urfave/cli/v3" ) @@ -42,7 +43,10 @@ const ( func main() { top, err := func() (string, error) { - if v, err := sh("git", "rev-parse", "--show-toplevel"); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + if v, err := sh(ctx, "git", "rev-parse", "--show-toplevel"); err == nil { return strings.TrimSpace(v), nil } @@ -159,8 +163,8 @@ func main() { } } -func sh(exe string, args ...string) (string, error) { - cmd := exec.Command(exe, args...) +func sh(ctx context.Context, exe string, args ...string) (string, error) { + cmd := exec.CommandContext(ctx, exe, args...) cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr @@ -170,17 +174,17 @@ func sh(exe string, args ...string) (string, error) { } func topRunAction(arg string, args ...string) cli.ActionFunc { - return func(cCtx *cli.Context) error { - if err := os.Chdir(cCtx.String("top")); err != nil { + return func(ctx context.Context, cmd *cli.Command) error { + if err := os.Chdir(cmd.String("top")); err != nil { return err } - return runCmd(arg, args...) + return runCmd(ctx, arg, args...) } } -func runCmd(arg string, args ...string) error { - cmd := exec.Command(arg, args...) +func runCmd(ctx context.Context, arg string, args ...string) error { + cmd := exec.CommandContext(ctx, arg, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -227,14 +231,14 @@ func downloadFile(src, dest string, dirPerm, perm os.FileMode) error { return os.Chmod(dest, perm) } -func VetActionFunc(cCtx *cli.Context) error { - return runCmd("go", "vet", cCtx.String("top")+"/...") +func VetActionFunc(ctx context.Context, cmd *cli.Command) error { + return runCmd(ctx, "go", "vet", cmd.String("top")+"/...") } -func TestActionFunc(c *cli.Context) error { - tags := c.String("tags") +func TestActionFunc(ctx context.Context, cmd *cli.Command) error { + tags := cmd.String("tags") - for _, pkg := range c.StringSlice("packages") { + for _, pkg := range cmd.StringSlice("packages") { packageName := "github.com/urfave/cli/v3" if pkg != "cli" { @@ -255,12 +259,12 @@ func TestActionFunc(c *cli.Context) error { packageName, }...) - if err := runCmd("go", args...); err != nil { + if err := runCmd(ctx, "go", args...); err != nil { return err } } - return testCleanup(c.StringSlice("packages")) + return testCleanup(cmd.StringSlice("packages")) } func testCleanup(packages []string) error { @@ -288,8 +292,8 @@ func testCleanup(packages []string) error { return os.WriteFile("coverage.txt", out.Bytes(), 0644) } -func GfmrunActionFunc(cCtx *cli.Context) error { - top := cCtx.String("top") +func GfmrunActionFunc(ctx context.Context, cmd *cli.Command) error { + top := cmd.String("top") bash, err := exec.LookPath("bash") if err != nil { @@ -312,9 +316,9 @@ func GfmrunActionFunc(cCtx *cli.Context) error { return err } - fmt.Fprintf(cCtx.Command.ErrWriter, "# ---> workspace/TMPDIR is %q\n", tmpDir) + fmt.Fprintf(cmd.ErrWriter, "# ---> workspace/TMPDIR is %q\n", tmpDir) - if err := runCmd("go", "work", "init", top); err != nil { + if err := runCmd(ctx, "go", "work", "init", top); err != nil { return err } @@ -324,12 +328,12 @@ func GfmrunActionFunc(cCtx *cli.Context) error { return err } - dirPath := cCtx.Args().Get(0) + dirPath := cmd.Args().Get(0) if dirPath == "" { dirPath = "README.md" } - walk := cCtx.Bool("walk") + walk := cmd.Bool("walk") sources := []string{} if walk { @@ -392,7 +396,7 @@ func GfmrunActionFunc(cCtx *cli.Context) error { gfmArgs = append(gfmArgs, "--sources", src) } - if err := runCmd("gfmrun", gfmArgs...); err != nil { + if err := runCmd(ctx, "gfmrun", gfmArgs...); err != nil { return err } @@ -402,7 +406,7 @@ func GfmrunActionFunc(cCtx *cli.Context) error { // checkBinarySizeActionFunc checks the size of an example binary to ensure that we are keeping size down // this was originally inspired by https://github.com/urfave/cli/issues/1055, and followed up on as a part // of https://github.com/urfave/cli/issues/1057 -func checkBinarySizeActionFunc(c *cli.Context) (err error) { +func checkBinarySizeActionFunc(ctx context.Context, cmd *cli.Command) (err error) { const ( cliSourceFilePath = "./internal/example-cli/example-cli.go" cliBuiltFilePath = "./internal/example-cli/built-example" @@ -413,16 +417,16 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) { mbStringFormatter = "%.1fMB" ) - tags := c.String("tags") + tags := cmd.String("tags") // get cli example size - cliSize, err := getSize(cliSourceFilePath, cliBuiltFilePath, tags) + cliSize, err := getSize(ctx, cliSourceFilePath, cliBuiltFilePath, tags) if err != nil { return err } // get hello world size - helloSize, err := getSize(helloSourceFilePath, helloBuiltFilePath, tags) + helloSize, err := getSize(ctx, helloSourceFilePath, helloBuiltFilePath, tags) if err != nil { return err } @@ -484,10 +488,10 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) { return nil } -func GenerateActionFunc(cCtx *cli.Context) error { - top := cCtx.String("top") +func GenerateActionFunc(ctx context.Context, cmd *cli.Command) error { + top := cmd.String("top") - cliDocs, err := sh("go", "doc", "-all", top) + cliDocs, err := sh(ctx, "go", "doc", "-all", top) if err != nil { return err } @@ -499,25 +503,26 @@ func GenerateActionFunc(cCtx *cli.Context) error { ) } -func DiffCheckActionFunc(cCtx *cli.Context) error { - if err := os.Chdir(cCtx.String("top")); err != nil { +func DiffCheckActionFunc(ctx context.Context, cmd *cli.Command) error { + if err := os.Chdir(cmd.String("top")); err != nil { return err } - if err := runCmd("git", "diff", "--exit-code"); err != nil { + if err := runCmd(ctx, "git", "diff", "--exit-code"); err != nil { return err } - return runCmd("git", "diff", "--cached", "--exit-code") + return runCmd(ctx, "git", "diff", "--cached", "--exit-code") } -func EnsureGoimportsActionFunc(cCtx *cli.Context) error { - top := cCtx.String("top") +func EnsureGoimportsActionFunc(ctx context.Context, cmd *cli.Command) error { + top := cmd.String("top") if err := os.Chdir(top); err != nil { return err } if err := runCmd( + ctx, "goimports", "-d", filepath.Join(top, "internal/build/build.go"), @@ -527,18 +532,18 @@ func EnsureGoimportsActionFunc(cCtx *cli.Context) error { os.Setenv("GOBIN", filepath.Join(top, ".local/bin")) - return runCmd("go", "install", "golang.org/x/tools/cmd/goimports@latest") + return runCmd(ctx, "go", "install", "golang.org/x/tools/cmd/goimports@latest") } -func EnsureGfmrunActionFunc(cCtx *cli.Context) error { - top := cCtx.String("top") +func EnsureGfmrunActionFunc(ctx context.Context, cmd *cli.Command) error { + top := cmd.String("top") gfmrunExe := filepath.Join(top, ".local/bin/gfmrun") if err := os.Chdir(top); err != nil { return err } - if v, err := sh(gfmrunExe, "--version"); err == nil && strings.TrimSpace(v) == gfmrunVersion { + if v, err := sh(ctx, gfmrunExe, "--version"); err == nil && strings.TrimSpace(v) == gfmrunVersion { return nil } @@ -555,58 +560,59 @@ func EnsureGfmrunActionFunc(cCtx *cli.Context) error { return downloadFile(gfmrunURL.String(), gfmrunExe, 0755, 0755) } -func EnsureMkdocsActionFunc(cCtx *cli.Context) error { - if err := os.Chdir(cCtx.String("top")); err != nil { +func EnsureMkdocsActionFunc(ctx context.Context, cmd *cli.Command) error { + if err := os.Chdir(cmd.String("top")); err != nil { return err } - if err := runCmd("mkdocs", "--version"); err == nil { + if err := runCmd(ctx, "mkdocs", "--version"); err == nil { return nil } - if cCtx.Bool("upgrade-pip") { - if err := runCmd("pip", "install", "-U", "pip"); err != nil { + if cmd.Bool("upgrade-pip") { + if err := runCmd(ctx, "pip", "install", "-U", "pip"); err != nil { return err } } - return runCmd("pip", "install", "-r", "mkdocs-reqs.txt") + return runCmd(ctx, "pip", "install", "-r", "mkdocs-reqs.txt") } -func SetMkdocsRemoteActionFunc(cCtx *cli.Context) error { - ghToken := strings.TrimSpace(cCtx.String("github-token")) +func SetMkdocsRemoteActionFunc(ctx context.Context, cmd *cli.Command) error { + ghToken := strings.TrimSpace(cmd.String("github-token")) if ghToken == "" { return errors.New("empty github token") } - if err := os.Chdir(cCtx.String("top")); err != nil { + if err := os.Chdir(cmd.String("top")); err != nil { return err } - if err := runCmd("git", "remote", "rm", "origin"); err != nil { + if err := runCmd(ctx, "git", "remote", "rm", "origin"); err != nil { return err } return runCmd( + ctx, "git", "remote", "add", "origin", fmt.Sprintf("https://x-access-token:%[1]s@github.com/urfave/cli.git", ghToken), ) } -func LintActionFunc(cCtx *cli.Context) error { - top := cCtx.String("top") +func LintActionFunc(ctx context.Context, cmd *cli.Command) error { + top := cmd.String("top") if err := os.Chdir(top); err != nil { return err } - out, err := sh(filepath.Join(top, ".local/bin/goimports"), "-l", ".") + out, err := sh(ctx, filepath.Join(top, ".local/bin/goimports"), "-l", ".") if err != nil { return err } if strings.TrimSpace(out) != "" { - fmt.Fprintln(cCtx.Command.ErrWriter, "# ---> goimports -l is non-empty:") - fmt.Fprintln(cCtx.Command.ErrWriter, out) + fmt.Fprintln(cmd.ErrWriter, "# ---> goimports -l is non-empty:") + fmt.Fprintln(cmd.ErrWriter, out) return errors.New("goimports needed") } @@ -614,17 +620,18 @@ func LintActionFunc(cCtx *cli.Context) error { return nil } -func V3Diff(cCtx *cli.Context) error { - if err := os.Chdir(cCtx.String("top")); err != nil { +func V3Diff(ctx context.Context, cmd *cli.Command) error { + if err := os.Chdir(cmd.String("top")); err != nil { return err } err := runCmd( + ctx, "diff", "--ignore-all-space", "--minimal", "--color="+func() string { - if cCtx.Bool("color") { + if cmd.Bool("color") { return "always" } return "auto" @@ -644,7 +651,7 @@ func V3Diff(cCtx *cli.Context) error { return err } -func getSize(sourcePath, builtPath, tags string) (int64, error) { +func getSize(ctx context.Context, sourcePath, builtPath, tags string) (int64, error) { args := []string{"build"} if tags != "" { @@ -657,7 +664,7 @@ func getSize(sourcePath, builtPath, tags string) (int64, error) { sourcePath, }...) - if err := runCmd("go", args...); err != nil { + if err := runCmd(ctx, "go", args...); err != nil { fmt.Println("issue getting size for example binary") return 0, err } From e7d97913e0644a2e5bee745d1945f33dfd4e0c1d Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 26 Jun 2023 19:22:00 -0400 Subject: [PATCH 2/8] Correctly failing :tada: --- args.go | 29 ++--- command.go | 243 +++++++++++++++++++++++++++------------- command_test.go | 62 +++++----- flag.go | 4 +- flag_bool.go | 3 + godoc-current.txt | 199 +++++++++++++++----------------- help.go | 15 +-- testdata/godoc-v3.x.txt | 199 +++++++++++++++----------------- 8 files changed, 405 insertions(+), 349 deletions(-) diff --git a/args.go b/args.go index bd65c17bde..ee132bf89b 100644 --- a/args.go +++ b/args.go @@ -16,39 +16,42 @@ type Args interface { Slice() []string } -type args []string +type stringSliceArgs struct { + v []string +} -func (a *args) Get(n int) string { - if len(*a) > n { - return (*a)[n] +func (a *stringSliceArgs) Get(n int) string { + if len(a.v) > n { + return a.v[n] } return "" } -func (a *args) First() string { +func (a *stringSliceArgs) First() string { return a.Get(0) } -func (a *args) Tail() []string { +func (a *stringSliceArgs) Tail() []string { if a.Len() >= 2 { - tail := []string((*a)[1:]) + tail := a.v[1:] ret := make([]string, len(tail)) copy(ret, tail) return ret } + return []string{} } -func (a *args) Len() int { - return len(*a) +func (a *stringSliceArgs) Len() int { + return len(a.v) } -func (a *args) Present() bool { +func (a *stringSliceArgs) Present() bool { return a.Len() != 0 } -func (a *args) Slice() []string { - ret := make([]string, len(*a)) - copy(ret, *a) +func (a *stringSliceArgs) Slice() []string { + ret := make([]string, len(a.v)) + copy(ret, a.v) return ret } diff --git a/command.go b/command.go index bf61078cd1..7006192451 100644 --- a/command.go +++ b/command.go @@ -164,59 +164,60 @@ func (cmd *Command) Command(name string) *Command { return nil } -func (cmd *Command) setupDefaults(arguments []string) { +func (cmd *Command) setupDefaults(osArgs []string) { if cmd.didSetupDefaults { - tracef("already did setup") + tracef("already did setup (cmd=%[1]q)", cmd.Name) return } cmd.didSetupDefaults = true isRoot := cmd.parent == nil - tracef("isRoot? %[1]v", isRoot) + tracef("isRoot? %[1]v (cmd=%[2]q)", isRoot, cmd.Name) if cmd.ShellComplete == nil { - tracef("setting default ShellComplete") + tracef("setting default ShellComplete (cmd=%[1]q)", cmd.Name) cmd.ShellComplete = DefaultCompleteWithFlags(cmd) } if cmd.Name == "" && isRoot { - tracef("setting cmd.Name from first arg basename") - cmd.Name = filepath.Base(arguments[0]) + name := filepath.Base(osArgs[0]) + tracef("setting cmd.Name from first arg basename (cmd=%[1]q)", name) + cmd.Name = name } if cmd.Usage == "" && isRoot { - tracef("setting default Usage") + tracef("setting default Usage (cmd=%[1]q)", cmd.Name) cmd.Usage = "A new cli application" } if cmd.Version == "" { - tracef("setting HideVersion=true due to empty Version") + tracef("setting HideVersion=true due to empty Version (cmd=%[1]q)", cmd.Name) cmd.HideVersion = true } if cmd.Action == nil { - tracef("setting default Action as help command action") + tracef("setting default Action as help command action (cmd=%[1]q)", cmd.Name) cmd.Action = helpCommandAction } if cmd.Reader == nil { - tracef("setting default Reader as os.Stdin") + tracef("setting default Reader as os.Stdin (cmd=%[1]q)", cmd.Name) cmd.Reader = os.Stdin } if cmd.Writer == nil { - tracef("setting default Writer as os.Stdout") + tracef("setting default Writer as os.Stdout (cmd=%[1]q)", cmd.Name) cmd.Writer = os.Stdout } if cmd.ErrWriter == nil { - tracef("setting default ErrWriter as os.Stderr") + tracef("setting default ErrWriter as os.Stderr (cmd=%[1]q)", cmd.Name) cmd.ErrWriter = os.Stderr } if cmd.AllowExtFlags { - tracef("visiting all flags given AllowExtFlags=true") + tracef("visiting all flags given AllowExtFlags=true (cmd=%[1]q)", cmd.Name) // add global flags added by other packages flag.VisitAll(func(f *flag.Flag) { // skip test flags @@ -227,20 +228,19 @@ func (cmd *Command) setupDefaults(arguments []string) { } for _, subCmd := range cmd.Commands { - tracef("setting sub-command parent as self") + tracef("setting sub-command (cmd=%[1]q) parent as self (cmd=%[2]q)", subCmd.Name, cmd.Name) subCmd.parent = cmd } - tracef("ensuring help command and flag") cmd.ensureHelp() if !cmd.HideVersion && isRoot { - tracef("appending version flag") + tracef("appending version flag (cmd=%[1]q)", cmd.Name) cmd.appendFlag(VersionFlag) } if cmd.PrefixMatchCommands && cmd.SuggestCommandFunc == nil { - tracef("setting default SuggestCommandFunc") + tracef("setting default SuggestCommandFunc (cmd=%[1]q)", cmd.Name) cmd.SuggestCommandFunc = suggestCommand } @@ -248,42 +248,48 @@ func (cmd *Command) setupDefaults(arguments []string) { completionCommand := buildCompletionCommand() if cmd.ShellCompletionCommandName != "" { - tracef("setting completion command name from ShellCompletionCommandName") + tracef( + "setting completion command name (%[1]q) from "+ + "cmd.ShellCompletionCommandName (cmd=%[2]q)", + cmd.ShellCompletionCommandName, cmd.Name, + ) completionCommand.Name = cmd.ShellCompletionCommandName } - tracef("appending completionCommand") + tracef("appending completionCommand (cmd=%[1]q)", cmd.Name) cmd.appendCommand(completionCommand) } - tracef("setting command categories") + tracef("setting command categories (cmd=%[1]q)", cmd.Name) cmd.categories = newCommandCategories() for _, subCmd := range cmd.Commands { cmd.categories.AddCommand(subCmd.Category, subCmd) } - tracef("sorting command categories") + tracef("sorting command categories (cmd=%[1]q)", cmd.Name) sort.Sort(cmd.categories.(*commandCategories)) - tracef("setting flag categories") + tracef("setting flag categories (cmd=%[1]q)", cmd.Name) cmd.flagCategories = newFlagCategoriesFromFlags(cmd.Flags) if cmd.Metadata == nil { - tracef("setting default Metadata") + tracef("setting default Metadata (cmd=%[1]q)", cmd.Name) cmd.Metadata = map[string]any{} } if len(cmd.SliceFlagSeparator) != 0 { - tracef("setting defaultSliceFlagSeparator from cmd.SliceFlagSeparator") + tracef("setting defaultSliceFlagSeparator from cmd.SliceFlagSeparator (cmd=%[1]q)", cmd.Name) defaultSliceFlagSeparator = cmd.SliceFlagSeparator } - tracef("setting disableSliceFlagSeparator from cmd.DisableSliceFlagSeparator") + tracef("setting disableSliceFlagSeparator from cmd.DisableSliceFlagSeparator (cmd=%[1]q)", cmd.Name) disableSliceFlagSeparator = cmd.DisableSliceFlagSeparator } func (cmd *Command) setupCommandGraph() { + tracef("setting up command graph (cmd=%[1]q)", cmd.Name) + for _, subCmd := range cmd.Commands { subCmd.parent = cmd subCmd.setupSubcommand() @@ -292,34 +298,38 @@ func (cmd *Command) setupCommandGraph() { } func (cmd *Command) setupSubcommand() { + tracef("setting up self as sub-command (cmd=%[1]q)", cmd.Name) + cmd.ensureHelp() - tracef("setting command categories") + tracef("setting command categories (cmd=%[1]q)", cmd.Name) cmd.categories = newCommandCategories() for _, subCmd := range cmd.Commands { cmd.categories.AddCommand(subCmd.Category, subCmd) } - tracef("sorting command categories") + tracef("sorting command categories (cmd=%[1]q)", cmd.Name) sort.Sort(cmd.categories.(*commandCategories)) - tracef("setting flag categories") + tracef("setting flag categories (cmd=%[1]q)", cmd.Name) cmd.flagCategories = newFlagCategoriesFromFlags(cmd.Flags) } func (cmd *Command) ensureHelp() { + tracef("ensuring help (cmd=%[1]q)", cmd.Name) + helpCommand := buildHelpCommand(true) if cmd.Command(helpCommand.Name) == nil && !cmd.HideHelp { if !cmd.HideHelpCommand { - tracef("appending helpCommand") + tracef("appending helpCommand (cmd=%[1]q)", cmd.Name) cmd.appendCommand(helpCommand) } } if HelpFlag != nil && !cmd.HideHelp { - tracef("appending HelpFlag") + tracef("appending HelpFlag (cmd=%[1]q)", cmd.Name) cmd.appendFlag(HelpFlag) } } @@ -327,10 +337,12 @@ func (cmd *Command) ensureHelp() { // Run is the entry point to the command graph. The positional // arguments are parsed according to the Flag and Command // definitions and the matching Action functions are run. -func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error) { - cmd.setupDefaults(arguments) +func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) { + tracef("running with arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name) + cmd.setupDefaults(osArgs) if v, ok := ctx.Value(commandContextKey).(*Command); ok { + tracef("setting parent (cmd=%[1]q) command from context.Context value (cmd=%[2]q)", v.Name, cmd.Name) cmd.parent = v } @@ -340,25 +352,35 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error // flag name as the value of the flag before it which is undesirable // note that we can only do this because the shell autocomplete function // always appends the completion flag at the end of the command - enableShellCompletion, arguments := checkShellCompleteFlag(cmd, arguments) + enableShellCompletion, osArgs := checkShellCompleteFlag(cmd, osArgs) + + tracef("setting cmd.EnableShellCompletion=%[1]v from checkShellCompleteFlag (cmd=%[2]q)", enableShellCompletion, cmd.Name) cmd.EnableShellCompletion = enableShellCompletion + tracef("using post-checkShellCompleteFlag arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name) + + tracef("setting self as cmd in context (cmd=%[1]q)", cmd.Name) ctx = context.WithValue(ctx, commandContextKey, cmd) if cmd.parent == nil { cmd.setupCommandGraph() } - argsArguments := args(arguments) - set, err := cmd.parseFlags(&argsArguments) + // TODO: this is some convoluted yikes { + var args Args = &stringSliceArgs{v: osArgs} + set, err := cmd.parseFlags(args) cmd.flagSet = set + args = cmd.Args() + // } + + tracef("using post-parse arguments %[1]q (cmd=%[2]q)", args, cmd.Name) if checkCompletions(ctx, cmd) { return nil } if err != nil { - tracef("setting deferErr from %[1]v", err) + tracef("setting deferErr from %[1]v (cmd=%[2]q)", err, cmd.Name) deferErr = err cmd.isInError = true @@ -377,7 +399,7 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error if cmd.parent == nil { tracef("running ShowAppHelp") if err := ShowAppHelp(cmd); err != nil { - tracef("SILENTLY IGNORING ERROR running ShowAppHelp %[1]v", err) + tracef("SILENTLY IGNORING ERROR running ShowAppHelp %[1]v (cmd=%[2]q)", err, cmd.Name) } } else { tracef("running ShowCommandHelp with %[1]q", cmd.Name) @@ -390,8 +412,10 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error return err } - if checkHelp(cmd) { + if cmd.checkHelp() { return helpCommandAction(ctx, cmd) + } else { + tracef("no help is wanted (cmd=%[1]q)", cmd.Name) } if cmd.parent == nil && !cmd.HideVersion && checkVersion(cmd) { @@ -433,13 +457,21 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error } } + tracef("running flag actions (cmd=%[1]q)", cmd.Name) + if err := runFlagActions(ctx, cmd, cmd.appliedFlags); err != nil { return err } var subCmd *Command - if argsArguments.Present() { - name := argsArguments.First() + + if args.Present() { + tracef("checking positional args %[1]q (cmd=%[2]q)", args, cmd.Name) + + name := args.First() + + tracef("using first positional argument as sub-command name=%[1]q (cmd=%[2]q)", name, cmd.Name) + if cmd.SuggestCommandFunc != nil { name = cmd.SuggestCommandFunc(cmd.Commands, name) } @@ -465,19 +497,22 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error } if isFlagName || (hasDefault && (defaultHasSubcommands && isDefaultSubcommand)) { - argsWithDefault := cmd.argsWithDefaultCommand(&argsArguments) - if !reflect.DeepEqual(argsArguments, argsWithDefault) { + argsWithDefault := cmd.argsWithDefaultCommand(args) + if !reflect.DeepEqual(args, argsWithDefault) { subCmd = cmd.Command(argsWithDefault.First()) } } } } else if cmd.parent == nil && cmd.DefaultCommand != "" { + tracef("no positional args present; checking default command %[1]q (cmd=%[2]q)", cmd.DefaultCommand, cmd.Name) + if dc := cmd.Command(cmd.DefaultCommand); dc != cmd { subCmd = dc } } if subCmd != nil { + tracef("running sub-command %[1]q with arguments %[2]q (cmd=%[3]q)", subCmd.Name, cmd.Args(), cmd.Name) return subCmd.Run(ctx, cmd.Args().Slice()) } @@ -486,17 +521,29 @@ func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error } if err := cmd.Action(ctx, cmd); err != nil { - tracef("calling handleExitCoder with %[1]v", err) + tracef("calling handleExitCoder with %[1]v (cmd=%[2]q)", err, cmd.Name) deferErr = cmd.handleExitCoder(ctx, err) } - tracef("returning deferErr") + tracef("returning deferErr (cmd=%[1]q)", cmd.Name) return deferErr } +func (cmd *Command) checkHelp() bool { + tracef("checking if help is wanted (cmd=%[1]q)", cmd.Name) + + for _, name := range HelpFlag.Names() { + if cmd.Bool(name) { + return true + } + } + + return false +} + func (cmd *Command) newFlagSet() (*flag.FlagSet, error) { cmd.appliedFlags = append(cmd.appliedFlags, cmd.allFlags()...) - return flagSet(cmd.Name, cmd.allFlags()) + return newFlagSet(cmd.Name, cmd.allFlags()) } func (cmd *Command) allFlags() []Flag { @@ -540,14 +587,17 @@ func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil } -func (cmd *Command) parseFlags(argsArguments Args) (*flag.FlagSet, error) { +func (cmd *Command) parseFlags(args Args) (*flag.FlagSet, error) { + tracef("parsing flags from arguments %[1]q (cmd=%[2]q)", args, cmd.Name) + set, err := cmd.newFlagSet() if err != nil { return nil, err } if cmd.SkipFlagParsing { - return set, set.Parse(append([]string{"--"}, argsArguments.Tail()...)) + tracef("skipping flag parsing (cmd=%[1]q)", cmd.Name) + return set, set.Parse(append([]string{"--"}, args.Tail()...)) } for pCmd := cmd.parent; pCmd != nil; pCmd = pCmd.parent { @@ -579,7 +629,7 @@ func (cmd *Command) parseFlags(argsArguments Args) (*flag.FlagSet, error) { } } - if err := parseIter(set, cmd, argsArguments.Tail(), cmd.EnableShellCompletion); err != nil { + if err := parseIter(set, cmd, args.Tail(), cmd.EnableShellCompletion); err != nil { return nil, err } @@ -677,9 +727,9 @@ func (cmd *Command) handleExitCoder(ctx context.Context, err error) error { func (cmd *Command) argsWithDefaultCommand(oldArgs Args) Args { if cmd.DefaultCommand != "" { rawArgs := append([]string{cmd.DefaultCommand}, oldArgs.Slice()...) - newArgs := args(rawArgs) + newArgs := &stringSliceArgs{v: rawArgs} - return &newArgs + return newArgs } return oldArgs @@ -699,12 +749,14 @@ func (cmd *Command) lookupFlag(name string) Flag { for _, f := range pCmd.Flags { for _, n := range f.Names() { if n == name { + tracef("flag found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return f } } } } + tracef("flag NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } @@ -713,20 +765,27 @@ func (cmd *Command) lookupFlagSet(name string) *flag.FlagSet { if pCmd.flagSet == nil { continue } + if f := pCmd.flagSet.Lookup(name); f != nil { + tracef("matching flag set found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return pCmd.flagSet } } + + tracef("matching flag set NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) cmd.onInvalidFlag(context.TODO(), name) return nil } func (cmd *Command) checkRequiredFlags() requiredFlagsErr { - var missingFlags []string + tracef("checking for required flags (cmd=%[1]q)", cmd.Name) + + missingFlags := []string{} + for _, f := range cmd.Flags { if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { - var flagPresent bool - var flagName string + flagPresent := false + flagName := "" for _, key := range f.Names() { flagName = key @@ -743,9 +802,13 @@ func (cmd *Command) checkRequiredFlags() requiredFlagsErr { } if len(missingFlags) != 0 { + tracef("found missing required flags %[1]q (cmd=%[2]q)", missingFlags, cmd.Name) + return &errRequiredFlags{missingFlags: missingFlags} } + tracef("all required flags set (cmd=%[1]q)", cmd.Name) + return nil } @@ -775,33 +838,48 @@ func (cmd *Command) Set(name, value string) error { // IsSet determines if the flag was actually set func (cmd *Command) IsSet(name string) bool { - if fs := cmd.lookupFlagSet(name); fs != nil { - isSet := false - fs.Visit(func(f *flag.Flag) { - if f.Name == name { - isSet = true - } - }) - if isSet { - return true - } + flSet := cmd.lookupFlagSet(name) + + if flSet == nil { + return false + } + + isSet := false - f := cmd.lookupFlag(name) - if f == nil { - return false + flSet.Visit(func(f *flag.Flag) { + if f.Name == name { + isSet = true } + }) - return f.IsSet() + if isSet { + tracef("flag with name %[1]q found via flag set lookup (cmd=%[2]q)", name, cmd.Name) + return true } - return false + fl := cmd.lookupFlag(name) + if fl == nil { + tracef("flag with name %[1]q NOT found; assuming not set (cmd=%[2]q)", name, cmd.Name) + return false + } + + isSet = fl.IsSet() + if isSet { + tracef("flag with name %[1]q is set (cmd=%[2]q)", name, cmd.Name) + } else { + tracef("flag with name %[1]q is NOT set (cmd=%[2]q)", name, cmd.Name) + } + + return isSet } // LocalFlagNames returns a slice of flag names used in this // command. func (cmd *Command) LocalFlagNames() []string { - var names []string + names := []string{} + cmd.flagSet.Visit(makeFlagNameVisitor(&names)) + // Check the flags which have been set via env or file if cmd.Flags != nil { for _, f := range cmd.Flags { @@ -814,34 +892,37 @@ func (cmd *Command) LocalFlagNames() []string { // Sort out the duplicates since flag could be set via multiple // paths m := map[string]struct{}{} - var unames []string + uniqNames := []string{} + for _, name := range names { if _, ok := m[name]; !ok { m[name] = struct{}{} - unames = append(unames, name) + uniqNames = append(uniqNames, name) } } - return unames + return uniqNames } // FlagNames returns a slice of flag names used by the this command // and all of its parent commands. func (cmd *Command) FlagNames() []string { - var names []string - for _, pCmd := range cmd.Lineage() { - names = append(names, pCmd.LocalFlagNames()...) + names := cmd.LocalFlagNames() + + if cmd.parent != nil { + names = append(cmd.parent.FlagNames(), names...) } + return names } // Lineage returns *this* command and all of its ancestor commands // in order from child to parent func (cmd *Command) Lineage() []*Command { - var lineage []*Command + lineage := []*Command{cmd} - for cur := cmd; cur != nil; cur = cur.parent { - lineage = append(lineage, cur) + if cmd.parent != nil { + lineage = append(lineage, cmd.parent.Lineage()...) } return lineage @@ -860,16 +941,18 @@ func (cmd *Command) Count(name string) int { // Value returns the value of the flag corresponding to `name` func (cmd *Command) Value(name string) interface{} { if fs := cmd.lookupFlagSet(name); fs != nil { + tracef("value found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return fs.Lookup(name).Value.(flag.Getter).Get() } + + tracef("value NOT found for name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } // Args returns the command line arguments associated with the // command. func (cmd *Command) Args() Args { - ret := args(cmd.flagSet.Args()) - return &ret + return &stringSliceArgs{v: cmd.flagSet.Args()} } // NArg returns the number of the command line arguments. diff --git a/command_test.go b/command_test.go index f84040f5a8..3b080a684a 100644 --- a/command_test.go +++ b/command_test.go @@ -194,28 +194,28 @@ func TestCommandFlagParsing(t *testing.T) { func TestParseAndRunShortOpts(t *testing.T) { testCases := []struct { - testArgs args + testArgs stringSliceArgs expectedErr string expectedArgs Args }{ - {testArgs: args{"test", "-a"}}, - {testArgs: args{"test", "-c", "arg1", "arg2"}, expectedArgs: &args{"arg1", "arg2"}}, - {testArgs: args{"test", "-f"}, expectedArgs: &args{}}, - {testArgs: args{"test", "-ac", "--fgh"}, expectedArgs: &args{}}, - {testArgs: args{"test", "-af"}, expectedArgs: &args{}}, - {testArgs: args{"test", "-cf"}, expectedArgs: &args{}}, - {testArgs: args{"test", "-acf"}, expectedArgs: &args{}}, - {testArgs: args{"test", "--acf"}, expectedErr: "flag provided but not defined: -acf"}, - {testArgs: args{"test", "-invalid"}, expectedErr: "flag provided but not defined: -invalid"}, - {testArgs: args{"test", "-acf", "-invalid"}, expectedErr: "flag provided but not defined: -invalid"}, - {testArgs: args{"test", "--invalid"}, expectedErr: "flag provided but not defined: -invalid"}, - {testArgs: args{"test", "-acf", "--invalid"}, expectedErr: "flag provided but not defined: -invalid"}, - {testArgs: args{"test", "-acf", "arg1", "-invalid"}, expectedArgs: &args{"arg1", "-invalid"}}, - {testArgs: args{"test", "-acf", "arg1", "--invalid"}, expectedArgs: &args{"arg1", "--invalid"}}, - {testArgs: args{"test", "-acfi", "not-arg", "arg1", "-invalid"}, expectedArgs: &args{"arg1", "-invalid"}}, - {testArgs: args{"test", "-i", "ivalue"}, expectedArgs: &args{}}, - {testArgs: args{"test", "-i", "ivalue", "arg1"}, expectedArgs: &args{"arg1"}}, - {testArgs: args{"test", "-i"}, expectedErr: "flag needs an argument: -i"}, + {testArgs: stringSliceArgs{"test", "-a"}}, + {testArgs: stringSliceArgs{"test", "-c", "arg1", "arg2"}, expectedArgs: &stringSliceArgs{"arg1", "arg2"}}, + {testArgs: stringSliceArgs{"test", "-f"}, expectedArgs: &stringSliceArgs{}}, + {testArgs: stringSliceArgs{"test", "-ac", "--fgh"}, expectedArgs: &stringSliceArgs{}}, + {testArgs: stringSliceArgs{"test", "-af"}, expectedArgs: &stringSliceArgs{}}, + {testArgs: stringSliceArgs{"test", "-cf"}, expectedArgs: &stringSliceArgs{}}, + {testArgs: stringSliceArgs{"test", "-acf"}, expectedArgs: &stringSliceArgs{}}, + {testArgs: stringSliceArgs{"test", "--acf"}, expectedErr: "flag provided but not defined: -acf"}, + {testArgs: stringSliceArgs{"test", "-invalid"}, expectedErr: "flag provided but not defined: -invalid"}, + {testArgs: stringSliceArgs{"test", "-acf", "-invalid"}, expectedErr: "flag provided but not defined: -invalid"}, + {testArgs: stringSliceArgs{"test", "--invalid"}, expectedErr: "flag provided but not defined: -invalid"}, + {testArgs: stringSliceArgs{"test", "-acf", "--invalid"}, expectedErr: "flag provided but not defined: -invalid"}, + {testArgs: stringSliceArgs{"test", "-acf", "arg1", "-invalid"}, expectedArgs: &stringSliceArgs{"arg1", "-invalid"}}, + {testArgs: stringSliceArgs{"test", "-acf", "arg1", "--invalid"}, expectedArgs: &stringSliceArgs{"arg1", "--invalid"}}, + {testArgs: stringSliceArgs{"test", "-acfi", "not-arg", "arg1", "-invalid"}, expectedArgs: &stringSliceArgs{"arg1", "-invalid"}}, + {testArgs: stringSliceArgs{"test", "-i", "ivalue"}, expectedArgs: &stringSliceArgs{}}, + {testArgs: stringSliceArgs{"test", "-i", "ivalue", "arg1"}, expectedArgs: &stringSliceArgs{"arg1"}}, + {testArgs: stringSliceArgs{"test", "-i"}, expectedErr: "flag needs an argument: -i"}, } for _, tc := range testCases { @@ -407,12 +407,12 @@ func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { func TestCommandSkipFlagParsing(t *testing.T) { cases := []struct { - testArgs args - expectedArgs *args + testArgs stringSliceArgs + expectedArgs *stringSliceArgs expectedErr error }{ - {testArgs: args{"some-command", "some-arg", "--flag", "foo"}, expectedArgs: &args{"some-arg", "--flag", "foo"}, expectedErr: nil}, - {testArgs: args{"some-command", "some-arg", "--flag=foo"}, expectedArgs: &args{"some-arg", "--flag=foo"}, expectedErr: nil}, + {testArgs: stringSliceArgs{"some-command", "some-arg", "--flag", "foo"}, expectedArgs: &stringSliceArgs{"some-arg", "--flag", "foo"}, expectedErr: nil}, + {testArgs: stringSliceArgs{"some-command", "some-arg", "--flag=foo"}, expectedArgs: &stringSliceArgs{"some-arg", "--flag=foo"}, expectedErr: nil}, } for _, c := range cases { @@ -438,14 +438,14 @@ func TestCommandSkipFlagParsing(t *testing.T) { func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) { cases := []struct { - testArgs args + testArgs stringSliceArgs expectedOut string }{ - {testArgs: args{"--undefined"}, expectedOut: "found 0 args"}, - {testArgs: args{"--number"}, expectedOut: "found 0 args"}, - {testArgs: args{"--number", "forty-two"}, expectedOut: "found 0 args"}, - {testArgs: args{"--number", "42"}, expectedOut: "found 0 args"}, - {testArgs: args{"--number", "42", "newArg"}, expectedOut: "found 1 args"}, + {testArgs: stringSliceArgs{"--undefined"}, expectedOut: "found 0 args"}, + {testArgs: stringSliceArgs{"--number"}, expectedOut: "found 0 args"}, + {testArgs: stringSliceArgs{"--number", "forty-two"}, expectedOut: "found 0 args"}, + {testArgs: stringSliceArgs{"--number", "42"}, expectedOut: "found 0 args"}, + {testArgs: stringSliceArgs{"--number", "42", "newArg"}, expectedOut: "found 1 args"}, } for _, c := range cases { @@ -467,7 +467,7 @@ func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) { }, } - osArgs := args{"bar"} + osArgs := stringSliceArgs{"bar"} osArgs = append(osArgs, c.testArgs...) osArgs = append(osArgs, "--generate-shell-completion") @@ -2465,7 +2465,7 @@ func TestCustomHelpVersionFlags(t *testing.T) { func TestHandleExitCoder_Default(t *testing.T) { app := buildMinimalTestCommand() - fs, err := flagSet(app.Name, app.Flags) + fs, err := newFlagSet(app.Name, app.Flags) if err != nil { t.Errorf("error creating FlagSet: %s", err) } diff --git a/flag.go b/flag.go index 0447851b16..5332b22201 100644 --- a/flag.go +++ b/flag.go @@ -170,7 +170,7 @@ type PersistentFlag interface { IsPersistent() bool } -func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { +func newFlagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) for _, f := range flags { @@ -178,7 +178,9 @@ func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { return nil, err } } + set.SetOutput(io.Discard) + return set, nil } diff --git a/flag_bool.go b/flag_bool.go index f2e6bbeacf..33c0341cf9 100644 --- a/flag_bool.go +++ b/flag_bool.go @@ -27,8 +27,11 @@ type boolValue struct { func (cmd *Command) Bool(name string) bool { if v, ok := cmd.Value(name).(bool); ok { + tracef("bool available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("bool NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return false } diff --git a/godoc-current.txt b/godoc-current.txt index 018e5ee2bb..65c7e5a8fb 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -156,11 +156,11 @@ var HelpPrinterCustom helpPrinterCustom = printHelpCustom FUNCTIONS -func DefaultAppComplete(cCtx *Context) +func DefaultAppComplete(ctx context.Context, cmd *Command) DefaultAppComplete prints the list of subcommands as the default app completion method -func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) +func DefaultCompleteWithFlags(cmd *Command) func(ctx context.Context, cmd *Command) func FlagNames(name string, aliases []string) []string func HandleExitCoder(err error) HandleExitCoder handles errors implementing ExitCoder by printing their @@ -172,42 +172,42 @@ func HandleExitCoder(err error) This function is the default error-handling behavior for an App. -func ShowAppHelp(cCtx *Context) error +func ShowAppHelp(cmd *Command) error ShowAppHelp is an action that displays the help. -func ShowAppHelpAndExit(c *Context, exitCode int) +func ShowAppHelpAndExit(cmd *Command, exitCode int) ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. -func ShowCommandHelp(cCtx *Context, commandName string) error +func ShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error ShowCommandHelp prints help for the given command -func ShowCommandHelpAndExit(c *Context, command string, code int) +func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) ShowCommandHelpAndExit - exits with code after showing help -func ShowSubcommandHelp(cCtx *Context) error +func ShowSubcommandHelp(cmd *Command) error ShowSubcommandHelp prints help for the given subcommand -func ShowSubcommandHelpAndExit(c *Context, exitCode int) +func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code. -func ShowVersion(cCtx *Context) +func ShowVersion(cmd *Command) ShowVersion prints the version number of the App TYPES -type ActionFunc func(*Context) error +type ActionFunc func(context.Context, *Command) error ActionFunc is the action to execute when no subcommands are specified type ActionableFlag interface { - RunAction(*Context) error + RunAction(context.Context, *Command) error } ActionableFlag is an interface that wraps Flag interface and RunAction operation. -type AfterFunc func(*Context) error +type AfterFunc func(context.Context, *Command) error AfterFunc is an action that executes after any subcommands are run and have finished. The AfterFunc is run even if Action() panics. @@ -227,7 +227,7 @@ type Args interface { Slice() []string } -type BeforeFunc func(*Context) error +type BeforeFunc func(context.Context, *Command) error BeforeFunc is an action that executes prior to any subcommands being run once the context is ready. If a non-nil error is returned, no subcommands are run. @@ -258,7 +258,7 @@ func (s *BoolWithInverseFlag) IsSet() bool func (s *BoolWithInverseFlag) Names() []string -func (s *BoolWithInverseFlag) RunAction(ctx *Context) error +func (s *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error func (s *BoolWithInverseFlag) String() string Example for BoolFlag{Name: "env"} --env (default: false) || --no-env @@ -379,8 +379,29 @@ type Command struct { string slice of arguments such as os.Args. A given Command may contain Flags and sub-commands in Commands. +func (cmd *Command) Args() Args + Args returns the command line arguments associated with the command. + +func (cmd *Command) Bool(name string) bool + func (cmd *Command) Command(name string) *Command +func (cmd *Command) Count(name string) int + Count returns the num of occurrences of this flag + +func (cmd *Command) Duration(name string) time.Duration + +func (cmd *Command) FlagNames() []string + FlagNames returns a slice of flag names used by the this command and all of + its parent commands. + +func (cmd *Command) Float(name string) float64 + Int looks up the value of a local IntFlag, returns 0 if not found + +func (cmd *Command) FloatSlice(name string) []float64 + FloatSlice looks up the value of a local FloatSliceFlag, returns nil if not + found + func (cmd *Command) FullName() string FullName returns the full name of the command. For commands with parents this ensures that the parent commands are part of the command path. @@ -388,21 +409,70 @@ func (cmd *Command) FullName() string func (cmd *Command) HasName(name string) bool HasName returns true if Command.Name matches given name +func (cmd *Command) Int(name string) int64 + Int64 looks up the value of a local Int64Flag, returns 0 if not found + +func (cmd *Command) IntSlice(name string) []int64 + IntSlice looks up the value of a local IntSliceFlag, returns nil if not + found + +func (cmd *Command) IsSet(name string) bool + IsSet determines if the flag was actually set + +func (cmd *Command) Lineage() []*Command + Lineage returns *this* command and all of its ancestor commands in order + from child to parent + +func (cmd *Command) LocalFlagNames() []string + LocalFlagNames returns a slice of flag names used in this command. + +func (cmd *Command) NArg() int + NArg returns the number of the command line arguments. + func (cmd *Command) Names() []string Names returns the names including short names and aliases. +func (cmd *Command) NumFlags() int + NumFlags returns the number of flags set + func (cmd *Command) Root() *Command Root returns the Command at the root of the graph -func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error) +func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) Run is the entry point to the command graph. The positional arguments are parsed according to the Flag and Command definitions and the matching Action functions are run. +func (cmd *Command) Set(name, value string) error + Set sets a context flag to a value. + +func (cmd *Command) String(name string) string + +func (cmd *Command) StringMap(name string) map[string]string + StringMap looks up the value of a local StringMapFlag, returns nil if not + found + +func (cmd *Command) StringSlice(name string) []string + StringSlice looks up the value of a local StringSliceFlag, returns nil if + not found + +func (cmd *Command) Timestamp(name string) *time.Time + Timestamp gets the timestamp from a flag name + func (cmd *Command) ToFishCompletion() (string, error) ToFishCompletion creates a fish completion string for the `*App` The function errors if either parsing or writing of the string fails. +func (cmd *Command) Uint(name string) uint64 + Uint looks up the value of a local Uint64Flag, returns 0 if not found + +func (cmd *Command) UintSlice(name string) []uint64 + UintSlice looks up the value of a local UintSliceFlag, returns nil if not + found + +func (cmd *Command) Value(name string) interface{} + Value returns the value of the flag corresponding to `name` + func (cmd *Command) VisibleCategories() []CommandCategory VisibleCategories returns a slice of categories and commands that are Hidden=false @@ -433,92 +503,9 @@ type CommandCategory interface { } CommandCategory is a category containing commands. -type CommandNotFoundFunc func(*Context, string) +type CommandNotFoundFunc func(context.Context, *Command, string) CommandNotFoundFunc is executed if the proper command cannot be found -type Context struct { - context.Context - Command *Command - - // Has unexported fields. -} - Context is a type that is passed through to each Handler action in a cli - application. Context can be used to retrieve context-specific args and - parsed command-line options. - -func NewContext(cmd *Command, set *flag.FlagSet, parent *Context) *Context - NewContext creates a new context. For use in when invoking a Command action. - -func (cCtx *Context) Args() Args - Args returns the command line arguments associated with the context. - -func (cCtx *Context) Bool(name string) bool - -func (cCtx *Context) Count(name string) int - Count returns the num of occurrences of this flag - -func (cCtx *Context) Duration(name string) time.Duration - -func (cCtx *Context) FlagNames() []string - FlagNames returns a slice of flag names used by the this context and all of - its parent contexts. - -func (cCtx *Context) Float(name string) float64 - Int looks up the value of a local IntFlag, returns 0 if not found - -func (cCtx *Context) FloatSlice(name string) []float64 - FloatSlice looks up the value of a local FloatSliceFlag, returns nil if not - found - -func (cCtx *Context) Int(name string) int64 - Int64 looks up the value of a local Int64Flag, returns 0 if not found - -func (cCtx *Context) IntSlice(name string) []int64 - IntSlice looks up the value of a local IntSliceFlag, returns nil if not - found - -func (cCtx *Context) IsSet(name string) bool - IsSet determines if the flag was actually set - -func (cCtx *Context) Lineage() []*Context - Lineage returns *this* context and all of its ancestor contexts in order - from child to parent - -func (cCtx *Context) LocalFlagNames() []string - LocalFlagNames returns a slice of flag names used in this context. - -func (cCtx *Context) NArg() int - NArg returns the number of the command line arguments. - -func (cCtx *Context) NumFlags() int - NumFlags returns the number of flags set - -func (cCtx *Context) Set(name, value string) error - Set sets a context flag to a value. - -func (cCtx *Context) String(name string) string - -func (cCtx *Context) StringMap(name string) map[string]string - StringMap looks up the value of a local StringMapFlag, returns nil if not - found - -func (cCtx *Context) StringSlice(name string) []string - StringSlice looks up the value of a local StringSliceFlag, returns nil if - not found - -func (cCtx *Context) Timestamp(name string) *time.Time - Timestamp gets the timestamp from a flag name - -func (cCtx *Context) Uint(name string) uint64 - Uint looks up the value of a local Uint64Flag, returns 0 if not found - -func (cCtx *Context) UintSlice(name string) []uint64 - UintSlice looks up the value of a local UintSliceFlag, returns nil if not - found - -func (cCtx *Context) Value(name string) interface{} - Value returns the value of the flag corresponding to `name` - type Countable interface { Count() int } @@ -576,7 +563,7 @@ func Exit(message interface{}, exitCode int) ExitCoder can be avoided by overriding the ExitErrHandler function on an App or the package-global OsExiter function. -type ExitErrHandlerFunc func(cCtx *Context, err error) +type ExitErrHandlerFunc func(context.Context, *Command, error) ExitErrHandlerFunc is executed if provided in order to handle exitError values returned by Actions and Before/After functions. @@ -638,7 +625,7 @@ type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { TakesFile bool // whether this flag takes a file argument, mainly for shell completion purposes - Action func(*Context, T) error // Action callback to be called when flag is set + Action func(context.Context, *Command, T) error // Action callback to be called when flag is set Config C // Additional/Custom configuration associated with this flag type @@ -656,7 +643,7 @@ type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { func (f *FlagBase[T, C, V]) Apply(set *flag.FlagSet) error Apply populates the flag given the flag set and environment -func (f *FlagBase[T, C, V]) Get(ctx *Context) T +func (f *FlagBase[T, C, V]) Get(cmd *Command) T Get returns the flag’s value in the given Context. func (f *FlagBase[T, C, V]) GetCategory() string @@ -694,7 +681,7 @@ func (f *FlagBase[T, C, V]) IsVisible() bool func (f *FlagBase[T, C, V]) Names() []string Names returns the names of the flag -func (f *FlagBase[T, C, V]) RunAction(ctx *Context) error +func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set func (f *FlagBase[T, C, V]) String() string @@ -769,7 +756,7 @@ type IntegerConfig struct { } IntegerConfig is the configuration for all integer type flags -type InvalidFlagAccessFunc func(*Context, string) +type InvalidFlagAccessFunc func(context.Context, *Command, string) InvalidFlagAccessFunc is executed when an invalid flag is accessed from the context. @@ -820,7 +807,7 @@ type MutuallyExclusiveFlags struct { type NoConfig struct{} NoConfig is for flags which dont need a custom configuration -type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error +type OnUsageErrorFunc func(ctx context.Context, cmd *Command, err error, isSubcommand bool) error OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying customized usage error messages. This function is able to replace the original error messages. If this function is not set, the "Incorrect @@ -845,7 +832,7 @@ type Serializer interface { } Serializer is used to circumvent the limitations of flag.FlagSet.Set -type ShellCompleteFunc func(*Context) +type ShellCompleteFunc func(context.Context, *Command) ShellCompleteFunc is an action to execute when the shell completion flag is set diff --git a/help.go b/help.go index 9a6dc1629d..1051df2cad 100644 --- a/help.go +++ b/help.go @@ -313,6 +313,7 @@ func ShowSubcommandHelp(cmd *Command) error { // ShowVersion prints the version number of the App func ShowVersion(cmd *Command) { + tracef("showing version via VersionPrinter (cmd=%[1]q)", cmd.Name) VersionPrinter(cmd) } @@ -429,18 +430,6 @@ func checkVersion(cmd *Command) bool { return found } -func checkHelp(cmd *Command) bool { - found := false - for _, name := range HelpFlag.Names() { - if cmd.Bool(name) { - found = true - break - } - } - - return found -} - func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) { if !c.EnableShellCompletion { return false, arguments @@ -457,6 +446,8 @@ func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) { } func checkCompletions(ctx context.Context, cmd *Command) bool { + tracef("checking completions on command %[1]q", cmd.Name) + if !cmd.EnableShellCompletion { return false } diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 018e5ee2bb..65c7e5a8fb 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -156,11 +156,11 @@ var HelpPrinterCustom helpPrinterCustom = printHelpCustom FUNCTIONS -func DefaultAppComplete(cCtx *Context) +func DefaultAppComplete(ctx context.Context, cmd *Command) DefaultAppComplete prints the list of subcommands as the default app completion method -func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) +func DefaultCompleteWithFlags(cmd *Command) func(ctx context.Context, cmd *Command) func FlagNames(name string, aliases []string) []string func HandleExitCoder(err error) HandleExitCoder handles errors implementing ExitCoder by printing their @@ -172,42 +172,42 @@ func HandleExitCoder(err error) This function is the default error-handling behavior for an App. -func ShowAppHelp(cCtx *Context) error +func ShowAppHelp(cmd *Command) error ShowAppHelp is an action that displays the help. -func ShowAppHelpAndExit(c *Context, exitCode int) +func ShowAppHelpAndExit(cmd *Command, exitCode int) ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. -func ShowCommandHelp(cCtx *Context, commandName string) error +func ShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error ShowCommandHelp prints help for the given command -func ShowCommandHelpAndExit(c *Context, command string, code int) +func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) ShowCommandHelpAndExit - exits with code after showing help -func ShowSubcommandHelp(cCtx *Context) error +func ShowSubcommandHelp(cmd *Command) error ShowSubcommandHelp prints help for the given subcommand -func ShowSubcommandHelpAndExit(c *Context, exitCode int) +func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code. -func ShowVersion(cCtx *Context) +func ShowVersion(cmd *Command) ShowVersion prints the version number of the App TYPES -type ActionFunc func(*Context) error +type ActionFunc func(context.Context, *Command) error ActionFunc is the action to execute when no subcommands are specified type ActionableFlag interface { - RunAction(*Context) error + RunAction(context.Context, *Command) error } ActionableFlag is an interface that wraps Flag interface and RunAction operation. -type AfterFunc func(*Context) error +type AfterFunc func(context.Context, *Command) error AfterFunc is an action that executes after any subcommands are run and have finished. The AfterFunc is run even if Action() panics. @@ -227,7 +227,7 @@ type Args interface { Slice() []string } -type BeforeFunc func(*Context) error +type BeforeFunc func(context.Context, *Command) error BeforeFunc is an action that executes prior to any subcommands being run once the context is ready. If a non-nil error is returned, no subcommands are run. @@ -258,7 +258,7 @@ func (s *BoolWithInverseFlag) IsSet() bool func (s *BoolWithInverseFlag) Names() []string -func (s *BoolWithInverseFlag) RunAction(ctx *Context) error +func (s *BoolWithInverseFlag) RunAction(ctx context.Context, cmd *Command) error func (s *BoolWithInverseFlag) String() string Example for BoolFlag{Name: "env"} --env (default: false) || --no-env @@ -379,8 +379,29 @@ type Command struct { string slice of arguments such as os.Args. A given Command may contain Flags and sub-commands in Commands. +func (cmd *Command) Args() Args + Args returns the command line arguments associated with the command. + +func (cmd *Command) Bool(name string) bool + func (cmd *Command) Command(name string) *Command +func (cmd *Command) Count(name string) int + Count returns the num of occurrences of this flag + +func (cmd *Command) Duration(name string) time.Duration + +func (cmd *Command) FlagNames() []string + FlagNames returns a slice of flag names used by the this command and all of + its parent commands. + +func (cmd *Command) Float(name string) float64 + Int looks up the value of a local IntFlag, returns 0 if not found + +func (cmd *Command) FloatSlice(name string) []float64 + FloatSlice looks up the value of a local FloatSliceFlag, returns nil if not + found + func (cmd *Command) FullName() string FullName returns the full name of the command. For commands with parents this ensures that the parent commands are part of the command path. @@ -388,21 +409,70 @@ func (cmd *Command) FullName() string func (cmd *Command) HasName(name string) bool HasName returns true if Command.Name matches given name +func (cmd *Command) Int(name string) int64 + Int64 looks up the value of a local Int64Flag, returns 0 if not found + +func (cmd *Command) IntSlice(name string) []int64 + IntSlice looks up the value of a local IntSliceFlag, returns nil if not + found + +func (cmd *Command) IsSet(name string) bool + IsSet determines if the flag was actually set + +func (cmd *Command) Lineage() []*Command + Lineage returns *this* command and all of its ancestor commands in order + from child to parent + +func (cmd *Command) LocalFlagNames() []string + LocalFlagNames returns a slice of flag names used in this command. + +func (cmd *Command) NArg() int + NArg returns the number of the command line arguments. + func (cmd *Command) Names() []string Names returns the names including short names and aliases. +func (cmd *Command) NumFlags() int + NumFlags returns the number of flags set + func (cmd *Command) Root() *Command Root returns the Command at the root of the graph -func (cmd *Command) Run(ctx context.Context, arguments []string) (deferErr error) +func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) Run is the entry point to the command graph. The positional arguments are parsed according to the Flag and Command definitions and the matching Action functions are run. +func (cmd *Command) Set(name, value string) error + Set sets a context flag to a value. + +func (cmd *Command) String(name string) string + +func (cmd *Command) StringMap(name string) map[string]string + StringMap looks up the value of a local StringMapFlag, returns nil if not + found + +func (cmd *Command) StringSlice(name string) []string + StringSlice looks up the value of a local StringSliceFlag, returns nil if + not found + +func (cmd *Command) Timestamp(name string) *time.Time + Timestamp gets the timestamp from a flag name + func (cmd *Command) ToFishCompletion() (string, error) ToFishCompletion creates a fish completion string for the `*App` The function errors if either parsing or writing of the string fails. +func (cmd *Command) Uint(name string) uint64 + Uint looks up the value of a local Uint64Flag, returns 0 if not found + +func (cmd *Command) UintSlice(name string) []uint64 + UintSlice looks up the value of a local UintSliceFlag, returns nil if not + found + +func (cmd *Command) Value(name string) interface{} + Value returns the value of the flag corresponding to `name` + func (cmd *Command) VisibleCategories() []CommandCategory VisibleCategories returns a slice of categories and commands that are Hidden=false @@ -433,92 +503,9 @@ type CommandCategory interface { } CommandCategory is a category containing commands. -type CommandNotFoundFunc func(*Context, string) +type CommandNotFoundFunc func(context.Context, *Command, string) CommandNotFoundFunc is executed if the proper command cannot be found -type Context struct { - context.Context - Command *Command - - // Has unexported fields. -} - Context is a type that is passed through to each Handler action in a cli - application. Context can be used to retrieve context-specific args and - parsed command-line options. - -func NewContext(cmd *Command, set *flag.FlagSet, parent *Context) *Context - NewContext creates a new context. For use in when invoking a Command action. - -func (cCtx *Context) Args() Args - Args returns the command line arguments associated with the context. - -func (cCtx *Context) Bool(name string) bool - -func (cCtx *Context) Count(name string) int - Count returns the num of occurrences of this flag - -func (cCtx *Context) Duration(name string) time.Duration - -func (cCtx *Context) FlagNames() []string - FlagNames returns a slice of flag names used by the this context and all of - its parent contexts. - -func (cCtx *Context) Float(name string) float64 - Int looks up the value of a local IntFlag, returns 0 if not found - -func (cCtx *Context) FloatSlice(name string) []float64 - FloatSlice looks up the value of a local FloatSliceFlag, returns nil if not - found - -func (cCtx *Context) Int(name string) int64 - Int64 looks up the value of a local Int64Flag, returns 0 if not found - -func (cCtx *Context) IntSlice(name string) []int64 - IntSlice looks up the value of a local IntSliceFlag, returns nil if not - found - -func (cCtx *Context) IsSet(name string) bool - IsSet determines if the flag was actually set - -func (cCtx *Context) Lineage() []*Context - Lineage returns *this* context and all of its ancestor contexts in order - from child to parent - -func (cCtx *Context) LocalFlagNames() []string - LocalFlagNames returns a slice of flag names used in this context. - -func (cCtx *Context) NArg() int - NArg returns the number of the command line arguments. - -func (cCtx *Context) NumFlags() int - NumFlags returns the number of flags set - -func (cCtx *Context) Set(name, value string) error - Set sets a context flag to a value. - -func (cCtx *Context) String(name string) string - -func (cCtx *Context) StringMap(name string) map[string]string - StringMap looks up the value of a local StringMapFlag, returns nil if not - found - -func (cCtx *Context) StringSlice(name string) []string - StringSlice looks up the value of a local StringSliceFlag, returns nil if - not found - -func (cCtx *Context) Timestamp(name string) *time.Time - Timestamp gets the timestamp from a flag name - -func (cCtx *Context) Uint(name string) uint64 - Uint looks up the value of a local Uint64Flag, returns 0 if not found - -func (cCtx *Context) UintSlice(name string) []uint64 - UintSlice looks up the value of a local UintSliceFlag, returns nil if not - found - -func (cCtx *Context) Value(name string) interface{} - Value returns the value of the flag corresponding to `name` - type Countable interface { Count() int } @@ -576,7 +563,7 @@ func Exit(message interface{}, exitCode int) ExitCoder can be avoided by overriding the ExitErrHandler function on an App or the package-global OsExiter function. -type ExitErrHandlerFunc func(cCtx *Context, err error) +type ExitErrHandlerFunc func(context.Context, *Command, error) ExitErrHandlerFunc is executed if provided in order to handle exitError values returned by Actions and Before/After functions. @@ -638,7 +625,7 @@ type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { TakesFile bool // whether this flag takes a file argument, mainly for shell completion purposes - Action func(*Context, T) error // Action callback to be called when flag is set + Action func(context.Context, *Command, T) error // Action callback to be called when flag is set Config C // Additional/Custom configuration associated with this flag type @@ -656,7 +643,7 @@ type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { func (f *FlagBase[T, C, V]) Apply(set *flag.FlagSet) error Apply populates the flag given the flag set and environment -func (f *FlagBase[T, C, V]) Get(ctx *Context) T +func (f *FlagBase[T, C, V]) Get(cmd *Command) T Get returns the flag’s value in the given Context. func (f *FlagBase[T, C, V]) GetCategory() string @@ -694,7 +681,7 @@ func (f *FlagBase[T, C, V]) IsVisible() bool func (f *FlagBase[T, C, V]) Names() []string Names returns the names of the flag -func (f *FlagBase[T, C, V]) RunAction(ctx *Context) error +func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set func (f *FlagBase[T, C, V]) String() string @@ -769,7 +756,7 @@ type IntegerConfig struct { } IntegerConfig is the configuration for all integer type flags -type InvalidFlagAccessFunc func(*Context, string) +type InvalidFlagAccessFunc func(context.Context, *Command, string) InvalidFlagAccessFunc is executed when an invalid flag is accessed from the context. @@ -820,7 +807,7 @@ type MutuallyExclusiveFlags struct { type NoConfig struct{} NoConfig is for flags which dont need a custom configuration -type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error +type OnUsageErrorFunc func(ctx context.Context, cmd *Command, err error, isSubcommand bool) error OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying customized usage error messages. This function is able to replace the original error messages. If this function is not set, the "Incorrect @@ -845,7 +832,7 @@ type Serializer interface { } Serializer is used to circumvent the limitations of flag.FlagSet.Set -type ShellCompleteFunc func(*Context) +type ShellCompleteFunc func(context.Context, *Command) ShellCompleteFunc is an action to execute when the shell completion flag is set From e5ad000ebc40d9619959837ad08943552f7585b5 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 26 Jun 2023 21:14:34 -0400 Subject: [PATCH 3/8] Tests run + args less convoluted --- command.go | 35 ++++++++-------- command_test.go | 104 ++++++++++++++++++++++++------------------------ 2 files changed, 69 insertions(+), 70 deletions(-) diff --git a/command.go b/command.go index 7006192451..dd3bb4e974 100644 --- a/command.go +++ b/command.go @@ -366,12 +366,7 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) { cmd.setupCommandGraph() } - // TODO: this is some convoluted yikes { - var args Args = &stringSliceArgs{v: osArgs} - set, err := cmd.parseFlags(args) - cmd.flagSet = set - args = cmd.Args() - // } + args, err := cmd.parseFlags(&stringSliceArgs{v: osArgs}) tracef("using post-parse arguments %[1]q (cmd=%[2]q)", args, cmd.Name) @@ -587,17 +582,19 @@ func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil } -func (cmd *Command) parseFlags(args Args) (*flag.FlagSet, error) { +func (cmd *Command) parseFlags(args Args) (Args, error) { tracef("parsing flags from arguments %[1]q (cmd=%[2]q)", args, cmd.Name) - set, err := cmd.newFlagSet() - if err != nil { - return nil, err + if v, err := cmd.newFlagSet(); err != nil { + return args, err + } else { + cmd.flagSet = v } if cmd.SkipFlagParsing { tracef("skipping flag parsing (cmd=%[1]q)", cmd.Name) - return set, set.Parse(append([]string{"--"}, args.Tail()...)) + + return cmd.Args(), cmd.flagSet.Parse(append([]string{"--"}, args.Tail()...)) } for pCmd := cmd.parent; pCmd != nil; pCmd = pCmd.parent { @@ -608,7 +605,7 @@ func (cmd *Command) parseFlags(args Args) (*flag.FlagSet, error) { } applyPersistentFlag := true - set.VisitAll(func(f *flag.Flag) { + cmd.flagSet.VisitAll(func(f *flag.Flag) { for _, name := range fl.Names() { if name == f.Name { applyPersistentFlag = false @@ -621,23 +618,23 @@ func (cmd *Command) parseFlags(args Args) (*flag.FlagSet, error) { continue } - if err := fl.Apply(set); err != nil { - return nil, err + if err := fl.Apply(cmd.flagSet); err != nil { + return cmd.Args(), err } cmd.appliedFlags = append(cmd.appliedFlags, fl) } } - if err := parseIter(set, cmd, args.Tail(), cmd.EnableShellCompletion); err != nil { - return nil, err + if err := parseIter(cmd.flagSet, cmd, args.Tail(), cmd.EnableShellCompletion); err != nil { + return cmd.Args(), err } - if err := normalizeFlags(cmd.Flags, set); err != nil { - return nil, err + if err := normalizeFlags(cmd.Flags, cmd.flagSet); err != nil { + return cmd.Args(), err } - return set, nil + return cmd.Args(), nil } // Names returns the names including short names and aliases. diff --git a/command_test.go b/command_test.go index 3b080a684a..fb20bb95a4 100644 --- a/command_test.go +++ b/command_test.go @@ -194,32 +194,32 @@ func TestCommandFlagParsing(t *testing.T) { func TestParseAndRunShortOpts(t *testing.T) { testCases := []struct { - testArgs stringSliceArgs + testArgs *stringSliceArgs expectedErr string expectedArgs Args }{ - {testArgs: stringSliceArgs{"test", "-a"}}, - {testArgs: stringSliceArgs{"test", "-c", "arg1", "arg2"}, expectedArgs: &stringSliceArgs{"arg1", "arg2"}}, - {testArgs: stringSliceArgs{"test", "-f"}, expectedArgs: &stringSliceArgs{}}, - {testArgs: stringSliceArgs{"test", "-ac", "--fgh"}, expectedArgs: &stringSliceArgs{}}, - {testArgs: stringSliceArgs{"test", "-af"}, expectedArgs: &stringSliceArgs{}}, - {testArgs: stringSliceArgs{"test", "-cf"}, expectedArgs: &stringSliceArgs{}}, - {testArgs: stringSliceArgs{"test", "-acf"}, expectedArgs: &stringSliceArgs{}}, - {testArgs: stringSliceArgs{"test", "--acf"}, expectedErr: "flag provided but not defined: -acf"}, - {testArgs: stringSliceArgs{"test", "-invalid"}, expectedErr: "flag provided but not defined: -invalid"}, - {testArgs: stringSliceArgs{"test", "-acf", "-invalid"}, expectedErr: "flag provided but not defined: -invalid"}, - {testArgs: stringSliceArgs{"test", "--invalid"}, expectedErr: "flag provided but not defined: -invalid"}, - {testArgs: stringSliceArgs{"test", "-acf", "--invalid"}, expectedErr: "flag provided but not defined: -invalid"}, - {testArgs: stringSliceArgs{"test", "-acf", "arg1", "-invalid"}, expectedArgs: &stringSliceArgs{"arg1", "-invalid"}}, - {testArgs: stringSliceArgs{"test", "-acf", "arg1", "--invalid"}, expectedArgs: &stringSliceArgs{"arg1", "--invalid"}}, - {testArgs: stringSliceArgs{"test", "-acfi", "not-arg", "arg1", "-invalid"}, expectedArgs: &stringSliceArgs{"arg1", "-invalid"}}, - {testArgs: stringSliceArgs{"test", "-i", "ivalue"}, expectedArgs: &stringSliceArgs{}}, - {testArgs: stringSliceArgs{"test", "-i", "ivalue", "arg1"}, expectedArgs: &stringSliceArgs{"arg1"}}, - {testArgs: stringSliceArgs{"test", "-i"}, expectedErr: "flag needs an argument: -i"}, + {testArgs: &stringSliceArgs{v: []string{"test", "-a"}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-c", "arg1", "arg2"}}, expectedArgs: &stringSliceArgs{v: []string{"arg1", "arg2"}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-f"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-ac", "--fgh"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-af"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-cf"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-acf"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "--acf"}}, expectedErr: "flag provided but not defined: -acf"}, + {testArgs: &stringSliceArgs{v: []string{"test", "-invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, + {testArgs: &stringSliceArgs{v: []string{"test", "-acf", "-invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, + {testArgs: &stringSliceArgs{v: []string{"test", "--invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, + {testArgs: &stringSliceArgs{v: []string{"test", "-acf", "--invalid"}}, expectedErr: "flag provided but not defined: -invalid"}, + {testArgs: &stringSliceArgs{v: []string{"test", "-acf", "arg1", "-invalid"}}, expectedArgs: &stringSliceArgs{v: []string{"arg1", "-invalid"}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-acf", "arg1", "--invalid"}}, expectedArgs: &stringSliceArgs{v: []string{"arg1", "--invalid"}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-acfi", "not-arg", "arg1", "-invalid"}}, expectedArgs: &stringSliceArgs{v: []string{"arg1", "-invalid"}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-i", "ivalue"}}, expectedArgs: &stringSliceArgs{v: []string{}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-i", "ivalue", "arg1"}}, expectedArgs: &stringSliceArgs{v: []string{"arg1"}}}, + {testArgs: &stringSliceArgs{v: []string{"test", "-i"}}, expectedErr: "flag needs an argument: -i"}, } for _, tc := range testCases { - t.Run(strings.Join(tc.testArgs, " "), func(t *testing.T) { + t.Run(strings.Join(tc.testArgs.v, " "), func(t *testing.T) { state := map[string]Args{"args": nil} cmd := &Command{ @@ -240,7 +240,7 @@ func TestParseAndRunShortOpts(t *testing.T) { }, } - err := cmd.Run(buildTestContext(t), tc.testArgs) + err := cmd.Run(buildTestContext(t), tc.testArgs.Slice()) r := require.New(t) @@ -407,49 +407,51 @@ func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { func TestCommandSkipFlagParsing(t *testing.T) { cases := []struct { - testArgs stringSliceArgs + testArgs *stringSliceArgs expectedArgs *stringSliceArgs expectedErr error }{ - {testArgs: stringSliceArgs{"some-command", "some-arg", "--flag", "foo"}, expectedArgs: &stringSliceArgs{"some-arg", "--flag", "foo"}, expectedErr: nil}, - {testArgs: stringSliceArgs{"some-command", "some-arg", "--flag=foo"}, expectedArgs: &stringSliceArgs{"some-arg", "--flag=foo"}, expectedErr: nil}, + {testArgs: &stringSliceArgs{v: []string{"some-command", "some-arg", "--flag", "foo"}}, expectedArgs: &stringSliceArgs{v: []string{"some-arg", "--flag", "foo"}}, expectedErr: nil}, + {testArgs: &stringSliceArgs{v: []string{"some-command", "some-arg", "--flag=foo"}}, expectedArgs: &stringSliceArgs{v: []string{"some-arg", "--flag=foo"}}, expectedErr: nil}, } for _, c := range cases { - var args Args - cmd := &Command{ - SkipFlagParsing: true, - Name: "some-command", - Flags: []Flag{ - &StringFlag{Name: "flag"}, - }, - Action: func(_ context.Context, cmd *Command) error { - args = cmd.Args() - return nil - }, - Writer: io.Discard, - } + t.Run(strings.Join(c.testArgs.Slice(), " "), func(t *testing.T) { + var args Args + cmd := &Command{ + SkipFlagParsing: true, + Name: "some-command", + Flags: []Flag{ + &StringFlag{Name: "flag"}, + }, + Action: func(_ context.Context, cmd *Command) error { + args = cmd.Args() + return nil + }, + Writer: io.Discard, + } - err := cmd.Run(buildTestContext(t), c.testArgs) - expect(t, err, c.expectedErr) - expect(t, args, c.expectedArgs) + err := cmd.Run(buildTestContext(t), c.testArgs.Slice()) + expect(t, err, c.expectedErr) + expect(t, args, c.expectedArgs) + }) } } func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) { cases := []struct { - testArgs stringSliceArgs + testArgs *stringSliceArgs expectedOut string }{ - {testArgs: stringSliceArgs{"--undefined"}, expectedOut: "found 0 args"}, - {testArgs: stringSliceArgs{"--number"}, expectedOut: "found 0 args"}, - {testArgs: stringSliceArgs{"--number", "forty-two"}, expectedOut: "found 0 args"}, - {testArgs: stringSliceArgs{"--number", "42"}, expectedOut: "found 0 args"}, - {testArgs: stringSliceArgs{"--number", "42", "newArg"}, expectedOut: "found 1 args"}, + {testArgs: &stringSliceArgs{v: []string{"--undefined"}}, expectedOut: "found 0 args"}, + {testArgs: &stringSliceArgs{v: []string{"--number"}}, expectedOut: "found 0 args"}, + {testArgs: &stringSliceArgs{v: []string{"--number", "forty-two"}}, expectedOut: "found 0 args"}, + {testArgs: &stringSliceArgs{v: []string{"--number", "42"}}, expectedOut: "found 0 args"}, + {testArgs: &stringSliceArgs{v: []string{"--number", "42", "newArg"}}, expectedOut: "found 1 args"}, } for _, c := range cases { - t.Run(strings.Join(c.testArgs, " "), func(t *testing.T) { + t.Run(strings.Join(c.testArgs.Slice(), " "), func(t *testing.T) { out := &bytes.Buffer{} cmd := &Command{ Writer: out, @@ -467,13 +469,13 @@ func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) { }, } - osArgs := stringSliceArgs{"bar"} - osArgs = append(osArgs, c.testArgs...) - osArgs = append(osArgs, "--generate-shell-completion") + osArgs := &stringSliceArgs{v: []string{"bar"}} + osArgs.v = append(osArgs.v, c.testArgs.Slice()...) + osArgs.v = append(osArgs.v, "--generate-shell-completion") r := require.New(t) - r.NoError(cmd.Run(buildTestContext(t), osArgs)) + r.NoError(cmd.Run(buildTestContext(t), osArgs.Slice())) r.Equal(c.expectedOut, out.String()) }) } From 46876faa9ed0f54fe07c9686d214336be369ddb9 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Tue, 27 Jun 2023 09:21:48 -0400 Subject: [PATCH 4/8] Fixing some of the tests --- command_test.go | 258 ++++++++++++++++++++++++++---------------------- help.go | 2 +- 2 files changed, 139 insertions(+), 121 deletions(-) diff --git a/command_test.go b/command_test.go index fb20bb95a4..d2d4d7765b 100644 --- a/command_test.go +++ b/command_test.go @@ -1835,145 +1835,163 @@ func TestCommand_CommandNotFound(t *testing.T) { } func TestCommand_OrderOfOperations(t *testing.T) { - counts := &opCounts{} + buildCmdCounts := func() (*Command, *opCounts) { + counts := &opCounts{} - resetCounts := func() { counts = &opCounts{} } + cmd := &Command{ + EnableShellCompletion: true, + ShellComplete: func(context.Context, *Command) { + counts.Total++ + counts.ShellComplete = counts.Total + }, + OnUsageError: func(context.Context, *Command, error, bool) error { + counts.Total++ + counts.OnUsageError = counts.Total + return errors.New("hay OnUsageError") + }, + Writer: io.Discard, + } - cmd := &Command{ - EnableShellCompletion: true, - ShellComplete: func(context.Context, *Command) { + beforeNoError := func(context.Context, *Command) error { counts.Total++ - counts.ShellComplete = counts.Total - }, - OnUsageError: func(context.Context, *Command, error, bool) error { + counts.Before = counts.Total + return nil + } + + cmd.Before = beforeNoError + cmd.CommandNotFound = func(context.Context, *Command, string) { counts.Total++ - counts.OnUsageError = counts.Total - return errors.New("hay OnUsageError") - }, - Writer: io.Discard, - } + counts.CommandNotFound = counts.Total + } - beforeNoError := func(context.Context, *Command) error { - counts.Total++ - counts.Before = counts.Total - return nil - } + afterNoError := func(context.Context, *Command) error { + counts.Total++ + counts.After = counts.Total + return nil + } - beforeError := func(context.Context, *Command) error { - counts.Total++ - counts.Before = counts.Total - return errors.New("hay Before") - } + cmd.After = afterNoError + cmd.Commands = []*Command{ + { + Name: "bar", + Action: func(context.Context, *Command) error { + counts.Total++ + counts.SubCommand = counts.Total + return nil + }, + }, + } - cmd.Before = beforeNoError - cmd.CommandNotFound = func(context.Context, *Command, string) { - counts.Total++ - counts.CommandNotFound = counts.Total - } + cmd.Action = func(context.Context, *Command) error { + counts.Total++ + counts.Action = counts.Total + return nil + } - afterNoError := func(context.Context, *Command) error { - counts.Total++ - counts.After = counts.Total - return nil + return cmd, counts } - afterError := func(context.Context, *Command) error { - counts.Total++ - counts.After = counts.Total - return errors.New("hay After") - } + t.Run("on usage error", func(t *testing.T) { + cmd, counts := buildCmdCounts() + r := require.New(t) - cmd.After = afterNoError - cmd.Commands = []*Command{ - { - Name: "bar", - Action: func(context.Context, *Command) error { - counts.Total++ - counts.SubCommand = counts.Total - return nil - }, - }, - } + _ = cmd.Run(buildTestContext(t), []string{"command", "--nope"}) + r.Equal(1, counts.OnUsageError) + r.Equal(1, counts.Total) + }) - cmd.Action = func(context.Context, *Command) error { - counts.Total++ - counts.Action = counts.Total - return nil - } + t.Run("shell complete", func(t *testing.T) { + cmd, counts := buildCmdCounts() + r := require.New(t) - _ = cmd.Run(buildTestContext(t), []string{"command", "--nope"}) - expect(t, counts.OnUsageError, 1) - expect(t, counts.Total, 1) + _ = cmd.Run(buildTestContext(t), []string{"command", "--" + "generate-shell-completion"}) + r.Equal(1, counts.ShellComplete) + r.Equal(1, counts.Total) + }) - resetCounts() + t.Run("nil on usage error", func(t *testing.T) { + cmd, counts := buildCmdCounts() + cmd.OnUsageError = nil - _ = cmd.Run(buildTestContext(t), []string{"command", fmt.Sprintf("--%s", "generate-shell-completion")}) - expect(t, counts.ShellComplete, 1) - expect(t, counts.Total, 1) + _ = cmd.Run(buildTestContext(t), []string{"command", "--nope"}) + require.Equal(t, 0, counts.Total) + }) + + t.Run("before after action hooks", func(t *testing.T) { + cmd, counts := buildCmdCounts() + r := require.New(t) + + _ = cmd.Run(buildTestContext(t), []string{"command", "foo"}) + r.Equal(0, counts.OnUsageError) + r.Equal(1, counts.Before) + r.Equal(0, counts.CommandNotFound) + r.Equal(2, counts.Action) + r.Equal(3, counts.After) + r.Equal(3, counts.Total) + }) - resetCounts() + t.Run("before with error", func(t *testing.T) { + cmd, counts := buildCmdCounts() + cmd.Before = func(context.Context, *Command) error { + counts.Total++ + counts.Before = counts.Total + return errors.New("hay Before") + } - oldOnUsageError := cmd.OnUsageError - cmd.OnUsageError = nil - _ = cmd.Run(buildTestContext(t), []string{"command", "--nope"}) - expect(t, counts.Total, 0) - cmd.OnUsageError = oldOnUsageError + r := require.New(t) - resetCounts() + _ = cmd.Run(buildTestContext(t), []string{"command", "bar"}) + r.Equal(0, counts.OnUsageError) + r.Equal(1, counts.Before) + r.Equal(2, counts.After) + r.Equal(2, counts.Total) + }) - _ = cmd.Run(buildTestContext(t), []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() - - cmd.Before = beforeError - _ = cmd.Run(buildTestContext(t), []string{"command", "bar"}) - expect(t, counts.OnUsageError, 0) - expect(t, counts.Before, 1) - expect(t, counts.After, 2) - expect(t, counts.Total, 2) - cmd.Before = beforeNoError - - resetCounts() - - cmd.After = nil - _ = cmd.Run(buildTestContext(t), []string{"command", "bar"}) - expect(t, counts.OnUsageError, 0) - expect(t, counts.Before, 1) - expect(t, counts.SubCommand, 2) - expect(t, counts.Total, 2) - cmd.After = afterNoError - - resetCounts() - - cmd.After = afterError - err := cmd.Run(buildTestContext(t), []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) - cmd.After = afterNoError - - resetCounts() - - oldCommands := cmd.Commands - cmd.Commands = nil - _ = cmd.Run(buildTestContext(t), []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) - cmd.Commands = oldCommands + t.Run("nil after", func(t *testing.T) { + cmd, counts := buildCmdCounts() + cmd.After = nil + r := require.New(t) + + _ = cmd.Run(buildTestContext(t), []string{"command", "bar"}) + r.Equal(0, counts.OnUsageError) + r.Equal(1, counts.Before) + r.Equal(2, counts.SubCommand) + r.Equal(2, counts.Total) + }) + + t.Run("after errors", func(t *testing.T) { + cmd, counts := buildCmdCounts() + cmd.After = func(context.Context, *Command) error { + counts.Total++ + counts.After = counts.Total + return errors.New("hay After") + } + + r := require.New(t) + + err := cmd.Run(buildTestContext(t), []string{"command", "bar"}) + if err == nil { + t.Fatalf("expected a non-nil error") + } + r.Equal(0, counts.OnUsageError) + r.Equal(1, counts.Before) + r.Equal(2, counts.SubCommand) + r.Equal(3, counts.After) + r.Equal(3, counts.Total) + }) + + t.Run("nil commands", func(t *testing.T) { + cmd, counts := buildCmdCounts() + cmd.Commands = nil + r := require.New(t) + + _ = cmd.Run(buildTestContext(t), []string{"command"}) + r.Equal(0, counts.OnUsageError) + r.Equal(1, counts.Before) + r.Equal(2, counts.Action) + r.Equal(3, counts.After) + r.Equal(3, counts.Total) + }) } func TestCommand_Run_CommandWithSubcommandHasHelpTopic(t *testing.T) { diff --git a/help.go b/help.go index 1051df2cad..2ee9f7658c 100644 --- a/help.go +++ b/help.go @@ -110,7 +110,7 @@ func helpCommandAction(ctx context.Context, cmd *Command) error { } tracef("running HelpPrinter with command %[1]q", cmd.Name) - HelpPrinter(cmd.Root().Writer, tmpl, cmd.Command) + HelpPrinter(cmd.Root().Writer, tmpl, cmd) return nil } From a47982cc55b84090c7cadf9f19f63a2e794a20b8 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Tue, 27 Jun 2023 09:27:30 -0400 Subject: [PATCH 5/8] Fix short option handling test --- command.go | 2 +- command_test.go | 50 ++++++++++++++++++++++++++----------------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/command.go b/command.go index dd3bb4e974..3f3e4f98c4 100644 --- a/command.go +++ b/command.go @@ -553,7 +553,7 @@ func (cmd *Command) allFlags() []Flag { } func (cmd *Command) useShortOptionHandling() bool { - return cmd.UseShortOptionHandling + return cmd.Root().UseShortOptionHandling } func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, error) { diff --git a/command_test.go b/command_test.go index d2d4d7765b..1ee6cf9f87 100644 --- a/command_test.go +++ b/command_test.go @@ -1132,35 +1132,39 @@ func TestCommand_UseShortOptionHandlingCommand_missing_value(t *testing.T) { func TestCommand_UseShortOptionHandlingSubCommand(t *testing.T) { var one, two bool var name string - expected := "expectedName" cmd := buildMinimalTestCommand() cmd.UseShortOptionHandling = true - command := &Command{ - Name: "cmd", - } - subCommand := &Command{ - Name: "sub", - Flags: []Flag{ - &BoolFlag{Name: "one", Aliases: []string{"o"}}, - &BoolFlag{Name: "two", Aliases: []string{"t"}}, - &StringFlag{Name: "name", Aliases: []string{"n"}}, - }, - Action: func(_ context.Context, cmd *Command) error { - one = cmd.Bool("one") - two = cmd.Bool("two") - name = cmd.String("name") - return nil + cmd.Commands = []*Command{ + { + Name: "cmd", + Commands: []*Command{ + { + Name: "sub", + Flags: []Flag{ + &BoolFlag{Name: "one", Aliases: []string{"o"}}, + &BoolFlag{Name: "two", Aliases: []string{"t"}}, + &StringFlag{Name: "name", Aliases: []string{"n"}}, + }, + Action: func(_ context.Context, cmd *Command) error { + one = cmd.Bool("one") + two = cmd.Bool("two") + name = cmd.String("name") + return nil + }, + }, + }, }, } - command.Commands = []*Command{subCommand} - cmd.Commands = []*Command{command} - err := cmd.Run(buildTestContext(t), []string{"", "cmd", "sub", "-on", expected}) - expect(t, err, nil) - expect(t, one, true) - expect(t, two, false) - expect(t, name, expected) + r := require.New(t) + + expected := "expectedName" + + r.NoError(cmd.Run(buildTestContext(t), []string{"", "cmd", "sub", "-on", expected})) + r.True(one) + r.False(two) + r.Equal(expected, name) } func TestCommand_UseShortOptionHandlingSubCommand_missing_value(t *testing.T) { From 4966c5bb30057ed4bde4d40e47d669df6ab1f3df Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 28 Jun 2023 18:15:36 -0400 Subject: [PATCH 6/8] More tracing and working on busted tests --- command.go | 52 +++++++++++++++++++++++++++++++++++++----- flag_duration.go | 3 +++ flag_test.go | 59 ++++++++++++++++++++++++------------------------ parse.go | 29 +++++++++++++++++++----- 4 files changed, 101 insertions(+), 42 deletions(-) diff --git a/command.go b/command.go index 3f3e4f98c4..b032239153 100644 --- a/command.go +++ b/command.go @@ -375,7 +375,7 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) { } if err != nil { - tracef("setting deferErr from %[1]v (cmd=%[2]q)", err, cmd.Name) + tracef("setting deferErr from %[1]q (cmd=%[2]q)", err, cmd.Name) deferErr = err cmd.isInError = true @@ -537,8 +537,13 @@ func (cmd *Command) checkHelp() bool { } func (cmd *Command) newFlagSet() (*flag.FlagSet, error) { - cmd.appliedFlags = append(cmd.appliedFlags, cmd.allFlags()...) - return newFlagSet(cmd.Name, cmd.allFlags()) + allFlags := cmd.allFlags() + + cmd.appliedFlags = append(cmd.appliedFlags, allFlags...) + + tracef("making new flag set (cmd=%[1]q)", cmd.Name) + + return newFlagSet(cmd.Name, allFlags) } func (cmd *Command) allFlags() []Flag { @@ -552,8 +557,16 @@ func (cmd *Command) allFlags() []Flag { return flags } +// useShortOptionHandling traverses Lineage() for *any* ancestors +// with UseShortOptionHandling func (cmd *Command) useShortOptionHandling() bool { - return cmd.Root().UseShortOptionHandling + for _, pCmd := range cmd.Lineage() { + if pCmd.UseShortOptionHandling { + return true + } + } + + return false } func (cmd *Command) suggestFlagFromError(err error, commandName string) (string, error) { @@ -597,16 +610,32 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { return cmd.Args(), cmd.flagSet.Parse(append([]string{"--"}, args.Tail()...)) } + tracef("walking command lineage for persistent flags (cmd=%[1]q)", cmd.Name) + for pCmd := cmd.parent; pCmd != nil; pCmd = pCmd.parent { + tracef( + "checking ancestor command=%[1]q for persistent flags (cmd=%[2]q)", + pCmd.Name, cmd.Name, + ) + for _, fl := range pCmd.Flags { + flNames := fl.Names() + pfl, ok := fl.(PersistentFlag) if !ok || !pfl.IsPersistent() { + tracef("skipping non-persistent flag %[1]q (cmd=%[2]q)", flNames, cmd.Name) continue } + tracef( + "checking for applying persistent flag=%[1]q pCmd=%[2]q (cmd=%[3]q)", + flNames, pCmd.Name, cmd.Name, + ) + applyPersistentFlag := true + cmd.flagSet.VisitAll(func(f *flag.Flag) { - for _, name := range fl.Names() { + for _, name := range flNames { if name == f.Name { applyPersistentFlag = false break @@ -615,25 +644,36 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { }) if !applyPersistentFlag { + tracef("not applying as persistent flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) + continue } + tracef("applying as persistent flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) + if err := fl.Apply(cmd.flagSet); err != nil { return cmd.Args(), err } + tracef("appending to applied flags flag=%[1]q (cmd=%[2]q)", flNames, cmd.Name) cmd.appliedFlags = append(cmd.appliedFlags, fl) } } - if err := parseIter(cmd.flagSet, cmd, args.Tail(), cmd.EnableShellCompletion); err != nil { + tracef("parsing flags iteratively tail=%[1]q (cmd=%[2]q)", args.Tail(), cmd.Name) + + if err := parseIter(cmd.flagSet, cmd, args.Tail(), cmd.Root().EnableShellCompletion); err != nil { return cmd.Args(), err } + tracef("normalizing flags (cmd=%[1]q)", cmd.Name) + if err := normalizeFlags(cmd.Flags, cmd.flagSet); err != nil { return cmd.Args(), err } + tracef("done parsing flags (cmd=%[1]q)", cmd.Name) + return cmd.Args(), nil } diff --git a/flag_duration.go b/flag_duration.go index b487cf94b6..2a79458c28 100644 --- a/flag_duration.go +++ b/flag_duration.go @@ -38,7 +38,10 @@ func (d *durationValue) String() string { return (*time.Duration)(d).String() } func (cmd *Command) Duration(name string) time.Duration { if v, ok := cmd.Value(name).(time.Duration); ok { + tracef("duration available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("bool NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return 0 } diff --git a/flag_test.go b/flag_test.go index 2c62edf5ce..5c7aa5c5c4 100644 --- a/flag_test.go +++ b/flag_test.go @@ -2832,38 +2832,37 @@ func TestTimestampFlagApply_WithDestination(t *testing.T) { func TestSliceShortOptionHandle(t *testing.T) { wasCalled := false err := (&Command{ - Commands: []*Command{ - { - Name: "foobar", - UseShortOptionHandling: true, - Action: func(_ context.Context, cmd *Command) error { - wasCalled = true - if cmd.Bool("i") != true { - t.Error("bool i not set") - } - if cmd.Bool("t") != true { - t.Error("bool i not set") - } - ss := cmd.StringSlice("net") - if !reflect.DeepEqual(ss, []string{"foo"}) { - t.Errorf("Got different slice(%v) than expected", ss) - } - return nil - }, - Flags: []Flag{ - &StringSliceFlag{Name: "net"}, - &BoolFlag{Name: "i"}, - &BoolFlag{Name: "t"}, - }, - }, + Name: "foobar", + UseShortOptionHandling: true, + Action: func(_ context.Context, cmd *Command) error { + wasCalled = true + + if !cmd.Bool("i") { + return fmt.Errorf("bool i not set") + } + + if !cmd.Bool("t") { + return fmt.Errorf("bool i not set") + } + + ss := cmd.StringSlice("net") + if !reflect.DeepEqual(ss, []string{"foo"}) { + return fmt.Errorf("got different slice %q than expected", ss) + } + + return nil + }, + Flags: []Flag{ + &StringSliceFlag{Name: "net"}, + &BoolFlag{Name: "i"}, + &BoolFlag{Name: "t"}, }, }).Run(buildTestContext(t), []string{"run", "foobar", "--net=foo", "-it"}) - if err != nil { - t.Fatal(err) - } - if !wasCalled { - t.Fatal("Action callback was never called") - } + + r := require.New(t) + + r.NoError(err) + r.Truef(wasCalled, "action callback was never called") } // Test issue #1541 diff --git a/parse.go b/parse.go index d79f15a18e..8056343900 100644 --- a/parse.go +++ b/parse.go @@ -18,34 +18,50 @@ type iterativeParser interface { // completion when, the user-supplied options may be incomplete. func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComplete bool) error { for { + tracef("parsing args %[1]q with %[2]T (name=%[3]q)", args, set, set.Name()) + err := set.Parse(args) if !ip.useShortOptionHandling() || err == nil { if shellComplete { + tracef("returning nil due to shellComplete=true") + return nil } + + tracef("returning err %[1]q", err) + return err } + tracef("finding flag from error %[1]q", err) + trimmed, trimErr := flagFromError(err) if trimErr != nil { return err } - // regenerate the initial args with the split short opts + tracef("regenerating the initial args with the split short opts") + argsWereSplit := false for i, arg := range args { - // skip args that are not part of the error message + tracef("skipping args that are not part of the error message (i=%[1]v arg=%[2]q)", i, arg) + if name := strings.TrimLeft(arg, "-"); name != trimmed { continue } - // if we can't split, the error was accurate + tracef("trying to split short option (arg=%[1]q)", arg) + shortOpts := splitShortOptions(set, arg) if len(shortOpts) == 1 { return err } - // swap current argument with the split version + tracef( + "swapping current argument with the split version (shortOpts=%[1]q args=%[2]q)", + shortOpts, args, + ) + // do not include args that parsed correctly so far as it would // trigger Value.Set() on those args and would result in // duplicates for slice type flags @@ -54,8 +70,9 @@ func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComple break } - // This should be an impossible to reach code path, but in case the arg - // splitting failed to happen, this will prevent infinite loops + tracef("this should be an impossible to reach code path") + // but in case the arg splitting failed to happen, this + // will prevent infinite loops if !argsWereSplit { return err } From a5db8a9ecfbeb151674db580101ffd1b531080f8 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 28 Jun 2023 18:26:06 -0400 Subject: [PATCH 7/8] Fix remaining tests (? :crossed_fingers:) --- command_test.go | 57 +++++++++++------------------------------ flag_impl.go | 2 +- flag_test.go | 2 +- godoc-current.txt | 2 +- testdata/godoc-v3.x.txt | 2 +- 5 files changed, 19 insertions(+), 46 deletions(-) diff --git a/command_test.go b/command_test.go index d5f1ad0f15..4c1a4e8631 100644 --- a/command_test.go +++ b/command_test.go @@ -168,26 +168,27 @@ func TestCommandFlagParsing(t *testing.T) { for _, c := range cases { t.Run(strings.Join(c.testArgs, " "), func(t *testing.T) { cmd := &Command{ - Writer: io.Discard, - Commands: []*Command{ - { - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(context.Context, *Command) error { return nil }, - SkipFlagParsing: c.skipFlagParsing, - - flagSet: flag.NewFlagSet("test", 0), - }, - }, + Writer: io.Discard, + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(context.Context, *Command) error { return nil }, + SkipFlagParsing: c.skipFlagParsing, } ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) t.Cleanup(cancel) + r := require.New(t) + err := cmd.Run(ctx, c.testArgs) - require.EqualError(t, err, c.expectedErr) + + if c.expectedErr != "" { + r.EqualError(err, c.expectedErr) + } else { + r.NoError(err) + } }) } } @@ -871,34 +872,6 @@ func TestCommand_Setup_defaultsWriter(t *testing.T) { expect(t, cmd.Writer, os.Stdout) } -func TestCommand_RunAsSubcommandParseFlags(t *testing.T) { - cmd := &Command{ - Commands: []*Command{ - { - Name: "foo", - Action: func(context.Context, *Command) error { - return nil - }, - Flags: []Flag{ - &StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - }, - }, - Before: func(context.Context, *Command) error { return nil }, - }, - }, - } - - r := require.New(t) - - r.NoError(cmd.Run(buildTestContext(t), []string{"", "foo", "--lang", "spanish", "abcd"})) - - r.Equal("abcd", cmd.Args().Get(0)) - r.Equal("spanish", cmd.String("lang")) -} - func TestCommand_CommandWithFlagBeforeTerminator(t *testing.T) { var parsedOption string var args Args diff --git a/flag_impl.go b/flag_impl.go index 63863c9c3b..e4930195b9 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -263,7 +263,7 @@ func (f *FlagBase[T, C, V]) GetDefaultText() string { return v.ToString(f.Value) } -// Get returns the flag’s value in the given Context. +// Get returns the flag’s value in the given Command. func (f *FlagBase[T, C, V]) Get(cmd *Command) T { if v, ok := cmd.Value(f.Name).(T); ok { return v diff --git a/flag_test.go b/flag_test.go index 5c7aa5c5c4..d973c471de 100644 --- a/flag_test.go +++ b/flag_test.go @@ -2857,7 +2857,7 @@ func TestSliceShortOptionHandle(t *testing.T) { &BoolFlag{Name: "i"}, &BoolFlag{Name: "t"}, }, - }).Run(buildTestContext(t), []string{"run", "foobar", "--net=foo", "-it"}) + }).Run(buildTestContext(t), []string{"foobar", "--net=foo", "-it"}) r := require.New(t) diff --git a/godoc-current.txt b/godoc-current.txt index fcf502e20f..5229697ea0 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -646,7 +646,7 @@ func (f *FlagBase[T, C, V]) Apply(set *flag.FlagSet) error Apply populates the flag given the flag set and environment func (f *FlagBase[T, C, V]) Get(cmd *Command) T - Get returns the flag’s value in the given Context. + Get returns the flag’s value in the given Command. func (f *FlagBase[T, C, V]) GetCategory() string GetCategory returns the category of the flag diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index fcf502e20f..5229697ea0 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -646,7 +646,7 @@ func (f *FlagBase[T, C, V]) Apply(set *flag.FlagSet) error Apply populates the flag given the flag set and environment func (f *FlagBase[T, C, V]) Get(cmd *Command) T - Get returns the flag’s value in the given Context. + Get returns the flag’s value in the given Command. func (f *FlagBase[T, C, V]) GetCategory() string GetCategory returns the category of the flag From ef500f2b73d17911c86a3799ba5d7ac4cb9de906 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 28 Jun 2023 18:36:24 -0400 Subject: [PATCH 8/8] Fill in tracing on command flag value accessor funcs --- flag_float.go | 3 +++ flag_float_slice.go | 18 +++++++++++------- flag_int.go | 3 +++ flag_int_slice.go | 2 ++ flag_string.go | 3 +++ flag_string_map.go | 3 +++ flag_string_slice.go | 3 +++ flag_timestamp.go | 17 ++++++++++++----- flag_uint.go | 3 +++ flag_uint_slice.go | 3 +++ 10 files changed, 46 insertions(+), 12 deletions(-) diff --git a/flag_float.go b/flag_float.go index 7dbe8eabb9..058440fccc 100644 --- a/flag_float.go +++ b/flag_float.go @@ -39,7 +39,10 @@ func (f *floatValue) String() string { return strconv.FormatFloat(float64(*f), ' // 0 if not found func (cmd *Command) Float(name string) float64 { if v, ok := cmd.Value(name).(float64); ok { + tracef("float available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("float NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return 0 } diff --git a/flag_float_slice.go b/flag_float_slice.go index ac89bee2c4..837e911da8 100644 --- a/flag_float_slice.go +++ b/flag_float_slice.go @@ -12,18 +12,22 @@ var NewFloatSlice = NewSliceBase[float64, NoConfig, floatValue] // FloatSlice looks up the value of a local FloatSliceFlag, returns // nil if not found func (cmd *Command) FloatSlice(name string) []float64 { - if fs := cmd.lookupFlagSet(name); fs != nil { - return lookupFloatSlice(name, fs) + if flSet := cmd.lookupFlagSet(name); flSet != nil { + return lookupFloatSlice(name, flSet, cmd.Name) } + return nil } -func lookupFloatSlice(name string, set *flag.FlagSet) []float64 { - f := set.Lookup(name) - if f != nil { - if slice, ok := f.Value.(flag.Getter).Get().([]float64); ok { - return slice +func lookupFloatSlice(name string, set *flag.FlagSet, cmdName string) []float64 { + fl := set.Lookup(name) + if fl != nil { + if v, ok := fl.Value.(flag.Getter).Get().([]float64); ok { + tracef("float slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmdName) + return v } } + + tracef("float slice NOT available for flag name %[1]q (cmd=%[2]q)", name, cmdName) return nil } diff --git a/flag_int.go b/flag_int.go index 384cc80cdf..4403dae0f2 100644 --- a/flag_int.go +++ b/flag_int.go @@ -50,7 +50,10 @@ func (i *intValue) String() string { return strconv.FormatInt(int64(*i.val), 10) // 0 if not found func (cmd *Command) Int(name string) int64 { if v, ok := cmd.Value(name).(int64); ok { + tracef("int available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("int NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return 0 } diff --git a/flag_int_slice.go b/flag_int_slice.go index c30af56a3e..24bb2616eb 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -9,8 +9,10 @@ var NewIntSlice = NewSliceBase[int64, IntegerConfig, intValue] // nil if not found func (cmd *Command) IntSlice(name string) []int64 { if v, ok := cmd.Value(name).([]int64); ok { + tracef("int slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + tracef("int slice NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } diff --git a/flag_string.go b/flag_string.go index 809063a9b3..c56c99b5af 100644 --- a/flag_string.go +++ b/flag_string.go @@ -57,7 +57,10 @@ func (s *stringValue) String() string { func (cmd *Command) String(name string) string { if v, ok := cmd.Value(name).(string); ok { + tracef("string available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("string NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return "" } diff --git a/flag_string_map.go b/flag_string_map.go index f4fd9b335a..62dfb34c32 100644 --- a/flag_string_map.go +++ b/flag_string_map.go @@ -9,7 +9,10 @@ var NewStringMap = NewMapBase[string, StringConfig, stringValue] // nil if not found func (cmd *Command) StringMap(name string) map[string]string { if v, ok := cmd.Value(name).(map[string]string); ok { + tracef("string map available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("string map NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } diff --git a/flag_string_slice.go b/flag_string_slice.go index 7590d3fbcd..d2ae095d84 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -9,7 +9,10 @@ var NewStringSlice = NewSliceBase[string, StringConfig, stringValue] // nil if not found func (cmd *Command) StringSlice(name string) []string { if v, ok := cmd.Value(name).([]string); ok { + tracef("string slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("string slice NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil } diff --git a/flag_timestamp.go b/flag_timestamp.go index 18e811c4cd..6c5cf963a1 100644 --- a/flag_timestamp.go +++ b/flag_timestamp.go @@ -87,16 +87,23 @@ func (t *timestampValue) Get() any { // Timestamp gets the timestamp from a flag name func (cmd *Command) Timestamp(name string) *time.Time { if fs := cmd.lookupFlagSet(name); fs != nil { - return lookupTimestamp(name, fs) + return lookupTimestamp(name, fs, cmd.Name) } return nil } // Fetches the timestamp value from the local timestampWrap -func lookupTimestamp(name string, set *flag.FlagSet) *time.Time { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*timestampValue)).Value() +func lookupTimestamp(name string, set *flag.FlagSet, cmdName string) *time.Time { + fl := set.Lookup(name) + if fl != nil { + if tv, ok := fl.Value.(*timestampValue); ok { + v := tv.Value() + + tracef("timestamp available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmdName) + return v + } } + + tracef("timestamp NOT available for flag name %[1]q (cmd=%[2]q)", name, cmdName) return nil } diff --git a/flag_uint.go b/flag_uint.go index d722f10671..1f3c82da6d 100644 --- a/flag_uint.go +++ b/flag_uint.go @@ -45,7 +45,10 @@ func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i.val), // 0 if not found func (cmd *Command) Uint(name string) uint64 { if v, ok := cmd.Value(name).(uint64); ok { + tracef("uint available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("uint NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return 0 } diff --git a/flag_uint_slice.go b/flag_uint_slice.go index ac60c7fcc5..cce21ef3f7 100644 --- a/flag_uint_slice.go +++ b/flag_uint_slice.go @@ -9,7 +9,10 @@ var NewUintSlice = NewSliceBase[uint64, IntegerConfig, uintValue] // nil if not found func (cmd *Command) UintSlice(name string) []uint64 { if v, ok := cmd.Value(name).([]uint64); ok { + tracef("uint slice available for flag name %[1]q with value=%[2]v (cmd=%[3]q)", name, v, cmd.Name) return v } + + tracef("uint slice NOT available for flag name %[1]q (cmd=%[2]q)", name, cmd.Name) return nil }