@poe-platform/safe-bash 0.1.748 → 0.1.750

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.
@@ -9405,7 +9405,7 @@ export class Runtime {
9405
9405
  }
9406
9406
  }
9407
9407
  if (command.kind === "conditional") {
9408
- if (rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(command.expression, 0, rawState))
9408
+ if (rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(command.expression, 0, rawState, true))
9409
9409
  return false;
9410
9410
  const checkCondNoFile = (e) => {
9411
9411
  if (e.kind === "nonempty")
@@ -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);
@@ -9775,9 +9776,15 @@ export class Runtime {
9775
9776
  }
9776
9777
  return false;
9777
9778
  }
9778
- canSyncConditional(expr, depth = 0, rawState) {
9779
+ canSyncConditional(expr, depth = 0, rawState, requireTotal = false) {
9779
9780
  if (depth > 8)
9780
9781
  return false;
9782
+ // Loops and pure substitutions cannot replay after an unsupported result.
9783
+ // These operators depend on arithmetic, filesystem or collation state that
9784
+ // the speculative synchronous evaluator does not fully support.
9785
+ if (requireTotal && ((expr.kind === "unary" && syncConditionalFileUnaryOps.has(expr.operator)) ||
9786
+ (expr.kind === "binary" && expr.operator !== "==" && expr.operator !== "=" && expr.operator !== "!=" && expr.operator !== "=~")))
9787
+ return false;
9781
9788
  const okWord = (w) => !Runtime.wordMayMutate(w) || (rawState !== undefined && this.isPureSyncValueWord(w, rawState));
9782
9789
  if (expr.kind === "nonempty")
9783
9790
  return okWord(expr.operand);
@@ -9800,9 +9807,9 @@ export class Runtime {
9800
9807
  return (expr.operator === "<" || expr.operator === ">" || expr.operator === "-eq" || expr.operator === "-ne" || expr.operator === "-lt" || expr.operator === "-le" || expr.operator === "-gt" || expr.operator === "-ge");
9801
9808
  }
9802
9809
  if (expr.kind === "not")
9803
- return this.canSyncConditional(expr.operand, depth + 1, rawState);
9810
+ return this.canSyncConditional(expr.operand, depth + 1, rawState, requireTotal);
9804
9811
  if (expr.kind === "and" || expr.kind === "or")
9805
- return this.canSyncConditional(expr.left, depth + 1, rawState) && this.canSyncConditional(expr.right, depth + 1, rawState);
9812
+ return this.canSyncConditional(expr.left, depth + 1, rawState, requireTotal) && this.canSyncConditional(expr.right, depth + 1, rawState, requireTotal);
9806
9813
  return false;
9807
9814
  }
9808
9815
  tryEvalFastMemoryFileUnary(op, rawVal, rawState) {
@@ -13069,6 +13076,7 @@ export class Runtime {
13069
13076
  positionalSetVersion,
13070
13077
  savedPositionals: resumeSavedPos,
13071
13078
  callerLoopDepth,
13079
+ lastArgument: fnArgs.at(-1) ?? w0Plain,
13072
13080
  listIndex: bodyRes.listIndex,
13073
13081
  pipelineIndex: bodyRes.pipelineIndex,
13074
13082
  });
@@ -13133,6 +13141,7 @@ export class Runtime {
13133
13141
  this.setSyncPipeStatusCell(activeExisting, statusStr);
13134
13142
  activeStore.changed(monitor.chargeInternal(syncPipeStatusCharge, syncPipeStatusTickets), "PIPESTATUS");
13135
13143
  }
13144
+ rawState.lastArgument = fnArgs.at(-1) ?? w0Plain;
13136
13145
  rawState.status = exitStatus;
13137
13146
  if (exitStatus !== 0 && !ignored && rawState.errexit)
13138
13147
  throw new Flow("exit", exitStatus);
@@ -13730,25 +13739,12 @@ export class Runtime {
13730
13739
  if (list.terminator || list.pipelines.length !== 1)
13731
13740
  return false;
13732
13741
  const p = list.pipelines[0];
13733
- if (p.negate || p.commands.length < 1 || p.commands.length > 5)
13742
+ // Pipeline buffers can overflow after variable or stage expansion.
13743
+ // A synchronous loop cannot resume after earlier body effects, so keep
13744
+ // bounded pipelines on the normal substitution execution path.
13745
+ if (p.negate || p.commands.length !== 1)
13734
13746
  return false;
13735
13747
  let cmd = p.commands[0];
13736
- const hasSingleStdinRedir = cmd.kind === "simple" &&
13737
- cmd.redirects.length === 1 &&
13738
- cmd.redirects[0].operator === "<" &&
13739
- (cmd.redirects[0].descriptor === undefined || cmd.redirects[0].descriptor === 0) &&
13740
- !cmd.redirects[0].move &&
13741
- !cmd.redirects[0].document &&
13742
- cmd.redirects[0].target.plain !== undefined &&
13743
- !cmd.redirects[0].target.plain.startsWith("-") &&
13744
- cmd.redirects[0].target.plain !== "/dev/stdin";
13745
- const hasSingleHereStringRedir = cmd.kind === "simple" &&
13746
- cmd.redirects.length === 1 &&
13747
- cmd.redirects[0].operator === "<<<" &&
13748
- (cmd.redirects[0].descriptor === undefined || cmd.redirects[0].descriptor === 0) &&
13749
- !cmd.redirects[0].move &&
13750
- !cmd.redirects[0].document &&
13751
- this.isPureSyncValueWord(cmd.redirects[0].target, rawState);
13752
13748
  // Loop substitutions require a real subshell: header admission can
13753
13749
  // decline at runtime, and induction writes also change export attributes.
13754
13750
  if (cmd.kind === "for" || cmd.kind === "arithmetic-for" || cmd.kind === "while")
@@ -13756,7 +13752,7 @@ export class Runtime {
13756
13752
  if (p.commands.length === 1 && (cmd.kind === "if" || cmd.kind === "case") && this.isPureSyncSubIfOrCase(cmd, rawState)) {
13757
13753
  continue;
13758
13754
  }
13759
- if (cmd.kind !== "simple" || (cmd.redirects.length > 0 && !hasSingleStdinRedir && !hasSingleHereStringRedir) || cmd.words.length === 0)
13755
+ if (cmd.kind !== "simple" || cmd.redirects.length > 0 || cmd.words.length === 0)
13760
13756
  return false;
13761
13757
  let w0Plain = cmd.words[0].plain;
13762
13758
  // Range-aware tr and Buffer-free 76-col base64 are handled below.
@@ -13771,10 +13767,10 @@ export class Runtime {
13771
13767
  fnBody.body.lists[0].pipelines[0].commands.length === 1 &&
13772
13768
  fnBody.body.lists[0].pipelines[0].commands[0].kind === "simple" &&
13773
13769
  fnBody.body.lists[0].pipelines[0].commands[0].redirects.length === 0 &&
13774
- cmd.words.slice(1).every(w => this.isPureSyncValueWord(w, rawState))) {
13770
+ 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))))) {
13775
13771
  const innerCmd = fnBody.body.lists[0].pipelines[0].commands[0];
13776
13772
  const innerW0 = innerCmd.words[0]?.plain;
13777
- if (innerW0 && !rawState.functions.has(innerW0) && !rawState.extensions?.builtins.has(innerW0)) {
13773
+ 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)) {
13778
13774
  cmd = innerCmd;
13779
13775
  w0Plain = innerW0;
13780
13776
  }
