@poe-platform/safe-bash 0.1.527 → 0.1.529

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.
@@ -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-platform/safe-fs/core";
@@ -30,7 +30,8 @@ import { cloneGetoptsState, createGetoptsInput, createGetoptsState, GetoptsError
30
30
  import { activateChildCancellation, prepareChildCancellation, selectRuntimeCancellationOutcome, subscribeCancellation, } from "./cancellation.js";
31
31
  import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker, prefixNameQuoteGroups } from "./arrays/syntax.js";
32
32
  import { ArrayFailure, ArrayOwner, exactSum } from "./arrays/ledger.js";
33
- 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";
34
35
  import { arrayStore, guestArrays, requireArrays, snapshotState, stateMonitor, trackState } from "./arrays/state.js";
35
36
  import { publishPipelineStatus } from "./pipestatus.js";
36
37
  import { EreProfileLimitError, EreSyntaxError, EreUnsupportedError } from "../commands/regex-execution/ere/errors.js";
@@ -58,7 +59,7 @@ export const defaultLimits = {
58
59
  pipeHighWaterMark: 64 * 1024,
59
60
  };
60
61
  const shellBuiltinNames = new Set([
61
- ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read",
62
+ ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read", "declare", "mapfile", "readarray",
62
63
  "exit", "return", "break", "continue", "command", "builtin", "type", "readonly", "echo", "printf", "test", "[", ".", "source", "eval", "getopts", "let", "pushd", "dirs", "popd", "shopt",
63
64
  ]);
64
65
  const implementedBuiltins = new Set([...shellBuiltinNames].filter(name => !["echo", "printf", "test", "["].includes(name)));
@@ -1081,6 +1082,7 @@ class InvocationCancellationOwner {
1081
1082
  this.#failures.push(...result.failures);
1082
1083
  }
1083
1084
  }
1085
+ const mapfileCallbackStates = new WeakSet();
1084
1086
  export class Runtime {
1085
1087
  fs;
1086
1088
  commands;
@@ -1435,7 +1437,7 @@ export class Runtime {
1435
1437
  }
1436
1438
  variable(state, name) {
1437
1439
  const binding = arrayStore(state)?.get(name);
1438
- 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];
1439
1441
  }
1440
1442
  requireParameter(value, name, state, io, line) {
1441
1443
  if (state.nounset && value === undefined)
@@ -1446,7 +1448,7 @@ export class Runtime {
1446
1448
  this.writeVariable(state, name, value, origin);
1447
1449
  return;
1448
1450
  }
1449
- await this.arrayZero(state, name, async () => shellValueText(value));
1451
+ await this.arrayZero(state, name, async () => value);
1450
1452
  }
1451
1453
  async prepareVariable(state, name, saved, scalarLegacy = false) {
1452
1454
  const store = requireArrays(state);
@@ -1591,7 +1593,7 @@ export class Runtime {
1591
1593
  await operation.ledger.checkpoint(this.signal, 2);
1592
1594
  }
1593
1595
  if (typeof index === "number")
1594
- staged.values.get(index)?.slot.release();
1596
+ staged.remove(index);
1595
1597
  staged.maximum = maximum;
1596
1598
  }
1597
1599
  this.signal.throwIfAborted();
