updates
This commit is contained in:
+1
-1
@@ -341,7 +341,7 @@ Pear-safe path resolution (same idea as Holepunch [pear-rti](https://github.com/
|
|||||||
|
|
||||||
**`executeKernel(disk, store, swarm, initSource)`** (`store`/`swarm` unused but kept for signature symmetry / future use)
|
**`executeKernel(disk, store, swarm, initSource)`** (`store`/`swarm` unused but kept for signature symmetry / future use)
|
||||||
|
|
||||||
- Builds `shellEnv` with **guest** defaults: `USER`/`LOGNAME`=`guest`, `HOME`/`PWD`=`/home/guest`, `UID`/`GID`=`65534`, `BARE_OS_IDENTITY=guest`, `BARE_OS_EXIT_STATUS`=`0`, `BARE_OS_CTX_API_VERSION`, `PATH=/bin`, `SHELL`, `HOSTNAME`, `0`. `createVfs(drive, personalDrive, shellEnv, vfsMountRef, { procSnapshot })` → `ctx.vfs` (same `env` object as `ctx.env`).
|
- Builds `shellEnv` with **guest** defaults: `USER`/`LOGNAME`=`guest`, `HOME`/`PWD`=`/home/guest`, `UID`/`GID`=`65534`, `BARE_OS_IDENTITY=guest`, `BARE_OS_EXIT_STATUS`=`0`, `BARE_OS_CTX_API_VERSION`, `PATH=/bin`, `SHELL`, `HOSTNAME`, `0`. `createVfs(drive, personalDrive, shellEnv, vfsMountRef, { procSnapshot, bootStartedMs, initdRunText })` → `ctx.vfs` (same `env` object as `ctx.env`). Optional **`bootStartedMs`** and **`initdRunText`** feed synthetic **`/proc/uptime`** and **`/run/bare-os/units`**.
|
||||||
- **`applyGuestEnv(ctx)`** then **`ensureGuestHome(ctx)`** — normalizes `ctx.identity` and seeds `/.bare/` (and a guest marker) on the personal drive.
|
- **`applyGuestEnv(ctx)`** then **`ensureGuestHome(ctx)`** — normalizes `ctx.identity` and seeds `/.bare/` (and a guest marker) on the personal drive.
|
||||||
- **`createReadLine()`** always resolves stdio first and returns `stdout` (may be `null`) alongside `readLine` so the kernel can write to the **same** stream as the REPL (including `bare-stdio` under Pear).
|
- **`createReadLine()`** always resolves stdio first and returns `stdout` (may be `null`) alongside `readLine` so the kernel can write to the **same** stream as the REPL (including `bare-stdio` under Pear).
|
||||||
- Builds `ctx`: **`bareOsCtxApiVersion`** (from [`bare-os-ctx-api.js`](packages/bare-os-booter/lib/bare-os-ctx-api.js)), `disk`, `drive`, `personalDrive`, `vfs`, `env`, `console`, `b4a`, `topic: topicKey()`, `readLine`, **`writeScreen(str)`**, `execLine` → `execShellLine(ctx, line)` (returns `'ok'` or `'exit'`; updates **`BARE_OS_EXIT_STATUS`** in **`vfs.env`**; bare **`exit`** line sets status then **`requestBooterExit`**), **`runBinCommand(argv)`** → delegates to **`runBinCommand(this, argv)`** from `kernel-runner.js` (for `/bin/time` and similar), plus identity hooks for `/bin` and builtins:
|
- Builds `ctx`: **`bareOsCtxApiVersion`** (from [`bare-os-ctx-api.js`](packages/bare-os-booter/lib/bare-os-ctx-api.js)), `disk`, `drive`, `personalDrive`, `vfs`, `env`, `console`, `b4a`, `topic: topicKey()`, `readLine`, **`writeScreen(str)`**, `execLine` → `execShellLine(ctx, line)` (returns `'ok'` or `'exit'`; updates **`BARE_OS_EXIT_STATUS`** in **`vfs.env`**; bare **`exit`** line sets status then **`requestBooterExit`**), **`runBinCommand(argv)`** → delegates to **`runBinCommand(this, argv)`** from `kernel-runner.js` (for `/bin/time` and similar), plus identity hooks for `/bin` and builtins:
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ The following are set on `ctx` before the kernel starts (unless noted as overwri
|
|||||||
|
|
||||||
| Field | Role |
|
| Field | Role |
|
||||||
| ----- | ---- |
|
| ----- | ---- |
|
||||||
| **`bareOsCtxApiVersion`** | String semver for the documented **`ctx`** contract (e.g. **`1.0.0`**). Bump in [`bare-os-ctx-api.js`](../packages/bare-os-booter/lib/bare-os-ctx-api.js) when you make breaking changes to stable fields. |
|
| **`bareOsCtxApiVersion`** | String semver for the documented **`ctx`** contract (e.g. **`1.1.0`**). Bump in [`bare-os-ctx-api.js`](../packages/bare-os-booter/lib/bare-os-ctx-api.js) when you make breaking changes to stable fields. |
|
||||||
|
| **`bareOsBootStartedMs`** | Epoch milliseconds when the booter started building the session (used for synthetic **`/proc/uptime`**). |
|
||||||
|
| **`bareOsSkipRepl`** | **`true`** when **`BARE_OS_SKIP_REPL=1`** (non-interactive stdin); kernels may shorten banners. |
|
||||||
| **`disk`** | Disk bundle used during boot (includes drives and helpers); advanced use |
|
| **`disk`** | Disk bundle used during boot (includes drives and helpers); advanced use |
|
||||||
| **`drive`** | **System** Hyperdrive (`ctx.drive` is the OS image: `/bin`, `/boot`, …) |
|
| **`drive`** | **System** Hyperdrive (`ctx.drive` is the OS image: `/bin`, `/boot`, …) |
|
||||||
| **`personalDrive`** | **Personal** Hyperdrive (mutable per-user state; VFS maps **`$HOME`** to **`/.bare-os/home/<HOME-basename>/…`** and session **`/var/log`** to **`/.bare-os/var/log/<basename>/…`** so guest vs unlocked trees do not share the same keys) |
|
| **`personalDrive`** | **Personal** Hyperdrive (mutable per-user state; VFS maps **`$HOME`** to **`/.bare-os/home/<HOME-basename>/…`** and session **`/var/log`** to **`/.bare-os/var/log/<basename>/…`** so guest vs unlocked trees do not share the same keys) |
|
||||||
|
|||||||
@@ -39,25 +39,36 @@ Cleanup path closes swarm/drives and calls **`session.cleanup()`**, which runs *
|
|||||||
|
|
||||||
Virtual listings include **`/home`** (session-specific), **`/mnt`** when HDMS mounts exist, and injected root entries **`proc`**, **`sys`**, **`tmp`** when absent from the system image.
|
Virtual listings include **`/home`** (session-specific), **`/mnt`** when HDMS mounts exist, and injected root entries **`proc`**, **`sys`**, **`tmp`** when absent from the system image.
|
||||||
|
|
||||||
### Pseudo **`/proc`** and **`/sys`** (read-only)
|
### Pseudo **`/proc`**, **`/sys`**, **`/run`**, **`/dev`** (mostly read-only)
|
||||||
|
|
||||||
These paths are **synthetic** (not stored on either Hyperdrive). They exist for inspection and scripting ergonomics, **not** Linux ABI compatibility.
|
These paths are **synthetic** (not stored on either Hyperdrive). They exist for inspection and scripting ergonomics, **not** Linux ABI compatibility.
|
||||||
|
|
||||||
- **`/proc`**: **`version`**, **`bare_os_version`** (same payload as **`version`**), **`self/`** with **`environ`** (null-separated **`KEY=value`** pairs) and **`cmdline`**. **`environ`** omits keys whose names look secret-bearing (e.g. **`PASSWORD`**, **`TOKEN`**, **`VAULT`**) and only includes a small public set plus **`BARE_OS_*`**.
|
- **`/proc`**: **`version`**, **`bare_os_version`**, **`uptime`**, **`meminfo`** (static, Linux-shaped text), **`self/`** with **`environ`** (null-separated **`KEY=value`** pairs) and **`cmdline`**. **`environ`** omits keys whose names look secret-bearing (e.g. **`PASSWORD`**, **`TOKEN`**, **`VAULT`**) and only includes a small public set plus **`BARE_OS_*`**.
|
||||||
- **`/sys/fs/bare_os/version`**: same text as **`/proc/version`**.
|
- **`/sys/fs/bare_os/version`**: same text as **`/proc/version`**.
|
||||||
|
- **`/run/bare-os/units`**: tab-separated snapshot of **bare-initd** registered units (phase, start time, description).
|
||||||
|
- **`/dev/null`**, **`/dev/zero`**: minimal device semantics — **`null`** discards writes and reads empty; **`zero`** reads a fixed 64 KiB zero buffer. Not infinite **`/dev/zero`** like Linux.
|
||||||
|
|
||||||
**Non-goals:** no real PIDs, **`/proc/meminfo`**, device nodes, or guarantees of path parity with Linux.
|
**Non-goals:** no real PIDs, accurate **`meminfo`**, or guarantees of path parity with Linux.
|
||||||
|
|
||||||
**Implementation note:** pseudo-file content is UTF-8 encoded with **`b4a`**, not **`TextEncoder`**, because some Bare/Pear runtimes omit the Web Encoding globals (`TextEncoder` / `TextDecoder`). The same applies elsewhere in the booter and in-image utilities that must run on Bare.
|
**Implementation note:** pseudo-file content is UTF-8 encoded with **`b4a`**, not **`TextEncoder`**, because some Bare/Pear runtimes omit the Web Encoding globals (`TextEncoder` / `TextDecoder`). The same applies elsewhere in the booter and in-image utilities that must run on Bare.
|
||||||
|
|
||||||
### **`/dev`** and **`/run`** (not implemented)
|
### **`/dev`** and **`/run`** (minimal subset)
|
||||||
|
|
||||||
Special device files (**`/dev/null`**, **`/dev/zero`**, …) would need dedicated read/write semantics across **`cat`**, redirection, and **`cp`**; **`/run`** could mirror **bare-initd** state. Both are **out of scope** until explicitly specified and tested.
|
See **Pseudo `/proc`, `/sys`, `/run`, `/dev`** above. A full device tree and **`/run`** parity with Linux are still **out of scope**.
|
||||||
|
|
||||||
### Kernel / booter follow-ons (backlog)
|
### Kernel / booter follow-ons (backlog)
|
||||||
|
|
||||||
Priorities from existing gap docs: bounded **shell pipeline** limits (bytes/lines, stderr) per [Chapter 9](09-posix-utilities-shell-and-vfs.md); **TLS trust** and cookie storage for **`curl`** / **`wget`** per **`packages/bare-os-booter/CLI_PARITY.md`**; optional extra **`bare_os.*`** seeder RPC beyond **`version`** in **`bare-os-protocol`**; coreutils stubs and flag gaps per **DOCUMENTATION.md** §14a.
|
Priorities from existing gap docs: bounded **shell pipeline** limits (bytes/lines, stderr) per [Chapter 9](09-posix-utilities-shell-and-vfs.md); **TLS trust** and cookie storage for **`curl`** / **`wget`** per **`packages/bare-os-booter/CLI_PARITY.md`**; optional extra **`bare_os.*`** seeder RPC beyond **`version`** in **`bare-os-protocol`**; coreutils stubs and flag gaps per **DOCUMENTATION.md** §14a.
|
||||||
|
|
||||||
|
### Implementation priority (milestones)
|
||||||
|
|
||||||
|
Work is sequenced for **POSIX/script ergonomics first**, then networking and long-running service features:
|
||||||
|
|
||||||
|
1. **VFS and shell** — Synthetic **`/proc`** / **`/run`** / minimal **`/dev`** paths, bounded **pipeline** capture in **`execShellLine`**, kernel **boot trace** and **banner** hooks on the system image.
|
||||||
|
2. **`/bin` and help** — **`man -w`**, optional **sliced** long pages via **`MAN_SLICE`** / **`PAGER=bare-slice`**, incremental **coreutils** flags (e.g. **`grep -w`**).
|
||||||
|
3. **Networking** — **`CLI_PARITY.md`** items (cookies, TLS trust, finer timeouts) once storage and trust policy exist.
|
||||||
|
4. **Init and protocol** — Persistent **`systemctl enable`**, richer **`bare_os.*`** RPC, after the above stabilize.
|
||||||
|
|
||||||
**Directories:** **`vfs.mkdir(path, { recursive })`** and **`vfs.rmdir(path)`** implement POSIX-like tree creation and removal using a **`.bareos_empty`** marker file for empty directories (aligned with **`git-fs-adapter`**). See [Chapter 9](09-posix-utilities-shell-and-vfs.md).
|
**Directories:** **`vfs.mkdir(path, { recursive })`** and **`vfs.rmdir(path)`** implement POSIX-like tree creation and removal using a **`.bareos_empty`** marker file for empty directories (aligned with **`git-fs-adapter`**). See [Chapter 9](09-posix-utilities-shell-and-vfs.md).
|
||||||
|
|
||||||
**`ctx.runBinCommand(argv)`** — same resolution as external commands in the shell; exposed for utilities such as **`/bin/time`**.
|
**`ctx.runBinCommand(argv)`** — same resolution as external commands in the shell; exposed for utilities such as **`/bin/time`**.
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ Hyperdrive does not always behave like a POSIX directory tree. Empty directories
|
|||||||
- **`vfs.mkdir(path, { recursive })`** — creates directories by writing **`dirname/.bareos_empty`**. **`-p` / `--parents`** is implemented by **`/bin/mkdir`**.
|
- **`vfs.mkdir(path, { recursive })`** — creates directories by writing **`dirname/.bareos_empty`**. **`-p` / `--parents`** is implemented by **`/bin/mkdir`**.
|
||||||
- **`vfs.rmdir(path)`** — removes a directory only if it has **no entries other than** `.bareos_empty` (and removes the marker).
|
- **`vfs.rmdir(path)`** — removes a directory only if it has **no entries other than** `.bareos_empty` (and removes the marker).
|
||||||
|
|
||||||
### 2.3 Pseudo **`/proc`**, **`/sys`**, and session **`/tmp`**
|
### 2.3 Pseudo **`/proc`**, **`/sys`**, **`/run`**, **`/dev`**, and session **`/tmp`**
|
||||||
|
|
||||||
- **`/proc`** and **`/sys/...`** — read-only synthetic files and directories (see [Chapter 4](04-the-booter-runtime.md)). **`writeFile`**, **`unlink`**, **`chmod`**, and **`symlink`** on these paths fail.
|
- **`/proc`**, **`/sys`**, **`/run`**, **`/dev`** — read-only synthetic trees except **`/dev/null`** and **`/dev/zero`** accept writes that are discarded (see [Chapter 4](04-the-booter-runtime.md)). Other pseudo **`writeFile`** / **`unlink`** / **`chmod`** paths fail as documented there.
|
||||||
- **`/tmp`** — writable on the **personal** drive under **`/.bare-os/tmp/<HOME-basename>/…`**, isolated like **`$HOME`** and **`/var/log`**.
|
- **`/tmp`** — writable on the **personal** drive under **`/.bare-os/tmp/<HOME-basename>/…`**, isolated like **`$HOME`** and **`/var/log`**.
|
||||||
|
|
||||||
### 2.4 `chmod` (octal and symbolic)
|
### 2.4 `chmod` (octal and symbolic)
|
||||||
@@ -62,6 +62,8 @@ There is **no** **`chown`** / **`chgrp`** that changes stored ownership in a mul
|
|||||||
|
|
||||||
**Unsupported:** lone **`&`** (background) is rejected with an error. There is no job control.
|
**Unsupported:** lone **`&`** (background) is rejected with an error. There is no job control.
|
||||||
|
|
||||||
|
**Pipeline limits:** simulated pipe capture is bounded. Defaults: **`BARE_OS_PIPELINE_MAX_STAGES`** (32), **`BARE_OS_PIPELINE_MAX_BYTES`** (2 MiB), **`BARE_OS_PIPELINE_MAX_LINES`** (50 000). Exceeding a limit fails the pipeline with exit status **1** and an error on stderr.
|
||||||
|
|
||||||
**Last exit status:** after each full **`execLine`** evaluation, **`vfs.env.BARE_OS_EXIT_STATUS`** is updated (decimal string). Words expand **`$?`** and **`${?}`** from that value (default **`0`** if unset), similar to POSIX **`$?`**.
|
**Last exit status:** after each full **`execLine`** evaluation, **`vfs.env.BARE_OS_EXIT_STATUS`** is updated (decimal string). Words expand **`$?`** and **`${?}`** from that value (default **`0`** if unset), similar to POSIX **`$?`**.
|
||||||
|
|
||||||
Beyond **`alias`**, **`unalias`**, **`cd`**, **`export`**, **`login`**, **`logout`**, **`exit`**:
|
Beyond **`alias`**, **`unalias`**, **`cd`**, **`export`**, **`login`**, **`logout`**, **`exit`**:
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ The merged **`man.json`** adds **`schemaVersion`**, **`generatedAt`**, **`pages`
|
|||||||
|
|
||||||
## Future work
|
## Future work
|
||||||
|
|
||||||
- **`PAGER`** / scrollable view on TTY.
|
- Interactive **`PAGER`** (keypress paging on TTY) beyond **`PAGER=bare-slice`** section breaks.
|
||||||
- **`man -w`** printing the logical path **`/share/man/man.json`** (or per-page anchors).
|
- **`man -w`** is implemented (prints **`/share/man/man.json`**); per-page anchor paths remain future work.
|
||||||
- HTML export for Pear / browser shells.
|
- HTML export for Pear / browser shells.
|
||||||
- Section **7** overview pages and i18n.
|
- Section **7** overview pages and i18n.
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Files in this directory are **read from disk by the seeder** (or copied into `pa
|
|||||||
- **`share/man/man.json`** — Merged manual database for **`/bin/man`** (built by **`bare-os-coreutils`**; see [handbook ch.10](../handbook/10-manpages-and-online-help.md)).
|
- **`share/man/man.json`** — Merged manual database for **`/bin/man`** (built by **`bare-os-coreutils`**; see [handbook ch.10](../handbook/10-manpages-and-online-help.md)).
|
||||||
- **`etc/os-release`** — Static OS metadata (`NAME`, `VERSION`, …).
|
- **`etc/os-release`** — Static OS metadata (`NAME`, `VERSION`, …).
|
||||||
- **`etc/motd`** — Optional message printed after **`os-release`** (distributors can customize).
|
- **`etc/motd`** — Optional message printed after **`os-release`** (distributors can customize).
|
||||||
|
- **`etc/bare-os/banner`** or **`/etc/issue`** — If present on the system drive, the default kernel prints one of these instead of the built-in session hint (unless **`BARE_OS_SKIP_REPL`** shortens the banner). Set **`BARE_OS_BOOT_TRACE=1`** in the session environment to log boot phase timings on stderr.
|
||||||
- **`etc/bare-os/rc`** — Optional boot snippet: one **`execLine`** per non-comment line (trusted).
|
- **`etc/bare-os/rc`** — Optional boot snippet: one **`execLine`** per non-comment line (trusted).
|
||||||
- **`etc/bare-os/rc.d/`** — Optional extra snippets (basename must start with a digit), same line rules, run after **`rc`** in filename order. Human-oriented notes live in **`.README`** (a dotfile so legacy **`init.js`** never executes it).
|
- **`etc/bare-os/rc.d/`** — Optional extra snippets (basename must start with a digit), same line rules, run after **`rc`** in filename order. Human-oriented notes live in **`.README`** (a dotfile so legacy **`init.js`** never executes it).
|
||||||
|
|
||||||
|
|||||||
+18
-7
@@ -78,13 +78,14 @@ async function run(ctx, argv) {
|
|||||||
let suppressErrors = false
|
let suppressErrors = false
|
||||||
let forceFilename = false
|
let forceFilename = false
|
||||||
let noFilename = false
|
let noFilename = false
|
||||||
|
let word = false
|
||||||
|
|
||||||
const args = argv.slice(1)
|
const args = argv.slice(1)
|
||||||
let i = 0
|
let i = 0
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
ctx.console.error(
|
ctx.console.error(
|
||||||
'usage: grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
|
'usage: grep [-E|-F] [-i] [-v] [-w] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
|
||||||
)
|
)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
}
|
}
|
||||||
@@ -169,6 +170,9 @@ async function run(ctx, argv) {
|
|||||||
case 'h':
|
case 'h':
|
||||||
noFilename = true
|
noFilename = true
|
||||||
break
|
break
|
||||||
|
case 'w':
|
||||||
|
word = true
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
ctx.console.error('grep: invalid option -- ' + c)
|
ctx.console.error('grep: invalid option -- ' + c)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -217,7 +221,7 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
let matchers
|
let matchers
|
||||||
try {
|
try {
|
||||||
matchers = buildMatchers(patterns, { fixed, icase })
|
matchers = buildMatchers(patterns, { fixed, icase, word })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.console.error('grep: ' + (e.message || e))
|
ctx.console.error('grep: ' + (e.message || e))
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -304,7 +308,7 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string[]} patterns
|
* @param {string[]} patterns
|
||||||
* @param {{ fixed: boolean, icase: boolean }} o
|
* @param {{ fixed: boolean, icase: boolean, word?: boolean }} o
|
||||||
*/
|
*/
|
||||||
function buildMatchers(patterns, o) {
|
function buildMatchers(patterns, o) {
|
||||||
if (patterns.length === 0) throw new Error('no pattern')
|
if (patterns.length === 0) throw new Error('no pattern')
|
||||||
@@ -312,15 +316,22 @@ function buildMatchers(patterns, o) {
|
|||||||
const pats = o.icase
|
const pats = o.icase
|
||||||
? patterns.map((p) => p.toLowerCase())
|
? patterns.map((p) => p.toLowerCase())
|
||||||
: patterns.slice()
|
: patterns.slice()
|
||||||
return pats.map(
|
return pats.map((p) => {
|
||||||
(p) => (line) =>
|
if (o.word) {
|
||||||
|
const esc = p.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
|
const flags = o.icase ? 'i' : ''
|
||||||
|
const re = new RegExp('(?:^|[^0-9A-Za-z_])' + esc + '(?:$|[^0-9A-Za-z_])', flags)
|
||||||
|
return (line) => re.test(line)
|
||||||
|
}
|
||||||
|
return (line) =>
|
||||||
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
|
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
const flags = o.icase ? 'i' : ''
|
const flags = o.icase ? 'i' : ''
|
||||||
return patterns.map((p) => {
|
return patterns.map((p) => {
|
||||||
try {
|
try {
|
||||||
const re = new RegExp(p, flags)
|
const body = o.word ? '\\b(?:' + p + ')\\b' : p
|
||||||
|
const re = new RegExp(body, flags)
|
||||||
return (line) => re.test(line)
|
return (line) => re.test(line)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('invalid regex: ' + (e.message || e))
|
throw new Error('invalid regex: ' + (e.message || e))
|
||||||
|
|||||||
+38
-3
@@ -274,9 +274,10 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
ctx.console.error(
|
ctx.console.error(
|
||||||
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' +
|
'usage: man [-k keyword] [-f name] [-l] [-w] [[section] name]\n' +
|
||||||
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
|
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
|
||||||
' Data: /share/man/man.json on the system drive.'
|
' Data: /share/man/man.json on the system drive.\n' +
|
||||||
|
' Long pages: set PAGER=bare-slice and optional MAN_SLICE=N (default 24) for section breaks.'
|
||||||
)
|
)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
}
|
}
|
||||||
@@ -318,6 +319,10 @@ async function run(ctx, argv) {
|
|||||||
mode = 'list'
|
mode = 'list'
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (a === '-w' || a === '--where' || a === '--path') {
|
||||||
|
ctx.console.log('/share/man/man.json')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (a.startsWith('-')) {
|
if (a.startsWith('-')) {
|
||||||
ctx.console.error('man: unknown option: ' + a)
|
ctx.console.error('man: unknown option: ' + a)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -474,5 +479,35 @@ async function run(ctx, argv) {
|
|||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.log(bareManRenderPage(page, ctx, width).replace(/\n$/, ''))
|
const rendered = bareManRenderPage(page, ctx, width).replace(/\n$/, '')
|
||||||
|
if (bareManSlicePage(rendered, env, ctx.console)) return
|
||||||
|
ctx.console.log(rendered)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @param {{ log: (s: string) => void }} cons
|
||||||
|
*/
|
||||||
|
function bareManSlicePage(text, env, cons) {
|
||||||
|
const pager = env.PAGER || ''
|
||||||
|
if (pager !== 'bare-slice' && pager !== 'bare_slice') return false
|
||||||
|
const raw = env.MAN_SLICE || '24'
|
||||||
|
const n = Number.parseInt(String(raw), 10)
|
||||||
|
const sliceLines = Number.isFinite(n) && n > 0 ? n : 24
|
||||||
|
const lines = text.split('\n')
|
||||||
|
const total = lines.length
|
||||||
|
for (let i = 0; i < total; i += sliceLines) {
|
||||||
|
const chunk = lines.slice(i, i + sliceLines).join('\n')
|
||||||
|
cons.log(chunk)
|
||||||
|
if (i + sliceLines < total) {
|
||||||
|
const hi = Math.min(i + sliceLines, total)
|
||||||
|
cons.log('')
|
||||||
|
cons.log(
|
||||||
|
`--- man: lines ${i + 1}-${hi} of ${total} (PAGER=bare-slice) ---`
|
||||||
|
)
|
||||||
|
cons.log('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-9
@@ -3,10 +3,31 @@
|
|||||||
* Loaded by the booter with an injected ctx object (trusted replication source).
|
* Loaded by the booter with an injected ctx object (trusted replication source).
|
||||||
*
|
*
|
||||||
* Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/*
|
* Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/*
|
||||||
* (sorted by filename) → session banner → interactive loop.
|
* (sorted; digit-prefixed snippet names) → session banner → interactive loop.
|
||||||
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
|
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
*/
|
||||||
|
function wantBootTrace(ctx) {
|
||||||
|
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
|
||||||
|
return v === '1' || v === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @param {string} label
|
||||||
|
* @param {() => void | Promise<void>} fn
|
||||||
|
*/
|
||||||
|
async function bootTimed(ctx, label, fn) {
|
||||||
|
const t0 = Date.now()
|
||||||
|
await fn()
|
||||||
|
if (wantBootTrace(ctx)) {
|
||||||
|
ctx.console.error(`[boot] ${label}: ${Date.now() - t0}ms`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Record<string, unknown>} ctx
|
* @param {Record<string, unknown>} ctx
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
@@ -112,19 +133,37 @@ async function runBareOsRcDir(ctx) {
|
|||||||
/**
|
/**
|
||||||
* @param {Record<string, unknown>} ctx
|
* @param {Record<string, unknown>} ctx
|
||||||
*/
|
*/
|
||||||
function printSessionBanner(ctx) {
|
async function printSessionBanner(ctx) {
|
||||||
ctx.console.log(
|
const { drive, b4a, console } = ctx
|
||||||
|
for (const p of ['/etc/bare-os/banner', '/etc/issue']) {
|
||||||
|
try {
|
||||||
|
const buf = await drive.get(p)
|
||||||
|
if (buf) {
|
||||||
|
console.log(b4a.toString(buf).trimEnd())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const defaultBanner =
|
||||||
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
||||||
)
|
if (ctx.bareOsSkipRepl) {
|
||||||
|
console.log(
|
||||||
|
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(defaultBanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function start(ctx) {
|
async function start(ctx) {
|
||||||
const { readLine, execLine, console } = ctx
|
const { readLine, execLine, console } = ctx
|
||||||
await printOsRelease(ctx)
|
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx))
|
||||||
await printMotd(ctx)
|
await bootTimed(ctx, 'motd', () => printMotd(ctx))
|
||||||
await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc')
|
await bootTimed(ctx, 'rc', () => runRcFileAt(ctx, '/etc/bare-os/rc', 'rc'))
|
||||||
await runBareOsRcDir(ctx)
|
await bootTimed(ctx, 'rc.d', () => runBareOsRcDir(ctx))
|
||||||
printSessionBanner(ctx)
|
await printSessionBanner(ctx)
|
||||||
while (true) {
|
while (true) {
|
||||||
const line = await readLine('')
|
const line = await readLine('')
|
||||||
if (line == null) break
|
if (line == null) break
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -34,7 +34,9 @@ import {
|
|||||||
import { HdmsController, runHdmsCli } from './lib/hdms-manager.js'
|
import { HdmsController, runHdmsCli } from './lib/hdms-manager.js'
|
||||||
import {
|
import {
|
||||||
startBareInitd,
|
startBareInitd,
|
||||||
registerKernelShutdownHook
|
registerKernelShutdownHook,
|
||||||
|
listBareServices,
|
||||||
|
getBareServiceRuntime
|
||||||
} from './lib/bare-initd.js'
|
} from './lib/bare-initd.js'
|
||||||
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
|
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
|
||||||
import './lib/bare-cron.js'
|
import './lib/bare-cron.js'
|
||||||
@@ -228,10 +230,26 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
}
|
}
|
||||||
/** @type {{ getMounts: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> }} */
|
/** @type {{ getMounts: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> }} */
|
||||||
const vfsMountRef = { getMounts: () => new Map() }
|
const vfsMountRef = { getMounts: () => new Map() }
|
||||||
|
const bootStartedMs = Date.now()
|
||||||
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, {
|
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, {
|
||||||
procSnapshot: {
|
procSnapshot: {
|
||||||
version: BARE_OS_CTX_API_VERSION,
|
version: BARE_OS_CTX_API_VERSION,
|
||||||
cmdline: 'bare-os-booter'
|
cmdline: 'bare-os-booter'
|
||||||
|
},
|
||||||
|
bootStartedMs,
|
||||||
|
initdRunText() {
|
||||||
|
const lines = [
|
||||||
|
'# bare-initd units (name<TAB>phase<TAB>startedAtMs<TAB>description)',
|
||||||
|
''
|
||||||
|
]
|
||||||
|
for (const s of listBareServices()) {
|
||||||
|
const rt = getBareServiceRuntime(s.name)
|
||||||
|
const phase = rt?.phase ?? 'inactive'
|
||||||
|
const started = rt?.startedAtMs ?? 0
|
||||||
|
const desc = (s.description || '').replace(/\t/g, ' ').replace(/\n/g, ' ')
|
||||||
|
lines.push(`${s.name}\t${phase}\t${started}\t${desc}`)
|
||||||
|
}
|
||||||
|
return lines.join('\n') + '\n'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -244,6 +262,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
const ctx = {
|
const ctx = {
|
||||||
/** Documented `ctx` contract version; bump in lib/bare-os-ctx-api.js when the surface changes. */
|
/** Documented `ctx` contract version; bump in lib/bare-os-ctx-api.js when the surface changes. */
|
||||||
bareOsCtxApiVersion: BARE_OS_CTX_API_VERSION,
|
bareOsCtxApiVersion: BARE_OS_CTX_API_VERSION,
|
||||||
|
/** Milliseconds since Unix epoch when this session started VFS construction (for `/proc/uptime`). */
|
||||||
|
bareOsBootStartedMs: bootStartedMs,
|
||||||
|
/** True when `BARE_OS_SKIP_REPL=1` — stdin is non-interactive; `readLine` yields EOF immediately after boot. */
|
||||||
|
bareOsSkipRepl: skipInteractive,
|
||||||
disk,
|
disk,
|
||||||
drive: disk.drive,
|
drive: disk.drive,
|
||||||
personalDrive: disk.personalDrive,
|
personalDrive: disk.personalDrive,
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
* Semantic version of the booter `ctx` contract for custom kernels.
|
* Semantic version of the booter `ctx` contract for custom kernels.
|
||||||
* Bump when adding/removing/renaming documented `ctx` fields or changing behavior.
|
* Bump when adding/removing/renaming documented `ctx` fields or changing behavior.
|
||||||
*/
|
*/
|
||||||
export const BARE_OS_CTX_API_VERSION = '1.0.0'
|
export const BARE_OS_CTX_API_VERSION = '1.1.0'
|
||||||
|
|||||||
@@ -47,6 +47,38 @@ export function syncBareOsExitStatusEnv(ctx) {
|
|||||||
/** Max alias indirections (prevents cycles). */
|
/** Max alias indirections (prevents cycles). */
|
||||||
const MAX_ALIAS_DEPTH = 16
|
const MAX_ALIAS_DEPTH = 16
|
||||||
|
|
||||||
|
/** Default caps for simulated pipeline capture (`console.log` between stages). */
|
||||||
|
export const DEFAULT_PIPELINE_MAX_STAGES = 32
|
||||||
|
export const DEFAULT_PIPELINE_MAX_CAPTURE_BYTES = 2 * 1024 * 1024
|
||||||
|
export const DEFAULT_PIPELINE_MAX_CAPTURE_LINES = 50000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, string> | null | undefined} env
|
||||||
|
*/
|
||||||
|
function pipelineLimitsFromEnv(env) {
|
||||||
|
const o = env && typeof env === 'object' ? env : {}
|
||||||
|
const parse = (key, def) => {
|
||||||
|
const v = o[key]
|
||||||
|
if (v == null || v === '') return def
|
||||||
|
const n = Number.parseInt(String(v), 10)
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : def
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
maxStages: parse(
|
||||||
|
'BARE_OS_PIPELINE_MAX_STAGES',
|
||||||
|
DEFAULT_PIPELINE_MAX_STAGES
|
||||||
|
),
|
||||||
|
maxBytes: parse(
|
||||||
|
'BARE_OS_PIPELINE_MAX_BYTES',
|
||||||
|
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES
|
||||||
|
),
|
||||||
|
maxLines: parse(
|
||||||
|
'BARE_OS_PIPELINE_MAX_LINES',
|
||||||
|
DEFAULT_PIPELINE_MAX_CAPTURE_LINES
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table.
|
* Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table.
|
||||||
* @returns {Record<string, string>}
|
* @returns {Record<string, string>}
|
||||||
@@ -546,9 +578,18 @@ function segmentHasCommand(seg) {
|
|||||||
async function execParsedPipeline(ctx, pipeline) {
|
async function execParsedPipeline(ctx, pipeline) {
|
||||||
const vfs = ctx.vfs
|
const vfs = ctx.vfs
|
||||||
const env = vfs.env
|
const env = vfs.env
|
||||||
|
const lim = pipelineLimitsFromEnv(env)
|
||||||
|
if (pipeline.length > lim.maxStages) {
|
||||||
|
ctx.console.error(
|
||||||
|
`shell: pipeline exceeds BARE_OS_PIPELINE_MAX_STAGES (${lim.maxStages})`
|
||||||
|
)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
let stdinText = typeof ctx.shellStdin === 'string' ? ctx.shellStdin : null
|
let stdinText = typeof ctx.shellStdin === 'string' ? ctx.shellStdin : null
|
||||||
|
|
||||||
|
try {
|
||||||
for (let pi = 0; pi < pipeline.length; pi++) {
|
for (let pi = 0; pi < pipeline.length; pi++) {
|
||||||
const cmd = pipeline[pi]
|
const cmd = pipeline[pi]
|
||||||
const isLast = pi === pipeline.length - 1
|
const isLast = pi === pipeline.length - 1
|
||||||
@@ -599,6 +640,18 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
if (!isLast || cmd.redirOut) {
|
if (!isLast || cmd.redirOut) {
|
||||||
ctx.console.log = (...args) => {
|
ctx.console.log = (...args) => {
|
||||||
outChunks.push(args.map(String).join(' ') + '\n')
|
outChunks.push(args.map(String).join(' ') + '\n')
|
||||||
|
const joined = outChunks.join('')
|
||||||
|
if (joined.length > lim.maxBytes) {
|
||||||
|
throw new Error(
|
||||||
|
`shell: pipeline output exceeds BARE_OS_PIPELINE_MAX_BYTES (${lim.maxBytes})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const lineCount = joined.split('\n').length - 1
|
||||||
|
if (lineCount > lim.maxLines) {
|
||||||
|
throw new Error(
|
||||||
|
`shell: pipeline output exceeds BARE_OS_PIPELINE_MAX_LINES (${lim.maxLines})`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx.console.error = ctx.console.log
|
ctx.console.error = ctx.console.log
|
||||||
}
|
}
|
||||||
@@ -828,6 +881,11 @@ async function execParsedPipeline(ctx, pipeline) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
} catch (e) {
|
||||||
|
ctx.console.error((e && e.message) || String(e))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return 'ok'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -330,7 +330,10 @@ export function isVirtualMountPoint(abs) {
|
|||||||
n === '/proc/self' ||
|
n === '/proc/self' ||
|
||||||
n === '/sys' ||
|
n === '/sys' ||
|
||||||
n === '/sys/fs' ||
|
n === '/sys/fs' ||
|
||||||
n === '/sys/fs/bare_os'
|
n === '/sys/fs/bare_os' ||
|
||||||
|
n === '/run' ||
|
||||||
|
n === '/run/bare-os' ||
|
||||||
|
n === '/dev'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,16 @@ const DIR_MARKER = '.bareos_empty'
|
|||||||
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME,
|
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME,
|
||||||
* optional HDMS mounts under /mnt/<label>/…, virtual /var with writable /var/log/…
|
* optional HDMS mounts under /mnt/<label>/…, virtual /var with writable /var/log/…
|
||||||
* on the personal drive under /.bare-os/var/log/<home-seg>/… (session-isolated).
|
* on the personal drive under /.bare-os/var/log/<home-seg>/… (session-isolated).
|
||||||
* Read-only pseudo `proc` and `sys` under `/`; session `tmp` maps to `/.bare-os/tmp/<seg>/` on the personal drive.
|
* Read-only pseudo `proc`, `sys`, `run`, `dev` under `/`; session `tmp` maps to `/.bare-os/tmp/<seg>/` on the personal drive.
|
||||||
* @param {import('hyperdrive').default} systemDrive
|
* @param {import('hyperdrive').default} systemDrive
|
||||||
* @param {import('hyperdrive').default} personalDrive
|
* @param {import('hyperdrive').default} personalDrive
|
||||||
* @param {Record<string, string>} env
|
* @param {Record<string, string>} env
|
||||||
* @param {{ getMounts?: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> } | null} [mntRef]
|
* @param {{ getMounts?: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> } | null} [mntRef]
|
||||||
* @param {{ procSnapshot?: { version?: string, cmdline?: string } }} [vfsOptions]
|
* @param {{
|
||||||
|
* procSnapshot?: { version?: string, cmdline?: string },
|
||||||
|
* bootStartedMs?: number,
|
||||||
|
* initdRunText?: () => string
|
||||||
|
* }} [vfsOptions]
|
||||||
*/
|
*/
|
||||||
export function createVfs(
|
export function createVfs(
|
||||||
systemDrive,
|
systemDrive,
|
||||||
@@ -38,6 +42,14 @@ export function createVfs(
|
|||||||
vfsOptions = {}
|
vfsOptions = {}
|
||||||
) {
|
) {
|
||||||
const procSnapshot = vfsOptions.procSnapshot || null
|
const procSnapshot = vfsOptions.procSnapshot || null
|
||||||
|
const bootStartedMs =
|
||||||
|
typeof vfsOptions.bootStartedMs === 'number'
|
||||||
|
? vfsOptions.bootStartedMs
|
||||||
|
: null
|
||||||
|
const initdRunText =
|
||||||
|
typeof vfsOptions.initdRunText === 'function'
|
||||||
|
? vfsOptions.initdRunText
|
||||||
|
: null
|
||||||
const HOME = () => env.HOME || '/home/guest'
|
const HOME = () => env.HOME || '/home/guest'
|
||||||
let cwd = env.PWD || HOME()
|
let cwd = env.PWD || HOME()
|
||||||
|
|
||||||
@@ -132,11 +144,43 @@ export function createVfs(
|
|||||||
return utf8Encode(parts.join(''))
|
return utf8Encode(parts.join(''))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pseudoUptimeText() {
|
||||||
|
const start = bootStartedMs != null ? bootStartedMs : Date.now()
|
||||||
|
const up = Math.max(0, (Date.now() - start) / 1000)
|
||||||
|
return `${up.toFixed(2)} ${up.toFixed(2)}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
function pseudoMeminfoText() {
|
||||||
|
return [
|
||||||
|
'MemTotal: 524288 kB',
|
||||||
|
'MemFree: 262144 kB',
|
||||||
|
'MemAvailable: 262144 kB',
|
||||||
|
'SwapTotal: 0 kB',
|
||||||
|
'SwapFree: 0 kB',
|
||||||
|
''
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
function pseudoFileBytes(routePseudo) {
|
function pseudoFileBytes(routePseudo) {
|
||||||
const f = routePseudo.file
|
const f = routePseudo.file
|
||||||
if (f === 'version') return utf8Encode(pseudoVersionText())
|
const k = routePseudo.kind
|
||||||
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText())
|
if (f === 'version' && (k === 'proc' || k === 'sys')) {
|
||||||
if (f === 'environ') return pseudoEnvironBytes()
|
return utf8Encode(pseudoVersionText())
|
||||||
|
}
|
||||||
|
if (k === 'proc') {
|
||||||
|
if (f === 'cmdline') return utf8Encode(pseudoCmdlineText())
|
||||||
|
if (f === 'environ') return pseudoEnvironBytes()
|
||||||
|
if (f === 'uptime') return utf8Encode(pseudoUptimeText())
|
||||||
|
if (f === 'meminfo') return utf8Encode(pseudoMeminfoText())
|
||||||
|
}
|
||||||
|
if (k === 'run' && f === 'units') {
|
||||||
|
const t = initdRunText
|
||||||
|
? initdRunText()
|
||||||
|
: '# bare-initd: no snapshot provider\n'
|
||||||
|
return utf8Encode(t)
|
||||||
|
}
|
||||||
|
if (k === 'dev' && f === 'null') return utf8Encode('')
|
||||||
|
if (k === 'dev' && f === 'zero') return new Uint8Array(65536)
|
||||||
return utf8Encode('')
|
return utf8Encode('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,8 +207,38 @@ export function createVfs(
|
|||||||
if (sub === 'self/cmdline') {
|
if (sub === 'self/cmdline') {
|
||||||
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'cmdline' }
|
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'cmdline' }
|
||||||
}
|
}
|
||||||
|
if (sub === 'uptime') {
|
||||||
|
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'uptime' }
|
||||||
|
}
|
||||||
|
if (sub === 'meminfo') {
|
||||||
|
return { virtualPseudo: true, kind: 'proc', node: 'file', file: 'meminfo' }
|
||||||
|
}
|
||||||
return { virtualPseudo: true, kind: 'proc', node: 'enoent' }
|
return { virtualPseudo: true, kind: 'proc', node: 'enoent' }
|
||||||
}
|
}
|
||||||
|
if (n === '/run' || n.startsWith('/run/')) {
|
||||||
|
if (n === '/run') {
|
||||||
|
return { virtualPseudo: true, kind: 'run', node: 'root' }
|
||||||
|
}
|
||||||
|
if (n === '/run/bare-os') {
|
||||||
|
return { virtualPseudo: true, kind: 'run', node: 'dir', dir: 'bare_os' }
|
||||||
|
}
|
||||||
|
if (n === '/run/bare-os/units') {
|
||||||
|
return { virtualPseudo: true, kind: 'run', node: 'file', file: 'units' }
|
||||||
|
}
|
||||||
|
return { virtualPseudo: true, kind: 'run', node: 'enoent' }
|
||||||
|
}
|
||||||
|
if (n === '/dev' || n.startsWith('/dev/')) {
|
||||||
|
if (n === '/dev') {
|
||||||
|
return { virtualPseudo: true, kind: 'dev', node: 'root' }
|
||||||
|
}
|
||||||
|
if (n === '/dev/null') {
|
||||||
|
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'null' }
|
||||||
|
}
|
||||||
|
if (n === '/dev/zero') {
|
||||||
|
return { virtualPseudo: true, kind: 'dev', node: 'file', file: 'zero' }
|
||||||
|
}
|
||||||
|
return { virtualPseudo: true, kind: 'dev', node: 'enoent' }
|
||||||
|
}
|
||||||
if (n === '/sys' || n.startsWith('/sys/')) {
|
if (n === '/sys' || n.startsWith('/sys/')) {
|
||||||
if (n === '/sys') {
|
if (n === '/sys') {
|
||||||
return { virtualPseudo: true, kind: 'sys', node: 'root' }
|
return { virtualPseudo: true, kind: 'sys', node: 'root' }
|
||||||
@@ -653,7 +727,7 @@ export function createVfs(
|
|||||||
throw new Error('Not a directory: ' + abs)
|
throw new Error('Not a directory: ' + abs)
|
||||||
}
|
}
|
||||||
if (pr.kind === 'proc' && pr.node === 'root') {
|
if (pr.kind === 'proc' && pr.node === 'root') {
|
||||||
return ['bare_os_version', 'self', 'version']
|
return ['bare_os_version', 'meminfo', 'self', 'uptime', 'version']
|
||||||
}
|
}
|
||||||
if (pr.kind === 'proc' && pr.node === 'dir' && pr.dir === 'self') {
|
if (pr.kind === 'proc' && pr.node === 'dir' && pr.dir === 'self') {
|
||||||
return ['cmdline', 'environ']
|
return ['cmdline', 'environ']
|
||||||
@@ -667,6 +741,15 @@ export function createVfs(
|
|||||||
if (pr.kind === 'sys' && pr.node === 'dir' && pr.dir === 'bare_os') {
|
if (pr.kind === 'sys' && pr.node === 'dir' && pr.dir === 'bare_os') {
|
||||||
return ['version']
|
return ['version']
|
||||||
}
|
}
|
||||||
|
if (pr.kind === 'run' && pr.node === 'root') {
|
||||||
|
return ['bare-os']
|
||||||
|
}
|
||||||
|
if (pr.kind === 'run' && pr.node === 'dir' && pr.dir === 'bare_os') {
|
||||||
|
return ['units']
|
||||||
|
}
|
||||||
|
if (pr.kind === 'dev' && pr.node === 'root') {
|
||||||
|
return ['null', 'zero']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const r = route(abs)
|
const r = route(abs)
|
||||||
if (r.virtualMntRoot) {
|
if (r.virtualMntRoot) {
|
||||||
@@ -697,6 +780,12 @@ export function createVfs(
|
|||||||
if (abs === '/' && !names.includes('tmp')) {
|
if (abs === '/' && !names.includes('tmp')) {
|
||||||
names.push('tmp')
|
names.push('tmp')
|
||||||
}
|
}
|
||||||
|
if (abs === '/' && !names.includes('run')) {
|
||||||
|
names.push('run')
|
||||||
|
}
|
||||||
|
if (abs === '/' && !names.includes('dev')) {
|
||||||
|
names.push('dev')
|
||||||
|
}
|
||||||
return names.sort()
|
return names.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,6 +862,14 @@ export function createVfs(
|
|||||||
*/
|
*/
|
||||||
async function writeFileAtAbs(abs, buf, opts = {}) {
|
async function writeFileAtAbs(abs, buf, opts = {}) {
|
||||||
const r = route(abs)
|
const r = route(abs)
|
||||||
|
if (
|
||||||
|
r.virtualPseudo &&
|
||||||
|
r.kind === 'dev' &&
|
||||||
|
r.node === 'file' &&
|
||||||
|
(r.file === 'null' || r.file === 'zero')
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
r.virtualHomeDir ||
|
r.virtualHomeDir ||
|
||||||
r.virtualMntRoot ||
|
r.virtualMntRoot ||
|
||||||
|
|||||||
@@ -450,15 +450,21 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
|||||||
BARE_OS_CTX_API_VERSION: '9.9.9-test'
|
BARE_OS_CTX_API_VERSION: '9.9.9-test'
|
||||||
}
|
}
|
||||||
const vfs = createVfs(sys, personal, env, null, {
|
const vfs = createVfs(sys, personal, env, null, {
|
||||||
procSnapshot: { version: '1.2.3-test', cmdline: 'unit-test' }
|
procSnapshot: { version: '1.2.3-test', cmdline: 'unit-test' },
|
||||||
|
bootStartedMs: Date.now() - 4000,
|
||||||
|
initdRunText: () => 'demo-unit\tactive\t1\tdemo\n'
|
||||||
})
|
})
|
||||||
const root = await vfs.readdir('/')
|
const root = await vfs.readdir('/')
|
||||||
t.ok(root.includes('proc'))
|
t.ok(root.includes('proc'))
|
||||||
t.ok(root.includes('sys'))
|
t.ok(root.includes('sys'))
|
||||||
t.ok(root.includes('tmp'))
|
t.ok(root.includes('tmp'))
|
||||||
|
t.ok(root.includes('run'))
|
||||||
|
t.ok(root.includes('dev'))
|
||||||
t.alike(await vfs.readdir('/proc').then((a) => [...a].sort()), [
|
t.alike(await vfs.readdir('/proc').then((a) => [...a].sort()), [
|
||||||
'bare_os_version',
|
'bare_os_version',
|
||||||
|
'meminfo',
|
||||||
'self',
|
'self',
|
||||||
|
'uptime',
|
||||||
'version'
|
'version'
|
||||||
])
|
])
|
||||||
t.alike(await vfs.readdir('/proc/self').then((a) => [...a].sort()), [
|
t.alike(await vfs.readdir('/proc/self').then((a) => [...a].sort()), [
|
||||||
@@ -474,6 +480,20 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
|
|||||||
t.ok(sysVer.includes('1.2.3-test'))
|
t.ok(sysVer.includes('1.2.3-test'))
|
||||||
const cmd = b4a.toString(await vfs.readFile('/proc/self/cmdline'))
|
const cmd = b4a.toString(await vfs.readFile('/proc/self/cmdline'))
|
||||||
t.ok(cmd.includes('unit-test'))
|
t.ok(cmd.includes('unit-test'))
|
||||||
|
const mem = b4a.toString(await vfs.readFile('/proc/meminfo'))
|
||||||
|
t.ok(mem.includes('MemTotal:'))
|
||||||
|
const up = b4a.toString(await vfs.readFile('/proc/uptime'))
|
||||||
|
t.ok(/^\d+\.\d+\s+\d+\.\d+/.test(up.trim()))
|
||||||
|
t.alike(await vfs.readdir('/dev').then((a) => [...a].sort()), [
|
||||||
|
'null',
|
||||||
|
'zero'
|
||||||
|
])
|
||||||
|
await vfs.writeFile('/dev/null', b4a.from('gone'))
|
||||||
|
t.is((await vfs.readFile('/dev/null'))?.byteLength ?? 0, 0)
|
||||||
|
const z = await vfs.readFile('/dev/zero')
|
||||||
|
t.ok(z && z.byteLength === 65536)
|
||||||
|
const units = b4a.toString(await vfs.readFile('/run/bare-os/units'))
|
||||||
|
t.ok(units.includes('demo-unit'))
|
||||||
let writeErr = null
|
let writeErr = null
|
||||||
try {
|
try {
|
||||||
await vfs.writeFile('/proc/version', b4a.from('x'))
|
await vfs.writeFile('/proc/version', b4a.from('x'))
|
||||||
@@ -784,6 +804,51 @@ test('loadBarerc createSkeletonIfMissing writes ~/.barerc when absent', async (t
|
|||||||
rmSync(dir, { recursive: true, force: true })
|
rmSync(dir, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('execShellLine pipeline stage and byte limits', async (t) => {
|
||||||
|
const dir = testCorestoreDir('shpipe')
|
||||||
|
const store = new Corestore(dir)
|
||||||
|
const drive = new Hyperdrive(store)
|
||||||
|
const personal = new Hyperdrive(store.namespace('spipe'))
|
||||||
|
await drive.ready()
|
||||||
|
await personal.ready()
|
||||||
|
const echo = `
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
ctx.console.log(argv.slice(1).join(' '))
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const cat = `
|
||||||
|
async function run(ctx) {
|
||||||
|
ctx.console.log(bareStdin(ctx).replace(/\\n$/, ''))
|
||||||
|
}
|
||||||
|
`
|
||||||
|
await drive.put('/bin/echo', b4a.from(echo))
|
||||||
|
await drive.put('/bin/cat', b4a.from(cat))
|
||||||
|
const spam = `
|
||||||
|
async function run(ctx) {
|
||||||
|
ctx.console.log('z'.repeat(200))
|
||||||
|
}
|
||||||
|
`
|
||||||
|
await drive.put('/bin/spam', b4a.from(spam))
|
||||||
|
const logs = []
|
||||||
|
const ctx = testCtx(drive, personal)
|
||||||
|
ctx.console = {
|
||||||
|
log: (...a) => logs.push(a.join(' ')),
|
||||||
|
error: (...a) => logs.push(a.join(' '))
|
||||||
|
}
|
||||||
|
ctx.env.BARE_OS_PIPELINE_MAX_STAGES = '2'
|
||||||
|
await execShellLine(ctx, 'echo a | echo b | echo c')
|
||||||
|
t.is(ctx.exitCode, 1)
|
||||||
|
t.ok(logs.some((l) => l.includes('BARE_OS_PIPELINE_MAX_STAGES')))
|
||||||
|
logs.length = 0
|
||||||
|
ctx.env.BARE_OS_PIPELINE_MAX_STAGES = '32'
|
||||||
|
ctx.env.BARE_OS_PIPELINE_MAX_BYTES = '80'
|
||||||
|
await execShellLine(ctx, 'spam | cat')
|
||||||
|
t.is(ctx.exitCode, 1)
|
||||||
|
t.ok(logs.some((l) => l.includes('BARE_OS_PIPELINE_MAX_BYTES')))
|
||||||
|
await store.close()
|
||||||
|
rmSync(dir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
test('execShellLine runs cd and external', async (t) => {
|
test('execShellLine runs cd and external', async (t) => {
|
||||||
const dir = testCorestoreDir('sh')
|
const dir = testCorestoreDir('sh')
|
||||||
const store = new Corestore(dir)
|
const store = new Corestore(dir)
|
||||||
@@ -1872,6 +1937,10 @@ test('runBinCommand man ls prints manual text', async (t) => {
|
|||||||
t.ok(text.includes('SYNOPSIS'))
|
t.ok(text.includes('SYNOPSIS'))
|
||||||
t.ok(text.includes('EXAMPLES'))
|
t.ok(text.includes('EXAMPLES'))
|
||||||
t.ok(text.includes('ls'))
|
t.ok(text.includes('ls'))
|
||||||
|
logs.length = 0
|
||||||
|
await runBinCommand(ctx, ['man', '-w'])
|
||||||
|
t.is(ctx.exitCode, 0)
|
||||||
|
t.is(logs.join('\n').trim(), '/share/man/man.json')
|
||||||
await store.close()
|
await store.close()
|
||||||
rmSync(dir, { recursive: true, force: true })
|
rmSync(dir, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"section": 1,
|
"section": 1,
|
||||||
"title": "pattern matching utility",
|
"title": "pattern matching utility",
|
||||||
"synopsis": [
|
"synopsis": [
|
||||||
"grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]"
|
"grep [-E|-F] [-i] [-v] [-w] [-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.",
|
"description": "Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.",
|
||||||
"options": [
|
"options": [
|
||||||
@@ -23,6 +23,10 @@
|
|||||||
"flag": "-v",
|
"flag": "-v",
|
||||||
"meaning": "Invert match"
|
"meaning": "Invert match"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"flag": "-w",
|
||||||
|
"meaning": "Match whole words (regex: \\b…\\b; fixed: non-alphanumeric boundaries)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"flag": "-n",
|
"flag": "-n",
|
||||||
"meaning": "Prefix lines with line number"
|
"meaning": "Prefix lines with line number"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"section": 1,
|
"section": 1,
|
||||||
"title": "display on-line manual pages",
|
"title": "display on-line manual pages",
|
||||||
"synopsis": [
|
"synopsis": [
|
||||||
"man [-k keyword] [-f name] [-l] [[section] name]",
|
"man [-k keyword] [-f name] [-l] [-w] [[section] name]",
|
||||||
"man reads /share/man/man.json on the system drive."
|
"man reads /share/man/man.json on the system drive."
|
||||||
],
|
],
|
||||||
"description": "Displays manual pages from the merged JSON database. Section 1: /bin and git/shell pages. Section 7: handbook (man handbook) and developer guide (man devguide), merged at build from handbook/*.md and developer-guide/*.md.",
|
"description": "Displays manual pages from the merged JSON database. Section 1: /bin and git/shell pages. Section 7: handbook (man handbook) and developer guide (man devguide), merged at build from handbook/*.md and developer-guide/*.md.",
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
{
|
{
|
||||||
"flag": "-l, --list",
|
"flag": "-l, --list",
|
||||||
"meaning": "List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically"
|
"meaning": "List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"flag": "-w, --where, --path",
|
||||||
|
"meaning": "Print logical path to the manual database (/share/man/man.json)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -33,7 +37,8 @@
|
|||||||
],
|
],
|
||||||
"environment": [
|
"environment": [
|
||||||
"MANWIDTH — wrap width (default 72, min 40)",
|
"MANWIDTH — wrap width (default 72, min 40)",
|
||||||
"NO_COLOR — disable bold headings on TTY"
|
"NO_COLOR — disable bold headings on TTY",
|
||||||
|
"PAGER=bare-slice — insert section breaks in long pages (optional MAN_SLICE lines per chunk, default 24)"
|
||||||
],
|
],
|
||||||
"seeAlso": [
|
"seeAlso": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ async function run(ctx, argv) {
|
|||||||
let suppressErrors = false
|
let suppressErrors = false
|
||||||
let forceFilename = false
|
let forceFilename = false
|
||||||
let noFilename = false
|
let noFilename = false
|
||||||
|
let word = false
|
||||||
|
|
||||||
const args = argv.slice(1)
|
const args = argv.slice(1)
|
||||||
let i = 0
|
let i = 0
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
ctx.console.error(
|
ctx.console.error(
|
||||||
'usage: grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
|
'usage: grep [-E|-F] [-i] [-v] [-w] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
|
||||||
)
|
)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
}
|
}
|
||||||
@@ -107,6 +108,9 @@ async function run(ctx, argv) {
|
|||||||
case 'h':
|
case 'h':
|
||||||
noFilename = true
|
noFilename = true
|
||||||
break
|
break
|
||||||
|
case 'w':
|
||||||
|
word = true
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
ctx.console.error('grep: invalid option -- ' + c)
|
ctx.console.error('grep: invalid option -- ' + c)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -155,7 +159,7 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
let matchers
|
let matchers
|
||||||
try {
|
try {
|
||||||
matchers = buildMatchers(patterns, { fixed, icase })
|
matchers = buildMatchers(patterns, { fixed, icase, word })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.console.error('grep: ' + (e.message || e))
|
ctx.console.error('grep: ' + (e.message || e))
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -242,7 +246,7 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string[]} patterns
|
* @param {string[]} patterns
|
||||||
* @param {{ fixed: boolean, icase: boolean }} o
|
* @param {{ fixed: boolean, icase: boolean, word?: boolean }} o
|
||||||
*/
|
*/
|
||||||
function buildMatchers(patterns, o) {
|
function buildMatchers(patterns, o) {
|
||||||
if (patterns.length === 0) throw new Error('no pattern')
|
if (patterns.length === 0) throw new Error('no pattern')
|
||||||
@@ -250,15 +254,22 @@ function buildMatchers(patterns, o) {
|
|||||||
const pats = o.icase
|
const pats = o.icase
|
||||||
? patterns.map((p) => p.toLowerCase())
|
? patterns.map((p) => p.toLowerCase())
|
||||||
: patterns.slice()
|
: patterns.slice()
|
||||||
return pats.map(
|
return pats.map((p) => {
|
||||||
(p) => (line) =>
|
if (o.word) {
|
||||||
|
const esc = p.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
|
const flags = o.icase ? 'i' : ''
|
||||||
|
const re = new RegExp('(?:^|[^0-9A-Za-z_])' + esc + '(?:$|[^0-9A-Za-z_])', flags)
|
||||||
|
return (line) => re.test(line)
|
||||||
|
}
|
||||||
|
return (line) =>
|
||||||
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
|
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
const flags = o.icase ? 'i' : ''
|
const flags = o.icase ? 'i' : ''
|
||||||
return patterns.map((p) => {
|
return patterns.map((p) => {
|
||||||
try {
|
try {
|
||||||
const re = new RegExp(p, flags)
|
const body = o.word ? '\\b(?:' + p + ')\\b' : p
|
||||||
|
const re = new RegExp(body, flags)
|
||||||
return (line) => re.test(line)
|
return (line) => re.test(line)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('invalid regex: ' + (e.message || e))
|
throw new Error('invalid regex: ' + (e.message || e))
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
ctx.console.error(
|
ctx.console.error(
|
||||||
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' +
|
'usage: man [-k keyword] [-f name] [-l] [-w] [[section] name]\n' +
|
||||||
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
|
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
|
||||||
' Data: /share/man/man.json on the system drive.'
|
' Data: /share/man/man.json on the system drive.\n' +
|
||||||
|
' Long pages: set PAGER=bare-slice and optional MAN_SLICE=N (default 24) for section breaks.'
|
||||||
)
|
)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,10 @@ async function run(ctx, argv) {
|
|||||||
mode = 'list'
|
mode = 'list'
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (a === '-w' || a === '--where' || a === '--path') {
|
||||||
|
ctx.console.log('/share/man/man.json')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (a.startsWith('-')) {
|
if (a.startsWith('-')) {
|
||||||
ctx.console.error('man: unknown option: ' + a)
|
ctx.console.error('man: unknown option: ' + a)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -205,5 +210,35 @@ async function run(ctx, argv) {
|
|||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.log(bareManRenderPage(page, ctx, width).replace(/\n$/, ''))
|
const rendered = bareManRenderPage(page, ctx, width).replace(/\n$/, '')
|
||||||
|
if (bareManSlicePage(rendered, env, ctx.console)) return
|
||||||
|
ctx.console.log(rendered)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @param {{ log: (s: string) => void }} cons
|
||||||
|
*/
|
||||||
|
function bareManSlicePage(text, env, cons) {
|
||||||
|
const pager = env.PAGER || ''
|
||||||
|
if (pager !== 'bare-slice' && pager !== 'bare_slice') return false
|
||||||
|
const raw = env.MAN_SLICE || '24'
|
||||||
|
const n = Number.parseInt(String(raw), 10)
|
||||||
|
const sliceLines = Number.isFinite(n) && n > 0 ? n : 24
|
||||||
|
const lines = text.split('\n')
|
||||||
|
const total = lines.length
|
||||||
|
for (let i = 0; i < total; i += sliceLines) {
|
||||||
|
const chunk = lines.slice(i, i + sliceLines).join('\n')
|
||||||
|
cons.log(chunk)
|
||||||
|
if (i + sliceLines < total) {
|
||||||
|
const hi = Math.min(i + sliceLines, total)
|
||||||
|
cons.log('')
|
||||||
|
cons.log(
|
||||||
|
`--- man: lines ${i + 1}-${hi} of ${total} (PAGER=bare-slice) ---`
|
||||||
|
)
|
||||||
|
cons.log('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,13 +78,14 @@ async function run(ctx, argv) {
|
|||||||
let suppressErrors = false
|
let suppressErrors = false
|
||||||
let forceFilename = false
|
let forceFilename = false
|
||||||
let noFilename = false
|
let noFilename = false
|
||||||
|
let word = false
|
||||||
|
|
||||||
const args = argv.slice(1)
|
const args = argv.slice(1)
|
||||||
let i = 0
|
let i = 0
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
ctx.console.error(
|
ctx.console.error(
|
||||||
'usage: grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
|
'usage: grep [-E|-F] [-i] [-v] [-w] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
|
||||||
)
|
)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
}
|
}
|
||||||
@@ -169,6 +170,9 @@ async function run(ctx, argv) {
|
|||||||
case 'h':
|
case 'h':
|
||||||
noFilename = true
|
noFilename = true
|
||||||
break
|
break
|
||||||
|
case 'w':
|
||||||
|
word = true
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
ctx.console.error('grep: invalid option -- ' + c)
|
ctx.console.error('grep: invalid option -- ' + c)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -217,7 +221,7 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
let matchers
|
let matchers
|
||||||
try {
|
try {
|
||||||
matchers = buildMatchers(patterns, { fixed, icase })
|
matchers = buildMatchers(patterns, { fixed, icase, word })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.console.error('grep: ' + (e.message || e))
|
ctx.console.error('grep: ' + (e.message || e))
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -304,7 +308,7 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string[]} patterns
|
* @param {string[]} patterns
|
||||||
* @param {{ fixed: boolean, icase: boolean }} o
|
* @param {{ fixed: boolean, icase: boolean, word?: boolean }} o
|
||||||
*/
|
*/
|
||||||
function buildMatchers(patterns, o) {
|
function buildMatchers(patterns, o) {
|
||||||
if (patterns.length === 0) throw new Error('no pattern')
|
if (patterns.length === 0) throw new Error('no pattern')
|
||||||
@@ -312,15 +316,22 @@ function buildMatchers(patterns, o) {
|
|||||||
const pats = o.icase
|
const pats = o.icase
|
||||||
? patterns.map((p) => p.toLowerCase())
|
? patterns.map((p) => p.toLowerCase())
|
||||||
: patterns.slice()
|
: patterns.slice()
|
||||||
return pats.map(
|
return pats.map((p) => {
|
||||||
(p) => (line) =>
|
if (o.word) {
|
||||||
|
const esc = p.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||||
|
const flags = o.icase ? 'i' : ''
|
||||||
|
const re = new RegExp('(?:^|[^0-9A-Za-z_])' + esc + '(?:$|[^0-9A-Za-z_])', flags)
|
||||||
|
return (line) => re.test(line)
|
||||||
|
}
|
||||||
|
return (line) =>
|
||||||
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
|
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
const flags = o.icase ? 'i' : ''
|
const flags = o.icase ? 'i' : ''
|
||||||
return patterns.map((p) => {
|
return patterns.map((p) => {
|
||||||
try {
|
try {
|
||||||
const re = new RegExp(p, flags)
|
const body = o.word ? '\\b(?:' + p + ')\\b' : p
|
||||||
|
const re = new RegExp(body, flags)
|
||||||
return (line) => re.test(line)
|
return (line) => re.test(line)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('invalid regex: ' + (e.message || e))
|
throw new Error('invalid regex: ' + (e.message || e))
|
||||||
|
|||||||
@@ -274,9 +274,10 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
ctx.console.error(
|
ctx.console.error(
|
||||||
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' +
|
'usage: man [-k keyword] [-f name] [-l] [-w] [[section] name]\n' +
|
||||||
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
|
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
|
||||||
' Data: /share/man/man.json on the system drive.'
|
' Data: /share/man/man.json on the system drive.\n' +
|
||||||
|
' Long pages: set PAGER=bare-slice and optional MAN_SLICE=N (default 24) for section breaks.'
|
||||||
)
|
)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
}
|
}
|
||||||
@@ -318,6 +319,10 @@ async function run(ctx, argv) {
|
|||||||
mode = 'list'
|
mode = 'list'
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (a === '-w' || a === '--where' || a === '--path') {
|
||||||
|
ctx.console.log('/share/man/man.json')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (a.startsWith('-')) {
|
if (a.startsWith('-')) {
|
||||||
ctx.console.error('man: unknown option: ' + a)
|
ctx.console.error('man: unknown option: ' + a)
|
||||||
ctx.exitCode = 2
|
ctx.exitCode = 2
|
||||||
@@ -474,5 +479,35 @@ async function run(ctx, argv) {
|
|||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.log(bareManRenderPage(page, ctx, width).replace(/\n$/, ''))
|
const rendered = bareManRenderPage(page, ctx, width).replace(/\n$/, '')
|
||||||
|
if (bareManSlicePage(rendered, env, ctx.console)) return
|
||||||
|
ctx.console.log(rendered)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {Record<string, string>} env
|
||||||
|
* @param {{ log: (s: string) => void }} cons
|
||||||
|
*/
|
||||||
|
function bareManSlicePage(text, env, cons) {
|
||||||
|
const pager = env.PAGER || ''
|
||||||
|
if (pager !== 'bare-slice' && pager !== 'bare_slice') return false
|
||||||
|
const raw = env.MAN_SLICE || '24'
|
||||||
|
const n = Number.parseInt(String(raw), 10)
|
||||||
|
const sliceLines = Number.isFinite(n) && n > 0 ? n : 24
|
||||||
|
const lines = text.split('\n')
|
||||||
|
const total = lines.length
|
||||||
|
for (let i = 0; i < total; i += sliceLines) {
|
||||||
|
const chunk = lines.slice(i, i + sliceLines).join('\n')
|
||||||
|
cons.log(chunk)
|
||||||
|
if (i + sliceLines < total) {
|
||||||
|
const hi = Math.min(i + sliceLines, total)
|
||||||
|
cons.log('')
|
||||||
|
cons.log(
|
||||||
|
`--- man: lines ${i + 1}-${hi} of ${total} (PAGER=bare-slice) ---`
|
||||||
|
)
|
||||||
|
cons.log('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,31 @@
|
|||||||
* Loaded by the booter with an injected ctx object (trusted replication source).
|
* Loaded by the booter with an injected ctx object (trusted replication source).
|
||||||
*
|
*
|
||||||
* Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/*
|
* Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/*
|
||||||
* (sorted by filename) → session banner → interactive loop.
|
* (sorted; digit-prefixed snippet names) → session banner → interactive loop.
|
||||||
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
|
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
*/
|
||||||
|
function wantBootTrace(ctx) {
|
||||||
|
const v = ctx.env && ctx.env.BARE_OS_BOOT_TRACE
|
||||||
|
return v === '1' || v === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} ctx
|
||||||
|
* @param {string} label
|
||||||
|
* @param {() => void | Promise<void>} fn
|
||||||
|
*/
|
||||||
|
async function bootTimed(ctx, label, fn) {
|
||||||
|
const t0 = Date.now()
|
||||||
|
await fn()
|
||||||
|
if (wantBootTrace(ctx)) {
|
||||||
|
ctx.console.error(`[boot] ${label}: ${Date.now() - t0}ms`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Record<string, unknown>} ctx
|
* @param {Record<string, unknown>} ctx
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
@@ -112,19 +133,37 @@ async function runBareOsRcDir(ctx) {
|
|||||||
/**
|
/**
|
||||||
* @param {Record<string, unknown>} ctx
|
* @param {Record<string, unknown>} ctx
|
||||||
*/
|
*/
|
||||||
function printSessionBanner(ctx) {
|
async function printSessionBanner(ctx) {
|
||||||
ctx.console.log(
|
const { drive, b4a, console } = ctx
|
||||||
|
for (const p of ['/etc/bare-os/banner', '/etc/issue']) {
|
||||||
|
try {
|
||||||
|
const buf = await drive.get(p)
|
||||||
|
if (buf) {
|
||||||
|
console.log(b4a.toString(buf).trimEnd())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const defaultBanner =
|
||||||
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
||||||
)
|
if (ctx.bareOsSkipRepl) {
|
||||||
|
console.log(
|
||||||
|
'Bare operating system — non-interactive session (BARE_OS_SKIP_REPL).'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(defaultBanner)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function start(ctx) {
|
async function start(ctx) {
|
||||||
const { readLine, execLine, console } = ctx
|
const { readLine, execLine, console } = ctx
|
||||||
await printOsRelease(ctx)
|
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx))
|
||||||
await printMotd(ctx)
|
await bootTimed(ctx, 'motd', () => printMotd(ctx))
|
||||||
await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc')
|
await bootTimed(ctx, 'rc', () => runRcFileAt(ctx, '/etc/bare-os/rc', 'rc'))
|
||||||
await runBareOsRcDir(ctx)
|
await bootTimed(ctx, 'rc.d', () => runBareOsRcDir(ctx))
|
||||||
printSessionBanner(ctx)
|
await printSessionBanner(ctx)
|
||||||
while (true) {
|
while (true) {
|
||||||
const line = await readLine('')
|
const line = await readLine('')
|
||||||
if (line == null) break
|
if (line == null) break
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user