carlos / platform Public

cli: claim -a -c -e -v through four alias helpers

The spec's four letters, registered only via appFlag/channelFlag/
environmentFlag/versionFlag (aliasedString underneath for the one site
whose default is not the helper's). The alias's StringVar assigns
through the shared pointer, so the registrar repeats the primary's
default at both registrations — the default-value trap the helpers
exist to make unrepresentable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 2e75214cbac0a8b997351f32033f6e277e14c0c6 parent 9f3cf41
2 files changed, +146 −0
  • flags.go +65 −0
  • flags_test.go +81 −0
diff --git a/flags.go b/flags.go
new file mode 100644
index 000000000..991c603bf
--- /dev/null
+++ b/flags.go
@@ -0,0 +1,65 @@
+package main
+
+import "flag"
+
+// 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.
+
+// 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",
+}
+
+// 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).
+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
+}
+
+// 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.
+
+func appFlag(fs *flag.FlagSet, usage string) *string {
+ if usage == "" {
+ usage = "app name"
+ }
+ return aliasedString(fs, "app", "", usage)
+}
+
+func channelFlag(fs *flag.FlagSet, usage string) *string {
+ if usage == "" {
+ usage = "release channel"
+ }
+ return aliasedString(fs, "channel", "", usage)
+}
+
+func environmentFlag(fs *flag.FlagSet, usage string) *string {
+ if usage == "" {
+ usage = "named environment bundle (omit for the default bundle)"
+ }
+ return aliasedString(fs, "environment", "", usage)
+}
+
+func versionFlag(fs *flag.FlagSet, usage string) *string {
+ if usage == "" {
+ usage = "version (git short sha)"
+ }
+ return aliasedString(fs, "version", "", usage)
+}
diff --git a/flags_test.go b/flags_test.go
new file mode 100644
index 000000000..5b236d697
--- /dev/null
+++ b/flags_test.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+ "flag"
+ "io"
+ "testing"
+)
+
+func quietFlagSet(name string) *flag.FlagSet {
+ fs := flag.NewFlagSet(name, flag.ContinueOnError)
+ fs.SetOutput(io.Discard)
+ return fs
+}
+
+// Every spelling of the app flag lands in the same variable.
+func TestAppFlagParsesAllSpellings(t *testing.T) {
+ for _, args := range [][]string{
+ {"-app", "blah"}, {"--app", "blah"}, {"-app=blah"},
+ {"--app=blah"}, {"-a", "blah"}, {"-a=blah"},
+ } {
+ fs := quietFlagSet("t")
+ app := appFlag(fs, "")
+ if err := fs.Parse(args); err != nil {
+ t.Fatalf("%v: %v", args, err)
+ }
+ if *app != "blah" {
+ t.Fatalf("%v: app=%q, want blah", args, *app)
+ }
+ }
+}
+
+// The four helpers each claim exactly their ruled letter.
+func TestClaimedLetters(t *testing.T) {
+ fs := quietFlagSet("t")
+ appFlag(fs, "")
+ channelFlag(fs, "")
+ environmentFlag(fs, "")
+ versionFlag(fs, "")
+ for primary, short := range map[string]string{
+ "app": "a", "channel": "c", "environment": "e", "version": "v",
+ } {
+ if fs.Lookup(short) == nil {
+ t.Errorf("-%s not registered for --%s", short, primary)
+ }
+ }
+}
+
+// The default-value trap: registering the alias must not reset a
+// non-empty primary default (StringVar assigns through the pointer).
+func TestAliasKeepsThePrimaryDefault(t *testing.T) {
+ fs := quietFlagSet("t")
+ ch := aliasedString(fs, "channel", "stable", "release channel to follow")
+ if err := fs.Parse(nil); err != nil {
+ t.Fatal(err)
+ }
+ if *ch != "stable" {
+ t.Fatalf("default clobbered by alias registration: %q", *ch)
+ }
+ fs2 := quietFlagSet("t")
+ ch2 := aliasedString(fs2, "channel", "stable", "")
+ if err := fs2.Parse([]string{"-c", "edge"}); err != nil {
+ t.Fatal(err)
+ }
+ if *ch2 != "edge" {
+ t.Fatalf("-c did not assign: %q", *ch2)
+ }
+}
+
+// An empty usage argument takes the flag's common wording.
+func TestHelperDefaultUsage(t *testing.T) {
+ fs := quietFlagSet("t")
+ appFlag(fs, "")
+ if got := fs.Lookup("app").Usage; got != "app name" {
+ t.Fatalf("default usage = %q", got)
+ }
+ fs2 := quietFlagSet("t")
+ appFlag(fs2, "app the instance belongs to")
+ if got := fs2.Lookup("app").Usage; got != "app the instance belongs to" {
+ t.Fatalf("bespoke usage lost: %q", got)
+ }
+}