@poe-platform/safe-bash 0.1.522 → 0.1.524
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/safe-bash/core.browser.js +381 -58
- package/dist/safe-bash/core.browser.js.map +4 -4
- package/dist/safe-bash/shell/arrays/bindings.d.ts +1 -0
- package/dist/safe-bash/shell/arrays/bindings.d.ts.map +1 -1
- package/dist/safe-bash/shell/arrays/bindings.js +3 -0
- package/dist/safe-bash/shell/arrays/bindings.js.map +1 -1
- package/dist/safe-bash/shell/arrays/syntax.d.ts +1 -0
- package/dist/safe-bash/shell/arrays/syntax.d.ts.map +1 -1
- package/dist/safe-bash/shell/arrays/syntax.js +4 -0
- package/dist/safe-bash/shell/arrays/syntax.js.map +1 -1
- package/dist/safe-bash/shell/parameter-transforms.d.ts +12 -0
- package/dist/safe-bash/shell/parameter-transforms.d.ts.map +1 -0
- package/dist/safe-bash/shell/parameter-transforms.js +166 -0
- package/dist/safe-bash/shell/parameter-transforms.js.map +1 -0
- package/dist/safe-bash/shell/parser.d.ts +2 -0
- package/dist/safe-bash/shell/parser.d.ts.map +1 -1
- package/dist/safe-bash/shell/parser.js +41 -5
- package/dist/safe-bash/shell/parser.js.map +1 -1
- package/dist/safe-bash/shell/runtime.d.ts +4 -1
- package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
- package/dist/safe-bash/shell/runtime.js +236 -53
- package/dist/safe-bash/shell/runtime.js.map +1 -1
- package/package.json +2 -2
|
@@ -27,7 +27,7 @@ import { formatPrintf, printfCommand } from "../commands/basic.js";
|
|
|
27
27
|
import { pathOf, UsageError } from "../commands/internal.js";
|
|
28
28
|
import { cloneGetoptsState, createGetoptsInput, createGetoptsState, GetoptsError, getoptsInputAllocationSize, scanGetopts, withGetoptsIndex } from "./getopts.js";
|
|
29
29
|
import { activateChildCancellation, prepareChildCancellation, selectRuntimeCancellationOutcome, subscribeCancellation, } from "./cancellation.js";
|
|
30
|
-
import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker } from "./arrays/syntax.js";
|
|
30
|
+
import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker, prefixNameQuoteGroups } from "./arrays/syntax.js";
|
|
31
31
|
import { ArrayFailure, ArrayOwner, exactSum } from "./arrays/ledger.js";
|
|
32
32
|
import { controlNames, IndexedBinding, textToken } from "./arrays/bindings.js";
|
|
33
33
|
import { arrayStore, guestArrays, requireArrays, snapshotState, stateMonitor, trackState } from "./arrays/state.js";
|
|
@@ -37,6 +37,7 @@ import { EreLedger } from "../commands/regex-execution/ere/limits.js";
|
|
|
37
37
|
import { compileEre } from "../commands/regex-execution/ere/syntax.js";
|
|
38
38
|
import { matchEre } from "../commands/regex-execution/ere/matcher.js";
|
|
39
39
|
import { PathLookup, pathTargets } from "./path-lookup.js";
|
|
40
|
+
import { transformParameter } from "./parameter-transforms.js";
|
|
40
41
|
export const defaultLimits = {
|
|
41
42
|
maxParseUnits: defaultMaxParseUnits,
|
|
42
43
|
maxInputBytes: 32 * 1024 * 1024,
|
|
@@ -76,6 +77,40 @@ export function resolveLimits(...limits) {
|
|
|
76
77
|
return result;
|
|
77
78
|
}
|
|
78
79
|
const budgetedSinks = new WeakMap();
|
|
80
|
+
async function sortExpansionStrings(values, work) {
|
|
81
|
+
const compare = async (left, right) => {
|
|
82
|
+
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
|
83
|
+
const pending = stringCheckpoint(work);
|
|
84
|
+
if (pending)
|
|
85
|
+
await pending;
|
|
86
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
87
|
+
if (difference)
|
|
88
|
+
return difference;
|
|
89
|
+
}
|
|
90
|
+
return left.length - right.length;
|
|
91
|
+
};
|
|
92
|
+
const sift = async (root, end) => {
|
|
93
|
+
while (root * 2 + 1 < end) {
|
|
94
|
+
let child = root * 2 + 1;
|
|
95
|
+
if (child + 1 < end && await compare(values[child], values[child + 1]) < 0)
|
|
96
|
+
child++;
|
|
97
|
+
if (await compare(values[root], values[child]) >= 0)
|
|
98
|
+
return;
|
|
99
|
+
const saved = values[root];
|
|
100
|
+
values[root] = values[child];
|
|
101
|
+
values[child] = saved;
|
|
102
|
+
root = child;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
for (let index = Math.floor(values.length / 2) - 1; index >= 0; index--)
|
|
106
|
+
await sift(index, values.length);
|
|
107
|
+
for (let end = values.length - 1; end > 0; end--) {
|
|
108
|
+
const saved = values[0];
|
|
109
|
+
values[0] = values[end];
|
|
110
|
+
values[end] = saved;
|
|
111
|
+
await sift(0, end);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
79
114
|
export class Budget {
|
|
80
115
|
limits;
|
|
81
116
|
onInternalError;
|
|
@@ -261,7 +296,7 @@ const typedSavedVariables = new WeakMap();
|
|
|
261
296
|
const valueScope = Symbol("shell value allocation scope");
|
|
262
297
|
const invokedValues = new WeakMap();
|
|
263
298
|
function isolateIO(io) {
|
|
264
|
-
return { ...io, ...(io.descriptors ? { descriptors: new Map([...io.descriptors].map(([number, descriptor]) => [number, { ...descriptor }])) } : {}) };
|
|
299
|
+
return { ...io, nameExpansionContext: undefined, ...(io.descriptors ? { descriptors: new Map([...io.descriptors].map(([number, descriptor]) => [number, { ...descriptor }])) } : {}) };
|
|
265
300
|
}
|
|
266
301
|
function activeIO(io) {
|
|
267
302
|
const input = io.descriptors?.get(0);
|
|
@@ -1536,6 +1571,7 @@ export class Runtime {
|
|
|
1536
1571
|
const current = store.get(name);
|
|
1537
1572
|
if (index !== undefined && current) {
|
|
1538
1573
|
staged = index === "members" ? IndexedBinding.create(store.owner) : await current.copy(this.signal);
|
|
1574
|
+
staged.assigned = current.assigned;
|
|
1539
1575
|
let maximum = -1;
|
|
1540
1576
|
for (const key of staged.values.keys()) {
|
|
1541
1577
|
operation.reserve({ work: 2 }).release();
|
|
@@ -1777,7 +1813,8 @@ export class Runtime {
|
|
|
1777
1813
|
throw new ArrayFailure("readonly binding");
|
|
1778
1814
|
if (!watch.valid())
|
|
1779
1815
|
throw new ArrayFailure("stale binding");
|
|
1780
|
-
|
|
1816
|
+
staged.assigned = true;
|
|
1817
|
+
if (!(assignment.kind === "compound" && assignment.append && writes === 0 && current?.assigned)) {
|
|
1781
1818
|
let released;
|
|
1782
1819
|
stateMonitor(state).publish(tickets, name, () => {
|
|
1783
1820
|
supersede();
|
|
@@ -2094,8 +2131,8 @@ export class Runtime {
|
|
|
2094
2131
|
fs: this.fs, cwd: state.cwd, signal: this.signal,
|
|
2095
2132
|
locale: state.variables.LC_ALL || state.variables.LC_COLLATE || state.variables.LANG || "C",
|
|
2096
2133
|
work: { remaining: this.budget.limits.maxExpansionBytes, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes"), allocation },
|
|
2097
|
-
expand: async (word, pattern = false) => (await this.word(word, state, io, false, pattern, false, pattern)).join(""),
|
|
2098
|
-
regex: (subject, pattern) => this.ere(subject, pattern, state, io),
|
|
2134
|
+
expand: async (word, pattern = false) => (await this.word(word, state, { ...io, nameExpansionContext: "conditional" }, false, pattern, false, pattern)).join(""),
|
|
2135
|
+
regex: (subject, pattern) => this.ere(subject, pattern, state, { ...io, nameExpansionContext: "conditional" }),
|
|
2099
2136
|
option: name => name === "braceexpand" ? state.braceexpand !== false : name === "errexit" ? !!state.errexit : name === "nounset" ? !!state.nounset : name === "pipefail" ? state.pipefail : false,
|
|
2100
2137
|
present: name => {
|
|
2101
2138
|
const match = /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\[(0|[1-9][0-9]*|[@*])\])?$/u.exec(name);
|
|
@@ -2348,6 +2385,7 @@ export class Runtime {
|
|
|
2348
2385
|
async document(document, state, io, line = document.endLine) {
|
|
2349
2386
|
this.signal.throwIfAborted();
|
|
2350
2387
|
let value = "";
|
|
2388
|
+
let fragments;
|
|
2351
2389
|
let size = 0;
|
|
2352
2390
|
let words = 0;
|
|
2353
2391
|
const warnings = [];
|
|
@@ -2358,18 +2396,28 @@ export class Runtime {
|
|
|
2358
2396
|
await writeDiagnostic(io.stderr, `shell: warning: ${warning}\n`);
|
|
2359
2397
|
if (++words % 128 === 0)
|
|
2360
2398
|
await yieldTurn(this.signal);
|
|
2361
|
-
const part = (await this.
|
|
2362
|
-
size +=
|
|
2399
|
+
const part = concatShellValues(await this.valueWord(word, state, { ...io, nameExpansionContext: "document" }, false), io[valueScope]);
|
|
2400
|
+
size += shellValueByteLength(part);
|
|
2363
2401
|
if (size > this.budget.limits.maxExpansionBytes)
|
|
2364
2402
|
this.budget.fail("maxExpansionBytes");
|
|
2365
|
-
|
|
2403
|
+
if (!fragments && typeof part === "string")
|
|
2404
|
+
value += part;
|
|
2405
|
+
else {
|
|
2406
|
+
if (!fragments) {
|
|
2407
|
+
io[valueScope]?.reserve(64 + value.length * 2, 0);
|
|
2408
|
+
fragments = [value];
|
|
2409
|
+
value = "";
|
|
2410
|
+
}
|
|
2411
|
+
io[valueScope]?.reserve(32 + (typeof part === "string" ? part.length * 2 : 0), 0);
|
|
2412
|
+
fragments.push(part);
|
|
2413
|
+
}
|
|
2366
2414
|
}
|
|
2367
2415
|
}
|
|
2368
2416
|
finally {
|
|
2369
2417
|
for (const warning of warnings.splice(0))
|
|
2370
2418
|
await writeDiagnostic(io.stderr, `shell: warning: ${warning}\n`);
|
|
2371
2419
|
}
|
|
2372
|
-
return value;
|
|
2420
|
+
return fragments ? concatShellValues(fragments, io[valueScope]) : value;
|
|
2373
2421
|
}
|
|
2374
2422
|
async redirect(redirects, state, io, inputs, outputs, isolatedInlineInput = false, persistMoves = false, fileShortcut = false, line) {
|
|
2375
2423
|
this.signal.throwIfAborted();
|
|
@@ -2401,6 +2449,7 @@ export class Runtime {
|
|
|
2401
2449
|
const stdinIsDefault = descriptor?.input ? descriptor.stdinIsDefault : false;
|
|
2402
2450
|
return {
|
|
2403
2451
|
[invocationScope]: io[invocationScope],
|
|
2452
|
+
...(io[valueScope] === undefined ? {} : { [valueScope]: io[valueScope] }),
|
|
2404
2453
|
...(io.execution === undefined ? {} : { execution: io.execution }),
|
|
2405
2454
|
...(io.diagnosticLine === undefined ? {} : { diagnosticLine: io.diagnosticLine }),
|
|
2406
2455
|
...(io.diagnosticOffset === undefined ? {} : { diagnosticOffset: io.diagnosticOffset }),
|
|
@@ -2421,7 +2470,7 @@ export class Runtime {
|
|
|
2421
2470
|
const hereString = redirect.operator === "<<<";
|
|
2422
2471
|
let value;
|
|
2423
2472
|
try {
|
|
2424
|
-
value = redirect.document ? await this.document(redirect.document, state, currentIO(), line) : (await this.
|
|
2473
|
+
value = redirect.document ? await this.document(redirect.document, state, currentIO(), line) : concatShellValues(await this.valueWord(redirect.target, state, currentIO(), false, false, hereString), io[valueScope]);
|
|
2425
2474
|
}
|
|
2426
2475
|
catch (error) {
|
|
2427
2476
|
if (error instanceof NounsetFailure)
|
|
@@ -2435,11 +2484,11 @@ export class Runtime {
|
|
|
2435
2484
|
throw error;
|
|
2436
2485
|
}
|
|
2437
2486
|
if (hereString) {
|
|
2438
|
-
if (
|
|
2487
|
+
if (shellValueByteLength(value) >= this.budget.limits.maxExpansionBytes)
|
|
2439
2488
|
this.budget.fail("maxExpansionBytes");
|
|
2440
|
-
value
|
|
2489
|
+
value = concatShellValues([value, "\n"], io[valueScope]);
|
|
2441
2490
|
}
|
|
2442
|
-
const input = new ShellInput(toByteSource(value), this.budget, this.signal);
|
|
2491
|
+
const input = new ShellInput(toByteSource(typeof value === "string" ? value : shellValueBytes(value, io[valueScope])), this.budget, this.signal);
|
|
2443
2492
|
inputs.add(input);
|
|
2444
2493
|
descriptors.set(redirect.descriptor, { input, stdinIsDefault: false });
|
|
2445
2494
|
continue;
|
|
@@ -5208,7 +5257,112 @@ export class Runtime {
|
|
|
5208
5257
|
async part(part, state, io, hereString = false) {
|
|
5209
5258
|
return shellValueText(await this.valuePart(part, state, io, hereString));
|
|
5210
5259
|
}
|
|
5260
|
+
async *prefixNames(prefix, state) {
|
|
5261
|
+
const allocation = this.budget.values.scope();
|
|
5262
|
+
const store = arrayStore(state);
|
|
5263
|
+
const holding = store?.owner.hold();
|
|
5264
|
+
const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, this.budget.limits.maxExpansionBytes * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
|
|
5265
|
+
try {
|
|
5266
|
+
allocation.reserveBytes(128);
|
|
5267
|
+
const names = [];
|
|
5268
|
+
const seen = new Set();
|
|
5269
|
+
let bytes = 0;
|
|
5270
|
+
const consider = async (name, assigned) => {
|
|
5271
|
+
const scanned = await scanString(name, work);
|
|
5272
|
+
const pending = stringCheckpoint(work, name.length + 1);
|
|
5273
|
+
if (pending)
|
|
5274
|
+
await pending;
|
|
5275
|
+
if (!assigned || !name.startsWith(prefix) || seen.has(name))
|
|
5276
|
+
return;
|
|
5277
|
+
for (let index = 0; index < name.length; index++) {
|
|
5278
|
+
const pending = stringCheckpoint(work);
|
|
5279
|
+
if (pending)
|
|
5280
|
+
await pending;
|
|
5281
|
+
const code = name.charCodeAt(index);
|
|
5282
|
+
if (!(code === 95 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || index > 0 && code >= 48 && code <= 57))
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
if (names.length >= this.budget.limits.maxExpansionFields)
|
|
5286
|
+
this.budget.fail("maxExpansionFields");
|
|
5287
|
+
if (scanned.bytes > this.budget.limits.maxExpansionBytes - bytes)
|
|
5288
|
+
this.budget.fail("maxExpansionBytes");
|
|
5289
|
+
bytes += scanned.bytes;
|
|
5290
|
+
allocation.reserveBytes(name.length * 2 + 64);
|
|
5291
|
+
seen.add(name);
|
|
5292
|
+
names.push(name);
|
|
5293
|
+
};
|
|
5294
|
+
for (const name in state.variables)
|
|
5295
|
+
if (Object.hasOwn(state.variables, name))
|
|
5296
|
+
await consider(name, state.variables[name] !== undefined);
|
|
5297
|
+
if (store)
|
|
5298
|
+
for (const [name, entry] of store.bindings)
|
|
5299
|
+
await consider(name, entry.binding.assigned);
|
|
5300
|
+
// Bash lists initialized names from dynamically enclosing local scopes,
|
|
5301
|
+
// even when the current local value shadows them with an unset value.
|
|
5302
|
+
for (const frame of state.locals)
|
|
5303
|
+
for (const [name, saved] of frame) {
|
|
5304
|
+
await consider(name, saved.value !== undefined || typedSavedVariables.get(saved)?.binding?.assigned === true);
|
|
5305
|
+
}
|
|
5306
|
+
await sortExpansionStrings(names, work);
|
|
5307
|
+
for (const name of names) {
|
|
5308
|
+
const pending = stringCheckpoint(work);
|
|
5309
|
+
if (pending)
|
|
5310
|
+
await pending;
|
|
5311
|
+
yield name;
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
finally {
|
|
5315
|
+
allocation.close();
|
|
5316
|
+
holding?.release();
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5211
5319
|
async valuePart(part, state, io, hereString = false) {
|
|
5320
|
+
if (part.kind === "variable" && part.transform) {
|
|
5321
|
+
const selector = getArraySelector(part);
|
|
5322
|
+
if (selector?.kind === "members" || part.name === "@" || part.name === "*") {
|
|
5323
|
+
const members = selector ? await this.arrayMembers(part.name, state) : this.positionalValues(state);
|
|
5324
|
+
const ifs = state.variables.IFS ?? " ";
|
|
5325
|
+
const separator = ifs.length ? String.fromCodePoint(ifs.codePointAt(0)) : "";
|
|
5326
|
+
const fragments = [];
|
|
5327
|
+
let bytes = 0;
|
|
5328
|
+
for (const member of members) {
|
|
5329
|
+
const value = await this.transformValue(member, part.transform, state, io);
|
|
5330
|
+
bytes += shellValueByteLength(value) + (fragments.length ? Buffer.byteLength(separator) : 0);
|
|
5331
|
+
if (bytes > this.budget.limits.maxExpansionBytes)
|
|
5332
|
+
this.budget.fail("maxExpansionBytes");
|
|
5333
|
+
io[valueScope]?.reserve(32, 0);
|
|
5334
|
+
if (fragments.length)
|
|
5335
|
+
fragments.push(separator);
|
|
5336
|
+
fragments.push(value);
|
|
5337
|
+
}
|
|
5338
|
+
return concatShellValues(fragments, io[valueScope]);
|
|
5339
|
+
}
|
|
5340
|
+
const base = { ...part };
|
|
5341
|
+
delete base.transform;
|
|
5342
|
+
copyArraySelector(part, base);
|
|
5343
|
+
const existing = selector?.kind === "element" ? arrayStore(state)?.get(part.name)?.get(numericIndex(selector.index) ?? -1) ?? (numericIndex(selector.index) === 0 ? this.variable(state, part.name) : undefined)
|
|
5344
|
+
: /^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name) ? this.variable(state, part.name)
|
|
5345
|
+
: /^[1-9][0-9]*$/u.test(part.name) ? state.positional[Number(part.name) - 1] : "";
|
|
5346
|
+
const value = await this.valuePart(base, state, io, hereString);
|
|
5347
|
+
return existing === undefined ? "" : this.transformValue(value, part.transform, state, io);
|
|
5348
|
+
}
|
|
5349
|
+
if (part.kind === "variable" && part.prefixNames) {
|
|
5350
|
+
const ifs = state.variables.IFS ?? " ";
|
|
5351
|
+
const separator = io.nameExpansionContext === "document" || io.nameExpansionContext === "conditional" && part.prefixNames === "@"
|
|
5352
|
+
? " " : ifs.length ? String.fromCodePoint(ifs.codePointAt(0)) : "";
|
|
5353
|
+
const fragments = [];
|
|
5354
|
+
let bytes = 0;
|
|
5355
|
+
for await (const name of this.prefixNames(part.name, state)) {
|
|
5356
|
+
const join = fragments.length ? separator : "";
|
|
5357
|
+
bytes += Buffer.byteLength(name) + Buffer.byteLength(join);
|
|
5358
|
+
if (bytes > this.budget.limits.maxExpansionBytes)
|
|
5359
|
+
this.budget.fail("maxExpansionBytes");
|
|
5360
|
+
io[valueScope]?.reserve((name.length + join.length) * 2 + 32, 0);
|
|
5361
|
+
fragments.push(join, name);
|
|
5362
|
+
}
|
|
5363
|
+
io[valueScope]?.reserve(bytes * 2, 0);
|
|
5364
|
+
return fragments.join("");
|
|
5365
|
+
}
|
|
5212
5366
|
const binding = part.kind === "variable" ? arrayStore(state)?.get(part.name) : undefined;
|
|
5213
5367
|
const holding = binding ? requireArrays(state).owner.hold() : undefined;
|
|
5214
5368
|
const selector = getArraySelector(part);
|
|
@@ -5233,6 +5387,19 @@ export class Runtime {
|
|
|
5233
5387
|
throw new ShellSyntaxError("Syntax nesting exceeds 64", word.offset);
|
|
5234
5388
|
return { ...io, parameterDepth };
|
|
5235
5389
|
}
|
|
5390
|
+
async transformValue(value, operator, state, io) {
|
|
5391
|
+
const allocation = io[valueScope] ?? this.budget.values.scope();
|
|
5392
|
+
try {
|
|
5393
|
+
return await transformParameter(value, operator, {
|
|
5394
|
+
maximumBytes: this.budget.limits.maxExpansionBytes, byteLocale: byteLocale(state.variables), allocation,
|
|
5395
|
+
work: { remaining: Math.min(Number.MAX_SAFE_INTEGER, this.budget.limits.maxExpansionBytes * 8 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") },
|
|
5396
|
+
});
|
|
5397
|
+
}
|
|
5398
|
+
finally {
|
|
5399
|
+
if (!io[valueScope])
|
|
5400
|
+
allocation.close();
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5236
5403
|
async partValue(part, state, io, hereString) {
|
|
5237
5404
|
this.signal.throwIfAborted();
|
|
5238
5405
|
if (part.kind === "compound-substitution-eof") {
|
|
@@ -5656,7 +5823,8 @@ export class Runtime {
|
|
|
5656
5823
|
}
|
|
5657
5824
|
return fields;
|
|
5658
5825
|
}
|
|
5659
|
-
const arrayOwned = word.parts.some(part => part.kind === "variable" && (getArraySelector(part) !== undefined || arrayStore(state)?.get(part.name) !== undefined));
|
|
5826
|
+
const arrayOwned = word.parts.some(part => part.kind === "variable" && !part.prefixNames && (getArraySelector(part) !== undefined || arrayStore(state)?.get(part.name) !== undefined));
|
|
5827
|
+
const prefixOwned = word.parts.some(part => part.kind === "variable" && (part.prefixNames === "@" || part.transform && part.name === "@"));
|
|
5660
5828
|
const owner = arrayOwned ? requireArrays(state).owner : undefined;
|
|
5661
5829
|
const holding = owner?.hold();
|
|
5662
5830
|
const scratch = !owner && split && state.variables.IFS !== "" && word.parts.some(part => !part.quoted && part.kind !== "text")
|
|
@@ -5667,12 +5835,14 @@ export class Runtime {
|
|
|
5667
5835
|
owner?.reserve({ metadata: 128 + word.parts.length * 32, allocatedSlots: word.parts.length + 1, work: word.parts.length + 5 });
|
|
5668
5836
|
scratch?.reserve(word.parts.length * 32, 0);
|
|
5669
5837
|
const fields = [];
|
|
5838
|
+
let emptyNameGroups;
|
|
5839
|
+
let quoteGroup;
|
|
5670
5840
|
const addField = () => {
|
|
5671
5841
|
if (fields.length >= this.budget.limits.maxExpansionFields)
|
|
5672
5842
|
this.budget.fail("maxExpansionFields");
|
|
5673
5843
|
scratch?.reserve(32, 0);
|
|
5674
5844
|
owner?.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
|
|
5675
|
-
fields.push({ fragments: [], bytes: false, patterns: undefined, present: false });
|
|
5845
|
+
fields.push({ fragments: [], bytes: false, patterns: undefined, present: false, independentPresence: false });
|
|
5676
5846
|
};
|
|
5677
5847
|
addField();
|
|
5678
5848
|
let expansionBytes = 0;
|
|
@@ -5712,6 +5882,15 @@ export class Runtime {
|
|
|
5712
5882
|
regexAppend?.(text, !glob);
|
|
5713
5883
|
field.fragments.push(value);
|
|
5714
5884
|
field.present ||= present;
|
|
5885
|
+
if (present) {
|
|
5886
|
+
if (size > 0 || !quoteGroup)
|
|
5887
|
+
field.independentPresence = true;
|
|
5888
|
+
else {
|
|
5889
|
+
io[valueScope]?.reserve(32, 0);
|
|
5890
|
+
field.quoteGroups ??= [];
|
|
5891
|
+
field.quoteGroups.push(quoteGroup);
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5715
5894
|
};
|
|
5716
5895
|
const appendSplit = async (value) => {
|
|
5717
5896
|
this.budget.cpuCheckpoint();
|
|
@@ -5768,7 +5947,8 @@ export class Runtime {
|
|
|
5768
5947
|
const parts = word.parts.map((part) => ({ part, splitText: false, io }));
|
|
5769
5948
|
for (let index = 0; index < parts.length; index++) {
|
|
5770
5949
|
const { part, splitText, io: partIO } = parts[index];
|
|
5771
|
-
|
|
5950
|
+
quoteGroup = prefixNameQuoteGroups.get(part);
|
|
5951
|
+
const quotedPresence = part.quoted && !((arrayOwned || prefixOwned) && isQuoteMarker(part));
|
|
5772
5952
|
if (part.kind === "variable" && ["-", "+", ":-", ":+"].includes(part.operator ?? "") && /^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name)) {
|
|
5773
5953
|
const value = this.variable(state, part.name);
|
|
5774
5954
|
const missing = value === undefined || (part.operator.startsWith(":") && value === "");
|
|
@@ -5783,17 +5963,36 @@ export class Runtime {
|
|
|
5783
5963
|
}
|
|
5784
5964
|
}
|
|
5785
5965
|
const selector = getArraySelector(part);
|
|
5786
|
-
if (part.kind === "variable" &&
|
|
5787
|
-
|
|
5966
|
+
if (part.kind === "variable" && part.prefixNames === "@" && split && (part.quoted || state.variables.IFS === "")) {
|
|
5967
|
+
let position = 0;
|
|
5968
|
+
for await (const name of this.prefixNames(part.name, state)) {
|
|
5969
|
+
if (position++)
|
|
5970
|
+
addField();
|
|
5971
|
+
append(name, !part.quoted, true);
|
|
5972
|
+
}
|
|
5973
|
+
if (position === 0 && quoteGroup) {
|
|
5974
|
+
io[valueScope]?.reserve(32, 0);
|
|
5975
|
+
emptyNameGroups ??= new Set();
|
|
5976
|
+
emptyNameGroups.add(quoteGroup);
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
else if (part.kind === "variable" && split && (selector?.kind === "members" && !part.length && (!part.quoted || selector.separator === "@") || part.transform && part.name === "@")) {
|
|
5980
|
+
const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state) : this.positionalValues(state);
|
|
5788
5981
|
for (let position = 0; position < members.length; position++) {
|
|
5789
5982
|
if (position > 0)
|
|
5790
5983
|
addField();
|
|
5791
|
-
const
|
|
5984
|
+
const original = members[position];
|
|
5985
|
+
const value = part.transform ? await this.transformValue(original, part.transform, state, partIO) : original;
|
|
5792
5986
|
if (part.quoted || state.variables.IFS === "")
|
|
5793
|
-
append(value, !part.quoted, part.quoted || value
|
|
5987
|
+
append(value, !part.quoted, part.quoted || shellValueByteLength(value) > 0);
|
|
5794
5988
|
else
|
|
5795
5989
|
await appendSplit(value);
|
|
5796
5990
|
}
|
|
5991
|
+
if (part.transform && members.length === 0 && quoteGroup) {
|
|
5992
|
+
io[valueScope]?.reserve(32, 0);
|
|
5993
|
+
emptyNameGroups ??= new Set();
|
|
5994
|
+
emptyNameGroups.add(quoteGroup);
|
|
5995
|
+
}
|
|
5797
5996
|
}
|
|
5798
5997
|
else if (part.kind === "text" && !splitText) {
|
|
5799
5998
|
let value = invokedValues.get(part) ?? part.byteValue ?? part.value;
|
|
@@ -5801,7 +6000,7 @@ export class Runtime {
|
|
|
5801
6000
|
value = (state.variables.HOME ?? "~") + value.slice(1);
|
|
5802
6001
|
append(value, !part.quoted, quotedPresence || shellValueByteLength(value) > 0);
|
|
5803
6002
|
}
|
|
5804
|
-
else if (part.kind === "variable" && part.name === "@" && part.quoted && !part.operator && split) {
|
|
6003
|
+
else if (part.kind === "variable" && part.name === "@" && part.quoted && !part.operator && !part.transform && split) {
|
|
5805
6004
|
for (let position = 0; position < state.positional.length; position++) {
|
|
5806
6005
|
if (position > 0)
|
|
5807
6006
|
addField();
|
|
@@ -5827,6 +6026,21 @@ export class Runtime {
|
|
|
5827
6026
|
for (const field of fields) {
|
|
5828
6027
|
if (!field.present && split)
|
|
5829
6028
|
continue;
|
|
6029
|
+
if (split && !field.independentPresence && field.quoteGroups) {
|
|
6030
|
+
const work = { remaining: this.budget.limits.maxExpansionBytes, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
|
|
6031
|
+
let empty = true;
|
|
6032
|
+
for (const group of field.quoteGroups) {
|
|
6033
|
+
const pending = stringCheckpoint(work);
|
|
6034
|
+
if (pending)
|
|
6035
|
+
await pending;
|
|
6036
|
+
if (!emptyNameGroups?.has(group)) {
|
|
6037
|
+
empty = false;
|
|
6038
|
+
break;
|
|
6039
|
+
}
|
|
6040
|
+
}
|
|
6041
|
+
if (empty)
|
|
6042
|
+
continue;
|
|
6043
|
+
}
|
|
5830
6044
|
if (scratch && field.fragments.length > 1 && !field.bytes)
|
|
5831
6045
|
scratch.reserve(field.fragments.reduce((bytes, value) => bytes + shellValueText(value).length * 2, 0), 0);
|
|
5832
6046
|
const assembled = concatShellValues(field.fragments, io[valueScope]);
|
|
@@ -6156,38 +6370,7 @@ export class Runtime {
|
|
|
6156
6370
|
}
|
|
6157
6371
|
// In-place heap sort preserves ordinary UTF-16 pathname order while
|
|
6158
6372
|
// charging comparisons, including long common prefixes, and yielding.
|
|
6159
|
-
|
|
6160
|
-
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
|
6161
|
-
const pending = stringCheckpoint(work);
|
|
6162
|
-
if (pending)
|
|
6163
|
-
await pending;
|
|
6164
|
-
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
6165
|
-
if (difference)
|
|
6166
|
-
return difference;
|
|
6167
|
-
}
|
|
6168
|
-
return left.length - right.length;
|
|
6169
|
-
};
|
|
6170
|
-
const sift = async (root, end) => {
|
|
6171
|
-
while (root * 2 + 1 < end) {
|
|
6172
|
-
let child = root * 2 + 1;
|
|
6173
|
-
if (child + 1 < end && await compare(found[child], found[child + 1]) < 0)
|
|
6174
|
-
child++;
|
|
6175
|
-
if (await compare(found[root], found[child]) >= 0)
|
|
6176
|
-
return;
|
|
6177
|
-
const saved = found[root];
|
|
6178
|
-
found[root] = found[child];
|
|
6179
|
-
found[child] = saved;
|
|
6180
|
-
root = child;
|
|
6181
|
-
}
|
|
6182
|
-
};
|
|
6183
|
-
for (let index = Math.floor(found.length / 2) - 1; index >= 0; index--)
|
|
6184
|
-
await sift(index, found.length);
|
|
6185
|
-
for (let end = found.length - 1; end > 0; end--) {
|
|
6186
|
-
const saved = found[0];
|
|
6187
|
-
found[0] = found[end];
|
|
6188
|
-
found[end] = saved;
|
|
6189
|
-
await sift(0, end);
|
|
6190
|
-
}
|
|
6373
|
+
await sortExpansionStrings(found, work);
|
|
6191
6374
|
return found.length ? found : [value];
|
|
6192
6375
|
}
|
|
6193
6376
|
finally {
|