@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futpib/parser",
3
- "version": "1.0.3",
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
+ });