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
29 changes: 21 additions & 8 deletions crates/bashkit/src/builtins/jq/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,10 @@ async fn run_jq(ctx: Context<'_>, parsed: JqArgs<'_>) -> Result<ExecResult> {
// THREAT[TM-DOS-110]: the meter aborts a run by unwinding where jaq
// gives it no error channel (see jaq_json::meter::Abort).
let input_error = std::cell::RefCell::new(None);
// Set once the filter pulls past the last parsed value. With `-n`, jq
// only parses stdin as `input`/`inputs` read it, so a parse error past
// the values the filter actually read is never reported.
let stream_exhausted = std::cell::Cell::new(false);
let run_filter = std::panic::AssertUnwindSafe(|| -> Result<Option<ExecResult>> {
let env_val = jq_to_val(&env_obj);
let args_val = jq_to_val(&args_obj);
Expand All @@ -556,13 +560,20 @@ async fn run_jq(ctx: Context<'_>, parsed: JqArgs<'_>) -> Result<ExecResult> {
})
});
let mut value_iter: Box<dyn Iterator<Item = std::result::Result<Val, String>> + '_> =
Box::new(values.map(|value| {
value.map_err(|error| {
let message = error.to_string();
*input_error.borrow_mut() = Some(error);
message
})
}));
Box::new(
values
.map(|value| {
value.map_err(|error| {
let message = error.to_string();
*input_error.borrow_mut() = Some(error);
message
})
})
.chain(std::iter::from_fn(|| {
stream_exhausted.set(true);
None
})),
);
if parsed.slurp && !parsed.raw_input && parse_error.is_none() {
let mut values = jaq_json::meter::Metered::<Vec<Val>>::default();
for value in value_iter {
Expand Down Expand Up @@ -752,7 +763,9 @@ async fn run_jq(ctx: Context<'_>, parsed: JqArgs<'_>) -> Result<ExecResult> {

stderr_out.push_str(&messages::take());
let mut code = status.unwrap_or(if parsed.exit_status { 4 } else { 0 });
if let Some(e) = parse_error.filter(|_| halted.is_none()) {
if let Some(e) =
parse_error.filter(|_| halted.is_none() && (!null_input || stream_exhausted.get()))
{
stderr_out.push_str(&e);
stderr_out.push('\n');
code = 5;
Expand Down
29 changes: 29 additions & 0 deletions crates/bashkit/src/builtins/jq/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1832,6 +1832,35 @@ async fn null_input_leaves_stream_to_input_and_inputs() {
);
}

/// jq 1.7: with `-n`, stdin is only parsed when `input`/`inputs` reach it,
/// so invalid JSON on stdin is ignored by a filter that never reads it.
#[tokio::test]
async fn null_input_ignores_unread_invalid_stdin() {
let result = run_jq_result_with_args(&["-n", "1"], "not json")
.await
.unwrap();
assert_eq!(result.stdout, "1\n");
assert_eq!(result.stderr, "");
assert_eq!(result.exit_code, 0);

// Values before the bad text are still readable.
let result = run_jq_result_with_args(&["-n", "input"], "1 x")
.await
.unwrap();
assert_eq!(result.stdout, "1\n");
assert_eq!(result.stderr, "");
assert_eq!(result.exit_code, 0);
}

#[tokio::test]
async fn null_input_reports_invalid_stdin_once_reached() {
let result = run_jq_result_with_args(&["-nc", "[inputs]"], "1 x")
.await
.unwrap();
assert!(result.stderr.starts_with("jq: "), "{}", result.stderr);
assert_eq!(result.exit_code, 5);
}

#[tokio::test]
async fn slurp_of_empty_input_is_empty_array() {
assert_eq!(run_jq_with_args(&["-s", "."], "").await.unwrap(), "[]\n");
Expand Down
1 change: 1 addition & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2026-10-09

* **Fix**: `jq -n` reported invalid JSON on stdin even when the filter never read it (`printf x | jq -n 1` printed `1` plus an `invalid JSON` error and exited 5). With `-n`, a stdin parse error is now reported only once `input`/`inputs` read past the last valid value, like jq 1.7 (`printf '1 x' | jq -n input` prints `1`, status 0).
* **Test**: `bashkit_hard` eval (`just eval-hard`): 10 tasks written after `bashkit_bash` saturated (55/58 for top models), 25-turn budget, 9 categories: interacting bugs revealed one test at a time, quoting/glob/leading-dash file names, masked `set -euo pipefail` failures, trap/exit-code contracts, Makefile prerequisites checked with `make -q`, a rename that must spare lookalikes and frozen messages, and golden reports (CSV/TSV reconciliation, nearest-rank p95 over a generated log, jq rollups, deterministic topological order). New check `file_equals` (whole-file golden output). Reference solutions replay in `cargo test -p bashkit-eval` alongside the repo set (`reference.rs` now iterates both datasets) and were also checked under real bash. See operations/eval "Hard Eval".
* **Fix**: gaps found by the `bashkit_hard` eval references. A child `bash script` / `bash -c` started in an `if`/`||`/`&&`/`!` context ran with `set -e` disabled (the caller's condition depth leaked into the child), so `if bash test.sh` never saw `test.sh` fail; the child now starts outside any condition, like a new process. `dirname -- PATH` treats `--` as end of options (it printed `.` for it); `ls -A` is accepted (same listing as `-a`, which never shows `.`/`..`); `stat -c %Y` prints the mtime in epoch seconds (it printed `%Y`).
* **Fix**: last BashBox corpus gaps. `x<(true)`, `<(true)x`, `2<(true)` and `x=<(true)` join the substitution to the word it touches (it was a separate word, and `x=<(true)` ran `/dev/fd/63`). `$'\c?'` is DEL (0x7f). A simple command's `3<file` no longer feeds its stdin (`cat 3<f` read `f`); only `read -u 3` takes it, compound bodies keep the old feed (limitations "Named fds"). `xargs -t`/`--verbose` runs the command again (its output was dropped once the trace line streamed; a plan driver's `Run` output now streams after its `Emit`s) and quotes the trace like GNU (`'it'\''s'$'\t''a b'`). `printf %q` escapes `#` only first and `~` only first or after `=`/`:`, and escapes `,` and `^` (bash's rules, checked per character against bash 5.2). `cat -E` shows a CR before a newline as `^M$`; `tr` expands `[:space:]`, `[:blank:]`, `[:alpha:]`, `[:alnum:]` in byte order; `rev` keeps a missing final newline missing. The lexer counted the re-read rest of a here-document's command line twice, so every span after a here-doc was off (`$LINENO` one line late, and the per-line alias re-parse restarted mid-word: `alias c=cat; c <<EOF` made the next line `cho ...`); that rest is now re-read at its own source position (`cat <<EOF; echo $LINENO` is the `cat` line, as bash). BashBox bash-verified failures 15 to 4 (`declare -p` of `$'\xff'` under L-STREAM-001, `du -bh` behind the printf width cap, two newline-in-name cases under L-FS-004).
Expand Down
Loading