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
31 changes: 31 additions & 0 deletions crates/bashkit/src/builtins/jq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@ use crate::interpreter::ExecResult;
/// produce deeply nested parse trees in jaq.
const MAX_JQ_JSON_DEPTH: usize = 100;

/// Custom jq definitions prepended to every filter to patch jaq limitations:
/// - `setpath(p; v)`: recursive path-setting (not in jaq stdlib)
/// - `leaf_paths`: paths to scalar leaves (not in jaq stdlib)
/// - `match` override: adds `"name":null` to unnamed captures
/// - `scan` override: uses "g" flag for global matching (jq default)
const JQ_COMPAT_DEFS: &str = r#"
def setpath(p; v):
if (p | length) == 0 then v
else p[0] as $k |
(if . == null then
if ($k | type) == "number" then [] else {} end
else . end) |
.[$k] |= setpath(p[1:]; v)
end;
def leaf_paths: paths(scalars);
def match(re; flags):
matches(re; flags)[] |
.[0] as $m |
{ offset: $m.offset, length: $m.length, string: $m.string,
captures: [.[1:][] | { offset: .offset, length: .length, string: .string,
name: (if has("name") then .name else null end) }] };
def match(re): match(re; "");
def scan(re; flags): matches(re; "g" + flags)[] | .[0].string;
def scan(re): scan(re; "");
"#;

/// RAII guard that restores process env vars when dropped.
/// Ensures cleanup even on early-return error paths.
struct EnvRestoreGuard(Vec<(String, Option<String>)>);
Expand Down Expand Up @@ -297,6 +323,11 @@ impl Builtin for Jq {
let loader = load::Loader::new(jaq_std::defs().chain(jaq_json::defs()));
let arena = load::Arena::default();

// Prepend compatibility definitions (setpath, leaf_paths, match, scan)
// to override jaq's defaults with jq-compatible behavior.
let compat_filter = format!("{}{}", JQ_COMPAT_DEFS, filter);
let filter = compat_filter.as_str();

// Parse the filter
let program = load::File {
code: filter,
Expand Down
26 changes: 17 additions & 9 deletions crates/bashkit/tests/spec_cases/jq/jq.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -512,10 +512,13 @@ echo '{"a":{"b":1}}' | jq 'getpath(["a","b"])'
### end

### jq_setpath
### skip: setpath not available in jaq standard library
# Set value at path
echo '{"a":1}' | jq 'setpath(["b"];2)'
### expect
{"a":1,"b":2}
{
"a": 1,
"b": 2
}
### end

### jq_del
Expand Down Expand Up @@ -577,10 +580,15 @@ echo '{"a":{"b":1}}' | jq '[paths]'
### end

### jq_leaf_paths
### skip: leaf_paths not available in jaq standard library
# Get paths to leaf (scalar) values
echo '{"a":{"b":1}}' | jq '[leaf_paths]'
### expect
[["a","b"]]
[
[
"a",
"b"
]
]
### end

### jq_any
Expand Down Expand Up @@ -758,15 +766,15 @@ true
### end

### jq_match
### skip: jaq omits capture name field (real jq includes "name":null)
echo '"hello"' | jq 'match("e(ll)o")'
### bash_diff: jaq/serde_json sorts object keys alphabetically vs jq insertion order
echo '"hello"' | jq -c 'match("e(ll)o")'
### expect
{"offset":1,"length":4,"string":"ello","captures":[{"offset":2,"length":2,"string":"ll","name":null}]}
{"captures":[{"length":2,"name":null,"offset":2,"string":"ll"}],"length":4,"offset":1,"string":"ello"}
### end

### jq_scan
### skip: jaq scan requires explicit "g" flag for global match
echo '"hello hello"' | jq '[scan("hel")]'
# Scan for all regex matches
echo '"hello hello"' | jq -c '[scan("hel")]'
### expect
["hel","hel"]
### end
Expand Down
8 changes: 2 additions & 6 deletions crates/bashkit/tests/spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! - `### skip: reason` - Skip test entirely (not run in any test)
//! - `### bash_diff: reason` - Known difference from real bash (runs in spec tests, excluded from comparison)
//!
//! ## Skipped Tests (15 total)
//! ## Skipped Tests (14 total)
//!
//! Actual `### skip:` markers across spec test files:
//!
Expand All @@ -21,12 +21,8 @@
//! - [ ] od output format varies
//! - [ ] hexdump -C output format varies
//!
//! ### jq.test.sh (5 skipped)
//! ### jq.test.sh (1 skipped)
//! - [ ] jaq errors on .foo applied to null instead of returning null for //
//! - [ ] setpath not available in jaq standard library
//! - [ ] leaf_paths not available in jaq standard library
//! - [ ] jaq omits capture name field (real jq includes "name":null)
//! - [ ] jaq scan requires explicit "g" flag for global match
//!
//! ### python.test.sh (8 skipped)
//! - [ ] Monty does not support set & and | operators yet
Expand Down
Loading