@sjawhar/pi-legion-envoy 5.16.9 → 5.17.0

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 (2) hide show
  1. package/dist/legion.js +1058 -145
  2. package/package.json +1 -1
package/dist/legion.js CHANGED
@@ -33473,7 +33473,7 @@ import { logger } from "@oh-my-pi/pi-utils";
33473
33473
  // package.json
33474
33474
  var package_default = {
33475
33475
  name: "@sjawhar/pi-legion-envoy",
33476
- version: "5.16.9",
33476
+ version: "5.17.0",
33477
33477
  type: "module",
33478
33478
  omp: {
33479
33479
  extensions: [
@@ -40317,6 +40317,148 @@ class Parser {
40317
40317
  }
40318
40318
  }
40319
40319
 
40320
+ // src/legion/pane-guard-bash.ts
40321
+ function isAscii(text) {
40322
+ for (let i = 0;i < text.length; i += 1)
40323
+ if (text.charCodeAt(i) > 127)
40324
+ return false;
40325
+ return true;
40326
+ }
40327
+ var ANY_RUN = -1;
40328
+ var ANY_ONE = -2;
40329
+ function globWork(glob, n, operator) {
40330
+ const characters = operator === "/" || operator === "//" ? n * (n + 3) / 2 + 1 : n + 1;
40331
+ return characters * (glob.length + 8);
40332
+ }
40333
+ function prefixMatches(glob, value, from) {
40334
+ const m = glob.length;
40335
+ const matches = new Uint8Array(value.length - from + 1);
40336
+ let reached = new Uint8Array(m + 1);
40337
+ let next = new Uint8Array(m + 1);
40338
+ reached[0] = 1;
40339
+ closeRuns(glob, reached);
40340
+ for (let k = 0;; k += 1) {
40341
+ matches[k] = reached[m];
40342
+ if (from + k === value.length)
40343
+ break;
40344
+ const c = value.charCodeAt(from + k);
40345
+ next.fill(0);
40346
+ let any2 = false;
40347
+ for (let i = 0;i < m; i += 1) {
40348
+ if (reached[i] !== 1)
40349
+ continue;
40350
+ const element = glob[i];
40351
+ if (element === ANY_RUN)
40352
+ next[i] = 1;
40353
+ else if (element === ANY_ONE || element === c)
40354
+ next[i + 1] = 1;
40355
+ else
40356
+ continue;
40357
+ any2 = true;
40358
+ }
40359
+ if (!any2)
40360
+ break;
40361
+ closeRuns(glob, next);
40362
+ [reached, next] = [next, reached];
40363
+ }
40364
+ return matches;
40365
+ }
40366
+ function closeRuns(glob, reached) {
40367
+ for (let i = 0;i < glob.length; i += 1) {
40368
+ if (reached[i] === 1 && glob[i] === ANY_RUN)
40369
+ reached[i + 1] = 1;
40370
+ }
40371
+ }
40372
+ function globMatches(glob, value) {
40373
+ return prefixMatches(glob, value, 0)[value.length] === 1;
40374
+ }
40375
+ function matchEdge(value, glob, from, longest) {
40376
+ const matches = from === "start" ? prefixMatches(glob, value, 0) : prefixMatches([...glob].reverse(), [...value].reverse().join(""), 0);
40377
+ const length = longest ? matches.lastIndexOf(1) : matches.indexOf(1);
40378
+ return length === -1 ? undefined : length;
40379
+ }
40380
+ function removePattern(value, operator, glob) {
40381
+ const from = operator.startsWith("#") ? "start" : "end";
40382
+ const length = matchEdge(value, glob, from, operator.length === 2);
40383
+ if (length === undefined)
40384
+ return value;
40385
+ return from === "start" ? value.slice(length) : value.slice(0, value.length - length);
40386
+ }
40387
+ function replacePattern(value, operator, glob, replacement) {
40388
+ const n = value.length;
40389
+ if (operator === "/#" || operator === "/%") {
40390
+ const from = operator === "/#" ? "start" : "end";
40391
+ const length = matchEdge(value, glob, from, true);
40392
+ if (length === undefined)
40393
+ return value;
40394
+ return from === "start" ? replacement + value.slice(length) : value.slice(0, n - length) + replacement;
40395
+ }
40396
+ if (n === 0)
40397
+ return globMatches(glob, "") ? replacement : value;
40398
+ let out = "";
40399
+ let at = 0;
40400
+ while (at < n) {
40401
+ const length = prefixMatches(glob, value, at).lastIndexOf(1);
40402
+ if (length <= 0) {
40403
+ out += value[at];
40404
+ at += 1;
40405
+ continue;
40406
+ }
40407
+ out += replacement;
40408
+ at += length;
40409
+ if (operator === "/")
40410
+ return out + value.slice(at);
40411
+ }
40412
+ return out;
40413
+ }
40414
+ function evaluateTest(operands) {
40415
+ const [left = "", operator = "", right = ""] = operands;
40416
+ if (operands.length === 1)
40417
+ return left !== "";
40418
+ if (operands.length === 2) {
40419
+ if (left === "!")
40420
+ return operator === "";
40421
+ if (left === "-n")
40422
+ return operator !== "";
40423
+ if (left === "-z")
40424
+ return operator === "";
40425
+ return;
40426
+ }
40427
+ if (operands.length !== 3)
40428
+ return;
40429
+ if (operator === "=" || operator === "==")
40430
+ return left === right;
40431
+ if (operator === "!=")
40432
+ return left !== right;
40433
+ if (!/^-?[0-9]+$/.test(left) || !/^-?[0-9]+$/.test(right))
40434
+ return;
40435
+ const a = Number(left);
40436
+ const b = Number(right);
40437
+ switch (operator) {
40438
+ case "-eq":
40439
+ return a === b;
40440
+ case "-ne":
40441
+ return a !== b;
40442
+ case "-gt":
40443
+ return a > b;
40444
+ case "-ge":
40445
+ return a >= b;
40446
+ case "-lt":
40447
+ return a < b;
40448
+ case "-le":
40449
+ return a <= b;
40450
+ default:
40451
+ return;
40452
+ }
40453
+ }
40454
+ function shellQuoted(text) {
40455
+ if (text === "")
40456
+ return "''";
40457
+ if (/^[A-Za-z0-9_/.:@%+=,-]+$/.test(text))
40458
+ return text;
40459
+ return `'${text.replaceAll("'", "'\\''")}'`;
40460
+ }
40461
+
40320
40462
  // src/legion/pane-guard-code.ts
40321
40463
  var UNKNOWN_MARKER = "__LEGION_GUARD_UNKNOWN__";
40322
40464
  var PY_STRING_PREFIX = new Set(["r", "u", "b", "f", "br", "rb", "fr", "rf"]);
@@ -41287,6 +41429,19 @@ function scanIpythonShell(source, sinks) {
41287
41429
  }
41288
41430
 
41289
41431
  // src/legion/pane-guard.ts
41432
+ var UNSET = [{ kind: "literal", text: "", unset: true }];
41433
+ function isUnset(value) {
41434
+ return value?.length === 1 && value[0]?.unset === true;
41435
+ }
41436
+ var NO_ELEMENTS = { kind: "literal", text: "", noElements: true };
41437
+ function stored(value) {
41438
+ if (!value.some((piece) => piece.noElements === true))
41439
+ return value;
41440
+ return value.map((piece) => piece.noElements === true ? literal2("") : piece);
41441
+ }
41442
+ var ANY_NAME = "\x00";
41443
+ var IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
41444
+ var ARRAY_ELEMENT = /^([A-Za-z_][A-Za-z0-9_]*)\[.*\]$/s;
41290
41445
  var definitions = new WeakMap;
41291
41446
  function trapSignal(operand) {
41292
41447
  const name = operand.toUpperCase();
@@ -41322,7 +41477,10 @@ function wrapRefusal(error48, site, label) {
41322
41477
  }
41323
41478
  var MAX_DEPTH = 8;
41324
41479
  var MAX_WALK_STEPS = 1e5;
41480
+ var WALK_LIMIT = `the guard reached its walk limit of ${MAX_WALK_STEPS} nodes; run a smaller script`;
41325
41481
  var MAX_ALTERNATIVES = 64;
41482
+ var MAX_PATTERN_VALUE = 512;
41483
+ var PATTERN_WORK_PER_STEP = 1024;
41326
41484
  var MAX_SCRIPT_BYTES = 1024 * 1024;
41327
41485
  var FRESH_TEMP_NAME = "tmp.XXXXXXXXXX";
41328
41486
  var PROTECTED_SCRATCH = [
@@ -41410,7 +41568,7 @@ function realish(abs, followFinal) {
41410
41568
  return path4.join(realExisting(path4.dirname(resolved)), path4.basename(resolved));
41411
41569
  }
41412
41570
  function isProtectedScratchComponent(component, roots, partial2 = false) {
41413
- return roots.protectedScratch.some((name) => component.startsWith(name) || partial2 && name.startsWith(component));
41571
+ return roots.protectedScratch.some((name) => component.startsWith(name) || partial2 && name.startsWith(component)) || roots.protectedDirectories.some((name) => component === name || partial2 && name.startsWith(component));
41414
41572
  }
41415
41573
  function inWorkspace(real, roots) {
41416
41574
  const workspace = roots.workspace;
@@ -41516,12 +41674,81 @@ function unquotedLiteral(source, tildeAt, st, ctx) {
41516
41674
  function lookup(name, st, ctx) {
41517
41675
  const own = st.vars.get(name);
41518
41676
  if (own !== undefined)
41519
- return own;
41677
+ return isUnset(own) ? undefined : own;
41678
+ const any2 = st.vars.get(ANY_NAME)?.[0];
41679
+ if (any2 !== undefined)
41680
+ return [{ ...any2, why: `\`$${name}\`, which ${any2.why}` }];
41520
41681
  if (name === "PWD" && st.cwd !== undefined)
41521
41682
  return [literal2(st.cwd)];
41522
41683
  const value = ctx.env[name];
41523
41684
  return value === undefined ? undefined : [literal2(value)];
41524
41685
  }
41686
+ function assignScalar(st, name, value) {
41687
+ if (attributed(st, name)) {
41688
+ keepAttributed(st, name);
41689
+ return;
41690
+ }
41691
+ const kept = stored(value);
41692
+ st.vars.set(name, kept);
41693
+ const elements = st.arrays.get(name);
41694
+ if (elements !== undefined)
41695
+ st.arrays.set(name, new Map(elements).set("0", kept));
41696
+ }
41697
+ function attributed(st, name) {
41698
+ return st.vars.get(name)?.some((piece) => piece.rewrites === true) === true;
41699
+ }
41700
+ function keepAttributed(st, name, unsets = false) {
41701
+ const before = st.vars.get(name);
41702
+ const maybeUnset = unsets || before === undefined || isUnset(before) || before.some((piece) => piece.maybeUnset === true);
41703
+ const kept = [
41704
+ {
41705
+ ...unknown2(`\`$${name}\`, whose \`readonly\`, \`-i\`, \`-l\` or \`-u\` refuses or rewrites a write`),
41706
+ rewrites: true,
41707
+ ...maybeUnset ? { maybeUnset: true } : {}
41708
+ }
41709
+ ];
41710
+ st.vars.set(name, kept);
41711
+ if (st.arrays.has(name))
41712
+ st.arrays.set(name, new Map([[UNKNOWN_ARRAY_INDEX, kept]]));
41713
+ }
41714
+ function assignByName(st, name, value, writer) {
41715
+ if (name === undefined) {
41716
+ forgetVariables(st, `${writer} under a name the guard cannot read may have changed`);
41717
+ return;
41718
+ }
41719
+ if (IDENTIFIER.test(name)) {
41720
+ assignScalar(st, name, value);
41721
+ return;
41722
+ }
41723
+ const base = ARRAY_ELEMENT.exec(name)?.[1];
41724
+ if (base === undefined)
41725
+ return;
41726
+ if (attributed(st, base)) {
41727
+ keepAttributed(st, base, true);
41728
+ return;
41729
+ }
41730
+ const element = [
41731
+ { ...unknown2(`\`$${base}\`, whose element ${writer} assigns`), maybeUnset: true }
41732
+ ];
41733
+ st.vars.set(base, element);
41734
+ st.arrays.set(base, new Map([[UNKNOWN_ARRAY_INDEX, element]]));
41735
+ }
41736
+ function forgetVariables(st, why) {
41737
+ const forgotten = (name) => [
41738
+ {
41739
+ ...unknown2(`\`$${name}\`, which ${why}`),
41740
+ maybeUnset: true,
41741
+ ...attributed(st, name) ? { rewrites: true } : {}
41742
+ }
41743
+ ];
41744
+ for (const name of st.vars.keys())
41745
+ if (name !== ANY_NAME)
41746
+ st.vars.set(name, forgotten(name));
41747
+ st.vars.set(ANY_NAME, [{ ...unknown2(why), maybeUnset: true }]);
41748
+ for (const name of st.arrays.keys()) {
41749
+ st.arrays.set(name, new Map([[UNKNOWN_ARRAY_INDEX, forgotten(name)]]));
41750
+ }
41751
+ }
41525
41752
  function resolveArrayIndex(index, st, ctx) {
41526
41753
  if (index === undefined)
41527
41754
  return;
@@ -41534,7 +41761,7 @@ function parameter(name, quoted, st, ctx) {
41534
41761
  if (name === "@" || name === "*") {
41535
41762
  if (st.positional === undefined)
41536
41763
  return [[unknown2(`\`$${name}\` (the positional parameters)`)]];
41537
- return st.positional.length === 0 ? [[literal2("")]] : st.positional.map((p) => [...p]);
41764
+ return elements(st.positional, name === "*" && quoted);
41538
41765
  }
41539
41766
  if (/^[0-9]+$/.test(name)) {
41540
41767
  if (name === "0")
@@ -41542,6 +41769,8 @@ function parameter(name, quoted, st, ctx) {
41542
41769
  const value2 = st.positional?.[Number(name) - 1];
41543
41770
  if (value2 !== undefined)
41544
41771
  return [[...value2]];
41772
+ if (st.positional !== undefined)
41773
+ return [[literal2("")]];
41545
41774
  return [[unknown2(`\`$${name}\` (a positional parameter)`)]];
41546
41775
  }
41547
41776
  if (name === "$") {
@@ -41550,8 +41779,12 @@ function parameter(name, quoted, st, ctx) {
41550
41779
  if (name === "!") {
41551
41780
  return [[unknown2("`$!` (the last background job)", st.backgroundStarted ? true : undefined)]];
41552
41781
  }
41782
+ if (name === "#" && st.positional !== undefined)
41783
+ return [[literal2(String(st.positional.length))]];
41553
41784
  if (["?", "#", "-"].includes(name))
41554
41785
  return [[unknown2(`\`$${name}\``)]];
41786
+ if (isUnset(st.vars.get(name)))
41787
+ return [[literal2("")]];
41555
41788
  const value = lookup(name, st, ctx);
41556
41789
  if (value === undefined) {
41557
41790
  return [[unknown2(`\`$${name}\`, which is not set in this command or the pane's environment`)]];
@@ -41569,6 +41802,30 @@ function parameter(name, quoted, st, ctx) {
41569
41802
  }
41570
41803
  return [pieces];
41571
41804
  }
41805
+ function elements(values, joined) {
41806
+ if (joined)
41807
+ return [values.flatMap((value, index) => index === 0 ? value : [literal2(" "), ...value])];
41808
+ return values.length === 0 ? [[NO_ELEMENTS]] : values.map((value) => [...value]);
41809
+ }
41810
+ function operatorValue(name, st, ctx) {
41811
+ const unknownValue = [unknown2(`\`$${name}\``)];
41812
+ if (!/^([0-9]+|[@*#?$!-])$/.test(name)) {
41813
+ const value = lookup(name, st, ctx);
41814
+ if (value === undefined)
41815
+ return { set: false, value: [] };
41816
+ return { set: value.some((piece) => piece.maybeUnset === true) ? undefined : true, value };
41817
+ }
41818
+ if (/^[1-9][0-9]*$/.test(name) || name === "@" || name === "*") {
41819
+ if (st.positional === undefined)
41820
+ return { set: undefined, value: unknownValue };
41821
+ const value = name === "@" || name === "*" ? st.positional.length === 0 ? undefined : elements(st.positional, true)[0] : st.positional[Number(name) - 1];
41822
+ return value === undefined ? { set: false, value: [] } : { set: true, value };
41823
+ }
41824
+ if (name === "!" && !st.backgroundStarted)
41825
+ return { set: undefined, value: unknownValue };
41826
+ const alternatives = parameter(name, true, st, ctx);
41827
+ return { set: true, value: alternatives.length === 1 ? alternatives[0] ?? [] : unknownValue };
41828
+ }
41572
41829
  function parameterExpansion(part, quoted, st, ctx) {
41573
41830
  const name = part.parameter;
41574
41831
  if (name === "BASH_SOURCE" && part.index === "0" && st.script !== undefined) {
@@ -41580,8 +41837,7 @@ function parameterExpansion(part, quoted, st, ctx) {
41580
41837
  const unknownElement = array2.get(UNKNOWN_ARRAY_INDEX);
41581
41838
  if (unknownElement !== undefined)
41582
41839
  return [[...unknownElement]];
41583
- const values = [...array2.values()];
41584
- return values.length === 0 ? [[literal2("")]] : values.map((value2) => [...value2]);
41840
+ return elements([...array2.values()], part.index === "*" && quoted);
41585
41841
  }
41586
41842
  return parameter(name, quoted, st, ctx);
41587
41843
  }
@@ -41592,33 +41848,50 @@ function parameterExpansion(part, quoted, st, ctx) {
41592
41848
  }
41593
41849
  return [[unknown2(`\`${part.text}\``)]];
41594
41850
  }
41595
- if (part.length || part.indirect || part.slice || part.replace) {
41851
+ if (part.length || part.indirect || part.slice) {
41596
41852
  return [[unknown2(`\`${part.text}\``)]];
41597
41853
  }
41854
+ if (part.replace !== undefined || ["#", "##", "%", "%%"].includes(part.operator ?? "")) {
41855
+ return patternExpansion(part, quoted, st, ctx);
41856
+ }
41598
41857
  const operator = part.operator;
41599
41858
  if (operator === undefined)
41600
41859
  return parameter(name, quoted, st, ctx);
41601
- const value = lookup(name, st, ctx);
41602
- const set2 = value !== undefined;
41603
- const nonEmpty2 = set2 && value.map((p) => p.text).join("") !== "";
41604
- const known = !set2 || value.every((p) => p.kind === "literal");
41860
+ const { set: set2, value } = operatorValue(name, st, ctx);
41861
+ const nonEmpty2 = set2 === true && value.map((p) => p.text).join("") !== "";
41862
+ const known = set2 === false || set2 === true && value.every((p) => p.kind === "literal");
41863
+ const surelyNonEmpty = set2 === true && value.some((p) => p.descendantPid === true || p.kind === "literal" && p.text !== "");
41864
+ const undecided = [[unknown2(`\`${part.text}\``)]];
41605
41865
  const operand = () => part.operand === undefined ? [[literal2("")]] : expandWord(part.operand, st, ctx);
41866
+ const assigning = (result, maybe) => {
41867
+ if ((operator === ":=" || operator === "=") && IDENTIFIER.test(name)) {
41868
+ const only = result.length === 1 && !maybe ? result[0] : undefined;
41869
+ assignScalar(st, name, only ?? [unknown2(`\`$${name}\`, which \`${part.text}\` may assign`)]);
41870
+ }
41871
+ return result;
41872
+ };
41606
41873
  switch (operator) {
41607
41874
  case ":-":
41608
41875
  case ":=":
41876
+ if (surelyNonEmpty)
41877
+ return parameter(name, quoted, st, ctx);
41609
41878
  if (!known)
41610
- return [[unknown2(`\`${part.text}\``)]];
41611
- return nonEmpty2 ? parameter(name, quoted, st, ctx) : operand();
41879
+ return assigning(undecided, true);
41880
+ return nonEmpty2 ? parameter(name, quoted, st, ctx) : assigning(operand(), false);
41612
41881
  case "-":
41613
41882
  case "=":
41614
- if (!known)
41615
- return [[unknown2(`\`${part.text}\``)]];
41616
- return set2 ? parameter(name, quoted, st, ctx) : operand();
41883
+ if (set2 === undefined)
41884
+ return assigning(undecided, true);
41885
+ return set2 ? parameter(name, quoted, st, ctx) : assigning(operand(), false);
41617
41886
  case ":+":
41887
+ if (surelyNonEmpty)
41888
+ return operand();
41618
41889
  if (!known)
41619
- return [[unknown2(`\`${part.text}\``)]];
41890
+ return undecided;
41620
41891
  return nonEmpty2 ? operand() : [[literal2("")]];
41621
41892
  case "+":
41893
+ if (set2 === undefined)
41894
+ return undecided;
41622
41895
  return set2 ? operand() : [[literal2("")]];
41623
41896
  case ":?":
41624
41897
  case "?":
@@ -41627,6 +41900,57 @@ function parameterExpansion(part, quoted, st, ctx) {
41627
41900
  return [[unknown2(`\`${part.text}\``)]];
41628
41901
  }
41629
41902
  }
41903
+ function patternExpansion(part, quoted, st, ctx) {
41904
+ const unknownResult = [[unknown2(`\`${part.text}\``)]];
41905
+ const because = (pieces) => {
41906
+ const cause = pieces.find((piece) => piece.kind === "unknown")?.why;
41907
+ return cause === undefined ? unknownResult : [[unknown2(`\`${part.text}\` (${cause})`)]];
41908
+ };
41909
+ const operator = part.operator ?? "";
41910
+ const head = `\${${part.parameter}${operator}`;
41911
+ const accounted = part.replace === undefined ? part.text === `${head}${part.operand?.text ?? ""}}` : part.text === `${head}${part.replace.pattern.text}/${part.replace.replacement.text}}` || part.replace.replacement.text === "" && part.text === `${head}${part.replace.pattern.text}}`;
41912
+ const slashPattern = operator === "/" || operator === "//";
41913
+ if (!accounted || slashPattern && part.replace?.pattern.text === "")
41914
+ return unknownResult;
41915
+ const { set: set2, value: held } = operatorValue(part.parameter, st, ctx);
41916
+ const knownUnset = set2 === false && (/^([1-9][0-9]*|[@*])$/.test(part.parameter) || isUnset(st.vars.get(part.parameter)));
41917
+ const value = set2 === true ? literalText(held) : knownUnset ? "" : undefined;
41918
+ if (value === undefined)
41919
+ return because(held);
41920
+ if (value.length > MAX_PATTERN_VALUE || !isAscii(value))
41921
+ return unknownResult;
41922
+ const patternWord = part.replace?.pattern ?? part.operand;
41923
+ const patterns = patternWord === undefined ? [[literal2("")]] : expandWord(patternWord, st, ctx);
41924
+ const glob = patterns.length === 1 ? patternGlob(patterns[0]) : undefined;
41925
+ if (glob === undefined || slashPattern && glob.length === 0)
41926
+ return unknownResult;
41927
+ if (!spendPatternWork(globWork(glob, value.length, operator), ctx))
41928
+ return unknownResult;
41929
+ let result;
41930
+ if (part.replace === undefined) {
41931
+ result = removePattern(value, operator, glob);
41932
+ } else {
41933
+ const replacements = expandWord(part.replace.replacement, st, ctx);
41934
+ const pieces = replacements.length === 1 ? replacements[0] : undefined;
41935
+ if (pieces === undefined)
41936
+ return unknownResult;
41937
+ if (pieces.some((piece) => piece.kind === "unknown"))
41938
+ return because(pieces);
41939
+ const replacement = pieces.map((piece) => piece.text).join("");
41940
+ if (/[&\\]/.test(replacement))
41941
+ return unknownResult;
41942
+ const most = value.length + (operator === "//" ? Math.max(value.length, 1) : 1) * replacement.length;
41943
+ if (most > MAX_VALUE_LENGTH)
41944
+ return [[overLong(`\`${part.text}\``)]];
41945
+ result = replacePattern(value, operator, glob, replacement);
41946
+ }
41947
+ if (quoted)
41948
+ return [[literal2(result)]];
41949
+ if (/\s/.test(result)) {
41950
+ return [[unknown2(`unquoted \`${part.text}\`, which splits into several words`)]];
41951
+ }
41952
+ return [unquotedLiteral(result.replaceAll("\\", "\\\\"), undefined, st, ctx)];
41953
+ }
41630
41954
  function substitution(script, text, st, ctx) {
41631
41955
  const pidRead = /^\$\(<(.+)\)$/.exec(text);
41632
41956
  const variableRead = /^\$\(<"?\$([A-Za-z_][A-Za-z0-9_]*)"?\)$/.exec(text);
@@ -41693,6 +42017,8 @@ function substitutionPath(base, list, st) {
41693
42017
  const target = literalText(operands(list, "").operands.at(-1)?.exp);
41694
42018
  if (target === undefined || st.cwd === undefined)
41695
42019
  return;
42020
+ if (target === "")
42021
+ return base === "dirname" ? "." : "";
41696
42022
  const abs = path4.resolve(st.cwd, target);
41697
42023
  if (base === "dirname")
41698
42024
  return path4.dirname(abs);
@@ -41771,11 +42097,23 @@ function product(left, right) {
41771
42097
  if (out.length >= MAX_ALTERNATIVES) {
41772
42098
  return [[unknown2(`a brace expansion with more than ${MAX_ALTERNATIVES} alternatives`)]];
41773
42099
  }
41774
- out.push([...a, ...b]);
42100
+ const long = a.length + b.length > MAX_VALUE_PIECES || textLength(a) + textLength(b) > MAX_VALUE_LENGTH;
42101
+ out.push(long ? [overLong("a word")] : [...a, ...b]);
41775
42102
  }
41776
42103
  }
41777
42104
  return out;
41778
42105
  }
42106
+ var MAX_VALUE_LENGTH = 65536;
42107
+ var MAX_VALUE_PIECES = 4096;
42108
+ function overLong(what) {
42109
+ return unknown2(`${what}, longer than the guard builds (${MAX_VALUE_LENGTH} characters or ${MAX_VALUE_PIECES} pieces)`);
42110
+ }
42111
+ function textLength(pieces) {
42112
+ let length = 0;
42113
+ for (const piece of pieces)
42114
+ length += piece.text.length;
42115
+ return length;
42116
+ }
41779
42117
  function expandWord(word, st, ctx) {
41780
42118
  let alternatives = [[]];
41781
42119
  if (word.parts === undefined)
@@ -41790,8 +42128,38 @@ function expandWord(word, st, ctx) {
41790
42128
  function runtimeText(exp) {
41791
42129
  return exp.map((piece) => piece.kind === "unknown" ? UNKNOWN_MARKER : piece.text).join("");
41792
42130
  }
42131
+ var SPLIT_PARTS = new Set([
42132
+ "SimpleExpansion",
42133
+ "ParameterExpansion",
42134
+ "CommandExpansion",
42135
+ "ArithmeticExpansion"
42136
+ ]);
41793
42137
  function args(words, st, ctx) {
41794
- return words.flatMap((word) => expandWord(word, st, ctx).map((exp) => ({ text: word.text, exp })));
42138
+ return words.flatMap((word) => {
42139
+ const parts = word.parts ?? [];
42140
+ const unquoted = parts.some((part) => SPLIT_PARTS.has(part.type));
42141
+ const onlyExpansions = parts.length > 0 && parts.every((part) => SPLIT_PARTS.has(part.type));
42142
+ return expandWord(word, st, ctx).map((exp) => {
42143
+ if (exp.some((piece) => piece.kind === "glob")) {
42144
+ return { text: word.text, exp, fields: "unknown" };
42145
+ }
42146
+ if (unquoted && exp.some((piece) => piece.kind === "unknown" || /[ \t\n]/.test(piece.text))) {
42147
+ return { text: word.text, exp, fields: "unknown" };
42148
+ }
42149
+ if (onlyExpansions && exp.every((piece) => piece.text === "")) {
42150
+ return { text: word.text, exp, fields: "none" };
42151
+ }
42152
+ if (exp.length > 0 && exp.every((piece) => piece.noElements === true)) {
42153
+ return { text: word.text, exp, fields: "none" };
42154
+ }
42155
+ return { text: word.text, exp };
42156
+ });
42157
+ });
42158
+ }
42159
+ function positionalOf(list) {
42160
+ if (list.some((arg) => arg.fields === "unknown"))
42161
+ return;
42162
+ return list.filter((arg) => arg.fields !== "none").map((arg) => arg.exp);
41795
42163
  }
41796
42164
  function literalText(exp) {
41797
42165
  if (exp === undefined || exp.some((piece) => piece.kind !== "literal"))
@@ -41838,6 +42206,19 @@ function visitWord(word, st, ctx) {
41838
42206
  if (word !== undefined)
41839
42207
  visitParts(word.parts, st, ctx);
41840
42208
  }
42209
+ var ARITHMETIC_ASSIGNMENTS = new Set([
42210
+ "=",
42211
+ "+=",
42212
+ "-=",
42213
+ "*=",
42214
+ "/=",
42215
+ "%=",
42216
+ "<<=",
42217
+ ">>=",
42218
+ "&=",
42219
+ "^=",
42220
+ "|="
42221
+ ]);
41841
42222
  function visitArithmetic(expression, st, ctx) {
41842
42223
  if (expression === undefined)
41843
42224
  return;
@@ -41845,9 +42226,14 @@ function visitArithmetic(expression, st, ctx) {
41845
42226
  case "ArithmeticBinary":
41846
42227
  visitArithmetic(expression.left, st, ctx);
41847
42228
  visitArithmetic(expression.right, st, ctx);
42229
+ if (ARITHMETIC_ASSIGNMENTS.has(expression.operator))
42230
+ assignArithmetic(expression.left, st);
41848
42231
  break;
41849
42232
  case "ArithmeticUnary":
41850
42233
  visitArithmetic(expression.operand, st, ctx);
42234
+ if (expression.operator === "++" || expression.operator === "--") {
42235
+ assignArithmetic(expression.operand, st);
42236
+ }
41851
42237
  break;
41852
42238
  case "ArithmeticTernary":
41853
42239
  visitArithmetic(expression.test, st, ctx);
@@ -41865,6 +42251,13 @@ function visitArithmetic(expression, st, ctx) {
41865
42251
  break;
41866
42252
  }
41867
42253
  }
42254
+ function assignArithmetic(target, st) {
42255
+ if (target.type !== "ArithmeticWord")
42256
+ return;
42257
+ const name = /[$`]/.test(target.value) ? undefined : target.value;
42258
+ const number4 = [unknown2(`\`$${target.value}\`, an arithmetic result`)];
42259
+ assignByName(st, name, number4, "an arithmetic expression");
42260
+ }
41868
42261
  function visitTest(expression, st, ctx) {
41869
42262
  switch (expression.type) {
41870
42263
  case "TestUnary":
@@ -41899,11 +42292,12 @@ function clone2(st) {
41899
42292
  ...st,
41900
42293
  vars: new Map(st.vars),
41901
42294
  exported: new Set(st.exported),
41902
- arrays: new Map([...st.arrays].map(([name, elements]) => [name, new Map(elements)])),
42295
+ arrays: new Map([...st.arrays].map(([name, elements2]) => [name, new Map(elements2)])),
41903
42296
  functions: new Map(st.functions),
41904
42297
  traps: [...st.traps],
41905
42298
  runningFunctions: new Set(st.runningFunctions),
41906
- pidFiles: st.pidFiles
42299
+ pidFiles: st.pidFiles,
42300
+ locals: st.locals === undefined ? undefined : new Map(st.locals)
41907
42301
  };
41908
42302
  }
41909
42303
  function merge2(target, branches) {
@@ -41917,9 +42311,35 @@ function merge2(target, branches) {
41917
42311
  if (first !== undefined && values.every((value) => value === values[0])) {
41918
42312
  target.vars.set(name, first);
41919
42313
  } else {
41920
- target.vars.set(name, [unknown2(`\`$${name}\`, which a branch sets differently`)]);
42314
+ const signalSafe = branches.every((branch) => {
42315
+ const value = branch.vars.get(name);
42316
+ return value !== undefined && (literalText(value) === "" || value.length > 0 && value.every((piece) => piece.descendantPid === true));
42317
+ });
42318
+ const maybeUnset = branches.some((branch) => {
42319
+ const value = branch.vars.get(name);
42320
+ return value === undefined || isUnset(value) || value.some((piece) => piece.maybeUnset);
42321
+ });
42322
+ const rewrites = branches.some((branch) => attributed(branch, name));
42323
+ const cause = branches.flatMap((branch) => branch.vars.get(name) ?? []).find((piece) => piece.kind === "unknown")?.why;
42324
+ const merged2 = unknown2(name === ANY_NAME ? `${cause ?? "a write the guard cannot read may have changed"}, in a branch or loop body` : `\`$${name}\`, which a branch sets differently${cause === undefined ? "" : ` (${cause})`}`, signalSafe ? true : undefined);
42325
+ target.vars.set(name, [
42326
+ {
42327
+ ...merged2,
42328
+ ...maybeUnset ? { maybeUnset: true } : {},
42329
+ ...rewrites ? { rewrites: true } : {}
42330
+ }
42331
+ ]);
41921
42332
  }
41922
42333
  }
42334
+ if (target.locals !== undefined) {
42335
+ const locals = new Map;
42336
+ for (const branch of branches) {
42337
+ for (const name of branch.locals?.keys() ?? []) {
42338
+ locals.set(name, branches.every((other) => other.locals?.get(name) === true));
42339
+ }
42340
+ }
42341
+ target.locals = locals;
42342
+ }
41923
42343
  const arrayNames = new Set;
41924
42344
  for (const branch of branches)
41925
42345
  for (const name of branch.arrays.keys())
@@ -42035,13 +42455,108 @@ function walkList(statements, st, ctx) {
42035
42455
  for (const statement of statements)
42036
42456
  walkNode(statement, st, ctx, false);
42037
42457
  }
42458
+ var MAX_DECIDED_PASSES = 64;
42459
+ function walkLoopAnyPasses(node, st, ctx) {
42460
+ const before = st.output;
42461
+ const body = clone2(st);
42462
+ walkList(node.clause.commands, body, ctx);
42463
+ walkList(node.body.commands, body, ctx);
42464
+ merge2(st, [clone2(st), body]);
42465
+ if (outputChanged(before, body.output))
42466
+ st.output = undefined;
42467
+ }
42468
+ function walkLoop(node, st, ctx) {
42469
+ if (!/\b(break|continue)\b/.test(st.source.slice(node.body.pos, node.body.end))) {
42470
+ for (let pass = 0;pass < MAX_DECIDED_PASSES; pass += 1) {
42471
+ const holds = decideCondition(node.clause, st, ctx);
42472
+ if (holds === undefined)
42473
+ break;
42474
+ walkList(node.clause.commands, st, ctx);
42475
+ if (holds === (node.kind === "until"))
42476
+ return;
42477
+ walkList(node.body.commands, st, ctx);
42478
+ }
42479
+ }
42480
+ walkLoopAnyPasses(node, st, ctx);
42481
+ }
42482
+ function decideCondition(clause, st, ctx) {
42483
+ const [statement, ...others] = clause.commands;
42484
+ if (statement === undefined || others.length > 0 || statement.background)
42485
+ return;
42486
+ const command = statement.command;
42487
+ if (statement.redirects.length > 0 || command.type !== "Command" || command.name === undefined || command.prefix.length > 0 || command.redirects.length > 0) {
42488
+ return;
42489
+ }
42490
+ const name = command.name.text;
42491
+ if (name !== "[" && name !== "test")
42492
+ return;
42493
+ const argv = args(command.suffix, st, ctx);
42494
+ if (argv.length !== command.suffix.length)
42495
+ return;
42496
+ const words = argv.map((arg) => literalText(arg.exp));
42497
+ if (words.some((word) => word === undefined || word === ""))
42498
+ return;
42499
+ const operands = words;
42500
+ if (name === "[")
42501
+ return operands.at(-1) === "]" ? evaluateTest(operands.slice(0, -1)) : undefined;
42502
+ return evaluateTest(operands);
42503
+ }
42504
+ function decideCase(node, st, ctx) {
42505
+ const words = expandWord(node.word, st, ctx);
42506
+ const value = words.length === 1 ? literalText(words[0]) : undefined;
42507
+ if (value === undefined)
42508
+ return;
42509
+ for (const item of node.items) {
42510
+ for (const word of item.pattern) {
42511
+ const patterns = expandWord(word, st, ctx);
42512
+ const matched = patterns.length === 1 ? matchesPattern(patterns[0], value, ctx) : undefined;
42513
+ if (matched === undefined)
42514
+ return;
42515
+ if (matched)
42516
+ return item.terminator === undefined || item.terminator === ";;" ? item : undefined;
42517
+ }
42518
+ }
42519
+ return;
42520
+ }
42521
+ function matchesPattern(pattern, value, ctx) {
42522
+ const glob = patternGlob(pattern);
42523
+ if (glob === undefined || !isAscii(value))
42524
+ return;
42525
+ if (!spendPatternWork(globWork(glob, value.length, ""), ctx))
42526
+ return;
42527
+ return globMatches(glob, value);
42528
+ }
42529
+ function patternGlob(pattern) {
42530
+ const glob = [];
42531
+ for (const piece of pattern) {
42532
+ if (piece.kind === "unknown" || !isAscii(piece.text))
42533
+ return;
42534
+ if (piece.kind === "glob") {
42535
+ if (piece.text === "*")
42536
+ glob.push(ANY_RUN);
42537
+ else if (piece.text === "?")
42538
+ glob.push(ANY_ONE);
42539
+ else
42540
+ return;
42541
+ } else if (/[()|]/.test(piece.text))
42542
+ return;
42543
+ else
42544
+ for (let i = 0;i < piece.text.length; i += 1)
42545
+ glob.push(piece.text.charCodeAt(i));
42546
+ }
42547
+ return glob;
42548
+ }
42549
+ function spendPatternWork(work, ctx) {
42550
+ ctx.steps.count += Math.ceil(work / PATTERN_WORK_PER_STEP);
42551
+ return ctx.steps.count <= MAX_WALK_STEPS;
42552
+ }
42038
42553
  function outputChanged(before, after) {
42039
42554
  return before !== undefined && (after === undefined || JSON.stringify(before) !== JSON.stringify(after));
42040
42555
  }
42041
42556
  function walkNode(node, st, ctx, pipeIn) {
42042
42557
  if (++ctx.steps.count > MAX_WALK_STEPS) {
42043
42558
  const site = siteOf(node, st);
42044
- throw new Refusal(site.snippet, site.line, `the guard reached its walk limit of ${MAX_WALK_STEPS} nodes; run a smaller script`);
42559
+ throw new Refusal(site.snippet, site.line, WALK_LIMIT);
42045
42560
  }
42046
42561
  switch (node.type) {
42047
42562
  case "Statement": {
@@ -42056,6 +42571,13 @@ function walkNode(node, st, ctx, pipeIn) {
42056
42571
  st.backgroundStarted = true;
42057
42572
  return;
42058
42573
  }
42574
+ if (node.command.type === "BraceGroup") {
42575
+ const written = groupOutputFile(node, st, ctx);
42576
+ if (written !== undefined) {
42577
+ st.files.set(written, walkRenderingGroup(node.command, st, ctx));
42578
+ return;
42579
+ }
42580
+ }
42059
42581
  walkNode(node.command, st, ctx, pipeIn);
42060
42582
  return;
42061
42583
  }
@@ -42126,7 +42648,7 @@ function walkNode(node, st, ctx, pipeIn) {
42126
42648
  if (node.type === "For" && known.every((w) => w !== undefined) && known.length <= 16) {
42127
42649
  for (const value of known) {
42128
42650
  const body = clone2(st);
42129
- body.vars.set(name, [literal2(value)]);
42651
+ assignScalar(body, name, [literal2(value)]);
42130
42652
  walkList(node.body.commands, body, ctx);
42131
42653
  branches.push(body);
42132
42654
  }
@@ -42136,7 +42658,9 @@ function walkNode(node, st, ctx, pipeIn) {
42136
42658
  const text = literalText(word.exp);
42137
42659
  return text === "" || word.exp.length > 0 && word.exp.every((piece) => piece.descendantPid);
42138
42660
  });
42139
- body.vars.set(name, signalSafe ? [unknown2(`\`$${name}\`, a loop variable`, true)] : [unknown2(`\`$${name}\`, a loop variable`)]);
42661
+ assignScalar(body, name, signalSafe ? [unknown2(`\`$${name}\`, a loop variable`, true)] : [unknown2(`\`$${name}\`, a loop variable`)]);
42662
+ if (node.type === "Select")
42663
+ assignScalar(body, "REPLY", [unknown2("`$REPLY`, read from input")]);
42140
42664
  walkList(node.body.commands, body, ctx);
42141
42665
  branches.push(body);
42142
42666
  }
@@ -42158,16 +42682,19 @@ function walkNode(node, st, ctx, pipeIn) {
42158
42682
  return;
42159
42683
  }
42160
42684
  case "While": {
42161
- const before = st.output;
42162
- const body = clone2(st);
42163
- walkList(node.clause.commands, body, ctx);
42164
- walkList(node.body.commands, body, ctx);
42165
- merge2(st, [clone2(st), body]);
42166
- if (outputChanged(before, body.output))
42167
- st.output = undefined;
42685
+ walkLoop(node, st, ctx);
42168
42686
  return;
42169
42687
  }
42170
42688
  case "Case": {
42689
+ const chosen = decideCase(node, st, ctx);
42690
+ if (chosen !== undefined) {
42691
+ visitWord(node.word, st, ctx);
42692
+ for (const item of node.items)
42693
+ for (const word of item.pattern)
42694
+ visitWord(word, st, ctx);
42695
+ walkList(chosen.body.commands, st, ctx);
42696
+ return;
42697
+ }
42171
42698
  const before = st.output;
42172
42699
  visitWord(node.word, st, ctx);
42173
42700
  const branches = [clone2(st)];
@@ -42237,15 +42764,18 @@ function checkRedirects(redirects, command, site, st, ctx) {
42237
42764
  const file2 = path4.resolve(st.cwd ?? "/", target);
42238
42765
  let content = null;
42239
42766
  if (command?.name === "cat" && command.args.length === 0) {
42240
- content = heredoc?.content ?? (herestring ? herestringText(herestring, st, ctx) : null);
42241
- } else if (command?.name === "echo" || command?.name === "printf") {
42242
- const words = command.args.map((arg) => literalText(arg.exp));
42243
- content = words.every((word) => word !== undefined) ? `${words.join(" ")}
42244
- ` : null;
42767
+ if (heredoc !== undefined)
42768
+ content = heredocText(heredoc);
42769
+ else if (herestring !== undefined)
42770
+ content = herestringText(herestring, st, ctx);
42771
+ } else if (command?.name === "echo") {
42772
+ content = echoText(command.args);
42773
+ } else if (command?.name === "printf") {
42774
+ content = printfText(command.args, MAX_SCRIPT_BYTES, true) ?? null;
42245
42775
  }
42246
42776
  st.pidFiles.delete(file2);
42247
42777
  const before = appends ? st.files.get(file2) : "";
42248
- st.files.set(file2, content === null || before === null ? null : `${before ?? ""}${content}`);
42778
+ st.files.set(file2, content === null || before === null ? null : readable(`${before ?? ""}${content}`));
42249
42779
  if (writes && !appends && command?.name === "echo" && command.args.length === 1 && command.args[0]?.exp.length > 0 && command.args[0]?.exp.every((piece) => piece.descendantPid)) {
42250
42780
  st.pidFiles.set(file2, command.args[0].exp);
42251
42781
  }
@@ -42257,6 +42787,137 @@ function herestringText(redirect, st, ctx) {
42257
42787
  return null;
42258
42788
  return literalText(expandWord(redirect.target, st, ctx)[0]) ?? null;
42259
42789
  }
42790
+ function groupOutputFile(statement, st, ctx) {
42791
+ const [redirect, ...others] = statement.redirects;
42792
+ if (redirect === undefined || others.length > 0 || redirect.fileDescriptor !== undefined) {
42793
+ return;
42794
+ }
42795
+ if (redirect.operator !== ">" && redirect.operator !== ">|" || redirect.target === undefined) {
42796
+ return;
42797
+ }
42798
+ const targets = expandWord(redirect.target, st, ctx);
42799
+ const target = targets.length === 1 ? literalText(targets[0]) : undefined;
42800
+ if (target === undefined || st.cwd === undefined && !target.startsWith("/"))
42801
+ return;
42802
+ return path4.resolve(st.cwd ?? "/", target);
42803
+ }
42804
+ function walkRenderingGroup(group, st, ctx) {
42805
+ let content = "";
42806
+ for (const statement of group.body.commands) {
42807
+ if (content !== null) {
42808
+ const text = statementOutput(statement, st, ctx);
42809
+ content = text === undefined ? null : readable(content + text);
42810
+ }
42811
+ walkNode(statement, st, ctx, false);
42812
+ }
42813
+ return content;
42814
+ }
42815
+ function echoText(list) {
42816
+ if (literalText(list[0]?.exp)?.startsWith("-"))
42817
+ return null;
42818
+ const words = [];
42819
+ let length = 0;
42820
+ for (const arg of list) {
42821
+ const word = literalText(arg.exp);
42822
+ if (word === undefined)
42823
+ return null;
42824
+ words.push(word);
42825
+ length += word.length + 1;
42826
+ }
42827
+ return length > MAX_SCRIPT_BYTES ? null : `${words.join(" ")}
42828
+ `;
42829
+ }
42830
+ function readable(content) {
42831
+ return content.length > MAX_SCRIPT_BYTES ? null : content;
42832
+ }
42833
+ function heredocText(heredoc) {
42834
+ const text = heredoc.content;
42835
+ if (text === undefined)
42836
+ return null;
42837
+ return !heredoc.heredocQuoted && /[$`]|\\[$`\\\n]/.test(text) ? null : text;
42838
+ }
42839
+ function statementOutput(statement, st, ctx) {
42840
+ const command = statement.command;
42841
+ if (statement.background || statement.redirects.length > 0 || command.type !== "Command") {
42842
+ return;
42843
+ }
42844
+ if (command.name === undefined)
42845
+ return command.redirects.length === 0 ? "" : undefined;
42846
+ const name = literalText(expandWord(command.name, st, ctx)[0]);
42847
+ const rest = args(command.suffix, st, ctx);
42848
+ if (name === "cat" && rest.length === 0) {
42849
+ const [heredoc, ...others] = command.redirects;
42850
+ if (heredoc === undefined || others.length > 0)
42851
+ return;
42852
+ if (heredoc.operator !== "<<" && heredoc.operator !== "<<-")
42853
+ return;
42854
+ return heredocText(heredoc) ?? undefined;
42855
+ }
42856
+ if (command.redirects.length > 0)
42857
+ return;
42858
+ if (name === "printf")
42859
+ return printfText(rest, MAX_SCRIPT_BYTES, true);
42860
+ if (name === "echo")
42861
+ return echoText(rest) ?? undefined;
42862
+ return;
42863
+ }
42864
+ function printfText(list, limit, script = false) {
42865
+ const [formatArg, ...values] = list;
42866
+ const format = literalText(formatArg?.exp);
42867
+ if (format === undefined || format.startsWith("-"))
42868
+ return;
42869
+ if (script && values.some((value) => value.fields === "unknown"))
42870
+ return;
42871
+ const parts = [];
42872
+ for (let i = 0;i < format.length; i += 1) {
42873
+ const char = format.charAt(i);
42874
+ const next = format.charAt(i + 1);
42875
+ if (char === "%") {
42876
+ if (next === "%")
42877
+ parts.push({ text: "%" });
42878
+ else if (next === "s" || next === "q" || next === "d")
42879
+ parts.push({ conversion: next });
42880
+ else
42881
+ return;
42882
+ i += 1;
42883
+ } else if (char === "\\") {
42884
+ const text = next === "n" ? `
42885
+ ` : next === "t" ? "\t" : next === "\\" ? "\\" : undefined;
42886
+ if (text === undefined)
42887
+ return;
42888
+ parts.push({ text });
42889
+ i += 1;
42890
+ } else
42891
+ parts.push({ text: char });
42892
+ }
42893
+ const conversions = parts.filter((part) => ("conversion" in part)).length;
42894
+ let out = "";
42895
+ let index = 0;
42896
+ do {
42897
+ for (const part of parts) {
42898
+ if ("text" in part) {
42899
+ out += part.text;
42900
+ continue;
42901
+ }
42902
+ const value = values[index];
42903
+ index += 1;
42904
+ if (script && part.conversion !== "d" && value !== undefined) {
42905
+ if (literalText(value.exp) === undefined)
42906
+ return;
42907
+ }
42908
+ const text = value === undefined ? "" : runtimeText(value.exp);
42909
+ if (part.conversion === "s")
42910
+ out += text;
42911
+ else if (part.conversion === "d")
42912
+ out += /^-?[0-9]+$/.test(text) ? text : value === undefined ? "0" : UNKNOWN_MARKER;
42913
+ else
42914
+ out += shellQuoted(text);
42915
+ if (out.length > limit)
42916
+ return out;
42917
+ }
42918
+ } while (conversions > 0 && index < values.length);
42919
+ return out;
42920
+ }
42260
42921
  function refusal(site, detail, ctx) {
42261
42922
  return new Refusal(site.snippet, site.line, `${detail}, outside ${describeRoots(ctx.roots)}. ${HINT}`);
42262
42923
  }
@@ -42270,39 +42931,51 @@ function handleCommand(command, st, ctx, pipeIn) {
42270
42931
  visitWord(word, st, ctx);
42271
42932
  if (assignment.name === undefined)
42272
42933
  continue;
42273
- const expanded = assignment.value === undefined ? [literal2("")] : expandWord(assignment.value, st, ctx)[0] ?? [];
42934
+ const expanded = stored(assignment.value === undefined ? [literal2("")] : expandWord(assignment.value, st, ctx)[0] ?? []);
42274
42935
  const arrayAssignment = assignment.array !== undefined || assignment.append || assignment.index !== undefined;
42275
42936
  const descendantPid = expanded.length > 0 && expanded.every((piece) => piece.descendantPid);
42276
42937
  const value = arrayAssignment ? [unknown2(`\`$${assignment.name}\`, an array or appended value`, descendantPid || undefined)] : expanded;
42277
- overlay.set(assignment.name, value);
42938
+ overlay.set(assignment.name, attributed(st, assignment.name) ? [{ ...unknown2(`\`$${assignment.name}\`, an attributed variable`), rewrites: true }] : value);
42278
42939
  if (command.name === undefined) {
42279
- if (arrayAssignment) {
42280
- const elements = assignment.array !== undefined && !assignment.append ? new Map : new Map(st.arrays.get(assignment.name) ?? []);
42940
+ if (!arrayAssignment) {
42941
+ assignScalar(st, assignment.name, value);
42942
+ } else if (attributed(st, assignment.name)) {
42943
+ keepAttributed(st, assignment.name);
42944
+ } else {
42945
+ const elements2 = assignment.array !== undefined && !assignment.append ? new Map : new Map(st.arrays.get(assignment.name) ?? []);
42281
42946
  if (assignment.array !== undefined) {
42282
- let index = assignment.append ? Math.max(0, ...[...elements.keys()].filter((key) => /^[0-9]+$/.test(key)).map((key) => Number(key) + 1)) : 0;
42283
- for (const word of assignment.array) {
42284
- elements.set(String(index), expandWord(word, st, ctx)[0] ?? []);
42285
- index += 1;
42947
+ let index = assignment.append ? Math.max(0, ...[...elements2.keys()].filter((key) => /^[0-9]+$/.test(key)).map((key) => Number(key) + 1)) : 0;
42948
+ const words2 = args(assignment.array, st, ctx);
42949
+ if (words2.some((word) => word.fields === "unknown")) {
42950
+ elements2.clear();
42951
+ elements2.set(UNKNOWN_ARRAY_INDEX, [
42952
+ unknown2(`\`$${assignment.name}\`, an array a word of which may be several elements`)
42953
+ ]);
42954
+ } else {
42955
+ for (const word of words2) {
42956
+ if (word.fields === "none")
42957
+ continue;
42958
+ elements2.set(String(index), stored(word.exp));
42959
+ index += 1;
42960
+ }
42286
42961
  }
42287
42962
  } else if (assignment.append) {
42288
- const index = Math.max(0, ...[...elements.keys()].filter((key) => /^[0-9]+$/.test(key)).map((key) => Number(key) + 1));
42289
- elements.set(String(index), expanded);
42963
+ const index = Math.max(0, ...[...elements2.keys()].filter((key) => /^[0-9]+$/.test(key)).map((key) => Number(key) + 1));
42964
+ elements2.set(String(index), expanded);
42290
42965
  } else {
42291
42966
  const index = resolveArrayIndex(assignment.index, st, ctx);
42292
42967
  if (index === undefined) {
42293
- elements.clear();
42294
- elements.set(UNKNOWN_ARRAY_INDEX, [
42968
+ elements2.clear();
42969
+ elements2.set(UNKNOWN_ARRAY_INDEX, [
42295
42970
  unknown2(`\`$${assignment.name}\`, an array element with an unresolved index`)
42296
42971
  ]);
42297
42972
  } else {
42298
- elements.set(index, expanded);
42973
+ elements2.set(index, expanded);
42299
42974
  }
42300
42975
  }
42301
- st.arrays.set(assignment.name, elements);
42302
- } else {
42303
- st.arrays.delete(assignment.name);
42976
+ st.arrays.set(assignment.name, elements2);
42977
+ st.vars.set(assignment.name, value);
42304
42978
  }
42305
- st.vars.set(assignment.name, value);
42306
42979
  }
42307
42980
  }
42308
42981
  const words = command.name === undefined ? command.suffix : [command.name, ...command.suffix];
@@ -42441,7 +43114,7 @@ function unwrap2(argv, st, ctx, site) {
42441
43114
  list = rest.slice(i);
42442
43115
  state = { ...clone2(state), cwd, cwdWhy: "`env -C`" };
42443
43116
  for (const name2 of unset) {
42444
- state.vars.set(name2, [literal2("")]);
43117
+ state.vars.set(name2, UNSET);
42445
43118
  state.exported.delete(name2);
42446
43119
  }
42447
43120
  for (const [name2, value] of overlay) {
@@ -42458,9 +43131,10 @@ function runFunction(name, definition, args2, outer, ctx) {
42458
43131
  return;
42459
43132
  const child = {
42460
43133
  ...clone2(outer),
42461
- positional: args2.map((arg) => arg.exp),
43134
+ positional: positionalOf(args2),
42462
43135
  source: definition.source,
42463
- script: definition.file ?? outer.script
43136
+ script: definition.file ?? outer.script,
43137
+ locals: new Map
42464
43138
  };
42465
43139
  child.runningFunctions.add(name);
42466
43140
  try {
@@ -42468,11 +43142,12 @@ function runFunction(name, definition, args2, outer, ctx) {
42468
43142
  } catch (error48) {
42469
43143
  locateRefusal(error48, definition.file);
42470
43144
  }
43145
+ returnLocals(name, child, outer);
42471
43146
  outer.vars = child.vars;
42472
43147
  outer.exported = child.exported;
42473
43148
  outer.arrays.clear();
42474
- for (const [name2, elements] of child.arrays)
42475
- outer.arrays.set(name2, new Map(elements));
43149
+ for (const [name2, elements2] of child.arrays)
43150
+ outer.arrays.set(name2, new Map(elements2));
42476
43151
  outer.cwd = child.cwd;
42477
43152
  outer.cwdWhy = child.cwdWhy;
42478
43153
  outer.functions = child.functions;
@@ -42480,15 +43155,47 @@ function runFunction(name, definition, args2, outer, ctx) {
42480
43155
  outer.output = child.output;
42481
43156
  outer.backgroundStarted ||= child.backgroundStarted;
42482
43157
  }
43158
+ function returnLocals(name, child, caller) {
43159
+ for (const [local, everyPath] of child.locals ?? []) {
43160
+ const value = caller.vars.get(local);
43161
+ const elements2 = caller.arrays.get(local);
43162
+ if (everyPath) {
43163
+ if (value === undefined)
43164
+ child.vars.delete(local);
43165
+ else
43166
+ child.vars.set(local, value);
43167
+ if (elements2 === undefined)
43168
+ child.arrays.delete(local);
43169
+ else
43170
+ child.arrays.set(local, new Map(elements2));
43171
+ if (caller.exported.has(local))
43172
+ child.exported.add(local);
43173
+ else
43174
+ child.exported.delete(local);
43175
+ continue;
43176
+ }
43177
+ const same = JSON.stringify(child.vars.get(local) ?? null) === JSON.stringify(value ?? null) && JSON.stringify([...child.arrays.get(local) ?? []]) === JSON.stringify([...elements2 ?? []]);
43178
+ if (same)
43179
+ continue;
43180
+ const either = [
43181
+ { ...unknown2(`\`$${local}\`, local to \`${name}\` on some paths`), maybeUnset: true }
43182
+ ];
43183
+ child.vars.set(local, either);
43184
+ if (elements2 !== undefined || child.arrays.has(local)) {
43185
+ child.arrays.set(local, new Map([[UNKNOWN_ARRAY_INDEX, either]]));
43186
+ }
43187
+ }
43188
+ }
42483
43189
  function functionOutput(name, definition, args2, outer, ctx) {
42484
43190
  if (outer.runningFunctions.has(name))
42485
43191
  return;
42486
43192
  const child = {
42487
43193
  ...clone2(outer),
42488
- positional: args2.map((arg) => arg.exp),
43194
+ positional: positionalOf(args2),
42489
43195
  source: definition.source,
42490
43196
  script: definition.file ?? outer.script,
42491
- output: []
43197
+ output: [],
43198
+ locals: new Map
42492
43199
  };
42493
43200
  child.runningFunctions.add(name);
42494
43201
  try {
@@ -42529,7 +43236,9 @@ function dispatch(invocation, outer, ctx) {
42529
43236
  return;
42530
43237
  }
42531
43238
  const stdoutRedirected = invocation.redirects.some((redirect) => (redirect.fileDescriptor === undefined || redirect.fileDescriptor === 1) && [">", ">>", ">|", "&>", "&>>", ">&"].includes(redirect.operator));
42532
- if (outer.output !== undefined && !stdoutRedirected && base !== "echo" && base !== "printf" && FUNCTION_OUTPUT_SILENT[base] !== true) {
43239
+ const umaskWords = base === "umask" ? rest.map((arg) => literalText(arg.exp)) : [];
43240
+ const silentUmask = umaskWords.every((word) => word !== undefined) && umaskWords.some((word) => !word?.startsWith("-"));
43241
+ if (outer.output !== undefined && !stdoutRedirected && base !== "echo" && base !== "printf" && !silentUmask && FUNCTION_OUTPUT_SILENT[base] !== true) {
42533
43242
  outer.output = undefined;
42534
43243
  }
42535
43244
  switch (base) {
@@ -42553,35 +43262,68 @@ function dispatch(invocation, outer, ctx) {
42553
43262
  case "typeset":
42554
43263
  case "local":
42555
43264
  case "readonly":
42556
- declaration(base, rest, outer, ctx);
43265
+ declaration(base, rest, outer, ctx, site);
42557
43266
  return;
42558
- case "unset":
42559
- for (const arg of operands(rest, "").operands) {
42560
- const variable = literalText(arg.exp);
42561
- if (variable !== undefined) {
42562
- outer.vars.set(variable, [literal2("")]);
42563
- outer.arrays.delete(variable);
43267
+ case "unset": {
43268
+ const found = operands(rest, "");
43269
+ const functionsOnly = found.options.some((option) => /^-[a-zA-Z]*f/.test(option));
43270
+ const variablesOnly = found.options.some((option) => /^-[a-zA-Z]*v/.test(option));
43271
+ for (const arg of found.operands) {
43272
+ const name2 = literalText(arg.exp);
43273
+ if (!variablesOnly) {
43274
+ if (name2 === undefined) {
43275
+ for (const [fn, callee2] of outer.functions)
43276
+ outer.functions.set(fn, orCommand(callee2));
43277
+ } else {
43278
+ const callee2 = outer.functions.get(name2);
43279
+ const held = functionsOnly ? undefined : lookup(name2, outer, ctx);
43280
+ if (callee2 !== undefined && held === undefined)
43281
+ outer.functions.delete(name2);
43282
+ else if (callee2 !== undefined && held?.some((piece) => piece.maybeUnset)) {
43283
+ outer.functions.set(name2, orCommand(callee2));
43284
+ }
43285
+ }
42564
43286
  }
43287
+ if (functionsOnly)
43288
+ continue;
43289
+ if (name2 === undefined || !IDENTIFIER.test(name2)) {
43290
+ assignByName(outer, name2, [], "`unset`");
43291
+ continue;
43292
+ }
43293
+ if (attributed(outer, name2)) {
43294
+ keepAttributed(outer, name2, true);
43295
+ continue;
43296
+ }
43297
+ outer.vars.set(name2, UNSET);
43298
+ outer.exported.delete(name2);
43299
+ outer.arrays.delete(name2);
42565
43300
  }
42566
43301
  return;
43302
+ }
42567
43303
  case "read":
42568
43304
  case "mapfile":
42569
43305
  case "readarray":
42570
43306
  case "getopts": {
42571
- const found = operands(rest, base === "read" || base === "getopts" ? "adnNptui" : "");
42572
- const names = base === "getopts" ? found.operands.slice(1, 2) : found.operands;
42573
- const arrayAt = base === "read" ? rest.findIndex((arg) => literalText(arg.exp) === "-a") : -1;
42574
- const readsArray = base === "mapfile" || base === "readarray" || arrayAt !== -1;
42575
- const destinations = arrayAt === -1 || rest[arrayAt + 1] === undefined ? names : [rest[arrayAt + 1]];
42576
- for (const arg of destinations) {
42577
- const variable = literalText(arg.exp);
42578
- if (variable !== undefined) {
42579
- outer.vars.set(variable, [unknown2(`\`$${variable}\`, read from input`)]);
42580
- if (readsArray) {
42581
- const unknownArray = [unknown2(`\`$${variable}\`, read from input`)];
42582
- outer.arrays.set(variable, new Map([[UNKNOWN_ARRAY_INDEX, unknownArray]]));
42583
- }
42584
- }
43307
+ readInput(base, rest, outer);
43308
+ return;
43309
+ }
43310
+ case "wait": {
43311
+ for (const [index, arg] of rest.entries()) {
43312
+ if (!/^-[a-zA-Z]*p$/.test(literalText(arg.exp) ?? ""))
43313
+ continue;
43314
+ const pid = [{ ...unknown2("the pid `wait -p` assigns", true), maybeUnset: true }];
43315
+ assignByName(outer, literalText(rest[index + 1]?.exp), pid, "`wait -p`");
43316
+ }
43317
+ return;
43318
+ }
43319
+ case "let": {
43320
+ for (const arg of rest) {
43321
+ const text = literalText(arg.exp);
43322
+ const command = text === undefined ? undefined : parse5(`(( ${text} ))`).commands[0]?.command;
43323
+ if (command?.type === "ArithmeticCommand")
43324
+ visitArithmetic(command.expression, outer, ctx);
43325
+ else
43326
+ forgetVariables(outer, "`let` in text the guard cannot read may have changed");
42585
43327
  }
42586
43328
  return;
42587
43329
  }
@@ -42589,22 +43331,46 @@ function dispatch(invocation, outer, ctx) {
42589
43331
  const list = rest.map((arg) => literalText(arg.exp));
42590
43332
  const dashDash = list.indexOf("--");
42591
43333
  if (dashDash !== -1)
42592
- outer.positional = rest.slice(dashDash + 1).map((arg) => arg.exp);
43334
+ outer.positional = positionalOf(rest.slice(dashDash + 1));
42593
43335
  else if (list.length > 0 && !list[0]?.startsWith("-") && !list[0]?.startsWith("+")) {
42594
- outer.positional = rest.map((arg) => arg.exp);
43336
+ outer.positional = positionalOf(rest);
42595
43337
  }
42596
43338
  return;
42597
43339
  }
42598
43340
  case "shift": {
42599
- const count = Number(literalText(rest[0]?.exp) ?? "1");
42600
- outer.positional = Number.isInteger(count) ? outer.positional?.slice(count) : undefined;
43341
+ const text = rest.length === 0 ? "1" : literalText(rest[0]?.exp);
43342
+ if (text === undefined)
43343
+ outer.positional = undefined;
43344
+ else if (/^[0-9]+$/.test(text) && outer.positional !== undefined) {
43345
+ const count = Number(text);
43346
+ if (count <= outer.positional.length)
43347
+ outer.positional = outer.positional.slice(count);
43348
+ }
42601
43349
  return;
42602
43350
  }
42603
43351
  case "printf": {
42604
- const at = rest.findIndex((arg) => literalText(arg.exp) === "-v");
42605
- const variable = at === -1 ? undefined : literalText(rest[at + 1]?.exp);
42606
- if (variable !== undefined) {
42607
- outer.vars.set(variable, [unknown2(`\`$${variable}\` (printf -v)`)]);
43352
+ const head = rest[0]?.exp ?? [];
43353
+ const leading = head[0];
43354
+ const spaced = literalText(head) === "-v";
43355
+ const joined = !spaced && leading?.kind === "literal" && leading.text.startsWith("-v") && (leading.text.length > 2 || head.length > 1);
43356
+ if (spaced || joined) {
43357
+ const variable = spaced ? literalText(rest[1]?.exp) : literalText([
43358
+ { ...leading, text: leading?.text.slice(2) ?? "" },
43359
+ ...head.slice(1)
43360
+ ]);
43361
+ const printed = rest.slice(spaced ? 2 : 1);
43362
+ const [format, value, ...others] = printed;
43363
+ let assigned;
43364
+ if (literalText(format?.exp) === "%s" && value !== undefined && others.length === 0) {
43365
+ assigned = value.exp;
43366
+ } else {
43367
+ const text = printfText(printed, MAX_VALUE_LENGTH);
43368
+ const cause = printed.flatMap((arg) => arg.exp).find((p) => p.kind === "unknown")?.why;
43369
+ assigned = text === undefined || text.includes(UNKNOWN_MARKER) ? [
43370
+ unknown2(`\`$${variable ?? "?"}\` (printf -v${cause === undefined ? "" : `: ${cause}`})`)
43371
+ ] : text.length > MAX_VALUE_LENGTH ? [overLong(`\`$${variable ?? "?"}\` (printf -v)`)] : [literal2(text)];
43372
+ }
43373
+ assignByName(outer, variable, assigned, "`printf -v`");
42608
43374
  return;
42609
43375
  }
42610
43376
  if (outer.output !== undefined && !stdoutRedirected) {
@@ -42613,7 +43379,11 @@ function dispatch(invocation, outer, ctx) {
42613
43379
  return;
42614
43380
  }
42615
43381
  case "eval": {
42616
- runText(rest.map((arg) => runtimeText(arg.exp)).join(" "), "the `eval` text", outer, ctx, site);
43382
+ const text = rest.map((arg) => runtimeText(arg.exp)).join(" ");
43383
+ runText(text, "the `eval` text", outer, ctx, site);
43384
+ if (text.includes(UNKNOWN_MARKER)) {
43385
+ forgetVariables(outer, "`eval` of text the guard cannot read may have changed");
43386
+ }
42617
43387
  return;
42618
43388
  }
42619
43389
  case "trap": {
@@ -42621,12 +43391,12 @@ function dispatch(invocation, outer, ctx) {
42621
43391
  if (handler === undefined)
42622
43392
  return;
42623
43393
  const text = literalText(handler.exp);
42624
- const readable = conditions.map((arg) => literalText(arg.exp));
42625
- const signals = readable.map((condition) => condition === undefined ? UNREADABLE_CONDITION : trapSignal(condition));
43394
+ const readable2 = conditions.map((arg) => literalText(arg.exp));
43395
+ const signals = readable2.map((condition) => condition === undefined ? UNREADABLE_CONDITION : trapSignal(condition));
42626
43396
  const handled = signals.length === 0 ? ["EXIT"] : signals;
42627
43397
  const file2 = outer.script ?? outer.source;
42628
43398
  if (text === "-") {
42629
- const named = readable.flatMap((condition) => condition === undefined ? [] : [trapSignal(condition)]);
43399
+ const named = readable2.flatMap((condition) => condition === undefined ? [] : [trapSignal(condition)]);
42630
43400
  outer.traps = outer.traps.flatMap((trap) => {
42631
43401
  const signals2 = trap.signals.filter((signal) => !named.includes(signal));
42632
43402
  return signals2.length === 0 ? [] : [{ ...trap, signals: signals2 }];
@@ -42759,7 +43529,8 @@ function dispatch(invocation, outer, ctx) {
42759
43529
  if (target !== undefined && st.cwd !== undefined) {
42760
43530
  const file2 = path4.resolve(st.cwd, target);
42761
43531
  st.pidFiles.delete(file2);
42762
- st.files.set(file2, append ? null : heredoc?.content ?? null);
43532
+ const text = heredoc === undefined ? null : heredocText(heredoc);
43533
+ st.files.set(file2, append || text === null ? null : readable(text));
42763
43534
  }
42764
43535
  }
42765
43536
  return;
@@ -42862,37 +43633,156 @@ function changeDirectory(target, st, ctx) {
42862
43633
  }
42863
43634
  st.cwd = path4.resolve(st.cwd, text);
42864
43635
  }
42865
- function declaration(builtin, list, st, ctx) {
42866
- const array2 = list.some((arg) => /^-[a-zA-Z]*[aA]/.test(literalText(arg.exp) ?? ""));
43636
+ function declaration(builtin, list, st, ctx, site) {
43637
+ const options = list.map((arg) => literalText(arg.exp) ?? "").filter((text) => /^[-+][a-zA-Z]+$/.test(text));
43638
+ const option = (letter) => options.some((text) => text.startsWith("-") && text.includes(letter));
43639
+ if (builtin !== "export" && option("n")) {
43640
+ throw new Refusal(site.snippet, site.line, "the guard does not follow a nameref (`declare -n`), whose writes land in the variable it names");
43641
+ }
43642
+ if (option("f") || option("F"))
43643
+ return;
43644
+ if (builtin === "local" && st.locals === undefined)
43645
+ return;
43646
+ const array2 = option("a") || option("A");
43647
+ const exports = builtin === "export" || option("x");
43648
+ const keeps = builtin === "readonly" || option("r");
43649
+ const rewrites = option("i") || option("l") || option("u");
43650
+ const locals = builtin === "local" || (builtin === "declare" || builtin === "typeset") && !option("g") ? st.locals : undefined;
43651
+ const writer = `\`${builtin}\``;
42867
43652
  for (const arg of list) {
42868
- const first = arg.exp[0];
42869
- const equals = arg.text.indexOf("=");
42870
- const variable = equals === -1 ? literalText(arg.exp) : arg.text.slice(0, equals);
42871
- if (variable === undefined || variable.startsWith("-"))
43653
+ const text = literalText(arg.exp);
43654
+ if (text !== undefined && /^[-+][a-zA-Z]+$/.test(text))
43655
+ continue;
43656
+ const { name, value: assigned } = nameAndValue(arg);
43657
+ if (name === undefined) {
43658
+ forgetVariables(st, `${writer} under a name the guard cannot read may have changed`);
43659
+ continue;
43660
+ }
43661
+ const base = ARRAY_ELEMENT.exec(name)?.[1] ?? name;
43662
+ if (!IDENTIFIER.test(base))
43663
+ continue;
43664
+ if (exports)
43665
+ st.exported.add(base);
43666
+ if (rewrites || attributed(st, base) && (assigned !== undefined || locals !== undefined)) {
43667
+ keepAttributed(st, base);
42872
43668
  continue;
42873
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable))
43669
+ }
43670
+ if (attributed(st, base))
42874
43671
  continue;
42875
- if (builtin === "export")
42876
- st.exported.add(variable);
42877
- if (equals === -1)
43672
+ if (locals !== undefined) {
43673
+ locals.set(base, true);
43674
+ if (assigned === undefined) {
43675
+ st.vars.set(base, UNSET);
43676
+ st.arrays.delete(base);
43677
+ }
43678
+ }
43679
+ if (assigned === undefined) {
43680
+ if (keeps)
43681
+ markKept(st, base, ctx);
42878
43682
  continue;
42879
- let value = first?.kind === "literal" && first.text.startsWith(`${variable}=`) ? [
42880
- ...first.text.length === equals + 1 ? [] : [literal2(first.text.slice(equals + 1))],
42881
- ...arg.exp.slice(1)
42882
- ] : [...arg.exp];
42883
- if (arg.text.charAt(equals + 1) === "~" && arg.text.startsWith(`${variable}=~`)) {
42884
- value = [...unquotedLiteral(arg.text.slice(equals + 1), 0, st, ctx), ...arg.exp.slice(1)];
42885
- }
42886
- st.vars.set(variable, array2 ? [unknown2(`\`$${variable}\`, an array`)] : value);
42887
- if (array2) {
42888
- if (arg.text.slice(equals + 1) === "()") {
42889
- st.arrays.set(variable, new Map);
43683
+ }
43684
+ const value = arg.text.startsWith(`${name}=~`) ? [...unquotedLiteral(arg.text.slice(name.length + 1), 0, st, ctx), ...arg.exp.slice(1)] : assigned;
43685
+ if (!array2 || name !== base) {
43686
+ assignByName(st, name, value, writer);
43687
+ } else {
43688
+ st.vars.set(name, [unknown2(`\`$${name}\`, an array`)]);
43689
+ if (arg.text === `${name}=()`) {
43690
+ st.arrays.set(name, new Map);
42890
43691
  } else {
42891
- const unknownArray = [unknown2(`\`$${variable}\`, an array assignment`)];
42892
- st.arrays.set(variable, new Map([[UNKNOWN_ARRAY_INDEX, unknownArray]]));
43692
+ const unknownArray = [unknown2(`\`$${name}\`, an array assignment`)];
43693
+ st.arrays.set(name, new Map([[UNKNOWN_ARRAY_INDEX, unknownArray]]));
42893
43694
  }
42894
43695
  }
43696
+ if (keeps)
43697
+ markKept(st, base, ctx);
43698
+ }
43699
+ }
43700
+ function markKept(st, name, ctx) {
43701
+ const held = st.vars.get(name) ?? lookup(name, st, ctx) ?? UNSET;
43702
+ const value = held.length === 0 ? [literal2("")] : held;
43703
+ st.vars.set(name, value.map((piece) => ({ ...piece, rewrites: true })));
43704
+ }
43705
+ function readInput(builtin, rest, st) {
43706
+ const valued = builtin === "read" ? "adinNptu" : builtin === "getopts" ? "" : "dnOsuCc";
43707
+ const writer = `\`${builtin}\``;
43708
+ const input = (name) => [
43709
+ unknown2(`\`$${name ?? "?"}\`, read from input`)
43710
+ ];
43711
+ let array2;
43712
+ let arrayOption = false;
43713
+ const names = [];
43714
+ let options = builtin !== "getopts";
43715
+ for (let i = 0;i < rest.length; i += 1) {
43716
+ const arg = rest[i];
43717
+ const text = literalText(arg.exp);
43718
+ if (options && text === "--") {
43719
+ options = false;
43720
+ continue;
43721
+ }
43722
+ if (options && text !== undefined && /^-./.test(text)) {
43723
+ for (let j = 1;j < text.length; j += 1) {
43724
+ const flag = text.charAt(j);
43725
+ if (!valued.includes(flag))
43726
+ continue;
43727
+ const attached = text.slice(j + 1);
43728
+ i += attached === "" ? 1 : 0;
43729
+ const value = attached === "" ? rest[i] : { text: attached, exp: [literal2(attached)] };
43730
+ if (flag === "a" && builtin === "read") {
43731
+ arrayOption = true;
43732
+ array2 = value;
43733
+ }
43734
+ break;
43735
+ }
43736
+ continue;
43737
+ }
43738
+ options = false;
43739
+ names.push(arg);
43740
+ }
43741
+ const fill = (arg, fallback) => {
43742
+ const name = arg === undefined ? fallback : literalText(arg.exp);
43743
+ if (name === undefined || !IDENTIFIER.test(name) || attributed(st, name)) {
43744
+ assignByName(st, name, input(name), writer);
43745
+ return;
43746
+ }
43747
+ st.vars.set(name, input(name));
43748
+ st.arrays.set(name, new Map([[UNKNOWN_ARRAY_INDEX, input(name)]]));
43749
+ };
43750
+ if (builtin === "getopts") {
43751
+ const name = names[1];
43752
+ if (name !== undefined)
43753
+ assignByName(st, literalText(name.exp), input(name.text), writer);
43754
+ for (const variable of ["OPTARG", "OPTIND"])
43755
+ assignScalar(st, variable, input(variable));
43756
+ } else if (builtin !== "read") {
43757
+ fill(names[0], "MAPFILE");
43758
+ } else if (arrayOption) {
43759
+ if (array2 !== undefined)
43760
+ fill(array2, "");
43761
+ } else if (names.length === 0) {
43762
+ assignScalar(st, "REPLY", input("REPLY"));
43763
+ } else {
43764
+ for (const name of names)
43765
+ assignByName(st, literalText(name.exp), input(name.text), writer);
43766
+ }
43767
+ }
43768
+ function orCommand(callee) {
43769
+ return callee.includes(undefined) ? callee : [...callee, undefined];
43770
+ }
43771
+ function nameAndValue(arg) {
43772
+ let name = "";
43773
+ for (const [index, piece] of arg.exp.entries()) {
43774
+ if (piece.kind !== "literal")
43775
+ return { name: undefined, value: undefined };
43776
+ const equals = piece.text.indexOf("=");
43777
+ if (equals === -1) {
43778
+ name += piece.text;
43779
+ continue;
43780
+ }
43781
+ const rest = piece.text.slice(equals + 1);
43782
+ const value = [...rest === "" ? [] : [{ ...piece, text: rest }], ...arg.exp.slice(index + 1)];
43783
+ return { name: name + piece.text.slice(0, equals), value };
42895
43784
  }
43785
+ return { name, value: undefined };
42896
43786
  }
42897
43787
  function checkTargets(program, verb, targets, follow, st, ctx, site) {
42898
43788
  for (const target of targets) {
@@ -43118,11 +44008,18 @@ function childState(st, overlay) {
43118
44008
  if (value !== undefined)
43119
44009
  vars.set(name, value);
43120
44010
  }
44011
+ for (const [name, value] of st.vars)
44012
+ if (isUnset(value))
44013
+ vars.set(name, value);
43121
44014
  for (const [name, value] of overlay)
43122
44015
  vars.set(name, value);
44016
+ const exported = new Set(vars.keys());
44017
+ const any2 = st.vars.get(ANY_NAME);
44018
+ if (any2 !== undefined)
44019
+ vars.set(ANY_NAME, any2);
43123
44020
  return {
43124
44021
  vars,
43125
- exported: new Set(vars.keys()),
44022
+ exported,
43126
44023
  cwd: st.cwd,
43127
44024
  cwdWhy: st.cwdWhy,
43128
44025
  positional: [],
@@ -43138,7 +44035,8 @@ function childState(st, overlay) {
43138
44035
  arrays: new Map,
43139
44036
  functions: new Map,
43140
44037
  traps: [],
43141
- runningFunctions: new Set
44038
+ runningFunctions: new Set,
44039
+ locals: undefined
43142
44040
  };
43143
44041
  }
43144
44042
  function runText(text, label, st, ctx, site) {
@@ -43185,19 +44083,23 @@ function runShell(list, invocation, st, ctx) {
43185
44083
  child.argv0 = operand[1]?.exp;
43186
44084
  if (command) {
43187
44085
  const text = operand[0] === undefined ? "" : runtimeText(operand[0].exp);
43188
- child.positional = operand.slice(2).map((arg) => arg.exp);
44086
+ child.positional = positionalOf(operand.slice(2));
43189
44087
  runText(text, "its `-c` text", child, ctx, site);
43190
44088
  return;
43191
44089
  }
43192
44090
  const file2 = operand[0];
43193
44091
  if (file2 !== undefined) {
43194
- child.positional = operand.slice(1).map((arg) => arg.exp);
44092
+ child.positional = positionalOf(operand.slice(1));
43195
44093
  runFile(file2, operand.slice(1), child, ctx, site, "shell");
43196
44094
  return;
43197
44095
  }
43198
44096
  const heredoc = invocation.redirects.find((r) => r.operator === "<<" || r.operator === "<<-");
43199
- if (heredoc?.content !== undefined) {
43200
- runText(heredoc.content, "the script it reads from the heredoc", child, ctx, site);
44097
+ if (heredoc !== undefined) {
44098
+ const text = heredocText(heredoc);
44099
+ if (text === null) {
44100
+ throw new Refusal(site.snippet, site.line, "the guard cannot read the script this shell reads from its here-document, which bash expands first; quote its delimiter (`<<'EOF'`) or write the script with the write tool");
44101
+ }
44102
+ runText(text, "the script it reads from the heredoc", child, ctx, site);
43201
44103
  return;
43202
44104
  }
43203
44105
  const herestring = invocation.redirects.find((r) => r.operator === "<<<");
@@ -43253,8 +44155,12 @@ function runInterpreter(base, language, list, invocation, st, ctx) {
43253
44155
  return;
43254
44156
  if (word === "-") {
43255
44157
  const heredoc = invocation.redirects.find((r) => r.operator === "<<" || r.operator === "<<-");
43256
- if (heredoc?.content !== undefined) {
43257
- runCode(language, { text: "<heredoc>", exp: [literal2(heredoc.content)] }, invocation, st, ctx, site);
44158
+ if (heredoc !== undefined) {
44159
+ const text = heredocText(heredoc);
44160
+ if (text === null) {
44161
+ throw new Refusal(site.snippet, site.line, "the guard cannot read the program this interpreter reads from its here-document, which bash expands first; quote its delimiter (`<<'PY'`) or write the program with the write tool");
44162
+ }
44163
+ runCode(language, { text: "<heredoc>", exp: [literal2(text)] }, invocation, st, ctx, site);
43258
44164
  }
43259
44165
  return;
43260
44166
  }
@@ -43290,7 +44196,7 @@ function runFile(file2, positional, st, ctx, site, kind, source = false) {
43290
44196
  if (st.files.has(abs)) {
43291
44197
  const written = st.files.get(abs);
43292
44198
  if (written === null || written === undefined) {
43293
- throw new Refusal(site.snippet, site.line, `this command writes ${abs} in a way the guard cannot read before running it; write the script with the write tool first`);
44199
+ throw new Refusal(site.snippet, site.line, `this command writes ${abs} in a way the guard cannot read before running it (output it cannot render, or more than the ${MAX_SCRIPT_BYTES} bytes it reads of a script); write the script with the write tool first`);
43294
44200
  }
43295
44201
  content = written;
43296
44202
  } else {
@@ -43326,22 +44232,24 @@ function runFile(file2, positional, st, ctx, site, kind, source = false) {
43326
44232
  throw new Refusal(site.snippet, site.line, "the guard stops following scripts this deep; run the inner script directly");
43327
44233
  }
43328
44234
  const child = { ...clone2(st), source: content, script: abs, depth: st.depth + 1 };
43329
- const operands2 = positional.map((arg) => arg.exp);
43330
- child.positional = source && operands2.length === 0 ? st.positional : operands2;
44235
+ const operands2 = positionalOf(positional);
44236
+ const noOperands = operands2 !== undefined && operands2.length === 0;
44237
+ child.positional = source && noOperands ? st.positional : operands2;
43331
44238
  walkScript(parse5(content), child, ctx, !source);
43332
44239
  if (source) {
43333
- st.positional = operands2.length === 0 ? child.positional : child.positional === operands2 ? st.positional : undefined;
44240
+ st.positional = noOperands ? child.positional : child.positional === operands2 ? st.positional : undefined;
43334
44241
  st.vars = child.vars;
43335
44242
  st.exported = child.exported;
43336
44243
  st.arrays.clear();
43337
- for (const [name, elements] of child.arrays)
43338
- st.arrays.set(name, new Map(elements));
44244
+ for (const [name, elements2] of child.arrays)
44245
+ st.arrays.set(name, new Map(elements2));
43339
44246
  st.cwd = child.cwd;
43340
44247
  st.cwdWhy = child.cwdWhy;
43341
44248
  st.functions = child.functions;
43342
44249
  st.traps = child.traps;
43343
44250
  st.output = child.output;
43344
44251
  st.backgroundStarted ||= child.backgroundStarted;
44252
+ st.locals = child.locals;
43345
44253
  }
43346
44254
  } else
43347
44255
  checkCode(language, content, st, ctx);
@@ -43383,7 +44291,9 @@ function checkCode(language, code, st, ctx, ipython = false) {
43383
44291
  const env = { ...ctx.env };
43384
44292
  for (const [name, value] of st.vars) {
43385
44293
  const text = literalText(value);
43386
- if (text !== undefined)
44294
+ if (isUnset(value))
44295
+ delete env[name];
44296
+ else if (text !== undefined)
43387
44297
  env[name] = text;
43388
44298
  }
43389
44299
  scanCode(language, code, { env, cwd: cwd ?? "/", tmpdir: env.TMPDIR ?? "/tmp", ipython }, {
@@ -43465,12 +44375,14 @@ function createPaneGuard(options) {
43465
44375
  if (workspace === "/" || home !== undefined && (home === workspace || home.startsWith(`${workspace}/`))) {
43466
44376
  workspace = undefined;
43467
44377
  }
43468
- const protectedScratch = [...PROTECTED_SCRATCH];
43469
44378
  const tmuxDir = env.TMUX_TMPDIR === undefined ? undefined : realish(env.TMUX_TMPDIR, true);
43470
- if (tmuxDir?.startsWith(`${scratch}/`)) {
43471
- protectedScratch.push(tmuxDir.slice(scratch.length + 1).split("/")[0] ?? "");
44379
+ const protectedDirectories = [];
44380
+ for (const owned of [tmuxDir, home]) {
44381
+ const component = owned?.startsWith(`${scratch}/`) ? owned.slice(scratch.length + 1).split("/")[0] : undefined;
44382
+ if (component !== undefined && component !== "")
44383
+ protectedDirectories.push(component);
43472
44384
  }
43473
- return { workspace, scratch, protectedScratch };
44385
+ return { workspace, scratch, protectedScratch: PROTECTED_SCRATCH, protectedDirectories };
43474
44386
  };
43475
44387
  const context = (env) => ({
43476
44388
  roots: roots(env),
@@ -43497,12 +44409,13 @@ function createPaneGuard(options) {
43497
44409
  pidFiles: new Map,
43498
44410
  arrays: new Map,
43499
44411
  functions: new Map,
43500
- traps: []
44412
+ traps: [],
44413
+ locals: undefined
43501
44414
  });
43502
- const run = (attempt, fallback) => {
44415
+ const run = (ctx, attempt, fallback) => {
43503
44416
  try {
43504
44417
  attempt();
43505
- return;
44418
+ return ctx.steps.count > MAX_WALK_STEPS ? `refused \`${fallback}\`: ${WALK_LIMIT}` : undefined;
43506
44419
  } catch (error48) {
43507
44420
  if (!(error48 instanceof Refusal))
43508
44421
  throw error48;
@@ -43512,13 +44425,13 @@ function createPaneGuard(options) {
43512
44425
  return {
43513
44426
  bash: (command, cwd, env) => {
43514
44427
  const ctx = context(env);
43515
- return run(() => walkScript(parse5(command), initial(cwd, command), ctx), command.trim());
44428
+ return run(ctx, () => walkScript(parse5(command), initial(cwd, command), ctx), command.trim());
43516
44429
  },
43517
44430
  argv: (argv, cwd, env) => {
43518
44431
  const ctx = context(env);
43519
44432
  const list = argv.map((item) => ({ text: item, exp: [literal2(item)] }));
43520
44433
  const snippet = argv.join(" ");
43521
- return run(() => dispatch({
44434
+ return run(ctx, () => dispatch({
43522
44435
  args: list,
43523
44436
  site: { snippet, line: 1 },
43524
44437
  overlay: new Map,
@@ -43528,7 +44441,7 @@ function createPaneGuard(options) {
43528
44441
  },
43529
44442
  code: (language, code, cwd, env) => {
43530
44443
  const ctx = context(env);
43531
- return run(() => checkCode(language, code, initial(cwd, code), ctx, true), `${language} code`);
44444
+ return run(ctx, () => checkCode(language, code, initial(cwd, code), ctx, true), `${language} code`);
43532
44445
  }
43533
44446
  };
43534
44447
  }