@@ -13790,143 +13786,23 @@ export class Runtime {
13790
13786
  return false;
13791
13787
  }
13792
13788
  if ((w0Plain === "command" && cmd.words[1]?.plain === "-v") || (w0Plain === "type" && cmd.words[1]?.plain === "-t")) {
13793
- if (p.commands.length === 1 &&
13794
- cmd.words.length === 3 &&
13795
- !rawState.functions.has(w0Plain) &&
13796
- !rawState.extensions?.builtins.has(w0Plain) &&
13797
- this.isPureSyncValueWord(cmd.words[2], rawState) &&
13798
- ((cmd.words[2].plain !== undefined && this.tryResolveSyncDiscovery(w0Plain === "command" ? "name" : "kind", cmd.words[2].plain, rawState) !== undefined) ||
13799
- (this._isMemoryBackingFs && this.backingFs?.capabilities?.permissions !== true))) {
13800
- continue;
13801
- }
13789
+ // PATH entries can change after admission, making discovery decline.
13802
13790
  return false;
13803
13791
  }
13804
- const isFileCommand = w0Plain === "cat" ||
13805
- w0Plain === "head" ||
13806
- w0Plain === "tail" ||
13807
- w0Plain === "jq" ||
13808
- w0Plain === "awk" ||
13809
- w0Plain === "grep" ||
13810
- w0Plain === "sed" ||
13811
- w0Plain === "cut" ||
13812
- w0Plain === "wc" ||
13813
- w0Plain === "sort" ||
13814
- w0Plain === "uniq" ||
13815
- w0Plain === "tr" ||
13816
- (hasSingleHereStringRedir && (w0Plain === "base64" || w0Plain === "rev" || w0Plain === "tac" || w0Plain === "nl" || w0Plain === "paste" || w0Plain === "numfmt"));
13817
- const def = w0Plain ? (this.commands.get(w0Plain) ?? ((w0Plain === "rev" || w0Plain === "tac" || w0Plain === "nl" || w0Plain === "paste" || w0Plain === "jq") ? this.getExternalCommand(w0Plain) : undefined)) : undefined;
13818
- if (!w0Plain || (w0Plain !== "printf" && w0Plain !== "echo" && w0Plain !== "dirname" && w0Plain !== "basename" && w0Plain !== "seq" && !isFileCommand) || !def || rawState.functions.has(w0Plain) || rawState.extensions?.builtins.has(w0Plain))
13819
- return false;
13820
- if ((hasSingleStdinRedir || hasSingleHereStringRedir) && !isFileCommand)
13792
+ // File and here-string evaluators can decline on mutable input. Keep
13793
+ // these on the normal path, which can handle arbitrary input sizes.
13794
+ const def = w0Plain ? this.commands.get(w0Plain) : undefined;
13795
+ if (!w0Plain || (w0Plain !== "printf" && w0Plain !== "echo" && w0Plain !== "dirname" && w0Plain !== "basename" && w0Plain !== "seq") || !def || rawState.functions.has(w0Plain) || rawState.extensions?.builtins.has(w0Plain))
13821
13796
  return false;
13822
- 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))
13797
+ 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))
13823
13798
  return false;
13824
13799
  if (!cmd.words.slice(1).every(w => this.isPureSyncValueWord(w, rawState)))
13825
13800
  return false;
