Commit 0c24b0baa2
0c24b0baa2882287acbdf9c8e3ce6ca21ef42430
parent: 3a0bc31b3d
Verified · cmc ci/build: success ci/test: success
cmc <hello@cleberg.net> · 2026-09-12 06:26 UTC
control: an argument refusal prints the usage too
The shared reference helpers said "expected <owner/name> <number>" and
stopped; they now add the registered usage on a second line.
Closes #215
internal/control/build.go
+1 −1
| @@ -85,7 +85,7 @@ func buildToOut(b store.Build) BuildOut { |
| 85 | 85 | |
| 86 | 86 | func buildRef(c *Ctx, args []string) (store.Repo, store.Build, int) { |
| 87 | 87 | if len(args) != 2 { |
| 88 | | return store.Repo{}, store.Build{}, c.fail(protocol.ExitUsage, "expected <owner/name> <number>") |
| 88 | return store.Repo{}, store.Build{}, c.usageWith("expected <owner/name> <number>") |
| 89 | 89 | } |
| 90 | 90 | repo, code := resolveRepo(c, args[0], policy.CanRead) |
| 91 | 91 | if code >= 0 { |
internal/control/control.go
+6
| @@ -47,6 +47,12 @@ func (c *Ctx) usage() int { |
| 47 | 47 | return c.fail(protocol.ExitUsage, "usage: %s", c.Cmd.Usage) |
| 48 | 48 | } |
| 49 | 49 | |
| 50 | // usageWith reports a specific problem with the arguments, then the |
| 51 | // registered usage, so a person always sees the shape that was expected. |
| 52 | func (c *Ctx) usageWith(msg string) int { |
| 53 | return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.Cmd.Usage) |
| 54 | } |
| 55 | |
| 50 | 56 | type Command struct { |
| 51 | 57 | Path []string // e.g. ["keys", "add"] |
| 52 | 58 | // Summary is one line of prose: what the command does, no argument |
internal/control/control_test.go
+17
| @@ -254,3 +254,20 @@ func TestFailErrExitCodes(t *testing.T) { |
| 254 | 254 | } |
| 255 | 255 | } |
| 256 | 256 | } |
| 257 | |
| 258 | // TestArgumentRefusalsNameTheUsage: a missing positional argument is a |
| 259 | // usage error that prints the registered usage, the shared reference |
| 260 | // helpers included (#215). |
| 261 | func TestArgumentRefusalsNameTheUsage(t *testing.T) { |
| 262 | for _, argv := range [][]string{{"build", "show"}, {"release", "show"}, {"mr", "resolve"}, {"issue", "show"}} { |
| 263 | var out, errOut bytes.Buffer |
| 264 | c := &Ctx{Scope: "full", Stdout: &out, Stderr: &errOut} |
| 265 | if code := Dispatch(c, argv); code != protocol.ExitUsage { |
| 266 | t.Errorf("%v: exit %d, want %d (%s)", argv, code, protocol.ExitUsage, errOut.String()) |
| 267 | continue |
| 268 | } |
| 269 | if !strings.Contains(errOut.String(), "usage: "+strings.Join(argv, " ")) { |
| 270 | t.Errorf("%v: no usage line: %q", argv, errOut.String()) |
| 271 | } |
| 272 | } |
| 273 | } |
internal/control/release.go
+1 −1
| @@ -59,7 +59,7 @@ func assetDir(root string, repo store.Repo, releaseID int64) string { |
| 59 | 59 | // releaseRef loads a release for "<owner/name> <tag>" with the permission. |
| 60 | 60 | func releaseRef(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool) (store.Repo, store.Release, int) { |
| 61 | 61 | if len(args) < 2 { |
| 62 | | return store.Repo{}, store.Release{}, c.fail(protocol.ExitUsage, "expected <owner/name> <tag>") |
| 62 | return store.Repo{}, store.Release{}, c.usageWith("expected <owner/name> <tag>") |
| 63 | 63 | } |
| 64 | 64 | repo, code := resolveRepo(c, args[0], perm) |
| 65 | 65 | if code >= 0 { |
internal/control/thread.go
+1 −1
| @@ -18,7 +18,7 @@ import ( |
| 18 | 18 | // refArgs resolves "<owner/name> <n>" to the repository and the number. |
| 19 | 19 | func refArgs(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool, noun string) (store.Repo, int64, int) { |
| 20 | 20 | if len(args) < 2 { |
| 21 | | return store.Repo{}, 0, c.fail(protocol.ExitUsage, "expected <owner/name> <number>") |
| 21 | return store.Repo{}, 0, c.usageWith("expected <owner/name> <number>") |
| 22 | 22 | } |
| 23 | 23 | repo, code := resolveRepo(c, args[0], perm) |
| 24 | 24 | if code >= 0 { |