carlos / platform Public

cli: alias table, registrar and printer move to internal/cliflag; vet joins

carlos vet lives in internal/vet, outside package main — leaving it
long-only while every sibling verb takes -a is the papercut the spec
closes, and open-coding an alias pair there is the default-value trap.
The table, the registrar and the usage printer become a package; the
four helpers and flagWasSet stay in flags.go as the root package's
face, so the 79 call sites and the tests read the same.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 597c2de50a16afe6e6da3411c4d7564419470466 parent 4d1116a
4 files changed, +132 −92
  • flags.go +21 −89
  • flags_test.go +5 −0
  • internal/cliflag/cliflag.go +99 −0
  • internal/vet/vet.go +7 −3
diff --git a/flags.go b/flags.go
index b1ba4079b..146f6bd26 100644
--- a/flags.go
+++ b/flags.go
@@ -2,43 +2,34 @@ package main
import (
"flag"
- "fmt"
- "strings"
+
+ "github.com/carlosframework/platform/internal/cliflag"
)
-// flags.go is the single home of the CLI's claimed short-flag namespace
-// (spec 2026-08-19-cli-flag-conventions-design.md): four letters, four
-// helpers, one usage printer. --app is the documented face; -app parses
-// forever (Go's flag treats one and two dashes alike). Letters already
-// claimed elsewhere and untouched: -f (logs, follow), -n (logs, line
-// count), -y (update, skip prompt). -vv is reserved for a future verbose
-// mode so it can never quietly claim -v and collide with --version;
-// -console stays long-only by ruling.
+// flags.go is the carlos CLI's face of internal/cliflag (the spec's
+// claimed short-flag namespace: -a app, -c channel, -e environment,
+// -v version). The four helpers below are the only way the aliased
+// flags are registered in this package; sites with something bespoke to
+// say pass it through, and an empty usage takes the flag's common
+// wording. The table itself, the registrar, and the usage printer live
+// in internal/cliflag so out-of-package verbs (internal/vet) register
+// through the same table instead of open-coding an alias pair.
-// cliAliases maps a primary flag name to its short alias. flagWasSet
-// resolves through this same table, which is what keeps "did the user
-// type it" true no matter which spelling was typed.
-var cliAliases = map[string]string{
- "app": "a",
- "channel": "c",
- "environment": "e",
- "version": "v",
-}
+// cliAliases is the claimed table, shared with every registrar caller.
+var cliAliases = cliflag.Aliases
-// aliasedString registers name and its short form against one variable.
-// The alias's StringVar assigns through the shared pointer, so both
-// registrations MUST pass the same default — repeating def here is the
-// reason this helper exists rather than two open-coded calls (a
-// mismatched alias default silently clobbers the primary's).
+// aliasedString registers name and its short form against one variable
+// with one default — see cliflag.String for the default-value trap this
+// signature exists to prevent.
func aliasedString(fs *flag.FlagSet, name, def, usage string) *string {
- p := fs.String(name, def, usage)
- fs.StringVar(p, cliAliases[name], def, "alias for --"+name)
- return p
+ return cliflag.String(fs, name, def, usage)
}
-// The four helpers below are the only way the aliased flags are
-// registered. An empty usage takes the flag's common wording; sites with
-// something bespoke to say pass it through.
+// flagUsage renders a flag set with aliases folded into their primary
+// and -- canonical; see cliflag.Usage.
+func flagUsage(fs *flag.FlagSet, header string) func() {
+ return cliflag.Usage(fs, header)
+}
func appFlag(fs *flag.FlagSet, usage string) *string {
if usage == "" {
@@ -85,62 +76,3 @@ func flagWasSet(fs *flag.FlagSet, name string) bool {
})
return set
}
-
-// flagUsage replaces the stock PrintDefaults face for every carlos flag
-// set: the alias folds into its primary ("--app, -a"), long flags render
-// with the documented double dash, single-letter primaries (-f, -n, -y)
-// keep their one dash, and alias registrations never print a row of
-// their own. Output derives from the live flag set, so help cannot
-// drift from what the parser accepts — the reason to generate rather
-// than hand-write 90 flag lists. header is the command's synopsis
-// ("usage: carlos ..."); empty derives one from the set's name.
-func flagUsage(fs *flag.FlagSet, header string) func() {
- return func() {
- w := fs.Output()
- if header == "" {
- header = "usage: carlos " + fs.Name() + " [flags]"
- }
- fmt.Fprintln(w, strings.TrimRight(header, "\n"))
- aliasOf := make(map[string]string, len(cliAliases))
- for primary, short := range cliAliases {
- aliasOf[short] = primary
- }
- type row struct{ head, usage string }
- var rows []row
- width := 0
- fs.VisitAll(func(f *flag.Flag) {
- // Fold only pairs the table AND the set both know: a
- // command registering a plain -a of its own without the
- // helper would neither be hidden nor claim the fold.
- if p, ok := aliasOf[f.Name]; ok && fs.Lookup(p) != nil {
- return
- }
- head := "--" + f.Name
- if len(f.Name) == 1 {
- head = "-" + f.Name
- }
- if short := cliAliases[f.Name]; short != "" && fs.Lookup(short) != nil {
- head += ", -" + short
- }
- arg, usage := flag.UnquoteUsage(f)
- if arg != "" {
- head += " <" + arg + ">"
- }
- switch f.DefValue {
- case "", "false", "0":
- default:
- usage += " (default: " + f.DefValue + ")"
- }
- if len(head) > width {
- width = len(head)
- }
- rows = append(rows, row{head, usage})
- })
- if len(rows) > 0 {
- fmt.Fprintln(w, "flags:")
- }
- for _, r := range rows {
- fmt.Fprintf(w, " %-*s %s\n", width, r.head, r.usage)
- }
- }
-}
diff --git a/flags_test.go b/flags_test.go
index 287d8a22f..57e3766f9 100644
--- a/flags_test.go
+++ b/flags_test.go
@@ -149,6 +149,8 @@ func TestFlagUsageShowsRealDefaultsOnly(t *testing.T) {
fs := quietFlagSet("t")
aliasedString(fs, "channel", "stable", "release channel to follow")
fs.Int("n", 0, "at most this many lines")
+ var fields multiFlag
+ fs.Var(&fields, "field", "only structured lines with this k=v field (repeatable)")
out := printedUsage(fs, "")
if !strings.Contains(out, "(default: stable)") {
t.Errorf("stable default not shown:\n%s", out)
@@ -156,4 +158,7 @@ func TestFlagUsageShowsRealDefaultsOnly(t *testing.T) {
if strings.Contains(out, "default: 0") {
t.Errorf("zero default shown:\n%s", out)
}
+ if strings.Contains(out, "default: []") {
+ t.Errorf("empty repeatable default shown:\n%s", out)
+ }
}
diff --git a/internal/cliflag/cliflag.go b/internal/cliflag/cliflag.go
new file mode 100644
index 000000000..92b2440f1
--- /dev/null
+++ b/internal/cliflag/cliflag.go
@@ -0,0 +1,99 @@
+// Package cliflag is the single home of the carlos CLI's claimed
+// short-flag namespace (spec 2026-08-19-cli-flag-conventions-design.md):
+// the alias table, the one registrar that cannot get the default-value
+// trap wrong, and the usage printer that folds an alias into its
+// primary. It is a package rather than a corner of package main so that
+// verbs living outside main (internal/vet) register through the same
+// table instead of open-coding an alias pair.
+package cliflag
+
+import (
+ "flag"
+ "fmt"
+ "strings"
+)
+
+// Aliases maps a primary flag name to its short alias. Four letters,
+// and nothing else: -vv is reserved for a future verbose mode so it can
+// never quietly claim -v and collide with --version; -console stays
+// long-only by ruling; -f, -n, -y are claimed by logs/update as
+// primaries and are not aliases.
+var Aliases = map[string]string{
+ "app": "a",
+ "channel": "c",
+ "environment": "e",
+ "version": "v",
+}
+
+// String registers name — and its short form, when the table claims one
+// — against one variable. The alias's StringVar assigns through the
+// shared pointer, so both registrations MUST pass the same default;
+// repeating def here is the reason this registrar exists rather than
+// two open-coded calls (a mismatched alias default silently clobbers
+// the primary's).
+func String(fs *flag.FlagSet, name, def, usage string) *string {
+ p := fs.String(name, def, usage)
+ if short := Aliases[name]; short != "" {
+ fs.StringVar(p, short, def, "alias for --"+name)
+ }
+ return p
+}
+
+// Usage replaces the stock PrintDefaults face for every carlos flag
+// set: the alias folds into its primary ("--app, -a"), long flags render
+// with the documented double dash, single-letter primaries (-f, -n, -y)
+// keep their one dash, and alias registrations never print a row of
+// their own. Output derives from the live flag set, so help cannot
+// drift from what the parser accepts — the reason to generate rather
+// than hand-write 90 flag lists. header is the command's synopsis
+// ("usage: carlos ..."); empty derives one from the set's name.
+func Usage(fs *flag.FlagSet, header string) func() {
+ return func() {
+ w := fs.Output()
+ if header == "" {
+ header = "usage: carlos " + fs.Name() + " [flags]"
+ }
+ fmt.Fprintln(w, strings.TrimRight(header, "\n"))
+ aliasOf := make(map[string]string, len(Aliases))
+ for primary, short := range Aliases {
+ aliasOf[short] = primary
+ }
+ type row struct{ head, usage string }
+ var rows []row
+ width := 0
+ fs.VisitAll(func(f *flag.Flag) {
+ // Fold only pairs the table AND the set both know: a
+ // command registering a plain -a of its own without the
+ // registrar would neither be hidden nor claim the fold.
+ if p, ok := aliasOf[f.Name]; ok && fs.Lookup(p) != nil {
+ return
+ }
+ head := "--" + f.Name
+ if len(f.Name) == 1 {
+ head = "-" + f.Name
+ }
+ if short := Aliases[f.Name]; short != "" && fs.Lookup(short) != nil {
+ head += ", -" + short
+ }
+ arg, usage := flag.UnquoteUsage(f)
+ if arg != "" {
+ head += " <" + arg + ">"
+ }
+ switch f.DefValue {
+ case "", "false", "0", "[]":
+ default:
+ usage += " (default: " + f.DefValue + ")"
+ }
+ if len(head) > width {
+ width = len(head)
+ }
+ rows = append(rows, row{head, usage})
+ })
+ if len(rows) > 0 {
+ fmt.Fprintln(w, "flags:")
+ }
+ for _, r := range rows {
+ fmt.Fprintf(w, " %-*s %s\n", width, r.head, r.usage)
+ }
+ }
+}
diff --git a/internal/vet/vet.go b/internal/vet/vet.go
index 5a929f2e8..a52736f29 100644
--- a/internal/vet/vet.go
+++ b/internal/vet/vet.go
@@ -29,6 +29,7 @@ import (
"strings"
"time"
+ "github.com/carlosframework/platform/internal/cliflag"
"github.com/carlosframework/platform/internal/release"
)
@@ -177,19 +178,22 @@ func bootHealthz(ctx context.Context, bin []byte) error {
return fmt.Errorf("no 200 from /healthz within %s: %v", bootTimeout, last)
}
+const vetUsage = "usage: carlos vet --app <a> --version <v> [--account <sqid>] [--boot]"
+
// Run is the `carlos vet` verb: vet a shipped release against the store the
// environment names.
func Run(args []string) {
fs := flag.NewFlagSet("vet", flag.ExitOnError)
- app := fs.String("app", "", "app name")
+ app := cliflag.String(fs, "app", "", "app name")
// Sqid only, deliberately: vet has no -registry flag to resolve a name
// against, and guessing an account would vet the wrong app's bytes.
acct := fs.String("account", release.OpsAccount, "account sqid the app lives under")
- ver := fs.String("version", "", "shipped version to vet")
+ ver := cliflag.String(fs, "version", "", "shipped version to vet")
boot := fs.Bool("boot", false, "also boot the binary with -socket and poll /healthz (binary kind)")
+ fs.Usage = cliflag.Usage(fs, vetUsage)
_ = fs.Parse(args)
if *app == "" || *ver == "" {
- log.Fatal("usage: carlos vet -app <a> -version <v> [-account <sqid>] [-boot]")
+ log.Fatal(vetUsage)
}
if !release.ValidAccount(*acct) {
log.Fatalf("vet: invalid account sqid %q", *acct)