@@ -1629,6 +1631,7 @@ export class Runtime {
1629
1631
  const store = requireArrays(state);
1630
1632
  const operation = ArrayOwner.create(store.owner.ledger, store.owner);
1631
1633
  const holding = store.owner.hold();
1634
+ const valueAllocation = this.budget.values.scope();
1632
1635
  let staged;
1633
1636
  try {
1634
1637
  const watch = await store.watch(name, operation, this.signal);
@@ -1649,10 +1652,13 @@ export class Runtime {
1649
1652
  if (!current)
1650
1653
  throw new ArrayFailure("stale binding");
1651
1654
  staged = await current.copy(this.signal);
1652
- const value = append ? await this.arrayJoin(operation, [current.get(0) ?? "", expanded], "") : expanded;
1653
- 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);
1654
1660
  try {
1655
- staged.insert(0, token);
1661
+ staged.insert(index, token);
1656
1662
  }
1657
1663
  catch (error) {
1658
1664
  token.release();
@@ -1683,6 +1689,7 @@ export class Runtime {
1683
1689
  await operation.close();
1684
1690
  }
1685
1691
  finally {
1692
+ valueAllocation.close();
1686
1693
  holding.release();
1687
1694
  }
1688
1695
  }
@@ -1701,11 +1708,35 @@ export class Runtime {
1701
1708
  await owner.ledger.checkpoint(this.signal);
1702
1709
  return result;
1703
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
+ }
1704
1730
  async arrayAssignment(assignment, state, io) {
1705
1731
  const name = assignment.name;
1706
1732
  this.signal.throwIfAborted();
1707
- 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
+ }
1708
1738
  throw new ArrayFailure("readonly binding");
1739
+ }
1709
1740
  if (controlNames.has(name))
1710
1741
  throw new ArrayFailure("control binding cannot be indexed");
1711
1742
  if (state.exported.has(name))
@@ -1717,11 +1748,13 @@ export class Runtime {
1717
1748
  try {
1718
1749
  const watch = await store.watch(name, operation, this.signal);
1719
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");
1720
1753
  const initialMaximum = current?.maximum ?? (state.variables[name] === undefined ? -1 : 0);
1721
1754
  let planned = assignment.append && assignment.kind === "compound" ? initialMaximum + 1 : 0;
1722
1755
  if (assignment.kind === "element") {
1723
1756
  operation.reserve({ work: assignment.index.decimal.length + 1 }).release();
1724
- if (numericIndex(assignment.index) === undefined)
1757
+ if (!current?.associative && numericIndex(literalIndex(assignment.index.source ?? assignment.index.decimal, 0, this.budget.parsing)) === undefined)
1725
1758
  throw new ArrayFailure("index outside 0..2147483647");
1726
1759
  }
1727
1760
  else
@@ -1755,9 +1788,9 @@ export class Runtime {
1755
1788
  const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
1756
1789
  const prepared = await store.prepareName(name, operation, this.signal);
1757
1790
  const preserve = assignment.kind === "element" || assignment.append;
1758
- 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);
1759
1792
  if (preserve && !current && state.variables[name] !== undefined) {
1760
- 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);
1761
1794
  try {
1762
1795
  staged.insert(0, token);
1763
1796
  }
@@ -1771,7 +1804,7 @@ export class Runtime {
1771
1804
  const insert = async (index, value) => {
1772
1805
  if (index > 2147483647)
1773
1806
  throw new ArrayFailure("index outside 0..2147483647");
1774
- const token = await textToken(staged.owner, value, this.signal);
1807
+ const token = await valueToken(staged.owner, value, this.signal);
1775
1808
  try {
1776
1809
  staged.insert(index, token);
1777
1810
  }
@@ -1781,12 +1814,14 @@ export class Runtime {
1781
1814
  }
1782
1815
  writes++;
1783
1816
  };
1817
+ const join = async (values) => values.every(value => typeof value === "string")
1818
+ ? this.arrayJoin(operation, values, "") : concatShellValues(values, io[valueScope]);
1784
1819
  if (assignment.kind === "element") {
1785
- const index = numericIndex(assignment.index);
1786
- const fields = await this.word(assignment.value, state, io, false);
1787
- 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);
1788
1823
  if (assignment.append)
1789
- value = await this.arrayJoin(operation, [staged.get(index) ?? "", value], "");
1824
+ value = await join([staged.values.get(index)?.text.rawValue ?? staged.get(index) ?? "", value]);
1790
1825
  await insert(index, value);
1791
1826
  }
1792
1827
  else
@@ -1800,17 +1835,17 @@ export class Runtime {
1800
1835
  expandedEntry = true;
1801
1836
  const values = await this.valueWord(expanded, state, io, true, false, false, false, undefined, false);
1802
1837
  for (const value of values) {
1803
- await insert(cursor, shellValueText(value));
1838
+ await insert(cursor, value);
1804
1839
  cursor++;
1805
1840
  }
1806
1841
  }
1807
1842
  if (expandedEntry)
1808
1843
  continue;
1809
1844
  }
1810
- 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);
1811
1846
  if (entry.index) {
1812
1847
  const index = numericIndex(entry.index);
1813
- await insert(index, await this.arrayJoin(operation, fields, ""));
1848
+ await insert(index, await join(fields));
1814
1849
  cursor = index + 1;
1815
1850
  }
1816
1851
  else
@@ -2818,8 +2853,8 @@ export class Runtime {
2818
2853
  throw new ArrayFailure("indexed binding cannot be a command prefix");
2819
2854
  }
2820
2855
  await this.arrayZero(state, assignment.name, async () => {
2821
- const fields = await this.word(assignment.value, state, io, false);
2822
- 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]);
2823
2858
  }, assignment.append);
