shell-dsl 0.0.35 → 0.0.36
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/README.md +25 -2
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/index.cjs +2 -7
- package/dist/cjs/src/index.cjs.map +3 -3
- package/dist/cjs/src/interpreter/interpreter.cjs +186 -68
- package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
- package/dist/cjs/src/parser/ast.cjs +5 -25
- package/dist/cjs/src/parser/ast.cjs.map +3 -3
- package/dist/cjs/src/parser/index.cjs +2 -7
- package/dist/cjs/src/parser/index.cjs.map +3 -3
- package/dist/cjs/src/parser/parser.cjs +132 -82
- package/dist/cjs/src/parser/parser.cjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/src/index.mjs +4 -14
- package/dist/mjs/src/index.mjs.map +3 -3
- package/dist/mjs/src/interpreter/interpreter.mjs +186 -68
- package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
- package/dist/mjs/src/parser/ast.mjs +5 -25
- package/dist/mjs/src/parser/ast.mjs.map +3 -3
- package/dist/mjs/src/parser/index.mjs +4 -14
- package/dist/mjs/src/parser/index.mjs.map +3 -3
- package/dist/mjs/src/parser/parser.mjs +132 -82
- package/dist/mjs/src/parser/parser.mjs.map +3 -3
- package/dist/types/src/index.d.ts +2 -2
- package/dist/types/src/interpreter/interpreter.d.ts +15 -1
- package/dist/types/src/parser/ast.d.ts +34 -38
- package/dist/types/src/parser/index.d.ts +2 -2
- package/dist/types/src/parser/parser.d.ts +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -257,7 +257,10 @@ await sh`FOO=bar && echo $FOO`.text(); // "bar\n"
|
|
|
257
257
|
Assign variables for a single command (scoped):
|
|
258
258
|
|
|
259
259
|
```ts
|
|
260
|
-
await sh`FOO=bar echo
|
|
260
|
+
await sh`FOO=bar echo ok`.text(); // "ok\n"
|
|
261
|
+
await sh`FOO=bar echo $FOO`.text(); // "\n"
|
|
262
|
+
// FOO is available to the executed command via ctx.env,
|
|
263
|
+
// but sibling shell expansion still uses the previous shell env
|
|
261
264
|
// FOO is not set after this command
|
|
262
265
|
```
|
|
263
266
|
|
|
@@ -326,15 +329,35 @@ const myls: Command = async (ctx) => {
|
|
|
326
329
|
};
|
|
327
330
|
```
|
|
328
331
|
|
|
332
|
+
## Field Splitting
|
|
333
|
+
|
|
334
|
+
Unquoted parameter, command, and arithmetic expansions follow shell-style word expansion:
|
|
335
|
+
|
|
336
|
+
1. Expand variables / `$(...)` / `$((...))`
|
|
337
|
+
2. Split unquoted results using `IFS` (default: space, tab, newline)
|
|
338
|
+
3. Apply pathname expansion (globbing) to the resulting fields
|
|
339
|
+
|
|
340
|
+
Quoted expansions stay single-field, and assignment values plus redirect targets use scalar expansion only.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
await sh`LIST="alpha beta" && for item in $LIST; do echo $item; done`.text();
|
|
344
|
+
// "alpha\nbeta\n"
|
|
345
|
+
|
|
346
|
+
await sh`LIST="alpha,beta,,gamma" && IFS=, && for item in $LIST; do echo "[$item]"; done`.text();
|
|
347
|
+
// "[alpha]\n[beta]\n[]\n[gamma]\n"
|
|
348
|
+
```
|
|
349
|
+
|
|
329
350
|
## Glob Expansion
|
|
330
351
|
|
|
331
|
-
Globs
|
|
352
|
+
Globs run after field splitting, and only wildcard characters from unquoted text or unquoted expansions participate:
|
|
332
353
|
|
|
333
354
|
```ts
|
|
334
355
|
await sh`ls *.txt`; // Matches: a.txt, b.txt, ...
|
|
335
356
|
await sh`cat src/**/*.ts`; // Recursive glob
|
|
336
357
|
await sh`echo file[123].txt`; // Character classes
|
|
337
358
|
await sh`echo {a,b,c}.txt`; // Brace expansion: a.txt b.txt c.txt
|
|
359
|
+
await sh`pattern='*.txt'; echo $pattern`; // Expands matches
|
|
360
|
+
await sh`pattern='*.txt'; echo "$pattern"`; // Literal "*.txt"
|
|
338
361
|
```
|
|
339
362
|
|
|
340
363
|
## Command Substitution
|
package/dist/cjs/package.json
CHANGED
package/dist/cjs/src/index.cjs
CHANGED
|
@@ -42,22 +42,17 @@ __export(exports_src, {
|
|
|
42
42
|
tokenToString: () => import_lexer.tokenToString,
|
|
43
43
|
parse: () => import_parser.parse,
|
|
44
44
|
lex: () => import_lexer.lex,
|
|
45
|
+
isWordNode: () => import_parser2.isWordNode,
|
|
45
46
|
isWhileNode: () => import_parser2.isWhileNode,
|
|
46
|
-
isVariableNode: () => import_parser2.isVariableNode,
|
|
47
47
|
isUntilNode: () => import_parser2.isUntilNode,
|
|
48
|
-
isSubstitutionNode: () => import_parser2.isSubstitutionNode,
|
|
49
48
|
isSequenceNode: () => import_parser2.isSequenceNode,
|
|
50
49
|
isRawValue: () => import_types.isRawValue,
|
|
51
50
|
isPipelineNode: () => import_parser2.isPipelineNode,
|
|
52
51
|
isOrNode: () => import_parser2.isOrNode,
|
|
53
|
-
isLiteralNode: () => import_parser2.isLiteralNode,
|
|
54
52
|
isIfNode: () => import_parser2.isIfNode,
|
|
55
|
-
isGlobNode: () => import_parser2.isGlobNode,
|
|
56
53
|
isForNode: () => import_parser2.isForNode,
|
|
57
|
-
isConcatNode: () => import_parser2.isConcatNode,
|
|
58
54
|
isCommandNode: () => import_parser2.isCommandNode,
|
|
59
55
|
isCaseNode: () => import_parser2.isCaseNode,
|
|
60
|
-
isArithmeticNode: () => import_parser2.isArithmeticNode,
|
|
61
56
|
isAndNode: () => import_parser2.isAndNode,
|
|
62
57
|
globVirtualFS: () => import_utils.globVirtualFS,
|
|
63
58
|
escapeForInterpolation: () => import_utils.escapeForInterpolation,
|
|
@@ -103,4 +98,4 @@ var import_io2 = require("./io/index.cjs");
|
|
|
103
98
|
var import_utils = require("./utils/index.cjs");
|
|
104
99
|
var import_vcs = require("./vcs/index.cjs");
|
|
105
100
|
|
|
106
|
-
//# debugId=
|
|
101
|
+
//# debugId=188D4F3252D1BB8C64756E2164756E21
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.cjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.cjs\";\n\n// Types\nexport type {\n VirtualFS,\n VirtualFSWritable,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n RawValue,\n} from \"./types.cjs\";\nexport { isRawValue } from \"./types.cjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.cjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.cjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.cjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.cjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n
|
|
5
|
+
"// Main class exports\nexport { ShellDSL, createShellDSL, type Program } from \"./shell-dsl.cjs\";\nexport { ShellPromise, type ShellPromiseOptions } from \"./shell-promise.cjs\";\n\n// Types\nexport type {\n VirtualFS,\n VirtualFSWritable,\n FileStat,\n Command,\n CommandContext,\n Stdin,\n Stdout,\n Stderr,\n OutputCollector,\n ExecResult,\n ShellConfig,\n RawValue,\n} from \"./types.cjs\";\nexport { isRawValue } from \"./types.cjs\";\n\n// Errors\nexport { ShellError, LexError, ParseError } from \"./errors.cjs\";\n\n// Lexer\nexport { Lexer, lex, tokenToString } from \"./lexer/index.cjs\";\nexport type { Token, RedirectMode } from \"./lexer/index.cjs\";\n\n// Parser\nexport { Parser, parse } from \"./parser/index.cjs\";\nexport type {\n ASTNode,\n Redirect,\n CommandNode,\n PipelineNode,\n AndNode,\n OrNode,\n SequenceNode,\n WordNode,\n WordPart,\n TextPart,\n VariablePart,\n SubstitutionPart,\n ArithmeticPart,\n IfNode,\n ForNode,\n WhileNode,\n UntilNode,\n CaseNode,\n CaseClause,\n} from \"./parser/index.cjs\";\nexport {\n isWordNode,\n isCommandNode,\n isPipelineNode,\n isAndNode,\n isOrNode,\n isSequenceNode,\n isIfNode,\n isForNode,\n isWhileNode,\n isUntilNode,\n isCaseNode,\n} from \"./parser/index.cjs\";\n\n// Interpreter\nexport { Interpreter, type InterpreterOptions, BreakException, ContinueException } from \"./interpreter/index.cjs\";\n\n// Filesystem\nexport { createVirtualFS } from \"./fs/index.cjs\";\nexport {\n FileSystem,\n ReadOnlyFileSystem,\n WebFileSystem,\n createWebUnderlyingFS,\n type PathOps,\n type Permission,\n type PermissionRules,\n type UnderlyingFS,\n} from \"./fs/index.cjs\";\n\n// I/O\nexport { createStdin, StdinImpl } from \"./io/index.cjs\";\nexport { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from \"./io/index.cjs\";\n\n// Utilities\nexport { escape, escapeForInterpolation, globVirtualFS } from \"./utils/index.cjs\";\nexport type { GlobVirtualFS, GlobOptions } from \"./utils/index.cjs\";\n\n// Version Control\nexport { VersionControlSystem } from \"./vcs/index.cjs\";\nexport type {\n VCSConfig,\n VCSAttributeRule,\n VCSResolvedAttributes,\n VCSDiffMode,\n VCSPatchSuppressionReason,\n Revision,\n DiffEntry,\n TreeManifest,\n TreeEntry,\n FileEntry,\n DirectoryEntry,\n VCSIndexEntry,\n VCSIndexFile,\n CommitOptions,\n CheckoutOptions,\n LogOptions,\n LogEntry,\n BranchInfo,\n} from \"./vcs/index.cjs\";\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACuD,IAAvD;AACuD,IAAvD;AAiB2B,IAA3B;AAGiD,IAAjD;AAG0C,IAA1C;AAI8B,IAA9B;AAkCO,IAZP;AAewF,IAAxF;AAGgC,IAAhC;AAUO,IATP;AAYuC,IAAvC;AACwF,IAAxF;AAG8D,IAA9D;AAIqC,IAArC;",
|
|
8
|
+
"debugId": "188D4F3252D1BB8C64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -48,6 +48,9 @@ var import_context = require("./context.cjs");
|
|
|
48
48
|
var import_stdin = require("../io/stdin.cjs");
|
|
49
49
|
var import_stdout = require("../io/stdout.cjs");
|
|
50
50
|
var import_special_files = require("../fs/special-files.cjs");
|
|
51
|
+
var DEFAULT_IFS = `
|
|
52
|
+
`;
|
|
53
|
+
var GLOB_META_CHARS = /[*?[{]/;
|
|
51
54
|
|
|
52
55
|
class BreakException extends Error {
|
|
53
56
|
levels;
|
|
@@ -119,34 +122,23 @@ class Interpreter {
|
|
|
119
122
|
case "case":
|
|
120
123
|
return this.executeCase(node, stdinSource, stdout, stderr);
|
|
121
124
|
default:
|
|
122
|
-
throw new Error(
|
|
125
|
+
throw new Error("Cannot execute unknown node type");
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
async executeCommand(node, stdinSource, stdout, stderr) {
|
|
126
|
-
const
|
|
129
|
+
const assignmentEnv = { ...this.env };
|
|
127
130
|
for (const assignment of node.assignments) {
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
const args = [];
|
|
138
|
-
for (const arg of node.args) {
|
|
139
|
-
const evaluated = await this.evaluateNode(arg, localEnv);
|
|
140
|
-
if (arg.type === "glob") {
|
|
141
|
-
const matches = await this.fs.glob(evaluated, { cwd: this.cwd });
|
|
142
|
-
if (matches.length > 0) {
|
|
143
|
-
args.push(...matches);
|
|
144
|
-
} else {
|
|
145
|
-
args.push(evaluated);
|
|
131
|
+
assignmentEnv[assignment.name] = await this.expandWordScalar(assignment.value, assignmentEnv);
|
|
132
|
+
}
|
|
133
|
+
const expandedWords = await this.expandCommandWords(node, this.env);
|
|
134
|
+
const [name, ...args] = expandedWords;
|
|
135
|
+
if (name === undefined || name === "") {
|
|
136
|
+
if (node.assignments.length > 0) {
|
|
137
|
+
for (const assignment of node.assignments) {
|
|
138
|
+
this.env[assignment.name] = assignmentEnv[assignment.name] ?? "";
|
|
146
139
|
}
|
|
147
|
-
} else {
|
|
148
|
-
args.push(evaluated);
|
|
149
140
|
}
|
|
141
|
+
return 0;
|
|
150
142
|
}
|
|
151
143
|
let actualStdin = stdinSource;
|
|
152
144
|
let actualStdout = stdout;
|
|
@@ -156,7 +148,7 @@ class Interpreter {
|
|
|
156
148
|
const fileWritePromises = [];
|
|
157
149
|
for (const redirect of node.redirects) {
|
|
158
150
|
try {
|
|
159
|
-
const result = await this.handleRedirect(redirect, actualStdin, actualStdout, actualStderr);
|
|
151
|
+
const result = await this.handleRedirect(redirect, actualStdin, actualStdout, actualStderr, this.env);
|
|
160
152
|
actualStdin = result.stdin;
|
|
161
153
|
actualStdout = result.stdout;
|
|
162
154
|
actualStderr = result.stderr;
|
|
@@ -166,7 +158,7 @@ class Interpreter {
|
|
|
166
158
|
fileWritePromises.push(result.fileWritePromise);
|
|
167
159
|
}
|
|
168
160
|
} catch (err) {
|
|
169
|
-
const target = await this.
|
|
161
|
+
const target = await this.expandWordScalar(redirect.target, this.env);
|
|
170
162
|
const message = err instanceof Error ? err.message : String(err);
|
|
171
163
|
await stderr.writeText(`sh: ${target}: ${message}
|
|
172
164
|
`);
|
|
@@ -204,7 +196,7 @@ class Interpreter {
|
|
|
204
196
|
stderr: subStderr,
|
|
205
197
|
fs: this.fs,
|
|
206
198
|
cwd: this.cwd,
|
|
207
|
-
env: { ...
|
|
199
|
+
env: { ...assignmentEnv },
|
|
208
200
|
setCwd: (path) => this.setCwd(path),
|
|
209
201
|
exec
|
|
210
202
|
});
|
|
@@ -235,7 +227,7 @@ class Interpreter {
|
|
|
235
227
|
stderr: actualStderr,
|
|
236
228
|
fs: this.fs,
|
|
237
229
|
cwd: this.cwd,
|
|
238
|
-
env:
|
|
230
|
+
env: assignmentEnv,
|
|
239
231
|
setCwd: (path) => this.setCwd(path),
|
|
240
232
|
exec
|
|
241
233
|
});
|
|
@@ -262,15 +254,15 @@ class Interpreter {
|
|
|
262
254
|
} catch (err) {
|
|
263
255
|
const message = err instanceof Error ? err.message : String(err);
|
|
264
256
|
const writeRedirects = node.redirects.filter((r) => r.mode !== "<" && r.mode !== "2>&1" && r.mode !== "1>&2");
|
|
265
|
-
const target = writeRedirects.length > 0 ? await this.
|
|
257
|
+
const target = writeRedirects.length > 0 ? await this.expandWordScalar(writeRedirects[writeRedirects.length - 1].target, this.env) : "unknown";
|
|
266
258
|
await stderr.writeText(`sh: ${target}: ${message}
|
|
267
259
|
`);
|
|
268
260
|
exitCode = 1;
|
|
269
261
|
}
|
|
270
262
|
return exitCode;
|
|
271
263
|
}
|
|
272
|
-
async handleRedirect(redirect, stdin, stdout, stderr) {
|
|
273
|
-
const target = await this.
|
|
264
|
+
async handleRedirect(redirect, stdin, stdout, stderr, env) {
|
|
265
|
+
const target = await this.expandWordScalar(redirect.target, env);
|
|
274
266
|
if (target in this.redirectObjects) {
|
|
275
267
|
return this.handleObjectRedirect(redirect.mode, this.redirectObjects[target], stdin, stdout, stderr);
|
|
276
268
|
}
|
|
@@ -502,17 +494,7 @@ class Interpreter {
|
|
|
502
494
|
async executeFor(node, stdinSource, stdout, stderr) {
|
|
503
495
|
const expandedItems = [];
|
|
504
496
|
for (const item of node.items) {
|
|
505
|
-
|
|
506
|
-
if (item.type === "glob") {
|
|
507
|
-
const matches = await this.fs.glob(evaluated, { cwd: this.cwd });
|
|
508
|
-
if (matches.length > 0) {
|
|
509
|
-
expandedItems.push(...matches);
|
|
510
|
-
} else {
|
|
511
|
-
expandedItems.push(evaluated);
|
|
512
|
-
}
|
|
513
|
-
} else {
|
|
514
|
-
expandedItems.push(evaluated);
|
|
515
|
-
}
|
|
497
|
+
expandedItems.push(...await this.expandWordForCommand(item, this.env));
|
|
516
498
|
}
|
|
517
499
|
if (expandedItems.length === 0) {
|
|
518
500
|
return 0;
|
|
@@ -616,10 +598,10 @@ class Interpreter {
|
|
|
616
598
|
return lastExitCode;
|
|
617
599
|
}
|
|
618
600
|
async executeCase(node, stdinSource, stdout, stderr) {
|
|
619
|
-
const word = await this.
|
|
601
|
+
const word = await this.expandWordScalar(node.word, this.env);
|
|
620
602
|
for (const clause of node.clauses) {
|
|
621
603
|
for (const patternNode of clause.patterns) {
|
|
622
|
-
const pattern = await this.
|
|
604
|
+
const pattern = await this.expandWordScalar(patternNode, this.env);
|
|
623
605
|
if (this.matchCasePattern(word, pattern)) {
|
|
624
606
|
return this.executeNode(clause.body, stdinSource, stdout, stderr);
|
|
625
607
|
}
|
|
@@ -660,34 +642,170 @@ class Interpreter {
|
|
|
660
642
|
return word === pattern;
|
|
661
643
|
}
|
|
662
644
|
}
|
|
663
|
-
async
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
645
|
+
async expandCommandWords(node, env) {
|
|
646
|
+
const expanded = [];
|
|
647
|
+
for (const word of [node.name, ...node.args]) {
|
|
648
|
+
expanded.push(...await this.expandWordForCommand(word, env));
|
|
649
|
+
}
|
|
650
|
+
return expanded;
|
|
651
|
+
}
|
|
652
|
+
async expandWordForCommand(word, env) {
|
|
653
|
+
const fields = await this.expandWordFields(word, env);
|
|
654
|
+
const expanded = [];
|
|
655
|
+
for (const field of fields) {
|
|
656
|
+
expanded.push(...await this.expandPathname(field));
|
|
657
|
+
}
|
|
658
|
+
return expanded;
|
|
659
|
+
}
|
|
660
|
+
async expandWordScalar(word, env) {
|
|
661
|
+
let result = "";
|
|
662
|
+
for (const part of word.parts) {
|
|
663
|
+
result += await this.expandWordPart(part, env);
|
|
664
|
+
}
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
async expandWordFields(word, env) {
|
|
668
|
+
const fields = [this.createExpandedField()];
|
|
669
|
+
const ifs = this.getIFS(env);
|
|
670
|
+
for (const part of word.parts) {
|
|
671
|
+
if (part.type === "text") {
|
|
672
|
+
this.appendSegment(fields[fields.length - 1], part.value, part.quoted);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const value = await this.expandWordPart(part, env);
|
|
676
|
+
if (part.quoted) {
|
|
677
|
+
this.appendSegment(fields[fields.length - 1], value, true);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const splitFields = this.splitUnquotedExpansion(value, ifs);
|
|
681
|
+
if (splitFields.length === 0) {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
this.appendSegment(fields[fields.length - 1], splitFields[0], false);
|
|
685
|
+
for (let i = 1;i < splitFields.length; i++) {
|
|
686
|
+
const field = this.createExpandedField();
|
|
687
|
+
this.appendSegment(field, splitFields[i], false);
|
|
688
|
+
fields.push(field);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return fields.filter((field) => field.segments.length > 0);
|
|
692
|
+
}
|
|
693
|
+
async expandWordPart(part, env) {
|
|
694
|
+
switch (part.type) {
|
|
695
|
+
case "text":
|
|
696
|
+
return part.value;
|
|
668
697
|
case "variable":
|
|
669
|
-
return env[
|
|
670
|
-
case "
|
|
671
|
-
return
|
|
672
|
-
case "
|
|
673
|
-
|
|
674
|
-
return parts.join("");
|
|
675
|
-
}
|
|
676
|
-
case "substitution": {
|
|
677
|
-
const subStdout = import_stdout.createStdout();
|
|
678
|
-
const subStderr = import_stdout.createStderr();
|
|
679
|
-
await this.executeNode(node.command, null, subStdout, subStderr);
|
|
680
|
-
subStdout.close();
|
|
681
|
-
const output = await subStdout.collect();
|
|
682
|
-
return output.toString("utf-8").replace(/\n+$/, "");
|
|
683
|
-
}
|
|
684
|
-
case "arithmetic": {
|
|
685
|
-
const result = this.evaluateArithmetic(node.expression, env);
|
|
686
|
-
return String(result);
|
|
687
|
-
}
|
|
698
|
+
return env[part.name] ?? "";
|
|
699
|
+
case "substitution":
|
|
700
|
+
return this.executeSubstitution(part.command, env);
|
|
701
|
+
case "arithmetic":
|
|
702
|
+
return String(this.evaluateArithmetic(part.expression, env));
|
|
688
703
|
default:
|
|
689
|
-
throw new Error(
|
|
704
|
+
throw new Error("Cannot expand unknown word part");
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async executeSubstitution(command, env) {
|
|
708
|
+
const interpreter = new Interpreter({
|
|
709
|
+
fs: this.fs,
|
|
710
|
+
cwd: this.cwd,
|
|
711
|
+
env,
|
|
712
|
+
commands: this.commands,
|
|
713
|
+
redirectObjects: this.redirectObjects,
|
|
714
|
+
isTTY: false
|
|
715
|
+
});
|
|
716
|
+
const result = await interpreter.execute(command);
|
|
717
|
+
return result.stdout.toString("utf-8").replace(/\n+$/, "");
|
|
718
|
+
}
|
|
719
|
+
getIFS(env) {
|
|
720
|
+
return env.IFS ?? DEFAULT_IFS;
|
|
721
|
+
}
|
|
722
|
+
splitUnquotedExpansion(value, ifs) {
|
|
723
|
+
if (value.length === 0) {
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
if (ifs === "") {
|
|
727
|
+
return [value];
|
|
690
728
|
}
|
|
729
|
+
const ifsChars = new Set(ifs);
|
|
730
|
+
const isIfsWhitespace = (char) => ifsChars.has(char) && (char === " " || char === "\t" || char === `
|
|
731
|
+
`);
|
|
732
|
+
const isIfsNonWhitespace = (char) => ifsChars.has(char) && !isIfsWhitespace(char);
|
|
733
|
+
const fields = [];
|
|
734
|
+
let i = 0;
|
|
735
|
+
while (i < value.length && isIfsWhitespace(value[i])) {
|
|
736
|
+
i++;
|
|
737
|
+
}
|
|
738
|
+
let fieldStart = i;
|
|
739
|
+
let lastDelimiterWasNonWhitespace = false;
|
|
740
|
+
while (i < value.length) {
|
|
741
|
+
const char = value[i];
|
|
742
|
+
if (isIfsNonWhitespace(char)) {
|
|
743
|
+
fields.push(value.slice(fieldStart, i));
|
|
744
|
+
i++;
|
|
745
|
+
while (i < value.length && isIfsWhitespace(value[i])) {
|
|
746
|
+
i++;
|
|
747
|
+
}
|
|
748
|
+
fieldStart = i;
|
|
749
|
+
lastDelimiterWasNonWhitespace = true;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (isIfsWhitespace(char)) {
|
|
753
|
+
fields.push(value.slice(fieldStart, i));
|
|
754
|
+
while (i < value.length && isIfsWhitespace(value[i])) {
|
|
755
|
+
i++;
|
|
756
|
+
}
|
|
757
|
+
if (i < value.length && isIfsNonWhitespace(value[i])) {
|
|
758
|
+
i++;
|
|
759
|
+
while (i < value.length && isIfsWhitespace(value[i])) {
|
|
760
|
+
i++;
|
|
761
|
+
}
|
|
762
|
+
lastDelimiterWasNonWhitespace = true;
|
|
763
|
+
} else {
|
|
764
|
+
lastDelimiterWasNonWhitespace = false;
|
|
765
|
+
}
|
|
766
|
+
fieldStart = i;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
lastDelimiterWasNonWhitespace = false;
|
|
770
|
+
i++;
|
|
771
|
+
}
|
|
772
|
+
if (fieldStart < value.length) {
|
|
773
|
+
fields.push(value.slice(fieldStart));
|
|
774
|
+
} else if (lastDelimiterWasNonWhitespace) {
|
|
775
|
+
fields.push("");
|
|
776
|
+
}
|
|
777
|
+
return fields;
|
|
778
|
+
}
|
|
779
|
+
createExpandedField() {
|
|
780
|
+
return { segments: [] };
|
|
781
|
+
}
|
|
782
|
+
appendSegment(field, value, quoted) {
|
|
783
|
+
const lastSegment = field.segments[field.segments.length - 1];
|
|
784
|
+
if (lastSegment && lastSegment.quoted === quoted) {
|
|
785
|
+
lastSegment.value += value;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
field.segments.push({ value, quoted });
|
|
789
|
+
}
|
|
790
|
+
async expandPathname(field) {
|
|
791
|
+
if (!this.hasUnquotedGlobMeta(field)) {
|
|
792
|
+
return [this.fieldToString(field)];
|
|
793
|
+
}
|
|
794
|
+
const pattern = this.fieldToGlobPattern(field);
|
|
795
|
+
const matches = await this.fs.glob(pattern, { cwd: this.cwd });
|
|
796
|
+
return matches.length > 0 ? matches : [this.fieldToString(field)];
|
|
797
|
+
}
|
|
798
|
+
hasUnquotedGlobMeta(field) {
|
|
799
|
+
return field.segments.some((segment) => !segment.quoted && GLOB_META_CHARS.test(segment.value));
|
|
800
|
+
}
|
|
801
|
+
fieldToString(field) {
|
|
802
|
+
return field.segments.map((segment) => segment.value).join("");
|
|
803
|
+
}
|
|
804
|
+
fieldToGlobPattern(field) {
|
|
805
|
+
return field.segments.map((segment) => segment.quoted ? this.escapeLiteralGlobChars(segment.value) : segment.value).join("");
|
|
806
|
+
}
|
|
807
|
+
escapeLiteralGlobChars(value) {
|
|
808
|
+
return value.replaceAll("[", "[[]").replaceAll("]", "[]]").replaceAll("*", "[*]").replaceAll("?", "[?]").replaceAll("{", "[{]").replaceAll("}", "[}]");
|
|
691
809
|
}
|
|
692
810
|
evaluateArithmetic(expression, env) {
|
|
693
811
|
let expandedExpr = expression;
|
|
@@ -867,4 +985,4 @@ class Interpreter {
|
|
|
867
985
|
}
|
|
868
986
|
}
|
|
869
987
|
|
|
870
|
-
//# debugId=
|
|
988
|
+
//# debugId=1238E19B5ED5404064756E2164756E21
|