13826
- if (isFileCommand) {
13827
- if (w0Plain === "tr" && !hasSingleStdinRedir && !hasSingleHereStringRedir)
13828
- return false;
13829
- if (w0Plain === "grep" && rawState.errexit)
13830
- return false;
13831
- if (!hasSingleHereStringRedir) {
13832
- const fPlain = hasSingleStdinRedir ? cmd.redirects[0].target.plain : cmd.words[cmd.words.length - 1]?.plain;
13833
- if (!fPlain || fPlain.startsWith("-") || fPlain === "/dev/stdin")
13834
- return false;
13835
- const vCheck = this.tryReadMemoryFileViewSync(resolvePath(rawState.cwd, fPlain), false);
13836
- if (!vCheck || vCheck.byteLength > 16384 || vCheck.includes(0))
13837
- return false;
13838
- }
13839
- const opWords = (hasSingleStdinRedir || hasSingleHereStringRedir) ? cmd.words.slice(1) : cmd.words.slice(1, -1);
13840
- const opCount = opWords.length;
13841
- const op0 = opWords[0]?.plain;
13842
- const op1 = opWords[1]?.plain;
13843
- if (w0Plain === "cat" || w0Plain === "rev" || w0Plain === "tac") {
13844
- if (opCount !== 0)
13845
- return false;
13846
- }
13847
- else {
13848
- if (p.commands.length !== 1 && !hasSingleHereStringRedir)
13849
- return false;
13850
- if (w0Plain === "head" || w0Plain === "tail") {
13851
- const validHT = opCount === 0 ||
13852
- (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)))) ||
13853
- (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))));
13854
- if (!validHT)
13855
- return false;
13856
- }
13857
- const plainOps = [];
13858
- for (let oi = 0; oi < opCount; oi++) {
13859
- const ow = opWords[oi];
13860
- const pw = ow.plain ?? (ow.parts.length > 0 && ow.parts.every(pt => pt.kind === "text") ? ow.parts.map(pt => pt.value).join("") : undefined);
13861
- if (pw === undefined)
13862
- break;
13863
- plainOps.push(pw);
13864
- }
13865
- if (w0Plain === "cut") {
13866
- // Probe non-ASCII input: loop data can change after admission.
13867
- if (plainOps.length !== opCount || this.evalSyncCut(["é"], plainOps, byteLocale(rawState.variables)) === undefined)
13868
- return false;
13869
- }
13870
- else if (w0Plain === "sed") {
13871
- if (plainOps.length !== opCount || this.evalSyncSed([], plainOps) === undefined)
13872
- return false;
13873
- }
13874
- else if (w0Plain === "grep") {
13875
- if (plainOps.length !== opCount || this.evalSyncGrep([], plainOps, Boolean(rawState.errexit)) === undefined)
13876
- return false;
13877
- }
13878
- else if (w0Plain === "awk") {
13879
- const input = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState);
13880
- if (plainOps.length !== opCount || input === undefined)
13881
- return false;
13882
- const lines = input.length === 0 ? [] : input.split("\n");
13883
- if (input.endsWith("\n"))
13884
- lines.pop();
13885
- if (this.evalSyncAwk(lines, plainOps) === undefined)
13886
- return false;
13887
- }
13888
- else if (w0Plain === "jq") {
13889
- if (plainOps.length !== opCount)
13890
- return false;
13891
- const jqIn = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState)?.trim();
13892
- if (jqIn === undefined || this.evalSyncJq(jqIn, plainOps) === undefined)
13893
- return false;
13894
- }
13895
- else if (w0Plain === "wc") {
13896
- if (opCount !== 1 || this.normalizeSyncWcFlag(op0) === undefined)
13897
- return false;
13898
- }
13899
- else if (w0Plain === "sort") {
13900
- if (plainOps.length !== opCount || this.evalSyncSort([], plainOps, byteLocale(rawState.variables)) === undefined)
13901
- return false;
13902
- }
13903
- else if (w0Plain === "uniq") {
13904
- if (plainOps.length !== opCount || this.evalSyncUniq([], plainOps) === undefined)
13905
- return false;
13906
- }
13907
- else if (w0Plain === "base64") {
13908
- if (opCount > 1 || (opCount === 1 && op0 !== "-d" && op0 !== "--decode"))
13909
- return false;
13910
- }
13911
- else if (w0Plain === "tr") {
13912
- if (plainOps.length !== opCount || this.evalSyncTr("", plainOps) === undefined)
13913
- return false;
13914
- }
13915
- else if (w0Plain === "nl") {
13916
- if (plainOps.length !== opCount || this.evalSyncNl([], plainOps) === undefined)
13917
- return false;
13918
- }
13919
- else if (w0Plain === "paste") {
13920
- if (plainOps.length !== opCount || this.evalSyncPaste([], plainOps) === undefined)
13921
- return false;
13922
- }
13923
- else if (w0Plain === "numfmt") {
13924
- const nmCheck = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState)?.trim();
13925
- if (nmCheck === undefined || plainOps.length !== opCount || this.evalSyncNumfmt(nmCheck.split("\n"), plainOps) === undefined)
13926
- return false;
13927
- }
13928
- }
13929
- }
13801
+ // These substitutions consume one value per word. Unquoted expansions
13802
+ // can split, glob, or disappear, including after loop admission.
13803
+ if ((w0Plain === "echo" || w0Plain === "basename" || w0Plain === "dirname") &&
13804
+ !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)))))
13805
+ return false;
13930
13806
  if (w0Plain === "printf") {
13931
13807
  if (!this.isSyncPrintfCallOk(cmd, 1, rawState, this._activePrintfInductionName))
13932
13808
  return false;
@@ -13966,138 +13842,18 @@ export class Runtime {
13966
13842
  return false;
13967
13843
  }
13968
13844
  else if (w0Plain === "seq") {
13969
- const isSeqIntWord = (w) => {
13970
- if (w.plain !== undefined)
13971
- return /^-?[0-9]{1,7}$/.test(w.plain);
13972
- if (w.parts.length === 1) {
13973
- const p0 = w.parts[0];
13974
- if (p0.kind === "arithmetic")
13975
- return true;
13976
- if (p0.kind === "variable" && !p0.indirect && !p0.prefixNames && !p0.substring && !p0.transform && p0.operator === undefined && getArraySelector(p0) === undefined) {
13977
- const cur = rawState.variables[p0.name];
13978
- return cur !== undefined && /^-?[0-9]{1,7}$/.test(cur);
13979
- }
13980
- }
13845
+ // Literal operands cannot change after admission. Arithmetic and
13846
+ // variables use the normal path, where seq can report invalid input.
13847
+ if (cmd.words.length < 2 || cmd.words.length > 4)
13981
13848
  return false;
13982
- };
13983
- if (cmd.words.length < 2 || cmd.words.length > 4 || !cmd.words.slice(1).every(isSeqIntWord))
13849
+ const operands = cmd.words.slice(1).map(w => w.plain);
13850
+ if (!operands.every(value => value !== undefined && Number.isInteger(Number(value)) && String(Number(value)) === value && Math.abs(Number(value)) <= 9999999))
13984
13851
  return false;
13985
- }
13986
- if (p.commands.length >= 2) {
13987
- if ((w0Plain !== "printf" && w0Plain !== "echo" && w0Plain !== "seq" && w0Plain !== "cat" && !hasSingleHereStringRedir) || syncPurePipelineSlotInUse || !this.budget.canSyncPurePipe || rawState.errexit || rawState.nounset)
13852
+ const first = operands.length === 1 ? 1 : Number(operands[0]);
13853
+ const increment = operands.length === 3 ? Number(operands[1]) : 1;
13854
+ const last = Number(operands[operands.length - 1]);
13855
+ if (increment === 0 || Math.abs((last - first) / increment) > 1024)
13988
13856
  return false;
13989
- if (w0Plain === "printf") {
13990
- if (!this.isSyncPrintfCallOk(cmd, 1, rawState, this._activePrintfInductionName))
13991
- return false;
13992
- }
13993
- for (let sIdx = 1; sIdx < p.commands.length; sIdx++) {
13994
- const sCmd = p.commands[sIdx];
13995
- if (sCmd.kind !== "simple" || sCmd.redirects.length !== 0 || sCmd.words.length === 0)
13996
- return false;
13997
- const sName = sCmd.words[0].plain;
13998
- // Range-aware tr and Buffer-free 76-col base64 are handled in pipeline stages.
13999
- 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"))
14000
- return false;
14001
- const sDef = this.commands.get(sName) ?? ((sName === "rev" || sName === "tac" || sName === "nl" || sName === "paste" || sName === "jq") ? this.getExternalCommand(sName) : undefined);
14002
- if (!sDef || customRegisteredCommands.has(sDef.execute) || (sName !== "rev" && sName !== "tac" && sName !== "nl" && sName !== "paste" && sName !== "jq" && !builtInDirectContextExecutors.has(sDef.execute)))
14003
- return false;
14004
- if (rawState.functions.has(sName) || rawState.extensions?.builtins.has(sName))
14005
- return false;
14006
- const sArgWords = sCmd.words.slice(1);
14007
- const sPlainArgs = [];
14008
- let allPlainStageArgs = true;
14009
- for (let k = 0; k < sArgWords.length; k++) {
14010
- const sw = sArgWords[k];
14011
- const sp = sw.plain ?? (sw.parts.length > 0 && sw.parts.every(pt => pt.kind === "text") ? sw.parts.map(pt => pt.value).join("") : undefined);
14012
- if (sp === undefined) {
14013
- allPlainStageArgs = false;
14014
- break;
14015
- }
14016
- sPlainArgs.push(sp);
14017
- }
14018
- if (!allPlainStageArgs)
14019
- return false;
14020
- if (sName === "sort") {
14021
- if (this.evalSyncSort([], sPlainArgs, byteLocale(rawState.variables)) === undefined)
14022
- return false;
14023
- }
14024
- else if (sName === "uniq") {
14025
- if (this.evalSyncUniq([], sPlainArgs) === undefined)
14026
- return false;
14027
- }
14028
- else if (sName === "wc") {
14029
- if (sPlainArgs.length !== 1 || this.normalizeSyncWcFlag(sPlainArgs[0]) === undefined)
14030
- return false;
14031
- }
14032
- else if (sName === "head" || sName === "tail") {
14033
- 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]))));
14034
- if (!okHT)
14035
- return false;
14036
- }
14037
- else if (sName === "cut") {
14038
- // Byte cuts may reject non-ASCII data on a later iteration.
14039
- if (this.evalSyncCut(["é"], sPlainArgs, byteLocale(rawState.variables)) === undefined)
14040
- return false;
14041
- }
14042
- else if (sName === "sed") {
14043
- if (this.evalSyncSed([], sPlainArgs) === undefined)
14044
- return false;
14045
- }
14046
- else if (sName === "awk") {
14047
- if (sIdx !== 1)
14048
- return false;
14049
- const input = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState);
14050
- if (input === undefined)
14051
- return false;
14052
- const lines = input.length === 0 ? [] : input.split("\n");
14053
- if (input.endsWith("\n"))
14054
- lines.pop();
14055
- if (this.evalSyncAwk(lines, sPlainArgs) === undefined)
14056
- return false;
14057
- }
14058
- else if (sName === "grep") {
14059
- if (this.evalSyncGrep([], sPlainArgs, Boolean(rawState.errexit)) === undefined)
14060
- return false;
14061
- }
14062
- else if (sName === "tr") {
14063
- if (this.evalSyncTr("", sPlainArgs) === undefined)
14064
- return false;
14065
- }
14066
- else if (sName === "jq") {
14067
- if (sIdx !== 1)
14068
- return false;
14069
- const jqIn = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState)?.trim();
14070
- if (jqIn === undefined || this.evalSyncJq(jqIn, sPlainArgs) === undefined)
14071
- return false;
14072
- }
14073
- else if (sName === "base64") {
14074
- if (sPlainArgs.length !== 0)
14075
- return false;
14076
- }
14077
- else if (sName === "tac") {
14078
- if (sPlainArgs.length !== 0 || (w0Plain !== "echo" && !hasSingleHereStringRedir))
14079
- return false;
14080
- }
14081
- else if (sName === "nl") {
14082
- if ((w0Plain !== "echo" && !hasSingleHereStringRedir) || this.evalSyncNl([], sPlainArgs) === undefined)
14083
- return false;
14084
- }
14085
- else if (sName === "paste") {
14086
- if (this.evalSyncPaste([], sPlainArgs) === undefined)
14087
- return false;
14088
- }
14089
- else if (sName === "numfmt") {
14090
- if (sIdx !== 1)
14091
- return false;
14092
- const nmCheck = this.extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState)?.trim();
14093
- if (nmCheck === undefined || this.evalSyncNumfmt(nmCheck.split("\n"), sPlainArgs) === undefined)
14094
- return false;
14095
- }
14096
- else if (sName === "rev") {
14097
- if (sPlainArgs.length !== 0)
14098
- return false;
14099
- }
14100
- }
14101
13857
  }
