buninu-linux 0.8.55 → 0.9.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +46 -46
- package/README.md +7 -6
- package/initramfs/bin/tar +34 -46
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +103 -0
- package/initramfs/buninu/apps/bunmsh/PORTING.md +133 -15
- package/initramfs/buninu/apps/bunmsh/help/awk.md +38 -0
- package/initramfs/buninu/apps/bunmsh/help/break.md +21 -0
- package/initramfs/buninu/apps/bunmsh/help/chmod.md +8 -2
- package/initramfs/buninu/apps/bunmsh/help/cmp.md +29 -0
- package/initramfs/buninu/apps/bunmsh/help/continue.md +22 -0
- package/initramfs/buninu/apps/bunmsh/help/cp.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/diff.md +30 -0
- package/initramfs/buninu/apps/bunmsh/help/egrep.md +26 -0
- package/initramfs/buninu/apps/bunmsh/help/export.md +2 -2
- package/initramfs/buninu/apps/bunmsh/help/expr.md +30 -0
- package/initramfs/buninu/apps/bunmsh/help/fgrep.md +25 -0
- package/initramfs/buninu/apps/bunmsh/help/grep.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/ls.md +4 -2
- package/initramfs/buninu/apps/bunmsh/help/mkdir.md +1 -1
- package/initramfs/buninu/apps/bunmsh/help/print.md +3 -0
- package/initramfs/buninu/apps/bunmsh/help/return.md +23 -0
- package/initramfs/buninu/apps/bunmsh/help/sed.md +8 -3
- package/initramfs/buninu/apps/bunmsh/help/set.md +12 -3
- package/initramfs/buninu/apps/bunmsh/help/tr.md +4 -0
- package/initramfs/buninu/apps/bunmsh/help/trap.md +22 -0
- package/initramfs/buninu/apps/bunmsh/help/uniq.md +27 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/awk.js +203 -0
- package/initramfs/buninu/apps/bunmsh/src/diff.js +222 -0
- package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +15 -4
- package/initramfs/buninu/apps/bunmsh/src/main.js +21 -5
- package/initramfs/buninu/apps/bunmsh/src/sed.js +159 -0
- package/initramfs/buninu/apps/bunmsh/src/shell.js +1283 -540
- package/initramfs/buninu/apps/bunmsh/test/awk-reference.test.js +35 -0
- package/initramfs/buninu/apps/bunmsh/test/awk.test.js +62 -0
- package/initramfs/buninu/apps/bunmsh/test/posix-bootstrap.test.js +595 -0
- package/initramfs/buninu/apps/bunmsh/test/utility-reference-cases.mjs +98 -0
- package/initramfs/buninu/apps/bunmsh/test/utility-reference.test.js +23 -0
- package/initramfs/buninu/apps/jsgotty/client-kitty.mjs +61 -0
- package/initramfs/buninu/apps/jsgotty/client.mjs +16 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsgotty/tests/client-kitty.test.mjs +43 -0
- package/initramfs/etc/resolv.conf +2 -1
- package/initramfs/usr/share/doc/buninu-linux/tar.md +12 -1
- package/package.json +1 -1
- package/scripts/arch.js +5 -5
- package/scripts/arch.sh +5 -5
- package/scripts/fetch.js +2 -0
- package/scripts/npm-packages.md +4 -3
- package/test/fetch-npm.test.js +1 -1
- package/test/tar-strip.test.js +36 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { createState, execute, finishShell } from "../src/shell.js";
|
|
3
|
+
import {mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync, chmodSync, utimesSync, statSync} from "node:fs";
|
|
4
|
+
import {tmpdir} from "node:os";
|
|
5
|
+
import {join} from "node:path";
|
|
6
|
+
|
|
7
|
+
const decode = (value) => new TextDecoder().decode(value);
|
|
8
|
+
test('external stderr duplication reaches pipelines without losing buffered output',async()=>{
|
|
9
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-pipe-stderr-'));
|
|
10
|
+
try {
|
|
11
|
+
const script=join(dir,'writer.js');writeFileSync(script,'process.stdout.write("out\\n");process.stderr.write("Thread model: posix\\n");');
|
|
12
|
+
const cmd=`'${process.execPath}' '${script}'`;
|
|
13
|
+
const r=await run(`${cmd} 2>&1 | builtin sed -n 's/^Thread model: //p'`);
|
|
14
|
+
expect(r.stdout).toBe('posix\n');expect(r.stderr).toBe('');expect(r.status).toBe(0);
|
|
15
|
+
const both=await run(`${cmd} 2>&1 | builtin cat`);expect(both.stdout.split('\n').filter(Boolean).sort()).toEqual(['Thread model: posix','out']);expect(both.stderr).toBe('');
|
|
16
|
+
const moved=await run(`${cmd} 2>&1 > '${dir}/out' | builtin cat`);expect(moved.stdout).toBe('Thread model: posix\n');expect(readFileSync(join(dir,'out'),'utf8')).toBe('out\n');
|
|
17
|
+
const away=await run(`${cmd} 1>&2 | builtin cat`);expect(away.stdout).toBe('');expect(away.stderr).toContain('out\n');expect(away.stderr).toContain('Thread model: posix\n');
|
|
18
|
+
writeFileSync(script,'process.on("SIGPIPE",()=>process.exit(0));while(true){process.stdout.write("out\\n");process.stderr.write("err\\n");await Bun.sleep(1);}');
|
|
19
|
+
const early=await run(`${cmd} 2>&1 | builtin head -n 1`);expect(early.status).toBe(0);expect(['out\n','err\n']).toContain(early.stdout);
|
|
20
|
+
}finally{rmSync(dir,{recursive:true,force:true});}
|
|
21
|
+
});
|
|
22
|
+
test('cp -p preserves regular file contents, mode and timestamps without truncating itself',async()=>{
|
|
23
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-cp-p-'));
|
|
24
|
+
try {
|
|
25
|
+
const source=join(dir,'source'),target=join(dir,'target');writeFileSync(source,'preserved\n');chmodSync(source,0o640);utimesSync(source,1234567890,1234567891);
|
|
26
|
+
const r=await run(`builtin cp -p '${source}' '${target}'`);
|
|
27
|
+
expect(r.status).toBe(0);expect(r.stderr).toBe('');
|
|
28
|
+
const st=statSync(target);expect(st.mode&0o777).toBe(0o640);expect(st.atimeMs).toBe(1234567890000);expect(st.mtimeMs).toBe(1234567891000);expect(readFileSync(target,'utf8')).toBe('preserved\n');
|
|
29
|
+
const same=await run(`builtin cp -p '${source}' '${source}'`);expect(same.status).toBe(1);expect(readFileSync(source,'utf8')).toBe('preserved\n');
|
|
30
|
+
}finally{rmSync(dir,{recursive:true,force:true});}
|
|
31
|
+
});
|
|
32
|
+
test('if negation applies to its pipeline rather than the entire AND/OR list',async()=>{
|
|
33
|
+
const r=await run('if ! false && false; then echo BAD; else echo and-ok; fi; if ! true || true; then echo or-ok; fi; if ! true; then echo BAD; elif ! false; then echo elif-ok; fi; if ! false | false; then echo pipe-ok; fi');
|
|
34
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe('and-ok\nor-ok\nelif-ok\npipe-ok\n');expect(r.stderr).toBe('');
|
|
35
|
+
});
|
|
36
|
+
async function run(source) {
|
|
37
|
+
const state = createState();
|
|
38
|
+
const value = await execute(source, state, {capture:true});
|
|
39
|
+
const final = await finishShell(state,state.exitRequested ? state.exitStatus : value.status,{capture:true});
|
|
40
|
+
return {status:final.status, stdout:decode(value.stdout)+decode(final.stdout), stderr:decode(value.stderr)+decode(final.stderr), state};
|
|
41
|
+
}
|
|
42
|
+
test("POSIX function bodies allow inline and newline opening braces", async () => {
|
|
43
|
+
const r = await run('a () { echo a; }\nb ()\n{\n a\n}\nb');
|
|
44
|
+
expect(r.stdout).toBe('a\n'); expect(r.stderr).toBe(''); expect(r.status).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
test("return crosses conditionals and restores function arguments", async () => {
|
|
47
|
+
const r = await run('f () { if true; then echo "$1"; return 7; fi; echo BAD; }\nset -- outer\nf inner\necho "$? $1"');
|
|
48
|
+
expect(r.stdout).toBe('inner\n7 outer\n'); expect(r.stderr).toBe('');
|
|
49
|
+
});
|
|
50
|
+
test("break and continue unwind exactly the requested loop levels", async () => {
|
|
51
|
+
const r = await run('for i in a b; do for j in 1 2; do echo "$i$j"; continue 2; done; echo BAD; done\nfor i in a b; do for j in 1 2; do echo "$i$j"; break 2; done; echo BAD; done\necho done');
|
|
52
|
+
expect(r.stdout).toBe('a1\nb1\na1\ndone\n'); expect(r.stderr).toBe('');
|
|
53
|
+
});
|
|
54
|
+
test("brace groups preserve state and stream into a pipeline", async () => {
|
|
55
|
+
const r = await run('{ x=kept; }\necho "$x"\n{ echo a; echo b; } | cat');
|
|
56
|
+
expect(r.stdout).toBe('kept\na\nb\n'); expect(r.stderr).toBe('');
|
|
57
|
+
});
|
|
58
|
+
test("groups short circuit and preserve variable changes through redirects", async () => {
|
|
59
|
+
const r = await run('false && { echo BAD; }\ntrue && { x=yes; }\n{ { echo hidden; } >/dev/null; x=kept; }\necho "$x"');
|
|
60
|
+
expect(r.stdout).toBe('kept\n'); expect(r.stderr).toBe('');
|
|
61
|
+
});
|
|
62
|
+
test("persistent descriptors duplicate, close, and restore command redirects", async () => {
|
|
63
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-fds-'));
|
|
64
|
+
try {
|
|
65
|
+
const log = join(dir,'log');
|
|
66
|
+
const r = await run(`exec 5>'${log}'\necho first >&5\n{ echo second; x=kept; } >&5\necho "$x"\nexec 6>&5\nexec 5>&-\necho third >&6\nexec 6>&-`);
|
|
67
|
+
expect(r.stdout).toBe('kept\n'); expect(r.stderr).toBe('');
|
|
68
|
+
expect(readFileSync(log,'utf8')).toBe('first\nsecond\nthird\n');
|
|
69
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
70
|
+
});
|
|
71
|
+
test("redirect order snapshots the original stdout and truncates null commands", async () => {
|
|
72
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-io-order-'));
|
|
73
|
+
try {
|
|
74
|
+
const path = join(dir,'out');
|
|
75
|
+
const cmd = `'${process.execPath}' -e 'console.log("out");console.error("err")'`;
|
|
76
|
+
let r = await run(`${cmd} 2>&1 >'${path}'`);
|
|
77
|
+
expect(r.stdout).toBe('err\n'); expect(r.stderr).toBe('');
|
|
78
|
+
expect(readFileSync(path,'utf8')).toBe('out\n');
|
|
79
|
+
r = await run(`${cmd} >'${path}' 2>&1`);
|
|
80
|
+
expect(r.stdout).toBe(''); expect(r.stderr).toBe('');
|
|
81
|
+
expect(readFileSync(path,'utf8')).toBe('out\nerr\n');
|
|
82
|
+
await run(`>'${path}'`);
|
|
83
|
+
expect(readFileSync(path,'utf8')).toBe('');
|
|
84
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
85
|
+
});
|
|
86
|
+
test("errexit is suppressed in conditions, negation and tested functions", async () => {
|
|
87
|
+
let r = await run('set -e\nfalse\necho BAD');
|
|
88
|
+
expect(r.status).toBe(1); expect(r.stdout).toBe('');
|
|
89
|
+
r = await run('set -e\nf () { false; echo allowed; }\nif f; then echo yes; fi\nfalse && echo BAD\n! true\necho done');
|
|
90
|
+
expect(r.stdout).toBe('allowed\nyes\ndone\n'); expect(r.stderr).toBe('');
|
|
91
|
+
r = await run('set -e\nf () { false; echo BAD; }\nf\necho BAD');
|
|
92
|
+
expect(r.stdout).toBe(''); expect(r.status).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
test("EXIT traps preserve status, run once, and allow an explicit override", async () => {
|
|
95
|
+
let r = await run("trap 'echo status=$?' 0\nexit 7");
|
|
96
|
+
expect(r.stdout).toBe('status=7\n'); expect(r.status).toBe(7);
|
|
97
|
+
r = await run("trap 'echo once; exit 9' EXIT\nexit 7");
|
|
98
|
+
expect(r.stdout).toBe('once\n'); expect(r.status).toBe(9);
|
|
99
|
+
r = await run("trap 'echo BAD' 0\ntrap - 0\necho ok");
|
|
100
|
+
expect(r.stdout).toBe('ok\n'); expect(r.stderr).toBe('');
|
|
101
|
+
});
|
|
102
|
+
test("source returns without exiting its caller", async () => {
|
|
103
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-source-return-'));
|
|
104
|
+
try {
|
|
105
|
+
const path=join(dir,'source'); writeFileSync(path,'echo "$1"\nreturn 8\necho BAD\n');
|
|
106
|
+
const r=await run(`set -- outer\n. '${path}' inner\necho "$? $1"`);
|
|
107
|
+
expect(r.stdout).toBe('inner\n8 outer\n'); expect(r.stderr).toBe('');
|
|
108
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
109
|
+
});
|
|
110
|
+
test("descriptor inheritance reaches external children and subshells", async () => {
|
|
111
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-inherit-'));
|
|
112
|
+
try {
|
|
113
|
+
const path=join(dir,'log');
|
|
114
|
+
const r=await run(`exec 5>'${path}'\n'${process.execPath}' -e 'require("fs").writeSync(5,"external\\n")'\n(echo subshell >&5)\necho done`);
|
|
115
|
+
expect(r.stdout).toBe('done\n'); expect(r.stderr).toBe('');
|
|
116
|
+
expect(readFileSync(path,'utf8')).toBe('external\nsubshell\n');
|
|
117
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
118
|
+
});
|
|
119
|
+
test("signal traps defer execution and preserve the interrupted status", async () => {
|
|
120
|
+
const state = createState();
|
|
121
|
+
const before=process.listenerCount('SIGUSR1');
|
|
122
|
+
try {
|
|
123
|
+
await execute("trap 'echo caught=$?' USR1",state,{capture:true});
|
|
124
|
+
state.lastStatus=7;
|
|
125
|
+
process.emit('SIGUSR1');
|
|
126
|
+
expect(state.lastStatus).toBe(7);
|
|
127
|
+
const r=await execute('echo after=$?',state,{capture:true});
|
|
128
|
+
expect(decode(r.stdout)).toBe('caught=7\nafter=7\n');
|
|
129
|
+
} finally { await finishShell(state,0,{capture:true}); }
|
|
130
|
+
expect(process.listenerCount('SIGUSR1')).toBe(before);
|
|
131
|
+
});
|
|
132
|
+
test("noglob and nounset preserve explicit parameter defaults", async () => {
|
|
133
|
+
let r = await run('set -f\necho *\nset -u\necho "${BOOTSTRAP_UNSET_TEST-default}"\necho "$BOOTSTRAP_UNSET_TEST"\necho BAD');
|
|
134
|
+
expect(r.stdout).toBe('*\ndefault\n'); expect(r.status).toBe(1);
|
|
135
|
+
expect(r.stderr).toContain('parameter not set');
|
|
136
|
+
});
|
|
137
|
+
test("background registry keeps original objects and completed indices", async () => {
|
|
138
|
+
const previous=Bun.sha.procs;
|
|
139
|
+
Bun.sha.procs=[];
|
|
140
|
+
try {
|
|
141
|
+
const r=await run(`'${process.execPath}' -e 'await Bun.sleep(40)' &`);
|
|
142
|
+
expect(r.status).toBe(0);
|
|
143
|
+
const proc=Bun.sha.procs[0];
|
|
144
|
+
expect(typeof proc.kill).toBe('function');
|
|
145
|
+
await proc.exited;
|
|
146
|
+
await run(`'${process.execPath}' -e 'await Bun.sleep(40)' &`);
|
|
147
|
+
expect(Bun.sha.procs).toHaveLength(2);
|
|
148
|
+
expect(Bun.sha.procs[0]).toBe(proc);
|
|
149
|
+
await Bun.sha.procs[1].exited;
|
|
150
|
+
} finally { Bun.sha.procs=previous; }
|
|
151
|
+
});
|
|
152
|
+
test("redirected compound pipelines keep their streams and variable scope", async () => {
|
|
153
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-group-pipe-'));
|
|
154
|
+
try {
|
|
155
|
+
const path=join(dir,'out');
|
|
156
|
+
const r=await run(`{ printf hello | cat; x=kept; } >'${path}'\necho "$x"\ncat '${path}'\n(cat '${path}') >'${path}.copy'`);
|
|
157
|
+
expect(r.stdout).toBe('kept\nhello'); expect(r.stderr).toBe('');
|
|
158
|
+
expect(readFileSync(path+'.copy','utf8')).toBe('hello');
|
|
159
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
160
|
+
});
|
|
161
|
+
test("multiline pipelines and Autoconf-shaped nested groups parse completely", async () => {
|
|
162
|
+
const r=await run('case arm in arm)\n{ x=ok\n { { echo "$x"; } >/dev/null\n echo kept; }; }\n;; esac\necho pipe |\ncat');
|
|
163
|
+
expect(r.stdout).toBe('kept\npipe\n'); expect(r.stderr).toBe('');
|
|
164
|
+
});
|
|
165
|
+
test("exec can retain a descriptor from a temporarily redirected group", async () => {
|
|
166
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-retained-fd-'));
|
|
167
|
+
try {
|
|
168
|
+
const path=join(dir,'out');
|
|
169
|
+
const r=await run(`{ exec 6>&1; echo inside; } >'${path}'\necho outside >&6\necho terminal`);
|
|
170
|
+
expect(r.stdout).toBe('terminal\n'); expect(r.stderr).toBe('');
|
|
171
|
+
expect(readFileSync(path,'utf8')).toBe('inside\noutside\n');
|
|
172
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
173
|
+
});
|
|
174
|
+
test("fd numbers are recognized only at unquoted token boundaries", async () => {
|
|
175
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-io-number-'));
|
|
176
|
+
try {
|
|
177
|
+
const path=join(dir,'out');
|
|
178
|
+
const r=await run(`echo word2>'${path}'\ncat '${path}'`);
|
|
179
|
+
expect(r.stdout).toBe('word2\n'); expect(r.stderr).toBe('');
|
|
180
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
181
|
+
});
|
|
182
|
+
test("parameter operands retain quoted argument boundaries and empty arguments", async () => {
|
|
183
|
+
let r = await run('set -- "one two" "" "*"\nprintf "<%s>\\n" ${1+"$@"}\nprintf "<%s>\\n" pre"$@"post');
|
|
184
|
+
expect(r.stdout).toBe('<one two>\n<>\n<*>\n<preone two>\n<>\n<*post>\n'); expect(r.stderr).toBe('');
|
|
185
|
+
r = await run('set --\nfor x in ${1+"$@"} "$@"; do echo BAD; done\nprintf "<%s>\\n" ${missing:-"a b"} ${missing:-${other:-"c d"}}');
|
|
186
|
+
expect(r.stdout).toBe('<a b>\n<c d>\n'); expect(r.stderr).toBe('');
|
|
187
|
+
});
|
|
188
|
+
test("LINENO uses physical source lines through compound normalization", async () => {
|
|
189
|
+
const r = await run('echo "$LINENO"\nif true; then echo "$LINENO"; fi\n# comment\necho "$LINENO"\nf () {\n echo "$LINENO"\n}\nf');
|
|
190
|
+
expect(r.stdout).toBe('1\n2\n4\n6\n'); expect(r.stderr).toBe('');
|
|
191
|
+
});
|
|
192
|
+
test("command substitutions capture stdout inside redirected groups", async () => {
|
|
193
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-substitution-'));
|
|
194
|
+
try {
|
|
195
|
+
const path=join(dir,'log');
|
|
196
|
+
const r=await run(`{ x=$(printf kept); echo "$x"; } >'${path}'\necho "$x"`);
|
|
197
|
+
expect(r.stdout).toBe('kept\n'); expect(readFileSync(path,'utf8')).toBe('kept\n'); expect(r.stderr).toBe('');
|
|
198
|
+
} finally {rmSync(dir,{recursive:true,force:true});}
|
|
199
|
+
});
|
|
200
|
+
test("nested compound conditions do not terminate at an inner then", async () => {
|
|
201
|
+
const r=await run('f () {\n if { if true; then echo inner; fi; } && { test x = x; }\n then echo outer\n else echo BAD\n fi\n}\nf\n(\necho subshell\n) | cat');
|
|
202
|
+
expect(r.stdout).toBe('inner\nouter\nsubshell\n'); expect(r.stderr).toBe('');
|
|
203
|
+
});
|
|
204
|
+
test("large function state survives pipeline subprocess transport", async () => {
|
|
205
|
+
const r=await run(`f () {\n : '${'padding'.repeat(1000)}'\n echo function\n}\nf | cat`);
|
|
206
|
+
expect(r.stdout).toBe('function\n'); expect(r.stderr).toBe(''); expect(r.status).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
test("expr supports BRE captures, arithmetic precedence and exit statuses", async () => {
|
|
209
|
+
const r=await run("expr 2 + 3 \\* 4\nexpr 00001 : '.*\\(...\\)'\nexpr '' \\| fallback\nexpr a : z\necho $?");
|
|
210
|
+
expect(r.stdout).toBe('14\n001\nfallback\n0\n1\n'); expect(r.stderr).toBe('');
|
|
211
|
+
});
|
|
212
|
+
test("sed handles delimiters, branches, hold space and multiline pattern space", async () => {
|
|
213
|
+
let r=await run("printf 'a;b\\nc\\n' | sed -n 's/;/!/; t changed; b; :changed; h; n; G; p'");
|
|
214
|
+
expect(r.stdout).toBe('c\na!b\n'); expect(r.stderr).toBe('');
|
|
215
|
+
r=await run("printf 'a\\nb\\nc\\n' | sed -n '1,2{N;s/\\n/:/;p;}'");
|
|
216
|
+
expect(r.stdout).toBe('a:b\n'); expect(r.stderr).toBe('');
|
|
217
|
+
});
|
|
218
|
+
test("only posix mode removes ls icons and survives pipeline state transfer", async () => {
|
|
219
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-posix-ls-'));
|
|
220
|
+
try {
|
|
221
|
+
const path=join(dir,'file'); writeFileSync(path,'x');
|
|
222
|
+
const r=await run(`ls '${path}'\nset -o posix\nls '${path}' | cat\nlsfancy '${path}'\nset +o posix\nls '${path}'`);
|
|
223
|
+
expect(r.stdout).toBe(`📄 ${path}\n${path}\n📄 ${path}\n📄 ${path}\n`); expect(r.stderr).toBe('');
|
|
224
|
+
const options=await run('set -o\nset -o posix\necho "flags=$-"\nset +o');
|
|
225
|
+
expect(options.stdout).toContain('posix off\n'); expect(options.stdout).toContain('flags=\n'); expect(options.stdout).toContain('set -o posix\n');
|
|
226
|
+
} finally {rmSync(dir,{recursive:true,force:true});}
|
|
227
|
+
});
|
|
228
|
+
test("LINENO retains original lines in nested case arms", async () => {
|
|
229
|
+
const r=await run('echo "$LINENO"\ncase x in\nx)\n case y in y)\n echo "$LINENO"\n ;; esac\n ;;\nesac\necho "$LINENO"');
|
|
230
|
+
expect(r.stdout).toBe('1\n5\n9\n'); expect(r.stderr).toBe('');
|
|
231
|
+
});
|
|
232
|
+
test("posix isolates external environments while default mode exports all values", async () => {
|
|
233
|
+
const child=`'${process.execPath}' -e 'console.log([process.env.BUNMSH_PRIVATE??"missing",process.env.BUNMSH_PUBLIC??"missing"].join("/"))'`;
|
|
234
|
+
const r=await run(`unset BUNMSH_PRIVATE BUNMSH_PUBLIC\nBUNMSH_PRIVATE=private\nBUNMSH_PUBLIC=public\n${child}\nset -o posix\nexport BUNMSH_PUBLIC\n${child}\nBUNMSH_PRIVATE=temporary ${child}\necho "$BUNMSH_PRIVATE"\nunset BUNMSH_PUBLIC\nBUNMSH_PUBLIC=unexported\n${child}\nset +o posix\n${child}`);
|
|
235
|
+
expect(r.stdout).toBe('private/public\nmissing/public\ntemporary/public\nprivate\nmissing/missing\nprivate/unexported\n'); expect(r.stderr).toBe('');
|
|
236
|
+
});
|
|
237
|
+
test("posix subshells retain private values without exporting them to grandchildren", async () => {
|
|
238
|
+
const child=`'${process.execPath}' -e 'console.log(process.env.BUNMSH_PRIVATE??"missing")'`;
|
|
239
|
+
const r=await run(`set -o posix\nunset BUNMSH_PRIVATE\nBUNMSH_PRIVATE='${'x'.repeat(6000)}'\nf () { printf "%s\\n" "\${#BUNMSH_PRIVATE}"; ${child}; }\nf | cat\n(f)\nprintf '%s\\n' "$(f)"`);
|
|
240
|
+
expect(r.stdout).toBe('6000\nmissing\n6000\nmissing\n6000\nmissing\n'); expect(r.stderr).toBe('');
|
|
241
|
+
});
|
|
242
|
+
test("posix env assignments and export attributes survive mode toggles", async () => {
|
|
243
|
+
const r=await run('unset BUNMSH_PRIVATE BUNMSH_PUBLIC\nBUNMSH_PRIVATE=hidden\nexport BUNMSH_PUBLIC=shown\nset -o posix\nenv -i BUNMSH_PRIVATE=explicit\nexport -p\nset +o posix\nset -o posix\nenv');
|
|
244
|
+
expect(r.stdout).toContain('BUNMSH_PRIVATE=explicit\n');
|
|
245
|
+
expect(r.stdout).not.toContain('BUNMSH_PRIVATE=hidden');
|
|
246
|
+
expect(r.stdout).toContain('BUNMSH_PUBLIC=shown\n'); expect(r.stderr).toBe('');
|
|
247
|
+
});
|
|
248
|
+
test("Autoconf detects and enables posix from multiline option output", async () => {
|
|
249
|
+
const r=await run('case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac\nset -o');
|
|
250
|
+
expect(r.stdout).toContain('posix on\n'); expect(r.stderr).toBe('');
|
|
251
|
+
});
|
|
252
|
+
test("rm force accepts no operands and a pipeline can feed a compound case", async () => {
|
|
253
|
+
const r=await run('rm -f && rm -fr && rm -rf\nprintf hello | case x in x) cat;; esac');
|
|
254
|
+
expect(r.stdout).toBe('hello'); expect(r.stderr).toBe(''); expect(r.status).toBe(0);
|
|
255
|
+
});
|
|
256
|
+
test("subshell rewriting leaves C parentheses in heredocs untouched", async () => {
|
|
257
|
+
const r=await run("ac_ext=c\ncat <<EOF\nint main (void)\n{ return 0; }\nEOF");
|
|
258
|
+
expect(r.stdout).toBe('int main (void)\n{ return 0; }\n'); expect(r.stderr).toBe('');
|
|
259
|
+
});
|
|
260
|
+
test("grep accepts multiple explicit patterns and counts selected lines", async () => {
|
|
261
|
+
const r=await run("printf 'apple\\npear\\napricot\\n' | grep -e '^a' -e 'pear'\nprintf 'a\\nb\\na\\n' | grep -c -e '^a$'");
|
|
262
|
+
expect(r.stdout).toBe('apple\npear\napricot\n2\n'); expect(r.stderr).toBe('');
|
|
263
|
+
});
|
|
264
|
+
test("sed reads scripts from a file", async () => {
|
|
265
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-sed-file-'));
|
|
266
|
+
try {
|
|
267
|
+
const path=join(dir,'script'); writeFileSync(path,'s/a/b/\nt done\n:done\np\n');
|
|
268
|
+
const r=await run(`printf 'a\\n' | sed -n -f '${path}'`);
|
|
269
|
+
expect(r.stdout).toBe('b\n'); expect(r.stderr).toBe('');
|
|
270
|
+
} finally {rmSync(dir,{recursive:true,force:true});}
|
|
271
|
+
});
|
|
272
|
+
test("diff distinguishes identical, changed, and inaccessible files", async () => {
|
|
273
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-diff-'));
|
|
274
|
+
try {
|
|
275
|
+
const a=join(dir,'a'),b=join(dir,'b'); writeFileSync(a,'same\nold\n'); writeFileSync(b,'same\nnew\n');
|
|
276
|
+
let r=await run(`diff '${a}' '${a}'`); expect(r.status).toBe(0); expect(r.stdout).toBe('');
|
|
277
|
+
r=await run(`diff '${a}' '${b}'`); expect(r.status).toBe(1); expect(r.stdout).toBe('2c2\n< old\n---\n> new\n');
|
|
278
|
+
r=await run(`diff '${a}' '${b}.missing'`); expect(r.status).toBe(2);
|
|
279
|
+
} finally {rmSync(dir,{recursive:true,force:true});}
|
|
280
|
+
});
|
|
281
|
+
test("diff aligns separated insertions, deletions, repeated lines and final newlines", async () => {
|
|
282
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-diff-align-'));
|
|
283
|
+
try {
|
|
284
|
+
const a=join(dir,'a'), b=join(dir,'b');
|
|
285
|
+
const cases=[
|
|
286
|
+
['a\nb\nc\nd\n','a\nnew\nb\nc\nmore\nd\n','1a2\n> new\n3a5\n> more\n'],
|
|
287
|
+
['a\nnew\nb\nc\nmore\nd\n','a\nb\nc\nd\n','2d1\n< new\n5d3\n< more\n'],
|
|
288
|
+
['a\nb\nc\nd\ne\n','a\nx\nb\nc\nD\ne\n','1a2\n> x\n4c5\n< d\n---\n> D\n'],
|
|
289
|
+
['a\nsame\nsame\nz\n','a\nsame\nnew\nsame\nz\n','2a3\n> new\n'],
|
|
290
|
+
['', 'new\n', '0a1\n> new\n'],
|
|
291
|
+
['old\n', '', '1d0\n< old\n'],
|
|
292
|
+
['a\n', 'a', '1c1\n< a\n---\n> a\n\\n'],
|
|
293
|
+
['前\n後\n', '前\n中\n後\n', '1a2\n> 中\n'],
|
|
294
|
+
];
|
|
295
|
+
for (const [before,after,expected] of cases) {
|
|
296
|
+
writeFileSync(a,before); writeFileSync(b,after);
|
|
297
|
+
const r=await run(`diff '${a}' '${b}'`);
|
|
298
|
+
expect(r.status).toBe(1); expect(r.stderr).toBe(''); expect(r.stdout).toBe(expected);
|
|
299
|
+
}
|
|
300
|
+
} finally {rmSync(dir,{recursive:true,force:true});}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("arithmetic expands Autoconf positional expressions and nested substitutions", async () => {
|
|
304
|
+
const r=await run('as_fn_arith () { as_val=$(( $* )); }\nas_fn_arith 10 + 1\necho "$as_val"\nx=3\necho "$(( ${x} + $(echo 2) + $((1 + 1)) ))"');
|
|
305
|
+
expect(r.status).toBe(0); expect(r.stdout).toBe('11\n7\n'); expect(r.stderr).toBe('');
|
|
306
|
+
});
|
|
307
|
+
test("compound pipelines retain multiline quoted command operands", async () => {
|
|
308
|
+
for (const first of ["(printf 'hello\\n')", "case x in x) printf 'hello\\n';; esac", "{ printf 'hello\\n'; }"]) {
|
|
309
|
+
for (const separator of [" ", "\n"]) {
|
|
310
|
+
const r=await run(`${first} |${separator}sed '\n s/hello/world/\n '\necho after`);
|
|
311
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe('world\nafter\n');expect(r.stderr).toBe('');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("heredoc bodies do not terminate a pending logical connector", async () => {
|
|
317
|
+
const r=await run("cat <<EOF ||\nhello\nEOF\necho BAD\ncat <<EOF &&\nworld\nEOF\necho after");
|
|
318
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe('hello\nworld\nafter\n');expect(r.stderr).toBe('');
|
|
319
|
+
});
|
|
320
|
+
test("ASCII control characters survive quoted expansion and heredocs", async () => {
|
|
321
|
+
const literal=String.fromCharCode(1,2,3,4,5,6,7);
|
|
322
|
+
const r=await run(`x='${literal}'\nprintf '%s' "$x"\ncat <<EOF\n${literal}\nEOF`);
|
|
323
|
+
expect(r.stdout).toBe(literal+literal+'\n');expect(r.stderr).toBe('');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("pipelines preserve redirected compound operands and logical suffixes", async () => {
|
|
327
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-compound-source-'));
|
|
328
|
+
try {
|
|
329
|
+
const file=join(dir,'out');
|
|
330
|
+
const r=await run(`printf 'hello\\n' |\nif true; then\n cat\nelse\n echo BAD\nfi >'${file}' || echo ERROR\ncat '${file}'`);
|
|
331
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe('hello\n');expect(r.stderr).toBe('');
|
|
332
|
+
} finally {rmSync(dir,{recursive:true,force:true});}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("compound parsing preserves quoted backslash newlines", async () => {
|
|
336
|
+
const value="a\\\nb";
|
|
337
|
+
const r=await run(`if true; then\n printf '%s\\n' '${value}'\n cat <<'EOF'\n${value}\nEOF\n echo a\\\nb\nfi`);
|
|
338
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe(value+'\n'+value+'\nab\n');expect(r.stderr).toBe('');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("case terminators remain intact after nested groups", async () => {
|
|
342
|
+
const r=await run(`x='none needed'
|
|
343
|
+
case $x in #(
|
|
344
|
+
'cannot detect') { { echo BAD; }; echo BAD; } ;; #(
|
|
345
|
+
'none needed') x='' ;; #(
|
|
346
|
+
*) x=BAD ;;
|
|
347
|
+
esac
|
|
348
|
+
printf '<%s>\\n' "$x"`);
|
|
349
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe('<>\n');expect(r.stderr).toBe('');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("parameter trimming uses shell bracket patterns and quoted literals", async () => {
|
|
353
|
+
const source="w='$(am_make_OBJECTS)'\nw=${w#\\$[\\(\\{]}\nprintf '%s\\n' \"${w%[\\)\\}]}\"\nlo='${LIBOBJDIR}alloca.o'\nprintf '%s\\n' \"${lo#'${LIBOBJDIR}'}\"\nx='*abc123'\nprintf '%s\\n' \"${x#'*'}\" \"${x%%[0-9]*}\" \"${x%[0-9]*}\"\nx=abc\nprintf '%s\\n' \"${x##[a-c]*}\"";
|
|
354
|
+
const r=await run(source);
|
|
355
|
+
expect(r.status).toBe(0);expect(r.stderr).toBe('');
|
|
356
|
+
expect(r.stdout).toBe('am_make_OBJECTS\nalloca.o\nabc123\n*abc\n*abc12\n\n');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("subshell state is captured after preceding commands in the same list", async () => {
|
|
360
|
+
const r=await run("x=old; x=new; (printf '%s\\n' \"$x\"); printf '%s\\n' \"$x\"");
|
|
361
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe('new\nnew\n');expect(r.stderr).toBe('');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("subshell transport does not leak into later external shells", async () => {
|
|
365
|
+
const r=await run("(true); printf '<%s>\\n' \"${BUNMSH_INTERNAL_PIPELINE_STATE-unset}\"");
|
|
366
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe('<unset>\n');expect(r.stderr).toBe('');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("continued recipe lists retain compound keyword boundaries", async () => {
|
|
370
|
+
const r=await run("fail=; \\\nif (target_option=k; test \"$target_option\" = k); then \\\n echo yes; \\\nelse \\\n echo no; \\\nfi\n");
|
|
371
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe("yes\n");expect(r.stderr).toBe("");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("continued recipe function headers start a separate compound node", async () => {
|
|
375
|
+
const r=await run("x=old; \\\nf () \\\n{ \\\n x=new; echo \"$x\"; \\\n}; \\\nf\n");
|
|
376
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe("new\n");expect(r.stderr).toBe("");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("print accepts Autoconf combined raw/no-newline flags", async () => {
|
|
380
|
+
const r=await run("print -rn -- 'a\\nb'; print -r -- end");
|
|
381
|
+
expect(r.status).toBe(0);expect(r.stdout).toBe('a\\nbend\n');expect(r.stderr).toBe('');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("compound bodies preserve multiline parameter expansion words", async () => {
|
|
385
|
+
const r = await run('if true; then { echo "${missing-first\nsecond}"; }; fi');
|
|
386
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
387
|
+
expect(r.stdout).toBe('first\nsecond\n');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("legacy egrep and fgrep names use builtin grep modes", async () => {
|
|
391
|
+
const r = await run("printf 'alpha\\nbeta\\na+b\\n' | builtin egrep 'alpha|beta'; printf 'ab\\na+b\\n' | builtin fgrep 'a+b'");
|
|
392
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
393
|
+
expect(r.stdout).toBe('alpha\nbeta\na+b\n');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("mv option delimiter protects filenames beginning with hyphens", async () => {
|
|
397
|
+
const cwd = mkdtempSync(join(tmpdir(), 'bunmsh-mv-'));
|
|
398
|
+
try {
|
|
399
|
+
writeFileSync(join(cwd, '-f'), 'kept');
|
|
400
|
+
const r = await execute('builtin mv -f -- -f --target', createState({cwd}), {capture:true});
|
|
401
|
+
expect(r.status).toBe(0); expect(decode(r.stderr)).toBe('');
|
|
402
|
+
expect(readFileSync(join(cwd, '--target'), 'utf8')).toBe('kept');
|
|
403
|
+
} finally { rmSync(cwd, {recursive:true, force:true}); }
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("rm option delimiter removes option-looking filenames and preserves force with no operands", async () => {
|
|
407
|
+
const cwd = mkdtempSync(join(tmpdir(), 'bunmsh-rm-'));
|
|
408
|
+
try {
|
|
409
|
+
for (const name of ['-f', '--', 'unchanged']) writeFileSync(join(cwd, name), 'data');
|
|
410
|
+
const r = await execute('builtin rm -f -- -f -- unchanged; builtin rm -f --', createState({cwd}), {capture:true});
|
|
411
|
+
expect(r.status).toBe(0); expect(decode(r.stderr)).toBe('');
|
|
412
|
+
for (const name of ['-f', '--', 'unchanged']) expect(existsSync(join(cwd, name))).toBe(false);
|
|
413
|
+
} finally { rmSync(cwd, {recursive:true, force:true}); }
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("read preserves a persistent input descriptor and its shared offset", async () => {
|
|
417
|
+
const cwd = mkdtempSync(join(tmpdir(), 'bunmsh-read-fd-'));
|
|
418
|
+
try {
|
|
419
|
+
writeFileSync(join(cwd, 'input'), 'first\nsecond\n第三\ntail');
|
|
420
|
+
const r = await execute('exec 5<&0 <input; read a; exec 6<&0; builtin read b; command read c <&6; read d; last=$?; exec 0<&5 5<&- 6<&-; printf "%s|%s|%s|%s|%s\\n" "$a" "$b" "$c" "$d" "$last"', createState({cwd}), {capture:true});
|
|
421
|
+
expect(r.status).toBe(0); expect(decode(r.stderr)).toBe('');
|
|
422
|
+
expect(decode(r.stdout)).toBe('first|second|第三|tail|1\n');
|
|
423
|
+
} finally { rmSync(cwd, {recursive:true, force:true}); }
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("parameter lengths include positional parameters used by libtool", async () => {
|
|
427
|
+
const r = await run('set -- abc "" 3 4 5 6 7 8 9 tenth; printf "%s %s %s %s\\n" "${#1}" "${#2}" "${#10}" "${##}"');
|
|
428
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
429
|
+
expect(r.stdout).toBe('3 0 5 2\n');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("comments between logical operators and compounds preserve short circuiting", async () => {
|
|
433
|
+
const r = await run('true ||\n\n# fallback\n{ echo BAD; }\nfalse &&\n# skipped\n{ echo BAD; }\nfalse ||\n# needed\n{ echo fallback; }\ntrue &&\n# needed\n{ echo success; }');
|
|
434
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
435
|
+
expect(r.stdout).toBe('fallback\nsuccess\n');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("compound pipelines can include simple stages before a trailing loop", async () => {
|
|
439
|
+
const r = await run('for x in one two; do echo "$x"; done | builtin cat | while read x; do echo "<$x>"; done');
|
|
440
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
441
|
+
expect(r.stdout).toBe('<one>\n<two>\n');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("compound AND and OR lists retain left associativity", async () => {
|
|
445
|
+
const r = await run('{ false; } && true || { echo fallback; }\n{ true; } || false && { echo success; }');
|
|
446
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
447
|
+
expect(r.stdout).toBe('fallback\nsuccess\n');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("backquotes remove their escape layer before inner shell quoting", async () => {
|
|
451
|
+
const source = 'paths="/lib /usr/lib"; value=`' + String.raw`echo $paths | builtin sed -e 's/\([^ ][^ ]*\)/SEARCH_DIR(\\"\1\\");/g'` + '`; printf "%s\\n" "$value"';
|
|
452
|
+
const r = await run(source);
|
|
453
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
454
|
+
expect(r.stdout).toBe('SEARCH_DIR("/lib"); SEARCH_DIR("/usr/lib");\n');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("backquote escaping respects enclosing double quotes and unrelated escapes", async () => {
|
|
458
|
+
const body = String.raw`printf '%s' '\"'`;
|
|
459
|
+
const unquoted = await run('value=`' + body + '`; printf "%s\\n" "$value"');
|
|
460
|
+
const quoted = await run('value="`' + body + '`"; printf "%s\\n" "$value"');
|
|
461
|
+
expect(unquoted.status).toBe(0); expect(unquoted.stderr).toBe('');
|
|
462
|
+
expect(unquoted.stdout).toBe('\\' + '"\n');
|
|
463
|
+
expect(quoted.status).toBe(0); expect(quoted.stderr).toBe('');
|
|
464
|
+
expect(quoted.stdout).toBe('"\n');
|
|
465
|
+
const special = await run('value=`' + String.raw`printf '%s' '\$name|\q'` + '`; printf "%s\\n" "$value"');
|
|
466
|
+
expect(special.status).toBe(0); expect(special.stderr).toBe('');
|
|
467
|
+
expect(special.stdout).toBe('$name|\\q\n');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("compound producer pipelines honor enclosing descriptor redirects", async () => {
|
|
471
|
+
const cwd=mkdtempSync(join(tmpdir(),'bunmsh-compound-fd-'));
|
|
472
|
+
try {
|
|
473
|
+
const r=await execute('exec 5>log; { for x in b a; do echo "$x"; done | sort; } >&5; exec 5>&-',createState({cwd}),{capture:true});
|
|
474
|
+
expect(r.status).toBe(0); expect(decode(r.stdout)).toBe(''); expect(decode(r.stderr)).toBe('');
|
|
475
|
+
expect(readFileSync(join(cwd,'log'),'utf8')).toBe('a\nb\n');
|
|
476
|
+
} finally {rmSync(cwd,{recursive:true,force:true});}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("else comments remain branch syntax rather than commands", async () => {
|
|
480
|
+
const r=await run('if true; then echo yes; else # all_targets is true\n echo BAD; fi\nif false; then echo BAD; else # other branch\n echo no; fi');
|
|
481
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
482
|
+
expect(r.stdout).toBe('yes\nno\n');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("while and until accept comments after done", async () => {
|
|
486
|
+
const r=await run('while true; do echo while; break; done # argument parsing loop\nuntil false; do echo until; break; done # another loop\necho after');
|
|
487
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
488
|
+
expect(r.stdout).toBe('while\nuntil\nafter\n');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
test("subshell case clauses do not close at the first pattern parenthesis", async () => {
|
|
493
|
+
const r = await run(String.raw`ac_try='./conftest'
|
|
494
|
+
(case "(($ac_try" in
|
|
495
|
+
*\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
|
|
496
|
+
*) ac_try_echo=$ac_try;;
|
|
497
|
+
esac
|
|
498
|
+
printf '%s\n' "$ac_try_echo")
|
|
499
|
+
( case x in x) echo spaced;; esac )
|
|
500
|
+
((case x in x) echo nested;; esac))`);
|
|
501
|
+
expect(r.stderr).toBe('');
|
|
502
|
+
expect(r.status).toBe(0);
|
|
503
|
+
expect(r.stdout).toBe('./conftest\nspaced\nnested\n');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
test("uniq supports output files and rejects unsupported options", async () => {
|
|
508
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-uniq-'));
|
|
509
|
+
try {
|
|
510
|
+
writeFileSync(join(dir,'input'),'a\na\nb');
|
|
511
|
+
const r=await run(`uniq '${dir}/input' '${dir}/output'`);
|
|
512
|
+
expect(r.status).toBe(0); expect(r.stdout).toBe('');
|
|
513
|
+
expect(readFileSync(join(dir,'output'),'utf8')).toBe('a\nb\n');
|
|
514
|
+
expect((await run('uniq -z')).status).toBe(1);
|
|
515
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
516
|
+
});
|
|
517
|
+
test("rm force ignores empty filenames while still deleting real operands", async () => {
|
|
518
|
+
const dir = mkdtempSync(join(tmpdir(),'bunmsh-rm-empty-'));
|
|
519
|
+
try {
|
|
520
|
+
writeFileSync(join(dir,'file'),'delete');
|
|
521
|
+
const r=await run(`rm -f ''\nrm -f -- '' '${dir}/file'`);
|
|
522
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
523
|
+
expect(existsSync(dir)).toBe(true); expect(existsSync(join(dir,'file'))).toBe(false);
|
|
524
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
test("set minus consumes the delimiter, ends options and preserves bare-call args", async () => {
|
|
529
|
+
const r=await run(`set -- kept two
|
|
530
|
+
set -
|
|
531
|
+
printf '%s:%s:%s\\n' "$#" "$1" "$2"
|
|
532
|
+
set - first -e
|
|
533
|
+
printf '%s:%s:%s\\n' "$#" "$1" "$2"
|
|
534
|
+
IFS='%'
|
|
535
|
+
set - /one%two%three
|
|
536
|
+
IFS=' '
|
|
537
|
+
printf '%s:%s:%s:%s\\n' "$#" "$1" "$2" "$3"`);
|
|
538
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
539
|
+
expect(r.stdout).toBe('2:kept:two\n2:first:-e\n1:/one%two%three::\n');
|
|
540
|
+
const state=createState({shellOptions:{x:true,v:true,e:true}});
|
|
541
|
+
await execute('set -',state,{capture:true});
|
|
542
|
+
expect(state.shellOptions.x).toBe(false);expect(state.shellOptions.v).toBe(false);
|
|
543
|
+
expect(state.shellOptions.e).toBe(true);
|
|
544
|
+
const split=await run(`dstdir='/one/two/three'
|
|
545
|
+
IFS='%'
|
|
546
|
+
set - $(echo "$dstdir" | sed 's@/@%@g;s@^%@/@')
|
|
547
|
+
IFS=' '
|
|
548
|
+
printf '%s:%s:%s:%s\\n' "$#" "$1" "$2" "$3"`);
|
|
549
|
+
expect(split.stdout).toBe('3:/one:two:three\n'); expect(split.status).toBe(0);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
test("backquoted brace groups are not parsed as parameter expansions", async () => {
|
|
554
|
+
const tick=String.fromCharCode(96);
|
|
555
|
+
const r=await run('if false; then echo BAD; elif test "X'+tick+'{ echo ok; }'+tick+'" = Xok; then echo yes; fi\nx='+tick+'{ echo assigned; }'+tick+'\necho "$x"');
|
|
556
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
557
|
+
expect(r.stdout).toBe('yes\nassigned\n');
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
test("logical connectors retain heredoc bodies before compound operands", async () => {
|
|
562
|
+
const dir=mkdtempSync(join(tmpdir(),'bunmsh-heredoc-chain-'));
|
|
563
|
+
try {
|
|
564
|
+
const r=await run(`if true; then
|
|
565
|
+
echo 'BEGIN {' >'${dir}/script' &&
|
|
566
|
+
cat >>'${dir}/script' <<'ONE' &&
|
|
567
|
+
S["name"]="value"
|
|
568
|
+
ONE
|
|
569
|
+
cat >>'${dir}/script' <<TWO &&
|
|
570
|
+
}
|
|
571
|
+
TWO
|
|
572
|
+
if true; then cat '${dir}/script'; fi
|
|
573
|
+
fi`);
|
|
574
|
+
expect(r.status).toBe(0); expect(r.stderr).toBe('');
|
|
575
|
+
expect(r.stdout).toBe('BEGIN {\nS["name"]="value"\n}\n');
|
|
576
|
+
const piped=await run(`{ echo first; } | cat <<END &&
|
|
577
|
+
second
|
|
578
|
+
END
|
|
579
|
+
{ echo last; }`);
|
|
580
|
+
expect(piped.status).toBe(0); expect(piped.stderr).toBe('');
|
|
581
|
+
expect(piped.stdout).toBe('second\nlast\n');
|
|
582
|
+
} finally { rmSync(dir,{recursive:true,force:true}); }
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
test("mkdir processes parent and child operands in order", async () => {
|
|
587
|
+
const cwd=mkdtempSync(join(tmpdir(),'bunmsh-mkdir-order-'));
|
|
588
|
+
try {
|
|
589
|
+
for(let i=0;i<8;i++) {
|
|
590
|
+
const r=await execute(`builtin mkdir p${i} p${i}/child p${i}/child/grandchild`,createState({cwd}),{capture:true});
|
|
591
|
+
expect(r.status).toBe(0); expect(decode(r.stderr)).toBe('');
|
|
592
|
+
expect(existsSync(join(cwd,`p${i}/child/grandchild`))).toBe(true);
|
|
593
|
+
}
|
|
594
|
+
} finally { rmSync(cwd,{recursive:true,force:true}); }
|
|
595
|
+
});
|