@futpib/parser 1.0.3 → 1.0.4
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/build/bash.d.ts +84 -0
- package/build/bash.js +1 -0
- package/build/bashParser.d.ts +6 -0
- package/build/bashParser.js +294 -0
- package/build/bashParser.test.d.ts +1 -0
- package/build/bashParser.test.js +181 -0
- package/build/index.d.ts +24 -2
- package/build/index.js +22 -1
- package/build/regexpParser.d.ts +2 -0
- package/build/regexpParser.js +71 -0
- package/build/regexpParser.test.d.ts +1 -0
- package/build/regexpParser.test.js +83 -0
- package/build/regularExpression.d.ts +63 -0
- package/build/regularExpression.js +1 -0
- package/build/regularExpressionParser.d.ts +3 -0
- package/build/regularExpressionParser.js +580 -0
- package/build/regularExpressionParser.test.d.ts +1 -0
- package/build/regularExpressionParser.test.js +89 -0
- package/package.json +2 -1
- package/src/bash.ts +120 -0
- package/src/bashParser.test.ts +332 -0
- package/src/bashParser.ts +461 -0
- package/src/index.ts +113 -2
- package/src/regexpParser.test.ts +186 -0
- package/src/regexpParser.ts +94 -0
- package/src/regularExpression.ts +24 -0
- package/src/regularExpressionParser.test.ts +102 -0
- package/src/regularExpressionParser.ts +921 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futpib/parser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@ava/typescript": "^6.0.0",
|
|
26
26
|
"@fast-check/ava": "^2.0.2",
|
|
27
|
+
"@gruhn/regex-utils": "^2.7.3",
|
|
27
28
|
"@types/invariant": "^2.2.37",
|
|
28
29
|
"@types/node": "^24.4.0",
|
|
29
30
|
"ava": "^6.4.1",
|
package/src/bash.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Word: a single argument/token (may contain expansions)
|
|
2
|
+
export type BashWord = {
|
|
3
|
+
parts: BashWordPart[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type BashWordPart =
|
|
7
|
+
| BashWordPartLiteral
|
|
8
|
+
| BashWordPartSingleQuoted
|
|
9
|
+
| BashWordPartDoubleQuoted
|
|
10
|
+
| BashWordPartVariable
|
|
11
|
+
| BashWordPartVariableBraced
|
|
12
|
+
| BashWordPartCommandSubstitution
|
|
13
|
+
| BashWordPartBacktickSubstitution
|
|
14
|
+
| BashWordPartArithmeticExpansion;
|
|
15
|
+
|
|
16
|
+
export type BashWordPartLiteral = {
|
|
17
|
+
type: 'literal';
|
|
18
|
+
value: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type BashWordPartSingleQuoted = {
|
|
22
|
+
type: 'singleQuoted';
|
|
23
|
+
value: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type BashWordPartDoubleQuoted = {
|
|
27
|
+
type: 'doubleQuoted';
|
|
28
|
+
parts: BashWordPart[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type BashWordPartVariable = {
|
|
32
|
+
type: 'variable';
|
|
33
|
+
name: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type BashWordPartVariableBraced = {
|
|
37
|
+
type: 'variableBraced';
|
|
38
|
+
name: string;
|
|
39
|
+
operator?: string;
|
|
40
|
+
operand?: BashWord;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type BashWordPartCommandSubstitution = {
|
|
44
|
+
type: 'commandSubstitution';
|
|
45
|
+
command: BashCommand;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type BashWordPartBacktickSubstitution = {
|
|
49
|
+
type: 'backtickSubstitution';
|
|
50
|
+
command: BashCommand;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type BashWordPartArithmeticExpansion = {
|
|
54
|
+
type: 'arithmeticExpansion';
|
|
55
|
+
expression: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Redirect: file descriptor operations
|
|
59
|
+
export type BashRedirect = {
|
|
60
|
+
fd?: number;
|
|
61
|
+
operator: '>' | '>>' | '<' | '<<' | '<<<' | '>&' | '<&' | '>|';
|
|
62
|
+
target: BashWord | BashHereDoc;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type BashHereDoc = {
|
|
66
|
+
type: 'hereDoc';
|
|
67
|
+
delimiter: string;
|
|
68
|
+
content: string;
|
|
69
|
+
quoted: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Assignment
|
|
73
|
+
export type BashAssignment = {
|
|
74
|
+
name: string;
|
|
75
|
+
value?: BashWord;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Simple command: name + args + redirects
|
|
79
|
+
export type BashSimpleCommand = {
|
|
80
|
+
type: 'simple';
|
|
81
|
+
name?: BashWord;
|
|
82
|
+
args: BashWord[];
|
|
83
|
+
redirects: BashRedirect[];
|
|
84
|
+
assignments: BashAssignment[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Compound commands (structural syntax only)
|
|
88
|
+
export type BashSubshell = {
|
|
89
|
+
type: 'subshell';
|
|
90
|
+
body: BashCommand;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type BashBraceGroup = {
|
|
94
|
+
type: 'braceGroup';
|
|
95
|
+
body: BashCommand;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type BashCommandUnit = BashSimpleCommand | BashSubshell | BashBraceGroup;
|
|
99
|
+
|
|
100
|
+
// Pipeline: cmd1 | cmd2 | cmd3
|
|
101
|
+
export type BashPipeline = {
|
|
102
|
+
type: 'pipeline';
|
|
103
|
+
negated: boolean;
|
|
104
|
+
commands: BashCommandUnit[];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type BashCommandListSeparator = '&&' | '||' | ';' | '&' | '\n';
|
|
108
|
+
|
|
109
|
+
// Command list: pipelines connected by && || ; &
|
|
110
|
+
export type BashCommandList = {
|
|
111
|
+
type: 'list';
|
|
112
|
+
entries: {
|
|
113
|
+
pipeline: BashPipeline;
|
|
114
|
+
separator?: BashCommandListSeparator;
|
|
115
|
+
}[];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Top-level
|
|
119
|
+
export type BashCommand = BashCommandList;
|
|
120
|
+
export type BashScript = BashCommand;
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { runParser, runParserWithRemainingInput } from './parser.js';
|
|
3
|
+
import { stringParserInputCompanion } from './parserInputCompanion.js';
|
|
4
|
+
import { bashScriptParser, bashWordParser, bashSimpleCommandParser } from './bashParser.js';
|
|
5
|
+
|
|
6
|
+
test('simple command parser - single word', async t => {
|
|
7
|
+
const result = await runParser(
|
|
8
|
+
bashSimpleCommandParser,
|
|
9
|
+
'cmd',
|
|
10
|
+
stringParserInputCompanion,
|
|
11
|
+
{ errorStack: true },
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
t.is(result.type, 'simple');
|
|
15
|
+
t.deepEqual(result.name, { parts: [{ type: 'literal', value: 'cmd' }] });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('simple command parser - two words', async t => {
|
|
19
|
+
const result = await runParser(
|
|
20
|
+
bashSimpleCommandParser,
|
|
21
|
+
'echo hello',
|
|
22
|
+
stringParserInputCompanion,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
t.is(result.type, 'simple');
|
|
26
|
+
t.deepEqual(result.name, { parts: [{ type: 'literal', value: 'echo' }] });
|
|
27
|
+
t.is(result.args.length, 1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('word parser - simple literal', async t => {
|
|
31
|
+
const result = await runParser(
|
|
32
|
+
bashWordParser,
|
|
33
|
+
'hello',
|
|
34
|
+
stringParserInputCompanion,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
t.deepEqual(result, {
|
|
38
|
+
parts: [{ type: 'literal', value: 'hello' }],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('word parser - variable', async t => {
|
|
43
|
+
const result = await runParser(
|
|
44
|
+
bashWordParser,
|
|
45
|
+
'$HOME',
|
|
46
|
+
stringParserInputCompanion,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
t.deepEqual(result, {
|
|
50
|
+
parts: [{ type: 'variable', name: 'HOME' }],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('simple command', async t => {
|
|
55
|
+
const result = await runParser(
|
|
56
|
+
bashScriptParser,
|
|
57
|
+
'echo hello',
|
|
58
|
+
stringParserInputCompanion,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
t.deepEqual(result, {
|
|
62
|
+
type: 'list',
|
|
63
|
+
entries: [{
|
|
64
|
+
pipeline: {
|
|
65
|
+
type: 'pipeline',
|
|
66
|
+
negated: false,
|
|
67
|
+
commands: [{
|
|
68
|
+
type: 'simple',
|
|
69
|
+
name: { parts: [{ type: 'literal', value: 'echo' }] },
|
|
70
|
+
args: [{ parts: [{ type: 'literal', value: 'hello' }] }],
|
|
71
|
+
redirects: [],
|
|
72
|
+
assignments: [],
|
|
73
|
+
}],
|
|
74
|
+
},
|
|
75
|
+
separator: undefined,
|
|
76
|
+
}],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('simple command with multiple args', async t => {
|
|
81
|
+
const result = await runParser(
|
|
82
|
+
bashScriptParser,
|
|
83
|
+
'echo hello world',
|
|
84
|
+
stringParserInputCompanion,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
t.is(result.entries[0].pipeline.commands[0].type, 'simple');
|
|
88
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
89
|
+
if (cmd.type === 'simple') {
|
|
90
|
+
t.is(cmd.args.length, 2);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('pipeline', async t => {
|
|
95
|
+
const result = await runParser(
|
|
96
|
+
bashScriptParser,
|
|
97
|
+
'cat file | grep pattern',
|
|
98
|
+
stringParserInputCompanion,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
t.is(result.entries[0].pipeline.commands.length, 2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('redirect output', async t => {
|
|
105
|
+
const result = await runParser(
|
|
106
|
+
bashScriptParser,
|
|
107
|
+
'echo foo > file',
|
|
108
|
+
stringParserInputCompanion,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
112
|
+
if (cmd.type === 'simple') {
|
|
113
|
+
t.is(cmd.redirects.length, 1);
|
|
114
|
+
t.is(cmd.redirects[0].operator, '>');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('redirect with fd', async t => {
|
|
119
|
+
const result = await runParser(
|
|
120
|
+
bashScriptParser,
|
|
121
|
+
'cmd 2>&1',
|
|
122
|
+
stringParserInputCompanion,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
126
|
+
if (cmd.type === 'simple') {
|
|
127
|
+
t.is(cmd.redirects.length, 1);
|
|
128
|
+
t.is(cmd.redirects[0].fd, 2);
|
|
129
|
+
t.is(cmd.redirects[0].operator, '>&');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('single quoted string', async t => {
|
|
134
|
+
const result = await runParser(
|
|
135
|
+
bashScriptParser,
|
|
136
|
+
"echo 'hello world'",
|
|
137
|
+
stringParserInputCompanion,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
141
|
+
if (cmd.type === 'simple') {
|
|
142
|
+
t.deepEqual(cmd.args[0], {
|
|
143
|
+
parts: [{ type: 'singleQuoted', value: 'hello world' }],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('double quoted string with variable', async t => {
|
|
149
|
+
const result = await runParser(
|
|
150
|
+
bashScriptParser,
|
|
151
|
+
'echo "hello $name"',
|
|
152
|
+
stringParserInputCompanion,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
156
|
+
if (cmd.type === 'simple') {
|
|
157
|
+
t.deepEqual(cmd.args[0], {
|
|
158
|
+
parts: [{
|
|
159
|
+
type: 'doubleQuoted',
|
|
160
|
+
parts: [
|
|
161
|
+
{ type: 'literal', value: 'hello ' },
|
|
162
|
+
{ type: 'variable', name: 'name' },
|
|
163
|
+
],
|
|
164
|
+
}],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('simple variable', async t => {
|
|
170
|
+
const result = await runParser(
|
|
171
|
+
bashScriptParser,
|
|
172
|
+
'echo $HOME',
|
|
173
|
+
stringParserInputCompanion,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
177
|
+
if (cmd.type === 'simple') {
|
|
178
|
+
t.deepEqual(cmd.args[0], {
|
|
179
|
+
parts: [{ type: 'variable', name: 'HOME' }],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('command substitution', async t => {
|
|
185
|
+
const result = await runParser(
|
|
186
|
+
bashScriptParser,
|
|
187
|
+
'echo $(pwd)',
|
|
188
|
+
stringParserInputCompanion,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
192
|
+
if (cmd.type === 'simple') {
|
|
193
|
+
t.is(cmd.args[0].parts[0].type, 'commandSubstitution');
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('backtick substitution', async t => {
|
|
198
|
+
const result = await runParser(
|
|
199
|
+
bashScriptParser,
|
|
200
|
+
'echo `pwd`',
|
|
201
|
+
stringParserInputCompanion,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
205
|
+
if (cmd.type === 'simple') {
|
|
206
|
+
t.is(cmd.args[0].parts[0].type, 'backtickSubstitution');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('subshell', async t => {
|
|
211
|
+
const result = await runParser(
|
|
212
|
+
bashScriptParser,
|
|
213
|
+
'(cd dir; pwd)',
|
|
214
|
+
stringParserInputCompanion,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
t.is(result.entries[0].pipeline.commands[0].type, 'subshell');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('brace group', async t => {
|
|
221
|
+
const result = await runParser(
|
|
222
|
+
bashScriptParser,
|
|
223
|
+
'{ echo hello; }',
|
|
224
|
+
stringParserInputCompanion,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
t.is(result.entries[0].pipeline.commands[0].type, 'braceGroup');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('command list with &&', async t => {
|
|
231
|
+
const result = await runParser(
|
|
232
|
+
bashScriptParser,
|
|
233
|
+
'cmd1 && cmd2',
|
|
234
|
+
stringParserInputCompanion,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
t.is(result.entries.length, 2);
|
|
238
|
+
t.is(result.entries[0].separator, '&&');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('command list with ||', async t => {
|
|
242
|
+
const result = await runParser(
|
|
243
|
+
bashScriptParser,
|
|
244
|
+
'cmd1 || cmd2',
|
|
245
|
+
stringParserInputCompanion,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
t.is(result.entries.length, 2);
|
|
249
|
+
t.is(result.entries[0].separator, '||');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('command list with ;', async t => {
|
|
253
|
+
const result = await runParser(
|
|
254
|
+
bashScriptParser,
|
|
255
|
+
'cmd1; cmd2',
|
|
256
|
+
stringParserInputCompanion,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
t.is(result.entries.length, 2);
|
|
260
|
+
t.is(result.entries[0].separator, ';');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('background command', async t => {
|
|
264
|
+
const result = await runParser(
|
|
265
|
+
bashScriptParser,
|
|
266
|
+
'cmd &',
|
|
267
|
+
stringParserInputCompanion,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
t.is(result.entries[0].separator, '&');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('assignment', async t => {
|
|
274
|
+
const result = await runParser(
|
|
275
|
+
bashScriptParser,
|
|
276
|
+
'VAR=value cmd',
|
|
277
|
+
stringParserInputCompanion,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
281
|
+
if (cmd.type === 'simple') {
|
|
282
|
+
t.is(cmd.assignments.length, 1);
|
|
283
|
+
t.is(cmd.assignments[0].name, 'VAR');
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('negated pipeline', async t => {
|
|
288
|
+
const result = await runParser(
|
|
289
|
+
bashScriptParser,
|
|
290
|
+
'! cmd',
|
|
291
|
+
stringParserInputCompanion,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
t.is(result.entries[0].pipeline.negated, true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('complex pipeline with redirects', async t => {
|
|
298
|
+
const result = await runParser(
|
|
299
|
+
bashScriptParser,
|
|
300
|
+
'cat file 2>/dev/null | grep pattern | sort > output',
|
|
301
|
+
stringParserInputCompanion,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
t.is(result.entries[0].pipeline.commands.length, 3);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('[[ treated as command name', async t => {
|
|
308
|
+
const result = await runParser(
|
|
309
|
+
bashScriptParser,
|
|
310
|
+
'[[ -f file ]]',
|
|
311
|
+
stringParserInputCompanion,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
315
|
+
if (cmd.type === 'simple') {
|
|
316
|
+
t.deepEqual(cmd.name, { parts: [{ type: 'literal', value: '[[' }] });
|
|
317
|
+
t.is(cmd.args.length, 3); // -f, file, ]]
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('if treated as command name', async t => {
|
|
322
|
+
const result = await runParser(
|
|
323
|
+
bashScriptParser,
|
|
324
|
+
'if true',
|
|
325
|
+
stringParserInputCompanion,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const cmd = result.entries[0].pipeline.commands[0];
|
|
329
|
+
if (cmd.type === 'simple') {
|
|
330
|
+
t.deepEqual(cmd.name, { parts: [{ type: 'literal', value: 'if' }] });
|
|
331
|
+
}
|
|
332
|
+
});
|