diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 988172bd2..891dea7cc 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -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), }; diff --git a/crates/bashkit/src/parser/lexer.rs b/crates/bashkit/src/parser/lexer.rs index 4b5ec816c..c3e4a50b2 100644 --- a/crates/bashkit/src/parser/lexer.rs +++ b/crates/bashkit/src/parser/lexer.rs @@ -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 diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index 6e8c3ba26..768ed8107 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -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 } diff --git a/crates/bashkit/tests/spec_cases/bash/eval-syntax-error.test.sh b/crates/bashkit/tests/spec_cases/bash/eval-syntax-error.test.sh new file mode 100644 index 000000000..f547caeed --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/eval-syntax-error.test.sh @@ -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 diff --git a/knowledge/log.md b/knowledge/log.md index bb8909b5e..2a0aa2eaa 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -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 <