Files
bun.sh/test/js/bun/shell/lex.test.ts
2024-09-03 21:32:52 -07:00

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();
});
});
});