Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions interp/allowed_paths_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestAllowedPathsExecBlocked(t *testing.T) {
AllowedPaths(append([]string{dir}, systemExecAllowedPaths(t)...)),
)
assert.Equal(t, 127, exitCode)
assert.Contains(t, stderr, "command not found")
assert.Contains(t, stderr, "unknown command")
}

func TestAllowedPathsExecNonexistent(t *testing.T) {
Expand All @@ -93,7 +93,7 @@ func TestAllowedPathsExecNonexistent(t *testing.T) {
AllowedPaths(append([]string{dir}, systemExecAllowedPaths(t)...)),
)
assert.Equal(t, 127, exitCode)
assert.Contains(t, stderr, "command not found")
assert.Contains(t, stderr, "unknown command")
}

func TestAllowedPathsExecViaPathLookup(t *testing.T) {
Expand Down Expand Up @@ -146,7 +146,7 @@ func TestAllowedPathsExecSymlinkEscape(t *testing.T) {
AllowedPaths([]string{dir}),
)
assert.Equal(t, 127, exitCode)
assert.Contains(t, stderr, "command not found")
assert.Contains(t, stderr, "unknown command")
}

func TestRunRecoversPanic(t *testing.T) {
Expand Down Expand Up @@ -195,10 +195,10 @@ func TestAllowedPathsExecDefaultBlocksAll(t *testing.T) {
dir := t.TempDir()
// No AllowedPaths option — default noExecHandler blocks all external commands.
// With AllowAllCommands (set by runScriptInternal), the command reaches the
// exec handler which returns "command not found" via noExecHandler.
// exec handler which returns "unknown command" via noExecHandler.
_, stderr, exitCode := runScriptInternal(t, `/bin/echo hello`, dir)
assert.Equal(t, 127, exitCode)
assert.Contains(t, stderr, "command not found")
assert.Contains(t, stderr, "unknown command")
}

// TestHostPrefixAfterAllowedPaths verifies that HostPrefix applied after
Expand Down
4 changes: 2 additions & 2 deletions interp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,10 +403,10 @@ func (r *Runner) Reset() {
r.readDirHandler = func(ctx context.Context, path string) ([]os.DirEntry, error) {
return r.sandbox.ReadDirForGlob(path, HandlerCtx(ctx).Dir)
}
r.execHandler = noExecHandler()
r.execHandler = noExecHandler(r.allowAllCommands || r.allowedCommands["help"])
}
if r.execHandler == nil {
r.execHandler = noExecHandler()
r.execHandler = noExecHandler(r.allowAllCommands || r.allowedCommands["help"])
}
}
// Reset only the mutable state; config is preserved.
Expand Down
12 changes: 8 additions & 4 deletions interp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,19 @@ type ReadDirHandlerFunc func(ctx context.Context, path string) ([]fs.DirEntry, e
type ExecHandlerFunc func(ctx context.Context, args []string) error

// noExecHandler returns an [ExecHandlerFunc] that rejects all commands.
// It prints "<cmd>: command not found" to stderr and returns exit code 127,
// without ever searching PATH or executing host binaries.
func noExecHandler() ExecHandlerFunc {
// It prints "rshell: <cmd>: unknown command" to stderr and returns exit
// code 127, without ever searching PATH or executing host binaries.
// When helpAvailable is true, a hint directing the user to 'help' is appended.
func noExecHandler(helpAvailable bool) ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
if len(args) == 0 {
return fmt.Errorf("exec handler called with no arguments")
}
hc := HandlerCtx(ctx)
fmt.Fprintf(hc.Stderr, "%s: command not found\n", args[0])
fmt.Fprintf(hc.Stderr, "rshell: %s: unknown command\n", args[0])
if helpAvailable {
fmt.Fprintf(hc.Stderr, "Run 'help' to see available commands.\n")
}
return ExitStatus(127)
}
}
9 changes: 6 additions & 3 deletions interp/runner_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,10 @@ func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) {
name := args[0]

if !r.allowAllCommands && !r.allowedCommands[name] {
r.errf("%s: command not allowed\n", name)
r.errf("rshell: %s: command not allowed\n", name)
if r.allowedCommands["help"] {
r.errf("Run 'help' to see allowed commands.\n")
}
r.exit.code = 127
return
}
Expand All @@ -266,11 +269,11 @@ func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) {
var runCmd func(context.Context, string, string, []string) (uint8, error)
runCmd = func(ctx context.Context, dir string, cmdName string, cmdArgs []string) (uint8, error) {
if !r.allowAllCommands && !r.allowedCommands[cmdName] {
return 127, fmt.Errorf("%s: command not allowed", cmdName)
return 127, fmt.Errorf("rshell: %s: command not allowed", cmdName)
}
cmdFn, ok := builtins.Lookup(cmdName)
if !ok {
return 127, fmt.Errorf("%s: command not found", cmdName)
return 127, fmt.Errorf("rshell: %s: unknown command", cmdName)
}
child := &builtins.CallContext{
Stdout: r.stdout,
Expand Down
4 changes: 3 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/basic/after_echo.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: An unknown command after a successful echo still fails.
input:
script: |+
Expand All @@ -6,5 +8,5 @@ input:
expect:
stdout: |+
hello
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 127
4 changes: 3 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/basic/before_echo.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: An unknown command before echo on the next line does not stop execution.
input:
script: |+
Expand All @@ -6,5 +8,5 @@ input:
expect:
stdout: |+
hello
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 0
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: Multiple unknown commands on separate lines all execute and report errors.
input:
script: |+
Expand All @@ -6,5 +8,5 @@ input:
baz
expect:
stdout: ""
stderr_contains: ["foo", "bar", "baz", "command not found"]
stderr_contains: ["foo", "bar", "baz", "unknown command"]
exit_code: 127
6 changes: 4 additions & 2 deletions tests/scenarios/cmd/unknown_cmd/basic/multiword_name.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
description: An unknown command with a multi-word-like name (with hyphens) prints "command not found".
# skip: error message format differs from bash
skip_assert_against_bash: true
description: An unknown command with a multi-word-like name (with hyphens) prints "unknown command".
input:
script: |+
my-unknown-command
expect:
stdout: ""
stderr_contains: ["my-unknown-command", "command not found"]
stderr_contains: ["my-unknown-command", "unknown command"]
exit_code: 127
6 changes: 4 additions & 2 deletions tests/scenarios/cmd/unknown_cmd/basic/simple.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
description: A simple unknown command prints "command not found" to stderr.
# skip: error message format differs from bash
skip_assert_against_bash: true
description: A simple unknown command prints "unknown command" to stderr.
input:
script: |+
foo
expect:
stdout: ""
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 127
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: Unknown command error goes to stderr while stdout remains clean.
input:
script: |+
Expand All @@ -10,5 +12,5 @@ expect:
after
stderr_contains:
- "nonexistent_cmd"
- "command not found"
- "unknown command"
exit_code: 0
6 changes: 4 additions & 2 deletions tests/scenarios/cmd/unknown_cmd/basic/underscore_name.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
description: An unknown command with underscores in the name prints "command not found".
# skip: error message format differs from bash
skip_assert_against_bash: true
description: An unknown command with underscores in the name prints "unknown command".
input:
script: |+
my_unknown_command
expect:
stdout: ""
stderr_contains: ["my_unknown_command", "command not found"]
stderr_contains: ["my_unknown_command", "unknown command"]
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/bash.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
bash: command not found
rshell: bash: unknown command
Run 'help' to see available commands.
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/chmod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
chmod: command not found
rshell: chmod: unknown command
Run 'help' to see available commands.
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/cp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
cp: command not found
rshell: cp: unknown command
Run 'help' to see available commands.
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/curl.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
curl: command not found
rshell: curl: unknown command
Run 'help' to see available commands.
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/mv.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
mv: command not found
rshell: mv: unknown command
Run 'help' to see available commands.
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
python: command not found
rshell: python: unknown command
Run 'help' to see available commands.
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/rm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
rm: command not found
rshell: rm: unknown command
Run 'help' to see available commands.
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/sh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
sh: command not found
rshell: sh: unknown command
Run 'help' to see available commands.
exit_code: 127
3 changes: 2 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/common_progs/wget.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ input:
expect:
stdout: ""
stderr: |+
wget: command not found
rshell: wget: unknown command
Run 'help' to see available commands.
exit_code: 127
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: Unknown command in logical OR chain falls through to the next command.
input:
script: |+
Expand All @@ -9,5 +11,5 @@ expect:
0
stderr_contains:
- "nonexistent_cmd"
- "command not found"
- "unknown command"
exit_code: 0
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: The && operator skips the next command after an unknown command.
input:
script: |+
foo && echo should_not_run
expect:
stdout: ""
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 127
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: Mixed && and || operators work correctly with unknown commands.
input:
script: |+
foo && echo no || echo yes
expect:
stdout: |+
yes
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 0
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: An unknown command sets exit code 127.
input:
script: |+
nonexistent
expect:
stdout: ""
stderr_contains: ["nonexistent", "command not found"]
stderr_contains: ["nonexistent", "unknown command"]
exit_code: 127
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: The exit code of an unknown command is captured by $? when using semicolons.
input:
script: |+
nonexistent; echo $?
expect:
stdout: |+
127
stderr_contains: ["nonexistent", "command not found"]
stderr_contains: ["nonexistent", "unknown command"]
exit_code: 0
4 changes: 3 additions & 1 deletion tests/scenarios/cmd/unknown_cmd/exit_code/or_chain.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: A chain of unknown commands with || reaches the first valid command.
input:
script: |+
foo || bar || echo reached
expect:
stdout: |+
reached
stderr_contains: ["foo", "bar", "command not found"]
stderr_contains: ["foo", "bar", "unknown command"]
exit_code: 0
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: The || operator runs the next command after an unknown command.
input:
script: |+
foo || echo fallback
expect:
stdout: |+
fallback
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 0
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# skip: error message format differs from bash
skip_assert_against_bash: true
description: A semicolon separator allows execution to continue after an unknown command.
input:
script: |+
foo; echo after
expect:
stdout: |+
after
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 0
6 changes: 4 additions & 2 deletions tests/scenarios/cmd/unknown_cmd/with_args/args.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
description: An unknown command with arguments still prints "command not found" for the command name.
# skip: error message format differs from bash
skip_assert_against_bash: true
description: An unknown command with arguments still prints "unknown command" for the command name.
input:
script: |+
foo arg1 arg2 arg3
expect:
stdout: ""
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 127
6 changes: 4 additions & 2 deletions tests/scenarios/cmd/unknown_cmd/with_args/flags.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
description: An unknown command with flag-like arguments still prints "command not found".
# skip: error message format differs from bash
skip_assert_against_bash: true
description: An unknown command with flag-like arguments still prints "unknown command".
input:
script: |+
foo --verbose -n 5
expect:
stdout: ""
stderr_contains: ["foo", "command not found"]
stderr_contains: ["foo", "unknown command"]
exit_code: 127
Loading
Loading