buninu 0.4.17 → 0.4.19

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.
@@ -1,5 +1,5 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
- import { chmodSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { chmodSync, mkdirSync, mkdtempSync, readlinkSync, readdirSync, rmSync, statSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { createState, decode, execute } from "../src/shell.js";
@@ -86,6 +86,15 @@ async function expectBuiltinSedLike(args, input, cwd = root) {
86
86
  expect(actual).toEqual(reference);
87
87
  }
88
88
 
89
+ async function expectBuiltinLikeSystem(name, args, input = "", cwd = root) {
90
+ const [actual, reference] = await Promise.all([
91
+ invokeArgvWithInput(
92
+ [process.execPath, join(root, "src/main.js"), "-cc", "builtin", name, ...args], input, cwd),
93
+ invokeArgvWithInput([Bun.which(name), ...args], input, cwd),
94
+ ]);
95
+ expect(actual).toEqual(reference);
96
+ }
97
+
89
98
  describe("/bin/sh reference", () => {
90
99
  describe("quote and word expansion", () => {
91
100
  test("removes LF and CRLF backslash continuations before tokenization", async () => {
@@ -402,6 +411,79 @@ printf ':%s' $?
402
411
  });
403
412
  });
404
413
 
414
+ describe("system multi-operand reference", () => {
415
+ test("printf repeats its format until all arguments are consumed", async () => {
416
+ await expectBuiltinLikeSystem("printf", ["%s=%d\n", "one", "1", "two", "2"]);
417
+ await expectBuiltinLikeSystem("printf", ["[%s][%s]\n", "one", "two", "three"]);
418
+ });
419
+
420
+ test("ln links each source into a target directory", async () => {
421
+ const directory = mkdtempSync(join(tmpdir(), "bunmsh-ln-reference-"));
422
+ const actualDir = join(directory, "actual");
423
+ const systemDir = join(directory, "system");
424
+ try {
425
+ for (const cwd of [actualDir, systemDir]) {
426
+ mkdirSync(cwd);
427
+ mkdirSync(join(cwd, "hard"));
428
+ mkdirSync(join(cwd, "soft"));
429
+ await Bun.write(join(cwd, "one"), "first\n");
430
+ await Bun.write(join(cwd, "two"), "second\n");
431
+ }
432
+ for (const args of [["one", "two", "hard"], ["-s", "one", "two", "soft"]]) {
433
+ const [actual, reference] = await Promise.all([
434
+ invokeArgvWithInput([process.execPath, join(root, "src/main.js"), "-cc", "builtin", "ln", ...args], "", actualDir),
435
+ invokeArgvWithInput([Bun.which("ln"), ...args], "", systemDir),
436
+ ]);
437
+ expect(actual).toEqual(reference);
438
+ }
439
+ for (const name of ["one", "two"]) {
440
+ expect(readdirSync(join(actualDir, "hard"))).toEqual(readdirSync(join(systemDir, "hard")));
441
+ expect(statSync(join(actualDir, "hard", name)).ino).toBe(statSync(join(actualDir, name)).ino);
442
+ expect(readlinkSync(join(actualDir, "soft", name)))
443
+ .toBe(readlinkSync(join(systemDir, "soft", name)));
444
+ }
445
+ } finally { rmSync(directory, { recursive: true, force: true }); }
446
+ });
447
+
448
+ test("head and tail select lines independently from each file", async () => {
449
+ const directory = mkdtempSync(join(tmpdir(), "bunmsh-parts-reference-"));
450
+ try {
451
+ await Bun.write(join(directory, "one"), "alpha\nbeta\n");
452
+ await Bun.write(join(directory, "two"), "gamma\ndelta\n");
453
+ for (const args of [["-n", "1", "one", "two"], ["-c", "2", "one", "two"],
454
+ ["-n", "0", "one", "two"]])
455
+ await expectBuiltinLikeSystem("head", args, "", directory);
456
+ for (const args of [["-n", "1", "one", "two"], ["-n", "+2", "one", "two"],
457
+ ["-n", "0", "one", "two"]])
458
+ await expectBuiltinLikeSystem("tail", args, "", directory);
459
+ } finally { rmSync(directory, { recursive: true, force: true }); }
460
+ });
461
+
462
+ test("wc prints each file count and a total; cut reads each file", async () => {
463
+ const directory = mkdtempSync(join(tmpdir(), "bunmsh-text-reference-"));
464
+ try {
465
+ await Bun.write(join(directory, "one"), "alpha beta\ngamma\n");
466
+ await Bun.write(join(directory, "two"), "delta\nepsilon zeta\n");
467
+ for (const args of [["-l", "one", "two"], ["-lwc", "one", "two"]])
468
+ await expectBuiltinLikeSystem("wc", args, "", directory);
469
+ await expectBuiltinLikeSystem("cut", ["-c", "1-3", "one", "two"], "", directory);
470
+ await expectBuiltinLikeSystem("cut", ["-c", "1", "one"], "", directory);
471
+ } finally { rmSync(directory, { recursive: true, force: true }); }
472
+ });
473
+
474
+ test("md5sum and sha256sum hash each file separately", async () => {
475
+ const directory = mkdtempSync(join(tmpdir(), "bunmsh-hash-reference-"));
476
+ try {
477
+ await Bun.write(join(directory, "one"), "alpha\n");
478
+ await Bun.write(join(directory, "two"), "beta\n");
479
+ for (const name of ["md5sum", "sha256sum"]) {
480
+ await expectBuiltinLikeSystem(name, ["one", "two"], "", directory);
481
+ await expectBuiltinLikeSystem(name, [], "stdin text\n", directory);
482
+ }
483
+ } finally { rmSync(directory, { recursive: true, force: true }); }
484
+ });
485
+ });
486
+
405
487
  describe("system tail reference", () => {
406
488
  test("supports -n +N and compact -n+N from-start modes", async () => {
407
489
  const input = "one\ntwo\nthree\nfour\n";
@@ -450,6 +532,18 @@ describe("system grep reference", () => {
450
532
  await expectBuiltinGrepLike(["-n", "needle", "tree/a.txt", "tree/nested/b.txt"], "", directory);
451
533
  } finally { rmSync(directory, { recursive: true, force: true }); }
452
534
  });
535
+
536
+ test("searches the current directory for -r without a path", async () => {
537
+ const directory = mkdtempSync(join(tmpdir(), "bunmsh-grep-cwd-reference-"));
538
+ try {
539
+ mkdirSync(join(directory, "nested"));
540
+ await Bun.write(join(directory, "nested", "a.txt"), "needle one\nnone\n");
541
+ await expectBuiltinGrepLike(["-r", "needle"], "", directory);
542
+ await expectBuiltinGrepLike(["-rn", "needle"], "", directory);
543
+ await expectBuiltinGrepLike(["-r", "missing"], "", directory);
544
+ await expectBuiltinGrepLike(["-rq", "needle"], "", directory);
545
+ } finally { rmSync(directory, { recursive: true, force: true }); }
546
+ });
453
547
  });
454
548
 
455
549
  describe("system sed reference", () => {
@@ -93,6 +93,28 @@ describe("parser", () => {
93
93
  expect(parse("echo hi | tr a-z A-Z")[0].pipeline).toHaveLength(2);
94
94
  });
95
95
 
96
+ test("parses a trailing background operator", () => {
97
+ const [job] = parse("sleep 30 &");
98
+ expect(job.background).toBe(true);
99
+ expect(job.pipeline).toHaveLength(1);
100
+ const [foreground] = parse("echo before & after");
101
+ expect(foreground.background).toBe(false);
102
+ expect(foreground.pipeline[0].words[2].fragments[0].text).toBe("&");
103
+ expect(parse(`echo "&"`)[0].background).toBe(false);
104
+ expect(parse("echo \\&")[0].background).toBe(false);
105
+ });
106
+
107
+ test("recognizes a trailing & from tokens, independently of comments and redirect targets", () => {
108
+ for (const source of ["sleep 30 & # comment", "sleep 30 & # comment &", "sleep 30 &\r\n# comment\r\n"]) {
109
+ const [job] = parse(source);
110
+ expect(job.background).toBe(true);
111
+ expect(job.pipeline[0].words).toHaveLength(2);
112
+ }
113
+ for (const source of ["echo & > file&", "echo & <<< input&", "echo &; # comment &", "echo hi # &"]) {
114
+ expect(parse(source)[0].background).toBe(false);
115
+ }
116
+ });
117
+
96
118
  test("a bare trailing backslash asks for one more interactive line, then joins", () => {
97
119
  // This is what the interactive prompt loop's `pending` string looks like
98
120
  // the instant Enter is pressed after typing "echo hi \" -- the next
@@ -398,6 +420,69 @@ describe("execution", () => {
398
420
  });
399
421
  });
400
422
 
423
+ test("parses POSIX case clauses independently of physical lines", async () => {
424
+ expect(await run('case "a b" in a\\ b|no) echo escaped;; *) echo bad;; esac')).toMatchObject({
425
+ status: 0,
426
+ stdout: "escaped\n",
427
+ stderr: "",
428
+ });
429
+ expect(await run("case x in (no) echo bad ;; (x) echo parenthesized\nesac")).toMatchObject({
430
+ status: 0,
431
+ stdout: "parenthesized\n",
432
+ stderr: "",
433
+ });
434
+ expect(await run("case x in x) case y in y) echo nested;; esac ;; *) echo bad;; esac")).toMatchObject({
435
+ status: 0,
436
+ stdout: "nested\n",
437
+ stderr: "",
438
+ });
439
+ expect(await run("case outer in outer) case inner in inner) echo autoconf;; esac;; *) echo bad;; esac")).toMatchObject({
440
+ status: 0,
441
+ stdout: "autoconf\n",
442
+ stderr: "",
443
+ });
444
+ expect(await run("case x in x) echo case;; esac; echo after")).toMatchObject({
445
+ status: 0,
446
+ stdout: "case\nafter\n",
447
+ stderr: "",
448
+ });
449
+ expect(await run("case b in [!ac]) echo bracket;; *) echo bad;; esac; case '*' in '*') echo quoted;; esac")).toMatchObject({
450
+ stdout: "bracket\nquoted\n",
451
+ });
452
+ });
453
+
454
+ test("implements mksh case fall-through terminators", async () => {
455
+ expect(await run("case b in a) echo a ;& b) echo b ;& c) echo c ;; esac")).toMatchObject({
456
+ stdout: "b\nc\n",
457
+ });
458
+ expect(await run("case b in a) echo a ;| b) echo b ;| *) echo star ;; esac")).toMatchObject({
459
+ stdout: "b\nstar\n",
460
+ });
461
+ });
462
+
463
+ test("accepts POSIX for headers with do on the next line", async () => {
464
+ expect(await run("for value in one two\ndo\n echo $value\ndone")).toMatchObject({
465
+ status: 0,
466
+ stdout: "one\ntwo\n",
467
+ stderr: "",
468
+ });
469
+ expect(await run("set -- left right; for arg\ndo echo $arg; done")).toMatchObject({
470
+ status: 0,
471
+ stdout: "left\nright\n",
472
+ stderr: "",
473
+ });
474
+ expect(await run("set -- a b; for arg; do printf '<%s>\\n' \"$arg\"; done")).toMatchObject({
475
+ status: 0,
476
+ stdout: "<a>\n<b>\n",
477
+ stderr: "",
478
+ });
479
+ expect(await run("for value in; do echo bad; done; echo empty")).toMatchObject({
480
+ status: 0,
481
+ stdout: "empty\n",
482
+ stderr: "",
483
+ });
484
+ });
485
+
401
486
  test("provides basic fallback text and utility commands", async () => {
402
487
  expect(await run("printf 'c\\na\\nb\\n' | builtin sort")).toMatchObject({ stdout: "a\nb\nc\n" });
403
488
  expect(await run("printf 'one two\\nthree\\n' | builtin wc -lwc")).toMatchObject({ stdout: "2 3 14\n" });
@@ -911,6 +996,7 @@ fi
911
996
  test("expands variables while respecting single quotes", async () => {
912
997
  const output = await run(`X=world; print -r -- "hello $X"; print -r -- '$X'`);
913
998
  expect(output.stdout).toBe("hello world\n$X\n");
999
+ expect((await run("printf '%s\\n' \"literal\\`backtick\"")).stdout).toBe("literal`backtick\n");
914
1000
  });
915
1001
 
916
1002
  test("supports status connectors", async () => {
@@ -926,6 +1012,75 @@ fi
926
1012
  expect(output.status).toBe(0);
927
1013
  });
928
1014
 
1015
+ test("returns immediately and exposes a background external process through Bun.sha.procs", async () => {
1016
+ const previous = Bun.sha.procs;
1017
+ Bun.sha.procs = [];
1018
+ try {
1019
+ const started = performance.now();
1020
+ const output = await run(`${process.execPath} -e 'await Bun.sleep(30000)' &`);
1021
+ const first = Bun.sha.procs.at(-1);
1022
+ const secondOutput = await run(`${process.execPath} -e 'await Bun.sleep(30000)' &`);
1023
+ const proc = Bun.sha.procs.at(-1);
1024
+
1025
+ expect(performance.now() - started).toBeLessThan(1000);
1026
+ expect(output.status).toBe(0);
1027
+ expect(output.stderr).toBe(`[1] ${first.pid} Bun.sha.procs[0]\n`);
1028
+ expect(secondOutput.status).toBe(0);
1029
+ expect(secondOutput.stderr).toBe(`[2] ${proc.pid} Bun.sha.procs[1]\n`);
1030
+ expect(Bun.sha.procs).toHaveLength(2);
1031
+ expect(proc).not.toBe(first);
1032
+ expect(proc.pid).toBeGreaterThan(0);
1033
+ expect(typeof proc.kill).toBe("function");
1034
+ first.kill();
1035
+ proc.kill();
1036
+ await Promise.all([first.exited, proc.exited]);
1037
+ } finally {
1038
+ if (previous === undefined) delete Bun.sha.procs;
1039
+ else Bun.sha.procs = previous;
1040
+ }
1041
+ });
1042
+
1043
+ test("keeps the minimal background form limited to one external command", async () => {
1044
+ const builtin = await run("echo nope &");
1045
+ const pipeline = await run("printf x | cat &");
1046
+ expect(builtin.status).toBe(2);
1047
+ expect(pipeline.status).toBe(2);
1048
+ expect(builtin.stderr).toContain("only a single external command");
1049
+ expect(pipeline.stderr).toContain("only a single external command");
1050
+ });
1051
+
1052
+ test("rejects background redirects before expanding, opening files, or spawning", async () => {
1053
+ const directory = mkdtempSync(join(tmpdir(), "bunmsh-background-redirect-"));
1054
+ const previousProcs = Bun.sha.procs;
1055
+ const previousCount = previousProcs?.length;
1056
+ try {
1057
+ await Bun.write(join(directory, "input"), "input content\n");
1058
+ await Bun.write(join(directory, "output"), "keep this content\n");
1059
+ const command = `'${process.execPath.replaceAll("'", "'\\''")}' -e 'await Bun.write("spawned", "ran")'`;
1060
+ const sources = [
1061
+ ...["> output", ">> output", "2> output", "2>> output", "> new-output", "< input", "< missing-input", "<<< payload", "1>&2", "2>&1"]
1062
+ .map((redirect) => `${command} ${redirect} & # comment`),
1063
+ `${command} <<EOF &\nbody\nEOF\n`,
1064
+ `${command} <<-EOF &\n\tbody\n\tEOF\n`,
1065
+ `${command} > $(echo expanded > expanded-marker; echo output) &`,
1066
+ ];
1067
+ for (const source of sources) {
1068
+ const output = await run(source, { cwd: directory });
1069
+ expect(output.status).toBe(2);
1070
+ expect(output.stdout).toBe("");
1071
+ expect(output.stderr).toContain("without redirections");
1072
+ }
1073
+ expect(await Bun.file(join(directory, "output")).text()).toBe("keep this content\n");
1074
+ expect(await Bun.file(join(directory, "input")).text()).toBe("input content\n");
1075
+ for (const name of ["spawned", "new-output", "expanded-marker"])
1076
+ expect(await Bun.file(join(directory, name)).exists()).toBe(false);
1077
+ expect(Bun.sha.procs).toBe(previousProcs);
1078
+ expect(Bun.sha.procs?.length).toBe(previousCount);
1079
+ } finally {
1080
+ rmSync(directory, { recursive: true, force: true });
1081
+ }
1082
+ });
1083
+
929
1084
  test("keeps builtin state outside pipelines", async () => {
930
1085
  const output = await run("export BUNMSH_TEST=value; print -r -- $BUNMSH_TEST");
931
1086
  expect(output.stdout).toBe("value\n");
@@ -0,0 +1,21 @@
1
+ Copyright (c) Microsoft Corporation. All rights reserved.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
2
+ Copyright (c) 2013 Arnout Kazemier and contributors
3
+ Copyright (c) 2016 Luigi Pinca and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.