@poe-platform/safe-bash 0.1.749 → 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.
@@ -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);
@@ -13075,6 +13076,7 @@ export class Runtime {
13075
13076
  positionalSetVersion,
13076
13077
  savedPositionals: resumeSavedPos,
13077
13078
  callerLoopDepth,
13079
+ lastArgument: fnArgs.at(-1) ?? w0Plain,
13078
13080
  listIndex: bodyRes.listIndex,
13079
13081
  pipelineIndex: bodyRes.pipelineIndex,
13080
13082
  });
@@ -13139,6 +13141,7 @@ export class Runtime {
13139
13141
  this.setSyncPipeStatusCell(activeExisting, statusStr);
13140
13142
  activeStore.changed(monitor.chargeInternal(syncPipeStatusCharge, syncPipeStatusTickets), "PIPESTATUS");
13141
13143
  }
13144
+ rawState.lastArgument = fnArgs.at(-1) ?? w0Plain;
13142
13145
  rawState.status = exitStatus;
13143
13146
  if (exitStatus !== 0 && !ignored && rawState.errexit)
13144
13147
  throw new Flow("exit", exitStatus);
@@ -13736,25 +13739,12 @@ export class Runtime {
13736
13739
  if (list.terminator || list.pipelines.length !== 1)
13737
13740
  return false;
13738
13741
  const p = list.pipelines[0];
13739
- 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)
13740
13746
  return false;
13741
13747
  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
13748
  // Loop substitutions require a real subshell: header admission can
13759
13749
  // decline at runtime, and induction writes also change export attributes.
13760
13750
  if (cmd.kind === "for" || cmd.kind === "arithmetic-for" || cmd.kind === "while")
@@ -13762,7 +13752,7 @@ export class Runtime {
13762
13752
  if (p.commands.length === 1 && (cmd.kind === "if" || cmd.kind === "case") && this.isPureSyncSubIfOrCase(cmd, rawState)) {
13763
13753
  continue;
13764
13754
  }
13765
- 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)
13766
13756
  return false;
13767
13757
  let w0Plain = cmd.words[0].plain;
13768
13758
  // Range-aware tr and Buffer-free 76-col base64 are handled below.
@@ -13777,10 +13767,10 @@ export class Runtime {
13777
13767
  fnBody.body.lists[0].pipelines[0].commands.length === 1 &&
13778
13768
  fnBody.body.lists[0].pipelines[0].commands[0].kind === "simple" &&
13779
13769
  fnBody.body.lists[0].pipelines[0].commands[0].redirects.length === 0 &&
13780
- 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))))) {
13781
13771
  const innerCmd = fnBody.body.lists[0].pipelines[0].commands[0];
13782
13772
  const innerW0 = innerCmd.words[0]?.plain;
13783
- 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)) {
13784
13774
  cmd = innerCmd;
13785
13775
  w0Plain = innerW0;
13786
13776
  }
@@ -13796,143 +13786,23 @@ export class Runtime {
13796
13786
  return false;
13797
13787
  }
13798
13788
  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
- }
13789
+ // PATH entries can change after admission, making discovery decline.
13808
13790
  return false;
13809
13791
  }
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))
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))
13825
13796
  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))
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))
13829
13798
  return false;
13830
13799
  if (!cmd.words.slice(1).every(w => this.isPureSyncValueWord(w, rawState)))
13831
13800
  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
- }
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;
13936
13806
  if (w0Plain === "printf") {
13937
13807
  if (!this.isSyncPrintfCallOk(cmd, 1, rawState, this._activePrintfInductionName))
13938
13808
  return false;
@@ -13972,138 +13842,18 @@ export class Runtime {
13972
13842
  return false;
13973
13843
  }
13974
13844
  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
- }
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)
13987
13848
  return false;
13988
- };
13989
- 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))
13990
13851
  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)
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)
13994
13856
  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
13857
  }
14108
13858
  continue;
14109
13859
  }
@@ -14646,7 +14396,8 @@ export class Runtime {
14646
14396
  return false;
14647
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");
14648
14398
  }
14649
- 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)));
14650
14401
  }
14651
14402
  if (e.kind === "not")
14652
14403
  return checkLeaf(e.operand);
