From 827f487c48ff7aba4ec40a0e1d6dd084cbbaa849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=BCger?= Date: Mon, 10 Aug 2026 17:50:32 +0200 Subject: [PATCH] Fix index out of range panic when Run is given no arguments Command.Run indexes into the argument slice in two places without first checking that it is non-empty, so passing an empty slice panics rather than returning: * setupDefaults reads osArgs[0] to derive a default command name, panicking with "index out of range [0] with length 0" for any root command that does not set Name explicitly. * checkShellCompleteFlag reads arguments[len(arguments)-1], panicking with "index out of range [-1]" whenever EnableShellCompletion is set. Guard both. A command with no arguments now runs its Action, and simply keeps an empty Name when there is no argv[0] to derive one from. Co-Authored-By: Claude Opus 5 (1M context) --- command_setup.go | 2 +- command_test.go | 26 ++++++++++++++++++++++++++ help.go | 4 ++++ help_test.go | 9 +++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/command_setup.go b/command_setup.go index 646e270ce7..51c4040039 100644 --- a/command_setup.go +++ b/command_setup.go @@ -24,7 +24,7 @@ func (cmd *Command) setupDefaults(osArgs []string) { cmd.ShellComplete = DefaultCompleteWithFlags } - if cmd.Name == "" && isRoot { + if cmd.Name == "" && isRoot && len(osArgs) > 0 { name := filepath.Base(osArgs[0]) tracef("setting cmd.Name from first arg basename (cmd=%[1]q)", name) cmd.Name = name diff --git a/command_test.go b/command_test.go index cad70b7801..8ee7facbdc 100644 --- a/command_test.go +++ b/command_test.go @@ -6450,3 +6450,29 @@ func TestCommand_Walk_NilFn(t *testing.T) { cmd := &Command{Name: "foo"} assert.Nil(t, cmd.Walk(nil)) } + +// TestRunWithNoOsArgs checks that Run does not panic when handed an empty +// argument slice. +func TestRunWithNoOsArgs(t *testing.T) { + for _, tst := range []struct { + name string + cmd *Command + }{ + {name: "plain", cmd: &Command{Name: "foo"}}, + {name: "shell completion enabled", cmd: &Command{Name: "foo", EnableShellCompletion: true}}, + {name: "no name", cmd: &Command{}}, + } { + t.Run(tst.name, func(t *testing.T) { + called := false + tst.cmd.Action = func(context.Context, *Command) error { + called = true + return nil + } + + require.NotPanics(t, func() { + require.NoError(t, tst.cmd.Run(buildTestContext(t), []string{})) + }) + assert.True(t, called) + }) + } +} diff --git a/help.go b/help.go index 4bedf87d5d..2bb53b3e90 100644 --- a/help.go +++ b/help.go @@ -476,6 +476,10 @@ func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) { return false, arguments } + if len(arguments) == 0 { + return false, arguments + } + pos := len(arguments) - 1 lastArg := arguments[pos] diff --git a/help_test.go b/help_test.go index d3c831371d..11fbf09409 100644 --- a/help_test.go +++ b/help_test.go @@ -1945,6 +1945,15 @@ func Test_checkShellCompleteFlag(t *testing.T) { wantShellCompletion: true, wantArgs: []string{"foo", "--"}, }, + { + name: "no arguments at all", + arguments: []string{}, + cmd: &Command{ + EnableShellCompletion: true, + }, + wantShellCompletion: false, + wantArgs: []string{}, + }, } for _, tt := range tests {