@poe-platform/safe-bash 0.1.750 → 0.1.752

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.
@@ -1245,8 +1245,6 @@ class DiscardCommandFailure extends CommandFailure {
1245
1245
  }
1246
1246
  class ParameterExpansionFailure extends ExpansionFailure {
1247
1247
  }
1248
- class SubstringRangeFailure extends ExpansionFailure {
1249
- }
1250
1248
  class NounsetFailure extends ExpansionFailure {
1251
1249
  status;
1252
1250
  constructor(message, line, status = 1) {
@@ -9796,10 +9794,19 @@ export class Runtime {
9796
9794
  // Matching publishes BASH_REMATCH. A sibling may still require fallback,
9797
9795
  // so only a standalone match can be attempted without replaying writes.
9798
9796
  if (expr.operator === "=~")
9799
- return depth === 0 && this.extractSimpleErePattern(expr.right, rawState) !== undefined;
9797
+ return !requireTotal && depth === 0 && this.extractSimpleErePattern(expr.right, rawState) !== undefined;
9800
9798
  if (expr.operator === "==" || expr.operator === "=" || expr.operator === "!=") {
9801
9799
  if (expr.right.parts.length > 0 && expr.right.parts.every(p => p.quoted))
9802
9800
  return true;
9801
+ if (requireTotal) {
9802
+ // Variable patterns can change during the loop. Only admit a static
9803
+ // pattern whose synchronous matcher is guaranteed to return a result.
9804
+ if (!expr.right.parts.every((p, idx) => p.kind === "text" && !p.byteValue && (p.quoted || idx > 0 || !p.value.startsWith("~"))))
9805
+ return false;
9806
+ const pattern = expr.right.parts.map(p => p.kind === "text" ? (p.quoted ? p.value.replace(/[\\*?[\]()]/g, "\\$&") : p.value) : "").join("");
9807
+ const work = { remaining: 4096, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
9808
+ return tryMatchesPatternSync(pattern, "", work, false, true) !== undefined;
9809
+ }
9803
9810
  if (expr.right.parts.length > 0 && expr.right.parts.every((p, idx) => (p.kind === "text" && !p.byteValue && (p.quoted || idx > 0 || !p.value.startsWith("~"))) || (p.kind === "variable" && !p.indirect && !p.prefixNames && !p.length && !p.substring && !p.transform && p.operator === undefined && getArraySelector(p) === undefined)))
9804
9811
  return true;
9805
9812
  return false;
@@ -9988,7 +9995,10 @@ export class Runtime {
9988
9995
  return undefined;
9989
9996
  const syncTouched = this._syncArithRawWriteOnly ? this._syncArithTouched : undefined;
9990
9997
  const lTrim = left.trim() || "0";
9991
- let lNum = resolveSimpleArithOperand(lTrim, rawState, monitor, store, syncTouched);
9998
+ // Preserve all signed 64-bit literal digits instead of rounding through Number.
9999
+ let lNum = lTrim.length > 13 && /^-?[0-9]{1,19}$/.test(lTrim)
10000
+ ? BigInt.asIntN(64, BigInt(lTrim))
10001
+ : resolveSimpleArithOperand(lTrim, rawState, monitor, store, syncTouched);
9992
10002
  if (lNum === undefined) {
9993
10003
  const lExpr = tryEvalSimpleExpandedArith(lTrim, rawState, monitor, store, syncTouched);
9994
10004
  if (lExpr !== undefined)
@@ -9997,7 +10007,9 @@ export class Runtime {
9997
10007
  if (lNum === undefined)
9998
10008
  return undefined;
9999
10009
  const rTrim = right.trim() || "0";
10000
- let rNum = resolveSimpleArithOperand(rTrim, rawState, monitor, store, syncTouched);
10010
+ let rNum = rTrim.length > 13 && /^-?[0-9]{1,19}$/.test(rTrim)
10011
+ ? BigInt.asIntN(64, BigInt(rTrim))
10012
+ : resolveSimpleArithOperand(rTrim, rawState, monitor, store, syncTouched);
10001
10013
  if (rNum === undefined) {
10002
10014
  const rExpr = tryEvalSimpleExpandedArith(rTrim, rawState, monitor, store, syncTouched);
10003
10015
  if (rExpr !== undefined)
@@ -10005,6 +10017,10 @@ export class Runtime {
10005
10017
  }
10006
10018
  if (rNum === undefined)
10007
10019
  return undefined;
10020
+ if (typeof lNum !== typeof rNum) {
10021
+ lNum = BigInt(lNum);
10022
+ rNum = BigInt(rNum);
10023
+ }
10008
10024
  let ok;
10009
10025
  switch (expr.operator) {
10010
10026
  case "-eq":
@@ -13026,7 +13042,6 @@ export class Runtime {
13026
13042
  rawState.substitutionStatus = 0;
13027
13043
  if (rawState.variables._ !== undefined)
13028
13044
  delete rawState.variables._;
13029
- rawState.lastArgument = fnArgs.length > 0 ? fnArgs[fnArgs.length - 1] : w0Plain;
13030
13045
  const positional = rawState.positional;
13031
13046
  let fnArgsBytes = 0;
13032
13047
  for (let i = 0; i < fnArgs.length; i++)
@@ -13240,7 +13255,6 @@ export class Runtime {
13240
13255
  rawState.substitutionStatus = 0;
13241
13256
  if (rawState.variables._ !== undefined)
13242
13257
  delete rawState.variables._;
13243
- rawState.lastArgument = lastCmdArg;
13244
13258
  rawState.depth++;
13245
13259
  let evalStatus;
13246
13260
  try {
@@ -13250,6 +13264,7 @@ export class Runtime {
13250
13264
  rawState.depth--;
13251
13265
  }
13252
13266
  if (typeof evalStatus === "number") {
13267
+ rawState.lastArgument = lastCmdArg;
13253
13268
  if (cachedUnit.script.lists.length === 0) {
13254
13269
  const activeStore = monitor.store;
13255
13270
  const activeExisting = activeStore?.get("PIPESTATUS");
@@ -13589,6 +13604,11 @@ export class Runtime {
13589
13604
  continue;
13590
13605
  }
13591
13606
  if (part.kind === "variable") {
13607
+ // Unsubscripted array references expand element zero through the normal
13608
+ // evaluator. fastValueWord cannot guarantee that expansion throughout
13609
+ // a loop, so reject it before executing any loop effects.
13610
+ if (stateMonitor(rawState)?.store?.get(part.name))
13611
+ return false;
13592
13612
  // These expansions can require multiple fields or asynchronous quoting
13593
13613
  // as loop state changes. Decide before the loop produces any effects.
13594
13614
  if (this._syncLoopFnCheckDepth > 0 && (part.substring || getArraySelector(part)?.kind === "element"))
@@ -13653,21 +13673,17 @@ export class Runtime {
13653
13673
  // their alternates rather than risking corruption or replay.
13654
13674
  return false;
13655
13675
  }
13656
- if (part.length || part.substring || part.operator !== undefined) {
13676
+ // Substrings and pattern/case operators can decline as loop values
13677
+ // change to Unicode or lengths become invalid. Loop steps cannot resume
13678
+ // after that decline without corrupting assignments or replaying effects.
13679
+ if (part.substring)
13680
+ return false;
13681
+ if (part.length || part.operator !== undefined) {
13657
13682
  if (this.budget.limits.maxExpansionBytes !== Infinity || rawState.depth >= 32 || rawState.nounset || rawState.nocasematch || byteLocale(rawState.variables) !== false) {
13658
13683
  return false;
13659
13684
  }
13660
13685
  if (part.length) {
13661
- if (part.operator !== undefined || part.substring)
13662
- return false;
13663
- }
13664
- else if (part.substring) {
13665
- if (part.operator !== undefined || !part.substring.offset.parts.every(p => p.kind === "text" || (p.kind === "variable" && !p.operator && !p.substring && !p.length)) || (part.substring.length && !part.substring.length.parts.every(p => p.kind === "text" || (p.kind === "variable" && !p.operator && !p.substring && !p.length)))) {
13666
- return false;
13667
- }
13668
- }
13669
- else if (part.operator === "^" || part.operator === "^^" || part.operator === "," || part.operator === ",,") {
13670
- if (part.alternate && part.alternate.parts.length > 0)
13686
+ if (part.operator !== undefined)
13671
13687
  return false;
13672
13688
  }
13673
13689
  else if (part.operator === "-" || part.operator === ":-" || part.operator === "+" || part.operator === ":+") {
@@ -13676,29 +13692,6 @@ export class Runtime {
13676
13692
  if (!this.isPureSyncValueWord(part.alternate, rawState, allowAt))
13677
13693
  return false;
13678
13694
  }
13679
- else if (part.operator === "#" || part.operator === "##" || part.operator === "%" || part.operator === "%%") {
13680
- if (!part.alternate || part.alternate.parts.length === 0)
13681
- return false;
13682
- const q0 = part.alternate.parts[0].quoted;
13683
- const trimOp = part.operator;
13684
- if (!part.alternate.parts.every((p, idx) => p.quoted === q0 && ((p.kind === "text" && !p.byteValue && (p.quoted || !hasGlobOrEscape(p.value, !!rawState.extglob) || (idx === 0 && (trimOp === "#" || trimOp === "##") && p.value === "*") || (idx === part.alternate.parts.length - 1 && (trimOp === "%" || trimOp === "%%") && p.value === "*") || (part.alternate.parts.length === 1 && Boolean(tryCompileTrimGlobToRegex(p.value, trimOp, !!rawState.extglob))))) || (p.kind === "variable" && !p.operator && !p.substring && !p.length && !p.indirect && !p.prefixNames && !p.specialParameter && !p.transform && (getArraySelector(p) === undefined || (p.name === "BASH_REMATCH" && getArraySelector(p)?.kind === "element" && /^[0-9]{1,2}$/.test(getArraySelector(p).index.decimal))) && isShellIdentifier(p.name))))) {
13685
- return false;
13686
- }
13687
- }
13688
- else if (part.operator === "/" || part.operator === "//" || part.operator === "/#" || part.operator === "/%") {
13689
- if (!part.alternate || part.alternate.parts.length === 0)
13690
- return false;
13691
- const q0 = part.alternate.parts[0].quoted;
13692
- const repOp = part.operator;
13693
- if (!part.alternate.parts.every(p => p.quoted === q0 && ((p.kind === "text" && !p.byteValue && (p.quoted || !hasGlobOrEscape(p.value, !!rawState.extglob) || (part.alternate.parts.length === 1 && Boolean(tryCompileFixedGlobToRegex(p.value, repOp, !!rawState.extglob))))) || (p.kind === "variable" && !p.operator && !p.substring && !p.length && !p.indirect && !p.prefixNames && !p.specialParameter && !p.transform && getArraySelector(p) === undefined && isShellIdentifier(p.name))))) {
13694
- return false;
13695
- }
13696
- if (part.replacement && part.replacement.parts.length > 0) {
13697
- if (!part.replacement.parts.every(p => ((p.kind === "text" && !p.byteValue && !p.value.includes("&") && !p.value.includes("\\") && (p.quoted || !p.value.startsWith("~"))) || (p.kind === "variable" && !p.operator && !p.substring && !p.length && !p.indirect && !p.prefixNames && !p.specialParameter && !p.transform && getArraySelector(p) === undefined && isShellIdentifier(p.name))))) {
13698
- return false;
13699
- }
13700
- }
13701
- }
13702
13695
  else
13703
13696
  return false;
13704
13697
  }
@@ -13808,14 +13801,8 @@ export class Runtime {
13808
13801
  return false;
13809
13802
  }
13810
13803
  else if (w0Plain === "echo") {
13811
- const firstArg = cmd.words[1];
13812
- if (firstArg) {
13813
- const prefix = firstArg.parts.find(part => part.kind !== "text" || part.value.length > 0);
13814
- // Substitution execution rejects options, including those that appear
13815
- // only on later iterations. Require a stable non-option prefix.
13816
- if (prefix?.kind !== "text" || prefix.value.startsWith("-"))
13817
- return false;
13818
- }
13804
+ if (!this.isSyncSubEchoCallOk(cmd, rawState))
13805
+ return false;
13819
13806
  }
13820
13807
  else if (w0Plain === "dirname" || w0Plain === "basename") {
13821
13808
  const simArgs = [];
@@ -13887,11 +13874,11 @@ export class Runtime {
13887
13874
  if (isNumOp && checkIntVars) {
13888
13875
  const isGuaranteedIntWord = (w) => {
13889
13876
  if (w.plain !== undefined)
13890
- return /^-?[0-9]+$/.test(w.plain);
13877
+ return /^-?[0-9]{1,16}$/.test(w.plain);
13891
13878
  if (w.parts.length === 1) {
13892
13879
  const p0 = w.parts[0];
13893
13880
  if (p0.kind === "arithmetic")
13894
- return true;
13881
+ return false;
13895
13882
  if (p0.kind === "variable" && !p0.indirect && !p0.prefixNames && !p0.substring && !p0.transform && p0.operator === undefined && getArraySelector(p0) === undefined) {
13896
13883
  if (script) {
13897
13884
  for (const lst of script.lists) {
@@ -13899,14 +13886,14 @@ export class Runtime {
13899
13886
  const c0 = pl.commands[0];
13900
13887
  if (c0?.kind === "simple" && c0.words.length === 1) {
13901
13888
  const asg = this.assignment(c0.words[0]);
13902
- if (asg?.name === p0.name && (!asg.value.plain || !/^-?[0-9]+$/.test(asg.value.plain)))
13889
+ if (asg?.name === p0.name && (!asg.value.plain || !/^-?[0-9]{1,16}$/.test(asg.value.plain)))
13903
13890
  return false;
13904
13891
  }
13905
13892
  }
13906
13893
  }
13907
13894
  }
13908
13895
  const cur = rawState.variables[p0.name];
13909
- return cur === undefined || /^-?[0-9]+$/.test(cur);
13896
+ return cur === undefined || /^-?[0-9]{1,16}$/.test(cur);
13910
13897
  }
13911
13898
  }
13912
13899
  return false;
@@ -14034,6 +14021,10 @@ export class Runtime {
14034
14021
  return true;
14035
14022
  if (words.length === 0 || words.length > 64)
14036
14023
  return false;
14024
+ // Slice expressions and member transformations may stop being synchronous
14025
+ // after body mutations. Preserve member expansion through the normal path.
14026
+ if (words.some(w => w.parts.some(p => p.kind === "variable" && (p.substring || p.operator !== undefined || p.transform))))
14027
+ return false;
14037
14028
  if (words.length === 1 && this.isQuotedAtWord(words[0]))
14038
14029
  return true;
14039
14030
  // Unquoted expansions need splitting and globbing against the state at
@@ -14075,9 +14066,10 @@ export class Runtime {
14075
14066
  }
14076
14067
  if (words.length === 1) {
14077
14068
  const w0 = words[0];
14078
- const arrMembers = this.tryExpandSyncArrayMembersWord(w0, rawState, io, line);
14079
- if (arrMembers !== undefined)
14080
- return arrMembers;
14069
+ if (this.canSyncArrayMembersWord(w0, rawState)) {
14070
+ // Member words must never degrade into a single joined scalar word.
14071
+ return this.tryExpandSyncArrayMembersWord(w0, rawState, io, line);
14072
+ }
14081
14073
  const p0 = w0.parts.length === 1 ? w0.parts[0] : (w0.parts.length === 2 && w0.parts[0].kind === "text" && w0.parts[0].value === "" ? w0.parts[1] : undefined);
14082
14074
  if (p0 && !p0.quoted && (rawState.variables.IFS === undefined || rawState.variables.IFS === " \t\n") && !rawState.noglob) {
14083
14075
  if (p0.kind === "variable" && !p0.indirect && !p0.prefixNames && !p0.length && !p0.transform && getArraySelector(p0) === undefined) {
@@ -14307,6 +14299,45 @@ export class Runtime {
14307
14299
  }
14308
14300
  return { ifsWord, varNames, isBareReply, inputWord: r0.target, ...(delimChar !== undefined ? { delimChar } : {}), ...(maxChars !== undefined ? { maxChars } : {}), ...(exactChars !== undefined ? { exactChars } : {}) };
14309
14301
  }
14302
+ canSyncLoopConditionalOperands(expression, rawState, allowRegex = true) {
14303
+ const checkLeaf = (e) => {
14304
+ if (e.kind === "nonempty")
14305
+ return this.isPureSyncValueWord(e.operand, rawState);
14306
+ if (e.kind === "unary")
14307
+ return (e.operator === "-n" || e.operator === "-z" || e.operator === "-v" || (syncConditionalFileUnaryOps.has(e.operator) && this._isMemoryBackingFs && this.canFastMemoryRedirect && this.budget.limits.maxFileSystemOperations >= 1000 && (this._fileWrites === undefined || this._fileWrites.size === 0) && (this._outputFiles === undefined || this._outputFiles.size === 0))) && this.isPureSyncValueWord(e.operand, rawState);
14308
+ if (e.kind === "binary") {
14309
+ if (e.operator === "=~") {
14310
+ if (!allowRegex)
14311
+ return false;
14312
+ const patInfo = this.extractSimpleErePattern(e.right, rawState);
14313
+ // A variable pattern can change through expansions or called functions.
14314
+ if (patInfo?.isVar)
14315
+ return false;
14316
+ 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") && !(stateMonitor(rawState)?.store ?? requireArrays(rawState))?.watches.has("BASH_REMATCH");
14317
+ }
14318
+ // A synchronous loop cannot hand off an unsupported pattern mid-iteration.
14319
+ 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)));
14320
+ }
14321
+ if (e.kind === "not")
14322
+ return checkLeaf(e.operand);
14323
+ if (e.kind === "and" || e.kind === "or")
14324
+ return checkLeaf(e.left) && checkLeaf(e.right);
14325
+ return false;
14326
+ };
14327
+ return checkLeaf(expression);
14328
+ }
14329
+ isSyncSubEchoCallOk(cmd, rawState) {
14330
+ if (!this.isSyncEchoCallOk(cmd, rawState))
14331
+ return false;
14332
+ if (!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)))))
14333
+ return false;
14334
+ const firstArg = cmd.words[1];
14335
+ if (!firstArg)
14336
+ return true;
14337
+ const prefix = firstArg.parts.find(part => part.kind !== "text" || part.value.length > 0);
14338
+ // Substitution execution cannot resume when an option appears mid-loop.
14339
+ return prefix?.kind === "text" && !prefix.value.startsWith("-");
14340
+ }
14310
14341
  isPureSyncSubIfOrCase(cmd, rawState) {
14311
14342
  if (cmd.redirects.length !== 0)
14312
14343
  return false;
@@ -14321,7 +14352,7 @@ export class Runtime {
14321
14352
  return false;
14322
14353
  const w0 = sc.words[0]?.plain;
14323
14354
  if (w0 === "echo")
14324
- return this.isSyncEchoCallOk(sc, rawState);
14355
+ return this.isSyncSubEchoCallOk(sc, rawState);
14325
14356
  if (w0 === "printf" && sc.words[1]?.plain !== "-v")
14326
14357
  return this.isSyncPrintfCallOk(sc, 1, rawState);
14327
14358
  return false;
@@ -14341,11 +14372,13 @@ export class Runtime {
14341
14372
  else if (bc.kind === "conditional") {
14342
14373
  if (bc.redirects.length !== 0 || rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(bc.expression, 0, rawState, true))
14343
14374
  return false;
14344
- if (bc.expression.kind === "binary" && bc.expression.operator === "=~")
14375
+ if (!this.canSyncLoopConditionalOperands(bc.expression, rawState, false))
14345
14376
  return false;
14346
14377
  }
14347
- else if (!this.extractPosixBracketCondExpr(bc, rawState, true, br.body)) {
14348
- return false;
14378
+ else {
14379
+ const expression = this.extractPosixBracketCondExpr(bc, rawState, true, br.body);
14380
+ if (!expression || !this.canSyncLoopConditionalOperands(expression, rawState, false))
14381
+ return false;
14349
14382
  }
14350
14383
  if (!isSimpleEchoOrPrintf(br.body))
14351
14384
  return false;
@@ -14383,29 +14416,7 @@ export class Runtime {
14383
14416
  const canSyncLoopCondCmd = (c) => {
14384
14417
  if (c.redirects.length !== 0 || rawState.nocasematch || byteLocale(rawState.variables) || !this.canSyncConditional(c.expression, 0, rawState, true))
14385
14418
  return false;
14386
- const checkLeaf = (e) => {
14387
- if (e.kind === "nonempty")
14388
- return this.isPureSyncValueWord(e.operand, rawState);
14389
- if (e.kind === "unary")
14390
- return (e.operator === "-n" || e.operator === "-z" || e.operator === "-v" || (syncConditionalFileUnaryOps.has(e.operator) && this._isMemoryBackingFs && this.canFastMemoryRedirect && this.budget.limits.maxFileSystemOperations >= 1000 && (this._fileWrites === undefined || this._fileWrites.size === 0) && (this._outputFiles === undefined || this._outputFiles.size === 0))) && this.isPureSyncValueWord(e.operand, rawState);
14391
- if (e.kind === "binary") {
14392
- if (e.operator === "=~") {
14393
- const patInfo = this.extractSimpleErePattern(e.right, rawState);
14394
- // A variable pattern can change through expansions or called functions.
14395
- if (patInfo?.isVar)
14396
- return false;
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");
14398
- }
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)));
14401
- }
14402
- if (e.kind === "not")
14403
- return checkLeaf(e.operand);
14404
- if (e.kind === "and" || e.kind === "or")
14405
- return checkLeaf(e.left) && checkLeaf(e.right);
14406
- return false;
14407
- };
14408
- return checkLeaf(c.expression);
14419
+ return this.canSyncLoopConditionalOperands(c.expression, rawState);
14409
14420
  };
14410
14421
  if (cmd.kind === "arithmetic") {
14411
14422
  if (cmd.redirects.length === 0) {
@@ -14577,11 +14588,16 @@ export class Runtime {
14577
14588
  (w0DevPlain === "pwd" || this.commands.has(w0DevPlain))) {
14578
14589
  const devWordOk = (w) => this.isPureSyncValueWord(w, rawState) &&
14579
14590
  w.parts.every(p => p.quoted || (p.kind === "text" && p.value.length > 0 && !p.value.includes(" ") && !p.value.includes("\t") && !p.value.includes("\n") && !p.value.includes("{") && !hasGlobOrEscape(p.value, true)));
14591
+ // Only a nonempty literal prefix proves an expansion cannot become an option.
14592
+ const devOperandOk = (w) => {
14593
+ const first = w.parts[0];
14594
+ return first?.kind === "text" && first.value.length > 0 && !first.value.startsWith("-") && devWordOk(w);
14595
+ };
14580
14596
  const devShapeOk = (w0DevPlain === "pwd" && cmd.words.length === 1) ||
14581
- ((w0DevPlain === "dirname" || w0DevPlain === "echo") && cmd.words.length === 2 && !cmd.words[1].plain?.startsWith("-") && devWordOk(cmd.words[1])) ||
14582
- (w0DevPlain === "basename" && ((cmd.words.length === 2 && !cmd.words[1].plain?.startsWith("-") && devWordOk(cmd.words[1])) ||
14583
- (cmd.words.length === 3 && !cmd.words[1].plain?.startsWith("-") && devWordOk(cmd.words[1]) && devWordOk(cmd.words[2])) ||
14584
- (cmd.words.length === 4 && cmd.words[1].plain === "-s" && cmd.words[2].plain !== undefined && !cmd.words[3].plain?.startsWith("-") && devWordOk(cmd.words[3]))));
14597
+ ((w0DevPlain === "dirname" || w0DevPlain === "echo") && cmd.words.length === 2 && devOperandOk(cmd.words[1])) ||
14598
+ (w0DevPlain === "basename" && ((cmd.words.length === 2 && devOperandOk(cmd.words[1])) ||
14599
+ (cmd.words.length === 3 && devOperandOk(cmd.words[1]) && devWordOk(cmd.words[2])) ||
14600
+ (cmd.words.length === 4 && cmd.words[1].plain === "-s" && cmd.words[2].plain !== undefined && devOperandOk(cmd.words[3]))));
14585
14601
  if (devShapeOk)
14586
14602
  continue;
14587
14603
  }
@@ -14722,9 +14738,9 @@ export class Runtime {
14722
14738
  return false;
14723
14739
  }
14724
14740
  const assignment = this.assignment(w0);
14725
- // Eligibility is checked against the entry locale. Changing it can make
14726
- // otherwise pure expansions require the asynchronous byte-aware path.
14727
- if (assignment && (assignment.name === "LC_ALL" || assignment.name === "LC_CTYPE" || assignment.name === "LC_COLLATE" || assignment.name === "LANG"))
14741
+ // Eligibility uses the entry locale and IFS. Mutations can require
14742
+ // field splitting or byte-aware expansion during a later iteration.
14743
+ if (assignment && (assignment.name === "IFS" || assignment.name === "LC_ALL" || assignment.name === "LC_CTYPE" || assignment.name === "LC_COLLATE" || assignment.name === "LANG"))
14728
14744
  return false;
14729
14745
  if (!assignment || (assignment.append && this.budget.limits.maxExpansionBytes !== Infinity) || assignment.name === "OPTIND" || assignment.name === "PIPESTATUS" || assignment.name.includes("[") || rawState.readonlyVariables?.has(assignment.name) || rawState.variableAttributes?.get(assignment.name) || store?.get(assignment.name) || !this.isPureSyncValueWord(assignment.value, rawState)) {
14730
14746
  return false;
@@ -15705,7 +15721,8 @@ export class Runtime {
15705
15721
  }
15706
15722
  else {
15707
15723
  monitor.values.invalidate(varName);
15708
- monitor.proxy.exported.delete(varName);
15724
+ if (!rawState.exported.has(varName))
15725
+ monitor.proxy.exported.delete(varName);
15709
15726
  }
15710
15727
  }
15711
15728
  }
@@ -16146,7 +16163,8 @@ export class Runtime {
16146
16163
  }
16147
16164
  else {
16148
16165
  monitor.values.invalidate(varName);
16149
- monitor.proxy.exported.delete(varName);
16166
+ if (!rawState.exported.has(varName))
16167
+ monitor.proxy.exported.delete(varName);
16150
16168
  }
16151
16169
  }
16152
16170
  }
@@ -16686,7 +16704,24 @@ export class Runtime {
16686
16704
  }
16687
16705
  if (command.kind !== "simple" || hasShellFunction(rawState, command.words[0]?.plain ?? "") || command.words.some(word => getArrayAssignment(word) !== undefined))
16688
16706
  return false;
16689
- const w0 = command.words[0]?.plain;
16707
+ let commandIndex = 0;
16708
+ while (commandIndex < command.words.length && this.assignment(command.words[commandIndex]))
16709
+ commandIndex++;
16710
+ const commandWord = command.words[commandIndex];
16711
+ if (!commandWord?.parts.every(part => part.kind === "text"))
16712
+ return false;
16713
+ const w0 = commandWord.parts.map(part => part.kind === "text" ? part.value : "").join("");
16714
+ // read targets and printf -v targets may be quoted or expanded. Both can
16715
+ // invalidate the integer assumption used by induction formatting.
16716
+ if (w0 === "read")
16717
+ return false;
16718
+ if (w0 === "printf") {
16719
+ const option = command.words[commandIndex + 1];
16720
+ if (!option?.parts.every(part => part.kind === "text"))
16721
+ return false;
16722
+ if (option.parts.map(part => part.kind === "text" ? part.value : "").join("") === "-v")
16723
+ return false;
16724
+ }
16690
16725
  if (w0 === "let" || w0 === "declare" || w0 === "typeset" || w0 === "local" || w0 === "export" || w0 === "unset" || w0 === "mapfile" || w0 === "readarray")
16691
16726
  return false;
16692
16727
  if (inductionName !== undefined) {
@@ -16695,10 +16730,6 @@ export class Runtime {
16695
16730
  return p0?.kind === "text" && !p0.quoted && (p0.value.startsWith(inductionName + "=") || p0.value.startsWith(inductionName + "+="));
16696
16731
  }))
16697
16732
  return false;
16698
- if (w0 === "read" && (inductionName === "REPLY" || command.words.slice(1).some(w => w.plain === inductionName || w.plain === "-a" || w.plain === "-ra" || w.plain === "-ar")))
16699
- return false;
16700
- if (w0 === "printf" && command.words[1]?.plain === "-v" && command.words[2]?.plain === inductionName)
16701
- return false;
16702
16733
  }
16703
16734
  for (const word of [...command.words, ...command.redirects.map(redirect => redirect.target)])
16704
16735
  for (const part of word.parts) {
@@ -16977,6 +17008,7 @@ export class Runtime {
16977
17008
  execSyncLocalDeclStep(decl, rawState, io, monitor, touched, line) {
16978
17009
  const locals = (decl.kind === "local" || decl.kind === "declare") && rawState.locals.length > 0 ? rawState.locals[rawState.locals.length - 1] : undefined;
16979
17010
  let lastArg = decl.kind;
17011
+ const values = decl.items.map(item => item.value === undefined ? undefined : this.fastValueWord(item.value, rawState, io, false, false, false, rawState.braceexpand !== false, 0, line));
16980
17012
  for (let i = 0; i < decl.items.length; i++) {
16981
17013
  const item = decl.items[i];
16982
17014
  const isNewLocal = locals ? !locals.has(item.name) : false;
@@ -16994,7 +17026,7 @@ export class Runtime {
16994
17026
  touched.add(item.name);
16995
17027
  }
16996
17028
  if (item.value !== undefined) {
16997
- const val = this.fastValueWord(item.value, rawState, io, false, false, false, rawState.braceexpand !== false, 0, line);
17029
+ const val = values[i];
16998
17030
  if (item.append) {
16999
17031
  const curRaw = rawState.variables[item.name];
17000
17032
  const curStr = curRaw === undefined ? "" : (touched.has(item.name) ? curRaw : (monitor.values.get(item.name, curRaw) ?? curRaw));
@@ -17004,7 +17036,7 @@ export class Runtime {
17004
17036
  rawState.variables[item.name] = val;
17005
17037
  }
17006
17038
  touched.add(item.name);
17007
- lastArg = `${item.name}=${rawState.variables[item.name]}`;
17039
+ lastArg = `${item.name}${item.append ? "+=" : "="}${val}`;
17008
17040
  }
17009
17041
  else {
17010
17042
  lastArg = item.name;
@@ -17103,7 +17135,28 @@ export class Runtime {
17103
17135
  if (this._syncStdoutBatch.length >= 8192)
17104
17136
  this.flushSyncStdoutBatch(io);
17105
17137
  }
17138
+ runSyncDiscardStep(step, rawState, io) {
17139
+ this.budget.fileSystemOperation();
17140
+ const args = step.discardWords.map(word => this.fastValueWord(word, rawState, io, false, false, false, false, 0, step.line));
17141
+ const cmdName = step.cmd.words[0].plain;
17142
+ let output;
17143
+ if (cmdName === "pwd")
17144
+ output = rawState.variables.PWD ?? rawState.cwd;
17145
+ else if (cmdName === "echo")
17146
+ output = args[0];
17147
+ else if (cmdName === "dirname")
17148
+ output = this.evalSyncDirname(args);
17149
+ else
17150
+ output = this.evalSyncBasename(step.discardSuffix === undefined ? args : ["-s", step.discardSuffix, ...args]);
17151
+ const bytes = shellValueByteLength(output) + 1;
17152
+ if (this.budget.bytes + bytes > this.budget.maxOutputBytesSmi && bytes > this.budget.limits.maxOutputBytes - this.budget.bytes)
17153
+ this.budget.fail("maxOutputBytes");
17154
+ this.budget.bytes += bytes;
17155
+ return args[args.length - 1] ?? cmdName;
17156
+ }
17106
17157
  writeSyncRedirectStep(step, encoded, append, mode, rawState, io) {
17158
+ if (this.budget.bytes + encoded.byteLength > this.budget.maxOutputBytesSmi && encoded.byteLength > this.budget.limits.maxOutputBytes - this.budget.bytes)
17159
+ this.budget.fail("maxOutputBytes");
17107
17160
  // The memory fast writer may retain its input; the encoder scratch is reused.
17108
17161
  encoded = encoded.slice();
17109
17162
  if (step.targetDirPrefix !== undefined && step.targetNamePrefix !== undefined) {
@@ -17267,31 +17320,7 @@ export class Runtime {
17267
17320
  lastArg = pLastArg;
17268
17321
  }
17269
17322
  else if (step.isDiscardDevNull && step.discardWords !== undefined) {
17270
- this.budget.fileSystemOperation();
17271
- const dw = step.discardWords;
17272
- const cmdName = step.cmd.words[0].plain;
17273
- let evalFirst = "";
17274
- let evalLast = cmdName;
17275
- for (let wi = 0; wi < dw.length; wi++) {
17276
- const v = this.fastValueWord(dw[wi], rawState, io, false, false, false, false, 0, step.line);
17277
- if (wi === 0)
17278
- evalFirst = v;
17279
- evalLast = v;
17280
- }
17281
- let outChars = 0;
17282
- if (cmdName === "pwd")
17283
- outChars = (rawState.variables.PWD ?? rawState.cwd).length + 1;
17284
- else if (cmdName === "echo")
17285
- outChars = evalFirst.length + 1;
17286
- else if (cmdName === "dirname")
17287
- outChars = dirname(evalFirst).length + 1;
17288
- else if (cmdName === "basename") {
17289
- const sfx = step.discardSuffix !== undefined ? step.discardSuffix : (dw.length === 2 ? evalLast : undefined);
17290
- const baseStr = /^\/+$/u.test(evalFirst) ? "/" : basename(evalFirst);
17291
- outChars = (sfx && baseStr !== sfx && baseStr.endsWith(sfx) ? baseStr.length - sfx.length : baseStr.length) + 1;
17292
- }
17293
- this.budget.bytes += outChars;
17294
- lastArg = step.discardSuffix !== undefined ? evalLast : (dw.length > 0 ? evalLast : cmdName);
17323
+ lastArg = this.runSyncDiscardStep(step, rawState, io);
17295
17324
  }
17296
17325
  else if (step.name !== undefined && step.value !== undefined) {
17297
17326
  rawState.substitutionStatus = 0;
@@ -17851,31 +17880,7 @@ export class Runtime {
17851
17880
  lastArg = pLastArg;
17852
17881
  }
17853
17882
  else if (step.isDiscardDevNull && step.discardWords !== undefined) {
17854
- this.budget.fileSystemOperation();
17855
- const dw = step.discardWords;
17856
- const cmdName = step.cmd.words[0].plain;
17857
- let evalFirst = "";
17858
- let evalLast = cmdName;
17859
- for (let wi = 0; wi < dw.length; wi++) {
17860
- const v = this.fastValueWord(dw[wi], rawState, io, false, false, false, false, 0, step.line);
17861
- if (wi === 0)
17862
- evalFirst = v;
17863
- evalLast = v;
17864
- }
17865
- let outChars = 0;
17866
- if (cmdName === "pwd")
17867
- outChars = (rawState.variables.PWD ?? rawState.cwd).length + 1;
17868
- else if (cmdName === "echo")
17869
- outChars = evalFirst.length + 1;
17870
- else if (cmdName === "dirname")
17871
- outChars = dirname(evalFirst).length + 1;
17872
- else if (cmdName === "basename") {
17873
- const sfx = step.discardSuffix !== undefined ? step.discardSuffix : (dw.length === 2 ? evalLast : undefined);
17874
- const baseStr = /^\/+$/u.test(evalFirst) ? "/" : basename(evalFirst);
17875
- outChars = (sfx && baseStr !== sfx && baseStr.endsWith(sfx) ? baseStr.length - sfx.length : baseStr.length) + 1;
17876
- }
17877
- this.budget.bytes += outChars;
17878
- lastArg = step.discardSuffix !== undefined ? evalLast : (dw.length > 0 ? evalLast : cmdName);
17883
+ lastArg = this.runSyncDiscardStep(step, rawState, io);
17879
17884
  }
17880
17885
  else if (step.name !== undefined && step.value !== undefined) {
17881
17886
  rawState.substitutionStatus = 0;
@@ -18031,31 +18036,7 @@ export class Runtime {
18031
18036
  lastArg = pLastArg;
18032
18037
  }
18033
18038
  else if (step.isDiscardDevNull && step.discardWords !== undefined) {
18034
- this.budget.fileSystemOperation();
18035
- const dw = step.discardWords;
18036
- const cmdName = step.cmd.words[0].plain;
18037
- let evalFirst = "";
18038
- let evalLast = cmdName;
18039
- for (let wi = 0; wi < dw.length; wi++) {
18040
- const v = this.fastValueWord(dw[wi], rawState, io, false, false, false, false, 0, step.line);
18041
- if (wi === 0)
18042
- evalFirst = v;
18043
- evalLast = v;
18044
- }
18045
- let outChars = 0;
18046
- if (cmdName === "pwd")
18047
- outChars = (rawState.variables.PWD ?? rawState.cwd).length + 1;
18048
- else if (cmdName === "echo")
18049
- outChars = evalFirst.length + 1;
18050
- else if (cmdName === "dirname")
18051
- outChars = dirname(evalFirst).length + 1;
18052
- else if (cmdName === "basename") {
18053
- const sfx = step.discardSuffix !== undefined ? step.discardSuffix : (dw.length === 2 ? evalLast : undefined);
18054
- const baseStr = /^\/+$/u.test(evalFirst) ? "/" : basename(evalFirst);
18055
- outChars = (sfx && baseStr !== sfx && baseStr.endsWith(sfx) ? baseStr.length - sfx.length : baseStr.length) + 1;
18056
- }
18057
- this.budget.bytes += outChars;
18058
- lastArg = step.discardSuffix !== undefined ? evalLast : (dw.length > 0 ? evalLast : cmdName);
18039
+ lastArg = this.runSyncDiscardStep(step, rawState, io);
18059
18040
  }
18060
18041
  else if (step.name !== undefined && step.value !== undefined) {
18061
18042
  rawState.substitutionStatus = 0;
@@ -18990,8 +18971,6 @@ export class Runtime {
18990
18971
  const rawState = stateMonitor(state)?.raw ?? state;
18991
18972
  if (rawState.variables._ !== undefined)
18992
18973
  delete rawState.variables._;
18993
- rawState.lastArgument = wordValues[wordValues.length - 1] ?? w0Plain;
18994
- state.lastArgument = rawState.lastArgument;
18995
18974
  return await this.dispatchFastFunction(w0Plain, state.functions.get(w0Plain), wordValues.slice(1), state, originalIO);
18996
18975
  }
18997
18976
  }
@@ -19796,7 +19775,7 @@ export class Runtime {
19796
19775
  publicDiagnosticMessage(failure, this.budget.onInternalError);
19797
19776
  }
19798
19777
  }
19799
- if ((error instanceof ExpansionFailure && !(error instanceof SubstringRangeFailure && state.loopDepth > 0)) || error instanceof BraceExpansionFailure)
19778
+ if (error instanceof ExpansionFailure || error instanceof BraceExpansionFailure)
19800
19779
  throw completedExit(error instanceof ParameterExpansionFailure && !state.isolated ? 127 : 1);
19801
19780
  if (error instanceof FatalCommandFailure)
19802
19781
  throw completedExit(error.status);
@@ -20265,8 +20244,10 @@ export class Runtime {
20265
20244
  const rawState = stateMonitor(state)?.raw ?? state;
20266
20245
  if (!state._readOnlyStage && rawState.variables._ !== undefined)
20267
20246
  delete rawState.variables._;
20268
- rawState.lastArgument = words.length > 0 ? words[words.length - 1] : "";
20269
- state.lastArgument = rawState.lastArgument;
20247
+ if (!state.functions.has(words[0] ?? "") && !["eval", ".", "source"].includes(words[declarationIndex] ?? "")) {
20248
+ rawState.lastArgument = words.at(-1) ?? "";
20249
+ state.lastArgument = rawState.lastArgument;
20250
+ }
20270
20251
  if (!words.length)
20271
20252
  return state.substitutionStatus;
20272
20253
  const args = words.slice(1);
@@ -20530,8 +20511,10 @@ export class Runtime {
20530
20511
  const rawState = stateMonitor(state)?.raw ?? state;
20531
20512
  if (rawState.variables._ !== undefined && !assignments.some(assignment => assignment.name === "_"))
20532
20513
  delete rawState.variables._;
20533
- rawState.lastArgument = words.length > 0 ? words[words.length - 1] : "";
20534
- state.lastArgument = rawState.lastArgument;
20514
+ if (!functionCommand && !["eval", ".", "source"].includes(words[declarationIndex] ?? "")) {
20515
+ rawState.lastArgument = words.at(-1) ?? "";
20516
+ state.lastArgument = rawState.lastArgument;
20517
+ }
20535
20518
  }
20536
20519
  if (!words.length)
20537
20520
  return state.substitutionStatus;
@@ -21402,10 +21385,16 @@ export class Runtime {
21402
21385
  }
21403
21386
  if (context.command === "command" || context.command === "builtin" || context.command === "type")
21404
21387
  return { exitCode: await this.discoveryBuiltin(context, state, io, assignments, defaultPath) };
21405
- if (context.command === "." || context.command === "source")
21406
- return { exitCode: await this.sourceBuiltin(context, state, { ...io, ...context }, special) };
21407
- if (context.command === "eval")
21408
- return { exitCode: await this.evalBuiltin(context, state, { ...io, ...context }, special) };
21388
+ if (context.command === "." || context.command === "source") {
21389
+ const exitCode = await this.sourceBuiltin(context, state, { ...io, ...context }, special);
21390
+ state.lastArgument = context.args.at(-1) ?? context.command;
21391
+ return { exitCode };
21392
+ }
21393
+ if (context.command === "eval") {
21394
+ const exitCode = await this.evalBuiltin(context, state, { ...io, ...context }, special);
21395
+ state.lastArgument = context.args.at(-1) ?? context.command;
21396
+ return { exitCode };
21397
+ }
21409
21398
  const builtinWork = this.builtin({ ...context, [declarationArrays]: io[declarationArrays] }, state, assignments, (error, diagnostic) => { builtinFailure = { error, diagnostic }; }, bypassFunctions);
21410
21399
  const builtin = await interruptible(builtinWork, this.signal);
21411
21400
  if (builtin !== undefined) {
@@ -25517,7 +25506,7 @@ export class Runtime {
25517
25506
  const length = await arithmetic(expression.length);
25518
25507
  end = length.value < 0n ? size + length.value : offset + length.value;
25519
25508
  if (end < offset)
25520
- throw new SubstringRangeFailure(`${length.source}: substring expression < 0`, line);
25509
+ throw new ExpansionFailure(`${length.source}: substring expression < 0`, line);
25521
25510
  if (end > size)
25522
25511
  end = size;
25523
25512
  }
@@ -26824,6 +26813,13 @@ export class Runtime {
26824
26813
  return out;
26825
26814
  }
26826
26815
  evalSyncTr(input, opArgs) {
26816
+ // Non-ASCII sets require the byte parser; reject them before running producers.
26817
+ for (const value of opArgs) {
26818
+ for (let i = 0; i < value.length; i++) {
26819
+ if (value.charCodeAt(i) >= 128)
26820
+ return undefined;
26821
+ }
26822
+ }
26827
26823
  let idx = 0;
26828
26824
  let flagC = false;
26829
26825
  let flagD = false;
@@ -27908,6 +27904,10 @@ export class Runtime {
27908
27904
  const core = anchorEnd ? core1.slice(0, -1) : core1;
27909
27905
  if (core.length === 0 && !anchorStart && !anchorEnd)
27910
27906
  return undefined;
27907
+ // POSIX-only brackets and leftmost-longest alternatives belong to sed's matcher.
27908
+ if (core.includes("[:") || core.includes("[.") || core.includes("[=") ||
27909
+ core.includes("[]") || core.includes("[^]") || isExtended && core.includes("|"))
27910
+ return undefined;
27911
27911
  let reSrc;
27912
27912
  if (/^[a-zA-Z0-9_ :;,=-]*$/.test(core)) {
27913
27913
  if (/\\[1-9]/.test(rep))
@@ -28121,7 +28121,9 @@ export class Runtime {
28121
28121
  const isOnlyMatching = mode.includes("o");
28122
28122
  // POSIX bracket classes, collating symbols and equivalence classes have
28123
28123
  // different meanings in JS RegExp. Let the command matcher parse them.
28124
- if (isExtended && !isFixed && rawPatterns.some(pattern => pattern.includes("[:") || pattern.includes("[.") || pattern.includes("[=")))
28124
+ if (!isFixed && rawPatterns.some(pattern => pattern.includes("[:") || pattern.includes("[.") || pattern.includes("[=") ||
28125
+ pattern.includes("[]") || pattern.includes("[^]") ||
28126
+ isExtended && pattern.includes("[") && pattern.includes("|")))
28125
28127
  return undefined;
28126
28128
  if (isCount && (isLineNumber || isOnlyMatching || beforeCtx > 0 || afterCtx > 0))
28127
28129
  return undefined;
@@ -28154,15 +28156,19 @@ export class Runtime {
28154
28156
  if (!re)
28155
28157
  return undefined;
28156
28158
  const out = [];
28159
+ let matched = false;
28157
28160
  for (let li = 0; li < rawLines.length; li++) {
28158
28161
  const matches = rawLines[li].match(re);
28159
28162
  if (matches) {
28163
+ matched = true;
28160
28164
  for (let mi = 0; mi < matches.length; mi++) {
28165
+ if (matches[mi].length === 0)
28166
+ continue;
28161
28167
  out.push(isLineNumber ? `${li + 1}:${matches[mi]}` : matches[mi]);
28162
28168
  }
28163
28169
  }
28164
28170
  }
28165
- return { lines: out, status: out.length > 0 ? 0 : 1 };
28171
+ return { lines: out, status: matched ? 0 : 1 };
28166
28172
  }
28167
28173
  if (isExtended && isFixed)
28168
28174
  return undefined;
@@ -28854,7 +28860,7 @@ export class Runtime {
28854
28860
  const out = [];
28855
28861
  let curStyle = bodyStyle;
28856
28862
  let num = start;
28857
- const unnumberedPrefix = " ".repeat(width + sep.length);
28863
+ const unnumberedPrefix = " ".repeat(width + shellValueByteLength(sep));
28858
28864
  for (let i = 0; i < rawLines.length; i++) {
28859
28865
  const l = rawLines[i];
28860
28866
  if (l === "\\:" || l === "\\:\\:" || l === "\\:\\:\\:") {
@@ -29019,7 +29025,11 @@ export class Runtime {
29019
29025
  const units = "KMGTPEZY";
29020
29026
  const out = [];
29021
29027
  for (let i = 0; i < rawLines.length; i++) {
29022
- const raw = rawLines[i].trim();
29028
+ const line = rawLines[i];
29029
+ const raw = line.trim();
29030
+ // Preserve field whitespace and padding through the command implementation.
29031
+ if (line !== raw)
29032
+ return undefined;
29023
29033
  if (raw.length === 0) {
29024
29034
  out.push("");
29025
29035
  continue;
@@ -29361,7 +29371,7 @@ export class Runtime {
29361
29371
  return "-L";
29362
29372
  return undefined;
29363
29373
  }
29364
- evalSyncUniq(rawLines, opArgs) {
29374
+ evalSyncUniq(rawLines, opArgs, isByteLocale = false) {
29365
29375
  let ignoreCase = false;
29366
29376
  let hasCount = false;
29367
29377
  let onlyRepeated = false;
@@ -29420,6 +29430,21 @@ export class Runtime {
29420
29430
  return undefined;
29421
29431
  }
29422
29432
  }
29433
+ if (isByteLocale && (skipChars !== 0 || checkChars !== Infinity) && rawLines.some(line => {
29434
+ for (let i = 0; i < line.length; i++)
29435
+ if (line.charCodeAt(i) > 127)
29436
+ return true;
29437
+ return false;
29438
+ }))
29439
+ return undefined;
29440
+ const foldAscii = (value) => {
29441
+ let folded = "";
29442
+ for (let i = 0; i < value.length; i++) {
29443
+ const code = value.charCodeAt(i);
29444
+ folded += code >= 97 && code <= 122 ? String.fromCharCode(code - 32) : value[i];
29445
+ }
29446
+ return folded;
29447
+ };
29423
29448
  const keyOf = (l) => {
29424
29449
  let offset = 0;
29425
29450
  for (let f = 0; f < skipFields; f++) {
@@ -29430,11 +29455,11 @@ export class Runtime {
29430
29455
  }
29431
29456
  if (skipChars === 0 && checkChars === Infinity) {
29432
29457
  const sub = l.slice(offset);
29433
- return ignoreCase ? sub.toLowerCase() : sub;
29458
+ return ignoreCase ? foldAscii(sub) : sub;
29434
29459
  }
29435
29460
  const chars = Array.from(l.slice(offset));
29436
29461
  const sub = (checkChars === Infinity ? chars.slice(skipChars) : chars.slice(skipChars, skipChars + checkChars)).join("");
29437
- return ignoreCase ? sub.toLowerCase() : sub;
29462
+ return ignoreCase ? foldAscii(sub) : sub;
29438
29463
  };
29439
29464
  const outLines = [];
29440
29465
  let uIdx = 0;
@@ -29455,6 +29480,11 @@ export class Runtime {
29455
29480
  return this.evalSyncSort(rawLines, flag !== undefined ? [flag] : [], false) ?? [...rawLines];
29456
29481
  }
29457
29482
  translateSyncTr(input, set1Spec, set2Spec, truncateSet1 = false) {
29483
+ // Other string2 classes require the tr parser's class and alignment validation.
29484
+ if (set2Spec.includes("[:") &&
29485
+ !(set1Spec === "[:lower:]" && set2Spec === "[:upper:]") &&
29486
+ !(set1Spec === "[:upper:]" && set2Spec === "[:lower:]"))
29487
+ return undefined;
29458
29488
  if ((set1Spec === "a-z" || set1Spec === "[:lower:]") && (set2Spec === "A-Z" || set2Spec === "[:upper:]")) {
29459
29489
  return input.replace(/[a-z]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 32));
29460
29490
  }
@@ -29830,7 +29860,7 @@ export class Runtime {
29830
29860
  const firstName = stageNames[sIdx];
29831
29861
  const extDef = stageDefs[sIdx];
29832
29862
  const stageArgs = stageArgsList[sIdx];
29833
- const nextBuf = (index & 1) === 0 ? sharedSyncPipeBuf0 : sharedSyncPipeBuf1;
29863
+ let nextBuf = (index & 1) === 0 ? sharedSyncPipeBuf0 : sharedSyncPipeBuf1;
29834
29864
  const isInlineCutField = firstName === "cut";
29835
29865
  const isInlineTr = firstName === "tr";
29836
29866
  const isInlineAwk = firstName === "awk";
@@ -29879,6 +29909,8 @@ export class Runtime {
29879
29909
  if (nextTotalBytes > this.budget.maxOutputBytesSmi && sub.byteLength > this.budget.limits.maxOutputBytes - this.budget.bytes)
29880
29910
  this.budget.fail("maxOutputBytes");
29881
29911
  this.budget.bytes = nextTotalBytes;
29912
+ if (sub.byteLength > nextBuf.byteLength)
29913
+ nextBuf = new Uint8Array(sub.byteLength);
29882
29914
  nextBuf.set(sub, 0);
29883
29915
  prevBuf = nextBuf;
29884
29916
  prevLen = sub.byteLength;
@@ -29904,8 +29936,9 @@ export class Runtime {
29904
29936
  outLines = [String(nl)];
29905
29937
  }
29906
29938
  else if (stageArgs[0] === "-w") {
29907
- const trimmed = inStr.trim();
29908
- outLines = [String(trimmed.length === 0 ? 0 : trimmed.split(/[ \t\n\r\f\v]+/).length)];
29939
+ if (!byteLocale(rawState.variables) && prevBuf.subarray(0, prevLen).some(byte => byte > 127))
29940
+ return undefined;
29941
+ outLines = [String(inStr.split(/[ \t\n\r\f\v]+/).filter(Boolean).length)];
29909
29942
  }
29910
29943
  else if (stageArgs[0] === "-m") {
29911
29944
  outLines = [String(byteLocale(rawState.variables) ? prevLen : Array.from(inStr).length)];
@@ -29928,12 +29961,12 @@ export class Runtime {
29928
29961
  if (transformed === undefined)
29929
29962
  return undefined;
29930
29963
  const outByteLen = shellValueByteLength(transformed);
29931
- if (outByteLen > nextBuf.byteLength)
29932
- return undefined;
29933
29964
  const nextTotalBytes = this.budget.bytes + outByteLen;
29934
29965
  if (nextTotalBytes > this.budget.maxOutputBytesSmi && outByteLen > this.budget.limits.maxOutputBytes - this.budget.bytes)
29935
29966
  this.budget.fail("maxOutputBytes");
29936
29967
  this.budget.bytes = nextTotalBytes;
29968
+ if (outByteLen > nextBuf.byteLength)
29969
+ nextBuf = new Uint8Array(outByteLen);
29937
29970
  const written = transformed.length === 0 ? 0 : fastSharedTextEncoder.encodeInto(transformed, nextBuf).written;
29938
29971
  prevBuf = nextBuf;
29939
29972
  prevLen = written;
@@ -29943,7 +29976,7 @@ export class Runtime {
29943
29976
  outLines = this.evalSyncSort(rawLines, stageArgs, false) ?? [];
29944
29977
  }
29945
29978
  else if (isInlineUniq) {
29946
- const uniqRes = this.evalSyncUniq(rawLines, stageArgs);
29979
+ const uniqRes = this.evalSyncUniq(rawLines, stageArgs, byteLocale(rawState.variables));
29947
29980
  if (uniqRes === undefined)
29948
29981
  return undefined;
29949
29982
  outLines = uniqRes;
@@ -29971,9 +30004,10 @@ export class Runtime {
29971
30004
  outLines = grepRes.lines;
29972
30005
  }
29973
30006
  else if (isInlineTac) {
29974
- if (inStr.length > 0 && !inStr.endsWith("\n"))
29975
- return undefined;
29976
30007
  outLines = [...rawLines].reverse();
30008
+ if (!inStr.endsWith("\n") && outLines.length > 1) {
30009
+ outLines.splice(0, 2, outLines[0] + outLines[1]);
30010
+ }
29977
30011
  }
29978
30012
  else if (isInlineNl) {
29979
30013
  const nlRes = this.evalSyncNl(rawLines, stageArgs);
@@ -30003,12 +30037,14 @@ export class Runtime {
30003
30037
  if (cleaned.length % 4 !== 0 || (cleaned.length > 0 && !/^[A-Za-z0-9+/]+={0,2}$/.test(cleaned)))
30004
30038
  return undefined;
30005
30039
  const decoded = this.syncBase64DecodeBytes(cleaned);
30006
- if (decoded.some(byte => byte === 0 || byte >= 128) || decoded.byteLength > nextBuf.byteLength)
30040
+ if (decoded.some(byte => byte === 0 || byte >= 128))
30007
30041
  return undefined;
30008
30042
  const nextTotalBytes = this.budget.bytes + decoded.byteLength;
30009
30043
  if (nextTotalBytes > this.budget.maxOutputBytesSmi && decoded.byteLength > this.budget.limits.maxOutputBytes - this.budget.bytes)
30010
30044
  this.budget.fail("maxOutputBytes");
30011
30045
  this.budget.bytes = nextTotalBytes;
30046
+ if (decoded.byteLength > nextBuf.byteLength)
30047
+ nextBuf = new Uint8Array(decoded.byteLength);
30012
30048
  nextBuf.set(decoded, 0);
30013
30049
  prevBuf = nextBuf;
30014
30050
  prevLen = decoded.byteLength;
@@ -30023,15 +30059,15 @@ export class Runtime {
30023
30059
  }
30024
30060
  // These filters preserve the terminator of the final selected input line.
30025
30061
  const preservesTerminator = firstName === "head" || firstName === "tail" || firstName === "rev" || firstName === "sed";
30026
- const terminated = sedTerminated || !preservesTerminator || inStr.endsWith("\n") || firstName === "head" && outLines.length < rawLines.length;
30062
+ const terminated = isInlineTac ? inStr.includes("\n") : sedTerminated || !preservesTerminator || inStr.endsWith("\n") || firstName === "head" && outLines.length < rawLines.length;
30027
30063
  const outStr = outLines.length > 0 ? outLines.join("\n") + (terminated ? "\n" : "") : "";
30028
30064
  const outByteLen = shellValueByteLength(outStr);
30029
- if (outByteLen > nextBuf.byteLength)
30030
- return undefined;
30031
30065
  const nextTotalBytes = this.budget.bytes + outByteLen;
30032
30066
  if (nextTotalBytes > this.budget.maxOutputBytesSmi && outByteLen > this.budget.limits.maxOutputBytes - this.budget.bytes)
30033
30067
  this.budget.fail("maxOutputBytes");
30034
30068
  this.budget.bytes = nextTotalBytes;
30069
+ if (outByteLen > nextBuf.byteLength)
30070
+ nextBuf = new Uint8Array(outByteLen);
30035
30071
  const written = outStr.length === 0 ? 0 : fastSharedTextEncoder.encodeInto(outStr, nextBuf).written;
30036
30072
  prevBuf = nextBuf;
30037
30073
  prevLen = written;
@@ -30341,7 +30377,7 @@ export class Runtime {
30341
30377
  fileRes = renderLines(sortRes);
30342
30378
  }
30343
30379
  else if (w0Plain === "uniq") {
30344
- const uniqRes = this.evalSyncUniq(rawLines, opArgs);
30380
+ const uniqRes = this.evalSyncUniq(rawLines, opArgs, byteLocale(rawState.variables));
30345
30381
  if (uniqRes !== undefined)
30346
30382
  fileRes = renderLines(uniqRes);
30347
30383
  }
@@ -30352,7 +30388,10 @@ export class Runtime {
30352
30388
  if (opArgs.length === 0)
30353
30389
  fileRes = this.syncBase64Encode(view) + "\n";
30354
30390
  else {
30355
- const decoded = this.syncBase64DecodeBytes(fileStr);
30391
+ const cleaned = fileStr.replace(/[ \t\r\n]+/g, "");
30392
+ if (cleaned.length % 4 !== 0 || (cleaned.length > 0 && !/^[A-Za-z0-9+/]+={0,2}$/.test(cleaned)))
30393
+ return undefined;
30394
+ const decoded = this.syncBase64DecodeBytes(cleaned);
30356
30395
  // Text-only substitutions cannot retain binary byte provenance.
30357
30396
  if (decoded.some(byte => byte === 0 || byte >= 128))
30358
30397
  return undefined;