Skip to content

Commit 273bd16

Browse files
feat(jq): Add dedicated tokenizer, parser, and evaluator (#26)
* fix: add tokenizer and parser * fix: split tests * fix: add maxJqIterations limits
1 parent 182198b commit 273bd16

File tree

14 files changed

+4642
-695
lines changed

14 files changed

+4642
-695
lines changed

src/commands/jq/evaluator.ts

Lines changed: 1826 additions & 0 deletions
Large diffs are not rendered by default.

src/commands/jq/jq.basic.test.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Bash } from "../../Bash.js";
3+
4+
describe("jq basic", () => {
5+
describe("identity filter", () => {
6+
it("should pass through JSON with .", async () => {
7+
const env = new Bash();
8+
const result = await env.exec("echo '{\"a\":1}' | jq '.'");
9+
expect(result.stdout).toBe('{\n "a": 1\n}\n');
10+
expect(result.stderr).toBe("");
11+
expect(result.exitCode).toBe(0);
12+
});
13+
14+
it("should pretty print arrays", async () => {
15+
const env = new Bash();
16+
const result = await env.exec("echo '[1,2,3]' | jq '.'");
17+
expect(result.stdout).toBe("[\n 1,\n 2,\n 3\n]\n");
18+
expect(result.exitCode).toBe(0);
19+
});
20+
});
21+
22+
describe("object access", () => {
23+
it("should access object key with .key", async () => {
24+
const env = new Bash();
25+
const result = await env.exec("echo '{\"name\":\"test\"}' | jq '.name'");
26+
expect(result.stdout).toBe('"test"\n');
27+
expect(result.exitCode).toBe(0);
28+
});
29+
30+
it("should access nested key with .a.b", async () => {
31+
const env = new Bash();
32+
const result = await env.exec(
33+
'echo \'{"a":{"b":"nested"}}\' | jq \'.a.b\'',
34+
);
35+
expect(result.stdout).toBe('"nested"\n');
36+
expect(result.exitCode).toBe(0);
37+
});
38+
39+
it("should return null for missing key", async () => {
40+
const env = new Bash();
41+
const result = await env.exec("echo '{\"a\":1}' | jq '.missing'");
42+
expect(result.stdout).toBe("null\n");
43+
expect(result.exitCode).toBe(0);
44+
});
45+
46+
it("should access numeric values", async () => {
47+
const env = new Bash();
48+
const result = await env.exec("echo '{\"count\":42}' | jq '.count'");
49+
expect(result.stdout).toBe("42\n");
50+
expect(result.exitCode).toBe(0);
51+
});
52+
53+
it("should access boolean values", async () => {
54+
const env = new Bash();
55+
const result = await env.exec("echo '{\"active\":true}' | jq '.active'");
56+
expect(result.stdout).toBe("true\n");
57+
expect(result.exitCode).toBe(0);
58+
});
59+
});
60+
61+
describe("array access", () => {
62+
it("should access array element with .[0]", async () => {
63+
const env = new Bash();
64+
const result = await env.exec('echo \'["a","b","c"]\' | jq \'.[0]\'');
65+
expect(result.stdout).toBe('"a"\n');
66+
expect(result.exitCode).toBe(0);
67+
});
68+
69+
it("should access last element with .[-1]", async () => {
70+
const env = new Bash();
71+
const result = await env.exec('echo \'["a","b","c"]\' | jq \'.[-1]\'');
72+
expect(result.stdout).toBe('"c"\n');
73+
expect(result.exitCode).toBe(0);
74+
});
75+
76+
it("should return null for out of bounds index", async () => {
77+
const env = new Bash();
78+
const result = await env.exec("echo '[1,2]' | jq '.[99]'");
79+
expect(result.stdout).toBe("null\n");
80+
expect(result.exitCode).toBe(0);
81+
});
82+
});
83+
84+
describe("array iteration", () => {
85+
it("should iterate array with .[]", async () => {
86+
const env = new Bash();
87+
const result = await env.exec("echo '[1,2,3]' | jq '.[]'");
88+
expect(result.stdout).toBe("1\n2\n3\n");
89+
expect(result.exitCode).toBe(0);
90+
});
91+
92+
it("should iterate object values with .[]", async () => {
93+
const env = new Bash();
94+
const result = await env.exec("echo '{\"a\":1,\"b\":2}' | jq '.[]'");
95+
expect(result.stdout).toBe("1\n2\n");
96+
expect(result.exitCode).toBe(0);
97+
});
98+
99+
it("should iterate nested array with .items[]", async () => {
100+
const env = new Bash();
101+
const result = await env.exec(
102+
"echo '{\"items\":[1,2,3]}' | jq '.items[]'",
103+
);
104+
expect(result.stdout).toBe("1\n2\n3\n");
105+
expect(result.exitCode).toBe(0);
106+
});
107+
});
108+
109+
describe("pipes", () => {
110+
it("should pipe filters with |", async () => {
111+
const env = new Bash();
112+
const result = await env.exec(
113+
"echo '{\"data\":{\"value\":42}}' | jq '.data | .value'",
114+
);
115+
expect(result.stdout).toBe("42\n");
116+
expect(result.exitCode).toBe(0);
117+
});
118+
119+
it("should chain multiple pipes", async () => {
120+
const env = new Bash();
121+
const result = await env.exec(
122+
'echo \'{"a":{"b":{"c":"deep"}}}\' | jq \'.a | .b | .c\'',
123+
);
124+
expect(result.stdout).toBe('"deep"\n');
125+
expect(result.exitCode).toBe(0);
126+
});
127+
});
128+
129+
describe("array slicing", () => {
130+
it("should slice with start and end", async () => {
131+
const env = new Bash();
132+
const result = await env.exec("echo '[0,1,2,3,4,5]' | jq '.[2:4]'");
133+
expect(result.stdout).toBe("[\n 2,\n 3\n]\n");
134+
});
135+
136+
it("should slice from start", async () => {
137+
const env = new Bash();
138+
const result = await env.exec("echo '[0,1,2,3,4]' | jq '.[:3]'");
139+
expect(result.stdout).toBe("[\n 0,\n 1,\n 2\n]\n");
140+
});
141+
142+
it("should slice to end", async () => {
143+
const env = new Bash();
144+
const result = await env.exec("echo '[0,1,2,3,4]' | jq '.[3:]'");
145+
expect(result.stdout).toBe("[\n 3,\n 4\n]\n");
146+
});
147+
148+
it("should slice strings", async () => {
149+
const env = new Bash();
150+
const result = await env.exec("echo '\"hello\"' | jq '.[1:4]'");
151+
expect(result.stdout).toBe('"ell"\n');
152+
});
153+
154+
it("should access with negative index in slice", async () => {
155+
const env = new Bash();
156+
const result = await env.exec("echo '[0,1,2,3,4]' | jq '.[-2:]'");
157+
expect(result.stdout).toBe("[\n 3,\n 4\n]\n");
158+
});
159+
});
160+
161+
describe("comma operator", () => {
162+
it("should output multiple values", async () => {
163+
const env = new Bash();
164+
const result = await env.exec("echo '{\"a\":1,\"b\":2}' | jq '.a, .b'");
165+
expect(result.stdout).toBe("1\n2\n");
166+
});
167+
168+
it("should work with three values", async () => {
169+
const env = new Bash();
170+
const result = await env.exec(
171+
'echo \'{"x":1,"y":2,"z":3}\' | jq \'.x, .y, .z\'',
172+
);
173+
expect(result.stdout).toBe("1\n2\n3\n");
174+
});
175+
});
176+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Bash } from "../../Bash.js";
3+
4+
describe("jq construction", () => {
5+
describe("object construction", () => {
6+
it("should construct object with static keys", async () => {
7+
const env = new Bash();
8+
const result = await env.exec(
9+
'echo \'{"name":"test","value":42}\' | jq -c \'{n: .name, v: .value}\'',
10+
);
11+
expect(result.stdout).toBe('{"n":"test","v":42}\n');
12+
});
13+
14+
it("should construct object with shorthand", async () => {
15+
const env = new Bash();
16+
const result = await env.exec(
17+
'echo \'{"name":"test","value":42}\' | jq -c \'{name, value}\'',
18+
);
19+
expect(result.stdout).toBe('{"name":"test","value":42}\n');
20+
});
21+
22+
it("should construct object with dynamic keys", async () => {
23+
const env = new Bash();
24+
const result = await env.exec(
25+
'echo \'{"key":"foo","val":42}\' | jq -c \'{(.key): .val}\'',
26+
);
27+
expect(result.stdout).toBe('{"foo":42}\n');
28+
});
29+
30+
it("should allow pipes in object values", async () => {
31+
const env = new Bash();
32+
const result = await env.exec(
33+
"echo '[[1,2],[3,4]]' | jq -c '{a: .[0] | add, b: .[1] | add}'",
34+
);
35+
expect(result.stdout).toBe('{"a":3,"b":7}\n');
36+
});
37+
});
38+
39+
describe("array construction", () => {
40+
it("should construct array from iterator", async () => {
41+
const env = new Bash();
42+
const result = await env.exec("echo '{\"a\":1,\"b\":2}' | jq '[.a, .b]'");
43+
expect(result.stdout).toBe("[\n 1,\n 2\n]\n");
44+
});
45+
46+
it("should construct array from object values", async () => {
47+
const env = new Bash();
48+
const result = await env.exec(
49+
'echo \'{"a":1,"b":2,"c":3}\' | jq \'[.[]]\'',
50+
);
51+
expect(result.stdout).toBe("[\n 1,\n 2,\n 3\n]\n");
52+
});
53+
});
54+
});

src/commands/jq/jq.filters.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Bash } from "../../Bash.js";
3+
4+
describe("jq filters", () => {
5+
describe("select and map", () => {
6+
it("should filter with select", async () => {
7+
const env = new Bash();
8+
const result = await env.exec(
9+
"echo '[1,2,3,4,5]' | jq '[.[] | select(. > 3)]'",
10+
);
11+
expect(result.stdout).toBe("[\n 4,\n 5\n]\n");
12+
});
13+
14+
it("should transform with map", async () => {
15+
const env = new Bash();
16+
const result = await env.exec("echo '[1,2,3]' | jq 'map(. * 2)'");
17+
expect(result.stdout).toBe("[\n 2,\n 4,\n 6\n]\n");
18+
});
19+
20+
it("should chain select and map", async () => {
21+
const env = new Bash();
22+
const result = await env.exec(
23+
"echo '[1,2,3,4,5]' | jq '[.[] | select(. > 2) | . * 10]'",
24+
);
25+
expect(result.stdout).toBe("[\n 30,\n 40,\n 50\n]\n");
26+
});
27+
28+
it("should select objects by field", async () => {
29+
const env = new Bash();
30+
const result = await env.exec(
31+
'echo \'[{"n":1},{"n":5},{"n":2}]\' | jq -c \'[.[] | select(.n > 2)]\'',
32+
);
33+
expect(result.stdout).toBe('[{"n":5}]\n');
34+
});
35+
});
36+
37+
describe("has and in", () => {
38+
it("should check has for object", async () => {
39+
const env = new Bash();
40+
const result = await env.exec("echo '{\"foo\":42}' | jq 'has(\"foo\")'");
41+
expect(result.stdout).toBe("true\n");
42+
});
43+
44+
it("should check has for missing key", async () => {
45+
const env = new Bash();
46+
const result = await env.exec("echo '{\"foo\":42}' | jq 'has(\"bar\")'");
47+
expect(result.stdout).toBe("false\n");
48+
});
49+
50+
it("should check has for array", async () => {
51+
const env = new Bash();
52+
const result = await env.exec("echo '[1,2,3]' | jq 'has(1)'");
53+
expect(result.stdout).toBe("true\n");
54+
});
55+
});
56+
57+
describe("contains", () => {
58+
it("should check array contains", async () => {
59+
const env = new Bash();
60+
const result = await env.exec("echo '[1,2,3]' | jq 'contains([2])'");
61+
expect(result.stdout).toBe("true\n");
62+
});
63+
64+
it("should check object contains", async () => {
65+
const env = new Bash();
66+
const result = await env.exec(
67+
'echo \'{"a":1,"b":2}\' | jq \'contains({"a":1})\'',
68+
);
69+
expect(result.stdout).toBe("true\n");
70+
});
71+
});
72+
73+
describe("any and all", () => {
74+
it("should check any with expression", async () => {
75+
const env = new Bash();
76+
const result = await env.exec("echo '[1,2,3,4,5]' | jq 'any(. > 3)'");
77+
expect(result.stdout).toBe("true\n");
78+
});
79+
80+
it("should check all with expression", async () => {
81+
const env = new Bash();
82+
const result = await env.exec("echo '[1,2,3]' | jq 'all(. > 0)'");
83+
expect(result.stdout).toBe("true\n");
84+
});
85+
});
86+
87+
describe("conditionals", () => {
88+
it("should evaluate if-then-else", async () => {
89+
const env = new Bash();
90+
const result = await env.exec(
91+
"echo '5' | jq 'if . > 3 then \"big\" else \"small\" end'",
92+
);
93+
expect(result.stdout).toBe('"big"\n');
94+
});
95+
96+
it("should evaluate else branch", async () => {
97+
const env = new Bash();
98+
const result = await env.exec(
99+
"echo '2' | jq 'if . > 3 then \"big\" else \"small\" end'",
100+
);
101+
expect(result.stdout).toBe('"small"\n');
102+
});
103+
104+
it("should evaluate elif", async () => {
105+
const env = new Bash();
106+
const result = await env.exec(
107+
'echo \'5\' | jq \'if . > 10 then "big" elif . > 3 then "medium" else "small" end\'',
108+
);
109+
expect(result.stdout).toBe('"medium"\n');
110+
});
111+
});
112+
113+
describe("optional operator", () => {
114+
it("should return null for missing key with ?", async () => {
115+
const env = new Bash();
116+
const result = await env.exec("echo 'null' | jq '.foo?'");
117+
expect(result.stdout).toBe("null\n");
118+
});
119+
120+
it("should return value if present with ?", async () => {
121+
const env = new Bash();
122+
const result = await env.exec("echo '{\"foo\":42}' | jq '.foo?'");
123+
expect(result.stdout).toBe("42\n");
124+
});
125+
});
126+
127+
describe("try-catch", () => {
128+
it("should catch errors", async () => {
129+
const env = new Bash();
130+
const result = await env.exec(
131+
"echo '1' | jq 'try error(\"oops\") catch \"caught\"'",
132+
);
133+
expect(result.stdout).toBe('"caught"\n');
134+
});
135+
});
136+
137+
describe("variables", () => {
138+
it("should bind and use variable", async () => {
139+
const env = new Bash();
140+
const result = await env.exec("echo '5' | jq '. as $x | $x * $x'");
141+
expect(result.stdout).toBe("25\n");
142+
});
143+
144+
it("should use variable in object construction", async () => {
145+
const env = new Bash();
146+
const result = await env.exec(
147+
"echo '3' | jq -c '. as $n | {value: $n, doubled: ($n * 2)}'",
148+
);
149+
expect(result.stdout).toBe('{"value":3,"doubled":6}\n');
150+
});
151+
});
152+
});

0 commit comments

Comments
 (0)