shfs 0.1.1 → 0.1.3

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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,3 @@
1
- import { map, pipe } from "remeda";
2
-
3
1
  //#region ../compiler/dist/index.mjs
4
2
  /**
5
3
  * Create a literal ExpandedWord.
@@ -62,9 +60,9 @@ function extractPathsFromExpandedWords(words) {
62
60
  }
63
61
  });
64
62
  }
65
- const NEGATIVE_NUMBER_REGEX$2 = /^(?:-\d+(?:\.\d+)?|-\.\d+)$/;
63
+ const NEGATIVE_NUMBER_REGEX = /^(?:-\d+(?:\.\d+)?|-\.\d+)$/;
66
64
  function isNegativeNumberToken(token) {
67
- return NEGATIVE_NUMBER_REGEX$2.test(token);
65
+ return NEGATIVE_NUMBER_REGEX.test(token);
68
66
  }
69
67
  function startsWithLongPrefix(token) {
70
68
  return token.length >= 2 && token[0] === "-" && token[1] === "-";
@@ -81,68 +79,203 @@ function splitNameBeforeEquals(token) {
81
79
  }
82
80
  const SHORT_NAME_REGEX = /^[A-Za-z]$/;
83
81
  const LONG_NAME_REGEX = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
84
- function parseArgs(args, flagDefs) {
82
+ const UNKNOWN_FLAG_PREFIX$2 = "Unknown flag: ";
83
+ function createArgParser(flagDefs) {
85
84
  const index = buildFlagIndex(flagDefs);
86
- const flags$1 = Object.create(null);
85
+ return (args, options) => {
86
+ return parseArgsWithIndex(args, index, options);
87
+ };
88
+ }
89
+ function createWordParser(flagDefs, wordToString) {
90
+ const parseWithIndex = createArgParser(flagDefs);
91
+ return (words, options) => {
92
+ const parsed = parseWithIndex(words.map(wordToString), options);
93
+ const positionalWords = parsed.positionalIndices.flatMap((index) => {
94
+ const word = words[index];
95
+ return word === void 0 ? [] : [word];
96
+ });
97
+ return {
98
+ ...parsed,
99
+ positionalWords
100
+ };
101
+ };
102
+ }
103
+ function parseArgsWithIndex(args, index, options) {
104
+ const normalizedOptions = normalizeOptions$1(options);
105
+ const negativeNumberValueEntry = getNegativeNumberValueEntry(normalizedOptions, index);
106
+ let consumedValueIndices = Object.create(null);
107
+ let flags = Object.create(null);
87
108
  const positional = [];
109
+ const positionalIndices = [];
88
110
  let endOfFlags = false;
89
111
  for (let i = 0; i < args.length; i++) {
90
112
  const token = args[i];
91
113
  if (token === void 0) continue;
92
- if (endOfFlags) {
93
- positional.push(token);
94
- continue;
95
- }
96
- if (token === "--") {
97
- endOfFlags = true;
98
- continue;
99
- }
100
- if (token === "-") {
101
- positional.push(token);
102
- continue;
103
- }
104
- if (isNegativeNumberToken(token)) {
105
- positional.push(token);
106
- continue;
107
- }
108
- if (startsWithLongPrefix(token)) {
109
- i = parseLongToken(args, i, token, index, flags$1);
110
- continue;
111
- }
112
- if (startsWithShortPrefix(token)) {
113
- i = parseShortToken(args, i, token, index, flags$1);
114
- continue;
114
+ const result = processToken({
115
+ args,
116
+ consumedValueIndices,
117
+ endOfFlags,
118
+ flags,
119
+ flagsIndex: index,
120
+ index: i,
121
+ negativeNumberValueEntry,
122
+ positional,
123
+ positionalIndices,
124
+ token,
125
+ unknownFlagPolicy: normalizedOptions.unknownFlagPolicy
126
+ });
127
+ consumedValueIndices = result.consumedValueIndices;
128
+ endOfFlags = result.endOfFlags;
129
+ flags = result.flags;
130
+ i = result.newIndex;
131
+ }
132
+ return {
133
+ consumedValueIndices,
134
+ flags,
135
+ positional,
136
+ positionalIndices
137
+ };
138
+ }
139
+ function processToken(params) {
140
+ const { args, consumedValueIndices, endOfFlags, flags, flagsIndex, index, negativeNumberValueEntry, positional, positionalIndices, token, unknownFlagPolicy } = params;
141
+ if (endOfFlags || token === "-") {
142
+ appendPositional(positional, positionalIndices, token, index);
143
+ return {
144
+ consumedValueIndices,
145
+ endOfFlags,
146
+ flags,
147
+ newIndex: index
148
+ };
149
+ }
150
+ if (token === "--") return {
151
+ consumedValueIndices,
152
+ endOfFlags: true,
153
+ flags,
154
+ newIndex: index
155
+ };
156
+ if (isNegativeNumberToken(token)) {
157
+ if (!negativeNumberValueEntry) {
158
+ appendPositional(positional, positionalIndices, token, index);
159
+ return {
160
+ consumedValueIndices,
161
+ endOfFlags,
162
+ flags,
163
+ newIndex: index
164
+ };
115
165
  }
116
- positional.push(token);
166
+ setValue(flags, consumedValueIndices, negativeNumberValueEntry, token.slice(1), index);
167
+ return {
168
+ consumedValueIndices,
169
+ endOfFlags,
170
+ flags,
171
+ newIndex: index
172
+ };
173
+ }
174
+ const parser = getTokenParser(token);
175
+ if (!parser) {
176
+ appendPositional(positional, positionalIndices, token, index);
177
+ return {
178
+ consumedValueIndices,
179
+ endOfFlags,
180
+ flags,
181
+ newIndex: index
182
+ };
117
183
  }
184
+ const parsed = parsePotentialFlagToken(args, index, token, flagsIndex, flags, consumedValueIndices, unknownFlagPolicy, parser);
185
+ if (!parsed) {
186
+ handleUnrecognizedToken(unknownFlagPolicy, positional, positionalIndices, token, index);
187
+ return {
188
+ consumedValueIndices,
189
+ endOfFlags,
190
+ flags,
191
+ newIndex: index
192
+ };
193
+ }
194
+ return {
195
+ consumedValueIndices: parsed.consumedValueIndices,
196
+ endOfFlags,
197
+ flags: parsed.flags,
198
+ newIndex: parsed.newIndex
199
+ };
200
+ }
201
+ function getTokenParser(token) {
202
+ if (startsWithLongPrefix(token)) return parseLongToken;
203
+ if (startsWithShortPrefix(token)) return parseShortToken;
204
+ }
205
+ function normalizeOptions$1(options) {
118
206
  return {
119
- flags: flags$1,
120
- positional
207
+ negativeNumberPolicy: options?.negativeNumberPolicy ?? "positional",
208
+ negativeNumberFlag: options?.negativeNumberFlag,
209
+ unknownFlagPolicy: options?.unknownFlagPolicy ?? "error"
210
+ };
211
+ }
212
+ function getNegativeNumberValueEntry(options, index) {
213
+ if (options.negativeNumberPolicy === "positional") return;
214
+ if (!options.negativeNumberFlag) throw new Error("negativeNumberFlag is required when negativeNumberPolicy is \"value\".");
215
+ const entry = index.canonical.get(options.negativeNumberFlag);
216
+ if (!entry) throw new Error(`Unknown negativeNumberFlag: "${options.negativeNumberFlag}".`);
217
+ if (!entry.def.takesValue) throw new Error(`negativeNumberFlag "${options.negativeNumberFlag}" must reference a flag that takes a value.`);
218
+ return entry;
219
+ }
220
+ function appendPositional(positional, positionalIndices, token, index) {
221
+ positional.push(token);
222
+ positionalIndices.push(index);
223
+ }
224
+ function parsePotentialFlagToken(args, index, token, flagsIndex, currentFlags, currentConsumedValueIndices, unknownFlagPolicy, parser) {
225
+ if (unknownFlagPolicy === "error") return {
226
+ consumedValueIndices: currentConsumedValueIndices,
227
+ flags: currentFlags,
228
+ newIndex: parser(args, index, token, flagsIndex, currentFlags, currentConsumedValueIndices)
121
229
  };
230
+ const candidateFlags = cloneFlags(currentFlags);
231
+ const candidateConsumedValueIndices = cloneConsumedValueIndices(currentConsumedValueIndices);
232
+ try {
233
+ return {
234
+ consumedValueIndices: candidateConsumedValueIndices,
235
+ flags: candidateFlags,
236
+ newIndex: parser(args, index, token, flagsIndex, candidateFlags, candidateConsumedValueIndices)
237
+ };
238
+ } catch (error) {
239
+ if (isUnknownFlagError(error)) return null;
240
+ throw error;
241
+ }
242
+ }
243
+ function handleUnrecognizedToken(policy, positional, positionalIndices, token, index) {
244
+ if (policy === "positional") appendPositional(positional, positionalIndices, token, index);
245
+ }
246
+ function cloneFlags(source) {
247
+ const cloned = Object.create(null);
248
+ for (const [key, value] of Object.entries(source)) cloned[key] = Array.isArray(value) ? [...value] : value;
249
+ return cloned;
250
+ }
251
+ function cloneConsumedValueIndices(source) {
252
+ const cloned = Object.create(null);
253
+ for (const [key, value] of Object.entries(source)) cloned[key] = [...value];
254
+ return cloned;
122
255
  }
123
256
  function buildFlagIndex(flagDefs) {
257
+ const canonical = /* @__PURE__ */ new Map();
124
258
  const short = /* @__PURE__ */ new Map();
125
259
  const long = /* @__PURE__ */ new Map();
