poe-code 15.0.4 → 15.0.5

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.
Files changed (88) hide show
  1. package/dist/metafile.json +1 -1
  2. package/package.json +8 -2
  3. package/packages/safe-bash/dist/chunks/chunk-2GV4WO7O.js +136 -0
  4. package/packages/safe-bash/dist/chunks/chunk-2GV4WO7O.js.map +7 -0
  5. package/packages/safe-bash/dist/chunks/chunk-54Z5EUCW.js +340 -0
  6. package/packages/safe-bash/dist/chunks/chunk-54Z5EUCW.js.map +7 -0
  7. package/packages/safe-bash/dist/chunks/{chunk-NYHAKXQ7.js → chunk-6MHYEBAJ.js} +16 -140
  8. package/packages/safe-bash/dist/chunks/chunk-6MHYEBAJ.js.map +7 -0
  9. package/packages/safe-bash/dist/chunks/{chunk-5MSOBYRG.js → chunk-IKATLB4I.js} +5 -3
  10. package/packages/safe-bash/dist/chunks/{chunk-5MSOBYRG.js.map → chunk-IKATLB4I.js.map} +1 -1
  11. package/packages/safe-bash/dist/chunks/{chunk-5OCQTFXT.js → chunk-LUJYX5H5.js} +6 -332
  12. package/packages/safe-bash/dist/chunks/{chunk-5OCQTFXT.js.map → chunk-LUJYX5H5.js.map} +4 -4
  13. package/packages/safe-bash/dist/chunks/chunk-UGMUVHIY.js +1603 -0
  14. package/packages/safe-bash/dist/chunks/chunk-UGMUVHIY.js.map +7 -0
  15. package/packages/safe-bash/dist/commands/network/args.d.ts +3 -0
  16. package/packages/safe-bash/dist/commands/network/args.d.ts.map +1 -1
  17. package/packages/safe-bash/dist/commands/network/args.js +1 -1
  18. package/packages/safe-bash/dist/commands/network/args.js.map +1 -1
  19. package/packages/safe-bash/dist/commands/network/body.js +2 -2
  20. package/packages/safe-bash/dist/commands/network/body.js.map +1 -1
  21. package/packages/safe-bash/dist/commands/network/curl.d.ts +12 -2
  22. package/packages/safe-bash/dist/commands/network/curl.d.ts.map +1 -1
  23. package/packages/safe-bash/dist/commands/network/curl.js +46 -27
  24. package/packages/safe-bash/dist/commands/network/curl.js.map +1 -1
  25. package/packages/safe-bash/dist/commands/network/index.browser.js +29 -0
  26. package/packages/safe-bash/dist/commands/network/index.browser.js.map +7 -0
  27. package/packages/safe-bash/dist/commands/network/index.d.ts +1 -10
  28. package/packages/safe-bash/dist/commands/network/index.d.ts.map +1 -1
  29. package/packages/safe-bash/dist/commands/network/index.js +1 -22
  30. package/packages/safe-bash/dist/commands/network/index.js.map +1 -1
  31. package/packages/safe-bash/dist/commands/network/platform.d.ts +4 -0
  32. package/packages/safe-bash/dist/commands/network/platform.d.ts.map +1 -0
  33. package/packages/safe-bash/dist/commands/network/platform.js +4 -0
  34. package/packages/safe-bash/dist/commands/network/platform.js.map +1 -0
  35. package/packages/safe-bash/dist/commands/network/public.d.ts +12 -0
  36. package/packages/safe-bash/dist/commands/network/public.d.ts.map +1 -0
  37. package/packages/safe-bash/dist/commands/network/public.js +27 -0
  38. package/packages/safe-bash/dist/commands/network/public.js.map +1 -0
  39. package/packages/safe-bash/dist/commands/network/shared.js +1 -1
  40. package/packages/safe-bash/dist/commands/network/shared.js.map +1 -1
  41. package/packages/safe-bash/dist/commands/network/wget.d.ts +4 -0
  42. package/packages/safe-bash/dist/commands/network/wget.d.ts.map +1 -0
  43. package/packages/safe-bash/dist/commands/network/wget.js +155 -0
  44. package/packages/safe-bash/dist/commands/network/wget.js.map +1 -0
  45. package/packages/safe-bash/dist/commands/xml/index.browser.js +3 -2
  46. package/packages/safe-bash/dist/commands/yq/index.browser.js +4 -2
  47. package/packages/safe-bash/dist/core.browser.js +1054 -459
  48. package/packages/safe-bash/dist/core.browser.js.map +4 -4
  49. package/packages/safe-bash/dist/core.d.ts +1 -5
  50. package/packages/safe-bash/dist/core.d.ts.map +1 -1
  51. package/packages/safe-bash/dist/core.js +1 -3
  52. package/packages/safe-bash/dist/core.js.map +1 -1
  53. package/packages/safe-bash/dist/shell/arrays/bindings.d.ts +15 -2
  54. package/packages/safe-bash/dist/shell/arrays/bindings.d.ts.map +1 -1
  55. package/packages/safe-bash/dist/shell/arrays/bindings.js +118 -5
  56. package/packages/safe-bash/dist/shell/arrays/bindings.js.map +1 -1
  57. package/packages/safe-bash/dist/shell/arrays/syntax.d.ts +5 -2
  58. package/packages/safe-bash/dist/shell/arrays/syntax.d.ts.map +1 -1
  59. package/packages/safe-bash/dist/shell/arrays/syntax.js +19 -6
  60. package/packages/safe-bash/dist/shell/arrays/syntax.js.map +1 -1
  61. package/packages/safe-bash/dist/shell/display.d.ts.map +1 -1
  62. package/packages/safe-bash/dist/shell/display.js +5 -1
  63. package/packages/safe-bash/dist/shell/display.js.map +1 -1
  64. package/packages/safe-bash/dist/shell/input.d.ts +10 -0
  65. package/packages/safe-bash/dist/shell/input.d.ts.map +1 -1
  66. package/packages/safe-bash/dist/shell/input.js +110 -0
  67. package/packages/safe-bash/dist/shell/input.js.map +1 -1
  68. package/packages/safe-bash/dist/shell/mapfile.d.ts +29 -0
  69. package/packages/safe-bash/dist/shell/mapfile.d.ts.map +1 -0
  70. package/packages/safe-bash/dist/shell/mapfile.js +134 -0
  71. package/packages/safe-bash/dist/shell/mapfile.js.map +1 -0
  72. package/packages/safe-bash/dist/shell/parameter-transforms.d.ts +12 -0
  73. package/packages/safe-bash/dist/shell/parameter-transforms.d.ts.map +1 -0
  74. package/packages/safe-bash/dist/shell/parameter-transforms.js +166 -0
  75. package/packages/safe-bash/dist/shell/parameter-transforms.js.map +1 -0
  76. package/packages/safe-bash/dist/shell/parser.d.ts +12 -0
  77. package/packages/safe-bash/dist/shell/parser.d.ts.map +1 -1
  78. package/packages/safe-bash/dist/shell/parser.js +75 -14
  79. package/packages/safe-bash/dist/shell/parser.js.map +1 -1
  80. package/packages/safe-bash/dist/shell/runtime.d.ts +7 -3
  81. package/packages/safe-bash/dist/shell/runtime.d.ts.map +1 -1
  82. package/packages/safe-bash/dist/shell/runtime.js +440 -63
  83. package/packages/safe-bash/dist/shell/runtime.js.map +1 -1
  84. package/packages/safe-bash/dist/shell/select-menu.d.ts +6 -0
  85. package/packages/safe-bash/dist/shell/select-menu.d.ts.map +1 -0
  86. package/packages/safe-bash/dist/shell/select-menu.js +103 -0
  87. package/packages/safe-bash/dist/shell/select-menu.js.map +1 -0
  88. package/packages/safe-bash/dist/chunks/chunk-NYHAKXQ7.js.map +0 -7
