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
187 changes: 187 additions & 0 deletions packages/just-bash/src/commands/jq/jq.arg.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { describe, expect, it } from "vitest";
import { Bash } from "../../Bash.js";

describe("jq named-argument flags", () => {
describe("--arg", () => {
it("binds $NAME to the string VALUE", async () => {
const env = new Bash();
const result = await env.exec(
"echo '{}' | jq --arg name World '{greeting: (\"Hello \" + $name)}'",
);
expect(result.stdout).toBe('{\n "greeting": "Hello World"\n}\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("always binds a string even when VALUE looks numeric", async () => {
const env = new Bash();
const result = await env.exec("jq -n --arg x 5 '$x'");
expect(result.stdout).toBe('"5"\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});
});

describe("multiple --arg", () => {
it("populates $ARGS.named in order", async () => {
const env = new Bash();
const result = await env.exec(
"jq -cn --arg a foo --arg b bar --arg c baz '$ARGS.named'",
);
expect(result.stdout).toBe('{"a":"foo","b":"bar","c":"baz"}\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("binds each $NAME for use in a filter", async () => {
const env = new Bash();
const result = await env.exec(
"jq -n --arg first Ada --arg last Lovelace '$first + \" \" + $last'",
);
expect(result.stdout).toBe('"Ada Lovelace"\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});
});

describe("--argjson", () => {
it("binds $NAME to a JSON number", async () => {
const env = new Bash();
const result = await env.exec("jq -n --argjson x 5 '$x'");
expect(result.stdout).toBe("5\n");
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("binds $NAME to a JSON object", async () => {
const env = new Bash();
const result = await env.exec("jq -n --argjson x '{\"a\":1}' '$x.a'");
expect(result.stdout).toBe("1\n");
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("errors on invalid JSON with exit code 2", async () => {
const env = new Bash();
const result = await env.exec("jq -n --argjson x notjson '$x'");
expect(result.stdout).toBe("");
expect(result.stderr).toBe("jq: invalid JSON text passed to --argjson\n");
expect(result.exitCode).toBe(2);
});
});

describe("--rawfile", () => {
it("binds $NAME to the raw file contents including newlines", async () => {
const env = new Bash();
const result = await env.exec(
"printf 'line1\\nline2\\n' > rf.txt && jq -n --rawfile r rf.txt '$r'",
);
expect(result.stdout).toBe('"line1\\nline2\\n"\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});
});

describe("--slurpfile", () => {
it("binds $NAME to the array of JSON values in FILE", async () => {
const env = new Bash();
const result = await env.exec(
"printf '1 2 3\\n' > sf.json && jq -cn --slurpfile s sf.json '$s'",
);
expect(result.stdout).toBe("[1,2,3]\n");
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("errors with exit code 2 when the file is missing", async () => {
const env = new Bash();
const result = await env.exec("jq -n --slurpfile s nope.json '$s'");
expect(result.stdout).toBe("");
expect(result.exitCode).toBe(2);
});
});

describe("$ARGS", () => {
it("exposes all named bindings via $ARGS.named", async () => {
const env = new Bash();
const result = await env.exec(
"jq -cn --arg a 1 --argjson b 2 '$ARGS.named'",
);
expect(result.stdout).toBe('{"a":"1","b":2}\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("orders $ARGS as positional then named", async () => {
const env = new Bash();
const result = await env.exec("jq -cn --arg a 1 '$ARGS'");
expect(result.stdout).toBe('{"positional":[],"named":{"a":"1"}}\n');
expect(result.exitCode).toBe(0);
});

it("returns {} for $ARGS.named when no args are given", async () => {
const env = new Bash();
const result = await env.exec("jq -cn '$ARGS.named'");
expect(result.stdout).toBe("{}\n");
expect(result.exitCode).toBe(0);
});

it("returns [] for $ARGS.positional", async () => {
const env = new Bash();
const result = await env.exec("jq -cn '$ARGS.positional'");
expect(result.stdout).toBe("[]\n");
expect(result.exitCode).toBe(0);
});
});

describe("errors and safety", () => {
it("errors with exit code 2 on a missing operand", async () => {
const env = new Bash();
const result = await env.exec("jq -n --arg x");
expect(result.stdout).toBe("");
expect(result.stderr).toBe(
"jq: --arg takes two parameters (e.g. --arg varname value)\n",
);
expect(result.exitCode).toBe(2);
});

it("keeps a __proto__ arg name as a plain data key (faithful to real jq)", async () => {
const env = new Bash();
const result = await env.exec(
"jq -cn --arg __proto__ pwned '$ARGS.named'",
);
expect(result.stdout).toBe('{"__proto__":"pwned"}\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("reads through a __proto__ arg without inheriting (returns null)", async () => {
const env = new Bash();
const result = await env.exec(
"jq -cn --argjson __proto__ '{\"pwned\":123}' '$ARGS.named.pwned'",
);
expect(result.stdout).toBe("null\n");
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("does not pollute Object.prototype via a __proto__ arg name", async () => {
const env = new Bash();
const result = await env.exec(
"jq -cn --arg __proto__ pwned '$ARGS.named'",
);
expect(result.stdout).toBe('{"__proto__":"pwned"}\n');
expect(result.exitCode).toBe(0);
expect(({} as Record<string, unknown>).pwned).toBeUndefined();
expect(Object.getPrototypeOf({})).toBe(Object.prototype);
});

it("leaves other unknown long options unchanged", async () => {
const env = new Bash();
const result = await env.exec("jq -n --bogus '.'");
expect(result.stdout).toBe("");
expect(result.stderr).toBe("jq: unrecognized option '--bogus'\n");
expect(result.exitCode).toBe(1);
});
});
});
111 changes: 111 additions & 0 deletions packages/just-bash/src/commands/jq/jq.args-positional.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { describe, expect, it } from "vitest";
import { Bash } from "../../Bash.js";

describe("jq positional-argument flags", () => {
describe("--args", () => {
it("collects remaining tokens as string positional args", async () => {
const env = new Bash();
const result = await env.exec("jq -cn '$ARGS.positional' --args a b c");
expect(result.stdout).toBe('["a","b","c"]\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("always keeps positional args as strings even when numeric-looking", async () => {
const env = new Bash();
const result = await env.exec("jq -cn '$ARGS.positional' --args 1 2 3");
expect(result.stdout).toBe('["1","2","3"]\n');
expect(result.exitCode).toBe(0);
});

it("treats the first non-option token as the filter, not a positional", async () => {
const env = new Bash();
const result = await env.exec("jq -cn --args '$ARGS.positional' a b c");
expect(result.stdout).toBe('["a","b","c"]\n');
expect(result.exitCode).toBe(0);
});

it("still parses known flags after --args", async () => {
const env = new Bash();
const result = await env.exec("jq -n --args '$ARGS.positional' a -c b");
expect(result.stdout).toBe('["a","b"]\n');
expect(result.exitCode).toBe(0);
});
});

describe("--jsonargs", () => {
it("parses remaining tokens as JSON positional args", async () => {
const env = new Bash();
const result = await env.exec(
"jq -cn '$ARGS.positional' --jsonargs 1 '\"x\"' true",
);
expect(result.stdout).toBe('[1,"x",true]\n');
expect(result.stderr).toBe("");
expect(result.exitCode).toBe(0);
});

it("parses JSON objects and arrays as positional args", async () => {
const env = new Bash();
const result = await env.exec(
"jq -cn '$ARGS.positional' --jsonargs '{\"a\":1}' '[1,2]'",
);
expect(result.stdout).toBe('[{"a":1},[1,2]]\n');
expect(result.exitCode).toBe(0);
});

it("errors with exit code 2 on invalid JSON", async () => {
const env = new Bash();
const result = await env.exec(
"jq -n '$ARGS.positional' --jsonargs 1 notjson",
);
expect(result.stdout).toBe("");
expect(result.stderr).toBe(
"jq: invalid JSON text passed to --jsonargs\n",
);
expect(result.exitCode).toBe(2);
});
});

describe("mode switching and combinations", () => {
it("switches from --args to --jsonargs mode mid-stream", async () => {
const env = new Bash();
const result = await env.exec(
"jq -cn '$ARGS.positional' --args a --jsonargs 1",
);
expect(result.stdout).toBe('["a",1]\n');
expect(result.exitCode).toBe(0);
});

it("populates both $ARGS.named and $ARGS.positional", async () => {
const env = new Bash();
const result = await env.exec("jq -cn '$ARGS' --arg k v --args a b");
expect(result.stdout).toBe(
'{"positional":["a","b"],"named":{"k":"v"}}\n',
);
expect(result.exitCode).toBe(0);
});

it("interleaves --arg after --args", async () => {
const env = new Bash();
const result = await env.exec("jq -cn '$ARGS' --args a --arg k v");
expect(result.stdout).toBe('{"positional":["a"],"named":{"k":"v"}}\n');
expect(result.exitCode).toBe(0);
});
});

describe("empty and default", () => {
it("returns [] when --args is given with no following tokens", async () => {
const env = new Bash();
const result = await env.exec("jq -cn '$ARGS.positional' --args");
expect(result.stdout).toBe("[]\n");
expect(result.exitCode).toBe(0);
});

it("does not treat --args positionals as input files", async () => {
const env = new Bash();
const result = await env.exec("echo '{\"v\":1}' | jq -c '.v' --args a b");
expect(result.stdout).toBe("1\n");
expect(result.exitCode).toBe(0);
});
});
});
Loading
Loading