Update man to add the manual guide
This commit is contained in:
@@ -20,22 +20,22 @@ node index.js
|
||||
|
||||
## Major subsystems (lib/)
|
||||
|
||||
| Module | Role |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------- |
|
||||
| `index.js` | Swarm, `SwarmDisk`, boot timeout, `executeKernel`, ctx assembly |
|
||||
| `swarm-disk.js` | Peer mux, MBR/read RPC, replication hooks |
|
||||
| `kernel-runner.js` | `runKernelFromSource`, `runBinCommand` (paths, `*.js` in cwd, PATH) |
|
||||
| `vfs.js` | Two-drive routing: system vs `$HOME` on personal drive |
|
||||
| `shell.js` | Tokenize, pipelines, redirections, builtins, `execShellLine` |
|
||||
| `identity-session.js` / `identity-account.js` | Guest vs unlocked user, `/.bare/account`, vault |
|
||||
| `hdms-manager.js` | Extra Hyperdrives, mounts under `/mnt`, Autopass pair/invite |
|
||||
| `bare-initd.js` | Service registry, `startBareInitd`, `stopBareInitd`, **kernel-logger** |
|
||||
| `bare-cron.js` | `~/.crontab`, minute scheduler, `ctx.execLine` jobs |
|
||||
| `repl-session.js` | Fish-style TTY line editor, synced `console`, cleanup → `stopBareInitd` |
|
||||
| `boot-splash.js` | TTY boot progress UI |
|
||||
| `fish-readline.js` | History, completion helpers |
|
||||
| `cli-readline.js`, `resolve-stdio.js`, `debug-repl.js` | Stdio and debug tooling |
|
||||
| `paths.js` | Pear-safe package root and Corestore path |
|
||||
| Module | Role |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `index.js` | Swarm, `SwarmDisk`, boot timeout, `executeKernel`, ctx assembly |
|
||||
| `swarm-disk.js` | Peer mux, MBR/read RPC, replication hooks |
|
||||
| `kernel-runner.js` | `runKernelFromSource`, `runBinCommand`, `resolveBinInPath` (PATH on system drive) |
|
||||
| `vfs.js` | Two-drive routing; `mkdir`/`rmdir` (`.bareos_empty`), `chmod`, `symlink`, … |
|
||||
| `shell.js` | Tokenize, pipelines, redirections, builtins (`unset`, `readonly`, `umask`, `command`, `type`, …), `execShellLine` |
|
||||
| `identity-session.js` / `identity-account.js` | Guest vs unlocked user, `/.bare/account`, vault |
|
||||
| `hdms-manager.js` | Extra Hyperdrives, mounts under `/mnt`, Autopass pair/invite |
|
||||
| `bare-initd.js` | Service registry, `startBareInitd`, `stopBareInitd`, **kernel-logger** |
|
||||
| `bare-cron.js` | `~/.crontab`, minute scheduler, `ctx.execLine` jobs |
|
||||
| `repl-session.js` | Fish-style TTY line editor, synced `console`, cleanup → `stopBareInitd` |
|
||||
| `boot-splash.js` | TTY boot progress UI |
|
||||
| `fish-readline.js` | History, completion helpers |
|
||||
| `cli-readline.js`, `resolve-stdio.js`, `debug-repl.js` | Stdio and debug tooling |
|
||||
| `paths.js` | Pear-safe package root and Corestore path |
|
||||
|
||||
## Environment
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import Hyperdrive from 'hyperdrive'
|
||||
import Corestore from 'corestore'
|
||||
import { topicKey, parseMbr } from 'bare-os-protocol'
|
||||
import { SwarmDisk } from './lib/swarm-disk.js'
|
||||
import { runKernelFromSource } from './lib/kernel-runner.js'
|
||||
import { runKernelFromSource, runBinCommand } from './lib/kernel-runner.js'
|
||||
import { createVfs } from './lib/vfs.js'
|
||||
import { execShellLine } from './lib/shell.js'
|
||||
import { packageRootDir, defaultBootCorestorePath } from './lib/paths.js'
|
||||
@@ -306,7 +306,14 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
await saveVaultToDrive(this)
|
||||
},
|
||||
/** Populated by `loadBarerc` / first `execShellLine` (default alias table). */
|
||||
shellAliases: undefined
|
||||
shellAliases: undefined,
|
||||
/**
|
||||
* Run a /bin or PATH command (same resolution as the shell). Used by utilities like `time`.
|
||||
* @param {string[]} argv
|
||||
*/
|
||||
async runBinCommand(argv) {
|
||||
return runBinCommand(this, argv)
|
||||
}
|
||||
}
|
||||
|
||||
await applyGuestEnv(ctx)
|
||||
|
||||
@@ -118,3 +118,23 @@ export async function runBinCommand(ctx, argv) {
|
||||
|
||||
ctx.console.log('unknown command: ' + cmd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an executable name on PATH (system drive only).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} name
|
||||
* @returns {Promise<string | null>}
|
||||
*/
|
||||
export async function resolveBinInPath(ctx, name) {
|
||||
if (!name || name.includes('/')) return null
|
||||
const systemDrive = ctx.drive
|
||||
const vfs = ctx.vfs
|
||||
const pathEnv = (vfs && vfs.env && vfs.env.PATH) || '/bin'
|
||||
const dirs = pathEnv.split(':').filter(Boolean)
|
||||
for (const dir of dirs) {
|
||||
const p = unixPathResolve(dir, name)
|
||||
const buf = await systemDrive.get(p, { follow: true })
|
||||
if (buf) return p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
import { runBinCommand } from './kernel-runner.js'
|
||||
import { runBinCommand, resolveBinInPath } from './kernel-runner.js'
|
||||
|
||||
const SHELL_BUILTINS = new Set([
|
||||
'alias',
|
||||
'unalias',
|
||||
'cd',
|
||||
'export',
|
||||
'unset',
|
||||
'readonly',
|
||||
'umask',
|
||||
':',
|
||||
'command',
|
||||
'type',
|
||||
'login',
|
||||
'logout',
|
||||
'exit'
|
||||
])
|
||||
|
||||
function isShellBuiltin(cmd) {
|
||||
return SHELL_BUILTINS.has(cmd)
|
||||
}
|
||||
|
||||
/** Max alias indirections (prevents cycles). */
|
||||
const MAX_ALIAS_DEPTH = 16
|
||||
@@ -428,11 +448,7 @@ export async function execShellLine(ctx, line) {
|
||||
const cmd = pipeline[pi]
|
||||
const isLast = pi === pipeline.length - 1
|
||||
|
||||
for (const [k, val] of Object.entries(cmd.assign)) {
|
||||
env[k] = expandWord(val, env)
|
||||
}
|
||||
|
||||
if (!cmd.argv.length) continue
|
||||
if (!cmd.argv.length && !Object.keys(cmd.assign).length) continue
|
||||
|
||||
if (!ctx.shellAliases) ctx.shellAliases = { ...defaultShellAliases() }
|
||||
|
||||
@@ -454,6 +470,17 @@ export async function execShellLine(ctx, line) {
|
||||
const outChunks = []
|
||||
const origLog = ctx.console.log
|
||||
const origErr = ctx.console.error
|
||||
|
||||
for (const [k, val] of Object.entries(cmd.assign)) {
|
||||
if (ctx.shellReadonlyVars instanceof Set && ctx.shellReadonlyVars.has(k)) {
|
||||
origErr.call(ctx.console, k + ': readonly variable')
|
||||
continue
|
||||
}
|
||||
env[k] = expandWord(val, env)
|
||||
}
|
||||
|
||||
if (!cmd.argv.length) continue
|
||||
|
||||
if (!isLast || cmd.redirOut) {
|
||||
ctx.console.log = (...args) => {
|
||||
outChunks.push(args.map(String).join(' ') + '\n')
|
||||
@@ -492,7 +519,83 @@ export async function execShellLine(ctx, line) {
|
||||
} else if (name === 'export') {
|
||||
for (const a of argv.slice(1)) {
|
||||
const eq = a.indexOf('=')
|
||||
if (eq > 0) env[a.slice(0, eq)] = expandWord(a.slice(eq + 1), env)
|
||||
if (eq > 0) {
|
||||
const k = a.slice(0, eq)
|
||||
if (ctx.shellReadonlyVars instanceof Set && ctx.shellReadonlyVars.has(k)) {
|
||||
origErr.call(ctx.console, k + ': readonly variable')
|
||||
continue
|
||||
}
|
||||
env[k] = expandWord(a.slice(eq + 1), env)
|
||||
}
|
||||
}
|
||||
} else if (name === 'unset') {
|
||||
if (!ctx.shellReadonlyVars) ctx.shellReadonlyVars = new Set()
|
||||
for (const a of argv.slice(1)) {
|
||||
if (a.startsWith('-')) continue
|
||||
if (ctx.shellReadonlyVars.has(a)) {
|
||||
origErr.call(
|
||||
ctx.console,
|
||||
'unset: ' + a + ': cannot unset: readonly variable'
|
||||
)
|
||||
continue
|
||||
}
|
||||
delete env[a]
|
||||
}
|
||||
} else if (name === 'readonly') {
|
||||
if (!ctx.shellReadonlyVars) ctx.shellReadonlyVars = new Set()
|
||||
for (const a of argv.slice(1)) {
|
||||
if (a.startsWith('-')) continue
|
||||
const eq = a.indexOf('=')
|
||||
if (eq > 0) {
|
||||
const k = a.slice(0, eq)
|
||||
env[k] = expandWord(a.slice(eq + 1), env)
|
||||
ctx.shellReadonlyVars.add(k)
|
||||
} else ctx.shellReadonlyVars.add(a)
|
||||
}
|
||||
} else if (name === 'umask') {
|
||||
if (argv[1] != null) {
|
||||
const oct = argv[1]
|
||||
if (!/^[0-7]{1,4}$/.test(oct)) {
|
||||
origErr.call(ctx.console, 'umask: invalid octal mask')
|
||||
} else {
|
||||
env.UMASK = oct
|
||||
}
|
||||
} else {
|
||||
const u = env.UMASK || '022'
|
||||
origLog.call(ctx.console, String(u).padStart(4, '0'))
|
||||
}
|
||||
} else if (name === ':') {
|
||||
/* no-op */
|
||||
} else if (name === 'command') {
|
||||
const cargs = argv.slice(1)
|
||||
if (cargs.length === 0) {
|
||||
origErr.call(ctx.console, 'command: missing operand')
|
||||
} else if (cargs[0] === '-v' || cargs[0] === '-V') {
|
||||
const cmdn = cargs[1]
|
||||
if (!cmdn) origErr.call(ctx.console, 'command: missing operand')
|
||||
else if (isShellBuiltin(cmdn)) {
|
||||
origLog.call(ctx.console, cmdn)
|
||||
} else {
|
||||
const p = await resolveBinInPath(ctx, cmdn)
|
||||
if (p) origLog.call(ctx.console, p)
|
||||
else origErr.call(ctx.console, 'command: ' + cmdn + ': not found')
|
||||
}
|
||||
} else {
|
||||
const childCtx =
|
||||
stdinText != null
|
||||
? Object.assign({}, ctx, { shellStdin: stdinText, env })
|
||||
: Object.assign({}, ctx, { env })
|
||||
await runBinCommand(childCtx, cargs)
|
||||
}
|
||||
} else if (name === 'type') {
|
||||
const cmdn = argv[1]
|
||||
if (!cmdn) origErr.call(ctx.console, 'type: missing operand')
|
||||
else if (isShellBuiltin(cmdn)) {
|
||||
origLog.call(ctx.console, cmdn + ' is a shell builtin')
|
||||
} else {
|
||||
const p = await resolveBinInPath(ctx, cmdn)
|
||||
if (p) origLog.call(ctx.console, cmdn + ' is ' + p)
|
||||
else origErr.call(ctx.console, 'type: ' + cmdn + ': not found')
|
||||
}
|
||||
} else if (name === 'login') {
|
||||
const rest = argv.slice(1)
|
||||
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
S_IFREG
|
||||
} from './vfs-posix-meta.js'
|
||||
|
||||
/** Empty-directory marker (must match {@link ./git-fs-adapter.js}). */
|
||||
const DIR_MARKER = '.bareos_empty'
|
||||
|
||||
/**
|
||||
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME,
|
||||
* optional HDMS mounts under /mnt/<label>/…
|
||||
@@ -468,6 +471,46 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} abs logical absolute path
|
||||
* @param {Uint8Array | ArrayBuffer} buf
|
||||
*/
|
||||
async function writeFileAtAbs(abs, buf, opts = {}) {
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + abs)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
throw new Error('Read-only mount: ' + abs)
|
||||
}
|
||||
const { drive, path: p } = r
|
||||
if (r.mntReadOnly === false) {
|
||||
if (isHyperdriveRootPath(p)) {
|
||||
throw new Error('Cannot write directory: ' + abs)
|
||||
}
|
||||
} else if (drive !== personalDrive) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + abs)
|
||||
}
|
||||
if (isHyperdriveRootPath(p)) {
|
||||
throw new Error('Cannot write directory: ' + abs)
|
||||
}
|
||||
const existing = await entryOn(drive, p, { follow: false })
|
||||
const hadBlob = !!(existing?.value?.blob)
|
||||
if (hadBlob) await assertTraverseTo(abs, 'write')
|
||||
else await assertParentWritableForCreate(abs)
|
||||
const value = existing?.value
|
||||
const prevBare = extractBareOs(value)
|
||||
const bareOs = mergeBareOsOnWrite(prevBare, env, {
|
||||
executable: opts.executable,
|
||||
bumpMtime: true,
|
||||
legacyExecutable: !!value?.executable
|
||||
})
|
||||
const executable =
|
||||
opts.executable !== undefined ? !!opts.executable : !!value?.executable
|
||||
const metadata = mergeEntryMetadata(value?.metadata, bareOs)
|
||||
return drive.put(p, buf, { executable, metadata })
|
||||
}
|
||||
|
||||
return {
|
||||
get home() {
|
||||
return normalizeHome()
|
||||
@@ -504,40 +547,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
},
|
||||
|
||||
async writeFile(userPath, buf, opts = {}) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
if (r.virtualHomeDir || r.virtualMntRoot) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + userPath)
|
||||
}
|
||||
if (r.mntReadOnly === true) {
|
||||
throw new Error('Read-only mount: ' + userPath)
|
||||
}
|
||||
const { drive, path: p } = r
|
||||
if (r.mntReadOnly === false) {
|
||||
if (isHyperdriveRootPath(p)) {
|
||||
throw new Error('Cannot write directory: ' + userPath)
|
||||
}
|
||||
} else if (drive !== personalDrive) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + userPath)
|
||||
}
|
||||
if (isHyperdriveRootPath(p)) {
|
||||
throw new Error('Cannot write directory: ' + userPath)
|
||||
}
|
||||
const existing = await entryOn(drive, p, { follow: false })
|
||||
const hadBlob = !!(existing?.value?.blob)
|
||||
if (hadBlob) await assertTraverseTo(abs, 'write')
|
||||
else await assertParentWritableForCreate(abs)
|
||||
const value = existing?.value
|
||||
const prevBare = extractBareOs(value)
|
||||
const bareOs = mergeBareOsOnWrite(prevBare, env, {
|
||||
executable: opts.executable,
|
||||
bumpMtime: true,
|
||||
legacyExecutable: !!value?.executable
|
||||
})
|
||||
const executable =
|
||||
opts.executable !== undefined ? !!opts.executable : !!value?.executable
|
||||
const metadata = mergeEntryMetadata(value?.metadata, bareOs)
|
||||
return drive.put(p, buf, { executable, metadata })
|
||||
return writeFileAtAbs(resolveLogical(userPath), buf, opts)
|
||||
},
|
||||
|
||||
async unlink(userPath) {
|
||||
@@ -671,6 +681,72 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
|
||||
return lstatFromAbs(resolveLogical(userPath))
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a directory using a hidden marker file (Hyperdrive has no empty dirs).
|
||||
* @param {string} userPath
|
||||
* @param {{ recursive?: boolean }} [opts]
|
||||
*/
|
||||
async mkdir(userPath, opts = {}) {
|
||||
const recursive = Boolean(opts.recursive)
|
||||
let abs = resolveLogical(userPath)
|
||||
if (abs !== '/' && abs.endsWith('/')) {
|
||||
abs = abs.replace(/\/+$/, '') || '/'
|
||||
}
|
||||
if (abs === '/') {
|
||||
throw new Error('mkdir: cannot create /')
|
||||
}
|
||||
const existing = await lstatFromAbs(abs)
|
||||
if (existing) {
|
||||
if (existing.type === 'directory') return
|
||||
throw new Error('mkdir: File exists')
|
||||
}
|
||||
const empty = new Uint8Array(0)
|
||||
if (!recursive) {
|
||||
const parent = dirnameAbs(abs)
|
||||
const pst = await lstatFromAbs(parent)
|
||||
if (!pst || pst.type !== 'directory') {
|
||||
throw new Error('mkdir: No such file or directory')
|
||||
}
|
||||
await writeFileAtAbs(joinLogical(abs, DIR_MARKER), empty, {})
|
||||
return
|
||||
}
|
||||
const prefs = pathPrefixes(abs)
|
||||
for (let i = 1; i < prefs.length; i++) {
|
||||
const pre = prefs[i]
|
||||
const st = await lstatFromAbs(pre)
|
||||
if (st) {
|
||||
if (st.type === 'directory') continue
|
||||
throw new Error('mkdir: File exists')
|
||||
}
|
||||
const markerAbs = joinLogical(pre, DIR_MARKER)
|
||||
await writeFileAtAbs(markerAbs, empty, {})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an empty directory (only the marker file may remain besides hidden marker).
|
||||
*/
|
||||
async rmdir(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const st = await lstatFromAbs(abs)
|
||||
if (!st) {
|
||||
throw new Error('rmdir: No such file or directory')
|
||||
}
|
||||
if (st.type !== 'directory') {
|
||||
throw new Error('rmdir: Not a directory')
|
||||
}
|
||||
const names = await readdirFromAbs(abs)
|
||||
const rest = names.filter((n) => n !== DIR_MARKER)
|
||||
if (rest.length) {
|
||||
throw new Error('rmdir: Directory not empty')
|
||||
}
|
||||
if (names.includes(DIR_MARKER)) {
|
||||
const mabs = joinLogical(abs, DIR_MARKER)
|
||||
await assertUnlink(mabs)
|
||||
await delFromAbs(mabs)
|
||||
}
|
||||
},
|
||||
|
||||
async readlink(userPath) {
|
||||
const abs = resolveLogical(userPath)
|
||||
const r = route(abs)
|
||||
|
||||
@@ -3,7 +3,7 @@ import b4a from 'b4a'
|
||||
import Hyperdrive from 'hyperdrive'
|
||||
import Corestore from 'corestore'
|
||||
import { mkdirSync, rmSync } from 'node:fs'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { readFile, readdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { PassThrough } from 'node:stream'
|
||||
@@ -796,6 +796,53 @@ test('runGitCli init and status on personal drive', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('kernel share/man/man.json page count matches coreutils + extras + handbook', async (t) => {
|
||||
const manPath = path.join(__dirname, '../../kernel/share/man/man.json')
|
||||
const raw = JSON.parse(await readFile(manPath, 'utf8'))
|
||||
const { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } = await import(
|
||||
'../bare-os-coreutils/lib/commands.mjs'
|
||||
)
|
||||
const handbookDir = path.join(__dirname, '../../handbook')
|
||||
const handbookMd = (await readdir(handbookDir)).filter((f) => f.endsWith('.md')).length
|
||||
t.is(
|
||||
raw.pages.length,
|
||||
COREUTILS_COMMANDS.length + MAN_EXTRA_PAGES.length + handbookMd
|
||||
)
|
||||
t.ok(Array.isArray(raw.apropos) && raw.apropos.length > 0)
|
||||
t.is(typeof raw.index.ls, 'number')
|
||||
t.is(typeof raw.index.handbook, 'number')
|
||||
const hb = raw.pages[raw.index.handbook]
|
||||
t.is(hb.section, 7)
|
||||
t.is(hb.name, 'bare-os-handbook')
|
||||
})
|
||||
|
||||
test('runBinCommand man ls prints manual text', async (t) => {
|
||||
const dir = testCorestoreDir('mann')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pmn'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const manPath = path.join(__dirname, '../../kernel/share/man/man.json')
|
||||
await drive.put('/share/man/man.json', b4a.from(await readFile(manPath, 'utf8')))
|
||||
await drive.put('/bin/man', b4a.from(await readBuiltBin('man')))
|
||||
const logs = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['man', 'ls'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
const text = logs.join('\n')
|
||||
t.ok(text.includes('SYNOPSIS'))
|
||||
t.ok(text.includes('EXAMPLES'))
|
||||
t.ok(text.includes('ls'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function readBuiltBin(name) {
|
||||
const fs = await import('node:fs/promises')
|
||||
const p = path.join(__dirname, '../../kernel/bin', name)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 1–8`)
|
||||
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()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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/)'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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$/, ''))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.error('mkfifo: FIFOs are not supported in this JavaScript VFS.')
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user