@@ -5,7 +5,7 @@ import { ACCESS_MODES, FsError, composeMiddleware, createBytePipe, pipeBytes, re
5
5
  import { concatShellValues, shellValueByteLength, shellValueBytes, shellValueFromBytes, shellValueText } from "../contracts/value.js";
6
6
  import { createCommandArguments, getCommandArguments } from "../contracts/command.js";
7
7
  import { ValueArena } from "./value-state.js";
8
- import { compoundEntryWords, HereDocumentSyntaxError, hereDocumentWords, parseShellInputUnit, parseShellUnit } from "./parser.js";
8
+ import { parseArraySubscript, compoundEntryWords, HereDocumentSyntaxError, hereDocumentWords, parseShellInputUnit, parseShellUnit } from "./parser.js";
9
9
  import { SourceLineIndex } from "./source-line-index.js";
10
10
  import { ShellLimitError, ShellSyntaxError } from "./types.js";
11
11
  import { scopeFileSystem } from "poe-code/safe-fs/core";
@@ -16,6 +16,7 @@ import { BraceExpansionFailure, expandBraces } from "./brace-expansion.js";
16
16
  import { evaluatePositionalArithmetic } from "./arithmetic-parameters.js";
17
17
  import { compilePattern, compilePatternBoundaries, matchesPattern } from "./pattern.js";
18
18
  import { nextCodePointOffset, previousCodePointOffset, scanString, stringCheckpoint } from "./string-operations.js";
19
+ import { selectMenu } from "./select-menu.js";
19
20
  import { byteLocale } from "./locale.js";
20
21
  import { functionDisplay } from "./display.js";
21
22
  import { ConditionalUnsupported, evaluateConditional } from "./conditional.js";
@@ -29,7 +30,8 @@ import { cloneGetoptsState, createGetoptsInput, createGetoptsState, GetoptsError
29
30
  import { activateChildCancellation, prepareChildCancellation, selectRuntimeCancellationOutcome, subscribeCancellation, } from "./cancellation.js";
30
31
  import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker, prefixNameQuoteGroups } from "./arrays/syntax.js";
31
32
  import { ArrayFailure, ArrayOwner, exactSum } from "./arrays/ledger.js";
32
- import { controlNames, IndexedBinding, textToken } from "./arrays/bindings.js";
33
+ import { controlNames, IndexedBinding, textToken, valueToken } from "./arrays/bindings.js";
34
+ import { collectMapfile, mapfileOptions, MapfileUsageError } from "./mapfile.js";
33
35
  import { arrayStore, guestArrays, requireArrays, snapshotState, stateMonitor, trackState } from "./arrays/state.js";
34
36
  import { publishPipelineStatus } from "./pipestatus.js";
35
37
  import { EreProfileLimitError, EreSyntaxError, EreUnsupportedError } from "../commands/regex-execution/ere/errors.js";
@@ -37,6 +39,7 @@ import { EreLedger } from "../commands/regex-execution/ere/limits.js";
37
39
  import { compileEre } from "../commands/regex-execution/ere/syntax.js";
38
40
  import { matchEre } from "../commands/regex-execution/ere/matcher.js";
39
41
  import { PathLookup, pathTargets } from "./path-lookup.js";
42
+ import { transformParameter } from "./parameter-transforms.js";
40
43
  export const defaultLimits = {
41
44
  maxParseUnits: defaultMaxParseUnits,
42
45
  maxInputBytes: 32 * 1024 * 1024,
@@ -56,7 +59,7 @@ export const defaultLimits = {
56
59
  pipeHighWaterMark: 64 * 1024,
57
60
  };
58
61
  const shellBuiltinNames = new Set([
59
- ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read",
62
+ ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read", "declare", "mapfile", "readarray",
60
63
  "exit", "return", "break", "continue", "command", "builtin", "type", "readonly", "echo", "printf", "test", "[", ".", "source", "eval", "getopts", "let", "pushd", "dirs", "popd", "shopt",
61
64
  ]);
62
65
  const implementedBuiltins = new Set([...shellBuiltinNames].filter(name => !["echo", "printf", "test", "["].includes(name)));
@@ -1079,6 +1082,7 @@ class InvocationCancellationOwner {
1079
1082
  this.#failures.push(...result.failures);
1080
1083
  }
1081
1084
  }
1085
+ const mapfileCallbackStates = new WeakSet();
1082
1086
  export class Runtime {
1083
1087
  fs;
1084
1088
  commands;
@@ -1407,6 +1411,16 @@ export class Runtime {
1407
1411
  if (error instanceof NounsetFailure || error instanceof Flow || error instanceof ShellLimitError || error instanceof ShellSyntaxError)
1408
1412
  throw error;
1409
1413
  }
1414
+ arithmeticValue(program, state, io) {
1415
+ return evaluatePositionalArithmetic(program, {
1416
+ parseBudget: this.budget.parsing,
1417
+ positional: state.positional, arg0: state.arg0 ?? "virtual-bash", owner: arrayStore(state)?.owner,
1418
+ maximumBytes: this.budget.limits.maxExpansionBytes,
1419
+ checkpoint: () => this.signal.throwIfAborted(),
1420
+ requireParameter: (name, value) => this.requireParameter(value, name, state, io),
1421
+ limit: () => this.budget.fail("maxExpansionBytes"),
1422
+ }, prepared => evaluateArithmetic(prepared, this.arithmeticVariables(state, io.diagnosticLine), this.budget.parsing));
1423
+ }
1410
1424
  arithmeticVariables(state, line) {
1411
1425
  return new Proxy(state.variables, {
1412
1426
  get: (target, key) => {
@@ -1423,7 +1437,7 @@ export class Runtime {
1423
1437
  }
1424
1438
  variable(state, name) {
1425
1439
  const binding = arrayStore(state)?.get(name);
1426
- return binding ? binding.get(0) : state.variables[name];
1440
+ return binding ? binding.get(binding.associative ? binding.keys.get("30")?.index ?? -1 : 0) : state.variables[name];
1427
1441
  }
1428
1442
  requireParameter(value, name, state, io, line) {
1429
1443
  if (state.nounset && value === undefined)
@@ -1434,7 +1448,7 @@ export class Runtime {
1434
1448
  this.writeVariable(state, name, value, origin);
1435
1449
  return;
1436
1450
  }
1437
- await this.arrayZero(state, name, async () => shellValueText(value));
1451
+ await this.arrayZero(state, name, async () => value);
1438
1452
  }
1439
1453
  async prepareVariable(state, name, saved, scalarLegacy = false) {
1440
1454
  const store = requireArrays(state);
@@ -1579,7 +1593,7 @@ export class Runtime {
1579
1593
  await operation.ledger.checkpoint(this.signal, 2);
1580
1594
  }
1581
1595
  if (typeof index === "number")
1582
- staged.values.get(index)?.slot.release();
1596
+ staged.remove(index);
1583
1597
  staged.maximum = maximum;
1584
1598
  }
1585
1599
  this.signal.throwIfAborted();
@@ -1617,6 +1631,7 @@ export class Runtime {
1617
1631
  const store = requireArrays(state);
1618
1632
  const operation = ArrayOwner.create(store.owner.ledger, store.owner);
1619
1633
  const holding = store.owner.hold();
1634
+ const valueAllocation = this.budget.values.scope();
1620
1635
  let staged;
1621
1636
  try {
1622
1637
  const watch = await store.watch(name, operation, this.signal);
@@ -1637,10 +1652,13 @@ export class Runtime {
1637
1652
  if (!current)
1638
1653
  throw new ArrayFailure("stale binding");
1639
1654
  staged = await current.copy(this.signal);
1640
- const value = append ? await this.arrayJoin(operation, [current.get(0) ?? "", expanded], "") : expanded;
1641
- const token = await textToken(staged.owner, value, this.signal);
1655
+ const index = staged.associative ? (await staged.keyIndex("0", operation, this.signal, true)) : 0;
1656
+ const previous = current.values.get(index)?.text.rawValue ?? current.get(index) ?? "";
1657
+ const value = !append ? expanded : typeof previous === "string" && typeof expanded === "string"
1658
+ ? await this.arrayJoin(operation, [previous, expanded], "") : concatShellValues([previous, expanded], valueAllocation);
1659
+ const token = await valueToken(staged.owner, value, this.signal);
1642
1660
  try {
1643
- staged.insert(0, token);
1661
+ staged.insert(index, token);
1644
1662
  }
1645
1663
  catch (error) {
1646
1664
  token.release();
@@ -1671,6 +1689,7 @@ export class Runtime {
1671
1689
  await operation.close();
1672
1690
  }
1673
1691
  finally {
1692
+ valueAllocation.close();
1674
1693
  holding.release();
1675
1694
  }
1676
1695
  }
@@ -1689,11 +1708,35 @@ export class Runtime {
1689
1708
  await owner.ledger.checkpoint(this.signal);
1690
1709
  return result;
1691
1710
  }
1711
+ async arrayIndex(binding, index, state, io, owner, create = false) {
1712
+ if (!binding?.associative) {
1713
+ const selected = index.source === undefined ? index : literalIndex(index.source, 0, this.budget.parsing);
1714
+ const number = numericIndex(selected);
1715
+ if (number === undefined)
1716
+ throw new ArrayFailure("index outside 0..2147483647");
1717
+ return number;
1718
+ }
1719
+ const word = index.word ?? parseArraySubscript(index.source ?? index.decimal, this.budget.parsing, byteLocale(state.variables), state.depth);
1720
+ const fields = await this.valueWord(word, state, io, false);
1721
+ const value = fields.length === 1 ? fields[0] : concatShellValues(fields, io[valueScope]);
1722
+ if (shellValueByteLength(value) === 0) {
1723
+ await this.diagnostic(io, "associative array: bad array subscript");
1724
+ if (create)
1725
+ throw completedExit(1);
1726
+ return undefined;
1727
+ }
1728
+ return binding.keyIndex(value, owner, this.signal, create);
1729
+ }
1692
1730
  async arrayAssignment(assignment, state, io) {
1693
1731
  const name = assignment.name;
1694
1732
  this.signal.throwIfAborted();
1695
- if (state.readonlyVariables?.has(name))
1733
+ if (state.readonlyVariables?.has(name)) {
1734
+ if (arrayStore(state)?.get(name)?.associative) {
1735
+ await this.diagnostic(io, `${name}: readonly variable`);
1736
+ throw completedExit(1);
1737
+ }
1696
1738
  throw new ArrayFailure("readonly binding");
1739
+ }
1697
1740
  if (controlNames.has(name))
1698
1741
  throw new ArrayFailure("control binding cannot be indexed");
1699
1742
  if (state.exported.has(name))
@@ -1705,11 +1748,13 @@ export class Runtime {
1705
1748
  try {
1706
1749
  const watch = await store.watch(name, operation, this.signal);
1707
1750
  const current = store.get(name);
1751
+ if (current?.associative && assignment.kind === "compound" && assignment.entries.length)
1752
+ throw new ArrayFailure("nonempty associative compound assignment is unsupported");
1708
1753
  const initialMaximum = current?.maximum ?? (state.variables[name] === undefined ? -1 : 0);
1709
1754
  let planned = assignment.append && assignment.kind === "compound" ? initialMaximum + 1 : 0;
1710
1755
  if (assignment.kind === "element") {
1711
1756
  operation.reserve({ work: assignment.index.decimal.length + 1 }).release();
1712
- if (numericIndex(assignment.index) === undefined)
1757
+ if (!current?.associative && numericIndex(literalIndex(assignment.index.source ?? assignment.index.decimal, 0, this.budget.parsing)) === undefined)
1713
1758
  throw new ArrayFailure("index outside 0..2147483647");
1714
1759
  }
1715
1760
  else
@@ -1743,9 +1788,9 @@ export class Runtime {
1743
1788
  const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
1744
1789
  const prepared = await store.prepareName(name, operation, this.signal);
1745
1790
  const preserve = assignment.kind === "element" || assignment.append;
1746
- staged = preserve && current ? await current.copy(this.signal) : IndexedBinding.create(store.owner);
1791
+ staged = preserve && current ? await current.copy(this.signal) : IndexedBinding.create(store.owner, current?.associative);
1747
1792
  if (preserve && !current && state.variables[name] !== undefined) {
1748
- const token = await textToken(staged.owner, state.variables[name], this.signal);
1793
+ const token = await valueToken(staged.owner, stateMonitor(state)?.values.get(name, state.variables[name]) ?? state.variables[name], this.signal);
1749
1794
  try {
1750
1795
  staged.insert(0, token);
1751
1796
  }
@@ -1759,7 +1804,7 @@ export class Runtime {
1759
1804
  const insert = async (index, value) => {
1760
1805
  if (index > 2147483647)
1761
1806
  throw new ArrayFailure("index outside 0..2147483647");
1762
- const token = await textToken(staged.owner, value, this.signal);
1807
+ const token = await valueToken(staged.owner, value, this.signal);
1763
1808
  try {
1764
1809
  staged.insert(index, token);
1765
1810
  }
@@ -1769,12 +1814,14 @@ export class Runtime {
1769
1814
  }
1770
1815
  writes++;
1771
1816
  };
1817
+ const join = async (values) => values.every(value => typeof value === "string")
1818
+ ? this.arrayJoin(operation, values, "") : concatShellValues(values, io[valueScope]);
1772
1819
  if (assignment.kind === "element") {
1773
- const index = numericIndex(assignment.index);
1774
- const fields = await this.word(assignment.value, state, io, false);
1775
- let value = await this.arrayJoin(operation, fields, "");
1820
+ const index = (await this.arrayIndex(staged, assignment.index, state, io, operation, true));
1821
+ const fields = await this.valueWord(assignment.value, state, io, false);
1822
+ let value = await join(fields);
1776
1823
  if (assignment.append)
1777
- value = await this.arrayJoin(operation, [staged.get(index) ?? "", value], "");
1824
+ value = await join([staged.values.get(index)?.text.rawValue ?? staged.get(index) ?? "", value]);
1778
1825
  await insert(index, value);
1779
1826
  }
1780
1827
  else
@@ -1788,17 +1835,17 @@ export class Runtime {
1788
1835
  expandedEntry = true;
1789
1836
  const values = await this.valueWord(expanded, state, io, true, false, false, false, undefined, false);
1790
1837
  for (const value of values) {
1791
- await insert(cursor, shellValueText(value));
1838
+ await insert(cursor, value);
1792
1839
  cursor++;
1793
1840
  }
1794
1841
  }
1795
1842
  if (expandedEntry)
1796
1843
  continue;
1797
1844
  }
1798
- const fields = await this.word(entry.value, state, io, entry.index === undefined);
1845
+ const fields = await this.valueWord(entry.value, state, io, entry.index === undefined);
1799
1846
  if (entry.index) {
1800
1847
  const index = numericIndex(entry.index);
1801
- await insert(index, await this.arrayJoin(operation, fields, ""));
1848
+ await insert(index, await join(fields));
1802
1849
  cursor = index + 1;
1803
1850
  }
1804
1851
  else
@@ -2188,14 +2235,7 @@ export class Runtime {
2188
2235
  }
2189
2236
  if (command.kind === "arithmetic") {
2190
2237
  try {
2191
- return Number(evaluatePositionalArithmetic(command.expression, {
2192
- parseBudget: this.budget.parsing,
2193
- positional: state.positional, arg0: state.arg0 ?? "virtual-bash", owner: arrayStore(state)?.owner,
2194
- maximumBytes: this.budget.limits.maxExpansionBytes,
2195
- checkpoint: () => this.signal.throwIfAborted(),
2196
- requireParameter: (name, value) => this.requireParameter(value, name, state, io),
2197
- limit: () => this.budget.fail("maxExpansionBytes"),
2198
- }, (prepared) => evaluateArithmetic(prepared, this.arithmeticVariables(state, io.diagnosticLine), this.budget.parsing)) === 0n);
2238
+ return Number(this.arithmeticValue(command.expression, state, io) === 0n);
2199
2239
  }
2200
2240
  catch (error) {
2201
2241
  this.rethrowArithmeticControl(error);
@@ -2265,6 +2305,101 @@ export class Runtime {
2265
2305
  break;
2266
2306
  }
2267
2307
  }
2308
+ else if (command.kind === "arithmetic-for") {
2309
+ const evaluate = async (program) => {
2310
+ if (!program)
2311
+ return 1n;
2312
+ try {
2313
+ return this.arithmeticValue(program, state, io);
2314
+ }
2315
+ catch (error) {
2316
+ this.rethrowArithmeticControl(error);
2317
+ await this.diagnostic(io, `((: ${message(error, this.budget.onInternalError)}`);
2318
+ return undefined;
2319
+ }
2320
+ };
2321
+ if (await evaluate(command.expressions[0]) === undefined)
2322
+ return 1;
2323
+ while (true) {
2324
+ this.budget.loop();
2325
+ await yieldTurn(this.signal);
2326
+ const condition = await evaluate(command.expressions[1]);
2327
+ if (condition === undefined)
2328
+ return 1;
2329
+ if (condition === 0n)
2330
+ break;
2331
+ const result = await this.loopBody(command.body, state, io);
2332
+ status = result.status;
2333
+ if (result.stop)
2334
+ break;
2335
+ if (await evaluate(command.expressions[2]) === undefined)
2336
+ return 1;
2337
+ }
2338
+ }
2339
+ else if (command.kind === "select") {
2340
+ const values = command.words ? await this.valueWords(command.words, state, io) : this.positionalValues(state);
2341
+ if (!values.length)
2342
+ return 0;
2343
+ const input = io.stdin instanceof ShellInput ? io.stdin : new ShellInput(io.stdin, this.budget, this.signal);
2344
+ const work = { remaining: this.budget.limits.maxExpansionBytes * 8, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
2345
+ let showMenu = true;
2346
+ while (true) {
2347
+ this.budget.loop();
2348
+ if (showMenu)
2349
+ await selectMenu(values, state.variables.COLUMNS, byteLocale(state.variables), io.stderr, work, allocation);
2350
+ const prompt = stateMonitor(state)?.values.get("PS3", state.variables.PS3 ?? "#? ") ?? state.variables.PS3 ?? "#? ";
2351
+ await io.stderr.write(shellValueBytes(prompt, allocation));
2352
+ const line = await input.selectLine(allocation);
2353
+ if (state.readonlyVariables?.has("REPLY")) {
2354
+ await this.diagnostic(io, "REPLY: readonly variable");
2355
+ await writeText(io.stdout, "\n");
2356
+ return 1;
2357
+ }
2358
+ this.writeVariable(state, "REPLY", line.value);
2359
+ if (!line.terminated) {
2360
+ await writeText(io.stdout, "\n");
2361
+ return 1;
2362
+ }
2363
+ const bytes = shellValueBytes(line.value, allocation);
2364
+ if (!bytes.length) {
2365
+ showMenu = true;
2366
+ continue;
2367
+ }
2368
+ let choice = 0;
2369
+ let phase = "start";
2370
+ let valid = true;
2371
+ let negative = false;
2372
+ let digits = false;
2373
+ for (const byte of bytes) {
2374
+ const pending = stringCheckpoint(work);
2375
+ if (pending)
2376
+ await pending;
2377
+ if (byte === 32 || byte >= 9 && byte <= 13) {
2378
+ if (phase === "digits")
2379
+ phase = "tail";
2380
+ continue;
2381
+ }
2382
+ if (phase === "start" && (byte === 43 || byte === 45)) {
2383
+ negative = byte === 45;
2384
+ phase = "digits";
2385
+ continue;
2386
+ }
2387
+ if (phase === "tail" || byte < 48 || byte > 57) {
2388
+ valid = false;
2389
+ continue;
2390
+ }
2391
+ phase = "digits";
2392
+ digits = true;
2393
+ choice = Math.min(values.length + 1, choice * 10 + byte - 48);
2394
+ }
2395
+ await this.assignVariable(state, command.name, valid && digits && !negative && choice >= 1 && choice <= values.length ? values[choice - 1] : "");
2396
+ const result = await this.loopBody(command.body, state, io);
2397
+ status = result.status;
2398
+ if (result.stop)
2399
+ break;
2400
+ showMenu = shellValueByteLength(stateMonitor(state)?.values.get("REPLY", state.variables.REPLY ?? "") ?? state.variables.REPLY ?? "") === 0;
2401
+ }
2402
+ }
2268
2403
  else {
2269
2404
  while (true) {
2270
2405
  this.budget.loop();
@@ -2384,6 +2519,7 @@ export class Runtime {
2384
2519
  async document(document, state, io, line = document.endLine) {
2385
2520
  this.signal.throwIfAborted();
2386
2521
  let value = "";
2522
+ let fragments;
2387
2523
  let size = 0;
2388
2524
  let words = 0;
2389
2525
  const warnings = [];
@@ -2394,18 +2530,28 @@ export class Runtime {
2394
2530
  await writeDiagnostic(io.stderr, `shell: warning: ${warning}\n`);
2395
2531
  if (++words % 128 === 0)
2396
2532
  await yieldTurn(this.signal);
2397
- const part = (await this.word(word, state, { ...io, nameExpansionContext: "document" }, false)).join("");
2398
- size += Buffer.byteLength(part);
2533
+ const part = concatShellValues(await this.valueWord(word, state, { ...io, nameExpansionContext: "document" }, false), io[valueScope]);
2534
+ size += shellValueByteLength(part);
2399
2535
  if (size > this.budget.limits.maxExpansionBytes)
2400
2536
  this.budget.fail("maxExpansionBytes");
2401
- value += part;
2537
+ if (!fragments && typeof part === "string")
2538
+ value += part;
2539
+ else {
2540
+ if (!fragments) {
2541
+ io[valueScope]?.reserve(64 + value.length * 2, 0);
2542
+ fragments = [value];
2543
+ value = "";
2544
+ }
2545
+ io[valueScope]?.reserve(32 + (typeof part === "string" ? part.length * 2 : 0), 0);
2546
+ fragments.push(part);
2547
+ }
2402
2548
  }
2403
2549
  }
2404
2550
  finally {
2405
2551
  for (const warning of warnings.splice(0))
2406
2552
  await writeDiagnostic(io.stderr, `shell: warning: ${warning}\n`);
2407
2553
  }
2408
- return value;
2554
+ return fragments ? concatShellValues(fragments, io[valueScope]) : value;
2409
2555
  }
2410
2556
  async redirect(redirects, state, io, inputs, outputs, isolatedInlineInput = false, persistMoves = false, fileShortcut = false, line) {
2411
2557
  this.signal.throwIfAborted();
@@ -2437,6 +2583,7 @@ export class Runtime {
2437
2583
  const stdinIsDefault = descriptor?.input ? descriptor.stdinIsDefault : false;
2438
2584
  return {
2439
2585
  [invocationScope]: io[invocationScope],
2586
+ ...(io[valueScope] === undefined ? {} : { [valueScope]: io[valueScope] }),
2440
2587
  ...(io.execution === undefined ? {} : { execution: io.execution }),
2441
2588
  ...(io.diagnosticLine === undefined ? {} : { diagnosticLine: io.diagnosticLine }),
2442
2589
  ...(io.diagnosticOffset === undefined ? {} : { diagnosticOffset: io.diagnosticOffset }),
@@ -2457,7 +2604,7 @@ export class Runtime {
2457
2604
  const hereString = redirect.operator === "<<<";
2458
2605
  let value;
2459
2606
  try {
2460
- value = redirect.document ? await this.document(redirect.document, state, currentIO(), line) : (await this.word(redirect.target, state, currentIO(), false, false, hereString)).join("");
2607
+ value = redirect.document ? await this.document(redirect.document, state, currentIO(), line) : concatShellValues(await this.valueWord(redirect.target, state, currentIO(), false, false, hereString), io[valueScope]);
2461
2608
  }
2462
2609
  catch (error) {
2463
2610
  if (error instanceof NounsetFailure)
@@ -2471,11 +2618,11 @@ export class Runtime {
2471
2618
  throw error;
2472
2619
  }
2473
2620
  if (hereString) {
2474
- if (Buffer.byteLength(value) >= this.budget.limits.maxExpansionBytes)
2621
+ if (shellValueByteLength(value) >= this.budget.limits.maxExpansionBytes)
2475
2622
  this.budget.fail("maxExpansionBytes");
2476
- value += "\n";
2623
+ value = concatShellValues([value, "\n"], io[valueScope]);
2477
2624
  }
2478
- const input = new ShellInput(toByteSource(value), this.budget, this.signal);
2625
+ const input = new ShellInput(toByteSource(typeof value === "string" ? value : shellValueBytes(value, io[valueScope])), this.budget, this.signal);
2479
2626
  inputs.add(input);
2480
2627
  descriptors.set(redirect.descriptor, { input, stdinIsDefault: false });
2481
2628
  continue;
@@ -2706,8 +2853,8 @@ export class Runtime {
2706
2853
  throw new ArrayFailure("indexed binding cannot be a command prefix");
2707
2854
  }
2708
2855
  await this.arrayZero(state, assignment.name, async () => {
2709
- const fields = await this.word(assignment.value, state, io, false);
2710
- return this.arrayJoin(requireArrays(state).owner, fields, "");
2856
+ const fields = await this.valueWord(assignment.value, state, io, false);
2857
+ return fields.every(value => typeof value === "string") ? this.arrayJoin(requireArrays(state).owner, fields, "") : concatShellValues(fields, io[valueScope]);
2711
2858
  }, assignment.append);
2712
2859
  continue;
2713
2860
  }
@@ -2870,6 +3017,7 @@ export class Runtime {
2870
3017
  }
2871
3018
  async dispatchScoped(name, values, state, io, assignments, bypassFunctions) {
2872
3019
  const { [invocationScope]: scope, ...publicIO } = io;
3020
+ Reflect.deleteProperty(publicIO, valueScope);
2873
3021
  const allocation = this.budget.values.scope();
2874
3022
  scope.register(() => allocation.close());
2875
3023
  const argumentValues = this.admitArguments(values, allocation);
@@ -2911,7 +3059,8 @@ export class Runtime {
2911
3059
  scope.assertOpen();
2912
3060
  const forwardedValues = getCommandArguments(forwarded);
2913
3061
  const admitted = forwardedValues === argumentValues ? argumentValues : this.admitArguments(forwardedValues.values, allocation);
2914
- const context = { ...forwarded, args: admitted.args, argumentValues: admitted, [invocationScope]: scope };
3062
+ const context = { ...forwarded, args: admitted.args, argumentValues: admitted, [invocationScope]: scope,
3063
+ ...(io[valueScope] === undefined ? {} : { [valueScope]: io[valueScope] }) };
2915
3064
  const previous = new Map();
2916
3065
  const cwd = state.cwd;
2917
3066
  const directoryStackCwdPublication = state.directoryStackCwdPublication;
@@ -3047,6 +3196,9 @@ export class Runtime {
3047
3196
  functionRestoration?.close();
3048
3197
  throw error;
3049
3198
  }
3199
+ const callerLoopDepth = state.loopDepth;
3200
+ if (mapfileCallbackStates.has(state))
3201
+ state.loopDepth = 0;
3050
3202
  try {
3051
3203
  return { exitCode: await this.command(body, state, { ...io, ...context, scriptName: body.sourceName ?? io.scriptName ?? "shell" }) };
3052
3204
  }
@@ -3061,6 +3213,7 @@ export class Runtime {
3061
3213
  savedPositionals.close();
3062
3214
  state.positionalSetVersion = positionalSetVersion;
3063
3215
  state.functionDepth--;
3216
+ state.loopDepth = callerLoopDepth;
3064
3217
  state.depth--;
3065
3218
  state.locals.pop();
3066
3219
  };
@@ -3558,6 +3711,8 @@ export class Runtime {
3558
3711
  return invocation;
3559
3712
  },
3560
3713
  };
3714
+ Reflect.deleteProperty(context, invocationScope);
3715
+ Reflect.deleteProperty(context, valueScope);
3561
3716
  bindCommandIO(context);
3562
3717
  bindFileOutputBudget(context, sink => this.budget.sink(sink, runtime.signal));
3563
3718
  if (argumentValues.values.every(value => typeof value === "string"))
@@ -4677,6 +4832,137 @@ export class Runtime {
4677
4832
  allocation.close();
4678
4833
  }
4679
4834
  }
4835
+ async mapfileBuiltin(context, state) {
4836
+ const allocation = this.budget.values.scope();
4837
+ const work = { remaining: this.budget.limits.maxExpansionBytes * 8, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
4838
+ let pinned;
4839
+ let releaseHolding;
4840
+ try {
4841
+ const options = await mapfileOptions(context, work, allocation);
4842
+ if (arrayStore(state)?.get(options.name)?.associative)
4843
+ throw new MapfileUsageError(`${options.name}: not an indexed array`, 1);
4844
+ if (state.readonlyVariables?.has(options.name))
4845
+ throw new MapfileUsageError(`${options.name}: readonly variable`, 1);
4846
+ if (controlNames.has(options.name) || state.exported.has(options.name))
4847
+ throw new ArrayFailure("control or exported binding cannot be indexed");
4848
+ const store = requireArrays(state);
4849
+ const holding = store.owner.hold();
4850
+ releaseHolding = () => holding.release();
4851
+ if (!options.preserve || !store.get(options.name))
4852
+ await this.arrayAssignment({ kind: "compound", name: options.name, append: options.preserve, entries: [] }, state, context);
4853
+ const entry = store.bindings.get(options.name);
4854
+ allocation.reserve(128 + Buffer.byteLength(options.name) * 2, 0);
4855
+ pinned = entry.binding.retain();
4856
+ const input = context.stdin instanceof ShellInput ? context.stdin : new ShellInput(context.stdin, this.budget, this.signal);
4857
+ let pendingFlow;
4858
+ await collectMapfile(options, input, {
4859
+ allocation: () => this.budget.values.scope(),
4860
+ loop: () => this.budget.loop(),
4861
+ write: async (index, value) => {
4862
+ if (pinned !== entry.binding) {
4863
+ const previous = pinned;
4864
+ pinned = entry.binding.retain();
4865
+ await previous.release();
4866
+ }
4867
+ const operation = ArrayOwner.create(store.owner.ledger, store.owner);
4868
+ let staged;
4869
+ try {
4870
+ const current = pinned;
4871
+ const attached = store.bindings.get(options.name) === entry;
4872
+ const watch = attached ? await store.watch(options.name, operation, this.signal) : undefined;
4873
+ const supersede = attached ? await stateMonitor(state).prepareTypedPublication(options.name, operation, this.signal) : undefined;
4874
+ if (attached)
4875
+ await this.prepareArrayObservers(state, operation);
4876
+ const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
4877
+ // Exclude the mapfile pin and the live name's reference from COW.
4878
+ staged = current.references > (attached ? 2 : 1) ? await current.copy(this.signal) : current.retain();
4879
+ const token = await valueToken(staged.owner, value, this.signal);
4880
+ try {
4881
+ this.signal.throwIfAborted();
4882
+ if (watch && !watch.valid())
4883
+ throw new ArrayFailure("stale binding");
4884
+ staged.insert(index, token);
4885
+ }
4886
+ catch (error) {
4887
+ token.release();
4888
+ throw error;
4889
+ }
4890
+ if (attached) {
4891
+ let released;
4892
+ stateMonitor(state).publish(tickets, options.name, () => {
4893
+ supersede();
4894
+ released = store.publish(options.name, staged, tickets);
4895
+ });
4896
+ staged = undefined;
4897
+ await released;
4898
+ }
4899
+ else {
4900
+ const previous = pinned;
4901
+ pinned = staged;
4902
+ entry.binding = staged;
4903
+ staged = undefined;
4904
+ await previous.release();
4905
+ }
4906
+ }
4907
+ finally {
4908
+ try {
4909
+ await staged?.release();
4910
+ }
4911
+ finally {
4912
+ await operation.close();
4913
+ }
4914
+ }
4915
+ },
4916
+ callback: async (source, index, value) => {
4917
+ if (pendingFlow)
4918
+ return;
4919
+ const callbackAllocation = this.budget.values.scope();
4920
+ const nested = mapfileCallbackStates.has(state);
4921
+ try {
4922
+ const quoted = shellValueText(await transformParameter(value, "Q", { maximumBytes: this.budget.limits.maxExpansionBytes, byteLocale: byteLocale(state.variables), work, allocation: callbackAllocation }));
4923
+ if (Buffer.byteLength(source) + Buffer.byteLength(quoted) + String(index).length + 2 > this.budget.limits.maxExpansionBytes)
4924
+ this.budget.fail("maxExpansionBytes");
4925
+ mapfileCallbackStates.add(state);
4926
+ await this.evalBuiltin({ ...context, args: [source, String(index), quoted] }, state, context, false);
4927
+ }
4928
+ catch (error) {
4929
+ if (error instanceof Flow && (error.kind === "break" || error.kind === "continue"))
4930
+ pendingFlow = error;
4931
+ else
4932
+ throw error;
4933
+ }
4934
+ finally {
4935
+ if (!nested)
4936
+ mapfileCallbackStates.delete(state);
4937
+ callbackAllocation.close();
4938
+ }
4939
+ },
4940
+ });
4941
+ if (pendingFlow)
4942
+ throw pendingFlow;
4943
+ return 0;
4944
+ }
4945
+ catch (error) {
4946
+ this.signal.throwIfAborted();
4947
+ if (!(error instanceof MapfileUsageError))
4948
+ throw error;
4949
+ await this.diagnostic(context, `${context.command}: ${error.message}`);
4950
+ return error.status;
4951
+ }
4952
+ finally {
4953
+ try {
4954
+ await pinned?.release();
4955
+ }
4956
+ finally {
4957
+ try {
4958
+ releaseHolding?.();
4959
+ }
4960
+ finally {
4961
+ allocation.close();
4962
+ }
4963
+ }
4964
+ }
4965
+ }
4680
4966
  async builtin(context, state, assignments, diagnose, suppressSpecial = false) {
4681
4967
  const { command, args, stdout, stderr } = context;
4682
4968
  if (command === ":" || command === "true")
@@ -4687,6 +4973,8 @@ export class Runtime {
4687
4973
  return this.shoptBuiltin(context, state);
4688
4974
  if (command === "let")
4689
4975
  return this.letBuiltin(context, state);
4976
+ if (command === "mapfile" || command === "readarray")
4977
+ return this.mapfileBuiltin(context, state);
4690
4978
  if (command === "getopts")
4691
4979
  return this.getoptsBuiltin(context, state);
4692
4980
  if (command === "pushd" || command === "dirs" || command === "popd")
@@ -4760,9 +5048,16 @@ export class Runtime {
4760
5048
  this.replacePositionals(state, this.positionalValues(state).slice(count));
4761
5049
  return 0;
4762
5050
  }
4763
- if (command === "export" || command === "local" || command === "readonly") {
5051
+ if (command === "export" || command === "local" || command === "readonly" || command === "declare") {
5052
+ const associativeDeclaration = command === "declare";
4764
5053
  const declarationArgs = [...args];
4765
- let indexedLocal = false;
5054
+ let indexedLocal = associativeDeclaration;
5055
+ if (associativeDeclaration) {
5056
+ if (declarationArgs.shift() !== "-A" || !declarationArgs.length) {
5057
+ await this.diagnostic(context, "declare: only -A NAME is supported");
5058
+ return 2;
5059
+ }
5060
+ }
4766
5061
  if (command === "local") {
4767
5062
  const options = localDeclarationOptions(declarationArgs, this.signal);
4768
5063
  if (options.error !== undefined) {
@@ -4825,13 +5120,13 @@ export class Runtime {
4825
5120
  status = 1;
4826
5121
  continue;
4827
5122
  }
4828
- if (command === "local" && indexedLocal) {
5123
+ if ((command === "local" || associativeDeclaration) && indexedLocal) {
4829
5124
  if (controlNames.has(name))
4830
5125
  throw new ArrayFailure("control binding cannot be indexed");
4831
5126
  if (state.exported.has(name))
4832
5127
  throw new ArrayFailure("exported binding cannot be indexed");
4833
- const existingLocal = locals.get(name);
4834
- const saved = existingLocal ? undefined : assignments.get(name) ?? saveVariable(state, name);
5128
+ const existingLocal = locals?.get(name);
5129
+ const saved = !locals || existingLocal ? undefined : assignments.get(name) ?? saveVariable(state, name);
4835
5130
  let operation;
4836
5131
  let holding;
4837
5132
  let shadow;
@@ -4856,12 +5151,15 @@ export class Runtime {
4856
5151
  const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
4857
5152
  const prepared = await store.prepareName(name, operation, this.signal);
4858
5153
  const current = store.get(name);
4859
- shadow = !saved && current ? await current.copy(this.signal) : IndexedBinding.create(store.owner);
5154
+ if (!saved && current && current.associative !== associativeDeclaration)
5155
+ throw new ArrayFailure("cannot convert array kind");
5156
+ shadow = !saved && current ? await current.copy(this.signal) : IndexedBinding.create(store.owner, associativeDeclaration);
4860
5157
  const value = match[2] ?? (!saved && !current && Object.hasOwn(state.variables, name) ? state.variables[name] : undefined);
4861
5158
  if (value !== undefined) {
4862
- const token = await textToken(shadow.owner, value, this.signal);
5159
+ const retained = associativeDeclaration && match[2] === undefined ? stateMonitor(state).values.get(name, value) : value;
5160
+ const token = await valueToken(shadow.owner, retained, this.signal);
4863
5161
  try {
4864
- shadow.insert(0, token);
5162
+ shadow.insert(associativeDeclaration ? (await shadow.keyIndex("0", operation, this.signal, true)) : 0, token);
4865
5163
  }
4866
5164
  catch (error) {
4867
5165
  token.release();
@@ -5036,7 +5334,8 @@ export class Runtime {
5036
5334
  }
5037
5335
  if (command === "unset") {
5038
5336
  let status = 0;
5039
- for (const name of args) {
5337
+ for (let argument = 0; argument < args.length; argument++) {
5338
+ const name = args[argument];
5040
5339
  const selected = /^([a-zA-Z_][a-zA-Z_0-9]*)\[(.*)\]$/su.exec(name);
5041
5340
  if (selected) {
5042
5341
  const base = selected[1];
@@ -5046,7 +5345,16 @@ export class Runtime {
5046
5345
  status = 1;
5047
5346
  continue;
5048
5347
  }
5049
- if (selector === "@" || selector === "*")
5348
+ const binding = arrayStore(state)?.get(base);
5349
+ if (binding?.associative) {
5350
+ const original = getCommandArguments(context).values[argument];
5351
+ const index = typeof original === "string"
5352
+ ? await this.arrayIndex(binding, { decimal: selector, source: selector }, state, context, binding.owner)
5353
+ : await binding.keyIndex(shellValueFromBytes(shellValueBytes(original, context[valueScope]).subarray(base.length + 1, shellValueByteLength(original) - 1), context[valueScope]), binding.owner, this.signal);
5354
+ if (index !== undefined)
5355
+ await this.unsetIndexed(state, base, index);
5356
+ }
5357
+ else if (selector === "@" || selector === "*")
5050
5358
  await this.unsetIndexed(state, base, "members");
5051
5359
  else {
5052
5360
  let index;
@@ -5304,6 +5612,35 @@ export class Runtime {
5304
5612
  }
5305
5613
  }
5306
5614
  async valuePart(part, state, io, hereString = false) {
5615
+ if (part.kind === "variable" && part.transform) {
5616
+ const selector = getArraySelector(part);
5617
+ if (selector?.kind === "members" || part.name === "@" || part.name === "*") {
5618
+ const members = selector ? await this.arrayMembers(part.name, state, io, part.keys) : this.positionalValues(state);
5619
+ const ifs = state.variables.IFS ?? " ";
5620
+ const separator = ifs.length ? String.fromCodePoint(ifs.codePointAt(0)) : "";
5621
+ const fragments = [];
5622
+ let bytes = 0;
5623
+ for (const member of members) {
5624
+ const value = await this.transformValue(member, part.transform, state, io);
5625
+ bytes += shellValueByteLength(value) + (fragments.length ? Buffer.byteLength(separator) : 0);
5626
+ if (bytes > this.budget.limits.maxExpansionBytes)
5627
+ this.budget.fail("maxExpansionBytes");
5628
+ io[valueScope]?.reserve(32, 0);
5629
+ if (fragments.length)
5630
+ fragments.push(separator);
5631
+ fragments.push(value);
5632
+ }
5633
+ return concatShellValues(fragments, io[valueScope]);
5634
+ }
5635
+ const base = { ...part };
5636
+ delete base.transform;
5637
+ copyArraySelector(part, base);
5638
+ 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)
5639
+ : /^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name) ? this.variable(state, part.name)
5640
+ : /^[1-9][0-9]*$/u.test(part.name) ? state.positional[Number(part.name) - 1] : "";
5641
+ const value = await this.valuePart(base, state, io, hereString);
5642
+ return existing === undefined ? "" : this.transformValue(value, part.transform, state, io);
5643
+ }
5307
5644
  if (part.kind === "variable" && part.prefixNames) {
5308
5645
  const ifs = state.variables.IFS ?? " ";
5309
5646
  const separator = io.nameExpansionContext === "document" || io.nameExpansionContext === "conditional" && part.prefixNames === "@"
@@ -5324,10 +5661,25 @@ export class Runtime {
5324
5661
  const binding = part.kind === "variable" ? arrayStore(state)?.get(part.name) : undefined;
5325
5662
  const holding = binding ? requireArrays(state).owner.hold() : undefined;
5326
5663
  const selector = getArraySelector(part);
5327
- const index = selector?.kind === "element" ? numericIndex(selector.index) : 0;
5664
+ if (selector?.kind === "element" && binding?.associative) {
5665
+ try {
5666
+ const index = await this.arrayIndex(binding, selector.index, state, io, binding.owner);
5667
+ const token = index === undefined ? undefined : binding.values.get(index)?.text;
5668
+ this.requireParameter(token?.value, part.kind === "variable" ? part.name : "", state, io);
5669
+ if (part.kind === "variable" && part.length)
5670
+ return this.parameterLength(token?.value ?? "");
5671
+ return token?.rawValue ? shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]) : token?.value ?? "";
5672
+ }
5673
+ finally {
5674
+ holding?.release();
5675
+ }
5676
+ }
5677
+ const index = selector?.kind === "element" ? numericIndex(selector.index) : binding?.associative ? binding.keys.get("30")?.index : 0;
5328
5678
  const token = index === undefined || selector?.kind === "members" ? undefined : binding?.values.get(index)?.text;
5329
5679
  token?.retain();
5330
5680
  try {
5681
+ if (token?.rawValue && part.kind === "variable" && !part.length && !part.operator && !part.substring)
5682
+ return shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]);
5331
5683
  const value = await this.partValue(part, state, io, hereString);
5332
5684
  if (binding)
5333
5685
  await textToken(requireArrays(state).owner, shellValueText(value), this.signal);
@@ -5345,6 +5697,19 @@ export class Runtime {
5345
5697
  throw new ShellSyntaxError("Syntax nesting exceeds 64", word.offset);
5346
5698
  return { ...io, parameterDepth };
5347
5699
  }
5700
+ async transformValue(value, operator, state, io) {
5701
+ const allocation = io[valueScope] ?? this.budget.values.scope();
5702
+ try {
5703
+ return await transformParameter(value, operator, {
5704
+ maximumBytes: this.budget.limits.maxExpansionBytes, byteLocale: byteLocale(state.variables), allocation,
5705
+ work: { remaining: Math.min(Number.MAX_SAFE_INTEGER, this.budget.limits.maxExpansionBytes * 8 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") },
5706
+ });
5707
+ }
5708
+ finally {
5709
+ if (!io[valueScope])
5710
+ allocation.close();
5711
+ }
5712
+ }
5348
5713
  async partValue(part, state, io, hereString) {
5349
5714
  this.signal.throwIfAborted();
5350
5715
  if (part.kind === "compound-substitution-eof") {
@@ -5444,8 +5809,12 @@ export class Runtime {
5444
5809
  }
5445
5810
  if (part.length)
5446
5811
  return String(binding?.values.size ?? (state.variables[part.name] === undefined ? 0 : 1));
5447
- const values = await this.arrayMembers(part.name, state);
5448
- return this.arrayJoin(store.owner, values, Array.from(state.variables.IFS ?? " ")[0] ?? "");
5812
+ const values = await this.arrayMembers(part.name, state, io, part.keys);
5813
+ const separator = Array.from(state.variables.IFS ?? " ")[0] ?? "";
5814
+ if (values.every(value => typeof value === "string"))
5815
+ return this.arrayJoin(store.owner, values, separator);
5816
+ io[valueScope]?.reserve(values.length * 32 + 64, 0);
5817
+ return concatShellValues(values.flatMap((value, index) => index ? [separator, value] : [value]), io[valueScope]);
5449
5818
  }
5450
5819
  let value = part.name === "?" ? String(state.status)
5451
5820
  : part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}${state.braceexpand !== false ? "B" : ""}`
@@ -5769,7 +6138,7 @@ export class Runtime {
5769
6138
  return fields;
5770
6139
  }
5771
6140
  const arrayOwned = word.parts.some(part => part.kind === "variable" && !part.prefixNames && (getArraySelector(part) !== undefined || arrayStore(state)?.get(part.name) !== undefined));
5772
- const prefixOwned = word.parts.some(part => part.kind === "variable" && part.prefixNames === "@");
6141
+ const prefixOwned = word.parts.some(part => part.kind === "variable" && (part.prefixNames === "@" || part.transform && part.name === "@"));
5773
6142
  const owner = arrayOwned ? requireArrays(state).owner : undefined;
5774
6143
  const holding = owner?.hold();
5775
6144
  const scratch = !owner && split && state.variables.IFS !== "" && word.parts.some(part => !part.quoted && part.kind !== "text")
@@ -5921,17 +6290,23 @@ export class Runtime {
5921
6290
  emptyNameGroups.add(quoteGroup);
5922
6291
  }
5923
6292
  }
5924
- else if (part.kind === "variable" && selector?.kind === "members" && !part.length && split && (!part.quoted || selector.separator === "@")) {
5925
- const members = await this.arrayMembers(part.name, state);
6293
+ else if (part.kind === "variable" && split && (selector?.kind === "members" && !part.length && (!part.quoted || selector.separator === "@") || part.transform && part.name === "@")) {
6294
+ const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state, io, part.keys) : this.positionalValues(state);
5926
6295
  for (let position = 0; position < members.length; position++) {
5927
6296
  if (position > 0)
5928
6297
  addField();
5929
- const value = members[position];
6298
+ const original = members[position];
6299
+ const value = part.transform ? await this.transformValue(original, part.transform, state, partIO) : original;
5930
6300
  if (part.quoted || state.variables.IFS === "")
5931
- append(value, !part.quoted, part.quoted || value.length > 0);
6301
+ append(value, !part.quoted, part.quoted || shellValueByteLength(value) > 0);
5932
6302
  else
5933
6303
  await appendSplit(value);
5934
6304
  }
6305
+ if (part.transform && members.length === 0 && quoteGroup) {
6306
+ io[valueScope]?.reserve(32, 0);
6307
+ emptyNameGroups ??= new Set();
6308
+ emptyNameGroups.add(quoteGroup);
6309
+ }
5935
6310
  }
5936
6311
  else if (part.kind === "text" && !splitText) {
5937
6312
  let value = invokedValues.get(part) ?? part.byteValue ?? part.value;
@@ -5939,7 +6314,7 @@ export class Runtime {
5939
6314
  value = (state.variables.HOME ?? "~") + value.slice(1);
5940
6315
  append(value, !part.quoted, quotedPresence || shellValueByteLength(value) > 0);
5941
6316
  }
5942
- else if (part.kind === "variable" && part.name === "@" && part.quoted && !part.operator && split) {
6317
+ else if (part.kind === "variable" && part.name === "@" && part.quoted && !part.operator && !part.transform && split) {
5943
6318
  for (let position = 0; position < state.positional.length; position++) {
5944
6319
  if (position > 0)
5945
6320
  addField();
@@ -6091,7 +6466,7 @@ export class Runtime {
6091
6466
  allocation.hold(value);
6092
6467
  return createCommandArguments(values, allocation);
6093
6468
  }
6094
- async arrayMembers(name, state) {
6469
+ async arrayMembers(name, state, io, keys = false) {
6095
6470
  const store = requireArrays(state);
6096
6471
  const holding = store.owner.hold();
6097
6472
  try {
@@ -6103,7 +6478,7 @@ export class Runtime {
6103
6478
  return [];
6104
6479
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
6105
6480
  await textToken(store.owner, value, this.signal);
6106
- return [value];
6481
+ return [keys ? "0" : value];
6107
6482
  }
6108
6483
  binding.retain();
6109
6484
  try {
@@ -6111,8 +6486,10 @@ export class Runtime {
6111
6486
  const values = [];
6112
6487
  for (const index of indices) {
6113
6488
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 4 });
6114
- const value = binding.get(index);
6115
- await textToken(store.owner, value, this.signal);
6489
+ const token = keys ? binding.associative ? binding.keys.get(binding.keyByIndex.get(index)).text : await textToken(store.owner, String(index), this.signal) : binding.values.get(index).text;
6490
+ const value = token.rawValue === undefined ? token.value : shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]);
6491
+ if (typeof value === "string")
6492
+ await textToken(store.owner, value, this.signal);
6116
6493
  values.push(value);
6117
6494
  await store.owner.ledger.checkpoint(this.signal);
6118
6495
  }