mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
750 lines
16 KiB
TypeScript
750 lines
16 KiB
TypeScript
import { $ } from "bun";
|
|
import { shellInternals } from "bun:internal-for-testing";
|
|
import { createTestBuilder, redirect } from "./util";
|
|
const { lex } = shellInternals;
|
|
const TestBuilder = createTestBuilder(import.meta.path);
|
|
|
|
const BUN = process.argv0;
|
|
|
|
$.nothrow();
|
|
|
|
describe("lex shell", () => {
|
|
test("basic", () => {
|
|
const expected = [{ "Text": "next" }, { "Delimit": {} }, { "Text": "dev" }, { "Delimit": {} }, { "Eof": {} }];
|
|
const result = JSON.parse(lex`next dev`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("var edgecase", () => {
|
|
expect(JSON.parse(lex`$PWD/test.txt`)).toEqual([
|
|
{ "Var": "PWD" },
|
|
{ "Text": "/test.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
]);
|
|
});
|
|
|
|
test("vars", () => {
|
|
const expected = [
|
|
{ "Text": "next" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "dev" },
|
|
{ "Delimit": {} },
|
|
{ "Var": "PORT" },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`next dev $PORT`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("quoted_var", () => {
|
|
const expected = [
|
|
{ "Text": "next" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "dev" },
|
|
{ "Delimit": {} },
|
|
{ "Var": "PORT" },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`next dev "$PORT"`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("quoted_edge_case", () => {
|
|
const expected = [
|
|
{ "Text": "next" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "dev" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "foo" },
|
|
{ "Var": "PORT" },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`next dev foo"$PORT"`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("quote_multi", () => {
|
|
const expected = [
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "foo" },
|
|
{ "Var": "NICE" },
|
|
{ "Text": "good" },
|
|
{ "DoubleQuotedText": "NICE" },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`echo foo"$NICE"good"NICE"`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("semicolon", () => {
|
|
const expected = [
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "foo" },
|
|
{ "Delimit": {} },
|
|
{ "Semicolon": {} },
|
|
{ "Text": "bar" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "baz" },
|
|
{ "Delimit": {} },
|
|
{ "Semicolon": {} },
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "DoubleQuotedText": "NICE;" },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`echo foo; bar baz; echo "NICE;"`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("single_quote", () => {
|
|
const expected = [
|
|
{ "Text": "next" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "dev" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "hello how is it going" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`next dev 'hello how is it going'`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("env_vars", () => {
|
|
const expected = [
|
|
{ "Text": "NAME=zack" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "FULLNAME=" },
|
|
{ "Var": "NAME" },
|
|
{ "DoubleQuotedText": " radisic" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "LOL=" },
|
|
{ "Delimit": {} },
|
|
{ "Semicolon": {} },
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Var": "FULLNAME" },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`NAME=zack FULLNAME="$NAME radisic" LOL= ; echo $FULLNAME`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("env_vars2", () => {
|
|
const expected = [
|
|
{
|
|
Text: "NAME=zack",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "foo=",
|
|
},
|
|
{
|
|
Var: "bar",
|
|
},
|
|
{ Delimit: {} },
|
|
{
|
|
Text: "echo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Var: "NAME",
|
|
},
|
|
{
|
|
Eof: {},
|
|
},
|
|
];
|
|
const result = JSON.parse(lex`NAME=zack foo=$bar echo $NAME`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("env_vars exported", () => {
|
|
const expected = [
|
|
{
|
|
Text: "export",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "NAME=zack",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "FOO=bar",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "export",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "NICE=lmao",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Eof: {},
|
|
},
|
|
];
|
|
const result = JSON.parse(lex`export NAME=zack FOO=bar export NICE=lmao`);
|
|
// console.log(result);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("brace_expansion", () => {
|
|
const expected = [
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "BraceBegin": {} },
|
|
{ "Text": "ts" },
|
|
{ "Comma": {} },
|
|
{ "Text": "tsx" },
|
|
{ "Comma": {} },
|
|
{ "Text": "js" },
|
|
{ "Comma": {} },
|
|
{ "Text": "jsx" },
|
|
{ "BraceEnd": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`echo {ts,tsx,js,jsx}`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("op_and", () => {
|
|
const expected = [
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "foo" },
|
|
{ "Delimit": {} },
|
|
{ "DoubleAmpersand": {} },
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "bar" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`echo foo && echo bar`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("op_or", () => {
|
|
const expected = [
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "foo" },
|
|
{ "Delimit": {} },
|
|
{ "DoublePipe": {} },
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "bar" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`echo foo || echo bar`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("op_pipe", () => {
|
|
const expected = [
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "foo" },
|
|
{ "Delimit": {} },
|
|
{ "Pipe": {} },
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "bar" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`echo foo | echo bar`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("op_bg", () => {
|
|
const expected = [
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "foo" },
|
|
{ "Delimit": {} },
|
|
{ "Ampersand": {} },
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "bar" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
const result = JSON.parse(lex`echo foo & echo bar`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("op_redirect", () => {
|
|
let expected = [
|
|
{ "Text": "echo" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "foo" },
|
|
{ "Delimit": {} },
|
|
{
|
|
"Redirect": redirect({ stdout: true }),
|
|
},
|
|
{ "Text": "cat" },
|
|
{ "Delimit": {} },
|
|
{ "Text": "secrets.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
let result = JSON.parse(lex`echo foo > cat secrets.txt`);
|
|
expect(result).toEqual(expected);
|
|
|
|
expected = [
|
|
{ "Text": "cmd1" },
|
|
{ "Delimit": {} },
|
|
{
|
|
"Redirect": {
|
|
"stdin": true,
|
|
"stdout": false,
|
|
"stderr": false,
|
|
"append": false,
|
|
duplicate_out: false,
|
|
"__unused": 0,
|
|
},
|
|
},
|
|
{ "Text": "file.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
result = lex`cmd1 0> file.txt`;
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
|
|
expected = [
|
|
{ "Text": "cmd1" },
|
|
{ "Delimit": {} },
|
|
{
|
|
"Redirect": {
|
|
"stdin": false,
|
|
"stdout": true,
|
|
"stderr": false,
|
|
"append": false,
|
|
duplicate_out: false,
|
|
"__unused": 0,
|
|
},
|
|
},
|
|
{ "Text": "file.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
result = lex`cmd1 1> file.txt`;
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
|
|
expected = [
|
|
{ "Text": "cmd1" },
|
|
{ "Delimit": {} },
|
|
{
|
|
"Redirect": {
|
|
"stdin": false,
|
|
"stdout": false,
|
|
"stderr": true,
|
|
"append": false,
|
|
duplicate_out: false,
|
|
"__unused": 0,
|
|
},
|
|
},
|
|
{ "Text": "file.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
result = lex`cmd1 2> file.txt`;
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
|
|
expected = [
|
|
{ "Text": "cmd1" },
|
|
{ "Delimit": {} },
|
|
{
|
|
"Redirect": {
|
|
"stdin": false,
|
|
"stdout": true,
|
|
"stderr": true,
|
|
"append": false,
|
|
duplicate_out: false,
|
|
"__unused": 0,
|
|
},
|
|
},
|
|
{ "Text": "file.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
result = lex`cmd1 &> file.txt`;
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
|
|
expected = [
|
|
{ "Text": "cmd1" },
|
|
{ "Delimit": {} },
|
|
{
|
|
"Redirect": {
|
|
"stdin": false,
|
|
"stdout": true,
|
|
"stderr": false,
|
|
"append": true,
|
|
duplicate_out: false,
|
|
"__unused": 0,
|
|
},
|
|
},
|
|
{ "Text": "file.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
result = lex`cmd1 1>> file.txt`;
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
|
|
expected = [
|
|
{ "Text": "cmd1" },
|
|
{ "Delimit": {} },
|
|
{
|
|
"Redirect": {
|
|
"stdin": false,
|
|
"stdout": false,
|
|
"stderr": true,
|
|
"append": true,
|
|
duplicate_out: false,
|
|
"__unused": 0,
|
|
},
|
|
},
|
|
{ "Text": "file.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
result = lex`cmd1 2>> file.txt`;
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
|
|
expected = [
|
|
{ "Text": "cmd1" },
|
|
{ "Delimit": {} },
|
|
{
|
|
"Redirect": {
|
|
"stdin": false,
|
|
"stdout": true,
|
|
"stderr": true,
|
|
"append": true,
|
|
duplicate_out: false,
|
|
"__unused": 0,
|
|
},
|
|
},
|
|
{ "Text": "file.txt" },
|
|
{ "Delimit": {} },
|
|
{ "Eof": {} },
|
|
];
|
|
result = lex`cmd1 &>> file.txt`;
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
});
|
|
|
|
test("obj_ref", () => {
|
|
const expected = [
|
|
{
|
|
Text: "echo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "foo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Redirect: redirect({ stdout: true }),
|
|
},
|
|
{
|
|
JSObjRef: 0,
|
|
},
|
|
{
|
|
DoubleAmpersand: {},
|
|
},
|
|
{
|
|
Text: "echo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "lmao",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Redirect: redirect({ stdout: true }),
|
|
},
|
|
{
|
|
JSObjRef: 1,
|
|
},
|
|
{
|
|
Eof: {},
|
|
},
|
|
];
|
|
const buffer = new Uint8Array(1 << 20);
|
|
const buffer2 = new Uint8Array(1 << 20);
|
|
const result = JSON.parse(lex`echo foo > ${buffer} && echo lmao > ${buffer2}`);
|
|
expect(result).toEqual(expected);
|
|
});
|
|
|
|
test("cmd_sub_dollar", () => {
|
|
const expected = [
|
|
{
|
|
Text: "echo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "foo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstBegin: {},
|
|
},
|
|
{
|
|
Text: "ls",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstEnd: {},
|
|
},
|
|
{
|
|
Eof: {},
|
|
},
|
|
];
|
|
|
|
const result = lex`echo foo $(ls)`;
|
|
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
});
|
|
|
|
test("cmd_sub_dollar_nested", () => {
|
|
const expected = [
|
|
{
|
|
Text: "echo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "foo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstBegin: {},
|
|
},
|
|
{
|
|
Text: "ls",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstBegin: {},
|
|
},
|
|
{
|
|
Text: "ls",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstEnd: {},
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstBegin: {},
|
|
},
|
|
{
|
|
Text: "ls",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstEnd: {},
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstEnd: {},
|
|
},
|
|
{
|
|
Eof: {},
|
|
},
|
|
];
|
|
|
|
const result = lex`echo foo $(ls $(ls) $(ls))`;
|
|
// console.log(JSON.parse(result));
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
});
|
|
|
|
test("cmd_sub_edgecase", () => {
|
|
const expected = [
|
|
{
|
|
Text: "echo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstBegin: {},
|
|
},
|
|
{
|
|
Text: "FOO=bar",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Var: "FOO",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstEnd: {},
|
|
},
|
|
{
|
|
Eof: {},
|
|
},
|
|
];
|
|
|
|
const result = lex`echo $(FOO=bar $FOO)`;
|
|
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
});
|
|
|
|
test("cmd_sub_combined_word", () => {
|
|
const expected = [
|
|
{
|
|
Text: "echo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
CmdSubstBegin: {},
|
|
},
|
|
{
|
|
Text: "FOO=bar",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Var: "FOO",
|
|
},
|
|
{ Delimit: {} },
|
|
{
|
|
CmdSubstEnd: {},
|
|
},
|
|
{ Text: "NICE" },
|
|
{ Delimit: {} },
|
|
{
|
|
Eof: {},
|
|
},
|
|
];
|
|
|
|
const result = lex`echo $(FOO=bar $FOO)NICE`;
|
|
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
});
|
|
|
|
test("cmd_sub_backtick", () => {
|
|
const expected = [
|
|
{
|
|
Text: "echo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "foo",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Text: "`ls`",
|
|
},
|
|
{
|
|
Delimit: {},
|
|
},
|
|
{
|
|
Eof: {},
|
|
},
|
|
];
|
|
|
|
const result = lex`echo foo \`ls\``;
|
|
|
|
expect(JSON.parse(result)).toEqual(expected);
|
|
});
|
|
|
|
describe("errors", async () => {
|
|
// This is disallowed because the js object references get turned into special vars: $__bun_0, $__bun_1, etc.
|
|
// this will break things inside of a quote.
|
|
test("JS object ref in quotes", async () => {
|
|
const buffer = new Uint8Array(1);
|
|
await TestBuilder.command`FOO=bar ${BUN} -e "console.log(process.env) > ${buffer}"`
|
|
.error("JS object reference not allowed in double quotes")
|
|
.run();
|
|
});
|
|
|
|
describe("Unexpected ')'", async () => {
|
|
TestBuilder.command`echo )`.error("Unexpected ')'").runAsTest("lone closing paren");
|
|
TestBuilder.command`echo (echo hi)`.error("Unexpected token: `(`").runAsTest("subshell in invalid position");
|
|
TestBuilder.command`echo "()"`.stdout("()\n").runAsTest("quoted parens");
|
|
});
|
|
|
|
test("Unexpected EOF", async () => {
|
|
await TestBuilder.command`echo hi |`.error("Unexpected EOF").run();
|
|
await TestBuilder.command`echo hi &`.error('Background commands "&" are not supported yet.').run();
|
|
});
|
|
|
|
test("Unclosed subshell", async () => {
|
|
await TestBuilder.command`echo hi && $(echo uh oh`.error("Unclosed command substitution").run();
|
|
await TestBuilder.command`echo hi && $(echo uh oh)`
|
|
.stdout("hi\n")
|
|
.stderr("bun: command not found: uh\n")
|
|
.exitCode(1)
|
|
.run();
|
|
|
|
await TestBuilder.command`echo hi && ${{ raw: "`echo uh oh" }}`.error("Unclosed command substitution").run();
|
|
await TestBuilder.command`echo hi && ${{ raw: "`echo uh oh`" }}`
|
|
.stdout("hi\n")
|
|
.stderr("bun: command not found: uh\n")
|
|
.exitCode(1)
|
|
.run();
|
|
|
|
await TestBuilder.command`echo hi && (echo uh oh`.error("Unclosed subshell").run();
|
|
});
|
|
});
|
|
});
|