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
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
RG_VERSION: 15.1.0
JQ_VERSION: 1.8.1

jobs:
lint:
Expand Down Expand Up @@ -257,7 +258,14 @@ jobs:

test:
name: Test
runs-on: ubuntu-latest
# Pinned, not `ubuntu-latest`: this job's `*_match_gnu` differential tests
# need real GNU coreutils as the oracle, and 24.04 is the last image that
# ships it as the default. Ubuntu 26.04 installs uutils as `coreutils`
# instead, so `dd`/`join`/`od`/`pr` stopped reporting "GNU coreutils" and
# those tests fail closed by design rather than skipping. Unpin once the
# GNU oracle is installed explicitly, the way `rg` and `jq` already are;
# see "Pinned External Oracles" in knowledge/operations/testing.md.
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -302,6 +310,12 @@ jobs:
- name: Install pinned ripgrep for differential tests
run: scripts/install-ripgrep-ci.sh "$RG_VERSION"

# Same reason for `jq`, and the runner image already proved it: an
# unpinned oracle made the jq parity suite a function of which
# `ubuntu-latest` image the job landed on.
- name: Install pinned jq for differential tests
run: scripts/install-jq-ci.sh "$JQ_VERSION"

# Examples have a dedicated job below; skipping them here avoids
# duplicated example links that can exhaust runner disk on main.
- name: Run tests
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ env:
CARGO_TERM_COLOR: always
LLVM_COV_IGNORE_FILENAME_REGEX: "(tests/|test_cases/|/tests\\.rs$)"
RG_VERSION: 15.1.0
JQ_VERSION: 1.8.1

jobs:
coverage:
name: Generate Rust Coverage
runs-on: ubuntu-latest
# Pinned for the GNU coreutils oracle, like ci.yml's `test` job; see the
# note there.
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -48,6 +51,9 @@ jobs:
- name: Install pinned ripgrep for differential tests
run: scripts/install-ripgrep-ci.sh "$RG_VERSION"

- name: Install pinned jq for differential tests
run: scripts/install-jq-ci.sh "$JQ_VERSION"

# bashkit-eval is excluded: it enables bashkit's cpython and sqlite
# features, and under --workspace feature unification that would pull
# the slow CPython/sqlite tests into the instrumented bashkit binaries
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
RG_VERSION: 15.1.0
JQ_VERSION: 1.8.1

jobs:
miri:
Expand Down Expand Up @@ -124,7 +125,9 @@ jobs:

asan:
name: AddressSanitizer
runs-on: ubuntu-latest
# Pinned for the GNU coreutils oracle, like ci.yml's `test` job; see the
# note there.
runs-on: ubuntu-24.04
# ASAN with --test-threads=1 plus the expanded rg/coreutils
# differential corpora needs well over the previous 30-minute cap;
# the last two nightly runs cancelled mid-suite at exactly 30 min.
Expand All @@ -149,10 +152,14 @@ jobs:
with:
cache-on-failure: true

# ASAN runs the Rust test suite, including `rg` differential tests.
# ASAN runs the Rust test suite, including the `rg` and `jq`
# differential tests.
- name: Install pinned ripgrep for differential tests
run: scripts/install-ripgrep-ci.sh "$RG_VERSION"

- name: Install pinned jq for differential tests
run: scripts/install-jq-ci.sh "$JQ_VERSION"

