@poe-platform/safe-bash 0.1.749 → 0.1.751

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.
@@ -9439,7 +9439,8 @@ export class Runtime {
9439
9439
  }
9440
9440
  return !rem.associative && rem.references === 1;
9441
9441
  }
9442
- return this.isPureSyncValueWord(e.left, rawState) && (e.operator === "==" || e.operator === "=" || e.operator === "!=" ? this.canSyncPatternWordParts(e.right, rawState) : this.isPureSyncValueWord(e.right, rawState));
9442
+ // Unquoted patterns may need the asynchronous matcher after values change.
9443
+ return this.isPureSyncValueWord(e.left, rawState) && this.isPureSyncValueWord(e.right, rawState) && (!(e.operator === "==" || e.operator === "=" || e.operator === "!=") || (e.right.parts.length > 0 && e.right.parts.every(p => p.quoted)));
9443
9444
  }
9444
9445
  if (e.kind === "not")
9445
9446
  return checkCondNoFile(e.operand);
@@ -9795,10 +9796,19 @@ export class Runtime {
9795
9796
  // Matching publishes BASH_REMATCH. A sibling may still require fallback,
9796
9797
  // so only a standalone match can be attempted without replaying writes.
9797
9798
  if (expr.operator === "=~")
9798
- return depth === 0 && this.extractSimpleErePattern(expr.right, rawState) !== undefined;
9799
+ return !requireTotal && depth === 0 && this.extractSimpleErePattern(expr.right, rawState) !== undefined;
9799
9800
  if (expr.operator === "==" || expr.operator === "=" || expr.operator === "!=") {
9800
9801
  if (expr.right.parts.length > 0 && expr.right.parts.every(p => p.quoted))
9801
9802
  return true;
9803
+ if (requireTotal) {
9804
+ // Variable patterns can change during the loop. Only admit a static
9805
+ // pattern whose synchronous matcher is guaranteed to return a result.
9806
+ if (!expr.right.parts.every((p, idx) => p.kind === "text" && !p.byteValue && (p.quoted || idx > 0 || !p.value.startsWith("~"))))
9807
+ return false;
9808
+ const pattern = expr.right.parts.map(p => p.kind === "text" ? (p.quoted ? p.value.replace(/[\\*?[\]()]/g, "\\$&") : p.value) : "").join("");
9809
+ const work = { remaining: 4096, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
9810
+ return tryMatchesPatternSync(pattern, "", work, false, true) !== undefined;
9811
+ }
9802
9812
  if (expr.right.parts.length > 0 && expr.right.parts.every((p, idx) => (p.kind === "text" && !p.byteValue && (p.quoted || idx > 0 || !p.value.startsWith("~"))) || (p.kind === "variable" && !p.indirect && !p.prefixNames && !p.length && !p.substring && !p.transform && p.operator === undefined && getArraySelector(p) === undefined)))
9803
9813
  return true;
9804
9814
  return false;
@@ -9987,7 +9997,10 @@ export class Runtime {
9987
9997
  return undefined;
9988
9998
  const syncTouched = this._syncArithRawWriteOnly ? this._syncArithTouched : undefined;
9989
9999
  const lTrim = left.trim() || "0";
9990
- let lNum = resolveSimpleArithOperand(lTrim, rawState, monitor, store, syncTouched);
10000
+ // Preserve all signed 64-bit literal digits instead of rounding through Number.
10001
+ let lNum = lTrim.length > 13 && /^-?[0-9]{1,19}$/.test(lTrim)
10002
+ ? BigInt.asIntN(64, BigInt(lTrim))
10003
+ : resolveSimpleArithOperand(lTrim, rawState, monitor, store, syncTouched);
9991
10004
  if (lNum === undefined) {
9992
10005
  const lExpr = tryEvalSimpleExpandedArith(lTrim, rawState, monitor, store, syncTouched);
9993
10006
  if (lExpr !== undefined)
@@ -9996,7 +10009,9 @@ export class Runtime {
9996
10009
  if (lNum === undefined)
9997
10010
  return undefined;
9998
10011
  const rTrim = right.trim() || "0";
9999
- let rNum = resolveSimpleArithOperand(rTrim, rawState, monitor, store, syncTouched);
10012
+ let rNum = rTrim.length > 13 && /^-?[0-9]{1,19}$/.test(rTrim)
10013
+ ? BigInt.asIntN(64, BigInt(rTrim))
10014
+ : resolveSimpleArithOperand(rTrim, rawState, monitor, store, syncTouched);
10000
10015
  if (rNum === undefined) {
10001
10016
  const rExpr = tryEvalSimpleExpandedArith(rTrim, rawState, monitor, store, syncTouched);
10002
10017
  if (rExpr !== undefined)
@@ -10004,6 +10019,10 @@ export class Runtime {
10004
10019
  }
10005
10020
  if (rNum === undefined)
10006
10021
  return undefined;
10022
+ if (typeof lNum !== typeof rNum) {
10023
+ lNum = BigInt(lNum);
10024
+ rNum = BigInt(rNum);
10025
+ }
10007
10026
  let ok;
10008
10027
  switch (expr.operator) {
10009
10028
  case "-eq":
@@ -13025,7 +13044,6 @@ export class Runtime {
13025
13044
  rawState.substitutionStatus = 0;
13026
13045
  if (rawState.variables._ !== undefined)
13027
13046
  delete rawState.variables._;
13028
- rawState.lastArgument = fnArgs.length > 0 ? fnArgs[fnArgs.length - 1] : w0Plain;
13029
13047
  const positional = rawState.positional;
13030
13048
  let fnArgsBytes = 0;
13031
13049
  for (let i = 0; i < fnArgs.length; i++)
@@ -13075,6 +13093,7 @@ export class Runtime {
13075
13093
  positionalSetVersion,
13076
13094
  savedPositionals: resumeSavedPos,
13077
13095
  callerLoopDepth,
13096
+ lastArgument: fnArgs.at(-1) ?? w0Plain,
13078
13097
  listIndex: bodyRes.listIndex,
13079
13098
  pipelineIndex: bodyRes.pipelineIndex,
13080
13099
  });
@@ -13139,6 +13158,7 @@ export class Runtime {
13139
13158
  this.setSyncPipeStatusCell(activeExisting, statusStr);
13140
13159
  activeStore.changed(monitor.chargeInternal(syncPipeStatusCharge, syncPipeStatusTickets), "PIPESTATUS");
13141
13160
  }
13161
+ rawState.lastArgument = fnArgs.at(-1) ?? w0Plain;
13142
13162
  rawState.status = exitStatus;
13143
13163
  if (exitStatus !== 0 && !ignored && rawState.errexit)
13144
13164
  throw new Flow("exit", exitStatus);
@@ -13237,7 +13257,6 @@ export class Runtime {
13237
13257
  rawState.substitutionStatus = 0;
13238
13258
  if (rawState.variables._ !== undefined)
13239
13259
  delete rawState.variables._;
13240
- rawState.lastArgument = lastCmdArg;
13241
13260
  rawState.depth++;
13242
13261
  let evalStatus;
13243
13262
  try {
@@ -13247,6 +13266,7 @@ export class Runtime {
13247
13266
  rawState.depth--;
13248
13267
  }
13249
13268
  if (typeof evalStatus === "number") {
13269
+ rawState.lastArgument = lastCmdArg;
13250
13270
  if (cachedUnit.script.lists.length === 0) {
13251
13271
  const activeStore = monitor.store;
13252
13272
  const activeExisting = activeStore?.get("PIPESTATUS");
@@ -13586,6 +13606,11 @@ export class Runtime {
13586
13606
  continue;
13587
13607
  }
13588
13608
  if (part.kind === "variable") {
13609
+ // Unsubscripted array references expand element zero through the normal
13610
+ // evaluator. fastValueWord cannot guarantee that expansion throughout
13611
+ // a loop, so reject it before executing any loop effects.
13612
+ if (stateMonitor(rawState)?.store?.get(part.name))
13613
+ return false;
13589
13614
  // These expansions can require multiple fields or asynchronous quoting
13590
13615
  // as loop state changes. Decide before the loop produces any effects.
13591
13616
  if (this._syncLoopFnCheckDepth > 0 && (part.substring || getArraySelector(part)?.kind === "element"))
@@ -13736,25 +13761,12 @@ export class Runtime {
13736
13761
  if (list.terminator || list.pipelines.length !== 1)
13737
13762
  return false;
13738
13763
  const p = list.pipelines[0];
13739
- if (p.negate || p.commands.length < 1 || p.commands.length > 5)
13764
+ // Pipeline buffers can overflow after variable or stage expansion.
13765
+ // A synchronous loop cannot resume after earlier body effects, so keep
13766
+ // bounded pipelines on the normal substitution execution path.
13767
+ if (p.negate || p.commands.length !== 1)
13740
13768
  return false;
13741
13769
  let cmd = p.commands[0];
13742
- const hasSingleStdinRedir = cmd.kind === "simple" &&
13743
- cmd.redirects.length === 1 &&
13744
- cmd.redirects[0].operator === "<" &&
13745
- (cmd.redirects[0].descriptor === undefined || cmd.redirects[0].descriptor === 0) &&
13746
- !cmd.redirects[0].move &&
13747
- !cmd.redirects[0].document &&
13748
- cmd.redirects[0].target.plain !== undefined &&
13749
- !cmd.redirects[0].target.plain.startsWith("-") &&
13750
- cmd.redirects[0].target.plain !== "/dev/stdin";
13751
- const hasSingleHereStringRedir = cmd.kind === "simple" &&
13752
- cmd.redirects.length === 1 &&
13753
- cmd.redirects[0].operator === "<<<" &&
13754
- (cmd.redirects[0].descriptor === undefined || cmd.redirects[0].descriptor === 0) &&
13755
- !cmd.redirects[0].move &&
13756
- !cmd.redirects[0].document &&
13757
- this.isPureSyncValueWord(cmd.redirects[0].target, rawState);
13758
13770
  // Loop substitutions require a real subshell: header admission can
13759
13771
  // decline at runtime, and induction writes also change export attributes.
13760
13772
  if (cmd.kind === "for" || cmd.kind === "arithmetic-for" || cmd.kind === "while")
@@ -13762,7 +13774,7 @@ export class Runtime {
13762
13774
  if (p.commands.length === 1 && (cmd.kind === "if" || cmd.kind === "case") && this.isPureSyncSubIfOrCase(cmd, rawState)) {
13763
13775
  continue;
13764
13776
  }
13765
- if (cmd.kind !== "simple" || (cmd.redirects.length > 0 && !hasSingleStdinRedir && !hasSingleHereStringRedir) || cmd.words.length === 0)
13777
+ if (cmd.kind !== "simple" || cmd.redirects.length > 0 || cmd.words.length === 0)
13766
13778
  return false;
13767
13779
  let w0Plain = cmd.words[0].plain;
13768
13780
  // Range-aware tr and Buffer-free 76-col base64 are handled below.
@@ -13777,10 +13789,10 @@ export class Runtime {
13777
13789
  fnBody.body.lists[0].pipelines[0].commands.length === 1 &&
13778
13790
  fnBody.body.lists[0].pipelines[0].commands[0].kind === "simple" &&
13779
13791
  fnBody.body.lists[0].pipelines[0].commands[0].redirects.length === 0 &&
13780
- cmd.words.slice(1).every(w => this.isPureSyncValueWord(w, rawState))) {
13792
+ cmd.words.slice(1).every(w => this.isPureSyncValueWord(w, rawState) && w.parts.length > 0 && w.parts.every(part => part.quoted || (part.kind === "text" && part.value.length > 0 && !part.value.includes(" ") && !part.value.includes("\t") && !part.value.includes("\n") && !part.value.includes("{") && !hasGlobOrEscape(part.value, true))))) {
13781
13793
  const innerCmd = fnBody.body.lists[0].pipelines[0].commands[0];
13782
13794
  const innerW0 = innerCmd.words[0]?.plain;
13783
- if (innerW0 && !rawState.functions.has(innerW0) && !rawState.extensions?.builtins.has(innerW0)) {
13795
+ if (innerW0 && (innerW0 === "echo" || innerW0 === "printf" || innerW0 === "dirname" || innerW0 === "basename" || innerW0 === "pwd" || innerW0 === "command" || innerW0 === "type" || innerW0 === "seq") && !rawState.functions.has(innerW0) && !rawState.extensions?.builtins.has(innerW0)) {
13784
13796
  cmd = innerCmd;
13785
13797
  w0Plain = innerW0;
13786
13798
  }
@@ -13796,143 +13808,23 @@ export class Runtime {
13796
13808
  return false;
13797
13809
  }
13798
13810
  if ((w0Plain === "command" && cmd.words[1]?.plain === "-v") || (w0Plain === "type" && cmd.words[1]?.plain === "-t")) {
13799
- if (p.commands.length === 1 &&
13800
- cmd.words.length === 3 &&
13801
- !rawState.functions.has(w0Plain) &&
13802
- !rawState.extensions?.builtins.has(w0Plain) &&
13803
- this.isPureSyncValueWord(cmd.words[2], rawState) &&
13804
- ((cmd.words[2].plain !== undefined && this.tryResolveSyncDiscovery(w0Plain === "command" ? "name" : "kind", cmd.words[2].plain, rawState) !== undefined) ||
13805
- (this._isMemoryBackingFs && this.backingFs?.capabilities?.permissions !== true))) {
13806
- continue;
13807
- }
13811
+ // PATH entries can change after admission, making discovery decline.
13808
13812
  return false;
13809
13813
  }
13810
- const isFileCommand = w0Plain === "cat" ||
13811
- w0Plain === "head" ||
13812
- w0Plain === "tail" ||
13813
- w0Plain === "jq" ||
13814
- w0Plain === "awk" ||
13815
- w0Plain === "grep" ||
13816
- w0Plain === "sed" ||
13817
- w0Plain === "cut" ||
13818
- w0Plain === "wc" ||
13819
- w0Plain === "sort" ||
13820
- w0Plain === "uniq" ||
13821
- w0Plain === "tr" ||
13822
- (hasSingleHereStringRedir && (w0Plain === "base64" || w0Plain === "rev" || w0Plain === "tac" || w0Plain === "nl" || w0Plain === "paste" || w0Plain === "numfmt"));
13823
- const def = w0Plain ? (this.commands.get(w0Plain) ?? ((w0Plain === "rev" || w0Plain === "tac" || w0Plain === "nl" || w0Plain === "paste" || w0Plain === "jq") ? this.getExternalCommand(w0Plain) : undefined)) : undefined;
13824
- if (!w0Plain || (w0Plain !== "printf" && w0Plain !== "echo" && w0Plain !== "dirname" && w0Plain !== "basename" && w0Plain !== "seq" && !isFileCommand) || !def || rawState.functions.has(w0Plain) || rawState.extensions?.builtins.has(w0Plain))
13814
+ // File and here-string evaluators can decline on mutable input. Keep
13815
+ // these on the normal path, which can handle arbitrary input sizes.
13816
+ const def = w0Plain ? this.commands.get(w0Plain) : undefined;
13817
+ if (!w0Plain || (w0Plain !== "printf" && w0Plain !== "echo" && w0Plain !== "dirname" && w0Plain !== "basename" && w0Plain !== "seq") || !def || rawState.functions.has(w0Plain) || rawState.extensions?.builtins.has(w0Plain))
13825
13818
  return false;
13826
- if ((hasSingleStdinRedir || hasSingleHereStringRedir) && !isFileCommand)
13827
- return false;
13828
- if (w0Plain === "printf" ? def.execute !== printfCommand.execute : w0Plain === "echo" ? !defaultEchoExecutors.has(def.execute) : (w0Plain === "seq" || w0Plain === "rev" || w0Plain === "tac" || w0Plain === "nl" || w0Plain === "paste" || w0Plain === "jq") ? (!builtInDirectContextExecutors.has(def.execute) && customRegisteredCommands.has(def.execute)) : !builtInDirectContextExecutors.has(def.execute))
13819
+ if (w0Plain === "printf" ? def.execute !== printfCommand.execute : w0Plain === "echo" ? !defaultEchoExecutors.has(def.execute) : w0Plain === "seq" ? (!builtInDirectContextExecutors.has(def.execute) && customRegisteredCommands.has(def.execute)) : !builtInDirectContextExecutors.has(def.execute))
13829
13820
  return false;
13830
13821
  if (!cmd.words.slice(1).every(w => this.isPureSyncValueWord(w, rawState)))
13831
13822
  return false;
13832
- if (isFileCommand) {
13833
- if (w0Plain === "tr" && !hasSingleStdinRedir && !hasSingleHereStringRedir)
13834
- return false;
13835
- if (w0Plain === "grep" && rawState.errexit)
13836
- return false;
13837
- if (!hasSingleHereStringRedir) {
13838
- const fPlain = hasSingleStdinRedir ? cmd.redirects[0].target.plain : cmd.words[cmd.words.length - 1]?.plain;
13839
- if (!fPlain || fPlain.startsWith("-") || fPlain === "/dev/stdin")
13840
- return false;
13841
- const vCheck = this.tryReadMemoryFileViewSync(resolvePath(rawState.cwd, fPlain), false);
13842
- if (!vCheck || vCheck.byteLength > 16384 || vCheck.includes(0))
13843
- return false;
13844
- }
13845
- const opWords = (hasSingleStdinRedir || hasSingleHereStringRedir) ? cmd.words.slice(1) : cmd.words.slice(1, -1);
13846
- const opCount = opWords.length;
13847
- const op0 = opWords[0]?.plain;
13848
- const op1 = opWords[1]?.plain;
13849
- if (w0Plain === "cat" || w0Plain === "rev" || w0Plain === "tac") {
13850
- if (opCount !== 0)
13851
- return false;
13852
- }
13853
- else {
13854
- if (p.commands.length !== 1 && !hasSingleHereStringRedir)
13855
- return false;
13856
- if (w0Plain === "head" || w0Plain === "tail") {
13857
- const validHT = opCount === 0 ||
13858
- (opCount === 1 && op0 !== undefined && (/^-(?:n|c|--lines=|--bytes=)?[0-9]{1,5}$/.test(op0) || /^--(?:lines|bytes)=[0-9]{1,5}$/.test(op0) || (w0Plain === "tail" && /^(?:-n|--lines=)\+[0-9]{1,5}$/.test(op0)) || (w0Plain === "head" && /^(?:-n|--lines=)-[0-9]{1,5}$/.test(op0)))) ||
13859
- (opCount === 2 && ((op0 === "-n" && op1 !== undefined && (/^[0-9]{1,5}$/.test(op1) || (w0Plain === "tail" && /^\+[0-9]{1,5}$/.test(op1)) || (w0Plain === "head" && /^-[0-9]{1,5}$/.test(op1)))) || (op0 === "-c" && op1 !== undefined && /^[0-9]{1,5}$/.test(op1))));
13860
- if (!validHT)
13861
- return false;
13862
- }
13863
- const plainOps = [];
13864
- for (let oi = 0; oi < opCount; oi++) {
13865
- const ow = opWords[oi];
13866
- const pw = ow.plain ?? (ow.parts.length > 0 && ow.parts.every(pt => pt.kind === "text") ? ow.parts.map(pt => pt.value).join("") : undefined);
13867
- if (pw === undefined)
13868
- break;
13869
- plainOps.push(pw);
13870
- }
13871
- if (w0Plain === "cut") {
13872
- // Probe non-ASCII input: loop data can change after admission.
13873
- if (plainOps.length !== opCount || this.evalSyncCut(["é"], plainOps, byteLocale(rawState.variables)) === undefined)
13874
- return false;
13875
- }
13876
- else if (w0Plain === "sed") {
13877
- if (plainOps.length !== opCount || this.evalSyncSed([], plainOps) === undefined)
13878
- return false;
13879
- }
13880
- else if (w0Plain === "grep") {
13881
- if (plainOps.length !== opCount || this.evalSyncGrep([], plainOps, Boolean(rawState.errexit)) === undefined)
13882
- return false;
13883
- }
13884
- else if (w0Plain === "awk") {
13885
- const input = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState);
13886
- if (plainOps.length !== opCount || input === undefined)
13887
- return false;
13888
- const lines = input.length === 0 ? [] : input.split("\n");
13889
- if (input.endsWith("\n"))
13890
- lines.pop();
13891
- if (this.evalSyncAwk(lines, plainOps) === undefined)
13892
- return false;
13893
- }
13894
- else if (w0Plain === "jq") {
13895
- if (plainOps.length !== opCount)
13896
- return false;
13897
- const jqIn = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState)?.trim();
13898
- if (jqIn === undefined || this.evalSyncJq(jqIn, plainOps) === undefined)
13899
- return false;
13900
- }
13901
- else if (w0Plain === "wc") {
13902
- if (opCount !== 1 || this.normalizeSyncWcFlag(op0) === undefined)
13903
- return false;
13904
- }
13905
- else if (w0Plain === "sort") {
13906
- if (plainOps.length !== opCount || this.evalSyncSort([], plainOps, byteLocale(rawState.variables)) === undefined)
13907
- return false;
13908
- }
13909
- else if (w0Plain === "uniq") {
13910
- if (plainOps.length !== opCount || this.evalSyncUniq([], plainOps) === undefined)
13911
- return false;
13912
- }
13913
- else if (w0Plain === "base64") {
13914
- if (opCount > 1 || (opCount === 1 && op0 !== "-d" && op0 !== "--decode"))
13915
- return false;
13916
- }
13917
- else if (w0Plain === "tr") {
13918
- if (plainOps.length !== opCount || this.evalSyncTr("", plainOps) === undefined)
13919
- return false;
13920
- }
13921
- else if (w0Plain === "nl") {
13922
- if (plainOps.length !== opCount || this.evalSyncNl([], plainOps) === undefined)
13923
- return false;
13924
- }
13925
- else if (w0Plain === "paste") {
13926
- if (plainOps.length !== opCount || this.evalSyncPaste([], plainOps) === undefined)
13927
- return false;
13928
- }
13929
- else if (w0Plain === "numfmt") {
13930
- const nmCheck = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState)?.trim();
13931
- if (nmCheck === undefined || plainOps.length !== opCount || this.evalSyncNumfmt(nmCheck.split("\n"), plainOps) === undefined)
13932
- return false;
13933
- }
13934
- }
13935
- }
13823
+ // These substitutions consume one value per word. Unquoted expansions
13824
+ // can split, glob, or disappear, including after loop admission.
13825
+ if ((w0Plain === "echo" || w0Plain === "basename" || w0Plain === "dirname") &&
13826
+ !cmd.words.slice(1).every(w => w.parts.length > 0 && w.parts.every(part => part.quoted || (part.kind === "text" && part.value.length > 0 && !part.value.includes(" ") && !part.value.includes("\t") && !part.value.includes("\n") && !part.value.includes("{") && !hasGlobOrEscape(part.value, true)))))
13827
+ return false;
13936
13828
  if (w0Plain === "printf") {
13937
13829
  if (!this.isSyncPrintfCallOk(cmd, 1, rawState, this._activePrintfInductionName))
13938
13830
  return false;
@@ -13972,138 +13864,18 @@ export class Runtime {
13972
13864
  return false;
13973
13865
  }
13974
13866
  else if (w0Plain === "seq") {
13975
- const isSeqIntWord = (w) => {
13976
- if (w.plain !== undefined)
13977
- return /^-?[0-9]{1,7}$/.test(w.plain);
13978
- if (w.parts.length === 1) {
13979
- const p0 = w.parts[0];
13980
- if (p0.kind === "arithmetic")
13981
- return true;
13982
- if (p0.kind === "variable" && !p0.indirect && !p0.prefixNames && !p0.substring && !p0.transform && p0.operator === undefined && getArraySelector(p0) === undefined) {
13983
- const cur = rawState.variables[p0.name];
13984
- return cur !== undefined && /^-?[0-9]{1,7}$/.test(cur);
13985
- }
13986
- }
13867
+ // Literal operands cannot change after admission. Arithmetic and
13868
+ // variables use the normal path, where seq can report invalid input.
13869
+ if (cmd.words.length < 2 || cmd.words.length > 4)
13987
13870
  return false;
13988
- };
13989
- if (cmd.words.length < 2 || cmd.words.length > 4 || !cmd.words.slice(1).every(isSeqIntWord))
13871
+ const operands = cmd.words.slice(1).map(w => w.plain);
13872
+ if (!operands.every(value => value !== undefined && Number.isInteger(Number(value)) && String(Number(value)) === value && Math.abs(Number(value)) <= 9999999))
13990
13873
  return false;
13991
- }
13992
- if (p.commands.length >= 2) {
13993
- if ((w0Plain !== "printf" && w0Plain !== "echo" && w0Plain !== "seq" && w0Plain !== "cat" && !hasSingleHereStringRedir) || syncPurePipelineSlotInUse || !this.budget.canSyncPurePipe || rawState.errexit || rawState.nounset)
13874
+ const first = operands.length === 1 ? 1 : Number(operands[0]);
13875
+ const increment = operands.length === 3 ? Number(operands[1]) : 1;
13876
+ const last = Number(operands[operands.length - 1]);
13877
+ if (increment === 0 || Math.abs((last - first) / increment) > 1024)
13994
13878
  return false;
13995
- if (w0Plain === "printf") {
13996
- if (!this.isSyncPrintfCallOk(cmd, 1, rawState, this._activePrintfInductionName))
13997
- return false;
13998
- }
13999
- for (let sIdx = 1; sIdx < p.commands.length; sIdx++) {
14000
- const sCmd = p.commands[sIdx];
14001
- if (sCmd.kind !== "simple" || sCmd.redirects.length !== 0 || sCmd.words.length === 0)
14002
- return false;
14003
- const sName = sCmd.words[0].plain;
14004
- // Range-aware tr and Buffer-free 76-col base64 are handled in pipeline stages.
14005
- if (!sName || (sName !== "cut" && sName !== "tr" && sName !== "sort" && sName !== "head" && sName !== "tail" && sName !== "wc" && sName !== "sed" && sName !== "uniq" && sName !== "rev" && sName !== "awk" && sName !== "grep" && sName !== "jq" && sName !== "base64" && sName !== "tac" && sName !== "nl" && sName !== "paste" && sName !== "numfmt"))
14006
- return false;
14007
- const sDef = this.commands.get(sName) ?? ((sName === "rev" || sName === "tac" || sName === "nl" || sName === "paste" || sName === "jq") ? this.getExternalCommand(sName) : undefined);
14008
- if (!sDef || customRegisteredCommands.has(sDef.execute) || (sName !== "rev" && sName !== "tac" && sName !== "nl" && sName !== "paste" && sName !== "jq" && !builtInDirectContextExecutors.has(sDef.execute)))
14009
- return false;
14010
- if (rawState.functions.has(sName) || rawState.extensions?.builtins.has(sName))
14011
- return false;
14012
- const sArgWords = sCmd.words.slice(1);
14013
- const sPlainArgs = [];
14014
- let allPlainStageArgs = true;
14015
- for (let k = 0; k < sArgWords.length; k++) {
14016
- const sw = sArgWords[k];
14017
- const sp = sw.plain ?? (sw.parts.length > 0 && sw.parts.every(pt => pt.kind === "text") ? sw.parts.map(pt => pt.value).join("") : undefined);
14018
- if (sp === undefined) {
14019
- allPlainStageArgs = false;
14020
- break;
14021
- }
14022
- sPlainArgs.push(sp);
14023
- }
14024
- if (!allPlainStageArgs)
14025
- return false;
14026
- if (sName === "sort") {
14027
- if (this.evalSyncSort([], sPlainArgs, byteLocale(rawState.variables)) === undefined)
14028
- return false;
14029
- }
14030
- else if (sName === "uniq") {
14031
- if (this.evalSyncUniq([], sPlainArgs) === undefined)
14032
- return false;
14033
- }
14034
- else if (sName === "wc") {
14035
- if (sPlainArgs.length !== 1 || this.normalizeSyncWcFlag(sPlainArgs[0]) === undefined)
14036
- return false;
14037
- }
14038
- else if (sName === "head" || sName === "tail") {
14039
- const okHT = sPlainArgs.length === 0 || (sPlainArgs.length === 1 && (/^-(?:n|c)?[0-9]{1,5}$/.test(sPlainArgs[0]) || /^--(?:lines|bytes)=[0-9]{1,5}$/.test(sPlainArgs[0]) || (sName === "tail" && /^(?:-n|--lines=)\+[0-9]{1,5}$/.test(sPlainArgs[0])) || (sName === "head" && /^(?:-n|--lines=)-[0-9]{1,5}$/.test(sPlainArgs[0])))) || (sPlainArgs.length === 2 && ((sPlainArgs[0] === "-n" && (/^[0-9]{1,5}$/.test(sPlainArgs[1]) || (sName === "tail" && /^\+[0-9]{1,5}$/.test(sPlainArgs[1])) || (sName === "head" && /^-[0-9]{1,5}$/.test(sPlainArgs[1])))) || (sPlainArgs[0] === "-c" && /^[0-9]{1,5}$/.test(sPlainArgs[1]))));
14040
- if (!okHT)
14041
- return false;
14042
- }
14043
- else if (sName === "cut") {
14044
- // Byte cuts may reject non-ASCII data on a later iteration.
14045
- if (this.evalSyncCut(["é"], sPlainArgs, byteLocale(rawState.variables)) === undefined)
14046
- return false;
14047
- }
14048
- else if (sName === "sed") {
14049
- if (this.evalSyncSed([], sPlainArgs) === undefined)
14050
- return false;
14051
- }
14052
- else if (sName === "awk") {
14053
- if (sIdx !== 1)
14054
- return false;
14055
- const input = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState);
14056
- if (input === undefined)
14057
- return false;
14058
- const lines = input.length === 0 ? [] : input.split("\n");
14059
- if (input.endsWith("\n"))
14060
- lines.pop();
14061
- if (this.evalSyncAwk(lines, sPlainArgs) === undefined)
14062
- return false;
14063
- }
14064
- else if (sName === "grep") {
14065
- if (this.evalSyncGrep([], sPlainArgs, Boolean(rawState.errexit)) === undefined)
14066
- return false;
14067
- }
14068
- else if (sName === "tr") {
14069
- if (this.evalSyncTr("", sPlainArgs) === undefined)
14070
- return false;
14071
- }
14072
- else if (sName === "jq") {
14073
- if (sIdx !== 1)
14074
- return false;
14075
- const jqIn = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState)?.trim();
14076
- if (jqIn === undefined || this.evalSyncJq(jqIn, sPlainArgs) === undefined)
14077
- return false;
14078
- }
14079
- else if (sName === "base64") {
14080
- if (sPlainArgs.length !== 0)
14081
- return false;
14082
- }
14083
- else if (sName === "tac") {
14084
- if (sPlainArgs.length !== 0 || (w0Plain !== "echo" && !hasSingleHereStringRedir))
14085
- return false;
14086
- }
14087
- else if (sName === "nl") {
14088
- if ((w0Plain !== "echo" && !hasSingleHereStringRedir) || this.evalSyncNl([], sPlainArgs) === undefined)
14089
- return false;
14090
- }
14091
- else if (sName === "paste") {
14092
- if (this.evalSyncPaste([], sPlainArgs) === undefined)
14093
- return false;
14094
- }
14095
- else if (sName === "numfmt") {
14096
- if (sIdx !== 1)
14097
- return false;
14098
- const nmCheck = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState)?.trim();
14099
- if (nmCheck === undefined || this.evalSyncNumfmt(nmCheck.split("\n"), sPlainArgs) === undefined)
14100
- return false;
14101
- }
14102
- else if (sName === "rev") {
14103
- if (sPlainArgs.length !== 0)
14104
- return false;
14105
- }
14106
- }
14107
13879
  }
14108
13880
  continue;
14109
13881
  }
@@ -14137,11 +13909,11 @@ export class Runtime {
14137
13909
  if (isNumOp && checkIntVars) {
14138
13910
  const isGuaranteedIntWord = (w) => {
14139
13911
  if (w.plain !== undefined)
14140
- return /^-?[0-9]+$/.test(w.plain);
13912
+ return /^-?[0-9]{1,16}$/.test(w.plain);
14141
13913
  if (w.parts.length === 1) {
14142
13914
  const p0 = w.parts[0];
14143
13915
  if (p0.kind === "arithmetic")
14144
- return true;
13916
+ return false;
14145
13917
  if (p0.kind === "variable" && !p0.indirect && !p0.prefixNames && !p0.substring && !p0.transform && p0.operator === undefined && getArraySelector(p0) === undefined) {
14146
13918
  if (script) {
14147
13919
  for (const lst of script.lists) {
@@ -14149,14 +13921,14 @@ export class Runtime {
14149
13921
  const c0 = pl.commands[0];
14150
13922
  if (c0?.kind === "simple" && c0.words.length === 1) {
14151
13923
  const asg = this.assignment(c0.words[0]);
14152
- if (asg?.name === p0.name && (!asg.value.plain || !/^-?[0-9]+$/.test(asg.value.plain)))
13924
+ if (asg?.name === p0.name && (!asg.value.plain || !/^-?[0-9]{1,16}$/.test(asg.value.plain)))
14153
13925
  return false;
14154
13926
  }
14155
13927
  }
14156
13928
  }
14157
13929
  }
14158
13930
  const cur = rawState.variables[p0.name];
14159
- return cur === undefined || /^-?[0-9]+$/.test(cur);
13931
+ return cur === undefined || /^-?[0-9]{1,16}$/.test(cur);
14160
13932
  }
14161
13933
  }
14162
13934
  return false;
@@ -14646,7 +14418,8 @@ export class Runtime {
14646
14418
  return false;
14647
14419
  return this.isPureSyncValueWord(e.left, rawState) && this.getFastAnchoredEreRegex(e.right, rawState) !== undefined && !rawState.readonlyVariables?.has("BASH_REMATCH") && !rawState.exported.has("BASH_REMATCH") && !stateMonitor(rawState)?.hasOverlay("BASH_REMATCH") && !(store ?? requireArrays(rawState))?.watches.has("BASH_REMATCH");
14648
14420
  }
14649
- return this.isPureSyncValueWord(e.left, rawState) && (e.operator === "==" || e.operator === "=" || e.operator === "!=" ? this.canSyncPatternWordParts(e.right, rawState) : this.isPureSyncValueWord(e.right, rawState));
14421
+ // A synchronous loop cannot hand off an unsupported pattern mid-iteration.
14422
+ return this.isPureSyncValueWord(e.left, rawState) && this.isPureSyncValueWord(e.right, rawState) && (!(e.operator === "==" || e.operator === "=" || e.operator === "!=") || (e.right.parts.length > 0 && e.right.parts.every(p => p.quoted)));
14650
14423
  }
14651
14424
  if (e.kind === "not")
14652
14425
  return checkLeaf(e.operand);
@@ -14807,6 +14580,8 @@ export class Runtime {
14807
14580
  // Nested step execution does not implement redirects; use normal execution.
14808
14581
  if (nested && cmd.redirects.length !== 0)
14809
14582
  return false;
14583
+ if (cmd.words.length === 0)
14584
+ return false;
14810
14585
  if (cmd.redirects.length === 1) {
14811
14586
  const r0Check = cmd.redirects[0];
14812
14587
  const w0DevPlain = cmd.words[0].plain;
@@ -14824,11 +14599,16 @@ export class Runtime {
14824
14599
  (w0DevPlain === "pwd" || this.commands.has(w0DevPlain))) {
14825
14600
  const devWordOk = (w) => this.isPureSyncValueWord(w, rawState) &&
14826
14601
  w.parts.every(p => p.quoted || (p.kind === "text" && p.value.length > 0 && !p.value.includes(" ") && !p.value.includes("\t") && !p.value.includes("\n") && !p.value.includes("{") && !hasGlobOrEscape(p.value, true)));
14602
+ // Only a nonempty literal prefix proves an expansion cannot become an option.
14603
+ const devOperandOk = (w) => {
14604
+ const first = w.parts[0];
14605
+ return first?.kind === "text" && first.value.length > 0 && !first.value.startsWith("-") && devWordOk(w);
14606
+ };
14827
14607
  const devShapeOk = (w0DevPlain === "pwd" && cmd.words.length === 1) ||
14828
- ((w0DevPlain === "dirname" || w0DevPlain === "echo") && cmd.words.length === 2 && !cmd.words[1].plain?.startsWith("-") && devWordOk(cmd.words[1])) ||
14829
- (w0DevPlain === "basename" && ((cmd.words.length === 2 && !cmd.words[1].plain?.startsWith("-") && devWordOk(cmd.words[1])) ||
14830
- (cmd.words.length === 3 && !cmd.words[1].plain?.startsWith("-") && devWordOk(cmd.words[1]) && devWordOk(cmd.words[2])) ||
14831
- (cmd.words.length === 4 && cmd.words[1].plain === "-s" && cmd.words[2].plain !== undefined && !cmd.words[3].plain?.startsWith("-") && devWordOk(cmd.words[3]))));
14608
+ ((w0DevPlain === "dirname" || w0DevPlain === "echo") && cmd.words.length === 2 && devOperandOk(cmd.words[1])) ||
14609
+ (w0DevPlain === "basename" && ((cmd.words.length === 2 && devOperandOk(cmd.words[1])) ||
14610
+ (cmd.words.length === 3 && devOperandOk(cmd.words[1]) && devWordOk(cmd.words[2])) ||
14611
+ (cmd.words.length === 4 && cmd.words[1].plain === "-s" && cmd.words[2].plain !== undefined && devOperandOk(cmd.words[3]))));
14832
14612
  if (devShapeOk)
14833
14613
  continue;
14834
14614
  }
@@ -15480,22 +15260,13 @@ export class Runtime {
15480
15260
  ? whilePrintfInd
15481
15261
  : (forPrintfInd && this.canSyncPrintfInductionBody(command.body, rawState, forPrintfInd) ? forPrintfInd : undefined));
15482
15262
  const prevActivePrintfInd = this._activePrintfInductionName;
15483
- const prevActiveLoopScript = this._activeLoopBodyScript;
15484
- const prevActiveLoopCond = this._activeLoopCondScript;
15485
- const prevActiveLoopVar = this._activeLoopVarName;
15486
15263
  this._activePrintfInductionName = printfInductionName;
15487
- this._activeLoopBodyScript = command.body;
15488
- this._activeLoopCondScript = command.kind === "while" ? command.condition : undefined;
15489
- this._activeLoopVarName = command.kind === "for" ? command.name : (printfInductionName ?? (whilePrintfInd || undefined));
15490
15264
  let bodySyncOk = false;
15491
15265
  try {
15492
15266
  bodySyncOk = !(guestArrays(rawState) && (guestArrays(rawState).watches.size > 0 || this.budget.limits.maxExpansionBytes < 65536)) && this.canSyncLoopBody(command.body, rawState, io, false, printfInductionName);
15493
15267
  }
15494
15268
  finally {
15495
15269
  this._activePrintfInductionName = prevActivePrintfInd;
15496
- this._activeLoopBodyScript = prevActiveLoopScript;
15497
- this._activeLoopCondScript = prevActiveLoopCond;
15498
- this._activeLoopVarName = prevActiveLoopVar;
15499
15270
  }
15500
15271
  if (!bodySyncOk) {
15501
15272
  command._skipTrySyncLoop = true;
@@ -16101,7 +15872,7 @@ export class Runtime {
16101
15872
  !monitor.hasOverlay("BASH_REMATCH") &&
16102
15873
  !(store ?? requireArrays(rawState))?.watches.has("BASH_REMATCH"));
16103
15874
  }
16104
- return this.isPureSyncValueWord(e.left, rawState) && (e.operator === "==" || e.operator === "=" || e.operator === "!=" ? this.canSyncPatternWordParts(e.right, rawState) : this.isPureSyncValueWord(e.right, rawState));
15875
+ return this.isPureSyncValueWord(e.left, rawState) && this.isPureSyncValueWord(e.right, rawState) && (!(e.operator === "==" || e.operator === "=" || e.operator === "!=") || (e.right.parts.length > 0 && e.right.parts.every(p => p.quoted)));
16105
15876
  }
16106
15877
  if (e.kind === "not")
16107
15878
  return checkWhileCond(e.operand, depth + 1);
@@ -16432,7 +16203,7 @@ export class Runtime {
16432
16203
  store.epoch = restEpoch;
16433
16204
  return loopStatus;
16434
16205
  }
16435
- if (!isShellIdentifier(command.name) || command.name === "OPTIND" || command.name.includes("[") || store?.get(command.name) || (command.words && (command.words.length === 0 || command.words.length > 64)) || rawState.braceexpand === false) {
16206
+ if (!isShellIdentifier(command.name) || command.name === "OPTIND" || controlNames.has(command.name) || command.name === "LC_COLLATE" || command.name.includes("[") || store?.get(command.name) || (command.words && (command.words.length === 0 || command.words.length > 64)) || rawState.braceexpand === false) {
16436
16207
  return undefined;
16437
16208
  }
16438
16209
  const activeValScope = io[valueScope];
@@ -16931,187 +16702,6 @@ export class Runtime {
16931
16702
  return pos <= 128 ? fastRedirectScratchViews[pos] : fastRedirectScratchBytes.subarray(0, pos);
16932
16703
  }
16933
16704
  _activePrintfInductionName = undefined;
16934
- _activeLoopBodyScript = undefined;
16935
- _activeLoopCondScript = undefined;
16936
- _activeLoopVarName = undefined;
16937
- isVarUnmutatedInScript(varName, script) {
16938
- for (const list of script.lists) {
16939
- for (const pipeline of list.pipelines) {
16940
- for (const cmd of pipeline.commands) {
16941
- if (cmd.kind === "arithmetic") {
16942
- if (cmd.expression.hasMutation && cmd.expression.source.includes(varName))
16943
- return false;
16944
- continue;
16945
- }
16946
- if (cmd.kind === "if") {
16947
- for (const br of cmd.branches) {
16948
- if (!this.isVarUnmutatedInScript(varName, br.condition) || !this.isVarUnmutatedInScript(varName, br.body))
16949
- return false;
16950
- }
16951
- if (cmd.otherwise && !this.isVarUnmutatedInScript(varName, cmd.otherwise))
16952
- return false;
16953
- continue;
16954
- }
16955
- if (cmd.kind === "case") {
16956
- for (const cl of cmd.clauses) {
16957
- if (!this.isVarUnmutatedInScript(varName, cl.body))
16958
- return false;
16959
- }
16960
- continue;
16961
- }
16962
- if (cmd.kind === "for") {
16963
- if (cmd.name === varName || !this.isVarUnmutatedInScript(varName, cmd.body))
16964
- return false;
16965
- continue;
16966
- }
16967
- if (cmd.kind === "arithmetic-for") {
16968
- if (cmd.expressions.some(e => e && e.hasMutation && e.source.includes(varName)) || !this.isVarUnmutatedInScript(varName, cmd.body))
16969
- return false;
16970
- continue;
16971
- }
16972
- if (cmd.kind === "while") {
16973
- if (!this.isVarUnmutatedInScript(varName, cmd.condition) || !this.isVarUnmutatedInScript(varName, cmd.body))
16974
- return false;
16975
- continue;
16976
- }
16977
- if (cmd.kind === "group" || cmd.kind === "subshell") {
16978
- if (!this.isVarUnmutatedInScript(varName, cmd.body))
16979
- return false;
16980
- continue;
16981
- }
16982
- if (cmd.kind !== "simple")
16983
- return false;
16984
- for (const w of cmd.words) {
16985
- const arrAss = getArrayAssignment(w);
16986
- if (arrAss && arrAss.name === varName)
16987
- return false;
16988
- const p0 = w.parts[0];
16989
- if (p0?.kind === "text" && !p0.quoted && (p0.value.startsWith(varName + "=") || p0.value.startsWith(varName + "+=") || p0.value.startsWith(varName + "[")))
16990
- return false;
16991
- }
16992
- const w0 = cmd.words[0]?.plain;
16993
- if (w0 === "read" || w0 === "unset" || w0 === "declare" || w0 === "typeset" || w0 === "local" || w0 === "export" || w0 === "mapfile" || w0 === "readarray") {
16994
- if (w0 === "read" && varName === "REPLY")
16995
- return false;
16996
- if (cmd.words.slice(1).some(w => (w.plain ?? "").includes(varName)))
16997
- return false;
16998
- }
16999
- if (w0 === "printf" && cmd.words[1]?.plain === "-v" && (cmd.words[2]?.plain ?? "").startsWith(varName))
17000
- return false;
17001
- for (const w of [...cmd.words, ...cmd.redirects.map(r => r.target)]) {
17002
- for (const pt of w.parts) {
17003
- if (pt.kind === "arithmetic" && pt.expression.hasMutation && pt.expression.source.includes(varName))
17004
- return false;
17005
- if (pt.kind === "substitution" && !this.isVarUnmutatedInScript(varName, pt.script))
17006
- return false;
17007
- }
17008
- }
17009
- }
17010
- }
17011
- }
17012
- return true;
17013
- }
17014
- isVarUnmutatedInActiveLoop(varName) {
17015
- if (varName === this._activeLoopVarName || varName === this._activePrintfInductionName)
17016
- return false;
17017
- if (this._activeLoopCondScript && !this.isVarUnmutatedInScript(varName, this._activeLoopCondScript))
17018
- return false;
17019
- if (this._activeLoopBodyScript && !this.isVarUnmutatedInScript(varName, this._activeLoopBodyScript))
17020
- return false;
17021
- return true;
17022
- }
17023
- isFileReadOnlyScript(script, rawState) {
17024
- const visit = (value) => {
17025
- if (value === null || typeof value !== "object")
17026
- return true;
17027
- if (Array.isArray(value))
17028
- return value.every(visit);
17029
- const node = value;
17030
- if (typeof node.operator === "string" && "target" in node && !["<", "<<<", "<<", "<<-"].includes(node.operator))
17031
- return false;
17032
- if (node.kind === "simple") {
17033
- const cmd = value;
17034
- // Unknown commands (including shell functions) cannot promise read-only I/O.
17035
- const name = cmd.words[0]?.plain;
17036
- if (name === undefined && !cmd.words.every(word => this.assignment(word) || getArrayAssignment(word)))
17037
- return false;
17038
- if (name !== undefined && (hasShellFunction(rawState, name) || !["jq", "cat", "echo", "printf", "test", "[", ":", "true", "false"].includes(name)))
17039
- return false;
17040
- }
17041
- return Object.values(node).every(visit);
17042
- };
17043
- return visit(script);
17044
- }
17045
- extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState) {
17046
- const w0 = cmd.words[0]?.plain;
17047
- const resolveInvariantWord = (w) => {
17048
- if (w.plain !== undefined)
17049
- return w.plain;
17050
- if (w.parts.length > 0 && w.parts.every(pt => pt.kind === "text"))
17051
- return w.parts.map(pt => pt.value).join("");
17052
- if (w.parts.length === 1 && w.parts[0].kind === "variable") {
17053
- const vp = w.parts[0];
17054
- if (!vp.indirect && !vp.operator && !vp.substring && !vp.transform && !vp.length && !vp.prefixNames && getArraySelector(vp) === undefined && this.isVarUnmutatedInActiveLoop(vp.name)) {
17055
- const val = rawState.variables[vp.name];
17056
- if (typeof val === "string")
17057
- return val;
17058
- }
17059
- }
17060
- return undefined;
17061
- };
17062
- if (w0 === "jq" || w0 === "numfmt" || w0 === "awk") {
17063
- if (hasSingleHereStringRedir) {
17064
- const resolved = resolveInvariantWord(cmd.redirects[0].target);
17065
- return resolved !== undefined ? resolved + "\n" : undefined;
17066
- }
17067
- if (this._fileWrites?.size || this._outputFiles?.size)
17068
- return undefined;
17069
- if ([this._activeLoopBodyScript, this._activeLoopCondScript].some(script => script && !this.isFileReadOnlyScript(script, rawState)))
17070
- return undefined;
17071
- const fPlain = hasSingleStdinRedir ? cmd.redirects[0].target.plain : cmd.words[cmd.words.length - 1]?.plain;
17072
- if (!fPlain || fPlain.startsWith("-") || fPlain === "/dev/stdin")
17073
- return undefined;
17074
- const vCheck = this.tryReadMemoryFileViewSync(resolvePath(rawState.cwd, fPlain), false);
17075
- if (!vCheck || vCheck.byteLength > 16384 || vCheck.includes(0))
17076
- return undefined;
17077
- return sharedSyncPipeDecoder.decode(vCheck);
17078
- }
17079
- if (w0 === "cat") {
17080
- if (hasSingleHereStringRedir) {
17081
- const resolved = resolveInvariantWord(cmd.redirects[0].target);
17082
- return resolved !== undefined ? resolved + "\n" : undefined;
17083
- }
17084
- if (this._fileWrites?.size || this._outputFiles?.size)
17085
- return undefined;
17086
- if ([this._activeLoopBodyScript, this._activeLoopCondScript].some(script => script && !this.isFileReadOnlyScript(script, rawState)))
17087
- return undefined;
17088
- const fPlain = hasSingleStdinRedir ? cmd.redirects[0].target.plain : cmd.words[1]?.plain;
17089
- if (!fPlain || fPlain.startsWith("-") || fPlain === "/dev/stdin")
17090
- return undefined;
17091
- const vCheck = this.tryReadMemoryFileViewSync(resolvePath(rawState.cwd, fPlain), false);
17092
- if (!vCheck || vCheck.byteLength > 16384 || vCheck.includes(0))
17093
- return undefined;
17094
- return sharedSyncPipeDecoder.decode(vCheck);
17095
- }
17096
- if (w0 === "echo" && cmd.words.length === 2) {
17097
- const resolved = resolveInvariantWord(cmd.words[1]);
17098
- if (resolved !== undefined && !resolved.startsWith("-"))
17099
- return resolved + "\n";
17100
- return undefined;
17101
- }
17102
- if (w0 === "printf" && cmd.words.length >= 2) {
17103
- const args = [];
17104
- for (let i = 1; i < cmd.words.length; i++) {
17105
- const r = resolveInvariantWord(cmd.words[i]);
17106
- if (r === undefined)
17107
- return undefined;
17108
- args.push(r);
17109
- }
17110
- const formatted = tryFastPrintf(args);
17111
- return formatted;
17112
- }
17113
- return undefined;
17114
- }
17115
16705
  canSyncPrintfInductionBody(script, rawState, inductionName) {
17116
16706
  for (const list of script.lists)
17117
16707
  for (const pipeline of list.pipelines)
@@ -17123,7 +16713,24 @@ export class Runtime {
17123
16713
  }
17124
16714
  if (command.kind !== "simple" || hasShellFunction(rawState, command.words[0]?.plain ?? "") || command.words.some(word => getArrayAssignment(word) !== undefined))
17125
16715
  return false;
17126
- const w0 = command.words[0]?.plain;
16716
+ let commandIndex = 0;
16717
+ while (commandIndex < command.words.length && this.assignment(command.words[commandIndex]))
16718
+ commandIndex++;
16719
+ const commandWord = command.words[commandIndex];
16720
+ if (!commandWord?.parts.every(part => part.kind === "text"))
16721
+ return false;
16722
+ const w0 = commandWord.parts.map(part => part.kind === "text" ? part.value : "").join("");
16723
+ // read targets and printf -v targets may be quoted or expanded. Both can
16724
+ // invalidate the integer assumption used by induction formatting.
16725
+ if (w0 === "read")
16726
+ return false;
16727
+ if (w0 === "printf") {
16728
+ const option = command.words[commandIndex + 1];
16729
+ if (!option?.parts.every(part => part.kind === "text"))
16730
+ return false;
16731
+ if (option.parts.map(part => part.kind === "text" ? part.value : "").join("") === "-v")
16732
+ return false;
16733
+ }
17127
16734
  if (w0 === "let" || w0 === "declare" || w0 === "typeset" || w0 === "local" || w0 === "export" || w0 === "unset" || w0 === "mapfile" || w0 === "readarray")
17128
16735
  return false;
17129
16736
  if (inductionName !== undefined) {
@@ -17132,10 +16739,6 @@ export class Runtime {
17132
16739
  return p0?.kind === "text" && !p0.quoted && (p0.value.startsWith(inductionName + "=") || p0.value.startsWith(inductionName + "+="));
17133
16740
  }))
17134
16741
  return false;
17135
- if (w0 === "read" && (inductionName === "REPLY" || command.words.slice(1).some(w => w.plain === inductionName || w.plain === "-a" || w.plain === "-ra" || w.plain === "-ar")))
17136
- return false;
17137
- if (w0 === "printf" && command.words[1]?.plain === "-v" && command.words[2]?.plain === inductionName)
17138
- return false;
17139
16742
  }
17140
16743
  for (const word of [...command.words, ...command.redirects.map(redirect => redirect.target)])
17141
16744
  for (const part of word.parts) {
@@ -17414,6 +17017,7 @@ export class Runtime {
17414
17017
  execSyncLocalDeclStep(decl, rawState, io, monitor, touched, line) {
17415
17018
  const locals = (decl.kind === "local" || decl.kind === "declare") && rawState.locals.length > 0 ? rawState.locals[rawState.locals.length - 1] : undefined;
17416
17019
  let lastArg = decl.kind;
17020
+ const values = decl.items.map(item => item.value === undefined ? undefined : this.fastValueWord(item.value, rawState, io, false, false, false, rawState.braceexpand !== false, 0, line));
17417
17021
  for (let i = 0; i < decl.items.length; i++) {
17418
17022
  const item = decl.items[i];
17419
17023
  const isNewLocal = locals ? !locals.has(item.name) : false;
@@ -17431,7 +17035,7 @@ export class Runtime {
17431
17035
  touched.add(item.name);
17432
17036
  }
17433
17037
  if (item.value !== undefined) {
17434
- const val = this.fastValueWord(item.value, rawState, io, false, false, false, rawState.braceexpand !== false, 0, line);
17038
+ const val = values[i];
17435
17039
  if (item.append) {
17436
17040
  const curRaw = rawState.variables[item.name];
17437
17041
  const curStr = curRaw === undefined ? "" : (touched.has(item.name) ? curRaw : (monitor.values.get(item.name, curRaw) ?? curRaw));
@@ -17441,7 +17045,7 @@ export class Runtime {
17441
17045
  rawState.variables[item.name] = val;
17442
17046
  }
17443
17047
  touched.add(item.name);
17444
- lastArg = `${item.name}=${rawState.variables[item.name]}`;
17048
+ lastArg = `${item.name}${item.append ? "+=" : "="}${val}`;
17445
17049
  }
17446
17050
  else {
17447
17051
  lastArg = item.name;
@@ -17540,7 +17144,28 @@ export class Runtime {
17540
17144
  if (this._syncStdoutBatch.length >= 8192)
17541
17145
  this.flushSyncStdoutBatch(io);
17542
17146
  }
17147
+ runSyncDiscardStep(step, rawState, io) {
17148
+ this.budget.fileSystemOperation();
17149
+ const args = step.discardWords.map(word => this.fastValueWord(word, rawState, io, false, false, false, false, 0, step.line));
17150
+ const cmdName = step.cmd.words[0].plain;
17151
+ let output;
17152
+ if (cmdName === "pwd")
17153
+ output = rawState.variables.PWD ?? rawState.cwd;
17154
+ else if (cmdName === "echo")
17155
+ output = args[0];
17156
+ else if (cmdName === "dirname")
17157
+ output = this.evalSyncDirname(args);
17158
+ else
17159
+ output = this.evalSyncBasename(step.discardSuffix === undefined ? args : ["-s", step.discardSuffix, ...args]);
17160
+ const bytes = shellValueByteLength(output) + 1;
17161
+ if (this.budget.bytes + bytes > this.budget.maxOutputBytesSmi && bytes > this.budget.limits.maxOutputBytes - this.budget.bytes)
17162
+ this.budget.fail("maxOutputBytes");
17163
+ this.budget.bytes += bytes;
17164
+ return args[args.length - 1] ?? cmdName;
17165
+ }
17543
17166
  writeSyncRedirectStep(step, encoded, append, mode, rawState, io) {
17167
+ if (this.budget.bytes + encoded.byteLength > this.budget.maxOutputBytesSmi && encoded.byteLength > this.budget.limits.maxOutputBytes - this.budget.bytes)
17168
+ this.budget.fail("maxOutputBytes");
17544
17169
  // The memory fast writer may retain its input; the encoder scratch is reused.
17545
17170
  encoded = encoded.slice();
17546
17171
  if (step.targetDirPrefix !== undefined && step.targetNamePrefix !== undefined) {
@@ -17704,31 +17329,7 @@ export class Runtime {
17704
17329
  lastArg = pLastArg;
17705
17330
  }
17706
17331
  else if (step.isDiscardDevNull && step.discardWords !== undefined) {
17707
- this.budget.fileSystemOperation();
17708
- const dw = step.discardWords;
17709
- const cmdName = step.cmd.words[0].plain;
17710
- let evalFirst = "";
17711
- let evalLast = cmdName;
17712
- for (let wi = 0; wi < dw.length; wi++) {
17713
- const v = this.fastValueWord(dw[wi], rawState, io, false, false, false, false, 0, step.line);
17714
- if (wi === 0)
17715
- evalFirst = v;
17716
- evalLast = v;
17717
- }
17718
- let outChars = 0;
17719
- if (cmdName === "pwd")
17720
- outChars = (rawState.variables.PWD ?? rawState.cwd).length + 1;
17721
- else if (cmdName === "echo")
17722
- outChars = evalFirst.length + 1;
17723
- else if (cmdName === "dirname")
17724
- outChars = dirname(evalFirst).length + 1;
17725
- else if (cmdName === "basename") {
17726
- const sfx = step.discardSuffix !== undefined ? step.discardSuffix : (dw.length === 2 ? evalLast : undefined);
17727
- const baseStr = /^\/+$/u.test(evalFirst) ? "/" : basename(evalFirst);
17728
- outChars = (sfx && baseStr !== sfx && baseStr.endsWith(sfx) ? baseStr.length - sfx.length : baseStr.length) + 1;
17729
- }
17730
- this.budget.bytes += outChars;
17731
- lastArg = step.discardSuffix !== undefined ? evalLast : (dw.length > 0 ? evalLast : cmdName);
17332
+ lastArg = this.runSyncDiscardStep(step, rawState, io);
17732
17333
  }
17733
17334
  else if (step.name !== undefined && step.value !== undefined) {
17734
17335
  rawState.substitutionStatus = 0;
@@ -17806,6 +17407,10 @@ export class Runtime {
17806
17407
  return check(tree);
17807
17408
  }
17808
17409
  canSyncErrorArithmeticStmt(expr, rawState) {
17410
+ // Expansion can introduce array, attributed, or control-variable references.
17411
+ // Their reads and writes require the full arithmetic evaluator.
17412
+ if (expr.source.includes("$"))
17413
+ return false;
17809
17414
  if (expr.source.includes("/") || expr.source.includes("%") || expr.source.includes("<<") || expr.source.includes(">>") || expr.source.includes("**") || expr.source.includes("["))
17810
17415
  return false;
17811
17416
  try {
@@ -17968,19 +17573,34 @@ export class Runtime {
17968
17573
  const prevFastSubPos = this._fastSubPositional;
17969
17574
  rawState.positional = fnArgs;
17970
17575
  this._fastSubPositional = fnArgs;
17971
- const savedLocals = new Array(fnCall.locals.length);
17576
+ const savedLocals = [];
17577
+ const seenLocals = new Set();
17972
17578
  try {
17973
- for (let i = 0; i < fnCall.locals.length; i++) {
17974
- const loc = fnCall.locals[i];
17975
- if (loc.isFirstInCmd)
17976
- this.budget.tick();
17977
- savedLocals[i] = { name: loc.name, val: rawState.variables[loc.name], wasTouched: touched.has(loc.name) };
17978
- if (loc.valueWord !== undefined) {
17979
- rawState.variables[loc.name] = this.fastValueWord(loc.valueWord, rawState, io, false, false, false, false, 0, step.line);
17980
- touched.add(loc.name);
17981
- }
17982
- else {
17983
- delete rawState.variables[loc.name];
17579
+ for (let i = 0; i < fnCall.locals.length;) {
17580
+ this.budget.tick();
17581
+ let end = i + 1;
17582
+ while (end < fnCall.locals.length && !fnCall.locals[end].isFirstInCmd)
17583
+ end++;
17584
+ // Expand the entire simple command before any of its bindings change.
17585
+ const values = fnCall.locals.slice(i, end).map(loc => loc.valueWord === undefined
17586
+ ? undefined
17587
+ : this.fastValueWord(loc.valueWord, rawState, io, false, false, false, false, 0, step.line));
17588
+ const start = i;
17589
+ for (; i < end; i++) {
17590
+ const loc = fnCall.locals[i];
17591
+ const alreadyLocal = seenLocals.has(loc.name);
17592
+ if (!alreadyLocal) {
17593
+ savedLocals.push({ name: loc.name, val: rawState.variables[loc.name], wasTouched: touched.has(loc.name) });
17594
+ seenLocals.add(loc.name);
17595
+ }
17596
+ const value = values[i - start];
17597
+ if (value !== undefined) {
17598
+ rawState.variables[loc.name] = value;
17599
+ touched.add(loc.name);
17600
+ }
17601
+ else if (!alreadyLocal) {
17602
+ delete rawState.variables[loc.name];
17603
+ }
17984
17604
  }
17985
17605
  }
17986
17606
  if (fnCall.steps.length > 0) {
@@ -17988,11 +17608,12 @@ export class Runtime {
17988
17608
  }
17989
17609
  else {
17990
17610
  rawState.status = 0;
17991
- onUpdate({ lastCmd: step.cmd, lastArg: fnArgs.length > 0 ? fnArgs[fnArgs.length - 1] : fnCall.name });
17992
17611
  }
17612
+ rawState.lastArgument = fnArgs.at(-1) ?? fnCall.name;
17613
+ onUpdate({ lastCmd: step.cmd, lastArg: rawState.lastArgument });
17993
17614
  }
17994
17615
  finally {
17995
- for (let i = fnCall.locals.length - 1; i >= 0; i--) {
17616
+ for (let i = savedLocals.length - 1; i >= 0; i--) {
17996
17617
  const saved = savedLocals[i];
17997
17618
  if (saved.val !== undefined) {
17998
17619
  rawState.variables[saved.name] = saved.val;
@@ -18268,31 +17889,7 @@ export class Runtime {
18268
17889
  lastArg = pLastArg;
18269
17890
  }
18270
17891
  else if (step.isDiscardDevNull && step.discardWords !== undefined) {
18271
- this.budget.fileSystemOperation();
18272
- const dw = step.discardWords;
18273
- const cmdName = step.cmd.words[0].plain;
18274
- let evalFirst = "";
18275
- let evalLast = cmdName;
18276
- for (let wi = 0; wi < dw.length; wi++) {
18277
- const v = this.fastValueWord(dw[wi], rawState, io, false, false, false, false, 0, step.line);
18278
- if (wi === 0)
18279
- evalFirst = v;
18280
- evalLast = v;
18281
- }
18282
- let outChars = 0;
18283
- if (cmdName === "pwd")
18284
- outChars = (rawState.variables.PWD ?? rawState.cwd).length + 1;
18285
- else if (cmdName === "echo")
18286
- outChars = evalFirst.length + 1;
18287
- else if (cmdName === "dirname")
18288
- outChars = dirname(evalFirst).length + 1;
18289
- else if (cmdName === "basename") {
18290
- const sfx = step.discardSuffix !== undefined ? step.discardSuffix : (dw.length === 2 ? evalLast : undefined);
18291
- const baseStr = /^\/+$/u.test(evalFirst) ? "/" : basename(evalFirst);
18292
- outChars = (sfx && baseStr !== sfx && baseStr.endsWith(sfx) ? baseStr.length - sfx.length : baseStr.length) + 1;
18293
- }
18294
- this.budget.bytes += outChars;
18295
- lastArg = step.discardSuffix !== undefined ? evalLast : (dw.length > 0 ? evalLast : cmdName);
17892
+ lastArg = this.runSyncDiscardStep(step, rawState, io);
18296
17893
  }
18297
17894
  else if (step.name !== undefined && step.value !== undefined) {
18298
17895
  rawState.substitutionStatus = 0;
@@ -18448,31 +18045,7 @@ export class Runtime {
18448
18045
  lastArg = pLastArg;
18449
18046
  }
18450
18047
  else if (step.isDiscardDevNull && step.discardWords !== undefined) {
18451
- this.budget.fileSystemOperation();
18452
- const dw = step.discardWords;
18453
- const cmdName = step.cmd.words[0].plain;
18454
- let evalFirst = "";
18455
- let evalLast = cmdName;
18456
- for (let wi = 0; wi < dw.length; wi++) {
18457
- const v = this.fastValueWord(dw[wi], rawState, io, false, false, false, false, 0, step.line);
18458
- if (wi === 0)
18459
- evalFirst = v;
18460
- evalLast = v;
18461
- }
18462
- let outChars = 0;
18463
- if (cmdName === "pwd")
18464
- outChars = (rawState.variables.PWD ?? rawState.cwd).length + 1;
18465
- else if (cmdName === "echo")
18466
- outChars = evalFirst.length + 1;
18467
- else if (cmdName === "dirname")
18468
- outChars = dirname(evalFirst).length + 1;
18469
- else if (cmdName === "basename") {
18470
- const sfx = step.discardSuffix !== undefined ? step.discardSuffix : (dw.length === 2 ? evalLast : undefined);
18471
- const baseStr = /^\/+$/u.test(evalFirst) ? "/" : basename(evalFirst);
18472
- outChars = (sfx && baseStr !== sfx && baseStr.endsWith(sfx) ? baseStr.length - sfx.length : baseStr.length) + 1;
18473
- }
18474
- this.budget.bytes += outChars;
18475
- lastArg = step.discardSuffix !== undefined ? evalLast : (dw.length > 0 ? evalLast : cmdName);
18048
+ lastArg = this.runSyncDiscardStep(step, rawState, io);
18476
18049
  }
18477
18050
  else if (step.name !== undefined && step.value !== undefined) {
18478
18051
  rawState.substitutionStatus = 0;
@@ -19407,8 +18980,6 @@ export class Runtime {
19407
18980
  const rawState = stateMonitor(state)?.raw ?? state;
19408
18981
  if (rawState.variables._ !== undefined)
19409
18982
  delete rawState.variables._;
19410
- rawState.lastArgument = wordValues[wordValues.length - 1] ?? w0Plain;
19411
- state.lastArgument = rawState.lastArgument;
19412
18983
  return await this.dispatchFastFunction(w0Plain, state.functions.get(w0Plain), wordValues.slice(1), state, originalIO);
19413
18984
  }
19414
18985
  }
@@ -20682,8 +20253,10 @@ export class Runtime {
20682
20253
  const rawState = stateMonitor(state)?.raw ?? state;
20683
20254
  if (!state._readOnlyStage && rawState.variables._ !== undefined)
20684
20255
  delete rawState.variables._;
20685
- rawState.lastArgument = words.length > 0 ? words[words.length - 1] : "";
20686
- state.lastArgument = rawState.lastArgument;
20256
+ if (!state.functions.has(words[0] ?? "") && !["eval", ".", "source"].includes(words[declarationIndex] ?? "")) {
20257
+ rawState.lastArgument = words.at(-1) ?? "";
20258
+ state.lastArgument = rawState.lastArgument;
20259
+ }
20687
20260
  if (!words.length)
20688
20261
  return state.substitutionStatus;
20689
20262
  const args = words.slice(1);
@@ -20947,8 +20520,10 @@ export class Runtime {
20947
20520
  const rawState = stateMonitor(state)?.raw ?? state;
20948
20521
  if (rawState.variables._ !== undefined && !assignments.some(assignment => assignment.name === "_"))
20949
20522
  delete rawState.variables._;
20950
- rawState.lastArgument = words.length > 0 ? words[words.length - 1] : "";
20951
- state.lastArgument = rawState.lastArgument;
20523
+ if (!functionCommand && !["eval", ".", "source"].includes(words[declarationIndex] ?? "")) {
20524
+ rawState.lastArgument = words.at(-1) ?? "";
20525
+ state.lastArgument = rawState.lastArgument;
20526
+ }
20952
20527
  }
20953
20528
  if (!words.length)
20954
20529
  return state.substitutionStatus;
@@ -21282,6 +20857,7 @@ export class Runtime {
21282
20857
  }
21283
20858
  if (hasThrown)
21284
20859
  throw thrownError;
20860
+ state.lastArgument = pendingResume?.lastArgument ?? args.at(-1) ?? name;
21285
20861
  return exitStatus;
21286
20862
  }
21287
20863
  finally {
@@ -21801,6 +21377,7 @@ export class Runtime {
21801
21377
  }
21802
21378
  if (outcome.kind === "throw")
21803
21379
  throw outcome.reason;
21380
+ state.lastArgument = context.args.at(-1) ?? context.command;
21804
21381
  return outcome.value;
21805
21382
  }
21806
21383
  if (selectedKind === "builtin" && !(state.externalInvocation && this.commands.has(context.command))) {
@@ -21817,10 +21394,16 @@ export class Runtime {
21817
21394
  }
21818
21395
  if (context.command === "command" || context.command === "builtin" || context.command === "type")
21819
21396
  return { exitCode: await this.discoveryBuiltin(context, state, io, assignments, defaultPath) };
21820
- if (context.command === "." || context.command === "source")
21821
- return { exitCode: await this.sourceBuiltin(context, state, { ...io, ...context }, special) };
21822
- if (context.command === "eval")
21823
- return { exitCode: await this.evalBuiltin(context, state, { ...io, ...context }, special) };
21397
+ if (context.command === "." || context.command === "source") {
21398
+ const exitCode = await this.sourceBuiltin(context, state, { ...io, ...context }, special);
21399
+ state.lastArgument = context.args.at(-1) ?? context.command;
21400
+ return { exitCode };
21401
+ }
21402
+ if (context.command === "eval") {
21403
+ const exitCode = await this.evalBuiltin(context, state, { ...io, ...context }, special);
21404
+ state.lastArgument = context.args.at(-1) ?? context.command;
21405
+ return { exitCode };
21406
+ }
21824
21407
  const builtinWork = this.builtin({ ...context, [declarationArrays]: io[declarationArrays] }, state, assignments, (error, diagnostic) => { builtinFailure = { error, diagnostic }; }, bypassFunctions);
21825
21408
  const builtin = await interruptible(builtinWork, this.signal);
21826
21409
  if (builtin !== undefined) {
@@ -27162,6 +26745,10 @@ export class Runtime {
27162
26745
  expandSyncTrCharArray(spec) {
27163
26746
  if (spec.length === 0)
27164
26747
  return undefined;
26748
+ // Non-ASCII sets require the byte-oriented tr parser.
26749
+ for (let i = 0; i < spec.length; i++)
26750
+ if (spec.charCodeAt(i) >= 128)
26751
+ return undefined;
27165
26752
  // Escaped endpoints need the tr parser before range expansion.
27166
26753
  if (spec.includes("\\") && spec.includes("-"))
27167
26754
  return undefined;
@@ -27694,10 +27281,10 @@ export class Runtime {
27694
27281
  if (!el || typeof el !== "object" || Array.isArray(el))
27695
27282
  return undefined;
27696
27283
  const rec = el;
27697
- const k = rec.key ?? rec.name ?? rec.k;
27284
+ const k = [rec.key, rec.Key, rec.name, rec.Name].find(value => value !== undefined && value !== null && value !== false);
27698
27285
  if (typeof k !== "string")
27699
27286
  return undefined;
27700
- obj[k] = Object.hasOwn(rec, "value") ? rec.value : (Object.hasOwn(rec, "v") ? rec.v : null);
27287
+ obj[k] = Object.hasOwn(rec, "value") ? rec.value : (Object.hasOwn(rec, "Value") ? rec.Value : null);
27701
27288
  }
27702
27289
  return [obj];
27703
27290
  }
@@ -27730,7 +27317,7 @@ export class Runtime {
27730
27317
  return [item.map(x => x === null ? "" : String(x)).join(arg)];
27731
27318
  }
27732
27319
  if (fn === "split")
27733
- return typeof item === "string" ? [item.split(arg)] : undefined;
27320
+ return typeof item === "string" ? [arg === "" ? Array.from(item) : item.split(arg)] : undefined;
27734
27321
  if (fn === "ltrimstr")
27735
27322
  return typeof item === "string" ? [item.startsWith(arg) ? item.slice(arg.length) : item] : undefined;
27736
27323
  if (fn === "rtrimstr")
@@ -27992,9 +27579,8 @@ export class Runtime {
27992
27579
  break;
27993
27580
  }
27994
27581
  if (!a.startsWith("-") || a === "-") {
27995
- operands.push(a);
27996
- idx++;
27997
- continue;
27582
+ operands.push(...args.slice(idx));
27583
+ break;
27998
27584
  }
27999
27585
  if (a === "-a" || a === "--multiple") {
28000
27586
  multiple = true;
@@ -28320,6 +27906,10 @@ export class Runtime {
28320
27906
  const core = anchorEnd ? core1.slice(0, -1) : core1;
28321
27907
  if (core.length === 0 && !anchorStart && !anchorEnd)
28322
27908
  return undefined;
27909
+ // POSIX-only brackets and leftmost-longest alternatives belong to sed's matcher.
27910
+ if (core.includes("[:") || core.includes("[.") || core.includes("[=") ||
27911
+ core.includes("[]") || core.includes("[^]") || isExtended && core.includes("|"))
27912
+ return undefined;
28323
27913
  let reSrc;
28324
27914
  if (/^[a-zA-Z0-9_ :;,=-]*$/.test(core)) {
28325
27915
  if (/\\[1-9]/.test(rep))
@@ -28376,7 +27966,9 @@ export class Runtime {
28376
27966
  }
28377
27967
  if (st.kind === "=") {
28378
27968
  out.push(String(idx1));
28379
- lastInputIndex = i;
27969
+ // Line numbers are generated records, always terminated, even when
27970
+ // the final input record is partial. Later prints can override this.
27971
+ lastInputIndex = -1;
28380
27972
  continue;
28381
27973
  }
28382
27974
  if (st.kind === "q") {
@@ -28419,8 +28011,10 @@ export class Runtime {
28419
28011
  }
28420
28012
  return replacement;
28421
28013
  });
28422
- if (matchedOnce && st.printOnMatch)
28014
+ if (matchedOnce && st.printOnMatch) {
28423
28015
  out.push(l);
28016
+ lastInputIndex = i;
28017
+ }
28424
28018
  }
28425
28019
  if (quitNow) {
28426
28020
  break;
@@ -28529,7 +28123,9 @@ export class Runtime {
28529
28123
  const isOnlyMatching = mode.includes("o");
28530
28124
  // POSIX bracket classes, collating symbols and equivalence classes have
28531
28125
  // different meanings in JS RegExp. Let the command matcher parse them.
28532
- if (isExtended && !isFixed && rawPatterns.some(pattern => pattern.includes("[:") || pattern.includes("[.") || pattern.includes("[=")))
28126
+ if (!isFixed && rawPatterns.some(pattern => pattern.includes("[:") || pattern.includes("[.") || pattern.includes("[=") ||
28127
+ pattern.includes("[]") || pattern.includes("[^]") ||
28128
+ isExtended && pattern.includes("[") && pattern.includes("|")))
28533
28129
  return undefined;
28534
28130
  if (isCount && (isLineNumber || isOnlyMatching || beforeCtx > 0 || afterCtx > 0))
28535
28131
  return undefined;
@@ -28562,15 +28158,19 @@ export class Runtime {
28562
28158
  if (!re)
28563
28159
  return undefined;
28564
28160
  const out = [];
28161
+ let matched = false;
28565
28162
  for (let li = 0; li < rawLines.length; li++) {
28566
28163
  const matches = rawLines[li].match(re);
28567
28164
  if (matches) {
28165
+ matched = true;
28568
28166
  for (let mi = 0; mi < matches.length; mi++) {
28167
+ if (matches[mi].length === 0)
28168
+ continue;
28569
28169
  out.push(isLineNumber ? `${li + 1}:${matches[mi]}` : matches[mi]);
28570
28170
  }
28571
28171
  }
28572
28172
  }
28573
- return { lines: out, status: out.length > 0 ? 0 : 1 };
28173
+ return { lines: out, status: matched ? 0 : 1 };
28574
28174
  }
28575
28175
  if (isExtended && isFixed)
28576
28176
  return undefined;
@@ -29606,22 +29206,16 @@ export class Runtime {
29606
29206
  if (endField !== undefined && endField < startField)
29607
29207
  return undefined;
29608
29208
  const kf = km[3] ?? "";
29609
- if (kf.includes("n"))
29610
- keyNum = true;
29611
- if (kf.includes("h"))
29612
- keyHuman = true;
29613
- if (kf.includes("M"))
29614
- keyMonth = true;
29615
- if (kf.includes("d"))
29616
- keyDict = true;
29617
- if (kf.includes("r"))
29618
- keyRev = true;
29619
- if (kf.includes("f"))
29620
- keyFold = true;
29621
- if (kf.includes("b"))
29622
- keyBlanks = true;
29623
- if (kf.includes("V"))
29624
- keyVer = true;
29209
+ if (kf.length > 0) {
29210
+ keyNum = kf.includes("n");
29211
+ keyHuman = kf.includes("h");
29212
+ keyMonth = kf.includes("M");
29213
+ keyDict = kf.includes("d");
29214
+ keyRev = kf.includes("r");
29215
+ keyFold = kf.includes("f");
29216
+ keyBlanks = kf.includes("b");
29217
+ keyVer = kf.includes("V");
29218
+ }
29625
29219
  }
29626
29220
  const extractKey = (l) => {
29627
29221
  let raw = keySpec === undefined
@@ -29653,7 +29247,14 @@ export class Runtime {
29653
29247
  raw = raw.replace(/^[ \t]+/, "");
29654
29248
  if (keyDict)
29655
29249
  raw = raw.replace(/[^a-zA-Z0-9 \t]+/g, "");
29656
- return keyFold ? raw.toUpperCase() : raw;
29250
+ if (!keyFold)
29251
+ return raw;
29252
+ let folded = "";
29253
+ for (let i = 0; i < raw.length; i++) {
29254
+ const code = raw.charCodeAt(i);
29255
+ folded += code >= 97 && code <= 122 ? String.fromCharCode(code - 32) : raw[i];
29256
+ }
29257
+ return folded;
29657
29258
  };
29658
29259
  const parseNum = (s) => {
29659
29260
  const m = /^[ \t]*(-?(?:\d+(?:\.\d*)?|\.\d+))/.exec(s);
@@ -29686,6 +29287,24 @@ export class Runtime {
29686
29287
  const idx = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"].indexOf(m[1].toUpperCase());
29687
29288
  return idx === -1 ? 0 : idx + 1;
29688
29289
  };
29290
+ const encoded = new Map();
29291
+ const bytesOf = (line) => {
29292
+ let bytes = encoded.get(line);
29293
+ if (bytes === undefined) {
29294
+ bytes = fastSharedTextEncoder.encode(line);
29295
+ encoded.set(line, bytes);
29296
+ }
29297
+ return bytes;
29298
+ };
29299
+ const compareBytes = (left, right) => {
29300
+ const a = bytesOf(left);
29301
+ const b = bytesOf(right);
29302
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
29303
+ if (a[i] !== b[i])
29304
+ return a[i] - b[i];
29305
+ }
29306
+ return a.length - b.length;
29307
+ };
29689
29308
  const sorted = [...rawLines].sort((a, b) => {
29690
29309
  const ka = extractKey(a);
29691
29310
  const kb = extractKey(b);
@@ -29712,15 +29331,13 @@ export class Runtime {
29712
29331
  return keyRev ? -vc : vc;
29713
29332
  }
29714
29333
  else if (ka !== kb) {
29715
- return ka < kb ? (keyRev ? 1 : -1) : (keyRev ? -1 : 1);
29334
+ const comparison = compareBytes(ka, kb);
29335
+ return keyRev ? -comparison : comparison;
29716
29336
  }
29717
29337
  if (uniq || stable)
29718
29338
  return 0;
29719
- const fa = fold ? a.toUpperCase() : a;
29720
- const fb = fold ? b.toUpperCase() : b;
29721
- if (fa !== fb)
29722
- return fa < fb ? (rev ? 1 : -1) : (rev ? -1 : 1);
29723
- return a < b ? (rev ? 1 : -1) : a > b ? (rev ? -1 : 1) : 0;
29339
+ const comparison = compareBytes(a, b);
29340
+ return rev ? -comparison : comparison;
29724
29341
  });
29725
29342
  if (!uniq)
29726
29343
  return sorted;
@@ -29846,6 +29463,11 @@ export class Runtime {
29846
29463
  return this.evalSyncSort(rawLines, flag !== undefined ? [flag] : [], false) ?? [...rawLines];
29847
29464
  }
29848
29465
  translateSyncTr(input, set1Spec, set2Spec, truncateSet1 = false) {
29466
+ // Other string2 classes require the tr parser's class and alignment validation.
29467
+ if (set2Spec.includes("[:") &&
29468
+ !(set1Spec === "[:lower:]" && set2Spec === "[:upper:]") &&
29469
+ !(set1Spec === "[:upper:]" && set2Spec === "[:lower:]"))
29470
+ return undefined;
29849
29471
  if ((set1Spec === "a-z" || set1Spec === "[:lower:]") && (set2Spec === "A-Z" || set2Spec === "[:upper:]")) {
29850
29472
  return input.replace(/[a-z]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 32));
29851
29473
  }
@@ -30221,7 +29843,7 @@ export class Runtime {
30221
29843
  const firstName = stageNames[sIdx];
30222
29844
  const extDef = stageDefs[sIdx];
30223
29845
  const stageArgs = stageArgsList[sIdx];
30224
- const nextBuf = (index & 1) === 0 ? sharedSyncPipeBuf0 : sharedSyncPipeBuf1;
29846
+ let nextBuf = (index & 1) === 0 ? sharedSyncPipeBuf0 : sharedSyncPipeBuf1;
30225
29847
  const isInlineCutField = firstName === "cut";
30226
29848
  const isInlineTr = firstName === "tr";
30227
29849
  const isInlineAwk = firstName === "awk";
@@ -30270,6 +29892,8 @@ export class Runtime {
30270
29892
  if (nextTotalBytes > this.budget.maxOutputBytesSmi && sub.byteLength > this.budget.limits.maxOutputBytes - this.budget.bytes)
30271
29893
  this.budget.fail("maxOutputBytes");
30272
29894
  this.budget.bytes = nextTotalBytes;
29895
+ if (sub.byteLength > nextBuf.byteLength)
29896
+ nextBuf = new Uint8Array(sub.byteLength);
30273
29897
  nextBuf.set(sub, 0);
30274
29898
  prevBuf = nextBuf;
30275
29899
  prevLen = sub.byteLength;
@@ -30319,12 +29943,12 @@ export class Runtime {
30319
29943
  if (transformed === undefined)
30320
29944
  return undefined;
30321
29945
  const outByteLen = shellValueByteLength(transformed);
30322
- if (outByteLen > nextBuf.byteLength)
30323
- return undefined;
30324
29946
  const nextTotalBytes = this.budget.bytes + outByteLen;
30325
29947
  if (nextTotalBytes > this.budget.maxOutputBytesSmi && outByteLen > this.budget.limits.maxOutputBytes - this.budget.bytes)
30326
29948
  this.budget.fail("maxOutputBytes");
30327
29949
  this.budget.bytes = nextTotalBytes;
29950
+ if (outByteLen > nextBuf.byteLength)
29951
+ nextBuf = new Uint8Array(outByteLen);
30328
29952
  const written = transformed.length === 0 ? 0 : fastSharedTextEncoder.encodeInto(transformed, nextBuf).written;
30329
29953
  prevBuf = nextBuf;
30330
29954
  prevLen = written;
@@ -30362,9 +29986,10 @@ export class Runtime {
30362
29986
  outLines = grepRes.lines;
30363
29987
  }
30364
29988
  else if (isInlineTac) {
30365
- if (inStr.length > 0 && !inStr.endsWith("\n"))
30366
- return undefined;
30367
29989
  outLines = [...rawLines].reverse();
29990
+ if (!inStr.endsWith("\n") && outLines.length > 1) {
29991
+ outLines.splice(0, 2, outLines[0] + outLines[1]);
29992
+ }
30368
29993
  }
30369
29994
  else if (isInlineNl) {
30370
29995
  const nlRes = this.evalSyncNl(rawLines, stageArgs);
@@ -30394,12 +30019,14 @@ export class Runtime {
30394
30019
  if (cleaned.length % 4 !== 0 || (cleaned.length > 0 && !/^[A-Za-z0-9+/]+={0,2}$/.test(cleaned)))
30395
30020
  return undefined;
30396
30021
  const decoded = this.syncBase64DecodeBytes(cleaned);
30397
- if (decoded.some(byte => byte === 0 || byte >= 128) || decoded.byteLength > nextBuf.byteLength)
30022
+ if (decoded.some(byte => byte === 0 || byte >= 128))
30398
30023
  return undefined;
30399
30024
  const nextTotalBytes = this.budget.bytes + decoded.byteLength;
30400
30025
  if (nextTotalBytes > this.budget.maxOutputBytesSmi && decoded.byteLength > this.budget.limits.maxOutputBytes - this.budget.bytes)
30401
30026
  this.budget.fail("maxOutputBytes");
30402
30027
  this.budget.bytes = nextTotalBytes;
30028
+ if (decoded.byteLength > nextBuf.byteLength)
30029
+ nextBuf = new Uint8Array(decoded.byteLength);
30403
30030
  nextBuf.set(decoded, 0);
30404
30031
  prevBuf = nextBuf;
30405
30032
  prevLen = decoded.byteLength;
@@ -30414,15 +30041,15 @@ export class Runtime {
30414
30041
  }
30415
30042
  // These filters preserve the terminator of the final selected input line.
30416
30043
  const preservesTerminator = firstName === "head" || firstName === "tail" || firstName === "rev" || firstName === "sed";
30417
- const terminated = sedTerminated || !preservesTerminator || inStr.endsWith("\n") || firstName === "head" && outLines.length < rawLines.length;
30044
+ const terminated = isInlineTac ? inStr.includes("\n") : sedTerminated || !preservesTerminator || inStr.endsWith("\n") || firstName === "head" && outLines.length < rawLines.length;
30418
30045
  const outStr = outLines.length > 0 ? outLines.join("\n") + (terminated ? "\n" : "") : "";
30419
30046
  const outByteLen = shellValueByteLength(outStr);
30420
- if (outByteLen > nextBuf.byteLength)
30421
- return undefined;
30422
30047
  const nextTotalBytes = this.budget.bytes + outByteLen;
30423
30048
  if (nextTotalBytes > this.budget.maxOutputBytesSmi && outByteLen > this.budget.limits.maxOutputBytes - this.budget.bytes)
30424
30049
  this.budget.fail("maxOutputBytes");
30425
30050
  this.budget.bytes = nextTotalBytes;
30051
+ if (outByteLen > nextBuf.byteLength)
30052
+ nextBuf = new Uint8Array(outByteLen);
30426
30053
  const written = outStr.length === 0 ? 0 : fastSharedTextEncoder.encodeInto(outStr, nextBuf).written;
30427
30054
  prevBuf = nextBuf;
30428
30055
  prevLen = written;