126
- const add = (map$1, token, entry) => {
127
- const prev = map$1.get(token);
260
+ const add = (map, token, entry) => {
261
+ const prev = map.get(token);
128
262
  if (!prev) {
129
- map$1.set(token, entry);
263
+ map.set(token, entry);
130
264
  return;
131
265
  }
132
266
  throw new Error(`Duplicate flag token "${token}" for "${entry.canonical}" and "${prev.canonical}"`);
133
267
  };
134
- for (const [canonical, def] of Object.entries(flagDefs)) {
135
- if (!SHORT_NAME_REGEX.test(def.short)) throw new Error(`Invalid short flag for "${canonical}": "${def.short}". Expected a single letter [A-Za-z].`);
136
- add(short, `-${def.short}`, {
137
- canonical,
268
+ for (const [canonicalName, def] of Object.entries(flagDefs)) {
269
+ if (!SHORT_NAME_REGEX.test(def.short)) throw new Error(`Invalid short flag for "${canonicalName}": "${def.short}". Expected a single letter [A-Za-z].`);
270
+ const entry = {
271
+ canonical: canonicalName,
138
272
  def
139
- });
273
+ };
274
+ canonical.set(canonicalName, entry);
275
+ add(short, `-${def.short}`, entry);
140
276
  if (def.long) {
141
- if (!LONG_NAME_REGEX.test(def.long)) throw new Error(`Invalid long flag for "${canonical}": "${def.long}". Expected [A-Za-z0-9][A-Za-z0-9-]*.`);
142
- add(long, `--${def.long}`, {
143
- canonical,
144
- def
145
- });
277
+ if (!LONG_NAME_REGEX.test(def.long)) throw new Error(`Invalid long flag for "${canonicalName}": "${def.long}". Expected [A-Za-z0-9][A-Za-z0-9-]*.`);
278
+ add(long, `--${def.long}`, entry);
146
279
  }
147
280
  }
148
281
  const isFlagToken = (token) => {
@@ -166,16 +299,17 @@ function buildFlagIndex(flagDefs) {
166
299
  return false;
167
300
  };
168
301
  return {
302
+ canonical,
169
303
  short,
170
304
  long,
171
305
  isFlagToken
172
306
  };
173
307
  }
174
- function parseLongToken(args, index, token, flagsIndex, out) {
308
+ function parseLongToken(args, index, token, flagsIndex, out, consumedValueIndices) {
175
309
  if (startsWithNoLongPrefix(token) && !token.includes("=")) {
176
310
  const base = `--${token.slice(5)}`;
177
311
  const entry$1 = flagsIndex.long.get(base);
178
- if (!entry$1) throw new Error(`Unknown flag: ${token}`);
312
+ if (!entry$1) throwUnknownFlag(token);
179
313
  if (entry$1.def.takesValue) throw new Error(`Flag ${base} takes a value; "${token}" is invalid.`);
180
314
  setBoolean(out, entry$1.canonical, false);
181
315
  return index;
@@ -185,69 +319,91 @@ function parseLongToken(args, index, token, flagsIndex, out) {
185
319
  const name = token.slice(0, eq);
186
320
  const value$1 = token.slice(eq + 1);
187
321
  const entry$1 = flagsIndex.long.get(name);
188
- if (!entry$1) throw new Error(`Unknown flag: ${name}`);
322
+ if (!entry$1) throwUnknownFlag(name);
189
323
  if (!entry$1.def.takesValue) throw new Error(`Flag ${name} does not take a value.`);
190
- setValue(out, entry$1, value$1);
324
+ setValue(out, consumedValueIndices, entry$1, value$1, index);
191
325
  return index;
192
326
  }
193
327
  const entry = flagsIndex.long.get(token);
194
- if (!entry) throw new Error(`Unknown flag: ${token}`);
328
+ if (!entry) throwUnknownFlag(token);
195
329
  if (!entry.def.takesValue) {
196
330
  setBoolean(out, entry.canonical, true);
197
331
  return index;
198
332
  }
199
- const { value, newIndex } = consumeValue(args, index, token, flagsIndex);
200
- setValue(out, entry, value);
333
+ const { newIndex, value, valueIndex } = consumeValue(args, index, token, flagsIndex);
334
+ setValue(out, consumedValueIndices, entry, value, valueIndex);
201
335
  return newIndex;
202
336
  }
203
- function parseShortToken(args, index, token, flagsIndex, out) {
204
- if (token.length >= 3 && token[2] === "=") {
205
- const name = token.slice(0, 2);
206
- const value = token.slice(3);
207
- const entry = flagsIndex.short.get(name);
208
- if (!entry) throw new Error(`Unknown flag: ${name}`);
209
- if (!entry.def.takesValue) throw new Error(`Flag ${name} does not take a value.`);
210
- setValue(out, entry, value);
337
+ function parseShortToken(args, index, token, flagsIndex, out, consumedValueIndices) {
338
+ if (token.length >= 3 && token[2] === "=") return parseShortEqualsToken(index, token, flagsIndex, out, consumedValueIndices);
339
+ if (token.length === 2) return parseSingleShortToken(args, index, token, flagsIndex, out, consumedValueIndices);
340
+ return parseShortClusterToken(args, index, token, flagsIndex, out, consumedValueIndices);
341
+ }
342
+ function parseShortEqualsToken(index, token, flagsIndex, out, consumedValueIndices) {
343
+ const name = token.slice(0, 2);
344
+ const value = token.slice(3);
345
+ const entry = getRequiredShortEntry(flagsIndex, name);
346
+ assertTakesValue(entry, name);
347
+ setValue(out, consumedValueIndices, entry, value, index);
348
+ return index;
349
+ }
350
+ function parseSingleShortToken(args, index, token, flagsIndex, out, consumedValueIndices) {
351
+ const entry = getRequiredShortEntry(flagsIndex, token);
352
+ if (!entry.def.takesValue) {
353
+ setBoolean(out, entry.canonical, true);
211
354
  return index;
212
355
  }
213
- if (token.length === 2) {
214
- const entry = flagsIndex.short.get(token);
215
- if (!entry) throw new Error(`Unknown flag: ${token}`);
216
- if (!entry.def.takesValue) {
217
- setBoolean(out, entry.canonical, true);
218
- return index;
219
- }
220
- const { value, newIndex } = consumeValue(args, index, token, flagsIndex);
221
- setValue(out, entry, value);
222
- return newIndex;
223
- }
356
+ const { newIndex, value, valueIndex } = consumeValue(args, index, token, flagsIndex);
357
+ setValue(out, consumedValueIndices, entry, value, valueIndex);
358
+ return newIndex;
359
+ }
360
+ function parseShortClusterToken(args, index, token, flagsIndex, out, consumedValueIndices) {
224
361
  for (let j = 1; j < token.length; j++) {
225
362
  const ch = token[j] ?? "";
226
- if (!SHORT_NAME_REGEX.test(ch)) throw new Error(`Invalid short flag character "${ch}" in "${token}". Short flags must be letters.`);
363
+ assertValidShortCharacter(token, ch);
227
364
  const name = `-${ch}`;
228
- const entry = flagsIndex.short.get(name);
229
- if (!entry) throw new Error(`Unknown flag: ${name}`);
365
+ const entry = getRequiredShortEntry(flagsIndex, name);
230
366
  if (!entry.def.takesValue) {
231
367
  setBoolean(out, entry.canonical, true);
232
368
  continue;
233
369
  }
234
- const rest = token.slice(j + 1);
235
- if (rest.startsWith("=")) {
236
- setValue(out, entry, rest.slice(1));
237
- return index;
238
- }
239
- if (rest.length === 0) {
240
- const { value, newIndex } = consumeValue(args, index, name, flagsIndex);
241
- setValue(out, entry, value);
242
- return newIndex;
243
- }
244
- const first = rest[0] ?? "";
245
- if (SHORT_NAME_REGEX.test(first) && flagsIndex.short.has(`-${first}`)) throw new Error(`Ambiguous short flag cluster "${token}": ${name} takes a value, but "${rest}" begins with "-${first}" which is also a flag. Use "${name}=${rest}" or pass the value as a separate argument.`);
246
- setValue(out, entry, rest);
370
+ return parseValueFlagInShortCluster(args, index, token, j, name, entry, flagsIndex, out, consumedValueIndices);
371
+ }
372
+ return index;
373
+ }
374
+ function parseValueFlagInShortCluster(args, index, token, flagPosition, name, entry, flagsIndex, out, consumedValueIndices) {
375
+ const rest = token.slice(flagPosition + 1);
376
+ if (rest.startsWith("=")) {
377
+ setValue(out, consumedValueIndices, entry, rest.slice(1), index);
247
378
  return index;
248
379
  }
380
+ if (rest.length === 0) {
381
+ const { newIndex, value, valueIndex } = consumeValue(args, index, name, flagsIndex);
382
+ setValue(out, consumedValueIndices, entry, value, valueIndex);
383
+ return newIndex;
384
+ }
385
+ assertNotAmbiguousShortValue(token, name, rest, flagsIndex);
386
+ setValue(out, consumedValueIndices, entry, rest, index);
249
387
  return index;
250
388
  }
389
+ function getRequiredShortEntry(flagsIndex, name) {
390
+ const entry = flagsIndex.short.get(name);
391
+ if (!entry) throwUnknownFlag(name);
392
+ return entry;
393
+ }
394
+ function assertValidShortCharacter(token, ch) {
395
+ if (SHORT_NAME_REGEX.test(ch)) return;
396
+ throw new Error(`Invalid short flag character "${ch}" in "${token}". Short flags must be letters.`);
397
+ }
398
+ function assertTakesValue(entry, token) {
399
+ if (entry.def.takesValue) return;
400
+ throw new Error(`Flag ${token} does not take a value.`);
401
+ }
402
+ function assertNotAmbiguousShortValue(token, name, rest, flagsIndex) {
403
+ const first = rest[0] ?? "";
404
+ if (!(SHORT_NAME_REGEX.test(first) && flagsIndex.short.has(`-${first}`))) return;
405
+ throw new Error(`Ambiguous short flag cluster "${token}": ${name} takes a value, but "${rest}" begins with "-${first}" which is also a flag. Use "${name}=${rest}" or pass the value as a separate argument.`);
406
+ }
251
407
  function consumeValue(args, index, flagToken, flagsIndex) {
252
408
  const nextIndex = index + 1;
253
409
  if (nextIndex >= args.length) throw new Error(`Flag ${flagToken} requires a value.`);
@@ -257,34 +413,53 @@ function consumeValue(args, index, flagToken, flagsIndex) {
257
413
  if (flagsIndex.isFlagToken(next)) throw new Error(`Flag ${flagToken} requires a value (got "${next}").`);
258
414
  return {
259
415
  value: next,
260
- newIndex: nextIndex
416
+ newIndex: nextIndex,
417
+ valueIndex: nextIndex
261
418
  };
262
419
  }
263
420
  function setBoolean(out, canonical, value) {
264
421
  out[canonical] = value;
265
422
  }
266
- function setValue(out, entry, value) {
423
+ function setValue(out, consumedValueIndices, entry, value, valueIndex) {
267
424
  const { canonical, def } = entry;
268
425
  const existing = out[canonical];
269
426
  if (existing === void 0) {
270
427
  out[canonical] = value;
428
+ recordConsumedValueIndex(consumedValueIndices, canonical, valueIndex);
271
429
  return;
272
430
  }
273
431
  if (!def.multiple) throw new Error(`Duplicate flag "${canonical}". If it is intended to repeat, set { multiple: true } in its definition.`);
274
432
  if (Array.isArray(existing)) {
275
433
  existing.push(value);
434
+ recordConsumedValueIndex(consumedValueIndices, canonical, valueIndex);
276
435
  return;
277
436
  }
278
437
  if (typeof existing === "string") {
279
438
  out[canonical] = [existing, value];
439
+ recordConsumedValueIndex(consumedValueIndices, canonical, valueIndex);
280
440
  return;
281
441
  }
282
442
  throw new Error(`Invalid state for flag "${canonical}".`);
283
443
  }
444
+ function recordConsumedValueIndex(consumedValueIndices, canonical, valueIndex) {
445
+ const existing = consumedValueIndices[canonical];
446
+ if (!existing) {
447
+ consumedValueIndices[canonical] = [valueIndex];
448
+ return;
449
+ }
450
+ existing.push(valueIndex);
451
+ }
452
+ function throwUnknownFlag(token) {
453
+ throw new Error(`${UNKNOWN_FLAG_PREFIX$2}${token}`);
454
+ }
455
+ function isUnknownFlagError(error) {
456
+ if (!(error instanceof Error)) return false;
457
+ return error.message.startsWith(UNKNOWN_FLAG_PREFIX$2);
458
+ }
284
459
  /**
285
460
  * cat command handler for the AST-based compiler.
286
461
  */
287
- const flags = {
462
+ const parseCatArgs = createArgParser({
288
463
  number: {
289
464
  short: "n",
290
465
  takesValue: false
@@ -313,14 +488,17 @@ const flags = {
313
488
  short: "s",
314
489
  takesValue: false
315
490
  }
316
- };
491
+ });
317
492
  /**
318
493
  * Compile a cat command from SimpleCommandIR to StepIR.
319
494
  */
320
495
  function compileCat(cmd$1) {
321
- const parsed = parseArgs(cmd$1.args.map(expandedWordToString), flags);
496
+ const parsed = parseCatArgs(cmd$1.args.map(expandedWordToString));
322
497
  const fileArgs = [];
323
- for (const arg of cmd$1.args) if (!expandedWordToString(arg).startsWith("-")) fileArgs.push(arg);
498
+ for (const positionalIndex of parsed.positionalIndices) {
499
+ const arg = cmd$1.args[positionalIndex];
500
+ if (arg !== void 0) fileArgs.push(arg);
501
+ }
324
502
  const hasInputRedirection = cmd$1.redirections.some((redirection) => redirection.kind === "input");
325
503
  if (fileArgs.length === 0 && !hasInputRedirection) throw new Error("cat requires at least one file");
326
504
  return {
@@ -338,19 +516,45 @@ function compileCat(cmd$1) {
338
516
  };
339
517
  }
340
518
  /**
519
+ * cd command handler for the AST-based compiler.
520
+ */
521
+ const ROOT_DIRECTORY$3 = "/";
522
+ /**
523
+ * Compile a cd command from SimpleCommandIR to StepIR.
524
+ */
525
+ function compileCd(cmd$1) {
526
+ if (cmd$1.args.length > 1) throw new Error("cd accepts at most one path");
527
+ return {
528
+ cmd: "cd",
529
+ args: { path: cmd$1.args[0] ?? literal(ROOT_DIRECTORY$3) }
530
+ };
531
+ }
532
+ /**
341
533
  * cp command handler for the AST-based compiler.
342
534
  */
535
+ const parseCpArgs = createWordParser({
536
+ force: {
537
+ short: "f",
538
+ takesValue: false
539
+ },
540
+ interactive: {
541
+ short: "i",
542
+ takesValue: false
543
+ },
544
+ recursive: {
545
+ short: "r",
546
+ takesValue: false
547
+ }
548
+ }, expandedWordToString);
343
549
  /**
344
550
  * Compile a cp command from SimpleCommandIR to StepIR.
345
551
  */
346
552
  function compileCp(cmd$1) {
347
- let recursive = false;
348
- const filteredArgs = [];
349
- for (const arg of cmd$1.args) {
350
- const argStr = expandedWordToString(arg);
351
- if (argStr === "-r") recursive = true;
352
- else if (argStr !== "-f" && argStr !== "-i") filteredArgs.push(arg);
353
- }
553
+ const parsed = parseCpArgs(cmd$1.args, { unknownFlagPolicy: "positional" });
554
+ const recursive = parsed.flags.recursive === true;
555
+ const force = parsed.flags.force === true;
556
+ const interactive = parsed.flags.interactive === true;
557
+ const filteredArgs = parsed.positionalWords;
354
558
  if (filteredArgs.length < 2) throw new Error("cp requires source and destination");
355
559
  const dest = filteredArgs.pop();
356
560
  if (!dest) throw new Error("cp requires source and destination");
@@ -358,6 +562,8 @@ function compileCp(cmd$1) {
358
562
  cmd: "cp",
359
563
  args: {
360
564
  dest,
565
+ force,
566
+ interactive,
361
567
  recursive,
362
568
  srcs: filteredArgs
363
569
  }
@@ -366,67 +572,102 @@ function compileCp(cmd$1) {
366
572
  /**
367
573
  * head command handler for the AST-based compiler.
368
574
  */
369
- const NEGATIVE_NUMBER_REGEX$1 = /^-\d+$/;
575
+ const DEFAULT_LINE_COUNT$1 = 10;
576
+ const parseHeadArgs = createWordParser({ lines: {
577
+ multiple: true,
578
+ short: "n",
579
+ takesValue: true
580
+ } }, expandedWordToString);
581
+ const MISSING_N_VALUE_PREFIX$1 = "Flag -n requires a value";
582
+ const UNKNOWN_FLAG_PREFIX$1 = "Unknown flag:";
370
583
  /**
371
584
  * Compile a head command from SimpleCommandIR to StepIR.
372
585
  */
373
586
  function compileHead(cmd$1) {
374
- let n = 10;
375
- const files$1 = [];
376
- let skipNext = false;
377
- for (let i = 0; i < cmd$1.args.length; i++) {
378
- if (skipNext) {
379
- skipNext = false;
380
- continue;
381
- }
382
- const arg = cmd$1.args[i];
383
- if (!arg) continue;
384
- const argStr = expandedWordToString(arg);
385
- if (argStr === "-n") {
386
- const numArg = cmd$1.args[i + 1];
387
- if (!numArg) throw new Error("head -n requires a number");
388
- n = Number(expandedWordToString(numArg));
389
- if (!Number.isFinite(n)) throw new Error("Invalid head count");
390
- skipNext = true;
391
- } else if (argStr.startsWith("-") && NEGATIVE_NUMBER_REGEX$1.test(argStr)) n = Number(argStr.slice(1));
392
- else if (argStr.startsWith("-")) throw new Error("Unknown head option");
393
- else files$1.push(arg);
394
- }
587
+ const parsed = parseHeadArgsOrThrow(cmd$1.args);
588
+ const n = parseHeadCount(parsed.flags.lines);
395
589
  return {
396
590
  cmd: "head",
397
591
  args: {
398
- files: files$1,
592
+ files: parsed.positionalWords,
399
593
  n
400
594
  }
401
595
  };
402
596
  }
597
+ function parseHeadCount(value) {
598
+ const lastValue = getLastValueToken$1(value);
599
+ if (lastValue === void 0) return DEFAULT_LINE_COUNT$1;
600
+ const parsedValue = Number(lastValue);
601
+ if (!Number.isFinite(parsedValue)) throw new Error("Invalid head count");
602
+ return parsedValue;
603
+ }
604
+ function getLastValueToken$1(value) {
605
+ if (value === void 0) return;
606
+ if (typeof value === "string") return value;
607
+ if (Array.isArray(value)) return value.at(-1);
608
+ throw new Error("Invalid head count");
609
+ }
610
+ function parseHeadArgsOrThrow(args) {
611
+ try {
612
+ return parseHeadArgs(args, {
613
+ negativeNumberFlag: "lines",
614
+ negativeNumberPolicy: "value"
615
+ });
616
+ } catch (error) {
617
+ if (!(error instanceof Error)) throw new Error("Unknown head option");
618
+ if (error.message.startsWith(MISSING_N_VALUE_PREFIX$1)) throw new Error("head -n requires a number");
619
+ if (error.message.startsWith(UNKNOWN_FLAG_PREFIX$1)) throw new Error("Unknown head option");
620
+ throw error;
621
+ }
622
+ }
403
623
  /**
404
624
  * ls command handler for the AST-based compiler.
405
625
  */
626
+ const parseLsArgs = createWordParser({
627
+ longFormat: {
628
+ short: "l",
629
+ takesValue: false
630
+ },
631
+ showAll: {
632
+ short: "a",
633
+ takesValue: false
634
+ }
635
+ }, expandedWordToString);
406
636
  /**
407
637
  * Compile an ls command from SimpleCommandIR to StepIR.
408
638
  */
409
639
  function compileLs(cmd$1) {
640
+ const parsed = parseLsArgs(cmd$1.args, { unknownFlagPolicy: "positional" });
641
+ const paths = parsed.positionalWords.length === 0 ? [literal(".")] : parsed.positionalWords;
410
642
  return {
411
643
  cmd: "ls",
412
- args: { paths: cmd$1.args.length === 0 ? [literal(".")] : cmd$1.args }
644
+ args: {
645
+ longFormat: parsed.flags.longFormat === true,
646
+ paths,
647
+ showAll: parsed.flags.showAll === true
648
+ }
413
649
  };
414
650
  }
415
651
  /**
416
652
  * mkdir command handler for the AST-based compiler.
417
653
  */
654
+ const parseMkdirArgs = createWordParser({ parents: {
655
+ short: "p",
656
+ takesValue: false
657
+ } }, expandedWordToString);
418
658
  /**
419
659
  * Compile a mkdir command from SimpleCommandIR to StepIR.
420
660
  */
421
661
  function compileMkdir(cmd$1) {
422
- let recursive = false;
423
- const paths = [];
424
- for (const arg of cmd$1.args) if (expandedWordToString(arg) === "-p") recursive = true;
425
- else paths.push(arg);
662
+ const parsed = parseMkdirArgs(cmd$1.args, { unknownFlagPolicy: "positional" });
663
+ const parents = parsed.flags.parents === true;
664
+ const recursive = parents;
665
+ const paths = parsed.positionalWords;
426
666
  if (paths.length === 0) throw new Error("mkdir requires at least one path");
427
667
  return {
428
668
  cmd: "mkdir",
429
669
  args: {
670
+ parents,
430
671
  paths,
431
672
  recursive
432
673
  }
@@ -435,15 +676,24 @@ function compileMkdir(cmd$1) {
435
676
  /**
436
677
  * mv command handler for the AST-based compiler.
437
678
  */
679
+ const parseMvArgs = createWordParser({
680
+ force: {
681
+ short: "f",
682
+ takesValue: false
683
+ },
684
+ interactive: {
685
+ short: "i",
686
+ takesValue: false
687
+ }
688
+ }, expandedWordToString);
438
689
  /**
439
690
  * Compile a mv command from SimpleCommandIR to StepIR.
440
691
  */
441
692
  function compileMv(cmd$1) {
442
- const filteredArgs = [];
443
- for (const arg of cmd$1.args) {
444
- const argStr = expandedWordToString(arg);
445
- if (argStr !== "-f" && argStr !== "-i") filteredArgs.push(arg);
446
- }
693
+ const parsed = parseMvArgs(cmd$1.args, { unknownFlagPolicy: "positional" });
694
+ const force = parsed.flags.force === true;
695
+ const interactive = parsed.flags.interactive === true;
696
+ const filteredArgs = parsed.positionalWords;
447
697
  if (filteredArgs.length < 2) throw new Error("mv requires source and destination");
448
698
  const dest = filteredArgs.pop();
449
699
  if (!dest) throw new Error("mv requires source and destination");
@@ -451,28 +701,54 @@ function compileMv(cmd$1) {
451
701
  cmd: "mv",
452
702
  args: {
453
703
  dest,
704
+ force,
705
+ interactive,
454
706
  srcs: filteredArgs
455
707
  }
456
708
  };
457
709
  }
458
710
  /**
711
+ * Compile a pwd command from SimpleCommandIR to StepIR.
712
+ */
713
+ function compilePwd(cmd$1) {
714
+ if (cmd$1.args.length > 0) throw new Error("pwd does not take any arguments");
715
+ return {
716
+ cmd: "pwd",
717
+ args: {}
718
+ };
719
+ }
720
+ /**
459
721
  * rm command handler for the AST-based compiler.
460
722
  */
723
+ const parseRmArgs = createWordParser({
724
+ force: {
725
+ short: "f",
726
+ takesValue: false
727
+ },
728
+ interactive: {
729
+ short: "i",
730
+ takesValue: false
731
+ },
732
+ recursive: {
733
+ short: "r",
734
+ takesValue: false
735
+ }
736
+ }, expandedWordToString);
461
737
  /**
462
738
  * Compile a rm command from SimpleCommandIR to StepIR.
463
739
  */
464
740
  function compileRm(cmd$1) {
465
- let recursive = false;
466
- const paths = [];
467
- for (const arg of cmd$1.args) {
468
- const argStr = expandedWordToString(arg);
469
- if (argStr === "-r") recursive = true;
470
- else if (argStr !== "-f" && argStr !== "-i") paths.push(arg);
471
- }
741
+ const parsed = parseRmArgs(cmd$1.args, { unknownFlagPolicy: "positional" });
742
+ const recursive = parsed.flags.recursive === true;
743
+ const force = parsed.flags.force === true;
744
+ const interactive = parsed.flags.interactive === true;
745
+ const paths = parsed.positionalWords;
472
746
  if (paths.length === 0) throw new Error("rm requires at least one path");
473
747
  return {
474
748
  cmd: "rm",
475
749
  args: {
750
+ force,
751
+ interactive,
476
752
  paths,
477
753
  recursive
478
754
  }
@@ -481,64 +757,101 @@ function compileRm(cmd$1) {
481
757
  /**
482
758
  * tail command handler for the AST-based compiler.
483
759
  */
484
- const NEGATIVE_NUMBER_REGEX = /^-\d+$/;
760
+ const DEFAULT_LINE_COUNT = 10;
761
+ const parseTailArgs = createWordParser({ lines: {
762
+ multiple: true,
763
+ short: "n",
764
+ takesValue: true
765
+ } }, expandedWordToString);
766
+ const MISSING_N_VALUE_PREFIX = "Flag -n requires a value";
767
+ const UNKNOWN_FLAG_PREFIX = "Unknown flag:";
485
768
  /**
486
769
  * Compile a tail command from SimpleCommandIR to StepIR.
487
770
  */
488
771
  function compileTail(cmd$1) {
489
- let n = 10;
490
- const files$1 = [];
491
- let skipNext = false;
492
- for (let i = 0; i < cmd$1.args.length; i++) {
493
- if (skipNext) {
494
- skipNext = false;
495
- continue;
496
- }
497
- const arg = cmd$1.args[i];
498
- if (!arg) continue;
499
- const argStr = expandedWordToString(arg);
500
- if (argStr === "-n") {
501
- const numArg = cmd$1.args[i + 1];
502
- if (!numArg) throw new Error("tail -n requires a number");
503
- n = Number(expandedWordToString(numArg));
504
- if (!Number.isFinite(n)) throw new Error("Invalid tail count");
505
- skipNext = true;
506
- } else if (argStr.startsWith("-") && NEGATIVE_NUMBER_REGEX.test(argStr)) n = Number(argStr.slice(1));
507
- else if (argStr.startsWith("-")) throw new Error("Unknown tail option");
508
- else files$1.push(arg);
509
- }
772
+ const parsed = parseTailArgsOrThrow(cmd$1.args);
773
+ const n = parseTailCount(parsed.flags.lines);
510
774
  return {
511
775
  cmd: "tail",
512
776
  args: {
513
- files: files$1,
777
+ files: parsed.positionalWords,
514
778
  n
515
779
  }
516
780
  };
517
781
  }
782
+ function parseTailCount(value) {
783
+ const lastValue = getLastValueToken(value);
784
+ if (lastValue === void 0) return DEFAULT_LINE_COUNT;
785
+ const parsedValue = Number(lastValue);
786
+ if (!Number.isFinite(parsedValue)) throw new Error("Invalid tail count");
787
+ return parsedValue;
788
+ }
789
+ function getLastValueToken(value) {
790
+ if (value === void 0) return;
791
+ if (typeof value === "string") return value;
792
+ if (Array.isArray(value)) return value.at(-1);
793
+ throw new Error("Invalid tail count");
794
+ }
795
+ function parseTailArgsOrThrow(args) {
796
+ try {
797
+ return parseTailArgs(args, {
798
+ negativeNumberFlag: "lines",
799
+ negativeNumberPolicy: "value"
800
+ });
801
+ } catch (error) {
802
+ throw normalizeTailParseError(error);
803
+ }
804
+ }
805
+ function normalizeTailParseError(error) {
806
+ if (!(error instanceof Error)) return /* @__PURE__ */ new Error("Unknown tail option");
807
+ if (error.message.startsWith(MISSING_N_VALUE_PREFIX)) return /* @__PURE__ */ new Error("tail -n requires a number");
808
+ if (error.message.startsWith(UNKNOWN_FLAG_PREFIX)) return /* @__PURE__ */ new Error("Unknown tail option");
809
+ return error;
810
+ }
518
811
  /**
519
812
  * touch command handler for the AST-based compiler.
520
813
  */
814
+ const parseTouchArgs = createWordParser({
815
+ accessTimeOnly: {
816
+ short: "a",
817
+ takesValue: false
818
+ },
819
+ modificationTimeOnly: {
820
+ short: "m",
821
+ takesValue: false
822
+ }
823
+ }, expandedWordToString);
521
824
  /**
522
825
  * Compile a touch command from SimpleCommandIR to StepIR.
523
826
  */
524
827
  function compileTouch(cmd$1) {
525
- const files$1 = [];
526
- for (const arg of cmd$1.args) if (!expandedWordToString(arg).startsWith("-")) files$1.push(arg);
828
+ const parsed = parseTouchArgs(cmd$1.args, { unknownFlagPolicy: "positional" });
829
+ const accessTimeOnly = parsed.flags.accessTimeOnly === true;
830
+ const modificationTimeOnly = parsed.flags.modificationTimeOnly === true;
831
+ const files$1 = parsed.positionalWords.filter((arg) => {
832
+ return !expandedWordToString(arg).startsWith("-");
833
+ });
527
834
  if (files$1.length === 0) throw new Error("touch requires at least one file");
528
835
  return {
529
836
  cmd: "touch",
530
- args: { files: files$1 }
837
+ args: {
838
+ accessTimeOnly,
839
+ files: files$1,
840
+ modificationTimeOnly
841
+ }
531
842
  };
532
843
  }
533
844
  let CommandHandler;
534
845
  (function(_CommandHandler) {
535
846
  const handlers = {
536
847
  cat: compileCat,
848
+ cd: compileCd,
537
849
  cp: compileCp,
538
850
  head: compileHead,
539
851
  ls: compileLs,
540
852
  mkdir: compileMkdir,
541
853
  mv: compileMv,
854
+ pwd: compilePwd,
542
855
  rm: compileRm,
543
856
  tail: compileTail,
544
857
  touch: compileTouch
@@ -880,8 +1193,8 @@ function createEmptyFlags() {
880
1193
  /**
881
1194
  * Check if any quote flag is set.
882
1195
  */
883
- function isQuoted(flags$1) {
884
- return flags$1.quoted || flags$1.singleQuoted || flags$1.doubleQuoted;
1196
+ function isQuoted(flags) {
1197
+ return flags.quoted || flags.singleQuoted || flags.doubleQuoted;
885
1198
  }
886
1199
  /**
887
1200
  * Human-readable names for token kinds.
@@ -923,11 +1236,11 @@ var Token = class Token$1 {
923
1236
  spelling;
924
1237
  span;
925
1238
  flags;
926
- constructor(kind, spelling, span, flags$1 = createEmptyFlags()) {
1239
+ constructor(kind, spelling, span, flags = createEmptyFlags()) {
927
1240
  this.kind = kind;
928
1241
  this.spelling = spelling;
929
1242
  this.span = span;
930
- this.flags = flags$1;
1243
+ this.flags = flags;
931
1244
  }
932
1245
  /**
933
1246
  * Get the canonical spelling for a token kind.
@@ -1184,16 +1497,16 @@ var Scanner = class {
1184
1497
  readComplexWord(start) {
1185
1498
  this.stateCtx.reset();
1186
1499
  let spelling = "";
1187
- let flags$1 = createEmptyFlags();
1500
+ let flags = createEmptyFlags();
1188
1501
  while (!this.source.eof) {
1189
1502
  const c = this.source.peek();
1190
1503
  if (!this.stateCtx.inQuotes && this.isWordBoundary(c)) break;
1191
1504
  const result = this.processChar(c);
1192
1505
  spelling += result.chars;
1193
- flags$1 = mergeFlags(flags$1, result.flags);
1506
+ flags = mergeFlags(flags, result.flags);
1194
1507
  if (result.done) break;
1195
1508
  }
1196
- return this.classifyWord(spelling, start, flags$1);
1509
+ return this.classifyWord(spelling, start, flags);
1197
1510
  }
1198
1511
  processChar(c) {
1199
1512
  if (c === "'" && !this.stateCtx.inDoubleQuote) return this.handleSingleQuote();
@@ -1211,11 +1524,11 @@ var Scanner = class {
1211
1524
  }
1212
1525
  handleGlobChar(c) {
1213
1526
  this.source.advance();
1214
- const flags$1 = createEmptyFlags();
1215
- flags$1.containsGlob = true;
1527
+ const flags = createEmptyFlags();
1528
+ flags.containsGlob = true;
1216
1529
  return {
1217
1530
  chars: c,
1218
- flags: flags$1,
1531
+ flags,
1219
1532
  done: false
1220
1533
  };
1221
1534
  }
@@ -1231,12 +1544,12 @@ var Scanner = class {
1231
1544
  }
1232
1545
  this.stateCtx.push(LexerState.SINGLE_QUOTED);
1233
1546
  this.source.advance();
1234
- const flags$1 = createEmptyFlags();
1235
- flags$1.singleQuoted = true;
1236
- flags$1.quoted = true;
1547
+ const flags = createEmptyFlags();
1548
+ flags.singleQuoted = true;
1549
+ flags.quoted = true;
1237
1550
  return {
1238
1551
  chars: "",
1239
- flags: flags$1,
1552
+ flags,
1240
1553
  done: false
1241
1554
  };
1242
1555
  }
@@ -1252,12 +1565,12 @@ var Scanner = class {
1252
1565
  }
1253
1566
  this.stateCtx.push(LexerState.DOUBLE_QUOTED);
1254
1567
  this.source.advance();
1255
- const flags$1 = createEmptyFlags();
1256
- flags$1.doubleQuoted = true;
1257
- flags$1.quoted = true;
1568
+ const flags = createEmptyFlags();
1569
+ flags.doubleQuoted = true;
1570
+ flags.quoted = true;
1258
1571
  return {
1259
1572
  chars: "",
1260
- flags: flags$1,
1573
+ flags,
1261
1574
  done: false
1262
1575
  };
1263
1576
  }
@@ -1317,11 +1630,11 @@ var Scanner = class {
1317
1630
  if (!this.source.eof) result += this.source.advance();
1318
1631
  } else result += this.source.advance();
1319
1632
  }
1320
- const flags$1 = createEmptyFlags();
1321
- flags$1.containsExpansion = true;
1633
+ const flags = createEmptyFlags();
1634
+ flags.containsExpansion = true;
1322
1635
  return {
1323
1636
  chars: result,
1324
- flags: flags$1,
1637
+ flags,
1325
1638
  done: false
1326
1639
  };
1327
1640
  }
@@ -1332,11 +1645,11 @@ var Scanner = class {
1332
1645
  if (this.source.peek() === "]") result += this.source.advance();
1333
1646
  while (!this.source.eof && this.source.peek() !== "]") result += this.source.advance();
1334
1647
  if (this.source.peek() === "]") result += this.source.advance();
1335
- const flags$1 = createEmptyFlags();
1336
- flags$1.containsGlob = true;
1648
+ const flags = createEmptyFlags();
1649
+ flags.containsGlob = true;
1337
1650
  return {
1338
1651
  chars: result,
1339
- flags: flags$1,
1652
+ flags,
1340
1653
  done: false
1341
1654
  };
1342
1655
  }
@@ -1351,10 +1664,10 @@ var Scanner = class {
1351
1664
  if (this.source.peek() === quoteChar) result += this.source.advance();
1352
1665
  return result;
1353
1666
  }
1354
- classifyWord(spelling, start, flags$1) {
1355
- if (NUMBER_PATTERN.test(spelling)) return this.makeToken(TokenKind.NUMBER, spelling, start, flags$1);
1356
- if (NAME_PATTERN.test(spelling)) return this.makeToken(TokenKind.NAME, spelling, start, flags$1);
1357
- return this.makeToken(TokenKind.WORD, spelling, start, flags$1);
1667
+ classifyWord(spelling, start, flags) {
1668
+ if (NUMBER_PATTERN.test(spelling)) return this.makeToken(TokenKind.NUMBER, spelling, start, flags);
1669
+ if (NAME_PATTERN.test(spelling)) return this.makeToken(TokenKind.NAME, spelling, start, flags);
1670
+ return this.makeToken(TokenKind.WORD, spelling, start, flags);
1358
1671
  }
1359
1672
  skipWhitespace() {
1360
1673
  while (!this.source.eof) {
@@ -1377,8 +1690,8 @@ var Scanner = class {
1377
1690
  isWordBoundary(c) {
1378
1691
  return WORD_BOUNDARY_CHARS.has(c) || c === "\0";
1379
1692
  }
1380
- makeToken(kind, spelling, start, flags$1 = createEmptyFlags()) {
1381
- return new Token(kind, spelling, start.span(this.source.position), flags$1);
1693
+ makeToken(kind, spelling, start, flags = createEmptyFlags()) {
1694
+ return new Token(kind, spelling, start.span(this.source.position), flags);
1382
1695
  }
1383
1696
  };
1384
1697
  /**
@@ -2153,31 +2466,187 @@ function collect() {
2153
2466
 
2154
2467
  //#endregion
2155
2468
  //#region src/operator/cat/cat.ts
2156
- function cat(fs) {
2469
+ function isLineRecord(record) {
2470
+ return record.kind === "line";
2471
+ }
2472
+ function formatNonPrinting(text) {
2473
+ let formatted = "";
2474
+ for (const char of text) {
2475
+ const code = char.charCodeAt(0);
2476
+ if (code === 9) {
2477
+ formatted += " ";
2478
+ continue;
2479
+ }
2480
+ if (code < 32) {
2481
+ formatted += `^${String.fromCharCode(code + 64)}`;
2482
+ continue;
2483
+ }
2484
+ if (code === 127) {
2485
+ formatted += "^?";
2486
+ continue;
2487
+ }
2488
+ formatted += char;
2489
+ }
2490
+ return formatted;
2491
+ }
2492
+ function renderLineText(text, lineNumber, options) {
2493
+ let rendered = text;
2494
+ const showNonprinting = options.showAll || options.showNonprinting;
2495
+ const showTabs = options.showAll || options.showTabs;
2496
+ const showEnds = options.showAll || options.showEnds;
2497
+ if (showNonprinting) rendered = formatNonPrinting(rendered);
2498
+ if (showTabs) rendered = rendered.replaceAll(" ", "^I");
2499
+ if (showEnds) rendered = `${rendered}$`;
2500
+ if (lineNumber !== null) rendered = `${lineNumber.toString().padStart(6, " ")}\t${rendered}`;
2501
+ return rendered;
2502
+ }
2503
+ function normalizeOptions(options) {
2504
+ return {
2505
+ numberLines: options?.numberLines ?? false,
2506
+ numberNonBlank: options?.numberNonBlank ?? false,
2507
+ showAll: options?.showAll ?? false,
2508
+ showEnds: options?.showEnds ?? false,
2509
+ showNonprinting: options?.showNonprinting ?? false,
2510
+ showTabs: options?.showTabs ?? false,
2511
+ squeezeBlank: options?.squeezeBlank ?? false
2512
+ };
2513
+ }
2514
+ function nextRenderedLine(text, state, options) {
2515
+ const isBlank = text.length === 0;
2516
+ if (options.squeezeBlank && isBlank && state.previousWasBlank) return {
2517
+ isSkipped: true,
2518
+ lineNumber: null,
2519
+ text
2520
+ };
2521
+ state.previousWasBlank = isBlank;
2522
+ return {
2523
+ isSkipped: false,
2524
+ lineNumber: (options.numberNonBlank ? !isBlank : options.numberLines) ? state.renderedLineNumber++ : null,
2525
+ text
2526
+ };
2527
+ }
2528
+ async function* emitLineRecord(record, state, options) {
2529
+ const rendered = nextRenderedLine(record.text, state, options);
2530
+ if (rendered.isSkipped) return;
2531
+ yield {
2532
+ ...record,
2533
+ text: renderLineText(rendered.text, rendered.lineNumber, options)
2534
+ };
2535
+ }
2536
+ async function* emitJsonRecord(value, state, options) {
2537
+ const rendered = nextRenderedLine(JSON.stringify(value), state, options);
2538
+ if (rendered.isSkipped) return;
2539
+ yield {
2540
+ kind: "line",
2541
+ text: renderLineText(rendered.text, rendered.lineNumber, options)
2542
+ };
2543
+ }
2544
+ async function* emitFileLines(fs, path, state, options) {
2545
+ let sourceLineNum = 1;
2546
+ for await (const rawText of fs.readLines(path)) {
2547
+ const rendered = nextRenderedLine(rawText, state, options);
2548
+ if (rendered.isSkipped) continue;
2549
+ yield {
2550
+ file: path,
2551
+ kind: "line",
2552
+ lineNum: sourceLineNum++,
2553
+ text: renderLineText(rendered.text, rendered.lineNumber, options)
2554
+ };
2555
+ }
2556
+ }
2557
+ function cat(fs, options) {
2558
+ const normalized = normalizeOptions(options);
2559
+ const state = {
2560
+ previousWasBlank: false,
2561
+ renderedLineNumber: 1
2562
+ };
2157
2563
  return async function* (input) {
2158
- for await (const file of input) {
2159
- let lineNum = 1;
2160
- for await (const text of fs.readLines(file.path)) yield {
2161
- file: file.path,
2162
- kind: "line",
2163
- lineNum: lineNum++,
2164
- text
2165
- };
2564
+ for await (const record of input) {
2565
+ if (isLineRecord(record)) {
2566
+ yield* emitLineRecord(record, state, normalized);
2567
+ continue;
2568
+ }
2569
+ if (record.kind === "json") {
2570
+ yield* emitJsonRecord(record.value, state, normalized);
2571
+ continue;
2572
+ }
2573
+ yield* emitFileLines(fs, record.path, state, normalized);
2166
2574
  }
2167
2575
  };
2168
2576
  }
2169
2577
 
2170
2578
  //#endregion
2171
2579
  //#region src/operator/cp/cp.ts
2580
+ const TRAILING_SLASH_REGEX$3 = /\/+$/;
2581
+ const MULTIPLE_SLASH_REGEX$3 = /\/+/g;
2582
+ function trimTrailingSlash$2(path) {
2583
+ return path.replace(TRAILING_SLASH_REGEX$3, "");
2584
+ }
2585
+ function joinPath$1(base, suffix) {
2586
+ return `${trimTrailingSlash$2(base)}/${suffix}`.replace(MULTIPLE_SLASH_REGEX$3, "/");
2587
+ }
2588
+ function basename$1(path) {
2589
+ const normalized = trimTrailingSlash$2(path);
2590
+ const slashIndex = normalized.lastIndexOf("/");
2591
+ if (slashIndex === -1) return normalized;
2592
+ return normalized.slice(slashIndex + 1);
2593
+ }
2594
+ async function isDirectory$1(fs, path) {
2595
+ try {
2596
+ return (await fs.stat(path)).isDirectory;
2597
+ } catch {
2598
+ return false;
2599
+ }
2600
+ }
2601
+ async function assertCanWriteDestination(fs, path, force, interactive) {
2602
+ if (!await fs.exists(path)) return;
2603
+ if (interactive) throw new Error(`cp: destination exists (interactive): ${path}`);
2604
+ if (!force) throw new Error(`cp: destination exists (use -f to overwrite): ${path}`);
2605
+ }
2606
+ async function copyFileWithPolicy(fs, src, dest, force, interactive) {
2607
+ await assertCanWriteDestination(fs, dest, force, interactive);
2608
+ const content = await fs.readFile(src);
2609
+ await fs.writeFile(dest, content);
2610
+ }
2611
+ async function copyDirectoryRecursive(fs, srcDir, destDir, force, interactive) {
2612
+ const normalizedSrc = trimTrailingSlash$2(srcDir);
2613
+ const glob$1 = `${normalizedSrc}/**/*`;
2614
+ for await (const srcPath of fs.readdir(glob$1)) {
2615
+ const targetPath = joinPath$1(destDir, srcPath.slice(normalizedSrc.length + 1));
2616
+ if ((await fs.stat(srcPath)).isDirectory) continue;
2617
+ await copyFileWithPolicy(fs, srcPath, targetPath, force, interactive);
2618
+ }
2619
+ }
2172
2620
  function cp(fs) {
2173
- return async ({ src, dest }) => {
2174
- const content = await fs.readFile(src);
2175
- await fs.writeFile(dest, content);
2621
+ return async ({ srcs, dest, force = false, interactive = false, recursive }) => {
2622
+ if (srcs.length === 0) throw new Error("cp requires at least one source");
2623
+ const destinationIsDirectory = await isDirectory$1(fs, dest);
2624
+ if (srcs.length > 1 && !destinationIsDirectory) throw new Error("cp destination must be a directory for multiple sources");
2625
+ for (const src of srcs) {
2626
+ const srcStat = await fs.stat(src);
2627
+ const targetPath = destinationIsDirectory || srcs.length > 1 ? joinPath$1(dest, basename$1(src)) : dest;
2628
+ if (srcStat.isDirectory) {
2629
+ if (!recursive) throw new Error(`cp: omitting directory "${src}" (use -r)`);
2630
+ await copyDirectoryRecursive(fs, src, targetPath, force, interactive);
2631
+ continue;
2632
+ }
2633
+ await copyFileWithPolicy(fs, src, targetPath, force, interactive);
2634
+ }
2176
2635
  };
2177
2636
  }
2178
2637
 
2179
2638
  //#endregion
2180
2639
  //#region src/operator/head/head.ts
2640
+ function headLines(n) {
2641
+ return async function* (input) {
2642
+ let emitted = 0;
2643
+ for await (const line of input) {
2644
+ if (emitted >= n) break;
2645
+ emitted++;
2646
+ yield line;
2647
+ }
2648
+ };
2649
+ }
2181
2650
  function headWithN(fs, n) {
2182
2651
  return async function* (input) {
2183
2652
  for await (const file of input) {
@@ -2197,11 +2666,21 @@ function headWithN(fs, n) {
2197
2666
 
2198
2667
  //#endregion
2199
2668
  //#region src/operator/ls/ls.ts
2200
- async function* ls(fs, path) {
2201
- for await (const p of fs.readdir(path)) yield {
2202
- kind: "file",
2203
- path: p
2204
- };
2669
+ function basename(path) {
2670
+ const normalized = path.replace(/\/+$/g, "");
2671
+ const slashIndex = normalized.lastIndexOf("/");
2672
+ if (slashIndex === -1) return normalized;
2673
+ return normalized.slice(slashIndex + 1);
2674
+ }
2675
+ async function* ls(fs, path, options) {
2676
+ const showAll = options?.showAll ?? false;
2677
+ for await (const listedPath of fs.readdir(path)) {
2678
+ if (!showAll && basename(listedPath).startsWith(".")) continue;
2679
+ yield {
2680
+ kind: "file",
2681
+ path: listedPath
2682
+ };
2683
+ }
2205
2684
  }
2206
2685
 
2207
2686
  //#endregion
@@ -2214,27 +2693,42 @@ function mkdir(fs) {
2214
2693
 
2215
2694
  //#endregion
2216
2695
  //#region src/operator/mv/mv.ts
2217
- const MULTIPLE_SLASH_REGEX = /\/+/g;
2696
+ const TRAILING_SLASH_REGEX$2 = /\/+$/;
2697
+ const MULTIPLE_SLASH_REGEX$2 = /\/+/g;
2698
+ function trimTrailingSlash$1(path) {
2699
+ return path.replace(TRAILING_SLASH_REGEX$2, "");
2700
+ }
2218
2701
  function extractFileName(path) {
2219
- const lastSlashIndex = path.lastIndexOf("/");
2220
- if (lastSlashIndex === -1) return path;
2221
- return path.slice(lastSlashIndex + 1);
2702
+ const normalized = trimTrailingSlash$1(path);
2703
+ const lastSlashIndex = normalized.lastIndexOf("/");
2704
+ if (lastSlashIndex === -1) return normalized;
2705
+ return normalized.slice(lastSlashIndex + 1);
2706
+ }
2707
+ function joinPath(base, suffix) {
2708
+ return `${trimTrailingSlash$1(base)}/${suffix}`.replace(MULTIPLE_SLASH_REGEX$2, "/");
2709
+ }
2710
+ async function isDirectory(fs, path) {
2711
+ try {
2712
+ return (await fs.stat(path)).isDirectory;
2713
+ } catch {
2714
+ return false;
2715
+ }
2716
+ }
2717
+ async function assertCanMoveToDestination(fs, dest, force, interactive) {
2718
+ if (!await fs.exists(dest)) return;
2719
+ if (interactive) throw new Error(`mv: destination exists (interactive): ${dest}`);
2720
+ if (!force) throw new Error(`mv: destination exists (use -f to overwrite): ${dest}`);
2222
2721
  }
2223
2722
  function mv(fs) {
2224
- return async ({ srcs, dest }) => {
2225
- if (srcs.length === 1) {
2226
- const src = srcs[0];
2227
- if (src === void 0) throw new Error("Source path is required");
2228
- try {
2229
- if ((await fs.stat(dest)).isDirectory) await moveFile(fs, src, `${dest}/${extractFileName(src)}`.replace(MULTIPLE_SLASH_REGEX, "/"));
2230
- else throw new Error(`Destination file already exists: ${dest}`);
2231
- } catch (error) {
2232
- if (error.message.includes("already exists")) throw error;
2233
- await moveFile(fs, src, dest);
2234
- }
2235
- } else for (const src of srcs) {
2236
- const fileName = extractFileName(src);
2237
- await moveFile(fs, src, (dest.endsWith("/") ? dest + fileName : `${dest}/${fileName}`).replace(MULTIPLE_SLASH_REGEX, "/"));
2723
+ return async ({ srcs, dest, force = false, interactive = false }) => {
2724
+ if (srcs.length === 0) throw new Error("mv requires at least one source");
2725
+ const destinationIsDirectory = await isDirectory(fs, dest);
2726
+ if (srcs.length > 1 && !destinationIsDirectory) throw new Error("mv destination must be a directory for multiple sources");
2727
+ for (const src of srcs) {
2728
+ if ((await fs.stat(src)).isDirectory) throw new Error(`mv: directory moves are not supported: ${src}`);
2729
+ const targetPath = destinationIsDirectory || srcs.length > 1 ? joinPath(dest, extractFileName(src)) : dest;
2730
+ await assertCanMoveToDestination(fs, targetPath, force, interactive);
2731
+ await moveFile(fs, src, targetPath);
2238
2732
  }
2239
2733
  };
2240
2734
  }
@@ -2244,11 +2738,34 @@ async function moveFile(fs, src, dest) {
2244
2738
  await fs.deleteFile(src);
2245
2739
  }
2246
2740
 
2741
+ //#endregion
2742
+ //#region src/operator/pwd/pwd.ts
2743
+ const ROOT_DIRECTORY$2 = "/";
2744
+ async function* pwd(cwd = ROOT_DIRECTORY$2) {
2745
+ yield {
2746
+ kind: "line",
2747
+ text: cwd
2748
+ };
2749
+ }
2750
+
2247
2751
  //#endregion
2248
2752
  //#region src/operator/rm/rm.ts
2249
2753
  function rm(fs) {
2250
- return async ({ path }) => {
2251
- await fs.deleteFile(path);
2754
+ return async ({ path, recursive, force = false, interactive = false }) => {
2755
+ if (interactive) throw new Error(`rm: interactive mode is not supported: ${path}`);
2756
+ let stat = null;
2757
+ try {
2758
+ stat = await fs.stat(path);
2759
+ } catch {
2760
+ if (force) return;
2761
+ throw new Error(`File not found: ${path}`);
2762
+ }
2763
+ if (!stat.isDirectory) {
2764
+ await fs.deleteFile(path);
2765
+ return;
2766
+ }
2767
+ if (!recursive) throw new Error(`rm: cannot remove '${path}': Is a directory`);
2768
+ await fs.deleteDirectory(path, true);
2252
2769
  };
2253
2770
  }
2254
2771
 
@@ -2268,8 +2785,18 @@ function tail(n) {
2268
2785
  //#endregion
2269
2786
  //#region src/operator/touch/touch.ts
2270
2787
  function touch(fs) {
2271
- return async ({ files: files$1 }) => {
2272
- for (const file of files$1) if (!await fs.exists(file)) await fs.writeFile(file, new Uint8Array());
2788
+ return async ({ files: files$1, accessTimeOnly = false, modificationTimeOnly = false }) => {
2789
+ const shouldUpdateMtime = !accessTimeOnly || modificationTimeOnly;
2790
+ for (const file of files$1) {
2791
+ if (!await fs.exists(file)) {
2792
+ await fs.writeFile(file, new Uint8Array());
2793
+ continue;
2794
+ }
2795
+ if (shouldUpdateMtime) {
2796
+ const content = await fs.readFile(file);
2797
+ await fs.writeFile(file, content);
2798
+ }
2799
+ }
2273
2800
  };
2274
2801
  }
2275
2802
 
@@ -2285,115 +2812,274 @@ async function* files(...paths) {
2285
2812
  //#endregion
2286
2813
  //#region src/execute/execute.ts
2287
2814
  const textEncoder = new TextEncoder();
2815
+ const EFFECT_COMMANDS = new Set([
2816
+ "cd",
2817
+ "cp",
2818
+ "mkdir",
2819
+ "mv",
2820
+ "rm",
2821
+ "touch"
2822
+ ]);
2823
+ const LS_GLOB_PATTERN_REGEX = /[*?]/;
2824
+ const MULTIPLE_SLASH_REGEX$1 = /\/+/g;
2825
+ const TRAILING_SLASH_REGEX$1 = /\/+$/;
2826
+ const ROOT_DIRECTORY$1 = "/";
2827
+ function isEffectStep(step) {
2828
+ return EFFECT_COMMANDS.has(step.cmd);
2829
+ }
2830
+ async function* emptyStream() {}
2288
2831
  /**
2289
2832
  * Execute compiles a PipelineIR into an executable result.
2290
2833
  * Returns either a stream (for producers/transducers) or a promise (for sinks).
2291
2834
  */
2292
- function execute(ir, fs) {
2293
- const step = ir.steps[0];
2294
- if (!step) return {
2835
+ function execute(ir, fs, context = { cwd: ROOT_DIRECTORY$1 }) {
2836
+ const normalizedContext = normalizeContext(context);
2837
+ if (ir.steps.length === 0) return {
2838
+ kind: "stream",
2839
+ value: emptyStream()
2840
+ };
2841
+ const lastStep = ir.steps.at(-1);
2842
+ if (!lastStep) return {
2295
2843
  kind: "stream",
2296
- value: (async function* () {})()
2844
+ value: emptyStream()
2297
2845
  };
2298
- let result;
2846
+ if (isEffectStep(lastStep)) {
2847
+ for (const [index, step] of ir.steps.entries()) if (isEffectStep(step) && index !== ir.steps.length - 1) throw new Error(`Unsupported pipeline: "${step.cmd}" must be the final command`);
2848
+ return applyOutputRedirect({
2849
+ kind: "sink",
2850
+ value: executePipelineToSink(ir.steps, fs, normalizedContext)
2851
+ }, lastStep, fs);
2852
+ }
2853
+ return applyOutputRedirect({
2854
+ kind: "stream",
2855
+ value: executePipelineToStream(ir.steps, fs, normalizedContext)
2856
+ }, lastStep, fs);
2857
+ }
2858
+ function executePipelineToStream(steps, fs, context) {
2859
+ return (async function* () {
2860
+ let stream = null;
2861
+ for (const step of steps) {
2862
+ if (isEffectStep(step)) throw new Error(`Unsupported pipeline: "${step.cmd}" requires being the final command`);
2863
+ stream = executeStreamStep(step, fs, stream, context);
2864
+ }
2865
+ if (!stream) return;
2866
+ yield* stream;
2867
+ })();
2868
+ }
2869
+ async function executePipelineToSink(steps, fs, context) {
2870
+ const finalStep = steps.at(-1);
2871
+ if (!(finalStep && isEffectStep(finalStep))) return;
2872
+ if (steps.length > 1) {
2873
+ const stream = executePipelineToStream(steps.slice(0, -1), fs, context);
2874
+ for await (const _record of stream);
2875
+ }
2876
+ await executeEffectStep(finalStep, fs, context);
2877
+ }
2878
+ function executeStreamStep(step, fs, input, context) {
2299
2879
  switch (step.cmd) {
2300
2880
  case "cat": {
2301
- const inputPath = getRedirectPath(step.redirections, "input");
2302
- result = {
2303
- kind: "stream",
2304
- value: pipe(files(...withInputRedirect(extractPathsFromExpandedWords(step.args.files), inputPath)), cat(fs))
2881
+ const options = {
2882
+ numberLines: step.args.numberLines,
2883
+ numberNonBlank: step.args.numberNonBlank,
2884
+ showAll: step.args.showAll,
2885
+ showEnds: step.args.showEnds,
2886
+ showNonprinting: step.args.showNonprinting,
2887
+ showTabs: step.args.showTabs,
2888
+ squeezeBlank: step.args.squeezeBlank
2305
2889
  };
2306
- break;
2307
- }
2308
- case "cp": {
2309
- const srcPaths = extractPathsFromExpandedWords(step.args.srcs);
2310
- const destPath = expandedWordToString(step.args.dest);
2311
- result = {
2312
- kind: "sink",
2313
- value: Promise.all(map(srcPaths, (src) => cp(fs)({
2314
- src,
2315
- dest: destPath,
2316
- recursive: step.args.recursive
2317
- }))).then()
2318
- };
2319
- break;
2890
+ const inputPath = getRedirectPath(step.redirections, "input");
2891
+ const filePaths = withInputRedirect(extractPathsFromExpandedWords(step.args.files), inputPath);
2892
+ if (filePaths.length > 0) return cat(fs, options)(files(...filePaths));
2893
+ if (input) return cat(fs, options)(input);
2894
+ return emptyStream();
2320
2895
  }
2321
2896
  case "head": {
2322
2897
  const inputPath = getRedirectPath(step.redirections, "input");
2323
- result = {
2324
- kind: "stream",
2325
- value: pipe(files(...withInputRedirect(extractPathsFromExpandedWords(step.args.files), inputPath)), headWithN(fs, step.args.n))
2326
- };
2327
- break;
2898
+ const filePaths = withInputRedirect(extractPathsFromExpandedWords(step.args.files), inputPath);
2899
+ if (filePaths.length > 0) return headWithN(fs, step.args.n)(files(...filePaths));
2900
+ if (!input) return emptyStream();
2901
+ return headLines(step.args.n)(toLineStream(fs, input));
2328
2902
  }
2329
2903
  case "ls": {
2330
2904
  const paths = extractPathsFromExpandedWords(step.args.paths);
2331
- result = {
2332
- kind: "stream",
2333
- value: (async function* () {
2334
- const results = await Promise.all(map(paths, (path) => ls(fs, path))).then();
2335
- for (const file of results) yield* file;
2336
- })()
2337
- };
2905
+ return (async function* () {
2906
+ for (const inputPath of paths) {
2907
+ const resolvedPath = await resolveLsPath(fs, inputPath);
2908
+ for await (const fileRecord of ls(fs, resolvedPath, { showAll: step.args.showAll })) {
2909
+ if (step.args.longFormat) {
2910
+ const stat = await fs.stat(fileRecord.path);
2911
+ yield {
2912
+ kind: "line",
2913
+ text: formatLongListing(fileRecord.path, stat)
2914
+ };
2915
+ continue;
2916
+ }
2917
+ yield fileRecord;
2918
+ }
2919
+ }
2920
+ })();
2921
+ }
2922
+ case "tail": {
2923
+ const inputPath = getRedirectPath(step.redirections, "input");
2924
+ const filePaths = withInputRedirect(extractPathsFromExpandedWords(step.args.files), inputPath);
2925
+ if (filePaths.length > 0) return (async function* () {
2926
+ for (const filePath of filePaths) yield* tail(step.args.n)(cat(fs)(files(filePath)));
2927
+ })();
2928
+ if (!input) return emptyStream();
2929
+ return tail(step.args.n)(toLineStream(fs, input));
2930
+ }
2931
+ case "pwd": return pwd(context.cwd);
2932
+ default: {
2933
+ const _exhaustive = step;
2934
+ throw new Error(`Unknown command: ${String(_exhaustive.cmd)}`);
2935
+ }
2936
+ }
2937
+ }
2938
+ function normalizeAbsolutePath$1(path) {
2939
+ const segments = (path.startsWith(ROOT_DIRECTORY$1) ? path : `${ROOT_DIRECTORY$1}${path}`).replace(MULTIPLE_SLASH_REGEX$1, "/").split(ROOT_DIRECTORY$1);
2940
+ const normalizedSegments = [];
2941
+ for (const segment of segments) {
2942
+ if (segment === "" || segment === ".") continue;
2943
+ if (segment === "..") {
2944
+ normalizedSegments.pop();
2945
+ continue;
2946
+ }
2947
+ normalizedSegments.push(segment);
2948
+ }
2949
+ const normalizedPath = `${ROOT_DIRECTORY$1}${normalizedSegments.join(ROOT_DIRECTORY$1)}`;
2950
+ return normalizedPath === "" ? ROOT_DIRECTORY$1 : normalizedPath;
2951
+ }
2952
+ function normalizeCwd$1(cwd) {
2953
+ if (cwd === "") return ROOT_DIRECTORY$1;
2954
+ const trimmed = normalizeAbsolutePath$1(cwd).replace(TRAILING_SLASH_REGEX$1, "");
2955
+ return trimmed === "" ? ROOT_DIRECTORY$1 : trimmed;
2956
+ }
2957
+ function normalizeContext(context) {
2958
+ context.cwd = normalizeCwd$1(context.cwd);
2959
+ return context;
2960
+ }
2961
+ function resolvePathFromCwd(cwd, path) {
2962
+ if (path === "") return cwd;
2963
+ if (path.startsWith(ROOT_DIRECTORY$1)) return normalizeAbsolutePath$1(path);
2964
+ return normalizeAbsolutePath$1(`${cwd}/${path}`);
2965
+ }
2966
+ async function executeEffectStep(step, fs, context) {
2967
+ switch (step.cmd) {
2968
+ case "cd": {
2969
+ const requestedPath = expandedWordToString(step.args.path);
2970
+ const resolvedPath = resolvePathFromCwd(context.cwd, requestedPath);
2971
+ let stat;
2972
+ try {
2973
+ stat = await fs.stat(resolvedPath);
2974
+ } catch {
2975
+ throw new Error(`cd: no such file or directory: ${requestedPath}`);
2976
+ }
2977
+ if (!stat.isDirectory) throw new Error(`cd: not a directory: ${requestedPath}`);
2978
+ context.cwd = resolvedPath;
2979
+ break;
2980
+ }
2981
+ case "cp": {
2982
+ const srcPaths = extractPathsFromExpandedWords(step.args.srcs);
2983
+ const destPath = expandedWordToString(step.args.dest);
2984
+ await cp(fs)({
2985
+ srcs: srcPaths,
2986
+ dest: destPath,
2987
+ force: step.args.force,
2988
+ interactive: step.args.interactive,
2989
+ recursive: step.args.recursive
2990
+ });
2338
2991
  break;
2339
2992
  }
2340
2993
  case "mkdir": {
2341
2994
  const paths = extractPathsFromExpandedWords(step.args.paths);
2342
- result = {
2343
- kind: "sink",
2344
- value: Promise.all(map(paths, (path) => mkdir(fs)({
2345
- path,
2346
- recursive: step.args.recursive
2347
- }))).then()
2348
- };
2995
+ for (const path of paths) await mkdir(fs)({
2996
+ path,
2997
+ recursive: step.args.recursive
2998
+ });
2349
2999
  break;
2350
3000
  }
2351
3001
  case "mv": {
2352
3002
  const srcPaths = extractPathsFromExpandedWords(step.args.srcs);
2353
3003
  const destPath = expandedWordToString(step.args.dest);
2354
- result = {
2355
- kind: "sink",
2356
- value: mv(fs)({
2357
- srcs: srcPaths,
2358
- dest: destPath
2359
- })
2360
- };
3004
+ await mv(fs)({
3005
+ srcs: srcPaths,
3006
+ dest: destPath,
3007
+ force: step.args.force,
3008
+ interactive: step.args.interactive
3009
+ });
2361
3010
  break;
2362
3011
  }
2363
3012
  case "rm": {
2364
3013
  const paths = extractPathsFromExpandedWords(step.args.paths);
2365
- result = {
2366
- kind: "sink",
2367
- value: Promise.all(map(paths, (path) => rm(fs)({
2368
- path,
2369
- recursive: step.args.recursive
2370
- }))).then()
2371
- };
2372
- break;
2373
- }
2374
- case "tail": {
2375
- const inputPath = getRedirectPath(step.redirections, "input");
2376
- const filePaths = withInputRedirect(extractPathsFromExpandedWords(step.args.files), inputPath);
2377
- result = {
2378
- kind: "stream",
2379
- value: (async function* () {
2380
- const results = await Promise.all(map(filePaths, (file) => pipe(files(file), cat(fs), tail(step.args.n))));
2381
- for (const lines of results) yield* lines;
2382
- })()
2383
- };
3014
+ for (const path of paths) await rm(fs)({
3015
+ path,
3016
+ force: step.args.force,
3017
+ interactive: step.args.interactive,
3018
+ recursive: step.args.recursive
3019
+ });
2384
3020
  break;
2385
3021
  }
2386
3022
  case "touch": {
2387
3023
  const filePaths = extractPathsFromExpandedWords(step.args.files);
2388
- result = {
2389
- kind: "sink",
2390
- value: touch(fs)({ files: filePaths })
2391
- };
3024
+ await touch(fs)({
3025
+ files: filePaths,
3026
+ accessTimeOnly: step.args.accessTimeOnly,
3027
+ modificationTimeOnly: step.args.modificationTimeOnly
3028
+ });
2392
3029
  break;
2393
3030
  }
2394
- default: throw new Error(`Unknown command: ${String(step.cmd)}`);
3031
+ default: {
3032
+ const _exhaustive = step;
3033
+ throw new Error(`Unknown command: ${String(_exhaustive.cmd)}`);
3034
+ }
2395
3035
  }
2396
- return applyOutputRedirect(result, step, fs);
3036
+ }
3037
+ async function* toLineStream(fs, input) {
3038
+ for await (const record of input) {
3039
+ if (record.kind === "line") {
3040
+ yield record;
3041
+ continue;
3042
+ }
3043
+ if (record.kind === "file") {
3044
+ let lineNum = 1;
3045
+ for await (const text of fs.readLines(record.path)) yield {
3046
+ kind: "line",
3047
+ text,
3048
+ file: record.path,
3049
+ lineNum: lineNum++
3050
+ };
3051
+ continue;
3052
+ }
3053
+ yield {
3054
+ kind: "line",
3055
+ text: JSON.stringify(record.value)
3056
+ };
3057
+ }
3058
+ }
3059
+ function formatLongListing(path, stat) {
3060
+ return `${stat.isDirectory ? "d" : "-"} ${String(stat.size).padStart(8, " ")} ${stat.mtime.toISOString()} ${path}`;
3061
+ }
3062
+ function normalizeLsPath(path) {
3063
+ if (path === "." || path === "./") return "/";
3064
+ if (path.startsWith("./")) return `/${path.slice(2)}`;
3065
+ if (path.startsWith("/")) return path;
3066
+ return `/${path}`;
3067
+ }
3068
+ function trimTrailingSlash(path) {
3069
+ if (path === "/") return path;
3070
+ return path.replace(TRAILING_SLASH_REGEX$1, "");
3071
+ }
3072
+ async function resolveLsPath(fs, path) {
3073
+ const normalizedPath = normalizeLsPath(path);
3074
+ if (LS_GLOB_PATTERN_REGEX.test(normalizedPath)) return normalizedPath;
3075
+ try {
3076
+ if (!(await fs.stat(normalizedPath)).isDirectory) return normalizedPath;
3077
+ } catch {
3078
+ return normalizedPath;
3079
+ }
3080
+ const directoryPath = trimTrailingSlash(normalizedPath);
3081
+ if (directoryPath === "/") return "/*";
3082
+ return `${directoryPath}/*`;
2397
3083
  }
2398
3084
  function getRedirectPath(redirections, kind) {
2399
3085
  if (!redirections) return null;
@@ -2448,6 +3134,27 @@ function lazy(fn) {
2448
3134
 
2449
3135
  //#endregion
2450
3136
  //#region src/shell/shell.ts
3137
+ const ROOT_DIRECTORY = "/";
3138
+ const MULTIPLE_SLASH_REGEX = /\/+/g;
3139
+ const TRAILING_SLASH_REGEX = /\/+$/;
3140
+ function normalizeAbsolutePath(path) {
3141
+ const segments = (path.startsWith(ROOT_DIRECTORY) ? path : `${ROOT_DIRECTORY}${path}`).replace(MULTIPLE_SLASH_REGEX, "/").split(ROOT_DIRECTORY);
3142
+ const normalizedSegments = [];
3143
+ for (const segment of segments) {
3144
+ if (segment === "" || segment === ".") continue;
3145
+ if (segment === "..") {
3146
+ normalizedSegments.pop();
3147
+ continue;
3148
+ }
3149
+ normalizedSegments.push(segment);
3150
+ }
3151
+ return `${ROOT_DIRECTORY}${normalizedSegments.join(ROOT_DIRECTORY)}`;
3152
+ }
3153
+ function normalizeCwd(cwd) {
3154
+ if (cwd === "") return ROOT_DIRECTORY;
3155
+ const trimmed = normalizeAbsolutePath(cwd).replace(TRAILING_SLASH_REGEX, "");
3156
+ return trimmed === "" ? ROOT_DIRECTORY : trimmed;
3157
+ }
2451
3158
  async function collectRecords(result) {
2452
3159
  if (result.kind === "sink") {
2453
3160
  await result.value;
@@ -2457,8 +3164,10 @@ async function collectRecords(result) {
2457
3164
  }
2458
3165
  var Shell = class {
2459
3166
  fs;
2460
- constructor(fs) {
3167
+ currentCwd;
3168
+ constructor(fs, options = {}) {
2461
3169
  this.fs = fs;
3170
+ this.currentCwd = normalizeCwd(options.cwd ?? ROOT_DIRECTORY);
2462
3171
  }
2463
3172
  $ = (strings, ...exprs) => {
2464
3173
  return this._exec(strings, ...exprs);
@@ -2466,32 +3175,58 @@ var Shell = class {
2466
3175
  exec(strings, ...exprs) {
2467
3176
  return this._exec(strings, ...exprs);
2468
3177
  }
3178
+ cwd(newCwd) {
3179
+ this.currentCwd = normalizeCwd(newCwd);
3180
+ }
2469
3181
  _exec(strings, ...exprs) {
2470
3182
  const source = String.raw(strings, ...exprs);
2471
3183
  const fs = this.fs;
3184
+ let cwdOverride;
3185
+ const runWithContext = async () => {
3186
+ const commandStartCwd = normalizeCwd(cwdOverride ?? this.currentCwd);
3187
+ const context = { cwd: commandStartCwd };
3188
+ try {
3189
+ return await collectRecords(execute(ir(), fs, context));
3190
+ } finally {
3191
+ if (cwdOverride === void 0 || context.cwd !== commandStartCwd) this.currentCwd = context.cwd;
3192
+ }
3193
+ };
3194
+ const runStdoutWithContext = async () => {
3195
+ const commandStartCwd = normalizeCwd(cwdOverride ?? this.currentCwd);
3196
+ const context = { cwd: commandStartCwd };
3197
+ try {
3198
+ const result = execute(ir(), fs, context);
3199
+ if (result.kind === "sink") {
3200
+ await result.value;
3201
+ return;
3202
+ }
3203
+ for await (const r of result.value) if (r.kind === "line") process.stdout.write(`${r.text}\n`);
3204
+ } finally {
3205
+ if (cwdOverride === void 0 || context.cwd !== commandStartCwd) this.currentCwd = context.cwd;
3206
+ }
3207
+ };
2472
3208
  const ir = lazy(() => {
2473
3209
  return compile(parse(source));
2474
3210
  });
2475
- return {
3211
+ const command = {
3212
+ cwd(path) {
3213
+ cwdOverride = normalizeCwd(path);
3214
+ return command;
3215
+ },
2476
3216
  async json() {
2477
- return (await collectRecords(execute(ir(), fs))).filter((r) => r.kind === "json").map((r) => r.value);
3217
+ return (await runWithContext()).filter((r) => r.kind === "json").map((r) => r.value);
2478
3218
  },
2479
3219
  async lines() {
2480
- return (await collectRecords(execute(ir(), fs))).filter((r) => r.kind === "line").map((r) => r.text);
3220
+ return (await runWithContext()).filter((r) => r.kind === "line").map((r) => r.text);
2481
3221
  },
2482
3222
  async raw() {
2483
- return await collectRecords(execute(ir(), fs));
3223
+ return await runWithContext();
2484
3224
  },
2485
3225
  async stdout() {
2486
- const result = execute(ir(), fs);
2487
- if (result.kind === "sink") {
2488
- await result.value;
2489
- return;
2490
- }
2491
- for await (const r of result.value) if (r.kind === "line") process.stdout.write(`${r.text}\n`);
3226
+ await runStdoutWithContext();
2492
3227
  },
2493
3228
  async text() {
2494
- return (await collectRecords(execute(ir(), fs))).map((r) => {
3229
+ return (await runWithContext()).map((r) => {
2495
3230
  if (r.kind === "line") return r.text;
2496
3231
  if (r.kind === "file") return r.path;
2497
3232
  if (r.kind === "json") return JSON.stringify(r.value);
@@ -2499,6 +3234,7 @@ var Shell = class {
2499
3234
  }).join("\n");
2500
3235
  }
2501
3236
  };
3237
+ return command;
2502
3238
  }
2503
3239
  };
2504
3240