- name: Run tests with AddressSanitizer
env:
RUSTFLAGS: "-Z sanitizer=address"
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/publish-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,11 @@ jobs:
os: [linux, macos, windows]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
include:
# linux is pinned rather than `ubuntu-latest` because Ubuntu 26.04
# ships no hosted 3.9 build; see the note on `python.yml`'s `test`
# job, which is pinned for the same reason.
- os: linux
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
- os: macos
runs-on: macos-latest
- os: windows
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,16 @@ jobs:
test:
name: Test (Python ${{ matrix.python-version }})
needs: [build-wheel]
runs-on: ubuntu-latest
# Pinned, not `ubuntu-latest`: Ubuntu 26.04 ships no hosted 3.9 build, and
# `setup-python` has no fallback, so the 3.9 leg died the moment
# `ubuntu-latest` began rolling onto it ("The version '3.9' with
# architecture 'x64' was not found for Ubuntu 26.04"). The wheel is
# cp39-abi3 and the package claims >=3.9, so the fix is an image that can
# host the whole declared range, not a smaller matrix. Unpin when 3.9
# support is dropped; `publish-python.yml`'s `test-builds` is pinned for
# the same reason and the two matrices are kept in step by
# `just check-workflow-parity`.
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
Expand Down
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/bashkit/docs/jq.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Implemented:
| `-e`, `--exit-status` | Set exit code based on the output |
| `-j` | Like `-r` but suppresses trailing newlines |
| `--tab` | Use tabs for indentation |
| `--indent N` | Indent with N spaces, 0..7 (default 2). `0` still pretty-prints — newlines, no leading spaces — so `-c` is the only way to get compact output |
| `-a`, `--ascii-output` | Escape non-ASCII characters as `\uXXXX` (strings stay quoted even with `-r`) |
| `--raw-output0` | Like `-r` with a NUL after each output; a string containing NUL is an error |
| `--seq` | Write RS (0x1e) before each output; RS separates input values |
Expand All @@ -38,6 +39,10 @@ Implemented:
| `-h`, `--help` | Print help |
| Combined flags like `-snr` | Treated as the union of the individual flags |

`-c`, `--tab` and `--indent N` all choose the same thing — the output shape —
so the last one on the command line wins. `jq --indent 4 -c` is compact and
`jq -c --indent 4` is indented four spaces.

## Variables

