diff --git a/src/commands/jq/jq.test.ts b/src/commands/jq/jq.test.ts index 949561b9..e688c4a4 100644 --- a/src/commands/jq/jq.test.ts +++ b/src/commands/jq/jq.test.ts @@ -389,4 +389,115 @@ describe("jq", () => { expect(result.stdout).toBe("test\n"); }); }); + + describe("--arg", () => { + it("should bind a string variable", async () => { + const env = new Bash(); + const result = await env.exec( + "echo '{\"a\":1}' | jq --arg name hello '{x: $name}'", + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"x": "hello"'); + }); + + it("should use $var in filter expression", async () => { + const env = new Bash(); + const result = await env.exec( + 'echo \'[{"name":"alice"},{"name":"bob"}]\' | jq --arg who bob \'[.[] | select(.name == $who)]\'', + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"bob"'); + expect(result.stdout).not.toContain('"alice"'); + }); + + it("should support multiple --arg flags", async () => { + const env = new Bash(); + const result = await env.exec( + "echo 'null' | jq -n --arg a hello --arg b world '{x: $a, y: $b}'", + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"x": "hello"'); + expect(result.stdout).toContain('"y": "world"'); + }); + + it("should error if name is missing", async () => { + const env = new Bash(); + const result = await env.exec("echo '{}' | jq --arg"); + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain("--arg requires two arguments"); + }); + + it("should always bind as string (not number)", async () => { + const env = new Bash(); + const result = await env.exec( + "echo 'null' | jq -n --arg x 42 '($x | type)'", + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"string"'); + }); + }); + + describe("--argjson", () => { + it("should bind a JSON number", async () => { + const env = new Bash(); + const result = await env.exec( + "echo 'null' | jq -n --argjson x 42 '{val: $x}'", + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"val": 42'); + }); + + it("should bind a JSON object", async () => { + const env = new Bash(); + const result = await env.exec( + "echo 'null' | jq -n --argjson obj '{\"a\":1}' '$obj.a'", + ); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe("1"); + }); + + it("should bind a JSON array", async () => { + const env = new Bash(); + const result = await env.exec( + "echo 'null' | jq -n --argjson arr '[1,2,3]' '$arr | length'", + ); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe("3"); + }); + + it("should bind a JSON boolean", async () => { + const env = new Bash(); + const result = await env.exec( + "echo 'null' | jq -n --argjson flag true '$flag'", + ); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe("true"); + }); + + it("should error on invalid JSON", async () => { + const env = new Bash(); + const result = await env.exec( + "echo '{}' | jq --argjson x 'not-json' '.'", + ); + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain("Invalid JSON value for --argjson"); + }); + + it("should error if arguments are missing", async () => { + const env = new Bash(); + const result = await env.exec("echo '{}' | jq --argjson x"); + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain("--argjson requires two arguments"); + }); + + it("should work with --arg and --argjson together", async () => { + const env = new Bash(); + const result = await env.exec( + "echo 'null' | jq -n --arg name test --argjson count 5 '{name: $name, count: $count}'", + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"name": "test"'); + expect(result.stdout).toContain('"count": 5'); + }); + }); }); diff --git a/src/commands/jq/jq.ts b/src/commands/jq/jq.ts index cab1750d..696a94d2 100644 --- a/src/commands/jq/jq.ts +++ b/src/commands/jq/jq.ts @@ -127,6 +127,8 @@ const jqHelp = { "-S, --sort-keys sort object keys", "-C, --color colorize output (ignored)", "-M, --monochrome monochrome output (ignored)", + " --arg name v set $name to string value v", + " --argjson n v set $n to parsed JSON value v", " --tab use tabs for indentation", " --help display this help and exit", ], @@ -214,6 +216,7 @@ export const jqCommand: Command = { let filter = "."; let filterSet = false; const files: string[] = []; + const initialVars = new Map(); for (let i = 0; i < args.length; i++) { const a = args[i]; @@ -231,7 +234,41 @@ export const jqCommand: Command = { } else if (a === "-M" || a === "--monochrome") { /* ignored */ } else if (a === "--tab") useTab = true; - else if (a === "-") files.push("-"); + else if (a === "--arg") { + const name = args[++i]; + const value = args[++i]; + if (name === undefined || value === undefined) { + return { + stdout: "", + stderr: "jq: --arg requires two arguments (name and value)\n", + exitCode: 2, + }; + } + initialVars.set(`$${name}`, value); + } else if (a === "--argjson") { + const name = args[++i]; + const jsonStr = args[++i]; + if (name === undefined || jsonStr === undefined) { + return { + stdout: "", + stderr: + "jq: --argjson requires two arguments (name and JSON value)\n", + exitCode: 2, + }; + } + try { + initialVars.set( + `$${name}`, + sanitizeParsedData(JSON.parse(jsonStr)) as QueryValue, + ); + } catch { + return { + stdout: "", + stderr: `jq: Invalid JSON value for --argjson ${name}: ${jsonStr}\n`, + exitCode: 2, + }; + } + } else if (a === "-") files.push("-"); else if (a.startsWith("--")) return unknownOption("jq", a); else if (a.startsWith("-")) { for (const c of a.slice(1)) { @@ -294,6 +331,7 @@ export const jqCommand: Command = { ? { maxIterations: ctx.limits.maxJqIterations } : undefined, env: ctx.env, + initialVars: initialVars.size > 0 ? initialVars : undefined, coverage: ctx.coverage, requireDefenseContext: ctx.requireDefenseContext, }; diff --git a/src/commands/query-engine/evaluator.ts b/src/commands/query-engine/evaluator.ts index a8610325..084f5a22 100644 --- a/src/commands/query-engine/evaluator.ts +++ b/src/commands/query-engine/evaluator.ts @@ -132,7 +132,7 @@ export interface EvalContext { function createContext(options?: EvaluateOptions): EvalContext { return { - vars: new Map(), + vars: options?.initialVars ? new Map(options.initialVars) : new Map(), limits: { maxIterations: options?.limits?.maxIterations ?? DEFAULT_MAX_JQ_ITERATIONS, @@ -358,6 +358,8 @@ function applyPathTransform( export interface EvaluateOptions { limits?: QueryExecutionLimits; env?: Map; + /** Pre-defined variables (e.g. from jq --arg/--argjson). Keys should include $ prefix. */ + initialVars?: Map; coverage?: FeatureCoverageWriter; requireDefenseContext?: boolean; }