14102
13858
  continue;
14103
13859
  }
@@ -14583,7 +14339,7 @@ export class Runtime {
14583
14339
  return false;
14584
14340
  }
14585
14341
  else if (bc.kind === "conditional") {
14586
- if (bc.redirects.length !== 0 || rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(bc.expression, 0, rawState))
14342
+ if (bc.redirects.length !== 0 || rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(bc.expression, 0, rawState, true))
14587
14343
  return false;
14588
14344
  if (bc.expression.kind === "binary" && bc.expression.operator === "=~")
14589
14345
  return false;
@@ -14625,7 +14381,7 @@ export class Runtime {
14625
14381
  return false;
14626
14382
  const cmd = pipeline.commands[0];
14627
14383
  const canSyncLoopCondCmd = (c) => {
14628
- if (c.redirects.length !== 0 || rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(c.expression, 0, rawState))
14384
+ if (c.redirects.length !== 0 || rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(c.expression, 0, rawState, true))
14629
14385
  return false;
14630
14386
  const checkLeaf = (e) => {
14631
14387
  if (e.kind === "nonempty")
@@ -14640,7 +14396,8 @@ export class Runtime {
14640
14396
  return false;
14641
14397
  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");
14642
14398
  }
14643
- return this.isPureSyncValueWord(e.left, rawState) && (e.operator === "==" || e.operator === "=" || e.operator === "!=" ? this.canSyncPatternWordParts(e.right, rawState) : this.isPureSyncValueWord(e.right, rawState));
14399
+ // A synchronous loop cannot hand off an unsupported pattern mid-iteration.
14400
+ 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)));
14644
14401
  }
14645
14402
  if (e.kind === "not")
14646
14403
  return checkLeaf(e.operand);