| Variable | Behaviour |
Expand Down
10 changes: 5 additions & 5 deletions crates/bashkit/fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions crates/bashkit/src/builtins/jq/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ pub(super) struct JqArgs<'a> {
pub raw_input: bool,
pub raw_output: bool,
pub join_output: bool,
pub compact_output: bool,
pub null_input: bool,
pub slurp: bool,
pub sort_keys: bool,
Expand All @@ -55,6 +54,10 @@ pub(super) struct JqArgs<'a> {
pub stream: bool,
/// `-e` flag — set exit status from output.
pub exit_status: bool,
/// Output shape, set by `-c`/`--compact-output`, `--tab` and
/// `--indent N`. One field rather than a compact flag beside an indent
/// width, because real jq lets the last of those three on the command
/// line win: `jq --indent 4 -c` is compact and `jq -c --indent 4` is not.
pub indent: Indent,
pub file_args: Vec<&'a str>,
/// Named string/JSON variables: `(name_with_$, value)`.
Expand Down Expand Up @@ -147,7 +150,6 @@ pub(super) fn parse<'a>(args: &'a [String]) -> ParseOutcome<'a> {
raw_input: false,
raw_output: false,
join_output: false,
compact_output: false,
null_input: false,
slurp: false,
ascii_output: false,
Expand Down Expand Up @@ -212,7 +214,7 @@ pub(super) fn parse<'a>(args: &'a [String]) -> ParseOutcome<'a> {
match arg.as_str() {
"--raw-output" => out.raw_output = true,
"--raw-input" => out.raw_input = true,
"--compact-output" => out.compact_output = true,
"--compact-output" => out.indent = Indent::Compact,
"--null-input" => out.null_input = true,
"--sort-keys" => out.sort_keys = true,
"--slurp" => out.slurp = true,
Expand All @@ -230,9 +232,7 @@ pub(super) fn parse<'a>(args: &'a [String]) -> ParseOutcome<'a> {
"--indent" => match args.get(i + 1) {
Some(n) => match n.parse::<u8>() {
Ok(n) if n <= MAX_JQ_INDENT => {
if !matches!(out.indent, Indent::Tab) {
out.indent = Indent::Spaces(n);
}
out.indent = Indent::Spaces(n);
i += 2;
continue;
}
Expand Down Expand Up @@ -338,7 +338,7 @@ pub(super) fn parse<'a>(args: &'a [String]) -> ParseOutcome<'a> {
'f' => takes_file = true,
'r' => out.raw_output = true,
'R' => out.raw_input = true,
'c' => out.compact_output = true,
'c' => out.indent = Indent::Compact,
'n' => out.null_input = true,
'S' => out.sort_keys = true,
's' => out.slurp = true,
Expand Down
31 changes: 23 additions & 8 deletions crates/bashkit/src/builtins/jq/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

use super::convert::JqJson;

/// Output indentation choice. `Spaces(0)` matches `-c` semantics in tools
/// that bypass the compact path.
/// Output indentation choice. `-c` is the only way to get compact output;
/// `Spaces(0)` is still pretty-printed, just with zero-width indentation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Indent {
/// Compact output, no whitespace between tokens (matches `-c`).
Expand Down Expand Up @@ -36,13 +36,14 @@ pub(super) fn sort_keys(v: JqJson) -> JqJson {

/// Render a `JqJson` value using the chosen indent style.
///
/// Note: real jq treats `--indent 0` as equivalent to `-c` (compact, no
/// newlines), not "newlines without indentation". We match that.
/// Note: `--indent 0` is pretty output with zero-width indentation — newlines
/// between tokens, no leading spaces — not compact output. jq 1.7 and earlier
/// let `--indent 0` imply `-c`; jq 1.8 removed that implication
/// (jqlang/jq#3232) and we match current jq. `-c` is the only compact switch.
pub(super) fn render(v: &JqJson, indent: Indent) -> String {
let mut out = String::new();
match indent {
Indent::Compact => render_compact(v, &mut out),
Indent::Spaces(0) => render_compact(v, &mut out),
Indent::Spaces(n) => render_pretty(v, &mut out, &" ".repeat(n as usize), 0),
Indent::Tab => render_pretty(v, &mut out, "\t", 0),
}
Expand Down Expand Up @@ -194,11 +195,25 @@ mod tests {
}

#[test]
fn indent_zero_renders_compact() {
// Real jq: --indent 0 is equivalent to -c (no newlines at all).
fn indent_zero_renders_newlines_without_indent() {
// jq >= 1.8: --indent 0 keeps the newlines and drops the spaces.
let v = JqJson::Object(vec![("a".into(), n("1"))]);
let out = render(&v, Indent::Spaces(0));
assert_eq!(out, "{\"a\":1}");
assert_eq!(out, "{\n\"a\": 1\n}");
}

#[test]
fn indent_zero_nests_without_indent() {
let v = JqJson::Object(vec![("a".into(), JqJson::Array(vec![n("1"), n("2")]))]);
let out = render(&v, Indent::Spaces(0));
assert_eq!(out, "{\n\"a\": [\n1,\n2\n]\n}");
}

#[test]
fn compact_is_the_only_compact_switch() {
// -c stays byte-for-byte compact even though --indent 0 no longer is.
let v = JqJson::Object(vec![("a".into(), JqJson::Array(vec![n("1"), n("2")]))]);
assert_eq!(render(&v, Indent::Compact), "{\"a\":[1,2]}");
}

#[test]
Expand Down
8 changes: 2 additions & 6 deletions crates/bashkit/src/builtins/jq/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ use compat::{
};
use convert::{JqJson, jq_to_val, parse_json_stream, val_to_jq_capped};
use errors::{format_compile_errors, format_load_errors, format_runtime_error_at};
use format::{Indent, render, sort_keys as sort_jq_keys};
use format::{render, sort_keys as sort_jq_keys};

/// Custom DataT that holds both the LUT and a shared input iterator.
/// Required by jaq 3.0 for `input`/`inputs` filter support.
Expand Down Expand Up @@ -455,11 +455,7 @@ async fn run_jq(ctx: Context<'_>, parsed: JqArgs<'_>) -> Result<ExecResult> {
}
}

let indent = if parsed.compact_output {
Indent::Compact
} else {
parsed.indent
};
let indent = parsed.indent;

let mut output = String::new();

Expand Down
Loading
Loading