buninu 0.2.5 → 0.3.1

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.
Files changed (122) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +100 -2
  3. package/apps/bunmsh/CHANGELOG.md +262 -0
  4. package/apps/bunmsh/LICENSE +21 -0
  5. package/apps/bunmsh/LICENSE-MKSH +295 -0
  6. package/apps/bunmsh/PORTING.md +192 -0
  7. package/apps/bunmsh/README.md +801 -0
  8. package/apps/bunmsh/bunmsh +7 -0
  9. package/apps/bunmsh/bunshell.md +61 -0
  10. package/apps/bunmsh/help/README.md +1945 -0
  11. package/apps/bunmsh/help/alias.md +22 -0
  12. package/apps/bunmsh/help/basename.md +38 -0
  13. package/apps/bunmsh/help/builtin.md +25 -0
  14. package/apps/bunmsh/help/bun.md +27 -0
  15. package/apps/bunmsh/help/bunmsh.md +47 -0
  16. package/apps/bunmsh/help/cat.md +31 -0
  17. package/apps/bunmsh/help/catfancy.md +35 -0
  18. package/apps/bunmsh/help/cd.md +22 -0
  19. package/apps/bunmsh/help/chmod.md +27 -0
  20. package/apps/bunmsh/help/clear.md +21 -0
  21. package/apps/bunmsh/help/colon.md +22 -0
  22. package/apps/bunmsh/help/command.md +27 -0
  23. package/apps/bunmsh/help/cp.md +28 -0
  24. package/apps/bunmsh/help/cut.md +25 -0
  25. package/apps/bunmsh/help/date.md +25 -0
  26. package/apps/bunmsh/help/dirname.md +25 -0
  27. package/apps/bunmsh/help/echo.md +25 -0
  28. package/apps/bunmsh/help/env.md +27 -0
  29. package/apps/bunmsh/help/eval.md +22 -0
  30. package/apps/bunmsh/help/exec.md +22 -0
  31. package/apps/bunmsh/help/exit.md +21 -0
  32. package/apps/bunmsh/help/export.md +22 -0
  33. package/apps/bunmsh/help/false.md +22 -0
  34. package/apps/bunmsh/help/find.md +33 -0
  35. package/apps/bunmsh/help/getopts.md +28 -0
  36. package/apps/bunmsh/help/grep.md +34 -0
  37. package/apps/bunmsh/help/head.md +27 -0
  38. package/apps/bunmsh/help/kill.md +27 -0
  39. package/apps/bunmsh/help/ln.md +28 -0
  40. package/apps/bunmsh/help/ls.md +37 -0
  41. package/apps/bunmsh/help/lsbun.md +29 -0
  42. package/apps/bunmsh/help/lsfancy.md +37 -0
  43. package/apps/bunmsh/help/md5sum.md +21 -0
  44. package/apps/bunmsh/help/mkdir.md +27 -0
  45. package/apps/bunmsh/help/mktemp.md +26 -0
  46. package/apps/bunmsh/help/mv.md +26 -0
  47. package/apps/bunmsh/help/print.md +30 -0
  48. package/apps/bunmsh/help/printf.md +25 -0
  49. package/apps/bunmsh/help/pwd.md +21 -0
  50. package/apps/bunmsh/help/read.md +27 -0
  51. package/apps/bunmsh/help/readonly.md +27 -0
  52. package/apps/bunmsh/help/realpath.md +21 -0
  53. package/apps/bunmsh/help/rm.md +33 -0
  54. package/apps/bunmsh/help/rmdir.md +27 -0
  55. package/apps/bunmsh/help/sed.md +29 -0
  56. package/apps/bunmsh/help/seq.md +29 -0
  57. package/apps/bunmsh/help/serve.md +50 -0
  58. package/apps/bunmsh/help/set.md +26 -0
  59. package/apps/bunmsh/help/sha256sum.md +21 -0
  60. package/apps/bunmsh/help/shift.md +23 -0
  61. package/apps/bunmsh/help/sleep.md +25 -0
  62. package/apps/bunmsh/help/sort.md +29 -0
  63. package/apps/bunmsh/help/source.md +23 -0
  64. package/apps/bunmsh/help/tab.md +30 -0
  65. package/apps/bunmsh/help/tac.md +29 -0
  66. package/apps/bunmsh/help/tail.md +27 -0
  67. package/apps/bunmsh/help/tee.md +25 -0
  68. package/apps/bunmsh/help/test.md +30 -0
  69. package/apps/bunmsh/help/time.md +29 -0
  70. package/apps/bunmsh/help/touch.md +26 -0
  71. package/apps/bunmsh/help/tr.md +26 -0
  72. package/apps/bunmsh/help/true.md +22 -0
  73. package/apps/bunmsh/help/type.md +21 -0
  74. package/apps/bunmsh/help/umask.md +22 -0
  75. package/apps/bunmsh/help/unalias.md +28 -0
  76. package/apps/bunmsh/help/uname.md +31 -0
  77. package/apps/bunmsh/help/unset.md +28 -0
  78. package/apps/bunmsh/help/wc.md +27 -0
  79. package/apps/bunmsh/help/whence.md +26 -0
  80. package/apps/bunmsh/help/which.md +41 -0
  81. package/apps/bunmsh/help/yes.md +23 -0
  82. package/apps/bunmsh/package.json +26 -0
  83. package/apps/bunmsh/serve.js +468 -0
  84. package/apps/bunmsh/single-exe/README.md +402 -0
  85. package/apps/bunmsh/single-exe/assetsHelper.js +371 -0
  86. package/apps/bunmsh/single-exe/assetsLoader.mjs +85 -0
  87. package/apps/bunmsh/single-exe/assetsPacker.js +218 -0
  88. package/apps/bunmsh/single-exe/compiled.js +527 -0
  89. package/apps/bunmsh/single-exe/entry.mjs +17 -0
  90. package/apps/bunmsh/src/cat-fancy.js +117 -0
  91. package/apps/bunmsh/src/cat-operands.js +31 -0
  92. package/apps/bunmsh/src/completion.js +247 -0
  93. package/apps/bunmsh/src/environment.js +61 -0
  94. package/apps/bunmsh/src/fancy-ls.js +186 -0
  95. package/apps/bunmsh/src/find.js +119 -0
  96. package/apps/bunmsh/src/history.js +198 -0
  97. package/apps/bunmsh/src/main.js +776 -0
  98. package/apps/bunmsh/src/mouse.js +103 -0
  99. package/apps/bunmsh/src/shell.js +3557 -0
  100. package/apps/bunmsh/test/catfancy.test.js +102 -0
  101. package/apps/bunmsh/test/reference-source.sh +1 -0
  102. package/apps/bunmsh/test/reference.test.js +677 -0
  103. package/apps/bunmsh/test/serve/README.md +3 -0
  104. package/apps/bunmsh/test/serve/bad.json +1 -0
  105. package/apps/bunmsh/test/serve/data.json +1 -0
  106. package/apps/bunmsh/test/serve/data.json5 +1 -0
  107. package/apps/bunmsh/test/serve/data.jsonc +2 -0
  108. package/apps/bunmsh/test/serve/data.jsonl +2 -0
  109. package/apps/bunmsh/test/serve/data.ndjson +1 -0
  110. package/apps/bunmsh/test/serve/data.toml +3 -0
  111. package/apps/bunmsh/test/serve/data.ts +15 -0
  112. package/apps/bunmsh/test/serve/data.xml +1 -0
  113. package/apps/bunmsh/test/serve/data.yaml +3 -0
  114. package/apps/bunmsh/test/serve/data.yml +1 -0
  115. package/apps/bunmsh/test/serve.test.js +241 -0
  116. package/apps/bunmsh/test/shell.test.js +1778 -0
  117. package/apps/bunmsh/test/t.sh +12 -0
  118. package/apps/native-bridge/native-bridge.js +45 -0
  119. package/apps/xdg-open/xdg-open.js +74 -2
  120. package/bin/init.js +11 -3
  121. package/demo-server.js +64 -0
  122. package/package.json +4 -2
