From 9066fe6d989d245509287603caad2526b78b7d33 Mon Sep 17 00:00:00 2001 From: seipan Date: Tue, 19 Aug 2025 22:58:15 +0900 Subject: [PATCH 1/3] feat: run Before for help if EnableBeforeForHelp Signed-off-by: seipan --- command.go | 2 + command_run.go | 20 ++++++ command_test.go | 157 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) diff --git a/command.go b/command.go index 3eebbaff76..42faf03446 100644 --- a/command.go +++ b/command.go @@ -48,6 +48,8 @@ type Command struct { HideHelp bool `json:"hideHelp"` // Ignored if HideHelp is true. HideHelpCommand bool `json:"hideHelpCommand"` + // Boolean to enable Before function execution before showing help + EnableBeforeForHelp bool `json:"enableBeforeForHelp"` // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool `json:"hideVersion"` // Boolean to enable shell completion commands diff --git a/command_run.go b/command_run.go index 282e9d109d..2eb135df72 100644 --- a/command_run.go +++ b/command_run.go @@ -196,6 +196,26 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context } if cmd.checkHelp() { + if cmd.Root().EnableBeforeForHelp { + var cmdChain []*Command + for p := cmd; p != nil; p = p.parent { + cmdChain = append(cmdChain, p) + } + slices.Reverse(cmdChain) + + for _, c := range cmdChain { + if c.Before == nil { + continue + } + if bctx, err := c.Before(ctx, c); err != nil { + deferErr = cmd.handleExitCoder(ctx, err) + return ctx, deferErr + } else if bctx != nil { + ctx = bctx + } + } + } + return ctx, helpCommandAction(ctx, cmd) } else { tracef("no help is wanted (cmd=%[1]q)", cmd.Name) diff --git a/command_test.go b/command_test.go index 4b04706568..ada8c6f375 100644 --- a/command_test.go +++ b/command_test.go @@ -1615,6 +1615,155 @@ func TestCommand_BeforeFuncPersistentFlag(t *testing.T) { assert.Equal(t, 1, counts.SubCommand, "Subcommand not executed when expected") } +func TestCommand_BeforeWithHelpCommand(t *testing.T) { + counts := &opCounts{} + + cmd := &Command{ + Name: "testapp", + EnableBeforeForHelp: true, + Before: func(ctx context.Context, cmd *Command) (context.Context, error) { + counts.Before++ + return ctx, nil + }, + Commands: []*Command{ + { + Name: "subcmd", + Before: func(ctx context.Context, cmd *Command) (context.Context, error) { + counts.SubCommand++ + return ctx, nil + }, + Action: func(context.Context, *Command) error { + return nil + }, + }, + }, + Writer: io.Discard, + } + + testCases := []struct { + name string + args []string + expected opCounts + }{ + { + name: "help command should execute Before functions", + args: []string{"testapp", "help"}, + expected: opCounts{Before: 1, SubCommand: 0}, + }, + { + name: "help flag should execute Before functions", + args: []string{"testapp", "--help"}, + expected: opCounts{Before: 1, SubCommand: 0}, + }, + { + name: "help flag short form should execute Before functions", + args: []string{"testapp", "-h"}, + expected: opCounts{Before: 1, SubCommand: 0}, + }, + { + name: "subcommand help command should execute Before functions", + args: []string{"testapp", "subcmd", "help"}, + expected: opCounts{Before: 1, SubCommand: 1}, + }, + { + name: "subcommand help flag should execute Before functions", + args: []string{"testapp", "subcmd", "--help"}, + expected: opCounts{Before: 1, SubCommand: 1}, + }, + { + name: "subcommand help flag short should execute Before functions", + args: []string{"testapp", "subcmd", "-h"}, + expected: opCounts{Before: 1, SubCommand: 1}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + *counts = opCounts{} + + err := cmd.Run(buildTestContext(t), tc.args) + require.NoError(t, err) + + assert.Equal(t, tc.expected.Before, counts.Before, "Before() not executed expected number of times") + assert.Equal(t, tc.expected.SubCommand, counts.SubCommand, "SubCommand Before() not executed expected number of times") + }) + } +} + +func TestCommand_BeforeWithHelpCommand_DefaultBehavior(t *testing.T) { + counts := &opCounts{} + + cmd := &Command{ + Name: "testapp", + Before: func(ctx context.Context, cmd *Command) (context.Context, error) { + counts.Before++ + return ctx, nil + }, + Commands: []*Command{ + { + Name: "subcmd", + Before: func(ctx context.Context, cmd *Command) (context.Context, error) { + counts.SubCommand++ + return ctx, nil + }, + Action: func(context.Context, *Command) error { + return nil + }, + }, + }, + Writer: io.Discard, + } + + testCases := []struct { + name string + args []string + expected opCounts + }{ + { + name: "help command executes Before functions (normal command behavior)", + args: []string{"testapp", "help"}, + expected: opCounts{Before: 1, SubCommand: 0}, + }, + { + name: "help flag should NOT execute Before functions by default", + args: []string{"testapp", "--help"}, + expected: opCounts{Before: 0, SubCommand: 0}, + }, + { + name: "help flag short form should NOT execute Before functions by default", + args: []string{"testapp", "-h"}, + expected: opCounts{Before: 0, SubCommand: 0}, + }, + { + name: "subcommand help command executes Before functions (normal command behavior)", + args: []string{"testapp", "subcmd", "help"}, + expected: opCounts{Before: 1, SubCommand: 1}, + }, + { + name: "subcommand help flag should NOT execute Before functions by default", + args: []string{"testapp", "subcmd", "--help"}, + expected: opCounts{Before: 0, SubCommand: 0}, + }, + { + name: "subcommand help flag short should NOT execute Before functions by default", + args: []string{"testapp", "subcmd", "-h"}, + expected: opCounts{Before: 0, SubCommand: 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + *counts = opCounts{} + + err := cmd.Run(buildTestContext(t), tc.args) + require.NoError(t, err) + + assert.Equal(t, tc.expected.Before, counts.Before, "Before() execution count mismatch") + assert.Equal(t, tc.expected.SubCommand, counts.SubCommand, "SubCommand Before() execution count mismatch") + }) + } +} + func TestCommand_BeforeAfterFuncShellCompletion(t *testing.T) { t.Skip("TODO: is '--generate-shell-completion' (flag) still supported?") @@ -4747,6 +4896,7 @@ func TestJSONExportCommand(t *testing.T) { ], "hideHelp": false, "hideHelpCommand": false, + "enableBeforeForHelp": false, "hideVersion": false, "hidden": false, "authors": null, @@ -4810,6 +4960,7 @@ func TestJSONExportCommand(t *testing.T) { ], "hideHelp": false, "hideHelpCommand": false, + "enableBeforeForHelp": false, "hideVersion": false, "hidden": false, "authors": null, @@ -4844,6 +4995,7 @@ func TestJSONExportCommand(t *testing.T) { "flags": null, "hideHelp": false, "hideHelpCommand": false, + "enableBeforeForHelp": false, "hideVersion": false, "hidden": false, "authors": null, @@ -4875,6 +5027,7 @@ func TestJSONExportCommand(t *testing.T) { "flags": null, "hideHelp": false, "hideHelpCommand": false, + "enableBeforeForHelp": false, "hideVersion": false, "hidden": false, "authors": null, @@ -4925,6 +5078,7 @@ func TestJSONExportCommand(t *testing.T) { ], "hideHelp": false, "hideHelpCommand": false, + "enableBeforeForHelp": false, "hideVersion": false, "hidden": true, "authors": null, @@ -4992,6 +5146,7 @@ func TestJSONExportCommand(t *testing.T) { ], "hideHelp": false, "hideHelpCommand": false, + "enableBeforeForHelp": false, "hideVersion": false, "hidden": false, "authors": null, @@ -5055,6 +5210,7 @@ func TestJSONExportCommand(t *testing.T) { ], "hideHelp": false, "hideHelpCommand": false, + "enableBeforeForHelp": false, "hideVersion": false, "hidden": false, "authors": null, @@ -5156,6 +5312,7 @@ func TestJSONExportCommand(t *testing.T) { ], "hideHelp": false, "hideHelpCommand": false, + "enableBeforeForHelp": false, "hideVersion": false, "hidden": false, "authors": [ From ef2e7fc3047178b17647f1f2dbfdd8a40086d9da Mon Sep 17 00:00:00 2001 From: seipan Date: Tue, 19 Aug 2025 23:13:17 +0900 Subject: [PATCH 2/3] fix: make generate Signed-off-by: seipan --- godoc-current.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/godoc-current.txt b/godoc-current.txt index 46fb4d43aa..0f7d9b6656 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -449,6 +449,8 @@ type Command struct { HideHelp bool `json:"hideHelp"` // Ignored if HideHelp is true. HideHelpCommand bool `json:"hideHelpCommand"` + // Boolean to enable Before function execution before showing help + EnableBeforeForHelp bool `json:"enableBeforeForHelp"` // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool `json:"hideVersion"` // Boolean to enable shell completion commands From baf37f6c961f2e0dae539137a6d1f4e4a099f22b Mon Sep 17 00:00:00 2001 From: seipan Date: Tue, 19 Aug 2025 23:17:29 +0900 Subject: [PATCH 3/3] fix: make v3approve Signed-off-by: seipan --- testdata/godoc-v3.x.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 46fb4d43aa..0f7d9b6656 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -449,6 +449,8 @@ type Command struct { HideHelp bool `json:"hideHelp"` // Ignored if HideHelp is true. HideHelpCommand bool `json:"hideHelpCommand"` + // Boolean to enable Before function execution before showing help + EnableBeforeForHelp bool `json:"enableBeforeForHelp"` // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool `json:"hideVersion"` // Boolean to enable shell completion commands