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 {
8585
8686func buildRef(c *Ctx, args []string) (store.Repo, store.Build, int) {
8787 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>")
8989 }
9090 repo, code := resolveRepo(c, args[0], policy.CanRead)
9191 if code >= 0 {
internal/control/control.go +6
@@ -47,6 +47,12 @@ func (c *Ctx) usage() int {
4747 return c.fail(protocol.ExitUsage, "usage: %s", c.Cmd.Usage)
4848}
4949
50// usageWith reports a specific problem with the arguments, then the
51// registered usage, so a person always sees the shape that was expected.
52func (c *Ctx) usageWith(msg string) int {
53 return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.Cmd.Usage)
54}
55
5056type Command struct {
5157 Path []string // e.g. ["keys", "add"]
5258 // 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) {
254254 }
255255 }
256256}
257
258// TestArgumentRefusalsNameTheUsage: a missing positional argument is a
259// usage error that prints the registered usage, the shared reference
260// helpers included (#215).
261func 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 {
5959// releaseRef loads a release for "<owner/name> <tag>" with the permission.
6060func releaseRef(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool) (store.Repo, store.Release, int) {
6161 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>")
6363 }
6464 repo, code := resolveRepo(c, args[0], perm)
6565 if code >= 0 {
internal/control/thread.go +1 −1
@@ -18,7 +18,7 @@ import (
1818// refArgs resolves "<owner/name> <n>" to the repository and the number.
1919func refArgs(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool, noun string) (store.Repo, int64, int) {
2020 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>")
2222 }
2323 repo, code := resolveRepo(c, args[0], perm)
2424 if code >= 0 {