@@ -14807,6 +14558,8 @@ export class Runtime {
14807
14558
  // Nested step execution does not implement redirects; use normal execution.
14808
14559
  if (nested && cmd.redirects.length !== 0)
14809
14560
  return false;
14561
+ if (cmd.words.length === 0)
14562
+ return false;
14810
14563
  if (cmd.redirects.length === 1) {
14811
14564
  const r0Check = cmd.redirects[0];
14812
14565
  const w0DevPlain = cmd.words[0].plain;
@@ -15480,22 +15233,13 @@ export class Runtime {
15480
15233
  ? whilePrintfInd
15481
15234
  : (forPrintfInd && this.canSyncPrintfInductionBody(command.body, rawState, forPrintfInd) ? forPrintfInd : undefined));
15482
15235
  const prevActivePrintfInd = this._activePrintfInductionName;
15483
- const prevActiveLoopScript = this._activeLoopBodyScript;
15484
- const prevActiveLoopCond = this._activeLoopCondScript;
15485
- const prevActiveLoopVar = this._activeLoopVarName;
15486
15236
  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
15237
  let bodySyncOk = false;
15491
15238
  try {
15492
15239
  bodySyncOk = !(guestArrays(rawState) && (guestArrays(rawState).watches.size > 0 || this.budget.limits.maxExpansionBytes < 65536)) && this.canSyncLoopBody(command.body, rawState, io, false, printfInductionName);
15493
15240
  }
15494
15241
  finally {
15495
15242
  this._activePrintfInductionName = prevActivePrintfInd;
15496
- this._activeLoopBodyScript = prevActiveLoopScript;
15497
- this._activeLoopCondScript = prevActiveLoopCond;
15498
- this._activeLoopVarName = prevActiveLoopVar;
15499
15243
  }
15500
15244
  if (!bodySyncOk) {
15501
15245
  command._skipTrySyncLoop = true;
@@ -16101,7 +15845,7 @@ export class Runtime {
16101
15845
  !monitor.hasOverlay("BASH_REMATCH") &&
16102
15846
  !(store ?? requireArrays(rawState))?.watches.has("BASH_REMATCH"));
16103
15847
  }
16104
- 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)));
16105
15849
  }
16106
15850
  if (e.kind === "not")
16107
15851
  return checkWhileCond(e.operand, depth + 1);
@@ -16432,7 +16176,7 @@ export class Runtime {
16432
16176
  store.epoch = restEpoch;
16433
16177
  return loopStatus;
16434
16178
  }
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) {
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) {
16436
16180
  return undefined;
16437
16181
  }
16438
16182
  const activeValScope = io[valueScope];
@@ -16931,187 +16675,6 @@ export class Runtime {
16931
16675
  return pos <= 128 ? fastRedirectScratchViews[pos] : fastRedirectScratchBytes.subarray(0, pos);
16932
16676
  }
16933
16677
  _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
16678
  canSyncPrintfInductionBody(script, rawState, inductionName) {
17116
16679
  for (const list of script.lists)
17117
16680
  for (const pipeline of list.pipelines)
@@ -17806,6 +17369,10 @@ export class Runtime {
17806
17369
  return check(tree);
17807
17370
  }
17808
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;
17809
17376
  if (expr.source.includes("/") || expr.source.includes("%") || expr.source.includes("<<") || expr.source.includes(">>") || expr.source.includes("**") || expr.source.includes("["))
17810
17377
  return false;
17811
17378
  try {
@@ -17968,19 +17535,34 @@ export class Runtime {
17968
17535
  const prevFastSubPos = this._fastSubPositional;
17969
17536
  rawState.positional = fnArgs;
17970
17537
  this._fastSubPositional = fnArgs;
17971
- const savedLocals = new Array(fnCall.locals.length);
17538
+ const savedLocals = [];
17539
+ const seenLocals = new Set();
17972
17540
  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];
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
+ }
17984
17566
  }
17985
17567
  }
17986
17568
  if (fnCall.steps.length > 0) {
@@ -17988,11 +17570,12 @@ export class Runtime {
17988
17570
  }
17989
17571
  else {
17990
17572
  rawState.status = 0;
17991
- onUpdate({ lastCmd: step.cmd, lastArg: fnArgs.length > 0 ? fnArgs[fnArgs.length - 1] : fnCall.name });
17992
17573
  }
17574
+ rawState.lastArgument = fnArgs.at(-1) ?? fnCall.name;
17575
+ onUpdate({ lastCmd: step.cmd, lastArg: rawState.lastArgument });
17993
17576
  }
