Skip to content
Open
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
111 changes: 111 additions & 0 deletions src/commands/jq/jq.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
40 changes: 39 additions & 1 deletion src/commands/jq/jq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down Expand Up @@ -214,6 +216,7 @@ export const jqCommand: Command = {
let filter = ".";
let filterSet = false;
const files: string[] = [];
const initialVars = new Map<string, QueryValue>();

for (let i = 0; i < args.length; i++) {
const a = args[i];
Expand All @@ -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)) {
Expand Down Expand Up @@ -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,
};
Expand Down
4 changes: 3 additions & 1 deletion src/commands/query-engine/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -358,6 +358,8 @@ function applyPathTransform(
export interface EvaluateOptions {
limits?: QueryExecutionLimits;
env?: Map<string, string>;
/** Pre-defined variables (e.g. from jq --arg/--argjson). Keys should include $ prefix. */
initialVars?: Map<string, QueryValue>;
coverage?: FeatureCoverageWriter;
requireDefenseContext?: boolean;
}
Expand Down