From 6404aac38b8f12f5ddbddf530fbd02a4c653d2a5 Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Tue, 14 Apr 2026 18:37:41 +0200 Subject: [PATCH 01/11] feat: add version package and automated release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add internal/version package with a source constant as single source of truth for the release version, overridable via ldflags for dev builds. Update the Makefile to inject version + commit at build time via git describe. Add a GitHub Actions release workflow (workflow_dispatch) that auto-computes the next version from the latest git tag. Pick patch/minor/major from a dropdown and click Run — no version to type. The workflow bumps the constant, commits, tags, and creates the release. Access is gated by a "release" environment requiring reviewer approval. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/release.yml | 89 ++++++++++++++++++++++++++++++++ Makefile | 7 ++- internal/version/version.go | 29 +++++++++++ internal/version/version_test.go | 47 +++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 internal/version/version.go create mode 100644 internal/version/version_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1f50617b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,89 @@ +name: Release + +on: + workflow_dispatch: + inputs: + bump: + description: "Version bump type" + required: true + default: "patch" + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 5 + environment: release + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Compute next version + id: version + run: | + # Find the latest semver tag. + LATEST=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1) + if [ -z "$LATEST" ]; then + echo "::error::No existing version tags found." + exit 1 + fi + + # Strip the leading "v" and split into components. + CURRENT="${LATEST#v}" + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" + + BUMP="${{ inputs.bump }}" + case "$BUMP" in + patch) PATCH=$((PATCH + 1)) ;; + minor) MINOR=$((MINOR + 1)); PATCH=0 ;; + major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; + esac + + NEXT="${MAJOR}.${MINOR}.${PATCH}" + echo "current=$CURRENT" >> "$GITHUB_OUTPUT" + echo "next=$NEXT" >> "$GITHUB_OUTPUT" + echo "Releasing: v$CURRENT → v$NEXT ($BUMP bump)" + + - name: Check tag does not already exist + run: | + if git rev-parse "v${{ steps.version.outputs.next }}" >/dev/null 2>&1; then + echo "::error::Tag v${{ steps.version.outputs.next }} already exists." + exit 1 + fi + + - name: Update version constant + run: | + NEXT="${{ steps.version.outputs.next }}" + sed -i "s/^const version = \".*\"/const version = \"$NEXT\"/" internal/version/version.go + grep -q "const version = \"$NEXT\"" internal/version/version.go || { + echo "::error::Failed to update version constant." + exit 1 + } + + - name: Commit version bump + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add internal/version/version.go + git commit -m "release: bump version to ${{ steps.version.outputs.next }}" + + - name: Tag and push + run: | + git tag "v${{ steps.version.outputs.next }}" + git push origin main "v${{ steps.version.outputs.next }}" + + - name: Create GitHub release + run: | + gh release create "v${{ steps.version.outputs.next }}" \ + --title "v${{ steps.version.outputs.next }}" \ + --generate-notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index f3a15812..cdd1f329 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,12 @@ .PHONY: build fmt test test_all test_against_bash compliance +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo unknown) +COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) +LDFLAGS = -X github.com/DataDog/rshell/internal/version.Version=$(VERSION) \ + -X github.com/DataDog/rshell/internal/version.Commit=$(COMMIT) + build: - go build -o rshell ./cmd/rshell + go build -ldflags "$(LDFLAGS)" -o rshell ./cmd/rshell fmt: go fmt ./... diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 00000000..e9058072 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,29 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +// Package version exposes the build version of rshell. +// +// The source constant [version] is the single source of truth for the release +// version. It must be updated before tagging a new release. +// +// For development builds, Version can be overridden via ldflags to include +// git metadata (commit offset, dirty state, etc.): +// +// go build -ldflags "-X github.com/DataDog/rshell/internal/version.Version=v0.0.10-3-gabcdef1-dirty" +// +// When not overridden, Version defaults to the source constant, which is +// correct for both direct builds and library consumers who import rshell. +package version + +// version is the release version. This constant is the single source of truth +// and must match the git tag at release time. Update this before tagging. +const version = "0.0.10" + +// Version is the build version string. Defaults to the source constant. +// Overridden via ldflags at build time for dev builds (e.g. "0.0.10-3-gabcdef1-dirty"). +var Version = version + +// Commit is the short git commit hash. Set via ldflags at build time. +var Commit string diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 00000000..a69c48d1 --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,47 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package version + +import ( + "os/exec" + "strings" + "testing" +) + +func TestVersionConstantNotEmpty(t *testing.T) { + if version == "" { + t.Fatal("version constant must not be empty") + } +} + +func TestVersionDefaultMatchesConstant(t *testing.T) { + // When ldflags haven't overridden Version, it should equal the constant. + // In tests, ldflags are not set, so this always holds. + if Version != version { + t.Errorf("Version = %q, want source constant %q (was it overridden by ldflags in a test?)", Version, version) + } +} + +// TestVersionMatchesGitTag verifies that the source constant matches the +// latest git tag. This catches forgotten version bumps before a release. +// +// Skipped when: +// - git is not available +// - there are no tags (new clone / shallow clone) +// - HEAD is not exactly on a tag (development builds between releases) +func TestVersionMatchesGitTag(t *testing.T) { + // Check if HEAD is exactly a tag (git describe --exact-match fails otherwise). + out, err := exec.Command("git", "describe", "--tags", "--exact-match", "HEAD").CombinedOutput() + if err != nil { + t.Skipf("HEAD is not on a tag (expected during development): %v", err) + } + tag := strings.TrimSpace(string(out)) + tag = strings.TrimPrefix(tag, "v") + + if version != tag { + t.Errorf("version constant %q does not match git tag %q — update the constant in version.go before tagging", version, tag) + } +} From cdc07cfc9c647803210e305ad92a0573f2d82d32 Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Tue, 14 Apr 2026 18:40:38 +0200 Subject: [PATCH 02/11] feat(help): show version, command counts, and allowed/not-allowed builtins The help builtin now displays: - A header with the rshell version and builtin count summary ("All N builtins available" or "N of M builtins enabled") - A compact "Not allowed:" list when commands are restricted - With --all, full description tables for both allowed and not-allowed builtins - When all builtins are allowed and --all is used, a confirmation message "All builtins are allowed in this session." Co-Authored-By: Claude Opus 4.6 (1M context) --- analysis/symbols_builtins.go | 3 + builtins/help/help.go | 117 +++++++++++++++--- builtins/tests/help/help_test.go | 87 +++++++++++-- tests/scenarios/cmd/help/all_flag.yaml | 10 ++ tests/scenarios/cmd/help/list_commands.yaml | 2 +- .../cmd/help/restricted_commands.yaml | 6 +- 6 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 tests/scenarios/cmd/help/all_flag.yaml diff --git a/analysis/symbols_builtins.go b/analysis/symbols_builtins.go index bd063a52..31722bac 100644 --- a/analysis/symbols_builtins.go +++ b/analysis/symbols_builtins.go @@ -132,6 +132,8 @@ var builtinPerCommandSymbols = map[string][]string{ "help": { "bytes.Buffer", // 🟢 in-memory buffer to capture --help output from commands; no I/O side effects. "context.Context", // 🟢 deadline/cancellation plumbing; pure interface, no side effects. + "github.com/DataDog/rshell/internal/version.Version", // 🟢 build version string; read-only package-level variable, no I/O. + "strings.Builder", // 🟢 efficient string concatenation; pure in-memory buffer, no I/O. }, "head": { "bufio.NewScanner", // 🟢 line-by-line input reading (e.g. head, cat); no write or exec capability. @@ -413,6 +415,7 @@ var builtinAllowedSymbols = []string{ "errors.New", // 🟢 creates a simple error value; pure function, no I/O. "fmt.Errorf", // 🟢 error formatting; pure function, no I/O. "fmt.Sprintf", // 🟢 string formatting; pure function, no I/O. + "github.com/DataDog/rshell/internal/version.Version", // 🟢 build version string; read-only package-level variable, no I/O. "github.com/prometheus-community/pro-bing.NewPinger", // 🔴 creates an ICMP pinger by resolving host; network I/O is the explicit purpose of the ping builtin. "github.com/prometheus-community/pro-bing.NoopLogger", // 🟢 no-op logger that discards pro-bing internal messages; no side effects. "github.com/prometheus-community/pro-bing.Packet", // 🟢 ICMP packet descriptor struct (received packet data); pure data type, no I/O. diff --git a/builtins/help/help.go b/builtins/help/help.go index 2e6641df..1d83d1f8 100644 --- a/builtins/help/help.go +++ b/builtins/help/help.go @@ -7,11 +7,16 @@ // // help — display help for commands // -// Usage: help [command] +// Usage: help [--all] [command] // -// With no arguments, list all available builtin commands with a brief -// description. When a command name is given, display detailed help for -// that command. +// With no arguments, list allowed builtin commands with descriptions +// and a compact list of not-allowed builtins. When --all is given, +// both sections are shown as full description tables. When a command +// name is given, display detailed help for that command. +// +// Flags: +// +// --all show not-allowed builtins with descriptions (instead of a compact list) // // Exit codes: // @@ -22,8 +27,10 @@ package help import ( "bytes" "context" + "strings" "github.com/DataDog/rshell/builtins" + "github.com/DataDog/rshell/internal/version" ) // Cmd is the help builtin command descriptor. @@ -34,12 +41,13 @@ var Cmd = builtins.Command{ } func printUsage(callCtx *builtins.CallContext) { - callCtx.Out("Usage: help [command]\n") + callCtx.Out("Usage: help [--all] [command]\n") callCtx.Out("Display help for builtin commands.\n") } func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { helpFlag := fs.Bool("help", false, "print usage and exit") + allFlag := fs.Bool("all", false, "show not-allowed builtins with descriptions") return func(ctx context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { if *helpFlag { @@ -88,30 +96,103 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { return builtins.Result{} } - // No arguments — list all allowed commands. + // No arguments — list commands. allNames := builtins.Names() - var names []string + var allowed, notAllowed []string for _, name := range allNames { if callCtx.CommandAllowed != nil && !callCtx.CommandAllowed(name) { + notAllowed = append(notAllowed, name) continue } - names = append(names, name) + allowed = append(allowed, name) } - // Find the longest command name for alignment. - maxLen := 0 - for _, name := range names { - if len(name) > maxLen { - maxLen = len(name) - } - } + // Header: version + command counts. + printHeader(callCtx, len(allowed), len(allNames)) + + // Print allowed commands with descriptions. + printCommandTable(callCtx, allowed) - for _, name := range names { - meta, _ := builtins.Meta(name) - callCtx.Outf("%-*s %s\n", maxLen, name, meta.Description) + // Show not-allowed commands when restrictions are active. + if len(notAllowed) > 0 { + if *allFlag { + // --all: full description table for not-allowed commands. + callCtx.Out("\nNot allowed:\n") + printCommandTable(callCtx, notAllowed) + } else { + // Default: compact comma-separated list. + callCtx.Outf("\nNot allowed: %s\n", wrapNames(notAllowed, 80)) + } + } else if *allFlag { + callCtx.Out("\nAll builtins are allowed in this session.\n") } callCtx.Out("\nRun 'help ' for more information on a specific command.\n") return builtins.Result{} } } + +// printHeader writes the version line and command count summary. +func printHeader(callCtx *builtins.CallContext, allowed, total int) { + var header strings.Builder + header.WriteString("rshell") + if version.Version != "" { + header.WriteByte(' ') + header.WriteString(version.Version) + } + header.WriteString(" — ") + if allowed < total { + callCtx.Outf("%s%d of %d builtins enabled\n\n", header.String(), allowed, total) + } else { + callCtx.Outf("%sAll %d builtins available\n\n", header.String(), total) + } +} + +// printCommandTable prints an aligned name/description table. +func printCommandTable(callCtx *builtins.CallContext, names []string) { + maxLen := 0 + for _, name := range names { + if len(name) > maxLen { + maxLen = len(name) + } + } + for _, name := range names { + meta, _ := builtins.Meta(name) + callCtx.Outf("%-*s %s\n", maxLen, name, meta.Description) + } +} + +// wrapNames formats a list of names into comma-separated lines that stay +// within the given line width. Continuation lines are indented by two spaces. +func wrapNames(names []string, lineWidth int) string { + if len(names) == 0 { + return "" + } + var b strings.Builder + col := 0 + lineStart := true // true at the very start and after each newline indent + for i, name := range names { + token := name + if i < len(names)-1 { + token += "," + } + needed := len(token) + if !lineStart { + needed++ // space before token + } + if !lineStart && col+needed > lineWidth { + b.WriteString("\n ") + col = 2 + lineStart = true + needed = len(token) + } + if !lineStart { + b.WriteByte(' ') + col++ + } + b.WriteString(token) + col += len(token) + lineStart = false + } + return b.String() +} diff --git a/builtins/tests/help/help_test.go b/builtins/tests/help/help_test.go index ccb9d026..ea139c2b 100644 --- a/builtins/tests/help/help_test.go +++ b/builtins/tests/help/help_test.go @@ -60,6 +60,23 @@ func TestHelpExitCode(t *testing.T) { assert.NotEmpty(t, stdout) } +// --- Header --- + +func TestHelpHeaderShowsRshell(t *testing.T) { + stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) + assert.Equal(t, 0, code) + assert.Contains(t, stdout, "rshell") + assert.Contains(t, stdout, "All ") +} + +func TestHelpHeaderRestrictedShowsCount(t *testing.T) { + stdout, _, code := runScript(t, "help", "", + interp.AllowedCommands([]string{"rshell:echo", "rshell:help"})) + assert.Equal(t, 0, code) + assert.Contains(t, stdout, "2 of") + assert.Contains(t, stdout, "builtins enabled") +} + // --- Output content --- func TestHelpListsAllBuiltins(t *testing.T) { @@ -83,10 +100,10 @@ func TestHelpListsSorted(t *testing.T) { assert.Equal(t, 0, code) lines := strings.Split(strings.TrimSpace(stdout), "\n") - // Last line is the footer hint — exclude it and the blank line before it. + // Skip the header (first two lines: header + blank) and footer. var cmdLines []string for _, line := range lines { - if line == "" || strings.HasPrefix(line, "Run '") { + if line == "" || strings.HasPrefix(line, "Run '") || strings.HasPrefix(line, "rshell") { continue } cmdLines = append(cmdLines, line) @@ -137,7 +154,7 @@ func TestHelpColumnsAligned(t *testing.T) { // followed by a non-space character. descCol := -1 for _, line := range lines { - if line == "" || strings.HasPrefix(line, "Run '") { + if line == "" || strings.HasPrefix(line, "Run '") || strings.HasPrefix(line, "rshell") { continue } // Walk backwards from the end to find where the description text starts. @@ -165,16 +182,39 @@ func TestHelpColumnsAligned(t *testing.T) { // --- Restricted commands --- -func TestHelpRestrictedShowsOnlyAllowed(t *testing.T) { +func TestHelpRestrictedShowsOnlyAllowedInTable(t *testing.T) { stdout, stderr, code := runScript(t, "help", "", interp.AllowedCommands([]string{"rshell:echo", "rshell:help"})) assert.Equal(t, 0, code) assert.Empty(t, stderr) assert.Contains(t, stdout, "echo") assert.Contains(t, stdout, "help") - assert.NotContains(t, stdout, "cat") - assert.NotContains(t, stdout, "grep") - assert.NotContains(t, stdout, "ls") + // The allowed-commands table (lines before "Not allowed:") should only + // contain allowed commands. The "Not allowed:" line lists the rest. + inAllowedSection := true + for _, line := range strings.Split(stdout, "\n") { + if strings.HasPrefix(line, "Not allowed:") { + inAllowedSection = false + } + if !inAllowedSection || strings.HasPrefix(line, "rshell") || line == "" || strings.HasPrefix(line, "Run '") { + continue + } + fields := strings.Fields(line) + if len(fields) > 0 { + assert.True(t, fields[0] == "echo" || fields[0] == "help", + "unexpected command in allowed table: %q", fields[0]) + } + } +} + +func TestHelpRestrictedShowsNotAllowedList(t *testing.T) { + stdout, _, code := runScript(t, "help", "", + interp.AllowedCommands([]string{"rshell:echo", "rshell:help"})) + assert.Equal(t, 0, code) + assert.Contains(t, stdout, "Not allowed:") + assert.Contains(t, stdout, "cat") + assert.Contains(t, stdout, "grep") + assert.Contains(t, stdout, "ls") } func TestHelpRestrictedSingleCommand(t *testing.T) { @@ -183,7 +223,6 @@ func TestHelpRestrictedSingleCommand(t *testing.T) { assert.Equal(t, 0, code) assert.Contains(t, stdout, "help") assert.Contains(t, stdout, "ls") - assert.NotContains(t, stdout, "echo") } func TestHelpRestrictedAlignmentAdjusts(t *testing.T) { @@ -195,7 +234,7 @@ func TestHelpRestrictedAlignmentAdjusts(t *testing.T) { lines := strings.Split(strings.TrimSpace(stdout), "\n") for _, line := range lines { - if line == "" || strings.HasPrefix(line, "Run '") { + if line == "" || strings.HasPrefix(line, "Run '") || strings.HasPrefix(line, "rshell") { continue } // "strings" is the longest name (7 chars), so the description should @@ -225,6 +264,36 @@ func TestHelpAlwaysAvailableNoCommands(t *testing.T) { assert.Contains(t, stderr, "command not allowed") } +// --- --all flag --- + +func TestHelpAllFlagShowsNotAllowedWithDescriptions(t *testing.T) { + stdout, _, code := runScript(t, "help --all", "", + interp.AllowedCommands([]string{"rshell:echo", "rshell:help"})) + assert.Equal(t, 0, code) + assert.Contains(t, stdout, "Not allowed:") + // --all shows full description table for not-allowed commands. + assert.Contains(t, stdout, "concatenate and print files") // cat description + assert.Contains(t, stdout, "print lines that match patterns") // grep description +} + +func TestHelpAllFlagNoRestrictions(t *testing.T) { + // When all commands are allowed, --all should not show "Not allowed:" + // but should confirm that all builtins are allowed. + stdout, _, code := runScript(t, "help --all", "", interpoption.AllowAllCommands().(interp.RunnerOption)) + assert.Equal(t, 0, code) + assert.NotContains(t, stdout, "Not allowed:") + assert.Contains(t, stdout, "All builtins are allowed in this session.") +} + +func TestHelpAllFlagStillShowsAllowed(t *testing.T) { + stdout, _, code := runScript(t, "help --all", "", + interp.AllowedCommands([]string{"rshell:echo", "rshell:help"})) + assert.Equal(t, 0, code) + // Allowed commands should still appear with descriptions. + assert.Contains(t, stdout, "write arguments to stdout") + assert.Contains(t, stdout, "display help for commands") +} + // --- Error handling --- func TestHelpUnknownCommandShowsError(t *testing.T) { diff --git a/tests/scenarios/cmd/help/all_flag.yaml b/tests/scenarios/cmd/help/all_flag.yaml new file mode 100644 index 00000000..56df7810 --- /dev/null +++ b/tests/scenarios/cmd/help/all_flag.yaml @@ -0,0 +1,10 @@ +description: Help --all shows allowed and not-allowed commands with descriptions. +skip_assert_against_bash: true +input: + allowed_commands: ["rshell:echo", "rshell:help"] + script: |+ + help --all +expect: + stdout_contains: ["2 of", "builtins enabled", "echo", "help", "Not allowed:", "cat", "concatenate and print files", "grep", "print lines that match patterns"] + stderr: "" + exit_code: 0 diff --git a/tests/scenarios/cmd/help/list_commands.yaml b/tests/scenarios/cmd/help/list_commands.yaml index 6d17a968..0a6c9b39 100644 --- a/tests/scenarios/cmd/help/list_commands.yaml +++ b/tests/scenarios/cmd/help/list_commands.yaml @@ -4,6 +4,6 @@ input: script: |+ help expect: - stdout_contains: ["cat", "echo", "grep", "help", "ls", "test", "true", "false"] + stdout_contains: ["rshell", "All ", "builtins available", "cat", "echo", "grep", "help", "ls", "test", "true", "false"] stderr: "" exit_code: 0 diff --git a/tests/scenarios/cmd/help/restricted_commands.yaml b/tests/scenarios/cmd/help/restricted_commands.yaml index 9f7ed8e4..28a27ae5 100644 --- a/tests/scenarios/cmd/help/restricted_commands.yaml +++ b/tests/scenarios/cmd/help/restricted_commands.yaml @@ -5,10 +5,6 @@ input: script: |+ help expect: - stdout: | - echo write arguments to stdout - help display help for commands - - Run 'help ' for more information on a specific command. + stdout_contains: ["2 of", "builtins enabled", "echo", "help", "Not allowed:", "cat", "Run 'help '"] stderr: "" exit_code: 0 From 696467b87bdbcafdc27412f1b2884e8455edb7d9 Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Wed, 15 Apr 2026 16:53:39 +0200 Subject: [PATCH 03/11] fix(help): rename 'Not allowed' to 'Disabled builtin(s)' Address review feedback from matt-dz. Uses singular/plural form based on the count. Co-Authored-By: Claude Opus 4.6 (1M context) --- builtins/help/help.go | 12 ++++++++---- builtins/tests/help/help_test.go | 14 +++++++------- tests/scenarios/cmd/help/all_flag.yaml | 2 +- tests/scenarios/cmd/help/restricted_commands.yaml | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/builtins/help/help.go b/builtins/help/help.go index 1d83d1f8..64f0fd69 100644 --- a/builtins/help/help.go +++ b/builtins/help/help.go @@ -113,15 +113,19 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { // Print allowed commands with descriptions. printCommandTable(callCtx, allowed) - // Show not-allowed commands when restrictions are active. + // Show disabled builtins when restrictions are active. if len(notAllowed) > 0 { + label := "Disabled builtins" + if len(notAllowed) == 1 { + label = "Disabled builtin" + } if *allFlag { - // --all: full description table for not-allowed commands. - callCtx.Out("\nNot allowed:\n") + // --all: full description table for disabled builtins. + callCtx.Outf("\n%s:\n", label) printCommandTable(callCtx, notAllowed) } else { // Default: compact comma-separated list. - callCtx.Outf("\nNot allowed: %s\n", wrapNames(notAllowed, 80)) + callCtx.Outf("\n%s: %s\n", label, wrapNames(notAllowed, 80)) } } else if *allFlag { callCtx.Out("\nAll builtins are allowed in this session.\n") diff --git a/builtins/tests/help/help_test.go b/builtins/tests/help/help_test.go index ea139c2b..de98fde9 100644 --- a/builtins/tests/help/help_test.go +++ b/builtins/tests/help/help_test.go @@ -189,11 +189,11 @@ func TestHelpRestrictedShowsOnlyAllowedInTable(t *testing.T) { assert.Empty(t, stderr) assert.Contains(t, stdout, "echo") assert.Contains(t, stdout, "help") - // The allowed-commands table (lines before "Not allowed:") should only - // contain allowed commands. The "Not allowed:" line lists the rest. + // The allowed-commands table (lines before "Disabled builtin") should only + // contain allowed commands. The "Disabled builtin" line lists the rest. inAllowedSection := true for _, line := range strings.Split(stdout, "\n") { - if strings.HasPrefix(line, "Not allowed:") { + if strings.HasPrefix(line, "Disabled builtin") { inAllowedSection = false } if !inAllowedSection || strings.HasPrefix(line, "rshell") || line == "" || strings.HasPrefix(line, "Run '") { @@ -211,7 +211,7 @@ func TestHelpRestrictedShowsNotAllowedList(t *testing.T) { stdout, _, code := runScript(t, "help", "", interp.AllowedCommands([]string{"rshell:echo", "rshell:help"})) assert.Equal(t, 0, code) - assert.Contains(t, stdout, "Not allowed:") + assert.Contains(t, stdout, "Disabled builtin") assert.Contains(t, stdout, "cat") assert.Contains(t, stdout, "grep") assert.Contains(t, stdout, "ls") @@ -270,18 +270,18 @@ func TestHelpAllFlagShowsNotAllowedWithDescriptions(t *testing.T) { stdout, _, code := runScript(t, "help --all", "", interp.AllowedCommands([]string{"rshell:echo", "rshell:help"})) assert.Equal(t, 0, code) - assert.Contains(t, stdout, "Not allowed:") + assert.Contains(t, stdout, "Disabled builtin") // --all shows full description table for not-allowed commands. assert.Contains(t, stdout, "concatenate and print files") // cat description assert.Contains(t, stdout, "print lines that match patterns") // grep description } func TestHelpAllFlagNoRestrictions(t *testing.T) { - // When all commands are allowed, --all should not show "Not allowed:" + // When all commands are allowed, --all should not show "Disabled builtin" // but should confirm that all builtins are allowed. stdout, _, code := runScript(t, "help --all", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) - assert.NotContains(t, stdout, "Not allowed:") + assert.NotContains(t, stdout, "Disabled builtin") assert.Contains(t, stdout, "All builtins are allowed in this session.") } diff --git a/tests/scenarios/cmd/help/all_flag.yaml b/tests/scenarios/cmd/help/all_flag.yaml index 56df7810..c3cb457d 100644 --- a/tests/scenarios/cmd/help/all_flag.yaml +++ b/tests/scenarios/cmd/help/all_flag.yaml @@ -5,6 +5,6 @@ input: script: |+ help --all expect: - stdout_contains: ["2 of", "builtins enabled", "echo", "help", "Not allowed:", "cat", "concatenate and print files", "grep", "print lines that match patterns"] + stdout_contains: ["2 of", "builtins enabled", "echo", "help", "Disabled builtin", "cat", "concatenate and print files", "grep", "print lines that match patterns"] stderr: "" exit_code: 0 diff --git a/tests/scenarios/cmd/help/restricted_commands.yaml b/tests/scenarios/cmd/help/restricted_commands.yaml index 28a27ae5..b9aaf675 100644 --- a/tests/scenarios/cmd/help/restricted_commands.yaml +++ b/tests/scenarios/cmd/help/restricted_commands.yaml @@ -5,6 +5,6 @@ input: script: |+ help expect: - stdout_contains: ["2 of", "builtins enabled", "echo", "help", "Not allowed:", "cat", "Run 'help '"] + stdout_contains: ["2 of", "builtins enabled", "echo", "help", "Disabled builtin", "cat", "Run 'help '"] stderr: "" exit_code: 0 From ef38de81fbbd69c091f5be7361a9260930a74bca Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Thu, 16 Apr 2026 12:02:38 +0200 Subject: [PATCH 04/11] fix(help): improve --all flag wording and use full stdout in tests Address PR review feedback: reword --all flag description to "show all builtins (including not allowed) with descriptions" and replace stdout_contains with full stdout assertions in all three help scenario tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- builtins/help/help.go | 4 +-- tests/scenarios/cmd/help/all_flag.yaml | 36 ++++++++++++++++++- tests/scenarios/cmd/help/list_commands.yaml | 34 +++++++++++++++++- .../cmd/help/restricted_commands.yaml | 11 +++++- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/builtins/help/help.go b/builtins/help/help.go index 64f0fd69..421a43ef 100644 --- a/builtins/help/help.go +++ b/builtins/help/help.go @@ -16,7 +16,7 @@ // // Flags: // -// --all show not-allowed builtins with descriptions (instead of a compact list) +// --all show all builtins (including not allowed) with descriptions // // Exit codes: // @@ -47,7 +47,7 @@ func printUsage(callCtx *builtins.CallContext) { func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { helpFlag := fs.Bool("help", false, "print usage and exit") - allFlag := fs.Bool("all", false, "show not-allowed builtins with descriptions") + allFlag := fs.Bool("all", false, "show all builtins (including not allowed) with descriptions") return func(ctx context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { if *helpFlag { diff --git a/tests/scenarios/cmd/help/all_flag.yaml b/tests/scenarios/cmd/help/all_flag.yaml index c3cb457d..66165d15 100644 --- a/tests/scenarios/cmd/help/all_flag.yaml +++ b/tests/scenarios/cmd/help/all_flag.yaml @@ -5,6 +5,40 @@ input: script: |+ help --all expect: - stdout_contains: ["2 of", "builtins enabled", "echo", "help", "Disabled builtin", "cat", "concatenate and print files", "grep", "print lines that match patterns"] + stdout: |+ + rshell 0.0.10 — 2 of 28 builtins enabled + + echo write arguments to stdout + help display help for commands + + Disabled builtins: + [ evaluate conditional expression + break exit from a loop + cat concatenate and print files + continue continue a loop iteration + cut remove sections from each line + exit exit the shell + false return unsuccessful exit status + find search for files in a directory hierarchy + grep print lines that match patterns + head output the first part of files + ip show network interface and routing information + ls list directory contents + ping send ICMP echo requests to a network host + printf format and print data + ps report process status + sed stream editor for filtering and transforming text + sort sort lines of text files + ss display socket statistics + strings print printable character sequences + tail output the last part of files + test evaluate conditional expression + tr translate or delete characters + true return successful exit status + uname print system information + uniq report or omit repeated lines + wc print newline, word, and byte counts + + Run 'help ' for more information on a specific command. stderr: "" exit_code: 0 diff --git a/tests/scenarios/cmd/help/list_commands.yaml b/tests/scenarios/cmd/help/list_commands.yaml index 0a6c9b39..058f7c8e 100644 --- a/tests/scenarios/cmd/help/list_commands.yaml +++ b/tests/scenarios/cmd/help/list_commands.yaml @@ -4,6 +4,38 @@ input: script: |+ help expect: - stdout_contains: ["rshell", "All ", "builtins available", "cat", "echo", "grep", "help", "ls", "test", "true", "false"] + stdout: |+ + rshell 0.0.10 — All 28 builtins available + + [ evaluate conditional expression + break exit from a loop + cat concatenate and print files + continue continue a loop iteration + cut remove sections from each line + echo write arguments to stdout + exit exit the shell + false return unsuccessful exit status + find search for files in a directory hierarchy + grep print lines that match patterns + head output the first part of files + help display help for commands + ip show network interface and routing information + ls list directory contents + ping send ICMP echo requests to a network host + printf format and print data + ps report process status + sed stream editor for filtering and transforming text + sort sort lines of text files + ss display socket statistics + strings print printable character sequences + tail output the last part of files + test evaluate conditional expression + tr translate or delete characters + true return successful exit status + uname print system information + uniq report or omit repeated lines + wc print newline, word, and byte counts + + Run 'help ' for more information on a specific command. stderr: "" exit_code: 0 diff --git a/tests/scenarios/cmd/help/restricted_commands.yaml b/tests/scenarios/cmd/help/restricted_commands.yaml index b9aaf675..3331cb2d 100644 --- a/tests/scenarios/cmd/help/restricted_commands.yaml +++ b/tests/scenarios/cmd/help/restricted_commands.yaml @@ -5,6 +5,15 @@ input: script: |+ help expect: - stdout_contains: ["2 of", "builtins enabled", "echo", "help", "Disabled builtin", "cat", "Run 'help '"] + stdout: |+ + rshell 0.0.10 — 2 of 28 builtins enabled + + echo write arguments to stdout + help display help for commands + + Disabled builtins: [, break, cat, continue, cut, exit, false, find, grep, head, ip, ls, ping, + printf, ps, sed, sort, ss, strings, tail, test, tr, true, uname, uniq, wc + + Run 'help ' for more information on a specific command. stderr: "" exit_code: 0 From 1479bcf8a6d8b5a6bfa415cc693cd2dce552ef80 Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Thu, 16 Apr 2026 13:20:56 +0200 Subject: [PATCH 05/11] fix(help): hide "dev" version in help header Skip displaying the version when it is "dev" (the default for development/test builds). Real version strings from tagged releases or ldflags are still shown. Co-Authored-By: Claude Opus 4.6 (1M context) --- builtins/help/help.go | 4 ++-- tests/scenarios/cmd/help/all_flag.yaml | 2 +- tests/scenarios/cmd/help/list_commands.yaml | 2 +- tests/scenarios/cmd/help/restricted_commands.yaml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/builtins/help/help.go b/builtins/help/help.go index 421a43ef..7dd2a97f 100644 --- a/builtins/help/help.go +++ b/builtins/help/help.go @@ -140,9 +140,9 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { func printHeader(callCtx *builtins.CallContext, allowed, total int) { var header strings.Builder header.WriteString("rshell") - if version.Version != "" { + if v := version.Version; v != "" && v != "dev" { header.WriteByte(' ') - header.WriteString(version.Version) + header.WriteString(v) } header.WriteString(" — ") if allowed < total { diff --git a/tests/scenarios/cmd/help/all_flag.yaml b/tests/scenarios/cmd/help/all_flag.yaml index 236b151e..6ec88378 100644 --- a/tests/scenarios/cmd/help/all_flag.yaml +++ b/tests/scenarios/cmd/help/all_flag.yaml @@ -6,7 +6,7 @@ input: help --all expect: stdout: |+ - rshell dev — 2 of 28 builtins enabled + rshell — 2 of 28 builtins enabled echo write arguments to stdout help display help for commands diff --git a/tests/scenarios/cmd/help/list_commands.yaml b/tests/scenarios/cmd/help/list_commands.yaml index 1656cb75..6dc5b49a 100644 --- a/tests/scenarios/cmd/help/list_commands.yaml +++ b/tests/scenarios/cmd/help/list_commands.yaml @@ -5,7 +5,7 @@ input: help expect: stdout: |+ - rshell dev — All 28 builtins available + rshell — All 28 builtins available [ evaluate conditional expression break exit from a loop diff --git a/tests/scenarios/cmd/help/restricted_commands.yaml b/tests/scenarios/cmd/help/restricted_commands.yaml index 588332fd..850aad05 100644 --- a/tests/scenarios/cmd/help/restricted_commands.yaml +++ b/tests/scenarios/cmd/help/restricted_commands.yaml @@ -6,7 +6,7 @@ input: help expect: stdout: |+ - rshell dev — 2 of 28 builtins enabled + rshell — 2 of 28 builtins enabled echo write arguments to stdout help display help for commands From 82dd09489c0e83e602a5d385a274dd77cfd1efbc Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Thu, 16 Apr 2026 13:24:54 +0200 Subject: [PATCH 06/11] fix: revert Makefile ldflags from old version approach The version package on main uses debug.ReadBuildInfo() instead of ldflags, so the VERSION/COMMIT ldflags in the Makefile are unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index cdd1f329..f3a15812 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,7 @@ .PHONY: build fmt test test_all test_against_bash compliance -VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo unknown) -COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) -LDFLAGS = -X github.com/DataDog/rshell/internal/version.Version=$(VERSION) \ - -X github.com/DataDog/rshell/internal/version.Commit=$(COMMIT) - build: - go build -ldflags "$(LDFLAGS)" -o rshell ./cmd/rshell + go build -o rshell ./cmd/rshell fmt: go fmt ./... From 4288bfdd6f1f5940f050157feb150dcd09a72e71 Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Thu, 16 Apr 2026 13:27:36 +0200 Subject: [PATCH 07/11] test(help): add scenario for help --all with no restrictions Covers case 4 from the PR description: all builtins allowed with --all flag, showing the "All builtins are allowed in this session." message. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cmd/help/all_flag_unrestricted.yaml | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/scenarios/cmd/help/all_flag_unrestricted.yaml diff --git a/tests/scenarios/cmd/help/all_flag_unrestricted.yaml b/tests/scenarios/cmd/help/all_flag_unrestricted.yaml new file mode 100644 index 00000000..a6e539a4 --- /dev/null +++ b/tests/scenarios/cmd/help/all_flag_unrestricted.yaml @@ -0,0 +1,43 @@ +description: Help --all with no restrictions shows all builtins and a confirmation message. +skip_assert_against_bash: true +input: + script: |+ + help --all +expect: + stdout: |+ + rshell — All 28 builtins available + + [ evaluate conditional expression + break exit from a loop + cat concatenate and print files + continue continue a loop iteration + cut remove sections from each line + echo write arguments to stdout + exit exit the shell + false return unsuccessful exit status + find search for files in a directory hierarchy + grep print lines that match patterns + head output the first part of files + help display help for commands + ip show network interface and routing information + ls list directory contents + ping send ICMP echo requests to a network host + printf format and print data + ps report process status + sed stream editor for filtering and transforming text + sort sort lines of text files + ss display socket statistics + strings print printable character sequences + tail output the last part of files + test evaluate conditional expression + tr translate or delete characters + true return successful exit status + uname print system information + uniq report or omit repeated lines + wc print newline, word, and byte counts + + All builtins are allowed in this session. + + Run 'help ' for more information on a specific command. + stderr: "" + exit_code: 0 From a190bdfcd1650253808b1bf93623f2c456d3237f Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Thu, 16 Apr 2026 13:28:38 +0200 Subject: [PATCH 08/11] refactor(help): rename test scenarios for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Group the four help listing tests by restricted/unrestricted with an _all_flag suffix: - list_commands → unrestricted - all_flag_unrestricted → unrestricted_all_flag - restricted_commands → restricted - all_flag → restricted_all_flag Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cmd/help/{restricted_commands.yaml => restricted.yaml} | 0 .../cmd/help/{all_flag.yaml => restricted_all_flag.yaml} | 0 .../scenarios/cmd/help/{list_commands.yaml => unrestricted.yaml} | 0 .../{all_flag_unrestricted.yaml => unrestricted_all_flag.yaml} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/scenarios/cmd/help/{restricted_commands.yaml => restricted.yaml} (100%) rename tests/scenarios/cmd/help/{all_flag.yaml => restricted_all_flag.yaml} (100%) rename tests/scenarios/cmd/help/{list_commands.yaml => unrestricted.yaml} (100%) rename tests/scenarios/cmd/help/{all_flag_unrestricted.yaml => unrestricted_all_flag.yaml} (100%) diff --git a/tests/scenarios/cmd/help/restricted_commands.yaml b/tests/scenarios/cmd/help/restricted.yaml similarity index 100% rename from tests/scenarios/cmd/help/restricted_commands.yaml rename to tests/scenarios/cmd/help/restricted.yaml diff --git a/tests/scenarios/cmd/help/all_flag.yaml b/tests/scenarios/cmd/help/restricted_all_flag.yaml similarity index 100% rename from tests/scenarios/cmd/help/all_flag.yaml rename to tests/scenarios/cmd/help/restricted_all_flag.yaml diff --git a/tests/scenarios/cmd/help/list_commands.yaml b/tests/scenarios/cmd/help/unrestricted.yaml similarity index 100% rename from tests/scenarios/cmd/help/list_commands.yaml rename to tests/scenarios/cmd/help/unrestricted.yaml diff --git a/tests/scenarios/cmd/help/all_flag_unrestricted.yaml b/tests/scenarios/cmd/help/unrestricted_all_flag.yaml similarity index 100% rename from tests/scenarios/cmd/help/all_flag_unrestricted.yaml rename to tests/scenarios/cmd/help/unrestricted_all_flag.yaml From edf9c513fc71085922562be4ca85786c9d22de4e Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Thu, 16 Apr 2026 13:31:32 +0200 Subject: [PATCH 09/11] fix(version): restore ldflags support for standalone builds var Version = buildVersion() always overwrites ldflags at init time. Switch to an empty var with an init() guard so -X ldflags take precedence. Re-add the Makefile VERSION ldflag (without Commit, which no longer exists). Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 5 ++++- internal/version/version.go | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f3a15812..21768356 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ .PHONY: build fmt test test_all test_against_bash compliance +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +LDFLAGS = -X github.com/DataDog/rshell/internal/version.Version=$(VERSION) + build: - go build -o rshell ./cmd/rshell + go build -ldflags "$(LDFLAGS)" -o rshell ./cmd/rshell fmt: go fmt ./... diff --git a/internal/version/version.go b/internal/version/version.go index f6614f7b..26d1da70 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -14,7 +14,14 @@ import "runtime/debug" const modulePath = "github.com/DataDog/rshell" // Version is the build version string. -var Version = buildVersion() +// Set via ldflags for standalone builds; falls back to build info. +var Version string + +func init() { + if Version == "" { + Version = buildVersion() + } +} // buildVersion reads the rshell version from Go's embedded build info. // When rshell is a dependency (e.g. in the Datadog Agent), the version From 561880135d38450dac859f201b5c8bd936e730e9 Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Thu, 16 Apr 2026 13:32:39 +0200 Subject: [PATCH 10/11] fix(help): always display version in help header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the "dev" suppression — always show the version string regardless of its value. Restore Makefile and version.go to match main. Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 5 +---- builtins/help/help.go | 4 ++-- internal/version/version.go | 9 +-------- tests/scenarios/cmd/help/restricted.yaml | 2 +- tests/scenarios/cmd/help/restricted_all_flag.yaml | 2 +- tests/scenarios/cmd/help/unrestricted.yaml | 2 +- tests/scenarios/cmd/help/unrestricted_all_flag.yaml | 2 +- 7 files changed, 8 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 21768356..f3a15812 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,7 @@ .PHONY: build fmt test test_all test_against_bash compliance -VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) -LDFLAGS = -X github.com/DataDog/rshell/internal/version.Version=$(VERSION) - build: - go build -ldflags "$(LDFLAGS)" -o rshell ./cmd/rshell + go build -o rshell ./cmd/rshell fmt: go fmt ./... diff --git a/builtins/help/help.go b/builtins/help/help.go index 7dd2a97f..421a43ef 100644 --- a/builtins/help/help.go +++ b/builtins/help/help.go @@ -140,9 +140,9 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { func printHeader(callCtx *builtins.CallContext, allowed, total int) { var header strings.Builder header.WriteString("rshell") - if v := version.Version; v != "" && v != "dev" { + if version.Version != "" { header.WriteByte(' ') - header.WriteString(v) + header.WriteString(version.Version) } header.WriteString(" — ") if allowed < total { diff --git a/internal/version/version.go b/internal/version/version.go index 26d1da70..f6614f7b 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -14,14 +14,7 @@ import "runtime/debug" const modulePath = "github.com/DataDog/rshell" // Version is the build version string. -// Set via ldflags for standalone builds; falls back to build info. -var Version string - -func init() { - if Version == "" { - Version = buildVersion() - } -} +var Version = buildVersion() // buildVersion reads the rshell version from Go's embedded build info. // When rshell is a dependency (e.g. in the Datadog Agent), the version diff --git a/tests/scenarios/cmd/help/restricted.yaml b/tests/scenarios/cmd/help/restricted.yaml index 850aad05..588332fd 100644 --- a/tests/scenarios/cmd/help/restricted.yaml +++ b/tests/scenarios/cmd/help/restricted.yaml @@ -6,7 +6,7 @@ input: help expect: stdout: |+ - rshell — 2 of 28 builtins enabled + rshell dev — 2 of 28 builtins enabled echo write arguments to stdout help display help for commands diff --git a/tests/scenarios/cmd/help/restricted_all_flag.yaml b/tests/scenarios/cmd/help/restricted_all_flag.yaml index 6ec88378..236b151e 100644 --- a/tests/scenarios/cmd/help/restricted_all_flag.yaml +++ b/tests/scenarios/cmd/help/restricted_all_flag.yaml @@ -6,7 +6,7 @@ input: help --all expect: stdout: |+ - rshell — 2 of 28 builtins enabled + rshell dev — 2 of 28 builtins enabled echo write arguments to stdout help display help for commands diff --git a/tests/scenarios/cmd/help/unrestricted.yaml b/tests/scenarios/cmd/help/unrestricted.yaml index 6dc5b49a..1656cb75 100644 --- a/tests/scenarios/cmd/help/unrestricted.yaml +++ b/tests/scenarios/cmd/help/unrestricted.yaml @@ -5,7 +5,7 @@ input: help expect: stdout: |+ - rshell — All 28 builtins available + rshell dev — All 28 builtins available [ evaluate conditional expression break exit from a loop diff --git a/tests/scenarios/cmd/help/unrestricted_all_flag.yaml b/tests/scenarios/cmd/help/unrestricted_all_flag.yaml index a6e539a4..12122ab8 100644 --- a/tests/scenarios/cmd/help/unrestricted_all_flag.yaml +++ b/tests/scenarios/cmd/help/unrestricted_all_flag.yaml @@ -5,7 +5,7 @@ input: help --all expect: stdout: |+ - rshell — All 28 builtins available + rshell dev — All 28 builtins available [ evaluate conditional expression break exit from a loop From 18b7b65c2817cbabd51e256aa4b0c81f457234ad Mon Sep 17 00:00:00 2001 From: Jules Macret Date: Thu, 16 Apr 2026 13:33:12 +0200 Subject: [PATCH 11/11] fix(help): wrap version in parentheses in help header Display as "rshell (v0.0.10)" or "rshell (dev)" instead of "rshell v0.0.10". Co-Authored-By: Claude Opus 4.6 (1M context) --- builtins/help/help.go | 3 ++- tests/scenarios/cmd/help/restricted.yaml | 2 +- tests/scenarios/cmd/help/restricted_all_flag.yaml | 2 +- tests/scenarios/cmd/help/unrestricted.yaml | 2 +- tests/scenarios/cmd/help/unrestricted_all_flag.yaml | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/builtins/help/help.go b/builtins/help/help.go index 421a43ef..8790e339 100644 --- a/builtins/help/help.go +++ b/builtins/help/help.go @@ -141,8 +141,9 @@ func printHeader(callCtx *builtins.CallContext, allowed, total int) { var header strings.Builder header.WriteString("rshell") if version.Version != "" { - header.WriteByte(' ') + header.WriteString(" (") header.WriteString(version.Version) + header.WriteByte(')') } header.WriteString(" — ") if allowed < total { diff --git a/tests/scenarios/cmd/help/restricted.yaml b/tests/scenarios/cmd/help/restricted.yaml index 588332fd..bc2e34ce 100644 --- a/tests/scenarios/cmd/help/restricted.yaml +++ b/tests/scenarios/cmd/help/restricted.yaml @@ -6,7 +6,7 @@ input: help expect: stdout: |+ - rshell dev — 2 of 28 builtins enabled + rshell (dev) — 2 of 28 builtins enabled echo write arguments to stdout help display help for commands diff --git a/tests/scenarios/cmd/help/restricted_all_flag.yaml b/tests/scenarios/cmd/help/restricted_all_flag.yaml index 236b151e..b7b077c5 100644 --- a/tests/scenarios/cmd/help/restricted_all_flag.yaml +++ b/tests/scenarios/cmd/help/restricted_all_flag.yaml @@ -6,7 +6,7 @@ input: help --all expect: stdout: |+ - rshell dev — 2 of 28 builtins enabled + rshell (dev) — 2 of 28 builtins enabled echo write arguments to stdout help display help for commands diff --git a/tests/scenarios/cmd/help/unrestricted.yaml b/tests/scenarios/cmd/help/unrestricted.yaml index 1656cb75..3b2d164c 100644 --- a/tests/scenarios/cmd/help/unrestricted.yaml +++ b/tests/scenarios/cmd/help/unrestricted.yaml @@ -5,7 +5,7 @@ input: help expect: stdout: |+ - rshell dev — All 28 builtins available + rshell (dev) — All 28 builtins available [ evaluate conditional expression break exit from a loop diff --git a/tests/scenarios/cmd/help/unrestricted_all_flag.yaml b/tests/scenarios/cmd/help/unrestricted_all_flag.yaml index 12122ab8..fc0b019a 100644 --- a/tests/scenarios/cmd/help/unrestricted_all_flag.yaml +++ b/tests/scenarios/cmd/help/unrestricted_all_flag.yaml @@ -5,7 +5,7 @@ input: help --all expect: stdout: |+ - rshell dev — All 28 builtins available + rshell (dev) — All 28 builtins available [ evaluate conditional expression break exit from a loop