@poe-platform/safe-bash 0.1.145 → 0.1.147

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 (76) hide show
  1. package/README.md +1 -1
  2. package/dist/safe-bash/browser.d.ts +4 -0
  3. package/dist/safe-bash/browser.d.ts.map +1 -1
  4. package/dist/safe-bash/browser.js +4046 -2910
  5. package/dist/safe-bash/browser.js.map +4 -4
  6. package/dist/safe-bash/commands/basic.d.ts +3 -1
  7. package/dist/safe-bash/commands/basic.d.ts.map +1 -1
  8. package/dist/safe-bash/commands/basic.js +143 -141
  9. package/dist/safe-bash/commands/basic.js.map +1 -1
  10. package/dist/safe-bash/commands/execution.d.ts +4 -1
  11. package/dist/safe-bash/commands/execution.d.ts.map +1 -1
  12. package/dist/safe-bash/commands/execution.js +134 -32
  13. package/dist/safe-bash/commands/execution.js.map +1 -1
  14. package/dist/safe-bash/commands/filesystem.d.ts.map +1 -1
  15. package/dist/safe-bash/commands/filesystem.js +161 -39
  16. package/dist/safe-bash/commands/filesystem.js.map +1 -1
  17. package/dist/safe-bash/commands/find.d.ts.map +1 -1
  18. package/dist/safe-bash/commands/find.js +83 -3
  19. package/dist/safe-bash/commands/find.js.map +1 -1
  20. package/dist/safe-bash/commands/index.d.ts +4 -0
  21. package/dist/safe-bash/commands/index.d.ts.map +1 -1
  22. package/dist/safe-bash/commands/index.js +1 -1
  23. package/dist/safe-bash/commands/index.js.map +1 -1
  24. package/dist/safe-bash/commands/regex-execution/ere/matcher.d.ts.map +1 -1
  25. package/dist/safe-bash/commands/regex-execution/ere/matcher.js +35 -15
  26. package/dist/safe-bash/commands/regex-execution/ere/matcher.js.map +1 -1
  27. package/dist/safe-bash/commands/search/file-types.d.ts +2 -0
  28. package/dist/safe-bash/commands/search/file-types.d.ts.map +1 -0
  29. package/dist/safe-bash/commands/search/file-types.js +229 -0
  30. package/dist/safe-bash/commands/search/file-types.js.map +1 -0
  31. package/dist/safe-bash/commands/search/options.d.ts +4 -0
  32. package/dist/safe-bash/commands/search/options.d.ts.map +1 -1
  33. package/dist/safe-bash/commands/search/options.js +14 -1
  34. package/dist/safe-bash/commands/search/options.js.map +1 -1
  35. package/dist/safe-bash/commands/search/walk.d.ts +2 -0
  36. package/dist/safe-bash/commands/search/walk.d.ts.map +1 -1
  37. package/dist/safe-bash/commands/search/walk.js +32 -0
  38. package/dist/safe-bash/commands/search/walk.js.map +1 -1
  39. package/dist/safe-bash/commands/streams.d.ts +1 -1
  40. package/dist/safe-bash/commands/streams.d.ts.map +1 -1
  41. package/dist/safe-bash/commands/streams.js +7 -1
  42. package/dist/safe-bash/commands/streams.js.map +1 -1
  43. package/dist/safe-bash/commands/structured/interpreter.d.ts +9 -1
  44. package/dist/safe-bash/commands/structured/interpreter.d.ts.map +1 -1
  45. package/dist/safe-bash/commands/structured/interpreter.js +78 -2
  46. package/dist/safe-bash/commands/structured/interpreter.js.map +1 -1
  47. package/dist/safe-bash/commands/structured/parser.d.ts +13 -0
  48. package/dist/safe-bash/commands/structured/parser.d.ts.map +1 -1
  49. package/dist/safe-bash/commands/structured/parser.js +49 -2
  50. package/dist/safe-bash/commands/structured/parser.js.map +1 -1
  51. package/dist/safe-bash/commands/table-text/join.d.ts.map +1 -1
  52. package/dist/safe-bash/commands/table-text/join.js +7 -2
  53. package/dist/safe-bash/commands/table-text/join.js.map +1 -1
  54. package/dist/safe-bash/commands/text-programs/sed.d.ts.map +1 -1
  55. package/dist/safe-bash/commands/text-programs/sed.js +98 -29
  56. package/dist/safe-bash/commands/text-programs/sed.js.map +1 -1
  57. package/dist/safe-bash/commands/text.d.ts.map +1 -1
  58. package/dist/safe-bash/commands/text.js +26 -16
  59. package/dist/safe-bash/commands/text.js.map +1 -1
  60. package/dist/safe-bash/plugins/index.d.ts +3 -0
  61. package/dist/safe-bash/plugins/index.d.ts.map +1 -1
  62. package/dist/safe-bash/plugins/index.js +1 -1
  63. package/dist/safe-bash/plugins/index.js.map +1 -1
  64. package/dist/safe-bash/shell/brace-expansion.d.ts +6 -0
  65. package/dist/safe-bash/shell/brace-expansion.d.ts.map +1 -0
  66. package/dist/safe-bash/shell/brace-expansion.js +335 -0
  67. package/dist/safe-bash/shell/brace-expansion.js.map +1 -0
  68. package/dist/safe-bash/shell/parser.d.ts +10 -1
  69. package/dist/safe-bash/shell/parser.d.ts.map +1 -1
  70. package/dist/safe-bash/shell/parser.js +92 -18
  71. package/dist/safe-bash/shell/parser.js.map +1 -1
  72. package/dist/safe-bash/shell/runtime.d.ts +4 -2
  73. package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
  74. package/dist/safe-bash/shell/runtime.js +239 -38
  75. package/dist/safe-bash/shell/runtime.js.map +1 -1
  76. package/package.json +2 -2
