tarsec 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/parsers/bash/index.d.ts +47 -50
- package/dist/parsers/bash/index.js +47 -51
- package/dist/parsers/bash/parsers.d.ts +95 -0
- package/dist/parsers/bash/parsers.js +306 -0
- package/dist/parsers/bash/types.d.ts +59 -110
- package/dist/parsers/bash/types.js +3 -1
- package/package.json +1 -1
- package/dist/parsers/bash/commands.d.ts +0 -11
- package/dist/parsers/bash/commands.js +0 -197
- package/dist/parsers/bash/lexemes.d.ts +0 -27
- package/dist/parsers/bash/lexemes.js +0 -44
- package/dist/parsers/bash/lists.d.ts +0 -18
- package/dist/parsers/bash/lists.js +0 -85
- package/dist/parsers/bash/words.d.ts +0 -22
- package/dist/parsers/bash/words.js +0 -112
|
@@ -1,136 +1,85 @@
|
|
|
1
1
|
/** AST types for the bash parser. See `index.ts` for the supported subset. */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export type
|
|
2
|
+
export type Word = LiteralWord | PathWord | FlagWord | SingleQuotedWord | DoubleQuotedWord | VariableWord | InterpolatedVariableWord;
|
|
3
|
+
export type ScriptName = LiteralWord | PathWord;
|
|
4
|
+
export type LiteralWord = {
|
|
5
5
|
tag: "literal";
|
|
6
6
|
text: string;
|
|
7
|
-
}
|
|
7
|
+
};
|
|
8
|
+
export type PathWord = {
|
|
9
|
+
tag: "path";
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
export type FlagWord = {
|
|
13
|
+
tag: "flag";
|
|
14
|
+
flagName: string;
|
|
15
|
+
flagValue?: string;
|
|
16
|
+
};
|
|
17
|
+
export type SingleQuotedWord = {
|
|
8
18
|
tag: "singleQuoted";
|
|
9
19
|
text: string;
|
|
10
|
-
}
|
|
20
|
+
};
|
|
21
|
+
export type DoubleQuotedWord = {
|
|
11
22
|
tag: "doubleQuoted";
|
|
12
|
-
parts:
|
|
13
|
-
}
|
|
23
|
+
parts: Word[];
|
|
24
|
+
};
|
|
25
|
+
export type VariableWord = {
|
|
14
26
|
tag: "variable";
|
|
15
27
|
name: string;
|
|
16
|
-
} | {
|
|
17
|
-
tag: "paramExpansion";
|
|
18
|
-
expression: string;
|
|
19
|
-
} | {
|
|
20
|
-
tag: "commandSubstitution";
|
|
21
|
-
command: List;
|
|
22
|
-
} | {
|
|
23
|
-
tag: "arithmeticExpansion";
|
|
24
|
-
expression: string;
|
|
25
|
-
};
|
|
26
|
-
export type BashWord = {
|
|
27
|
-
tag: "word";
|
|
28
|
-
parts: WordPart[];
|
|
29
28
|
};
|
|
29
|
+
/** A word built from two or more adjacent parts: `$HOME.txt`, `"a"b`,
|
|
30
|
+
* `"$HOME"/x`. These are ONE word in bash; split into separate words they
|
|
31
|
+
* become separate arguments and the command means something else.
|
|
32
|
+
*
|
|
33
|
+
* Quoted parts belong here as much as variables do — quoting a variable
|
|
34
|
+
* and appending to it (`"$HOME"/bin`) is idiomatic shell. */
|
|
35
|
+
export type InterpolatedVariableWord = {
|
|
36
|
+
tag: "interpolatedVariable";
|
|
37
|
+
parts: (LiteralWord | VariableWord | SingleQuotedWord | DoubleQuotedWord)[];
|
|
38
|
+
};
|
|
39
|
+
export declare function literalWord(text: string): LiteralWord;
|
|
30
40
|
/** `name=value` (or `name=` with a null value) before a command. */
|
|
31
41
|
export type Assignment = {
|
|
32
42
|
tag: "assignment";
|
|
33
43
|
name: string;
|
|
34
|
-
value:
|
|
44
|
+
value: Word | null;
|
|
35
45
|
};
|
|
36
|
-
/** A redirect like `> out.txt`, `2
|
|
37
|
-
* explicit file descriptor (`2` in `2>`), or
|
|
46
|
+
/** A redirect like `> out.txt`, `>> log`, `2> err.txt` or `< in.txt`.
|
|
47
|
+
* `fd` is the explicit file descriptor (`2` in `2>`), or undefined for the
|
|
48
|
+
* default. Only `>`, `>>`, `<` and `&>` are recognized; `2>&1`, heredocs
|
|
49
|
+
* and here-strings are rejected rather than parsed. */
|
|
38
50
|
export type Redirect = {
|
|
39
51
|
tag: "redirect";
|
|
40
|
-
fd
|
|
52
|
+
fd?: number;
|
|
41
53
|
op: string;
|
|
42
|
-
target:
|
|
54
|
+
target: Word;
|
|
43
55
|
};
|
|
44
56
|
export type SimpleCommand = {
|
|
45
57
|
tag: "simpleCommand";
|
|
46
58
|
assignments: Assignment[];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
cond: List;
|
|
56
|
-
thenBody: List;
|
|
57
|
-
}[];
|
|
58
|
-
elseBody: List | null;
|
|
59
|
-
redirects: Redirect[];
|
|
60
|
-
};
|
|
61
|
-
export type LoopCommand = {
|
|
62
|
-
tag: "loop";
|
|
63
|
-
kind: "while" | "until";
|
|
64
|
-
cond: List;
|
|
65
|
-
body: List;
|
|
66
|
-
redirects: Redirect[];
|
|
67
|
-
};
|
|
68
|
-
export type ForCommand = {
|
|
69
|
-
tag: "for";
|
|
70
|
-
variable: string;
|
|
71
|
-
/** The `in word...` list, or null for the implicit `in "$@"`. */
|
|
72
|
-
words: BashWord[] | null;
|
|
73
|
-
body: List;
|
|
59
|
+
/** The command name, or null for an assignment-only line (`FOO=bar`).
|
|
60
|
+
* Bash requires at least one of a command name or an assignment. */
|
|
61
|
+
command: ScriptName | null;
|
|
62
|
+
/** Every word after the command name, in source order. There is no
|
|
63
|
+
* `subcommands` field: no syntactic rule separates `git status` from
|
|
64
|
+
* `echo status`, so splitting them would put a command's real
|
|
65
|
+
* arguments in whichever bucket the preceding word happened to pick. */
|
|
66
|
+
args: Word[];
|
|
74
67
|
redirects: Redirect[];
|
|
75
68
|
};
|
|
76
|
-
export type
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
};
|
|
82
|
-
export type CaseCommand = {
|
|
83
|
-
tag: "case";
|
|
84
|
-
subject: BashWord;
|
|
85
|
-
items: CaseItem[];
|
|
86
|
-
redirects: Redirect[];
|
|
87
|
-
};
|
|
88
|
-
export type Subshell = {
|
|
89
|
-
tag: "subshell";
|
|
90
|
-
body: List;
|
|
91
|
-
redirects: Redirect[];
|
|
92
|
-
};
|
|
93
|
-
export type Group = {
|
|
94
|
-
tag: "group";
|
|
95
|
-
body: List;
|
|
96
|
-
redirects: Redirect[];
|
|
97
|
-
};
|
|
98
|
-
/** `(( expression ))` as a command. The expression is kept as raw text. */
|
|
99
|
-
export type ArithmeticCommand = {
|
|
100
|
-
tag: "arithmeticCommand";
|
|
101
|
-
expression: string;
|
|
102
|
-
redirects: Redirect[];
|
|
103
|
-
};
|
|
104
|
-
export type FunctionDef = {
|
|
105
|
-
tag: "functionDef";
|
|
106
|
-
name: string;
|
|
107
|
-
body: Command;
|
|
108
|
-
};
|
|
109
|
-
export type Command = SimpleCommand | IfCommand | LoopCommand | ForCommand | CaseCommand | Subshell | Group | ArithmeticCommand | FunctionDef;
|
|
110
|
-
/** One or more commands joined by `|` (or `|&`), optionally negated. */
|
|
111
|
-
export type Pipeline = {
|
|
112
|
-
tag: "pipeline";
|
|
113
|
-
negated: boolean;
|
|
114
|
-
commands: Command[];
|
|
115
|
-
};
|
|
116
|
-
/** Pipelines joined by `&&` / `||`, left to right. */
|
|
117
|
-
export type AndOr = {
|
|
118
|
-
tag: "andOr";
|
|
119
|
-
first: Pipeline;
|
|
120
|
-
rest: {
|
|
121
|
-
op: "&&" | "||";
|
|
122
|
-
pipeline: Pipeline;
|
|
123
|
-
}[];
|
|
69
|
+
export type Command = SimpleCommand | And | Or | Parens;
|
|
70
|
+
export type And = {
|
|
71
|
+
tag: "and";
|
|
72
|
+
left: Command;
|
|
73
|
+
right: Command;
|
|
124
74
|
};
|
|
125
|
-
export type
|
|
126
|
-
tag: "
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
background: boolean;
|
|
75
|
+
export type Or = {
|
|
76
|
+
tag: "or";
|
|
77
|
+
left: Command;
|
|
78
|
+
right: Command;
|
|
130
79
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
tag: "list";
|
|
135
|
-
items: ListItem[];
|
|
80
|
+
export type Parens = {
|
|
81
|
+
tag: "parens";
|
|
82
|
+
command: Command;
|
|
136
83
|
};
|
|
84
|
+
export type BashNode = Command | SimpleCommand | Assignment | Redirect | Word | ScriptName;
|
|
85
|
+
export type BashAST = Command[];
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Parser, ParserResult } from "../../types.js";
|
|
2
|
-
import { Assignment, Command, Redirect } from "./types.js";
|
|
3
|
-
/** `> file`, `2>&1`, `<<< "$str"`, ... The file descriptor must be
|
|
4
|
-
* attached to the operator (`2>` redirects fd 2; `2 >` is an argument). */
|
|
5
|
-
export declare const redirectParser: Parser<Redirect>;
|
|
6
|
-
/** `name=value`, `name=`, or `name="$x"y` — value is a full word. */
|
|
7
|
-
export declare const assignmentParser: Parser<Assignment>;
|
|
8
|
-
/** Any single command: simple, compound (with trailing redirects), or a
|
|
9
|
-
* function definition. Declared as a function so the words/lists/commands
|
|
10
|
-
* module cycle is safe under any import order. */
|
|
11
|
-
export declare function commandParser(input: string): ParserResult<Command>;
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/** Commands: redirects, assignments, simple commands, compound commands,
|
|
2
|
-
* and function definitions. */
|
|
3
|
-
import { lazy, many, many1, many1WithJoin, map, not, optional, or, peek, seq, } from "../../combinators.js";
|
|
4
|
-
import { char, oneOf, str } from "../../parsers.js";
|
|
5
|
-
import { trace } from "../../trace.js";
|
|
6
|
-
import { failure, success } from "../../types.js";
|
|
7
|
-
import { bashLexemes, groupClose, groupOpen, linebreak, newlineToken, wordBoundary, } from "./lexemes.js";
|
|
8
|
-
import { list0 as list0Import, list1 as list1Import, list1WithEnd as list1WithEndImport, } from "./lists.js";
|
|
9
|
-
import { commandWord as commandWordImport, identifierRun as identifierRunImport, parenBody as parenBodyImport, wordParser as wordParserImport, } from "./words.js";
|
|
10
|
-
const L = bashLexemes;
|
|
11
|
-
// The words/lists/commands modules form an import cycle. Deferring every
|
|
12
|
-
// cross-module reference to parse time (via lazy) keeps the cycle safe
|
|
13
|
-
// regardless of module evaluation order or transform.
|
|
14
|
-
const list0 = lazy(() => list0Import);
|
|
15
|
-
const list1 = lazy(() => list1Import);
|
|
16
|
-
const commandWord = lazy(() => commandWordImport);
|
|
17
|
-
const identifierRun = lazy(() => identifierRunImport);
|
|
18
|
-
const parenBody = lazy(() => parenBodyImport);
|
|
19
|
-
const wordParser = lazy(() => wordParserImport);
|
|
20
|
-
const list1WithEnd = lazy(() => list1WithEndImport);
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Redirects and assignments
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Longest first, so ">>" wins over ">". The bare "<" refuses a following
|
|
25
|
-
// "<" so heredocs (unsupported) stay unparsed rather than half-parsed.
|
|
26
|
-
const redirectOp = or(str("&>>"), str("&>"), str(">>"), str(">|"), str(">&"), str("<<<"), str("<&"), str("<>"), str(">"), seq([str("<"), not(char("<"))], (results) => results[0]));
|
|
27
|
-
const fileDescriptor = many1WithJoin(oneOf("0123456789"));
|
|
28
|
-
/** `> file`, `2>&1`, `<<< "$str"`, ... The file descriptor must be
|
|
29
|
-
* attached to the operator (`2>` redirects fd 2; `2 >` is an argument). */
|
|
30
|
-
export const redirectParser = trace("bash:redirect", seq([optional(fileDescriptor), L.lexeme(redirectOp), wordParser], (results) => ({
|
|
31
|
-
tag: "redirect",
|
|
32
|
-
fd: results[0] === null ? null : parseInt(results[0], 10),
|
|
33
|
-
op: results[1],
|
|
34
|
-
target: results[2],
|
|
35
|
-
})));
|
|
36
|
-
/** `name=value`, `name=`, or `name="$x"y` — value is a full word. */
|
|
37
|
-
export const assignmentParser = L.lexeme(trace("bash:assignment", seq([identifierRun, str("="), optional(wordParser)], (results) => ({
|
|
38
|
-
tag: "assignment",
|
|
39
|
-
name: results[0],
|
|
40
|
-
value: results[2],
|
|
41
|
-
}))));
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// Simple commands
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
const prefixItem = or(assignmentParser, redirectParser);
|
|
46
|
-
function buildSimpleCommand(prefix, first, rest) {
|
|
47
|
-
const assignments = prefix.filter((item) => item.tag === "assignment");
|
|
48
|
-
const redirects = [
|
|
49
|
-
...prefix.filter((item) => item.tag === "redirect"),
|
|
50
|
-
...rest.filter((item) => item.tag === "redirect"),
|
|
51
|
-
];
|
|
52
|
-
const words = [
|
|
53
|
-
...(first === null ? [] : [first]),
|
|
54
|
-
...rest.filter((item) => item.tag === "word"),
|
|
55
|
-
];
|
|
56
|
-
return { tag: "simpleCommand", assignments, words, redirects };
|
|
57
|
-
}
|
|
58
|
-
// "At least one of prefix/word" expressed as the two valid shapes.
|
|
59
|
-
const simpleCommandParser = trace("bash:simpleCommand", or(seq([many(prefixItem), commandWord, many(or(redirectParser, wordParser))], (results) => buildSimpleCommand(results[0], results[1], results[2])), seq([many1(prefixItem)], (results) => buildSimpleCommand(results[0], null, []))));
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Compound commands
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
const elifClause = seq([L.keyword("elif"), list1, L.keyword("then"), list1], (results) => ({ cond: results[1], thenBody: results[3] }));
|
|
64
|
-
const elseClause = seq([L.keyword("else"), list1], (results) => results[1]);
|
|
65
|
-
const ifParser = seq([
|
|
66
|
-
L.keyword("if"), list1,
|
|
67
|
-
L.keyword("then"), list1,
|
|
68
|
-
many(elifClause),
|
|
69
|
-
optional(elseClause),
|
|
70
|
-
L.keyword("fi"),
|
|
71
|
-
], (results) => ({
|
|
72
|
-
tag: "if",
|
|
73
|
-
cond: results[1],
|
|
74
|
-
thenBody: results[3],
|
|
75
|
-
elifs: results[4],
|
|
76
|
-
elseBody: results[5],
|
|
77
|
-
redirects: [],
|
|
78
|
-
}));
|
|
79
|
-
const loopParser = seq([
|
|
80
|
-
or(L.keyword("while"), L.keyword("until")), list1,
|
|
81
|
-
L.keyword("do"), list1, L.keyword("done"),
|
|
82
|
-
], (results) => ({
|
|
83
|
-
tag: "loop",
|
|
84
|
-
kind: results[0],
|
|
85
|
-
cond: results[1],
|
|
86
|
-
body: results[3],
|
|
87
|
-
redirects: [],
|
|
88
|
-
}));
|
|
89
|
-
const forParser = seq([
|
|
90
|
-
L.keyword("for"), L.identifier,
|
|
91
|
-
optional(seq([L.keyword("in"), many(wordParser)], (results) => results[1])),
|
|
92
|
-
optional(or(L.operator(";"), newlineToken)),
|
|
93
|
-
linebreak,
|
|
94
|
-
L.keyword("do"), list1, L.keyword("done"),
|
|
95
|
-
], (results) => ({
|
|
96
|
-
tag: "for",
|
|
97
|
-
variable: results[1],
|
|
98
|
-
words: results[2],
|
|
99
|
-
body: results[6],
|
|
100
|
-
redirects: [],
|
|
101
|
-
}));
|
|
102
|
-
// Patterns via explicit seq/many: sepBy1 silently swallows a trailing
|
|
103
|
-
// separator when the next parse fails, which would hide a real error.
|
|
104
|
-
const casePatterns = seq([
|
|
105
|
-
wordParser,
|
|
106
|
-
many(seq([L.operator("|"), wordParser], (results) => results[1])),
|
|
107
|
-
], (results) => [results[0], ...results[1]]);
|
|
108
|
-
// Every item needs `;;` (or `;&` / `;;&`) unless `esac` follows directly.
|
|
109
|
-
const caseTerminator = or(seq([or(L.operator(";;&"), L.operator(";;"), L.operator(";&")), linebreak], (results) => results[0]), map(peek(L.keyword("esac")), () => null));
|
|
110
|
-
// Without a leading `(`, a bare `esac` cannot start a pattern — it ends
|
|
111
|
-
// the case statement (bash rejects `case x in esac) ...`).
|
|
112
|
-
const bareEsac = seq([str("esac"), wordBoundary], () => "esac");
|
|
113
|
-
const caseItemStart = or(seq([L.symbol("("), casePatterns], (results) => results[1]), seq([not(bareEsac), casePatterns], (results) => results[1]));
|
|
114
|
-
const caseItemParser = seq([caseItemStart, L.symbol(")"), list0, caseTerminator], (results) => ({
|
|
115
|
-
patterns: results[0],
|
|
116
|
-
body: results[2],
|
|
117
|
-
terminator: results[3],
|
|
118
|
-
}));
|
|
119
|
-
const caseParser = seq([
|
|
120
|
-
L.keyword("case"), wordParser, L.keyword("in"), linebreak,
|
|
121
|
-
many(caseItemParser),
|
|
122
|
-
L.keyword("esac"),
|
|
123
|
-
], (results) => ({
|
|
124
|
-
tag: "case",
|
|
125
|
-
subject: results[1],
|
|
126
|
-
items: results[4],
|
|
127
|
-
redirects: [],
|
|
128
|
-
}));
|
|
129
|
-
const arithmeticCommand = L.lexeme(seq([str("(("), parenBody, str("))")], (results) => ({
|
|
130
|
-
tag: "arithmeticCommand",
|
|
131
|
-
expression: results[1],
|
|
132
|
-
redirects: [],
|
|
133
|
-
})));
|
|
134
|
-
const subshellParser = seq([L.symbol("("), list1, L.symbol(")")], (results) => ({
|
|
135
|
-
tag: "subshell",
|
|
136
|
-
body: results[1],
|
|
137
|
-
redirects: [],
|
|
138
|
-
}));
|
|
139
|
-
/** Can `}` directly follow this command, as it can in bash? True for a
|
|
140
|
-
* compound command with no trailing redirects (`{ { echo a; } }`), and
|
|
141
|
-
* for a function definition whose body qualifies. A redirect re-enters
|
|
142
|
-
* word context (`{ { cat; } > log }` is rejected by bash), and `(( ))`
|
|
143
|
-
* does not qualify. */
|
|
144
|
-
function closesGroup(command) {
|
|
145
|
-
if (command.tag === "functionDef")
|
|
146
|
-
return closesGroup(command.body);
|
|
147
|
-
const isCompound = command.tag === "if" ||
|
|
148
|
-
command.tag === "loop" ||
|
|
149
|
-
command.tag === "for" ||
|
|
150
|
-
command.tag === "case" ||
|
|
151
|
-
command.tag === "subshell" ||
|
|
152
|
-
command.tag === "group";
|
|
153
|
-
return isCompound && command.redirects.length === 0;
|
|
154
|
-
}
|
|
155
|
-
function endsAtCommandPosition({ list, endedWithSeparator }) {
|
|
156
|
-
if (endedWithSeparator)
|
|
157
|
-
return true;
|
|
158
|
-
const lastItem = list.items[list.items.length - 1];
|
|
159
|
-
const andOr = lastItem.command;
|
|
160
|
-
const lastPipeline = andOr.rest.length > 0 ? andOr.rest[andOr.rest.length - 1].pipeline : andOr.first;
|
|
161
|
-
return closesGroup(lastPipeline.commands[lastPipeline.commands.length - 1]);
|
|
162
|
-
}
|
|
163
|
-
// `}` is a reserved word, recognized only at command position: the group
|
|
164
|
-
// body must end with a separator or a bare compound command. Without this
|
|
165
|
-
// check, `{ echo a }` would fail OPEN (bash treats that `}` as an
|
|
166
|
-
// argument to echo and rejects the unterminated group).
|
|
167
|
-
const groupParser = (input) => {
|
|
168
|
-
const result = seq([groupOpen, list1WithEnd, groupClose], (results) => results[1])(input);
|
|
169
|
-
if (!result.success)
|
|
170
|
-
return result;
|
|
171
|
-
if (!endsAtCommandPosition(result.result)) {
|
|
172
|
-
return failure("expected ; & or newline before }", input);
|
|
173
|
-
}
|
|
174
|
-
return success({ tag: "group", body: result.result.list, redirects: [] }, result.rest);
|
|
175
|
-
};
|
|
176
|
-
const compoundParser = or(ifParser, loopParser, forParser, caseParser, arithmeticCommand, subshellParser, groupParser);
|
|
177
|
-
const compoundWithRedirects = seq([compoundParser, many(redirectParser)], (results) => (Object.assign(Object.assign({}, results[0]), { redirects: results[1] })));
|
|
178
|
-
const functionParens = seq([L.symbol("("), L.symbol(")")], () => "()");
|
|
179
|
-
// "Needs `function` or `()`" expressed as the two valid shapes. The body
|
|
180
|
-
// must be a compound command (bash rejects `f() echo hi` and nested
|
|
181
|
-
// definitions like `f() g() { :; }`); trailing redirects are allowed.
|
|
182
|
-
const functionDefParser = or(seq([L.keyword("function"), L.identifier, optional(functionParens), linebreak, compoundWithRedirects], (results) => ({
|
|
183
|
-
tag: "functionDef",
|
|
184
|
-
name: results[1],
|
|
185
|
-
body: results[4],
|
|
186
|
-
})), seq([L.identifier, functionParens, linebreak, compoundWithRedirects], (results) => ({
|
|
187
|
-
tag: "functionDef",
|
|
188
|
-
name: results[0],
|
|
189
|
-
body: results[3],
|
|
190
|
-
})));
|
|
191
|
-
const commandParserImpl = trace("bash:command", or(compoundWithRedirects, functionDefParser, simpleCommandParser));
|
|
192
|
-
/** Any single command: simple, compound (with trailing redirects), or a
|
|
193
|
-
* function definition. Declared as a function so the words/lists/commands
|
|
194
|
-
* module cycle is safe under any import order. */
|
|
195
|
-
export function commandParser(input) {
|
|
196
|
-
return commandParserImpl(input);
|
|
197
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Parser } from "../../types.js";
|
|
2
|
-
/** Words that are reserved at command position (and rejected as
|
|
3
|
-
* identifiers). Includes constructs this parser deliberately does not
|
|
4
|
-
* support — `select`, `coproc`, `time`, `[[ ]]` — so they fail the parse
|
|
5
|
-
* instead of parsing as ordinary words. */
|
|
6
|
-
export declare const WORD_RESERVED: string[];
|
|
7
|
-
export declare const bashLexemes: import("../../lexeme.js").Lexemes;
|
|
8
|
-
/** Characters that end a word. `{` and `}` are metacharacters here
|
|
9
|
-
* (unlike bash) because brace expansion is unsupported: a brace can only
|
|
10
|
-
* be a group delimiter or quoted. */
|
|
11
|
-
export declare const METACHARACTERS = " \t\n|&;()<>{}";
|
|
12
|
-
/** The next character (unconsumed) would end a word, or input is done. */
|
|
13
|
-
export declare const wordBoundary: Parser<string | null>;
|
|
14
|
-
/** A reserved word as a whole token. Order doesn't matter: each
|
|
15
|
-
* alternative carries its own boundary check, so `do` cannot steal the
|
|
16
|
-
* front of `done`. */
|
|
17
|
-
export declare const reservedToken: Parser<string>;
|
|
18
|
-
/** `{`, `}`, and `!` must be followed by whitespace. This is stricter
|
|
19
|
-
* than bash on purpose: `!(` is extglob syntax (unsupported), not
|
|
20
|
-
* negation. */
|
|
21
|
-
export declare const groupOpen: Parser<string>;
|
|
22
|
-
export declare const groupClose: Parser<string>;
|
|
23
|
-
export declare const bangToken: Parser<string>;
|
|
24
|
-
/** A significant newline, with trailing blanks and comments skipped. */
|
|
25
|
-
export declare const newlineToken: Parser<"\n">;
|
|
26
|
-
/** Zero or more blank (or comment-only) lines. */
|
|
27
|
-
export declare const linebreak: Parser<"\n"[]>;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/** Lexeme layer and shared tokens for the bash grammar. */
|
|
2
|
-
import { many, or, peek, seq } from "../../combinators.js";
|
|
3
|
-
import { makeLexemes } from "../../lexeme.js";
|
|
4
|
-
import { char, eof, oneOf, str } from "../../parsers.js";
|
|
5
|
-
/** Words that are reserved at command position (and rejected as
|
|
6
|
-
* identifiers). Includes constructs this parser deliberately does not
|
|
7
|
-
* support — `select`, `coproc`, `time`, `[[ ]]` — so they fail the parse
|
|
8
|
-
* instead of parsing as ordinary words. */
|
|
9
|
-
export const WORD_RESERVED = [
|
|
10
|
-
"if", "then", "elif", "else", "fi",
|
|
11
|
-
"do", "done", "while", "until", "for", "in",
|
|
12
|
-
"case", "esac", "function",
|
|
13
|
-
"select", "coproc", "time", "[[", "]]",
|
|
14
|
-
];
|
|
15
|
-
const RESERVED_TOKENS = [...WORD_RESERVED, "{", "}", "!"];
|
|
16
|
-
export const bashLexemes = makeLexemes({
|
|
17
|
-
// Newlines are command separators, never skippable whitespace.
|
|
18
|
-
whitespace: " \t",
|
|
19
|
-
lineComment: "#",
|
|
20
|
-
lineContinuation: true,
|
|
21
|
-
operatorChars: "&|;<>",
|
|
22
|
-
keywords: WORD_RESERVED,
|
|
23
|
-
});
|
|
24
|
-
/** Characters that end a word. `{` and `}` are metacharacters here
|
|
25
|
-
* (unlike bash) because brace expansion is unsupported: a brace can only
|
|
26
|
-
* be a group delimiter or quoted. */
|
|
27
|
-
export const METACHARACTERS = " \t\n|&;()<>{}";
|
|
28
|
-
/** The next character (unconsumed) would end a word, or input is done. */
|
|
29
|
-
export const wordBoundary = or(peek(oneOf(METACHARACTERS)), eof);
|
|
30
|
-
/** A reserved word as a whole token. Order doesn't matter: each
|
|
31
|
-
* alternative carries its own boundary check, so `do` cannot steal the
|
|
32
|
-
* front of `done`. */
|
|
33
|
-
export const reservedToken = or(...RESERVED_TOKENS.map((word) => seq([str(word), wordBoundary], (results) => results[0])));
|
|
34
|
-
const whitespaceOrEnd = or(peek(oneOf(" \t\n")), eof);
|
|
35
|
-
/** `{`, `}`, and `!` must be followed by whitespace. This is stricter
|
|
36
|
-
* than bash on purpose: `!(` is extglob syntax (unsupported), not
|
|
37
|
-
* negation. */
|
|
38
|
-
export const groupOpen = bashLexemes.lexeme(seq([str("{"), whitespaceOrEnd], () => "{"));
|
|
39
|
-
export const groupClose = bashLexemes.lexeme(seq([str("}"), wordBoundary], () => "}"));
|
|
40
|
-
export const bangToken = bashLexemes.lexeme(seq([str("!"), whitespaceOrEnd], () => "!"));
|
|
41
|
-
/** A significant newline, with trailing blanks and comments skipped. */
|
|
42
|
-
export const newlineToken = bashLexemes.lexeme(char("\n"));
|
|
43
|
-
/** Zero or more blank (or comment-only) lines. */
|
|
44
|
-
export const linebreak = many(newlineToken);
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { ParserResult } from "../../types.js";
|
|
2
|
-
import { List } from "./types.js";
|
|
3
|
-
/** A parsed list plus whether it ended with an explicit separator —
|
|
4
|
-
* group parsing needs this to decide whether `}` is at command position. */
|
|
5
|
-
export type ListWithEnd = {
|
|
6
|
-
list: List;
|
|
7
|
-
endedWithSeparator: boolean;
|
|
8
|
-
};
|
|
9
|
-
/** A possibly-empty command list. Empty lists are only legal where bash
|
|
10
|
-
* allows them: the top level, `$()`, and case-item bodies. Declared as a
|
|
11
|
-
* function so the words/lists/commands module cycle is safe under any
|
|
12
|
-
* import order. */
|
|
13
|
-
export declare function list0(input: string): ParserResult<List>;
|
|
14
|
-
/** A command list with at least one command — the body of every compound
|
|
15
|
-
* command, subshell, and group. */
|
|
16
|
-
export declare function list1(input: string): ParserResult<List>;
|
|
17
|
-
/** `list1`, but also reporting whether it ended with a separator. */
|
|
18
|
-
export declare function list1WithEnd(input: string): ParserResult<ListWithEnd>;
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/** Pipelines, `&&`/`||` chains, and command lists.
|
|
2
|
-
*
|
|
3
|
-
* The core fail-closed rule lives here: commands are SEPARATED by `;`,
|
|
4
|
-
* `&`, or newline. A construct that stops early leaves text no separator
|
|
5
|
-
* precedes, which fails the enclosing parse instead of becoming a
|
|
6
|
-
* phantom second command (the way `files=(a b c)` would otherwise parse
|
|
7
|
-
* as an assignment followed by a subshell). */
|
|
8
|
-
import { lazy, many, optional, or, seq } from "../../combinators.js";
|
|
9
|
-
import { trace } from "../../trace.js";
|
|
10
|
-
import { bangToken, bashLexemes, linebreak, newlineToken, } from "./lexemes.js";
|
|
11
|
-
import { commandParser as commandParserImport } from "./commands.js";
|
|
12
|
-
const L = bashLexemes;
|
|
13
|
-
// Deferred to parse time: see the module-cycle note in commands.ts.
|
|
14
|
-
const commandParser = lazy(() => commandParserImport);
|
|
15
|
-
const pipeOperator = seq([or(L.operator("|&"), L.operator("|")), linebreak], (results) => results[0]);
|
|
16
|
-
// Explicit seq/many instead of sepBy1: `echo hi |` must FAIL, not parse
|
|
17
|
-
// as `echo hi` with the `|` silently swallowed.
|
|
18
|
-
const pipelineParser = seq([
|
|
19
|
-
optional(bangToken),
|
|
20
|
-
commandParser,
|
|
21
|
-
many(seq([pipeOperator, commandParser], (results) => results[1])),
|
|
22
|
-
], (results) => ({
|
|
23
|
-
tag: "pipeline",
|
|
24
|
-
negated: results[0] !== null,
|
|
25
|
-
commands: [results[1], ...results[2]],
|
|
26
|
-
}));
|
|
27
|
-
const andOrTail = seq([or(L.operator("&&"), L.operator("||")), linebreak, pipelineParser], (results) => ({
|
|
28
|
-
op: results[0],
|
|
29
|
-
pipeline: results[2],
|
|
30
|
-
}));
|
|
31
|
-
const andOrParser = seq([pipelineParser, many(andOrTail)], (results) => ({
|
|
32
|
-
tag: "andOr",
|
|
33
|
-
first: results[0],
|
|
34
|
-
rest: results[1],
|
|
35
|
-
}));
|
|
36
|
-
const itemSeparator = or(seq([L.operator("&"), linebreak], () => ({ background: true })), seq([L.operator(";"), linebreak], () => ({ background: false })), seq([newlineToken, linebreak], () => ({ background: false })));
|
|
37
|
-
const listBody = seq([
|
|
38
|
-
andOrParser,
|
|
39
|
-
many(seq([itemSeparator, andOrParser], (results) => ({
|
|
40
|
-
separator: results[0],
|
|
41
|
-
command: results[1],
|
|
42
|
-
}))),
|
|
43
|
-
optional(itemSeparator),
|
|
44
|
-
], (results) => {
|
|
45
|
-
const pairs = results[1];
|
|
46
|
-
const trailing = results[2];
|
|
47
|
-
const commands = [results[0], ...pairs.map((pair) => pair.command)];
|
|
48
|
-
// Separator i follows command i and carries its `&` marker.
|
|
49
|
-
const separators = [
|
|
50
|
-
...pairs.map((pair) => pair.separator),
|
|
51
|
-
trailing,
|
|
52
|
-
];
|
|
53
|
-
const items = commands.map((command, index) => {
|
|
54
|
-
var _a;
|
|
55
|
-
return ({
|
|
56
|
-
tag: "listItem",
|
|
57
|
-
command,
|
|
58
|
-
background: ((_a = separators[index]) === null || _a === void 0 ? void 0 : _a.background) === true,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
return {
|
|
62
|
-
list: { tag: "list", items },
|
|
63
|
-
endedWithSeparator: trailing !== null,
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
const EMPTY_LIST = { tag: "list", items: [] };
|
|
67
|
-
const list0Impl = trace("bash:list0", seq([L.whitespace, linebreak, optional(listBody)], (results) => { var _a, _b; return (_b = (_a = results[2]) === null || _a === void 0 ? void 0 : _a.list) !== null && _b !== void 0 ? _b : EMPTY_LIST; }));
|
|
68
|
-
const list1Impl = trace("bash:list1", seq([L.whitespace, linebreak, listBody], (results) => results[2].list));
|
|
69
|
-
const list1WithEndImpl = seq([L.whitespace, linebreak, listBody], (results) => results[2]);
|
|
70
|
-
/** A possibly-empty command list. Empty lists are only legal where bash
|
|
71
|
-
* allows them: the top level, `$()`, and case-item bodies. Declared as a
|
|
72
|
-
* function so the words/lists/commands module cycle is safe under any
|
|
73
|
-
* import order. */
|
|
74
|
-
export function list0(input) {
|
|
75
|
-
return list0Impl(input);
|
|
76
|
-
}
|
|
77
|
-
/** A command list with at least one command — the body of every compound
|
|
78
|
-
* command, subshell, and group. */
|
|
79
|
-
export function list1(input) {
|
|
80
|
-
return list1Impl(input);
|
|
81
|
-
}
|
|
82
|
-
/** `list1`, but also reporting whether it ended with a separator. */
|
|
83
|
-
export function list1WithEnd(input) {
|
|
84
|
-
return list1WithEndImpl(input);
|
|
85
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Parser, ParserResult } from "../../types.js";
|
|
2
|
-
import { BashWord } from "./types.js";
|
|
3
|
-
/** Balanced parens, for `$(( ))` and `(( ))`. Sound because quotes are
|
|
4
|
-
* not delimiters in bash arithmetic. Declared as a function so the
|
|
5
|
-
* words/commands module cycle is safe under any import order. */
|
|
6
|
-
export declare function parenBody(input: string): ParserResult<string>;
|
|
7
|
-
/** A word: quoting and expansions handled, trailing whitespace eaten.
|
|
8
|
-
* Declared as a function so the words/lists/commands module cycle is
|
|
9
|
-
* safe under any import order. */
|
|
10
|
-
export declare function wordParser(input: string): ParserResult<BashWord>;
|
|
11
|
-
/** A word shaped like an assignment (`name=` / `name+=`). At command
|
|
12
|
-
* position this is never a plain command word: `name=` was already taken
|
|
13
|
-
* by the assignment parser, so what reaches here is unsupported syntax
|
|
14
|
-
* (append, arrays) and must fail rather than parse as a command. */
|
|
15
|
-
export declare const assignmentLookalike: Parser<string>;
|
|
16
|
-
/** A word at command position: rejects reserved words standing alone,
|
|
17
|
-
* which is how body lists stop at `fi`/`done`/`esac`. Quoted or extended
|
|
18
|
-
* words (`"fi"`, `fi.txt`) pass, matching bash. */
|
|
19
|
-
export declare function commandWord(input: string): ParserResult<BashWord>;
|
|
20
|
-
/** A run of identifier characters, shared with the assignment parser.
|
|
21
|
-
* Declared as a function for module-cycle safety. */
|
|
22
|
-
export declare function identifierRun(input: string): ParserResult<string>;
|