[object Object]

← back to Cli Printing Press

fix(cli): match both singular and plural required-flag cobra outputs (#1413)

f5ea270df7583160348d1d4f83fe4798fbde153b · 2026-05-16 13:25:49 -0400 · klatt42

isCobraUsageError matched only the plural cobra form (`required flag(s)
"foo" not set`). Cobra emits the SINGULAR form (`required flag "foo"
not set`, no parens) when exactly one MarkFlagRequired flag is missing
on the command, and only switches to plural when multiple are missing
simultaneously. The single-flag case is by far the more common one in
practice.

Result before this fix: a CLI with one required flag, invoked without
it, exits 1 instead of the documented exit-2 usage convention from
#1194. The unknown-flag / unknown-command / flag-needs-argument paths
all worked correctly; only this single edge case fell through.

Adds the singular pattern alongside the plural one. Both stay anchored
to the literal trailing-space-quote (`required flag "`) so application
errors mentioning "required flag" as prose still classify correctly
(the prose-as-non-usage negative test continues to pass).

Discovered while regenerating rok-brain-cli against v4.6.1 — the
single-required-flag case in Brain's `query --query "..."` command
surfaced the gap because `query` has exactly one MarkFlagRequired flag.

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Files touched

Diff

commit f5ea270df7583160348d1d4f83fe4798fbde153b
Author: klatt42 <134792614+klatt42@users.noreply.github.com>
Date:   Sat May 16 13:25:49 2026 -0400

    fix(cli): match both singular and plural required-flag cobra outputs (#1413)
    
    isCobraUsageError matched only the plural cobra form (`required flag(s)
    "foo" not set`). Cobra emits the SINGULAR form (`required flag "foo"
    not set`, no parens) when exactly one MarkFlagRequired flag is missing
    on the command, and only switches to plural when multiple are missing
    simultaneously. The single-flag case is by far the more common one in
    practice.
    
    Result before this fix: a CLI with one required flag, invoked without
    it, exits 1 instead of the documented exit-2 usage convention from
    #1194. The unknown-flag / unknown-command / flag-needs-argument paths
    all worked correctly; only this single edge case fell through.
    
    Adds the singular pattern alongside the plural one. Both stay anchored
    to the literal trailing-space-quote (`required flag "`) so application
    errors mentioning "required flag" as prose still classify correctly
    (the prose-as-non-usage negative test continues to pass).
    
    Discovered while regenerating rok-brain-cli against v4.6.1 — the
    single-required-flag case in Brain's `query --query "..."` command
    surfaced the gap because `query` has exactly one MarkFlagRequired flag.
    
    Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
 internal/generator/templates/root.go.tmpl                     | 11 ++++++++++-
 internal/generator/templates/root_test.go.tmpl                |  3 ++-
 .../printing-press-rich-auth/internal/cli/root_test.go        |  3 ++-
 .../printing-press-golden/internal/cli/root.go                | 11 ++++++++++-
 .../printing-press-golden/internal/cli/root_test.go           |  3 ++-
 5 files changed, 26 insertions(+), 5 deletions(-)

diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 66cc0807..4c69d04e 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -114,10 +114,18 @@ func Execute() error {
 //   - "unknown flag: --foo"                            (pflag)
 //   - "unknown shorthand flag: 'x' in -x"              (pflag)
 //   - "unknown command \"foo\" for ..."                (Cobra)
-//   - "required flag(s) \"foo\" not set"               (Cobra MarkFlagRequired)
+//   - "required flag \"foo\" not set"                  (Cobra, single missing)
+//   - "required flag(s) \"foo\" not set"               (Cobra, multiple missing)
 //   - "flag needs an argument: --foo"                  (pflag, missing value)
 //   - "invalid argument \"x\" for \"--y\" flag: ..."   (pflag, parse failure)
 //
+// Cobra emits the singular form ("required flag") when exactly one
+// MarkFlagRequired flag is missing, and the plural form ("required
+// flag(s)") only when multiple are missing on the same command. Both
+// shapes must be anchored to avoid matching app-level errors that
+// happen to mention "required flag" as prose; the trailing space + quote
+// (`required flag "`) is the literal punctuation cobra emits.
+//
 // Returns false for nil err.
 func isCobraUsageError(err error) bool {
 	if err == nil {
@@ -127,6 +135,7 @@ func isCobraUsageError(err error) bool {
 	return strings.HasPrefix(msg, "unknown flag") ||
 		strings.HasPrefix(msg, "unknown shorthand flag") ||
 		strings.HasPrefix(msg, "unknown command") ||
+		strings.HasPrefix(msg, `required flag "`) ||
 		strings.HasPrefix(msg, `required flag(s) "`) ||
 		strings.HasPrefix(msg, "flag needs an argument:") ||
 		strings.HasPrefix(msg, `invalid argument "`)
diff --git a/internal/generator/templates/root_test.go.tmpl b/internal/generator/templates/root_test.go.tmpl
index b1cd048e..b223eb61 100644
--- a/internal/generator/templates/root_test.go.tmpl
+++ b/internal/generator/templates/root_test.go.tmpl
@@ -24,7 +24,8 @@ func TestIsCobraUsageError(t *testing.T) {
 		{"unknown flag", errors.New("unknown flag: --foob"), true},
 		{"unknown shorthand flag", errors.New("unknown shorthand flag: 'x' in -x"), true},
 		{"unknown command", errors.New("unknown command \"foo\" for \"cli\""), true},
-		{"required flag", errors.New("required flag(s) \"query\" not set"), true},
+		{"required flag (singular)", errors.New("required flag \"query\" not set"), true},
+		{"required flag(s) (plural)", errors.New("required flag(s) \"query\", \"vault\" not set"), true},
 		{"flag needs argument", errors.New("flag needs an argument: --query"), true},
 		{"invalid argument", errors.New("invalid argument \"abc\" for \"--limit\" flag: strconv.ParseInt: parsing \"abc\": invalid syntax"), true},
 		// Non-usage errors must NOT be flagged — they should retain their
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/root_test.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/root_test.go
index e5336e44..13072f24 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/root_test.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/root_test.go
@@ -24,7 +24,8 @@ func TestIsCobraUsageError(t *testing.T) {
 		{"unknown flag", errors.New("unknown flag: --foob"), true},
 		{"unknown shorthand flag", errors.New("unknown shorthand flag: 'x' in -x"), true},
 		{"unknown command", errors.New("unknown command \"foo\" for \"cli\""), true},
-		{"required flag", errors.New("required flag(s) \"query\" not set"), true},
+		{"required flag (singular)", errors.New("required flag \"query\" not set"), true},
+		{"required flag(s) (plural)", errors.New("required flag(s) \"query\", \"vault\" not set"), true},
 		{"flag needs argument", errors.New("flag needs an argument: --query"), true},
 		{"invalid argument", errors.New("invalid argument \"abc\" for \"--limit\" flag: strconv.ParseInt: parsing \"abc\": invalid syntax"), true},
 		// Non-usage errors must NOT be flagged — they should retain their
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 5c19c268..ca7494e9 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
@@ -108,10 +108,18 @@ func Execute() error {
 //   - "unknown flag: --foo"                            (pflag)
 //   - "unknown shorthand flag: 'x' in -x"              (pflag)
 //   - "unknown command \"foo\" for ..."                (Cobra)
-//   - "required flag(s) \"foo\" not set"               (Cobra MarkFlagRequired)
+//   - "required flag \"foo\" not set"                  (Cobra, single missing)
+//   - "required flag(s) \"foo\" not set"               (Cobra, multiple missing)
 //   - "flag needs an argument: --foo"                  (pflag, missing value)
 //   - "invalid argument \"x\" for \"--y\" flag: ..."   (pflag, parse failure)
 //
+// Cobra emits the singular form ("required flag") when exactly one
+// MarkFlagRequired flag is missing, and the plural form ("required
+// flag(s)") only when multiple are missing on the same command. Both
+// shapes must be anchored to avoid matching app-level errors that
+// happen to mention "required flag" as prose; the trailing space + quote
+// (`required flag "`) is the literal punctuation cobra emits.
+//
 // Returns false for nil err.
 func isCobraUsageError(err error) bool {
 	if err == nil {
@@ -121,6 +129,7 @@ func isCobraUsageError(err error) bool {
 	return strings.HasPrefix(msg, "unknown flag") ||
 		strings.HasPrefix(msg, "unknown shorthand flag") ||
 		strings.HasPrefix(msg, "unknown command") ||
+		strings.HasPrefix(msg, `required flag "`) ||
 		strings.HasPrefix(msg, `required flag(s) "`) ||
 		strings.HasPrefix(msg, "flag needs an argument:") ||
 		strings.HasPrefix(msg, `invalid argument "`)
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root_test.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root_test.go
index e5336e44..13072f24 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root_test.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root_test.go
@@ -24,7 +24,8 @@ func TestIsCobraUsageError(t *testing.T) {
 		{"unknown flag", errors.New("unknown flag: --foob"), true},
 		{"unknown shorthand flag", errors.New("unknown shorthand flag: 'x' in -x"), true},
 		{"unknown command", errors.New("unknown command \"foo\" for \"cli\""), true},
-		{"required flag", errors.New("required flag(s) \"query\" not set"), true},
+		{"required flag (singular)", errors.New("required flag \"query\" not set"), true},
+		{"required flag(s) (plural)", errors.New("required flag(s) \"query\", \"vault\" not set"), true},
 		{"flag needs argument", errors.New("flag needs an argument: --query"), true},
 		{"invalid argument", errors.New("invalid argument \"abc\" for \"--limit\" flag: strconv.ParseInt: parsing \"abc\": invalid syntax"), true},
 		// Non-usage errors must NOT be flagged — they should retain their

← 43c90ba0 feat(catalog): add OpenRouteService under new maps category  ·  back to Cli Printing Press  ·  fix(cli): validate-narrative splits piped recipes and strips 62838d04 →