← back to Cli Printing Press
fix(cli): stop generated main.go from printing every error twice (#1411)
46ad86b2b287dc18cbab40b5dfe0997a52e7a3be · 2026-05-14 15:05:13 -0700 · Trevin Chow
* fix(cli): stop generated main.go from printing every error twice
Every printed CLI's cmd/<cli>/main.go printed each error twice — once
from Cobra (default Error: prefix) and once from main.go's explicit
fmt.Fprintln, because root.go.tmpl set SilenceUsage:true but not
SilenceErrors. Drop the redundant Fprintln (and now-unused fmt import)
in main.go.tmpl so Cobra owns the single error print.
The unknown-flag hint path in root.go.tmpl previously rode on that
second print to surface "hint: did you mean --foo?" — Cobra prints err
before the wrap attaches the hint, so without main.go re-printing, the
suggestion would disappear. Emit the hint to stderr directly in
root.go.tmpl while keeping the existing wrap (which the usageErr
classification and isCobraUsageError tests depend on).
Closes #1407
* fix(cli): drop API names from regression-test comment
Greptile flagged the schoology/epic-myhealth references in the test
header as customer/project name leakage. AGENTS.md already forbids
referencing the current task or specific past incidents in code
comments, so rewrite the why-this-test prose without naming any
specific CLI.
Files touched
A internal/generator/main_no_double_print_test.goM internal/generator/templates/main.go.tmplM internal/generator/templates/root.go.tmplM testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/cmd/cafe-bistro-pp-cli/main.goM testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go
Diff
commit 46ad86b2b287dc18cbab40b5dfe0997a52e7a3be
Author: Trevin Chow <trevin@trevinchow.com>
Date: Thu May 14 15:05:13 2026 -0700
fix(cli): stop generated main.go from printing every error twice (#1411)
* fix(cli): stop generated main.go from printing every error twice
Every printed CLI's cmd/<cli>/main.go printed each error twice — once
from Cobra (default Error: prefix) and once from main.go's explicit
fmt.Fprintln, because root.go.tmpl set SilenceUsage:true but not
SilenceErrors. Drop the redundant Fprintln (and now-unused fmt import)
in main.go.tmpl so Cobra owns the single error print.
The unknown-flag hint path in root.go.tmpl previously rode on that
second print to surface "hint: did you mean --foo?" — Cobra prints err
before the wrap attaches the hint, so without main.go re-printing, the
suggestion would disappear. Emit the hint to stderr directly in
root.go.tmpl while keeping the existing wrap (which the usageErr
classification and isCobraUsageError tests depend on).
Closes #1407
* fix(cli): drop API names from regression-test comment
Greptile flagged the schoology/epic-myhealth references in the test
header as customer/project name leakage. AGENTS.md already forbids
referencing the current task or specific past incidents in code
comments, so rewrite the why-this-test prose without naming any
specific CLI.
---
internal/generator/main_no_double_print_test.go | 68 ++++++++++++++++++++++
internal/generator/templates/main.go.tmpl | 2 -
internal/generator/templates/root.go.tmpl | 7 +++
.../cafe-bistro/cmd/cafe-bistro-pp-cli/main.go | 2 -
.../cmd/printing-press-golden-pp-cli/main.go | 2 -
.../printing-press-golden/internal/cli/root.go | 7 +++
6 files changed, 82 insertions(+), 6 deletions(-)
diff --git a/internal/generator/main_no_double_print_test.go b/internal/generator/main_no_double_print_test.go
new file mode 100644
index 00000000..69d6baac
--- /dev/null
+++ b/internal/generator/main_no_double_print_test.go
@@ -0,0 +1,68 @@
+package generator
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+// TestMainNoDoublePrintErrors asserts the generated cmd/<cli>/main.go does
+// not print err.Error() to stderr. root.go.tmpl leaves Cobra's default
+// error printing on (SilenceUsage:true but no SilenceErrors), so a
+// second main.go-side print would emit every failing command's message
+// twice — once with Cobra's "Error:" prefix and once as the raw text.
+// Pin the structural shape here: no `fmt` import in main.go, no
+// `fmt.Fprintln(os.Stderr, err.Error())` call. Behavior is covered
+// by the emitted root.go's own tests (root_test.go.tmpl).
+func TestMainNoDoublePrintErrors(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("nodbl-canary")
+ outputDir := filepath.Join(t.TempDir(), "nodbl-canary-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ mainPath := filepath.Join(outputDir, "cmd", "nodbl-canary-pp-cli", "main.go")
+ mainSrc, err := os.ReadFile(mainPath)
+ require.NoError(t, err, "main.go must be emitted")
+ src := string(mainSrc)
+
+ require.NotContains(t, src, `"fmt"`,
+ `main.go must not import "fmt" — the only prior use was the double-print line`)
+ require.NotContains(t, src, "fmt.Fprintln(os.Stderr, err.Error())",
+ "main.go must not print err.Error(); Cobra prints it once via SilenceUsage=true/SilenceErrors=false")
+ require.NotContains(t, src, "fmt.Fprint(os.Stderr, err.Error())",
+ "main.go must not print err.Error() via Fprint either")
+
+ // Exit-code propagation must still flow through cli.ExitCode so
+ // usageErr → 2, configErr → 10, authErr → 5 stay intact.
+ require.Contains(t, src, "os.Exit(cli.ExitCode(err))",
+ "main.go must still propagate the typed exit code")
+}
+
+// TestRootEmitsUnknownFlagHintToStderr asserts root.go.tmpl emits an
+// explicit stderr write of the suggestion alongside the err wrap. Before
+// this fix, the hint was only ever visible because main.go re-printed
+// err.Error(); now that main.go is silent, Cobra prints `Error: unknown
+// flag: --foob` (without the hint, since Cobra prints before the wrap
+// happens), and root.go owns surfacing the hint.
+func TestRootEmitsUnknownFlagHintToStderr(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("hint-canary")
+ outputDir := filepath.Join(t.TempDir(), "hint-canary-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ rootSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "root.go"))
+ require.NoError(t, err, "root.go must be emitted")
+ src := string(rootSrc)
+
+ require.Contains(t, src, `fmt.Fprintf(os.Stderr, "hint: did you mean --%s?\n", suggestion)`,
+ "root.go must print the unknown-flag hint to stderr directly; main.go no longer does")
+ // And the wrap still has to happen so the err.Error() chain carries
+ // the hint for downstream consumers and isCobraUsageError keeps its
+ // classification (covered by TestUsageErrorExitCode_EmittedInRoot).
+ require.Contains(t, src, `err = fmt.Errorf("%w\nhint: did you mean --%s?", err, suggestion)`,
+ "root.go must still wrap err with the hint for downstream consumers")
+}
diff --git a/internal/generator/templates/main.go.tmpl b/internal/generator/templates/main.go.tmpl
index 8cef8696..1a60706b 100644
--- a/internal/generator/templates/main.go.tmpl
+++ b/internal/generator/templates/main.go.tmpl
@@ -4,7 +4,6 @@
package main
import (
- "fmt"
"os"
"{{modulePath}}/internal/cli"
@@ -12,7 +11,6 @@ import (
func main() {
if err := cli.Execute(); err != nil {
- fmt.Fprintln(os.Stderr, err.Error())
os.Exit(cli.ExitCode(err))
}
}
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index c41075ce..66cc0807 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -71,6 +71,13 @@ func Execute() error {
if idx := strings.Index(msg, "unknown flag: "); idx >= 0 {
flagStr := strings.TrimSpace(msg[idx+len("unknown flag: "):])
if suggestion := suggestFlag(flagStr, rootCmd); suggestion != "" {
+ // Cobra already printed `Error: unknown flag: --foob` before
+ // returning; the wrap below attaches the hint to err.Error()
+ // for downstream consumers and exit-code classification, but
+ // would never reach stderr now that main.go no longer prints
+ // err. Emit the hint explicitly so the suggestion still
+ // shows up under Cobra's error line.
+ fmt.Fprintf(os.Stderr, "hint: did you mean --%s?\n", suggestion)
err = fmt.Errorf("%w\nhint: did you mean --%s?", err, suggestion)
}
}
diff --git a/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/cmd/cafe-bistro-pp-cli/main.go b/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/cmd/cafe-bistro-pp-cli/main.go
index d7b5d06c..2bd24ffb 100644
--- a/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/cmd/cafe-bistro-pp-cli/main.go
+++ b/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/cmd/cafe-bistro-pp-cli/main.go
@@ -4,7 +4,6 @@
package main
import (
- "fmt"
"os"
"cafe-bistro-pp-cli/internal/cli"
@@ -12,7 +11,6 @@ import (
func main() {
if err := cli.Execute(); err != nil {
- fmt.Fprintln(os.Stderr, err.Error())
os.Exit(cli.ExitCode(err))
}
}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.go
index 6a5aa9d7..ad4c70bb 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.go
@@ -4,7 +4,6 @@
package main
import (
- "fmt"
"os"
"printing-press-golden-pp-cli/internal/cli"
@@ -12,7 +11,6 @@ import (
func main() {
if err := cli.Execute(); err != nil {
- fmt.Fprintln(os.Stderr, err.Error())
os.Exit(cli.ExitCode(err))
}
}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go
index efa2ef80..5c19c268 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go
@@ -65,6 +65,13 @@ func Execute() error {
if idx := strings.Index(msg, "unknown flag: "); idx >= 0 {
flagStr := strings.TrimSpace(msg[idx+len("unknown flag: "):])
if suggestion := suggestFlag(flagStr, rootCmd); suggestion != "" {
+ // Cobra already printed `Error: unknown flag: --foob` before
+ // returning; the wrap below attaches the hint to err.Error()
+ // for downstream consumers and exit-code classification, but
+ // would never reach stderr now that main.go no longer prints
+ // err. Emit the hint explicitly so the suggestion still
+ // shows up under Cobra's error line.
+ fmt.Fprintf(os.Stderr, "hint: did you mean --%s?\n", suggestion)
err = fmt.Errorf("%w\nhint: did you mean --%s?", err, suggestion)
}
}
← d0226602 fix(cli): derive doctor probe path from auth.verify_path or
·
back to Cli Printing Press
·
docs(cli): clarify issue ownership workflow (#1414) 770b65a6 →