@@ -14801,6 +14558,8 @@ export class Runtime {
14801
14558
  // Nested step execution does not implement redirects; use normal execution.
14802
14559
  if (nested && cmd.redirects.length !== 0)
14803
14560
  return false;
14561
+ if (cmd.words.length === 0)
14562
+ return false;
14804
14563
  if (cmd.redirects.length === 1) {
14805
14564
  const r0Check = cmd.redirects[0];
14806
14565
  const w0DevPlain = cmd.words[0].plain;
@@ -15474,22 +15233,13 @@ export class Runtime {
15474
15233
  ? whilePrintfInd
15475
15234
  : (forPrintfInd && this.canSyncPrintfInductionBody(command.body, rawState, forPrintfInd) ? forPrintfInd : undefined));
15476
15235
  const prevActivePrintfInd = this._activePrintfInductionName;
15477
- const prevActiveLoopScript = this._activeLoopBodyScript;
15478
- const prevActiveLoopCond = this._activeLoopCondScript;
15479
- const prevActiveLoopVar = this._activeLoopVarName;
15480
15236
  this._activePrintfInductionName = printfInductionName;
15481
- this._activeLoopBodyScript = command.body;
15482
- this._activeLoopCondScript = command.kind === "while" ? command.condition : undefined;
15483
- this._activeLoopVarName = command.kind === "for" ? command.name : (printfInductionName ?? (whilePrintfInd || undefined));
15484
15237
  let bodySyncOk = false;
15485
15238
  try {
15486
15239
  bodySyncOk = !(guestArrays(rawState) && (guestArrays(rawState).watches.size > 0 || this.budget.limits.maxExpansionBytes < 65536)) && this.canSyncLoopBody(command.body, rawState, io, false, printfInductionName);
15487
15240
  }
15488
15241
  finally {
15489
15242
  this._activePrintfInductionName = prevActivePrintfInd;
15490
- this._activeLoopBodyScript = prevActiveLoopScript;
15491
- this._activeLoopCondScript = prevActiveLoopCond;
15492
- this._activeLoopVarName = prevActiveLoopVar;
15493
15243
  }
15494
15244
  if (!bodySyncOk) {
15495
15245
  command._skipTrySyncLoop = true;
@@ -16074,7 +15824,7 @@ export class Runtime {
16074
15824
  whileArithProg = condCmd.expression;
16075
15825
  }
16076
15826
  else if (condCmd.kind === "conditional") {
16077
- if (condCmd.redirects.length !== 0 || rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(condCmd.expression, 0, rawState))
15827
+ if (condCmd.redirects.length !== 0 || rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(condCmd.expression, 0, rawState, true))
16078
15828
  return undefined;
16079
15829
  const checkWhileCond = (e, depth) => {
16080
15830
  if (e.kind === "nonempty")
@@ -16095,7 +15845,7 @@ export class Runtime {
16095
15845
  !monitor.hasOverlay("BASH_REMATCH") &&
16096
15846
  !(store ?? requireArrays(rawState))?.watches.has("BASH_REMATCH"));
16097
15847
  }
16098
- return this.isPureSyncValueWord(e.left, rawState) && (e.operator === "==" || e.operator === "=" || e.operator === "!=" ? this.canSyncPatternWordParts(e.right, rawState) : this.isPureSyncValueWord(e.right, rawState));
15848
+ 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)));
16099
15849
  }
16100
15850
  if (e.kind === "not")
16101
15851
  return checkWhileCond(e.operand, depth + 1);
@@ -16426,7 +16176,7 @@ export class Runtime {
16426
16176
  store.epoch = restEpoch;
16427
16177
  return loopStatus;
16428
16178
  }
16429
- 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) {
16179
+ 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) {
16430
16180
  return undefined;
16431
16181
  }
16432
16182
  const activeValScope = io[valueScope];
@@ -16925,187 +16675,6 @@ export class Runtime {
16925
16675
  return pos <= 128 ? fastRedirectScratchViews[pos] : fastRedirectScratchBytes.subarray(0, pos);
16926
16676
  }
16927
16677
  _activePrintfInductionName = undefined;