17994
17577
  finally {
17995
- for (let i = fnCall.locals.length - 1; i >= 0; i--) {
17578
+ for (let i = savedLocals.length - 1; i >= 0; i--) {
17996
17579
  const saved = savedLocals[i];
17997
17580
  if (saved.val !== undefined) {
17998
17581
  rawState.variables[saved.name] = saved.val;
@@ -21282,6 +20865,7 @@ export class Runtime {
21282
20865
  }
21283
20866
  if (hasThrown)
21284
20867
  throw thrownError;
20868
+ state.lastArgument = pendingResume?.lastArgument ?? args.at(-1) ?? name;
21285
20869
  return exitStatus;
21286
20870
  }
21287
20871
  finally {
@@ -21801,6 +21385,7 @@ export class Runtime {
21801
21385
  }
21802
21386
  if (outcome.kind === "throw")
21803
21387
  throw outcome.reason;
21388
+ state.lastArgument = context.args.at(-1) ?? context.command;
21804
21389
  return outcome.value;
21805
21390
  }
21806
21391
  if (selectedKind === "builtin" && !(state.externalInvocation && this.commands.has(context.command))) {
@@ -27162,6 +26747,10 @@ export class Runtime {
27162
26747
  expandSyncTrCharArray(spec) {
27163
26748
  if (spec.length === 0)
27164
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;
27165
26754
  // Escaped endpoints need the tr parser before range expansion.
27166
26755
  if (spec.includes("\\") && spec.includes("-"))
27167
26756
  return undefined;
@@ -27694,10 +27283,10 @@ export class Runtime {
27694
27283
  if (!el || typeof el !== "object" || Array.isArray(el))
27695
27284
  return undefined;
27696
27285
  const rec = el;
27697
- 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);
27698
27287
  if (typeof k !== "string")
27699
27288
  return undefined;
27700
- 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);
27701
27290
  }
27702
27291
  return [obj];
27703
27292
  }
@@ -27730,7 +27319,7 @@ export class Runtime {
27730
27319
  return [item.map(x => x === null ? "" : String(x)).join(arg)];
27731
27320
  }
27732
27321
  if (fn === "split")
27733
- return typeof item === "string" ? [item.split(arg)] : undefined;
27322
+ return typeof item === "string" ? [arg === "" ? Array.from(item) : item.split(arg)] : undefined;
27734
27323
  if (fn === "ltrimstr")
27735
27324
  return typeof item === "string" ? [item.startsWith(arg) ? item.slice(arg.length) : item] : undefined;
27736
27325
  if (fn === "rtrimstr")
@@ -27992,9 +27581,8 @@ export class Runtime {
27992
27581
  break;
27993
27582
  }
27994
27583
  if (!a.startsWith("-") || a === "-") {
27995
- operands.push(a);
27996
- idx++;
27997
- continue;
27584
+ operands.push(...args.slice(idx));
27585
+ break;
27998
27586
  }
27999
27587
  if (a === "-a" || a === "--multiple") {
28000
27588
  multiple = true;
@@ -28376,7 +27964,9 @@ export class Runtime {
28376
27964
  }
28377
27965
  if (st.kind === "=") {
28378
27966
  out.push(String(idx1));
28379
- 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;
28380
27970
  continue;
28381
27971
  }
28382
27972
  if (st.kind === "q") {
@@ -28419,8 +28009,10 @@ export class Runtime {
28419
28009
  }
28420
28010
  return replacement;
28421
28011
  });
28422
- if (matchedOnce && st.printOnMatch)
28012
+ if (matchedOnce && st.printOnMatch) {
28423
28013
  out.push(l);
28014
+ lastInputIndex = i;
28015
+ }
28424
28016
  }
28425
28017
  if (quitNow) {
28426
28018
  break;
@@ -29606,22 +29198,16 @@ export class Runtime {
29606
29198
  if (endField !== undefined && endField < startField)
29607
29199
  return undefined;
29608
29200
  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;
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
+ }
29625
29211
  }
29626
29212
  const extractKey = (l) => {
29627
29213
  let raw = keySpec === undefined
@@ -29653,7 +29239,14 @@ export class Runtime {
29653
29239
  raw = raw.replace(/^[ \t]+/, "");
29654
29240
  if (keyDict)
29655
29241
  raw = raw.replace(/[^a-zA-Z0-9 \t]+/g, "");
29656
- 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;
29657
29250
  };
29658
29251
  const parseNum = (s) => {
29659
29252
  const m = /^[ \t]*(-?(?:\d+(?:\.\d*)?|\.\d+))/.exec(s);
@@ -29686,6 +29279,24 @@ export class Runtime {
29686
29279
  const idx = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"].indexOf(m[1].toUpperCase());
29687
29280
  return idx === -1 ? 0 : idx + 1;
29688
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
+ };
29689
29300
  const sorted = [...rawLines].sort((a, b) => {
29690
29301
  const ka = extractKey(a);
29691
29302
  const kb = extractKey(b);
@@ -29712,15 +29323,13 @@ export class Runtime {
29712
29323
  return keyRev ? -vc : vc;
29713
29324
  }
29714
29325
  else if (ka !== kb) {
29715
- return ka < kb ? (keyRev ? 1 : -1) : (keyRev ? -1 : 1);
29326
+ const comparison = compareBytes(ka, kb);
29327
+ return keyRev ? -comparison : comparison;
29716
29328
  }
29717
29329
  if (uniq || stable)
29718
29330
  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;
29331
+ const comparison = compareBytes(a, b);
29332
+ return rev ? -comparison : comparison;
29724
29333
  });
29725
29334
  if (!uniq)
29726
29335
  return sorted;