← back to Cli Printing Press
fix(cli): hide const-default query flags from --help (#1287)
ee3638b2e846e38c3fa7f23e4de82aac6d781332 · 2026-05-13 11:45:23 -0700 · Trevin Chow
* fix(cli): hide const-default query flags from --help
When an OpenAPI param declares a single-value enum whose only value
matches the declared default, the generator was emitting a visible
--flag for it. Users see and could mis-override a flag whose only valid
value is the default — the canonical case is single-URL routing APIs
(Namecheap-style `Command=namecheap.X.Y`) where every endpoint exposes
a `--command` flag that may only ever take its default.
Adds a paramIsConstDefault helper and threads it through the
per-endpoint and promoted command templates as `isConstDefault`. When
true, the templates emit `_ = cmd.Flags().MarkHidden("<flag>")` right
after the StringVar registration. The flag stays registered so the
wire-side default still flows; only --help visibility changes.
Coverage: TestParamIsConstDefault locks the helper logic across
string, integer, and boolean defaults plus the negative cases
(mismatched default, multi-value enum, no default, no enum).
TestGenerateMarksConstDefaultFlagsHidden runs the real generator
against a fixture spec and asserts MarkHidden emission for both the
per-endpoint (multi-endpoint resource) and promoted (single-endpoint
resource) templates, plus a negative assertion that a multi-value-enum
sibling flag stays visible and the required-check is suppressed for
the hidden const-default flag (Default is set, so the existing
template gate `(not .Default)` already skips it).
Goldens: existing fixtures have no const-default params, so the new
branch is not exercised by the golden suite. The end-to-end generator
test above locks the new behavior more precisely than a golden fixture
would, without conflating this change with unrelated template details.
`scripts/golden.sh verify` passes (17 cases).
Closes #1273
* fix(cli): handle float const-defaults losslessly and note body-flag gap
Addresses two Greptile findings on #1287:
1. fmt.Sprintf("%v", float64(2.0)) yields "2", not "2.0", so a
heterogeneous spec declaring enum: ["2.0"] alongside default: 2.0
would silently miss the const-default shape. paramIsConstDefault now
parses the enum element as a float for float64/float32 defaults and
compares numerically, so "2", "2.0", and "2.00" all match
float64(2.0). Adds matching test coverage including a real-fraction
match and a mismatch negative.
2. paramIsConstDefault is wired into the {{range Endpoint.Params}} loop
in both command_endpoint.go.tmpl and command_promoted.go.tmpl
(query/path/header). Body params registered via
renderFlatBodyFlagReg take a separate code path and currently never
receive MarkHidden, even when they satisfy the const-default shape.
The primary use case (query-param routing selectors) is unaffected,
but document the extension point so a future API surfacing a
const-default body field has a clear hook.
Refs #1273
Files touched
A internal/generator/const_default_test.goM internal/generator/generator.goM internal/generator/templates/command_endpoint.go.tmplM internal/generator/templates/command_promoted.go.tmpl
Diff
commit ee3638b2e846e38c3fa7f23e4de82aac6d781332
Author: Trevin Chow <trevin@trevinchow.com>
Date: Wed May 13 11:45:23 2026 -0700
fix(cli): hide const-default query flags from --help (#1287)
* fix(cli): hide const-default query flags from --help
When an OpenAPI param declares a single-value enum whose only value
matches the declared default, the generator was emitting a visible
--flag for it. Users see and could mis-override a flag whose only valid
value is the default — the canonical case is single-URL routing APIs
(Namecheap-style `Command=namecheap.X.Y`) where every endpoint exposes
a `--command` flag that may only ever take its default.
Adds a paramIsConstDefault helper and threads it through the
per-endpoint and promoted command templates as `isConstDefault`. When
true, the templates emit `_ = cmd.Flags().MarkHidden("<flag>")` right
after the StringVar registration. The flag stays registered so the
wire-side default still flows; only --help visibility changes.
Coverage: TestParamIsConstDefault locks the helper logic across
string, integer, and boolean defaults plus the negative cases
(mismatched default, multi-value enum, no default, no enum).
TestGenerateMarksConstDefaultFlagsHidden runs the real generator
against a fixture spec and asserts MarkHidden emission for both the
per-endpoint (multi-endpoint resource) and promoted (single-endpoint
resource) templates, plus a negative assertion that a multi-value-enum
sibling flag stays visible and the required-check is suppressed for
the hidden const-default flag (Default is set, so the existing
template gate `(not .Default)` already skips it).
Goldens: existing fixtures have no const-default params, so the new
branch is not exercised by the golden suite. The end-to-end generator
test above locks the new behavior more precisely than a golden fixture
would, without conflating this change with unrelated template details.
`scripts/golden.sh verify` passes (17 cases).
Closes #1273
* fix(cli): handle float const-defaults losslessly and note body-flag gap
Addresses two Greptile findings on #1287:
1. fmt.Sprintf("%v", float64(2.0)) yields "2", not "2.0", so a
heterogeneous spec declaring enum: ["2.0"] alongside default: 2.0
would silently miss the const-default shape. paramIsConstDefault now
parses the enum element as a float for float64/float32 defaults and
compares numerically, so "2", "2.0", and "2.00" all match
float64(2.0). Adds matching test coverage including a real-fraction
match and a mismatch negative.
2. paramIsConstDefault is wired into the {{range Endpoint.Params}} loop
in both command_endpoint.go.tmpl and command_promoted.go.tmpl
(query/path/header). Body params registered via
renderFlatBodyFlagReg take a separate code path and currently never
receive MarkHidden, even when they satisfy the const-default shape.
The primary use case (query-param routing selectors) is unaffected,
but document the extension point so a future API surfacing a
const-default body field has a clear hook.
Refs #1273
---
internal/generator/const_default_test.go | 171 +++++++++++++++++++++
internal/generator/generator.go | 40 +++++
.../generator/templates/command_endpoint.go.tmpl | 3 +
.../generator/templates/command_promoted.go.tmpl | 3 +
4 files changed, 217 insertions(+)
diff --git a/internal/generator/const_default_test.go b/internal/generator/const_default_test.go
new file mode 100644
index 00000000..5ec8a79a
--- /dev/null
+++ b/internal/generator/const_default_test.go
@@ -0,0 +1,171 @@
+package generator
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParamIsConstDefault(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ p spec.Param
+ want bool
+ }{
+ {
+ name: "single-value enum with matching string default",
+ p: spec.Param{Name: "Command", Type: "string", Enum: []string{"namecheap.domains.getList"}, Default: "namecheap.domains.getList"},
+ want: true,
+ },
+ {
+ name: "single-value enum with matching integer default",
+ p: spec.Param{Name: "version", Type: "integer", Enum: []string{"2"}, Default: 2},
+ want: true,
+ },
+ {
+ name: "single-value enum with matching boolean default",
+ p: spec.Param{Name: "strict", Type: "boolean", Enum: []string{"true"}, Default: true},
+ want: true,
+ },
+ {
+ name: "single-value enum with matching float default preserving fractional form",
+ p: spec.Param{Name: "version", Type: "number", Enum: []string{"2.0"}, Default: float64(2.0)},
+ want: true,
+ },
+ {
+ name: "single-value enum with matching float default carrying real fraction",
+ p: spec.Param{Name: "ratio", Type: "number", Enum: []string{"1.5"}, Default: float64(1.5)},
+ want: true,
+ },
+ {
+ name: "single-value float enum that does not match the default",
+ p: spec.Param{Name: "ratio", Type: "number", Enum: []string{"1.5"}, Default: float64(2.5)},
+ want: false,
+ },
+ {
+ name: "single-value enum with mismatched default is not const",
+ p: spec.Param{Name: "Command", Type: "string", Enum: []string{"a"}, Default: "b"},
+ want: false,
+ },
+ {
+ name: "multi-value enum with matching default is not const",
+ p: spec.Param{Name: "order", Type: "string", Enum: []string{"asc", "desc"}, Default: "asc"},
+ want: false,
+ },
+ {
+ name: "single-value enum with no default is not const",
+ p: spec.Param{Name: "Command", Type: "string", Enum: []string{"namecheap.x"}},
+ want: false,
+ },
+ {
+ name: "no enum but default is not const",
+ p: spec.Param{Name: "limit", Type: "integer", Default: 25},
+ want: false,
+ },
+ {
+ name: "no enum and no default is not const",
+ p: spec.Param{Name: "name", Type: "string"},
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ assert.Equal(t, tt.want, paramIsConstDefault(tt.p))
+ })
+ }
+}
+
+// TestGenerateMarksConstDefaultFlagsHidden is the end-to-end check that
+// when an endpoint Param has a single-value enum whose only value equals
+// the default, the generated command registers the flag (so the wire-side
+// default still flows) but marks it hidden so --help does not list a flag
+// whose only valid value is the default. This is the single-URL routing
+// API shape, where an operation is selected via a fixed query param.
+func TestGenerateMarksConstDefaultFlagsHidden(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("const-default")
+ constParam := spec.Param{
+ Name: "Command",
+ Type: "string",
+ Description: "Constant operation selector",
+ Required: true,
+ Enum: []string{"namecheap.domains.getList"},
+ Default: "namecheap.domains.getList",
+ }
+ multiEnumParam := spec.Param{
+ Name: "SortBy",
+ Type: "string",
+ Description: "Sort field",
+ Enum: []string{"name", "created"},
+ Default: "name",
+ }
+
+ apiSpec.Resources["domains"] = spec.Resource{
+ Description: "Manage domains",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/",
+ Description: "List domains",
+ Params: []spec.Param{constParam, multiEnumParam},
+ },
+ "renew": {
+ Method: "POST",
+ Path: "/",
+ Description: "Renew a domain",
+ Params: []spec.Param{constParam},
+ },
+ },
+ }
+ apiSpec.Resources["dns"] = spec.Resource{
+ Description: "Manage DNS",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {
+ Method: "GET",
+ Path: "/",
+ Description: "List DNS records",
+ Params: []spec.Param{constParam},
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "const-default-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ cliDir := filepath.Join(outputDir, "internal", "cli")
+
+ endpointSrc, err := os.ReadFile(filepath.Join(cliDir, "domains_list.go"))
+ require.NoError(t, err)
+ endpointGot := string(endpointSrc)
+
+ require.Contains(t, endpointGot, `cmd.Flags().StringVar(&flagCommand, "command",`,
+ "per-endpoint template: const-default flag must still be registered so its default is sent on the wire")
+ require.Contains(t, endpointGot, `_ = cmd.Flags().MarkHidden("command")`,
+ "per-endpoint template: const-default flag must be marked hidden")
+ require.Contains(t, endpointGot, `cmd.Flags().StringVar(&flagSortBy, "sort-by",`,
+ "per-endpoint template: multi-value-enum flag must remain visible")
+ require.NotContains(t, endpointGot, `_ = cmd.Flags().MarkHidden("sort-by")`,
+ "per-endpoint template: multi-value-enum flag must not be marked hidden")
+ // A required flag with a wired default never needs a runtime "required flag not set"
+ // check — the template gates that emission on `(not .Default)`. Lock that interaction
+ // so a future template edit re-enabling the check for Required:true would not produce
+ // a generated binary that errors at runtime on a hidden const-default flag.
+ require.NotContains(t, endpointGot, `required flag "command" not set`,
+ "per-endpoint template: required-check must be suppressed for const-default (Default is set)")
+
+ promotedSrc, err := os.ReadFile(filepath.Join(cliDir, "promoted_dns.go"))
+ require.NoError(t, err)
+ promotedGot := string(promotedSrc)
+
+ require.Contains(t, promotedGot, `cmd.Flags().StringVar(&flagCommand, "command",`,
+ "promoted template: const-default flag must still be registered so its default is sent on the wire")
+ require.Contains(t, promotedGot, `_ = cmd.Flags().MarkHidden("command")`,
+ "promoted template: const-default flag must be marked hidden")
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 6529c138..6b903c52 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -209,6 +209,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
"cobraFlagFuncForParam": cobraFlagFuncForParam,
"defaultVal": defaultVal,
"defaultValForParam": defaultValForParam,
+ "isConstDefault": paramIsConstDefault,
"zeroVal": zeroVal,
"zeroValForParam": func(name, t string) string {
kind := primitiveKind(t)
@@ -3070,6 +3071,38 @@ func defaultValForParam(p spec.Param) string {
return defaultVal(p)
}
+// paramIsConstDefault holds for single-value-enum params whose default
+// equals the only enum value. Templates emit MarkHidden for these so
+// --help does not list a flag whose only valid value is the default,
+// while the flag stays registered so the wire-side default still flows.
+// This catches the single-URL routing-selector shape, where an API
+// selects an operation via a fixed query param.
+func paramIsConstDefault(p spec.Param) bool {
+ if len(p.Enum) != 1 || p.Default == nil {
+ return false
+ }
+ // Float defaults round-trip ambiguously under any single string format:
+ // fmt.Sprintf("%v", float64(2.0)) and strconv.FormatFloat with -1
+ // precision both yield "2", which would miss a heterogeneous spec
+ // declaring `enum: ["2.0"]` alongside `default: 2.0`. Parse the enum
+ // element as a float and compare numerically so "2", "2.0", and "2.00"
+ // are all equivalent to float64(2.0).
+ switch v := p.Default.(type) {
+ case float64:
+ if enumF, err := strconv.ParseFloat(p.Enum[0], 64); err == nil {
+ return enumF == v
+ }
+ return false
+ case float32:
+ if enumF, err := strconv.ParseFloat(p.Enum[0], 32); err == nil {
+ return float32(enumF) == v
+ }
+ return false
+ default:
+ return fmt.Sprintf("%v", p.Default) == p.Enum[0]
+ }
+}
+
type jsonFlagSuggestion struct {
FlagName string
Values []string
@@ -3359,6 +3392,13 @@ func renderBodyFlagRegs(b *strings.Builder, body []spec.Param, identPrefix, flag
}
}
+// renderFlatBodyFlagReg emits cobra flag registrations for a single body
+// param. Const-default detection via paramIsConstDefault is intentionally
+// not wired through here yet: the primary use case is the query-param
+// routing-selector shape, not body fields. If a future API surfaces a
+// const-default body field, extend the emission here with a MarkHidden
+// line gated by paramIsConstDefault, mirroring the command_endpoint and
+// command_promoted templates.
func renderFlatBodyFlagReg(b *strings.Builder, p spec.Param, identPrefix, flagPrefix string, topLevel bool) {
ident := identPrefix + toCamel(paramIdent(p))
flag := joinFlag(flagPrefix, publicFlagName(p))
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index ba4033de..a81a253b 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -760,6 +760,9 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- if not .Positional}}
{{- $param := .}}
cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel (paramIdent .)}}, "{{publicFlagName .}}", {{defaultValForParam .}}, "{{oneline .Description}}{{enumDescriptionHint .Enum}}")
+{{- if isConstDefault .}}
+ _ = cmd.Flags().MarkHidden("{{publicFlagName .}}")
+{{- end}}
{{- range publicFlagAliases $param}}
cmd.Flags().{{cobraFlagFuncForParam $param.Name $param.Type}}(&flag{{camel (paramIdent $param)}}, "{{.}}", {{defaultValForParam $param}}, "{{oneline $param.Description}}{{enumDescriptionHint $param.Enum}}")
_ = cmd.Flags().MarkHidden("{{.}}")
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index f8857aa6..f0664903 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -304,6 +304,9 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
{{- if not .Positional}}
{{- $param := .}}
cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel (paramIdent .)}}, "{{publicFlagName .}}", {{defaultValForParam .}}, "{{oneline .Description}}{{enumDescriptionHint .Enum}}")
+{{- if isConstDefault .}}
+ _ = cmd.Flags().MarkHidden("{{publicFlagName .}}")
+{{- end}}
{{- range publicFlagAliases $param}}
cmd.Flags().{{cobraFlagFuncForParam $param.Name $param.Type}}(&flag{{camel (paramIdent $param)}}, "{{.}}", {{defaultValForParam $param}}, "{{oneline $param.Description}}{{enumDescriptionHint $param.Enum}}")
_ = cmd.Flags().MarkHidden("{{.}}")
← b8488ee4 fix(cli): route in:query params to URL on PUT/DELETE/POST/PA
·
back to Cli Printing Press
·
fix(cli): raise API error body truncation cap from 200 to 40 513ae01d →