@@ -3,12 +3,13 @@ import { ACCESS_MODES, FsError, composeMiddleware, createBytePipe, pipeBytes, re
3
3
  import { concatShellValues, shellValueByteLength, shellValueBytes, shellValueFromBytes, shellValueText } from "../contracts/value.js";
4
4
  import { createCommandArguments, getCommandArguments } from "../contracts/command.js";
5
5
  import { ValueArena } from "./value-state.js";
6
- import { HereDocumentSyntaxError, hereDocumentWords, parseShellInputUnit, parseShellUnit } from "./parser.js";
6
+ import { compoundEntryWords, HereDocumentSyntaxError, hereDocumentWords, parseShellInputUnit, parseShellUnit } from "./parser.js";
7
7
  import { SourceLineIndex } from "./source-line-index.js";
8
8
  import { ShellLimitError, ShellSyntaxError } from "./types.js";
9
9
  import { fileInput, ShellInput } from "./input.js";
10
10
  import { evaluateArithmetic, prepareArithmetic } from "./arithmetic.js";
11
11
  import { defaultMaxParseUnits, ParseBudget } from "./parse-budget.js";
12
+ import { BraceExpansionFailure, expandBraces } from "./brace-expansion.js";
12
13
  import { evaluatePositionalArithmetic } from "./arithmetic-parameters.js";
13
14
  import { compilePattern, matchesPattern } from "./pattern.js";
14
15
  import { nextCodePointOffset, previousCodePointOffset, scanString, stringCheckpoint } from "./string-operations.js";
@@ -19,6 +20,8 @@ import { invocationScope, throwCleanupFailures } from "./cleanup.js";
19
20
  import { bindFileOutputBudget, openFileOutput } from "../contracts/filesystem-output.js";
20
21
  import { outputFailure } from "../contracts/io.js";
21
22
  import { executionCommands } from "../commands/execution.js";
23
+ import { formatPrintf, printfCommand } from "../commands/basic.js";
24
+ import { UsageError } from "../commands/internal.js";
22
25
  import { cloneGetoptsState, createGetoptsState, GetoptsError, scanGetopts, withGetoptsIndex } from "./getopts.js";
23
26
  import { activateChildCancellation, prepareChildCancellation, selectRuntimeCancellationOutcome, subscribeCancellation, } from "./cancellation.js";
24
27
  import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker } from "./arrays/syntax.js";