16928
- _activeLoopBodyScript = undefined;
16929
- _activeLoopCondScript = undefined;
16930
- _activeLoopVarName = undefined;
16931
- isVarUnmutatedInScript(varName, script) {
16932
- for (const list of script.lists) {
16933
- for (const pipeline of list.pipelines) {
16934
- for (const cmd of pipeline.commands) {
16935
- if (cmd.kind === "arithmetic") {
16936
- if (cmd.expression.hasMutation && cmd.expression.source.includes(varName))
16937
- return false;
16938
- continue;
16939
- }
16940
- if (cmd.kind === "if") {
16941
- for (const br of cmd.branches) {
16942
- if (!this.isVarUnmutatedInScript(varName, br.condition) || !this.isVarUnmutatedInScript(varName, br.body))
16943
- return false;
16944
- }
16945
- if (cmd.otherwise && !this.isVarUnmutatedInScript(varName, cmd.otherwise))
16946
- return false;
16947
- continue;
16948
- }
16949
- if (cmd.kind === "case") {
16950
- for (const cl of cmd.clauses) {
16951
- if (!this.isVarUnmutatedInScript(varName, cl.body))
16952
- return false;
16953
- }
16954
- continue;
16955
- }
16956
- if (cmd.kind === "for") {
16957
- if (cmd.name === varName || !this.isVarUnmutatedInScript(varName, cmd.body))
16958
- return false;
16959
- continue;
16960
- }
16961
- if (cmd.kind === "arithmetic-for") {
16962
- if (cmd.expressions.some(e => e && e.hasMutation && e.source.includes(varName)) || !this.isVarUnmutatedInScript(varName, cmd.body))
16963
- return false;
16964
- continue;
16965
- }
16966
- if (cmd.kind === "while") {
16967
- if (!this.isVarUnmutatedInScript(varName, cmd.condition) || !this.isVarUnmutatedInScript(varName, cmd.body))
16968
- return false;
16969
- continue;
16970
- }
16971
- if (cmd.kind === "group" || cmd.kind === "subshell") {
16972
- if (!this.isVarUnmutatedInScript(varName, cmd.body))
16973
- return false;
16974
- continue;
16975
- }
16976
- if (cmd.kind !== "simple")
16977
- return false;
16978
- for (const w of cmd.words) {
16979
- const arrAss = getArrayAssignment(w);
16980
- if (arrAss && arrAss.name === varName)
16981
- return false;
16982
- const p0 = w.parts[0];
16983
- if (p0?.kind === "text" && !p0.quoted && (p0.value.startsWith(varName + "=") || p0.value.startsWith(varName + "+=") || p0.value.startsWith(varName + "[")))
16984
- return false;
16985
- }
16986
- const w0 = cmd.words[0]?.plain;
16987
- if (w0 === "read" || w0 === "unset" || w0 === "declare" || w0 === "typeset" || w0 === "local" || w0 === "export" || w0 === "mapfile" || w0 === "readarray") {
16988
- if (w0 === "read" && varName === "REPLY")
16989
- return false;
16990
- if (cmd.words.slice(1).some(w => (w.plain ?? "").includes(varName)))
16991
- return false;
16992
- }
16993
- if (w0 === "printf" && cmd.words[1]?.plain === "-v" && (cmd.words[2]?.plain ?? "").startsWith(varName))
16994
- return false;
16995
- for (const w of [...cmd.words, ...cmd.redirects.map(r => r.target)]) {
16996
- for (const pt of w.parts) {
16997
- if (pt.kind === "arithmetic" && pt.expression.hasMutation && pt.expression.source.includes(varName))
16998
- return false;
16999
- if (pt.kind === "substitution" && !this.isVarUnmutatedInScript(varName, pt.script))
17000
- return false;
17001
- }
17002
- }
17003
- }
17004
- }
17005
- }
17006
- return true;
17007
- }
17008
- isVarUnmutatedInActiveLoop(varName) {
17009
- if (varName === this._activeLoopVarName || varName === this._activePrintfInductionName)
17010
- return false;
17011
- if (this._activeLoopCondScript && !this.isVarUnmutatedInScript(varName, this._activeLoopCondScript))
17012
- return false;
17013
- if (this._activeLoopBodyScript && !this.isVarUnmutatedInScript(varName, this._activeLoopBodyScript))
17014
- return false;
17015
- return true;
17016
- }
17017
- isFileReadOnlyScript(script, rawState) {
17018
- const visit = (value) => {
17019
- if (value === null || typeof value !== "object")
17020
- return true;
17021
- if (Array.isArray(value))
17022
- return value.every(visit);
17023
- const node = value;
17024
- if (typeof node.operator === "string" && "target" in node && !["<", "<<<", "<<", "<<-"].includes(node.operator))
17025
- return false;
17026
- if (node.kind === "simple") {
17027
- const cmd = value;
17028
- // Unknown commands (including shell functions) cannot promise read-only I/O.
17029
- const name = cmd.words[0]?.plain;
17030
- if (name === undefined && !cmd.words.every(word => this.assignment(word) || getArrayAssignment(word)))
17031
- return false;
17032
- if (name !== undefined && (hasShellFunction(rawState, name) || !["jq", "cat", "echo", "printf", "test", "[", ":", "true", "false"].includes(name)))
17033
- return false;
17034
- }
17035
- return Object.values(node).every(visit);
17036
- };
17037
- return visit(script);
17038
- }
17039
- extractLoopInvariantInput(cmd, hasSingleStdinRedir, hasSingleHereStringRedir, rawState) {
17040
- const w0 = cmd.words[0]?.plain;
17041
- const resolveInvariantWord = (w) => {
17042
- if (w.plain !== undefined)
17043
- return w.plain;
17044
- if (w.parts.length > 0 && w.parts.every(pt => pt.kind === "text"))
17045
- return w.parts.map(pt => pt.value).join("");
17046
- if (w.parts.length === 1 && w.parts[0].kind === "variable") {
17047
- const vp = w.parts[0];
17048
- if (!vp.indirect && !vp.operator && !vp.substring && !vp.transform && !vp.length && !vp.prefixNames && getArraySelector(vp) === undefined && this.isVarUnmutatedInActiveLoop(vp.name)) {
17049
- const val = rawState.variables[vp.name];
17050
- if (typeof val === "string")
17051
- return val;
17052
- }
17053
- }
17054
- return undefined;
17055
- };
17056
- if (w0 === "jq" || w0 === "numfmt" || w0 === "awk") {
17057
- if (hasSingleHereStringRedir) {
17058
- const resolved = resolveInvariantWord(cmd.redirects[0].target);
17059
- return resolved !== undefined ? resolved + "\n" : undefined;
17060
- }
17061
- if (this._fileWrites?.size || this._outputFiles?.size)
17062
- return undefined;
17063
- if ([this._activeLoopBodyScript, this._activeLoopCondScript].some(script => script && !this.isFileReadOnlyScript(script, rawState)))
17064
- return undefined;
17065
- const fPlain = hasSingleStdinRedir ? cmd.redirects[0].target.plain : cmd.words[cmd.words.length - 1]?.plain;
17066
- if (!fPlain || fPlain.startsWith("-") || fPlain === "/dev/stdin")
17067
- return undefined;
17068
- const vCheck = this.tryReadMemoryFileViewSync(resolvePath(rawState.cwd, fPlain), false);
17069
- if (!vCheck || vCheck.byteLength > 16384 || vCheck.includes(0))
17070
- return undefined;
17071
- return sharedSyncPipeDecoder.decode(vCheck);
17072
- }
17073
- if (w0 === "cat") {
17074
- if (hasSingleHereStringRedir) {
17075
- const resolved = resolveInvariantWord(cmd.redirects[0].target);
17076
- return resolved !== undefined ? resolved + "\n" : undefined;
17077
- }
17078
- if (this._fileWrites?.size || this._outputFiles?.size)
17079
- return undefined;
17080
- if ([this._activeLoopBodyScript, this._activeLoopCondScript].some(script => script && !this.isFileReadOnlyScript(script, rawState)))
17081
- return undefined;
17082
- const fPlain = hasSingleStdinRedir ? cmd.redirects[0].target.plain : cmd.words[1]?.plain;
17083
- if (!fPlain || fPlain.startsWith("-") || fPlain === "/dev/stdin")
17084
- return undefined;
17085
- const vCheck = this.tryReadMemoryFileViewSync(resolvePath(rawState.cwd, fPlain), false);
17086
- if (!vCheck || vCheck.byteLength > 16384 || vCheck.includes(0))
17087
- return undefined;
17088
- return sharedSyncPipeDecoder.decode(vCheck);
17089
- }
17090
- if (w0 === "echo" && cmd.words.length === 2) {
17091
- const resolved = resolveInvariantWord(cmd.words[1]);
17092
- if (resolved !== undefined && !resolved.startsWith("-"))
17093
- return resolved + "\n";
17094
- return undefined;
17095
- }
17096
- if (w0 === "printf" && cmd.words.length >= 2) {
17097
- const args = [];
17098
- for (let i = 1; i < cmd.words.length; i++) {
17099
- const r = resolveInvariantWord(cmd.words[i]);
17100
- if (r === undefined)
17101
- return undefined;
17102
- args.push(r);
17103
- }
17104
- const formatted = tryFastPrintf(args);
17105
- return formatted;
17106
- }
17107
- return undefined;
17108
- }
17109
16678
  canSyncPrintfInductionBody(script, rawState, inductionName) {
17110
16679
  for (const list of script.lists)
17111
16680
  for (const pipeline of list.pipelines)
@@ -17800,6 +17369,10 @@ export class Runtime {
17800
17369
  return check(tree);
17801
17370
  }
17802
17371
  canSyncErrorArithmeticStmt(expr, rawState) {
17372
+ // Expansion can introduce array, attributed, or control-variable references.
17373
+ // Their reads and writes require the full arithmetic evaluator.
17374
+ if (expr.source.includes("$"))
17375
+ return false;
17803
17376
  if (expr.source.includes("/") || expr.source.includes("%") || expr.source.includes("<<") || expr.source.includes(">>") || expr.source.includes("**") || expr.source.includes("["))
17804
17377
  return false;
17805
17378
  try {
@@ -17962,19 +17535,34 @@ export class Runtime {
17962
17535
  const prevFastSubPos = this._fastSubPositional;
17963
17536
  rawState.positional = fnArgs;
17964
17537
  this._fastSubPositional = fnArgs;
17965
- const savedLocals = new Array(fnCall.locals.length);
17538
+ const savedLocals = [];
17539
+ const seenLocals = new Set();
17966
17540
  try {
17967
- for (let i = 0; i < fnCall.locals.length; i++) {
17968
- const loc = fnCall.locals[i];
17969
- if (loc.isFirstInCmd)
17970
- this.budget.tick();
17971
- savedLocals[i] = { name: loc.name, val: rawState.variables[loc.name], wasTouched: touched.has(loc.name) };
17972
- if (loc.valueWord !== undefined) {
17973
- rawState.variables[loc.name] = this.fastValueWord(loc.valueWord, rawState, io, false, false, false, false, 0, step.line);
17974
- touched.add(loc.name);
17975
- }
17976
- else {
17977
- delete rawState.variables[loc.name];
17541
+ for (let i = 0; i < fnCall.locals.length;) {
17542
+ this.budget.tick();
17543
+ let end = i + 1;
17544
+ while (end < fnCall.locals.length && !fnCall.locals[end].isFirstInCmd)
17545
+ end++;
17546
+ // Expand the entire simple command before any of its bindings change.
17547
+ const values = fnCall.locals.slice(i, end).map(loc => loc.valueWord === undefined
17548
+ ? undefined
17549
+ : this.fastValueWord(loc.valueWord, rawState, io, false, false, false, false, 0, step.line));
17550
+ const start = i;
17551
+ for (; i < end; i++) {
17552
+ const loc = fnCall.locals[i];
17553
+ const alreadyLocal = seenLocals.has(loc.name);
17554
+ if (!alreadyLocal) {
17555
+ savedLocals.push({ name: loc.name, val: rawState.variables[loc.name], wasTouched: touched.has(loc.name) });
17556
+ seenLocals.add(loc.name);
17557
+ }
17558
+ const value = values[i - start];
17559
+ if (value !== undefined) {
17560
+ rawState.variables[loc.name] = value;
17561
+ touched.add(loc.name);
17562
+ }
17563
+ else if (!alreadyLocal) {
17564
+ delete rawState.variables[loc.name];
17565
+ }
17978
17566
  }
17979
17567
  }
17980
17568
  if (fnCall.steps.length > 0) {
@@ -17982,11 +17570,12 @@ export class Runtime {
17982
17570
  }
17983
17571
  else {
17984
17572
  rawState.status = 0;
17985
- onUpdate({ lastCmd: step.cmd, lastArg: fnArgs.length > 0 ? fnArgs[fnArgs.length - 1] : fnCall.name });
17986
17573
  }
17574
+ rawState.lastArgument = fnArgs.at(-1) ?? fnCall.name;
17575
+ onUpdate({ lastCmd: step.cmd, lastArg: rawState.lastArgument });
17987
17576
  }
17988
17577
  finally {
17989
- for (let i = fnCall.locals.length - 1; i >= 0; i--) {
17578
+ for (let i = savedLocals.length - 1; i >= 0; i--) {
17990
17579
  const saved = savedLocals[i];
17991
17580
  if (saved.val !== undefined) {
17992
17581
  rawState.variables[saved.name] = saved.val;
@@ -21276,6 +20865,7 @@ export class Runtime {
21276
20865
  }
21277
20866
  if (hasThrown)
21278
20867
  throw thrownError;
20868
+ state.lastArgument = pendingResume?.lastArgument ?? args.at(-1) ?? name;
21279
20869
  return exitStatus;
21280
20870
  }
21281
20871
  finally {
@@ -21795,6 +21385,7 @@ export class Runtime {
21795
21385
  }
21796
21386
  if (outcome.kind === "throw")
21797
21387
  throw outcome.reason;
21388
+ state.lastArgument = context.args.at(-1) ?? context.command;
21798
21389
  return outcome.value;
21799
21390
  }
21800
21391
  if (selectedKind === "builtin" && !(state.externalInvocation && this.commands.has(context.command))) {
@@ -27156,6 +26747,10 @@ export class Runtime {
27156
26747
  expandSyncTrCharArray(spec) {
27157
26748
  if (spec.length === 0)
27158
26749
  return undefined;
26750
+ // Non-ASCII sets require the byte-oriented tr parser.
26751
+ for (let i = 0; i < spec.length; i++)
26752
+ if (spec.charCodeAt(i) >= 128)
26753
+ return undefined;
27159
26754
  // Escaped endpoints need the tr parser before range expansion.
27160
26755
  if (spec.includes("\\") && spec.includes("-"))
27161
26756
  return undefined;
@@ -27688,10 +27283,10 @@ export class Runtime {
27688
27283
  if (!el || typeof el !== "object" || Array.isArray(el))
27689
27284
  return undefined;
27690
27285
  const rec = el;
27691
- const k = rec.key ?? rec.name ?? rec.k;
27286
+ const k = [rec.key, rec.Key, rec.name, rec.Name].find(value => value !== undefined && value !== null && value !== false);
27692
27287
  if (typeof k !== "string")
27693
27288
  return undefined;
27694
- obj[k] = Object.hasOwn(rec, "value") ? rec.value : (Object.hasOwn(rec, "v") ? rec.v : null);
27289
+ obj[k] = Object.hasOwn(rec, "value") ? rec.value : (Object.hasOwn(rec, "Value") ? rec.Value : null);
27695
27290
  }
27696
27291
  return [obj];
27697
27292
  }
@@ -27724,7 +27319,7 @@ export class Runtime {
27724
27319
  return [item.map(x => x === null ? "" : String(x)).join(arg)];
27725
27320
  }
27726
27321
  if (fn === "split")
27727
- return typeof item === "string" ? [item.split(arg)] : undefined;
27322
+ return typeof item === "string" ? [arg === "" ? Array.from(item) : item.split(arg)] : undefined;
27728
27323
  if (fn === "ltrimstr")
27729
27324
  return typeof item === "string" ? [item.startsWith(arg) ? item.slice(arg.length) : item] : undefined;
27730
27325
  if (fn === "rtrimstr")
@@ -27986,9 +27581,8 @@ export class Runtime {
27986
27581
  break;
27987
27582
  }
27988
27583
  if (!a.startsWith("-") || a === "-") {
27989
- operands.push(a);
27990
- idx++;
27991
- continue;
27584
+ operands.push(...args.slice(idx));
27585
+ break;
27992
27586
  }
27993
27587
  if (a === "-a" || a === "--multiple") {
27994
27588
  multiple = true;
@@ -28370,7 +27964,9 @@ export class Runtime {
28370
27964
  }
28371
27965
  if (st.kind === "=") {
28372
27966
  out.push(String(idx1));
28373
- lastInputIndex = i;
27967
+ // Line numbers are generated records, always terminated, even when
27968
+ // the final input record is partial. Later prints can override this.
27969
+ lastInputIndex = -1;
28374
27970
  continue;
28375
27971
  }
28376
27972
  if (st.kind === "q") {
@@ -28413,8 +28009,10 @@ export class Runtime {
28413
28009
  }
28414
28010
  return replacement;
28415
28011
  });
28416
- if (matchedOnce && st.printOnMatch)
28012
+ if (matchedOnce && st.printOnMatch) {
28417
28013
  out.push(l);
28014
+ lastInputIndex = i;
28015
+ }
28418
28016
  }
28419
28017
  if (quitNow) {
28420
28018
  break;
@@ -29600,22 +29198,16 @@ export class Runtime {
29600
29198
  if (endField !== undefined && endField < startField)
29601
29199
  return undefined;
29602
29200
  const kf = km[3] ?? "";
29603
- if (kf.includes("n"))
29604
- keyNum = true;
29605
- if (kf.includes("h"))
29606
- keyHuman = true;
29607
- if (kf.includes("M"))
29608
- keyMonth = true;
29609
- if (kf.includes("d"))
29610
- keyDict = true;
29611
- if (kf.includes("r"))
29612
- keyRev = true;
29613
- if (kf.includes("f"))
29614
- keyFold = true;
29615
- if (kf.includes("b"))
29616
- keyBlanks = true;
29617
- if (kf.includes("V"))
29618
- keyVer = true;
29201
+ if (kf.length > 0) {
29202
+ keyNum = kf.includes("n");
29203
+ keyHuman = kf.includes("h");
29204
+ keyMonth = kf.includes("M");
29205
+ keyDict = kf.includes("d");
29206
+ keyRev = kf.includes("r");
29207
+ keyFold = kf.includes("f");
29208
+ keyBlanks = kf.includes("b");
29209
+ keyVer = kf.includes("V");
29210
+ }
29619
29211
  }
29620
29212
  const extractKey = (l) => {
29621
29213
  let raw = keySpec === undefined
@@ -29647,7 +29239,14 @@ export class Runtime {
29647
29239
  raw = raw.replace(/^[ \t]+/, "");
29648
29240
  if (keyDict)
29649
29241
  raw = raw.replace(/[^a-zA-Z0-9 \t]+/g, "");
29650
- return keyFold ? raw.toUpperCase() : raw;
29242
+ if (!keyFold)
29243
+ return raw;
29244
+ let folded = "";
29245
+ for (let i = 0; i < raw.length; i++) {
29246
+ const code = raw.charCodeAt(i);
29247
+ folded += code >= 97 && code <= 122 ? String.fromCharCode(code - 32) : raw[i];
29248
+ }
29249
+ return folded;
29651
29250
  };
29652
29251
  const parseNum = (s) => {
29653
29252
  const m = /^[ \t]*(-?(?:\d+(?:\.\d*)?|\.\d+))/.exec(s);
@@ -29680,6 +29279,24 @@ export class Runtime {
29680
29279
  const idx = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"].indexOf(m[1].toUpperCase());
29681
29280
  return idx === -1 ? 0 : idx + 1;
29682
29281
  };
29282
+ const encoded = new Map();
29283
+ const bytesOf = (line) => {
29284
+ let bytes = encoded.get(line);
29285
+ if (bytes === undefined) {
29286
+ bytes = fastSharedTextEncoder.encode(line);
29287
+ encoded.set(line, bytes);
29288
+ }
29289
+ return bytes;
29290
+ };
29291
+ const compareBytes = (left, right) => {
29292
+ const a = bytesOf(left);
29293
+ const b = bytesOf(right);
29294
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
29295
+ if (a[i] !== b[i])
29296
+ return a[i] - b[i];
29297
+ }
29298
+ return a.length - b.length;
29299
+ };
29683
29300
  const sorted = [...rawLines].sort((a, b) => {
29684
29301
  const ka = extractKey(a);
29685
29302
  const kb = extractKey(b);
@@ -29706,15 +29323,13 @@ export class Runtime {
29706
29323
  return keyRev ? -vc : vc;
29707
29324
  }
29708
29325
  else if (ka !== kb) {
29709
- return ka < kb ? (keyRev ? 1 : -1) : (keyRev ? -1 : 1);
29326
+ const comparison = compareBytes(ka, kb);
29327
+ return keyRev ? -comparison : comparison;
29710
29328
  }
29711
29329
  if (uniq || stable)
29712
29330
  return 0;
29713
- const fa = fold ? a.toUpperCase() : a;
29714
- const fb = fold ? b.toUpperCase() : b;
29715
- if (fa !== fb)
29716
- return fa < fb ? (rev ? 1 : -1) : (rev ? -1 : 1);
29717
- return a < b ? (rev ? 1 : -1) : a > b ? (rev ? -1 : 1) : 0;
29331
+ const comparison = compareBytes(a, b);
29332
+ return rev ? -comparison : comparison;
29718
29333
  });
29719
29334
  if (!uniq)
29720
29335
  return sorted;