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
20 changes: 15 additions & 5 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8570,11 +8570,21 @@ impl Interpreter {
let cmd = args.join(" ");
let script = match self.parse_embedded_script(&cmd).await {
Ok(script) => script,
Err(crate::error::Error::Parse { message, .. }) => {
return Ok(ExecResult::err(
format!("eval: parse error: {}", message),
1,
));
Err(crate::error::Error::Parse { message, line, .. }) => {
// Like bash: status 2, line counted from the eval's own line, then
// the offending source line. Redirects still apply (`2>&1`).
let at = self.current_line + line.max(1) - 1;
let src = cmd.lines().nth(line.max(1) - 1).unwrap_or("");
let message = if message.starts_with("syntax error") {
message
} else {
format!("syntax error: {message}")
};
let result = ExecResult::err(
format!("bash: eval: line {at}: {message}\nbash: eval: line {at}: `{src}'\n"),
2,
);
return self.apply_redirections(result, redirects).await;
}
Err(e) => return Err(e),
};
Expand Down
5 changes: 5 additions & 0 deletions crates/bashkit/src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ impl<'a> Lexer<'a> {
}
}

/// Move the line count forward by `by` (for input embedded at a later line).
pub(crate) fn shift_lines(&mut self, by: usize) {
self.position.line += by;
}

/// Get the current position in the input.
pub fn position(&self) -> Position {
self.position
Expand Down
8 changes: 8 additions & 0 deletions crates/bashkit/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3079,6 +3079,14 @@ impl<'a> Parser<'a> {
let remaining_depth = self.max_depth.saturating_sub(self.current_depth);
let mut parser = Box::new(Parser::with_limits(src, remaining_depth, self.fuel));
parser.execution_budget = self.execution_budget.clone();
// `$( )` spans count from the outer word's line, so `$LINENO` and
// error line numbers inside it match bash.
let shift = self.current_span.start.line.saturating_sub(1);
if shift > 0 {
parser.lexer.shift_lines(shift);
parser.current_span.start.line += shift;
parser.current_span.end.line += shift;
}
parser
}

Expand Down
36 changes: 36 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/eval-syntax-error.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
### eval_syntax_error_status
eval "if then" 2>/dev/null
echo "rc=$?"
eval 'echo (' 2>/dev/null
echo "rc=$?"
### expect
rc=2
rc=2
### end

### eval_syntax_error_redirected
x=$(eval "fi" 2>&1)
echo "rc=$? lines=$(printf '%s\n' "$x" | grep -c 'eval: line 1: syntax error')"
### expect
rc=2 lines=1
### end

### eval_syntax_error_line_and_newline
echo start
eval "fi" 2>&1 | grep -c '^bash: eval: line 2: syntax error.*[^ ]$'
echo done
### expect
start
1
done
### end

### lineno_inside_command_substitution
echo one >/dev/null
x=$(echo $LINENO)
y=$(
echo $LINENO)
echo "$x $y"
### expect
2 4
### end
1 change: 1 addition & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* **Feature**: awk rewritten to target gawk 5.2 (Debian's `awk`). New lexer, parser, async evaluator, ERE translation with POSIX leftmost-longest matching, gawk number formatting and `printf`, gawk's `rand()` sequence, `substr`/`getline`/`typeof` edge cases, `-f`/`-e`/`--` options and gawk's error messages. `system()`, `print | cmd` and `cmd | getline` now run through the sandbox shell (execution-plan driver). Debian-oracle score for awk went from 51% to 94% of 260 cases; the remaining gawk gap is unsorted `for (k in a)` order (L-AWK-001). All resource caps kept; new TM-DOS-128 for awk commands. See foundations/builtins "awk".
* **Fix**: array literals expand like command words. `arr=(*.txt x{1,2})` now runs brace and pathname expansion (honoring `nullglob`; quoted words stay literal), and `arr=([2]=x y)` sets indexed elements instead of storing `[2]=x`. Under `failglob` an unmatched pattern is stored as-is rather than aborting (WTF in code). `command -v a b ...` reports every name found and fails only if none was. `declare -a d=(...)` is unchanged here (declare/local work is in a separate branch).
* **Fix**: the `bashkit` CLI exits 2 (was 1) when a script or `-c` string has a syntax error before any runnable command, printing `bash: syntax error: ...` instead of the anyhow `Error: Failed to execute script` chain. Matches the partial-run path, which already exited 2. See foundations/parser "Partial execution before a syntax error".
* **Fix**: `eval` with a syntax error exits 2 like bash and reports `bash: eval: line N: syntax error: ...` plus the offending line, each newline-terminated, with the eval's own redirects applied (`$(eval bad 2>&1)` captures it). `$LINENO` inside `$( )` now counts from the outer line instead of restarting at 1. The parser's message text still differs from bash's `near unexpected token` wording.
* **Decision**: CPython's workload priority is agents running `python3` for scripted logic (processing a file, transforming data). Low-use stdlib modules are no longer shipped: `unittest`, `doctest`, profilers, `compileall`/`zipapp`, `dbm`/`shelve`, `plistlib`, `wave`, `netrc`, `cmd`, `tty`/`pty`, and `bz2`/`lzma`/`compression.zstd` (no C codec in the guest anyway). Stdlib zip 3.58 to 3.16 MB. L-CPY-009 updated. See [CPython WebAssembly Runtime](runtimes/cpython-wasm.md#workload-priority).
* **Feature**: `yes` and `seq` stream into pipelines. As a producer stage they write chunks into the pipe as they go (`Context::stdout_stream`), so `yes | head -1` and `seq 1000000 | head -1` stop after the first chunk with `PIPESTATUS` `141 0` instead of running to the output cap and printing a truncation notice. Step 2 of the roadmap (streaming builtin I/O), starting with the generators.
* **Feature**: `sudo` and `busybox`, the top missing commands in the gap replay (`sudo tee /etc/x <<EOF`, `busybox tar --help`). `sudo` skips its options and runs the command (leading `VAR=VALUE` through `env`, `-s`/`-i` through `bash -c`; `-v`/`-k` succeed, `-l` lists ALL); `busybox APPLET` runs the builtin of that name, `--list` lists them (L-SUDO-001). `nohup`/`nice`/`flock` no longer steal the inner command's `--help`.
Expand Down
Loading