Something went wrong. Try again.
Shell parser/AST for POSIX/Bash/mksh/zsh, in TypeScript
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263import { describe, expect, it } from "vitest";import type { ArithCmd, ArithExp, BinaryArithm, CStyleLoop, ParamExp, SimpleCommand,} from "../ast";import { parse } from "../parse";
function arithOf(src: string): ArithCmd { const { ast } = parse(src); const cmd = ast.body[0]?.command; if (cmd?.type !== "ArithCmd") throw new Error(`expected ArithCmd, got ${cmd?.type}`); return cmd;}
function arithExpOf(src: string): ArithExp { const { ast } = parse(src); const cmd = ast.body[0]?.command as SimpleCommand; const part = cmd.words?.[1]?.parts[0]; if (part?.type !== "ArithExp") throw new Error(`expected ArithExp, got ${part?.type}`); return part;}
const litArith = (value: string) => ({ type: "ArithLit", value });const variable = (name: string) => ({ type: "ParamExp", short: true, param: { type: "Literal", value: name },});
describe("arithmetic AST in (( ))", () => { it("parses a literal", () => { expect(arithOf("(( 42 )).x").x).toMatchAst(litArith("42")); });
it("accepts an empty arithmetic command", () => { // mvdan/sh has file tests for empty C-style arithmetic clauses, and Bash // itself accepts an empty arithmetic command. expect(() => parse("(( ))")).not.toThrow(); });
it("parses a variable", () => { expect(arithOf("(( i ))").x).toMatchAst(variable("i")); });
it("parses binary +", () => { expect(arithOf("(( 1 + 2 ))").x).toMatchAst({ type: "BinaryArithm", op: "+", x: litArith("1"), y: litArith("2"), }); });
it("respects * over +", () => { expect(arithOf("(( 1 + 2 * 3 ))").x).toMatchAst({ type: "BinaryArithm", op: "+", x: litArith("1"), y: { type: "BinaryArithm", op: "*", x: litArith("2"), y: litArith("3"), }, }); });
it("parses parens", () => { expect(arithOf("(( (1 + 2) * 3 ))").x).toMatchAst({ type: "BinaryArithm", op: "*", x: { type: "ParenArithm", x: { type: "BinaryArithm", op: "+", x: litArith("1"), y: litArith("2"), }, }, y: litArith("3"), }); });
it("parses unary -", () => { expect(arithOf("(( -1 ))").x).toMatchAst({ type: "UnaryArithm", op: "-", x: litArith("1"), }); });
it("parses pre-increment", () => { expect(arithOf("(( ++i ))").x).toMatchAst({ type: "UnaryArithm", op: "++", x: variable("i"), }); });
it("parses post-increment", () => { expect(arithOf("(( i++ ))").x).toMatchAst({ type: "UnaryArithm", op: "++", post: true, x: variable("i"), }); });
it("parses comparison", () => { expect(arithOf("(( a < b ))").x).toMatchAst({ type: "BinaryArithm", op: "<", x: variable("a"), y: variable("b"), }); });
it("parses assignment as right-associative", () => { expect(arithOf("(( a = b = 1 ))").x).toMatchAst({ type: "BinaryArithm", op: "=", x: variable("a"), y: { type: "BinaryArithm", op: "=", x: variable("b"), y: litArith("1"), }, }); });
it("parses ternary", () => { expect(arithOf("(( a ? b : c ))").x).toMatchAst({ type: "BinaryArithm", op: "?", x: variable("a"), y: { type: "BinaryArithm", op: ":", x: variable("b"), y: variable("c"), }, }); });
it("parses comma operator", () => { const x = arithOf("(( a , b ))").x as BinaryArithm; expect(x.op).toBe(","); });
it("spans binary arithmetic expressions from left to right operand", () => { const x = arithOf("((1+2))").x as BinaryArithm; expect(x.pos).toEqual({ offset: 2, line: 1, col: 3 }); expect(x.end).toEqual({ offset: 5, line: 1, col: 6 }); });
it("spans postfix arithmetic expressions from operand to operator end", () => { const x = arithOf("((i++))").x; if (!x) throw new Error("expected arithmetic expression"); expect(x.type).toBe("UnaryArithm"); expect(x.pos).toEqual({ offset: 2, line: 1, col: 3 }); expect(x.end).toEqual({ offset: 5, line: 1, col: 6 }); });});
describe("arithmetic integer base syntax", () => { it("parses base-16 literals", () => { expect(arithOf("(( 16#FF ))").x).toMatchAst(litArith("16#FF")); });
it("parses base-2 literals", () => { expect(arithOf("(( 2#1010 ))").x).toMatchAst(litArith("2#1010")); });
it("parses base-8 literals", () => { expect(arithOf("(( 8#77 ))").x).toMatchAst(litArith("8#77")); });
it("parses base-64 literals using @ and _", () => { expect(arithOf("(( 64#@_ ))").x).toMatchAst(litArith("64#@_")); });
it("allows mixed case for bases <= 36", () => { expect(arithOf("(( 16#Ff ))").x).toMatchAst(litArith("16#Ff")); });});
describe("arithmetic AST in $(( ))", () => { it("parses a single variable expansion", () => { expect(arithExpOf("echo $(( i + 1 ))").x).toMatchAst({ type: "BinaryArithm", op: "+", x: variable("i"), y: litArith("1"), }); });
it("parses special parameters in arithmetic expansions", () => { const x = arithExpOf("echo $(( $? + 0 ))").x as BinaryArithm; expect(x.op).toBe("+"); expect((x.x as ParamExp).param).toMatchAst({ type: "Literal", value: "?", }); });});
describe("arithmetic in C-style for loop", () => { it("accepts empty init/cond/post like mvdan/sh and Bash", () => { // Inspired by mvdan/sh syntax/filetests_test.go cases for // `for (( ; ; ))` and `for ((;;))`. expect(() => parse("for (( ; ; )); do foo; done")).not.toThrow(); expect(() => parse("for ((;;)); do foo; done")).not.toThrow(); });
it("parses init/cond/post", () => { const { ast } = parse("for (( i = 0; i < 10; i++ )); do :; done"); const cmd = ast.body[0]?.command as CStyleLoop; expect(cmd.type).toBe("CStyleLoop"); expect(cmd.init).toMatchAst({ type: "BinaryArithm", op: "=", x: variable("i"), y: litArith("0"), }); expect(cmd.cond).toMatchAst({ type: "BinaryArithm", op: "<", x: variable("i"), y: litArith("10"), }); expect(cmd.post).toMatchAst({ type: "UnaryArithm", op: "++", post: true, x: variable("i"), }); });
it("anchors cond and post positions at their own C-style loop segments", () => { const src = "for ((i=0;j;k++)); do :; done"; const { ast } = parse(src); const cmd = ast.body[0]?.command as CStyleLoop; expect(cmd.cond?.pos).toEqual({ offset: src.indexOf("j"), line: 1, col: 11, }); expect(cmd.post?.pos).toEqual({ offset: src.indexOf("k++"), line: 1, col: 13, }); });});