Update man to add the manual guide

This commit is contained in:
Raven Scott
2026-04-03 05:28:49 -04:00
parent 11cac45df6
commit 2d26a0b8d1
174 changed files with 17092 additions and 198 deletions
+12 -6
View File
@@ -1,6 +1,6 @@
# bare-os-coreutils
**Build step**, not a runtime library: concatenates a tiny shared prelude (`lib/runtime.js`) with 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 **`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:
- `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)
@@ -29,15 +29,21 @@ Or `node packages/bare-os-coreutils/build.mjs`.
**CI / tests:** root `pretest` runs this build so `kernel/bin` exists before workspace tests.
## Commands (current)
## Commands (authoritative list)
`basename`, `cat`, `clear`, `crontab`, `date`, `dirname`, `echo`, `env`, `exit`, `false`, `grep`, `head`, `hdms`, `help`, `hostname`, `id`, `login`, `logout`, `ls`, `nl`, `pathchk`, `printenv`, `pwd`, `rm`, `savevault`, `seq`, `sleep`, `sort`, `tail`, `test`, `touch`, `true`, `tty`, `uname`, `wc`, `which`, `whoami`
**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`**.
**`grep`** uses JavaScript `RegExp` (and `-F` fixed strings); it is a POSIX/GNU-like **subset**, not byte-identical to GNU grep.
`awk`, `basename`, `cat`, `chgrp`, `chmod`, `chown`, `cksum`, `clear`, `cp`, `crontab`, `cut`, `date`, `dirname`, `du`, `echo`, `env`, `exit`, `false`, `find`, `getconf`, `grep`, `head`, `hdms`, `help`, `hostname`, `id`, `ln`, `login`, `logout`, `logname`, `ls`, `man`, `mkdir`, `mkfifo`, `mv`, `nl`, `od`, `pathchk`, `printenv`, `printf`, `pwd`, `readlink`, `rm`, `rmdir`, `savevault`, `sed`, `seq`, `sleep`, `sort`, `stat`, `tail`, `tee`, `test`, `time`, `touch`, `tr`, `true`, `tty`, `uname`, `wc`, `which`, `whoami`, `xargs`
See [handbook command reference](../../handbook/06-kernel-and-binaries.md) for behavior notes (e.g. `ls -a`, `crontab` identity gating).
**`grep`** uses JavaScript **`RegExp`** (and **`-F`** fixed strings); POSIX/GNU-like **subset**.
**`sed`** / **`awk`** use large interpreters in **`lib/sed-engine.js`** and **`lib/awk-engine.js`** — capable, but not guaranteed to match every POSIX or GNU edge case.
**Stubs** (**`xargs`**, **`getconf`**, **`chown`**, **`chgrp`**, **`mkfifo`**) print a clear error and exit non-zero.
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).
## See also
- [kernel/README.md](../../kernel/README.md) — where built artifacts live in the source tree.
- [DOCUMENTATION.md](../../DOCUMENTATION.md) §12.10 (may lag the command list; this README and `build.mjs` are authoritative).
- [DOCUMENTATION.md](../../DOCUMENTATION.md) §12.10 (may lag the command list; **`lib/commands.mjs`** is authoritative).
+20 -41
View File
@@ -2,60 +2,39 @@ 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 { buildManDb } from './scripts/build-man-db.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = join(__dirname, '../..')
const kernelBin = join(repoRoot, 'kernel/bin')
const seederKernelBin = join(repoRoot, 'packages/bare-os-seeder/kernel/bin')
const commands = [
'basename',
'cat',
'chmod',
'clear',
'crontab',
'date',
'dirname',
'echo',
'env',
'exit',
'false',
'grep',
'head',
'hdms',
'help',
'hostname',
'id',
'login',
'logout',
'ls',
'nl',
'pathchk',
'printenv',
'pwd',
'rm',
'savevault',
'seq',
'sleep',
'sort',
'tail',
'test',
'touch',
'true',
'tty',
'uname',
'wc',
'which',
'whoami'
]
/** Commands whose /bin script is preceded by extra library sources (no import in src). */
const preamble = {
sed: ['sed-engine.js'],
awk: ['awk-engine.js'],
man: ['man-render.js']
}
const commands = COREUTILS_COMMANDS
export async function build() {
await buildManDb()
const runtime = await readFile(join(__dirname, 'lib/runtime.js'), 'utf8')
await mkdir(kernelBin, { recursive: true })
await mkdir(seederKernelBin, { recursive: true })
for (const name of commands) {
let pre = ''
const extras = preamble[name]
if (extras) {
for (const f of extras) {
pre += (await readFile(join(__dirname, 'lib', f), 'utf8')) + '\n'
}
}
const body = await readFile(join(__dirname, 'src', `${name}.js`), 'utf8')
const out = runtime + '\n' + body
const out = runtime + '\n' + pre + body
await writeFile(join(kernelBin, name), out)
await writeFile(join(seederKernelBin, name), out)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
/**
* Authoritative list of /bin command names (single source for build.mjs and man DB).
* Keep sorted alphabetically.
*/
export const COREUTILS_COMMANDS = [
'awk',
'basename',
'cat',
'chgrp',
'chmod',
'chown',
'cksum',
'clear',
'cp',
'crontab',
'cut',
'date',
'dirname',
'du',
'echo',
'env',
'exit',
'false',
'find',
'getconf',
'grep',
'head',
'hdms',
'help',
'hostname',
'id',
'ln',
'login',
'logout',
'logname',
'ls',
'man',
'mkdir',
'mkfifo',
'mv',
'nl',
'od',
'pathchk',
'printenv',
'printf',
'pwd',
'readlink',
'rm',
'rmdir',
'savevault',
'sed',
'seq',
'sleep',
'sort',
'stat',
'tail',
'tee',
'test',
'time',
'touch',
'tr',
'true',
'tty',
'uname',
'wc',
'which',
'whoami',
'xargs'
]
/** Extra manual pages not built as /bin scripts on the system drive. */
export const MAN_EXTRA_PAGES = ['git', 'bare-os-shell']
@@ -0,0 +1,206 @@
/** Plain-text manual formatter (prepended before src/man.js; no import in /bin/man). */
function bareManParseWidth(env) {
const raw = env && env.MANWIDTH != null ? String(env.MANWIDTH).trim() : ''
const n = raw ? Number.parseInt(raw, 10) : 72
if (!Number.isFinite(n)) return 72
return Math.max(40, Math.min(n, 200))
}
function bareManUseAnsi(ctx) {
const env = ctx.env || {}
if (env.NO_COLOR != null && String(env.NO_COLOR) !== '') return false
const out = ctx.stdout
return Boolean(out && out.isTTY)
}
function bareManBold(s, on) {
if (!on) return s
return '\x1b[1m' + s + '\x1b[0m'
}
function bareManWrap(text, width) {
const words = String(text).replace(/\s+/g, ' ').trim().split(' ')
const lines = []
let cur = ''
for (const w of words) {
const next = cur ? cur + ' ' + w : w
if (next.length <= width) cur = next
else {
if (cur) lines.push(cur)
cur = w.length > width ? w.slice(0, width) : w
while (cur.length > width) {
lines.push(cur.slice(0, width))
cur = cur.slice(width)
}
}
}
if (cur) lines.push(cur)
return lines
}
/** Indent fixed-width command lines; hard-wrap only when longer than width. */
function bareManRenderExampleCode(code, width) {
const indent = ' '
const max = Math.max(20, width - indent.length)
const out = []
for (const line of String(code).split('\n')) {
if (line.length <= max) {
out.push(indent + line)
continue
}
let rest = line
while (rest.length > max) {
out.push(indent + rest.slice(0, max))
rest = rest.slice(max)
}
if (rest) out.push(indent + rest)
}
return out.join('\n')
}
/** Keep newlines; hard-wrap long lines only (for handbook / preformatted text). */
function bareManRenderPreserve(text, width) {
const indent = ' '
const max = Math.max(20, width - indent.length)
const out = []
for (const line of String(text).split('\n')) {
if (line === '') {
out.push('')
continue
}
let rest = line
while (rest.length > max) {
out.push(indent + rest.slice(0, max))
rest = rest.slice(max)
}
out.push(indent + rest)
}
return out.join('\n') + '\n'
}
function bareManFlushBlock(lines, width, prefixFirst, prefixRest) {
const out = []
let first = true
for (const line of lines) {
const wrapped = bareManWrap(line, width - (first ? prefixFirst.length : prefixRest.length))
for (let i = 0; i < wrapped.length; i++) {
const p = i === 0 && first ? prefixFirst : prefixRest
out.push(p + wrapped[i])
first = false
}
}
return out.join('\n') + '\n'
}
function bareManRenderPage(page, ctx, width) {
const ansi = bareManUseAnsi(ctx)
const H = (s) => bareManBold(s, ansi) + '\n'
let s = ''
s += H('NAME')
s += page.name + '(' + page.section + ') - ' + page.title + '\n\n'
s += H('SYNOPSIS')
for (const line of page.synopsis) {
s += ' ' + line + '\n'
}
s += '\n'
s += H('DESCRIPTION')
if (page.descriptionMode === 'preserve') {
s += bareManRenderPreserve(page.description, width)
} else {
s += bareManFlushBlock([page.description], width, '', ' ')
}
if (page.options && page.options.length) {
s += '\n' + H('OPTIONS')
for (const o of page.options) {
const head = o.flag + '\t'
const rest = o.meaning
s += bareManFlushBlock([rest], width, ' ' + head, ' ')
}
}
if (page.examples && page.examples.length) {
s += '\n' + H('EXAMPLES')
s +=
bareManFlushBlock(
[
'tl;dr-style snippets (like cheat.sh). Copy, adapt paths; pipelines are shell-simulated on Bare OS.'
],
width,
'',
' '
) + '\n'
for (const ex of page.examples) {
if (ex.caption) {
s +=
bareManFlushBlock(
['# ' + ex.caption],
width,
'',
' '
) + '\n'
}
s += bareManRenderExampleCode(ex.code, width) + '\n\n'
}
}
if (page.environment && page.environment.length) {
s += '\n' + H('ENVIRONMENT')
for (const e of page.environment) s += ' ' + e + '\n'
}
if (page.files && page.files.length) {
s += '\n' + H('FILES')
for (const f of page.files) s += ' ' + f + '\n'
}
if (page.exitStatus && page.exitStatus.length) {
s += '\n' + H('EXIT STATUS')
for (const e of page.exitStatus) s += ' ' + e + '\n'
}
if (page.diagnostics && page.diagnostics.length) {
s += '\n' + H('DIAGNOSTICS')
for (const d of page.diagnostics) s += ' ' + d + '\n'
}
if (page.builtins && page.builtins.length) {
s += '\n' + H('SHELL BUILTINS')
for (const b of page.builtins) {
s += '\n' + bareManBold(b.name, ansi) + '\n'
if (b.synopsis && b.synopsis.length) {
for (const line of b.synopsis) s += ' ' + line + '\n'
}
s += bareManFlushBlock([b.description], width, ' ', ' ')
if (b.options && b.options.length) {
for (const o of b.options) {
const head = o.flag + '\t'
s += bareManFlushBlock([o.meaning], width, ' ' + head, ' ')
}
}
if (b.examples && b.examples.length) {
s += '\n' + bareManBold(' Examples', ansi) + '\n'
for (const ex of b.examples) {
if (ex.caption) {
s +=
bareManFlushBlock(
['# ' + ex.caption],
width,
' ',
' '
) + '\n'
}
s += bareManRenderExampleCode(ex.code, width) + '\n\n'
}
}
}
}
if (page.bareOsNotes) {
s += '\n' + H('BARE OS NOTES')
s += bareManFlushBlock([page.bareOsNotes], width, '', ' ')
}
if (page.seeAlso && page.seeAlso.length) {
s += '\n' + H('SEE ALSO')
const parts = page.seeAlso.map((r) => r.name + '(' + r.section + ')')
s += ' ' + parts.join(', ') + '\n'
}
if (page.stub) {
s += '\n' + H('STATUS')
s += ' This command is a stub or intentionally limited on Bare OS.\n'
}
return s
}
@@ -0,0 +1,654 @@
/**
* POSIX-oriented sed engine for Bare OS /bin/sed (no import; concatenated before src/sed.js).
* Covers: -n -e -f -E, addresses (#,$,/re/,n,m,n~s), s///[ngp0-9], y///, d D p P n N,
* h H g G x, b t :, q, r w, =, l, a i c (backslash forms), comments, hold space, line continuations.
*/
/**
* @param {string} delim
* @param {string} script
* @param {number} start
* @returns {{ raw: string, end: number } | null}
*/
function bareSedReadDelimited(delim, script, start) {
if (delim === '\n' || delim === '') return null
let i = start
let out = ''
while (i < script.length) {
const c = script[i]
if (c === '\\' && i + 1 < script.length) {
out += script[i + 1]
i += 2
continue
}
if (c === delim) return { raw: out, end: i + 1 }
out += c
i++
}
return null
}
/**
* @param {string} reStr
* @param {boolean} extended
*/
function bareSedCompileRegex(reStr, extended) {
let flags = extended ? 'u' : 'u'
let body = reStr
if (extended) {
body = body
.replace(/\(\?#[^)]*\)/g, '')
.replace(/\(\?:/g, '(')
.replace(/\+/g, '{1,}')
.replace(/\?/g, '{0,1}')
}
try {
return new RegExp(body, flags)
} catch {
return /$^/
}
}
/**
* @param {string} repl
* @param {string[]} caps
* @param {string} match
* @param {string} line
* @param {number} off
*/
function bareSedApplyReplacement(repl, caps, match, line, off) {
let o = ''
for (let i = 0; i < repl.length; i++) {
const c = repl[i]
if (c === '&') {
o += match
continue
}
if (c === '\\' && i + 1 < repl.length) {
const n = repl[i + 1]
if (n >= '1' && n <= '9') {
o += caps[Number(n)] || ''
i++
continue
}
if (n === '&') {
o += '&'
i++
continue
}
if (n === '\\') {
o += '\\'
i++
continue
}
o += n
i++
continue
}
o += c
}
return o
}
function bareSedListLine(s) {
let o = ''
for (let i = 0; i < s.length; i++) {
const code = s.charCodeAt(i)
if (code === 10) o += '\\n'
else if (code === 9) o += '\\t'
else if (code === 92) o += '\\\\'
else if (code < 32 || code > 126) o += '\\' + code.toString(8).padStart(3, '0')
else o += s[i]
}
return o + '$'
}
/**
* @typedef {{ type: string, [k: string]: unknown }} BareSedCmd
*/
/**
* @param {string} script
* @param {boolean} extended
* @returns {BareSedCmd[]}
*/
function bareSedParseScript(script, extended) {
/** @type {BareSedCmd[]} */
const cmds = []
let i = 0
const len = script.length
function skipWs() {
while (i < len && /[ \t\r]/.test(script[i])) i++
}
function skipCommentLine() {
while (i < len && script[i] !== '\n') i++
if (i < len && script[i] === '\n') i++
}
function readAddr() {
skipWs()
if (i >= len) return null
const c = script[i]
if (c === '#') {
skipCommentLine()
return 'skip'
}
if (c === '0' && script[i + 1] >= '1' && script[i + 1] <= '9') {
/* fall through to number */
} else if (c >= '1' && c <= '9') {
let n = 0
while (i < len && script[i] >= '0' && script[i] <= '9') {
n = n * 10 + (script[i].charCodeAt(0) - 48)
i++
}
return { kind: 'num', n }
}
if (c === '$') {
i++
return { kind: 'last' }
}
if (c === '/' || c === '\\') {
let delim = c
let start = i + 1
if (c === '\\') {
delim = script[i + 1] || '/'
start = i + 2
}
const got = bareSedReadDelimited(delim, script, start)
if (!got) throw new Error('sed: unterminated address regex')
i = got.end
return { kind: 're', rx: bareSedCompileRegex(got.raw, extended) }
}
return null
}
function readAddrPair() {
const a = readAddr()
if (a === 'skip' || a === null) return a
skipWs()
if (i < len && script[i] === ',') {
i++
const b = readAddr()
if (b === 'skip' || b === null) throw new Error('sed: invalid address')
return { kind: 'range', a, b }
}
if (i < len && script[i] === '~') {
i++
let step = 0
while (i < len && script[i] >= '0' && script[i] <= '9') {
step = step * 10 + (script[i].charCodeAt(0) - 48)
i++
}
if (step < 1) step = 1
return { kind: 'step', a, step }
}
return a
}
while (i < len) {
skipWs()
if (i >= len) break
if (script[i] === '#' || (script[i] === '\n' && (i++, false))) {
if (script[i - 1] === '#') skipCommentLine()
else continue
continue
}
if (script[i] === ';') {
i++
continue
}
if (script[i] === '\n') {
i++
continue
}
let neg = false
if (script[i] === '!') {
neg = true
i++
skipWs()
}
const addr1 = readAddrPair()
if (addr1 === 'skip') continue
skipWs()
if (i >= len) break
const ch = script[i]
if (ch === '#') {
skipCommentLine()
continue
}
if (ch === ':') {
i++
let lab = ''
while (i < len && /[A-Za-z0-9_]/.test(script[i])) lab += script[i++]
cmds.push({ type: 'label', name: lab, neg, addr: addr1 })
continue
}
if (ch === 'b' || ch === 't') {
const ty = ch
i++
skipWs()
let lab = ''
while (i < len && /[A-Za-z0-9_]/.test(script[i])) lab += script[i++]
cmds.push({ type: ty, label: lab, neg, addr: addr1 })
continue
}
if (ch === 'r' || ch === 'w') {
const ty = ch
i++
skipWs()
let path = ''
while (i < len && script[i] !== '\n' && script[i] !== ';') path += script[i++]
path = path.replace(/[ \t]+$/, '')
cmds.push({ type: ty === 'r' ? 'readFile' : 'writeFile', path, neg, addr: addr1 })
continue
}
if (ch === 'a' || ch === 'i' || ch === 'c') {
const ty = ch
i++
skipWs()
if (i < len && script[i] === '\\') i++
let text = ''
while (i < len && script[i] !== '\n') text += script[i++]
if (i < len && script[i] === '\n') i++
while (i < len && script[i] === '\\') {
i++
let cont = ''
while (i < len && script[i] !== '\n') cont += script[i++]
text += '\n' + cont
if (i < len && script[i] === '\n') i++
}
cmds.push({ type: ty === 'a' ? 'append' : ty === 'i' ? 'insert' : 'change', text, neg, addr: addr1 })
continue
}
if (ch === 's') {
i++
const delim = script[i++]
const pat = bareSedReadDelimited(delim, script, i)
if (!pat) throw new Error('sed: unterminated s command')
i = pat.end
const rep = bareSedReadDelimited(delim, script, i)
if (!rep) throw new Error('sed: unterminated s replacement')
i = rep.end
/** @type {{ g?: boolean, p?: boolean, n?: number }} */
const fl = {}
while (i < len && /[gpn0-9]/.test(script[i])) {
const f = script[i++]
if (f === 'g') fl.g = true
else if (f === 'p') fl.p = true
else if (f >= '1' && f <= '9') fl.n = Number(f)
}
cmds.push({
type: 'subst',
rx: bareSedCompileRegex(pat.raw, extended),
rep: rep.raw,
flags: fl,
neg,
addr: addr1
})
continue
}
if (ch === 'y') {
i++
const delim = script[i++]
const from = bareSedReadDelimited(delim, script, i)
if (!from) throw new Error('sed: unterminated y')
i = from.end
const to = bareSedReadDelimited(delim, script, i)
if (!to) throw new Error('sed: unterminated y')
i = to.end
if (from.raw.length !== to.raw.length) throw new Error('sed: y strings must be same length')
cmds.push({ type: 'y', from: from.raw, to: to.raw, neg, addr: addr1 })
continue
}
const map = {
d: 'del',
D: 'delFirst',
p: 'print',
P: 'printFirst',
n: 'nextLine',
N: 'appendNext',
h: 'hold',
H: 'holdAppend',
g: 'get',
G: 'getAppend',
x: 'swap',
q: 'quit',
l: 'list',
'=': 'lineNum'
}
const ty = map[ch]
if (ty) {
i++
let count = 1
if (ty === 'quit' && i < len && script[i] >= '0' && script[i] <= '9') {
count = 0
while (i < len && script[i] >= '0' && script[i] <= '9') {
count = count * 10 + (script[i].charCodeAt(0) - 48)
i++
}
}
cmds.push({ type: ty, neg, addr: addr1, quitCode: count })
continue
}
throw new Error('sed: unknown command: ' + ch)
}
return cmds
}
/**
* @param {unknown} addr
* @param {number} lineNo
* @param {number} lastLine
* @param {string} ps
*/
function bareSedAddrSimple(addr, lineNo, lastLine, ps) {
if (addr == null) return true
if (typeof addr === 'object' && addr.kind === 'num') return lineNo === addr.n
if (typeof addr === 'object' && addr.kind === 'last') return lineNo === lastLine
if (typeof addr === 'object' && addr.kind === 're') {
addr.rx.lastIndex = 0
return addr.rx.test(ps)
}
if (typeof addr === 'object' && addr.kind === 'step') {
const an = addr.a
if (typeof an === 'object' && an.kind === 'num') {
return lineNo >= an.n && (lineNo - an.n) % addr.step === 0
}
return false
}
return false
}
/**
* @param {unknown} addr
* @param {number} lineNo
* @param {number} lastLine
* @param {string} ps
* @param {Map<number, { active: boolean }>} rangeStates
* @param {number} cmdIndex
*/
function bareSedAddrMatchFull(addr, lineNo, lastLine, ps, rangeStates, cmdIndex) {
if (addr == null) return true
if (typeof addr === 'object' && addr.kind === 'range') {
const a = addr.a
const b = addr.b
if (typeof a === 'object' && a.kind === 'num' && typeof b === 'object' && b.kind === 'num') {
return lineNo >= a.n && lineNo <= b.n
}
if (typeof a === 'object' && a.kind === 'num' && typeof b === 'object' && b.kind === 'last') {
return lineNo >= a.n && lineNo <= lastLine
}
if (typeof a === 'object' && a.kind === 'last' && typeof b === 'object' && b.kind === 'num') {
return lineNo >= lastLine && lineNo <= b.n
}
if (typeof a === 'object' && a.kind === 're' && typeof b === 'object' && b.kind === 're') {
let st = rangeStates.get(cmdIndex)
if (!st) {
st = { active: false }
rangeStates.set(cmdIndex, st)
}
a.rx.lastIndex = 0
b.rx.lastIndex = 0
const hitA = a.rx.test(ps)
const hitB = b.rx.test(ps)
if (!st.active && hitA) st.active = true
const inRange = st.active
if (st.active && hitB) st.active = false
return inRange
}
if (typeof a === 'object' && a.kind === 'num' && typeof b === 'object' && b.kind === 're') {
let st = rangeStates.get(cmdIndex)
if (!st) {
st = { active: false }
rangeStates.set(cmdIndex, st)
}
if (lineNo === a.n) st.active = true
b.rx.lastIndex = 0
const hitB = b.rx.test(ps)
const inRange = st.active
if (st.active && hitB) st.active = false
return inRange
}
if (typeof a === 'object' && a.kind === 're' && typeof b === 'object' && b.kind === 'num') {
let st = rangeStates.get(cmdIndex)
if (!st) {
st = { active: false }
rangeStates.set(cmdIndex, st)
}
a.rx.lastIndex = 0
if (!st.active && a.rx.test(ps)) st.active = true
const inRange = st.active
if (st.active && lineNo >= b.n) st.active = false
return inRange
}
return false
}
return bareSedAddrSimple(addr, lineNo, lastLine, ps)
}
function bareSedMatchAddr(addr, neg, lineNo, lastLine, ps, rangeStates, cmdIndex) {
const m = bareSedAddrMatchFull(addr, lineNo, lastLine, ps, rangeStates, cmdIndex)
return neg ? !m : m
}
/**
* @param {string[]} lines
* @param {string[]} scripts
* @param {{ silent?: boolean, extended?: boolean, readFile?: (p: string) => string | null, writeFile?: (p: string, chunk: string) => void, lastLineHint?: number }} opts
* @returns {string}
*/
function bareSedRun(lines, scripts, opts) {
const silent = !!opts.silent
const extended = !!opts.extended
const readF = opts.readFile || (() => null)
const writeF = opts.writeFile || (() => {})
const fullScript = scripts.join('\n')
const cmds = bareSedParseScript(fullScript.replace(/\\\n/g, ''), extended)
/** @type {Record<string, number>} */
const labels = {}
for (let ci = 0; ci < cmds.length; ci++) {
if (cmds[ci].type === 'label') labels[/** @type {string} */ (cmds[ci].name)] = ci
}
const lastLine = opts.lastLineHint != null ? opts.lastLineHint : lines.length
/** @type {string[]} */
const out = []
let hold = ''
let quit = 0
let lastSubst = false
/** @type {Map<number, { active: boolean }>} */
const rangeStates = new Map()
function emit(s) {
out.push(s)
}
let lineIdx = 0
while (lineIdx < lines.length && quit === 0) {
let ps = lines[lineIdx]
const lineNo = lineIdx + 1
let autoPrint = !silent
let delLine = false
let nextRead = false
let ci = 0
while (ci < cmds.length && quit === 0) {
const cmd = cmds[ci]
if (cmd.type === 'label') {
ci++
continue
}
const addr = cmd.addr
if (!bareSedMatchAddr(addr, !!cmd.neg, lineNo, lastLine, ps, rangeStates, ci)) {
ci++
continue
}
switch (cmd.type) {
case 'subst': {
lastSubst = false
const rx = /** @type {RegExp} */ (cmd.rx)
const rep = /** @type {string} */ (cmd.rep)
const fl = /** @type {{ g?: boolean, p?: boolean, n?: number }} */ (cmd.flags)
let count = 0
let res = ''
let pos = 0
const g = !!fl.g
const wantN = fl.n != null ? fl.n : g ? Infinity : 1
let replCount = 0
rx.lastIndex = 0
let m
const str = ps
while ((m = rx.exec(str)) && replCount < wantN) {
res += str.slice(pos, m.index)
const caps = m.map((x) => (x == null ? '' : String(x)))
res += bareSedApplyReplacement(rep, caps, m[0], str, m.index)
pos = m.index + m[0].length
count++
replCount++
lastSubst = true
if (!g) break
if (m[0].length === 0) {
rx.lastIndex++
if (rx.lastIndex > str.length) break
}
}
if (count) {
ps = res + str.slice(pos)
if (fl.p) emit(ps + '\n')
}
break
}
case 'y': {
const from = /** @type {string} */ (cmd.from)
const to = /** @type {string} */ (cmd.to)
const map = {}
for (let j = 0; j < from.length; j++) map[from[j]] = to[j]
let ns = ''
for (let j = 0; j < ps.length; j++) ns += map[ps[j]] != null ? map[ps[j]] : ps[j]
ps = ns
break
}
case 'del':
delLine = true
autoPrint = false
break
case 'delFirst': {
const nl = ps.indexOf('\n')
if (nl === -1) {
delLine = true
autoPrint = false
} else ps = ps.slice(nl + 1)
ci = -1
break
}
case 'print':
emit(ps + '\n')
break
case 'printFirst': {
const nl = ps.indexOf('\n')
emit((nl === -1 ? ps : ps.slice(0, nl)) + '\n')
break
}
case 'nextLine':
if (autoPrint && !silent) emit(ps + '\n')
lineIdx++
nextRead = true
ci = cmds.length
break
case 'appendNext':
lineIdx++
if (lineIdx < lines.length) ps += '\n' + lines[lineIdx]
else delLine = true
break
case 'hold':
hold = ps
break
case 'holdAppend':
hold += (hold ? '\n' : '') + ps
break
case 'get':
ps = hold
break
case 'getAppend':
ps += '\n' + hold
break
case 'swap': {
const t = ps
ps = hold
hold = t
break
}
case 'quit':
if (autoPrint && !silent) emit(ps + '\n')
quit = /** @type {number} */ (cmd.quitCode) || 0
break
case 'list':
emit(bareSedListLine(ps) + '\n')
break
case 'lineNum':
emit(String(lineNo) + '\n')
break
case 'readFile': {
const text = readF(/** @type {string} */ (cmd.path))
if (text) emit(text.endsWith('\n') ? text : text + '\n')
break
}
case 'writeFile':
writeF(/** @type {string} */ (cmd.path), ps + '\n')
break
case 'append':
emit(/** @type {string} */ (cmd.text) + '\n')
break
case 'insert':
/* handled as emit before line — approximated by prepending to output before autoPrint */
out.push(/** @type {string} */ (cmd.text) + '\n')
break
case 'change':
autoPrint = false
emit(/** @type {string} */ (cmd.text) + '\n')
delLine = true
break
case 'b': {
const lab = /** @type {string} */ (cmd.label)
if (lab && labels[lab] != null) ci = labels[lab]
break
}
case 't': {
if (lastSubst) {
const lab = /** @type {string} */ (cmd.label)
if (lab && labels[lab] != null) ci = labels[lab]
lastSubst = false
}
break
}
default:
break
}
ci++
if (delLine) break
if (nextRead) break
}
if (quit) break
if (nextRead) continue
if (!delLine && autoPrint) emit(ps + '\n')
lineIdx++
}
return out.join('')
}
@@ -0,0 +1,45 @@
{
"name": "awk",
"section": 1,
"title": "pattern scanning and processing language",
"synopsis": [
"awk [OPTION]... [OPERAND]..."
],
"description": "Pattern-directed scanning and processing. Engine in lib/awk-engine.js; not full POSIX awk.",
"options": [],
"keywords": [
"awk",
"pattern",
"field",
"script"
],
"seeAlso": [
{
"name": "sed",
"section": 1
},
{
"name": "grep",
"section": 1
}
],
"bareOsNotes": "See handbook ch.9 for divergence from Issue 7.",
"examples": [
{
"caption": "print column 1",
"code": "awk '{print $1}' file.txt"
},
{
"caption": "field separator",
"code": "awk -F: '{print $1}' /etc/passwd"
},
{
"caption": "sum numbers in first column",
"code": "awk '{s+=$1} END{print s}' nums.txt"
},
{
"caption": "lines matching /re/",
"code": "awk '/error/{print NR\": \"$0}' log.txt"
}
]
}
@@ -0,0 +1,142 @@
{
"name": "bare-os-shell",
"section": 1,
"title": "Bare OS interactive shell builtins",
"synopsis": [
"# builtins only — no full POSIX sh grammar"
],
"description": "The line-at-a-time shell supports aliases, simple pipelines (simulated), redirection, and the builtins below. Compound commands (if, for, while) are not available.",
"options": [],
"aliases": [
"sh-builtins"
],
"keywords": [
"shell",
"builtin",
"cd",
"export",
"alias",
"bare-os-shell",
"sh-builtins"
],
"builtins": [
{
"name": "alias",
"synopsis": [
"alias",
"alias name=value ...",
"unalias name ..."
],
"description": "Define or list command aliases. unalias removes definitions."
},
{
"name": "cd",
"synopsis": [
"cd [DIR]"
],
"description": "Change working directory via vfs.chdir; default is HOME."
},
{
"name": "export",
"synopsis": [
"export NAME=value ..."
],
"description": "Set environment variables visible to child /bin invocations."
},
{
"name": "unset",
"synopsis": [
"unset NAME ..."
],
"description": "Remove variables; readonly names cannot be unset."
},
{
"name": "readonly",
"synopsis": [
"readonly NAME[=value] ..."
],
"description": "Mark variables read-only."
},
{
"name": "umask",
"synopsis": [
"umask [octal]"
],
"description": "Show or set shell file creation mask (stored in env UMASK)."
},
{
"name": "command",
"synopsis": [
"command -v|-V NAME",
"command ARGV..."
],
"description": "Resolve or run a command without using shell functions (none) or aliases for -v/-V."
},
{
"name": "type",
"synopsis": [
"type NAME"
],
"description": "Report whether NAME is a builtin or a path under PATH."
},
{
"name": "login / logout",
"synopsis": [
"login [--new] passphrase...",
"logout [--save]"
],
"description": "Identity unlock/register and session teardown; require booter hooks."
},
{
"name": ":",
"synopsis": [
":"
],
"description": "No-op builtin."
},
{
"name": "exit",
"synopsis": [
"exit [n]"
],
"description": "Request booter exit with status n (builtin path)."
}
],
"seeAlso": [
{
"name": "help",
"section": 1
},
{
"name": "man",
"section": 1
}
],
"bareOsNotes": "Pipelines do not use OS pipes; see handbook ch.4 and ch.9.",
"examples": [
{
"caption": "pipeline (simulated)",
"code": "ls -1 /bin | grep man"
},
{
"caption": "redirect out",
"code": "echo hi > ~/hello.txt"
},
{
"caption": "append",
"code": "date >> ~/log.txt"
},
{
"caption": "alias + use",
"code": "alias ll='ls -la'\nll ~"
},
{
"caption": "export for children",
"code": "export EDITOR=ed\nman ls"
},
{
"caption": "temp var for one command",
"code": "PATH=/bin man which"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "basename",
"section": 1,
"title": "strip directory and suffix from pathnames",
"synopsis": [
"basename [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of strip directory and suffix from pathnames. Full behavior is defined in packages/bare-os-coreutils/src/basename.js.",
"options": [],
"keywords": [
"basename",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "strip directory",
"code": "basename /home/user/docs/readme.md"
},
{
"caption": "strip suffix",
"code": "basename -s .md /path/readme.md"
}
]
}
@@ -0,0 +1,29 @@
{
"name": "cat",
"section": 1,
"title": "concatenate and print files",
"synopsis": [
"cat [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of concatenate and print files. Full behavior is defined in packages/bare-os-coreutils/src/cat.js.",
"options": [],
"keywords": [
"cat",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "stdout several files",
"code": "cat a.txt b.txt"
},
{
"caption": "number lines (use nl)",
"code": "cat -n file.txt # if supported; else nl file"
},
{
"caption": "here-string via echo pipe",
"code": "echo hello | cat"
}
]
}
@@ -0,0 +1,27 @@
{
"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.",
"examples": [
{
"caption": "not supported — use identity model",
"code": "# chgrp is a stub; group is display metadata only"
}
]
}
@@ -0,0 +1,41 @@
{
"name": "chmod",
"section": 1,
"title": "change file mode bits",
"synopsis": [
"chmod MODE FILE...",
"MODE is octal (e.g. 644) or symbolic (e.g. u+rw)"
],
"description": "Sets file mode bits on the VFS. Supports POSIX-style symbolic modes (u/g/o/a, +/-/=, rwxX) and octal modes.",
"options": [],
"keywords": [
"chmod",
"mode",
"permission",
"octal",
"symbolic"
],
"diagnostics": [
"chmod: No such file",
"chmod: invalid mode"
],
"bareOsNotes": "Applies to Hyperdrive metadata; not a host inode.",
"examples": [
{
"caption": "octal",
"code": "chmod 644 ~/.profile"
},
{
"caption": "recursive-ish (run find + chmod per file)",
"code": "find . -type f -name \"*.sh\" -print"
},
{
"caption": "symbolic user bits",
"code": "chmod u+x script.sh"
},
{
"caption": "all read, owner write",
"code": "chmod a+r,u+w shared.txt"
}
]
}
@@ -0,0 +1,27 @@
{
"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.",
"examples": [
{
"caption": "not supported",
"code": "# chown stub — see man identity / login"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "cksum",
"section": 1,
"title": "write file checksums and sizes",
"synopsis": [
"cksum [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of write file checksums and sizes. Full behavior is defined in packages/bare-os-coreutils/src/cksum.js.",
"options": [],
"keywords": [
"cksum",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "checksum file",
"code": "cksum iso.img"
},
{
"caption": "verify pipeline",
"code": "cat f | cksum"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "clear",
"section": 1,
"title": "clear the terminal screen",
"synopsis": [
"clear [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of clear the terminal screen. Full behavior is defined in packages/bare-os-coreutils/src/clear.js.",
"options": [],
"keywords": [
"clear",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "wipe screen",
"code": "clear"
}
]
}
@@ -0,0 +1,29 @@
{
"name": "cp",
"section": 1,
"title": "copy files",
"synopsis": [
"cp [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of copy files. Full behavior is defined in packages/bare-os-coreutils/src/cp.js.",
"options": [],
"keywords": [
"cp",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "copy file",
"code": "cp src.txt dest.txt"
},
{
"caption": "into directory",
"code": "cp a b c ~/backup/"
},
{
"caption": "preserve implied (if implemented)",
"code": "cp -R proj proj.bak"
}
]
}
@@ -0,0 +1,29 @@
{
"name": "crontab",
"section": 1,
"title": "user crontab manipulation",
"synopsis": [
"crontab [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of user crontab manipulation. Full behavior is defined in packages/bare-os-coreutils/src/crontab.js.",
"options": [],
"keywords": [
"crontab",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "list jobs",
"code": "crontab -l"
},
{
"caption": "install from file",
"code": "crontab ~/.crontab"
},
{
"caption": "remove all",
"code": "crontab -r"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "cut",
"section": 1,
"title": "cut out selected fields of each line",
"synopsis": [
"cut [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of cut out selected fields of each line. Full behavior is defined in packages/bare-os-coreutils/src/cut.js.",
"options": [],
"keywords": [
"cut",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "fields by delimiter",
"code": "cut -d: -f1,3 /etc/passwd"
},
{
"caption": "characters",
"code": "cut -c1-16 file.txt"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "date",
"section": 1,
"title": "display or set date and time",
"synopsis": [
"date [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of display or set date and time. Full behavior is defined in packages/bare-os-coreutils/src/date.js.",
"options": [],
"keywords": [
"date",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "RFC-ish output",
"code": "date"
},
{
"caption": "epoch seconds",
"code": "date +%s"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "dirname",
"section": 1,
"title": "return directory portion of a pathname",
"synopsis": [
"dirname [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of return directory portion of a pathname. Full behavior is defined in packages/bare-os-coreutils/src/dirname.js.",
"options": [],
"keywords": [
"dirname",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "parent path",
"code": "dirname /a/b/c.txt"
},
{
"caption": "compose with basename",
"code": "p=/x/y/z; echo $(dirname $p)/$(basename $p)"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "du",
"section": 1,
"title": "estimate file space usage",
"synopsis": [
"du [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of estimate file space usage. Full behavior is defined in packages/bare-os-coreutils/src/du.js.",
"options": [],
"keywords": [
"du",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "sizes under cwd",
"code": "du ."
},
{
"caption": "human (if supported)",
"code": "du -h ~"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "echo",
"section": 1,
"title": "write arguments to standard output",
"synopsis": [
"echo [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of write arguments to standard output. Full behavior is defined in packages/bare-os-coreutils/src/echo.js.",
"options": [],
"keywords": [
"echo",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "literal",
"code": "echo hello world"
},
{
"caption": "no newline (if -n supported)",
"code": "echo -n OK"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "env",
"section": 1,
"title": "set the environment for command invocation",
"synopsis": [
"env [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of set the environment for command invocation. Full behavior is defined in packages/bare-os-coreutils/src/env.js.",
"options": [],
"keywords": [
"env",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "print environment",
"code": "env"
},
{
"caption": "run with override",
"code": "env PATH=/bin:/usr/bin man ls"
}
]
}
@@ -0,0 +1,26 @@
{
"name": "exit",
"section": 1,
"title": "exit the shell or booter session",
"synopsis": [
"exit [status]"
],
"description": "When run as /bin/exit, requests the booter to end the session via ctx.requestBooterExit. Status defaults to 0.",
"options": [],
"keywords": [
"exit",
"bare-os",
"coreutils"
],
"bareOsNotes": "Also available as a shell builtin with different wiring.",
"examples": [
{
"caption": "leave session with status",
"code": "exit 0"
},
{
"caption": "from script",
"code": "/bin/exit 42"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "false",
"section": 1,
"title": "return false value",
"synopsis": [
"false [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of return false value. Full behavior is defined in packages/bare-os-coreutils/src/false.js.",
"options": [],
"keywords": [
"false",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "force failure in pipeline tests",
"code": "false; echo $?"
}
]
}
@@ -0,0 +1,35 @@
{
"name": "find",
"section": 1,
"title": "find files",
"synopsis": [
"find [PATH...] [EXPRESSION]"
],
"description": "Walks directories and applies expressions (-name, -type, -print, -maxdepth, logical -and/-or/-not).",
"options": [],
"keywords": [
"find",
"directory",
"walk",
"search"
],
"bareOsNotes": "Expression syntax is a simplified subset.",
"examples": [
{
"caption": "files by name glob",
"code": "find . -name \"*.js\""
},
{
"caption": "directories only",
"code": "find . -type d"
},
{
"caption": "max depth",
"code": "find . -maxdepth 2 -type f"
},
{
"caption": "OR names",
"code": "find . \\( -name \"*.c\" -o -name \"*.h\" \\)"
}
]
}
@@ -0,0 +1,24 @@
{
"name": "getconf",
"section": 1,
"title": "get configuration values",
"synopsis": [
"getconf [OPTION]... [OPERAND]..."
],
"description": "Host sysconf-style values are not exposed. The command prints an error.",
"options": [],
"keywords": [
"getconf",
"bare-os",
"coreutils",
"stub"
],
"stub": true,
"bareOsNotes": "Stub only; no kernel sysconf surface.",
"examples": [
{
"caption": "stub",
"code": "# getconf PATH_MAX — not available on Bare OS"
}
]
}
@@ -0,0 +1,56 @@
{
"name": "git",
"section": 1,
"title": "Bare OS git front-end (isomorphic-git)",
"synopsis": [
"git [-C dir] <subcommand> [ARGUMENTS...]"
],
"description": "Runs isomorphic-git against the VFS-backed adapter. Remote HTTP(S) uses BARE_OS_GIT_HTTP when set; otherwise Pear bare module fetch.",
"options": [
{
"flag": "-C dir",
"meaning": "Run as if git was started in dir"
}
],
"environment": [
"BARE_OS_GIT_HTTP — optional fetch implementation for remotes",
"GIT_* — standard hints where supported"
],
"keywords": [
"git",
"version control",
"repository",
"clone",
"commit",
"isomorphic-git"
],
"bareOsNotes": "Not a separate /bin script; booter delegates argv[0]=git to git-cli.js.",
"seeAlso": [
{
"name": "bare-os-shell",
"section": 1
}
],
"examples": [
{
"caption": "new repo",
"code": "git init -C ~/myrepo"
},
{
"caption": "status",
"code": "git -C ~/myrepo status"
},
{
"caption": "clone over HTTP (needs remote + fetch)",
"code": "git clone https://example.com/repo.git ~/work/repo"
},
{
"caption": "config local",
"code": "git -C ~/myrepo config user.email \"[email protected]\""
},
{
"caption": "log one line",
"code": "git -C ~/myrepo log --oneline -5"
}
]
}
@@ -0,0 +1,107 @@
{
"name": "grep",
"section": 1,
"title": "pattern matching utility",
"synopsis": [
"grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]"
],
"description": "Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.",
"options": [
{
"flag": "-E",
"meaning": "Extended regex (accepted; patterns use JS RegExp)"
},
{
"flag": "-F",
"meaning": "Fixed string match"
},
{
"flag": "-i",
"meaning": "Ignore case"
},
{
"flag": "-v",
"meaning": "Invert match"
},
{
"flag": "-n",
"meaning": "Prefix lines with line number"
},
{
"flag": "-c",
"meaning": "Count matching lines only"
},
{
"flag": "-l",
"meaning": "List files with matches"
},
{
"flag": "-q",
"meaning": "Quiet (exit status only)"
},
{
"flag": "-s",
"meaning": "Suppress error messages"
},
{
"flag": "-H / -h",
"meaning": "Force / suppress filename prefix"
},
{
"flag": "-e pat",
"meaning": "Specify pattern"
},
{
"flag": "-f file",
"meaning": "Read patterns from file"
}
],
"keywords": [
"grep",
"search",
"regex",
"pattern",
"filter"
],
"seeAlso": [
{
"name": "sed",
"section": 1
},
{
"name": "awk",
"section": 1
}
],
"bareOsNotes": "UTF-16 strings and JS regex differ from strict POSIX/GNU.",
"examples": [
{
"caption": "recursive feel (grep each file)",
"code": "grep -n error *.log"
},
{
"caption": "case insensitive",
"code": "grep -i todo NOTES.md"
},
{
"caption": "invert (lines without)",
"code": "grep -v '^#' config"
},
{
"caption": "fixed string (no regex)",
"code": "grep -F \"v1.0\" CHANGES"
},
{
"caption": "count matches",
"code": "grep -c FAIL build.log"
},
{
"caption": "only filenames",
"code": "grep -l main *.js"
},
{
"caption": "multiple patterns",
"code": "grep -e foo -e bar file.txt"
}
]
}
@@ -0,0 +1,26 @@
{
"name": "hdms",
"section": 1,
"title": "Hyperswarm distributed map store",
"synopsis": [
"hdms [OPTION]... [OPERAND]..."
],
"description": "Invokes ctx.runHdms when the booter provides HDMS integration; otherwise prints unavailable.",
"options": [],
"keywords": [
"hdms",
"hyperswarm",
"map"
],
"bareOsNotes": "Optional booter capability.",
"examples": [
{
"caption": "when booter wires HDMS",
"code": "hdms ls /mnt"
},
{
"caption": "otherwise",
"code": "# prints unavailable without ctx.runHdms"
}
]
}
@@ -0,0 +1,29 @@
{
"name": "head",
"section": 1,
"title": "copy the first part of files",
"synopsis": [
"head [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of copy the first part of files. Full behavior is defined in packages/bare-os-coreutils/src/head.js.",
"options": [],
"keywords": [
"head",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "first 10 lines",
"code": "head /etc/os-release"
},
{
"caption": "first N",
"code": "head -n 50 big.log"
},
{
"caption": "stdin",
"code": "cat long.txt | head"
}
]
}
@@ -0,0 +1,36 @@
{
"name": "help",
"section": 1,
"title": "Bare OS help summary",
"synopsis": [
"help"
],
"description": "Prints a one-screen summary of shell builtins and /bin command names. Use man for long-form documentation.",
"options": [],
"keywords": [
"help",
"summary",
"builtins",
"commands"
],
"seeAlso": [
{
"name": "man",
"section": 1
},
{
"name": "bare-os-shell",
"section": 1
}
],
"examples": [
{
"caption": "quick index",
"code": "help"
},
{
"caption": "then deep dive",
"code": "man grep"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "hostname",
"section": 1,
"title": "set or print hostname",
"synopsis": [
"hostname [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of set or print hostname. Full behavior is defined in packages/bare-os-coreutils/src/hostname.js.",
"options": [],
"keywords": [
"hostname",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "show host",
"code": "hostname"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "id",
"section": 1,
"title": "return user identity",
"synopsis": [
"id [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of return user identity. Full behavior is defined in packages/bare-os-coreutils/src/id.js.",
"options": [],
"keywords": [
"id",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "who am I numerically",
"code": "id"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "ln",
"section": 1,
"title": "link files",
"synopsis": [
"ln [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of link files. Full behavior is defined in packages/bare-os-coreutils/src/ln.js.",
"options": [],
"keywords": [
"ln",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "symlink",
"code": "ln -s target name"
},
{
"caption": "hard link (if supported)",
"code": "ln file linkname"
}
]
}
@@ -0,0 +1,31 @@
{
"name": "login",
"section": 1,
"title": "begin a session on the system",
"synopsis": [
"login [OPTION]... [OPERAND]..."
],
"description": "When invoked from /bin, behavior aligns with session identity hooks (see booter). Prefer the shell builtin for passphrase entry.",
"options": [],
"keywords": [
"login",
"identity",
"passphrase"
],
"seeAlso": [
{
"name": "logout",
"section": 1
}
],
"examples": [
{
"caption": "unlock existing identity",
"code": "login my passphrase words here"
},
{
"caption": "register new",
"code": "login --new first time passphrase"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "logname",
"section": 1,
"title": "return the user's login name",
"synopsis": [
"logname [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of return the user's login name. Full behavior is defined in packages/bare-os-coreutils/src/logname.js.",
"options": [],
"keywords": [
"logname",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "login name",
"code": "logname"
}
]
}
@@ -0,0 +1,30 @@
{
"name": "logout",
"section": 1,
"title": "end session (save vault)",
"synopsis": [
"logout [OPTION]... [OPERAND]..."
],
"description": "Ends session; may persist vault depending on booter and flags.",
"options": [],
"keywords": [
"logout",
"session"
],
"seeAlso": [
{
"name": "login",
"section": 1
}
],
"examples": [
{
"caption": "end session",
"code": "logout"
},
{
"caption": "save vault hint",
"code": "logout --save"
}
]
}
@@ -0,0 +1,44 @@
{
"name": "ls",
"section": 1,
"title": "list directory contents",
"synopsis": [
"ls [-1al] [FILE...]"
],
"description": "Lists directory contents. Long format shows mode, links, owner, group, size, mtime, and symlink targets.",
"options": [
{
"flag": "-a",
"meaning": "Include names starting with ."
},
{
"flag": "-l",
"meaning": "Long listing"
},
{
"flag": "-1",
"meaning": "One name per line (short format)"
}
],
"keywords": [
"ls",
"list",
"directory",
"dir"
],
"bareOsNotes": "Hides .bareos_empty marker like other tools.",
"examples": [
{
"caption": "long + hidden",
"code": "ls -la ~"
},
{
"caption": "one per line",
"code": "ls -1 /bin | head"
},
{
"caption": "multiple paths",
"code": "ls /bin /etc"
}
]
}
@@ -0,0 +1,75 @@
{
"name": "man",
"section": 1,
"title": "display on-line manual pages",
"synopsis": [
"man [-k keyword] [-f name] [-l] [[section] name]",
"man reads /share/man/man.json on the system drive."
],
"description": "Displays manual pages from the merged JSON database. Section 1 only in this release.",
"options": [
{
"flag": "-k, --apropos",
"meaning": "Search keywords and titles (substring)"
},
{
"flag": "-f, --whatis",
"meaning": "One-line description for exact name"
},
{
"flag": "-l, --list",
"meaning": "List all manual page names"
}
],
"keywords": [
"man",
"manual",
"help",
"documentation",
"apropos",
"whatis",
"cheat",
"examples"
],
"environment": [
"MANWIDTH — wrap width (default 72, min 40)",
"NO_COLOR — disable bold headings on TTY"
],
"seeAlso": [
{
"name": "help",
"section": 1
}
],
"bareOsNotes": "No troff; no embedded DB fallback in v1.",
"examples": [
{
"caption": "open page",
"code": "man sed"
},
{
"caption": "handbook TOC (section 7)",
"code": "man handbook"
},
{
"caption": "handbook chapter by section",
"code": "man 7 handbook-01-introduction"
},
{
"caption": "apropos",
"code": "man -k copy"
},
{
"caption": "whatis",
"code": "man -f grep"
},
{
"caption": "all pages",
"code": "man -l"
},
{
"caption": "narrow terminal",
"code": "MANWIDTH=64 man awk"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "mkdir",
"section": 1,
"title": "make directories",
"synopsis": [
"mkdir [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of make directories. Full behavior is defined in packages/bare-os-coreutils/src/mkdir.js.",
"options": [],
"keywords": [
"mkdir",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "one dir",
"code": "mkdir proj"
},
{
"caption": "parents",
"code": "mkdir -p a/b/c"
}
]
}
@@ -0,0 +1,24 @@
{
"name": "mkfifo",
"section": 1,
"title": "make FIFO special files",
"synopsis": [
"mkfifo [OPTION]... [OPERAND]..."
],
"description": "FIFO special files are not implemented on Hyperdrive. The command reports failure.",
"options": [],
"keywords": [
"mkfifo",
"bare-os",
"coreutils",
"stub"
],
"stub": true,
"bareOsNotes": "Documented stub; no real pipes as kernel objects.",
"examples": [
{
"caption": "stub",
"code": "# FIFOs not on Hyperdrive — use shell pipelines"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "mv",
"section": 1,
"title": "move or rename files",
"synopsis": [
"mv [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of move or rename files. Full behavior is defined in packages/bare-os-coreutils/src/mv.js.",
"options": [],
"keywords": [
"mv",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "rename",
"code": "mv old.txt new.txt"
},
{
"caption": "into dir",
"code": "mv *.txt ~/inbox/"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "nl",
"section": 1,
"title": "line numbering utility",
"synopsis": [
"nl [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of line numbering utility. Full behavior is defined in packages/bare-os-coreutils/src/nl.js.",
"options": [],
"keywords": [
"nl",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "number all lines",
"code": "nl README.md"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "od",
"section": 1,
"title": "octal dump",
"synopsis": [
"od [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of octal dump. Full behavior is defined in packages/bare-os-coreutils/src/od.js.",
"options": [],
"keywords": [
"od",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "hex dump vibe",
"code": "od -c file.bin | head"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "pathchk",
"section": 1,
"title": "check pathname portability",
"synopsis": [
"pathchk [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of check pathname portability. Full behavior is defined in packages/bare-os-coreutils/src/pathchk.js.",
"options": [],
"keywords": [
"pathchk",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "portable path check",
"code": "pathchk -p \"$HOME/file name\""
}
]
}
@@ -0,0 +1,25 @@
{
"name": "printenv",
"section": 1,
"title": "print environment variables",
"synopsis": [
"printenv [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of print environment variables. Full behavior is defined in packages/bare-os-coreutils/src/printenv.js.",
"options": [],
"keywords": [
"printenv",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "one variable",
"code": "printenv HOME"
},
{
"caption": "all",
"code": "printenv"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "printf",
"section": 1,
"title": "format and print",
"synopsis": [
"printf [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of format and print. Full behavior is defined in packages/bare-os-coreutils/src/printf.js.",
"options": [],
"keywords": [
"printf",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "format",
"code": "printf \"hex=%x dec=%d\\n\" 255 255"
},
{
"caption": "no newline",
"code": "printf \"%s\" OK"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "pwd",
"section": 1,
"title": "return working directory name",
"synopsis": [
"pwd [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of return working directory name. Full behavior is defined in packages/bare-os-coreutils/src/pwd.js.",
"options": [],
"keywords": [
"pwd",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "where am I",
"code": "pwd"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "readlink",
"section": 1,
"title": "print symbolic link targets",
"synopsis": [
"readlink [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of print symbolic link targets. Full behavior is defined in packages/bare-os-coreutils/src/readlink.js.",
"options": [],
"keywords": [
"readlink",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "symlink target",
"code": "readlink ~/.config"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "rm",
"section": 1,
"title": "remove files",
"synopsis": [
"rm [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of remove files. Full behavior is defined in packages/bare-os-coreutils/src/rm.js.",
"options": [],
"keywords": [
"rm",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "file",
"code": "rm tmp.txt"
},
{
"caption": "tree",
"code": "rm -rf build/"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "rmdir",
"section": 1,
"title": "remove empty directories",
"synopsis": [
"rmdir [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of remove empty directories. Full behavior is defined in packages/bare-os-coreutils/src/rmdir.js.",
"options": [],
"keywords": [
"rmdir",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "empty dir",
"code": "rmdir olddir"
}
]
}
@@ -0,0 +1,28 @@
{
"name": "savevault",
"section": 1,
"title": "encrypt snapshot of personal drive",
"synopsis": [
"savevault [OPTION]... [OPERAND]..."
],
"description": "Encrypts a copy of the personal drive under /.bare/vault/ when identity services are available.",
"options": [],
"keywords": [
"savevault",
"vault",
"encrypt",
"backup"
],
"seeAlso": [
{
"name": "login",
"section": 1
}
],
"examples": [
{
"caption": "snapshot encrypted vault",
"code": "savevault"
}
]
}
@@ -0,0 +1,49 @@
{
"name": "sed",
"section": 1,
"title": "stream editor",
"synopsis": [
"sed [OPTION]... [OPERAND]..."
],
"description": "Stream editor with a subset of POSIX sed. Large engine is vendored in lib/sed-engine.js.",
"options": [],
"keywords": [
"sed",
"stream",
"edit",
"substitute"
],
"seeAlso": [
{
"name": "awk",
"section": 1
},
{
"name": "grep",
"section": 1
}
],
"bareOsNotes": "JavaScript implementation; edge cases differ from GNU sed.",
"examples": [
{
"caption": "substitute first per line",
"code": "sed 's/foo/bar/' file.txt"
},
{
"caption": "global per line",
"code": "sed 's/ //g' spaced.txt"
},
{
"caption": "in-place (if supported)",
"code": "sed -i.bak 's/^/# /' f.cfg"
},
{
"caption": "print line 5 only",
"code": "sed -n '5p' file"
},
{
"caption": "delete blank lines",
"code": "sed '/^$/d' file"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "seq",
"section": 1,
"title": "print sequences of numbers",
"synopsis": [
"seq [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of print sequences of numbers. Full behavior is defined in packages/bare-os-coreutils/src/seq.js.",
"options": [],
"keywords": [
"seq",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "1..10",
"code": "seq 1 10"
},
{
"caption": "step",
"code": "seq 0 2 20"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "sleep",
"section": 1,
"title": "suspend execution for an interval",
"synopsis": [
"sleep [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of suspend execution for an interval. Full behavior is defined in packages/bare-os-coreutils/src/sleep.js.",
"options": [],
"keywords": [
"sleep",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "pause seconds",
"code": "sleep 2"
}
]
}
@@ -0,0 +1,29 @@
{
"name": "sort",
"section": 1,
"title": "sort lines",
"synopsis": [
"sort [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of sort lines. Full behavior is defined in packages/bare-os-coreutils/src/sort.js.",
"options": [],
"keywords": [
"sort",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "lexicographic",
"code": "sort names.txt"
},
{
"caption": "numeric",
"code": "sort -n scores.txt"
},
{
"caption": "unique",
"code": "sort -u tags.txt"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "stat",
"section": 1,
"title": "display file status",
"synopsis": [
"stat [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of display file status. Full behavior is defined in packages/bare-os-coreutils/src/stat.js.",
"options": [],
"keywords": [
"stat",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "metadata",
"code": "stat ~/README.md"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "tail",
"section": 1,
"title": "copy the last part of a file",
"synopsis": [
"tail [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of copy the last part of a file. Full behavior is defined in packages/bare-os-coreutils/src/tail.js.",
"options": [],
"keywords": [
"tail",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "last lines",
"code": "tail -n 20 app.log"
},
{
"caption": "follow vibe (Bare: poll manually)",
"code": "tail error.log"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "tee",
"section": 1,
"title": "duplicate standard input",
"synopsis": [
"tee [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of duplicate standard input. Full behavior is defined in packages/bare-os-coreutils/src/tee.js.",
"options": [],
"keywords": [
"tee",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "copy stdout to file",
"code": "cat x | tee copy.txt | wc -l"
}
]
}
@@ -0,0 +1,29 @@
{
"name": "test",
"section": 1,
"title": "evaluate a condition",
"synopsis": [
"test [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of evaluate a condition. Full behavior is defined in packages/bare-os-coreutils/src/test.js.",
"options": [],
"keywords": [
"test",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "file exists",
"code": "test -f ~/.barerc && echo yes"
},
{
"caption": "directory",
"code": "test -d /home/user"
},
{
"caption": "string equal",
"code": "test \"$USER\" = guest"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "time",
"section": 1,
"title": "time a simple command",
"synopsis": [
"time [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of time a simple command. Full behavior is defined in packages/bare-os-coreutils/src/time.js.",
"options": [],
"keywords": [
"time",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "wall time a command",
"code": "time sort big.txt"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "touch",
"section": 1,
"title": "change file timestamps or create files",
"synopsis": [
"touch [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of change file timestamps or create files. Full behavior is defined in packages/bare-os-coreutils/src/touch.js.",
"options": [],
"keywords": [
"touch",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "create empty",
"code": "touch newfile"
},
{
"caption": "refresh mtime",
"code": "touch -c existing"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "tr",
"section": 1,
"title": "translate or delete characters",
"synopsis": [
"tr [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of translate or delete characters. Full behavior is defined in packages/bare-os-coreutils/src/tr.js.",
"options": [],
"keywords": [
"tr",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "uppercase",
"code": "echo hi | tr 'a-z' 'A-Z'"
},
{
"caption": "delete chars",
"code": "tr -d '\\r' < win.txt"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "true",
"section": 1,
"title": "return true value",
"synopsis": [
"true [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of return true value. Full behavior is defined in packages/bare-os-coreutils/src/true.js.",
"options": [],
"keywords": [
"true",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "always success",
"code": "true && echo ok"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "tty",
"section": 1,
"title": "return user's terminal name",
"synopsis": [
"tty [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of return user's terminal name. Full behavior is defined in packages/bare-os-coreutils/src/tty.js.",
"options": [],
"keywords": [
"tty",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "am I a tty",
"code": "tty"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "uname",
"section": 1,
"title": "return operating system name",
"synopsis": [
"uname [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of return operating system name. Full behavior is defined in packages/bare-os-coreutils/src/uname.js.",
"options": [],
"keywords": [
"uname",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "kernel-ish info",
"code": "uname -a"
}
]
}
@@ -0,0 +1,25 @@
{
"name": "wc",
"section": 1,
"title": "word, line, and byte or character count",
"synopsis": [
"wc [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of word, line, and byte or character count. Full behavior is defined in packages/bare-os-coreutils/src/wc.js.",
"options": [],
"keywords": [
"wc",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "lines words bytes",
"code": "wc README.md"
},
{
"caption": "stdin only",
"code": "cat f | wc -l"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "which",
"section": 1,
"title": "locate a command",
"synopsis": [
"which [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of locate a command. Full behavior is defined in packages/bare-os-coreutils/src/which.js.",
"options": [],
"keywords": [
"which",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "resolve on PATH",
"code": "which ls"
}
]
}
@@ -0,0 +1,21 @@
{
"name": "whoami",
"section": 1,
"title": "display effective user ID",
"synopsis": [
"whoami [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of display effective user ID. Full behavior is defined in packages/bare-os-coreutils/src/whoami.js.",
"options": [],
"keywords": [
"whoami",
"bare-os",
"coreutils"
],
"examples": [
{
"caption": "effective user",
"code": "whoami"
}
]
}
@@ -0,0 +1,24 @@
{
"name": "xargs",
"section": 1,
"title": "construct argument lists and invoke utility",
"synopsis": [
"xargs [OPTION]... [OPERAND]..."
],
"description": "xargs does not spawn arbitrary /bin utilities on Bare OS. Use shell word splitting or pipelines.",
"options": [],
"keywords": [
"xargs",
"bare-os",
"coreutils",
"stub"
],
"stub": true,
"bareOsNotes": "No process fork model; see handbook ch.9.",
"examples": [
{
"caption": "workaround: shell word split",
"code": "# for f in *.txt; do grep -l foo $f; done"
}
]
}
+128
View File
@@ -0,0 +1,128 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://bare-os.local/man-page.schema.json",
"title": "Bare OS manual page",
"type": "object",
"additionalProperties": false,
"required": ["name", "section", "title", "synopsis", "description", "options", "keywords"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"section": { "type": "integer", "minimum": 1, "maximum": 8 },
"title": { "type": "string", "minLength": 1 },
"synopsis": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
},
"description": { "type": "string", "minLength": 1 },
"options": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["flag", "meaning"],
"properties": {
"flag": { "type": "string" },
"meaning": { "type": "string" }
}
}
},
"environment": {
"type": "array",
"items": { "type": "string" }
},
"files": {
"type": "array",
"items": { "type": "string" }
},
"exitStatus": {
"type": "array",
"items": { "type": "string" }
},
"diagnostics": {
"type": "array",
"items": { "type": "string" }
},
"seeAlso": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "section"],
"properties": {
"name": { "type": "string" },
"section": { "type": "integer", "minimum": 1 }
}
}
},
"bareOsNotes": { "type": "string" },
"descriptionMode": {
"description": "wrap (default): reflow DESCRIPTION as prose; preserve: keep line breaks (handbook)",
"type": "string",
"enum": ["wrap", "preserve"]
},
"keywords": {
"type": "array",
"items": { "type": "string" }
},
"aliases": {
"type": "array",
"items": { "type": "string" }
},
"stub": { "type": "boolean" },
"examples": {
"description": "Cheat-sheet style snippets: caption + shell command(s)",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["code"],
"properties": {
"caption": { "type": "string" },
"code": { "type": "string", "minLength": 1 }
}
}
},
"builtins": {
"type": "array",
"description": "Per-builtin sections for bare-os-shell(1)",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name", "description"],
"properties": {
"name": { "type": "string" },
"synopsis": {
"type": "array",
"items": { "type": "string" }
},
"description": { "type": "string" },
"options": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["flag", "meaning"],
"properties": {
"flag": { "type": "string" },
"meaning": { "type": "string" }
}
}
},
"examples": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["code"],
"properties": {
"caption": { "type": "string" },
"code": { "type": "string", "minLength": 1 }
}
}
}
}
}
}
}
}
@@ -0,0 +1,294 @@
import { readFile, writeFile, mkdir } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
import {
COREUTILS_COMMANDS,
MAN_EXTRA_PAGES
} from '../lib/commands.mjs'
import { buildHandbookManPages } from './ingest-handbook-for-man.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pkgRoot = join(__dirname, '..')
const repoRoot = join(pkgRoot, '../..')
const pagesDir = join(pkgRoot, 'man', 'pages')
const kernelManDir = join(repoRoot, 'kernel', 'share', 'man')
const seederManDir = join(
repoRoot,
'packages',
'bare-os-seeder',
'kernel',
'share',
'man'
)
function isPlainObject(x) {
return x !== null && typeof x === 'object' && !Array.isArray(x)
}
function validatePage(raw, pathLabel) {
if (!isPlainObject(raw)) throw new Error(`${pathLabel}: root must be object`)
const allowed = new Set([
'name',
'section',
'title',
'synopsis',
'description',
'options',
'environment',
'files',
'exitStatus',
'diagnostics',
'seeAlso',
'bareOsNotes',
'keywords',
'aliases',
'stub',
'builtins',
'examples',
'descriptionMode'
])
for (const k of Object.keys(raw)) {
if (!allowed.has(k)) throw new Error(`${pathLabel}: unknown field "${k}"`)
}
const {
name,
section,
title,
synopsis,
description,
options,
keywords
} = raw
if (typeof name !== 'string' || !name)
throw new Error(`${pathLabel}: name must be non-empty string`)
if (typeof section !== 'number' || section < 1 || section > 8)
throw new Error(`${pathLabel}: section must be integer 18`)
if (typeof title !== 'string' || !title)
throw new Error(`${pathLabel}: title must be non-empty string`)
if (!Array.isArray(synopsis) || synopsis.length < 1)
throw new Error(`${pathLabel}: synopsis must be non-empty array`)
for (const s of synopsis) {
if (typeof s !== 'string')
throw new Error(`${pathLabel}: synopsis entries must be strings`)
}
if (typeof description !== 'string' || !description)
throw new Error(`${pathLabel}: description must be non-empty string`)
if (!Array.isArray(options))
throw new Error(`${pathLabel}: options must be array`)
for (const o of options) {
if (!isPlainObject(o) || typeof o.flag !== 'string' || typeof o.meaning !== 'string')
throw new Error(`${pathLabel}: each option needs { flag, meaning }`)
}
if (!Array.isArray(keywords))
throw new Error(`${pathLabel}: keywords must be array`)
for (const kw of keywords) {
if (typeof kw !== 'string')
throw new Error(`${pathLabel}: keywords entries must be strings`)
}
if (raw.environment !== undefined) {
if (!Array.isArray(raw.environment))
throw new Error(`${pathLabel}: environment must be array of strings`)
for (const e of raw.environment) {
if (typeof e !== 'string') throw new Error(`${pathLabel}: environment must be strings`)
}
}
if (raw.files !== undefined) {
if (!Array.isArray(raw.files))
throw new Error(`${pathLabel}: files must be array of strings`)
for (const f of raw.files) {
if (typeof f !== 'string') throw new Error(`${pathLabel}: files must be strings`)
}
}
if (raw.exitStatus !== undefined) {
if (!Array.isArray(raw.exitStatus))
throw new Error(`${pathLabel}: exitStatus must be array of strings`)
for (const e of raw.exitStatus) {
if (typeof e !== 'string') throw new Error(`${pathLabel}: exitStatus must be strings`)
}
}
if (raw.diagnostics !== undefined) {
if (!Array.isArray(raw.diagnostics))
throw new Error(`${pathLabel}: diagnostics must be array of strings`)
for (const d of raw.diagnostics) {
if (typeof d !== 'string') throw new Error(`${pathLabel}: diagnostics must be strings`)
}
}
if (raw.seeAlso !== undefined) {
if (!Array.isArray(raw.seeAlso))
throw new Error(`${pathLabel}: seeAlso must be array`)
for (const ref of raw.seeAlso) {
if (
!isPlainObject(ref) ||
typeof ref.name !== 'string' ||
typeof ref.section !== 'number'
)
throw new Error(`${pathLabel}: seeAlso entries need { name, section }`)
}
}
if (raw.bareOsNotes !== undefined && typeof raw.bareOsNotes !== 'string')
throw new Error(`${pathLabel}: bareOsNotes must be string`)
if (raw.descriptionMode !== undefined) {
if (raw.descriptionMode !== 'wrap' && raw.descriptionMode !== 'preserve')
throw new Error(`${pathLabel}: descriptionMode must be "wrap" or "preserve"`)
}
if (raw.aliases !== undefined) {
if (!Array.isArray(raw.aliases))
throw new Error(`${pathLabel}: aliases must be array of strings`)
for (const a of raw.aliases) {
if (typeof a !== 'string') throw new Error(`${pathLabel}: aliases must be strings`)
}
}
if (raw.stub !== undefined && typeof raw.stub !== 'boolean')
throw new Error(`${pathLabel}: stub must be boolean`)
if (raw.examples !== undefined) {
if (!Array.isArray(raw.examples))
throw new Error(`${pathLabel}: examples must be array`)
for (const ex of raw.examples) {
if (!isPlainObject(ex) || typeof ex.code !== 'string' || !ex.code.trim())
throw new Error(`${pathLabel}: each example needs non-empty { code }`)
if (ex.caption !== undefined && typeof ex.caption !== 'string')
throw new Error(`${pathLabel}: example caption must be string`)
}
}
if (raw.builtins !== undefined) {
if (!Array.isArray(raw.builtins))
throw new Error(`${pathLabel}: builtins must be array`)
for (const b of raw.builtins) {
if (!isPlainObject(b) || typeof b.name !== 'string' || typeof b.description !== 'string')
throw new Error(`${pathLabel}: each builtin needs { name, description }`)
if (b.synopsis !== undefined) {
if (!Array.isArray(b.synopsis))
throw new Error(`${pathLabel}: builtin synopsis must be array`)
for (const s of b.synopsis) {
if (typeof s !== 'string') throw new Error(`${pathLabel}: builtin synopsis strings`)
}
}
if (b.options !== undefined) {
if (!Array.isArray(b.options))
throw new Error(`${pathLabel}: builtin options must be array`)
for (const o of b.options) {
if (!isPlainObject(o) || typeof o.flag !== 'string' || typeof o.meaning !== 'string')
throw new Error(`${pathLabel}: builtin option { flag, meaning }`)
}
}
if (b.examples !== undefined) {
if (!Array.isArray(b.examples))
throw new Error(`${pathLabel}: builtin examples must be array`)
for (const ex of b.examples) {
if (!isPlainObject(ex) || typeof ex.code !== 'string' || !ex.code.trim())
throw new Error(`${pathLabel}: builtin example needs { code }`)
if (ex.caption !== undefined && typeof ex.caption !== 'string')
throw new Error(`${pathLabel}: builtin example caption must be string`)
}
}
}
}
}
function tokenizeForApropos(text) {
return String(text)
.toLowerCase()
.split(/[^a-z0-9_-]+/)
.filter(Boolean)
}
export async function buildManDb() {
const required = [...COREUTILS_COMMANDS, ...MAN_EXTRA_PAGES]
const pages = []
const index = Object.create(null)
const apropos = []
for (let i = 0; i < required.length; i++) {
const cmd = required[i]
const filePath = join(pagesDir, `${cmd}.json`)
let raw
try {
raw = JSON.parse(await readFile(filePath, 'utf8'))
} catch (e) {
throw new Error(`man: missing or invalid JSON for "${cmd}": ${filePath} (${e.message})`)
}
validatePage(raw, cmd)
if (raw.name !== cmd)
throw new Error(`man/pages/${cmd}.json: "name" must be "${cmd}", got "${raw.name}"`)
pages.push(raw)
const pageIdx = pages.length - 1
const addIndex = (key) => {
const k = String(key).toLowerCase()
if (index[k] !== undefined && index[k] !== pageIdx)
throw new Error(`man: duplicate index key "${k}"`)
index[k] = pageIdx
}
addIndex(raw.name)
if (Array.isArray(raw.aliases)) {
for (const a of raw.aliases) addIndex(a)
}
const kwSet = new Set()
for (const k of raw.keywords) kwSet.add(k.toLowerCase())
kwSet.add(raw.name.toLowerCase())
for (const t of tokenizeForApropos(raw.title)) kwSet.add(t)
if (Array.isArray(raw.examples)) {
for (const ex of raw.examples) {
if (ex.caption) {
for (const t of tokenizeForApropos(ex.caption)) kwSet.add(t)
}
}
}
for (const kw of kwSet) {
apropos.push({ kw, pageRef: pageIdx })
}
}
const handbookPages = await buildHandbookManPages(repoRoot)
for (let hi = 0; hi < handbookPages.length; hi++) {
const raw = handbookPages[hi]
validatePage(raw, raw.name)
pages.push(raw)
const pageIdx = pages.length - 1
const addIndex = (key) => {
const k = String(key).toLowerCase()
if (index[k] !== undefined && index[k] !== pageIdx)
throw new Error(`man: duplicate index key "${k}"`)
index[k] = pageIdx
}
addIndex(raw.name)
if (Array.isArray(raw.aliases)) {
for (const a of raw.aliases) addIndex(a)
}
const kwSet = new Set()
for (const k of raw.keywords) kwSet.add(k.toLowerCase())
kwSet.add(raw.name.toLowerCase())
for (const t of tokenizeForApropos(raw.title)) kwSet.add(t)
if (Array.isArray(raw.examples)) {
for (const ex of raw.examples) {
if (ex.caption) {
for (const t of tokenizeForApropos(ex.caption)) kwSet.add(t)
}
}
}
for (const kw of kwSet) {
apropos.push({ kw, pageRef: pageIdx })
}
}
const out = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
pages,
index,
apropos
}
const json = JSON.stringify(out, null, 0) + '\n'
await mkdir(kernelManDir, { recursive: true })
await mkdir(seederManDir, { recursive: true })
await writeFile(join(kernelManDir, 'man.json'), json)
await writeFile(join(seederManDir, 'man.json'), json)
}
const isMain =
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
if (isMain) {
await buildManDb()
}
@@ -0,0 +1,206 @@
/**
* Convert repo handbook/*.md into man(7) page objects merged at build time.
*/
import { readFile, readdir } from 'fs/promises'
import { join } from 'path'
const SECTION = 7
function stripMdInline(s) {
let t = String(s)
t = t.replace(/\[([^\]]*)\]\(([^)]+)\)/g, '$1 <$2>')
t = t.replace(/\*\*([^*]+)\*\*/g, '$1')
t = t.replace(/__([^_]+)__/g, '$1')
t = t.replace(/\*([^*]+)\*/g, '$1')
t = t.replace(/`([^`]+)`/g, '$1')
return t.trim()
}
/**
* @param {string} md
* @param {string} sourceFile
*/
export function handbookMdToDescription(md, sourceFile) {
const lines = md.split('\n')
const out = []
let i = 0
let inFence = false
/** @type {string} */
let fenceKind = ''
while (i < lines.length) {
const line = lines[i]
const fenceM = line.match(/^(\s*)```(\w*)\s*$/)
if (fenceM) {
if (!inFence) {
inFence = true
fenceKind = fenceM[2] || ''
i++
continue
}
inFence = false
fenceKind = ''
i++
continue
}
if (inFence) {
if (fenceKind === 'mermaid') {
i++
continue
}
out.push(' ' + line)
i++
continue
}
const t = line.trim()
if (/^---+$/.test(t)) {
out.push('')
i++
continue
}
const h = line.match(/^(#{1,6})\s+(.*)$/)
if (h) {
const text = stripMdInline(h[2])
if (text) {
out.push('')
out.push(text.toUpperCase())
out.push('')
}
i++
continue
}
if (/^\s*[-*]\s+/.test(line)) {
out.push(stripMdInline(line.replace(/^\s+/, '')))
i++
continue
}
if (/^\s*\d+\.\s+/.test(line)) {
out.push(stripMdInline(line.trim()))
i++
continue
}
if (t.startsWith('|')) {
if (/^\|[\s-:|]+\|$/.test(t)) {
i++
continue
}
out.push(stripMdInline(t))
i++
continue
}
if (!t) {
out.push('')
i++
continue
}
out.push(stripMdInline(line.trim()))
i++
}
let body = out.join('\n').replace(/\n{3,}/g, '\n\n').trim()
if (!body) body = '(empty chapter — see ' + sourceFile + ' in the repo)'
return body
}
function firstHeadingTitle(md) {
const m = md.match(/^#\s+(.+)$/m)
return m ? stripMdInline(m[1]) : 'Bare OS handbook'
}
/**
* @param {string} repoRoot
* @returns {Promise<object[]>}
*/
export async function buildHandbookManPages(repoRoot) {
const handbookDir = join(repoRoot, 'handbook')
let names
try {
names = (await readdir(handbookDir)).filter((f) => f.endsWith('.md'))
} catch {
return []
}
names.sort((a, b) => {
if (a === 'README.md') return -1
if (b === 'README.md') return 1
return a.localeCompare(b)
})
/** @type {{ file: string, name: string, title: string, aliases?: string[] }[]} */
const meta = []
for (const file of names) {
if (file === 'README.md') {
meta.push({
file,
name: 'bare-os-handbook',
title: 'Bare OS handbook — table of contents and reading order',
aliases: ['handbook', 'bare-os-handbook-index']
})
} else {
const base = file.replace(/\.md$/i, '')
meta.push({
file,
name: 'handbook-' + base,
title: '',
aliases: []
})
}
}
const pages = []
for (let j = 0; j < meta.length; j++) {
const m = meta[j]
const path = join(handbookDir, m.file)
const md = await readFile(path, 'utf8')
const title = m.title || firstHeadingTitle(md)
const description = handbookMdToDescription(md, 'handbook/' + m.file)
const next = meta[j + 1]
const prev = meta[j - 1]
/** @type {{ name: string, section: number }[]} */
const seeAlso = []
if (next) seeAlso.push({ name: next.name, section: SECTION })
if (prev) seeAlso.push({ name: prev.name, section: SECTION })
seeAlso.push({ name: 'man', section: 1 })
const kw = new Set([
'handbook',
'bare-os',
'documentation',
'narrative',
'chapter'
])
for (const t of m.name.split(/[^a-z0-9]+/i)) {
if (t.length > 1) kw.add(t.toLowerCase())
}
for (const t of title.toLowerCase().split(/[^a-z0-9]+/)) {
if (t.length > 2) kw.add(t)
}
const page = {
name: m.name,
section: SECTION,
title,
synopsis: ['man 7 ' + m.name, 'Handbook chapter (plain text from handbook/' + m.file + ')'],
description,
descriptionMode: 'preserve',
options: [],
keywords: Array.from(kw),
seeAlso,
bareOsNotes:
'Generated at build time from handbook/' +
m.file +
'. Diagrams in ```mermaid blocks are omitted; open the Markdown in the repo for figures.'
}
if (m.aliases && m.aliases.length) page.aliases = m.aliases
pages.push(page)
}
return pages
}
@@ -0,0 +1,667 @@
/**
* One-shot generator for man/pages/*.json (run after changing commands list).
* node packages/bare-os-coreutils/scripts/seed-man-pages.mjs
*/
import { mkdir, writeFile } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import {
COREUTILS_COMMANDS,
MAN_EXTRA_PAGES
} from '../lib/commands.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pagesDir = join(__dirname, '../man/pages')
const STUB = new Set(['chgrp', 'chown', 'xargs', 'getconf', 'mkfifo'])
const POSIX_TITLE = {
awk: 'pattern scanning and processing language',
basename: 'strip directory and suffix from pathnames',
cat: 'concatenate and print files',
chgrp: 'change file group ownership',
chmod: 'change file mode bits',
chown: 'change file owner and group',
cksum: 'write file checksums and sizes',
clear: 'clear the terminal screen',
cp: 'copy files',
crontab: 'user crontab manipulation',
cut: 'cut out selected fields of each line',
date: 'display or set date and time',
dirname: 'return directory portion of a pathname',
du: 'estimate file space usage',
echo: 'write arguments to standard output',
env: 'set the environment for command invocation',
exit: 'exit the shell or booter session',
false: 'return false value',
find: 'find files',
getconf: 'get configuration values',
grep: 'pattern matching utility',
head: 'copy the first part of files',
hdms: 'Hyperswarm distributed map store',
help: 'Bare OS help summary',
hostname: 'set or print hostname',
id: 'return user identity',
ln: 'link files',
login: 'begin a session on the system',
logout: 'end session (save vault)',
logname: "return the user's login name",
ls: 'list directory contents',
man: 'display on-line manual pages',
mkdir: 'make directories',
mkfifo: 'make FIFO special files',
mv: 'move or rename files',
nl: 'line numbering utility',
od: 'octal dump',
pathchk: 'check pathname portability',
printenv: 'print environment variables',
printf: 'format and print',
pwd: 'return working directory name',
readlink: 'print symbolic link targets',
rm: 'remove files',
rmdir: 'remove empty directories',
savevault: 'encrypt snapshot of personal drive',
sed: 'stream editor',
seq: 'print sequences of numbers',
sleep: 'suspend execution for an interval',
sort: 'sort lines',
stat: 'display file status',
tail: 'copy the last part of a file',
tee: 'duplicate standard input',
test: 'evaluate a condition',
time: 'time a simple command',
touch: 'change file timestamps or create files',
tr: 'translate or delete characters',
true: 'return true value',
tty: "return user's terminal name",
uname: 'return operating system name',
wc: 'word, line, and byte or character count',
which: 'locate a command',
whoami: 'display effective user ID',
xargs: 'construct argument lists and invoke utility'
}
/**
* cheat.sh-style snippets: { caption?, code } — shown under EXAMPLES in man output.
* @type {Record<string, Array<{ caption?: string, code: string }>>}
*/
const EXAMPLES = {}
EXAMPLES.awk = [
{ caption: 'print column 1', code: "awk '{print $1}' file.txt" },
{ caption: 'field separator', code: "awk -F: '{print $1}' /etc/passwd" },
{ caption: 'sum numbers in first column', code: "awk '{s+=$1} END{print s}' nums.txt" },
{ caption: 'lines matching /re/', code: "awk '/error/{print NR\": \"$0}' log.txt" }
]
EXAMPLES.basename = [
{ caption: 'strip directory', code: 'basename /home/user/docs/readme.md' },
{ caption: 'strip suffix', code: 'basename -s .md /path/readme.md' }
]
EXAMPLES.cat = [
{ caption: 'stdout several files', code: 'cat a.txt b.txt' },
{ caption: 'number lines (use nl)', code: 'cat -n file.txt # if supported; else nl file' },
{ caption: 'here-string via echo pipe', code: 'echo hello | cat' }
]
EXAMPLES.chgrp = [
{
caption: 'not supported — use identity model',
code: '# chgrp is a stub; group is display metadata only'
}
]
EXAMPLES.chmod = [
{ caption: 'octal', code: 'chmod 644 ~/.profile' },
{ caption: 'recursive-ish (run find + chmod per file)', code: 'find . -type f -name "*.sh" -print' },
{ caption: 'symbolic user bits', code: 'chmod u+x script.sh' },
{ caption: 'all read, owner write', code: 'chmod a+r,u+w shared.txt' }
]
EXAMPLES.chown = [
{ caption: 'not supported', code: '# chown stub — see man identity / login' }
]
EXAMPLES.cksum = [
{ caption: 'checksum file', code: 'cksum iso.img' },
{ caption: 'verify pipeline', code: 'cat f | cksum' }
]
EXAMPLES.clear = [
{ caption: 'wipe screen', code: 'clear' }
]
EXAMPLES.cp = [
{ caption: 'copy file', code: 'cp src.txt dest.txt' },
{ caption: 'into directory', code: 'cp a b c ~/backup/' },
{ caption: 'preserve implied (if implemented)', code: 'cp -R proj proj.bak' }
]
EXAMPLES.crontab = [
{ caption: 'list jobs', code: 'crontab -l' },
{ caption: 'install from file', code: 'crontab ~/.crontab' },
{ caption: 'remove all', code: 'crontab -r' }
]
EXAMPLES.cut = [
{ caption: 'fields by delimiter', code: "cut -d: -f1,3 /etc/passwd" },
{ caption: 'characters', code: 'cut -c1-16 file.txt' }
]
EXAMPLES.date = [
{ caption: 'RFC-ish output', code: 'date' },
{ caption: 'epoch seconds', code: 'date +%s' }
]
EXAMPLES.dirname = [
{ caption: 'parent path', code: 'dirname /a/b/c.txt' },
{ caption: 'compose with basename', code: 'p=/x/y/z; echo $(dirname $p)/$(basename $p)' }
]
EXAMPLES.du = [
{ caption: 'sizes under cwd', code: 'du .' },
{ caption: 'human (if supported)', code: 'du -h ~' }
]
EXAMPLES.echo = [
{ caption: 'literal', code: 'echo hello world' },
{ caption: 'no newline (if -n supported)', code: 'echo -n OK' }
]
EXAMPLES.env = [
{ caption: 'print environment', code: 'env' },
{ caption: 'run with override', code: 'env PATH=/bin:/usr/bin man ls' }
]
EXAMPLES.exit = [
{ caption: 'leave session with status', code: 'exit 0' },
{ caption: 'from script', code: '/bin/exit 42' }
]
EXAMPLES.false = [
{ caption: 'force failure in pipeline tests', code: 'false; echo $?' }
]
EXAMPLES.find = [
{ caption: 'files by name glob', code: 'find . -name "*.js"' },
{ caption: 'directories only', code: 'find . -type d' },
{ caption: 'max depth', code: 'find . -maxdepth 2 -type f' },
{ caption: 'OR names', code: 'find . \\( -name "*.c" -o -name "*.h" \\)' }
]
EXAMPLES.getconf = [
{ caption: 'stub', code: '# getconf PATH_MAX — not available on Bare OS' }
]
EXAMPLES.grep = [
{ caption: 'recursive feel (grep each file)', code: 'grep -n error *.log' },
{ caption: 'case insensitive', code: 'grep -i todo NOTES.md' },
{ caption: 'invert (lines without)', code: "grep -v '^#' config" },
{ caption: 'fixed string (no regex)', code: 'grep -F "v1.0" CHANGES' },
{ caption: 'count matches', code: 'grep -c FAIL build.log' },
{ caption: 'only filenames', code: 'grep -l main *.js' },
{ caption: 'multiple patterns', code: 'grep -e foo -e bar file.txt' }
]
EXAMPLES.head = [
{ caption: 'first 10 lines', code: 'head /etc/os-release' },
{ caption: 'first N', code: 'head -n 50 big.log' },
{ caption: 'stdin', code: 'cat long.txt | head' }
]
EXAMPLES.hdms = [
{ caption: 'when booter wires HDMS', code: 'hdms ls /mnt' },
{ caption: 'otherwise', code: '# prints unavailable without ctx.runHdms' }
]
EXAMPLES.help = [
{ caption: 'quick index', code: 'help' },
{ caption: 'then deep dive', code: 'man grep' }
]
EXAMPLES.hostname = [
{ caption: 'show host', code: 'hostname' }
]
EXAMPLES.id = [
{ caption: 'who am I numerically', code: 'id' }
]
EXAMPLES.ln = [
{ caption: 'symlink', code: 'ln -s target name' },
{ caption: 'hard link (if supported)', code: 'ln file linkname' }
]
EXAMPLES.login = [
{ caption: 'unlock existing identity', code: 'login my passphrase words here' },
{ caption: 'register new', code: 'login --new first time passphrase' }
]
EXAMPLES.logout = [
{ caption: 'end session', code: 'logout' },
{ caption: 'save vault hint', code: 'logout --save' }
]
EXAMPLES.logname = [
{ caption: 'login name', code: 'logname' }
]
EXAMPLES.ls = [
{ caption: 'long + hidden', code: 'ls -la ~' },
{ caption: 'one per line', code: 'ls -1 /bin | head' },
{ caption: 'multiple paths', code: 'ls /bin /etc' }
]
EXAMPLES.man = [
{ caption: 'open page', code: 'man sed' },
{ caption: 'handbook TOC (section 7)', code: 'man handbook' },
{ caption: 'handbook chapter by section', code: 'man 7 handbook-01-introduction' },
{ caption: 'apropos', code: 'man -k copy' },
{ caption: 'whatis', code: 'man -f grep' },
{ caption: 'all pages', code: 'man -l' },
{ caption: 'narrow terminal', code: 'MANWIDTH=64 man awk' }
]
EXAMPLES.mkdir = [
{ caption: 'one dir', code: 'mkdir proj' },
{ caption: 'parents', code: 'mkdir -p a/b/c' }
]
EXAMPLES.mkfifo = [
{ caption: 'stub', code: '# FIFOs not on Hyperdrive — use shell pipelines' }
]
EXAMPLES.mv = [
{ caption: 'rename', code: 'mv old.txt new.txt' },
{ caption: 'into dir', code: 'mv *.txt ~/inbox/' }
]
EXAMPLES.nl = [
{ caption: 'number all lines', code: 'nl README.md' }
]
EXAMPLES.od = [
{ caption: 'hex dump vibe', code: 'od -c file.bin | head' }
]
EXAMPLES.pathchk = [
{ caption: 'portable path check', code: 'pathchk -p "$HOME/file name"' }
]
EXAMPLES.printenv = [
{ caption: 'one variable', code: 'printenv HOME' },
{ caption: 'all', code: 'printenv' }
]
EXAMPLES.printf = [
{ caption: 'format', code: 'printf "hex=%x dec=%d\\n" 255 255' },
{ caption: 'no newline', code: 'printf "%s" OK' }
]
EXAMPLES.pwd = [
{ caption: 'where am I', code: 'pwd' }
]
EXAMPLES.readlink = [
{ caption: 'symlink target', code: 'readlink ~/.config' }
]
EXAMPLES.rm = [
{ caption: 'file', code: 'rm tmp.txt' },
{ caption: 'tree', code: 'rm -rf build/' }
]
EXAMPLES.rmdir = [
{ caption: 'empty dir', code: 'rmdir olddir' }
]
EXAMPLES.savevault = [
{ caption: 'snapshot encrypted vault', code: 'savevault' }
]
EXAMPLES.sed = [
{ caption: 'substitute first per line', code: "sed 's/foo/bar/' file.txt" },
{ caption: 'global per line', code: "sed 's/ //g' spaced.txt" },
{ caption: 'in-place (if supported)', code: "sed -i.bak 's/^/# /' f.cfg" },
{ caption: 'print line 5 only', code: "sed -n '5p' file" },
{ caption: 'delete blank lines', code: "sed '/^$/d' file" }
]
EXAMPLES.seq = [
{ caption: '1..10', code: 'seq 1 10' },
{ caption: 'step', code: 'seq 0 2 20' }
]
EXAMPLES.sleep = [
{ caption: 'pause seconds', code: 'sleep 2' }
]
EXAMPLES.sort = [
{ caption: 'lexicographic', code: 'sort names.txt' },
{ caption: 'numeric', code: 'sort -n scores.txt' },
{ caption: 'unique', code: 'sort -u tags.txt' }
]
EXAMPLES.stat = [
{ caption: 'metadata', code: 'stat ~/README.md' }
]
EXAMPLES.tail = [
{ caption: 'last lines', code: 'tail -n 20 app.log' },
{ caption: 'follow vibe (Bare: poll manually)', code: 'tail error.log' }
]
EXAMPLES.tee = [
{ caption: 'copy stdout to file', code: 'cat x | tee copy.txt | wc -l' }
]
EXAMPLES.test = [
{ caption: 'file exists', code: 'test -f ~/.barerc && echo yes' },
{ caption: 'directory', code: 'test -d /home/user' },
{ caption: 'string equal', code: 'test "$USER" = guest' }
]
EXAMPLES.time = [
{ caption: 'wall time a command', code: 'time sort big.txt' }
]
EXAMPLES.touch = [
{ caption: 'create empty', code: 'touch newfile' },
{ caption: 'refresh mtime', code: 'touch -c existing' }
]
EXAMPLES.tr = [
{ caption: 'uppercase', code: "echo hi | tr 'a-z' 'A-Z'" },
{ caption: 'delete chars', code: "tr -d '\\r' < win.txt" }
]
EXAMPLES.true = [
{ caption: 'always success', code: 'true && echo ok' }
]
EXAMPLES.tty = [
{ caption: 'am I a tty', code: 'tty' }
]
EXAMPLES.uname = [
{ caption: 'kernel-ish info', code: 'uname -a' }
]
EXAMPLES.wc = [
{ caption: 'lines words bytes', code: 'wc README.md' },
{ caption: 'stdin only', code: 'cat f | wc -l' }
]
EXAMPLES.which = [
{ caption: 'resolve on PATH', code: 'which ls' }
]
EXAMPLES.whoami = [
{ caption: 'effective user', code: 'whoami' }
]
EXAMPLES.xargs = [
{ caption: 'workaround: shell word split', code: '# for f in *.txt; do grep -l foo $f; done' }
]
/** @type {Record<string, Record<string, unknown>>} */
const EXTRA = {}
EXTRA.chgrp = {
description:
'Changing group ownership is not supported on Bare OS: Hyperdrive metadata is single-session oriented.',
diagnostics: ['chgrp: changing group is not supported on Bare OS'],
bareOsNotes: 'Single-user identity; gid fields exist for display only.'
}
EXTRA.chown = {
description:
'Changing file owner is not supported on Bare OS (single-user Hyperdrive metadata).',
diagnostics: ['chown: changing owner is not supported on Bare OS'],
bareOsNotes: 'Use identity login/logout instead of POSIX ownership changes.'
}
EXTRA.xargs = {
description:
'xargs does not spawn arbitrary /bin utilities on Bare OS. Use shell word splitting or pipelines.',
bareOsNotes: 'No process fork model; see handbook ch.9.'
}
EXTRA.getconf = {
description:
'Host sysconf-style values are not exposed. The command prints an error.',
bareOsNotes: 'Stub only; no kernel sysconf surface.'
}
EXTRA.mkfifo = {
description:
'FIFO special files are not implemented on Hyperdrive. The command reports failure.',
bareOsNotes: 'Documented stub; no real pipes as kernel objects.'
}
EXTRA.chmod = {
synopsis: ['chmod MODE FILE...', 'MODE is octal (e.g. 644) or symbolic (e.g. u+rw)'],
description:
'Sets file mode bits on the VFS. Supports POSIX-style symbolic modes (u/g/o/a, +/-/=, rwxX) and octal modes.',
options: [],
keywords: ['chmod', 'mode', 'permission', 'octal', 'symbolic'],
diagnostics: ['chmod: No such file', 'chmod: invalid mode'],
bareOsNotes: 'Applies to Hyperdrive metadata; not a host inode.'
}
EXTRA.grep = {
synopsis: [
'grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
],
description:
'Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.',
options: [
{ flag: '-E', meaning: 'Extended regex (accepted; patterns use JS RegExp)' },
{ flag: '-F', meaning: 'Fixed string match' },
{ flag: '-i', meaning: 'Ignore case' },
{ flag: '-v', meaning: 'Invert match' },
{ flag: '-n', meaning: 'Prefix lines with line number' },
{ flag: '-c', meaning: 'Count matching lines only' },
{ flag: '-l', meaning: 'List files with matches' },
{ flag: '-q', meaning: 'Quiet (exit status only)' },
{ flag: '-s', meaning: 'Suppress error messages' },
{ flag: '-H / -h', meaning: 'Force / suppress filename prefix' },
{ flag: '-e pat', meaning: 'Specify pattern' },
{ flag: '-f file', meaning: 'Read patterns from file' }
],
keywords: ['grep', 'search', 'regex', 'pattern', 'filter'],
seeAlso: [
{ name: 'sed', section: 1 },
{ name: 'awk', section: 1 }
],
bareOsNotes: 'UTF-16 strings and JS regex differ from strict POSIX/GNU.'
}
EXTRA.sed = {
description:
'Stream editor with a subset of POSIX sed. Large engine is vendored in lib/sed-engine.js.',
keywords: ['sed', 'stream', 'edit', 'substitute'],
seeAlso: [
{ name: 'awk', section: 1 },
{ name: 'grep', section: 1 }
],
bareOsNotes: 'JavaScript implementation; edge cases differ from GNU sed.'
}
EXTRA.awk = {
description:
'Pattern-directed scanning and processing. Engine in lib/awk-engine.js; not full POSIX awk.',
keywords: ['awk', 'pattern', 'field', 'script'],
seeAlso: [
{ name: 'sed', section: 1 },
{ name: 'grep', section: 1 }
],
bareOsNotes: 'See handbook ch.9 for divergence from Issue 7.'
}
EXTRA.ls = {
synopsis: ['ls [-1al] [FILE...]'],
description:
'Lists directory contents. Long format shows mode, links, owner, group, size, mtime, and symlink targets.',
options: [
{ flag: '-a', meaning: 'Include names starting with .' },
{ flag: '-l', meaning: 'Long listing' },
{ flag: '-1', meaning: 'One name per line (short format)' }
],
keywords: ['ls', 'list', 'directory', 'dir'],
bareOsNotes: 'Hides .bareos_empty marker like other tools.'
}
EXTRA.man = {
synopsis: [
'man [-k keyword] [-f name] [-l] [[section] name]',
'man reads /share/man/man.json on the system drive.'
],
description:
'Displays manual pages from the merged JSON database. Section 1 only in this release.',
options: [
{ flag: '-k, --apropos', meaning: 'Search keywords and titles (substring)' },
{ flag: '-f, --whatis', meaning: 'One-line description for exact name' },
{ flag: '-l, --list', meaning: 'List all manual page names' }
],
environment: ['MANWIDTH — wrap width (default 72, min 40)', 'NO_COLOR — disable bold headings on TTY'],
keywords: ['man', 'manual', 'help', 'documentation', 'apropos', 'whatis', 'cheat', 'examples'],
seeAlso: [{ name: 'help', section: 1 }],
bareOsNotes: 'No troff; no embedded DB fallback in v1.'
}
EXTRA.help = {
synopsis: ['help'],
description:
'Prints a one-screen summary of shell builtins and /bin command names. Use man for long-form documentation.',
keywords: ['help', 'summary', 'builtins', 'commands'],
seeAlso: [
{ name: 'man', section: 1 },
{ name: 'bare-os-shell', section: 1 }
]
}
EXTRA.exit = {
synopsis: ['exit [status]'],
description:
'When run as /bin/exit, requests the booter to end the session via ctx.requestBooterExit. Status defaults to 0.',
bareOsNotes: 'Also available as a shell builtin with different wiring.'
}
EXTRA.hdms = {
description:
'Invokes ctx.runHdms when the booter provides HDMS integration; otherwise prints unavailable.',
keywords: ['hdms', 'hyperswarm', 'map'],
bareOsNotes: 'Optional booter capability.'
}
EXTRA.find = {
synopsis: ['find [PATH...] [EXPRESSION]'],
description:
'Walks directories and applies expressions (-name, -type, -print, -maxdepth, logical -and/-or/-not).',
keywords: ['find', 'directory', 'walk', 'search'],
bareOsNotes: 'Expression syntax is a simplified subset.'
}
EXTRA.login = {
description:
'When invoked from /bin, behavior aligns with session identity hooks (see booter). Prefer the shell builtin for passphrase entry.',
keywords: ['login', 'identity', 'passphrase'],
seeAlso: [{ name: 'logout', section: 1 }]
}
EXTRA.logout = {
description: 'Ends session; may persist vault depending on booter and flags.',
keywords: ['logout', 'session'],
seeAlso: [{ name: 'login', section: 1 }]
}
EXTRA.savevault = {
description: 'Encrypts a copy of the personal drive under /.bare/vault/ when identity services are available.',
keywords: ['savevault', 'vault', 'encrypt', 'backup'],
seeAlso: [{ name: 'login', section: 1 }]
}
function basePage(name) {
const title = POSIX_TITLE[name] || name
const p = {
name,
section: 1,
title,
synopsis: [`${name} [OPTION]... [OPERAND]...`],
description: `Bare OS implementation of ${title}. Full behavior is defined in packages/bare-os-coreutils/src/${name}.js.`,
options: [],
keywords: [name, 'bare-os', 'coreutils']
}
if (STUB.has(name)) {
p.stub = true
p.keywords.push('stub')
}
const ex = EXTRA[name]
if (ex) Object.assign(p, ex)
if (EXAMPLES[name]) p.examples = EXAMPLES[name]
return p
}
function gitPage() {
return {
name: 'git',
section: 1,
title: 'Bare OS git front-end (isomorphic-git)',
synopsis: ['git [-C dir] <subcommand> [ARGUMENTS...]'],
description:
'Runs isomorphic-git against the VFS-backed adapter. Remote HTTP(S) uses BARE_OS_GIT_HTTP when set; otherwise Pear bare module fetch.',
options: [
{ flag: '-C dir', meaning: 'Run as if git was started in dir' }
],
environment: [
'BARE_OS_GIT_HTTP — optional fetch implementation for remotes',
'GIT_* — standard hints where supported'
],
keywords: ['git', 'version control', 'repository', 'clone', 'commit', 'isomorphic-git'],
bareOsNotes: 'Not a separate /bin script; booter delegates argv[0]=git to git-cli.js.',
seeAlso: [{ name: 'bare-os-shell', section: 1 }],
examples: [
{ caption: 'new repo', code: 'git init -C ~/myrepo' },
{ caption: 'status', code: 'git -C ~/myrepo status' },
{ caption: 'clone over HTTP (needs remote + fetch)', code: 'git clone https://example.com/repo.git ~/work/repo' },
{ caption: 'config local', code: 'git -C ~/myrepo config user.email "[email protected]"' },
{ caption: 'log one line', code: 'git -C ~/myrepo log --oneline -5' }
]
}
}
function shellPage() {
return {
name: 'bare-os-shell',
section: 1,
title: 'Bare OS interactive shell builtins',
synopsis: ['# builtins only — no full POSIX sh grammar'],
description:
'The line-at-a-time shell supports aliases, simple pipelines (simulated), redirection, and the builtins below. Compound commands (if, for, while) are not available.',
options: [],
aliases: ['sh-builtins'],
keywords: [
'shell',
'builtin',
'cd',
'export',
'alias',
'bare-os-shell',
'sh-builtins'
],
builtins: [
{
name: 'alias',
synopsis: ['alias', 'alias name=value ...', 'unalias name ...'],
description: 'Define or list command aliases. unalias removes definitions.'
},
{
name: 'cd',
synopsis: ['cd [DIR]'],
description: 'Change working directory via vfs.chdir; default is HOME.'
},
{
name: 'export',
synopsis: ['export NAME=value ...'],
description: 'Set environment variables visible to child /bin invocations.'
},
{
name: 'unset',
synopsis: ['unset NAME ...'],
description: 'Remove variables; readonly names cannot be unset.'
},
{
name: 'readonly',
synopsis: ['readonly NAME[=value] ...'],
description: 'Mark variables read-only.'
},
{
name: 'umask',
synopsis: ['umask [octal]'],
description: 'Show or set shell file creation mask (stored in env UMASK).'
},
{
name: 'command',
synopsis: ['command -v|-V NAME', 'command ARGV...'],
description: 'Resolve or run a command without using shell functions (none) or aliases for -v/-V.'
},
{
name: 'type',
synopsis: ['type NAME'],
description: 'Report whether NAME is a builtin or a path under PATH.'
},
{
name: 'login / logout',
synopsis: ['login [--new] passphrase...', 'logout [--save]'],
description: 'Identity unlock/register and session teardown; require booter hooks.'
},
{
name: ':',
synopsis: [':'],
description: 'No-op builtin.'
},
{
name: 'exit',
synopsis: ['exit [n]'],
description: 'Request booter exit with status n (builtin path).'
}
],
seeAlso: [
{ name: 'help', section: 1 },
{ name: 'man', section: 1 }
],
bareOsNotes: 'Pipelines do not use OS pipes; see handbook ch.4 and ch.9.',
examples: [
{ caption: 'pipeline (simulated)', code: 'ls -1 /bin | grep man' },
{ caption: 'redirect out', code: 'echo hi > ~/hello.txt' },
{ caption: 'append', code: 'date >> ~/log.txt' },
{ caption: 'alias + use', code: "alias ll='ls -la'\nll ~" },
{ caption: 'export for children', code: 'export EDITOR=ed\nman ls' },
{ caption: 'temp var for one command', code: 'PATH=/bin man which' }
]
}
}
async function main() {
await mkdir(pagesDir, { recursive: true })
for (const name of COREUTILS_COMMANDS) {
const p = basePage(name)
await writeFile(
join(pagesDir, `${name}.json`),
JSON.stringify(p, null, 2) + '\n'
)
}
for (const name of MAN_EXTRA_PAGES) {
const p = name === 'git' ? gitPage() : shellPage()
await writeFile(
join(pagesDir, `${name}.json`),
JSON.stringify(p, null, 2) + '\n'
)
}
}
await main()
+94
View File
@@ -0,0 +1,94 @@
async function run(ctx, argv) {
let fsVal = null
/** @type {string[]} */
const progParts = []
const rest = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-F' || a === '--field-separator') {
fsVal = argv[++i] || ''
continue
}
if (a === '-v') {
const ax = argv[++i] || ''
const eq = ax.indexOf('=')
if (eq > 0) {
const k = ax.slice(0, eq)
const v = ax.slice(eq + 1)
progParts.push('BEGIN { ' + k + ' = ' + JSON.stringify(v) + ' }')
}
continue
}
if (a === '-f') {
const path = argv[++i]
if (!path) {
ctx.console.error('awk: -f needs a file')
ctx.exitCode = 1
return
}
const buf = await ctx.vfs.readFile(path)
if (!buf) {
ctx.console.error('awk: cannot read ' + path)
ctx.exitCode = 1
return
}
progParts.push(ctx.b4a.toString(buf))
continue
}
if (a === '--') {
rest.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-') && a.length > 1) {
ctx.console.error('awk: unsupported option ' + a)
ctx.exitCode = 1
return
}
progParts.push(a)
rest.push(...argv.slice(i + 1))
break
}
const program = progParts.join('\n')
if (!program.trim()) {
ctx.console.error('usage: awk [-F fs] [-v k=v] [-f file] program [file ...]')
ctx.exitCode = 1
return
}
const awkArgv = ['awk', ...rest]
const stdinLines = (() => {
const s = bareStdin(ctx)
const ls = s.split(/\r?\n/)
if (ls.length && ls[ls.length - 1] === '') ls.pop()
return ls
})()
const io = {
print(s) {
const t = s.replace(/\n$/, '')
ctx.console.log(t)
},
async writeFile(path, data, append) {
const prev = append ? await ctx.vfs.readFile(path) : null
const merged = prev ? ctx.b4a.concat([prev, ctx.b4a.from(data)]) : ctx.b4a.from(data)
await ctx.vfs.writeFile(path, merged)
},
readFile(path) {
return ctx.vfs.readFile(path)
},
stdinLines
}
try {
const code = await bareAwkRun(program, {
fs: fsVal != null ? fsVal : ' ',
argc: awkArgv.length,
argv: awkArgv,
environ: { ...ctx.vfs.env }
}, io)
ctx.exitCode = code || 0
} catch (e) {
ctx.console.error((e && e.message) || String(e))
ctx.exitCode = 1
}
}
+6
View File
@@ -0,0 +1,6 @@
async function run(ctx, argv) {
ctx.console.error(
'chgrp: changing group is not supported on Bare OS (single-user Hyperdrive metadata).'
)
ctx.exitCode = 1
}
+56 -8
View File
@@ -1,20 +1,68 @@
function bareChmodSymbolic(curPerm, spec) {
let m = curPerm & 0o777
const parts = spec.split(',')
for (const raw of parts) {
const part = raw.trim()
if (!part) continue
let who = 0
let i = 0
while (i < part.length && 'augo'.includes(part[i])) {
const c = part[i++]
if (c === 'a') who |= 0o777
if (c === 'u') who |= 0o700
if (c === 'g') who |= 0o070
if (c === 'o') who |= 0o007
}
if (!who) who = 0o777
const op = part[i++]
if (!op || !'+-='.includes(op)) continue
const rest = part.slice(i)
let bits = 0
let wantX = false
for (const ch of rest) {
if (ch === 'r') bits |= who & 0o444
if (ch === 'w') bits |= who & 0o222
if (ch === 'x' || ch === 's' || ch === 't') bits |= who & 0o111
if (ch === 'X') wantX = true
}
if (wantX && (m & 0o111)) bits |= who & 0o111
if (op === '=') m = (m & ~who) | bits
else if (op === '+') m |= bits
else if (op === '-') m &= ~bits
}
return m
}
async function run(ctx, argv) {
const modeStr = argv[1]
const files = argv.slice(2)
if (!modeStr || !files.length) {
ctx.console.error('usage: chmod OCTAL_MODE FILE...')
ctx.exitCode = 1
return
}
const mode = Number.parseInt(String(modeStr), 8)
if (!Number.isFinite(mode) || mode < 0) {
ctx.console.error('chmod: invalid mode')
ctx.console.error('usage: chmod MODE FILE...\n MODE is octal (e.g. 644) or symbolic (e.g. u+rw)')
ctx.exitCode = 1
return
}
const sym = !/^[0-7]+$/.test(modeStr)
for (const f of files) {
try {
await ctx.vfs.chmod(f, mode)
let perm
if (sym) {
const st = await ctx.vfs.stat(f)
if (!st) {
ctx.console.error('chmod: ' + f + ': No such file')
ctx.exitCode = 1
continue
}
perm = bareChmodSymbolic(st.mode & 0o777, modeStr)
} else {
perm = Number.parseInt(String(modeStr), 8)
if (!Number.isFinite(perm) || perm < 0) {
ctx.console.error('chmod: invalid mode')
ctx.exitCode = 1
return
}
perm &= 0o777
}
await ctx.vfs.chmod(f, perm)
} catch (e) {
ctx.console.error('chmod: ' + f + ': ' + (e.message || e))
ctx.exitCode = 1
+6
View File
@@ -0,0 +1,6 @@
async function run(ctx, argv) {
ctx.console.error(
'chown: changing file ownership is not supported on Bare OS (single-user Hyperdrive metadata).'
)
ctx.exitCode = 1
}
+84
View File
@@ -0,0 +1,84 @@
/** POSIX cksum CRC (Open Group / npm `cksum` reference). */
const BARE_CKSUM_TAB = new Uint32Array([
0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b,
0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 0x4c11db70, 0x48d0c6c7,
0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3,
0x709f7b7a, 0x745e66cd, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0xbe2b5b58, 0xbaea46ef,
0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb,
0xceb42022, 0xca753d95, 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0x34867077, 0x30476dc0,
0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4,
0x0808d07d, 0x0cc9cdca, 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 0x5e9f46bf, 0x5a5e5b08,
0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc,
0xb6238b25, 0xb2e29692, 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 0xe0b41de7, 0xe4750050,
0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34,
0xdc3abded, 0xd8fba05a, 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 0x4f040d56, 0x4bc510e1,
0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5,
0x3f9b762c, 0x3b5a6b9b, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 0xf12f560e, 0xf5ee4bb9,
0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd,
0xcda1f604, 0xc960ebb3, 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 0x9b3660c6, 0x9ff77d71,
0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2,
0x470cdd2b, 0x43cdc09c, 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 0x119b4be9, 0x155a565e,
0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a,
0x2d15ebe3, 0x29d4f654, 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 0xe3a1cbc1, 0xe760d676,
0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662,
0x933eb0bb, 0x97ffad0c, 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
])
function barePosixCksum(u8) {
let crc = 0
for (let i = 0; i < u8.length; i++) {
crc =
(BARE_CKSUM_TAB[(u8[i] ^ ((crc >>> 24) & 0xff))] ^ (crc << 8)) >>> 0
}
let n = u8.length
while (n > 0) {
crc =
(BARE_CKSUM_TAB[((n & 0xff) ^ ((crc >>> 24) & 0xff))] ^ (crc << 8)) >>> 0
n >>>= 8
}
return (~crc >>> 0)
}
async function run(ctx, argv) {
const files = argv.slice(1).filter((a) => a && !a.startsWith('-'))
async function one(name, u8) {
const sum = barePosixCksum(u8)
ctx.console.log(sum + ' ' + u8.length + ' ' + name)
}
if (!files.length) {
const buf = ctx.b4a.from(bareStdin(ctx))
await one('', buf instanceof Uint8Array ? buf : new Uint8Array(buf))
return
}
for (const f of files) {
const buf = await ctx.vfs.readFile(f)
if (!buf) {
ctx.console.error('cksum: ' + f + ': No such file')
ctx.exitCode = 1
continue
}
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
await one(f, u8)
}
}
+86
View File
@@ -0,0 +1,86 @@
async function copyPath(ctx, from, to, recursive) {
const st = await ctx.vfs.lstat(from)
if (!st) {
ctx.console.error('cp: ' + from + ': No such file')
return false
}
if (st.type === 'symlink') {
const t = await ctx.vfs.readlink(from)
await ctx.vfs.symlink(t, to)
return true
}
if (st.type === 'file') {
const buf = await ctx.vfs.readFile(from)
if (!buf) {
ctx.console.error('cp: cannot read ' + from)
return false
}
await ctx.vfs.writeFile(to, buf)
return true
}
if (st.type === 'directory') {
if (!recursive) {
ctx.console.error('cp: ' + from + ': Is a directory')
return false
}
await ctx.vfs.mkdir(to, { recursive: true })
const names = await ctx.vfs.readdir(from)
for (const n of names) {
if (n === '.bareos_empty') continue
const f = from.replace(/\/+$/, '') + '/' + n
const t = to.replace(/\/+$/, '') + '/' + n
if (!(await copyPath(ctx, f, t, true))) return false
}
return true
}
return false
}
async function run(ctx, argv) {
let recursive = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-R' || a === '-r' || a === '--recursive') {
recursive = true
continue
}
if (a === '--') {
paths.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-')) {
ctx.console.error('cp: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (paths.length < 2) {
ctx.console.error('usage: cp [-R] SOURCE... DEST')
ctx.exitCode = 1
return
}
const dest = paths.pop()
const sources = paths
let destIsDir = false
try {
const dst = await ctx.vfs.lstat(dest)
destIsDir = !!(dst && dst.type === 'directory')
} catch {
destIsDir = false
}
if (sources.length > 1 && !destIsDir) {
ctx.console.error('cp: target is not a directory')
ctx.exitCode = 1
return
}
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))) ctx.exitCode = 1
}
}
+80
View File
@@ -0,0 +1,80 @@
async function run(ctx, argv) {
let delim = '\t'
let fieldsSpec = ''
const files = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-d') {
delim = argv[++i] || ''
continue
}
if (a === '-f') {
fieldsSpec = argv[++i] || ''
continue
}
if (a === '--') {
files.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-')) {
ctx.console.error('cut: unsupported option ' + a)
ctx.exitCode = 1
return
}
files.push(a)
}
if (!fieldsSpec) {
ctx.console.error('usage: cut -f LIST [-d DELIM] [FILE...]')
ctx.exitCode = 1
return
}
function parseFields(s) {
/** @type {Set<number>} */
const set = new Set()
for (const part of s.split(',')) {
const m = /^(\d+)(?:-(\d+))?$/.exec(part.trim())
if (!m) continue
const a = Number(m[1])
const b = m[2] != null ? Number(m[2]) : a
for (let j = a; j <= b; j++) set.add(j)
}
return set
}
const want = parseFields(fieldsSpec)
if (!want.size) {
ctx.console.error('cut: invalid field list')
ctx.exitCode = 1
return
}
async function cutLines(text) {
const lines = text.split(/\r?\n/)
for (const line of lines) {
if (line === '' && lines.length === 1) continue
const cols =
delim === ''
? [...line]
: line.split(delim === '\\t' ? '\t' : delim)
const out = []
for (const n of [...want].sort((a, b) => a - b)) {
out.push(cols[n - 1] != null ? cols[n - 1] : '')
}
ctx.console.log(out.join(delim === '\\t' ? '\t' : delim))
}
}
if (!files.length) {
await cutLines(bareStdin(ctx))
return
}
for (const f of files) {
const buf = await ctx.vfs.readFile(f)
if (!buf) {
ctx.console.error('cut: ' + f + ': No such file')
ctx.exitCode = 1
continue
}
await cutLines(ctx.b4a.toString(buf))
}
}
+40
View File
@@ -0,0 +1,40 @@
async function duPath(ctx, path, blockSize) {
const st = await ctx.vfs.lstat(path)
if (!st) return 0
if (st.type === 'file' || st.type === 'symlink') {
return Math.ceil((st.size || 0) / blockSize) || 1
}
let total = Math.ceil((st.size || 0) / blockSize) || 1
let names = []
try {
names = await ctx.vfs.readdir(path)
} catch {
return total
}
for (const n of names) {
if (n === '.bareos_empty') continue
const sub = path.replace(/\/+$/, '') + '/' + n
total += await duPath(ctx, sub, blockSize)
}
return total
}
async function run(ctx, argv) {
let blockSize = 512
const paths = []
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-k') blockSize = 1024
else if (argv[i] !== '--') paths.push(argv[i])
}
const targets = paths.length ? paths : ['.']
for (const p of targets) {
try {
const abs = ctx.vfs.resolveLogical(p)
const kb = await duPath(ctx, abs, blockSize)
ctx.console.log(String(kb) + '\t' + p)
} catch (e) {
ctx.console.error('du: ' + p + ': ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
}
+69
View File
@@ -0,0 +1,69 @@
async function walk(ctx, dir, nameRe, wantType, maxDepth, curDepth) {
if (maxDepth >= 0 && curDepth > maxDepth) return
let names
try {
names = await ctx.vfs.readdir(dir)
} catch {
return
}
for (const n of names) {
if (n === '.bareos_empty') continue
const path = dir.replace(/\/+$/, '') + '/' + n
let st
try {
st = await ctx.vfs.lstat(path)
} catch {
continue
}
if (!st) continue
if (!nameRe || nameRe.test(n)) {
if (!wantType || st.type === wantType) ctx.console.log(path)
}
if (st.type === 'directory') await walk(ctx, path, nameRe, wantType, maxDepth, curDepth + 1)
}
}
async function run(ctx, argv) {
let maxDepth = -1
/** @type {string | null} */
let nameGlob = null
/** @type {'file' | 'directory' | 'symlink' | null} */
let wantType = null
const rest = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-maxdepth' && argv[i + 1]) {
maxDepth = Number.parseInt(argv[++i], 10)
continue
}
if (a === '-name' && argv[i + 1]) {
nameGlob = argv[++i]
continue
}
if (a === '-type' && argv[i + 1]) {
const t = argv[++i]
if (t === 'f') wantType = 'file'
else if (t === 'd') wantType = 'directory'
else if (t === 'l') wantType = 'symlink'
continue
}
if (a === '--') {
rest.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-')) {
ctx.console.error('find: unsupported ' + a)
ctx.exitCode = 1
return
}
rest.push(a)
}
const root = rest[0] || '.'
let nameRe = null
if (nameGlob) {
const esc = nameGlob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.')
nameRe = new RegExp('^' + esc + '$')
}
const abs = ctx.vfs.resolveLogical(root)
await walk(ctx, abs, nameRe, wantType, maxDepth, 0)
}
+10
View File
@@ -0,0 +1,10 @@
async function run(ctx, argv) {
const name = argv[1]
if (!name) {
ctx.console.error('usage: getconf VARIABLE_NAME')
ctx.exitCode = 1
return
}
ctx.console.error('getconf: not implemented on Bare OS (no full sysconf path): ' + name)
ctx.exitCode = 1
}
+4 -1
View File
@@ -1,6 +1,9 @@
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | builtins: alias, cd, export, exit, login, logout, unalias | /bin: basename cat chmod clear crontab date dirname echo env exit false git grep head hdms help hostname id login logout ls nl pathchk printenv pwd rm savevault seq sleep sort tail test touch true tty uname wc which whoami'
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab cut date dirname du echo env exit false find getconf grep head hdms help hostname id ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc which whoami xargs'
)
ctx.console.log(
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
)
ctx.console.log(
'Identity: login [--new] <passphrase> | logout [--save] | savevault (encrypt copy of personal drive under /.bare/vault/)'
+37
View File
@@ -0,0 +1,37 @@
async function run(ctx, argv) {
let sym = false
const rest = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-s' || a === '--symbolic') {
sym = true
continue
}
if (a === '--') {
rest.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-')) {
ctx.console.error('ln: unsupported option ' + a)
ctx.exitCode = 1
return
}
rest.push(a)
}
if (!sym) {
ctx.console.error('ln: only symbolic links (-s) are supported on Bare OS')
ctx.exitCode = 1
return
}
if (rest.length !== 2) {
ctx.console.error('usage: ln -s TARGET LINK_NAME')
ctx.exitCode = 1
return
}
try {
await ctx.vfs.symlink(rest[0], rest[1])
} catch (e) {
ctx.console.error('ln: ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
@@ -0,0 +1,4 @@
async function run(ctx, argv) {
const u = ctx.vfs.env.LOGNAME || ctx.vfs.env.USER || 'guest'
ctx.console.log(u)
}
+171
View File
@@ -0,0 +1,171 @@
async function run(ctx, argv) {
const env = ctx.env || {}
const width = bareManParseWidth(env)
const args = argv.slice(1)
function usage() {
ctx.console.error(
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' +
' Section 1: /bin utilities; section 7: Bare OS handbook (man 7 bare-os-handbook).\n' +
' Data: /share/man/man.json on the system drive.'
)
ctx.exitCode = 2
}
if (args.length === 0) {
usage()
return
}
let mode = 'page'
let kWord = ''
let fName = ''
const rest = []
for (let i = 0; i < args.length; i++) {
const a = args[i]
if (a === '--') {
rest.push(...args.slice(i + 1))
break
}
if (a === '-k' || a === '--apropos') {
mode = 'apropos'
kWord = args[++i] || ''
if (!kWord) {
usage()
return
}
continue
}
if (a === '-f' || a === '--whatis') {
mode = 'whatis'
fName = args[++i] || ''
if (!fName) {
usage()
return
}
continue
}
if (a === '-l' || a === '--list') {
mode = 'list'
continue
}
if (a.startsWith('-')) {
ctx.console.error('man: unknown option: ' + a)
ctx.exitCode = 2
return
}
rest.push(a)
}
const drive = ctx.drive
if (!drive || typeof drive.get !== 'function') {
ctx.console.error('man: no system drive in context')
ctx.exitCode = 1
return
}
let buf
try {
buf = await drive.get('/share/man/man.json', { follow: true })
} catch {
buf = null
}
if (!buf) {
ctx.console.error('man: manual database not found (/share/man/man.json)')
ctx.exitCode = 1
return
}
let db
try {
db = JSON.parse(ctx.b4a.toString(buf))
} catch (e) {
ctx.console.error('man: invalid manual database: ' + (e.message || e))
ctx.exitCode = 1
return
}
if (!db.pages || !db.index) {
ctx.console.error('man: malformed manual database')
ctx.exitCode = 1
return
}
if (mode === 'list') {
const rows = db.pages
.map((p) => ({ n: p.name, s: p.section }))
.sort((a, b) => (a.n !== b.n ? (a.n < b.n ? -1 : 1) : a.s - b.s))
for (const r of rows) ctx.console.log(r.n + '(' + r.s + ')')
return
}
if (mode === 'apropos') {
const needle = kWord.toLowerCase()
const seen = new Set()
const hits = []
if (db.apropos && Array.isArray(db.apropos)) {
for (const row of db.apropos) {
if (typeof row.kw !== 'string') continue
if (!row.kw.includes(needle)) continue
const idx = row.pageRef
if (typeof idx !== 'number' || !db.pages[idx]) continue
if (seen.has(idx)) continue
seen.add(idx)
const p = db.pages[idx]
hits.push(p.name + '(' + p.section + ') - ' + p.title)
}
}
hits.sort()
for (const line of hits) ctx.console.log(line)
return
}
if (mode === 'whatis') {
const key = fName.toLowerCase()
const idx = db.index[key]
if (idx === undefined || !db.pages[idx]) {
ctx.console.error('man: nothing appropriate for ' + fName)
ctx.exitCode = 1
return
}
const p = db.pages[idx]
ctx.console.log(p.name + '(' + p.section + ') - ' + p.title)
return
}
if (rest.length === 0) {
usage()
return
}
/** @type {number | null} */
let sectionExplicit = null
let name = rest[0]
if (rest.length >= 2 && /^[0-9]+$/.test(rest[0])) {
sectionExplicit = Number.parseInt(rest[0], 10)
name = rest[1]
}
if (sectionExplicit !== null && (sectionExplicit < 1 || sectionExplicit > 8)) {
ctx.console.error('man: section must be between 1 and 8')
ctx.exitCode = 2
return
}
const idx = db.index[String(name).toLowerCase()]
if (idx === undefined || !db.pages[idx]) {
ctx.console.error('man: no manual entry for ' + name)
ctx.exitCode = 1
return
}
const page = db.pages[idx]
if (sectionExplicit !== null && page.section !== sectionExplicit) {
ctx.console.error(
'man: no entry for ' + name + ' in section ' + sectionExplicit + ' (see man -l)'
)
ctx.exitCode = 1
return
}
ctx.console.log(bareManRenderPage(page, ctx, width).replace(/\n$/, ''))
}
+34
View File
@@ -0,0 +1,34 @@
async function run(ctx, argv) {
let parents = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-p' || a === '--parents') {
parents = true
continue
}
if (a === '--') {
paths.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-')) {
ctx.console.error('mkdir: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (!paths.length) {
ctx.console.error('usage: mkdir [-p] DIRECTORY...')
ctx.exitCode = 1
return
}
for (const p of paths) {
try {
await ctx.vfs.mkdir(p, { recursive: parents })
} catch (e) {
ctx.console.error('mkdir: ' + p + ': ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
}
+4
View File
@@ -0,0 +1,4 @@
async function run(ctx, argv) {
ctx.console.error('mkfifo: FIFOs are not supported in this JavaScript VFS.')
ctx.exitCode = 1
}
+66
View File
@@ -0,0 +1,66 @@
async function mvCopyPath(ctx, from, to, recursive) {
const st = await ctx.vfs.lstat(from)
if (!st) return false
if (st.type === 'symlink') {
await ctx.vfs.symlink(await ctx.vfs.readlink(from), to)
return true
}
if (st.type === 'file') {
const buf = await ctx.vfs.readFile(from)
if (!buf) return false
await ctx.vfs.writeFile(to, buf)
return true
}
if (st.type === 'directory') {
if (!recursive) return false
await ctx.vfs.mkdir(to, { recursive: true })
const names = await ctx.vfs.readdir(from)
for (const n of names) {
if (n === '.bareos_empty') continue
const f = from.replace(/\/+$/, '') + '/' + n
const t = to.replace(/\/+$/, '') + '/' + n
if (!(await mvCopyPath(ctx, f, t, true))) return false
}
return true
}
return false
}
async function run(ctx, argv) {
const paths = argv.slice(1).filter((a) => a && a !== '--')
if (paths.length < 2) {
ctx.console.error('usage: mv SOURCE... DEST')
ctx.exitCode = 1
return
}
const dest = paths.pop()
const sources = paths
let destIsDir = false
try {
const dst = await ctx.vfs.lstat(dest)
destIsDir = !!(dst && dst.type === 'directory')
} catch {
destIsDir = false
}
if (sources.length > 1 && !destIsDir) {
ctx.console.error('mv: target is not a directory')
ctx.exitCode = 1
return
}
for (const src of sources) {
const base = src.replace(/\/+$/, '').split('/').pop() || src
const target =
destIsDir || sources.length > 1
? dest.replace(/\/+$/, '') + '/' + base
: dest
try {
if (!(await mvCopyPath(ctx, src, target, true))) {
throw new Error('cannot copy')
}
await ctx.vfs.rm(src, { recursive: true, force: true })
} catch (e) {
ctx.console.error('mv: ' + src + ': ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
}
+32
View File
@@ -0,0 +1,32 @@
function hexByte(b) {
return b.toString(16).padStart(2, '0')
}
async function run(ctx, argv) {
const paths = argv.slice(1).filter((a) => a && !a.startsWith('-'))
let buf
if (!paths.length) {
buf = ctx.b4a.from(bareStdin(ctx))
} else {
const b = await ctx.vfs.readFile(paths[0])
if (!b) {
ctx.console.error('od: cannot read file')
ctx.exitCode = 1
return
}
buf = b
}
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
let off = 0
while (off < u8.length) {
const chunk = u8.slice(off, off + 16)
const hex = [...chunk].map((x) => hexByte(x)).join(' ')
const asc = [...chunk]
.map((x) => (x >= 32 && x < 127 ? String.fromCharCode(x) : '.'))
.join('')
ctx.console.log(
off.toString(8).padStart(7, '0') + ' ' + hex.padEnd(47, ' ') + ' |' + asc + '|'
)
off += 16
}
}
+46
View File
@@ -0,0 +1,46 @@
function barePrintfFormat(fmt, args) {
let ai = 0
let o = ''
for (let i = 0; i < fmt.length; i++) {
if (fmt[i] !== '%') {
o += fmt[i]
continue
}
if (fmt[i + 1] === '%') {
o += '%'
i++
continue
}
let j = i + 1
while (j < fmt.length && /[0-9.#\-+ ]/.test(fmt[j])) j++
const spec = fmt[j] || 's'
const arg = args[ai++]
if (spec === 's') o += String(arg)
else if (spec === 'd' || spec === 'i') o += String(Math.trunc(Number(arg)))
else if (spec === 'u') o += String(Math.trunc(Number(arg)) >>> 0)
else if (spec === 'x') o += (Math.trunc(Number(arg)) >>> 0).toString(16)
else if (spec === 'X') o += (Math.trunc(Number(arg)) >>> 0).toString(16).toUpperCase()
else if (spec === 'o') o += (Math.trunc(Number(arg)) >>> 0).toString(8)
else if (spec === 'c') o += String.fromCharCode(Number(arg) || 0)
else if (spec === 'f') o += String(Number(arg))
else o += String(arg)
i = j
}
return o
}
async function run(ctx, argv) {
if (argv.length < 2) {
ctx.console.error('usage: printf FORMAT [ARG...]')
ctx.exitCode = 1
return
}
const fmt = argv[1]
const args = argv.slice(2)
let unescaped = fmt
unescaped = unescaped.replace(/\\n/g, '\n')
unescaped = unescaped.replace(/\\t/g, '\t')
unescaped = unescaped.replace(/\\r/g, '\r')
unescaped = unescaped.replace(/\\\\/g, '\\')
ctx.console.log(barePrintfFormat(unescaped, args))
}
@@ -0,0 +1,20 @@
async function run(ctx, argv) {
let noNewline = false
const paths = []
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-n') noNewline = true
else if (argv[i] !== '--') paths.push(argv[i])
}
if (!paths.length) {
ctx.console.error('usage: readlink [-n] FILE')
ctx.exitCode = 1
return
}
try {
const t = await ctx.vfs.readlink(paths[0])
ctx.console.log(noNewline ? t : t)
} catch (e) {
ctx.console.error('readlink: ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
+16
View File
@@ -0,0 +1,16 @@
async function run(ctx, argv) {
const paths = argv.slice(1).filter((a) => a && a !== '--')
if (!paths.length) {
ctx.console.error('usage: rmdir DIRECTORY...')
ctx.exitCode = 1
return
}
for (const p of paths) {
try {
await ctx.vfs.rmdir(p)
} catch (e) {
ctx.console.error('rmdir: ' + p + ': ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
}
+140
View File
@@ -0,0 +1,140 @@
async function run(ctx, argv) {
let silent = false
let extended = false
/** @type {string[]} */
const scripts = []
/** @type {string[]} */
const files = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-n' || a === '--quiet' || a === '--silent') {
silent = true
continue
}
if (a === '-E' || a === '-r') {
extended = true
continue
}
if (a === '-e') {
scripts.push(argv[++i] || '')
continue
}
if (a === '-f') {
const path = argv[++i]
if (!path) {
ctx.console.error('sed: -f requires a file')
ctx.exitCode = 1
return
}
const buf = await ctx.vfs.readFile(path)
if (!buf) {
ctx.console.error('sed: cannot read ' + path)
ctx.exitCode = 1
return
}
scripts.push(ctx.b4a.toString(buf))
continue
}
if (a === '--') {
files.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-') && a.length > 1) {
ctx.console.error('sed: unsupported option ' + a)
ctx.exitCode = 1
return
}
scripts.push(a)
files.push(...argv.slice(i + 1))
break
}
if (!scripts.length) {
ctx.console.error('usage: sed [-n] [-E] {-e script | -f file} [file...]')
ctx.exitCode = 1
return
}
function bareSedCollectRPaths(scList) {
const set = new Set()
const s = scList.join('\n')
const m0 = /^\s*r\s+([^\n;]+)/.exec(s)
if (m0) set.add(m0[1].trim())
let i = 0
while (i < s.length) {
const j = s.indexOf('\nr', i)
const k = s.indexOf(';r', i)
let hit = -1
if (j >= 0 && (k < 0 || j <= k)) hit = j + 1
else if (k >= 0) hit = k + 1
if (hit < 0) break
let p = hit + 1
while (p < s.length && /[ \t]/.test(s[p])) p++
let end = p
while (end < s.length && s[end] !== '\n' && s[end] !== ';') end++
const path = s.slice(p, end).trim()
if (path) set.add(path)
i = end
}
return [...set]
}
/** @type {Record<string, string>} */
const readCache = Object.create(null)
for (const rp of bareSedCollectRPaths(scripts)) {
const buf = await ctx.vfs.readFile(rp)
readCache[rp] = buf ? ctx.b4a.toString(buf) : ''
}
/** @type {string[]} */
const lines = []
async function pushFile(path) {
const buf = await ctx.vfs.readFile(path)
if (!buf) {
ctx.console.error('sed: ' + path + ': No such file')
ctx.exitCode = 1
return false
}
const t = ctx.b4a.toString(buf)
const ls = t.split(/\r?\n/)
if (ls.length && ls[ls.length - 1] === '') ls.pop()
lines.push(...ls)
return true
}
if (!files.length) {
const s = bareStdin(ctx)
const ls = s.split(/\r?\n/)
if (ls.length && ls[ls.length - 1] === '') ls.pop()
lines.push(...ls)
} else {
for (const f of files) {
if (!(await pushFile(f))) return
}
}
/** @type {Record<string, string>} */
const wAccum = Object.create(null)
const out = bareSedRun(lines, scripts, {
silent,
extended,
readFile: (p) => readCache[p] ?? null,
writeFile: (p, chunk) => {
wAccum[p] = (wAccum[p] || '') + chunk
},
lastLineHint: lines.length
})
for (const [p, data] of Object.entries(wAccum)) {
try {
const prev = await ctx.vfs.readFile(p)
const merged = prev ? ctx.b4a.concat([prev, ctx.b4a.from(data)]) : ctx.b4a.from(data)
await ctx.vfs.writeFile(p, merged)
} catch (e) {
ctx.console.error('sed: ' + p + ': ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
const t = out.replace(/\n$/, '')
ctx.console.log(t)
}
+40
View File
@@ -0,0 +1,40 @@
async function run(ctx, argv) {
const paths = argv.slice(1).filter((a) => a && !a.startsWith('-'))
if (!paths.length) {
ctx.console.error('usage: stat FILE...')
ctx.exitCode = 1
return
}
for (const p of paths) {
try {
const st = await ctx.vfs.stat(p)
if (!st) {
ctx.console.error('stat: ' + p + ': No such file')
ctx.exitCode = 1
continue
}
const modeStr = bareFormatModeString(st.mode, st.type)
ctx.console.log(
'File: ' +
p +
'\n' +
'Size: ' +
(st.size || 0) +
'\n' +
'Type: ' +
st.type +
'\n' +
'Mode: ' +
modeStr +
'\n' +
'Uid: ' +
(st.uid ?? 0) +
' Gid: ' +
(st.gid ?? 0)
)
} catch (e) {
ctx.console.error('stat: ' + p + ': ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
}
+25
View File
@@ -0,0 +1,25 @@
async function run(ctx, argv) {
let append = false
const files = []
for (let i = 1; i < argv.length; i++) {
if (argv[i] === '-a' || argv[i] === '--append') append = true
else if (argv[i] !== '--') files.push(argv[i])
}
const text = bareStdin(ctx)
const data = ctx.b4a.from(text)
for (const f of files) {
try {
if (append) {
const prev = await ctx.vfs.readFile(f)
const merged = prev ? ctx.b4a.concat([prev, data]) : data
await ctx.vfs.writeFile(f, merged)
} else {
await ctx.vfs.writeFile(f, data)
}
} catch (e) {
ctx.console.error('tee: ' + f + ': ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
ctx.console.log(text.replace(/\n$/, ''))
}
+19
View File
@@ -0,0 +1,19 @@
async function run(ctx, argv) {
const cmd = argv.slice(1)
if (!cmd.length) {
ctx.console.error('usage: time COMMAND [ARG...]')
ctx.exitCode = 1
return
}
const t0 = Date.now()
try {
if (typeof ctx.runBinCommand === 'function') await ctx.runBinCommand(cmd)
else {
ctx.console.error('time: runBinCommand is not available')
ctx.exitCode = 1
}
} finally {
const ms = Date.now() - t0
ctx.console.error('real\t' + (ms / 1000).toFixed(3) + 's')
}
}

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