@poe-platform/safe-bash 0.1.78 → 0.1.80

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.
@@ -10,6 +10,7 @@ import { evaluateArithmetic, prepareArithmetic } from "./arithmetic.js";
10
10
  import { defaultMaxParseUnits, ParseBudget } from "./parse-budget.js";
11
11
  import { evaluatePositionalArithmetic } from "./arithmetic-parameters.js";
12
12
  import { compilePattern, matchesPattern } from "./pattern.js";
13
+ import { nextCodePointOffset, previousCodePointOffset, scanString, stringCheckpoint } from "./string-operations.js";
13
14
  import { byteLocale } from "./locale.js";
14
15
  import { functionDisplay } from "./display.js";
15
16
  import { ConditionalUnsupported, evaluateConditional } from "./conditional.js";
@@ -4939,7 +4940,15 @@ export class Runtime {
4939
4940
  holding?.release();
4940
4941
  }
4941
4942
  }
4943
+ parameterOperandIO(word, state, io) {
4944
+ this.signal.throwIfAborted();
4945
+ const parameterDepth = (io.parameterDepth ?? 0) + 1;
4946
+ if (state.depth + parameterDepth > 64)
4947
+ throw new ShellSyntaxError("Syntax nesting exceeds 64", word.offset);
4948
+ return { ...io, parameterDepth };
4949
+ }
4942
4950
  async partValue(part, state, io, hereString) {
4951
+ this.signal.throwIfAborted();
4943
4952
  if (part.kind === "failed-substitution") {
4944
4953
  if (state.depth >= this.budget.limits.maxSubstitutionDepth)
4945
4954
  this.budget.fail("maxSubstitutionDepth");
@@ -4966,6 +4975,9 @@ export class Runtime {
4966
4975
  if (part.kind === "substitution") {
4967
4976
  if (state.depth >= this.budget.limits.maxSubstitutionDepth)
4968
4977
  this.budget.fail("maxSubstitutionDepth");
4978
+ const parameterDepth = io.parameterDepth ?? 0;
4979
+ if (parameterDepth > 0 && state.depth + parameterDepth + 1 > 64)
4980
+ throw new ShellSyntaxError("Syntax nesting exceeds 64", 0);
4969
4981
  const capture = new Capture();
4970
4982
  const child = await cloneState(state, this.signal);
4971
4983
  child.isolated = true;
@@ -5022,7 +5034,7 @@ export class Runtime {
5022
5034
  throw new ArrayFailure("index outside 0..2147483647");
5023
5035
  const value = binding ? binding.get(index) : index === 0 ? state.variables[part.name] : undefined;
5024
5036
  this.requireParameter(value, `${part.name}[${selector.index}]`, state, io, part.line);
5025
- return part.length ? String(Array.from(value ?? "").length) : value ?? "";
5037
+ return part.length ? this.parameterLength(value ?? "") : value ?? "";
5026
5038
  }
5027
5039
  if (part.length)
5028
5040
  return String(binding?.values.size ?? (state.variables[part.name] === undefined ? 0 : 1));
@@ -5060,17 +5072,18 @@ export class Runtime {
5060
5072
  const missing = value === undefined || (part.operator.startsWith(":") && value === "");
5061
5073
  const operator = part.operator.at(-1);
5062
5074
  if ((operator === "+" && !missing) || (operator !== "+" && missing)) {
5075
+ const operandIO = this.parameterOperandIO(part.alternate, state, io);
5063
5076
  let alternate;
5064
5077
  if (operator === "=" && arrayStore(state)?.get(part.name)) {
5065
5078
  alternate = "";
5066
5079
  await this.arrayZero(state, part.name, async () => {
5067
- alternate = await this.arrayJoin(requireArrays(state).owner, await this.word(part.alternate, state, io, false, false, hereString), "");
5080
+ alternate = await this.arrayJoin(requireArrays(state).owner, await this.word(part.alternate, state, operandIO, false, false, hereString), "");
5068
5081
  return alternate;
5069
5082
  });
5070
5083
  value = alternate;
5071
- return part.length ? String(Array.from(value).length) : value;
5084
+ return part.length ? this.parameterLength(value) : value;
5072
5085
  }
5073
- retained = concatShellValues(await this.valueWord(part.alternate, state, io, false, false, hereString), io[valueScope]);
5086
+ retained = concatShellValues(await this.valueWord(part.alternate, state, operandIO, false, false, hereString), io[valueScope]);
5074
5087
  alternate = shellValueText(retained);
5075
5088
  if (operator === "?")
5076
5089
  throw new ParameterExpansionFailure(`${part.name}: ${alternate || (part.operator.startsWith(":") ? "parameter null or not set" : "parameter not set")}`, io.diagnosticLine ?? part.line);
@@ -5088,7 +5101,15 @@ export class Runtime {
5088
5101
  }
5089
5102
  else
5090
5103
  this.requireParameter(value, part.name, state, io, part.line);
5091
- return part.length ? String(Array.from(value ?? "").length) : retained ?? "";
5104
+ return part.length ? this.parameterLength(value ?? "") : retained ?? "";
5105
+ }
5106
+ async parameterLength(value) {
5107
+ const limit = this.budget.limits.maxExpansionBytes;
5108
+ const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
5109
+ const scanned = await scanString(value, work);
5110
+ if (scanned.bytes > limit)
5111
+ this.budget.fail("maxExpansionBytes");
5112
+ return String(scanned.count);
5092
5113
  }
5093
5114
  async substring(part, value, state, io) {
5094
5115
  const owner = arrayStore(state)?.get(part.name) ? requireArrays(state).owner : undefined;
@@ -5101,170 +5122,226 @@ export class Runtime {
5101
5122
  const limit = this.budget.limits.maxExpansionBytes;
5102
5123
  if (Buffer.byteLength(value) > limit)
5103
5124
  this.budget.fail("maxExpansionBytes");
5104
- const variables = new Proxy(this.arithmeticVariables(state, line), { get: (target, key) => {
5105
- this.signal.throwIfAborted();
5106
- const value = Reflect.get(target, key);
5107
- if (typeof value === "string" && Buffer.byteLength(value) > limit)
5108
- this.budget.fail("maxExpansionBytes");
5109
- return value;
5110
- } });
5111
- const arithmetic = async (word) => {
5112
- let source = "";
5113
- let bytes = 0;
5114
- for (const entry of word.parts) {
5125
+ const scratch = this.budget.values.scope();
5126
+ const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
5127
+ try {
5128
+ const variables = new Proxy(this.arithmeticVariables(state, line), { get: (target, key) => {
5129
+ this.signal.throwIfAborted();
5130
+ const value = Reflect.get(target, key);
5131
+ if (typeof value === "string" && Buffer.byteLength(value) > limit)
5132
+ this.budget.fail("maxExpansionBytes");
5133
+ return value;
5134
+ } });
5135
+ const arithmetic = async (word) => {
5136
+ const operandIO = this.parameterOperandIO(word, state, io);
5137
+ let source = "";
5138
+ let bytes = 0;
5139
+ let retained;
5140
+ for (const entry of word.parts) {
5141
+ this.signal.throwIfAborted();
5142
+ const text = entry.kind === "text" ? entry.value : await this.part(entry, state, operandIO);
5143
+ bytes += Buffer.byteLength(text);
5144
+ if (bytes > limit)
5145
+ this.budget.fail("maxExpansionBytes");
5146
+ owner?.reserve({ metadata: 32, payload: bytes, work: text.length + 4 });
5147
+ const pending = stringCheckpoint(work, text.length + 1);
5148
+ if (pending)
5149
+ await pending;
5150
+ const next = scratch.reserve((source.length + text.length) * 2, 0);
5151
+ source += text;
5152
+ retained?.release();
5153
+ retained = next;
5154
+ }
5115
5155
  this.signal.throwIfAborted();
5116
- const text = entry.kind === "text" ? entry.value : await this.part(entry, state, io);
5117
- bytes += Buffer.byteLength(text);
5118
- if (bytes > limit)
5119
- this.budget.fail("maxExpansionBytes");
5120
- owner?.reserve({ metadata: 32, payload: bytes, work: text.length + 4 });
5121
- source += text;
5156
+ try {
5157
+ return { value: evaluateArithmetic(prepareArithmetic(source, this.budget.parsing), variables, this.budget.parsing), source };
5158
+ }
5159
+ catch (error) {
5160
+ this.rethrowArithmeticControl(error);
5161
+ throw new ExpansionFailure(`${part.name}: ${message(error)}`, line);
5162
+ }
5163
+ finally {
5164
+ retained?.release();
5165
+ }
5166
+ };
5167
+ const offsetExpression = await arithmetic(expression.offset);
5168
+ let bytes;
5169
+ if (byteLocale(state.variables)) {
5170
+ scratch.reserve(Buffer.byteLength(value), 0);
5171
+ bytes = Buffer.from(value);
5172
+ }
5173
+ const size = BigInt(bytes?.byteLength ?? (await scanString(value, work)).count);
5174
+ const offset = offsetExpression.value < 0n ? size + offsetExpression.value : offsetExpression.value;
5175
+ if (offset < 0n || offset > size)
5176
+ return "";
5177
+ let end = size;
5178
+ if (expression.length) {
5179
+ const length = await arithmetic(expression.length);
5180
+ end = length.value < 0n ? size + length.value : offset + length.value;
5181
+ if (end < offset)
5182
+ throw new ExpansionFailure(`${length.source}: substring expression < 0`, line);
5183
+ if (end > size)
5184
+ end = size;
5122
5185
  }
5123
5186
  this.signal.throwIfAborted();
5187
+ if (!bytes) {
5188
+ const start = (await scanString(value, work, 0, value.length, Number(offset))).end;
5189
+ const finish = (await scanString(value, work, start, value.length, Number(end - offset))).end;
5190
+ if (finish > start)
5191
+ scratch.reserve((finish - start) * 2, 0);
5192
+ return value.slice(start, finish);
5193
+ }
5194
+ scratch.reserve(Number(end - offset) * 2, 0);
5124
5195
  try {
5125
- return { value: evaluateArithmetic(prepareArithmetic(source, this.budget.parsing), variables, this.budget.parsing), source };
5196
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes.subarray(Number(offset), Number(end)));
5126
5197
  }
5127
- catch (error) {
5128
- this.rethrowArithmeticControl(error);
5129
- throw new ExpansionFailure(`${part.name}: ${message(error)}`, line);
5198
+ catch {
5199
+ throw new ExpansionFailure("substring expansion splits a UTF-8 character in a byte locale", line);
5130
5200
  }
5131
- };
5132
- const offsetExpression = await arithmetic(expression.offset);
5133
- owner?.reserve({ metadata: 128 + value.length * 64, payload: Buffer.byteLength(value), allocatedSlots: value.length, work: value.length + 8 });
5134
- const characters = byteLocale(state.variables) ? undefined : Array.from(value);
5135
- const bytes = characters ? undefined : Buffer.from(value);
5136
- const size = BigInt(characters?.length ?? bytes.byteLength);
5137
- const offset = offsetExpression.value < 0n ? size + offsetExpression.value : offsetExpression.value;
5138
- if (offset < 0n || offset > size)
5139
- return "";
5140
- let end = size;
5141
- if (expression.length) {
5142
- const length = await arithmetic(expression.length);
5143
- end = length.value < 0n ? size + length.value : offset + length.value;
5144
- if (end < offset)
5145
- throw new ExpansionFailure(`${length.source}: substring expression < 0`, line);
5146
- if (end > size)
5147
- end = size;
5148
5201
  }
5149
- this.signal.throwIfAborted();
5150
- owner?.reserve({ metadata: 96 + value.length * 32, payload: Buffer.byteLength(value) * 3, allocatedSlots: value.length, work: value.length + 7 });
5151
- if (characters)
5152
- return characters.slice(Number(offset), Number(end)).join("");
5153
- try {
5154
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes.subarray(Number(offset), Number(end)));
5155
- }
5156
- catch {
5157
- throw new ExpansionFailure("substring expansion splits a UTF-8 character in a byte locale", line);
5202
+ finally {
5203
+ scratch.close();
5158
5204
  }
5159
5205
  }
5160
5206
  async parameterPattern(part, text, state, io, hereString) {
5161
- const owner = arrayStore(state)?.get(part.name) ? requireArrays(state).owner : undefined;
5162
5207
  const limit = this.budget.limits.maxExpansionBytes;
5163
5208
  if (Buffer.byteLength(text) > limit)
5164
5209
  this.budget.fail("maxExpansionBytes");
5165
- const patternFields = await this.word(part.alternate, state, io, false, true, hereString);
5166
- const pattern = owner ? await this.arrayJoin(owner, patternFields, "") : patternFields.join("");
5167
- owner?.reserve({ metadata: 128 + text.length * 64, payload: Buffer.byteLength(text), allocatedSlots: text.length, work: text.length + 8 });
5168
- const characters = Array.from(text);
5169
- const slice = (start, end = characters.length) => {
5170
- owner?.reserve({ metadata: 96 + (end - start) * 32, payload: (end - start) * 4, allocatedSlots: end - start, work: end - start + 7 });
5171
- return characters.slice(start, end).join("");
5172
- };
5173
- const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
5174
- const matches = await compilePattern(pattern, work);
5175
- let attempts = 0;
5176
- const match = async (start, end) => {
5177
- work.remaining -= end - start + 1;
5178
- if (work.remaining < 0)
5179
- work.exhausted();
5180
- if (++attempts % 256 === 0)
5181
- await yieldTurn(this.signal);
5182
- this.signal.throwIfAborted();
5183
- if (owner)
5184
- await owner.ledger.checkpoint(this.signal, end - start + 1);
5185
- return matches(slice(start, end));
5186
- };
5187
- const operator = part.operator;
5188
- if (!operator.startsWith("/")) {
5189
- const longest = operator.length === 2;
5190
- for (let length = longest ? characters.length : 0; longest ? length >= 0 : length <= characters.length; length += longest ? -1 : 1) {
5210
+ const scratch = this.budget.values.scope();
5211
+ const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes"), allocation: scratch };
5212
+ try {
5213
+ const patternFields = await this.word(part.alternate, state, this.parameterOperandIO(part.alternate, state, io), false, true, hereString);
5214
+ let patternUnits = 0;
5215
+ for (const field of patternFields) {
5216
+ const pending = stringCheckpoint(work, field.length + 1);
5217
+ if (pending)
5218
+ await pending;
5219
+ patternUnits += field.length;
5220
+ }
5221
+ scratch.reserve(patternUnits * 2, 0);
5222
+ const pattern = patternFields.join("");
5223
+ const size = (await scanString(text, work)).count;
5224
+ const matches = await compilePattern(pattern, work);
5225
+ const match = async (start, end, length) => {
5226
+ const pending = stringCheckpoint(work, length + 1);
5227
+ if (pending)
5228
+ await pending;
5229
+ return matches(text, start, end);
5230
+ };
5231
+ const operator = part.operator;
5232
+ if (!operator.startsWith("/")) {
5233
+ const longest = operator.length === 2;
5191
5234
  const prefix = operator.startsWith("#");
5192
- if (await match(prefix ? 0 : characters.length - length, prefix ? length : characters.length))
5193
- return prefix ? slice(length) : slice(0, characters.length - length);
5194
- }
5195
- return text;
5196
- }
5197
- const replacements = [];
5198
- owner?.reserve({ metadata: 64, work: 3 });
5199
- let replacementBytes = 0;
5200
- for (const [index, entry] of (part.replacement?.parts ?? []).entries()) {
5201
- let value = entry.kind === "text" ? entry.value : await this.part(entry, state, io, hereString);
5202
- if (index === 0 && !entry.quoted && /^~(?:\/|$)/u.test(value))
5203
- value = (state.variables.HOME ?? "~") + value.slice(1);
5204
- replacementBytes += Buffer.byteLength(value);
5205
- if (replacementBytes > limit)
5206
- this.budget.fail("maxExpansionBytes");
5207
- owner?.reserve({ metadata: 64, payload: Buffer.byteLength(value), allocatedSlots: 1, work: 5 });
5208
- replacements.push({ value, quoted: entry.quoted });
5209
- }
5210
- if (!pattern && operator !== "/#" && operator !== "/%")
5211
- return text;
5212
- let result = "";
5213
- let resultBytes = 0;
5214
- const append = (value) => {
5215
- resultBytes += Buffer.byteLength(value);
5216
- if (resultBytes > limit)
5217
- this.budget.fail("maxExpansionBytes");
5218
- owner?.reserve({ metadata: 32, payload: resultBytes, work: value.length + 4 });
5219
- result += value;
5220
- };
5221
- let position = 0;
5222
- while (position <= characters.length) {
5223
- let found = false;
5224
- for (let start = position; start <= characters.length; start++) {
5225
- if (operator === "/#" && start !== 0)
5226
- break;
5227
- for (let end = characters.length; end >= start; end--) {
5228
- if (operator === "/%" && end !== characters.length)
5235
+ let boundary = longest === prefix ? text.length : 0;
5236
+ for (let length = longest ? size : 0; longest ? length >= 0 : length <= size; length += longest ? -1 : 1) {
5237
+ if (await match(prefix ? 0 : boundary, prefix ? boundary : text.length, length)) {
5238
+ const start = prefix ? boundary : 0;
5239
+ const end = prefix ? text.length : boundary;
5240
+ scratch.reserve((end - start) * 2, 0);
5241
+ return text.slice(start, end);
5242
+ }
5243
+ boundary = longest === prefix ? previousCodePointOffset(text, boundary) : nextCodePointOffset(text, boundary);
5244
+ }
5245
+ return text;
5246
+ }
5247
+ scratch.reserve(64, 0);
5248
+ const replacements = [];
5249
+ let replacementBytes = 0;
5250
+ const replacementIO = part.replacement ? this.parameterOperandIO(part.replacement, state, io) : io;
5251
+ for (const [index, entry] of (part.replacement?.parts ?? []).entries()) {
5252
+ let value = entry.kind === "text" ? entry.value : await this.part(entry, state, replacementIO, hereString);
5253
+ if (index === 0 && !entry.quoted && /^~(?:\/|$)/u.test(value)) {
5254
+ const home = state.variables.HOME ?? "~";
5255
+ scratch.reserve((home.length + value.length - 1) * 2, 0);
5256
+ value = home + value.slice(1);
5257
+ }
5258
+ replacementBytes += Buffer.byteLength(value);
5259
+ if (replacementBytes > limit)
5260
+ this.budget.fail("maxExpansionBytes");
5261
+ const pending = stringCheckpoint(work, value.length + 1);
5262
+ if (pending)
5263
+ await pending;
5264
+ scratch.reserve(64 + value.length * 2, 0);
5265
+ replacements.push({ value, quoted: entry.quoted });
5266
+ }
5267
+ if (!pattern && operator !== "/#" && operator !== "/%")
5268
+ return text;
5269
+ let result = "";
5270
+ let resultBytes = 0;
5271
+ let retained;
5272
+ const append = async (value, start = 0, end = value.length) => {
5273
+ resultBytes += (await scanString(value, work, start, end)).bytes;
5274
+ if (resultBytes > limit)
5275
+ this.budget.fail("maxExpansionBytes");
5276
+ if (start === end)
5277
+ return;
5278
+ const fragment = scratch.reserve((end - start) * 2, 0);
5279
+ const next = scratch.reserve((result.length + end - start) * 2, 0);
5280
+ result += value.slice(start, end);
5281
+ fragment.release();
5282
+ retained?.release();
5283
+ retained = next;
5284
+ };
5285
+ let position = 0;
5286
+ let positionIndex = 0;
5287
+ while (positionIndex <= size) {
5288
+ let found = false;
5289
+ for (let start = position, startIndex = positionIndex; startIndex <= size; startIndex++, start = nextCodePointOffset(text, start)) {
5290
+ if (operator === "/#" && start !== 0)
5229
5291
  break;
5230
- if (!await match(start, end))
5231
- continue;
5232
- append(slice(position, start));
5233
- const matched = slice(start, end);
5234
- for (const replacement of replacements) {
5235
- if (replacement.quoted)
5236
- append(replacement.value);
5237
- else {
5238
- owner?.reserve({ metadata: 64 + replacement.value.length * 64, payload: Buffer.byteLength(replacement.value), allocatedSlots: replacement.value.length, work: replacement.value.length + 4 });
5239
- const pieces = replacement.value.split("&");
5240
- for (const [index, piece] of pieces.entries()) {
5241
- if (index)
5242
- append(matched);
5243
- append(piece);
5292
+ for (let end = text.length, endIndex = size; endIndex >= startIndex; endIndex--, end = previousCodePointOffset(text, end)) {
5293
+ if (operator === "/%" && end !== text.length)
5294
+ break;
5295
+ if (!await match(start, end, endIndex - startIndex))
5296
+ continue;
5297
+ await append(text, position, start);
5298
+ for (const replacement of replacements) {
5299
+ if (replacement.quoted)
5300
+ await append(replacement.value);
5301
+ else {
5302
+ let fragment = 0;
5303
+ for (let cursor = 0; cursor < replacement.value.length; cursor++) {
5304
+ const pending = stringCheckpoint(work);
5305
+ if (pending)
5306
+ await pending;
5307
+ if (replacement.value[cursor] !== "&")
5308
+ continue;
5309
+ await append(replacement.value, fragment, cursor);
5310
+ await append(text, start, end);
5311
+ fragment = cursor + 1;
5312
+ }
5313
+ await append(replacement.value, fragment);
5244
5314
  }
5245
5315
  }
5316
+ position = end;
5317
+ positionIndex = endIndex;
5318
+ found = true;
5319
+ if (operator !== "//" || end === text.length) {
5320
+ await append(text, end);
5321
+ return result;
5322
+ }
5323
+ if (end === start) {
5324
+ position = nextCodePointOffset(text, end);
5325
+ positionIndex++;
5326
+ await append(text, end, position);
5327
+ }
5328
+ break;
5246
5329
  }
5247
- position = end;
5248
- found = true;
5249
- if (operator !== "//" || end === characters.length) {
5250
- append(slice(end));
5251
- return result;
5252
- }
5253
- if (end === start) {
5254
- append(characters[position]);
5255
- position++;
5256
- }
5257
- break;
5330
+ if (found)
5331
+ break;
5258
5332
  }
5259
- if (found)
5333
+ if (!found) {
5334
+ if (positionIndex === 0)
5335
+ return text;
5336
+ await append(text, position);
5260
5337
  break;
5338
+ }
5261
5339
  }
5262
- if (!found) {
5263
- append(slice(position));
5264
- break;
5265
- }
5340
+ return result;
5341
+ }
5342
+ finally {
5343
+ scratch.close();
5266
5344
  }
5267
- return result;
5268
5345
  }
5269
5346
  async word(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend) {
5270
5347
  return (await this.valueWord(word, state, io, split, pattern, hereString, conditionalPattern, regexAppend)).map(shellValueText);
@@ -5349,16 +5426,17 @@ export class Runtime {
5349
5426
  if (boundary)
5350
5427
  addField();
5351
5428
  };
5352
- const parts = word.parts.map((part) => ({ part, splitText: false }));
5429
+ const parts = word.parts.map((part) => ({ part, splitText: false, io }));
5353
5430
  for (let index = 0; index < parts.length; index++) {
5354
- const { part, splitText } = parts[index];
5431
+ const { part, splitText, io: partIO } = parts[index];
5355
5432
  const quotedPresence = part.quoted && !(arrayOwned && isQuoteMarker(part));
5356
5433
  if (part.kind === "variable" && ["-", "+", ":-", ":+"].includes(part.operator ?? "") && /^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name)) {
5357
5434
  const value = this.variable(state, part.name);
5358
5435
  const missing = value === undefined || (part.operator.startsWith(":") && value === "");
5359
5436
  if (part.operator.endsWith("+") ? !missing : missing) {
5437
+ const operandIO = this.parameterOperandIO(part.alternate, state, partIO);
5360
5438
  scratch?.reserve(part.alternate.parts.length * 32, 0);
5361
- const alternate = part.alternate.parts.map((entry) => ({ part: copyArraySelector(entry, { ...entry, quoted: entry.quoted || part.quoted }), splitText: true }));
5439
+ const alternate = part.alternate.parts.map((entry) => ({ part: copyArraySelector(entry, { ...entry, quoted: entry.quoted || part.quoted }), splitText: true, io: operandIO }));
5362
5440
  if (!alternate.length && part.quoted)
5363
5441
  append("", false, true);
5364
5442
  parts.splice(index + 1, 0, ...alternate);
@@ -5394,7 +5472,7 @@ export class Runtime {
5394
5472
  fields[0].present = false;
5395
5473
  }
5396
5474
  else {
5397
- const value = part.kind === "text" ? part.byteValue ?? part.value : await this.valuePart(part, state, io, hereString);
5475
+ const value = part.kind === "text" ? part.byteValue ?? part.value : await this.valuePart(part, state, partIO, hereString);
5398
5476
  if (part.quoted || !split || state.variables.IFS === "")
5399
5477
  append(value, !part.quoted, quotedPresence || !split || shellValueByteLength(value) > 0);
5400
5478
  else