@@ -53,7 +56,7 @@ const shellBuiltinNames = new Set([
53
56
  const implementedBuiltins = new Set([...shellBuiltinNames].filter(name => !["echo", "printf", "test", "["].includes(name)));
54
57
  const specialBuiltinNames = new Set([":", ".", "break", "continue", "eval", "exit", "export", "readonly", "return", "set", "shift", "unset"]);
55
58
  const unsupportedSetOptionNames = new Set([
56
- "allexport", "braceexpand", "emacs", "errtrace", "functrace", "hashall", "histexpand", "history",
59
+ "allexport", "emacs", "errtrace", "functrace", "hashall", "histexpand", "history",
57
60
  "ignoreeof", "interactive-comments", "keyword", "monitor", "noclobber", "noexec", "noglob", "nolog",
58
61
  "notify", "onecmd", "physical", "posix", "privileged", "verbose", "vi", "xtrace",
59
62
  ]);
@@ -339,7 +342,7 @@ function signalSink(sink, signal) {
339
342
  budgetedSinks.set(output, { budget: owned.budget, write: output.write });
340
343
  return output;
341
344
  }
342
- async function cloneState(state, signal, scope) {
345
+ async function cloneState(state, signal, scope, inheritLocals = true) {
343
346
  const destination = await snapshotState(state, () => ({
344
347
  ...state,
345
348
  variables: Object.assign(Object.create(null), state.variables),
@@ -347,7 +350,7 @@ async function cloneState(state, signal, scope) {
347
350
  readonlyVariables: new Set(state.readonlyVariables),
348
351
  getopts: cloneGetoptsBinding(state),
349
352
  directoryStack: { entries: [...state.directoryStack?.entries ?? []], bytes: state.directoryStack?.bytes ?? 0 },
350
- locals: scope ? [] : state.locals.map((scope) => new Map([...scope].map(([name, saved]) => [name, { ...saved, ...(saved.getopts ? { getopts: { integer: saved.getopts.integer, cursor: cloneGetoptsState(saved.getopts.cursor) } } : {}) }]))),
353
+ locals: inheritLocals ? state.locals.map((scope) => new Map([...scope].map(([name, saved]) => [name, { ...saved, ...(saved.getopts ? { getopts: { integer: saved.getopts.integer, cursor: cloneGetoptsState(saved.getopts.cursor) } } : {}) }]))) : [],
351
354
  }), signal, async (destination, owner) => {
352
355
  const store = arrayStore(destination) ?? requireArrays(destination);
353
356
  for (let index = 0; index < destination.locals.length; index++) {
@@ -361,14 +364,19 @@ async function cloneState(state, signal, scope) {
361
364
  throw new ArrayFailure("stale state snapshot");
362
365
  const savedOwner = ArrayOwner.create(owner.ledger, owner);
363
366
  let binding;
367
+ let releaseBinding;
364
368
  try {
369
+ scope?.register(async () => { await savedOwner.completion; await releaseBinding; });
365
370
  const watch = await store.watch(name, savedOwner, signal);
366
371
  const tickets = savedOwner.reserve({ generation: true, version: true, epoch: true, slots: 1, metadata: 64, work: 14 });
367
372
  const token = await textToken(savedOwner, name, signal);
368
373
  const admission = savedOwner.reserve({ slots: 1, metadata: 32, work: 5 });
369
374
  binding = typed.binding?.retain();
370
375
  typedSavedVariables.set(copied, { owner: savedOwner, binding, tickets, prepared: { name: token, admission }, watch, scalarLegacy: typed.scalarLegacy });
371
- tickets.cleanup = () => { typedSavedVariables.delete(copied); };
376
+ tickets.cleanup = () => {
377
+ if (typedSavedVariables.delete(copied) && scope)
378
+ releaseBinding = scope.cleanup(async () => { await binding?.release(); });
379
+ };
372
380
  }
373
381
  catch (error) {
374
382
  await binding?.release();
@@ -1693,6 +1701,22 @@ export class Runtime {
1693
1701
  }
1694
1702
  else
1695
1703
  for (const entry of assignment.entries) {
1704
+ const original = compoundEntryWords.get(entry);
1705
+ if (entry.index && original && state.braceexpand !== false && original.parts.some(part => part.kind === "text" && !part.quoted && part.value.includes("{"))) {
1706
+ let expandedEntry = false;
1707
+ for await (const expanded of expandBraces(original, this.budget, this.signal)) {
1708
+ if (expanded === original)
1709
+ break;
1710
+ expandedEntry = true;
1711
+ const values = await this.valueWord(expanded, state, io, true, false, false, false, undefined, false);
1712
+ for (const value of values) {
1713
+ await insert(cursor, shellValueText(value));
1714
+ cursor++;
1715
+ }
1716
+ }
1717
+ if (expandedEntry)
1718
+ continue;
1719
+ }
1696
1720
  const fields = await this.word(entry.value, state, io, entry.index === undefined);
1697
1721
  if (entry.index) {
1698
1722
  const index = numericIndex(entry.index);
@@ -2002,6 +2026,8 @@ export class Runtime {
2002
2026
  originalIO = { ...originalIO, [valueScope]: allocation };
2003
2027
  let io = originalIO;
2004
2028
  let diagnosticFailure;
2029
+ let snapshotScope;
2030
+ let finishSnapshot;
2005
2031
  try {
2006
2032
  const execute = async () => {
2007
2033
  if (command.kind === "function") {
@@ -2013,7 +2039,11 @@ export class Runtime {
2013
2039
  return 0;
2014
2040
  }
2015
2041
  if (command.kind === "simple")
2016
- return await this.simple(command, state, originalIO, inputs, outputs, fileShortcut);
2042
+ return await this.simple(command, state, originalIO, inputs, outputs, () => {
2043
+ snapshotScope = originalIO[invocationScope].child();
2044
+ void snapshotScope.run(() => new Promise(resolve => { finishSnapshot = resolve; }));
2045
+ return snapshotScope;
2046
+ }, fileShortcut);
2017
2047
  io = await this.redirect(command.redirects, state, io, inputs, outputs, command.kind === "subshell", command.kind !== "subshell");
2018
2048
  if (command.kind === "conditional") {
2019
2049
  try {
@@ -2023,7 +2053,7 @@ export class Runtime {
2023
2053
  work: { remaining: this.budget.limits.maxExpansionBytes, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") },
2024
2054
  expand: async (word, pattern = false) => (await this.word(word, state, io, false, pattern, false, pattern)).join(""),
2025
2055
  regex: (subject, pattern) => this.ere(subject, pattern, state, io),
2026
- option: name => name === "errexit" ? !!state.errexit : name === "nounset" ? !!state.nounset : name === "pipefail" ? state.pipefail : false,
2056
+ option: name => name === "braceexpand" ? state.braceexpand !== false : name === "errexit" ? !!state.errexit : name === "nounset" ? !!state.nounset : name === "pipefail" ? state.pipefail : false,
2027
2057
  present: name => {
2028
2058
  const match = /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\[(0|[1-9][0-9]*|[@*])\])?$/u.exec(name);
2029
2059
  if (!match)
@@ -2230,7 +2260,7 @@ export class Runtime {
2230
2260
  this.signal.throwIfAborted();
2231
2261
  }
2232
2262
  }
2233
- if (error instanceof ExpansionFailure)
2263
+ if (error instanceof ExpansionFailure || error instanceof BraceExpansionFailure)
2234
2264
  throw completedExit(error instanceof ParameterExpansionFailure && !state.isolated ? 127 : 1);
2235
2265
  if (error instanceof FatalCommandFailure)
2236
2266
  throw completedExit(error.status);
@@ -2240,16 +2270,22 @@ export class Runtime {
2240
2270
  return status;
2241
2271
  }
2242
2272
  finally {
2243
- await Promise.allSettled([
2244
- ...[...outputs].map(async (close) => close({ reason: new FsError("ECANCELED", { syscall: "redirect" }) })),
2245
- ...[...inputs].map(async (input) => input.close()),
2246
- ]).then(results => {
2247
- const failures = results.filter(result => result.status === "rejected").map(result => result.reason);
2248
- if (diagnosticFailure)
2249
- io[invocationScope].failures.push(...failures);
2250
- else
2251
- throwCleanupFailures(failures);
2252
- }).finally(() => allocation.close());
2273
+ try {
2274
+ await Promise.allSettled([
2275
+ ...[...outputs].map(async (close) => close({ reason: new FsError("ECANCELED", { syscall: "redirect" }) })),
2276
+ ...[...inputs].map(async (input) => input.close()),
2277
+ ]).then(results => {
2278
+ const failures = results.filter(result => result.status === "rejected").map(result => result.reason);
2279
+ if (diagnosticFailure)
2280
+ io[invocationScope].failures.push(...failures);
2281
+ else
2282
+ throwCleanupFailures(failures);
2283
+ }).finally(() => allocation.close());
2284
+ }
2285
+ finally {
2286
+ finishSnapshot?.();
2287
+ await snapshotScope?.close();
2288
+ }
2253
2289
  }
2254
2290
  }
2255
2291
  async loopBody(body, state, io) {
@@ -2535,7 +2571,7 @@ export class Runtime {
2535
2571
  return undefined;
2536
2572
  return { name: match[1], append: match[2] === "+", value: { offset: word.offset, parts: [{ ...first, value: first.value.slice(match[0].length) }, ...word.parts.slice(1)] } };
2537
2573
  }
2538
- async simple(command, state, originalIO, inputs, outputs, fileShortcut = false) {
2574
+ async simple(command, state, originalIO, inputs, outputs, createSnapshotScope, fileShortcut = false) {
2539
2575
  state.substitutionStatus = 0;
2540
2576
  const assignments = [];
2541
2577
  let wordIndex = 0;
@@ -2558,9 +2594,8 @@ export class Runtime {
2558
2594
  const inlineInput = command.redirects.some((redirect) => redirect.document || redirect.operator === "<<<");
2559
2595
  const functionCommand = words.length > 0 && state.functions.has(words[0]);
2560
2596
  const isolatedInlineInput = inlineInput && words.length > 0 && !shellBuiltinNames.has(words[0]) && !functionCommand;
2561
- if (isolatedInlineInput)
2562
- state = await cloneState(state, this.signal);
2563
- let io = originalIO;
2597
+ const snapshotScope = isolatedInlineInput ? createSnapshotScope() : undefined;
2598
+ let io = snapshotScope ? { ...originalIO, [invocationScope]: snapshotScope } : originalIO;
2564
2599
  const previous = new Map();
2565
2600
  const assign = async () => {
2566
2601
  for (const assignment of assignments) {
@@ -2599,9 +2634,14 @@ export class Runtime {
2599
2634
  state.exported.add(assignment.name);
2600
2635
  }
2601
2636
  };
2602
- if (words.length)
2603
- stateMonitor(state)?.openOverlay(previous);
2637
+ let overlayOpen = false;
2604
2638
  try {
2639
+ if (snapshotScope)
2640
+ state = await cloneState(state, this.signal, snapshotScope);
2641
+ if (words.length) {
2642
+ stateMonitor(state)?.openOverlay(previous);
2643
+ overlayOpen = true;
2644
+ }
2605
2645
  if (inlineInput || (state.profile === "sh" || !words.length) && assignments.some(assignment => state.readonlyVariables?.has(assignment.name)))
2606
2646
  await assign();
2607
2647
  if (inlineInput && functionCommand && previous.size) {
@@ -2706,7 +2746,7 @@ export class Runtime {
2706
2746
  throw new ExecutionFailure(error, io);
2707
2747
  }
2708
2748
  finally {
2709
- if (words.length) {
2749
+ if (overlayOpen) {
2710
2750
  for (const [key, saved] of previous)
2711
2751
  await originalIO[invocationScope].cleanup(async () => {
2712
2752
  if (saved.superseded)
@@ -2719,8 +2759,6 @@ export class Runtime {
2719
2759
  else
2720
2760
  for (const saved of previous.values())
2721
2761
  saved.heldValue?.release();
2722
- if (isolatedInlineInput)
2723
- stateMonitor(state)?.closeValues();
2724
2762
  }
2725
2763
  }
2726
2764
  async dispatch(name, args, state, io, assignments, bypassFunctions = false, values = args) {
@@ -2972,6 +3010,9 @@ export class Runtime {
2972
3010
  }
2973
3011
  }
2974
3012
  const definition = this.commands.get(context.command);
3013
+ if (context.command === "printf" && definition?.execute === printfCommand.execute && context.args[0]?.startsWith("-v")) {
3014
+ return { exitCode: await this.printfVariable(context, state, assignments) };
3015
+ }
2975
3016
  if (!definition) {
2976
3017
  if (context.command === "bash" || context.command === "sh")
2977
3018
  return { exitCode: await this.interpreter(context, state, io) };
@@ -3228,21 +3269,30 @@ export class Runtime {
3228
3269
  let commandString = false;
3229
3270
  let standardInput = false;
3230
3271
  let errexit = false;
3272
+ let braceexpand = true;
3231
3273
  while (args.length && /^[+-]/u.test(args[0])) {
3232
3274
  const option = args.shift();
3233
3275
  if (option === "--" || option === "-")
3234
3276
  break;
3235
- if (!/^-[cse]+$|^\+e+$/u.test(option)) {
3236
- await writeText(context.stderr, `${context.command}: ${option}: unsupported option; supported flags are -c, -s, -e and +e\n`);
3277
+ if ((option === "-o" || option === "+o") && args[0] === "braceexpand") {
3278
+ braceexpand = option === "-o";
3279
+ args.shift();
3280
+ continue;
3281
+ }
3282
+ const flags = option.slice(1);
3283
+ if (!flags.length || [...flags].some(flag => !(option[0] === "-" ? "cseB" : "eB").includes(flag))) {
3284
+ await writeText(context.stderr, `${context.command}: ${option}: unsupported option; supported flags are -c, -s, -e, +e, -B, +B and +/-o braceexpand\n`);
3237
3285
  return 2;
3238
3286
  }
3239
3287
  commandString ||= option.includes("c");
3240
3288
  standardInput ||= option.includes("s");
3241
3289
  if (option.includes("e"))
3242
3290
  errexit = option.startsWith("-");
3291
+ if (option.includes("B"))
3292
+ braceexpand = option.startsWith("-");
3243
3293
  }
3244
3294
  if (!commandString && !standardInput && args.length)
3245
- return this.scriptFile(context, state, io, args[0], args.slice(1), false, errexit, loadedSource);
3295
+ return this.scriptFile(context, state, io, args[0], args.slice(1), false, errexit, loadedSource, braceexpand);
3246
3296
  const source = commandString ? args.shift() : undefined;
3247
3297
  if (commandString && source === undefined) {
3248
3298
  await writeText(context.stderr, `${context.command}: -c: option requires an argument\n`);
@@ -3251,6 +3301,7 @@ export class Runtime {
3251
3301
  const arg0 = commandString ? args.shift() ?? context.command : context.command;
3252
3302
  const child = this.processState(context, state, arg0, args);
3253
3303
  child.errexit = errexit;
3304
+ child.braceexpand = braceexpand;
3254
3305
  const childIO = isolateIO({ ...io, ...context, execution: { ignoreErrexit: false }, diagnosticLine: 1, diagnosticOffset: 0, scriptName: arg0 });
3255
3306
  if (source !== undefined) {
3256
3307
  this.budget.source(Buffer.byteLength(source));
@@ -3542,7 +3593,7 @@ export class Runtime {
3542
3593
  return { exitCode: validateExitCode(result.exitCode) };
3543
3594
  }, (runtime, forwarded, child, childIO) => (command, arguments_, options = {}) => runtime.shebangTarget(forwarded, child, childIO, command, arguments_, options, target, loadedSource));
3544
3595
  }
3545
- async scriptFile(context, state, io, target, args, direct, errexit = false, loadedSource) {
3596
+ async scriptFile(context, state, io, target, args, direct, errexit = false, loadedSource, braceexpand = true) {
3546
3597
  if (target === "")
3547
3598
  throw new CommandFailure(`${context.command}: : No such file or directory`, 127);
3548
3599
  if (state.depth >= this.budget.limits.maxSubstitutionDepth)
@@ -3612,6 +3663,7 @@ export class Runtime {
3612
3663
  }
3613
3664
  const child = this.processState(context, state, target, args);
3614
3665
  child.errexit = errexit;
3666
+ child.braceexpand = braceexpand;
3615
3667
  if (direct)
3616
3668
  child.profile = interpreterProfile ?? state.profile ?? "bash";
3617
3669
  const childIO = isolateIO({ ...io, ...context, execution: { ignoreErrexit: false }, diagnosticLine: 1, diagnosticOffset: 0, scriptName: target });
@@ -3850,7 +3902,7 @@ export class Runtime {
3850
3902
  const allocation = this.budget.values.scope();
3851
3903
  scope.register(() => allocation.close());
3852
3904
  const carrier = this.admitArguments(getCommandArguments({ args, ...(options.argumentValues ? { argumentValues: options.argumentValues } : {}) }).values, allocation);
3853
- const child = await cloneState(state, this.signal, scope);
3905
+ const child = await cloneState(state, this.signal, scope, false);
3854
3906
  child.cwd = resolvePath(context.cwd, options.cwd ?? ".");
3855
3907
  const env = options.replaceEnv ? { ...options.env } : { ...context.env, ...options.env, PWD: child.cwd };
3856
3908
  if (guestArrays(child) || Object.keys(env).some(key => arrayStore(child)?.get(key)))
@@ -3927,10 +3979,12 @@ export class Runtime {
3927
3979
  state.errexit = enabled;
3928
3980
  else if (flag === "u")
3929
3981
  state.nounset = enabled;
3982
+ else if (flag === "B")
3983
+ state.braceexpand = enabled;
3930
3984
  else if (flag === "o" && position === option.length - 1) {
3931
3985
  const name = args[index + 1];
3932
3986
  if (name === undefined) {
3933
- const options = [["errexit", !!state.errexit], ["nounset", !!state.nounset], ["pipefail", state.pipefail]];
3987
+ const options = [["braceexpand", state.braceexpand !== false], ["errexit", !!state.errexit], ["nounset", !!state.nounset], ["pipefail", state.pipefail]];
3934
3988
  for (const [name, active] of options)
3935
3989
  await writeText(stdout, enabled ? `${name}\t${active ? "on" : "off"}\n` : `set ${active ? "-" : "+"}o ${name}\n`);
3936
3990
  }
@@ -3941,6 +3995,8 @@ export class Runtime {
3941
3995
  state.nounset = enabled;
3942
3996
  else if (name === "pipefail")
3943
3997
  state.pipefail = enabled;
3998
+ else if (name === "braceexpand")
3999
+ state.braceexpand = enabled;
3944
4000
  else {
3945
4001
  const unsupported = unsupportedSetOptionNames.has(name);
3946
4002
  await this.diagnostic(context, `set: ${name}: ${unsupported ? "unsupported shell option" : "invalid option name"}`);
@@ -3953,7 +4009,7 @@ export class Runtime {
3953
4009
  valid = false;
3954
4010
  }
3955
4011
  if (!valid) {
3956
- await writeText(stderr, "set: unsupported shell option; supported forms are +/- e/u clusters, -- arguments and terminal o with pipefail, errexit or nounset\n");
4012
+ await writeText(stderr, "set: unsupported shell option; supported forms are +/- e/u/B clusters, -- arguments and terminal o with braceexpand, pipefail, errexit or nounset\n");
3957
4013
  return 1;
3958
4014
  }
3959
4015
  index++;
@@ -4365,6 +4421,131 @@ export class Runtime {
4365
4421
  }
4366
4422
  return status;
4367
4423
  }
4424
+ async printfVariable(context, state, assignments) {
4425
+ const incoming = getCommandArguments(context);
4426
+ let offset = 0;
4427
+ let name = "";
4428
+ let index;
4429
+ while (incoming.args[offset]?.startsWith("-v")) {
4430
+ const option = incoming.args[offset++];
4431
+ const target = option.slice(2) || incoming.args[offset++];
4432
+ if (target === undefined) {
4433
+ await writeText(context.stderr, "printf: -v: option requires an argument\n");
4434
+ return 2;
4435
+ }
4436
+ const bracket = target.indexOf("[");
4437
+ name = bracket < 0 ? target : target.slice(0, bracket);
4438
+ if (!/^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(name) || bracket >= 0 && !target.endsWith("]")) {
4439
+ await writeText(context.stderr, `printf: '${target}': not a valid identifier\n`);
4440
+ return 2;
4441
+ }
4442
+ index = undefined;
4443
+ if (bracket >= 0) {
4444
+ if (target[bracket + 1] === "'" || target[bracket + 1] === '"') {
4445
+ await writeText(context.stderr, `printf: '${target}': unsupported indexed-array subscript\n`);
4446
+ return 2;
4447
+ }
4448
+ try {
4449
+ index = literalIndex(target.slice(bracket + 1, -1), 0, this.budget.parsing);
4450
+ }
4451
+ catch (error) {
4452
+ this.signal.throwIfAborted();
4453
+ if (!(error instanceof ShellSyntaxError))
4454
+ throw error;
4455
+ await writeText(context.stderr, `printf: '${target}': unsupported indexed-array subscript\n`);
4456
+ return 2;
4457
+ }
4458
+ if (numericIndex(index) === undefined) {
4459
+ await writeText(context.stderr, "printf: index outside 0..2147483647\n");
4460
+ return 2;
4461
+ }
4462
+ }
4463
+ }
4464
+ const arguments_ = incoming.slice(offset);
4465
+ const format = arguments_.args[0] === "--" ? arguments_.args[1] : arguments_.args[0];
4466
+ if (format === undefined || arguments_.args[0] !== "--" && format.startsWith("-")) {
4467
+ await writeText(context.stderr, "printf: usage: printf [-v var] format [arguments]\n");
4468
+ return 2;
4469
+ }
4470
+ const allocation = this.budget.values.scope();
4471
+ context[invocationScope].register(() => allocation.close());
4472
+ const chunks = [];
4473
+ let length = 0;
4474
+ let terminated = false;
4475
+ const stdout = { write: async (chunk) => {
4476
+ this.budget.cpuCheckpoint();
4477
+ if (chunk.byteLength > this.budget.limits.maxExpansionBytes - length)
4478
+ this.budget.fail("maxExpansionBytes");
4479
+ length += chunk.byteLength;
4480
+ if (terminated || !chunk.byteLength)
4481
+ return;
4482
+ const nul = chunk.indexOf(0);
4483
+ const bytes = nul < 0 ? chunk : chunk.subarray(0, nul);
4484
+ if (bytes.byteLength)
4485
+ chunks.push(shellValueFromBytes(bytes, allocation));
4486
+ terminated = nul >= 0;
4487
+ } };
4488
+ try {
4489
+ let status;
4490
+ try {
4491
+ status = (await formatPrintf({ ...context, args: arguments_.args, argumentValues: arguments_, stdout })).exitCode;
4492
+ }
4493
+ catch (error) {
4494
+ this.signal.throwIfAborted();
4495
+ if (!(error instanceof UsageError))
4496
+ throw error;
4497
+ await writeText(context.stderr, `printf: ${error.message}\n`);
4498
+ status = 1;
4499
+ }
4500
+ const value = concatShellValues(chunks, allocation);
4501
+ this.signal.throwIfAborted();
4502
+ if (state.readonlyVariables?.has(name)) {
4503
+ await writeText(context.stderr, `printf: ${name}: readonly variable\n`);
4504
+ return 1;
4505
+ }
4506
+ if (index || arrayStore(state)?.get(name)) {
4507
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
4508
+ const decode = (bytes) => {
4509
+ try {
4510
+ return decoder.decode(bytes);
4511
+ }
4512
+ catch (error) {
4513
+ this.signal.throwIfAborted();
4514
+ if (!(error instanceof TypeError))
4515
+ throw error;
4516
+ return undefined;
4517
+ }
4518
+ };
4519
+ let text = decode(shellValueBytes(value, allocation));
4520
+ if (text !== undefined && index && numericIndex(index) !== 0 && !arrayStore(state)?.get(name) && state.variables[name] !== undefined) {
4521
+ const previous = stateMonitor(state).values.get(name, state.variables[name]);
4522
+ if (decode(shellValueBytes(previous, allocation)) === undefined)
4523
+ text = undefined;
4524
+ }
4525
+ if (text === undefined) {
4526
+ await writeText(context.stderr, "printf: indexed variables do not support non-UTF-8 bytes\n");
4527
+ return 1;
4528
+ }
4529
+ if (index)
4530
+ await this.arrayAssignment({ kind: "element", name, index, append: false, value: { offset: 0, parts: [{ kind: "text", value: text, quoted: true }] } }, state, context);
4531
+ else
4532
+ await this.assignVariable(state, name, text);
4533
+ }
4534
+ else
4535
+ this.writeVariable(state, name, value);
4536
+ const previous = assignments.get(name);
4537
+ if (previous) {
4538
+ if (!previous.exported)
4539
+ state.exported.delete(name);
4540
+ await this.discardVariable(previous);
4541
+ assignments.delete(name);
4542
+ }
4543
+ return status;
4544
+ }
4545
+ finally {
4546
+ allocation.close();
4547
+ }
4548
+ }
4368
4549
  async builtin(context, state, assignments, diagnose, suppressSpecial = false) {
4369
4550
  const { command, args, stdout, stderr } = context;
4370
4551
  if (command === ":" || command === "true")
@@ -4921,9 +5102,11 @@ export class Runtime {
4921
5102
  async valueWords(words, state, io, declaration = false) {
4922
5103
  const fields = [];
4923
5104
  for (const word of words) {
4924
- fields.push(...await this.valueWord(word, state, io, !(declaration && this.assignment(word))));
4925
- if (fields.length > this.budget.limits.maxExpansionFields)
5105
+ const values = await this.valueWord(word, state, io, !(declaration && this.assignment(word)), false, false, false, undefined, true);
5106
+ if (values.length > this.budget.limits.maxExpansionFields - fields.length)
4926
5107
  this.budget.fail("maxExpansionFields");
5108
+ for (const value of values)
5109
+ fields.push(value);
4927
5110
  }
4928
5111
  return fields;
4929
5112
  }
@@ -5050,7 +5233,7 @@ export class Runtime {
5050
5233
  return this.arrayJoin(store.owner, values, Array.from(state.variables.IFS ?? " ")[0] ?? "");
5051
5234
  }
5052
5235
  let value = part.name === "?" ? String(state.status)
5053
- : part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}`
5236
+ : part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}${state.braceexpand !== false ? "B" : ""}`
5054
5237
  : part.name === "#" ? String(state.positional.length)
5055
5238
  : part.name === "@" || part.name === "*" ? state.positional.join(hereString && (part.name === "@" || !part.quoted) ? " " : Array.from(state.variables.IFS ?? " ")[0] ?? "")
5056
5239
  : /^0+$/u.test(part.name) ? state.arg0 ?? "virtual-bash"
@@ -5354,7 +5537,25 @@ export class Runtime {
5354
5537
  async word(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend) {
5355
5538
  return (await this.valueWord(word, state, io, split, pattern, hereString, conditionalPattern, regexAppend)).map(shellValueText);
5356
5539
  }
5357
- async valueWord(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend) {
5540
+ async valueWord(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend, braces = split && !pattern && !hereString) {
5541
+ if (braces && state.braceexpand !== false && word.parts.some(part => part.kind === "text" && !part.quoted && part.value.includes("{"))) {
5542
+ const fields = [];
5543
+ let bytes = 0;
5544
+ for await (const expanded of expandBraces(word, this.budget, this.signal)) {
5545
+ const values = await this.valueWord(expanded, state, io, split, pattern, hereString, conditionalPattern, regexAppend, false);
5546
+ if (values.length > this.budget.limits.maxExpansionFields - fields.length)
5547
+ this.budget.fail("maxExpansionFields");
5548
+ for (const value of values) {
5549
+ const size = shellValueByteLength(value);
5550
+ if (size > this.budget.limits.maxExpansionBytes - bytes)
5551
+ this.budget.fail("maxExpansionBytes");
5552
+ bytes += size;
5553
+ io[valueScope]?.reserve(32 + (typeof value === "string" ? value.length * 2 : 0), 0);
5554
+ fields.push(value);
5555
+ }
5556
+ }
5557
+ return fields;
5558
+ }
5358
5559
  const arrayOwned = word.parts.some(part => part.kind === "variable" && (getArraySelector(part) !== undefined || arrayStore(state)?.get(part.name) !== undefined));
5359
5560
  const owner = arrayOwned ? requireArrays(state).owner : undefined;
5360
5561
  const holding = owner?.hold();