New utils

This commit is contained in:
Raven Scott
2026-04-03 23:04:42 -04:00
parent eeef3da6e1
commit f42f50eb73
214 changed files with 17562 additions and 615 deletions
+1 -1
View File
@@ -76,4 +76,4 @@ npm test -w bare-os-booter
- [Developer guide — Bare modules](../../developer-guide/12-bare-modules-and-pear-ecosystem.md)
- [Handbook — Booter runtime](../../handbook/04-the-booter-runtime.md)
- [DOCUMENTATION.md](../../DOCUMENTATION.md) §12 (detailed; some lists may lag code)
- [DOCUMENTATION.md](../../DOCUMENTATION.md) §12 **`packages/bare-os-coreutils/lib/commands.mjs`** is the authoritative **`/bin`** name list
+6 -1
View File
@@ -293,7 +293,12 @@ async function executeKernel(disk, store, swarm, initSource) {
'BARE_OS_HTTP_DENYLIST',
'BARE_OS_TLS_PIN_SHA256',
'BARE_OS_BARE_MODULES',
'BARE_OS_BARE_DRIVE_BUNDLES'
'BARE_OS_BARE_DRIVE_BUNDLES',
'BARE_OS_FIND_EXEC_MAX',
'BARE_OS_YES_MAX_LINES',
'BARE_OS_SHUF_MAX_LINES',
'BARE_OS_SPLIT_MAX_FILES',
'BARE_OS_NPROC'
]) {
const v = hostEnv[k]
if (v != null && v !== '') shellEnv[k] = v
+333 -11
View File
@@ -642,7 +642,9 @@ test('ls --color=always colors directory blue and executable green', async (t) =
await ctx.vfs.writeFile('xfile', b4a.from(''))
await ctx.vfs.chmod('xfile', 0o755)
await runBinCommand(ctx, ['ls', '--color=always'])
const shortLine = lines.find((l) => l.includes('subdir') && l.includes('xfile'))
const shortLine = lines.find(
(l) => l.includes('subdir') && l.includes('xfile')
)
t.ok(shortLine)
t.ok(
/\x1b\[[0-9;]*msubdir\x1b\[0m/.test(shortLine),
@@ -1233,7 +1235,11 @@ test('buildBareCtxObjectFromHost loads core keys on Node', async (t) => {
})
test('maybeMergeBareFromDrive fills missing keys from bundle (mock vfs)', async (t) => {
const repoRoot = path.join(fileURLToPath(new URL('.', import.meta.url)), '..', '..')
const repoRoot = path.join(
fileURLToPath(new URL('.', import.meta.url)),
'..',
'..'
)
const bundleAbs = path.join(repoRoot, 'kernel/lib/bare/bundles/b4a.js')
const bundleSrc = await readFile(bundleAbs)
const manifest = {
@@ -1243,7 +1249,8 @@ test('maybeMergeBareFromDrive fills missing keys from bundle (mock vfs)', async
const target = {}
const vfs = {
async readFile(p) {
if (p === '/lib/bare/manifest.json') return b4a.from(JSON.stringify(manifest))
if (p === '/lib/bare/manifest.json')
return b4a.from(JSON.stringify(manifest))
if (p === '/lib/bare/bundles/b4a.js') return new Uint8Array(bundleSrc)
return null
}
@@ -1273,7 +1280,10 @@ test('defaultShellAliases does not remap sed', async (t) => {
test('defaultShellAliases nano maps to edit', async (t) => {
t.is(defaultShellAliases().nano, 'edit')
t.alike(expandArgvAliases(['nano', 'x'], defaultShellAliases()), ['edit', 'x'])
t.alike(expandArgvAliases(['nano', 'x'], defaultShellAliases()), [
'edit',
'x'
])
})
test('expandArgvAliases throws on cyclic alias chain', async (t) => {
@@ -1422,7 +1432,9 @@ test('bareLsColorOpenSgrFromMap ca when stat has capabilities', async (t) => {
})
test('applyBareOsThemeFromEnv BARE_OS_COLOR_DEPTH=256 drops truecolor', async (t) => {
const ctx = { vfs: { env: { BARE_OS_THEME: 'nord', BARE_OS_COLOR_DEPTH: '256' } } }
const ctx = {
vfs: { env: { BARE_OS_THEME: 'nord', BARE_OS_COLOR_DEPTH: '256' } }
}
await applyBareOsThemeFromEnv(ctx)
const p = String(ctx.vfs.env.BARE_OS_COLOR_PROMPT || '')
t.ok(p.includes('38;5;'), '256-color palette index SGR')
@@ -1436,8 +1448,7 @@ test('dircolors -p includes TERM and di', async (t) => {
})
test('bareParseDircolorsDatabase TERM block', async (t) => {
const text =
'TERM xterm\ndi 01;34\nTERM none\nfi 00\nTERM *\nln 01;36\n'
const text = 'TERM xterm\ndi 01;34\nTERM none\nfi 00\nTERM *\nln 01;36\n'
const m = bareParseDircolorsDatabase(text, 'xterm')
t.is(m.di, '01;34')
t.is(m.ln, '01;36')
@@ -1676,10 +1687,9 @@ test('ls bareOsStdoutCaptured lists one name per line', async (t) => {
await ctx.vfs.writeFile('aaa', b4a.from(''))
await ctx.vfs.writeFile('lib', b4a.from(''))
await ctx.vfs.writeFile('zzz', b4a.from(''))
await runBinCommand(
Object.assign({}, ctx, { bareOsStdoutCaptured: true }),
['ls']
)
await runBinCommand(Object.assign({}, ctx, { bareOsStdoutCaptured: true }), [
'ls'
])
t.is(lines.length, 3)
t.ok(lines.includes('aaa') && lines.includes('lib') && lines.includes('zzz'))
await store.close()
@@ -2636,6 +2646,12 @@ async function run(ctx, argv) {
t.is(ctx.exitCode, 0)
t.is(lines.pop(), '4096')
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['getconf', 'BARE_OS_YES_MAX_LINES'])
t.is(ctx.exitCode, 0)
t.is(lines.pop(), '100000')
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['getconf', 'NOT_A_REAL_CONF_NAME'])
@@ -3580,6 +3596,312 @@ test('runBinCommand man ls prints manual text', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('coreutils sed and awk fixture corpus', async (t) => {
const corpusPath = path.join(
__dirname,
'../bare-os-coreutils/fixtures/sed-awk-corpus.json'
)
const corpus = JSON.parse(await readFile(corpusPath, 'utf8'))
const dir = testCorestoreDir('sedawkcorpus')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('psac'))
await drive.ready()
await personal.ready()
await drive.put('/bin/sed', b4a.from(await readBuiltBin('sed')))
await drive.put('/bin/awk', b4a.from(await readBuiltBin('awk')))
const lines = []
const ctx = testCtx(drive, personal)
ctx.exitCode = 0
ctx.console = {
log(s) {
lines.push(String(s))
},
error(...a) {
lines.push(a.join(' '))
}
}
function corpusNormLines(logs) {
return logs
.flatMap((l) => String(l).split('\n'))
.filter((x) => x.length > 0)
}
for (const c of corpus.sed) {
await ctx.vfs.writeFile(c.file, b4a.from(c.content))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, c.argv)
t.is(ctx.exitCode, 0, 'sed ' + c.name)
t.alike(corpusNormLines(lines), c.lines, 'sed ' + c.name)
}
for (const c of corpus.awk) {
if (c.progFile) {
await ctx.vfs.writeFile(c.progFile, b4a.from(c.progContent))
}
if (c.file) await ctx.vfs.writeFile(c.file, b4a.from(c.content))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, c.argv)
t.is(ctx.exitCode, 0, 'awk ' + c.name)
t.alike(corpusNormLines(lines), c.lines, 'awk ' + c.name)
}
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('coreutils matrix: cp find sort printf uniq realpath sha256sum base64 rm -d', async (t) => {
const dir = testCorestoreDir('corematrix')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pcm'))
await drive.ready()
await personal.ready()
const bins = [
'cp',
'find',
'sort',
'printf',
'uniq',
'realpath',
'sha256sum',
'base64',
'rm',
'mkdir',
'touch'
]
for (const b of bins) {
await drive.put('/bin/' + b, b4a.from(await readBuiltBin(b)))
}
await drive.put(
'/bin/_corpus_echo',
b4a.from(`async function run(ctx, argv) {
ctx.console.log(argv.slice(1).join(' '))
}
`)
)
const lines = []
const ctx = testCtx(drive, personal)
ctx.runBinCommand = (argv) => runBinCommand(ctx, argv)
ctx.exitCode = 0
ctx.console = {
log(s) {
lines.push(String(s))
},
error(...a) {
lines.push(a.join(' '))
}
}
await ctx.vfs.mkdir('tree', { recursive: true })
await ctx.vfs.writeFile('tree/a.txt', b4a.from('newdata'))
await ctx.vfs.writeFile('dest.txt', b4a.from('stale'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['touch', '-d', '@1', 'dest.txt'])
t.is(ctx.exitCode, 0)
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['touch', '-d', '@100000', 'tree/a.txt'])
t.is(ctx.exitCode, 0)
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['cp', '-u', 'tree/a.txt', 'dest.txt'])
t.is(ctx.exitCode, 0, 'cp -u')
t.is(ctx.b4a.toString(await ctx.vfs.readFile('dest.txt')), 'newdata')
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['find', '.', '-type', 'f', '-regex', '.*\\.txt$'])
t.is(ctx.exitCode, 0, 'find -regex')
t.ok(lines.some((l) => String(l).includes('a.txt')))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['find', 'tree', '-exec', '_corpus_echo', '{}', ';'])
t.is(ctx.exitCode, 0, 'find -exec')
t.ok(lines.some((l) => /a\.txt/.test(String(l))))
lines.length = 0
ctx.exitCode = 0
ctx.shellStdin = '3\n1\n2\n'
await runBinCommand(ctx, ['sort', '-n'])
t.is(lines.join('\n'), '1\n2\n3')
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['printf', '%s-%d', 'n', '7'])
t.is(lines[0], 'n-7')
await ctx.vfs.writeFile('u.txt', b4a.from('a\na\nb\n'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['uniq', '-c', 'u.txt'])
t.ok(lines.some((l) => /^\s*2\s+a$/.test(String(l))))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['realpath', 'tree/a.txt'])
t.ok(/a\.txt$/.test(lines[0]))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['sha256sum', 'u.txt'])
t.is(lines.length, 1)
t.ok(/^[a-f0-9]{64}\s+/.test(lines[0]))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['base64', '-w', '0', 'u.txt'])
const dec = Buffer.from(
lines.join('').replace(/\s+/g, ''),
'base64'
).toString()
t.is(dec, 'a\na\nb\n')
await ctx.vfs.mkdir('emptydir', { recursive: true })
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['rm', '-d', 'emptydir'])
t.is(ctx.exitCode, 0, 'rm -d')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('coreutils gnu-gap batch: paste tac rev md5sum expr tsort numfmt truncate install comm join', async (t) => {
const dir = testCorestoreDir('gnugap')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('ngg'))
await drive.ready()
await personal.ready()
const bins = [
'paste',
'tac',
'rev',
'md5sum',
'expr',
'tsort',
'numfmt',
'sync',
'truncate',
'install',
'unlink',
'comm',
'join',
'yes',
'cp',
'touch',
'mkdir',
'ls'
]
for (const b of bins) {
await drive.put('/bin/' + b, b4a.from(await readBuiltBin(b)))
}
const lines = []
const ctx = testCtx(drive, personal)
ctx.runBinCommand = (argv) => runBinCommand(ctx, argv)
ctx.exitCode = 0
ctx.console = {
log(s) {
lines.push(String(s))
},
error(...a) {
lines.push(a.join(' '))
}
}
ctx.vfs.env.BARE_OS_YES_MAX_LINES = '2'
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['yes', 'ok'])
t.is(ctx.exitCode, 0)
t.is(lines.filter((l) => l === 'ok').length, 2)
await ctx.vfs.writeFile('nums.txt', b4a.from('1\n2\n3\n'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['tac', 'nums.txt'])
t.is(lines.join('\n'), '3\n2\n1')
await ctx.vfs.writeFile('rv.txt', b4a.from('ab\ncd\n'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['rev', 'rv.txt'])
t.is(lines.join('\n'), 'ba\ndc')
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['md5sum', 'nums.txt'])
t.ok(/^[a-f0-9]{32}\s/.test(lines[0]))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['expr', '1', '+', '2', '*', '3'])
t.is(lines[0], '7')
await ctx.vfs.writeFile('ts.txt', b4a.from('a b\nb c\n'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['tsort', 'ts.txt'])
t.is(lines.join('\n'), 'a\nb\nc')
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['numfmt', '--to=iec', '1024'])
t.is(lines[0], '1K')
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['sync'])
t.is(ctx.exitCode, 0)
await ctx.vfs.writeFile('tr.txt', b4a.from('hello'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['truncate', '-s', '2', 'tr.txt'])
t.is(ctx.exitCode, 0)
t.is(ctx.b4a.toString(await ctx.vfs.readFile('tr.txt')), 'he')
await ctx.vfs.writeFile('src.txt', b4a.from('data'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['install', '-m', '600', 'src.txt', 'dest.txt'])
t.is(ctx.exitCode, 0)
t.is(ctx.b4a.toString(await ctx.vfs.readFile('dest.txt')), 'data')
await ctx.vfs.writeFile('s1.txt', b4a.from('a\nb\n'))
await ctx.vfs.writeFile('s2.txt', b4a.from('a\nc\n'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['comm', 's1.txt', 's2.txt'])
t.ok(lines.some((l) => l.includes('\t\ta')))
await ctx.vfs.writeFile('j1.txt', b4a.from('1 x\n2 y\n'))
await ctx.vfs.writeFile('j2.txt', b4a.from('1 p\n2 q\n'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['join', 'j1.txt', 'j2.txt'])
t.ok(lines.some((l) => /1\s+x\s+1\s+p/.test(String(l))))
await ctx.vfs.writeFile('u.txt', b4a.from('x'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['unlink', 'u.txt'])
t.is(ctx.exitCode, 0)
t.is(await ctx.vfs.readFile('u.txt'), null)
await ctx.vfs.writeFile('p1.txt', b4a.from('a\nb\n'))
await ctx.vfs.writeFile('p2.txt', b4a.from('1\n2\n'))
lines.length = 0
ctx.exitCode = 0
await runBinCommand(ctx, ['paste', 'p1.txt', 'p2.txt'])
t.is(lines[0], 'a\t1')
t.is(lines[1], 'b\t2')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
async function readBuiltBin(name) {
const fs = await import('node:fs/promises')
const p = path.join(__dirname, '../../kernel/bin', name)
+5 -3
View File
@@ -1,6 +1,6 @@
# bare-os-coreutils
**Build step**, not a runtime library: validates and merges **`man/pages/*.json`** into **`kernel/share/man/man.json`** (see **`scripts/build-man-db.mjs`**), then concatenates **`lib/runtime.js`**, optional **`lib/*-engine.js`** or **`lib/man-render.js`** chunks (see **`preamble`** in **`build.mjs`** **`sed`**, **`awk`**, **`man`**), then each **`src/<command>.js`**, and writes standalone scripts to:
**Build step**, not a runtime library: validates and merges **`man/pages/*.json`** into **`kernel/share/man/man.json`** (see **`scripts/build-man-db.mjs`**), then concatenates **`lib/runtime.js`**, optional preamble chunks (see **`preamble`** in **`build.mjs`** — e.g. **`md5sum`** **`lib/md5.js`**, **`sed`**, **`awk`**, **`jq`**, **`man`**, lscolors, **`edit`**/**`nano`** TUI), then each **`src/<command>.js`**, and writes standalone scripts to:
- `kernel/bin/<command>` — staged into the **system** Hyperdrive as `/bin/*`
- `packages/bare-os-seeder/kernel/bin/<command>`**vendored** copy for Pear bundles (seeder has no sibling `bare-os-coreutils` at runtime)
@@ -33,7 +33,7 @@ Or `node packages/bare-os-coreutils/build.mjs`.
**Source of truth:** **`lib/commands.mjs`** — **`COREUTILS_COMMANDS`** (imported by **`build.mjs`** and **`scripts/build-man-db.mjs`**). Each name must have **`man/pages/<name>.json`**.
`awk`, `basename`, `cat`, `chgrp`, `chmod`, `chown`, `cksum`, `clear`, `cp`, `crontab`, `cut`, `date`, `dirname`, `dircolors`, `du`, `edit`, `echo`, `env`, `exit`, `false`, `find`, `getconf`, `git-pear`, `grep`, `head`, `hdms`, `help`, `hostname`, `id`, `jq`, `ln`, `login`, `logout`, `logname`, `ls`, `man`, `mkdir`, `mkfifo`, `mktemp`, `mv`, `nano`, `nl`, `od`, `pathchk`, `printenv`, `printf`, `pwd`, `readlink`, `rm`, `rmdir`, `savevault`, `sed`, `seq`, `sleep`, `sort`, `stat`, `tail`, `tee`, `test`, `theme`, `time`, `touch`, `tr`, `true`, `tty`, `uname`, `wc`, `which`, `whoami`, `xargs`
`arch`, `awk`, `base32`, `base64`, `basename`, `basenc`, `cat`, `chgrp`, `chmod`, `chown`, `cksum`, `clear`, `comm`, `cp`, `crontab`, `cut`, `date`, `df`, `dir`, `dirname`, `dircolors`, `du`, `edit`, `echo`, `env`, `exit`, `expand`, `expr`, `factor`, `false`, `find`, `fmt`, `fold`, `getconf`, `git-pear`, `grep`, `groups`, `head`, `hdms`, `help`, `hostid`, `hostname`, `id`, `install`, `join`, `jq`, `ln`, `login`, `logout`, `logname`, `ls`, `man`, `md5sum`, `mkdir`, `mkfifo`, `mktemp`, `mv`, `nano`, `nl`, `nproc`, `numfmt`, `od`, `paste`, `pathchk`, `pr`, `printenv`, `printf`, `pwd`, `readlink`, `realpath`, `rev`, `rm`, `rmdir`, `savevault`, `sed`, `seq`, `sha1sum`, `sha256sum`, `sha512sum`, `shuf`, `sleep`, `sort`, `split`, `stat`, `sum`, `sync`, `tac`, `tail`, `tee`, `test`, `theme`, `time`, `touch`, `tr`, `truncate`, `true`, `tsort`, `tty`, `uname`, `uniq`, `unlink`, `unexpand`, `uptime`, `users`, `vdir`, `wc`, `which`, `who`, `whoami`, `xargs`, `yes`
**`edit`** is a full-screen TTY buffer editor (syntax highlighting, search, save). **`nano`** is built from the same **`src/edit.js`** with the same **`lib/edit-*.js`** preamble; **`/bin/nano`** exists for familiarity, and the stock shell alias **`nano``edit`** routes **`nano`** to that utility (see **`packages/bare-os-booter/lib/shell.js`**). Both require a real TTY (**`stdout.isTTY`**).
@@ -47,7 +47,9 @@ Or `node packages/bare-os-coreutils/build.mjs`.
**Pipelines:** the shell sets **`ctx.bareOsStdoutCaptured`** when a commands stdout is captured (pipe or **`>`** / **`>>`**). **`ls`** prints **one name per line** in short mode in that case (GNU-like), so **`ls | grep`** / **`sort`** / **`wc`** see one record per line.
**Recent parity / UX:** **`tail -f`** (watch or poll; **`BARE_OS_TAIL_F_*`** env), **`head`/`tail -c`** and **`+` line/byte offsets**, **`sort -n/-r/-u/-f/-k/-t`**, **`sort -c/-C`** (check), **`-s`** (stable), **`-o`**, **`wc -l/-w/-c`**, **`grep -A/-B/-C`** and **`--color`**, **`date +FORMAT`** (strftime-like subset), **`test`** integer compares and **`-h`/`-L`**, **`xargs -I`**, **`find -iname`/`-print0`**, **`du -h`**, **`basename -a`/`-s`**, **`dirname -z`**. Phase 2 adds **`cat -n/-A`**, **`env -i`**, **`touch -a/-m/-d/-r`**, **`mkdir -m`**, **`grep -r`**, **`cp`/`mv` `-L`/`-P`**, **`readlink -f`**, **`rmdir -p`**, **`du -a/-L`**, **`find -mtime`/`-newer`/`-prune`/`-empty`/`-delete`** (gated), **`stat --format`**, **`cut -s`**, **`tr [:class:]`**, richer **`od`/`nl`/`seq`/`pathchk`/`printf`/`time`**, and **`bareOsBinWrite`** for captured NUL output in tests.
**GNU-style text / data utilities (bounded where needed):** **`paste`**, **`split`** (**`BARE_OS_SPLIT_MAX_FILES`**), **`tac`**, **`rev`**, **`expand`**, **`unexpand`**, **`fold`**, **`fmt`**, **`comm`**, **`join`**, **`pr`**, **`yes`** (**`BARE_OS_YES_MAX_LINES`**), **`shuf`** (**`BARE_OS_SHUF_MAX_LINES`**), **`tsort`**, **`factor`**, **`expr`**, **`numfmt`**. **Checksums / encodings:** **`md5sum`**, **`sha1sum`**, **`sha256sum`**, **`sha512sum`**, **`sum`**, **`base32`**, **`basenc --base16`**. **Files / stubs:** **`truncate`**, **`unlink`**, **`install`**, **`df`**, **`sync`**, **`dir`**/**`vdir`** (delegate to **`ls`**), **`arch`**, **`groups`**, **`hostid`**, **`nproc`**, **`uptime`**, **`users`**, **`who`**.
**Earlier parity / UX:** **`tail -f`** (watch or poll; **`BARE_OS_TAIL_F_*`** env), **`head`/`tail -c`** and **`+` line/byte offsets**, **`sort -n/-r/-u/-f/-k/-t`**, **`sort -c/-C`** (check), **`-s`** (stable), **`-o`**, **`wc -l/-w/-c`**, **`grep -A/-B/-C`** and **`--color`**, **`date +FORMAT`** (strftime-like subset), **`test`** integer compares and **`-h`/`-L`**, **`xargs -I`**, **`find -iname`/`-print0`**, **`du -h`**, **`basename -a`/`-s`**, **`dirname -z`**, **`cat -n/-A`**, **`env -i`**, **`touch -a/-m/-d/-r`**, **`mkdir -m`**, **`grep -r`**, **`cp`/`mv` `-L`/`-P`**, **`readlink -f`**, **`rmdir -p`**, **`du -a/-L`**, **`find -mtime`/`-newer`/`-prune`/`-empty`/`-delete`** (gated), **`stat --format`**, **`uniq`**, **`realpath`**, **`base64`**, **`rm -d`**, **`cut -s`**, **`tr [:class:]`**, richer **`od`/`nl`/`seq`/`pathchk`/`printf`/`time`**, and **`bareOsBinWrite`** for captured NUL output in tests. Cap names for **`getconf`** include **`BARE_OS_FIND_EXEC_MAX`**, **`BARE_OS_YES_MAX_LINES`**, **`BARE_OS_SHUF_MAX_LINES`**, **`BARE_OS_SPLIT_MAX_FILES`**.
See [handbook §6 — Kernel and `/bin`](../../handbook/06-kernel-and-binaries.md), [handbook §9 — POSIX alignment](../../handbook/09-posix-utilities-shell-and-vfs.md), and [handbook §10 — `man` and online help](../../handbook/10-manpages-and-online-help.md).
+13 -3
View File
@@ -2,7 +2,7 @@ import { readFile, writeFile, mkdir } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
import { COREUTILS_COMMANDS } from './lib/commands.mjs'
import { COREUTILS_COMMANDS, HELP_DELEGATED_COMMANDS } from './lib/commands.mjs'
import { buildManDb } from './scripts/build-man-db.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -12,6 +12,7 @@ const seederKernelBin = join(repoRoot, 'packages/bare-os-seeder/kernel/bin')
/** Commands whose /bin script is preceded by extra library sources (no import in src). */
const preamble = {
md5sum: ['md5.js'],
sed: ['sed-engine.js'],
awk: ['awk-engine.js'],
jq: ['jq-engine.js'],
@@ -64,12 +65,21 @@ export async function build() {
? join(repoRoot, 'packages/bare-os-lscolors/bare-os-lscolors.js')
: join(__dirname, 'lib', f)
let chunk = await readFile(chunkPath, 'utf8')
if (f === 'bare-os-lscolors.js') chunk = stripLscolorsBundleExport(chunk)
if (f === 'bare-os-lscolors.js')
chunk = stripLscolorsBundleExport(chunk)
pre += chunk + '\n'
}
}
const srcName = commandSourceFile(name)
const body = await readFile(join(__dirname, 'src', `${srcName}.js`), 'utf8')
let body = await readFile(join(__dirname, 'src', `${srcName}.js`), 'utf8')
if (name === 'help') {
const helpBins = [...commands, ...HELP_DELEGATED_COMMANDS].sort()
body =
'var BARE_OS_HELP_BIN_SPACED = ' +
JSON.stringify(helpBins.join(' ')) +
'\n' +
body
}
const out = runtime + '\n' + pre + body
await writeFile(join(kernelBin, name), out)
await writeFile(join(seederKernelBin, name), out)
@@ -0,0 +1,38 @@
{
"schemaVersion": 1,
"description": "Regression snippets for Bare OS sed/awk engines; not a full POSIX conformance suite.",
"sed": [
{
"name": "substitute_first_per_line",
"file": "_corpus_sed1.txt",
"content": "foo bar\nbaz foo\n",
"argv": ["sed", "-e", "s/foo/FOO/", "_corpus_sed1.txt"],
"lines": ["FOO bar", "baz FOO"]
},
{
"name": "delete_matching_lines",
"file": "_corpus_sed2.txt",
"content": "keep\ndrop\nkeep2\n",
"argv": ["sed", "-e", "/drop/d", "_corpus_sed2.txt"],
"lines": ["keep", "keep2"]
}
],
"awk": [
{
"name": "begin_print_literal",
"argv": ["awk", "BEGIN{print 7}"],
"lines": ["7"]
},
{
"name": "begin_end_inline",
"file": "_corpus_awk2.txt",
"content": "x\n",
"argv": [
"awk",
"BEGIN{print \"start\"}END{print \"end\"}",
"_corpus_awk2.txt"
],
"lines": ["start", "end"]
}
]
}
+54 -1
View File
@@ -3,18 +3,25 @@
* Keep sorted alphabetically.
*/
export const COREUTILS_COMMANDS = [
'arch',
'awk',
'base32',
'base64',
'basename',
'basenc',
'cat',
'chgrp',
'chmod',
'chown',
'cksum',
'clear',
'comm',
'cp',
'crontab',
'cut',
'date',
'df',
'dir',
'dirname',
'dircolors',
'du',
@@ -22,16 +29,25 @@ export const COREUTILS_COMMANDS = [
'echo',
'env',
'exit',
'expand',
'expr',
'factor',
'false',
'find',
'fmt',
'fold',
'getconf',
'git-pear',
'grep',
'groups',
'head',
'hdms',
'help',
'hostid',
'hostname',
'id',
'install',
'join',
'jq',
'ln',
'login',
@@ -39,26 +55,41 @@ export const COREUTILS_COMMANDS = [
'logname',
'ls',
'man',
'md5sum',
'mkdir',
'mkfifo',
'mktemp',
'mv',
'nano',
'nl',
'nproc',
'numfmt',
'od',
'paste',
'pathchk',
'pr',
'printenv',
'printf',
'pwd',
'readlink',
'realpath',
'rev',
'rm',
'rmdir',
'savevault',
'sed',
'seq',
'sha1sum',
'sha256sum',
'sha512sum',
'shuf',
'sleep',
'sort',
'split',
'stat',
'sum',
'sync',
'tac',
'tail',
'tee',
'test',
@@ -66,13 +97,23 @@ export const COREUTILS_COMMANDS = [
'time',
'touch',
'tr',
'truncate',
'true',
'tsort',
'tty',
'uname',
'uniq',
'unlink',
'unexpand',
'uptime',
'users',
'vdir',
'wc',
'which',
'who',
'whoami',
'xargs'
'xargs',
'yes'
]
/** Extra manual pages not built as /bin scripts on the system drive. */
@@ -84,3 +125,15 @@ export const MAN_EXTRA_PAGES = [
'git',
'wget'
]
/**
* Booter-delegated or stub /bin names not in COREUTILS_COMMANDS; merged into `help` output (sorted).
* Keep sorted; see build.mjs.
*/
export const HELP_DELEGATED_COMMANDS = [
'curl',
'git',
'journalctl',
'systemctl',
'wget'
]
+69
View File
@@ -0,0 +1,69 @@
/** RFC 1321 MD5 — no Web Crypto; used by md5sum. */
function bareMd5DigestBytes(u8) {
const n = u8.length
const bitLen = (BigInt(n) * 8n) & 0xffffffffffffffffn
const padLen = (56 - ((n + 1) % 64) + 64) % 64
const total = n + 1 + padLen + 8
const buf = new Uint8Array(total)
buf.set(u8)
buf[n] = 0x80
const view = new DataView(buf.buffer)
view.setUint32(total - 8, Number(bitLen & 0xffffffffn), true)
view.setUint32(total - 4, Number((bitLen >> 32n) & 0xffffffffn), true)
let a0 = 0x67452301
let b0 = 0xefcdab89
let c0 = 0x98badcfe
let d0 = 0x10325476
const s = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9,
14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
]
const K = new Uint32Array(64)
for (let i = 0; i < 64; i++) K[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 0x100000000) >>> 0
const leftRotate = (x, c) => ((x << c) | (x >>> (32 - c))) >>> 0
for (let off = 0; off < total; off += 64) {
const M = new Uint32Array(16)
for (let i = 0; i < 16; i++) {
M[i] = view.getUint32(off + i * 4, true)
}
let A = a0
let B = b0
let C = c0
let D = d0
for (let i = 0; i < 64; i++) {
let F, g
if (i < 16) {
F = (B & C) | (~B & D)
g = i
} else if (i < 32) {
F = (D & B) | (~D & C)
g = (5 * i + 1) % 16
} else if (i < 48) {
F = B ^ C ^ D
g = (3 * i + 5) % 16
} else {
F = C ^ (B | ~D)
g = (7 * i) % 16
}
F = (F + A + K[i] + M[g]) >>> 0
A = D
D = C
C = B
B = (B + leftRotate(F, s[i])) >>> 0
}
a0 = (a0 + A) >>> 0
b0 = (b0 + B) >>> 0
c0 = (c0 + C) >>> 0
d0 = (d0 + D) >>> 0
}
const out = new Uint8Array(16)
const dv = new DataView(out.buffer)
dv.setUint32(0, a0, true)
dv.setUint32(4, b0, true)
dv.setUint32(8, c0, true)
dv.setUint32(12, d0, true)
return [...out].map((b) => b.toString(16).padStart(2, '0')).join('')
}
@@ -0,0 +1,27 @@
{
"name": "arch",
"section": 1,
"title": "print machine hardware name",
"synopsis": [
"arch"
],
"description": "Prints PROCESSOR_ARCHITECTURE or BARE_OS_ARCH (session stub).",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"arch",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "arch"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "base32",
"section": 1,
"title": "encode or decode base32",
"synopsis": [
"base32 [-d] [FILE]"
],
"description": "RFC 4648 Base32; decode emits raw bytes when stdout supports binary.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"base32",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "base32"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,21 @@
{
"name": "base64",
"section": 1,
"title": "encode or decode base64",
"synopsis": ["base64 [OPTION]... [FILE]", "base64 -d [OPTION]... [FILE]"],
"description": "Encodes binary input to Base64, or decodes Base64 to raw bytes. Default reads stdin or FILE; decode output uses raw writes when available (process.stdout.write or ctx.bareOsBinWrite).",
"options": [
{ "flag": "-d, --decode", "meaning": "Decode incoming Base64" },
{
"flag": "-w COLS, --wrap",
"meaning": "Wrap encoded lines at COLS (0 = no wrap)"
},
{ "flag": "-h, --help", "meaning": "Print usage" }
],
"keywords": ["base64", "bare-os", "coreutils"],
"examples": [
{ "caption": "encode a file", "code": "base64 < binary.bin > text.b64" },
{ "caption": "decode", "code": "base64 -d text.b64 > out.bin" }
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "basenc",
"section": 1,
"title": "encode or decode with alphabet",
"synopsis": [
"basenc --base16 [-d] [FILE]"
],
"description": "Hex (base16) encode/decode only; other alphabets not implemented.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"basenc",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "basenc"
}
],
"listCategory": "coreutils"
}
+23 -10
View File
@@ -2,17 +2,30 @@
"name": "chgrp",
"section": 1,
"title": "change file group ownership",
"synopsis": ["chgrp [OPTION]... [OPERAND]..."],
"description": "Changing group ownership is not supported on Bare OS: Hyperdrive metadata is single-session oriented.",
"options": [],
"keywords": ["chgrp", "bare-os", "coreutils", "stub"],
"stub": true,
"diagnostics": ["chgrp: changing group is not supported on Bare OS"],
"bareOsNotes": "Single-user identity; gid fields exist for display only.",
"synopsis": ["chgrp [-h] GROUP FILE..."],
"description": "Sets the group id (and optional group name) in Hyperdrive metadata on writable paths. GROUP may be a numeric gid or the name root, guest, nobody, or the current session GROUP.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage and exit"
}
],
"keywords": ["chgrp", "bare-os", "coreutils", "metadata"],
"diagnostics": [
"chgrp: invalid group",
"chgrp: not supported by this VFS",
"chgrp: unsupported option"
],
"bareOsNotes": "Same writable scope as chmod; gid is stored for display and permission checks, not a real multi-user group database.",
"examples": [
{
"caption": "not supported — use identity model",
"code": "# chgrp is a stub; group is display metadata only"
"caption": "set group by numeric gid",
"code": "chgrp 1000 ~/data/file"
},
{
"caption": "set group to current session group name",
"code": "chgrp guest ~/tmp/file"
}
]
],
"listCategory": "coreutils"
}
+23 -10
View File
@@ -2,17 +2,30 @@
"name": "chown",
"section": 1,
"title": "change file owner and group",
"synopsis": ["chown [OPTION]... [OPERAND]..."],
"description": "Changing file owner is not supported on Bare OS (single-user Hyperdrive metadata).",
"options": [],
"keywords": ["chown", "bare-os", "coreutils", "stub"],
"stub": true,
"diagnostics": ["chown: changing owner is not supported on Bare OS"],
"bareOsNotes": "Use identity login/logout instead of POSIX ownership changes.",
"synopsis": ["chown [-h] OWNER[:GROUP] FILE...", "chown [-h] :GROUP FILE..."],
"description": "Updates uid/gid and optional user/group names in Hyperdrive metadata on writable paths (personal drive, /tmp, etc.). Not a multi-user host kernel: OWNER and GROUP are limited to numeric ids and the names root, guest, nobody, or the current session USER/GROUP.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage and exit"
}
],
"keywords": ["chown", "bare-os", "coreutils", "metadata"],
"diagnostics": [
"chown: invalid owner or group",
"chown: not supported by this VFS",
"chown: unsupported option"
],
"bareOsNotes": "Requires vfs.chown. Identity is still single-session; use login/logout for Ed25519-backed identity, not arbitrary POSIX users.",
"examples": [
{
"caption": "not supported",
"code": "# chown stub — see man identity / login"
"caption": "set numeric owner and group on a file under $HOME",
"code": "chown 1000:1000 ~/notes.txt"
},
{
"caption": "change group only (leading colon), keep owner",
"code": "chown :guest ~/shared.txt"
}
]
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "comm",
"section": 1,
"title": "compare two sorted files line by line",
"synopsis": [
"comm [-123] FILE1 FILE2"
],
"description": "Three columns: lines only in FILE1, only in FILE2, both. Suppress with -1, -2, -3.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"comm",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "comm"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,28 @@
{
"name": "df",
"section": 1,
"title": "report file system disk space usage",
"synopsis": [
"df [-h] [FILE]"
],
"description": "Synthetic Hyperdrive free space; not real block devices.",
"options": [
{
"flag": "-h, --human-readable",
"meaning": "Print sizes in powers of 1024 (K, M, …)"
},
{ "flag": "--help", "meaning": "Print usage" }
],
"keywords": [
"df",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "df"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "dir",
"section": 1,
"title": "list directory contents",
"synopsis": [
"dir [OPTION]... [FILE]..."
],
"description": "Delegates to ls -C.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"dir",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "dir"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "expand",
"section": 1,
"title": "convert tabs to spaces",
"synopsis": [
"expand [-t N] [FILE]..."
],
"description": "Uniform tab width (default 8).",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"expand",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "expand"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "expr",
"section": 1,
"title": "evaluate expressions",
"synopsis": [
"expr EXPRESSION"
],
"description": "Integer + - * / %, comparisons, string = and !=.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"expr",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "expr"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "factor",
"section": 1,
"title": "factor numbers",
"synopsis": [
"factor [NUMBER]..."
],
"description": "Prime factors by trial division; safe integers only.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"factor",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "factor"
}
],
"listCategory": "coreutils"
}
+21 -4
View File
@@ -3,11 +3,28 @@
"section": 1,
"title": "find files",
"synopsis": ["find [PATH...] [EXPRESSION]"],
"description": "Walks a directory tree with a small predicate set: -maxdepth, -mindepth, -name, -iname, -path, -type, -print0. Does not implement full POSIX find expression grammar.",
"description": "Walks a directory tree with predicates including -maxdepth, -mindepth, -name, -iname, -path, -regex (full path, JS RegExp), -type, -empty, -mtime, -newer, -prune, -delete (gated by BARE_OS_FIND_DELETE), -exec/-ok … \\; (requires ctx.runBinCommand; BARE_OS_FIND_EXEC_MAX, default 64; -ok needs BARE_OS_FIND_OK=1), and -print0. Not full POSIX find grammar.",
"options": [
{ "flag": "-name / -iname", "meaning": "Base name glob match (case-sensitive / case-insensitive)" },
{ "flag": "-print0", "meaning": "Separate paths with NUL (requires host stdout)" },
{ "flag": "-type f|d|l", "meaning": "Restrict to file, directory, or symlink" }
{
"flag": "-name / -iname",
"meaning": "Base name glob match (case-sensitive / case-insensitive)"
},
{
"flag": "-regex PAT",
"meaning": "Full path must match JavaScript RegExp PAT"
},
{
"flag": "-exec / -ok",
"meaning": "Run a utility with {} replaced by path; terminate with ;"
},
{
"flag": "-print0",
"meaning": "Separate paths with NUL (requires host stdout)"
},
{
"flag": "-type f|d|l",
"meaning": "Restrict to file, directory, or symlink"
}
],
"keywords": ["find", "directory", "walk", "search"],
"bareOsNotes": "Expression syntax is a simplified subset.",
@@ -0,0 +1,27 @@
{
"name": "fmt",
"section": 1,
"title": "simple text formatter",
"synopsis": [
"fmt [-w WIDTH] [FILE]..."
],
"description": "Reflow paragraphs (blank-line separated).",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"fmt",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "fmt"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "fold",
"section": 1,
"title": "wrap each input line",
"synopsis": [
"fold [-w WIDTH] [FILE]..."
],
"description": "Fixed-width wrap without word break.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"fold",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "fold"
}
],
"listCategory": "coreutils"
}
@@ -18,7 +18,13 @@
"bare-os",
"coreutils"
],
"bareOsNotes": "Subset only; unknown names fail with exit status 1. See src/getconf.js for the name table.",
"environment": [
"BARE_OS_YES_MAX_LINES — max lines yes prints (host passthrough overrides default)",
"BARE_OS_SHUF_MAX_LINES — max lines shuf holds in memory",
"BARE_OS_SPLIT_MAX_FILES — max output files split may create",
"BARE_OS_FIND_EXEC_MAX — max find -exec/-ok invocations per run (also in table)"
],
"bareOsNotes": "Subset only; unknown names fail with exit status 1. See src/getconf.js for the name table. BARE_OS_NPROC overrides /bin/nproc when passed from the host (see DOCUMENTATION.md §14).",
"examples": [
{
"caption": "path length limit",
@@ -0,0 +1,27 @@
{
"name": "groups",
"section": 1,
"title": "print group names",
"synopsis": [
"groups [USER]"
],
"description": "Prints supplemental groups from env or primary GROUP.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"groups",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "groups"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "hostid",
"section": 1,
"title": "print numeric host identifier",
"synopsis": [
"hostid"
],
"description": "Eight hex digits from HOSTID env or session hash.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"hostid",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "hostid"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "install",
"section": 1,
"title": "copy files and set attributes",
"synopsis": [
"install [-m MODE] SOURCE DEST"
],
"description": "Copy one file; optional chmod.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"install",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "install"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "join",
"section": 1,
"title": "join lines of two files on a common field",
"synopsis": [
"join [-t CHAR] [-1 N] [-2 N] FILE1 FILE2"
],
"description": "Relational join on sorted files.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"join",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "join"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "md5sum",
"section": 1,
"title": "compute MD5 checksums",
"synopsis": [
"md5sum [FILE]..."
],
"description": "Bundled MD5 (not Web Crypto); GNU-style output lines.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"md5sum",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "md5sum"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "nproc",
"section": 1,
"title": "print number of processing units",
"synopsis": [
"nproc [--all]"
],
"description": "Counts processor lines in /proc/cpuinfo or BARE_OS_NPROC.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"nproc",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "nproc"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "numfmt",
"section": 1,
"title": "convert numbers",
"synopsis": [
"numfmt [--to=iec|--to=si] [NUMBER]..."
],
"description": "Human-readable IEC (1024) or SI (1000) scales.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"numfmt",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "numfmt"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "paste",
"section": 1,
"title": "merge lines of files",
"synopsis": [
"paste [-d LIST] [-s] [FILE]..."
],
"description": "Parallel or serial (-s) column merge.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"paste",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "paste"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "pr",
"section": 1,
"title": "paginate or columnate",
"synopsis": [
"pr [-w WIDTH] [-n] [FILE]..."
],
"description": "Minimal column print and optional line numbers.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"pr",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "pr"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,19 @@
{
"name": "realpath",
"section": 1,
"title": "print resolved logical path",
"synopsis": ["realpath [-m] FILE..."],
"description": "Prints the VFS-resolved absolute path (same as the shells logical resolution). Does not traverse the host filesystem.",
"options": [
{
"flag": "-m, --canonicalize-missing",
"meaning": "Do not require the path to exist"
},
{ "flag": "-h, --help", "meaning": "Print usage" }
],
"keywords": ["realpath", "bare-os", "coreutils"],
"examples": [
{ "caption": "resolve under $HOME", "code": "realpath ./notes.txt" }
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "rev",
"section": 1,
"title": "reverse lines characterwise",
"synopsis": [
"rev [FILE]..."
],
"description": "Reverses each line's characters.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"rev",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "rev"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "sha1sum",
"section": 1,
"title": "compute SHA-1 checksums",
"synopsis": [
"sha1sum [FILE]..."
],
"description": "Uses Web Crypto SHA-1 when available.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"sha1sum",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "sha1sum"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,14 @@
{
"name": "sha256sum",
"section": 1,
"title": "compute SHA-256 checksums",
"synopsis": ["sha256sum [FILE]..."],
"description": "Prints SHA-256 hex digests in GNU-style lines (hash, two spaces, name). Uses Web Crypto globalThis.crypto.subtle when available. Reads stdin when no operands or when FILE is -.",
"options": [{ "flag": "-h, --help", "meaning": "Print usage" }],
"keywords": ["sha256sum", "checksum", "bare-os", "coreutils"],
"examples": [
{ "caption": "checksum files", "code": "sha256sum *.js" },
{ "caption": "stdin", "code": "cat f | sha256sum" }
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "sha512sum",
"section": 1,
"title": "compute SHA-512 checksums",
"synopsis": [
"sha512sum [FILE]..."
],
"description": "Uses Web Crypto SHA-512 when available.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"sha512sum",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "sha512sum"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "shuf",
"section": 1,
"title": "shuffle lines",
"synopsis": [
"shuf [FILE]..."
],
"description": "Shuffles in memory; capped by BARE_OS_SHUF_MAX_LINES.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"shuf",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "shuf"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "split",
"section": 1,
"title": "split a file into pieces",
"synopsis": [
"split [-l N] [-b N] [INPUT [PREFIX]]"
],
"description": "Line or byte chunks; output count capped.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"split",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "split"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "sum",
"section": 1,
"title": "checksum and count blocks",
"synopsis": [
"sum [-r] [FILE]..."
],
"description": "SysV default or BSD (-r) 16-bit checksum.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"sum",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "sum"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "sync",
"section": 1,
"title": "flush file system buffers",
"synopsis": [
"sync"
],
"description": "No-op success on Bare OS.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"sync",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "sync"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "tac",
"section": 1,
"title": "concatenate and print lines in reverse",
"synopsis": [
"tac [FILE]..."
],
"description": "Last line first; per-file order.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"tac",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "tac"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "truncate",
"section": 1,
"title": "shrink or extend file size",
"synopsis": [
"truncate -s SIZE FILE"
],
"description": "Absolute size only; pads with zeros.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"truncate",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "truncate"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "tsort",
"section": 1,
"title": "topological sort",
"synopsis": [
"tsort [FILE]"
],
"description": "Directed edges as pairs per line (A B).",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"tsort",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "tsort"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "unexpand",
"section": 1,
"title": "convert spaces to tabs",
"synopsis": [
"unexpand [-t N] [FILE]..."
],
"description": "Uniform tab width spacing to tabs.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"unexpand",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "unexpand"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,22 @@
{
"name": "uniq",
"section": 1,
"title": "report or filter adjacent duplicate lines",
"synopsis": ["uniq [-c] [-d] [-u] [INPUT]"],
"description": "Filters adjacent duplicate lines (input should be sorted for POSIX semantics). Optional OUTPUT operand is accepted for familiarity but ignored; use shell redirects.",
"options": [
{ "flag": "-c, --count", "meaning": "Prefix lines with occurrence counts" },
{
"flag": "-d, --repeated",
"meaning": "Only print duplicate lines (one per group)"
},
{ "flag": "-u, --unique", "meaning": "Only print lines that appear once" },
{ "flag": "-h, --help", "meaning": "Print usage" }
],
"keywords": ["uniq", "bare-os", "coreutils"],
"examples": [
{ "caption": "unique sorted lines", "code": "sort names.txt | uniq" },
{ "caption": "counts", "code": "sort log | uniq -c" }
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "unlink",
"section": 1,
"title": "remove a file",
"synopsis": [
"unlink FILE"
],
"description": "Single-file unlink.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"unlink",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "unlink"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "uptime",
"section": 1,
"title": "show uptime",
"synopsis": [
"uptime"
],
"description": "Uses /proc/uptime and /proc/loadavg when present.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"uptime",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "uptime"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "users",
"section": 1,
"title": "print login names",
"synopsis": [
"users"
],
"description": "Single-session user name.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"users",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "users"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "vdir",
"section": 1,
"title": "verbose directory listing",
"synopsis": [
"vdir [OPTION]... [FILE]..."
],
"description": "Delegates to ls -l.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"vdir",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "vdir"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "who",
"section": 1,
"title": "show who is logged on",
"synopsis": [
"who [OPTION]..."
],
"description": "Minimal session table from environment.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"who",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "who"
}
],
"listCategory": "coreutils"
}
@@ -0,0 +1,27 @@
{
"name": "yes",
"section": 1,
"title": "output a string repeatedly",
"synopsis": [
"yes [STRING]"
],
"description": "Prints until BARE_OS_YES_MAX_LINES cap.",
"options": [
{
"flag": "-h, --help",
"meaning": "Print usage"
}
],
"keywords": [
"yes",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "basic",
"code": "yes"
}
],
"listCategory": "coreutils"
}
+1 -1
View File
@@ -6,6 +6,6 @@
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./build.mjs",
"test": "node ./test/edit-key-parse.test.mjs"
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs"
}
}
+17
View File
@@ -0,0 +1,17 @@
async function run(ctx, argv) {
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-h' || argv[i] === '--help') {
ctx.console.log('usage: arch\nPrint machine hardware name (same idea as uname -m).')
return
}
if (argv[i].startsWith('-')) {
ctx.console.error('arch: unknown option ' + argv[i])
ctx.exitCode = 1
return
}
}
const e = ctx.vfs.env || {}
ctx.console.log(
e.PROCESSOR_ARCHITECTURE || e.MACHINE || e.BARE_OS_ARCH || 'unknown'
)
}
+111
View File
@@ -0,0 +1,111 @@
const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
function bareB32EncodeBytes(u8) {
let out = ''
let i = 0
let buf = 0
let bits = 0
for (; i < u8.length; i++) {
buf = (buf << 8) | u8[i]
bits += 8
while (bits >= 5) {
bits -= 5
out += B32[(buf >> bits) & 31]
}
}
if (bits > 0) out += B32[(buf << (5 - bits)) & 31]
while (out.length % 8 !== 0) out += '='
return out
}
function bareB32DecodeToU8(s) {
const t = String(s).replace(/\s+/g, '').replace(/=+$/, '')
let buf = 0
let bits = 0
const bytes = []
for (let i = 0; i < t.length; i++) {
const c = t[i]
const v = B32.indexOf(c)
if (v < 0) throw new Error('invalid base32 character')
buf = (buf << 5) | v
bits += 5
if (bits >= 8) {
bits -= 8
bytes.push((buf >> bits) & 255)
}
}
return new Uint8Array(bytes)
}
async function run(ctx, argv) {
let decode = false
let wrap = 0
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: base32 [-d] [-w COLS] [FILE]\n' +
'RFC 4648 Base32; -d decodes to raw bytes.'
)
ctx.exitCode = 0
return
}
if (a === '-d' || a === '--decode') {
decode = true
continue
}
if ((a === '-w' || a === '--wrap') && argv[i + 1]) {
wrap = Number.parseInt(argv[++i], 10)
if (!Number.isFinite(wrap) || wrap < 0) wrap = 0
continue
}
if (a.startsWith('-')) {
ctx.console.error('base32: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
let buf
if (!paths.length || paths[0] === '-') {
buf = b4.from(bareStdin(ctx))
} else {
const b = await ctx.vfs.readFile(paths[0])
if (!b) {
ctx.console.error('base32: cannot read ' + paths[0])
ctx.exitCode = 1
return
}
buf = b instanceof Uint8Array ? b : new Uint8Array(b)
}
if (decode) {
const text = ctx.b4a.toString(buf)
let raw
try {
raw = bareB32DecodeToU8(text)
} catch (e) {
ctx.console.error('base32: ' + (e.message || e))
ctx.exitCode = 1
return
}
if (!bareOsEmitRaw(ctx, raw)) {
ctx.console.error(
'base32: decode output requires process.stdout.write or ctx.bareOsBinWrite'
)
ctx.exitCode = 1
}
return
}
let enc = bareB32EncodeBytes(buf)
if (wrap > 0) {
const lines = []
for (let i = 0; i < enc.length; i += wrap) {
lines.push(enc.slice(i, i + wrap))
}
ctx.console.log(lines.join('\n'))
} else {
ctx.console.log(enc)
}
}
+121
View File
@@ -0,0 +1,121 @@
/**
* base64 — encode or decode Base64 (RFC 4648).
* Uses btoa/atob or Buffer when available; otherwise a small inline encoder.
*/
function bareB64EncodeBytes(u8) {
const B = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
let out = ''
let i = 0
for (; i + 2 < u8.length; i += 3) {
const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2]
out += B[(n >> 18) & 63] + B[(n >> 12) & 63] + B[(n >> 6) & 63] + B[n & 63]
}
const rest = u8.length - i
if (rest === 1) {
const n = u8[i] << 16
out += B[(n >> 18) & 63] + B[(n >> 12) & 63] + '=='
} else if (rest === 2) {
const n = (u8[i] << 16) | (u8[i + 1] << 8)
out += B[(n >> 18) & 63] + B[(n >> 12) & 63] + B[(n >> 6) & 63] + '='
}
return out
}
function bareB64DecodeToU8(s) {
const t = String(s).replace(/\s+/g, '')
if (typeof globalThis.Buffer !== 'undefined') {
return new Uint8Array(globalThis.Buffer.from(t, 'base64'))
}
if (typeof globalThis.atob === 'function') {
const bin = globalThis.atob(t)
const out = new Uint8Array(bin.length)
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i) & 255
return out
}
throw new Error('base64 decode requires Buffer or atob')
}
async function run(ctx, argv) {
let decode = false
let wrap = 76
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: base64 [-d] [-w COLS] [FILE]\n' +
' base64 --decode [-w COLS] [FILE]\n' +
'Default: encode stdin or FILE; -d/--decode decodes to raw bytes on stdout.'
)
ctx.exitCode = 0
return
}
if (a === '-d' || a === '--decode') {
decode = true
continue
}
if ((a === '-w' || a === '--wrap') && argv[i + 1]) {
wrap = Number.parseInt(argv[++i], 10)
if (!Number.isFinite(wrap) || wrap < 0) wrap = 0
continue
}
if (a.startsWith('-')) {
ctx.console.error('base64: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
let buf
if (!paths.length || paths[0] === '-') {
buf = b4.from(bareStdin(ctx))
} else {
const b = await ctx.vfs.readFile(paths[0])
if (!b) {
ctx.console.error('base64: cannot read ' + paths[0])
ctx.exitCode = 1
return
}
buf = b instanceof Uint8Array ? b : new Uint8Array(b)
}
if (decode) {
const text = new TextDecoder().decode(buf)
let raw
try {
raw = bareB64DecodeToU8(text)
} catch (e) {
ctx.console.error('base64: ' + (e.message || e))
ctx.exitCode = 1
return
}
if (!bareOsEmitRaw(ctx, raw)) {
ctx.console.error(
'base64: decode output requires process.stdout.write or ctx.bareOsBinWrite'
)
ctx.exitCode = 1
}
return
}
let enc
if (typeof globalThis.btoa === 'function') {
let s = ''
const step = 0x8000
for (let i = 0; i < buf.length; i += step) {
const chunk = buf.subarray(i, i + step)
s += String.fromCharCode.apply(null, chunk)
}
enc = globalThis.btoa(s)
} else {
enc = bareB64EncodeBytes(buf)
}
if (wrap > 0) {
const lines = []
for (let i = 0; i < enc.length; i += wrap) {
lines.push(enc.slice(i, i + wrap))
}
ctx.console.log(lines.join('\n'))
} else {
ctx.console.log(enc)
}
}
+86
View File
@@ -0,0 +1,86 @@
function bareHexEncode(u8) {
let s = ''
for (let i = 0; i < u8.length; i++) {
s += u8[i].toString(16).padStart(2, '0')
}
return s
}
function bareHexDecode(s) {
const t = String(s).replace(/\s+/g, '')
if (t.length % 2 !== 0) throw new Error('odd hex length')
const out = new Uint8Array(t.length / 2)
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(t.slice(i * 2, i * 2 + 2), 16)
if (!Number.isFinite(out[i])) throw new Error('invalid hex')
}
return out
}
async function run(ctx, argv) {
let decode = false
let base16 = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: basenc --base16 [-d] [FILE]\n' +
'Encode or decode hex (Base16). Other bases are not implemented.'
)
ctx.exitCode = 0
return
}
if (a === '-d' || a === '--decode') {
decode = true
continue
}
if (a === '--base16') {
base16 = true
continue
}
if (a.startsWith('-')) {
ctx.console.error('basenc: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (!base16) {
ctx.console.error('basenc: requires --base16')
ctx.exitCode = 1
return
}
const b4 = ctx.b4a
let buf
if (!paths.length || paths[0] === '-') {
buf = b4.from(bareStdin(ctx))
} else {
const b = await ctx.vfs.readFile(paths[0])
if (!b) {
ctx.console.error('basenc: cannot read ' + paths[0])
ctx.exitCode = 1
return
}
buf = b instanceof Uint8Array ? b : new Uint8Array(b)
}
if (decode) {
const text = b4.toString(buf)
let raw
try {
raw = bareHexDecode(text)
} catch (e) {
ctx.console.error('basenc: ' + (e.message || e))
ctx.exitCode = 1
return
}
if (!bareOsEmitRaw(ctx, raw)) {
ctx.console.error(
'basenc: decode output requires process.stdout.write or ctx.bareOsBinWrite'
)
ctx.exitCode = 1
}
return
}
ctx.console.log(bareHexEncode(buf))
}
+75
View File
@@ -0,0 +1,75 @@
async function readLines(ctx, path) {
const b4 = ctx.b4a
if (path === '-') return bareStdin(ctx).split('\n')
const b = await ctx.vfs.readFile(path)
if (!b) return null
return b4.toString(b).split('\n')
}
async function run(ctx, argv) {
let c1 = true
let c2 = true
let c3 = true
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: comm [-123] FILE1 FILE2\nCompare sorted files; columns: only FILE1, only FILE2, both.'
)
return
}
if (a === '-1') c1 = false
else if (a === '-2') c2 = false
else if (a === '-3') c3 = false
else if (a.startsWith('-')) {
ctx.console.error('comm: unsupported option ' + a)
ctx.exitCode = 1
return
} else paths.push(a)
}
if (paths.length !== 2) {
ctx.console.error('usage: comm [-123] FILE1 FILE2')
ctx.exitCode = 1
return
}
const A = await readLines(ctx, paths[0])
const B = await readLines(ctx, paths[1])
if (!A || !B) {
ctx.console.error('comm: missing input file')
ctx.exitCode = 1
return
}
const trimNl = (arr) => {
if (arr.length && arr[arr.length - 1] === '') arr.pop()
return arr
}
const a = trimNl(A.slice())
const b = trimNl(B.slice())
let i = 0
let j = 0
while (i < a.length || j < b.length) {
if (i >= a.length) {
if (c2) ctx.console.log('\t' + b[j])
j++
continue
}
if (j >= b.length) {
if (c1) ctx.console.log(a[i])
i++
continue
}
const cmp = a[i].localeCompare(b[j])
if (cmp < 0) {
if (c1) ctx.console.log(a[i])
i++
} else if (cmp > 0) {
if (c2) ctx.console.log('\t' + b[j])
j++
} else {
if (c3) ctx.console.log('\t\t' + a[i])
i++
j++
}
}
}
+61 -7
View File
@@ -1,4 +1,7 @@
async function copyPath(ctx, from, to, recursive, followSymlink) {
async function copyPath(ctx, from, to, recursive, followSymlink, opts) {
const preserve = opts && opts.preserveTime
const update = opts && opts.update
const verbose = opts && opts.verbose
const st = await ctx.vfs.lstat(from)
if (!st) {
ctx.console.error('cp: ' + from + ': No such file')
@@ -8,6 +11,7 @@ async function copyPath(ctx, from, to, recursive, followSymlink) {
if (!followSymlink) {
const t = await ctx.vfs.readlink(from)
await ctx.vfs.symlink(t, to)
if (verbose) ctx.console.log("'" + from + "' -> '" + to + "'")
return true
}
const fst = await ctx.vfs.stat(from)
@@ -17,7 +21,19 @@ async function copyPath(ctx, from, to, recursive, followSymlink) {
ctx.console.error('cp: cannot read ' + from)
return false
}
await ctx.vfs.writeFile(to, buf)
if (update) {
const dst = await ctx.vfs.lstat(to)
if (dst && dst.mtimeMs >= fst.mtimeMs) {
if (verbose) ctx.console.log('skipped: ' + to)
return true
}
}
const wopts =
preserve && typeof fst.mtimeMs === 'number'
? { mtimeMs: fst.mtimeMs, ctimeMs: fst.ctimeMs }
: {}
await ctx.vfs.writeFile(to, buf, wopts)
if (verbose) ctx.console.log("'" + from + "' -> '" + to + "'")
return true
}
if (fst.type === 'directory') {
@@ -31,8 +47,10 @@ async function copyPath(ctx, from, to, recursive, followSymlink) {
if (n === '.bareos_empty') continue
const f = from.replace(/\/+$/, '') + '/' + n
const t = to.replace(/\/+$/, '') + '/' + n
if (!(await copyPath(ctx, f, t, true, followSymlink))) return false
if (!(await copyPath(ctx, f, t, true, followSymlink, opts)))
return false
}
if (verbose) ctx.console.log("'" + from + "' -> '" + to + "'")
return true
}
ctx.console.error('cp: cannot copy special file ' + from)
@@ -44,7 +62,19 @@ async function copyPath(ctx, from, to, recursive, followSymlink) {
ctx.console.error('cp: cannot read ' + from)
return false
}
await ctx.vfs.writeFile(to, buf)
if (update) {
const dst = await ctx.vfs.lstat(to)
if (dst && dst.mtimeMs >= st.mtimeMs) {
if (verbose) ctx.console.log('skipped: ' + to)
return true
}
}
const wopts =
preserve && typeof st.mtimeMs === 'number'
? { mtimeMs: st.mtimeMs, ctimeMs: st.ctimeMs }
: {}
await ctx.vfs.writeFile(to, buf, wopts)
if (verbose) ctx.console.log("'" + from + "' -> '" + to + "'")
return true
}
if (st.type === 'directory') {
@@ -58,8 +88,9 @@ async function copyPath(ctx, from, to, recursive, followSymlink) {
if (n === '.bareos_empty') continue
const f = from.replace(/\/+$/, '') + '/' + n
const t = to.replace(/\/+$/, '') + '/' + n
if (!(await copyPath(ctx, f, t, true, followSymlink))) return false
if (!(await copyPath(ctx, f, t, true, followSymlink, opts))) return false
}
if (verbose) ctx.console.log("'" + from + "' -> '" + to + "'")
return true
}
return false
@@ -68,6 +99,9 @@ async function copyPath(ctx, from, to, recursive, followSymlink) {
async function run(ctx, argv) {
let recursive = false
let followSymlink = false
let update = false
let verbose = false
let preserveTime = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
@@ -83,6 +117,23 @@ async function run(ctx, argv) {
followSymlink = false
continue
}
if (a === '-u' || a === '--update') {
update = true
continue
}
if (a === '-v' || a === '--verbose') {
verbose = true
continue
}
if (a === '--preserve' || a === '-p') {
preserveTime = true
continue
}
if (a.startsWith('--preserve=')) {
const v = a.slice('--preserve='.length)
if (v === 'timestamps' || v === 'time' || v === 'all') preserveTime = true
continue
}
if (a === '--') {
paths.push(...argv.slice(i + 1))
break
@@ -95,7 +146,9 @@ async function run(ctx, argv) {
paths.push(a)
}
if (paths.length < 2) {
ctx.console.error('usage: cp [-R] [-L|-P] SOURCE... DEST')
ctx.console.error(
'usage: cp [-R] [-L|-P] [-u] [-v] [-p|--preserve[=timestamps]] SOURCE... DEST'
)
ctx.exitCode = 1
return
}
@@ -113,13 +166,14 @@ async function run(ctx, argv) {
ctx.exitCode = 1
return
}
const opts = { preserveTime, update, verbose }
for (const src of sources) {
const base = src.replace(/\/+$/, '').split('/').pop() || src
const target =
destIsDir || sources.length > 1
? dest.replace(/\/+$/, '') + '/' + base
: dest
if (!(await copyPath(ctx, src, target, recursive, followSymlink)))
if (!(await copyPath(ctx, src, target, recursive, followSymlink, opts)))
ctx.exitCode = 1
}
}
+60
View File
@@ -0,0 +1,60 @@
async function run(ctx, argv) {
let human = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '--help') {
ctx.console.log(
'usage: df [-h] [FILE]...\nSynthetic disk free for Bare OS (Hyperdrive model; not block devices).'
)
return
}
if (a === '-h' || a === '--human-readable') {
human = true
continue
}
if (a.startsWith('-')) {
ctx.console.error('df: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
let totalK = 1024 * 1024
let usedK = 4096
try {
const buf = await ctx.vfs.readFile('/proc/bare_os_quotas')
if (buf) {
const j = JSON.parse(ctx.b4a.toString(buf))
if (j && typeof j.pipelineMaxBytes === 'number')
totalK = Math.max(1024, Math.ceil(j.pipelineMaxBytes / 1024) * 1024)
}
} catch {
/* ignore */
}
const freeK = Math.max(0, totalK - usedK)
const pct =
totalK > 0 ? Math.min(100, Math.round((usedK / totalK) * 100)) : 0
function fmt(n) {
if (!human) return String(n)
if (n < 1024) return n + 'K'
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + 'M'
return (n / 1024 / 1024).toFixed(1) + 'G'
}
const mount = paths.length ? paths[0] : '/'
ctx.console.log(
'Filesystem 1K-blocks Used Available Use% Mounted on'
)
ctx.console.log(
'bare-hyperdrive ' +
String(totalK).padStart(12) +
' ' +
String(usedK).padStart(8) +
' ' +
String(freeK).padStart(10) +
' ' +
String(pct).padStart(3) +
'% ' +
mount
)
}
+8
View File
@@ -0,0 +1,8 @@
async function run(ctx, argv) {
if (typeof ctx.runBinCommand !== 'function') {
ctx.console.error('dir: runBinCommand not available')
ctx.exitCode = 1
return
}
await ctx.runBinCommand(['ls', '-C'].concat(argv.slice(1)))
}
+88
View File
@@ -0,0 +1,88 @@
function parseUniformWidth(tabArg) {
const n = parseInt(String(tabArg).split(',')[0].trim(), 10)
return Number.isFinite(n) && n > 0 ? n : 8
}
function nextTabCol(col, w) {
return (Math.floor(col / w) + 1) * w
}
function expandLine(line, w) {
let col = 0
let out = ''
for (let i = 0; i < line.length; i++) {
const ch = line[i]
if (ch === '\t') {
const target = nextTabCol(col, w)
while (col < target) {
out += ' '
col++
}
} else {
out += ch
if (ch === '\n' || ch === '\r') col = 0
else col++
}
}
return out
}
async function run(ctx, argv) {
let tabArg = '8'
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: expand [-t N] [FILE]...\nConvert tabs to spaces (uniform tab width N, default 8).'
)
return
}
if (a === '-t' && argv[i + 1]) {
tabArg = argv[++i]
continue
}
if (a.startsWith('-t') && a.length > 2) {
tabArg = a.slice(2)
continue
}
if (a.startsWith('--tabs=')) {
tabArg = a.slice(7)
continue
}
if (a.startsWith('-')) {
ctx.console.error('expand: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const w = parseUniformWidth(tabArg)
const b4 = ctx.b4a
function proc(text) {
const lines = text.split('\n')
for (let li = 0; li < lines.length; li++) {
const isLast = li === lines.length - 1
const line = lines[li]
if (isLast && line === '' && lines.length > 1) continue
ctx.console.log(expandLine(line, w))
}
}
if (!paths.length) {
proc(bareStdin(ctx))
return
}
for (const p of paths) {
if (p === '-') {
proc(bareStdin(ctx))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('expand: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
proc(b4.toString(b))
}
}
+96
View File
@@ -0,0 +1,96 @@
async function run(ctx, argv) {
const tokens = argv.slice(1)
if (!tokens.length || tokens[0] === '--help' || tokens[0] === '-h') {
ctx.console.log(
'usage: expr EXPRESSION\nInteger arithmetic (+ - * / %), comparisons, and string = / !=.'
)
return
}
let i = 0
const peek = () => tokens[i]
const take = () => tokens[i++]
function parsePrimary() {
const t = take()
if (t === '(') {
const v = parseAdd()
if (take() !== ')') throw new Error('syntax error')
return v
}
if (/^-?\d+$/.test(t)) return { kind: 'n', v: parseInt(t, 10) }
return { kind: 's', v: t }
}
function parseMul() {
let left = parsePrimary()
while (peek() === '*' || peek() === '/' || peek() === '%') {
const op = take()
const right = parsePrimary()
if (left.kind !== 'n' || right.kind !== 'n') throw new Error('non-numeric')
if (op === '*') left = { kind: 'n', v: left.v * right.v }
else if (op === '/') {
if (right.v === 0) throw new Error('division by zero')
left = { kind: 'n', v: Math.trunc(left.v / right.v) }
} else {
if (right.v === 0) throw new Error('division by zero')
left = { kind: 'n', v: left.v % right.v }
}
}
return left
}
function parseAdd() {
let left = parseMul()
while (peek() === '+' || peek() === '-') {
const op = take()
const right = parseMul()
if (left.kind !== 'n' || right.kind !== 'n') throw new Error('non-numeric')
left = {
kind: 'n',
v: op === '+' ? left.v + right.v : left.v - right.v
}
}
return left
}
function parseCmp() {
let left = parseAdd()
const op = peek()
if (
op === '=' ||
op === '==' ||
op === '!=' ||
op === '<' ||
op === '<=' ||
op === '>' ||
op === '>='
) {
take()
const right = parseAdd()
if (op === '=' || op === '==') {
const eq =
left.kind === right.kind &&
(left.kind === 'n' ? left.v === right.v : left.v === right.v)
return { kind: 'n', v: eq ? 1 : 0 }
}
if (op === '!=') {
const eq =
left.kind === right.kind &&
(left.kind === 'n' ? left.v === right.v : left.v === right.v)
return { kind: 'n', v: eq ? 0 : 1 }
}
if (left.kind !== 'n' || right.kind !== 'n') throw new Error('non-numeric')
let ok = false
if (op === '<') ok = left.v < right.v
else if (op === '<=') ok = left.v <= right.v
else if (op === '>') ok = left.v > right.v
else if (op === '>=') ok = left.v >= right.v
return { kind: 'n', v: ok ? 1 : 0 }
}
return left
}
try {
const r = parseCmp()
if (i < tokens.length) throw new Error('syntax error')
ctx.console.log(String(r.kind === 'n' ? r.v : r.v))
} catch (e) {
ctx.console.error('expr: ' + (e.message || e))
ctx.exitCode = 2
}
}
+53
View File
@@ -0,0 +1,53 @@
function factorOne(n) {
const out = []
let x = n
let d = 2
while (d * d <= x) {
while (x % d === 0) {
out.push(d)
x /= d
}
d++
}
if (x > 1) out.push(x)
return out
}
async function run(ctx, argv) {
const nums = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log('usage: factor [NUMBER]...\nPrint prime factors (trial division).')
return
}
if (a.startsWith('-')) {
ctx.console.error('factor: unsupported option ' + a)
ctx.exitCode = 1
return
}
nums.push(a)
}
const parse = (s) => {
const n = parseInt(String(s), 10)
if (!Number.isFinite(n) || n < 0 || n > Number.MAX_SAFE_INTEGER) return null
return n
}
if (!nums.length) {
const lines = bareStdin(ctx).split(/\n')
for (const line of lines) {
const t = line.trim()
if (!t) continue
nums.push(t)
}
}
for (const s of nums) {
const n = parse(s)
if (n == null || n < 2) {
ctx.console.log(s + ':')
continue
}
const f = factorOne(n)
ctx.console.log(s + ': ' + f.join(' '))
}
}
+97 -1
View File
@@ -77,10 +77,12 @@ async function walk(ctx, dir, o, curDepth) {
const nameOk = !o.nameRe || o.nameRe.test(n)
let match = pathOk && nameOk && (!o.wantType || st.type === o.wantType)
if (match && o.mtimeSpec) match = match && findMatchMtime(st, o.mtimeSpec)
if (match && o.newerThanMs != null) match = match && st.mtimeMs > o.newerThanMs
if (match && o.newerThanMs != null)
match = match && st.mtimeMs > o.newerThanMs
if (match && o.wantEmpty) {
match = match && (await findIsEmpty(ctx, path, st))
}
if (match && o.regexPath && !o.regexPath.test(path)) match = false
if (match && gnuDepth >= o.minDepth) {
if (o.doDelete) {
if (ctx.vfs.env.BARE_OS_FIND_DELETE !== '1') {
@@ -100,6 +102,40 @@ async function walk(ctx, dir, o, curDepth) {
ctx.exitCode = 1
}
}
} else if (o.execTemplate && o.execTemplate.length) {
if (typeof ctx.runBinCommand !== 'function') {
const stw = /** @type {{ noRun?: boolean }} */ (o.deleteState)
if (!stw.noRun) {
ctx.console.error('find: -exec requires ctx.runBinCommand')
stw.noRun = true
}
ctx.exitCode = 1
} else if (o.execCount >= o.execMax) {
const stw = /** @type {{ execCap?: boolean }} */ (o.deleteState)
if (!stw.execCap) {
ctx.console.error(
'find: -exec/-ok: invocation limit (' +
o.execMax +
') exceeded (raise BARE_OS_FIND_EXEC_MAX)'
)
stw.execCap = true
}
ctx.exitCode = 1
} else {
const ok =
!o.execUseOk ||
ctx.vfs.env.BARE_OS_FIND_OK === '1' ||
ctx.vfs.env.BARE_OS_FIND_OK === 'true'
if (o.execUseOk && !ok) {
/* skip without running */
} else {
const subst = o.execTemplate.map((arg) =>
arg === '{}' ? path : arg.split('{}').join(path)
)
o.execCount++
await ctx.runBinCommand(subst)
}
}
} else {
findEmitLine(ctx, path, o.print0)
}
@@ -130,6 +166,11 @@ async function run(ctx, argv) {
let prunePath = null
let wantEmpty = false
let doDelete = false
/** @type {string[] | null} */
let execTemplate = null
let execUseOk = false
/** @type {string | null} */
let regexPathStr = null
const rest = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
@@ -192,6 +233,39 @@ async function run(ctx, argv) {
doDelete = true
continue
}
if ((a === '-exec' || a === '-ok') && argv[i + 1]) {
execUseOk = a === '-ok'
i++
const parts = []
while (i < argv.length && argv[i] !== ';') {
parts.push(argv[i++])
}
if (i >= argv.length || argv[i] !== ';') {
ctx.console.error('find: ' + a + ' must be terminated with ;')
ctx.exitCode = 1
return
}
i++
if (!parts.length) {
ctx.console.error('find: empty ' + a)
ctx.exitCode = 1
return
}
execTemplate = parts
continue
}
if (a === '-regex' && argv[i + 1]) {
const pat = argv[++i]
try {
new RegExp(pat)
regexPathStr = pat
} catch {
ctx.console.error('find: invalid -regex')
ctx.exitCode = 1
return
}
continue
}
if (a === '--') {
rest.push(...argv.slice(i + 1))
break
@@ -244,6 +318,23 @@ async function run(ctx, argv) {
if (prunePath) {
pruneAbs = ctx.vfs.resolveLogical(prunePath).replace(/\/+$/, '') || '/'
}
/** @type {RegExp | null} */
let regexPath = null
if (regexPathStr) {
try {
regexPath = new RegExp(regexPathStr)
} catch {
ctx.console.error('find: invalid -regex')
ctx.exitCode = 1
return
}
}
const execMaxRaw = ctx.vfs.env.BARE_OS_FIND_EXEC_MAX
let execMax = 64
if (execMaxRaw != null && String(execMaxRaw).trim() !== '') {
const n = Number.parseInt(String(execMaxRaw), 10)
if (Number.isFinite(n)) execMax = Math.min(4096, Math.max(1, n))
}
const abs = ctx.vfs.resolveLogical(root)
const o = {
maxDepth,
@@ -257,6 +348,11 @@ async function run(ctx, argv) {
pruneAbs,
wantEmpty,
doDelete,
regexPath,
execTemplate,
execUseOk,
execCount: 0,
execMax,
deleteState: {}
}
await walk(ctx, abs, o, 0)
+62
View File
@@ -0,0 +1,62 @@
async function run(ctx, argv) {
let width = 75
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: fmt [-w WIDTH] [FILE]...\nSimple paragraph reflow: join non-empty lines, wrap at spaces.'
)
return
}
if ((a === '-w' || a === '--width') && argv[i + 1]) {
width = parseInt(argv[++i], 10)
if (!Number.isFinite(width) || width < 1) width = 75
continue
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('fmt: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
function flushPara(words) {
if (!words.length) return
let line = ''
for (const w of words) {
if (!line) line = w
else if (line.length + 1 + w.length <= width) line += ' ' + w
else {
ctx.console.log(line)
line = w
}
}
if (line) ctx.console.log(line)
}
function proc(text) {
const paras = text.split(/\n\n+/)
for (const para of paras) {
const words = para.replace(/\s+/g, ' ').trim().split(' ').filter(Boolean)
flushPara(words)
}
}
if (!paths.length) {
proc(bareStdin(ctx))
return
}
for (const p of paths) {
if (p === '-') {
proc(bareStdin(ctx))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('fmt: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
proc(b4.toString(b))
}
}
+60
View File
@@ -0,0 +1,60 @@
async function run(ctx, argv) {
let width = 80
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log('usage: fold [-w WIDTH] [FILE]...\nWrap each input line to WIDTH columns.')
return
}
if ((a === '-w' || a === '--width') && argv[i + 1]) {
width = parseInt(argv[++i], 10)
if (!Number.isFinite(width) || width < 1) width = 80
continue
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('fold: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
function wrapLine(line) {
if (line.length <= width) return [line]
const rows = []
let rest = line
while (rest.length > width) {
rows.push(rest.slice(0, width))
rest = rest.slice(width)
}
if (rest.length) rows.push(rest)
return rows
}
function proc(text) {
const lines = text.split('\n')
for (let li = 0; li < lines.length; li++) {
const isLast = li === lines.length - 1
const line = lines[li]
if (isLast && line === '' && lines.length > 1) continue
for (const row of wrapLine(line)) ctx.console.log(row)
}
}
if (!paths.length) {
proc(bareStdin(ctx))
return
}
for (const p of paths) {
if (p === '-') {
proc(bareStdin(ctx))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('fold: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
proc(b4.toString(b))
}
}
+9 -1
View File
@@ -27,7 +27,15 @@ const CONF = {
/** Defaults for simulated pipelines (override with BARE_OS_PIPELINE_* env); see handbook §3. */
BARE_OS_PIPELINE_MAX_STAGES: '32',
BARE_OS_PIPELINE_MAX_BYTES: '2097152',
BARE_OS_PIPELINE_MAX_LINES: '50000'
BARE_OS_PIPELINE_MAX_LINES: '50000',
/** Default cap for find -exec/-ok invocations per run (override with env). */
BARE_OS_FIND_EXEC_MAX: '64',
/** Max lines `yes` prints before stopping (override with BARE_OS_YES_MAX_LINES). */
BARE_OS_YES_MAX_LINES: '100000',
/** Max input lines `shuf` will hold in memory (override with BARE_OS_SHUF_MAX_LINES). */
BARE_OS_SHUF_MAX_LINES: '50000',
/** Max output chunk files `split` may create (override with BARE_OS_SPLIT_MAX_FILES). */
BARE_OS_SPLIT_MAX_FILES: '10000'
}
async function run(ctx, argv) {
+17
View File
@@ -0,0 +1,17 @@
async function run(ctx, argv) {
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-h' || argv[i] === '--help') {
ctx.console.log('usage: groups [USER]\nPrint group memberships (session model; often one group).')
return
}
}
const e = ctx.vfs.env || {}
const u = e.USER || e.LOGNAME || 'guest'
const g = e.GROUP || u
const extra = e.GROUPS
if (extra && String(extra).trim()) {
ctx.console.log(String(extra).replace(/,/g, ' '))
return
}
ctx.console.log(g)
}
+2 -1
View File
@@ -1,6 +1,7 @@
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname dircolors du edit echo env exit false find getconf grep head hdms help hostname id journalctl jq ln login logout logname ls man mkdir mkfifo mktemp nano mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat systemctl tail tee test theme time touch tr true tty uname wc wget which whoami xargs'
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
BARE_OS_HELP_BIN_SPACED
)
ctx.console.log(
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
+23
View File
@@ -0,0 +1,23 @@
async function run(ctx, argv) {
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-h' || argv[i] === '--help') {
ctx.console.log('usage: hostid\nPrint a numeric host identifier (session-derived stub).')
return
}
if (argv[i].startsWith('-')) {
ctx.console.error('hostid: unknown option ' + argv[i])
ctx.exitCode = 1
return
}
}
const e = ctx.vfs.env || {}
if (e.HOSTID && /^[0-9a-fA-F]{8}$/.test(e.HOSTID)) {
ctx.console.log(e.HOSTID.toLowerCase())
return
}
const seed = e.BARE_OS_SESSION_ID || e.HOSTNAME || 'bare-os'
let h = 0
for (let i = 0; i < seed.length; i++)
h = (Math.imul(31, h) + seed.charCodeAt(i)) >>> 0
ctx.console.log(h.toString(16).padStart(8, '0'))
}
+48
View File
@@ -0,0 +1,48 @@
async function run(ctx, argv) {
let mode = null
const rest = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: install [-m MODE] SOURCE DEST\nCopy one file to DEST and optionally chmod.'
)
return
}
if (a === '-m' && argv[i + 1]) {
const m = parseInt(argv[++i], 8)
if (!Number.isFinite(m) || m < 0) {
ctx.console.error('install: invalid mode')
ctx.exitCode = 1
return
}
mode = m & 0o777
continue
}
if (a.startsWith('-')) {
ctx.console.error('install: unsupported option ' + a)
ctx.exitCode = 1
return
}
rest.push(a)
}
if (rest.length !== 2) {
ctx.console.error('usage: install [-m MODE] SOURCE DEST')
ctx.exitCode = 1
return
}
const [src, dest] = rest
const buf = await ctx.vfs.readFile(src)
if (!buf) {
ctx.console.error('install: cannot read ' + src)
ctx.exitCode = 1
return
}
try {
await ctx.vfs.writeFile(dest, buf)
if (mode != null) await ctx.vfs.chmod(dest, mode)
} catch (e) {
ctx.console.error('install: ' + (e.message || e))
ctx.exitCode = 1
}
}
+97
View File
@@ -0,0 +1,97 @@
async function readLines(ctx, path) {
const b4 = ctx.b4a
if (path === '-') return bareStdin(ctx).split('\n')
const b = await ctx.vfs.readFile(path)
if (!b) return null
return b4.toString(b).split('\n')
}
function field(line, delim, n) {
if (delim === null) {
const parts = line.split(/\s+/)
return parts[n - 1] != null ? parts[n - 1] : ''
}
const parts = line.split(delim)
return parts[n - 1] != null ? parts[n - 1] : ''
}
function key(line, delim, n) {
return field(line, delim, n)
}
async function run(ctx, argv) {
let delim = null
let f1 = 1
let f2 = 1
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: join [-t CHAR] [-1 N] [-2 N] FILE1 FILE2\nJoin lines on equal join fields (sorted input).'
)
return
}
if (a === '-t' && argv[i + 1]) {
const d = argv[++i]
delim = d === '\\t' ? '\t' : d.slice(0, 1)
continue
}
if (a === '-1' && argv[i + 1]) {
f1 = parseInt(argv[++i], 10)
continue
}
if (a === '-2' && argv[i + 1]) {
f2 = parseInt(argv[++i], 10)
continue
}
if (a.startsWith('-')) {
ctx.console.error('join: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (paths.length !== 2) {
ctx.console.error('usage: join [-t CHAR] [-1 N] [-2 N] FILE1 FILE2')
ctx.exitCode = 1
return
}
const A = await readLines(ctx, paths[0])
const B = await readLines(ctx, paths[1])
if (!A || !B) {
ctx.console.error('join: missing input file')
ctx.exitCode = 1
return
}
const trim = (arr) => {
if (arr.length && arr[arr.length - 1] === '') arr.pop()
return arr
}
const a = trim(A.slice())
const b = trim(B.slice())
const sep = delim != null ? delim : ' '
let i = 0
let j = 0
while (i < a.length && j < b.length) {
const ka = key(a[i], delim, f1)
const kb = key(b[j], delim, f2)
const cmp = ka.localeCompare(kb)
if (cmp < 0) i++
else if (cmp > 0) j++
else {
const k = ka
let i1 = i
while (i1 < a.length && key(a[i1], delim, f1) === k) i1++
let j1 = j
while (j1 < b.length && key(b[j1], delim, f2) === k) j1++
for (let ii = i; ii < i1; ii++) {
for (let jj = j; jj < j1; jj++) {
ctx.console.log(a[ii] + sep + b[jj])
}
}
i = i1
j = j1
}
}
}
+75 -3
View File
@@ -1,3 +1,42 @@
/**
* @param {unknown} vfs
* @param {string[]} names
* @param {string} t
* @param {string | null} singleEntryPath
* @param {'time' | 'size' | null} sortBy
* @param {boolean} reverse
*/
async function bareLsSortNames(
vfs,
names,
t,
singleEntryPath,
sortBy,
reverse
) {
if (singleEntryPath != null) return names
if (!sortBy && !reverse) return names
const pairs = []
for (const n of names) {
const sub = t === '.' || t === './' ? n : t.replace(/\/$/, '') + '/' + n
let st = null
try {
st = await vfs.lstat(sub)
} catch {
st = null
}
pairs.push({ n, st })
}
pairs.sort((a, b) => {
let cmp = 0
if (sortBy === 'time') cmp = (b.st?.mtimeMs ?? 0) - (a.st?.mtimeMs ?? 0)
else if (sortBy === 'size') cmp = (b.st?.size ?? 0) - (a.st?.size ?? 0)
else cmp = a.n.localeCompare(b.n)
return reverse ? -cmp : cmp
})
return pairs.map((p) => p.n)
}
async function run(ctx, argv) {
const vfs = ctx.vfs
let showAll = false
@@ -6,6 +45,9 @@ async function run(ctx, argv) {
let singleColumn = false
/** @type {'never' | 'auto' | 'always'} */
let colorMode = 'auto'
/** @type {'time' | 'size' | null} */
let sortBy = null
let reverseSort = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
@@ -21,7 +63,8 @@ async function run(ctx, argv) {
if (a.startsWith('--color=')) {
const v = a.slice(8).toLowerCase()
if (v === 'never' || v === 'none' || v === 'no') colorMode = 'never'
else if (v === 'always' || v === 'yes' || v === 'force') colorMode = 'always'
else if (v === 'always' || v === 'yes' || v === 'force')
colorMode = 'always'
else colorMode = 'auto'
continue
}
@@ -29,6 +72,14 @@ async function run(ctx, argv) {
singleColumn = true
continue
}
if (a === '--sort=time' || a === '--sort=none') {
sortBy = a.endsWith('time') ? 'time' : null
continue
}
if (a === '--sort=size') {
sortBy = 'size'
continue
}
ctx.console.error('ls: unrecognized option ' + a)
ctx.exitCode = 2
return
@@ -39,6 +90,9 @@ async function run(ctx, argv) {
if (c === 'a') showAll = true
else if (c === 'l') longFmt = true
else if (c === '1') singleColumn = true
else if (c === 't') sortBy = 'time'
else if (c === 'S') sortBy = 'size'
else if (c === 'r') reverseSort = true
}
continue
}
@@ -46,8 +100,7 @@ async function run(ctx, argv) {
}
const targets = paths.length ? paths : ['.']
const useColor = bareLsUseColor(ctx, colorMode)
const onePerLine =
singleColumn || ctx.bareOsStdoutCaptured === true
const onePerLine = singleColumn || ctx.bareOsStdoutCaptured === true
for (const t of targets) {
if (targets.length > 1) ctx.console.log(t + ':')
@@ -68,6 +121,14 @@ async function run(ctx, argv) {
continue
}
if (!showAll) names = names.filter((n) => !n.startsWith('.'))
names = await bareLsSortNames(
vfs,
names,
t,
singleEntryPath,
sortBy,
reverseSort
)
if (!longFmt) {
if (onePerLine) {
for (const n of names) {
@@ -161,6 +222,17 @@ async function run(ctx, argv) {
st
})
}
if ((sortBy || reverseSort) && singleEntryPath == null) {
rows.sort((a, b) => {
let cmp = 0
const sta = a.st
const stb = b.st
if (sortBy === 'time') cmp = (stb?.mtimeMs ?? 0) - (sta?.mtimeMs ?? 0)
else if (sortBy === 'size') cmp = (stb?.size ?? 0) - (sta?.size ?? 0)
else cmp = a.name.localeCompare(b.name)
return reverseSort ? -cmp : cmp
})
}
if (singleEntryPath == null) ctx.console.log('total ' + totalBlocks)
for (const r of rows) {
const tail = bareLsColorLongTail(r.name, r.arrow, r.st, useColor, ctx)
+48
View File
@@ -0,0 +1,48 @@
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: md5sum [FILE]...\n' +
'With no FILE, or when FILE is -, read standard input. Uses bundled MD5 (no Web Crypto MD5).'
)
ctx.exitCode = 0
return
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('md5sum: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
function one(name, buf) {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
try {
const hex = bareMd5DigestBytes(u8)
ctx.console.log(hex + ' ' + name)
} catch (e) {
ctx.console.error('md5sum: ' + (e.message || e))
ctx.exitCode = 1
}
}
if (!paths.length || (paths.length === 1 && paths[0] === '-')) {
one('-', b4.from(bareStdin(ctx)))
return
}
for (const p of paths) {
if (p === '-') {
one('-', b4.from(bareStdin(ctx)))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('md5sum: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
one(p, b)
}
}
+33
View File
@@ -0,0 +1,33 @@
async function run(ctx, argv) {
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: nproc [--all]\nPrint number of processing units (from /proc/cpuinfo or 1). --all is accepted for compatibility.'
)
return
}
if (a === '--all') {
/* same count as default in this environment */
} else if (a.startsWith('-')) {
ctx.console.error('nproc: unknown option ' + a)
ctx.exitCode = 1
return
}
}
let n = 1
try {
const buf = await ctx.vfs.readFile('/proc/cpuinfo')
if (buf) {
const t = ctx.b4a.toString(buf)
const m = t.match(/^processor\s*:/gim)
if (m && m.length) n = m.length
}
} catch {
/* ignore */
}
const e = ctx.vfs.env || {}
const envN = e.BARE_OS_NPROC
if (envN && /^\d+$/.test(envN)) n = Math.max(1, parseInt(envN, 10))
ctx.console.log(String(n))
}
+58
View File
@@ -0,0 +1,58 @@
function fmtIec(n, si) {
const base = si ? 1000 : 1024
const units = si
? ['', 'k', 'M', 'G', 'T', 'P']
: ['', 'K', 'M', 'G', 'T', 'P']
if (n === 0) return '0'
let sign = n < 0 ? -1 : 1
let x = Math.abs(n)
let u = 0
while (x >= base && u < units.length - 1) {
x /= base
u++
}
const s =
x >= 10 || u === 0 ? Math.round(x * 10) / 10 : Math.round(x * 100) / 100
const t = String(s).replace(/\.0$/, '')
return (sign < 0 ? '-' : '') + t + units[u]
}
async function run(ctx, argv) {
let toIec = false
let toSi = false
const rest = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: numfmt [--to=iec|--to=si] [NUMBER]...\nFormat numbers; reads stdin lines if no operands.'
)
return
}
if (a === '--to=iec') toIec = true
else if (a === '--to=si') toSi = true
else if (a.startsWith('-')) {
ctx.console.error('numfmt: unsupported option ' + a)
ctx.exitCode = 1
return
} else rest.push(a)
}
if (!toIec && !toSi) toIec = true
const nums = []
if (!rest.length) {
for (const line of bareStdin(ctx).split('\n')) {
const t = line.trim()
if (!t) continue
nums.push(t)
}
} else nums.push(...rest)
for (const s of nums) {
const n = parseInt(s, 10)
if (!Number.isFinite(n)) {
ctx.console.error('numfmt: invalid number ' + s)
ctx.exitCode = 1
return
}
ctx.console.log(fmtIec(n, toSi))
}
}
+72
View File
@@ -0,0 +1,72 @@
async function readLines(ctx, path) {
const b4 = ctx.b4a
if (path === '-') return bareStdin(ctx).split('\n')
const b = await ctx.vfs.readFile(path)
if (!b) return null
return b4.toString(b).split('\n')
}
async function run(ctx, argv) {
let delims = '\t'
let serial = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: paste [-d LIST] [-s] [FILE]...\nMerge corresponding lines; -s pastes one file per line.'
)
return
}
if ((a === '-d' || a === '--delimiters') && argv[i + 1]) {
delims = argv[++i]
continue
}
if (a === '-s' || a === '--serial') {
serial = true
continue
}
if (a.startsWith('-')) {
ctx.console.error('paste: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (!paths.length) paths.push('-')
const files = []
for (const p of paths) {
const L = await readLines(ctx, p)
if (L == null) {
ctx.console.error('paste: ' + p + ': No such file')
ctx.exitCode = 1
return
}
const trim = L.length && L[L.length - 1] === '' ? L.slice(0, -1) : L
files.push(trim)
}
if (serial) {
for (const lines of files) {
let out = ''
for (let r = 0; r < lines.length; r++) {
if (r) out += delims[0] || '\t'
out += lines[r]
}
ctx.console.log(out)
}
return
}
const maxR = Math.max(...files.map((f) => f.length), 0)
for (let r = 0; r < maxR; r++) {
const parts = []
for (let c = 0; c < files.length; c++) {
parts.push(files[c][r] != null ? files[c][r] : '')
}
let line = ''
for (let c = 0; c < parts.length; c++) {
if (c) line += delims[c % delims.length] || '\t'
line += parts[c]
}
ctx.console.log(line)
}
}
+65
View File
@@ -0,0 +1,65 @@
async function run(ctx, argv) {
let width = 72
let numberLines = false
let sep = '\t'
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: pr [-w WIDTH] [-n] [-s CHAR] [FILE]...\nMinimal print: merge files side by side with optional line numbers.'
)
return
}
if ((a === '-w' || a === '--width') && argv[i + 1]) {
width = parseInt(argv[++i], 10)
if (!Number.isFinite(width) || width < 1) width = 72
continue
}
if (a === '-n' || a === '--number') {
numberLines = true
continue
}
if ((a === '-s' || a === '--separator') && argv[i + 1]) {
sep = argv[++i] || '\t'
continue
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('pr: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
const files = []
if (!paths.length) {
files.push(bareStdin(ctx).split('\n'))
} else {
for (const p of paths) {
if (p === '-') files.push(bareStdin(ctx).split('\n'))
else {
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('pr: ' + p + ': No such file')
ctx.exitCode = 1
return
}
files.push(b4.toString(b).split('\n'))
}
}
}
const maxRows = Math.max(...files.map((f) => f.length), 0)
for (let r = 0; r < maxRows; r++) {
const parts = []
for (let c = 0; c < files.length; c++) {
let cell = files[c][r] != null ? files[c][r] : ''
if (numberLines && c === 0 && r < files[0].length)
cell = String(r + 1).padStart(6, ' ') + '\t' + cell
parts.push(cell)
}
let line = parts.join(sep)
if (line.length > width) line = line.slice(0, width)
ctx.console.log(line)
}
}
@@ -0,0 +1,55 @@
/**
* realpath — print resolved absolute path (VFS logical resolution).
*/
async function run(ctx, argv) {
let missingOk = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: realpath [-m] FILE...\n' +
' -m do not fail if the path does not exist (resolve only)'
)
ctx.exitCode = 0
return
}
if (a === '-m' || a === '--canonicalize-missing') {
missingOk = true
continue
}
if (a.startsWith('-')) {
ctx.console.error('realpath: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (!paths.length) {
ctx.console.error('usage: realpath [-m] FILE...')
ctx.exitCode = 1
return
}
const vfs = ctx.vfs
for (const p of paths) {
try {
if (!missingOk) {
const st = await vfs.stat(p)
if (!st) {
ctx.console.error('realpath: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
}
const resolved = vfs.resolveLogical(p)
ctx.console.log(resolved)
} catch (e) {
if (missingOk) {
ctx.console.log(vfs.resolveLogical(p))
} else {
ctx.console.error('realpath: ' + p + ': ' + (e.message || e))
ctx.exitCode = 1
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log('usage: rev [FILE]...\nReverse characters on each line.')
return
}
if (a.startsWith('-')) {
ctx.console.error('rev: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
function procText(text) {
const endsNl = text.endsWith('\n')
const lines = text.split('\n')
if (endsNl && lines.length && lines[lines.length - 1] === '') lines.pop()
for (const line of lines) {
ctx.console.log(line.split('').reverse().join(''))
}
}
if (!paths.length) {
procText(bareStdin(ctx))
return
}
for (const p of paths) {
let text
if (p === '-') text = bareStdin(ctx)
else {
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('rev: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
text = b4.toString(b)
}
procText(text)
}
}
+33 -1
View File
@@ -1,11 +1,12 @@
/**
* rm — remove files or directories.
* Flags: -r -R --recursive, -f --force, -- ; bundled e.g. -rf
* Flags: -r -R --recursive, -f --force, -d --dir, -- ; bundled e.g. -rf
*/
async function run(ctx, argv) {
const vfs = ctx.vfs
let recursive = false
let force = false
let dirEmptyOnly = false
const files = []
let dash = false
for (let i = 1; i < argv.length; i++) {
@@ -26,11 +27,16 @@ async function run(ctx, argv) {
force = true
continue
}
if (a === '--dir' || a === '-d') {
dirEmptyOnly = true
continue
}
if (a.startsWith('-') && a.length > 1) {
for (let j = 1; j < a.length; j++) {
const c = a[j]
if (c === 'r' || c === 'R') recursive = true
else if (c === 'f') force = true
else if (c === 'd') dirEmptyOnly = true
}
continue
}
@@ -41,6 +47,11 @@ async function run(ctx, argv) {
ctx.exitCode = 1
return
}
if (dirEmptyOnly && recursive) {
ctx.console.error('rm: cannot combine -d and -r')
ctx.exitCode = 1
return
}
const doRm =
vfs && typeof vfs.rm === 'function'
? (p) => vfs.rm(p, { recursive, force })
@@ -50,6 +61,27 @@ async function run(ctx, argv) {
}
for (const f of files) {
try {
if (dirEmptyOnly) {
const st = await vfs.lstat(f)
if (!st) {
if (!force) {
ctx.console.error('rm: ' + f + ': No such file')
ctx.exitCode = 1
}
continue
}
if (st.type !== 'directory') {
ctx.console.error('rm: cannot remove ' + f + ': Not a directory')
ctx.exitCode = 1
continue
}
if (typeof vfs.rmdir === 'function') await vfs.rmdir(f)
else {
ctx.console.error('rm: rmdir not supported by this VFS')
ctx.exitCode = 1
}
continue
}
await doRm(f)
} catch (e) {
if (force) continue
+59
View File
@@ -0,0 +1,59 @@
async function sha1Hex(u8) {
const subtle = globalThis.crypto?.subtle
if (!subtle || typeof subtle.digest !== 'function') {
throw new Error('crypto.subtle.digest (SHA-1) is not available')
}
const hash = await subtle.digest('SHA-1', u8)
return [...new Uint8Array(hash)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: sha1sum [FILE]...\n' +
'With no FILE, or when FILE is -, read standard input.'
)
ctx.exitCode = 0
return
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('sha1sum: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
async function one(name, buf) {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
try {
const hex = await sha1Hex(u8)
ctx.console.log(hex + ' ' + name)
} catch (e) {
ctx.console.error('sha1sum: ' + (e.message || e))
ctx.exitCode = 1
}
}
if (!paths.length || (paths.length === 1 && paths[0] === '-')) {
await one('-', b4.from(bareStdin(ctx)))
return
}
for (const p of paths) {
if (p === '-') {
await one('-', b4.from(bareStdin(ctx)))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('sha1sum: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
await one(p, b)
}
}
@@ -0,0 +1,63 @@
/**
* sha256sum — compute SHA-256 checksums (hex), GNU-like output line.
*/
async function sha256Hex(u8) {
const subtle = globalThis.crypto?.subtle
if (!subtle || typeof subtle.digest !== 'function') {
throw new Error('crypto.subtle.digest (SHA-256) is not available')
}
const hash = await subtle.digest('SHA-256', u8)
return [...new Uint8Array(hash)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: sha256sum [FILE]...\n' +
'With no FILE, or when FILE is -, read standard input.'
)
ctx.exitCode = 0
return
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('sha256sum: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
async function one(name, buf) {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
try {
const hex = await sha256Hex(u8)
ctx.console.log(hex + ' ' + name)
} catch (e) {
ctx.console.error('sha256sum: ' + (e.message || e))
ctx.exitCode = 1
}
}
if (!paths.length || (paths.length === 1 && paths[0] === '-')) {
const buf = b4.from(bareStdin(ctx))
await one('-', buf)
return
}
for (const p of paths) {
if (p === '-') {
await one('-', b4.from(bareStdin(ctx)))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('sha256sum: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
await one(p, b)
}
}
@@ -0,0 +1,59 @@
async function sha512Hex(u8) {
const subtle = globalThis.crypto?.subtle
if (!subtle || typeof subtle.digest !== 'function') {
throw new Error('crypto.subtle.digest (SHA-512) is not available')
}
const hash = await subtle.digest('SHA-512', u8)
return [...new Uint8Array(hash)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: sha512sum [FILE]...\n' +
'With no FILE, or when FILE is -, read standard input.'
)
ctx.exitCode = 0
return
}
if (a.startsWith('-') && a !== '-') {
ctx.console.error('sha512sum: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
async function one(name, buf) {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
try {
const hex = await sha512Hex(u8)
ctx.console.log(hex + ' ' + name)
} catch (e) {
ctx.console.error('sha512sum: ' + (e.message || e))
ctx.exitCode = 1
}
}
if (!paths.length || (paths.length === 1 && paths[0] === '-')) {
await one('-', b4.from(bareStdin(ctx)))
return
}
for (const p of paths) {
if (p === '-') {
await one('-', b4.from(bareStdin(ctx)))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('sha512sum: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
await one(p, b)
}
}
+55
View File
@@ -0,0 +1,55 @@
async function readLines(ctx, path) {
const b4 = ctx.b4a
if (path === '-') return bareStdin(ctx).split('\n')
const b = await ctx.vfs.readFile(path)
if (!b) return null
return b4.toString(b).split('\n')
}
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: shuf [FILE]...\nShuffle lines; memory-capped via BARE_OS_SHUF_MAX_LINES.'
)
return
}
if (a.startsWith('-')) {
ctx.console.error('shuf: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const cap =
parseInt(ctx.vfs.env.BARE_OS_SHUF_MAX_LINES || '50000', 10) || 50000
if (!paths.length) paths.push('-')
const b4 = ctx.b4a
const lines = []
for (const p of paths) {
const L = await readLines(ctx, p)
if (L == null) {
ctx.console.error('shuf: ' + p + ': No such file')
ctx.exitCode = 1
return
}
const trim = L.length && L[L.length - 1] === '' ? L.slice(0, -1) : L
for (const ln of trim) {
if (lines.length >= cap) {
ctx.console.error('shuf: input exceeds line cap')
ctx.exitCode = 1
return
}
lines.push(ln)
}
}
for (let i = lines.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const t = lines[i]
lines[i] = lines[j]
lines[j] = t
}
for (const ln of lines) ctx.console.log(ln)
}
+118
View File
@@ -0,0 +1,118 @@
function splitSuffix(prefix, i) {
const a = 'abcdefghijklmnopqrstuvwxyz'
const hi = Math.floor(i / 26) % 26
const lo = i % 26
return prefix + a[hi] + a[lo]
}
async function run(ctx, argv) {
let lineCount = 1000
let byteCount = null
let prefix = 'x'
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: split [-l N] [-b N] [INPUT [PREFIX]]\nSplit INPUT into pieces (default -l 1000).'
)
return
}
if (a === '-l' && argv[i + 1]) {
lineCount = parseInt(argv[++i], 10)
byteCount = null
if (!Number.isFinite(lineCount) || lineCount < 1) lineCount = 1000
continue
}
if (a.startsWith('-l') && a.length > 2) {
lineCount = parseInt(a.slice(2), 10)
byteCount = null
continue
}
if (a === '-b' && argv[i + 1]) {
byteCount = parseInt(argv[++i], 10)
lineCount = null
if (!Number.isFinite(byteCount) || byteCount < 1) byteCount = 512
continue
}
if (a.startsWith('-')) {
ctx.console.error('split: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const maxFiles =
parseInt(ctx.vfs.env.BARE_OS_SPLIT_MAX_FILES || '10000', 10) || 10000
let inputPath = '-'
if (paths.length === 1) inputPath = paths[0]
else if (paths.length >= 2) {
inputPath = paths[0]
prefix = paths[1]
}
const b4 = ctx.b4a
let text
if (inputPath === '-') text = bareStdin(ctx)
else {
const b = await ctx.vfs.readFile(inputPath)
if (!b) {
ctx.console.error('split: cannot read ' + inputPath)
ctx.exitCode = 1
return
}
text = b4.toString(b)
}
if (byteCount != null) {
const u8 = b4.from(text)
let off = 0
let idx = 0
while (off < u8.length) {
if (idx >= maxFiles) {
ctx.console.error('split: too many output files')
ctx.exitCode = 1
return
}
const chunk = u8.subarray(off, off + byteCount)
off += byteCount
await ctx.vfs.writeFile(splitSuffix(prefix, idx), chunk)
idx++
}
return
}
const endsNl = text.endsWith('\n')
const lines = text.split('\n')
if (endsNl && lines.length && lines[lines.length - 1] === '') lines.pop()
let idx = 0
let batch = []
function flush(forceNl) {
if (!batch.length) return
if (idx >= maxFiles) {
ctx.console.error('split: too many output files')
ctx.exitCode = 1
return
}
const body =
batch.join('\n') + (forceNl || batch.length === lineCount ? '\n' : '')
const p = splitSuffix(prefix, idx)
idx++
batch = []
return ctx.vfs.writeFile(p, b4.from(body))
}
for (let li = 0; li < lines.length; li++) {
batch.push(lines[li])
if (batch.length >= lineCount) {
await flush(true)
if (ctx.exitCode) return
}
}
if (batch.length) {
const tailNl = endsNl
const body = batch.join('\n') + (tailNl ? '\n' : '')
if (idx >= maxFiles) {
ctx.console.error('split: too many output files')
ctx.exitCode = 1
return
}
await ctx.vfs.writeFile(splitSuffix(prefix, idx), b4.from(body))
}
}
+8 -5
View File
@@ -8,8 +8,7 @@ function statApplyFormat(st, displayPath, fmt) {
for (let i = 0; i < fmt.length; i++) {
if (fmt[i] === '%' && i + 1 < fmt.length) {
const c = fmt[++i]
if (c === 'n')
out += displayPath.split('/').pop() || displayPath
if (c === 'n') out += displayPath.split('/').pop() || displayPath
else if (c === 'N') out += displayPath
else if (c === 's') out += String(st.size ?? 0)
else if (c === 'Y')
@@ -18,13 +17,17 @@ function statApplyFormat(st, displayPath, fmt) {
(typeof st.mtimeMs === 'number' ? st.mtimeMs : Date.now()) / 1000
)
)
else if (c === 'A')
out += bareFormatModeString(st.mode, st.type)
else if (c === 'A') out += bareFormatModeString(st.mode, st.type)
else if (c === 'U') out += String(st.user ?? '')
else if (c === 'G') out += String(st.group ?? '')
else if (c === 'u') out += String(st.uid ?? 0)
else if (c === 'g') out += String(st.gid ?? 0)
else if (c === '%') out += '%'
else if (c === 'F') {
const ty = st.type
if (ty === 'directory') out += 'directory'
else if (ty === 'symlink') out += 'symbolic link'
else out += 'regular file'
} else if (c === '%') out += '%'
else out += '%' + c
} else {
out += fmt[i]
+62
View File
@@ -0,0 +1,62 @@
function sumSysv(u8) {
let crc = 0
for (let i = 0; i < u8.length; i++) crc += u8[i]
crc = (crc & 0xffff) + ((crc >> 16) & 0xffff)
crc = (crc & 0xffff) + ((crc >> 16) & 0xffff)
return crc & 0xffff
}
function sumBsd(u8) {
let cksum = 0
for (let i = 0; i < u8.length; i++) {
cksum = (cksum >> 1) + ((cksum & 1) << 15)
cksum = (cksum + u8[i]) & 0xffff
}
return cksum
}
async function run(ctx, argv) {
let bsd = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: sum [-r] [FILE]...\n' +
' -r BSD algorithm (default is SysV / CRC16-style sum)'
)
ctx.exitCode = 0
return
}
if (a === '-r') bsd = true
else if (a.startsWith('-') && a !== '-') {
ctx.console.error('sum: unsupported option ' + a)
ctx.exitCode = 1
return
} else paths.push(a)
}
const b4 = ctx.b4a
function one(name, buf) {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
const blocks = Math.ceil(u8.length / 512) || 1
const v = bsd ? sumBsd(u8) : sumSysv(u8)
ctx.console.log(v + '\t' + blocks + '\t' + name)
}
if (!paths.length || (paths.length === 1 && paths[0] === '-')) {
one('-', b4.from(bareStdin(ctx)))
return
}
for (const p of paths) {
if (p === '-') {
one('-', b4.from(bareStdin(ctx)))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('sum: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
one(p, b)
}
}
+13
View File
@@ -0,0 +1,13 @@
async function run(ctx, argv) {
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-h' || argv[i] === '--help') {
ctx.console.log('usage: sync\nFlush filesystem buffers (no-op on Bare OS; exits 0).')
return
}
if (argv[i].startsWith('-')) {
ctx.console.error('sync: unknown option ' + argv[i])
ctx.exitCode = 1
return
}
}
}
+45
View File
@@ -0,0 +1,45 @@
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log('usage: tac [FILE]...\nConcatenate and print lines in reverse order.')
return
}
if (a.startsWith('-')) {
ctx.console.error('tac: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
function tacText(text) {
const raw = text.endsWith('\n') ? text.slice(0, -1) : text
if (raw === '') {
ctx.console.log('')
return
}
const lines = raw.split('\n')
lines.reverse()
ctx.console.log(lines.join('\n'))
}
if (!paths.length) {
tacText(bareStdin(ctx))
return
}
for (const p of paths) {
let text
if (p === '-') text = bareStdin(ctx)
else {
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('tac: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
text = b4.toString(b)
}
tacText(text)
}
}
@@ -0,0 +1,55 @@
async function run(ctx, argv) {
let size = null
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: truncate -s SIZE FILE\nSet file length to SIZE bytes (padded with zeros if growing).'
)
return
}
if ((a === '-s' || a === '--size') && argv[i + 1]) {
const raw = argv[++i]
if (raw.startsWith('+') || raw.startsWith('-')) {
ctx.console.error('truncate: relative sizes not supported')
ctx.exitCode = 1
return
}
size = parseInt(raw, 10)
if (!Number.isFinite(size) || size < 0) {
ctx.console.error('truncate: invalid size')
ctx.exitCode = 1
return
}
continue
}
if (a.startsWith('-')) {
ctx.console.error('truncate: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (size == null || paths.length !== 1) {
ctx.console.error('usage: truncate -s SIZE FILE')
ctx.exitCode = 1
return
}
const file = paths[0]
let cur = new Uint8Array(0)
try {
const b = await ctx.vfs.readFile(file)
if (b) cur = b instanceof Uint8Array ? b : new Uint8Array(b)
} catch {
/* new file */
}
const out = new Uint8Array(size)
out.set(cur.subarray(0, Math.min(cur.length, size)))
try {
await ctx.vfs.writeFile(file, out)
} catch (e) {
ctx.console.error('truncate: ' + (e.message || e))
ctx.exitCode = 1
}
}
+76
View File
@@ -0,0 +1,76 @@
async function run(ctx, argv) {
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: tsort [FILE]\nTopological sort of directed edges (one pair per line: A B).'
)
return
}
if (a.startsWith('-')) {
ctx.console.error('tsort: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
let text
if (!paths.length || paths[0] === '-') text = bareStdin(ctx)
else {
const b = await ctx.vfs.readFile(paths[0])
if (!b) {
ctx.console.error('tsort: cannot read ' + paths[0])
ctx.exitCode = 1
return
}
text = b4.toString(b)
}
const edges = []
const nodes = new Set()
for (const line of text.split('\n')) {
const t = line.trim()
if (!t) continue
const parts = t.split(/\s+/)
if (parts.length < 2) continue
const u = parts[0]
const v = parts[1]
edges.push([u, v])
nodes.add(u)
nodes.add(v)
}
const indeg = new Map()
const adj = new Map()
for (const n of nodes) {
indeg.set(n, 0)
adj.set(n, [])
}
for (const [u, v] of edges) {
indeg.set(v, (indeg.get(v) || 0) + 1)
adj.get(u).push(v)
}
const q = []
for (const [n, d] of indeg) {
if (d === 0) q.push(n)
}
q.sort()
const out = []
while (q.length) {
const u = q.shift()
out.push(u)
for (const v of adj.get(u) || []) {
indeg.set(v, indeg.get(v) - 1)
if (indeg.get(v) === 0) {
q.push(v)
q.sort()
}
}
}
if (out.length !== nodes.size) {
ctx.console.error('tsort: cycle in input')
ctx.exitCode = 1
return
}
for (const n of out) ctx.console.log(n)
}
+108
View File
@@ -0,0 +1,108 @@
function parseWidth(tabArg) {
const n = parseInt(String(tabArg).split(',')[0].trim(), 10)
return Number.isFinite(n) && n > 0 ? n : 8
}
function unexpandLine(line, w) {
let out = ''
let col = 0
let i = 0
while (i < line.length) {
if (line[i] === ' ') {
let j = i
while (j < line.length && line[j] === ' ') j++
const n = j - i
const posMod = col % w
if (posMod === 0 && n >= w) {
const tabs = Math.floor(n / w)
const rest = n % w
for (let k = 0; k < tabs; k++) out += '\t'
col += tabs * w
i += tabs * w
for (let k = 0; k < rest; k++) {
out += ' '
col++
i++
}
continue
}
const toNext = posMod === 0 ? w : w - posMod
if (n >= toNext && toNext > 0 && posMod !== 0) {
out += '\t'
col += toNext
i += toNext
continue
}
out += line.slice(i, j)
col += n
i = j
continue
}
const ch = line[i]
out += ch
if (ch === '\t') col = (Math.floor(col / w) + 1) * w
else if (ch === '\n' || ch === '\r') col = 0
else col++
i++
}
return out
}
async function run(ctx, argv) {
let w = 8
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: unexpand [-t N] [FILE]...\nConvert runs of spaces to tabs (width N, default 8).'
)
return
}
if (a === '-t' && argv[i + 1]) {
w = parseWidth(argv[++i])
continue
}
if (a.startsWith('-t') && a.length > 2) {
w = parseWidth(a.slice(2))
continue
}
if (a.startsWith('--tabs=')) {
w = parseWidth(a.slice(7))
continue
}
if (a.startsWith('-')) {
ctx.console.error('unexpand: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
const b4 = ctx.b4a
function proc(text) {
const lines = text.split('\n')
for (let li = 0; li < lines.length; li++) {
const isLast = li === lines.length - 1
const line = lines[li]
if (isLast && line === '' && lines.length > 1) continue
ctx.console.log(unexpandLine(line, w))
}
}
if (!paths.length) {
proc(bareStdin(ctx))
return
}
for (const p of paths) {
if (p === '-') {
proc(bareStdin(ctx))
continue
}
const b = await ctx.vfs.readFile(p)
if (!b) {
ctx.console.error('unexpand: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
proc(b4.toString(b))
}
}
+78
View File
@@ -0,0 +1,78 @@
/**
* uniq — filter adjacent duplicate lines from sorted input.
*/
async function run(ctx, argv) {
let count = false
let onlyDup = false
let onlyUniq = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-h' || a === '--help') {
ctx.console.log(
'usage: uniq [-c] [-d] [-u] [INPUT [OUTPUT]]\n' +
' -c prefix lines with repeat count\n' +
' -d only print duplicate lines (one of each group)\n' +
' -u only print lines that are not repeated\n' +
'Reads stdin if INPUT omitted; OUTPUT is ignored (VFS single-output via redirect).'
)
ctx.exitCode = 0
return
}
if (a === '-c' || a === '--count') {
count = true
continue
}
if (a === '-d' || a === '--repeated') {
onlyDup = true
continue
}
if (a === '-u' || a === '--unique') {
onlyUniq = true
continue
}
if (a.startsWith('-')) {
ctx.console.error('uniq: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (onlyDup && onlyUniq) {
ctx.console.error('uniq: -d and -u are mutually exclusive')
ctx.exitCode = 1
return
}
let text
if (!paths.length) {
text = bareStdin(ctx)
} else {
const b = await ctx.vfs.readFile(paths[0])
if (!b) {
ctx.console.error('uniq: cannot read ' + paths[0])
ctx.exitCode = 1
return
}
text = ctx.b4a.toString(b)
}
const raw = text.replace(/\r\n/g, '\n')
const lines = raw.length ? raw.split('\n') : []
if (lines.length && lines[lines.length - 1] === '') lines.pop()
let i = 0
while (i < lines.length) {
let j = i + 1
while (j < lines.length && lines[j] === lines[i]) j++
const reps = j - i
if (onlyDup && reps === 1) {
i = j
continue
}
if (onlyUniq && reps > 1) {
i = j
continue
}
if (count) ctx.console.log(String(reps) + ' ' + lines[i])
else ctx.console.log(lines[i])
i = j
}
}
+23
View File
@@ -0,0 +1,23 @@
async function run(ctx, argv) {
const args = argv.slice(1).filter((a) => a !== '--')
if (args.length === 1 && (args[0] === '-h' || args[0] === '--help')) {
ctx.console.log('usage: unlink FILE\nCall unlink(2) on one file.')
return
}
if (args.length !== 1) {
ctx.console.error('usage: unlink FILE')
ctx.exitCode = 1
return
}
if (args[0].startsWith('-')) {
ctx.console.error('unlink: unsupported option ' + args[0])
ctx.exitCode = 1
return
}
try {
await ctx.vfs.unlink(args[0])
} catch (e) {
ctx.console.error('unlink: ' + (e.message || e))
ctx.exitCode = 1
}
}
+53
View File
@@ -0,0 +1,53 @@
async function run(ctx, argv) {
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-h' || argv[i] === '--help') {
ctx.console.log('usage: uptime\nPrint load average and uptime from /proc/uptime when available.')
return
}
if (argv[i].startsWith('-')) {
ctx.console.error('uptime: unknown option ' + argv[i])
ctx.exitCode = 1
return
}
}
let up = 0
let idle = 0
try {
const buf = await ctx.vfs.readFile('/proc/uptime')
if (buf) {
const parts = ctx.b4a.toString(buf).trim().split(/\s+/)
up = parseFloat(parts[0]) || 0
idle = parseFloat(parts[1]) || 0
}
} catch {
/* ignore */
}
const now = new Date()
const timeStr = now.toTimeString().slice(0, 8)
const days = Math.floor(up / 86400)
const hrs = Math.floor((up % 86400) / 3600)
const mins = Math.floor((up % 3600) / 60)
let upStr =
days > 0
? days + ' day' + (days === 1 ? '' : 's') + ', '
: ''
upStr += String(hrs).padStart(2, '0') + ':' + String(mins).padStart(2, '0')
let load = '0.00 0.00 0.00'
try {
const la = await ctx.vfs.readFile('/proc/loadavg')
if (la) {
const t = ctx.b4a.toString(la).trim().split(/\s+/)
if (t.length >= 3) load = t[0] + ' ' + t[1] + ' ' + t[2]
}
} catch {
/* ignore */
}
void idle
ctx.console.log(
timeStr +
' up ' +
upStr +
', load average: ' +
load
)
}
+15
View File
@@ -0,0 +1,15 @@
async function run(ctx, argv) {
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-h' || argv[i] === '--help') {
ctx.console.log('usage: users\nPrint login names (single-session stub).')
return
}
if (argv[i].startsWith('-')) {
ctx.console.error('users: unknown option ' + argv[i])
ctx.exitCode = 1
return
}
}
const e = ctx.vfs.env || {}
ctx.console.log(e.USER || e.LOGNAME || 'guest')
}
+8
View File
@@ -0,0 +1,8 @@
async function run(ctx, argv) {
if (typeof ctx.runBinCommand !== 'function') {
ctx.console.error('vdir: runBinCommand not available')
ctx.exitCode = 1
return
}
await ctx.runBinCommand(['ls', '-l'].concat(argv.slice(1)))
}

Some files were not shown because too many files have changed in this diff Show More