From 34a2f682a536b162cd85ec8581b0b4d3dc77173b Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 29 Jun 2026 03:21:00 +0200 Subject: [PATCH] fix(cmd): print only the error on CLI usage errors cli v3.10 dumps the full command help to stdout on a usage error (unknown flag, missing required flag, etc.). Take over that reporting via OnUsageError so usage errors print only "Incorrect Usage: " to stderr and nothing to stdout, covering the auto-built completion subtree through ConfigureShellCompletionCommand. Also stop RunMainApp from printing the error a second time for non-unknown-flag usage errors. Assisted-by: Claude:Opus-4.8 --- cmd/cmdtest/cmd_test.go | 4 ++-- cmd/main.go | 29 +++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/cmd/cmdtest/cmd_test.go b/cmd/cmdtest/cmd_test.go index db6edae316b..18aaebe6e8c 100644 --- a/cmd/cmdtest/cmd_test.go +++ b/cmd/cmdtest/cmd_test.go @@ -202,8 +202,8 @@ func TestCliCmdError(t *testing.T) { r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such") assert.Error(t, err) assert.Equal(t, 1, r.ExitCode) - assert.NotEmpty(t, r.Stdout) // since cli v3.10, the usage help is printed to stdout on a usage error - assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr) + assert.Empty(t, r.Stdout) + assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n", r.Stderr) app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }}) r, err = runTestApp(app, "./gitea", "test-cmd") diff --git a/cmd/main.go b/cmd/main.go index 243995d634b..104c58121cf 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,10 +5,10 @@ package cmd import ( "context" + "errors" "fmt" "io" "os" - "strings" "gitea.dev/modules/log" "gitea.dev/modules/setting" @@ -154,16 +154,37 @@ func NewMainApp(appVer AppVersion) *cli.Command { return app } +// usageErr marks a usage error already reported by cliOnUsageError, so RunMainApp does not print it again. +type usageErr struct{ err error } + +func (e usageErr) Error() string { return e.err.Error() } +func (e usageErr) Unwrap() error { return e.err } + +// cliOnUsageError reports usage errors itself instead of letting cli dump the full help to stdout (since cli v3.10). +func cliOnUsageError(_ context.Context, cmd *cli.Command, err error, _ bool) error { + _, _ = fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n", err.Error()) + return usageErr{err} +} + +func setCLIOnUsageError(cmd *cli.Command) { + _ = cmd.Walk(func(c *cli.Command) error { + c.OnUsageError = cliOnUsageError + return nil + }) +} + func RunMainApp(app *cli.Command, args ...string) error { ctx, cancel := installSignals() defer cancel() + setCLIOnUsageError(app) + // the completion subcommands are built during app.Run, after the Walk above, so cover them via this hook + app.ConfigureShellCompletionCommand = setCLIOnUsageError err := app.Run(ctx, args) if err == nil { return nil } - if strings.HasPrefix(err.Error(), "flag provided but not defined:") { - // the cli package should already have output the error message, so just exit - cli.OsExiter(1) + if ue := (usageErr{}); errors.As(err, &ue) { + cli.OsExiter(1) // cliOnUsageError already reported it return err } _, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err)