@@ -0,0 +1,3557 @@
1
+ import {
2
+ constants as fsConstants,
3
+ accessSync,
4
+ closeSync,
5
+ chmodSync,
6
+ createReadStream,
7
+ createWriteStream,
8
+ lstatSync,
9
+ linkSync,
10
+ mkdirSync,
11
+ openSync,
12
+ readdirSync,
13
+ rmdirSync,
14
+ readFileSync,
15
+ realpathSync,
16
+ statSync,
17
+ symlinkSync,
18
+ unlinkSync,
19
+ } from "node:fs";
20
+ import { createHash } from "node:crypto";
21
+ import {
22
+ arch as osArch,
23
+ constants as osConstants,
24
+ hostname as osHostname,
25
+ release as osRelease,
26
+ type as osType,
27
+ version as osVersion,
28
+ } from "node:os";
29
+ import {
30
+ basename as pathBasename,
31
+ delimiter as pathDelimiter,
32
+ dirname as pathDirname,
33
+ isAbsolute,
34
+ resolve as resolvePath,
35
+ } from "node:path";
36
+ import { Readable, Writable } from "node:stream";
37
+ import { format as formatValue } from "node:util";
38
+ import { EXECUTABLE_COMMAND, IS_COMPILED } from "../single-exe/compiled.js";
39
+ import { readAssetText } from "../single-exe/assetsHelper.js";
40
+ import { dedupeBunmshHistory, saveBunmshHistory } from "./history.js";
41
+ import { fancyLs } from "./fancy-ls.js";
42
+ import { canonicalEnvironment, environmentValue } from "./environment.js";
43
+ import { findIsRegularBuiltin, runFind } from "./find.js";
44
+ import { main as startFileServer, parseServeArguments, waitForInterrupt } from "../serve.js";
45
+ import { main as catFancy } from "./cat-fancy.js";
46
+ import { parseCatOperands } from "./cat-operands.js";
47
+
48
+ const encoder = new TextEncoder();
49
+ const decoder = new TextDecoder();
50
+ const DEFAULT_ALIASES = {
51
+ ls: ["ls", "--color=auto"],
52
+ diff: ["diff", "--color=auto"],
53
+ grep: ["grep", "--color=auto"],
54
+ };
55
+ const DEFAULT_COMMAND_PATH = process.platform === "win32"
56
+ ? (environmentValue(process.env, "PATH") ?? "")
57
+ : ["/bin", "/usr/bin"].join(pathDelimiter);
58
+ const BUN_EXECUTABLE = Bun.which("bun") || process.argv0;
59
+ const BUN_RUNTIME_COMMAND = EXECUTABLE_COMMAND.length > 1
60
+ ? EXECUTABLE_COMMAND
61
+ : [BUN_EXECUTABLE];
62
+ const BUNMSH_ENTRY = resolvePath(import.meta.dirname, "main.js");
63
+ const PIPELINE_STATE_ENV = "BUNMSH_INTERNAL_PIPELINE_STATE";
64
+
65
+ const HELP_ALIASES = {
66
+ ":": "colon",
67
+ "[": "test",
68
+ __builtin: "builtin",
69
+ chdir: "cd",
70
+ };
71
+ const HELP_EXCLUDED = new Set([".", "..", "//", "-", "~"]);
72
+ const HELP_LONG_ONLY = new Set(["ls", "lsfancy"]);
73
+
74
+ function requestsBuiltinHelp(argv) {
75
+ return argv.length === 2 &&
76
+ (argv[1] === "--help" || (argv[1] === "-h" && !HELP_LONG_ONLY.has(argv[0])));
77
+ }
78
+
79
+ function hasBuiltinHelp(name) {
80
+ return !HELP_EXCLUDED.has(name);
81
+ }
82
+
83
+ async function builtinHelp(name) {
84
+ try {
85
+ const source = await readAssetText(`help/${HELP_ALIASES[name] ?? name}.md`);
86
+ return result(0, String(Bun.markdown.ansi(source, { hyperlinks: true })));
87
+ } catch (error) {
88
+ return result(1, "", `bunmsh: ${name}: cannot load help: ${error.message}\n`);
89
+ }
90
+ }
91
+
92
+ function shellPath(value) {
93
+ return process.platform === "win32" ? value.replaceAll("\\", "/") : value;
94
+ }
95
+
96
+ function nativePath(value) {
97
+ return process.platform === "win32" ? value.replaceAll("/", "\\") : value;
98
+ }
99
+
100
+ export class ShellSyntaxError extends Error {
101
+ constructor(message, offset = -1) {
102
+ super(message);
103
+ this.name = "ShellSyntaxError";
104
+ this.offset = offset;
105
+ }
106
+ }
107
+
108
+ function isNameStart(ch) {
109
+ return /[A-Za-z_]/.test(ch ?? "");
110
+ }
111
+
112
+ function isNameChar(ch) {
113
+ return /[A-Za-z0-9_]/.test(ch ?? "");
114
+ }
115
+
116
+ function isSpace(ch) {
117
+ return ch === " " || ch === "\t" || ch === "\r";
118
+ }
119
+
120
+ function lineContinuationLength(source, offset) {
121
+ if (source[offset] !== "\\") return 0;
122
+ if (source[offset + 1] === "\n") return 2;
123
+ if (source[offset + 1] === "\r" && source[offset + 2] === "\n") return 3;
124
+ return 0;
125
+ }
126
+
127
+ function normalizeSource(source) {
128
+ return String(source).replaceAll("\r\n", "\n");
129
+ }
130
+
131
+ function wordToken(offset) {
132
+ return { type: "word", fragments: [], offset };
133
+ }
134
+
135
+ function pushFragment(word, text, quote) {
136
+ if (text === "") {
137
+ if (quote !== "none") word.fragments.push({ text, quote });
138
+ return;
139
+ }
140
+ const previous = word.fragments.at(-1);
141
+ if (previous?.quote === quote) previous.text += text;
142
+ else word.fragments.push({ text, quote });
143
+ }
144
+
145
+ function substitutionEnd(source, start) {
146
+ if (source[start] === "`") {
147
+ for (let i = start + 1; i < source.length; i++) {
148
+ if (source[i] === "\\") i++;
149
+ else if (source[i] === "`") return i + 1;
150
+ }
151
+ throw new ShellSyntaxError("unterminated command substitution", start);
152
+ }
153
+ if (source[start] !== "$" || source[start + 1] !== "(") return start;
154
+ let depth = source[start + 2] === "(" ? 2 : 1;
155
+ let quote = null;
156
+ for (let i = start + 2 + (depth === 2 ? 1 : 0); i < source.length; i++) {
157
+ const ch = source[i];
158
+ if (ch === "\\") { i++; continue; }
159
+ if (quote) {
160
+ if (ch === quote) quote = null;
161
+ continue;
162
+ }
163
+ if (ch === "'" || ch === '"' || ch === "`") { quote = ch; continue; }
164
+ if (ch === "(") depth++;
165
+ else if (ch === ")" && --depth === 0) return i + 1;
166
+ }
167
+ throw new ShellSyntaxError("unterminated substitution", start);
168
+ }
169
+
170
+ function parameterEnd(source, start) {
171
+ let depth = 1;
172
+ for (let i = start + 2; i < source.length; i++) {
173
+ if (source[i] === "\\") { i++; continue; }
174
+ if (source[i] === "$" && source[i + 1] === "{") { depth++; i++; continue; }
175
+ if (source[i] === "}" && --depth === 0) return i + 1;
176
+ }
177
+ throw new ShellSyntaxError("unterminated parameter expansion", start);
178
+ }
179
+
180
+ // Real shells (dash, bash) are lenient when a script ends before a
181
+ // here-document's terminator line ever appears: whatever was read so far
182
+ // becomes the body instead of raising a syntax error. `strict` disables that
183
+ // leniency so a caller (see growHeredocLine) can use the throw as a signal
184
+ // that more input is still needed.
185
+ function consumeHeredocBodies(source, start, pending, strict) {
186
+ let i = start;
187
+ for (const entry of pending) {
188
+ let body = "";
189
+ while (true) {
190
+ const newline = source.indexOf("\n", i);
191
+ const atEnd = newline === -1;
192
+ const lineEnd = atEnd ? source.length : newline;
193
+ const rawLine = source.slice(i, lineEnd);
194
+ const line = entry.strip ? rawLine.replace(/^\t+/, "") : rawLine;
195
+ if (line === entry.delimiterText) {
196
+ i = atEnd ? lineEnd : newline + 1;
197
+ break;
198
+ }
199
+ if (atEnd) {
200
+ if (strict) {
201
+ throw new ShellSyntaxError(
202
+ `unterminated here-document (wanted '${entry.delimiterText}')`,
203
+ entry.token.offset,
204
+ );
205
+ }
206
+ body += line;
207
+ i = lineEnd;
208
+ break;
209
+ }
210
+ body += `${line}\n`;
211
+ i = newline + 1;
212
+ }
213
+ entry.token.heredoc = { body, expand: entry.expand };
214
+ }
215
+ return i;
216
+ }
217
+
218
+ export function tokenize(source, options = {}) {
219
+ source = normalizeSource(source);
220
+ const strict = Boolean(options.strict);
221
+ const tokens = [];
222
+ let i = 0;
223
+ let current = null;
224
+ let atWordBoundary = true;
225
+ let pendingHeredocs = [];
226
+ let awaitingHeredocDelimiter = false;
227
+ let currentHeredocStrip = false;
228
+
229
+ const finishWord = () => {
230
+ if (current) {
231
+ if (awaitingHeredocDelimiter) {
232
+ pendingHeredocs.push({
233
+ token: current,
234
+ strip: currentHeredocStrip,
235
+ expand: current.fragments.every((fragment) =>
236
+ fragment.quote === "none" && !fragment.text.includes("\u0000")),
237
+ delimiterText: current.fragments.map((fragment) =>
238
+ fragment.text.replace(/\u0000(.)/g, "$1")).join(""),
239
+ });
240
+ awaitingHeredocDelimiter = false;
241
+ }
242
+ tokens.push(current);
243
+ }
244
+ current = null;
245
+ };
246
+ const ensureWord = () => {
247
+ current ??= wordToken(i);
248
+ atWordBoundary = false;
249
+ return current;
250
+ };
251
+
252
+ while (i < source.length) {
253
+ const ch = source[i];
254
+
255
+ // A backslash-newline pair is removed before token recognition. Besides
256
+ // joining ordinary words, doing this here avoids creating a phantom empty
257
+ // word for the indented continuation lines commonly used in exec calls.
258
+ const continuation = lineContinuationLength(source, i);
259
+ if (continuation) {
260
+ i += continuation;
261
+ continue;
262
+ }
263
+
264
+ if (ch === "\n") {
265
+ finishWord();
266
+ tokens.push({ type: "op", value: ";", offset: i });
267
+ i++;
268
+ atWordBoundary = true;
269
+ if (pendingHeredocs.length) {
270
+ i = consumeHeredocBodies(source, i, pendingHeredocs, strict);
271
+ pendingHeredocs = [];
272
+ }
273
+ continue;
274
+ }
275
+ if (isSpace(ch)) {
276
+ finishWord();
277
+ i++;
278
+ atWordBoundary = true;
279
+ continue;
280
+ }
281
+ if (ch === "#" && atWordBoundary && !current) {
282
+ while (i < source.length && source[i] !== "\n") i++;
283
+ continue;
284
+ }
285
+
286
+ const two = source.slice(i, i + 2);
287
+ const fdDup = source.slice(i, i + 4);
288
+ const operatorAhead =
289
+ ["|", ";", "<", ">"].includes(ch) ||
290
+ two === "&&" ||
291
+ two === "||" ||
292
+ two === "2>" ||
293
+ fdDup === "1>&2" ||
294
+ fdDup === "2>&1";
295
+ if (current && operatorAhead) {
296
+ finishWord();
297
+ continue;
298
+ }
299
+ // A descriptor duplication begins with the shorter `2>` redirect token,
300
+ // so it must win before ordinary file redirects are considered.
301
+ if (!current && (fdDup === "1>&2" || fdDup === "2>&1")) {
302
+ tokens.push({ type: "op", value: fdDup, offset: i });
303
+ i += 4;
304
+ atWordBoundary = true;
305
+ continue;
306
+ }
307
+ if (!current && ["&&", "||", ">>", "2>", "2>>"].includes(
308
+ source.slice(i, i + 3) === "2>>" ? source.slice(i, i + 3) : two,
309
+ )) {
310
+ const value = source.slice(i, i + 3) === "2>>" ? "2>>" : two;
311
+ tokens.push({ type: "op", value, offset: i });
312
+ i += value.length;
313
+ atWordBoundary = true;
314
+ continue;
315
+ }
316
+ if (!current && two === "<<" && source[i + 2] === "<") {
317
+ tokens.push({ type: "op", value: "<<<", offset: i });
318
+ i += 3;
319
+ atWordBoundary = true;
320
+ continue;
321
+ }
322
+ if (!current && two === "<<") {
323
+ const value = source[i + 2] === "-" ? "<<-" : "<<";
324
+ tokens.push({ type: "op", value, offset: i });
325
+ currentHeredocStrip = value === "<<-";
326
+ awaitingHeredocDelimiter = true;
327
+ i += value.length;
328
+ atWordBoundary = true;
329
+ continue;
330
+ }
331
+ if (!current && ["|", ";", "<", ">"].includes(ch)) {
332
+ tokens.push({ type: "op", value: ch, offset: i++ });
333
+ atWordBoundary = true;
334
+ continue;
335
+ }
336
+
337
+ if (ch === "'") {
338
+ const word = ensureWord();
339
+ const start = ++i;
340
+ while (i < source.length && source[i] !== "'") i++;
341
+ if (i >= source.length)
342
+ throw new ShellSyntaxError("unterminated single quote", start - 1);
343
+ pushFragment(word, source.slice(start, i), "single");
344
+ i++;
345
+ continue;
346
+ }
347
+
348
+ if (ch === '"') {
349
+ const word = ensureWord();
350
+ i++;
351
+ let text = "";
352
+ let closed = false;
353
+ while (i < source.length) {
354
+ if (source[i] === '"') {
355
+ closed = true;
356
+ i++;
357
+ break;
358
+ }
359
+ const quotedContinuation = lineContinuationLength(source, i);
360
+ if (quotedContinuation) {
361
+ i += quotedContinuation;
362
+ continue;
363
+ }
364
+ if (source[i] === "\\" && i + 1 < source.length) {
365
+ const next = source[i + 1];
366
+ if (next === "$" || next === '"' || next === "\\") {
367
+ text += `\u0000${next}`;
368
+ i += 2;
369
+ continue;
370
+ }
371
+ }
372
+ if ((source[i] === "$" && ["(", "{"].includes(source[i + 1])) || source[i] === "`") {
373
+ const end = source[i + 1] === "{" ? parameterEnd(source, i) : substitutionEnd(source, i);
374
+ text += source.slice(i, end);
375
+ i = end;
376
+ continue;
377
+ }
378
+ text += source[i++];
379
+ }
380
+ if (!closed)
381
+ throw new ShellSyntaxError("unterminated double quote", i);
382
+ pushFragment(word, text, "double");
383
+ continue;
384
+ }
385
+
386
+ if (ch === "\\") {
387
+ const word = ensureWord();
388
+ if (i + 1 >= source.length) {
389
+ // A bare trailing backslash at the very end of the source is what an
390
+ // interactive line looks like the instant Enter is pressed, before
391
+ // the next physical line -- which would complete the continuation
392
+ // pair the branch above handles -- has even been typed. In strict
393
+ // mode (needsMoreInput's interactive-continuation check) that must
394
+ // wait for more input like any other unterminated construct, not
395
+ // silently become a literal "\" word character.
396
+ if (strict) throw new ShellSyntaxError("unterminated line continuation", i);
397
+ pushFragment(word, "\\", "none");
398
+ i++;
399
+ } else {
400
+ pushFragment(word, `\u0000${source[i + 1]}`, "none");
401
+ i += 2;
402
+ }
403
+ continue;
404
+ }
405
+
406
+ const word = ensureWord();
407
+ let text = "";
408
+ while (i < source.length) {
409
+ const c = source[i];
410
+ if ((c === "$" && ["(", "{"].includes(source[i + 1])) || c === "`") {
411
+ const end = source[i + 1] === "{" ? parameterEnd(source, i) : substitutionEnd(source, i);
412
+ text += source.slice(i, end);
413
+ i = end;
414
+ continue;
415
+ }
416
+ const maybeOp =
417
+ ["|", ";", "<", ">"].includes(c) ||
418
+ source.slice(i, i + 2) === "&&" ||
419
+ source.slice(i, i + 2) === "||" ||
420
+ source.slice(i, i + 2) === "2>" ||
421
+ source.slice(i, i + 4) === "1>&2" ||
422
+ source.slice(i, i + 4) === "2>&1";
423
+ if (isSpace(c) || c === "\n" || c === "'" || c === '"' || c === "\\" || maybeOp)
424
+ break;
425
+ text += c;
426
+ i++;
427
+ }
428
+ pushFragment(word, text, "none");
429
+ }
430
+
431
+ finishWord();
432
+ if (pendingHeredocs.length) {
433
+ if (strict) {
434
+ const first = pendingHeredocs[0];
435
+ throw new ShellSyntaxError(
436
+ `unterminated here-document (wanted '${first.delimiterText}')`,
437
+ first.token.offset,
438
+ );
439
+ }
440
+ for (const entry of pendingHeredocs) entry.token.heredoc = { body: "", expand: entry.expand };
441
+ }
442
+ return tokens;
443
+ }
444
+
445
+ function emptyCommand() {
446
+ return { words: [], redirects: [] };
447
+ }
448
+
449
+ export function parse(source) {
450
+ const tokens = tokenize(source);
451
+ const jobs = [];
452
+ let i = 0;
453
+ let connector = null;
454
+
455
+ const skipSemicolons = () => {
456
+ while (tokens[i]?.type === "op" && tokens[i].value === ";") i++;
457
+ };
458
+
459
+ skipSemicolons();
460
+ while (i < tokens.length) {
461
+ const pipeline = [];
462
+ while (true) {
463
+ const command = emptyCommand();
464
+ while (i < tokens.length) {
465
+ const token = tokens[i];
466
+ if (token.type === "op" && ["|", ";", "&&", "||"].includes(token.value))
467
+ break;
468
+ if (token.type === "op" && ["1>&2", "2>&1"].includes(token.value)) {
469
+ command.redirects.push({ op: token.value, target: null });
470
+ i++;
471
+ continue;
472
+ }
473
+ if (token.type === "op" && ["<", ">", ">>", "2>", "2>>", "<<", "<<-", "<<<"].includes(token.value)) {
474
+ const target = tokens[i + 1];
475
+ if (!target || target.type !== "word")
476
+ throw new ShellSyntaxError(
477
+ token.value === "<<<"
478
+ ? `redirection ${token.value} requires a word`
479
+ : token.value.startsWith("<<")
480
+ ? `redirection ${token.value} requires a here-document delimiter`
481
+ : `redirection ${token.value} requires a path`,
482
+ token.offset,
483
+ );
484
+ command.redirects.push({ op: token.value, target, heredoc: target.heredoc ?? null });
485
+ i += 2;
486
+ continue;
487
+ }
488
+ if (token.type !== "word")
489
+ throw new ShellSyntaxError(`unexpected operator ${token.value}`, token.offset);
490
+ command.words.push(token);
491
+ i++;
492
+ }
493
+ if (command.words.length === 0 && command.redirects.length === 0)
494
+ throw new ShellSyntaxError("expected a command", tokens[i]?.offset ?? source.length);
495
+ pipeline.push(command);
496
+
497
+ if (tokens[i]?.value === "|") {
498
+ i++;
499
+ if (!tokens[i])
500
+ throw new ShellSyntaxError("pipeline requires a command", source.length);
501
+ continue;
502
+ }
503
+ break;
504
+ }
505
+
506
+ let negate = false;
507
+ const firstWord = pipeline[0]?.words[0];
508
+ if (firstWord?.fragments.length === 1 && firstWord.fragments[0].quote === "none" &&
509
+ firstWord.fragments[0].text === "!") {
510
+ pipeline[0].words.shift();
511
+ if (pipeline[0].words.length === 0)
512
+ throw new ShellSyntaxError("! requires a command", firstWord.offset);
513
+ negate = true;
514
+ }
515
+ jobs.push({ connector, pipeline, negate });
516
+ const separator = tokens[i]?.value;
517
+ if (!separator) break;
518
+ if (![";", "&&", "||"].includes(separator))
519
+ throw new ShellSyntaxError(`unexpected operator ${separator}`, tokens[i].offset);
520
+ connector = separator;
521
+ i++;
522
+ skipSemicolons();
523
+ if (i >= tokens.length) break;
524
+ }
525
+ return jobs;
526
+ }
527
+
528
+ function parameterValue(name, state) {
529
+ if (name === "?") return String(state.lastStatus);
530
+ if (name === "$") return String(process.pid);
531
+ if (name === "#") return String(Math.max(0, state.args.length - 1));
532
+ if (name === "-") return "";
533
+ if (name === "0") return state.args[0] ?? "bunmsh";
534
+ if (name === "@" || name === "*") return state.args.slice(1).join((state.env.IFS ?? " ")[0] ?? " ");
535
+ if (/^[1-9][0-9]*$/.test(name)) return state.args[Number(name)] ?? "";
536
+ return state.env[name] ?? "";
537
+ }
538
+
539
+ function tildeDirectory(name, state) {
540
+ if (name === "") return state.env.HOME ?? "";
541
+ if (name === "+") return state.env.PWD ?? state.cwd;
542
+ if (name === "-") return state.env.OLDPWD ?? "~-";
543
+ try {
544
+ const entry = readFileSync("/etc/passwd", "utf8").split("\n")
545
+ .find((line) => line.split(":", 2)[0] === name);
546
+ return entry?.split(":")[5] ?? `~${name}`;
547
+ } catch { return `~${name}`; }
548
+ }
549
+
550
+ function expandTildes(text, state, assignment) {
551
+ const pattern = assignment ? /(^|[=:])~([A-Za-z0-9_.+-]*)(?=\/|:|$)/g : /^~([A-Za-z0-9_.+-]*)(?=\/|$)/;
552
+ if (assignment)
553
+ return text.replace(pattern, (_match, prefix, name) => `${prefix}${tildeDirectory(name, state)}`);
554
+ return text.replace(pattern, (_match, name) => tildeDirectory(name, state));
555
+ }
556
+
557
+ function arithmeticValue(source, state) {
558
+ const tokens = source.match(/(?:0[xX][0-9a-fA-F]+|\d+|[A-Za-z_][A-Za-z0-9_]*|\|\||&&|==|!=|<=|>=|<<|>>|[()+\-*/%<>&^|!~])/g) ?? [];
559
+ let i = 0;
560
+ const precedence = { "||": 1, "&&": 2, "|": 3, "^": 4, "&": 5, "==": 6, "!=": 6,
561
+ "<": 7, "<=": 7, ">": 7, ">=": 7, "<<": 8, ">>": 8, "+": 9, "-": 9,
562
+ "*": 10, "/": 10, "%": 10 };
563
+ const atom = () => {
564
+ const token = tokens[i++];
565
+ if (["+", "-", "!", "~"].includes(token)) {
566
+ const value = atom();
567
+ return token === "+" ? value : token === "-" ? -value : token === "!" ? Number(!value) : ~value;
568
+ }
569
+ if (token === "(") {
570
+ const value = expression(0);
571
+ if (tokens[i++] !== ")") throw new ShellSyntaxError("bad arithmetic expression");
572
+ return value;
573
+ }
574
+ if (/^[A-Za-z_]/.test(token ?? "")) return Number(state.env[token] ?? 0) || 0;
575
+ if (token === undefined) throw new ShellSyntaxError("bad arithmetic expression");
576
+ return Number(token);
577
+ };
578
+ const expression = (minimum) => {
579
+ let left = atom();
580
+ while ((precedence[tokens[i]] ?? -1) >= minimum) {
581
+ const op = tokens[i++], priority = precedence[op], right = expression(priority + 1);
582
+ left = { "||": Number(Boolean(left || right)), "&&": Number(Boolean(left && right)),
583
+ "|": left | right, "^": left ^ right, "&": left & right, "==": Number(left === right),
584
+ "!=": Number(left !== right), "<": Number(left < right), "<=": Number(left <= right),
585
+ ">": Number(left > right), ">=": Number(left >= right), "<<": left << right,
586
+ ">>": left >> right, "+": left + right, "-": left - right, "*": left * right,
587
+ "/": Math.trunc(left / right), "%": left % right }[op];
588
+ }
589
+ return left;
590
+ };
591
+ const value = expression(0);
592
+ if (i !== tokens.length || !Number.isFinite(value)) throw new ShellSyntaxError("bad arithmetic expression");
593
+ return String(value | 0);
594
+ }
595
+
596
+ function globPatternRegex(pattern, anchoredStart, anchoredEnd, greedy) {
597
+ let source = "";
598
+ for (const ch of pattern) {
599
+ if (ch === "*") source += greedy ? ".*" : ".*?";
600
+ else if (ch === "?") source += ".";
601
+ else source += ch.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
602
+ }
603
+ return new RegExp(`${anchoredStart ? "^" : ""}${source}${anchoredEnd ? "$" : ""}`);
604
+ }
605
+
606
+ async function parameterExpansion(body, state) {
607
+ if (/^[!%][A-Za-z_][A-Za-z0-9_]*$/.test(body)) {
608
+ const value = parameterValue(body.slice(1), state);
609
+ return body[0] === "!" ? parameterValue(value, state) : String([...value].length);
610
+ }
611
+ if (body.startsWith("#") && /^[A-Za-z_][A-Za-z0-9_]*$/.test(body.slice(1)))
612
+ return String(parameterValue(body.slice(1), state).length);
613
+ const trim = /^([A-Za-z_][A-Za-z0-9_]*|[?$#@*]|[0-9]+)(%%|%|##|#)(.*)$/s.exec(body);
614
+ if (trim) {
615
+ const value = parameterValue(trim[1], state);
616
+ const pattern = stripExpansionMarkers(await expandText(trim[3], state));
617
+ const matches = (part) => globPatternRegex(pattern, true, true, true).test(part);
618
+ if (trim[2] === "%") {
619
+ for (let i = value.length; i >= 0; i--) if (matches(value.slice(i))) return value.slice(0, i);
620
+ } else if (trim[2] === "%%") {
621
+ for (let i = 0; i <= value.length; i++) if (matches(value.slice(i))) return value.slice(0, i);
622
+ } else if (trim[2] === "#") {
623
+ for (let i = 0; i <= value.length; i++) if (matches(value.slice(0, i))) return value.slice(i);
624
+ } else {
625
+ for (let i = value.length; i >= 0; i--) if (matches(value.slice(0, i))) return value.slice(i);
626
+ }
627
+ return value;
628
+ }
629
+ const replace = /^([A-Za-z_][A-Za-z0-9_]*|[?$#@*-]|[0-9]+)\/(\/|#|%)?([^/]*)\/(.*)$/s.exec(body);
630
+ if (replace) {
631
+ const value = parameterValue(replace[1], state);
632
+ const pattern = stripExpansionMarkers(await expandText(replace[3], state));
633
+ const replacement = stripExpansionMarkers(await expandText(replace[4], state));
634
+ let source = "";
635
+ for (const ch of pattern) {
636
+ if (ch === "*") source += ".*";
637
+ else if (ch === "?") source += ".";
638
+ else source += ch.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
639
+ }
640
+ const prefix = replace[2] === "#" ? "^" : "";
641
+ const suffix = replace[2] === "%" ? "$" : "";
642
+ return value.replace(new RegExp(`${prefix}${source}${suffix}`, replace[2] === "/" ? "g" : ""), replacement);
643
+ }
644
+ const match = /^([A-Za-z_][A-Za-z0-9_]*|[?$#@*-]|[0-9]+)(?:(:?[-+=?])(.*))?$/s.exec(body);
645
+ if (!match) throw new ShellSyntaxError(`unsupported parameter expansion: \${${body}}`);
646
+ const [, name, operator, operand = ""] = match;
647
+ const value = parameterValue(name, state);
648
+ const unset = !Object.hasOwn(state.env, name) && !/^[?$#@*0-9]$/.test(name);
649
+ const empty = value === "";
650
+ if (!operator) return value;
651
+ const useEmpty = operator.startsWith(":");
652
+ const op = operator.at(-1);
653
+ const missing = unset || (useEmpty && empty);
654
+ const expandedOperand = async () => stripExpansionMarkers(await expandText(operand, state));
655
+ if (op === "-") return missing ? expandedOperand() : value;
656
+ if (op === "+") return missing ? "" : expandedOperand();
657
+ if (op === "=") {
658
+ if (missing) {
659
+ if (!/^[A-Za-z_]/.test(name)) throw new ShellSyntaxError("cannot assign positional parameter");
660
+ if (state.readonly?.has(name)) throw new ShellSyntaxError(`${name}: is read only`);
661
+ const expanded = await expandedOperand();
662
+ state.env[name] = expanded;
663
+ return expanded;
664
+ }
665
+ return value;
666
+ }
667
+ if (op === "?" && missing) throw new ShellSyntaxError(operand ? await expandedOperand() : `${name}: parameter null or not set`);
668
+ return value;
669
+ }
670
+
671
+ function markedExpansion(value) {
672
+ return `\u0001${value}\u0002`;
673
+ }
674
+
675
+ function stripExpansionMarkers(value) {
676
+ return value.replace(/[\u0001\u0002]/g, "");
677
+ }
678
+
679
+ async function commandSubstitution(source, state) {
680
+ const child = {
681
+ ...state,
682
+ env: { ...state.env },
683
+ aliases: { ...state.aliases },
684
+ readonly: new Set(state.readonly),
685
+ directoryHistory: [...state.directoryHistory],
686
+ tabs: [...state.tabs],
687
+ exitRequested: false,
688
+ exitStatus: 0,
689
+ };
690
+ const output = await execute(source, child, { capture: true });
691
+ state.expansionStatus = output.status;
692
+ return decode(output.stdout).replace(/\n+$/, "");
693
+ }
694
+
695
+ async function expandText(text, state) {
696
+ let output = "";
697
+ for (let i = 0; i < text.length; ) {
698
+ if (text[i] === "\u0000") {
699
+ output += `\u0000${text[i + 1] ?? ""}`;
700
+ i += 2;
701
+ continue;
702
+ }
703
+ if (text[i] === "`") {
704
+ const end = substitutionEnd(text, i);
705
+ output += markedExpansion(await commandSubstitution(text.slice(i + 1, end - 1), state));
706
+ i = end;
707
+ continue;
708
+ }
709
+ if (text[i] !== "$") {
710
+ output += text[i++];
711
+ continue;
712
+ }
713
+ if (text.startsWith("$((", i)) {
714
+ const end = substitutionEnd(text, i);
715
+ output += markedExpansion(arithmeticValue(text.slice(i + 3, end - 2), state));
716
+ i = end;
717
+ continue;
718
+ }
719
+ if (text.startsWith("$(", i)) {
720
+ const end = substitutionEnd(text, i);
721
+ output += markedExpansion(await commandSubstitution(text.slice(i + 2, end - 1), state));
722
+ i = end;
723
+ continue;
724
+ }
725
+ if (text[i + 1] === "{") {
726
+ const end = parameterEnd(text, i);
727
+ output += markedExpansion(await parameterExpansion(text.slice(i + 2, end - 1), state));
728
+ i = end;
729
+ continue;
730
+ }
731
+ const next = text[i + 1];
732
+ if (["?", "$", "#", "@", "*", "-"].includes(next) || /[0-9]/.test(next ?? "")) {
733
+ output += markedExpansion(parameterValue(next, state));
734
+ i += 2;
735
+ continue;
736
+ }
737
+ if (isNameStart(next)) {
738
+ let end = i + 2;
739
+ while (isNameChar(text[end])) end++;
740
+ output += markedExpansion(parameterValue(text.slice(i + 1, end), state));
741
+ i = end;
742
+ continue;
743
+ }
744
+ output += "$";
745
+ i++;
746
+ }
747
+ return output;
748
+ }
749
+
750
+ function splitFields(segments, state) {
751
+ const ifs = state.env.IFS === undefined ? " \t\n" : state.env.IFS;
752
+ const fields = [];
753
+ let current = [];
754
+ let forced = false;
755
+ let afterWhitespaceDelimiter = false;
756
+ const finish = (includeEmpty = false) => {
757
+ if (current.length || forced || includeEmpty) fields.push(current);
758
+ current = [];
759
+ forced = false;
760
+ };
761
+ for (const segment of segments) {
762
+ if (segment.quoted && segment.text === "") forced = true;
763
+ for (const ch of segment.text) {
764
+ if (!segment.quoted && segment.splittable && ifs.includes(ch)) {
765
+ const whitespace = " \t\n".includes(ch);
766
+ finish(!whitespace && !afterWhitespaceDelimiter);
767
+ afterWhitespaceDelimiter = whitespace;
768
+ } else {
769
+ current.push({ ch, quoted: segment.quoted });
770
+ afterWhitespaceDelimiter = false;
771
+ }
772
+ }
773
+ }
774
+ finish();
775
+ return fields;
776
+ }
777
+
778
+ async function pathnameFields(field, state) {
779
+ const value = field.map(({ ch }) => ch).join("");
780
+ if (!field.some(({ ch, quoted }) => !quoted && "*?[".includes(ch))) return [value];
781
+ const pattern = field.map(({ ch, quoted }) => {
782
+ if (!quoted) return ch;
783
+ if (ch === "*") return "[*]";
784
+ if (ch === "?") return "[?]";
785
+ if (ch === "[") return "[[]";
786
+ return ch;
787
+ }).join("");
788
+ const glob = new Bun.Glob(pattern);
789
+ const matches = [...glob.scanSync({ cwd: state.cwd, dot: false, onlyFiles: false })].sort();
790
+ return matches.length ? matches : [value];
791
+ }
792
+
793
+ function braceFields(field) {
794
+ let open = -1;
795
+ for (let i = 0; i < field.length; i++) {
796
+ if (!field[i].quoted && field[i].ch === "{") { open = i; break; }
797
+ }
798
+ if (open < 0) return [field];
799
+ let depth = 0, close = -1;
800
+ const commas = [];
801
+ for (let i = open; i < field.length; i++) {
802
+ if (field[i].quoted) continue;
803
+ if (field[i].ch === "{") depth++;
804
+ else if (field[i].ch === "}" && --depth === 0) { close = i; break; }
805
+ else if (field[i].ch === "," && depth === 1) commas.push(i);
806
+ }
807
+ if (close < 0 || commas.length === 0) return [field];
808
+ const boundaries = [open, ...commas, close];
809
+ const output = [];
810
+ for (let i = 0; i < boundaries.length - 1; i++) {
811
+ const replacement = field.slice(boundaries[i] + 1, boundaries[i + 1]);
812
+ output.push(...braceFields([...field.slice(0, open), ...replacement, ...field.slice(close + 1)]));
813
+ }
814
+ return output;
815
+ }
816
+
817
+ export async function expandWord(word, state, options = {}) {
818
+ if (!options.single && word.fragments.length === 1 &&
819
+ word.fragments[0].quote === "double" && word.fragments[0].text === "$@")
820
+ return state.args.slice(1);
821
+ const segments = [];
822
+ for (let index = 0; index < word.fragments.length; index++) {
823
+ const fragment = word.fragments[index];
824
+ const source = fragment.quote === "none" && !options.noTilde
825
+ ? expandTildes(fragment.text, state, Boolean(options.assignment))
826
+ : fragment.text;
827
+ let text = fragment.quote === "single"
828
+ ? source.replaceAll("\u0000", "")
829
+ : await expandText(source, state);
830
+ if (fragment.quote !== "none") {
831
+ segments.push({ text: stripExpansionMarkers(text.replaceAll("\u0000", "")), quoted: true, splittable: false });
832
+ } else {
833
+ let plain = "";
834
+ let splittable = false;
835
+ const flush = () => {
836
+ if (plain) segments.push({ text: plain, quoted: false, splittable });
837
+ plain = "";
838
+ };
839
+ for (let i = 0; i < text.length; i++) {
840
+ if (text[i] === "\u0001") { flush(); splittable = true; continue; }
841
+ if (text[i] === "\u0002") { flush(); splittable = false; continue; }
842
+ if (text[i] !== "\u0000") { plain += text[i]; continue; }
843
+ flush();
844
+ segments.push({ text: text[++i] ?? "", quoted: true, splittable: false });
845
+ }
846
+ flush();
847
+ }
848
+ }
849
+ if (options.single) return [segments.map(({ text }) => text).join("")];
850
+ const fields = splitFields(segments, state);
851
+ const output = [];
852
+ for (const field of fields)
853
+ for (const expanded of braceFields(field)) output.push(...await pathnameFields(expanded, state));
854
+ return output;
855
+ }
856
+
857
+ function splitAssignment(value) {
858
+ const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(value);
859
+ return match ? { name: match[1], value: match[2] } : null;
860
+ }
861
+
862
+ function bytes(value = "") {
863
+ return value instanceof Uint8Array ? value : encoder.encode(String(value));
864
+ }
865
+
866
+ function concatBytes(parts) {
867
+ const values = parts.map(bytes);
868
+ const length = values.reduce((sum, value) => sum + value.byteLength, 0);
869
+ const result = new Uint8Array(length);
870
+ let offset = 0;
871
+ for (const value of values) {
872
+ result.set(value, offset);
873
+ offset += value.byteLength;
874
+ }
875
+ return result;
876
+ }
877
+
878
+ function result(status = 0, stdout = "", stderr = "") {
879
+ return { status, stdout: bytes(stdout), stderr: bytes(stderr) };
880
+ }
881
+
882
+ function quoteShellWord(value) {
883
+ return `'${String(value).replaceAll("'", `'\\''`)}'`;
884
+ }
885
+
886
+ function findExecutable(name, state, defaultPath = false) {
887
+ if (!state.pathSearch) return null;
888
+ const path = defaultPath ? DEFAULT_COMMAND_PATH : (environmentValue(state.env, "PATH") ?? "");
889
+ const found = Bun.which(name, { PATH: path, cwd: nativePath(state.cwd) });
890
+ return found ? shellPath(found) : null;
891
+ }
892
+
893
+ function aliasWords(value) {
894
+ const tokens = tokenize(value);
895
+ if (tokens.some((token) => token.type !== "word")) return null;
896
+ return tokens.map((token) => token.fragments
897
+ .map((fragment) => fragment.text.replaceAll("\u0000", ""))
898
+ .join(""));
899
+ }
900
+
901
+ function readonlyError(name) {
902
+ return result(1, "", `bunmsh: ${name}: is read only\n`);
903
+ }
904
+
905
+ function writeStream(stream, data) {
906
+ return new Promise((resolve, reject) => {
907
+ stream.write(data, (error) => error ? reject(error) : resolve());
908
+ });
909
+ }
910
+
911
+ function parsePrintEscapes(value) {
912
+ let stop = false;
913
+ const text = value.replace(/\\(c|n|r|t|b|a|v|\\|0[0-7]{1,3})/g, (_, code) => {
914
+ if (code === "c") {
915
+ stop = true;
916
+ return "";
917
+ }
918
+ const map = { n: "\n", r: "\r", t: "\t", b: "\b", a: "\x07", v: "\v", "\\": "\\" };
919
+ if (code[0] === "0") return String.fromCharCode(parseInt(code.slice(1), 8));
920
+ return map[code] ?? code;
921
+ });
922
+ return { text, stop };
923
+ }
924
+
925
+ function parentDirectory(path) {
926
+ if (path === "/") return "/";
927
+ const end = path.endsWith("/") ? path.length - 1 : path.length;
928
+ const separator = path.lastIndexOf("/", end - 1);
929
+ return separator <= 0 ? "/" : path.slice(0, separator);
930
+ }
931
+
932
+ async function changeDirectory(state, target, { print = false } = {}) {
933
+ const path = isAbsolute(nativePath(target))
934
+ ? nativePath(target)
935
+ : resolvePath(nativePath(state.cwd), nativePath(target));
936
+ try {
937
+ const resolved = shellPath(resolvePath(path));
938
+ const stat = await Bun.file(nativePath(resolved)).stat();
939
+ if (!stat.isDirectory())
940
+ return result(1, "", `bunmsh: cd: ${target}: not a directory\n`);
941
+ const previous = state.cwd;
942
+ state.directoryHistory.push(previous);
943
+ state.cwd = resolved;
944
+ state.tabs[state.activeTab] = resolved;
945
+ state.env.OLDPWD = previous;
946
+ state.env.PWD = resolved;
947
+ return result(0, print ? `${resolved}\n` : "");
948
+ } catch (error) {
949
+ return result(1, "", `bunmsh: cd: ${target}: ${error.message}\n`);
950
+ }
951
+ }
952
+
953
+ async function previousChildDirectory(state) {
954
+ for (let i = state.directoryHistory.length - 1; i >= 0; i--) {
955
+ const candidate = state.directoryHistory[i];
956
+ if (parentDirectory(candidate) !== state.cwd) continue;
957
+ try {
958
+ if ((await Bun.file(candidate).stat()).isDirectory())
959
+ return changeDirectory(state, candidate);
960
+ } catch {}
961
+ }
962
+ return result(1, "", "bunmsh: //: no previous child directory\n");
963
+ }
964
+
965
+ async function sourceFile(argv, state) {
966
+ if (!argv[1]) return result(2, "", `bunmsh: ${argv[0]}: missing file operand\n`);
967
+ let path = argv[1];
968
+ if (!path.includes("/")) {
969
+ const found = (environmentValue(state.env, "PATH") ?? "").split(pathDelimiter)
970
+ .map((directory) => resolvePath(directory || state.cwd, path))
971
+ .find((candidate) => {
972
+ try { return statSync(candidate).isFile(); } catch { return false; }
973
+ });
974
+ if (found) path = found;
975
+ } else if (!isAbsolute(path)) path = resolvePath(state.cwd, path);
976
+ try {
977
+ const source = await Bun.file(path).text();
978
+ const previousArgs = state.args;
979
+ if (argv.length > 2) state.args = [previousArgs[0], ...argv.slice(2)];
980
+ try {
981
+ return await execute(source, state, { capture: true });
982
+ } finally {
983
+ state.args = previousArgs;
984
+ }
985
+ } catch (error) {
986
+ return result(1, "", `bunmsh: ${argv[0]}: ${argv[1]}: ${error.message}\n`);
987
+ }
988
+ }
989
+
990
+ function testStat(path, state) {
991
+ try { return statSync(isAbsolute(path) ? path : resolvePath(state.cwd, path)); }
992
+ catch { return null; }
993
+ }
994
+
995
+ function evaluateTest(argv, state) {
996
+ if (argv.length === 0) return false;
997
+ if (argv[0] === "!") return !evaluateTest(argv.slice(1), state);
998
+ const or = argv.indexOf("-o");
999
+ if (or >= 0) return evaluateTest(argv.slice(0, or), state) || evaluateTest(argv.slice(or + 1), state);
1000
+ const and = argv.indexOf("-a");
1001
+ if (and >= 0) return evaluateTest(argv.slice(0, and), state) && evaluateTest(argv.slice(and + 1), state);
1002
+ if (argv.length === 1) return argv[0].length > 0;
1003
+ if (argv.length === 2) {
1004
+ const [op, value] = argv;
1005
+ if (op === "-n") return value.length > 0;
1006
+ if (op === "-z") return value.length === 0;
1007
+ const stat = testStat(value, state);
1008
+ if (op === "-e") return stat !== null;
1009
+ if (op === "-f") return Boolean(stat?.isFile());
1010
+ if (op === "-d") return Boolean(stat?.isDirectory());
1011
+ if (op === "-b") return Boolean(stat?.isBlockDevice());
1012
+ if (op === "-c") return Boolean(stat?.isCharacterDevice());
1013
+ if (op === "-p") return Boolean(stat?.isFIFO());
1014
+ if (op === "-S") return Boolean(stat?.isSocket());
1015
+ if (op === "-L" || op === "-h") {
1016
+ try { return lstatSync(isAbsolute(value) ? value : resolvePath(state.cwd, value)).isSymbolicLink(); }
1017
+ catch { return false; }
1018
+ }
1019
+ if (op === "-s") return Boolean(stat && stat.size > 0);
1020
+ if (op === "-r" || op === "-w" || op === "-x") {
1021
+ try {
1022
+ const mode = op === "-r" ? fsConstants.R_OK : op === "-w" ? fsConstants.W_OK : fsConstants.X_OK;
1023
+ accessSync(isAbsolute(value) ? value : resolvePath(state.cwd, value), mode);
1024
+ return true;
1025
+ } catch { return false; }
1026
+ }
1027
+ return false;
1028
+ }
1029
+ if (argv.length === 3) {
1030
+ const [left, op, right] = argv;
1031
+ if (op === "=" || op === "==") return left === right;
1032
+ if (op === "!=") return left !== right;
1033
+ if (["-eq", "-ne", "-gt", "-ge", "-lt", "-le"].includes(op)) {
1034
+ const a = Number(left), b = Number(right);
1035
+ if (!Number.isInteger(a) || !Number.isInteger(b)) throw new Error("integer expression expected");
1036
+ return { "-eq": a === b, "-ne": a !== b, "-gt": a > b, "-ge": a >= b,
1037
+ "-lt": a < b, "-le": a <= b }[op];
1038
+ }
1039
+ const a = testStat(left, state), b = testStat(right, state);
1040
+ if (op === "-nt") return Boolean(a && (!b || a.mtimeMs > b.mtimeMs));
1041
+ if (op === "-ot") return Boolean(b && (!a || a.mtimeMs < b.mtimeMs));
1042
+ if (op === "-ef") return Boolean(a && b && a.dev === b.dev && a.ino === b.ino);
1043
+ }
1044
+ throw new Error("unsupported expression");
1045
+ }
1046
+
1047
+ const builtins = {
1048
+ ":": async () => result(),
1049
+ true: async () => result(),
1050
+ false: async () => result(1),
1051
+ // Execution is handled specially by runCommand so command can alter command
1052
+ // lookup without creating another parsed command.
1053
+ command: null,
1054
+ builtin: null,
1055
+ __builtin: null,
1056
+ time: null,
1057
+ // This is handled specially by runCommandArgv because its output is
1058
+ // unbounded and therefore cannot be represented by the normal buffered
1059
+ // builtin result.
1060
+ yes: null,
1061
+ whence: async (argv, state) => {
1062
+ let pathOnly = false, verbose = false, i = 1;
1063
+ for (; i < argv.length; i++) {
1064
+ if (argv[i] === "--") { i++; break; }
1065
+ if (argv[i] === "-p") pathOnly = true;
1066
+ else if (argv[i] === "-v") verbose = true;
1067
+ else if (argv[i].startsWith("-") && argv[i] !== "-")
1068
+ return result(1, "", `bunmsh: whence: ${argv[i]}: unknown option\n`);
1069
+ else break;
1070
+ }
1071
+ let status = 0, output = "";
1072
+ for (const name of argv.slice(i)) {
1073
+ let description;
1074
+ if (pathOnly) {
1075
+ const path = findExecutable(name, state);
1076
+ description = path ? `${path}\n` : null;
1077
+ } else description = describeCommand(name, state, verbose, false);
1078
+ if (description === null || description.endsWith(" not found\n")) status = 1;
1079
+ if (description) output += description;
1080
+ }
1081
+ return result(status, output);
1082
+ },
1083
+ which: async (argv, state) => {
1084
+ let i = 1;
1085
+ if (argv[i] === "--") i++;
1086
+ if (i >= argv.length)
1087
+ return result(1, "", "bunmsh: which: missing command name\n");
1088
+ let status = 0, output = "";
1089
+ for (; i < argv.length; i++) {
1090
+ // `which` is an explicit PATH query, so it remains useful even while
1091
+ // direct command-name lookup is disabled with `tab path off`.
1092
+ const path = findExecutable(argv[i], { ...state, pathSearch: true });
1093
+ if (path) output += `${path}\n`;
1094
+ else status = 1;
1095
+ }
1096
+ return result(status, output);
1097
+ },
1098
+ type: async (argv, state) => {
1099
+ let status = 0, output = "";
1100
+ for (const name of argv.slice(1)) {
1101
+ const description = describeCommand(name, state, true, false);
1102
+ if (description.endsWith(" not found\n")) status = 1;
1103
+ output += description;
1104
+ }
1105
+ return result(status, output);
1106
+ },
1107
+ alias: async (argv, state) => {
1108
+ if (argv.length === 1) {
1109
+ const output = Object.entries(state.aliases)
1110
+ .sort(([a], [b]) => a.localeCompare(b))
1111
+ .map(([name, words]) => `alias ${name}=${quoteShellWord(words.join(" "))}\n`)
1112
+ .join("");
1113
+ return result(0, output);
1114
+ }
1115
+ let status = 0;
1116
+ let output = "";
1117
+ let stderr = "";
1118
+ for (const item of argv.slice(1)) {
1119
+ const equal = item.indexOf("=");
1120
+ if (equal < 0) {
1121
+ const words = state.aliases[item];
1122
+ if (words) output += `alias ${item}=${quoteShellWord(words.join(" "))}\n`;
1123
+ else {
1124
+ status = 1;
1125
+ stderr += `bunmsh: alias: ${item}: not found\n`;
1126
+ }
1127
+ continue;
1128
+ }
1129
+ const name = item.slice(0, equal);
1130
+ const words = aliasWords(item.slice(equal + 1));
1131
+ if (!name || words === null) {
1132
+ status = 1;
1133
+ stderr += `bunmsh: alias: ${name || item}: invalid alias\n`;
1134
+ } else state.aliases[name] = words;
1135
+ }
1136
+ return result(status, output, stderr);
1137
+ },
1138
+ unalias: async (argv, state) => {
1139
+ let i = 1;
1140
+ if (argv[i] === "-a") {
1141
+ state.aliases = {};
1142
+ return result();
1143
+ }
1144
+ if (argv[i] === "--") i++;
1145
+ if (i === argv.length)
1146
+ return result(1, "", "bunmsh: unalias: missing alias name\n");
1147
+ let status = 0;
1148
+ let stderr = "";
1149
+ for (; i < argv.length; i++) {
1150
+ if (Object.hasOwn(state.aliases, argv[i])) delete state.aliases[argv[i]];
1151
+ else {
1152
+ status = 1;
1153
+ stderr += `bunmsh: unalias: ${argv[i]}: not found\n`;
1154
+ }
1155
+ }
1156
+ return result(status, "", stderr);
1157
+ },
1158
+ test: async (argv, state) => {
1159
+ try { return result(evaluateTest(argv.slice(1), state) ? 0 : 1); }
1160
+ catch (error) { return result(2, "", `bunmsh: test: ${error.message}\n`); }
1161
+ },
1162
+ "[": async (argv, state) => {
1163
+ if (argv.at(-1) !== "]") return result(2, "", "bunmsh: [: missing ]\n");
1164
+ try { return result(evaluateTest(argv.slice(1, -1), state) ? 0 : 1); }
1165
+ catch (error) { return result(2, "", `bunmsh: [: ${error.message}\n`); }
1166
+ },
1167
+ echo: async (argv) => {
1168
+ let newline = true;
1169
+ let start = 1;
1170
+ if (argv[1] === "-n") {
1171
+ newline = false;
1172
+ start = 2;
1173
+ }
1174
+ return result(0, argv.slice(start).join(" ") + (newline ? "\n" : ""));
1175
+ },
1176
+ print: async (argv) => {
1177
+ let raw = false;
1178
+ let newline = true;
1179
+ let separator = " ";
1180
+ let terminator = "\n";
1181
+ let fd = 1;
1182
+ let i = 1;
1183
+ for (; i < argv.length; i++) {
1184
+ const arg = argv[i];
1185
+ if (arg === "--") {
1186
+ i++;
1187
+ break;
1188
+ }
1189
+ if (!arg.startsWith("-") || arg === "-") break;
1190
+ if (/^-u[0-9]+$/.test(arg)) {
1191
+ fd = Number(arg.slice(2));
1192
+ continue;
1193
+ }
1194
+ if (arg === "-r" || arg === "-R") raw = true;
1195
+ else if (arg === "-n") newline = false;
1196
+ else if (arg === "-l") separator = "\n";
1197
+ else if (arg === "-N") separator = terminator = "\0";
1198
+ else return result(1, "", `bunmsh: print: ${arg}: unsupported option\n`);
1199
+ }
1200
+ let output = argv.slice(i).join(separator);
1201
+ if (!raw) {
1202
+ const expanded = parsePrintEscapes(output);
1203
+ output = expanded.text;
1204
+ if (expanded.stop) newline = false;
1205
+ }
1206
+ if (newline) output += terminator;
1207
+ if (fd === 2) return result(0, "", output);
1208
+ if (fd !== 1)
1209
+ return result(1, "", `bunmsh: print: file descriptor ${fd} is not supported yet\n`);
1210
+ return result(0, output);
1211
+ },
1212
+ printf: async (argv) => {
1213
+ if (argv.length < 2) return result(2, "", "bunmsh: printf: missing format\n");
1214
+ const format = argv[1];
1215
+ let ai = 2;
1216
+ let output = "";
1217
+ for (let i = 0; i < format.length; i++) {
1218
+ if (format[i] === "\\" && i + 1 < format.length) {
1219
+ const parsed = parsePrintEscapes(format.slice(i, i + 2));
1220
+ output += parsed.text;
1221
+ i++;
1222
+ continue;
1223
+ }
1224
+ if (format[i] !== "%") {
1225
+ output += format[i];
1226
+ continue;
1227
+ }
1228
+ if (format[i + 1] === "%") {
1229
+ output += "%";
1230
+ i++;
1231
+ continue;
1232
+ }
1233
+ const match = /^%([0-9]*)([sdi])/.exec(format.slice(i));
1234
+ if (!match) return result(2, "", `bunmsh: printf: unsupported format near ${format.slice(i)}\n`);
1235
+ const width = Number(match[1] || 0);
1236
+ const kind = match[2];
1237
+ const value = argv[ai++] ?? (kind === "s" ? "" : "0");
1238
+ let rendered = kind === "s" ? value : String(Number.parseInt(value, 10) || 0);
1239
+ if (width > rendered.length)
1240
+ rendered = (match[1]?.startsWith("0") ? "0" : " ").repeat(width - rendered.length) + rendered;
1241
+ output += rendered;
1242
+ i += match[0].length - 1;
1243
+ }
1244
+ return result(0, output);
1245
+ },
1246
+ read: async (argv, state, input) => {
1247
+ let raw = false, i = 1;
1248
+ if (argv[i] === "-r") { raw = true; i++; }
1249
+ if (argv[i] === "--") i++;
1250
+ const names = argv.slice(i);
1251
+ if (!names.length) names.push("REPLY");
1252
+ if (names.some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)))
1253
+ return result(2, "", "bunmsh: read: invalid variable name\n");
1254
+ let line, gotInput;
1255
+ if (state.readLines) {
1256
+ gotInput = state.readLines.length > 0;
1257
+ line = state.readLines.shift() ?? "";
1258
+ } else {
1259
+ const reader = fallbackInput(input).getReader();
1260
+ const chunks = [];
1261
+ let foundNewline = false;
1262
+ try {
1263
+ while (!foundNewline) {
1264
+ const { value, done } = await reader.read();
1265
+ if (done) break;
1266
+ const newline = value.indexOf(10);
1267
+ chunks.push(newline < 0 ? value : value.slice(0, newline));
1268
+ foundNewline = newline >= 0;
1269
+ }
1270
+ if (foundNewline) try { await reader.cancel(); } catch {}
1271
+ } finally { try { reader.releaseLock(); } catch {} }
1272
+ line = decoder.decode(concatBytes(chunks));
1273
+ gotInput = foundNewline || chunks.length > 0;
1274
+ }
1275
+ line = line.replace(/\r$/, "");
1276
+ if (!raw) line = line.replace(/\\(.)/gs, "$1");
1277
+ const ifs = state.env.IFS ?? " \t\n";
1278
+ const escaped = ifs.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
1279
+ const fields = ifs ? line.trim().split(new RegExp(`[${escaped}]+`)) : [line];
1280
+ for (let n = 0; n < names.length; n++) {
1281
+ if (state.readonly.has(names[n])) return readonlyError(names[n]);
1282
+ state.env[names[n]] = n === names.length - 1 ? fields.slice(n).join(" ") : (fields[n] ?? "");
1283
+ }
1284
+ return result(gotInput ? 0 : 1);
1285
+ },
1286
+ pwd: async (_argv, state) => result(0, `${state.cwd}\n`),
1287
+ cd: async (argv, state) => {
1288
+ if (argv[1] === "//") return previousChildDirectory(state);
1289
+ if (argv[1] === "-") {
1290
+ if (!state.env.OLDPWD)
1291
+ return result(1, "", "bunmsh: cd: OLDPWD is not set\n");
1292
+ return changeDirectory(state, state.env.OLDPWD, { print: true });
1293
+ }
1294
+ const target = argv[1] ?? state.env.HOME;
1295
+ if (!target) return result(1, "", "bunmsh: cd: HOME is not set\n");
1296
+ return changeDirectory(state, target);
1297
+ },
1298
+ chdir: async (argv, state) => builtins.cd(["cd", ...argv.slice(1)], state),
1299
+ tab: async (argv, state) => {
1300
+ if (argv.length > 3 || (argv.length > 2 && !["mouse", "path", "s", "save"].includes(argv[1])))
1301
+ return result(1, "", "bunmsh: tab: usage: tab [n|x|c|l|r|s|save [d|dedupe]|mouse [on|off|true|false]|path [on|off|true|false]|number]\n");
1302
+ const action = argv[1];
1303
+ const activate = (index) => {
1304
+ state.activeTab = index;
1305
+ state.cwd = state.tabs[index];
1306
+ state.env.PWD = state.cwd;
1307
+ };
1308
+ if (action === "n" || (action === undefined && state.tabs.length === 1)) {
1309
+ state.tabs.push(state.cwd);
1310
+ activate(state.tabs.length - 1);
1311
+ return result();
1312
+ }
1313
+ if (action === undefined || action === "r") {
1314
+ activate((state.activeTab + 1) % state.tabs.length);
1315
+ return result();
1316
+ }
1317
+ if (action === "l") {
1318
+ activate((state.activeTab - 1 + state.tabs.length) % state.tabs.length);
1319
+ return result();
1320
+ }
1321
+ if (action === "x" || action === "c") {
1322
+ if (state.tabs.length === 1)
1323
+ return result(1, "", "bunmsh: tab: cannot close the last tab\n");
1324
+ state.tabs.splice(state.activeTab, 1);
1325
+ activate(Math.min(state.activeTab, state.tabs.length - 1));
1326
+ return result();
1327
+ }
1328
+ if (action === "s" || action === "save") {
1329
+ const modifier = argv[2];
1330
+ if (modifier !== undefined && !["d", "dedupe"].includes(modifier))
1331
+ return result(1, "", "bunmsh: tab: save: expected d or dedupe\n");
1332
+ try {
1333
+ if (modifier === undefined) {
1334
+ const path = await saveBunmshHistory(state, state.env);
1335
+ return result(0, `${shellPath(path)}\n`);
1336
+ }
1337
+ // Rewrites the whole history file to drop duplicates, unlike the
1338
+ // plain save above — see dedupeBunmshHistory for why that is only
1339
+ // safe to do as an explicit, occasional choice.
1340
+ const { path, raced, count } = await dedupeBunmshHistory(state, state.env);
1341
+ const warning = raced
1342
+ ? " (warning: another session wrote to it during cleanup; some of its entries may have been lost — rerun if needed)"
1343
+ : "";
1344
+ return result(0, `${shellPath(path)}: ${count} unique entries${warning}\n`);
1345
+ } catch (error) {
1346
+ return result(1, "", `bunmsh: tab: cannot save history: ${error.message}\n`);
1347
+ }
1348
+ }
1349
+ if (action === "mouse") {
1350
+ const mode = argv[2];
1351
+ if (mode === undefined) state.mouseTracking = !state.mouseTracking;
1352
+ else if (mode === "on" || mode === "true") state.mouseTracking = true;
1353
+ else if (mode === "off" || mode === "false") state.mouseTracking = false;
1354
+ else return result(1, "", "bunmsh: tab: mouse: expected on, off, true, or false\n");
1355
+ return result();
1356
+ }
1357
+ if (action === "path") {
1358
+ const mode = argv[2];
1359
+ if (mode === undefined) state.pathSearch = !state.pathSearch;
1360
+ else if (mode === "on" || mode === "true") state.pathSearch = true;
1361
+ else if (mode === "off" || mode === "false") state.pathSearch = false;
1362
+ else return result(1, "", "bunmsh: tab: path: expected on, off, true, or false\n");
1363
+ return result();
1364
+ }
1365
+ if (/^[1-9][0-9]*$/.test(action ?? "")) {
1366
+ const index = Number(action) - 1;
1367
+ if (index >= state.tabs.length)
1368
+ return result(1, "", `bunmsh: tab: ${action}: no such tab\n`);
1369
+ activate(index);
1370
+ return result();
1371
+ }
1372
+ return result(1, "", "bunmsh: tab: usage: tab [n|x|c|l|r|s|save|mouse [on|off|true|false]|path [on|off|true|false]|number]\n");
1373
+ },
1374
+ "-": async (_argv, state) => builtins.cd(["cd", "-"], state),
1375
+ "~": async (_argv, state) => builtins.cd(["cd"], state),
1376
+ "..": async (_argv, state) => changeDirectory(state, ".."),
1377
+ "//": async (_argv, state) => previousChildDirectory(state),
1378
+ export: async (argv, state) => {
1379
+ if (argv.length === 1) {
1380
+ const listing = Object.entries(state.env)
1381
+ .sort(([a], [b]) => a.localeCompare(b))
1382
+ .map(([name, value]) => `export ${name}=${JSON.stringify(value)}\n`)
1383
+ .join("");
1384
+ return result(0, listing);
1385
+ }
1386
+ for (const item of argv.slice(1)) {
1387
+ const assignment = splitAssignment(item);
1388
+ if (assignment) {
1389
+ if (state.readonly.has(assignment.name)) return readonlyError(assignment.name);
1390
+ state.env[assignment.name] = assignment.value;
1391
+ }
1392
+ else if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(item))
1393
+ state.env[item] ??= "";
1394
+ else return result(1, "", `bunmsh: export: ${item}: invalid name\n`);
1395
+ }
1396
+ return result();
1397
+ },
1398
+ unset: async (argv, state) => {
1399
+ let i = 1;
1400
+ if (argv[i] === "-v" || argv[i] === "--") i++;
1401
+ for (const name of argv.slice(i)) {
1402
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
1403
+ return result(1, "", `bunmsh: unset: ${name}: invalid name\n`);
1404
+ if (state.readonly.has(name)) return readonlyError(name);
1405
+ delete state.env[name];
1406
+ }
1407
+ return result();
1408
+ },
1409
+ readonly: async (argv, state) => {
1410
+ if (argv.length === 1 || (argv.length === 2 && argv[1] === "-p")) {
1411
+ const output = [...state.readonly].sort()
1412
+ .map((name) => `readonly ${name}=${quoteShellWord(state.env[name] ?? "")}\n`)
1413
+ .join("");
1414
+ return result(0, output);
1415
+ }
1416
+ let i = argv[1] === "--" ? 2 : 1;
1417
+ for (; i < argv.length; i++) {
1418
+ const assignment = splitAssignment(argv[i]);
1419
+ const name = assignment?.name ?? argv[i];
1420
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
1421
+ return result(1, "", `bunmsh: readonly: ${name}: invalid name\n`);
1422
+ if (assignment) {
1423
+ if (state.readonly.has(name)) return readonlyError(name);
1424
+ state.env[name] = assignment.value;
1425
+ } else state.env[name] ??= "";
1426
+ state.readonly.add(name);
1427
+ }
1428
+ return result();
1429
+ },
1430
+ env: async (argv, state, input, context = {}) => {
1431
+ let env = { ...state.env };
1432
+ let i = 1;
1433
+ for (; i < argv.length; i++) {
1434
+ const item = argv[i];
1435
+ if (item === "--") { i++; break; }
1436
+ if (item === "-i" || item === "--ignore-environment") { env = {}; continue; }
1437
+ if (item === "-u" || item === "--unset") {
1438
+ const name = argv[++i];
1439
+ if (!name) return result(2, "", "bunmsh: env: option requires a name\n");
1440
+ delete env[name];
1441
+ continue;
1442
+ }
1443
+ if (item.startsWith("--unset=")) { delete env[item.slice(8)]; continue; }
1444
+ const assignment = splitAssignment(item);
1445
+ if (!assignment) break;
1446
+ env[assignment.name] = assignment.value;
1447
+ }
1448
+ if (i >= argv.length)
1449
+ return result(0, Object.entries(env).map(([k, v]) => `${k}=${v}\n`).join(""));
1450
+ const childState = {
1451
+ ...state,
1452
+ env,
1453
+ directoryHistory: [...state.directoryHistory],
1454
+ tabs: [...state.tabs],
1455
+ readonly: new Set(state.readonly),
1456
+ };
1457
+ return runCommandArgv(
1458
+ argv.slice(i),
1459
+ childState,
1460
+ input,
1461
+ context.captureStdout ?? true,
1462
+ context.captureStderr ?? true,
1463
+ context.options ?? {},
1464
+ );
1465
+ },
1466
+ exec: async (argv, state, input, context = {}) => {
1467
+ if (argv.length === 1) return result();
1468
+ const execution = await runCommandArgv(
1469
+ argv.slice(1), state, input,
1470
+ context.captureStdout ?? true,
1471
+ context.captureStderr ?? true,
1472
+ context.options ?? {},
1473
+ );
1474
+ state.exitRequested = true;
1475
+ state.exitStatus = execution.status;
1476
+ return execution;
1477
+ },
1478
+ exit: async (argv, state) => {
1479
+ const status = argv[1] === undefined ? state.lastStatus : Number(argv[1]);
1480
+ if (!Number.isInteger(status))
1481
+ return result(2, "", `bunmsh: exit: ${argv[1]}: numeric argument required\n`);
1482
+ state.exitRequested = true;
1483
+ state.exitStatus = status & 255;
1484
+ return result(state.exitStatus);
1485
+ },
1486
+ shift: async (argv, state) => {
1487
+ const count = argv[1] === undefined ? 1 : Number(argv[1]);
1488
+ if (!Number.isInteger(count) || count < 0)
1489
+ return result(1, "", `bunmsh: shift: ${argv[1]}: bad number\n`);
1490
+ if (count > state.args.length - 1) return result(1);
1491
+ state.args.splice(1, count);
1492
+ return result();
1493
+ },
1494
+ getopts: async (argv, state) => {
1495
+ if (argv.length < 3)
1496
+ return result(2, "", "bunmsh: getopts: usage: getopts optstring name [arg ...]\n");
1497
+ const optstring = argv[1];
1498
+ const variable = argv[2];
1499
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable))
1500
+ return result(2, "", `bunmsh: getopts: ${variable}: invalid name\n`);
1501
+ const args = argv.length > 3 ? argv.slice(3) : state.args.slice(1);
1502
+ let index = Number(state.env.OPTIND ?? "1");
1503
+ if (!Number.isInteger(index) || index < 1) index = 1;
1504
+ let offset = state.getoptsOffset ?? 1;
1505
+ const current = args[index - 1];
1506
+ if (!current || current === "--" || !current.startsWith("-") || current === "-") {
1507
+ if (current === "--") state.env.OPTIND = String(index + 1);
1508
+ state.getoptsOffset = 1;
1509
+ return result(1);
1510
+ }
1511
+ const option = current[offset];
1512
+ const position = optstring.indexOf(option);
1513
+ const requiresArgument = position >= 0 && optstring[position + 1] === ":";
1514
+ let stderr = "";
1515
+ delete state.env.OPTARG;
1516
+ if (position < 0 || option === ":") {
1517
+ state.env[variable] = "?";
1518
+ if (optstring[0] === ":") state.env.OPTARG = option;
1519
+ else stderr = `bunmsh: getopts: -${option}: unknown option\n`;
1520
+ } else if (requiresArgument) {
1521
+ if (offset + 1 < current.length) state.env.OPTARG = current.slice(offset + 1);
1522
+ else if (index < args.length) state.env.OPTARG = args[index++];
1523
+ else {
1524
+ state.env[variable] = optstring[0] === ":" ? ":" : "?";
1525
+ if (optstring[0] === ":") state.env.OPTARG = option;
1526
+ else stderr = `bunmsh: getopts: -${option}: argument expected\n`;
1527
+ state.env.OPTIND = String(index + 1);
1528
+ state.getoptsOffset = 1;
1529
+ return result(0, "", stderr);
1530
+ }
1531
+ state.env[variable] = option;
1532
+ offset = current.length;
1533
+ } else state.env[variable] = option;
1534
+ if (offset + 1 >= current.length) {
1535
+ index++;
1536
+ offset = 1;
1537
+ } else offset++;
1538
+ state.env.OPTIND = String(index);
1539
+ state.getoptsOffset = offset;
1540
+ return result(0, "", stderr);
1541
+ },
1542
+ eval: async (argv, state) => execute(argv.slice(1).join(" "), state, { capture: true }),
1543
+ ".": async (argv, state) => sourceFile(argv, state),
1544
+ source: async (argv, state) => sourceFile(argv, state),
1545
+ realpath: async (argv, state) => {
1546
+ let output = "";
1547
+ try {
1548
+ for (const item of argv.slice(1))
1549
+ output += `${realpathSync(isAbsolute(item) ? item : resolvePath(state.cwd, item))}\n`;
1550
+ return result(0, output);
1551
+ } catch (error) {
1552
+ return result(1, "", `bunmsh: realpath: ${error.message}\n`);
1553
+ }
1554
+ },
1555
+ umask: async (argv) => {
1556
+ if (argv.length === 1) return result(0, `${process.umask().toString(8).padStart(4, "0")}\n`);
1557
+ if (argv.length !== 2 || !/^[0-7]{1,4}$/.test(argv[1]))
1558
+ return result(1, "", "bunmsh: umask: bad mask\n");
1559
+ process.umask(Number.parseInt(argv[1], 8));
1560
+ return result();
1561
+ },
1562
+ kill: async (argv) => {
1563
+ let signal = "SIGTERM";
1564
+ let i = 1;
1565
+ if (argv[i] === "-l") {
1566
+ const names = Object.keys(osConstants.signals).map((name) => name.replace(/^SIG/, ""));
1567
+ return result(0, `${names.join(" ")}\n`);
1568
+ }
1569
+ if (argv[i]?.startsWith("-") && argv[i] !== "-") {
1570
+ const value = argv[i++].slice(1).toUpperCase();
1571
+ signal = /^\d+$/.test(value) ? Number(value) : `SIG${value.replace(/^SIG/, "")}`;
1572
+ }
1573
+ if (i >= argv.length) return result(2, "", "bunmsh: kill: missing pid\n");
1574
+ let status = 0, stderr = "";
1575
+ for (; i < argv.length; i++) {
1576
+ const pid = Number(argv[i]);
1577
+ try { process.kill(pid, signal); }
1578
+ catch (error) { status = 1; stderr += `bunmsh: kill: ${argv[i]}: ${error.message}\n`; }
1579
+ }
1580
+ return result(status, "", stderr);
1581
+ },
1582
+ set: async (argv, state) => {
1583
+ if (argv.length === 1)
1584
+ return result(0, Object.entries(state.env).sort().map(([k, v]) => `${k}=${JSON.stringify(v)}\n`).join(""));
1585
+ if (argv[1] === "--") {
1586
+ state.args = [state.args[0], ...argv.slice(2)];
1587
+ return result();
1588
+ }
1589
+ return result(2, "", "bunmsh: set: option handling is not implemented yet\n");
1590
+ },
1591
+ };
1592
+
1593
+ const runFindBuiltin = async (argv, state) => {
1594
+ const output = await runFind(argv, state, process.platform, (command) =>
1595
+ runCommandArgv(command, state, null, true, true));
1596
+ return result(output.status, output.stdout, output.stderr);
1597
+ };
1598
+
1599
+ if (findIsRegularBuiltin()) builtins.find = runFindBuiltin;
1600
+
1601
+ // These are used only when PATH does not provide an executable of the same
1602
+ // name. `builtin name` can still select them explicitly.
1603
+ export function bunShellFallbackArgv(argv) {
1604
+ let commandArgv = argv;
1605
+ if (argv[0] === "ls")
1606
+ commandArgv = commandArgv.filter((value, index) => index === 0 || value !== "--color=auto");
1607
+ if (argv[0] === "cp")
1608
+ commandArgv = commandArgv.map((value, index) =>
1609
+ index > 0 && /^-[^-]/.test(value) ? `-${value.slice(1).replaceAll("r", "R")}` : value);
1610
+ return commandArgv;
1611
+ }
1612
+
1613
+ async function runBunShellFallback(argv, state) {
1614
+ const shell = Bun.$`${bunShellFallbackArgv(argv)}`
1615
+ .cwd(nativePath(state.cwd))
1616
+ .env(state.env)
1617
+ .nothrow();
1618
+ if (state.pipelineChild) {
1619
+ const output = await shell;
1620
+ return result(output.exitCode);
1621
+ }
1622
+ const output = await shell.quiet();
1623
+ return result(output.exitCode, output.stdout, output.stderr);
1624
+ }
1625
+
1626
+ async function runFancyLsFallback(argv, state) {
1627
+ const output = fancyLs(argv, state);
1628
+ return result(output.status, output.stdout, output.stderr);
1629
+ }
1630
+
1631
+ async function runBunShellCpFallback(argv, state) {
1632
+ const cmd = IS_COMPILED
1633
+ ? [...EXECUTABLE_COMMAND, "--bun-shell-fallback", ...bunShellFallbackArgv(argv)]
1634
+ : [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "--bun-shell-fallback", ...bunShellFallbackArgv(argv)];
1635
+ const proc = Bun.spawn({
1636
+ cmd,
1637
+ cwd: nativePath(state.cwd),
1638
+ env: {
1639
+ ...state.env,
1640
+ BUN_ENABLE_EXPERIMENTAL_SHELL_BUILTINS: "1",
1641
+ },
1642
+ stdin: "inherit",
1643
+ stdout: state.pipelineChild ? "inherit" : "pipe",
1644
+ stderr: state.pipelineChild ? "inherit" : "pipe",
1645
+ });
1646
+ if (state.pipelineChild) return result(await proc.exited);
1647
+ const [status, stdout, stderr] = await Promise.all([
1648
+ proc.exited,
1649
+ new Response(proc.stdout).arrayBuffer(),
1650
+ new Response(proc.stderr).arrayBuffer(),
1651
+ ]);
1652
+ return result(status, new Uint8Array(stdout), new Uint8Array(stderr));
1653
+ }
1654
+
1655
+ async function runReflectedProcess(cmd, state) {
1656
+ const inherit = state.pipelineChild || Boolean(process.stdout.isTTY);
1657
+ try {
1658
+ const proc = Bun.spawn({
1659
+ cmd,
1660
+ cwd: nativePath(state.cwd),
1661
+ env: state.env,
1662
+ stdin: "inherit",
1663
+ stdout: inherit ? "inherit" : "pipe",
1664
+ stderr: inherit ? "inherit" : "pipe",
1665
+ });
1666
+ if (inherit) return result(await proc.exited);
1667
+ const [status, stdout, stderr] = await Promise.all([
1668
+ proc.exited,
1669
+ new Response(proc.stdout).arrayBuffer(),
1670
+ new Response(proc.stderr).arrayBuffer(),
1671
+ ]);
1672
+ return result(status, new Uint8Array(stdout), new Uint8Array(stderr));
1673
+ } catch (error) {
1674
+ return result(127, "", `bunmsh: ${cmd[0]}: ${error.message}\n`);
1675
+ }
1676
+ }
1677
+
1678
+ function runBunmshFallback(argv, state) {
1679
+ const cmd = IS_COMPILED
1680
+ ? [...EXECUTABLE_COMMAND, ...argv.slice(1)]
1681
+ : [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, ...argv.slice(1)];
1682
+ return runReflectedProcess(cmd, state);
1683
+ }
1684
+
1685
+ function runBunFallback(argv, state) {
1686
+ if (!IS_COMPILED)
1687
+ return runReflectedProcess([...BUN_RUNTIME_COMMAND, ...argv.slice(1)], state);
1688
+ const executable = process.argv0.includes("/") || process.argv0.includes("\\")
1689
+ ? process.argv0
1690
+ : (Bun.which(process.argv0) || process.argv0);
1691
+ return runReflectedProcess([executable, ...argv.slice(1)], state);
1692
+ }
1693
+
1694
+ export async function executeBunShellFallback(argv) {
1695
+ const output = await Bun.$`${argv}`.nothrow();
1696
+ return output.exitCode;
1697
+ }
1698
+
1699
+ async function runCatFallback(argv, state, input) {
1700
+ const parsed = parseCatOperands(argv, "cat");
1701
+ if (parsed.error) return result(1, "", parsed.error);
1702
+ const operands = parsed.operands;
1703
+
1704
+ const chunks = [];
1705
+ let status = 0;
1706
+ let stderr = "";
1707
+ const writer = state.pipelineChild ? Bun.stdout.writer() : null;
1708
+ const emit = async (chunk) => {
1709
+ if (writer) {
1710
+ writer.write(chunk);
1711
+ await writer.flush();
1712
+ } else chunks.push(bytes(chunk));
1713
+ };
1714
+
1715
+ try {
1716
+ for (const operand of operands) {
1717
+ try {
1718
+ const stream = operand === "-"
1719
+ ? (input instanceof ReadableStream ? input : Bun.stdin.stream())
1720
+ : Bun.file(nativePath(isAbsolute(nativePath(operand))
1721
+ ? operand
1722
+ : resolvePath(nativePath(state.cwd), nativePath(operand)))).stream();
1723
+ for await (const chunk of stream) await emit(chunk);
1724
+ } catch (error) {
1725
+ status = 1;
1726
+ stderr += `bunmsh: cat: ${operand}: ${error.message}\n`;
1727
+ }
1728
+ }
1729
+ } finally {
1730
+ if (writer) {
1731
+ try { writer.end(); } catch {}
1732
+ }
1733
+ }
1734
+ return result(status, writer ? "" : concatBytes(chunks), stderr);
1735
+ }
1736
+
1737
+ function fallbackInput(input) {
1738
+ return input instanceof ReadableStream ? input : Bun.stdin.stream();
1739
+ }
1740
+
1741
+ async function readFallbackInput(input) {
1742
+ return new Uint8Array(await new Response(fallbackInput(input)).arrayBuffer());
1743
+ }
1744
+
1745
+ async function readFallbackFiles(operands, state, input) {
1746
+ if (operands.length === 0 || (operands.length === 1 && operands[0] === "-"))
1747
+ return readFallbackInput(input);
1748
+ const parts = [];
1749
+ for (const operand of operands) {
1750
+ if (operand === "-") parts.push(await readFallbackInput(input));
1751
+ else {
1752
+ const path = isAbsolute(nativePath(operand))
1753
+ ? nativePath(operand)
1754
+ : resolvePath(nativePath(state.cwd), nativePath(operand));
1755
+ parts.push(new Uint8Array(await Bun.file(path).arrayBuffer()));
1756
+ }
1757
+ }
1758
+ return concatBytes(parts);
1759
+ }
1760
+
1761
+ async function runTacFallback(argv, state, input) {
1762
+ let operands = argv.slice(1);
1763
+ if (operands[0] === "--") operands = operands.slice(1);
1764
+ if (operands.some((value) => value.startsWith("-") && value !== "-"))
1765
+ return result(1, "", "bunmsh: tac: options are not supported\n");
1766
+ if (operands.length === 0) operands = ["-"];
1767
+
1768
+ const output = [];
1769
+ let status = 0;
1770
+ let stderr = "";
1771
+ for (const operand of operands) {
1772
+ try {
1773
+ const data = await readFallbackFiles([operand], state, input);
1774
+ const records = [];
1775
+ let start = 0;
1776
+ for (let i = 0; i < data.byteLength; i++) {
1777
+ if (data[i] !== 10) continue;
1778
+ records.push(data.slice(start, i + 1));
1779
+ start = i + 1;
1780
+ }
1781
+ if (start < data.byteLength) records.push(data.slice(start));
1782
+ records.reverse();
1783
+ output.push(concatBytes(records));
1784
+ } catch (error) {
1785
+ status = 1;
1786
+ stderr += `bunmsh: tac: ${operand}: ${error.message}\n`;
1787
+ }
1788
+ }
1789
+ return result(status, concatBytes(output), stderr);
1790
+ }
1791
+
1792
+ async function runHeadFallback(argv, state, input) {
1793
+ let count = 10, i = 1, bytesMode = false;
1794
+ if (argv[i] === "-n") count = Number(argv[++i]), i++;
1795
+ else if (argv[i] === "-c") bytesMode = true, count = Number(argv[++i]), i++;
1796
+ else if (/^-c\d+$/.test(argv[i] ?? "")) bytesMode = true, count = Number(argv[i++].slice(2));
1797
+ else if (/^-\d+$/.test(argv[i] ?? "")) count = Number(argv[i++].slice(1));
1798
+ if (!Number.isInteger(count) || count < 0)
1799
+ return result(1, "", "bunmsh: head: invalid line count\n");
1800
+ const operands = argv.slice(i);
1801
+ if (operands.length) {
1802
+ try {
1803
+ const data = await readFallbackFiles(operands, state, input);
1804
+ if (bytesMode) return result(0, data.slice(0, count));
1805
+ return result(0, decoder.decode(data).split(/(?<=\n)/).slice(0, count).join(""));
1806
+ } catch (error) { return result(1, "", `bunmsh: head: ${error.message}\n`); }
1807
+ }
1808
+ const reader = fallbackInput(input).getReader();
1809
+ const output = [];
1810
+ let lines = 0;
1811
+ try {
1812
+ let totalBytes = 0;
1813
+ while (bytesMode ? totalBytes < count : lines < count) {
1814
+ const { value, done } = await reader.read();
1815
+ if (done) break;
1816
+ let end = bytesMode ? Math.min(value.byteLength, count - totalBytes) : value.byteLength;
1817
+ if (!bytesMode) for (let j = 0; j < value.byteLength; j++) {
1818
+ if (value[j] === 10 && ++lines >= count) { end = j + 1; break; }
1819
+ }
1820
+ output.push(value.slice(0, end));
1821
+ totalBytes += end;
1822
+ if (end < value.byteLength) break;
1823
+ }
1824
+ } finally {
1825
+ try { await reader.cancel(); } catch {}
1826
+ }
1827
+ return result(0, concatBytes(output));
1828
+ }
1829
+
1830
+ async function runTextFilter(argv, state, input, kind) {
1831
+ try {
1832
+ let args = argv.slice(1), reverse = false, numeric = false, unique = false;
1833
+ let tailCount = 10, tailFromStart = false;
1834
+ if (kind === "tail") {
1835
+ if (args[0] === "-n") {
1836
+ const count = args[1] ?? "";
1837
+ tailFromStart = /^\+\d+$/.test(count);
1838
+ tailCount = Number(count);
1839
+ args = args.slice(2);
1840
+ } else if (/^-n\+\d+$/.test(args[0] ?? "")) {
1841
+ tailFromStart = true;
1842
+ tailCount = Number(args.shift().slice(2));
1843
+ }
1844
+ else if (/^-\d+$/.test(args[0] ?? "")) tailCount = Number(args.shift().slice(1));
1845
+ if (!Number.isInteger(tailCount) || tailCount < 0)
1846
+ return result(1, "", "bunmsh: tail: invalid line count\n");
1847
+ }
1848
+ if (kind === "sort") {
1849
+ while (args[0]?.startsWith("-") && args[0] !== "-") {
1850
+ const option = args.shift();
1851
+ reverse ||= option.includes("r"); numeric ||= option.includes("n"); unique ||= option.includes("u");
1852
+ }
1853
+ }
1854
+ const text = decoder.decode(await readFallbackFiles(args, state, input));
1855
+ let lines = text.split("\n");
1856
+ const trailing = lines.at(-1) === "";
1857
+ if (trailing) lines.pop();
1858
+ if (kind === "tail") {
1859
+ lines = tailFromStart
1860
+ ? lines.slice(Math.max(0, tailCount - 1))
1861
+ : lines.slice(-tailCount);
1862
+ } else {
1863
+ lines.sort((a, b) => numeric ? Number(a) - Number(b) : a.localeCompare(b));
1864
+ if (unique) lines = lines.filter((line, index) => index === 0 || line !== lines[index - 1]);
1865
+ if (reverse) lines.reverse();
1866
+ }
1867
+ return result(0, lines.length ? `${lines.join("\n")}\n` : "");
1868
+ } catch (error) { return result(1, "", `bunmsh: ${kind}: ${error.message}\n`); }
1869
+ }
1870
+
1871
+ async function runWcFallback(argv, state, input) {
1872
+ let flags = "", i = 1;
1873
+ while (/^-[lwc]+$/.test(argv[i] ?? "")) flags += argv[i++].slice(1);
1874
+ if (!flags) flags = "lwc";
1875
+ try {
1876
+ const data = await readFallbackFiles(argv.slice(i), state, input);
1877
+ const text = decoder.decode(data);
1878
+ const values = [];
1879
+ if (flags.includes("l")) values.push((text.match(/\n/g) ?? []).length);
1880
+ if (flags.includes("w")) values.push(text.trim() ? text.trim().split(/\s+/).length : 0);
1881
+ if (flags.includes("c")) values.push(data.byteLength);
1882
+ return result(0, `${values.join(" ")}\n`);
1883
+ } catch (error) { return result(1, "", `bunmsh: wc: ${error.message}\n`); }
1884
+ }
1885
+
1886
+ function expandTrSet(value) {
1887
+ return value.replace(/(.)-(.)/g, (_, a, b) => {
1888
+ let output = "";
1889
+ for (let code = a.charCodeAt(0); code <= b.charCodeAt(0); code++) output += String.fromCharCode(code);
1890
+ return output;
1891
+ });
1892
+ }
1893
+
1894
+ async function runTrFallback(argv, _state, input) {
1895
+ let del = false, i = 1;
1896
+ if (argv[i] === "-d") del = true, i++;
1897
+ if (!argv[i] || (!del && !argv[i + 1])) return result(1, "", "bunmsh: tr: missing operand\n");
1898
+ const from = expandTrSet(argv[i++]);
1899
+ const to = expandTrSet(argv[i] ?? "");
1900
+ const text = decoder.decode(await readFallbackInput(input));
1901
+ let output = "";
1902
+ for (const ch of text) {
1903
+ const index = from.indexOf(ch);
1904
+ if (index < 0) output += ch;
1905
+ else if (!del) output += to[Math.min(index, Math.max(0, to.length - 1))] ?? "";
1906
+ }
1907
+ return result(0, output);
1908
+ }
1909
+
1910
+ async function runTeeFallback(argv, state, input) {
1911
+ let append = false, i = 1;
1912
+ if (argv[i] === "-a") append = true, i++;
1913
+ const streams = argv.slice(i).map((name) => createWriteStream(
1914
+ isAbsolute(nativePath(name)) ? nativePath(name) : resolvePath(nativePath(state.cwd), nativePath(name)),
1915
+ { flags: append ? "a" : "w" },
1916
+ ));
1917
+ const output = [];
1918
+ try {
1919
+ for await (const chunk of fallbackInput(input)) {
1920
+ const data = bytes(chunk);
1921
+ if (state.pipelineChild) await writeStream(process.stdout, data); else output.push(data);
1922
+ await Promise.all(streams.map((stream) => writeStream(stream, data)));
1923
+ }
1924
+ } finally { for (const stream of streams) stream.end(); }
1925
+ return result(0, state.pipelineChild ? "" : concatBytes(output));
1926
+ }
1927
+
1928
+ async function runHashFallback(argv, state, input, algorithm) {
1929
+ const operands = argv.slice(1);
1930
+ try {
1931
+ const data = await readFallbackFiles(operands, state, input);
1932
+ const digest = createHash(algorithm).update(data).digest("hex");
1933
+ return result(0, `${digest}${operands.length === 1 && operands[0] !== "-" ? ` ${operands[0]}` : ""}\n`);
1934
+ } catch (error) { return result(1, "", `bunmsh: ${algorithm}sum: ${error.message}\n`); }
1935
+ }
1936
+
1937
+ function runMktempFallback(argv, state) {
1938
+ const directory = argv.includes("-d");
1939
+ const template = argv.find((value, index) => index > 0 && !value.startsWith("-")) ?? "tmp.XXXXXX";
1940
+ if (!template.endsWith("XXXXXX")) return result(1, "", "bunmsh: mktemp: template must end in XXXXXX\n");
1941
+ const prefix = template.slice(0, -6);
1942
+ for (let attempt = 0; attempt < 100; attempt++) {
1943
+ const suffix = Math.random().toString(36).slice(2, 8).padEnd(6, "0");
1944
+ const shown = `${prefix}${suffix}`;
1945
+ const path = isAbsolute(nativePath(shown)) ? nativePath(shown) : resolvePath(nativePath(state.cwd), nativePath(shown));
1946
+ try {
1947
+ if (directory) mkdirSync(path); else closeSync(openSync(path, "wx", 0o600));
1948
+ return result(0, `${shellPath(path)}\n`);
1949
+ } catch (error) { if (error.code !== "EEXIST") return result(1, "", `bunmsh: mktemp: ${error.message}\n`); }
1950
+ }
1951
+ return result(1, "", "bunmsh: mktemp: cannot create temporary file\n");
1952
+ }
1953
+
1954
+ async function runSleepFallback(argv) {
1955
+ if (argv.length !== 2) return result(1, "", "bunmsh: sleep: usage: sleep duration\n");
1956
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(argv[1]);
1957
+ if (!match) return result(1, "", `bunmsh: sleep: invalid duration: ${argv[1]}\n`);
1958
+ const scale = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 }[match[2] ?? "s"];
1959
+ await Bun.sleep(Number(match[1]) * scale);
1960
+ return result();
1961
+ }
1962
+
1963
+ function runRmdirFallback(argv, state) {
1964
+ let parents = false, operands = argv.slice(1);
1965
+ if (operands[0] === "-p" || operands[0] === "--parents") parents = true, operands.shift();
1966
+ if (!operands.length) return result(1, "", "bunmsh: rmdir: missing operand\n");
1967
+ let status = 0, stderr = "";
1968
+ for (const operand of operands) {
1969
+ let path = isAbsolute(nativePath(operand)) ? nativePath(operand) : resolvePath(nativePath(state.cwd), nativePath(operand));
1970
+ try {
1971
+ rmdirSync(path);
1972
+ if (parents) {
1973
+ let parent = pathDirname(path);
1974
+ while (parent !== path) {
1975
+ try { rmdirSync(parent); } catch { break; }
1976
+ path = parent;
1977
+ parent = pathDirname(path);
1978
+ }
1979
+ }
1980
+ } catch (error) { status = 1; stderr += `bunmsh: rmdir: ${operand}: ${error.message}\n`; }
1981
+ }
1982
+ return result(status, "", stderr);
1983
+ }
1984
+
1985
+ function runDateFallback(argv) {
1986
+ const date = new Date();
1987
+ if (argv.length === 1) return result(0, `${date.toString()}\n`);
1988
+ if (argv.length !== 2 || !argv[1].startsWith("+"))
1989
+ return result(1, "", "bunmsh: date: only +FORMAT is supported\n");
1990
+ const pad = (value) => String(value).padStart(2, "0");
1991
+ const values = {
1992
+ "%Y": date.getFullYear(), "%m": pad(date.getMonth() + 1), "%d": pad(date.getDate()),
1993
+ "%H": pad(date.getHours()), "%M": pad(date.getMinutes()), "%S": pad(date.getSeconds()),
1994
+ "%s": Math.floor(date.getTime() / 1000), "%F": `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
1995
+ "%T": `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`, "%%": "%",
1996
+ };
1997
+ return result(0, `${argv[1].slice(1).replace(/%[YmdHMSsFT%]/g, (key) => values[key])}\n`);
1998
+ }
1999
+
2000
+ function grepFiles(operands, state, recursive) {
2001
+ const output = [];
2002
+ const visit = (shown) => {
2003
+ const path = isAbsolute(nativePath(shown))
2004
+ ? nativePath(shown)
2005
+ : resolvePath(nativePath(state.cwd), nativePath(shown));
2006
+ const stat = statSync(path);
2007
+ if (!stat.isDirectory()) { output.push({ shown, path }); return; }
2008
+ if (!recursive) throw new Error(`${shown}: is a directory`);
2009
+ for (const entry of readdirSync(path, { withFileTypes: true }))
2010
+ visit(`${shown.replace(/\/$/, "")}/${entry.name}`);
2011
+ };
2012
+ for (const operand of operands) visit(operand);
2013
+ return output;
2014
+ }
2015
+
2016
+ function grepBasicRegex(pattern) {
2017
+ let output = "", inClass = false;
2018
+ for (let i = 0; i < pattern.length; i++) {
2019
+ const ch = pattern[i];
2020
+ if (ch === "[") inClass = true;
2021
+ if (ch === "]" && inClass) inClass = false;
2022
+ if (!inClass && ch === "\\" && i + 1 < pattern.length && "+?|(){}".includes(pattern[i + 1])) {
2023
+ output += pattern[++i];
2024
+ continue;
2025
+ }
2026
+ if (!inClass && "+?|(){}".includes(ch)) output += `\\${ch}`;
2027
+ else output += ch;
2028
+ }
2029
+ return output;
2030
+ }
2031
+
2032
+ function grepPosixClasses(pattern) {
2033
+ const classes = {
2034
+ alnum: "A-Za-z0-9", alpha: "A-Za-z", blank: " \\t", digit: "0-9",
2035
+ lower: "a-z", space: "\\s", upper: "A-Z", word: "A-Za-z0-9_",
2036
+ xdigit: "A-Fa-f0-9",
2037
+ };
2038
+ return pattern.replace(/\[\[:(alnum|alpha|blank|digit|lower|space|upper|word|xdigit):\]\]/g,
2039
+ (_match, name) => `[${classes[name]}]`);
2040
+ }
2041
+
2042
+ async function runGrepFallback(argv, state, input, context = {}) {
2043
+ let flags = "", colorMode = "never", i = 1;
2044
+ while (argv[i]?.startsWith("-") && argv[i] !== "-") {
2045
+ if (argv[i] === "--") { i++; break; }
2046
+ const colorOption = /^--colou?r(?:=(always|auto|never))?$/.exec(argv[i]);
2047
+ if (colorOption) {
2048
+ colorMode = colorOption[1] ?? "always";
2049
+ i++;
2050
+ continue;
2051
+ }
2052
+ if (!/^-[EFiqvnorx]+$/.test(argv[i]))
2053
+ return result(2, "", `bunmsh: grep: unsupported option: ${argv[i]}\n`);
2054
+ flags += argv[i++].slice(1);
2055
+ }
2056
+ const pattern = argv[i++];
2057
+ if (pattern === undefined) return result(2, "", "bunmsh: grep: missing pattern\n");
2058
+ let regex;
2059
+ const regexSource = flags.includes("F")
2060
+ ? pattern.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&")
2061
+ : grepPosixClasses(flags.includes("E") ? pattern : grepBasicRegex(pattern));
2062
+ try { regex = new RegExp(flags.includes("x") ? `^(?:${regexSource})$` : regexSource, `${flags.includes("i") ? "i" : ""}${flags.includes("o") ? "g" : ""}`); }
2063
+ catch (error) { return result(2, "", `bunmsh: grep: ${error.message}\n`); }
2064
+ const invert = flags.includes("v"), quiet = flags.includes("q"), numbers = flags.includes("n"), only = flags.includes("o");
2065
+ const color = colorMode === "always" ||
2066
+ (colorMode === "auto" && !context.captureStdout && Boolean(process.stdout.isTTY));
2067
+ const red = "\x1b[01;31m", reset = "\x1b[m";
2068
+ const colored = (value) => color && value.length ? `${red}${value}${reset}` : value;
2069
+ const highlightRegex = color && !only
2070
+ ? new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`)
2071
+ : null;
2072
+ let matched = false;
2073
+ const output = [];
2074
+ const processLine = (line, lineNumber, label, showLabel) => {
2075
+ regex.lastIndex = 0;
2076
+ const isMatch = regex.test(line);
2077
+ regex.lastIndex = 0;
2078
+ if (isMatch === invert) return false;
2079
+ matched = true;
2080
+ if (quiet) return true;
2081
+ const prefix = `${showLabel ? `${label}:` : ""}${numbers ? `${lineNumber}:` : ""}`;
2082
+ if (only) {
2083
+ if (invert) return false;
2084
+ for (const match of line.matchAll(regex))
2085
+ if (match[0].length) output.push(`${prefix}${colored(match[0])}\n`);
2086
+ } else {
2087
+ const rendered = !invert && highlightRegex
2088
+ ? line.replace(highlightRegex, (match) => colored(match))
2089
+ : line;
2090
+ output.push(`${prefix}${rendered}\n`);
2091
+ }
2092
+ return false;
2093
+ };
2094
+
2095
+ const operands = argv.slice(i);
2096
+ if (operands.length) {
2097
+ try {
2098
+ const files = grepFiles(operands, state, flags.includes("r"));
2099
+ const showLabel = files.length > 1 || flags.includes("r");
2100
+ for (const file of files) {
2101
+ const lines = (await Bun.file(file.path).text()).split("\n");
2102
+ if (lines.at(-1) === "") lines.pop();
2103
+ for (let line = 0; line < lines.length; line++)
2104
+ if (processLine(lines[line], line + 1, file.shown, showLabel)) return result(0);
2105
+ }
2106
+ } catch (error) { return result(2, "", `bunmsh: grep: ${error.message}\n`); }
2107
+ } else {
2108
+ const reader = fallbackInput(input).getReader();
2109
+ const decode = new TextDecoder();
2110
+ let pending = "", lineNumber = 0;
2111
+ try {
2112
+ while (true) {
2113
+ const { value, done } = await reader.read();
2114
+ pending += decode.decode(value, { stream: !done });
2115
+ const lines = pending.split("\n");
2116
+ pending = done ? "" : lines.pop();
2117
+ if (done && lines.at(-1) === "") lines.pop();
2118
+ for (const line of lines)
2119
+ if (processLine(line, ++lineNumber, "", false)) { await reader.cancel(); return result(0); }
2120
+ if (done) break;
2121
+ }
2122
+ } finally { try { reader.releaseLock(); } catch {} }
2123
+ }
2124
+ return result(matched ? 0 : 1, output.join(""));
2125
+ }
2126
+
2127
+ function splitSedCommands(script) {
2128
+ const commands = [];
2129
+ let start = 0, escaped = false;
2130
+ for (let i = 0; i < script.length; i++) {
2131
+ if (escaped) { escaped = false; continue; }
2132
+ if (script[i] === "\\") { escaped = true; continue; }
2133
+ if (script[i] === ";") {
2134
+ commands.push(script.slice(start, i).trim());
2135
+ start = i + 1;
2136
+ }
2137
+ }
2138
+ commands.push(script.slice(start).trim());
2139
+ return commands.filter(Boolean);
2140
+ }
2141
+
2142
+ function readSedField(command, start, delimiter) {
2143
+ let value = "";
2144
+ for (let i = start; i < command.length; i++) {
2145
+ if (command[i] === "\\" && i + 1 < command.length) {
2146
+ if (command[i + 1] === delimiter) value += command[++i];
2147
+ else value += command[i] + command[++i];
2148
+ } else if (command[i] === delimiter) return [value, i + 1];
2149
+ else value += command[i];
2150
+ }
2151
+ throw new Error("unterminated substitute command");
2152
+ }
2153
+
2154
+ function sedReplacement(value) {
2155
+ let output = "";
2156
+ for (let i = 0; i < value.length; i++) {
2157
+ if (value[i] === "\\" && /[1-9]/.test(value[i + 1] ?? "")) output += `$${value[++i]}`;
2158
+ else if (value[i] === "\\" && i + 1 < value.length) output += value[++i];
2159
+ else if (value[i] === "&") output += "$&";
2160
+ else if (value[i] === "$") output += "$$";
2161
+ else output += value[i];
2162
+ }
2163
+ return output;
2164
+ }
2165
+
2166
+ function compileSedCommand(source, extended) {
2167
+ const numberedPrint = /^(\d+)p$/.exec(source);
2168
+ if (numberedPrint) return { type: "print", line: Number(numberedPrint[1]) };
2169
+ if (!source.startsWith("s") || source.length < 2)
2170
+ throw new Error(`unsupported command: ${source}`);
2171
+ const delimiter = source[1];
2172
+ const [pattern, replacementStart] = readSedField(source, 2, delimiter);
2173
+ const [replacement, flagsStart] = readSedField(source, replacementStart, delimiter);
2174
+ const flags = source.slice(flagsStart);
2175
+ if (!/^[gp]*$/.test(flags)) throw new Error(`unsupported substitute flags: ${flags}`);
2176
+ const regexSource = grepPosixClasses(extended ? pattern : grepBasicRegex(pattern));
2177
+ return {
2178
+ type: "substitute",
2179
+ regex: new RegExp(regexSource, flags.includes("g") ? "g" : ""),
2180
+ replacement: sedReplacement(replacement),
2181
+ print: flags.includes("p"),
2182
+ };
2183
+ }
2184
+
2185
+ function applySed(text, commands, quiet) {
2186
+ const trailingNewline = text.endsWith("\n");
2187
+ const lines = text.split("\n");
2188
+ if (trailingNewline) lines.pop();
2189
+ const output = [];
2190
+ for (let index = 0; index < lines.length; index++) {
2191
+ let line = lines[index];
2192
+ for (const command of commands) {
2193
+ if (command.type === "print") {
2194
+ if (command.line === index + 1) output.push(line);
2195
+ continue;
2196
+ }
2197
+ command.regex.lastIndex = 0;
2198
+ const matched = command.regex.test(line);
2199
+ command.regex.lastIndex = 0;
2200
+ if (matched) {
2201
+ line = line.replace(command.regex, command.replacement);
2202
+ if (command.print) output.push(line);
2203
+ }
2204
+ }
2205
+ if (!quiet) output.push(line);
2206
+ }
2207
+ if (!output.length) return "";
2208
+ return output.join("\n") + (trailingNewline ? "\n" : "");
2209
+ }
2210
+
2211
+ async function runSedFallback(argv, state, input) {
2212
+ let quiet = false, extended = false, inPlace = false, i = 1;
2213
+ const scripts = [];
2214
+ while (i < argv.length && argv[i].startsWith("-") && argv[i] !== "-") {
2215
+ const option = argv[i++];
2216
+ if (option === "--") break;
2217
+ if (option === "-e") {
2218
+ if (i >= argv.length) return result(1, "", "bunmsh: sed: option requires an argument: -e\n");
2219
+ scripts.push(argv[i++]);
2220
+ continue;
2221
+ }
2222
+ if (option.startsWith("-e")) { scripts.push(option.slice(2)); continue; }
2223
+ if (!/^-[nEri]+$/.test(option))
2224
+ return result(1, "", `bunmsh: sed: unsupported option: ${option}\n`);
2225
+ quiet ||= option.includes("n");
2226
+ extended ||= option.includes("E") || option.includes("r");
2227
+ inPlace ||= option.includes("i");
2228
+ }
2229
+ if (!scripts.length && i < argv.length) scripts.push(argv[i++]);
2230
+ if (!scripts.length) return result(1, "", "bunmsh: sed: missing script\n");
2231
+ let commands;
2232
+ try { commands = scripts.flatMap(splitSedCommands).map((script) => compileSedCommand(script, extended)); }
2233
+ catch (error) { return result(1, "", `bunmsh: sed: ${error.message}\n`); }
2234
+ const operands = argv.slice(i);
2235
+ if (inPlace && !operands.length) return result(1, "", "bunmsh: sed: -i requires a file operand\n");
2236
+ try {
2237
+ if (!operands.length) {
2238
+ const text = decoder.decode(await readFallbackInput(input));
2239
+ return result(0, applySed(text, commands, quiet));
2240
+ }
2241
+ let stdout = "";
2242
+ for (const operand of operands) {
2243
+ const file = isAbsolute(nativePath(operand)) ? nativePath(operand) : resolvePath(nativePath(state.cwd), nativePath(operand));
2244
+ const changed = applySed(await Bun.file(file).text(), commands, quiet);
2245
+ if (inPlace) await Bun.write(file, changed);
2246
+ else stdout += changed;
2247
+ }
2248
+ return result(0, stdout);
2249
+ } catch (error) { return result(1, "", `bunmsh: sed: ${error.message}\n`); }
2250
+ }
2251
+
2252
+ function cutCharacterRanges(spec) {
2253
+ const ranges = [];
2254
+ for (const part of spec.split(",")) {
2255
+ const match = /^(\d+)?(?:-(\d+)?)?$/.exec(part);
2256
+ if (!match || (!match[1] && !match[2])) throw new Error(`invalid range: ${part}`);
2257
+ const start = Number(match[1] ?? 1);
2258
+ const end = match[0].includes("-") ? Number(match[2] ?? Number.MAX_SAFE_INTEGER) : start;
2259
+ ranges.push([start, end]);
2260
+ }
2261
+ return ranges;
2262
+ }
2263
+
2264
+ async function runCutFallback(argv, _state, input) {
2265
+ let spec, i = 1;
2266
+ if (argv[i] === "-c") spec = argv[++i], i++;
2267
+ else if (argv[i]?.startsWith("-c")) spec = argv[i++].slice(2);
2268
+ if (!spec || i !== argv.length) return result(1, "", "bunmsh: cut: usage: cut -c LIST\n");
2269
+ try {
2270
+ const ranges = cutCharacterRanges(spec);
2271
+ const text = decoder.decode(await readFallbackInput(input));
2272
+ const trailing = text.endsWith("\n");
2273
+ const lines = text.split("\n");
2274
+ if (trailing) lines.pop();
2275
+ const output = lines.map((line) => [...line].filter((_ch, index) =>
2276
+ ranges.some(([start, end]) => index + 1 >= start && index + 1 <= end)).join("")).join("\n");
2277
+ return result(0, output + (trailing ? "\n" : ""));
2278
+ } catch (error) { return result(1, "", `bunmsh: cut: ${error.message}\n`); }
2279
+ }
2280
+
2281
+ function runLnFallback(argv, state) {
2282
+ let symbolic = false, force = false, noTargetDirectory = false, i = 1;
2283
+ while (/^-[sfT]+$/.test(argv[i] ?? "")) {
2284
+ symbolic ||= argv[i].includes("s");
2285
+ force ||= argv[i].includes("f");
2286
+ noTargetDirectory ||= argv[i].includes("T");
2287
+ i++;
2288
+ }
2289
+ if (argv.length - i !== 2) return result(1, "", "bunmsh: ln: usage: ln [-sf] target link_name\n");
2290
+ const target = nativePath(argv[i]);
2291
+ let link = isAbsolute(nativePath(argv[i + 1]))
2292
+ ? nativePath(argv[i + 1])
2293
+ : resolvePath(nativePath(state.cwd), nativePath(argv[i + 1]));
2294
+ try {
2295
+ if (!noTargetDirectory) {
2296
+ try {
2297
+ if (statSync(link).isDirectory()) link = resolvePath(link, pathBasename(target));
2298
+ } catch {}
2299
+ }
2300
+ if (force) try { unlinkSync(link); } catch {}
2301
+ if (symbolic) symlinkSync(target, link); else {
2302
+ const source = isAbsolute(target) ? target : resolvePath(nativePath(state.cwd), target);
2303
+ linkSync(source, link);
2304
+ }
2305
+ return result();
2306
+ } catch (error) { return result(1, "", `bunmsh: ln: ${error.message}\n`); }
2307
+ }
2308
+
2309
+ function runChmodFallback(argv, state) {
2310
+ if (argv.length < 3) return result(1, "", "bunmsh: chmod: usage: chmod MODE FILE...\n");
2311
+ const mode = argv[1];
2312
+ let status = 0, stderr = "";
2313
+ for (const operand of argv.slice(2)) {
2314
+ const path = isAbsolute(nativePath(operand)) ? nativePath(operand) : resolvePath(nativePath(state.cwd), nativePath(operand));
2315
+ try {
2316
+ if (/^[0-7]{3,4}$/.test(mode)) chmodSync(path, Number.parseInt(mode, 8));
2317
+ else if (mode === "+x" || mode === "a+x") chmodSync(path, statSync(path).mode | 0o111);
2318
+ else throw new Error(`unsupported mode: ${mode}`);
2319
+ } catch (error) { status = 1; stderr += `bunmsh: chmod: ${operand}: ${error.message}\n`; }
2320
+ }
2321
+ return result(status, "", stderr);
2322
+ }
2323
+
2324
+ export function runUnameFallback(argv, system = {}) {
2325
+ const arch = system.arch ?? osArch();
2326
+ const machine = ({ arm64: "aarch64", x64: "x86_64" })[arch] ?? arch;
2327
+ const values = {
2328
+ s: system.type ?? osType(),
2329
+ n: system.hostname ?? osHostname(),
2330
+ r: system.release ?? osRelease(),
2331
+ v: system.version ?? osVersion(),
2332
+ m: machine,
2333
+ p: machine,
2334
+ };
2335
+ if (argv.length === 1) return result(0, `${values.s}\n`);
2336
+ const flags = new Set();
2337
+ for (const arg of argv.slice(1)) {
2338
+ if (arg === "-a") for (const flag of "snrvmp") flags.add(flag);
2339
+ else if (/^-[snrvmp]+$/.test(arg)) for (const flag of arg.slice(1)) flags.add(flag);
2340
+ else return result(1, "", `bunmsh: uname: unsupported option: ${arg}\n`);
2341
+ }
2342
+ return result(0, `${[..."snrvmp"].filter((flag) => flags.has(flag)).map((flag) => values[flag]).join(" ")}\n`);
2343
+ }
2344
+
2345
+ const fallbackBuiltins = {
2346
+ basename: async (argv) => {
2347
+ const i = argv[1] === "--" ? 2 : 1;
2348
+ if (i >= argv.length || argv.length - i > 2)
2349
+ return result(1, "", "bunmsh: basename: usage: basename string [suffix]\n");
2350
+ let name = pathBasename(argv[i]);
2351
+ const suffix = argv[i + 1];
2352
+ if (suffix && suffix !== name && name.endsWith(suffix)) name = name.slice(0, -suffix.length);
2353
+ return result(0, `${name}\n`);
2354
+ },
2355
+ dirname: async (argv) => {
2356
+ const i = argv[1] === "--" ? 2 : 1;
2357
+ if (argv.length - i !== 1)
2358
+ return result(1, "", "bunmsh: dirname: usage: dirname string\n");
2359
+ return result(0, `${pathDirname(argv[i])}\n`);
2360
+ },
2361
+ ls: runFancyLsFallback,
2362
+ // Bun Shell's own `ls`, kept reachable under a different name now that
2363
+ // plain `ls` falls back to lsfancy instead. Bun.$ dispatches on the
2364
+ // literal command name, so argv[0] is rewritten back to "ls" for it —
2365
+ // `lsbun` itself is not something Bun Shell knows how to run.
2366
+ lsbun: (argv, state) => runBunShellFallback(["ls", ...argv.slice(1)], state),
2367
+ lsfancy: runFancyLsFallback,
2368
+ mv: runBunShellFallback,
2369
+ rm: runBunShellFallback,
2370
+ mkdir: runBunShellFallback,
2371
+ seq: runBunShellFallback,
2372
+ touch: runBunShellFallback,
2373
+ cat: runCatFallback,
2374
+ tac: runTacFallback,
2375
+ catfancy: async (argv, state, input) => {
2376
+ const output = await catFancy(argv, state.cwd, fallbackInput(input));
2377
+ return result(output.status, output.stdout, output.stderr);
2378
+ },
2379
+ cp: runBunShellCpFallback,
2380
+ head: runHeadFallback,
2381
+ tail: (argv, state, input) => runTextFilter(argv, state, input, "tail"),
2382
+ wc: runWcFallback,
2383
+ tr: runTrFallback,
2384
+ sleep: runSleepFallback,
2385
+ tee: runTeeFallback,
2386
+ clear: async () => result(0, "\x1b[2J\x1b[H"),
2387
+ rmdir: async (argv, state) => runRmdirFallback(argv, state),
2388
+ mktemp: async (argv, state) => runMktempFallback(argv, state),
2389
+ sort: (argv, state, input) => runTextFilter(argv, state, input, "sort"),
2390
+ date: async (argv) => runDateFallback(argv),
2391
+ md5sum: (argv, state, input) => runHashFallback(argv, state, input, "md5"),
2392
+ sha256sum: (argv, state, input) => runHashFallback(argv, state, input, "sha256"),
2393
+ grep: runGrepFallback,
2394
+ sed: runSedFallback,
2395
+ cut: runCutFallback,
2396
+ ln: async (argv, state) => runLnFallback(argv, state),
2397
+ chmod: async (argv, state) => runChmodFallback(argv, state),
2398
+ uname: async (argv) => runUnameFallback(argv),
2399
+ bunmsh: runBunmshFallback,
2400
+ bun: runBunFallback,
2401
+ serve: async (argv, state) => {
2402
+ const options = parseServeArguments(argv.slice(1), state.env, state.cwd);
2403
+ if (options.error) return result(2, "", `bunmsh: serve: ${options.error}\n`);
2404
+ try {
2405
+ const server = startFileServer(options.directory, options);
2406
+ const signal = await waitForInterrupt(server, options);
2407
+ return result(signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 0);
2408
+ } catch (error) {
2409
+ return result(1, "", `bunmsh: serve: ${error.message}\n`);
2410
+ }
2411
+ },
2412
+ };
2413
+
2414
+ if (!findIsRegularBuiltin()) fallbackBuiltins.find = runFindBuiltin;
2415
+
2416
+ export function builtinNames() {
2417
+ return [...new Set([...Object.keys(builtins), ...Object.keys(fallbackBuiltins)])].sort();
2418
+ }
2419
+
2420
+ function formatMilliseconds(milliseconds, color = Boolean(process.stderr.isTTY)) {
2421
+ const rendered = milliseconds.toFixed(6);
2422
+ if (!color) return rendered;
2423
+ const decimal = rendered.indexOf(".");
2424
+ const palette = [46, 82, 118, 154, 190, 226, 220, 214, 208, 202, 196, 198, 201, 165, 129, 93, 57, 21, 27, 33, 39, 45];
2425
+ let output = "";
2426
+ for (let i = 0; i < rendered.length; i++) {
2427
+ if (rendered[i] === ".") {
2428
+ output += "\x1b[2m.\x1b[0m";
2429
+ continue;
2430
+ }
2431
+ const magnitude = i < decimal ? decimal - i - 1 : decimal - i;
2432
+ // Keep each three-digit magnitude region the same colour: units
2433
+ // (000), thousands (000), millions (000), then fractional groups.
2434
+ const group = Math.floor(magnitude / 3);
2435
+ const index = ((group % palette.length) + palette.length) % palette.length;
2436
+ output += `\x1b[38;5;${palette[index]}m${rendered[i]}\x1b[0m`;
2437
+ }
2438
+ return output;
2439
+ }
2440
+
2441
+ export function createState(options = {}) {
2442
+ const env = canonicalEnvironment({ ...process.env, ...(options.env ?? {}) });
2443
+ const cwd = shellPath(options.cwd ?? process.cwd());
2444
+ env.PWD = cwd;
2445
+ return {
2446
+ env,
2447
+ cwd,
2448
+ directoryHistory: [...(options.directoryHistory ?? [])],
2449
+ tabs: [...(options.tabs ?? [cwd])],
2450
+ activeTab: options.activeTab ?? 0,
2451
+ aliases: { ...DEFAULT_ALIASES, ...(options.aliases ?? {}) },
2452
+ functions: { ...(options.functions ?? {}) },
2453
+ history: [...(options.history ?? [])],
2454
+ mouseTracking: options.mouseTracking ?? /^(?:1|true|on|yes)$/i.test(env.BUNMSH_MOUSE ?? ""),
2455
+ pathSearch: options.pathSearch ?? true,
2456
+ readonly: new Set(options.readonly ?? []),
2457
+ getoptsOffset: 1,
2458
+ args: options.args ?? ["bunmsh"],
2459
+ lastStatus: 0,
2460
+ exitRequested: false,
2461
+ exitStatus: 0,
2462
+ pipelineChild: options.pipelineChild ?? false,
2463
+ };
2464
+ }
2465
+
2466
+ function hasCompoundSyntax(source) {
2467
+ if (/(^|\n)\s*(?:\(|[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\)\s*\{)/.test(source)) return true;
2468
+ let commandStart = true;
2469
+ for (const token of tokenize(source)) {
2470
+ if (token.type === "op") {
2471
+ if ([";", "&&", "||", "|"].includes(token.value)) commandStart = true;
2472
+ continue;
2473
+ }
2474
+ const word = token.fragments.length === 1 && token.fragments[0].quote === "none"
2475
+ ? token.fragments[0].text : null;
2476
+ if (commandStart && ["if", "while", "until", "for", "case"].includes(word)) return true;
2477
+ if (!(commandStart && /^[A-Za-z_][A-Za-z0-9_]*=/.test(word ?? ""))) commandStart = false;
2478
+ }
2479
+ return false;
2480
+ }
2481
+
2482
+ function compoundLogicalSource(source) {
2483
+ const tokens = tokenize(source);
2484
+ const breaks = new Set();
2485
+ let commandStart = true;
2486
+ const literal = (token) => token.type === "word" && token.fragments.length === 1 &&
2487
+ token.fragments[0].quote === "none" ? token.fragments[0].text : null;
2488
+ for (let index = 0; index < tokens.length; index++) {
2489
+ const token = tokens[index];
2490
+ if (token.type === "op") {
2491
+ if ([";", "&&", "||", "|"].includes(token.value)) commandStart = true;
2492
+ continue;
2493
+ }
2494
+ const word = literal(token);
2495
+ if (!commandStart) continue;
2496
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word ?? "")) continue;
2497
+ // Like mksh's KEYWORD lexer mode, these words are special only where a
2498
+ // command may begin. Physical newlines are not required around them.
2499
+ if (["if", "while", "until", "for", "case"].includes(word) && token.offset > 0)
2500
+ breaks.add(token.offset);
2501
+ if (["elif", "else", "fi", "done", "esac"].includes(word))
2502
+ breaks.add(token.offset);
2503
+ if (["then", "do", "else"].includes(word) && tokens[index + 1])
2504
+ breaks.add(tokens[index + 1].offset);
2505
+ commandStart = ["if", "while", "until", "elif", "then", "do", "else"].includes(word);
2506
+ }
2507
+ let logical = source;
2508
+ for (const offset of [...breaks].sort((a, b) => b - a))
2509
+ if (offset > 0 && source[offset - 1] !== "\n")
2510
+ logical = `${logical.slice(0, offset)}\n${logical.slice(offset)}`;
2511
+ return logical;
2512
+ }
2513
+
2514
+ // A leaf command line inside a compound body (if/while/for/case) is scanned
2515
+ // one physical line at a time, but a here-document's body lives on the lines
2516
+ // that follow it. Grow the line by re-tokenizing with more lines appended
2517
+ // until the heredoc(s) it opens are fully resolved, so the folded lines never
2518
+ // get misread as separate commands or as clauses of the surrounding compound.
2519
+ function growHeredocLine(lines, index) {
2520
+ const line = lines[index];
2521
+ if (!line.includes("<<")) return { source: line, next: index + 1 };
2522
+ let end = index;
2523
+ let candidate = line;
2524
+ while (true) {
2525
+ try {
2526
+ tokenize(candidate, { strict: true });
2527
+ return { source: candidate, next: end + 1 };
2528
+ } catch (error) {
2529
+ if (!(error instanceof ShellSyntaxError) ||
2530
+ !error.message.startsWith("unterminated here-document"))
2531
+ throw error;
2532
+ end++;
2533
+ if (end >= lines.length) throw error;
2534
+ candidate += `\n${lines[end]}`;
2535
+ }
2536
+ }
2537
+ }
2538
+
2539
+ function parseCompoundScript(source) {
2540
+ const lines = compoundLogicalSource(source.replace(/\\\r?\n/g, "")).split(/\r?\n/);
2541
+ const parseSequence = (start, stops = []) => {
2542
+ const nodes = [];
2543
+ let i = start;
2544
+ while (i < lines.length) {
2545
+ const line = lines[i];
2546
+ const trimmed = line.trim();
2547
+ if (stops.some((stop) => stop.test(trimmed))) break;
2548
+ if (!trimmed || trimmed.startsWith("#")) { i++; continue; }
2549
+
2550
+ const functionMatch = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*\{\s*$/.exec(trimmed);
2551
+ if (functionMatch) {
2552
+ const body = parseSequence(i + 1, [/^}\s*;?$/]);
2553
+ if (body.index >= lines.length) throw new ShellSyntaxError(`unterminated function ${functionMatch[1]}`);
2554
+ nodes.push({ type: "function", name: functionMatch[1], body: body.nodes });
2555
+ i = body.index + 1;
2556
+ continue;
2557
+ }
2558
+
2559
+ const subshellMatch = /^\((.*)\)\s*;?$/.exec(trimmed);
2560
+ if (subshellMatch) {
2561
+ nodes.push({ type: "subshell", source: subshellMatch[1] });
2562
+ i++;
2563
+ continue;
2564
+ }
2565
+
2566
+ if (/^if(?:\s|$)/.test(trimmed)) {
2567
+ const branches = [];
2568
+ let header = trimmed;
2569
+ while (!/;\s*then\s*$/.test(header) && ++i < lines.length)
2570
+ header += ` ${lines[i].trim()}`;
2571
+ if (!/;\s*then\s*$/.test(header)) throw new ShellSyntaxError("if requires then");
2572
+ let condition = header.replace(/^if\s*/, "").replace(/;\s*then\s*$/, "");
2573
+ i++;
2574
+ while (true) {
2575
+ const body = parseSequence(i, [/^elif(?:\s|$)/, /^else\s*;?$/, /^fi\s*;?$/]);
2576
+ branches.push({ condition, body: body.nodes });
2577
+ i = body.index;
2578
+ const stop = lines[i]?.trim() ?? "";
2579
+ if (/^elif(?:\s|$)/.test(stop)) {
2580
+ header = stop;
2581
+ while (!/;\s*then\s*$/.test(header) && ++i < lines.length)
2582
+ header += ` ${lines[i].trim()}`;
2583
+ if (!/;\s*then\s*$/.test(header)) throw new ShellSyntaxError("elif requires then");
2584
+ condition = header.replace(/^elif\s*/, "").replace(/;\s*then\s*$/, "");
2585
+ i++;
2586
+ continue;
2587
+ }
2588
+ let otherwise = [];
2589
+ if (/^else\s*;?$/.test(stop)) {
2590
+ const body = parseSequence(i + 1, [/^fi\s*;?$/]);
2591
+ otherwise = body.nodes;
2592
+ i = body.index;
2593
+ }
2594
+ if (!/^fi\s*;?$/.test(lines[i]?.trim() ?? ""))
2595
+ throw new ShellSyntaxError("unterminated if");
2596
+ nodes.push({ type: "if", branches, otherwise });
2597
+ i++;
2598
+ break;
2599
+ }
2600
+ continue;
2601
+ }
2602
+ if (/^(?:while|until)(?:\s|$)/.test(trimmed)) {
2603
+ const kind = trimmed.startsWith("until") ? "until" : "while";
2604
+ let header = trimmed;
2605
+ while (!/;\s*do\s*$/.test(header) && ++i < lines.length)
2606
+ header += ` ${lines[i].trim()}`;
2607
+ if (!/;\s*do\s*$/.test(header)) throw new ShellSyntaxError(`${kind} requires do`);
2608
+ const condition = header.replace(new RegExp(`^${kind}\\s*`), "").replace(/;\s*do\s*$/, "");
2609
+ const body = parseSequence(i + 1, [/^done(?:\s*<\s*.+)?\s*;?$/]);
2610
+ const done = /^done(?:\s*<\s*(.+?))?\s*;?$/.exec(lines[body.index]?.trim() ?? "");
2611
+ if (!done)
2612
+ throw new ShellSyntaxError(`unterminated ${kind}`);
2613
+ nodes.push({ type: "loop", kind, condition, body: body.nodes, input: done[1] ?? null });
2614
+ i = body.index + 1;
2615
+ continue;
2616
+ }
2617
+ if (/^for(?:\s|$)/.test(trimmed)) {
2618
+ let header = trimmed;
2619
+ while (!/;\s*do\s*$/.test(header) && ++i < lines.length)
2620
+ header += ` ${lines[i].trim()}`;
2621
+ const match = /^for\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+in\s+(.*?))?\s*;\s*do\s*$/.exec(header);
2622
+ if (!match) throw new ShellSyntaxError("invalid for command");
2623
+ const body = parseSequence(i + 1, [/^done\s*;?$/]);
2624
+ if (!/^done\s*;?$/.test(lines[body.index]?.trim() ?? ""))
2625
+ throw new ShellSyntaxError("unterminated for");
2626
+ nodes.push({ type: "for", name: match[1], words: match[2] ?? null, body: body.nodes });
2627
+ i = body.index + 1;
2628
+ continue;
2629
+ }
2630
+ if (/^case(?:\s|$)/.test(trimmed)) {
2631
+ let header = trimmed;
2632
+ while (!/\s+in\s*$/.test(header) && ++i < lines.length)
2633
+ header += ` ${lines[i].trim()}`;
2634
+ const match = /^case\s+(.+?)\s+in\s*$/.exec(header);
2635
+ if (!match) throw new ShellSyntaxError("case requires in");
2636
+ const arms = [];
2637
+ i++;
2638
+ while (i < lines.length && !/^esac\s*;?$/.test(lines[i].trim())) {
2639
+ if (!lines[i].trim() || lines[i].trim().startsWith("#")) { i++; continue; }
2640
+ const arm = /^(.+?)\)\s*$/.exec(lines[i].trim());
2641
+ if (!arm) throw new ShellSyntaxError("case pattern requires )");
2642
+ const bodyLines = [];
2643
+ i++;
2644
+ while (i < lines.length && !/^esac\s*;?$/.test(lines[i].trim())) {
2645
+ const current = lines[i];
2646
+ const terminator = current.indexOf(";;");
2647
+ if (terminator >= 0) {
2648
+ if (current.slice(0, terminator).trim()) bodyLines.push(current.slice(0, terminator));
2649
+ i++;
2650
+ break;
2651
+ }
2652
+ bodyLines.push(current);
2653
+ i++;
2654
+ }
2655
+ arms.push({ patterns: arm[1].split("|").map((pattern) => pattern.trim()), body: parseCompoundScript(bodyLines.join("\n")) });
2656
+ }
2657
+ if (!/^esac\s*;?$/.test(lines[i]?.trim() ?? "")) throw new ShellSyntaxError("unterminated case");
2658
+ nodes.push({ type: "case", word: match[1], arms });
2659
+ i++;
2660
+ continue;
2661
+ }
2662
+ const grown = growHeredocLine(lines, i);
2663
+ nodes.push({ type: "command", source: grown.source });
2664
+ i = grown.next;
2665
+ }
2666
+ return { nodes, index: i };
2667
+ };
2668
+ const parsed = parseSequence(0);
2669
+ return parsed.nodes;
2670
+ }
2671
+
2672
+ // Every "needs more input before this can run" condition in this parser
2673
+ // (open quotes, an open $()/${}/backtick, an open here-document, or a
2674
+ // compound command still missing its closing keyword) raises a
2675
+ // ShellSyntaxError whose message starts with "unterminated". An interactive
2676
+ // front end can use this, mirroring mksh's PS2 continuation prompt, to tell
2677
+ // "still typing this command" apart from a real syntax error.
2678
+ export function needsMoreInput(source) {
2679
+ source = normalizeSource(source);
2680
+ try {
2681
+ if (hasCompoundSyntax(source)) parseCompoundScript(source);
2682
+ else tokenize(source, { strict: true });
2683
+ } catch (error) {
2684
+ return error instanceof ShellSyntaxError && error.message.startsWith("unterminated");
2685
+ }
2686
+ return false;
2687
+ }
2688
+
2689
+ async function executeNodeList(nodes, state, options = {}) {
2690
+ const capture = options.capture ?? true;
2691
+ const stdout = [], stderr = [];
2692
+ let execution = result(state.lastStatus);
2693
+ const append = (value) => {
2694
+ if (!capture) return;
2695
+ if (value.stdout.byteLength) stdout.push(value.stdout);
2696
+ if (value.stderr.byteLength) stderr.push(value.stderr);
2697
+ };
2698
+ for (const node of nodes) {
2699
+ if (state.exitRequested) break;
2700
+ if (node.type === "function") {
2701
+ state.functions[node.name] = node.body;
2702
+ continue;
2703
+ }
2704
+ if (node.type === "command") {
2705
+ execution = await execute(node.source, state, { capture, simple: true });
2706
+ append(execution);
2707
+ continue;
2708
+ }
2709
+ if (node.type === "subshell") {
2710
+ const child = {
2711
+ ...state,
2712
+ env: { ...state.env },
2713
+ aliases: { ...state.aliases },
2714
+ functions: { ...state.functions },
2715
+ readonly: new Set(state.readonly),
2716
+ directoryHistory: [...state.directoryHistory],
2717
+ tabs: [...state.tabs],
2718
+ exitRequested: false,
2719
+ exitStatus: 0,
2720
+ };
2721
+ execution = await execute(node.source, child, { capture });
2722
+ append(execution);
2723
+ state.lastStatus = execution.status;
2724
+ continue;
2725
+ }
2726
+ if (node.type === "loop") {
2727
+ const previousReadLines = state.readLines;
2728
+ let ranBody = false;
2729
+ try {
2730
+ if (node.input !== null) {
2731
+ const parsed = parse(`__input ${node.input}`)[0]?.pipeline[0];
2732
+ const operand = (await expandWord(parsed.words[1], state, { single: true }))[0];
2733
+ const path = isAbsolute(nativePath(operand)) ? nativePath(operand) : resolvePath(nativePath(state.cwd), nativePath(operand));
2734
+ const text = await Bun.file(path).text();
2735
+ state.readLines = text.split("\n");
2736
+ if (state.readLines.at(-1) === "") state.readLines.pop();
2737
+ }
2738
+ while (true) {
2739
+ const checked = await execute(node.condition, state, { capture, simple: true });
2740
+ append(checked);
2741
+ const proceed = node.kind === "until" ? checked.status !== 0 : checked.status === 0;
2742
+ if (!proceed || state.exitRequested) {
2743
+ if (!ranBody) execution = result();
2744
+ break;
2745
+ }
2746
+ execution = await executeNodeList(node.body, state, { capture });
2747
+ ranBody = true;
2748
+ append(execution);
2749
+ if (state.exitRequested) break;
2750
+ }
2751
+ } catch (error) {
2752
+ execution = result(1, "", `bunmsh: ${error.message}\n`);
2753
+ append(execution);
2754
+ } finally {
2755
+ state.readLines = previousReadLines;
2756
+ }
2757
+ state.lastStatus = execution.status;
2758
+ continue;
2759
+ }
2760
+ if (node.type === "for") {
2761
+ let values;
2762
+ if (node.words === null) values = state.args.slice(1);
2763
+ else {
2764
+ const parsed = parse(`__for ${node.words}`)[0]?.pipeline[0];
2765
+ values = [];
2766
+ for (const word of parsed?.words.slice(1) ?? []) values.push(...await expandWord(word, state));
2767
+ }
2768
+ execution = result();
2769
+ for (const value of values) {
2770
+ if (state.readonly.has(node.name)) { execution = readonlyError(node.name); break; }
2771
+ state.env[node.name] = value;
2772
+ execution = await executeNodeList(node.body, state, { capture });
2773
+ append(execution);
2774
+ if (state.exitRequested) break;
2775
+ }
2776
+ state.lastStatus = execution.status;
2777
+ continue;
2778
+ }
2779
+ if (node.type === "case") {
2780
+ const parsed = parse(`__case ${node.word}`)[0]?.pipeline[0];
2781
+ const value = (await expandWord(parsed.words[1], state, { single: true }))[0] ?? "";
2782
+ execution = result();
2783
+ for (const arm of node.arms) {
2784
+ let matches = false;
2785
+ for (let pattern of arm.patterns) {
2786
+ pattern = pattern.replace(/^(['"])(.*)\1$/, "$2");
2787
+ pattern = stripExpansionMarkers(await expandText(pattern, state));
2788
+ if (globPatternRegex(pattern, true, true, true).test(value)) { matches = true; break; }
2789
+ }
2790
+ if (!matches) continue;
2791
+ execution = await executeNodeList(arm.body, state, { capture });
2792
+ append(execution);
2793
+ break;
2794
+ }
2795
+ state.lastStatus = execution.status;
2796
+ continue;
2797
+ }
2798
+ let selected = null;
2799
+ for (const branch of node.branches) {
2800
+ let condition = branch.condition.trim();
2801
+ let negate = false;
2802
+ while (/^!\s+/.test(condition)) { negate = !negate; condition = condition.replace(/^!\s+/, ""); }
2803
+ const checked = await execute(condition, state, { capture, simple: true });
2804
+ append(checked);
2805
+ if ((checked.status === 0) !== negate) { selected = branch.body; break; }
2806
+ }
2807
+ execution = selected
2808
+ ? await executeNodeList(selected, state, { capture })
2809
+ : await executeNodeList(node.otherwise, state, { capture });
2810
+ append(execution);
2811
+ state.lastStatus = execution.status;
2812
+ }
2813
+ return result(execution.status, concatBytes(stdout), concatBytes(stderr));
2814
+ }
2815
+
2816
+ async function executeFunction(name, argv, state) {
2817
+ const previousArgs = state.args;
2818
+ state.args = [previousArgs[0], ...argv.slice(1)];
2819
+ try { return await executeNodeList(state.functions[name], state); }
2820
+ finally { state.args = previousArgs; }
2821
+ }
2822
+
2823
+ async function runExternal(
2824
+ argv,
2825
+ state,
2826
+ input,
2827
+ captureStdout,
2828
+ captureStderr,
2829
+ stdoutSink = null,
2830
+ runtimeOptions = {},
2831
+ ) {
2832
+ // The shell language always exposes forward slashes. Only the executable
2833
+ // pathname needs native separators at the Windows process boundary.
2834
+ const spawnArgv = process.platform === "win32" && argv[0].includes("/")
2835
+ ? [nativePath(argv[0]), ...argv.slice(1)]
2836
+ : argv;
2837
+ const options = {
2838
+ cmd: spawnArgv,
2839
+ cwd: nativePath(state.cwd),
2840
+ env: state.env,
2841
+ stdin: input === null ? "inherit" : input,
2842
+ stdout: captureStdout || stdoutSink ? "pipe" : "inherit",
2843
+ stderr: captureStderr || runtimeOptions.stderrSink ? "pipe" : "inherit",
2844
+ onExit(proc, exitCode, signalCode, error) {
2845
+ runtimeOptions.onExit?.(proc, exitCode, signalCode, error);
2846
+ },
2847
+ };
2848
+ try {
2849
+ const proc = Bun.spawn(options);
2850
+ const pipelineKillSignal = runtimeOptions.pipelineKillSignal ?? "SIGPIPE";
2851
+ // A downstream pipeline stage that closes early (for example `head -n 3`)
2852
+ // leaves nothing reading the other end of `stdoutSink`/`stderrSink` (a
2853
+ // link's readable side, fed as the next stage's stdin). Bun never
2854
+ // releases the reader lock it holds on that side even once the process
2855
+ // reading it has exited, so a write already in flight into it (from the
2856
+ // pipeTo() calls below) backpressures forever instead of settling, and
2857
+ // there is no way to cancel or abort it from out here to unstick it —
2858
+ // every avenue requires a lock only Bun's own, already-exited consumer
2859
+ // holds. So this does not wait for that write to actually finish once
2860
+ // the caller (see onSpawn below) says the downstream stage is done;
2861
+ // it is abandoned in place, harmless since the killed process here
2862
+ // will not produce anything more for it to forward anyway.
2863
+ const abandonStreaming = new Promise((resolve) => {
2864
+ runtimeOptions.onSpawn?.(proc, pipelineKillSignal, resolve);
2865
+ });
2866
+ const outputPromise = captureStdout
2867
+ ? new Response(proc.stdout).arrayBuffer()
2868
+ : Promise.resolve(new ArrayBuffer(0));
2869
+ const streamPromise = stdoutSink
2870
+ ? Promise.race([
2871
+ proc.stdout.pipeTo(stdoutSink).catch(() => {
2872
+ try { proc.kill(pipelineKillSignal); } catch {}
2873
+ }),
2874
+ abandonStreaming,
2875
+ ])
2876
+ : Promise.resolve();
2877
+ const errorStreamPromise = runtimeOptions.stderrSink
2878
+ ? Promise.race([
2879
+ proc.stderr.pipeTo(runtimeOptions.stderrSink).catch(() => {}),
2880
+ abandonStreaming,
2881
+ ])
2882
+ : Promise.resolve();
2883
+ const errorPromise = captureStderr
2884
+ ? new Response(proc.stderr).arrayBuffer()
2885
+ : Promise.resolve(new ArrayBuffer(0));
2886
+ const [status, output, error] = await Promise.all([
2887
+ proc.exited,
2888
+ outputPromise,
2889
+ errorPromise,
2890
+ streamPromise,
2891
+ errorStreamPromise,
2892
+ ]);
2893
+ const execution = result(status, new Uint8Array(output), new Uint8Array(error));
2894
+ execution.stdoutStreamed = Boolean(stdoutSink);
2895
+ execution.stderrStreamed = Boolean(runtimeOptions.stderrSink);
2896
+ return execution;
2897
+ } catch (error) {
2898
+ return result(127, "", `bunmsh: ${argv[0]}: ${error.message}\n`);
2899
+ }
2900
+ }
2901
+
2902
+ function runsInPipelineSubprocess(argv, state) {
2903
+ if (["command", "builtin", "__builtin", "time"].includes(argv[0])) return true;
2904
+ if (Object.hasOwn(builtins, argv[0])) return true;
2905
+ if (Object.hasOwn(state.functions, argv[0])) return true;
2906
+ return !argv[0].includes("/") &&
2907
+ Object.hasOwn(fallbackBuiltins, argv[0]) &&
2908
+ !findExecutable(argv[0], state);
2909
+ }
2910
+
2911
+ function pipelineBuiltinArgv(argv) {
2912
+ return IS_COMPILED
2913
+ ? [...EXECUTABLE_COMMAND, "-cc", ...argv]
2914
+ : [...BUN_RUNTIME_COMMAND, BUNMSH_ENTRY, "-cc", ...argv];
2915
+ }
2916
+
2917
+ function pipelineBuiltinState(state) {
2918
+ return {
2919
+ ...state,
2920
+ env: {
2921
+ ...state.env,
2922
+ [PIPELINE_STATE_ENV]: JSON.stringify({
2923
+ aliases: state.aliases,
2924
+ functions: state.functions,
2925
+ readonly: [...state.readonly],
2926
+ args: state.args,
2927
+ pathSearch: state.pathSearch,
2928
+ }),
2929
+ },
2930
+ };
2931
+ }
2932
+
2933
+ function closedPipe(error) {
2934
+ const code = error?.code ?? error?.cause?.code;
2935
+ return code === "EPIPE" || code === "ECONNRESET" ||
2936
+ /broken pipe|closed pipe|connection reset/i.test(error?.message ?? "");
2937
+ }
2938
+
2939
+ async function runYes(argv) {
2940
+ const line = `${argv.length > 1 ? argv.slice(1).join(" ") : "y"}\n`;
2941
+ const repetitions = Math.max(1, Math.floor(8192 / Math.max(1, bytes(line).byteLength)));
2942
+ const chunk = bytes(line.repeat(repetitions));
2943
+ const writer = Bun.stdout.writer();
2944
+ try {
2945
+ while (true) {
2946
+ writer.write(chunk);
2947
+ await writer.flush();
2948
+ }
2949
+ } catch (error) {
2950
+ if (!closedPipe(error)) throw error;
2951
+ } finally {
2952
+ try { writer.end(); } catch {}
2953
+ }
2954
+ return result();
2955
+ }
2956
+
2957
+ function parseCommandOptions(argv) {
2958
+ let defaultPath = false;
2959
+ let query = null;
2960
+ let i = 1;
2961
+ for (; i < argv.length; i++) {
2962
+ const arg = argv[i];
2963
+ if (arg === "--") {
2964
+ i++;
2965
+ break;
2966
+ }
2967
+ if (arg === "-" || !arg.startsWith("-")) break;
2968
+ for (const option of arg.slice(1)) {
2969
+ if (option === "p") defaultPath = true;
2970
+ else if (option === "v" || option === "V") query = option;
2971
+ else return { error: option };
2972
+ }
2973
+ }
2974
+ return { defaultPath, query, operands: argv.slice(i) };
2975
+ }
2976
+
2977
+ function describeCommand(name, state, verbose, defaultPath) {
2978
+ const alias = state.aliases[name];
2979
+ if (alias) {
2980
+ const definition = quoteShellWord(alias.join(" "));
2981
+ return verbose
2982
+ ? `${name} is an alias for ${definition}\n`
2983
+ : `alias ${name}=${definition}\n`;
2984
+ }
2985
+ if (Object.hasOwn(builtins, name))
2986
+ return verbose ? `${name} is a shell builtin\n` : `${name}\n`;
2987
+ const path = findExecutable(name, state, defaultPath);
2988
+ if (path) return verbose ? `${name} is ${path}\n` : `${path}\n`;
2989
+ if (Object.hasOwn(fallbackBuiltins, name))
2990
+ return verbose ? `${name} is a fallback shell builtin\n` : `${name}\n`;
2991
+ return verbose ? `${name} not found\n` : null;
2992
+ }
2993
+
2994
+ async function runCommandArgv(
2995
+ argv,
2996
+ state,
2997
+ input,
2998
+ captureStdout,
2999
+ captureStderr,
3000
+ options = {},
3001
+ ) {
3002
+ let commandArgv = argv;
3003
+ let commandState = state;
3004
+ if (options.pipelineStage && runsInPipelineSubprocess(commandArgv, commandState))
3005
+ return runExternal(
3006
+ pipelineBuiltinArgv(commandArgv),
3007
+ pipelineBuiltinState(commandState),
3008
+ input,
3009
+ captureStdout,
3010
+ captureStderr,
3011
+ options.stdoutSink,
3012
+ { ...options, pipelineKillSignal: "SIGTERM" },
3013
+ );
3014
+ while (["command", "builtin", "__builtin"].includes(commandArgv[0])) {
3015
+ if (requestsBuiltinHelp(commandArgv) && hasBuiltinHelp(commandArgv[0]))
3016
+ return builtinHelp(commandArgv[0]);
3017
+ if (commandArgv[0] !== "command") {
3018
+ const builtinName = commandArgv[0];
3019
+ const start = commandArgv[1] === "--" ? 2 : 1;
3020
+ if (start >= commandArgv.length) {
3021
+ if (builtinName === "builtin")
3022
+ return result(0, `${builtinNames().join("\n")}\n`);
3023
+ return result();
3024
+ }
3025
+ commandArgv = commandArgv.slice(start);
3026
+ if (!Object.hasOwn(builtins, commandArgv[0]) &&
3027
+ !Object.hasOwn(fallbackBuiltins, commandArgv[0]))
3028
+ return result(1, "", `bunmsh: builtin: ${commandArgv[0]}: not found\n`);
3029
+ if (requestsBuiltinHelp(commandArgv) && hasBuiltinHelp(commandArgv[0]))
3030
+ return builtinHelp(commandArgv[0]);
3031
+ if (!["command", "builtin", "__builtin", "time", "yes"].includes(commandArgv[0]))
3032
+ return (builtins[commandArgv[0]] ?? fallbackBuiltins[commandArgv[0]])(
3033
+ commandArgv,
3034
+ commandState,
3035
+ input,
3036
+ { captureStdout, captureStderr, options },
3037
+ );
3038
+ continue;
3039
+ }
3040
+ const parsed = parseCommandOptions(commandArgv);
3041
+ if (parsed.error)
3042
+ return result(1, "", `bunmsh: command: -${parsed.error}: unknown option\n`);
3043
+ if (parsed.query) {
3044
+ let status = 0;
3045
+ let output = "";
3046
+ for (const name of parsed.operands) {
3047
+ const description = describeCommand(
3048
+ name,
3049
+ commandState,
3050
+ parsed.query === "V",
3051
+ parsed.defaultPath,
3052
+ );
3053
+ if (description === null || description.endsWith(" not found\n")) status = 1;
3054
+ if (description !== null) output += description;
3055
+ if (status && parsed.query === "v") break;
3056
+ }
3057
+ return result(status, output);
3058
+ }
3059
+ if (parsed.operands.length === 0) return result();
3060
+ commandArgv = parsed.operands;
3061
+ if (parsed.defaultPath) {
3062
+ commandState = { ...commandState, env: { ...commandState.env, PATH: DEFAULT_COMMAND_PATH } };
3063
+ }
3064
+ }
3065
+ if (requestsBuiltinHelp(commandArgv) && hasBuiltinHelp(commandArgv[0]) &&
3066
+ (Object.hasOwn(builtins, commandArgv[0]) ||
3067
+ (!findExecutable(commandArgv[0], commandState) &&
3068
+ Object.hasOwn(fallbackBuiltins, commandArgv[0]))))
3069
+ return builtinHelp(commandArgv[0]);
3070
+ if (commandArgv[0] === "time") {
3071
+ const started = Bun.nanoseconds();
3072
+ const execution = commandArgv.length === 1
3073
+ ? result()
3074
+ : await runCommandArgv(
3075
+ commandArgv.slice(1),
3076
+ commandState,
3077
+ input,
3078
+ captureStdout,
3079
+ captureStderr,
3080
+ );
3081
+ const milliseconds = (Bun.nanoseconds() - started) / 1_000_000;
3082
+ execution.stderr = concatBytes([
3083
+ execution.stderr,
3084
+ `real ${formatMilliseconds(milliseconds)} ms\n`,
3085
+ ]);
3086
+ return execution;
3087
+ }
3088
+ if (commandArgv[0] === "yes") return runYes(commandArgv);
3089
+ if (Object.hasOwn(commandState.functions, commandArgv[0]))
3090
+ return executeFunction(commandArgv[0], commandArgv, commandState);
3091
+ if (Object.hasOwn(builtins, commandArgv[0]))
3092
+ return builtins[commandArgv[0]](
3093
+ commandArgv,
3094
+ commandState,
3095
+ input,
3096
+ { captureStdout, captureStderr, options },
3097
+ );
3098
+ if (!commandArgv[0].includes("/") && Object.hasOwn(fallbackBuiltins, commandArgv[0]) &&
3099
+ !findExecutable(commandArgv[0], commandState))
3100
+ return fallbackBuiltins[commandArgv[0]](
3101
+ commandArgv,
3102
+ commandState,
3103
+ input,
3104
+ { captureStdout, captureStderr, options },
3105
+ );
3106
+ if (!commandState.pathSearch && !commandArgv[0].includes("/"))
3107
+ return result(127, "", `bunmsh: ${commandArgv[0]}: not found\n`);
3108
+ return runExternal(
3109
+ commandArgv,
3110
+ commandState,
3111
+ input,
3112
+ captureStdout,
3113
+ captureStderr,
3114
+ options.stdoutSink,
3115
+ options,
3116
+ );
3117
+ }
3118
+
3119
+ function redirectInput(path) {
3120
+ const fd = openSync(path, "r");
3121
+ return Readable.toWeb(createReadStream(path, { fd, autoClose: true }));
3122
+ }
3123
+
3124
+ function redirectOutput(path, append) {
3125
+ const fd = openSync(path, append ? "a" : "w");
3126
+ return Writable.toWeb(createWriteStream(path, { fd, autoClose: true }));
3127
+ }
3128
+
3129
+ // Mirrors the double-quote scanning in tokenize(): an unquoted heredoc body
3130
+ // only lets backslash escape $, `, \, and a trailing newline before the rest
3131
+ // goes through the normal $/`` expansion pipeline.
3132
+ function heredocLiteralPass(text) {
3133
+ let output = "";
3134
+ let i = 0;
3135
+ while (i < text.length) {
3136
+ if (text[i] === "\\" && i + 1 < text.length) {
3137
+ const next = text[i + 1];
3138
+ if (next === "$" || next === "\\" || next === "`" || next === "\n") {
3139
+ if (next !== "\n") output += `\u0000${next}`;
3140
+ i += 2;
3141
+ continue;
3142
+ }
3143
+ }
3144
+ if ((text[i] === "$" && ["(", "{"].includes(text[i + 1])) || text[i] === "`") {
3145
+ const end = text[i + 1] === "{" ? parameterEnd(text, i) : substitutionEnd(text, i);
3146
+ output += text.slice(i, end);
3147
+ i = end;
3148
+ continue;
3149
+ }
3150
+ output += text[i++];
3151
+ }
3152
+ return output;
3153
+ }
3154
+
3155
+ function redirectHeredoc(content) {
3156
+ return new Response(bytes(content)).body;
3157
+ }
3158
+
3159
+ async function runCommand(command, state, options = {}) {
3160
+ state.expansionStatus = null;
3161
+ const standaloneTilde =
3162
+ command.words.length === 1 &&
3163
+ command.words[0].fragments.length === 1 &&
3164
+ command.words[0].fragments[0].quote === "none" &&
3165
+ command.words[0].fragments[0].text === "~";
3166
+ let assignmentPrefix = true;
3167
+ let aliasEligible = false;
3168
+ const expanded = standaloneTilde ? ["~"] : [];
3169
+ if (!standaloneTilde) {
3170
+ for (const word of command.words) {
3171
+ const literal = word.fragments.map((fragment) => fragment.text.replaceAll("\u0000", "")).join("");
3172
+ const assignment = assignmentPrefix && /^[A-Za-z_][A-Za-z0-9_]*=/.test(literal);
3173
+ expanded.push(...await expandWord(word, state, { single: assignment, assignment }));
3174
+ if (!assignment) {
3175
+ if (assignmentPrefix)
3176
+ aliasEligible = word.fragments.every((fragment) =>
3177
+ fragment.quote === "none" && !fragment.text.includes("\u0000"));
3178
+ assignmentPrefix = false;
3179
+ }
3180
+ }
3181
+ }
3182
+ const localEnv = {};
3183
+ while (expanded.length) {
3184
+ const assignment = splitAssignment(expanded[0]);
3185
+ if (!assignment) break;
3186
+ localEnv[assignment.name] = assignment.value;
3187
+ expanded.shift();
3188
+ }
3189
+ if (expanded.length === 0) {
3190
+ for (const name of Object.keys(localEnv))
3191
+ if (state.readonly.has(name)) return readonlyError(name);
3192
+ Object.assign(state.env, localEnv);
3193
+ return result(state.expansionStatus ?? 0);
3194
+ }
3195
+ const alias = aliasEligible ? state.aliases[expanded[0]] : null;
3196
+ if (alias) expanded.splice(0, 1, ...alias);
3197
+
3198
+ let input = options.input ?? null;
3199
+ let stdoutPath = null;
3200
+ let stdoutAppend = false;
3201
+ let stderrPath = null;
3202
+ let stderrAppend = false;
3203
+ let stdoutToStderr = false;
3204
+ let stderrToStdout = false;
3205
+ for (const redirect of command.redirects) {
3206
+ if (redirect.op === "1>&2") { stdoutToStderr = true; continue; }
3207
+ if (redirect.op === "2>&1") { stderrToStdout = true; continue; }
3208
+ if (redirect.op === "<<" || redirect.op === "<<-") {
3209
+ const heredoc = redirect.heredoc ?? { body: "", expand: false };
3210
+ const content = heredoc.expand
3211
+ ? stripExpansionMarkers(
3212
+ (await expandText(heredocLiteralPass(heredoc.body), state)).replaceAll("\u0000", ""),
3213
+ )
3214
+ : heredoc.body;
3215
+ if (input instanceof ReadableStream) {
3216
+ try { await input.cancel(); } catch {}
3217
+ }
3218
+ input = redirectHeredoc(content);
3219
+ continue;
3220
+ }
3221
+ if (redirect.op === "<<<") {
3222
+ const [word] = await expandWord(redirect.target, state, { single: true, noTilde: true });
3223
+ if (input instanceof ReadableStream) {
3224
+ try { await input.cancel(); } catch {}
3225
+ }
3226
+ input = redirectHeredoc(`${word}\n`);
3227
+ continue;
3228
+ }
3229
+ const targets = await expandWord(redirect.target, state, { single: true });
3230
+ const target = targets[0];
3231
+ const path = target.startsWith("/") ? target : `${state.cwd}/${target}`;
3232
+ if (redirect.op === "<") {
3233
+ try {
3234
+ if (input instanceof ReadableStream) {
3235
+ try { await input.cancel(); } catch {}
3236
+ }
3237
+ input = redirectInput(path);
3238
+ } catch (error) {
3239
+ return result(1, "", `bunmsh: ${target}: ${error.message}\n`);
3240
+ }
3241
+ } else if (redirect.op === ">" || redirect.op === ">>") {
3242
+ stdoutPath = path;
3243
+ stdoutAppend = redirect.op === ">>";
3244
+ } else {
3245
+ stderrPath = path;
3246
+ stderrAppend = redirect.op === "2>>";
3247
+ }
3248
+ }
3249
+
3250
+
3251
+ let stdoutRedirect = null;
3252
+ let stderrRedirect = null;
3253
+ try {
3254
+ if (stdoutPath !== null) {
3255
+ // A command-level redirect replaces the pipeline's stdout connection.
3256
+ // Close that unused connection now so the next stage observes EOF.
3257
+ if (options.stdoutSink) {
3258
+ try { await options.stdoutSink.getWriter().close(); } catch {}
3259
+ }
3260
+ stdoutRedirect = redirectOutput(stdoutPath, stdoutAppend);
3261
+ }
3262
+ if (stderrPath !== null)
3263
+ stderrRedirect = redirectOutput(stderrPath, stderrAppend);
3264
+ } catch (error) {
3265
+ return result(1, "", `bunmsh: ${stdoutPath ?? stderrPath}: ${error.message}\n`);
3266
+ }
3267
+
3268
+ const commandState = options.subshell
3269
+ ? {
3270
+ ...state,
3271
+ env: { ...state.env, ...localEnv },
3272
+ directoryHistory: [...state.directoryHistory],
3273
+ tabs: [...state.tabs],
3274
+ readonly: new Set(state.readonly),
3275
+ }
3276
+ : state;
3277
+ if (!options.subshell) {
3278
+ for (const name of Object.keys(localEnv))
3279
+ if (commandState.readonly.has(name)) return readonlyError(name);
3280
+ Object.assign(commandState.env, localEnv);
3281
+ }
3282
+
3283
+ const captureStdout = (options.captureStdout || stdoutToStderr) && stdoutRedirect === null;
3284
+ const captureStderr = (options.captureStderr || stderrToStdout) && stderrRedirect === null;
3285
+ const execution = await runCommandArgv(
3286
+ expanded,
3287
+ commandState,
3288
+ input,
3289
+ captureStdout,
3290
+ captureStderr,
3291
+ {
3292
+ pipelineStage: Boolean(options.pipelineStage || stdoutRedirect || stderrRedirect),
3293
+ stdoutSink: stdoutRedirect ?? options.stdoutSink ?? null,
3294
+ stderrSink: stderrRedirect,
3295
+ onSpawn: options.onSpawn,
3296
+ onExit: options.onExit,
3297
+ },
3298
+ );
3299
+
3300
+ if (stdoutToStderr && execution.stdout.byteLength) {
3301
+ execution.stderr = concatBytes([execution.stderr, execution.stdout]);
3302
+ execution.stdout = bytes();
3303
+ }
3304
+ if (stderrToStdout && execution.stderr.byteLength) {
3305
+ execution.stdout = concatBytes([execution.stdout, execution.stderr]);
3306
+ execution.stderr = bytes();
3307
+ }
3308
+
3309
+ return execution;
3310
+ }
3311
+
3312
+ async function runPipeline(pipeline, state, options = {}) {
3313
+ if (pipeline.length === 1)
3314
+ return runCommand(pipeline[0], state, {
3315
+ captureStdout: Boolean(options.capture),
3316
+ captureStderr: Boolean(options.capture),
3317
+ });
3318
+
3319
+ const links = Array.from(
3320
+ { length: pipeline.length - 1 },
3321
+ () => new TransformStream(),
3322
+ );
3323
+ const linksConnected = links.map((_, i) => {
3324
+ const stdoutRedirected = pipeline[i].redirects.some((redirect) =>
3325
+ redirect.op === ">" || redirect.op === ">>");
3326
+ const stdinRedirected = pipeline[i + 1].redirects.some((redirect) =>
3327
+ ["<", "<<", "<<-", "<<<"].includes(redirect.op));
3328
+ return !stdoutRedirected && !stdinRedirected;
3329
+ });
3330
+ const processes = Array(pipeline.length).fill(null);
3331
+ const finished = Array(pipeline.length).fill(false);
3332
+ const stopUpstream = (index) => {
3333
+ if (index < 0 || !linksConnected[index]) return;
3334
+ const processInfo = processes[index];
3335
+ if (processInfo) {
3336
+ try { processInfo.proc.kill(processInfo.signal); } catch {}
3337
+ // Killing the process only stops it from producing more output. Its
3338
+ // own `proc.stdout.pipeTo()` forwarding that output into this link can
3339
+ // still be stuck mid-write, backpressured forever with nobody left to
3340
+ // read the link's other end now that the downstream stage is done —
3341
+ // see runExternal for why that write can't be canceled directly.
3342
+ // This just stops waiting on it instead.
3343
+ try { processInfo.detachStream(); } catch {}
3344
+ }
3345
+ };
3346
+ const stages = pipeline.map((command, i) => {
3347
+ const last = i === pipeline.length - 1;
3348
+ return runCommand(command, state, {
3349
+ input: i === 0 ? null : links[i - 1].readable,
3350
+ captureStdout: last && Boolean(options.capture),
3351
+ captureStderr: Boolean(options.capture),
3352
+ stdoutSink: last ? null : links[i].writable,
3353
+ pipelineStage: true,
3354
+ subshell: true,
3355
+ onSpawn(proc, signal, detachStream) {
3356
+ processes[i] = { proc, signal, detachStream };
3357
+ if (finished[i + 1]) stopUpstream(i);
3358
+ },
3359
+ onExit() {
3360
+ finished[i] = true;
3361
+ stopUpstream(i - 1);
3362
+ },
3363
+ }).then(async (execution) => {
3364
+ // Redirected commands and any future buffered pipeline stages do not
3365
+ // consume stdoutSink themselves. Forward their finite result and close
3366
+ // the link here. Streaming subprocesses already closed it via pipeTo().
3367
+ if (!last && !execution.stdoutStreamed) {
3368
+ const writer = links[i].writable.getWriter();
3369
+ try {
3370
+ if (execution.stdout.byteLength) await writer.write(execution.stdout);
3371
+ await writer.close();
3372
+ } catch {}
3373
+ }
3374
+ return execution;
3375
+ }).finally(() => {
3376
+ finished[i] = true;
3377
+ stopUpstream(i - 1);
3378
+ });
3379
+ });
3380
+ const executions = await Promise.all(stages);
3381
+ const final = executions.at(-1);
3382
+ if (options.capture) {
3383
+ final.stderr = concatBytes(executions.map((execution) => execution.stderr));
3384
+ }
3385
+ return final;
3386
+ }
3387
+
3388
+ export async function execute(source, state = createState(), io = {}) {
3389
+ source = normalizeSource(source);
3390
+ if (source === "?") {
3391
+ const execution = result(0, `${state.lastStatus}\n`);
3392
+ state.lastStatus = 0;
3393
+ if (!io.capture) await writeStream(process.stdout, execution.stdout);
3394
+ return execution;
3395
+ }
3396
+ const rawSource = source.trimStart();
3397
+ if (rawSource.startsWith("Bun.")) {
3398
+ let execution;
3399
+ const previousCwd = process.cwd();
3400
+ try {
3401
+ try {
3402
+ process.chdir(nativePath(state.cwd));
3403
+ const value = await eval(rawSource);
3404
+ execution = result(0, value === undefined ? "" : `${formatValue(value)}\n`);
3405
+ } finally {
3406
+ process.chdir(previousCwd);
3407
+ }
3408
+ } catch (error) {
3409
+ execution = result(1, "", `${error?.stack ?? formatValue(error)}\n`);
3410
+ }
3411
+ state.lastStatus = execution.status;
3412
+ if (!io.capture) {
3413
+ if (execution.stdout.byteLength) await writeStream(process.stdout, execution.stdout);
3414
+ if (execution.stderr.byteLength) await writeStream(process.stderr, execution.stderr);
3415
+ }
3416
+ return execution;
3417
+ }
3418
+ const lines = source.match(/.*(?:\n|$)/g)?.filter(Boolean) ?? [];
3419
+ if (lines.some((line) => line.trimStart().startsWith("Bun."))) {
3420
+ const parts = [];
3421
+ let shellSource = "";
3422
+ for (const line of lines) {
3423
+ if (!line.trimStart().startsWith("Bun.")) {
3424
+ shellSource += line;
3425
+ continue;
3426
+ }
3427
+ // Only split at a valid top-level shell boundary. This prevents a
3428
+ // Bun.-looking line inside an open quote or $(...) from escaping its
3429
+ // surrounding shell construct.
3430
+ try {
3431
+ if (shellSource.trim()) parse(shellSource);
3432
+ } catch {
3433
+ shellSource += line;
3434
+ continue;
3435
+ }
3436
+ if (shellSource.trim()) parts.push(shellSource);
3437
+ parts.push(line.replace(/\n$/, ""));
3438
+ shellSource = "";
3439
+ }
3440
+ if (shellSource.trim()) parts.push(shellSource);
3441
+ if (parts.length > 1) {
3442
+ let execution = result(state.lastStatus);
3443
+ const stdout = [], stderr = [];
3444
+ for (const part of parts) {
3445
+ execution = await execute(part, state, io);
3446
+ if (io.capture) {
3447
+ if (execution.stdout.byteLength) stdout.push(execution.stdout);
3448
+ if (execution.stderr.byteLength) stderr.push(execution.stderr);
3449
+ }
3450
+ if (state.exitRequested) break;
3451
+ }
3452
+ return io.capture
3453
+ ? result(execution.status, concatBytes(stdout), concatBytes(stderr))
3454
+ : execution;
3455
+ }
3456
+ }
3457
+ let compoundSyntax = false;
3458
+ try { compoundSyntax = !io.simple && hasCompoundSyntax(source); } catch {}
3459
+ if (compoundSyntax) {
3460
+ let execution;
3461
+ try {
3462
+ execution = await executeNodeList(parseCompoundScript(source), state, {
3463
+ capture: Boolean(io.capture),
3464
+ });
3465
+ }
3466
+ catch (error) {
3467
+ const message = error instanceof ShellSyntaxError ? error.message : String(error);
3468
+ execution = result(2, "", `bunmsh: syntax error: ${message}\n`);
3469
+ }
3470
+ state.lastStatus = execution.status;
3471
+ if (!io.capture) {
3472
+ if (execution.stdout.byteLength) await writeStream(process.stdout, execution.stdout);
3473
+ if (execution.stderr.byteLength) await writeStream(process.stderr, execution.stderr);
3474
+ }
3475
+ return execution;
3476
+ }
3477
+
3478
+ let jobs;
3479
+ try {
3480
+ jobs = parse(source);
3481
+ } catch (error) {
3482
+ const message = error instanceof ShellSyntaxError ? error.message : String(error);
3483
+ const output = `bunmsh: syntax error: ${message}\n`;
3484
+ if (io.capture) return result(2, "", output);
3485
+ console.error(output.trimEnd());
3486
+ state.lastStatus = 2;
3487
+ return result(2);
3488
+ }
3489
+
3490
+ let execution = result(state.lastStatus);
3491
+ const capturedStdout = [];
3492
+ const capturedStderr = [];
3493
+ for (const job of jobs) {
3494
+ if (job.connector === "&&" && execution.status !== 0) continue;
3495
+ if (job.connector === "||" && execution.status === 0) continue;
3496
+ execution = await runPipeline(job.pipeline, state, { capture: io.capture });
3497
+ if (job.negate) execution.status = execution.status === 0 ? 1 : 0;
3498
+ state.lastStatus = execution.status;
3499
+ if (execution.stdout.byteLength) {
3500
+ if (io.capture) capturedStdout.push(execution.stdout);
3501
+ else await writeStream(process.stdout, execution.stdout);
3502
+ }
3503
+ if (execution.stderr.byteLength) {
3504
+ if (io.capture) capturedStderr.push(execution.stderr);
3505
+ else await writeStream(process.stderr, execution.stderr);
3506
+ }
3507
+ if (state.exitRequested) break;
3508
+ }
3509
+ return io.capture
3510
+ ? result(execution.status, concatBytes(capturedStdout), concatBytes(capturedStderr))
3511
+ : execution;
3512
+ }
3513
+
3514
+ export async function executeArgv(argv, state = createState(), io = {}) {
3515
+ if (argv.length === 0)
3516
+ return result(2, "", "bunmsh: -cc requires a command\n");
3517
+ const commandArgv = [...argv];
3518
+ const execution = await runCommandArgv(
3519
+ commandArgv,
3520
+ state,
3521
+ null,
3522
+ Boolean(io.capture),
3523
+ Boolean(io.capture),
3524
+ );
3525
+ state.lastStatus = execution.status;
3526
+ if (!io.capture) {
3527
+ if (execution.stdout.byteLength) await writeStream(process.stdout, execution.stdout);
3528
+ if (execution.stderr.byteLength) await writeStream(process.stderr, execution.stderr);
3529
+ }
3530
+ return execution;
3531
+ }
3532
+
3533
+ export function pipelineChildState() {
3534
+ const serialized = process.env[PIPELINE_STATE_ENV];
3535
+ if (!serialized) return createState();
3536
+ try {
3537
+ const inherited = JSON.parse(serialized);
3538
+ const env = { ...process.env };
3539
+ delete env[PIPELINE_STATE_ENV];
3540
+ return createState({
3541
+ env,
3542
+ cwd: process.cwd(),
3543
+ aliases: inherited.aliases,
3544
+ functions: inherited.functions,
3545
+ readonly: inherited.readonly,
3546
+ args: inherited.args,
3547
+ pathSearch: inherited.pathSearch,
3548
+ pipelineChild: true,
3549
+ });
3550
+ } catch {
3551
+ return createState();
3552
+ }
3553
+ }
3554
+
3555
+ export function decode(data) {
3556
+ return decoder.decode(data);
3557
+ }