diff --git a/crates/bashkit/src/builtins/vars.rs b/crates/bashkit/src/builtins/vars.rs index aa146e583..f3ca41bbf 100644 --- a/crates/bashkit/src/builtins/vars.rs +++ b/crates/bashkit/src/builtins/vars.rs @@ -553,7 +553,7 @@ impl Builtin for Shopt { for opt in SHOPT_OPTIONS { let key = format!("SHOPT_{}", opt); let on = ctx.variables.get(&key).map(|v| v == "1").unwrap_or(false); - output.push_str(&format!("{:<32}{}\n", opt, if on { "on" } else { "off" })); + output.push_str(&format!("{:<15}\t{}\n", opt, if on { "on" } else { "off" })); } return Ok(ExecResult::ok(output)); } @@ -655,7 +655,11 @@ impl Builtin for Shopt { for opt in SHOPT_OPTIONS { let key = format!("SHOPT_{}", opt); let on = ctx.variables.get(&key).map(|v| v == "1").unwrap_or(false); - output.push_str(&format!("{:<32}{}\n", opt, if on { "on" } else { "off" })); + output.push_str(&format!( + "{:<15}\t{}\n", + opt, + if on { "on" } else { "off" } + )); } return Ok(ExecResult::ok(output)); } @@ -672,7 +676,7 @@ impl Builtin for Shopt { } let key = format!("SHOPT_{}", opt); let on = ctx.variables.get(&key).map(|v| v == "1").unwrap_or(false); - output.push_str(&format!("{:<32}{}\n", opt, if on { "on" } else { "off" })); + output.push_str(&format!("{:<15}\t{}\n", opt, if on { "on" } else { "off" })); } if any_invalid { Ok(ExecResult { diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index 28b025df0..e16ea662a 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -1720,6 +1720,16 @@ impl<'a> Parser<'a> { words.push(Word::literal(")")); self.advance(); } + // Inside `[[ ]]` these are string comparison operators, not + // redirections (bash's conditional grammar has no redirects). + Some(tokens::Token::RedirectIn) => { + words.push(Word::literal("<")); + self.advance(); + } + Some(tokens::Token::RedirectOut) => { + words.push(Word::literal(">")); + self.advance(); + } None => { return Err(crate::error::Error::parse( "unexpected end of input in [[ ]]".to_string(), diff --git a/crates/bashkit/tests/spec_cases/bash/conditional-string-order.test.sh b/crates/bashkit/tests/spec_cases/bash/conditional-string-order.test.sh new file mode 100644 index 000000000..1ffaf410b --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/conditional-string-order.test.sh @@ -0,0 +1,35 @@ +### dbracket_string_order +[[ a < B ]]; echo "a B ]]; echo "a>B:$?" +[[ 9 < 10 ]]; echo "9<10:$?" +[[ "" < B ]]; echo "empty:$?" +### expect +aB:0 +9<10:1 +empty:0 +### end + +### dbracket_string_order_with_expansions +x=apple; y=Banana +[[ $x < $y ]]; echo "$?" +[[ "$x" > "$y" ]] && echo gt +[[ $x < $y || $x > $y ]] && echo ordered +### expect +1 +gt +ordered +### end + +### shopt_listing_width +shopt extglob nullglob | cat -A +shopt -s extglob +shopt extglob +shopt -u extglob +### expect +extglob ^Ioff$ +nullglob ^Ioff$ +extglob on +### end diff --git a/crates/bashkit/tests/spec_cases/bash/variables.test.sh b/crates/bashkit/tests/spec_cases/bash/variables.test.sh index 6ab1e24c5..e3397b54f 100644 --- a/crates/bashkit/tests/spec_cases/bash/variables.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/variables.test.sh @@ -629,13 +629,12 @@ exit:1 ### shopt_show_specific # shopt shows status of named option -### bash_diff shopt nullglob shopt -s nullglob shopt nullglob ### expect -nullglob off -nullglob on +nullglob off +nullglob on ### end ### shopt_nullglob_no_matches diff --git a/knowledge/log.md b/knowledge/log.md index 97abb903c..4ed193b93 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -13,6 +13,7 @@ * **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. * **Fix**: parameter expansion and `mapfile` gaps from the Debian oracle. `${v~}`/`${v~~}` toggle case (also per array element). `\/` in a `${v/pat/rep}` replacement is a plain `/`. Unquoted `${!prefix@}`, `${!ref}`, `${v:off:len}`, `${a[@]:o:n}` and `${v@op}` now undergo field splitting like other expansions, so `for v in ${!cfg_@}` loops per name. `mapfile` gained `-d DELIM` (`''` is NUL), `-n`, `-s`, `-O` (keeps other elements) and grouped options; `-u` other than 0 and `-C`/`-c` exit 2 (L-MAPFILE-001). +* **Fix**: `[[ a < b ]]` and `[[ a > b ]]` compare strings instead of being swallowed as redirections (the conditional grammar has no redirects), so `[[ a < B ]]` is false in byte order like bash. `shopt` listings pad the name to 15 columns and separate the state with a tab, matching bash. * **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 <