From a973d0912db9473ced56e35bd42cc31969b4353e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 31 May 2026 03:31:57 +0000 Subject: [PATCH] Add focused CLI argument helpers --- internal/cli/inspect.go | 8 +-- internal/cli/root.go | 19 +++++++ internal/cli/root_test.go | 103 ++++++++++++++++++++++++++++++++++++++ internal/cli/run.go | 3 +- internal/cli/validate.go | 8 +-- 5 files changed, 127 insertions(+), 14 deletions(-) diff --git a/internal/cli/inspect.go b/internal/cli/inspect.go index a39b568..0ae83be 100644 --- a/internal/cli/inspect.go +++ b/internal/cli/inspect.go @@ -13,14 +13,10 @@ func inspectCommand(ctx context.Context, args []string, stdout, stderr io.Writer printInspectHelp(stdout) return exitOK } - if len(args) > 1 { - fmt.Fprintf(stderr, "%s: inspect accepts at most one path\n", app.Name) + path, ok := parseOptionalPathArg(stderr, "inspect", args) + if !ok { return exitUsage } - var path string - if len(args) == 1 { - path = args[0] - } if err := app.Inspect(ctx, app.InspectOptions{Path: path, Stdout: stdout}); err != nil { return fail(stderr, err) } diff --git a/internal/cli/root.go b/internal/cli/root.go index 8dc4fca..c020d4e 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -82,3 +82,22 @@ func rejectExtraArgs(stderr io.Writer, command string, args []string) bool { fmt.Fprintf(stderr, "%s: %s does not accept arguments: %s\n", app.Name, command, strings.Join(args, " ")) return true } + +func parseOptionalPathArg(stderr io.Writer, command string, args []string) (string, bool) { + if len(args) > 1 { + fmt.Fprintf(stderr, "%s: %s accepts at most one path\n", app.Name, command) + return "", false + } + if len(args) == 0 { + return "", true + } + return args[0], true +} + +func rejectPositionalArgs(stderr io.Writer, command string, args []string) bool { + if len(args) == 0 { + return false + } + fmt.Fprintf(stderr, "%s: %s does not accept positional arguments: %v\n", app.Name, command, args) + return true +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 98aab3e..d51b8e3 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -56,6 +56,51 @@ func TestExecuteValidate(t *testing.T) { } } +func TestExecuteValidateArgs(t *testing.T) { + validPath := filepath.Join("..", "bundle", "testdata", "valid_bundle") + tests := []struct { + name string + args []string + wantCode int + wantStdout string + wantStderr string + }{ + { + name: "zero args", + args: []string{"validate"}, + wantCode: exitError, + wantStderr: "requires a path", + }, + { + name: "one arg", + args: []string{"validate", validPath}, + wantCode: exitOK, + wantStdout: "Validated 1 bundle(s)", + }, + { + name: "two args", + args: []string{"validate", validPath, validPath}, + wantCode: exitUsage, + wantStderr: "accepts at most one path", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + code := Execute(context.Background(), tt.args, &stdout, &stderr) + if code != tt.wantCode { + t.Fatalf("exit code = %d, want %d; stderr = %q", code, tt.wantCode, stderr.String()) + } + if tt.wantStdout != "" && !strings.Contains(stdout.String(), tt.wantStdout) { + t.Fatalf("stdout = %q, want substring %q", stdout.String(), tt.wantStdout) + } + if tt.wantStderr != "" && !strings.Contains(stderr.String(), tt.wantStderr) { + t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr) + } + }) + } +} + func TestExecuteInspect(t *testing.T) { var stdout, stderr bytes.Buffer @@ -69,6 +114,51 @@ func TestExecuteInspect(t *testing.T) { } } +func TestExecuteInspectArgs(t *testing.T) { + validPath := filepath.Join("..", "bundle", "testdata", "valid_bundle") + tests := []struct { + name string + args []string + wantCode int + wantStdout string + wantStderr string + }{ + { + name: "zero args", + args: []string{"inspect"}, + wantCode: exitError, + wantStderr: "requires a path", + }, + { + name: "one arg", + args: []string{"inspect", validPath}, + wantCode: exitOK, + wantStdout: "id=weather.daily.brentwood.2026-05-30", + }, + { + name: "two args", + args: []string{"inspect", validPath, validPath}, + wantCode: exitUsage, + wantStderr: "accepts at most one path", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + code := Execute(context.Background(), tt.args, &stdout, &stderr) + if code != tt.wantCode { + t.Fatalf("exit code = %d, want %d; stderr = %q", code, tt.wantCode, stderr.String()) + } + if tt.wantStdout != "" && !strings.Contains(stdout.String(), tt.wantStdout) { + t.Fatalf("stdout = %q, want substring %q", stdout.String(), tt.wantStdout) + } + if tt.wantStderr != "" && !strings.Contains(stderr.String(), tt.wantStderr) { + t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr) + } + }) + } +} + func TestExecuteRunDryRun(t *testing.T) { sourceRoot := t.TempDir() writeCLIBundle(t, sourceRoot) @@ -103,6 +193,19 @@ pipelines: } } +func TestExecuteRunRejectsExtraPositionalArgs(t *testing.T) { + var stdout, stderr bytes.Buffer + + code := Execute(context.Background(), []string{"run", "--config", "config.yml", "extra"}, &stdout, &stderr) + + if code != exitUsage { + t.Fatalf("exit code = %d, want %d", code, exitUsage) + } + if !strings.Contains(stderr.String(), "does not accept positional arguments") { + t.Fatalf("stderr = %q, want positional argument error", stderr.String()) + } +} + func TestExecuteRunPublishes(t *testing.T) { sourceRoot := t.TempDir() destinationRoot := t.TempDir() diff --git a/internal/cli/run.go b/internal/cli/run.go index d74e5f0..66583c4 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -22,8 +22,7 @@ func runCommand(ctx context.Context, args []string, stdout, stderr io.Writer) in if err := flags.Parse(args); err != nil { return exitUsage } - if flags.NArg() > 0 { - fmt.Fprintf(stderr, "%s: run does not accept positional arguments: %v\n", app.Name, flags.Args()) + if rejectPositionalArgs(stderr, "run", flags.Args()) { return exitUsage } diff --git a/internal/cli/validate.go b/internal/cli/validate.go index abfc112..b36ca9b 100644 --- a/internal/cli/validate.go +++ b/internal/cli/validate.go @@ -13,14 +13,10 @@ func validateCommand(ctx context.Context, args []string, stdout, stderr io.Write printValidateHelp(stdout) return exitOK } - if len(args) > 1 { - fmt.Fprintf(stderr, "%s: validate accepts at most one path\n", app.Name) + path, ok := parseOptionalPathArg(stderr, "validate", args) + if !ok { return exitUsage } - var path string - if len(args) == 1 { - path = args[0] - } if err := app.Validate(ctx, app.ValidateOptions{Path: path, Stdout: stdout}); err != nil { return fail(stderr, err) }