@poe-platform/safe-bash 0.1.528 → 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";
@@ -59,7 +59,7 @@ export const defaultLimits = {
59
59
  pipeHighWaterMark: 64 * 1024,
60
60
  };
61
61
  const shellBuiltinNames = new Set([
62
- ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read", "mapfile", "readarray",
62
+ ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read", "declare", "mapfile", "readarray",
63
63
  "exit", "return", "break", "continue", "command", "builtin", "type", "readonly", "echo", "printf", "test", "[", ".", "source", "eval", "getopts", "let", "pushd", "dirs", "popd", "shopt",
64
64
  ]);
65
65
  const implementedBuiltins = new Set([...shellBuiltinNames].filter(name => !["echo", "printf", "test", "["].includes(name)));
@@ -1437,7 +1437,7 @@ export class Runtime {
1437
1437
  }
1438
1438
  variable(state, name) {
1439
1439
  const binding = arrayStore(state)?.get(name);
1440
- 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];
1441
1441
  }
1442
1442
  requireParameter(value, name, state, io, line) {
1443
1443
  if (state.nounset && value === undefined)
@@ -1593,7 +1593,7 @@ export class Runtime {
1593
1593
  await operation.ledger.checkpoint(this.signal, 2);
1594
1594
  }
1595
1595
  if (typeof index === "number")
1596
- staged.values.get(index)?.slot.release();
1596
+ staged.remove(index);
1597
1597
  staged.maximum = maximum;
1598
1598
  }
1599
1599
  this.signal.throwIfAborted();
@@ -1652,12 +1652,13 @@ export class Runtime {
1652
1652
  if (!current)
1653
1653
  throw new ArrayFailure("stale binding");
1654
1654
  staged = await current.copy(this.signal);
1655
- const previous = current.values.get(0)?.text.rawValue ?? current.get(0) ?? "";
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) ?? "";
1656
1657
  const value = !append ? expanded : typeof previous === "string" && typeof expanded === "string"
1657
1658
  ? await this.arrayJoin(operation, [previous, expanded], "") : concatShellValues([previous, expanded], valueAllocation);
1658
1659
  const token = await valueToken(staged.owner, value, this.signal);
1659
1660
  try {
1660
- staged.insert(0, token);
1661
+ staged.insert(index, token);
1661
1662
  }
1662
1663
  catch (error) {
1663
1664
  token.release();
@@ -1707,11 +1708,35 @@ export class Runtime {
1707
1708
  await owner.ledger.checkpoint(this.signal);
1708
1709
  return result;
1709
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
+ }
1710
1730
  async arrayAssignment(assignment, state, io) {
1711
1731
  const name = assignment.name;
1712
1732
  this.signal.throwIfAborted();
1713
- 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
+ }
1714
1738
  throw new ArrayFailure("readonly binding");
1739
+ }
1715
1740
  if (controlNames.has(name))
1716
1741
  throw new ArrayFailure("control binding cannot be indexed");
1717
1742
  if (state.exported.has(name))
