@poe-platform/safe-bash 0.1.145 → 0.1.147
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 +1 -1
- package/dist/safe-bash/browser.d.ts +4 -0
- package/dist/safe-bash/browser.d.ts.map +1 -1
- package/dist/safe-bash/browser.js +4046 -2910
- package/dist/safe-bash/browser.js.map +4 -4
- package/dist/safe-bash/commands/basic.d.ts +3 -1
- package/dist/safe-bash/commands/basic.d.ts.map +1 -1
- package/dist/safe-bash/commands/basic.js +143 -141
- package/dist/safe-bash/commands/basic.js.map +1 -1
- package/dist/safe-bash/commands/execution.d.ts +4 -1
- package/dist/safe-bash/commands/execution.d.ts.map +1 -1
- package/dist/safe-bash/commands/execution.js +134 -32
- package/dist/safe-bash/commands/execution.js.map +1 -1
- package/dist/safe-bash/commands/filesystem.d.ts.map +1 -1
- package/dist/safe-bash/commands/filesystem.js +161 -39
- package/dist/safe-bash/commands/filesystem.js.map +1 -1
- package/dist/safe-bash/commands/find.d.ts.map +1 -1
- package/dist/safe-bash/commands/find.js +83 -3
- package/dist/safe-bash/commands/find.js.map +1 -1
- package/dist/safe-bash/commands/index.d.ts +4 -0
- package/dist/safe-bash/commands/index.d.ts.map +1 -1
- package/dist/safe-bash/commands/index.js +1 -1
- package/dist/safe-bash/commands/index.js.map +1 -1
- package/dist/safe-bash/commands/regex-execution/ere/matcher.d.ts.map +1 -1
- package/dist/safe-bash/commands/regex-execution/ere/matcher.js +35 -15
- package/dist/safe-bash/commands/regex-execution/ere/matcher.js.map +1 -1
- package/dist/safe-bash/commands/search/file-types.d.ts +2 -0
- package/dist/safe-bash/commands/search/file-types.d.ts.map +1 -0
- package/dist/safe-bash/commands/search/file-types.js +229 -0
- package/dist/safe-bash/commands/search/file-types.js.map +1 -0
- package/dist/safe-bash/commands/search/options.d.ts +4 -0
- package/dist/safe-bash/commands/search/options.d.ts.map +1 -1
- package/dist/safe-bash/commands/search/options.js +14 -1
- package/dist/safe-bash/commands/search/options.js.map +1 -1
- package/dist/safe-bash/commands/search/walk.d.ts +2 -0
- package/dist/safe-bash/commands/search/walk.d.ts.map +1 -1
- package/dist/safe-bash/commands/search/walk.js +32 -0
- package/dist/safe-bash/commands/search/walk.js.map +1 -1
- package/dist/safe-bash/commands/streams.d.ts +1 -1
- package/dist/safe-bash/commands/streams.d.ts.map +1 -1
- package/dist/safe-bash/commands/streams.js +7 -1
- package/dist/safe-bash/commands/streams.js.map +1 -1
- package/dist/safe-bash/commands/structured/interpreter.d.ts +9 -1
- package/dist/safe-bash/commands/structured/interpreter.d.ts.map +1 -1
- package/dist/safe-bash/commands/structured/interpreter.js +78 -2
- package/dist/safe-bash/commands/structured/interpreter.js.map +1 -1
- package/dist/safe-bash/commands/structured/parser.d.ts +13 -0
- package/dist/safe-bash/commands/structured/parser.d.ts.map +1 -1
- package/dist/safe-bash/commands/structured/parser.js +49 -2
- package/dist/safe-bash/commands/structured/parser.js.map +1 -1
- package/dist/safe-bash/commands/table-text/join.d.ts.map +1 -1
- package/dist/safe-bash/commands/table-text/join.js +7 -2
- package/dist/safe-bash/commands/table-text/join.js.map +1 -1
- package/dist/safe-bash/commands/text-programs/sed.d.ts.map +1 -1
- package/dist/safe-bash/commands/text-programs/sed.js +98 -29
- package/dist/safe-bash/commands/text-programs/sed.js.map +1 -1
- package/dist/safe-bash/commands/text.d.ts.map +1 -1
- package/dist/safe-bash/commands/text.js +26 -16
- package/dist/safe-bash/commands/text.js.map +1 -1
- package/dist/safe-bash/plugins/index.d.ts +3 -0
- package/dist/safe-bash/plugins/index.d.ts.map +1 -1
- package/dist/safe-bash/plugins/index.js +1 -1
- package/dist/safe-bash/plugins/index.js.map +1 -1
- package/dist/safe-bash/shell/brace-expansion.d.ts +6 -0
- package/dist/safe-bash/shell/brace-expansion.d.ts.map +1 -0
- package/dist/safe-bash/shell/brace-expansion.js +335 -0
- package/dist/safe-bash/shell/brace-expansion.js.map +1 -0
- package/dist/safe-bash/shell/parser.d.ts +10 -1
- package/dist/safe-bash/shell/parser.d.ts.map +1 -1
- package/dist/safe-bash/shell/parser.js +92 -18
- package/dist/safe-bash/shell/parser.js.map +1 -1
- package/dist/safe-bash/shell/runtime.d.ts +4 -2
- package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
- package/dist/safe-bash/shell/runtime.js +239 -38
- package/dist/safe-bash/shell/runtime.js.map +1 -1
- package/package.json +2 -2
|
@@ -3,12 +3,13 @@ import { ACCESS_MODES, FsError, composeMiddleware, createBytePipe, pipeBytes, re
|
|
|
3
3
|
import { concatShellValues, shellValueByteLength, shellValueBytes, shellValueFromBytes, shellValueText } from "../contracts/value.js";
|
|
4
4
|
import { createCommandArguments, getCommandArguments } from "../contracts/command.js";
|
|
5
5
|
import { ValueArena } from "./value-state.js";
|
|
6
|
-
import { HereDocumentSyntaxError, hereDocumentWords, parseShellInputUnit, parseShellUnit } from "./parser.js";
|
|
6
|
+
import { compoundEntryWords, HereDocumentSyntaxError, hereDocumentWords, parseShellInputUnit, parseShellUnit } from "./parser.js";
|
|
7
7
|
import { SourceLineIndex } from "./source-line-index.js";
|
|
8
8
|
import { ShellLimitError, ShellSyntaxError } from "./types.js";
|
|
9
9
|
import { fileInput, ShellInput } from "./input.js";
|
|
10
10
|
import { evaluateArithmetic, prepareArithmetic } from "./arithmetic.js";
|
|
11
11
|
import { defaultMaxParseUnits, ParseBudget } from "./parse-budget.js";
|
|
12
|
+
import { BraceExpansionFailure, expandBraces } from "./brace-expansion.js";
|
|
12
13
|
import { evaluatePositionalArithmetic } from "./arithmetic-parameters.js";
|
|
13
14
|
import { compilePattern, matchesPattern } from "./pattern.js";
|
|
14
15
|
import { nextCodePointOffset, previousCodePointOffset, scanString, stringCheckpoint } from "./string-operations.js";
|
|
@@ -19,6 +20,8 @@ import { invocationScope, throwCleanupFailures } from "./cleanup.js";
|
|
|
19
20
|
import { bindFileOutputBudget, openFileOutput } from "../contracts/filesystem-output.js";
|
|
20
21
|
import { outputFailure } from "../contracts/io.js";
|
|
21
22
|
import { executionCommands } from "../commands/execution.js";
|
|
23
|
+
import { formatPrintf, printfCommand } from "../commands/basic.js";
|
|
24
|
+
import { UsageError } from "../commands/internal.js";
|
|
22
25
|
import { cloneGetoptsState, createGetoptsState, GetoptsError, scanGetopts, withGetoptsIndex } from "./getopts.js";
|
|
23
26
|
import { activateChildCancellation, prepareChildCancellation, selectRuntimeCancellationOutcome, subscribeCancellation, } from "./cancellation.js";
|
|
24
27
|
import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker } from "./arrays/syntax.js";
|
|
@@ -53,7 +56,7 @@ const shellBuiltinNames = new Set([
|
|
|
53
56
|
const implementedBuiltins = new Set([...shellBuiltinNames].filter(name => !["echo", "printf", "test", "["].includes(name)));
|
|
54
57
|
const specialBuiltinNames = new Set([":", ".", "break", "continue", "eval", "exit", "export", "readonly", "return", "set", "shift", "unset"]);
|
|
55
58
|
const unsupportedSetOptionNames = new Set([
|
|
56
|
-
"allexport", "
|
|
59
|
+
"allexport", "emacs", "errtrace", "functrace", "hashall", "histexpand", "history",
|
|
57
60
|
"ignoreeof", "interactive-comments", "keyword", "monitor", "noclobber", "noexec", "noglob", "nolog",
|
|
58
61
|
"notify", "onecmd", "physical", "posix", "privileged", "verbose", "vi", "xtrace",
|
|
59
62
|
]);
|
|
@@ -339,7 +342,7 @@ function signalSink(sink, signal) {
|
|
|
339
342
|
budgetedSinks.set(output, { budget: owned.budget, write: output.write });
|
|
340
343
|
return output;
|
|
341
344
|
}
|
|
342
|
-
async function cloneState(state, signal, scope) {
|
|
345
|
+
async function cloneState(state, signal, scope, inheritLocals = true) {
|
|
343
346
|
const destination = await snapshotState(state, () => ({
|
|
344
347
|
...state,
|
|
345
348
|
variables: Object.assign(Object.create(null), state.variables),
|
|
@@ -347,7 +350,7 @@ async function cloneState(state, signal, scope) {
|
|
|
347
350
|
readonlyVariables: new Set(state.readonlyVariables),
|
|
348
351
|
getopts: cloneGetoptsBinding(state),
|
|
349
352
|
directoryStack: { entries: [...state.directoryStack?.entries ?? []], bytes: state.directoryStack?.bytes ?? 0 },
|
|
350
|
-
locals:
|
|
353
|
+
locals: inheritLocals ? state.locals.map((scope) => new Map([...scope].map(([name, saved]) => [name, { ...saved, ...(saved.getopts ? { getopts: { integer: saved.getopts.integer, cursor: cloneGetoptsState(saved.getopts.cursor) } } : {}) }]))) : [],
|
|
351
354
|
}), signal, async (destination, owner) => {
|
|
352
355
|
const store = arrayStore(destination) ?? requireArrays(destination);
|
|
353
356
|
for (let index = 0; index < destination.locals.length; index++) {
|
|
@@ -361,14 +364,19 @@ async function cloneState(state, signal, scope) {
|
|
|
361
364
|
throw new ArrayFailure("stale state snapshot");
|
|
362
365
|
const savedOwner = ArrayOwner.create(owner.ledger, owner);
|
|
363
366
|
let binding;
|
|
367
|
+
let releaseBinding;
|
|
364
368
|
try {
|
|
369
|
+
scope?.register(async () => { await savedOwner.completion; await releaseBinding; });
|
|
365
370
|
const watch = await store.watch(name, savedOwner, signal);
|
|
366
371
|
const tickets = savedOwner.reserve({ generation: true, version: true, epoch: true, slots: 1, metadata: 64, work: 14 });
|
|
367
372
|
const token = await textToken(savedOwner, name, signal);
|
|
368
373
|
const admission = savedOwner.reserve({ slots: 1, metadata: 32, work: 5 });
|
|
369
374
|
binding = typed.binding?.retain();
|
|
370
375
|
typedSavedVariables.set(copied, { owner: savedOwner, binding, tickets, prepared: { name: token, admission }, watch, scalarLegacy: typed.scalarLegacy });
|
|
371
|
-
tickets.cleanup = () => {
|
|
376
|
+
tickets.cleanup = () => {
|
|
377
|
+
if (typedSavedVariables.delete(copied) && scope)
|
|
378
|
+
releaseBinding = scope.cleanup(async () => { await binding?.release(); });
|
|
379
|
+
};
|
|
372
380
|
}
|
|
373
381
|
catch (error) {
|
|
374
382
|
await binding?.release();
|
|
@@ -1693,6 +1701,22 @@ export class Runtime {
|
|
|
1693
1701
|
}
|
|
1694
1702
|
else
|
|
1695
1703
|
for (const entry of assignment.entries) {
|
|
1704
|
+
const original = compoundEntryWords.get(entry);
|
|
1705
|
+
if (entry.index && original && state.braceexpand !== false && original.parts.some(part => part.kind === "text" && !part.quoted && part.value.includes("{"))) {
|
|
1706
|
+
let expandedEntry = false;
|
|
1707
|
+
for await (const expanded of expandBraces(original, this.budget, this.signal)) {
|
|
1708
|
+
if (expanded === original)
|
|
1709
|
+
break;
|
|
1710
|
+
expandedEntry = true;
|
|
1711
|
+
const values = await this.valueWord(expanded, state, io, true, false, false, false, undefined, false);
|
|
1712
|
+
for (const value of values) {
|
|
1713
|
+
await insert(cursor, shellValueText(value));
|
|
1714
|
+
cursor++;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (expandedEntry)
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1696
1720
|
const fields = await this.word(entry.value, state, io, entry.index === undefined);
|
|
1697
1721
|
if (entry.index) {
|
|
1698
1722
|
const index = numericIndex(entry.index);
|
|
@@ -2002,6 +2026,8 @@ export class Runtime {
|
|
|
2002
2026
|
originalIO = { ...originalIO, [valueScope]: allocation };
|
|
2003
2027
|
let io = originalIO;
|
|
2004
2028
|
let diagnosticFailure;
|
|
2029
|
+
let snapshotScope;
|
|
2030
|
+
let finishSnapshot;
|
|
2005
2031
|
try {
|
|
2006
2032
|
const execute = async () => {
|
|
2007
2033
|
if (command.kind === "function") {
|
|
@@ -2013,7 +2039,11 @@ export class Runtime {
|
|
|
2013
2039
|
return 0;
|
|
2014
2040
|
}
|
|
2015
2041
|
if (command.kind === "simple")
|
|
2016
|
-
return await this.simple(command, state, originalIO, inputs, outputs,
|
|
2042
|
+
return await this.simple(command, state, originalIO, inputs, outputs, () => {
|
|
2043
|
+
snapshotScope = originalIO[invocationScope].child();
|
|
2044
|
+
void snapshotScope.run(() => new Promise(resolve => { finishSnapshot = resolve; }));
|
|
2045
|
+
return snapshotScope;
|
|
2046
|
+
}, fileShortcut);
|
|
2017
2047
|
io = await this.redirect(command.redirects, state, io, inputs, outputs, command.kind === "subshell", command.kind !== "subshell");
|
|
2018
2048
|
if (command.kind === "conditional") {
|
|
2019
2049
|
try {
|
|
@@ -2023,7 +2053,7 @@ export class Runtime {
|
|
|
2023
2053
|
work: { remaining: this.budget.limits.maxExpansionBytes, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") },
|
|
2024
2054
|
expand: async (word, pattern = false) => (await this.word(word, state, io, false, pattern, false, pattern)).join(""),
|
|
2025
2055
|
regex: (subject, pattern) => this.ere(subject, pattern, state, io),
|
|
2026
|
-
option: name => name === "errexit" ? !!state.errexit : name === "nounset" ? !!state.nounset : name === "pipefail" ? state.pipefail : false,
|
|
2056
|
+
option: name => name === "braceexpand" ? state.braceexpand !== false : name === "errexit" ? !!state.errexit : name === "nounset" ? !!state.nounset : name === "pipefail" ? state.pipefail : false,
|
|
2027
2057
|
present: name => {
|
|
2028
2058
|
const match = /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\[(0|[1-9][0-9]*|[@*])\])?$/u.exec(name);
|
|
2029
2059
|
if (!match)
|
|
@@ -2230,7 +2260,7 @@ export class Runtime {
|
|
|
2230
2260
|
this.signal.throwIfAborted();
|
|
2231
2261
|
}
|
|
2232
2262
|
}
|
|
2233
|
-
if (error instanceof ExpansionFailure)
|
|
2263
|
+
if (error instanceof ExpansionFailure || error instanceof BraceExpansionFailure)
|
|
2234
2264
|
throw completedExit(error instanceof ParameterExpansionFailure && !state.isolated ? 127 : 1);
|
|
2235
2265
|
if (error instanceof FatalCommandFailure)
|
|
2236
2266
|
throw completedExit(error.status);
|
|
@@ -2240,16 +2270,22 @@ export class Runtime {
|
|
|
2240
2270
|
return status;
|
|
2241
2271
|
}
|
|
2242
2272
|
finally {
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2273
|
+
try {
|
|
2274
|
+
await Promise.allSettled([
|
|
2275
|
+
...[...outputs].map(async (close) => close({ reason: new FsError("ECANCELED", { syscall: "redirect" }) })),
|
|
2276
|
+
...[...inputs].map(async (input) => input.close()),
|
|
2277
|
+
]).then(results => {
|
|
2278
|
+
const failures = results.filter(result => result.status === "rejected").map(result => result.reason);
|
|
2279
|
+
if (diagnosticFailure)
|
|
2280
|
+
io[invocationScope].failures.push(...failures);
|
|
2281
|
+
else
|
|
2282
|
+
throwCleanupFailures(failures);
|
|
2283
|
+
}).finally(() => allocation.close());
|
|
2284
|
+
}
|
|
2285
|
+
finally {
|
|
2286
|
+
finishSnapshot?.();
|
|
2287
|
+
await snapshotScope?.close();
|
|
2288
|
+
}
|
|
2253
2289
|
}
|
|
2254
2290
|
}
|
|
2255
2291
|
async loopBody(body, state, io) {
|
|
@@ -2535,7 +2571,7 @@ export class Runtime {
|
|
|
2535
2571
|
return undefined;
|
|
2536
2572
|
return { name: match[1], append: match[2] === "+", value: { offset: word.offset, parts: [{ ...first, value: first.value.slice(match[0].length) }, ...word.parts.slice(1)] } };
|
|
2537
2573
|
}
|
|
2538
|
-
async simple(command, state, originalIO, inputs, outputs, fileShortcut = false) {
|
|
2574
|
+
async simple(command, state, originalIO, inputs, outputs, createSnapshotScope, fileShortcut = false) {
|
|
2539
2575
|
state.substitutionStatus = 0;
|
|
2540
2576
|
const assignments = [];
|
|
2541
2577
|
let wordIndex = 0;
|
|
@@ -2558,9 +2594,8 @@ export class Runtime {
|
|
|
2558
2594
|
const inlineInput = command.redirects.some((redirect) => redirect.document || redirect.operator === "<<<");
|
|
2559
2595
|
const functionCommand = words.length > 0 && state.functions.has(words[0]);
|
|
2560
2596
|
const isolatedInlineInput = inlineInput && words.length > 0 && !shellBuiltinNames.has(words[0]) && !functionCommand;
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
let io = originalIO;
|
|
2597
|
+
const snapshotScope = isolatedInlineInput ? createSnapshotScope() : undefined;
|
|
2598
|
+
let io = snapshotScope ? { ...originalIO, [invocationScope]: snapshotScope } : originalIO;
|
|
2564
2599
|
const previous = new Map();
|
|
2565
2600
|
const assign = async () => {
|
|
2566
2601
|
for (const assignment of assignments) {
|
|
@@ -2599,9 +2634,14 @@ export class Runtime {
|
|
|
2599
2634
|
state.exported.add(assignment.name);
|
|
2600
2635
|
}
|
|
2601
2636
|
};
|
|
2602
|
-
|
|
2603
|
-
stateMonitor(state)?.openOverlay(previous);
|
|
2637
|
+
let overlayOpen = false;
|
|
2604
2638
|
try {
|
|
2639
|
+
if (snapshotScope)
|
|
2640
|
+
state = await cloneState(state, this.signal, snapshotScope);
|
|
2641
|
+
if (words.length) {
|
|
2642
|
+
stateMonitor(state)?.openOverlay(previous);
|
|
2643
|
+
overlayOpen = true;
|
|
2644
|
+
}
|
|
2605
2645
|
if (inlineInput || (state.profile === "sh" || !words.length) && assignments.some(assignment => state.readonlyVariables?.has(assignment.name)))
|
|
2606
2646
|
await assign();
|
|
2607
2647
|
if (inlineInput && functionCommand && previous.size) {
|
|
@@ -2706,7 +2746,7 @@ export class Runtime {
|
|
|
2706
2746
|
throw new ExecutionFailure(error, io);
|
|
2707
2747
|
}
|
|
2708
2748
|
finally {
|
|
2709
|
-
if (
|
|
2749
|
+
if (overlayOpen) {
|
|
2710
2750
|
for (const [key, saved] of previous)
|
|
2711
2751
|
await originalIO[invocationScope].cleanup(async () => {
|
|
2712
2752
|
if (saved.superseded)
|
|
@@ -2719,8 +2759,6 @@ export class Runtime {
|
|
|
2719
2759
|
else
|
|
2720
2760
|
for (const saved of previous.values())
|
|
2721
2761
|
saved.heldValue?.release();
|
|
2722
|
-
if (isolatedInlineInput)
|
|
2723
|
-
stateMonitor(state)?.closeValues();
|
|
2724
2762
|
}
|
|
2725
2763
|
}
|
|
2726
2764
|
async dispatch(name, args, state, io, assignments, bypassFunctions = false, values = args) {
|
|
@@ -2972,6 +3010,9 @@ export class Runtime {
|
|
|
2972
3010
|
}
|
|
2973
3011
|
}
|
|
2974
3012
|
const definition = this.commands.get(context.command);
|
|
3013
|
+
if (context.command === "printf" && definition?.execute === printfCommand.execute && context.args[0]?.startsWith("-v")) {
|
|
3014
|
+
return { exitCode: await this.printfVariable(context, state, assignments) };
|
|
3015
|
+
}
|
|
2975
3016
|
if (!definition) {
|
|
2976
3017
|
if (context.command === "bash" || context.command === "sh")
|
|
2977
3018
|
return { exitCode: await this.interpreter(context, state, io) };
|
|
@@ -3228,21 +3269,30 @@ export class Runtime {
|
|
|
3228
3269
|
let commandString = false;
|
|
3229
3270
|
let standardInput = false;
|
|
3230
3271
|
let errexit = false;
|
|
3272
|
+
let braceexpand = true;
|
|
3231
3273
|
while (args.length && /^[+-]/u.test(args[0])) {
|
|
3232
3274
|
const option = args.shift();
|
|
3233
3275
|
if (option === "--" || option === "-")
|
|
3234
3276
|
break;
|
|
3235
|
-
if (
|
|
3236
|
-
|
|
3277
|
+
if ((option === "-o" || option === "+o") && args[0] === "braceexpand") {
|
|
3278
|
+
braceexpand = option === "-o";
|
|
3279
|
+
args.shift();
|
|
3280
|
+
continue;
|
|
3281
|
+
}
|
|
3282
|
+
const flags = option.slice(1);
|
|
3283
|
+
if (!flags.length || [...flags].some(flag => !(option[0] === "-" ? "cseB" : "eB").includes(flag))) {
|
|
3284
|
+
await writeText(context.stderr, `${context.command}: ${option}: unsupported option; supported flags are -c, -s, -e, +e, -B, +B and +/-o braceexpand\n`);
|
|
3237
3285
|
return 2;
|
|
3238
3286
|
}
|
|
3239
3287
|
commandString ||= option.includes("c");
|
|
3240
3288
|
standardInput ||= option.includes("s");
|
|
3241
3289
|
if (option.includes("e"))
|
|
3242
3290
|
errexit = option.startsWith("-");
|
|
3291
|
+
if (option.includes("B"))
|
|
3292
|
+
braceexpand = option.startsWith("-");
|
|
3243
3293
|
}
|
|
3244
3294
|
if (!commandString && !standardInput && args.length)
|
|
3245
|
-
return this.scriptFile(context, state, io, args[0], args.slice(1), false, errexit, loadedSource);
|
|
3295
|
+
return this.scriptFile(context, state, io, args[0], args.slice(1), false, errexit, loadedSource, braceexpand);
|
|
3246
3296
|
const source = commandString ? args.shift() : undefined;
|
|
3247
3297
|
if (commandString && source === undefined) {
|
|
3248
3298
|
await writeText(context.stderr, `${context.command}: -c: option requires an argument\n`);
|
|
@@ -3251,6 +3301,7 @@ export class Runtime {
|
|
|
3251
3301
|
const arg0 = commandString ? args.shift() ?? context.command : context.command;
|
|
3252
3302
|
const child = this.processState(context, state, arg0, args);
|
|
3253
3303
|
child.errexit = errexit;
|
|
3304
|
+
child.braceexpand = braceexpand;
|
|
3254
3305
|
const childIO = isolateIO({ ...io, ...context, execution: { ignoreErrexit: false }, diagnosticLine: 1, diagnosticOffset: 0, scriptName: arg0 });
|
|
3255
3306
|
if (source !== undefined) {
|
|
3256
3307
|
this.budget.source(Buffer.byteLength(source));
|
|
@@ -3542,7 +3593,7 @@ export class Runtime {
|
|
|
3542
3593
|
return { exitCode: validateExitCode(result.exitCode) };
|
|
3543
3594
|
}, (runtime, forwarded, child, childIO) => (command, arguments_, options = {}) => runtime.shebangTarget(forwarded, child, childIO, command, arguments_, options, target, loadedSource));
|
|
3544
3595
|
}
|
|
3545
|
-
async scriptFile(context, state, io, target, args, direct, errexit = false, loadedSource) {
|
|
3596
|
+
async scriptFile(context, state, io, target, args, direct, errexit = false, loadedSource, braceexpand = true) {
|
|
3546
3597
|
if (target === "")
|
|
3547
3598
|
throw new CommandFailure(`${context.command}: : No such file or directory`, 127);
|
|
3548
3599
|
if (state.depth >= this.budget.limits.maxSubstitutionDepth)
|
|
@@ -3612,6 +3663,7 @@ export class Runtime {
|
|
|
3612
3663
|
}
|
|
3613
3664
|
const child = this.processState(context, state, target, args);
|
|
3614
3665
|
child.errexit = errexit;
|
|
3666
|
+
child.braceexpand = braceexpand;
|
|
3615
3667
|
if (direct)
|
|
3616
3668
|
child.profile = interpreterProfile ?? state.profile ?? "bash";
|
|
3617
3669
|
const childIO = isolateIO({ ...io, ...context, execution: { ignoreErrexit: false }, diagnosticLine: 1, diagnosticOffset: 0, scriptName: target });
|
|
@@ -3850,7 +3902,7 @@ export class Runtime {
|
|
|
3850
3902
|
const allocation = this.budget.values.scope();
|
|
3851
3903
|
scope.register(() => allocation.close());
|
|
3852
3904
|
const carrier = this.admitArguments(getCommandArguments({ args, ...(options.argumentValues ? { argumentValues: options.argumentValues } : {}) }).values, allocation);
|
|
3853
|
-
const child = await cloneState(state, this.signal, scope);
|
|
3905
|
+
const child = await cloneState(state, this.signal, scope, false);
|
|
3854
3906
|
child.cwd = resolvePath(context.cwd, options.cwd ?? ".");
|
|
3855
3907
|
const env = options.replaceEnv ? { ...options.env } : { ...context.env, ...options.env, PWD: child.cwd };
|
|
3856
3908
|
if (guestArrays(child) || Object.keys(env).some(key => arrayStore(child)?.get(key)))
|
|
@@ -3927,10 +3979,12 @@ export class Runtime {
|
|
|
3927
3979
|
state.errexit = enabled;
|
|
3928
3980
|
else if (flag === "u")
|
|
3929
3981
|
state.nounset = enabled;
|
|
3982
|
+
else if (flag === "B")
|
|
3983
|
+
state.braceexpand = enabled;
|
|
3930
3984
|
else if (flag === "o" && position === option.length - 1) {
|
|
3931
3985
|
const name = args[index + 1];
|
|
3932
3986
|
if (name === undefined) {
|
|
3933
|
-
const options = [["errexit", !!state.errexit], ["nounset", !!state.nounset], ["pipefail", state.pipefail]];
|
|
3987
|
+
const options = [["braceexpand", state.braceexpand !== false], ["errexit", !!state.errexit], ["nounset", !!state.nounset], ["pipefail", state.pipefail]];
|
|
3934
3988
|
for (const [name, active] of options)
|
|
3935
3989
|
await writeText(stdout, enabled ? `${name}\t${active ? "on" : "off"}\n` : `set ${active ? "-" : "+"}o ${name}\n`);
|
|
3936
3990
|
}
|
|
@@ -3941,6 +3995,8 @@ export class Runtime {
|
|
|
3941
3995
|
state.nounset = enabled;
|
|
3942
3996
|
else if (name === "pipefail")
|
|
3943
3997
|
state.pipefail = enabled;
|
|
3998
|
+
else if (name === "braceexpand")
|
|
3999
|
+
state.braceexpand = enabled;
|
|
3944
4000
|
else {
|
|
3945
4001
|
const unsupported = unsupportedSetOptionNames.has(name);
|
|
3946
4002
|
await this.diagnostic(context, `set: ${name}: ${unsupported ? "unsupported shell option" : "invalid option name"}`);
|
|
@@ -3953,7 +4009,7 @@ export class Runtime {
|
|
|
3953
4009
|
valid = false;
|
|
3954
4010
|
}
|
|
3955
4011
|
if (!valid) {
|
|
3956
|
-
await writeText(stderr, "set: unsupported shell option; supported forms are +/- e/u clusters, -- arguments and terminal o with pipefail, errexit or nounset\n");
|
|
4012
|
+
await writeText(stderr, "set: unsupported shell option; supported forms are +/- e/u/B clusters, -- arguments and terminal o with braceexpand, pipefail, errexit or nounset\n");
|
|
3957
4013
|
return 1;
|
|
3958
4014
|
}
|
|
3959
4015
|
index++;
|
|
@@ -4365,6 +4421,131 @@ export class Runtime {
|
|
|
4365
4421
|
}
|
|
4366
4422
|
return status;
|
|
4367
4423
|
}
|
|
4424
|
+
async printfVariable(context, state, assignments) {
|
|
4425
|
+
const incoming = getCommandArguments(context);
|
|
4426
|
+
let offset = 0;
|
|
4427
|
+
let name = "";
|
|
4428
|
+
let index;
|
|
4429
|
+
while (incoming.args[offset]?.startsWith("-v")) {
|
|
4430
|
+
const option = incoming.args[offset++];
|
|
4431
|
+
const target = option.slice(2) || incoming.args[offset++];
|
|
4432
|
+
if (target === undefined) {
|
|
4433
|
+
await writeText(context.stderr, "printf: -v: option requires an argument\n");
|
|
4434
|
+
return 2;
|
|
4435
|
+
}
|
|
4436
|
+
const bracket = target.indexOf("[");
|
|
4437
|
+
name = bracket < 0 ? target : target.slice(0, bracket);
|
|
4438
|
+
if (!/^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(name) || bracket >= 0 && !target.endsWith("]")) {
|
|
4439
|
+
await writeText(context.stderr, `printf: '${target}': not a valid identifier\n`);
|
|
4440
|
+
return 2;
|
|
4441
|
+
}
|
|
4442
|
+
index = undefined;
|
|
4443
|
+
if (bracket >= 0) {
|
|
4444
|
+
if (target[bracket + 1] === "'" || target[bracket + 1] === '"') {
|
|
4445
|
+
await writeText(context.stderr, `printf: '${target}': unsupported indexed-array subscript\n`);
|
|
4446
|
+
return 2;
|
|
4447
|
+
}
|
|
4448
|
+
try {
|
|
4449
|
+
index = literalIndex(target.slice(bracket + 1, -1), 0, this.budget.parsing);
|
|
4450
|
+
}
|
|
4451
|
+
catch (error) {
|
|
4452
|
+
this.signal.throwIfAborted();
|
|
4453
|
+
if (!(error instanceof ShellSyntaxError))
|
|
4454
|
+
throw error;
|
|
4455
|
+
await writeText(context.stderr, `printf: '${target}': unsupported indexed-array subscript\n`);
|
|
4456
|
+
return 2;
|
|
4457
|
+
}
|
|
4458
|
+
if (numericIndex(index) === undefined) {
|
|
4459
|
+
await writeText(context.stderr, "printf: index outside 0..2147483647\n");
|
|
4460
|
+
return 2;
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
const arguments_ = incoming.slice(offset);
|
|
4465
|
+
const format = arguments_.args[0] === "--" ? arguments_.args[1] : arguments_.args[0];
|
|
4466
|
+
if (format === undefined || arguments_.args[0] !== "--" && format.startsWith("-")) {
|
|
4467
|
+
await writeText(context.stderr, "printf: usage: printf [-v var] format [arguments]\n");
|
|
4468
|
+
return 2;
|
|
4469
|
+
}
|
|
4470
|
+
const allocation = this.budget.values.scope();
|
|
4471
|
+
context[invocationScope].register(() => allocation.close());
|
|
4472
|
+
const chunks = [];
|
|
4473
|
+
let length = 0;
|
|
4474
|
+
let terminated = false;
|
|
4475
|
+
const stdout = { write: async (chunk) => {
|
|
4476
|
+
this.budget.cpuCheckpoint();
|
|
4477
|
+
if (chunk.byteLength > this.budget.limits.maxExpansionBytes - length)
|
|
4478
|
+
this.budget.fail("maxExpansionBytes");
|
|
4479
|
+
length += chunk.byteLength;
|
|
4480
|
+
if (terminated || !chunk.byteLength)
|
|
4481
|
+
return;
|
|
4482
|
+
const nul = chunk.indexOf(0);
|
|
4483
|
+
const bytes = nul < 0 ? chunk : chunk.subarray(0, nul);
|
|
4484
|
+
if (bytes.byteLength)
|
|
4485
|
+
chunks.push(shellValueFromBytes(bytes, allocation));
|
|
4486
|
+
terminated = nul >= 0;
|
|
4487
|
+
} };
|
|
4488
|
+
try {
|
|
4489
|
+
let status;
|
|
4490
|
+
try {
|
|
4491
|
+
status = (await formatPrintf({ ...context, args: arguments_.args, argumentValues: arguments_, stdout })).exitCode;
|
|
4492
|
+
}
|
|
4493
|
+
catch (error) {
|
|
4494
|
+
this.signal.throwIfAborted();
|
|
4495
|
+
if (!(error instanceof UsageError))
|
|
4496
|
+
throw error;
|
|
4497
|
+
await writeText(context.stderr, `printf: ${error.message}\n`);
|
|
4498
|
+
status = 1;
|
|
4499
|
+
}
|
|
4500
|
+
const value = concatShellValues(chunks, allocation);
|
|
4501
|
+
this.signal.throwIfAborted();
|
|
4502
|
+
if (state.readonlyVariables?.has(name)) {
|
|
4503
|
+
await writeText(context.stderr, `printf: ${name}: readonly variable\n`);
|
|
4504
|
+
return 1;
|
|
4505
|
+
}
|
|
4506
|
+
if (index || arrayStore(state)?.get(name)) {
|
|
4507
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
4508
|
+
const decode = (bytes) => {
|
|
4509
|
+
try {
|
|
4510
|
+
return decoder.decode(bytes);
|
|
4511
|
+
}
|
|
4512
|
+
catch (error) {
|
|
4513
|
+
this.signal.throwIfAborted();
|
|
4514
|
+
if (!(error instanceof TypeError))
|
|
4515
|
+
throw error;
|
|
4516
|
+
return undefined;
|
|
4517
|
+
}
|
|
4518
|
+
};
|
|
4519
|
+
let text = decode(shellValueBytes(value, allocation));
|
|
4520
|
+
if (text !== undefined && index && numericIndex(index) !== 0 && !arrayStore(state)?.get(name) && state.variables[name] !== undefined) {
|
|
4521
|
+
const previous = stateMonitor(state).values.get(name, state.variables[name]);
|
|
4522
|
+
if (decode(shellValueBytes(previous, allocation)) === undefined)
|
|
4523
|
+
text = undefined;
|
|
4524
|
+
}
|
|
4525
|
+
if (text === undefined) {
|
|
4526
|
+
await writeText(context.stderr, "printf: indexed variables do not support non-UTF-8 bytes\n");
|
|
4527
|
+
return 1;
|
|
4528
|
+
}
|
|
4529
|
+
if (index)
|
|
4530
|
+
await this.arrayAssignment({ kind: "element", name, index, append: false, value: { offset: 0, parts: [{ kind: "text", value: text, quoted: true }] } }, state, context);
|
|
4531
|
+
else
|
|
4532
|
+
await this.assignVariable(state, name, text);
|
|
4533
|
+
}
|
|
4534
|
+
else
|
|
4535
|
+
this.writeVariable(state, name, value);
|
|
4536
|
+
const previous = assignments.get(name);
|
|
4537
|
+
if (previous) {
|
|
4538
|
+
if (!previous.exported)
|
|
4539
|
+
state.exported.delete(name);
|
|
4540
|
+
await this.discardVariable(previous);
|
|
4541
|
+
assignments.delete(name);
|
|
4542
|
+
}
|
|
4543
|
+
return status;
|
|
4544
|
+
}
|
|
4545
|
+
finally {
|
|
4546
|
+
allocation.close();
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4368
4549
|
async builtin(context, state, assignments, diagnose, suppressSpecial = false) {
|
|
4369
4550
|
const { command, args, stdout, stderr } = context;
|
|
4370
4551
|
if (command === ":" || command === "true")
|
|
@@ -4921,9 +5102,11 @@ export class Runtime {
|
|
|
4921
5102
|
async valueWords(words, state, io, declaration = false) {
|
|
4922
5103
|
const fields = [];
|
|
4923
5104
|
for (const word of words) {
|
|
4924
|
-
|
|
4925
|
-
if (
|
|
5105
|
+
const values = await this.valueWord(word, state, io, !(declaration && this.assignment(word)), false, false, false, undefined, true);
|
|
5106
|
+
if (values.length > this.budget.limits.maxExpansionFields - fields.length)
|
|
4926
5107
|
this.budget.fail("maxExpansionFields");
|
|
5108
|
+
for (const value of values)
|
|
5109
|
+
fields.push(value);
|
|
4927
5110
|
}
|
|
4928
5111
|
return fields;
|
|
4929
5112
|
}
|
|
@@ -5050,7 +5233,7 @@ export class Runtime {
|
|
|
5050
5233
|
return this.arrayJoin(store.owner, values, Array.from(state.variables.IFS ?? " ")[0] ?? "");
|
|
5051
5234
|
}
|
|
5052
5235
|
let value = part.name === "?" ? String(state.status)
|
|
5053
|
-
: part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}`
|
|
5236
|
+
: part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}${state.braceexpand !== false ? "B" : ""}`
|
|
5054
5237
|
: part.name === "#" ? String(state.positional.length)
|
|
5055
5238
|
: part.name === "@" || part.name === "*" ? state.positional.join(hereString && (part.name === "@" || !part.quoted) ? " " : Array.from(state.variables.IFS ?? " ")[0] ?? "")
|
|
5056
5239
|
: /^0+$/u.test(part.name) ? state.arg0 ?? "virtual-bash"
|
|
@@ -5354,7 +5537,25 @@ export class Runtime {
|
|
|
5354
5537
|
async word(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend) {
|
|
5355
5538
|
return (await this.valueWord(word, state, io, split, pattern, hereString, conditionalPattern, regexAppend)).map(shellValueText);
|
|
5356
5539
|
}
|
|
5357
|
-
async valueWord(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend) {
|
|
5540
|
+
async valueWord(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend, braces = split && !pattern && !hereString) {
|
|
5541
|
+
if (braces && state.braceexpand !== false && word.parts.some(part => part.kind === "text" && !part.quoted && part.value.includes("{"))) {
|
|
5542
|
+
const fields = [];
|
|
5543
|
+
let bytes = 0;
|
|
5544
|
+
for await (const expanded of expandBraces(word, this.budget, this.signal)) {
|
|
5545
|
+
const values = await this.valueWord(expanded, state, io, split, pattern, hereString, conditionalPattern, regexAppend, false);
|
|
5546
|
+
if (values.length > this.budget.limits.maxExpansionFields - fields.length)
|
|
5547
|
+
this.budget.fail("maxExpansionFields");
|
|
5548
|
+
for (const value of values) {
|
|
5549
|
+
const size = shellValueByteLength(value);
|
|
5550
|
+
if (size > this.budget.limits.maxExpansionBytes - bytes)
|
|
5551
|
+
this.budget.fail("maxExpansionBytes");
|
|
5552
|
+
bytes += size;
|
|
5553
|
+
io[valueScope]?.reserve(32 + (typeof value === "string" ? value.length * 2 : 0), 0);
|
|
5554
|
+
fields.push(value);
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
return fields;
|
|
5558
|
+
}
|
|
5358
5559
|
const arrayOwned = word.parts.some(part => part.kind === "variable" && (getArraySelector(part) !== undefined || arrayStore(state)?.get(part.name) !== undefined));
|
|
5359
5560
|
const owner = arrayOwned ? requireArrays(state).owner : undefined;
|
|
5360
5561
|
const holding = owner?.hold();
|