buninu 0.3.0 → 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.
- package/README.md +21 -0
- package/apps/bunmsh/CHANGELOG.md +262 -0
- package/apps/bunmsh/LICENSE +21 -0
- package/apps/bunmsh/LICENSE-MKSH +295 -0
- package/apps/bunmsh/PORTING.md +192 -0
- package/apps/bunmsh/README.md +801 -0
- package/apps/bunmsh/bunmsh +7 -0
- package/apps/bunmsh/bunshell.md +61 -0
- package/apps/bunmsh/help/README.md +1945 -0
- package/apps/bunmsh/help/alias.md +22 -0
- package/apps/bunmsh/help/basename.md +38 -0
- package/apps/bunmsh/help/builtin.md +25 -0
- package/apps/bunmsh/help/bun.md +27 -0
- package/apps/bunmsh/help/bunmsh.md +47 -0
- package/apps/bunmsh/help/cat.md +31 -0
- package/apps/bunmsh/help/catfancy.md +35 -0
- package/apps/bunmsh/help/cd.md +22 -0
- package/apps/bunmsh/help/chmod.md +27 -0
- package/apps/bunmsh/help/clear.md +21 -0
- package/apps/bunmsh/help/colon.md +22 -0
- package/apps/bunmsh/help/command.md +27 -0
- package/apps/bunmsh/help/cp.md +28 -0
- package/apps/bunmsh/help/cut.md +25 -0
- package/apps/bunmsh/help/date.md +25 -0
- package/apps/bunmsh/help/dirname.md +25 -0
- package/apps/bunmsh/help/echo.md +25 -0
- package/apps/bunmsh/help/env.md +27 -0
- package/apps/bunmsh/help/eval.md +22 -0
- package/apps/bunmsh/help/exec.md +22 -0
- package/apps/bunmsh/help/exit.md +21 -0
- package/apps/bunmsh/help/export.md +22 -0
- package/apps/bunmsh/help/false.md +22 -0
- package/apps/bunmsh/help/find.md +33 -0
- package/apps/bunmsh/help/getopts.md +28 -0
- package/apps/bunmsh/help/grep.md +34 -0
- package/apps/bunmsh/help/head.md +27 -0
- package/apps/bunmsh/help/kill.md +27 -0
- package/apps/bunmsh/help/ln.md +28 -0
- package/apps/bunmsh/help/ls.md +37 -0
- package/apps/bunmsh/help/lsbun.md +29 -0
- package/apps/bunmsh/help/lsfancy.md +37 -0
- package/apps/bunmsh/help/md5sum.md +21 -0
- package/apps/bunmsh/help/mkdir.md +27 -0
- package/apps/bunmsh/help/mktemp.md +26 -0
- package/apps/bunmsh/help/mv.md +26 -0
- package/apps/bunmsh/help/print.md +30 -0
- package/apps/bunmsh/help/printf.md +25 -0
- package/apps/bunmsh/help/pwd.md +21 -0
- package/apps/bunmsh/help/read.md +27 -0
- package/apps/bunmsh/help/readonly.md +27 -0
- package/apps/bunmsh/help/realpath.md +21 -0
- package/apps/bunmsh/help/rm.md +33 -0
- package/apps/bunmsh/help/rmdir.md +27 -0
- package/apps/bunmsh/help/sed.md +29 -0
- package/apps/bunmsh/help/seq.md +29 -0
- package/apps/bunmsh/help/serve.md +50 -0
- package/apps/bunmsh/help/set.md +26 -0
- package/apps/bunmsh/help/sha256sum.md +21 -0
- package/apps/bunmsh/help/shift.md +23 -0
- package/apps/bunmsh/help/sleep.md +25 -0
- package/apps/bunmsh/help/sort.md +29 -0
- package/apps/bunmsh/help/source.md +23 -0
- package/apps/bunmsh/help/tab.md +30 -0
- package/apps/bunmsh/help/tac.md +29 -0
- package/apps/bunmsh/help/tail.md +27 -0
- package/apps/bunmsh/help/tee.md +25 -0
- package/apps/bunmsh/help/test.md +30 -0
- package/apps/bunmsh/help/time.md +29 -0
- package/apps/bunmsh/help/touch.md +26 -0
- package/apps/bunmsh/help/tr.md +26 -0
- package/apps/bunmsh/help/true.md +22 -0
- package/apps/bunmsh/help/type.md +21 -0
- package/apps/bunmsh/help/umask.md +22 -0
- package/apps/bunmsh/help/unalias.md +28 -0
- package/apps/bunmsh/help/uname.md +31 -0
- package/apps/bunmsh/help/unset.md +28 -0
- package/apps/bunmsh/help/wc.md +27 -0
- package/apps/bunmsh/help/whence.md +26 -0
- package/apps/bunmsh/help/which.md +41 -0
- package/apps/bunmsh/help/yes.md +23 -0
- package/apps/bunmsh/package.json +26 -0
- package/apps/bunmsh/serve.js +468 -0
- package/apps/bunmsh/single-exe/README.md +402 -0
- package/apps/bunmsh/single-exe/assetsHelper.js +371 -0
- package/apps/bunmsh/single-exe/assetsLoader.mjs +85 -0
- package/apps/bunmsh/single-exe/assetsPacker.js +218 -0
- package/apps/bunmsh/single-exe/compiled.js +527 -0
- package/apps/bunmsh/single-exe/entry.mjs +17 -0
- package/apps/bunmsh/src/cat-fancy.js +117 -0
- package/apps/bunmsh/src/cat-operands.js +31 -0
- package/apps/bunmsh/src/completion.js +247 -0
- package/apps/bunmsh/src/environment.js +61 -0
- package/apps/bunmsh/src/fancy-ls.js +186 -0
- package/apps/bunmsh/src/find.js +119 -0
- package/apps/bunmsh/src/history.js +198 -0
- package/apps/bunmsh/src/main.js +776 -0
- package/apps/bunmsh/src/mouse.js +103 -0
- package/apps/bunmsh/src/shell.js +3557 -0
- package/apps/bunmsh/test/catfancy.test.js +102 -0
- package/apps/bunmsh/test/reference-source.sh +1 -0
- package/apps/bunmsh/test/reference.test.js +677 -0
- package/apps/bunmsh/test/serve/README.md +3 -0
- package/apps/bunmsh/test/serve/bad.json +1 -0
- package/apps/bunmsh/test/serve/data.json +1 -0
- package/apps/bunmsh/test/serve/data.json5 +1 -0
- package/apps/bunmsh/test/serve/data.jsonc +2 -0
- package/apps/bunmsh/test/serve/data.jsonl +2 -0
- package/apps/bunmsh/test/serve/data.ndjson +1 -0
- package/apps/bunmsh/test/serve/data.toml +3 -0
- package/apps/bunmsh/test/serve/data.ts +15 -0
- package/apps/bunmsh/test/serve/data.xml +1 -0
- package/apps/bunmsh/test/serve/data.yaml +3 -0
- package/apps/bunmsh/test/serve/data.yml +1 -0
- package/apps/bunmsh/test/serve.test.js +241 -0
- package/apps/bunmsh/test/shell.test.js +1778 -0
- package/apps/bunmsh/test/t.sh +12 -0
- package/bin/init.js +11 -3
- package/package.json +2 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { delimiter, dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { safeHistoryEntry } from "./history.js";
|
|
5
|
+
import { environmentValue } from "./environment.js";
|
|
6
|
+
|
|
7
|
+
const WINDOWS_EXECUTABLE_EXTENSIONS = [".exe", ".com", ".cmd", ".bat"];
|
|
8
|
+
|
|
9
|
+
function commandName(entry, platform) {
|
|
10
|
+
if (platform !== "win32") return entry.name;
|
|
11
|
+
const lower = entry.name.toLowerCase();
|
|
12
|
+
const extension = WINDOWS_EXECUTABLE_EXTENSIONS.find((value) => lower.endsWith(value));
|
|
13
|
+
return extension ? entry.name.slice(0, -extension.length) : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function lowerBound(items, value) {
|
|
17
|
+
let left = 0;
|
|
18
|
+
let right = items.length;
|
|
19
|
+
while (left < right) {
|
|
20
|
+
const middle = (left + right) >>> 1;
|
|
21
|
+
if (items[middle] < value) left = middle + 1;
|
|
22
|
+
else right = middle;
|
|
23
|
+
}
|
|
24
|
+
return left;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function prefixMatches(items, prefix) {
|
|
28
|
+
if (!prefix) return [];
|
|
29
|
+
const start = lowerBound(items, prefix);
|
|
30
|
+
const end = lowerBound(items, `${prefix}\uffff`);
|
|
31
|
+
return items.slice(start, end);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function firstPrefixMatch(items, prefix) {
|
|
35
|
+
if (!prefix) return null;
|
|
36
|
+
const index = lowerBound(items, prefix);
|
|
37
|
+
return items[index]?.startsWith(prefix) ? items[index] : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function completionContext(line, cursor = line.length) {
|
|
41
|
+
const source = line.slice(0, cursor);
|
|
42
|
+
let command = true;
|
|
43
|
+
let wordStart = 0;
|
|
44
|
+
let quote = null;
|
|
45
|
+
let escaped = false;
|
|
46
|
+
let token = "";
|
|
47
|
+
|
|
48
|
+
const finishWord = () => {
|
|
49
|
+
if (!token) return;
|
|
50
|
+
if (command && /^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {
|
|
51
|
+
token = "";
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
command = false;
|
|
55
|
+
token = "";
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < source.length; i++) {
|
|
59
|
+
const ch = source[i];
|
|
60
|
+
if (escaped) {
|
|
61
|
+
token += ch;
|
|
62
|
+
escaped = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "\\" && quote !== "'") {
|
|
66
|
+
escaped = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (quote) {
|
|
70
|
+
if (ch === quote) quote = null;
|
|
71
|
+
else token += ch;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === "'" || ch === '"') {
|
|
75
|
+
quote = ch;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
|
|
79
|
+
finishWord();
|
|
80
|
+
command = true;
|
|
81
|
+
token = "";
|
|
82
|
+
wordStart = i + 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (/\s/.test(ch)) {
|
|
86
|
+
finishWord();
|
|
87
|
+
wordStart = i + 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!token) wordStart = i;
|
|
91
|
+
token += ch;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const assignment = command && /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
95
|
+
return {
|
|
96
|
+
command: command && !assignment && quote === null,
|
|
97
|
+
prefix: token,
|
|
98
|
+
start: wordStart,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function historyGhost(history, line) {
|
|
103
|
+
if (!line) return null;
|
|
104
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
105
|
+
const entry = history[i];
|
|
106
|
+
if (!safeHistoryEntry(entry)) continue;
|
|
107
|
+
if (entry !== line && entry.startsWith(line)) return entry.slice(line.length);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function nextGhostChunk(ghost) {
|
|
113
|
+
if (!ghost) return "";
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < ghost.length && /\s/.test(ghost[i])) i++;
|
|
116
|
+
while (i < ghost.length && !/\s/.test(ghost[i])) i++;
|
|
117
|
+
while (i < ghost.length && /\s/.test(ghost[i])) i++;
|
|
118
|
+
return ghost.slice(0, i || ghost.length);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function fitGhost(ghost, available) {
|
|
122
|
+
let output = "", width = 0;
|
|
123
|
+
for (const character of ghost ?? "") {
|
|
124
|
+
const characterWidth = Bun.stringWidth(character);
|
|
125
|
+
if (width + characterWidth > Math.max(0, available)) break;
|
|
126
|
+
output += character;
|
|
127
|
+
width += characterWidth;
|
|
128
|
+
}
|
|
129
|
+
return { output, width };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class CommandIndex {
|
|
133
|
+
constructor(builtins, options = {}) {
|
|
134
|
+
this.builtins = builtins;
|
|
135
|
+
this.platform = options.platform ?? process.platform;
|
|
136
|
+
this.pathDelimiter = options.pathDelimiter ?? delimiter;
|
|
137
|
+
this.pathNames = [];
|
|
138
|
+
this.names = [...builtins].sort();
|
|
139
|
+
this.pathValue = null;
|
|
140
|
+
this.cwd = null;
|
|
141
|
+
this.aliasSignature = null;
|
|
142
|
+
this.refreshing = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
syncShellNames(state) {
|
|
146
|
+
const aliases = Object.keys(state.aliases).sort();
|
|
147
|
+
const signature = aliases.join("\0");
|
|
148
|
+
if (signature === this.aliasSignature) return;
|
|
149
|
+
this.aliasSignature = signature;
|
|
150
|
+
this.names = [...new Set([...this.builtins, ...aliases, ...this.pathNames])].sort();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async refresh(state) {
|
|
154
|
+
if (this.refreshing) return this.refreshing;
|
|
155
|
+
const pathValue = environmentValue(state.env, "PATH", this.platform) ?? "";
|
|
156
|
+
const cwd = state.cwd;
|
|
157
|
+
this.refreshing = (async () => {
|
|
158
|
+
const directories = [...new Set(pathValue.split(this.pathDelimiter).map((directory) => {
|
|
159
|
+
if (!directory) return cwd;
|
|
160
|
+
return isAbsolute(directory) ? directory : resolve(cwd, directory);
|
|
161
|
+
}))];
|
|
162
|
+
const listings = await Promise.all(directories.map(async (directory) => {
|
|
163
|
+
try {
|
|
164
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
165
|
+
return entries
|
|
166
|
+
.filter((entry) => !entry.isDirectory())
|
|
167
|
+
.map((entry) => commandName(entry, this.platform))
|
|
168
|
+
.filter((name) => name !== null);
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}));
|
|
173
|
+
this.pathNames = listings.flat();
|
|
174
|
+
this.pathValue = pathValue;
|
|
175
|
+
this.cwd = cwd;
|
|
176
|
+
this.aliasSignature = null;
|
|
177
|
+
this.syncShellNames(state);
|
|
178
|
+
})().finally(() => {
|
|
179
|
+
this.refreshing = null;
|
|
180
|
+
});
|
|
181
|
+
return this.refreshing;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
refreshIfChanged(state) {
|
|
185
|
+
this.syncShellNames(state);
|
|
186
|
+
const pathValue = environmentValue(state.env, "PATH", this.platform) ?? "";
|
|
187
|
+
const relativePath = pathValue.split(this.pathDelimiter)
|
|
188
|
+
.some((directory) => !directory || !isAbsolute(directory));
|
|
189
|
+
if (this.pathValue !== pathValue || (relativePath && this.cwd !== state.cwd))
|
|
190
|
+
void this.refresh(state);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
matches(prefix) {
|
|
194
|
+
return prefixMatches(this.names, prefix);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
first(prefix) {
|
|
198
|
+
return firstPrefixMatch(this.names, prefix);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export class FileIndex {
|
|
203
|
+
constructor(ttl = 10_000) {
|
|
204
|
+
this.ttl = ttl;
|
|
205
|
+
this.directories = new Map();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
entries(directory) {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const cached = this.directories.get(directory);
|
|
211
|
+
if (cached && now - cached.at < this.ttl) return cached.names;
|
|
212
|
+
let names = [];
|
|
213
|
+
try {
|
|
214
|
+
names = readdirSync(directory, { withFileTypes: true })
|
|
215
|
+
.map((entry) => entry.name + (entry.isDirectory() ? "/" : ""))
|
|
216
|
+
.sort();
|
|
217
|
+
} catch {}
|
|
218
|
+
this.directories.set(directory, { at: now, names });
|
|
219
|
+
return names;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
context(prefix, state) {
|
|
223
|
+
const slash = prefix.lastIndexOf("/");
|
|
224
|
+
const directoryPart = slash < 0 ? "" : prefix.slice(0, slash + 1);
|
|
225
|
+
const basename = prefix.slice(slash + 1);
|
|
226
|
+
let directory;
|
|
227
|
+
if (directoryPart.startsWith("~/"))
|
|
228
|
+
directory = resolve(state.env.HOME ?? state.cwd, directoryPart.slice(2));
|
|
229
|
+
else if (isAbsolute(directoryPart)) directory = dirname(`${directoryPart}.`);
|
|
230
|
+
else directory = resolve(state.cwd, directoryPart || ".");
|
|
231
|
+
return { directoryPart, basename, directory };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
matches(prefix, state) {
|
|
235
|
+
const { directoryPart, basename, directory } = this.context(prefix, state);
|
|
236
|
+
const names = basename
|
|
237
|
+
? prefixMatches(this.entries(directory), basename)
|
|
238
|
+
: this.entries(directory);
|
|
239
|
+
return names
|
|
240
|
+
.filter((name) => basename.startsWith(".") || !name.startsWith("."))
|
|
241
|
+
.map((name) => `${directoryPart}${name}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
first(prefix, state) {
|
|
245
|
+
return this.matches(prefix, state)[0] ?? null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function environmentValue(env, name, platform = process.platform) {
|
|
2
|
+
if (Object.hasOwn(env, name)) return env[name];
|
|
3
|
+
if (platform !== "win32") return undefined;
|
|
4
|
+
const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
5
|
+
return key === undefined ? undefined : env[key];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function canonicalEnvironment(env, platform = process.platform) {
|
|
9
|
+
const output = { ...env };
|
|
10
|
+
if (platform !== "win32") return output;
|
|
11
|
+
const canonicalize = (name) => {
|
|
12
|
+
const keys = Object.keys(output).filter((key) => key.toLowerCase() === name.toLowerCase());
|
|
13
|
+
if (!keys.length) return undefined;
|
|
14
|
+
const value = output[keys.at(-1)];
|
|
15
|
+
for (const key of keys) delete output[key];
|
|
16
|
+
output[name] = value;
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
if (canonicalize("PATH") === undefined) delete output.PATH;
|
|
20
|
+
let home = canonicalize("HOME");
|
|
21
|
+
if (!home) {
|
|
22
|
+
home = environmentValue(output, "USERPROFILE", platform);
|
|
23
|
+
if (!home) {
|
|
24
|
+
const drive = environmentValue(output, "HOMEDRIVE", platform) ?? "";
|
|
25
|
+
const path = environmentValue(output, "HOMEPATH", platform) ?? "";
|
|
26
|
+
home = drive && path ? `${drive}${path}` : "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (home) {
|
|
30
|
+
output.HOME = String(home).replaceAll("\\", "/");
|
|
31
|
+
} else {
|
|
32
|
+
delete output.HOME;
|
|
33
|
+
}
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function homeRelativePath(cwd, home, platform = process.platform) {
|
|
38
|
+
if (!home) return null;
|
|
39
|
+
const trim = (value) => {
|
|
40
|
+
const normalized = String(value).replaceAll("\\", "/");
|
|
41
|
+
return normalized === "/" ? "/" : normalized.replace(/\/+$/, "");
|
|
42
|
+
};
|
|
43
|
+
// Android exposes app data through both /data/data/PACKAGE and
|
|
44
|
+
// /data/user/0/PACKAGE. They are the same app sandbox but string comparison
|
|
45
|
+
// alone cannot recognise HOME when the two spellings are mixed.
|
|
46
|
+
const comparable = (value) => trim(value).replace(
|
|
47
|
+
/^\/data\/user\/0\/([^/]+)(?=\/|$)/,
|
|
48
|
+
"/data/data/$1",
|
|
49
|
+
);
|
|
50
|
+
let comparedCwd = comparable(cwd);
|
|
51
|
+
let comparedHome = comparable(home);
|
|
52
|
+
if (platform === "win32") {
|
|
53
|
+
comparedCwd = comparedCwd.toLowerCase();
|
|
54
|
+
comparedHome = comparedHome.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
if (comparedCwd === comparedHome) return "";
|
|
57
|
+
if (comparedHome === "/" && comparedCwd.startsWith("/")) return comparedCwd;
|
|
58
|
+
return comparedCwd.startsWith(`${comparedHome}/`)
|
|
59
|
+
? comparable(cwd).slice(comparedHome.length)
|
|
60
|
+
: null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readlinkSync } from "node:fs";
|
|
2
|
+
import { basename, isAbsolute, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const IMAGE = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif", "heic"]);
|
|
5
|
+
const MUSIC = new Set(["mp3", "wav", "flac", "ogg", "m4a", "aac", "opus", "mid", "midi"]);
|
|
6
|
+
const VIDEO = new Set(["mp4", "mkv", "webm", "mov", "avi", "m4v", "mpeg", "mpg"]);
|
|
7
|
+
const ICONS = new Map([
|
|
8
|
+
["js", "🟨"], ["mjs", "🟨"], ["cjs", "🟨"],
|
|
9
|
+
["ts", "🟦"], ["mts", "🟦"], ["cts", "🟦"],
|
|
10
|
+
["py", "🐍"], ["c", "🔧"], ["h", "🧩"], ["rs", "🦀"],
|
|
11
|
+
["html", "🌐"], ["htm", "🌐"], ["css", "🎨"],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function extension(name) {
|
|
15
|
+
const index = name.lastIndexOf(".");
|
|
16
|
+
return index > 0 ? name.slice(index + 1).toLowerCase() : "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function iconFor(name, stats, path = null) {
|
|
20
|
+
if (stats.isDirectory()) return "📦";
|
|
21
|
+
if (stats.isSymbolicLink()) return path !== null && !existsSync(path) ? "🚫" : "🔗";
|
|
22
|
+
const ext = extension(name);
|
|
23
|
+
if (IMAGE.has(ext)) return "🖼️";
|
|
24
|
+
if (MUSIC.has(ext)) return "🎵";
|
|
25
|
+
if (VIDEO.has(ext)) return "🎬";
|
|
26
|
+
if (ICONS.has(ext)) return ICONS.get(ext);
|
|
27
|
+
if (stats.mode & 0o111) return "⚙️";
|
|
28
|
+
return "📄";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Only touches cross-platform-safe fs.Stats methods (isDirectory/
|
|
32
|
+
// isSymbolicLink/isSocket/isFIFO always return a real boolean on Windows
|
|
33
|
+
// too, just typically false for socket/FIFO since NTFS directory entries
|
|
34
|
+
// aren't reported as those) and the same mode-bit check iconFor already
|
|
35
|
+
// uses elsewhere in this file, so this needs no platform-specific handling.
|
|
36
|
+
function classifySuffix(stats) {
|
|
37
|
+
if (stats.isDirectory()) return "/";
|
|
38
|
+
if (stats.isSymbolicLink()) return "@";
|
|
39
|
+
if (stats.isSocket()) return "=";
|
|
40
|
+
if (stats.isFIFO()) return "|";
|
|
41
|
+
if (stats.mode & 0o111) return "*";
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function displayEntry(entry, options) {
|
|
46
|
+
const suffix = options.classify ? classifySuffix(entry.stats) : entry.stats.isDirectory() ? "/" : "";
|
|
47
|
+
return `${iconFor(entry.name, entry.stats, entry.path)} ${entry.name}${suffix}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function columns(items, width) {
|
|
51
|
+
if (!items.length) return "";
|
|
52
|
+
const widest = Math.max(...items.map((item) => Bun.stringWidth(item))) + 2;
|
|
53
|
+
const count = Math.max(1, Math.floor(width / widest));
|
|
54
|
+
let output = "";
|
|
55
|
+
for (let index = 0; index < items.length; index++) {
|
|
56
|
+
const last = index % count === count - 1 || index === items.length - 1;
|
|
57
|
+
output += items[index];
|
|
58
|
+
output += last ? "\n" : " ".repeat(widest - Bun.stringWidth(items[index]));
|
|
59
|
+
}
|
|
60
|
+
return output;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function modeString(mode) {
|
|
64
|
+
const kinds = [[0o400, "r"], [0o200, "w"], [0o100, "x"], [0o040, "r"], [0o020, "w"], [0o010, "x"], [0o004, "r"], [0o002, "w"], [0o001, "x"]];
|
|
65
|
+
return kinds.map(([bit, letter]) => mode & bit ? letter : "-").join("");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function linkTarget(path) {
|
|
69
|
+
try {
|
|
70
|
+
// Node/Bun read the reparse point's stored target on Windows the same
|
|
71
|
+
// way as a POSIX symlink; only the separator needs normalizing to match
|
|
72
|
+
// the forward-slash paths the rest of the shell displays.
|
|
73
|
+
const target = readlinkSync(path);
|
|
74
|
+
return process.platform === "win32" ? target.replaceAll("\\", "/") : target;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function localDateTime(ms) {
|
|
81
|
+
const date = new Date(ms);
|
|
82
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
83
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} `
|
|
84
|
+
+ `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function humanSize(bytes) {
|
|
88
|
+
if (bytes < 1024) return String(bytes);
|
|
89
|
+
const units = ["K", "M", "G", "T", "P"];
|
|
90
|
+
let value = bytes;
|
|
91
|
+
let unit = -1;
|
|
92
|
+
do { value /= 1024; unit++; } while (value >= 1024 && unit < units.length - 1);
|
|
93
|
+
return `${value >= 10 ? value.toFixed(0) : value.toFixed(1).replace(/\.0$/, "")}${units[unit]}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listDirectory(path, options) {
|
|
97
|
+
let entries = readdirSync(path, { withFileTypes: true })
|
|
98
|
+
.filter((entry) => options.all || options.almostAll || !entry.name.startsWith("."))
|
|
99
|
+
.map((entry) => {
|
|
100
|
+
const entryPath = resolve(path, entry.name);
|
|
101
|
+
return { name: entry.name, stats: lstatSync(entryPath), path: entryPath };
|
|
102
|
+
});
|
|
103
|
+
if (options.all) entries = [
|
|
104
|
+
{ name: ".", stats: lstatSync(path), path },
|
|
105
|
+
{ name: "..", stats: lstatSync(resolve(path, "..")), path: resolve(path, "..") },
|
|
106
|
+
...entries,
|
|
107
|
+
];
|
|
108
|
+
entries.sort(options.size
|
|
109
|
+
? (a, b) => b.stats.size - a.stats.size || a.name.localeCompare(b.name)
|
|
110
|
+
: options.time
|
|
111
|
+
? (a, b) => b.stats.mtimeMs - a.stats.mtimeMs || a.name.localeCompare(b.name)
|
|
112
|
+
: (a, b) => a.name.localeCompare(b.name));
|
|
113
|
+
if (options.reverse) entries.reverse();
|
|
114
|
+
return entries;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function fancyLs(argv, state, terminal = Boolean(process.stdout.isTTY)) {
|
|
118
|
+
const options = {
|
|
119
|
+
all: false, almostAll: false, directory: false, long: false, human: false,
|
|
120
|
+
recursive: false, time: false, reverse: false, size: false, one: false, classify: false,
|
|
121
|
+
};
|
|
122
|
+
const operands = [];
|
|
123
|
+
for (const argument of argv.slice(1)) {
|
|
124
|
+
if (argument === "--color=auto" || argument === "--color" || argument === "--") continue;
|
|
125
|
+
if (/^-[^-]/.test(argument)) {
|
|
126
|
+
for (const flag of argument.slice(1)) {
|
|
127
|
+
if (flag === "a") options.all = true;
|
|
128
|
+
else if (flag === "A") options.almostAll = true;
|
|
129
|
+
else if (flag === "d") options.directory = true;
|
|
130
|
+
else if (flag === "l") options.long = true;
|
|
131
|
+
else if (flag === "h") options.human = true;
|
|
132
|
+
else if (flag === "R") options.recursive = true;
|
|
133
|
+
else if (flag === "t") options.time = true;
|
|
134
|
+
else if (flag === "r") options.reverse = true;
|
|
135
|
+
else if (flag === "S") options.size = true;
|
|
136
|
+
else if (flag === "1") options.one = true;
|
|
137
|
+
else if (flag === "F") options.classify = true;
|
|
138
|
+
else return { status: 2, stdout: "", stderr: `bunmsh: ${argv[0]}: unsupported option: -${flag}\n` };
|
|
139
|
+
}
|
|
140
|
+
} else operands.push(argument);
|
|
141
|
+
}
|
|
142
|
+
if (!operands.length) operands.push(".");
|
|
143
|
+
let status = 0, stdout = "", stderr = "";
|
|
144
|
+
const visited = new Set();
|
|
145
|
+
const show = (operand, heading = operands.length > 1) => {
|
|
146
|
+
const path = isAbsolute(operand) ? operand : resolve(state.cwd, operand);
|
|
147
|
+
try {
|
|
148
|
+
const stats = lstatSync(path);
|
|
149
|
+
let entries;
|
|
150
|
+
if (!stats.isDirectory() || options.directory)
|
|
151
|
+
entries = [{ name: basename(operand) || operand, stats, path }];
|
|
152
|
+
else entries = listDirectory(path, options);
|
|
153
|
+
if (heading) stdout += `${operand}:\n`;
|
|
154
|
+
const rendered = entries.map((entry) => displayEntry(entry, options));
|
|
155
|
+
if (options.long) {
|
|
156
|
+
for (let i = 0; i < entries.length; i++) {
|
|
157
|
+
const item = entries[i];
|
|
158
|
+
const date = localDateTime(item.stats.mtimeMs);
|
|
159
|
+
const size = options.human ? humanSize(item.stats.size) : String(item.stats.size);
|
|
160
|
+
const target = item.stats.isSymbolicLink() ? linkTarget(item.path) : null;
|
|
161
|
+
const suffix = target === null ? "" : ` -> ${target}`;
|
|
162
|
+
stdout += `${item.stats.isDirectory() ? "d" : item.stats.isSymbolicLink() ? "l" : "-"}${modeString(item.stats.mode)} ${size.padStart(8)} ${date} ${rendered[i]}${suffix}\n`;
|
|
163
|
+
}
|
|
164
|
+
} else stdout += terminal && !options.one
|
|
165
|
+
? columns(rendered, process.stdout.columns ?? 80)
|
|
166
|
+
: `${rendered.join("\n")}${rendered.length ? "\n" : ""}`;
|
|
167
|
+
if (options.recursive && stats.isDirectory() && !options.directory) {
|
|
168
|
+
for (const entry of entries) if (entry.stats.isDirectory() && entry.name !== "." && entry.name !== "..") {
|
|
169
|
+
const child = resolve(path, entry.name);
|
|
170
|
+
if (visited.has(child)) continue;
|
|
171
|
+
visited.add(child);
|
|
172
|
+
stdout += "\n";
|
|
173
|
+
show(`${operand.replace(/\/$/, "")}/${entry.name}`, true);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
status = 1;
|
|
178
|
+
stderr += `bunmsh: ${argv[0]}: ${operand}: ${error.message}\n`;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
for (let i = 0; i < operands.length; i++) {
|
|
182
|
+
if (i) stdout += "\n";
|
|
183
|
+
show(operands[i]);
|
|
184
|
+
}
|
|
185
|
+
return { status, stdout, stderr };
|
|
186
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, isAbsolute, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const shellPath = (value) => value.replaceAll("\\", "/");
|
|
5
|
+
const nativePath = (value, platform) => platform === "win32" ? value.replaceAll("/", "\\") : value;
|
|
6
|
+
|
|
7
|
+
function globRegex(pattern, insensitive = false) {
|
|
8
|
+
let source = "^";
|
|
9
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
10
|
+
const ch = pattern[i];
|
|
11
|
+
if (ch === "*") source += ".*";
|
|
12
|
+
else if (ch === "?") source += ".";
|
|
13
|
+
else if (ch === "[") {
|
|
14
|
+
const end = pattern.indexOf("]", i + 1);
|
|
15
|
+
if (end < 0) source += "\\[";
|
|
16
|
+
else { source += pattern.slice(i, end + 1); i = end; }
|
|
17
|
+
} else source += ch.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
18
|
+
}
|
|
19
|
+
return new RegExp(`${source}$`, insensitive ? "i" : "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function findIsRegularBuiltin(platform = process.platform) {
|
|
23
|
+
return platform === "win32";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runFind(argv, state, platform = process.platform, execute = null) {
|
|
27
|
+
let index = 1;
|
|
28
|
+
const roots = [];
|
|
29
|
+
while (index < argv.length && argv[index] !== "!" && argv[index] !== "-not" &&
|
|
30
|
+
!argv[index].startsWith("-")) roots.push(argv[index++]);
|
|
31
|
+
if (!roots.length) roots.push(".");
|
|
32
|
+
let minDepth = 0, maxDepth = Infinity, print0 = false, negate = false, explicitPrint = false;
|
|
33
|
+
const predicates = [];
|
|
34
|
+
const actions = [];
|
|
35
|
+
while (index < argv.length) {
|
|
36
|
+
const option = argv[index++];
|
|
37
|
+
if (option === "!" || option === "-not") { negate = !negate; continue; }
|
|
38
|
+
if (option === "-print") { explicitPrint = true; continue; }
|
|
39
|
+
if (option === "-print0") { explicitPrint = true; print0 = true; continue; }
|
|
40
|
+
if (option === "-exec") {
|
|
41
|
+
const command = [];
|
|
42
|
+
while (index < argv.length && argv[index] !== ";" && argv[index] !== "+")
|
|
43
|
+
command.push(argv[index++]);
|
|
44
|
+
const terminator = argv[index++];
|
|
45
|
+
if (!command.length || (terminator !== ";" && terminator !== "+"))
|
|
46
|
+
return { status: 2, stdout: "", stderr: "bunmsh: find: -exec: missing command or terminator\n" };
|
|
47
|
+
if (!command.includes("{}"))
|
|
48
|
+
return { status: 2, stdout: "", stderr: "bunmsh: find: -exec: command must contain {}\n" };
|
|
49
|
+
actions.push({ command, mode: terminator });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const argument = argv[index++];
|
|
53
|
+
if (argument === undefined)
|
|
54
|
+
return { status: 2, stdout: "", stderr: `bunmsh: find: ${option}: missing argument\n` };
|
|
55
|
+
let predicate;
|
|
56
|
+
if (option === "-name" || option === "-iname") {
|
|
57
|
+
const regex = globRegex(argument, option === "-iname");
|
|
58
|
+
predicate = (entry) => regex.test(basename(entry.shown));
|
|
59
|
+
} else if (option === "-path" || option === "-ipath") {
|
|
60
|
+
const regex = globRegex(argument, option === "-ipath");
|
|
61
|
+
predicate = (entry) => regex.test(entry.shown);
|
|
62
|
+
} else if (option === "-type") {
|
|
63
|
+
if (!/^[fdl]$/.test(argument))
|
|
64
|
+
return { status: 2, stdout: "", stderr: `bunmsh: find: unsupported type: ${argument}\n` };
|
|
65
|
+
predicate = (entry) => argument === "f" ? entry.stats.isFile() :
|
|
66
|
+
argument === "d" ? entry.stats.isDirectory() : entry.stats.isSymbolicLink();
|
|
67
|
+
} else if (option === "-mindepth" || option === "-maxdepth") {
|
|
68
|
+
if (!/^\d+$/.test(argument))
|
|
69
|
+
return { status: 2, stdout: "", stderr: `bunmsh: find: ${option}: invalid depth: ${argument}\n` };
|
|
70
|
+
if (option === "-mindepth") minDepth = Number(argument); else maxDepth = Number(argument);
|
|
71
|
+
continue;
|
|
72
|
+
} else return { status: 2, stdout: "", stderr: `bunmsh: find: unsupported option: ${option}\n` };
|
|
73
|
+
predicates.push(negate ? (entry) => !predicate(entry) : predicate);
|
|
74
|
+
negate = false;
|
|
75
|
+
}
|
|
76
|
+
if (negate) return { status: 2, stdout: "", stderr: "bunmsh: find: expected expression after !\n" };
|
|
77
|
+
let status = 0, stdout = "", stderr = "";
|
|
78
|
+
const decoder = new TextDecoder();
|
|
79
|
+
const appendExecution = (execution) => {
|
|
80
|
+
stdout += typeof execution.stdout === "string" ? execution.stdout : decoder.decode(execution.stdout);
|
|
81
|
+
stderr += typeof execution.stderr === "string" ? execution.stderr : decoder.decode(execution.stderr);
|
|
82
|
+
};
|
|
83
|
+
const matches = [];
|
|
84
|
+
const emit = (shown) => { stdout += shown + (print0 ? "\0" : "\n"); };
|
|
85
|
+
const visit = async (native, shown, depth) => {
|
|
86
|
+
let stats;
|
|
87
|
+
try { stats = lstatSync(native); }
|
|
88
|
+
catch (error) { status = 1; stderr += `bunmsh: find: ${shown}: ${error.message}\n`; return; }
|
|
89
|
+
const entry = { shown, stats };
|
|
90
|
+
if (depth >= minDepth && predicates.every((predicate) => predicate(entry))) {
|
|
91
|
+
matches.push(shown);
|
|
92
|
+
if (!actions.length || explicitPrint) emit(shown);
|
|
93
|
+
for (const action of actions) if (action.mode === ";") {
|
|
94
|
+
if (!execute) return;
|
|
95
|
+
const command = action.command.map((argument) => argument === "{}" ? shown : argument);
|
|
96
|
+
appendExecution(await execute(command));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!stats.isDirectory() || depth >= maxDepth) return;
|
|
100
|
+
let children;
|
|
101
|
+
try { children = readdirSync(native).sort(); }
|
|
102
|
+
catch (error) { status = 1; stderr += `bunmsh: find: ${shown}: ${error.message}\n`; return; }
|
|
103
|
+
for (const child of children)
|
|
104
|
+
await visit(resolve(native, child), shown === "." ? `./${child}` : `${shown.replace(/\/$/, "")}/${child}`, depth + 1);
|
|
105
|
+
};
|
|
106
|
+
for (const root of roots) {
|
|
107
|
+
const native = isAbsolute(nativePath(root, platform))
|
|
108
|
+
? nativePath(root, platform) : resolve(nativePath(state.cwd, platform), nativePath(root, platform));
|
|
109
|
+
await visit(native, shellPath(root), 0);
|
|
110
|
+
}
|
|
111
|
+
for (const action of actions) if (action.mode === "+" && matches.length) {
|
|
112
|
+
if (!execute) continue;
|
|
113
|
+
const command = action.command.flatMap((argument) => argument === "{}" ? matches : [argument]);
|
|
114
|
+
const execution = await execute(command);
|
|
115
|
+
appendExecution(execution);
|
|
116
|
+
if (execution.status !== 0) status = 1;
|
|
117
|
+
}
|
|
118
|
+
return { status, stdout, stderr };
|
|
119
|
+
}
|