/** Shared helpers for drive-resident /bin scripts (prepended before each command). */ function bareStdin(ctx) { return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : '' } /** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */ function bareFormatModeString(mode, type) { const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-' const perm = mode & 0o777 const r = (bit) => (perm & bit ? 'r' : '-') const w = (bit) => (perm & bit ? 'w' : '-') const x = (bit) => (perm & bit ? 'x' : '-') return ( typeChar + r(0o400) + w(0o200) + x(0o100) + r(0o040) + w(0o020) + x(0o010) + r(0o004) + w(0o002) + x(0o001) ) } /** @param {number} mtimeMs @param {number} [nowMs] */ function bareFormatLsMtime(mtimeMs, nowMs) { const now = nowMs != null ? nowMs : Date.now() const d = new Date(mtimeMs) const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] const mon = months[d.getMonth()] const day = String(d.getDate()).padStart(2, ' ') const sixMo = 180 * 24 * 3600 * 1000 if (Math.abs(now - mtimeMs) > sixMo) { const yr = String(d.getFullYear()).padStart(4, ' ') return mon + ' ' + day + ' ' + yr } const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return mon + ' ' + day + ' ' + hh + ':' + mm } /** @param {number} size */ function barePosixBlocks(size) { return Math.ceil(Number(size) / 512) || 0 } /** 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 } 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; section 7: handbook + developer guide (man handbook, man devguide).\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 CAT_ORDER = ['coreutils', 'extra', 'handbook', 'devguide'] const CAT_HEADING = { coreutils: 'Section 1 — /bin utilities', extra: 'Section 1 — Git and shell', handbook: 'Section 7 — Handbook', devguide: 'Section 7 — Developer guide' } function listCategoryOf(p) { const c = p.listCategory if (c === 'coreutils' || c === 'extra' || c === 'handbook' || c === 'devguide') return c if (p.section === 7) { const n = p.name if (n.startsWith('devguide-') || n === 'bare-os-developer-guide') return 'devguide' return 'handbook' } if (p.name === 'git' || p.name === 'bare-os-shell') return 'extra' return 'coreutils' } const rows = db.pages.map((p) => ({ n: p.name, s: p.section, cat: listCategoryOf(p) })) rows.sort((a, b) => { const ia = CAT_ORDER.indexOf(a.cat) const ib = CAT_ORDER.indexOf(b.cat) const ca = ia === -1 ? 99 : ia const cb = ib === -1 ? 99 : ib if (ca !== cb) return ca - cb if (a.n !== b.n) return a.n < b.n ? -1 : 1 return a.s - b.s }) let prevCat = '' for (const r of rows) { if (r.cat !== prevCat) { if (prevCat !== '') ctx.console.log('') ctx.console.log(CAT_HEADING[r.cat] || r.cat) prevCat = r.cat } 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$/, '')) }