@@ -1723,11 +1748,13 @@ export class Runtime {
1723
1748
  try {
1724
1749
  const watch = await store.watch(name, operation, this.signal);
1725
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");
1726
1753
  const initialMaximum = current?.maximum ?? (state.variables[name] === undefined ? -1 : 0);
1727
1754
  let planned = assignment.append && assignment.kind === "compound" ? initialMaximum + 1 : 0;
1728
1755
  if (assignment.kind === "element") {
1729
1756
  operation.reserve({ work: assignment.index.decimal.length + 1 }).release();
1730
- if (numericIndex(assignment.index) === undefined)
1757
+ if (!current?.associative && numericIndex(literalIndex(assignment.index.source ?? assignment.index.decimal, 0, this.budget.parsing)) === undefined)
1731
1758
  throw new ArrayFailure("index outside 0..2147483647");
1732
1759
  }
1733
1760
  else
@@ -1761,7 +1788,7 @@ export class Runtime {
1761
1788
  const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
1762
1789
  const prepared = await store.prepareName(name, operation, this.signal);
1763
1790
  const preserve = assignment.kind === "element" || assignment.append;
1764
- 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);
1765
1792
  if (preserve && !current && state.variables[name] !== undefined) {
1766
1793
  const token = await valueToken(staged.owner, stateMonitor(state)?.values.get(name, state.variables[name]) ?? state.variables[name], this.signal);
1767
1794
  try {
@@ -1790,7 +1817,7 @@ export class Runtime {
1790
1817
  const join = async (values) => values.every(value => typeof value === "string")
1791
1818
  ? this.arrayJoin(operation, values, "") : concatShellValues(values, io[valueScope]);
1792
1819
  if (assignment.kind === "element") {
1793
- const index = numericIndex(assignment.index);
1820
+ const index = (await this.arrayIndex(staged, assignment.index, state, io, operation, true));
1794
1821
  const fields = await this.valueWord(assignment.value, state, io, false);
1795
1822
  let value = await join(fields);
1796
1823
  if (assignment.append)
@@ -4812,6 +4839,8 @@ export class Runtime {
4812
4839
  let releaseHolding;
4813
4840
  try {
4814
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);
4815
4844
  if (state.readonlyVariables?.has(options.name))
4816
4845
  throw new MapfileUsageError(`${options.name}: readonly variable`, 1);
4817
4846
  if (controlNames.has(options.name) || state.exported.has(options.name))
@@ -5019,9 +5048,16 @@ export class Runtime {
5019
5048
  this.replacePositionals(state, this.positionalValues(state).slice(count));
5020
5049
  return 0;
5021
5050
  }
5022
- if (command === "export" || command === "local" || command === "readonly") {
5051
+ if (command === "export" || command === "local" || command === "readonly" || command === "declare") {
5052
+ const associativeDeclaration = command === "declare";
5023
5053
  const declarationArgs = [...args];
5024
- 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
+ }
5025
5061
  if (command === "local") {
5026
5062
  const options = localDeclarationOptions(declarationArgs, this.signal);
5027
5063
  if (options.error !== undefined) {
@@ -5084,13 +5120,13 @@ export class Runtime {
5084
5120
  status = 1;
5085
5121
  continue;
5086
5122
  }
5087
- if (command === "local" && indexedLocal) {
5123
+ if ((command === "local" || associativeDeclaration) && indexedLocal) {
5088
5124
  if (controlNames.has(name))
5089
5125
  throw new ArrayFailure("control binding cannot be indexed");
5090
5126
  if (state.exported.has(name))
5091
5127
  throw new ArrayFailure("exported binding cannot be indexed");
5092
- const existingLocal = locals.get(name);
5093
- 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);
5094
5130
  let operation;
5095
5131
  let holding;
5096
5132
  let shadow;
@@ -5115,12 +5151,15 @@ export class Runtime {
5115
5151
  const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
5116
5152
  const prepared = await store.prepareName(name, operation, this.signal);
5117
5153
  const current = store.get(name);
5118
- 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);
5119
5157
  const value = match[2] ?? (!saved && !current && Object.hasOwn(state.variables, name) ? state.variables[name] : undefined);
5120
5158
  if (value !== undefined) {
5121
- 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);
5122
5161
  try {
5123
- shadow.insert(0, token);
5162
+ shadow.insert(associativeDeclaration ? (await shadow.keyIndex("0", operation, this.signal, true)) : 0, token);
5124
5163
  }
5125
5164
  catch (error) {
5126
5165
  token.release();
@@ -5295,7 +5334,8 @@ export class Runtime {
5295
5334
  }
5296
5335
  if (command === "unset") {
5297
5336
  let status = 0;
5298
- for (const name of args) {
5337
+ for (let argument = 0; argument < args.length; argument++) {
5338
+ const name = args[argument];
5299
5339
  const selected = /^([a-zA-Z_][a-zA-Z_0-9]*)\[(.*)\]$/su.exec(name);
5300
5340
  if (selected) {
5301
5341
  const base = selected[1];
@@ -5305,7 +5345,16 @@ export class Runtime {
5305
5345
  status = 1;
5306
5346
  continue;
5307
5347
  }
5308
- 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 === "*")
5309
5358
  await this.unsetIndexed(state, base, "members");
5310
5359
  else {
5311
5360
  let index;
@@ -5566,7 +5615,7 @@ export class Runtime {
5566
5615
  if (part.kind === "variable" && part.transform) {
5567
5616
  const selector = getArraySelector(part);
5568
5617
  if (selector?.kind === "members" || part.name === "@" || part.name === "*") {
5569
- const members = selector ? await this.arrayMembers(part.name, state, io) : this.positionalValues(state);
5618
+ const members = selector ? await this.arrayMembers(part.name, state, io, part.keys) : this.positionalValues(state);
5570
5619
  const ifs = state.variables.IFS ?? " ";
5571
5620
  const separator = ifs.length ? String.fromCodePoint(ifs.codePointAt(0)) : "";
5572
5621
  const fragments = [];
@@ -5612,7 +5661,20 @@ export class Runtime {
5612
5661
  const binding = part.kind === "variable" ? arrayStore(state)?.get(part.name) : undefined;
5613
5662
  const holding = binding ? requireArrays(state).owner.hold() : undefined;
5614
5663
  const selector = getArraySelector(part);
5615
- 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;
5616
5678
  const token = index === undefined || selector?.kind === "members" ? undefined : binding?.values.get(index)?.text;
5617
5679
  token?.retain();
5618
5680
  try {
@@ -5747,7 +5809,7 @@ export class Runtime {
5747
5809
  }
5748
5810
  if (part.length)
5749
5811
  return String(binding?.values.size ?? (state.variables[part.name] === undefined ? 0 : 1));
5750
- const values = await this.arrayMembers(part.name, state, io);
5812
+ const values = await this.arrayMembers(part.name, state, io, part.keys);
5751
5813
  const separator = Array.from(state.variables.IFS ?? " ")[0] ?? "";
5752
5814
  if (values.every(value => typeof value === "string"))
5753
5815
  return this.arrayJoin(store.owner, values, separator);
@@ -6229,7 +6291,7 @@ export class Runtime {
6229
6291
  }
6230
6292
  }
6231
6293
  else if (part.kind === "variable" && split && (selector?.kind === "members" && !part.length && (!part.quoted || selector.separator === "@") || part.transform && part.name === "@")) {
6232
- const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state, io) : this.positionalValues(state);
6294
+ const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state, io, part.keys) : this.positionalValues(state);
6233
6295
  for (let position = 0; position < members.length; position++) {
6234
6296
  if (position > 0)
6235
6297
  addField();
@@ -6404,7 +6466,7 @@ export class Runtime {
6404
6466
  allocation.hold(value);
6405
6467
  return createCommandArguments(values, allocation);
6406
6468
  }
6407
- async arrayMembers(name, state, io) {
6469
+ async arrayMembers(name, state, io, keys = false) {
6408
6470
  const store = requireArrays(state);
6409
6471
  const holding = store.owner.hold();
6410
6472
  try {
@@ -6416,7 +6478,7 @@ export class Runtime {
6416
6478
  return [];
6417
6479
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
6418
6480
  await textToken(store.owner, value, this.signal);
6419
- return [value];
6481
+ return [keys ? "0" : value];
6420
6482
  }
6421
6483
  binding.retain();
6422
6484
  try {
@@ -6424,7 +6486,7 @@ export class Runtime {
6424
6486
  const values = [];
6425
6487
  for (const index of indices) {
6426
6488
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 4 });
6427
- const token = binding.values.get(index).text;
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;
6428
6490
  const value = token.rawValue === undefined ? token.value : shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]);
6429
6491
  if (typeof value === "string")
6430
6492
  await textToken(store.owner, value, this.signal);