2824
2859
  continue;
2825
2860
  }
@@ -3161,6 +3196,9 @@ export class Runtime {
3161
3196
  functionRestoration?.close();
3162
3197
  throw error;
3163
3198
  }
3199
+ const callerLoopDepth = state.loopDepth;
3200
+ if (mapfileCallbackStates.has(state))
3201
+ state.loopDepth = 0;
3164
3202
  try {
3165
3203
  return { exitCode: await this.command(body, state, { ...io, ...context, scriptName: body.sourceName ?? io.scriptName ?? "shell" }) };
3166
3204
  }
@@ -3175,6 +3213,7 @@ export class Runtime {
3175
3213
  savedPositionals.close();
3176
3214
  state.positionalSetVersion = positionalSetVersion;
3177
3215
  state.functionDepth--;
3216
+ state.loopDepth = callerLoopDepth;
3178
3217
  state.depth--;
3179
3218
  state.locals.pop();
3180
3219
  };
@@ -4793,6 +4832,137 @@ export class Runtime {
4793
4832
  allocation.close();
4794
4833
  }
4795
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
+ }
4796
4966
  async builtin(context, state, assignments, diagnose, suppressSpecial = false) {
4797
4967
  const { command, args, stdout, stderr } = context;
4798
4968
  if (command === ":" || command === "true")
@@ -4803,6 +4973,8 @@ export class Runtime {
4803
4973
  return this.shoptBuiltin(context, state);
4804
4974
  if (command === "let")
4805
4975
  return this.letBuiltin(context, state);
4976
+ if (command === "mapfile" || command === "readarray")
4977
+ return this.mapfileBuiltin(context, state);
4806
4978
  if (command === "getopts")
4807
4979
  return this.getoptsBuiltin(context, state);
4808
4980
  if (command === "pushd" || command === "dirs" || command === "popd")
@@ -4876,9 +5048,16 @@ export class Runtime {
4876
5048
  this.replacePositionals(state, this.positionalValues(state).slice(count));
4877
5049
  return 0;
4878
5050
  }
4879
- if (command === "export" || command === "local" || command === "readonly") {
5051
+ if (command === "export" || command === "local" || command === "readonly" || command === "declare") {
5052
+ const associativeDeclaration = command === "declare";
4880
5053
  const declarationArgs = [...args];
4881
- 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
+ }
4882
5061
  if (command === "local") {
4883
5062
  const options = localDeclarationOptions(declarationArgs, this.signal);
4884
5063
  if (options.error !== undefined) {
@@ -4941,13 +5120,13 @@ export class Runtime {
4941
5120
  status = 1;
4942
5121
  continue;
4943
5122
  }
4944
- if (command === "local" && indexedLocal) {
5123
+ if ((command === "local" || associativeDeclaration) && indexedLocal) {
4945
5124
  if (controlNames.has(name))
4946
5125
  throw new ArrayFailure("control binding cannot be indexed");
4947
5126
  if (state.exported.has(name))
4948
5127
  throw new ArrayFailure("exported binding cannot be indexed");
4949
- const existingLocal = locals.get(name);
4950
- 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);
4951
5130
  let operation;
4952
5131
  let holding;
4953
5132
  let shadow;
@@ -4972,12 +5151,15 @@ export class Runtime {
4972
5151
  const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
4973
5152
  const prepared = await store.prepareName(name, operation, this.signal);
4974
5153
  const current = store.get(name);
4975
- 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);
4976
5157
  const value = match[2] ?? (!saved && !current && Object.hasOwn(state.variables, name) ? state.variables[name] : undefined);
4977
5158
  if (value !== undefined) {
4978
- 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);
4979
5161
  try {
4980
- shadow.insert(0, token);
5162
+ shadow.insert(associativeDeclaration ? (await shadow.keyIndex("0", operation, this.signal, true)) : 0, token);
4981
5163
  }
4982
5164
  catch (error) {
4983
5165
  token.release();
@@ -5152,7 +5334,8 @@ export class Runtime {
5152
5334
  }
5153
5335
  if (command === "unset") {
5154
5336
  let status = 0;
5155
- for (const name of args) {
5337
+ for (let argument = 0; argument < args.length; argument++) {
5338
+ const name = args[argument];
5156
5339
  const selected = /^([a-zA-Z_][a-zA-Z_0-9]*)\[(.*)\]$/su.exec(name);
5157
5340
  if (selected) {
5158
5341
  const base = selected[1];
@@ -5162,7 +5345,16 @@ export class Runtime {
5162
5345
  status = 1;
5163
5346
  continue;
5164
5347
  }
5165
- 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 === "*")
5166
5358
  await this.unsetIndexed(state, base, "members");
5167
5359
  else {
5168
5360
  let index;
@@ -5423,7 +5615,7 @@ export class Runtime {
5423
5615
  if (part.kind === "variable" && part.transform) {
5424
5616
  const selector = getArraySelector(part);
5425
5617
  if (selector?.kind === "members" || part.name === "@" || part.name === "*") {
5426
- const members = selector ? await this.arrayMembers(part.name, state) : this.positionalValues(state);
5618
+ const members = selector ? await this.arrayMembers(part.name, state, io, part.keys) : this.positionalValues(state);
5427
5619
  const ifs = state.variables.IFS ?? " ";
5428
5620
  const separator = ifs.length ? String.fromCodePoint(ifs.codePointAt(0)) : "";
5429
5621
  const fragments = [];
@@ -5469,10 +5661,25 @@ export class Runtime {
5469
5661
  const binding = part.kind === "variable" ? arrayStore(state)?.get(part.name) : undefined;
5470
5662
  const holding = binding ? requireArrays(state).owner.hold() : undefined;
5471
5663
  const selector = getArraySelector(part);
5472
- 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;
5473
5678
  const token = index === undefined || selector?.kind === "members" ? undefined : binding?.values.get(index)?.text;
5474
5679
  token?.retain();
5475
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]);
5476
5683
  const value = await this.partValue(part, state, io, hereString);
5477
5684
  if (binding)
5478
5685
  await textToken(requireArrays(state).owner, shellValueText(value), this.signal);
@@ -5602,8 +5809,12 @@ export class Runtime {
5602
5809
  }
5603
5810
  if (part.length)
5604
5811
  return String(binding?.values.size ?? (state.variables[part.name] === undefined ? 0 : 1));
5605
- const values = await this.arrayMembers(part.name, state);
5606
- 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]);
5607
5818
  }
5608
5819
  let value = part.name === "?" ? String(state.status)
5609
5820
  : part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}${state.braceexpand !== false ? "B" : ""}`
@@ -6080,7 +6291,7 @@ export class Runtime {
6080
6291
  }
6081
6292
  }
6082
6293
  else if (part.kind === "variable" && split && (selector?.kind === "members" && !part.length && (!part.quoted || selector.separator === "@") || part.transform && part.name === "@")) {
6083
- const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state) : this.positionalValues(state);
6294
+ const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state, io, part.keys) : this.positionalValues(state);
6084
6295
  for (let position = 0; position < members.length; position++) {
6085
6296
  if (position > 0)
6086
6297
  addField();
@@ -6255,7 +6466,7 @@ export class Runtime {
6255
6466
  allocation.hold(value);
6256
6467
  return createCommandArguments(values, allocation);
6257
6468
  }
6258
- async arrayMembers(name, state) {
6469
+ async arrayMembers(name, state, io, keys = false) {
6259
6470
  const store = requireArrays(state);
6260
6471
  const holding = store.owner.hold();
6261
6472
  try {
@@ -6267,7 +6478,7 @@ export class Runtime {
6267
6478
  return [];
6268
6479
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
6269
6480
  await textToken(store.owner, value, this.signal);
6270
- return [value];
6481
+ return [keys ? "0" : value];
6271
6482
  }
6272
6483
  binding.retain();
6273
6484
  try {
@@ -6275,8 +6486,10 @@ export class Runtime {
6275
6486
  const values = [];
6276
6487
  for (const index of indices) {
6277
6488
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 4 });
6278
- const value = binding.get(index);
6279
- 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);
6280
6493
  values.push(value);
6281
6494
  await store.owner.ledger.checkpoint(this.signal);
6282
6495
  }