@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.
@@ -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
- if (!(assignment.kind === "compound" && assignment.append && writes === 0)) {
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.word(word, state, io, false)).join("");
2362
- size += Buffer.byteLength(part);
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
- value += part;
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.word(redirect.target, state, currentIO(), false, false, hereString)).join("");
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 (Buffer.byteLength(value) >= this.budget.limits.maxExpansionBytes)
2487
+ if (shellValueByteLength(value) >= this.budget.limits.maxExpansionBytes)
2439
2488
  this.budget.fail("maxExpansionBytes");
2440
- value += "\n";
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
- const quotedPresence = part.quoted && !(arrayOwned && isQuoteMarker(part));
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" && selector?.kind === "members" && !part.length && split && (!part.quoted || selector.separator === "@")) {
5787
- const members = await this.arrayMembers(part.name, state);
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 value = members[position];
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.length > 0);
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
- const compare = async (left, right) => {
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 {