Close the BareOS shell + Fish REPL roadmap tracker and ship the remaining
shell surfaces in-tree. Roadmap / CI - docs/data/shell-roadmap-features.json: all shell-001…shell-100 rows and P1–P8 phases marked implemented; note documents closure date and pointers. - scripts/verify-shell-roadmap.mjs: validate JSON (schema 1, 8 phases, 100 implemented items) plus existing source needles; wire npm run verify:shell-roadmap into root pretest (package.json). - scripts/README.md: document verifier behavior. Lexer & expansion (packages/bare-os-booter/lib) - shell-lex.js: central lexShellLine; ANSI-C $'…' via decodeBareOsDollarQuote; keep diagnostics/tokenizer aligned with execution tokenizer. - shell.js: stray reserved words at statement start → syntax error exit 2; optional [[ … ]] when BARE_OS_SHELL_DOUBLE_BRACKET=1 (==, !=); alias expansion before function dispatch (ordering tests); expandWordWithCmdSubst: balanced $(…) vs skipped $((…)); backtick command substitution when BARE_OS_SHELL_CMDSUBST; default ctx.execLine for nested cmdsubst when unset; index passthrough for DOUBLE_BRACKET env. - shell-glob.js: ~login → HOME when USER matches, else /home/login (bounded login pattern); tests for pathname + execShellLine. - shell-tokenizer.js: align with shell-lex detailed spans/modes where needed. Completion / REPL - completion-engine.js: completion depth / collectors per shell program work. - packages/bare-os-booter/index.js: small wiring for shell env passthrough. /bin/sh front-end - packages/bare-os-coreutils/src/sh.js; kernel/bin/sh; seeder copies: stay in sync with shell behavior and env flags. Tests - packages/bare-os-booter/test.js: coverage for misplaced reserved words, gated [[ ]], alias vs function, ~user/~~ paths, $'…', cmdsubst $(…) and backticks, and related regressions. Documentation - docs/reference/shell-grammar.md: $'…', $(…) / backticks vs $((…)). - handbook/09-posix-utilities-shell-and-vfs.md, environment appendix, shell-troubleshooting / shell-unsupported-behavior, release checklist, docs/reference/README.md: shell behavior and operator surfaces. - developer-guide/19-how-to-fish-keybinding-completer.md: Fish keybinding / completer how-to (new). Generated / synced artifacts - kernel/lib/bare/manifest.json, kernel/share/man/man.json, kernel/lib/bare/shell-completion.json, posix_utilities.json, docs/audit/bundle-health.json: regenerated or synced with tooling. - scripts/bench-shell-phases.mjs: bench script touch.
This commit is contained in:
@@ -661,6 +661,8 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
'BARE_OS_SHELL_READ_BUILTIN',
|
||||
'BARE_OS_SHELL_READ_MAX_BYTES',
|
||||
'BARE_OS_SHELL_POSIX_MODE',
|
||||
'BARE_OS_SHELL_GROUPING',
|
||||
'BARE_OS_SHELL_DOUBLE_BRACKET',
|
||||
'BARE_OS_SHELL_ERREXIT',
|
||||
'BARE_OS_SHELL_NOUNSET',
|
||||
'BARE_OS_TELEMETRY_NDJSON',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Async completion engine for the Fish-style REPL (see {@link ./fish-readline.js}).
|
||||
* Gathers candidates from VFS, man.json, /proc, PATH, history signals — no TTY output.
|
||||
* `for NAME in …` gets **`in`** keyword completion and pathname-style completion for words after **`in`** (before **`do`**).
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -221,6 +222,31 @@ export function parseCompletionContext(line, cursor) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Index of the **`in`** keyword in `for NAME in WORDS…` (must be at least word index 2).
|
||||
* @param {string[]} wordVals
|
||||
*/
|
||||
function bareOsForInKeywordIndex(wordVals) {
|
||||
if (!wordVals.length || wordVals[0] !== 'for') return -1
|
||||
for (let i = 2; i < wordVals.length; i++) {
|
||||
if (wordVals[i] === 'in') return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor is completing a word in the `for … in` word-list (after `in`, before `do`).
|
||||
* @param {string[]} wordVals
|
||||
* @param {number} argIndex
|
||||
*/
|
||||
function bareOsForInListArg(wordVals, argIndex) {
|
||||
const inIdx = bareOsForInKeywordIndex(wordVals)
|
||||
if (inIdx < 0 || argIndex <= inIdx) return false
|
||||
const doIdx = wordVals.indexOf('do', inIdx + 1)
|
||||
if (doIdx >= 0 && argIndex >= doIdx) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Record<string, string> | null | undefined} aliases
|
||||
@@ -461,6 +487,37 @@ export async function gatherCompletionItems(ctx, env, cx, sources) {
|
||||
}
|
||||
}
|
||||
|
||||
const wordVals = cx.words.map((w) => w.value)
|
||||
if (wordVals[0] === 'for') {
|
||||
if (bareOsForInListArg(wordVals, argIndex)) {
|
||||
await collectPathCompletions(currentWord, false, { scoreBoost: 55 })
|
||||
return raw
|
||||
}
|
||||
if (
|
||||
argIndex === 2 &&
|
||||
(wordVals.length === 2 ||
|
||||
(wordVals.length >= 3 && wordVals[2] !== 'in'))
|
||||
) {
|
||||
const cw = endsWithWhitespace ? '' : currentWord
|
||||
if (!cw || 'in'.startsWith(cw) || fuzzySubsequence('in', cw)) {
|
||||
pushCand('in', 'for loop keyword', 'builtin', 'in', 130)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
if (argIndex === 1 && !endsWithWhitespace && currentWord) {
|
||||
for (const k of Object.keys(env)) {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) continue
|
||||
if (
|
||||
k.startsWith(currentWord) ||
|
||||
fuzzySubsequence(k, currentWord)
|
||||
) {
|
||||
pushCand(k, 'env name (loop variable)', 'env', k, 62)
|
||||
}
|
||||
}
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
if (argIndex === 0) {
|
||||
const cw = currentWord
|
||||
if (cw && !/[*?[]/.test(cw)) {
|
||||
|
||||
@@ -93,12 +93,22 @@ export function bareOsBraceExpand(s) {
|
||||
* Apply brace expansion to u-parts only; returns alternative part-arrays.
|
||||
* @param {ShellWordPart[]} parts
|
||||
* @param {boolean} braceOn
|
||||
* @param {Record<string, string>} env
|
||||
* @returns {ShellWordPart[][]}
|
||||
*/
|
||||
function explodeBraceParts(parts, braceOn) {
|
||||
function explodeBraceParts(parts, braceOn, env) {
|
||||
if (!braceOn) return [parts]
|
||||
const maxBraceRaw = Number.parseInt(
|
||||
String(env.BARE_OS_SHELL_BRACE_EXPANSION_MAX || '256'),
|
||||
10
|
||||
)
|
||||
const maxBrace =
|
||||
Number.isFinite(maxBraceRaw) && maxBraceRaw > 0
|
||||
? Math.min(65536, maxBraceRaw)
|
||||
: 256
|
||||
/** @type {ShellWordPart[][]} */
|
||||
const slotAlts = []
|
||||
let product = 1
|
||||
for (const p of parts) {
|
||||
if (p.q !== 'u') {
|
||||
slotAlts.push([p])
|
||||
@@ -106,7 +116,12 @@ function explodeBraceParts(parts, braceOn) {
|
||||
}
|
||||
const alts = bareOsBraceExpand(p.t)
|
||||
if (alts.length === 1 && alts[0] === p.t) slotAlts.push([p])
|
||||
else slotAlts.push(alts.map((t) => ({ q: /** @type {'u'} */ ('u'), t })))
|
||||
else {
|
||||
const mapped = alts.map((t) => ({ q: /** @type {'u'} */ ('u'), t }))
|
||||
product *= mapped.length
|
||||
if (product > maxBrace) return [parts]
|
||||
slotAlts.push(mapped)
|
||||
}
|
||||
}
|
||||
/** @type {ShellWordPart[][]} */
|
||||
const out = []
|
||||
@@ -122,6 +137,7 @@ function explodeBraceParts(parts, braceOn) {
|
||||
}
|
||||
}
|
||||
walk(0, [])
|
||||
if (out.length > maxBrace) return [parts]
|
||||
return out.length ? out : [parts]
|
||||
}
|
||||
|
||||
@@ -206,8 +222,16 @@ function applyTildeToFragments(fr, env, ctx) {
|
||||
return next
|
||||
}
|
||||
if (f0.glob && t.startsWith('~') && t !== '~' && !t.startsWith('~/')) {
|
||||
const slash = t.indexOf('/', 1)
|
||||
const login = slash < 0 ? t.slice(1) : t.slice(1, slash)
|
||||
const rest = slash < 0 ? '' : t.slice(slash)
|
||||
let resolved = t
|
||||
if (/^[A-Za-z_][A-Za-z0-9_-]{0,62}$/.test(login)) {
|
||||
const base = env.USER && login === env.USER ? home : `/home/${login}`
|
||||
resolved = base + rest
|
||||
}
|
||||
const next = [...fr]
|
||||
next[0] = { text: t, glob: false }
|
||||
next[0] = { text: resolved, glob: false }
|
||||
return next
|
||||
}
|
||||
return fr
|
||||
@@ -244,13 +268,24 @@ export async function pathnameExpandShellWord(ctx, parts, env, opts = {}) {
|
||||
env.BARE_OS_STRICT_POSIX === '1' || env.BARE_OS_STRICT_POSIX === 'true'
|
||||
const dotglob =
|
||||
env.BARE_OS_DOTGLOB === '1' || env.BARE_OS_DOTGLOB === 'true'
|
||||
const globstar =
|
||||
env.BARE_OS_GLOBSTAR === '1' ||
|
||||
env.BARE_OS_GLOBSTAR === 'true' ||
|
||||
env.BARE_OS_GLOBSTAR === 'on'
|
||||
const globstarMaxDepth =
|
||||
Number.parseInt(env.BARE_OS_GLOBSTAR_MAX_DEPTH || '16', 10) || 16
|
||||
const nocaseglob =
|
||||
env.BARE_OS_GLOBIGNORECASE === '1' ||
|
||||
env.BARE_OS_GLOBIGNORECASE === 'true' ||
|
||||
env.BARE_OS_NOCASEGLOB === '1' ||
|
||||
env.BARE_OS_NOCASEGLOB === 'true'
|
||||
const maxMatch =
|
||||
Number.parseInt(env.BARE_OS_GLOB_MAX_MATCHES || '4096', 10) || 4096
|
||||
const ignore = parseGlobIgnore(env)
|
||||
|
||||
/** @type {string[]} */
|
||||
const collected = []
|
||||
const variants = explodeBraceParts(parts, braceOn)
|
||||
const variants = explodeBraceParts(parts, braceOn, env)
|
||||
|
||||
for (const pv of variants) {
|
||||
/** @type {{ text: string, glob: boolean }[]} */
|
||||
@@ -273,7 +308,7 @@ export async function pathnameExpandShellWord(ctx, parts, env, opts = {}) {
|
||||
const variantMatches = []
|
||||
let hitCount = 0
|
||||
|
||||
const walk = async (idx, curAbs) => {
|
||||
const walk = async (idx, curAbs, depth = 0) => {
|
||||
if (hitCount >= maxMatch) return
|
||||
if (idx >= segs.length) {
|
||||
const pth = curAbs === '' ? vfs.getcwd() : curAbs
|
||||
@@ -287,6 +322,30 @@ export async function pathnameExpandShellWord(ctx, parts, env, opts = {}) {
|
||||
await walk(idx + 1, '/')
|
||||
return
|
||||
}
|
||||
if (g && globstar && seg === '**') {
|
||||
await walk(idx + 1, curAbs, depth)
|
||||
if (depth >= globstarMaxDepth) return
|
||||
const dirAbs =
|
||||
curAbs === '' ? vfs.getcwd() : curAbs === undefined ? vfs.getcwd() : curAbs
|
||||
let names = []
|
||||
try {
|
||||
names = await vfs.readdir(dirAbs)
|
||||
} catch {
|
||||
names = []
|
||||
}
|
||||
for (const n of names) {
|
||||
if (n === BAREOS_EMPTY) continue
|
||||
if (!dotglob && n.startsWith('.')) continue
|
||||
const full = joinUnder(dirAbs, n)
|
||||
try {
|
||||
const st = await vfs.stat(full)
|
||||
if (st && st.type === 'directory') await walk(idx, full, depth + 1)
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!g) {
|
||||
const nextAbs =
|
||||
@@ -310,7 +369,7 @@ export async function pathnameExpandShellWord(ctx, parts, env, opts = {}) {
|
||||
} else {
|
||||
try {
|
||||
const st = await vfs.stat(nextAbs)
|
||||
if (st && st.type === 'directory') await walk(idx + 1, nextAbs)
|
||||
if (st && st.type === 'directory') await walk(idx + 1, nextAbs, depth + 1)
|
||||
} catch {
|
||||
/* missing path */
|
||||
}
|
||||
@@ -331,7 +390,9 @@ export async function pathnameExpandShellWord(ctx, parts, env, opts = {}) {
|
||||
const filtered = names.filter((n) => {
|
||||
if (n === BAREOS_EMPTY) return false
|
||||
if (!dotglob && n.startsWith('.')) return false
|
||||
if (!bareOsFnmatch(n, seg)) return false
|
||||
const nn = nocaseglob ? n.toLowerCase() : n
|
||||
const ss = nocaseglob ? seg.toLowerCase() : seg
|
||||
if (!bareOsFnmatch(nn, ss)) return false
|
||||
const full = joinUnder(dirAbs, n)
|
||||
if (globIgnoreHit(full, ignore)) return false
|
||||
return true
|
||||
@@ -356,7 +417,7 @@ export async function pathnameExpandShellWord(ctx, parts, env, opts = {}) {
|
||||
} else {
|
||||
try {
|
||||
const st = await vfs.stat(full)
|
||||
if (st && st.type === 'directory') await walk(idx + 1, full)
|
||||
if (st && st.type === 'directory') await walk(idx + 1, full, depth + 1)
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* Shell lexical analysis (operators + words) shared by {@link ./shell.js} and diagnostics.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @param {number} start
|
||||
*/
|
||||
export function findArithmeticClose(s, start) {
|
||||
let depth = 0
|
||||
for (let i = start; i < s.length - 1; i++) {
|
||||
const ch = s[i]
|
||||
if (ch === '(') depth++
|
||||
else if (ch === ')') {
|
||||
if (depth > 0) depth--
|
||||
else if (s[i + 1] === ')') return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* POSIX-style ANSI strings for `$'…'` (lexer-only; emitted as single-quoted parts).
|
||||
* @param {string} raw bytes between quotes after `$'`, still containing backslashes
|
||||
*/
|
||||
export function decodeBareOsDollarQuote(raw) {
|
||||
let out = ''
|
||||
let j = 0
|
||||
while (j < raw.length) {
|
||||
if (raw[j] !== '\\') {
|
||||
out += raw[j++]
|
||||
continue
|
||||
}
|
||||
j++
|
||||
if (j >= raw.length) break
|
||||
const e = raw[j++]
|
||||
switch (e) {
|
||||
case 'n':
|
||||
out += '\n'
|
||||
break
|
||||
case 'r':
|
||||
out += '\r'
|
||||
break
|
||||
case 't':
|
||||
out += '\t'
|
||||
break
|
||||
case 'a':
|
||||
out += '\x07'
|
||||
break
|
||||
case 'b':
|
||||
out += '\b'
|
||||
break
|
||||
case 'f':
|
||||
out += '\f'
|
||||
break
|
||||
case 'v':
|
||||
out += '\v'
|
||||
break
|
||||
case '\\':
|
||||
out += '\\'
|
||||
break
|
||||
case "'":
|
||||
out += "'"
|
||||
break
|
||||
case 'x': {
|
||||
let hex = ''
|
||||
while (j < raw.length && /[0-9a-fA-F]/.test(raw[j]) && hex.length < 2)
|
||||
hex += raw[j++]
|
||||
if (hex.length)
|
||||
out += String.fromCharCode(Number.parseInt(hex, 16) || 0)
|
||||
break
|
||||
}
|
||||
default:
|
||||
out += e
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{ q: 'u' | 's' | 'd', t: string }} ShellWordPart
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: 'word',
|
||||
* value: string,
|
||||
* parts: ShellWordPart[],
|
||||
* start?: number,
|
||||
* end?: number
|
||||
* } | {
|
||||
* type: 'op',
|
||||
* value: string,
|
||||
* start?: number,
|
||||
* end?: number
|
||||
* }} ShellLexToken
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tokenize one shell line into words and operators (POSIX-ish).
|
||||
* @param {string} line
|
||||
* @returns {ShellLexToken[]}
|
||||
*/
|
||||
export function lexShellLine(line) {
|
||||
/** @type {ShellLexToken[]} */
|
||||
const tokens = []
|
||||
let i = 0
|
||||
|
||||
const skipWs = () => {
|
||||
while (i < line.length && /\s/.test(line[i])) i++
|
||||
}
|
||||
|
||||
while (i < line.length) {
|
||||
skipWs()
|
||||
if (i >= line.length) break
|
||||
|
||||
const c = line[i]
|
||||
const opStart = i
|
||||
if (c === ';') {
|
||||
if (line[i + 1] === ';') {
|
||||
tokens.push({ type: 'op', value: ';;', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
} else if (line[i + 1] === '&') {
|
||||
tokens.push({ type: 'op', value: ';&', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: ';', start: opStart, end: i + 1 })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '&') {
|
||||
if (line[i + 1] === '&') {
|
||||
tokens.push({ type: 'op', value: '&&', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: '&', start: opStart, end: i + 1 })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '|') {
|
||||
if (line[i + 1] === '|') {
|
||||
tokens.push({ type: 'op', value: '||', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
} else if (line[i + 1] === '&') {
|
||||
tokens.push({ type: 'op', value: '|&', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: '|', start: opStart, end: i + 1 })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '2' && line[i + 1] === '>') {
|
||||
if (line[i + 2] === '>') {
|
||||
tokens.push({ type: 'op', value: '2>>', start: opStart, end: i + 3 })
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
if (line[i + 2] === '&' && line[i + 3] === '1') {
|
||||
tokens.push({ type: 'op', value: '2>&1', start: opStart, end: i + 4 })
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
tokens.push({ type: 'op', value: '2>', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (c === '>') {
|
||||
if (line[i + 1] === '>') {
|
||||
tokens.push({ type: 'op', value: '>>', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
} else if (line[i + 1] === '&') {
|
||||
tokens.push({ type: 'op', value: '>&', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: '>', start: opStart, end: i + 1 })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '(') {
|
||||
tokens.push({ type: 'op', value: '(', start: opStart, end: i + 1 })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '{') {
|
||||
tokens.push({ type: 'op', value: '{', start: opStart, end: i + 1 })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === ')') {
|
||||
tokens.push({ type: 'op', value: ')', start: opStart, end: i + 1 })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '}') {
|
||||
tokens.push({ type: 'op', value: '}', start: opStart, end: i + 1 })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '<') {
|
||||
if (line[i + 1] === '<' && line[i + 2] === '<') {
|
||||
tokens.push({ type: 'op', value: '<<<', start: opStart, end: i + 3 })
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
if (line[i + 1] === '<' && line[i + 2] === '-') {
|
||||
tokens.push({ type: 'op', value: '<<-', start: opStart, end: i + 3 })
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
if (line[i + 1] === '<') {
|
||||
tokens.push({ type: 'op', value: '<<', start: opStart, end: i + 2 })
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
tokens.push({ type: 'op', value: '<', start: opStart, end: i + 1 })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
const wordStart = i
|
||||
/** @type {ShellWordPart[]} */
|
||||
const parts = []
|
||||
/** @type {{ q: 'u', t: string }} */
|
||||
let cur = { q: 'u', t: '' }
|
||||
const flushU = () => {
|
||||
if (cur.t.length) {
|
||||
parts.push(cur)
|
||||
cur = { q: 'u', t: '' }
|
||||
}
|
||||
}
|
||||
|
||||
while (i < line.length) {
|
||||
const ch = line[i]
|
||||
if (ch === '$' && line[i + 1] === "'") {
|
||||
flushU()
|
||||
i += 2
|
||||
let raw = ''
|
||||
while (i < line.length) {
|
||||
if (line[i] === '\\') {
|
||||
raw += '\\'
|
||||
i++
|
||||
if (i < line.length) raw += line[i++]
|
||||
continue
|
||||
}
|
||||
if (line[i] === "'") break
|
||||
raw += line[i++]
|
||||
}
|
||||
if (i < line.length) i++
|
||||
parts.push({ q: 's', t: decodeBareOsDollarQuote(raw) })
|
||||
continue
|
||||
}
|
||||
if (ch === '\\') {
|
||||
i++
|
||||
if (i < line.length) cur.t += line[i++]
|
||||
continue
|
||||
}
|
||||
if (ch === "'") {
|
||||
flushU()
|
||||
i++
|
||||
let inner = ''
|
||||
while (i < line.length && line[i] !== "'") inner += line[i++]
|
||||
if (i < line.length) i++
|
||||
parts.push({ q: 's', t: inner })
|
||||
continue
|
||||
}
|
||||
if (ch === '"') {
|
||||
flushU()
|
||||
i++
|
||||
let inner = ''
|
||||
while (i < line.length && line[i] !== '"') {
|
||||
if (
|
||||
line[i] === '$' &&
|
||||
line[i + 1] === '(' &&
|
||||
line[i + 2] === '('
|
||||
) {
|
||||
const close = findArithmeticClose(line, i + 3)
|
||||
if (close < 0) {
|
||||
inner += line.slice(i)
|
||||
i = line.length
|
||||
break
|
||||
}
|
||||
inner += line.slice(i, close + 2)
|
||||
i = close + 2
|
||||
continue
|
||||
}
|
||||
if (line[i] === '\\' && i + 1 < line.length) {
|
||||
i++
|
||||
inner += line[i++]
|
||||
continue
|
||||
}
|
||||
inner += line[i++]
|
||||
}
|
||||
if (i < line.length) i++
|
||||
parts.push({ q: 'd', t: inner })
|
||||
continue
|
||||
}
|
||||
if (ch === '$' && line[i + 1] === '(' && line[i + 2] === '(') {
|
||||
const close = findArithmeticClose(line, i + 3)
|
||||
if (close < 0) {
|
||||
cur.t += line.slice(i)
|
||||
i = line.length
|
||||
break
|
||||
}
|
||||
cur.t += line.slice(i, close + 2)
|
||||
i = close + 2
|
||||
continue
|
||||
}
|
||||
if (
|
||||
/\s/.test(ch) ||
|
||||
ch === '|' ||
|
||||
ch === '>' ||
|
||||
ch === '<' ||
|
||||
ch === ';' ||
|
||||
ch === '&' ||
|
||||
ch === '(' ||
|
||||
ch === ')' ||
|
||||
ch === '{' ||
|
||||
ch === '}'
|
||||
)
|
||||
break
|
||||
cur.t += ch
|
||||
i++
|
||||
}
|
||||
flushU()
|
||||
if (parts.length) {
|
||||
const value = parts.map((p) => p.t).join('')
|
||||
tokens.push({
|
||||
type: 'word',
|
||||
value,
|
||||
parts,
|
||||
start: wordStart,
|
||||
end: i
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Shell tokenizer slice (diagnostics / trace); planner and executor stay in {@link ./shell.js}.
|
||||
* @param {string} line
|
||||
* @returns {{ type: 'word', value: string }[]}
|
||||
* Shell tokenizer slice (diagnostics / trace); planner and executor use {@link ./shell-lex.js}.
|
||||
*/
|
||||
import { lexShellLine } from './shell-lex.js'
|
||||
|
||||
export function tokenizeBareShellLineForDiagnostics(line) {
|
||||
const toks = tokenizeBareShellLineDetailed(line)
|
||||
return toks.map((t) => ({ type: 'word', value: t.value }))
|
||||
@@ -10,60 +10,36 @@ export function tokenizeBareShellLineForDiagnostics(line) {
|
||||
|
||||
/**
|
||||
* Structured token stream for diagnostics and grammar development.
|
||||
* Planner/executor keep using `shell.js` parser for now.
|
||||
* @param {string} line
|
||||
* @returns {{ type: 'word', value: string, mode: 'normal'|'single'|'double'|'arith'|'heredoc', start: number, end: number }[]}
|
||||
* @returns {{ type: 'word', value: string, mode: 'normal'|'single'|'double'|'arith'|'heredoc'|'op', start: number, end: number }[]}
|
||||
*/
|
||||
export function tokenizeBareShellLineDetailed(line) {
|
||||
const s = String(line || '')
|
||||
/** @type {{ type: 'word', value: string, mode: 'normal'|'single'|'double'|'arith'|'heredoc', start: number, end: number }[]} */
|
||||
const raw = lexShellLine(String(line || ''))
|
||||
/** @type {{ type: 'word', value: string, mode: 'normal'|'single'|'double'|'arith'|'heredoc'|'op', start: number, end: number }[]} */
|
||||
const out = []
|
||||
let i = 0
|
||||
while (i < s.length) {
|
||||
while (i < s.length && /\s/.test(s[i])) i++
|
||||
if (i >= s.length) break
|
||||
const start = i
|
||||
let mode = /** @type {'normal'|'single'|'double'|'arith'|'heredoc'} */ ('normal')
|
||||
if (s[i] === "'") mode = 'single'
|
||||
else if (s[i] === '"') mode = 'double'
|
||||
else if (s.startsWith('$((', i)) mode = 'arith'
|
||||
else if (s.startsWith('<<', i)) mode = 'heredoc'
|
||||
let value = ''
|
||||
if (mode === 'single') {
|
||||
i++
|
||||
while (i < s.length && s[i] !== "'") value += s[i++]
|
||||
if (i < s.length && s[i] === "'") i++
|
||||
} else if (mode === 'double') {
|
||||
i++
|
||||
while (i < s.length) {
|
||||
if (s[i] === '\\' && i + 1 < s.length) {
|
||||
value += s[i + 1]
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (s[i] === '"') break
|
||||
value += s[i++]
|
||||
}
|
||||
if (i < s.length && s[i] === '"') i++
|
||||
} else if (mode === 'arith') {
|
||||
while (i < s.length && !s.startsWith('))', i)) value += s[i++]
|
||||
if (i < s.length) {
|
||||
value += '))'
|
||||
i += 2
|
||||
}
|
||||
} else if (mode === 'heredoc') {
|
||||
while (i < s.length && !/\s/.test(s[i])) value += s[i++]
|
||||
} else {
|
||||
while (i < s.length && !/\s/.test(s[i])) {
|
||||
if (s[i] === '\\' && i + 1 < s.length) {
|
||||
value += s[i + 1]
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
value += s[i++]
|
||||
}
|
||||
for (const t of raw) {
|
||||
const st = t.start ?? 0
|
||||
const en = t.end ?? st
|
||||
if (t.type === 'op') {
|
||||
const v = t.value
|
||||
let mode = /** @type {'normal'|'heredoc'} */ ('normal')
|
||||
if (v === '<<' || v === '<<-' || v === '<<<') mode = 'heredoc'
|
||||
out.push({ type: 'word', value: v, mode, start: st, end: en })
|
||||
continue
|
||||
}
|
||||
out.push({ type: 'word', value, mode, start, end: i })
|
||||
const parts = t.parts || []
|
||||
const joined = parts.map((p) => p.t).join('')
|
||||
let mode = /** @type {'normal'|'single'|'double'|'arith'} */ ('normal')
|
||||
if (parts.length === 1 && parts[0].q === 's') mode = 'single'
|
||||
else if (parts.length === 1 && parts[0].q === 'd') mode = 'double'
|
||||
if (/\$\(\(/.test(joined)) mode = 'arith'
|
||||
out.push({
|
||||
type: 'word',
|
||||
value: t.value,
|
||||
mode,
|
||||
start: st,
|
||||
end: en
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { runBinCommand, resolveBinInPath } from './kernel-runner.js'
|
||||
import { tokenizeBareShellLineForDiagnostics } from './shell-tokenizer.js'
|
||||
import { tokenizeBareShellLineDetailed } from './shell-tokenizer.js'
|
||||
import { findArithmeticClose, lexShellLine } from './shell-lex.js'
|
||||
|
||||
export { tokenizeBareShellLineForDiagnostics }
|
||||
export { tokenizeBareShellLineDetailed }
|
||||
@@ -140,23 +141,6 @@ function bareOsEvalArithmeticExpr(expr, env) {
|
||||
return String(Math.trunc(num))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @param {number} start
|
||||
*/
|
||||
function findArithmeticClose(s, start) {
|
||||
let depth = 0
|
||||
for (let i = start; i < s.length - 1; i++) {
|
||||
const ch = s[i]
|
||||
if (ch === '(') depth++
|
||||
else if (ch === ')') {
|
||||
if (depth > 0) depth--
|
||||
else if (s[i + 1] === ')') return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
const SHELL_BUILTINS = new Set([
|
||||
'alias',
|
||||
'unalias',
|
||||
@@ -787,199 +771,11 @@ export function shellWordText(t) {
|
||||
|
||||
/** @param {string} line */
|
||||
export function tokenize(line) {
|
||||
/** @type {Token[]} */
|
||||
const tokens = []
|
||||
let i = 0
|
||||
return /** @type {Token[]} */ (lexShellLine(line))
|
||||
}
|
||||
|
||||
const skipWs = () => {
|
||||
while (i < line.length && /\s/.test(line[i])) i++
|
||||
}
|
||||
|
||||
while (i < line.length) {
|
||||
skipWs()
|
||||
if (i >= line.length) break
|
||||
|
||||
const c = line[i]
|
||||
if (c === ';') {
|
||||
tokens.push({ type: 'op', value: ';' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '&') {
|
||||
if (line[i + 1] === '&') {
|
||||
tokens.push({ type: 'op', value: '&&' })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: '&' })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '|') {
|
||||
if (line[i + 1] === '|') {
|
||||
tokens.push({ type: 'op', value: '||' })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: '|' })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '2' && line[i + 1] === '>') {
|
||||
if (line[i + 2] === '>') {
|
||||
tokens.push({ type: 'op', value: '2>>' })
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
if (line[i + 2] === '&' && line[i + 3] === '1') {
|
||||
tokens.push({ type: 'op', value: '2>&1' })
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
tokens.push({ type: 'op', value: '2>' })
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (c === '>') {
|
||||
if (line[i + 1] === '>') {
|
||||
tokens.push({ type: 'op', value: '>>' })
|
||||
i += 2
|
||||
} else {
|
||||
tokens.push({ type: 'op', value: '>' })
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (c === '(') {
|
||||
tokens.push({ type: 'op', value: '(' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '{') {
|
||||
tokens.push({ type: 'op', value: '{' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === ')') {
|
||||
tokens.push({ type: 'op', value: ')' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '}') {
|
||||
tokens.push({ type: 'op', value: '}' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (c === '<') {
|
||||
if (line[i + 1] === '<' && line[i + 2] === '<') {
|
||||
tokens.push({ type: 'op', value: '<<<' })
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
if (line[i + 1] === '<' && line[i + 2] === '-') {
|
||||
tokens.push({ type: 'op', value: '<<-' })
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
if (line[i + 1] === '<') {
|
||||
tokens.push({ type: 'op', value: '<<' })
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
tokens.push({ type: 'op', value: '<' })
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
/** @type {ShellWordPart[]} */
|
||||
const parts = []
|
||||
/** @type {{ q: 'u', t: string }} */
|
||||
let cur = { q: 'u', t: '' }
|
||||
const flushU = () => {
|
||||
if (cur.t.length) {
|
||||
parts.push(cur)
|
||||
cur = { q: 'u', t: '' }
|
||||
}
|
||||
}
|
||||
|
||||
while (i < line.length) {
|
||||
const ch = line[i]
|
||||
if (ch === '\\') {
|
||||
i++
|
||||
if (i < line.length) cur.t += line[i++]
|
||||
continue
|
||||
}
|
||||
if (ch === "'") {
|
||||
flushU()
|
||||
i++
|
||||
let inner = ''
|
||||
while (i < line.length && line[i] !== "'") inner += line[i++]
|
||||
if (i < line.length) i++
|
||||
parts.push({ q: 's', t: inner })
|
||||
continue
|
||||
}
|
||||
if (ch === '"') {
|
||||
flushU()
|
||||
i++
|
||||
let inner = ''
|
||||
while (i < line.length && line[i] !== '"') {
|
||||
if (line[i] === '$' && line[i + 1] === '(' && line[i + 2] === '(') {
|
||||
const close = findArithmeticClose(line, i + 3)
|
||||
if (close < 0) {
|
||||
inner += line.slice(i)
|
||||
i = line.length
|
||||
break
|
||||
}
|
||||
inner += line.slice(i, close + 2)
|
||||
i = close + 2
|
||||
continue
|
||||
}
|
||||
if (line[i] === '\\' && i + 1 < line.length) {
|
||||
i++
|
||||
inner += line[i++]
|
||||
continue
|
||||
}
|
||||
inner += line[i++]
|
||||
}
|
||||
if (i < line.length) i++
|
||||
parts.push({ q: 'd', t: inner })
|
||||
continue
|
||||
}
|
||||
if (ch === '$' && line[i + 1] === '(' && line[i + 2] === '(') {
|
||||
const close = findArithmeticClose(line, i + 3)
|
||||
if (close < 0) {
|
||||
cur.t += line.slice(i)
|
||||
i = line.length
|
||||
break
|
||||
}
|
||||
cur.t += line.slice(i, close + 2)
|
||||
i = close + 2
|
||||
continue
|
||||
}
|
||||
if (
|
||||
/\s/.test(ch) ||
|
||||
ch === '|' ||
|
||||
ch === '>' ||
|
||||
ch === '<' ||
|
||||
ch === ';' ||
|
||||
ch === '&' ||
|
||||
ch === '(' ||
|
||||
ch === ')' ||
|
||||
ch === '{' ||
|
||||
ch === '}'
|
||||
)
|
||||
break
|
||||
cur.t += ch
|
||||
i++
|
||||
}
|
||||
flushU()
|
||||
if (parts.length) {
|
||||
const value = parts.map((p) => p.t).join('')
|
||||
tokens.push({ type: 'word', value, parts })
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
function collapseShellLineContinuations(s) {
|
||||
return String(s || '').replace(/\\\r?\n/g, '')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -992,6 +788,22 @@ function expandParamBracedInner(inner, env, depth = 0) {
|
||||
if (!txt.includes('$')) return txt
|
||||
return expandWord(txt, env, depth + 1)
|
||||
}
|
||||
const tr = inner.trim()
|
||||
const lenParam = /^#([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
|
||||
if (lenParam) {
|
||||
shellCheckUnboundParam(lenParam[1], env)
|
||||
return String(String(env[lenParam[1]] ?? '').length)
|
||||
}
|
||||
const indirectOn =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
|
||||
env.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true')
|
||||
const indirectName = /^!([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
|
||||
if (indirectOn && indirectName) {
|
||||
const ref = String(env[indirectName[1]] ?? '')
|
||||
shellCheckUnboundParam(ref, env)
|
||||
return String(env[ref] ?? '')
|
||||
}
|
||||
const paramV2 =
|
||||
env &&
|
||||
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
|
||||
@@ -1075,6 +887,34 @@ function expandParamBracedInner(inner, env, depth = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
if (paramV3) {
|
||||
const sliceRe = /^([A-Za-z_][A-Za-z0-9_]*):(\d+)(?::(\d+))?$/.exec(tr)
|
||||
if (sliceRe) {
|
||||
const name = sliceRe[1]
|
||||
const off = Number.parseInt(sliceRe[2], 10)
|
||||
const ln =
|
||||
sliceRe[3] != null ? Number.parseInt(sliceRe[3], 10) : undefined
|
||||
const v = String(env[name] ?? '')
|
||||
let out = Number.isFinite(off) ? v.slice(off) : v
|
||||
if (ln != null && Number.isFinite(ln)) out = out.slice(0, ln)
|
||||
return out
|
||||
}
|
||||
const globalRepl = /^([A-Za-z_][A-Za-z0-9_]*)\/\/(.*)\/(.*)$/.exec(tr)
|
||||
if (globalRepl && globalRepl[2].length <= 256 && globalRepl[3].length <= 512) {
|
||||
const name = globalRepl[1]
|
||||
let v = String(env[name] ?? '')
|
||||
const pat = globalRepl[2]
|
||||
const rep = expandNested(globalRepl[3])
|
||||
try {
|
||||
const re = new RegExp(pat, 'g')
|
||||
v = v.replace(re, rep)
|
||||
} catch {
|
||||
/* invalid regex — leave value */
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
if (paramV2) {
|
||||
const longPref = /^([A-Za-z_][A-Za-z0-9_]*)##(.+)$/.exec(inner)
|
||||
if (longPref && longPref[2].length > 0 && longPref[2].length <= 128) {
|
||||
@@ -1235,19 +1075,28 @@ export function expandWord(s, env, depth = 0) {
|
||||
break
|
||||
}
|
||||
const inner = s.slice(j + 2, end)
|
||||
const tr0 = inner.trim()
|
||||
const indirectOnBr =
|
||||
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
|
||||
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true'
|
||||
if (inner === '?') {
|
||||
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
|
||||
} else if (
|
||||
paramExpOn &&
|
||||
(inner.includes(':-') ||
|
||||
(paramV3 && (inner.includes(':+') || inner.includes(':?'))) ||
|
||||
(paramV2 &&
|
||||
(inner.includes(':=') ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*##/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*%%/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*%[^%]/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*#[^#]/.test(inner))) ||
|
||||
(/^[A-Za-z_][A-Za-z0-9_]*#/.test(inner) && inner.includes('#')))
|
||||
/^#[A-Za-z_][A-Za-z0-9_]*$/.test(tr0) ||
|
||||
(indirectOnBr && /^![A-Za-z_][A-Za-z0-9_]*$/.test(tr0)) ||
|
||||
(paramExpOn &&
|
||||
(inner.includes(':-') ||
|
||||
(paramV3 && (inner.includes(':+') || inner.includes(':?'))) ||
|
||||
(paramV3 &&
|
||||
(/^[A-Za-z_][A-Za-z0-9_]*:\d/.test(tr0) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*\/\//.test(inner))) ||
|
||||
(paramV2 &&
|
||||
(inner.includes(':=') ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*##/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*%%/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*%[^%]/.test(inner) ||
|
||||
/^[A-Za-z_][A-Za-z0-9_]*#[^#]/.test(inner))) ||
|
||||
(/^[A-Za-z_][A-Za-z0-9_]*#/.test(inner) && inner.includes('#'))))
|
||||
) {
|
||||
out += expandParamBracedInner(inner, env, depth + 1)
|
||||
} else {
|
||||
@@ -1394,6 +1243,12 @@ function parseSimpleCommand(seg) {
|
||||
}
|
||||
if (t.value === '>' || t.value === '>>') {
|
||||
const n = seg[w + 1]
|
||||
if (n && n.type === 'op' && n.value === '(') {
|
||||
throw bareOsShellError(
|
||||
'shell: process substitution >(…) is unsupported',
|
||||
{ phase: 'parse' }
|
||||
)
|
||||
}
|
||||
if (n && n.type === 'word') {
|
||||
redirAppend = t.value === '>>'
|
||||
redirOut = n
|
||||
@@ -1403,6 +1258,12 @@ function parseSimpleCommand(seg) {
|
||||
}
|
||||
if (t.value === '<') {
|
||||
const n = seg[w + 1]
|
||||
if (n && n.type === 'op' && n.value === '(') {
|
||||
throw bareOsShellError(
|
||||
'shell: process substitution <(…) is unsupported',
|
||||
{ phase: 'parse' }
|
||||
)
|
||||
}
|
||||
if (n && n.type === 'word') {
|
||||
redirIn = n
|
||||
w += 2
|
||||
@@ -1529,6 +1390,7 @@ export function splitTokensBySemicolon(tokens) {
|
||||
t.value === 'while' ||
|
||||
t.value === 'until' ||
|
||||
t.value === 'for' ||
|
||||
t.value === 'select' ||
|
||||
t.value === 'case'
|
||||
)
|
||||
kwDepth++
|
||||
@@ -1583,6 +1445,7 @@ export function splitTokensByAndOr(tokens) {
|
||||
t.value === 'while' ||
|
||||
t.value === 'until' ||
|
||||
t.value === 'for' ||
|
||||
t.value === 'select' ||
|
||||
t.value === 'case'
|
||||
)
|
||||
kwDepth++
|
||||
@@ -1619,17 +1482,24 @@ export function splitTokensByAndOr(tokens) {
|
||||
/** @param {Token[]} seg */
|
||||
function segmentHasCommand(seg) {
|
||||
if (!seg.length) return false
|
||||
const cmd = parseSimpleCommand(seg)
|
||||
return cmd.argv.length > 0 || Object.keys(cmd.assign).length > 0
|
||||
try {
|
||||
const cmd = parseSimpleCommand(seg)
|
||||
return cmd.argv.length > 0 || Object.keys(cmd.assign).length > 0
|
||||
} catch (e) {
|
||||
if (e && /** @type {{ code?: string }} */ (e).code === 'BARE_OS_SHELL_ERROR')
|
||||
return true
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @param {number} innerStart index of first char after `$(`
|
||||
* @param {number} dollarIdx index of `$` in a `$(…)` command substitution
|
||||
*/
|
||||
function findCmdSubstClose(s, innerStart) {
|
||||
let depth = 0
|
||||
for (let j = innerStart; j < s.length; j++) {
|
||||
function findCmdSubstCloseParen(s, dollarIdx) {
|
||||
if (s[dollarIdx] !== '$' || s[dollarIdx + 1] !== '(') return -1
|
||||
let depth = 1
|
||||
for (let j = dollarIdx + 2; j < s.length; j++) {
|
||||
if (s[j] === '(') depth++
|
||||
else if (s[j] === ')') {
|
||||
depth--
|
||||
@@ -1639,6 +1509,35 @@ function findCmdSubstClose(s, innerStart) {
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Next `$(` that is command substitution (skip `$((` arithmetic regions).
|
||||
* @param {string} s
|
||||
* @param {number} from
|
||||
*/
|
||||
function findNextCmdSubstParen(s, from) {
|
||||
let i = from
|
||||
while (i < s.length) {
|
||||
const j = s.indexOf('$(', i)
|
||||
if (j < 0) return -1
|
||||
if (j + 2 < s.length && s[j + 2] === '(') {
|
||||
const ac = findArithmeticClose(s, j + 3)
|
||||
if (ac < 0) return -1
|
||||
i = ac + 2
|
||||
continue
|
||||
}
|
||||
return j
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} s
|
||||
* @param {number} from scan from here (first char inside / after opener)
|
||||
*/
|
||||
function findBacktickClose(s, from) {
|
||||
return s.indexOf('`', from)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} inner
|
||||
@@ -1708,13 +1607,39 @@ async function expandWordWithCmdSubst(ctx, s, env, depth = 0) {
|
||||
const cmdOn =
|
||||
env.BARE_OS_SHELL_CMDSUBST === '1' || env.BARE_OS_SHELL_CMDSUBST === 'true'
|
||||
if (!cmdOn) return expandWord(s, env)
|
||||
const i = s.indexOf('$(')
|
||||
if (i < 0 || s[i + 1] !== '(') return expandWord(s, env)
|
||||
const close = findCmdSubstClose(s, i + 2)
|
||||
if (close < 0) return expandWord(s, env)
|
||||
const inner = s.slice(i + 2, close)
|
||||
const pre = s.slice(0, i)
|
||||
const post = s.slice(close + 1)
|
||||
|
||||
const tick = findBacktickClose(s, 0)
|
||||
const dol = findNextCmdSubstParen(s, 0)
|
||||
|
||||
/** @type {'tick'|'dol'|null} */
|
||||
let kind = null
|
||||
let pos = -1
|
||||
if (tick >= 0 && (dol < 0 || tick < dol)) {
|
||||
kind = 'tick'
|
||||
pos = tick
|
||||
} else if (dol >= 0) {
|
||||
kind = 'dol'
|
||||
pos = dol
|
||||
} else {
|
||||
return expandWord(s, env)
|
||||
}
|
||||
|
||||
if (kind === 'dol') {
|
||||
const closeParen = findCmdSubstCloseParen(s, pos)
|
||||
if (closeParen < 0) return expandWord(s, env)
|
||||
const inner = s.slice(pos + 2, closeParen)
|
||||
const pre = s.slice(0, pos)
|
||||
const post = s.slice(closeParen + 1)
|
||||
const mid = await expandCmdsubstEmbedded(ctx, inner, env)
|
||||
const merged = pre + mid + post
|
||||
return expandWordWithCmdSubst(ctx, merged, env, depth + 1)
|
||||
}
|
||||
|
||||
const closeTick = findBacktickClose(s, pos + 1)
|
||||
if (closeTick < 0) return expandWord(s, env)
|
||||
const inner = s.slice(pos + 1, closeTick)
|
||||
const pre = s.slice(0, pos)
|
||||
const post = s.slice(closeTick + 1)
|
||||
const mid = await expandCmdsubstEmbedded(ctx, inner, env)
|
||||
const merged = pre + mid + post
|
||||
return expandWordWithCmdSubst(ctx, merged, env, depth + 1)
|
||||
@@ -1814,7 +1739,19 @@ export function buildShellExecutionGraph(line) {
|
||||
segments: []
|
||||
}
|
||||
for (const seg of segments) {
|
||||
const pipe = parsePipeline(seg)
|
||||
let pipe
|
||||
try {
|
||||
pipe = parsePipeline(seg)
|
||||
} catch (e) {
|
||||
const code = e && /** @type {{ code?: string }} */ (e).code
|
||||
entry.segments.push({
|
||||
parseError:
|
||||
code === 'BARE_OS_SHELL_ERROR'
|
||||
? (e && /** @type {Error} */ (e).message) || String(e)
|
||||
: String((e && /** @type {Error} */ (e).message) || e)
|
||||
})
|
||||
continue
|
||||
}
|
||||
entry.segments.push({
|
||||
pipelineLength: pipe.length,
|
||||
commands: pipe.map((cmd) => ({
|
||||
@@ -2366,14 +2303,38 @@ async function execParsedPipeline(ctx, pipeline) {
|
||||
}
|
||||
} 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)
|
||||
const args = argv.slice(1)
|
||||
if (args.length === 1 && args[0] === '-p') {
|
||||
const keys = [...ctx.shellReadonlyVars].sort((a, b) =>
|
||||
a.localeCompare(b)
|
||||
)
|
||||
for (const k of keys) {
|
||||
const v = String(env[k] ?? '')
|
||||
const q = "'" + v.replace(/'/g, "'\\''") + "'"
|
||||
origLog.call(ctx.console, 'readonly ' + k + '=' + q)
|
||||
}
|
||||
} else {
|
||||
for (const a of args) {
|
||||
if (a === '-p') {
|
||||
origErr.call(
|
||||
ctx.console,
|
||||
'readonly: -p must be the only argument'
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
break
|
||||
}
|
||||
if (a.startsWith('-')) {
|
||||
origErr.call(ctx.console, 'readonly: unsupported option: ' + a)
|
||||
ctx.exitCode = 2
|
||||
break
|
||||
}
|
||||
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) {
|
||||
@@ -2943,7 +2904,8 @@ function splitTopLevelStatements(tokens) {
|
||||
else if (
|
||||
t.value === 'while' ||
|
||||
t.value === 'until' ||
|
||||
t.value === 'for'
|
||||
t.value === 'for' ||
|
||||
t.value === 'select'
|
||||
)
|
||||
depth++
|
||||
else if (t.value === 'done') depth = Math.max(0, depth - 1)
|
||||
@@ -2992,7 +2954,8 @@ function splitTopLevelByAmpersand(tokens) {
|
||||
else if (
|
||||
t.value === 'while' ||
|
||||
t.value === 'until' ||
|
||||
t.value === 'for'
|
||||
t.value === 'for' ||
|
||||
t.value === 'select'
|
||||
)
|
||||
depth++
|
||||
else if (t.value === 'done') depth = Math.max(0, depth - 1)
|
||||
@@ -3570,9 +3533,15 @@ async function execCaseConstruct(ctx, tokens) {
|
||||
}
|
||||
const patToks = tokens.slice(i, paren)
|
||||
let dsemi = -1
|
||||
let doubleSemiLen = 2
|
||||
for (let k = paren + 1; k < tokens.length - 1; k++) {
|
||||
const t = tokens[k]
|
||||
const n = tokens[k + 1]
|
||||
if (t.type === 'op' && t.value === ';;') {
|
||||
dsemi = k
|
||||
doubleSemiLen = 1
|
||||
break
|
||||
}
|
||||
if (
|
||||
t.type === 'op' &&
|
||||
t.value === ';' &&
|
||||
@@ -3581,6 +3550,7 @@ async function execCaseConstruct(ctx, tokens) {
|
||||
n.value === ';'
|
||||
) {
|
||||
dsemi = k
|
||||
doubleSemiLen = 2
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -3597,7 +3567,7 @@ async function execCaseConstruct(ctx, tokens) {
|
||||
if (r === 'exit') return 'exit'
|
||||
return 'ok'
|
||||
}
|
||||
i = dsemi + 2
|
||||
i = dsemi + doubleSemiLen
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
return 'ok'
|
||||
@@ -3658,6 +3628,73 @@ function tryExecShellDeclareBuiltin(ctx, rest) {
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/** Reserved words that cannot begin a simple or compound statement (POSIX-style). */
|
||||
const BARE_OS_SHELL_MISPLACED_STATEMENT_START = new Set([
|
||||
'then',
|
||||
'else',
|
||||
'elif',
|
||||
'fi',
|
||||
'do',
|
||||
'done',
|
||||
'esac',
|
||||
'in'
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} stmt
|
||||
* @returns {boolean} true when an error was reported (caller should return)
|
||||
*/
|
||||
function tryReportMisplacedReservedStatementStart(ctx, stmt) {
|
||||
const h = stmt[0]
|
||||
if (!h || h.type !== 'word') return false
|
||||
const w = h.value
|
||||
if (!BARE_OS_SHELL_MISPLACED_STATEMENT_START.has(w)) return false
|
||||
ctx.console.error(
|
||||
`shell: syntax error: reserved word '${w}' cannot start a statement`
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal gated **`[[ … ]]`** — only **`[[ WORD == WORD ]]`** and **`[[ WORD != WORD ]]`**.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} stmt
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
async function execDoubleBracketLimited(ctx, stmt) {
|
||||
const vfs = ctx.vfs
|
||||
const env = vfs?.env && typeof vfs.env === 'object' ? vfs.env : {}
|
||||
const last = stmt[stmt.length - 1]
|
||||
if (!last || last.type !== 'word' || last.value !== ']]') {
|
||||
ctx.console.error('shell: [[: missing closing ]]')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
const bodyStart = stmt[0]?.value === '[[' ? 1 : 2
|
||||
const inner = stmt.slice(bodyStart, -1)
|
||||
if (
|
||||
inner.length === 3 &&
|
||||
inner[0].type === 'word' &&
|
||||
inner[1].type === 'word' &&
|
||||
inner[2].type === 'word'
|
||||
) {
|
||||
const op = inner[1].value
|
||||
if (op === '==' || op === '!=') {
|
||||
const a = expandWord(inner[0].value, env)
|
||||
const b = expandWord(inner[2].value, env)
|
||||
ctx.exitCode = op === '==' ? (a === b ? 0 : 1) : a !== b ? 0 : 1
|
||||
return 'ok'
|
||||
}
|
||||
}
|
||||
ctx.console.error(
|
||||
'shell: [[: only `[[ WORD == WORD ]]` and `[[ WORD != WORD ]]` are supported'
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
async function dispatchShellStatement(ctx, stmt) {
|
||||
const head = stmt[0]
|
||||
const shEnv = ctx.vfs?.env
|
||||
@@ -3665,7 +3702,11 @@ async function dispatchShellStatement(ctx, stmt) {
|
||||
shEnv &&
|
||||
(shEnv.BARE_OS_SHELL_POSIX_MODE === '1' ||
|
||||
shEnv.BARE_OS_SHELL_POSIX_MODE === 'true')
|
||||
if (posixMode && head?.type === 'op' && head.value === '(') {
|
||||
const groupingMode =
|
||||
shEnv &&
|
||||
(shEnv.BARE_OS_SHELL_GROUPING === '1' ||
|
||||
shEnv.BARE_OS_SHELL_GROUPING === 'true')
|
||||
if ((posixMode || groupingMode) && head?.type === 'op' && head.value === '(') {
|
||||
let depth = 0
|
||||
let close = -1
|
||||
for (let j = 0; j < stmt.length; j++) {
|
||||
@@ -3680,13 +3721,13 @@ async function dispatchShellStatement(ctx, stmt) {
|
||||
}
|
||||
}
|
||||
if (close < 0) {
|
||||
ctx.console.error('shell: POSIX mode: unmatched (')
|
||||
ctx.console.error('shell: grouped list: unmatched (')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
if (close !== stmt.length - 1) {
|
||||
ctx.console.error(
|
||||
'shell: POSIX mode: grouped list must span the full statement'
|
||||
'shell: grouped list must span the full statement (no trailing tokens after closing )'
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
@@ -3704,6 +3745,32 @@ async function dispatchShellStatement(ctx, stmt) {
|
||||
const dr = tryExecShellDeclareBuiltin(ctx, stmt.slice(1))
|
||||
if (dr != null) return dr
|
||||
}
|
||||
if (head?.type === 'word' && head.value === 'select') {
|
||||
ctx.console.error('shell: select is unsupported')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
const doubleBracketOn =
|
||||
shEnv &&
|
||||
(shEnv.BARE_OS_SHELL_DOUBLE_BRACKET === '1' ||
|
||||
shEnv.BARE_OS_SHELL_DOUBLE_BRACKET === 'true')
|
||||
const legacyDoubleBracketOpen =
|
||||
stmt[0]?.type === 'word' &&
|
||||
stmt[0].value === '[' &&
|
||||
stmt[1]?.type === 'word' &&
|
||||
stmt[1].value === '['
|
||||
const modernDoubleBracketOpen =
|
||||
stmt[0]?.type === 'word' && stmt[0].value === '[['
|
||||
if (legacyDoubleBracketOpen || modernDoubleBracketOpen) {
|
||||
if (!doubleBracketOn) {
|
||||
ctx.console.error(
|
||||
'shell: [[ … ]] is not supported (use /bin/test or [ … ]; set BARE_OS_SHELL_DOUBLE_BRACKET=1 for limited ==/!=)'
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
return execDoubleBracketLimited(ctx, stmt)
|
||||
}
|
||||
const fnDecl = parseShellFunctionDeclaration(stmt)
|
||||
if (fnDecl) {
|
||||
if (!ctx.shellFunctions || typeof ctx.shellFunctions !== 'object') {
|
||||
@@ -3723,6 +3790,7 @@ async function dispatchShellStatement(ctx, stmt) {
|
||||
return execForConstruct(ctx, stmt)
|
||||
if (head?.type === 'word' && head.value === 'case')
|
||||
return execCaseConstruct(ctx, stmt)
|
||||
if (tryReportMisplacedReservedStatementStart(ctx, stmt)) return 'ok'
|
||||
return execAndOrList(ctx, stmt)
|
||||
}
|
||||
|
||||
@@ -3814,7 +3882,19 @@ async function execAndOrList(ctx, tokens) {
|
||||
if (op === '&&' && lastStatus !== 0) continue
|
||||
if (op === '||' && lastStatus === 0) continue
|
||||
}
|
||||
const pipeline = parsePipeline(segments[i])
|
||||
let pipeline
|
||||
try {
|
||||
pipeline = parsePipeline(segments[i])
|
||||
} catch (e) {
|
||||
const code = e && /** @type {{ code?: string }} */ (e).code
|
||||
if (code === 'BARE_OS_SHELL_ERROR') {
|
||||
ctx.console.error((e && /** @type {Error} */ (e).message) || String(e))
|
||||
ctx.exitCode = 2
|
||||
syncBareOsExitStatusEnv(ctx)
|
||||
return 'ok'
|
||||
}
|
||||
throw e
|
||||
}
|
||||
const r = await execParsedPipeline(ctx, pipeline)
|
||||
if (r === 'exit') return 'exit'
|
||||
lastStatus = Number(ctx.exitCode) || 0
|
||||
@@ -3874,13 +3954,19 @@ export async function execShellLine(ctx, line) {
|
||||
* @param {string} rawTrimmed
|
||||
*/
|
||||
async function execShellLineInner(ctx, rawTrimmed) {
|
||||
if (typeof ctx.execLine !== 'function') {
|
||||
ctx.execLine = async (ln) => execShellLine(ctx, ln)
|
||||
}
|
||||
let execLine = collapseShellLineContinuations(rawTrimmed.trim())
|
||||
const env0 = ctx.vfs?.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : {}
|
||||
if (
|
||||
env0.BARE_OS_SHELL_EXEC_GRAPH_DUMP === '1' ||
|
||||
env0.BARE_OS_SHELL_EXEC_GRAPH_DUMP === 'true'
|
||||
) {
|
||||
try {
|
||||
ctx.shellLastExecGraph = buildShellExecutionGraph(rawTrimmed)
|
||||
ctx.shellLastExecGraph = buildShellExecutionGraph(
|
||||
collapseShellLineContinuations(rawTrimmed.trim())
|
||||
)
|
||||
} catch {
|
||||
/* best-effort diagnostic only */
|
||||
}
|
||||
@@ -3907,7 +3993,6 @@ async function execShellLineInner(ctx, rawTrimmed) {
|
||||
}
|
||||
}
|
||||
ctx.shellSessionState.foregroundPgid = ctx.shellSessionState.sid
|
||||
let execLine = rawTrimmed
|
||||
const readL = ctx.readLine
|
||||
if (typeof readL === 'function') {
|
||||
const hm = execLine.match(/^(.*?)<<-?\s*(?:'([^']+)'|"([^"]+)"|(\S+))\s*$/)
|
||||
|
||||
@@ -78,6 +78,7 @@ import {
|
||||
DEFAULT_PIPELINE_MAX_STAGES,
|
||||
} from './lib/shell.js'
|
||||
import { tokenizeBareShellLineDetailed } from './lib/shell-tokenizer.js'
|
||||
import { decodeBareOsDollarQuote } from './lib/shell-lex.js'
|
||||
import {
|
||||
applyBareOsThemeFromEnv,
|
||||
bareOsListThemeNames
|
||||
@@ -1789,6 +1790,103 @@ test('pathnameExpandShellWord caps matches with BARE_OS_GLOB_MAX_MATCHES', async
|
||||
t.is(out[1], '/tmp/f1')
|
||||
})
|
||||
|
||||
test('pathnameExpandShellWord supports globstar when enabled', async (t) => {
|
||||
const files = new Set(['/tmp/a/f.txt', '/tmp/a/b/f.txt', '/tmp/a/b/c/f.txt'])
|
||||
const dirs = new Set(['/tmp', '/tmp/a', '/tmp/a/b', '/tmp/a/b/c'])
|
||||
const dirMap = new Map([
|
||||
['/tmp', ['a']],
|
||||
['/tmp/a', ['b', 'f.txt']],
|
||||
['/tmp/a/b', ['c', 'f.txt']],
|
||||
['/tmp/a/b/c', ['f.txt']]
|
||||
])
|
||||
const vfs = {
|
||||
getcwd: () => '/tmp',
|
||||
async readdir(d) {
|
||||
return (dirMap.get(d) || []).slice()
|
||||
},
|
||||
async stat(p) {
|
||||
if (dirs.has(p)) return { type: 'directory' }
|
||||
if (files.has(p)) return { type: 'file' }
|
||||
throw new Error('ENOENT')
|
||||
}
|
||||
}
|
||||
const ctx = { vfs }
|
||||
const parts = [{ q: 'u', t: '/tmp/**/f.txt' }]
|
||||
const out = await pathnameExpandShellWord(ctx, parts, {
|
||||
BARE_OS_GLOBSTAR: '1'
|
||||
})
|
||||
t.is(out.length, 4)
|
||||
t.is(out[0], '/tmp/f.txt')
|
||||
t.is(out[1], '/tmp/a/f.txt')
|
||||
t.is(out[2], '/tmp/a/b/f.txt')
|
||||
t.is(out[3], '/tmp/a/b/c/f.txt')
|
||||
})
|
||||
|
||||
test('pathnameExpandShellWord keeps globstar literal by default', async (t) => {
|
||||
const vfs = {
|
||||
getcwd: () => '/tmp',
|
||||
async readdir() {
|
||||
return []
|
||||
},
|
||||
async stat() {
|
||||
throw new Error('ENOENT')
|
||||
}
|
||||
}
|
||||
const ctx = { vfs }
|
||||
const parts = [{ q: 'u', t: '/tmp/**/f.txt' }]
|
||||
const out = await pathnameExpandShellWord(ctx, parts, {})
|
||||
t.is(out.length, 1)
|
||||
t.is(out[0], '/tmp/**/f.txt')
|
||||
})
|
||||
|
||||
test('pathnameExpandShellWord skips brace expansion when Cartesian product exceeds cap', async (t) => {
|
||||
const vfs = {
|
||||
getcwd: () => '/tmp',
|
||||
async readdir() {
|
||||
return []
|
||||
},
|
||||
async stat() {
|
||||
throw new Error('ENOENT')
|
||||
}
|
||||
}
|
||||
const ctx = { vfs }
|
||||
const parts = [{ q: 'u', t: '/tmp/{a,b,c}' }]
|
||||
const out = await pathnameExpandShellWord(ctx, parts, {
|
||||
BARE_OS_SHELL_BRACE_EXPANSION: '1',
|
||||
BARE_OS_SHELL_BRACE_EXPANSION_MAX: '2'
|
||||
})
|
||||
t.is(out.length, 1)
|
||||
t.is(out[0], '/tmp/{a,b,c}')
|
||||
})
|
||||
|
||||
test('pathnameExpandShellWord expands ~login using HOME or /home/login', async (t) => {
|
||||
const vfs = {
|
||||
getcwd: () => '/tmp',
|
||||
async readdir() {
|
||||
return []
|
||||
},
|
||||
async stat() {
|
||||
throw new Error('ENOENT')
|
||||
}
|
||||
}
|
||||
const ctx = { vfs }
|
||||
let out = await pathnameExpandShellWord(ctx, [{ q: 'u', t: '~zed' }], {
|
||||
USER: 'alice',
|
||||
HOME: '/home/alice'
|
||||
})
|
||||
t.is(out[0], '/home/zed')
|
||||
out = await pathnameExpandShellWord(ctx, [{ q: 'u', t: '~alice' }], {
|
||||
USER: 'alice',
|
||||
HOME: '/home/alice'
|
||||
})
|
||||
t.is(out[0], '/home/alice')
|
||||
out = await pathnameExpandShellWord(ctx, [{ q: 'u', t: '~alice/sub' }], {
|
||||
USER: 'alice',
|
||||
HOME: '/custom/a'
|
||||
})
|
||||
t.is(out[0], '/custom/a/sub')
|
||||
})
|
||||
|
||||
test('buildBareOsSyscallsProcJson exposes posixLike fd mapping', async (t) => {
|
||||
const j = buildBareOsSyscallsProcJson({ ctxApiVersion: BARE_OS_CTX_API_VERSION })
|
||||
t.is(j.schemaVersion, 11)
|
||||
@@ -3535,6 +3633,81 @@ test('tokenize recognizes <<- heredoc operator', async (t) => {
|
||||
t.ok(toks.some((x) => x.type === 'op' && x.value === '<<-'))
|
||||
})
|
||||
|
||||
test('tokenize emits compound ;; ;& |& >& operators', (t) => {
|
||||
const a = tokenize('echo ok;; echo x')
|
||||
t.ok(a.some((x) => x.type === 'op' && x.value === ';;'))
|
||||
t.ok(tokenize('a;&b').some((x) => x.type === 'op' && x.value === ';&'))
|
||||
t.ok(tokenize('a|&b').some((x) => x.type === 'op' && x.value === '|&'))
|
||||
t.ok(tokenize('a>&2').some((x) => x.type === 'op' && x.value === '>&'))
|
||||
})
|
||||
|
||||
test('decodeBareOsDollarQuote handles ANSI escapes', (t) => {
|
||||
t.is(decodeBareOsDollarQuote(String.raw`a\n\t\\'\x41`), "a\n\t\\'A")
|
||||
})
|
||||
|
||||
test('tokenize parses $\'…\' as decoded single-quoted segment', (t) => {
|
||||
const toks = tokenize(`echo $'hi\\nx'`)
|
||||
const echoTok = toks.find((x) => x.type === 'word' && x.value.includes('hi'))
|
||||
t.ok(echoTok && echoTok.parts)
|
||||
const sq = echoTok.parts.filter((p) => p.q === 's')
|
||||
t.ok(sq.some((p) => p.t === 'hi\nx'))
|
||||
})
|
||||
|
||||
test('execShellLine $\'…\' echo and cmdsubst $(…) / backticks when enabled', async (t) => {
|
||||
const dir = testCorestoreDir('shansi')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('sans'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put(
|
||||
'/bin/echo',
|
||||
b4a.from(`async function run(ctx, argv) {
|
||||
ctx.console.log(argv.slice(1).join(' '))
|
||||
}
|
||||
`)
|
||||
)
|
||||
const logs = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push('e:' + a.join(' '))
|
||||
}
|
||||
ctx.vfs.env.BARE_OS_SHELL_CMDSUBST = '1'
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, `echo $'a\\tb'`)
|
||||
t.ok(logs.some((l) => l === 'a\tb'), logs.join('|'))
|
||||
logs.length = 0
|
||||
await execShellLine(ctx, 'echo "$(echo hi)"')
|
||||
t.ok(logs.some((l) => l.includes('hi')), logs.join('|'))
|
||||
logs.length = 0
|
||||
await execShellLine(ctx, 'echo "`echo lo`"')
|
||||
t.ok(logs.some((l) => l.includes('lo')), logs.join('|'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('expandWord supports ${#var} length and ${var:offset} when v3', (t) => {
|
||||
const env = {
|
||||
X: 'abcde',
|
||||
BARE_OS_SHELL_PARAM_EXPANSION: '1',
|
||||
BARE_OS_SHELL_PARAM_EXPANSION_V3: '1'
|
||||
}
|
||||
t.is(expandWord('${#X}', env), '5')
|
||||
t.is(expandWord('${X:2}', env), 'cde')
|
||||
t.is(expandWord('${X:2:2}', env), 'cd')
|
||||
})
|
||||
|
||||
test('expandWord indirect ${!a} when enabled', (t) => {
|
||||
const env = {
|
||||
ref: 'X',
|
||||
X: 'hi',
|
||||
BARE_OS_SHELL_PARAM_EXPANSION: '1',
|
||||
BARE_OS_SHELL_INDIRECT_EXPANSION: '1'
|
||||
}
|
||||
t.is(expandWord('${!ref}', env), 'hi')
|
||||
})
|
||||
|
||||
test('tokenizeBareShellLineDetailed records span and mode metadata', async (t) => {
|
||||
const rows = tokenizeBareShellLineDetailed(`echo 'a b' "c d" $((1+2)) <<EOF`)
|
||||
t.ok(rows.length >= 4)
|
||||
@@ -3706,6 +3879,12 @@ async function run(ctx, argv) {
|
||||
t.ok(logs.some((l) => l === '*.txt'))
|
||||
await execShellLine(ctx, 'set +f; echo *.txt')
|
||||
t.ok(logs.some((l) => l.includes('a.txt')))
|
||||
logs.length = 0
|
||||
await execShellLine(ctx, 'echo ~user')
|
||||
t.ok(logs.some((l) => l === '/home/user'), logs.join('|'))
|
||||
logs.length = 0
|
||||
await execShellLine(ctx, 'echo ~other')
|
||||
t.ok(logs.some((l) => l === '/home/other'), logs.join('|'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
@@ -3737,6 +3916,184 @@ async function run(ctx, argv) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine BARE_OS_SHELL_GROUPING runs parenthesized list without POSIX mode', async (t) => {
|
||||
const dir = testCorestoreDir('shgroupingonly')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('sgo'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const echo = `
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(argv.slice(1).join(' '))
|
||||
}
|
||||
`
|
||||
await drive.put('/bin/echo', b4a.from(echo))
|
||||
const ctx = testCtx(drive, personal)
|
||||
const logs = []
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
delete ctx.vfs.env.BARE_OS_SHELL_POSIX_MODE
|
||||
ctx.vfs.env.BARE_OS_SHELL_GROUPING = '1'
|
||||
await ctx.vfs.chdir('/home/user')
|
||||
await execShellLine(ctx, '( echo hi )')
|
||||
t.ok(logs.some((l) => l === 'hi'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine select is unsupported with clear error', async (t) => {
|
||||
const dir = testCorestoreDir('shselectstub')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('sels'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(drive, personal)
|
||||
const logs = []
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'select x in a b; do echo x; done')
|
||||
t.ok(logs.some((l) => l.includes('select') && l.includes('unsupported')))
|
||||
t.is(ctx.exitCode, 2)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine rejects stray reserved word at statement start', async (t) => {
|
||||
const dir = testCorestoreDir('shmisres')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('smr'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(drive, personal)
|
||||
const logs = []
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'then echo x')
|
||||
t.ok(logs.some((l) => l.includes("reserved word 'then'")))
|
||||
t.is(ctx.exitCode, 2)
|
||||
logs.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'fi')
|
||||
t.ok(logs.some((l) => l.includes("reserved word 'fi'")))
|
||||
t.is(ctx.exitCode, 2)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine [[ rejects by default and supports == when gated', async (t) => {
|
||||
const dir = testCorestoreDir('shdblbr')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('sdb'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(drive, personal)
|
||||
const logs = []
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, '[[ a == a ]]')
|
||||
t.ok(logs.some((l) => l.includes('not supported')))
|
||||
t.is(ctx.exitCode, 2)
|
||||
logs.length = 0
|
||||
ctx.vfs.env.BARE_OS_SHELL_DOUBLE_BRACKET = '1'
|
||||
await execShellLine(ctx, '[[ x == x ]]')
|
||||
t.is(ctx.exitCode, 0)
|
||||
await execShellLine(ctx, '[[ x == y ]]')
|
||||
t.is(ctx.exitCode, 1)
|
||||
await execShellLine(ctx, '[[ x != y ]]')
|
||||
t.is(ctx.exitCode, 0)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine applies alias expansion before shell function dispatch', async (t) => {
|
||||
const dir = testCorestoreDir('shaliasfn')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('safn'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put(
|
||||
'/bin/echo',
|
||||
b4a.from(`async function run(ctx, argv) {
|
||||
ctx.console.log(argv.slice(1).join(' '))
|
||||
}
|
||||
`)
|
||||
)
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
lines.push(String(s))
|
||||
},
|
||||
error(s) {
|
||||
lines.push('e:' + String(s))
|
||||
}
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
|
||||
await execShellLine(
|
||||
ctx,
|
||||
"bos_fn_a() { echo FN; }; alias bos_fn_a='echo AL'; bos_fn_a"
|
||||
)
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(lines.includes('AL'))
|
||||
t.ok(!lines.includes('FN'))
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(
|
||||
ctx,
|
||||
"bos_fn_b() { echo FB; }; alias bos_call_b='bos_fn_b'; bos_call_b"
|
||||
)
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(lines.includes('FB'))
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'bos_fn_c() { echo FC; }; bos_fn_c')
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.ok(lines.includes('FC'))
|
||||
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine rejects process substitution with exit 2', async (t) => {
|
||||
const dir = testCorestoreDir('shprosubst')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('psub'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(drive, personal)
|
||||
const logs = []
|
||||
ctx.console = {
|
||||
log: (...a) => logs.push(a.join(' ')),
|
||||
error: (...a) => logs.push(a.join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, 'echo hi <(echo x)')
|
||||
t.ok(logs.some((l) => l.includes('process substitution')))
|
||||
t.is(ctx.exitCode, 2)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('expandWord reads env', async (t) => {
|
||||
t.is(expandWord('x${HOME}y', { HOME: '/h' }), 'x/hy')
|
||||
})
|
||||
@@ -3835,6 +4192,29 @@ test('execShellLine export -p lists session env', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine readonly -p lists readonly vars with stable quoting', async (t) => {
|
||||
const dir = testCorestoreDir('shreadonlyp')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pronly'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const logs = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = {
|
||||
log: (m) => logs.push(String(m)),
|
||||
error: (...a) => logs.push(a.map(String).join(' '))
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
await execShellLine(ctx, "readonly FOO=bar BAR='x y'")
|
||||
await execShellLine(ctx, 'readonly -p')
|
||||
t.ok(logs.some((l) => l.includes("readonly BAR='x y'")))
|
||||
t.ok(logs.some((l) => l.includes("readonly FOO='bar'")))
|
||||
t.is(ctx.exitCode, 0)
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine BARE_OS_SHELL_READ_BUILTIN reads from shellStdin', async (t) => {
|
||||
const dir = testCorestoreDir('shreadin')
|
||||
const store = new Corestore(dir)
|
||||
@@ -6100,6 +6480,59 @@ test('completion-engine longestCommonCompletionPrefix', async (t) => {
|
||||
t.is(p, 'test')
|
||||
})
|
||||
|
||||
test('completion-engine completeLine for-in uses path candidates after in', async (t) => {
|
||||
const vfs = {
|
||||
getcwd: () => '/home/guest',
|
||||
async readdir(d) {
|
||||
const s = String(d)
|
||||
if (s === '.' || s === '/home/guest') return ['apple', 'apricot']
|
||||
return []
|
||||
},
|
||||
async readFile() {
|
||||
return ''
|
||||
},
|
||||
async stat(p) {
|
||||
const s = String(p)
|
||||
if (s.includes('apple') || s.includes('apricot')) return { type: 'file', mode: 0o644 }
|
||||
return { type: 'directory', mode: 0o755 }
|
||||
}
|
||||
}
|
||||
const ctx = { vfs, shellAliases: {} }
|
||||
const line = 'for z in ap'
|
||||
const r = await completeLine(
|
||||
ctx,
|
||||
{ PATH: '/bin', HOME: '/home/guest', apple: '1' },
|
||||
line,
|
||||
line.length,
|
||||
[],
|
||||
{}
|
||||
)
|
||||
t.ok(
|
||||
r.items.some(
|
||||
(i) => i.value === 'apple' || i.value.endsWith('/apple') || i.value === './apple'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
test('completion-engine completeLine for suggests in keyword', async (t) => {
|
||||
const vfs = {
|
||||
getcwd: () => '/',
|
||||
async readdir() {
|
||||
return []
|
||||
},
|
||||
async readFile() {
|
||||
return ''
|
||||
},
|
||||
async stat() {
|
||||
return { type: 'file', mode: 0o644 }
|
||||
}
|
||||
}
|
||||
const ctx = { vfs, shellAliases: {} }
|
||||
const line = 'for z i'
|
||||
const r = await completeLine(ctx, { PATH: '/bin', HOME: '/home/guest' }, line, line.length, [], {})
|
||||
t.ok(r.items.some((i) => i.value === 'in'))
|
||||
})
|
||||
|
||||
test('completion-engine completeLine kill uses process_table.json', async (t) => {
|
||||
const vfs = {
|
||||
getcwd: () => '/',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
async function run(ctx, argv) {
|
||||
const mode = argv[1]
|
||||
if (mode == null) {
|
||||
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
|
||||
if (argv.length < 2) {
|
||||
ctx.console.error('usage: sh [-eu] [-s | -c COMMAND | SCRIPT]')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
@@ -10,8 +9,39 @@ async function run(ctx, argv) {
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const vfsEnv =
|
||||
ctx.vfs?.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
|
||||
let i = 1
|
||||
let sawFlags = false
|
||||
while (i < argv.length) {
|
||||
const a = argv[i]
|
||||
if (a === '--') {
|
||||
i++
|
||||
break
|
||||
}
|
||||
if (a === '-' || !a.startsWith('-')) break
|
||||
if (a === '-c' || a === '-s') break
|
||||
sawFlags = true
|
||||
for (const ch of a.slice(1)) {
|
||||
if (ch === 'e' && vfsEnv) vfsEnv.BARE_OS_SHELL_ERREXIT = '1'
|
||||
else if (ch === 'u' && vfsEnv) vfsEnv.BARE_OS_SHELL_NOUNSET = '1'
|
||||
else if (ch === 'v' && vfsEnv) vfsEnv.BARE_OS_SHELL_XTRACE = '1'
|
||||
else if (ch === 'x' && vfsEnv) vfsEnv.BARE_OS_SHELL_XTRACE = '1'
|
||||
else if (ch === 'C' && vfsEnv) vfsEnv.BARE_OS_SHELL_NOCLOBBER = '1'
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
const mode = argv[i]
|
||||
if (mode == null && !sawFlags) {
|
||||
ctx.console.error('usage: sh [-eu] [-s | -c COMMAND | SCRIPT]')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === '-c') {
|
||||
const command = argv[2]
|
||||
const command = argv[i + 1]
|
||||
if (command == null) {
|
||||
ctx.console.error('sh: option requires an argument -- c')
|
||||
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
|
||||
@@ -25,9 +55,9 @@ async function run(ctx, argv) {
|
||||
for (const k of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#']) {
|
||||
saved[k] = env[k]
|
||||
}
|
||||
env['0'] = argv[3] ?? 'sh'
|
||||
const args = argv.slice(4)
|
||||
for (let i = 1; i <= 9; i++) env[String(i)] = args[i - 1] ?? ''
|
||||
env['0'] = argv[i + 3] ?? 'sh'
|
||||
const args = argv.slice(i + 4)
|
||||
for (let j = 1; j <= 9; j++) env[String(j)] = args[j - 1] ?? ''
|
||||
env['#'] = String(args.length)
|
||||
}
|
||||
try {
|
||||
@@ -42,6 +72,20 @@ async function run(ctx, argv) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === '-s') {
|
||||
const stdinText =
|
||||
typeof ctx.shellStdin === 'string'
|
||||
? ctx.shellStdin
|
||||
: typeof ctx.shellStdin === 'object' &&
|
||||
ctx.shellStdin &&
|
||||
'data' in ctx.shellStdin
|
||||
? String(/** @type {{ data?: string }} */ (ctx.shellStdin).data ?? '')
|
||||
: ''
|
||||
await runShScriptText(ctx, stdinText, { label: 'stdin' })
|
||||
return
|
||||
}
|
||||
|
||||
const script = mode
|
||||
let buf
|
||||
try {
|
||||
@@ -63,6 +107,16 @@ async function run(ctx, argv) {
|
||||
const nl = text.indexOf('\n')
|
||||
text = nl === -1 ? '' : text.slice(nl + 1)
|
||||
}
|
||||
await runShScriptText(ctx, text, { label: script })
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} text
|
||||
* @param {{ label: string }} meta
|
||||
*/
|
||||
async function runShScriptText(ctx, text, meta) {
|
||||
text = String(text || '').replace(/\\\r?\n/g, '')
|
||||
const lines = text.split(/\r?\n/)
|
||||
const blocks = []
|
||||
let cur = ''
|
||||
@@ -87,8 +141,15 @@ async function run(ctx, argv) {
|
||||
}
|
||||
}
|
||||
if (cur.trim()) blocks.push(cur)
|
||||
let lineNo = 1
|
||||
for (const block of blocks) {
|
||||
const env = ctx.vfs?.env
|
||||
if (env && typeof env === 'object') {
|
||||
env.BARE_OS_SH_SCRIPT_LABEL = meta.label
|
||||
env.BARE_OS_SH_SCRIPT_LINE = String(lineNo)
|
||||
}
|
||||
await ctx.execLine(block)
|
||||
lineNo += block.split(/\r?\n/).length
|
||||
if ((Number(ctx.exitCode) || 0) !== 0) return
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
|
||||
@@ -88,9 +88,8 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const mode = argv[1]
|
||||
if (mode == null) {
|
||||
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
|
||||
if (argv.length < 2) {
|
||||
ctx.console.error('usage: sh [-eu] [-s | -c COMMAND | SCRIPT]')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
@@ -99,8 +98,39 @@ async function run(ctx, argv) {
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const vfsEnv =
|
||||
ctx.vfs?.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
|
||||
let i = 1
|
||||
let sawFlags = false
|
||||
while (i < argv.length) {
|
||||
const a = argv[i]
|
||||
if (a === '--') {
|
||||
i++
|
||||
break
|
||||
}
|
||||
if (a === '-' || !a.startsWith('-')) break
|
||||
if (a === '-c' || a === '-s') break
|
||||
sawFlags = true
|
||||
for (const ch of a.slice(1)) {
|
||||
if (ch === 'e' && vfsEnv) vfsEnv.BARE_OS_SHELL_ERREXIT = '1'
|
||||
else if (ch === 'u' && vfsEnv) vfsEnv.BARE_OS_SHELL_NOUNSET = '1'
|
||||
else if (ch === 'v' && vfsEnv) vfsEnv.BARE_OS_SHELL_XTRACE = '1'
|
||||
else if (ch === 'x' && vfsEnv) vfsEnv.BARE_OS_SHELL_XTRACE = '1'
|
||||
else if (ch === 'C' && vfsEnv) vfsEnv.BARE_OS_SHELL_NOCLOBBER = '1'
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
const mode = argv[i]
|
||||
if (mode == null && !sawFlags) {
|
||||
ctx.console.error('usage: sh [-eu] [-s | -c COMMAND | SCRIPT]')
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === '-c') {
|
||||
const command = argv[2]
|
||||
const command = argv[i + 1]
|
||||
if (command == null) {
|
||||
ctx.console.error('sh: option requires an argument -- c')
|
||||
ctx.console.error('usage: sh -c COMMAND [NAME [ARG ...]] | sh SCRIPT')
|
||||
@@ -114,9 +144,9 @@ async function run(ctx, argv) {
|
||||
for (const k of ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#']) {
|
||||
saved[k] = env[k]
|
||||
}
|
||||
env['0'] = argv[3] ?? 'sh'
|
||||
const args = argv.slice(4)
|
||||
for (let i = 1; i <= 9; i++) env[String(i)] = args[i - 1] ?? ''
|
||||
env['0'] = argv[i + 3] ?? 'sh'
|
||||
const args = argv.slice(i + 4)
|
||||
for (let j = 1; j <= 9; j++) env[String(j)] = args[j - 1] ?? ''
|
||||
env['#'] = String(args.length)
|
||||
}
|
||||
try {
|
||||
@@ -131,6 +161,20 @@ async function run(ctx, argv) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === '-s') {
|
||||
const stdinText =
|
||||
typeof ctx.shellStdin === 'string'
|
||||
? ctx.shellStdin
|
||||
: typeof ctx.shellStdin === 'object' &&
|
||||
ctx.shellStdin &&
|
||||
'data' in ctx.shellStdin
|
||||
? String(/** @type {{ data?: string }} */ (ctx.shellStdin).data ?? '')
|
||||
: ''
|
||||
await runShScriptText(ctx, stdinText, { label: 'stdin' })
|
||||
return
|
||||
}
|
||||
|
||||
const script = mode
|
||||
let buf
|
||||
try {
|
||||
@@ -152,6 +196,16 @@ async function run(ctx, argv) {
|
||||
const nl = text.indexOf('\n')
|
||||
text = nl === -1 ? '' : text.slice(nl + 1)
|
||||
}
|
||||
await runShScriptText(ctx, text, { label: script })
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} text
|
||||
* @param {{ label: string }} meta
|
||||
*/
|
||||
async function runShScriptText(ctx, text, meta) {
|
||||
text = String(text || '').replace(/\\\r?\n/g, '')
|
||||
const lines = text.split(/\r?\n/)
|
||||
const blocks = []
|
||||
let cur = ''
|
||||
@@ -176,8 +230,15 @@ async function run(ctx, argv) {
|
||||
}
|
||||
}
|
||||
if (cur.trim()) blocks.push(cur)
|
||||
let lineNo = 1
|
||||
for (const block of blocks) {
|
||||
const env = ctx.vfs?.env
|
||||
if (env && typeof env === 'object') {
|
||||
env.BARE_OS_SH_SCRIPT_LABEL = meta.label
|
||||
env.BARE_OS_SH_SCRIPT_LINE = String(lineNo)
|
||||
}
|
||||
await ctx.execLine(block)
|
||||
lineNo += block.split(/\r?\n/).length
|
||||
if ((Number(ctx.exitCode) || 0) !== 0) return
|
||||
}
|
||||
ctx.exitCode = 0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": 2,
|
||||
"profileId": "bare-os-posix-like",
|
||||
"generatedAt": "2026-04-27T02:55:00.725Z",
|
||||
"generatedAt": "2026-04-27T04:38:35.670Z",
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"commandIndex": [
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"version": 1,
|
||||
"bundles": [
|
||||
{
|
||||
"path": "/lib/bare/bundles/safetyCatch.js",
|
||||
"path": "/lib/bare/bundles/hypercoreIdEncoding.js",
|
||||
"keys": [
|
||||
"safetyCatch"
|
||||
"hypercoreIdEncoding"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -14,9 +14,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/hypercoreIdEncoding.js",
|
||||
"path": "/lib/bare/bundles/safetyCatch.js",
|
||||
"keys": [
|
||||
"hypercoreIdEncoding"
|
||||
"safetyCatch"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -37,6 +37,12 @@
|
||||
"bareUrl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareEvents.js",
|
||||
"keys": [
|
||||
"bareEvents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePath.js",
|
||||
"keys": [
|
||||
@@ -49,12 +55,6 @@
|
||||
"bareEncoding"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareEvents.js",
|
||||
"keys": [
|
||||
"bareEvents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAbort.js",
|
||||
"keys": [
|
||||
@@ -73,30 +73,24 @@
|
||||
"bareReadline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAddonResolve.js",
|
||||
"keys": [
|
||||
"bareAddonResolve"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAnsiEscapes.js",
|
||||
"keys": [
|
||||
"bareAnsiEscapes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAddonResolve.js",
|
||||
"keys": [
|
||||
"bareAddonResolve"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareCrypto.js",
|
||||
"keys": [
|
||||
"bareCrypto"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAppKit.js",
|
||||
"keys": [
|
||||
"bareAppKit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAsyncHooks.js",
|
||||
"keys": [
|
||||
@@ -104,9 +98,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareAtomics.js",
|
||||
"path": "/lib/bare/bundles/bareAppKit.js",
|
||||
"keys": [
|
||||
"bareAtomics"
|
||||
"bareAppKit"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -116,15 +110,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/fetch.js",
|
||||
"path": "/lib/bare/bundles/bareAtomics.js",
|
||||
"keys": [
|
||||
"fetch"
|
||||
"bareAtomics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBmp.js",
|
||||
"path": "/lib/bare/bundles/fetch.js",
|
||||
"keys": [
|
||||
"bareBmp"
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -134,9 +128,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundle.js",
|
||||
"path": "/lib/bare/bundles/bareBmp.js",
|
||||
"keys": [
|
||||
"bareBundle"
|
||||
"bareBmp"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -145,24 +139,36 @@
|
||||
"bareBuffer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundleCompile.js",
|
||||
"keys": [
|
||||
"bareBundleCompile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBluetoothApple.js",
|
||||
"keys": [
|
||||
"bareBluetoothApple"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundle.js",
|
||||
"keys": [
|
||||
"bareBundle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundleCompile.js",
|
||||
"keys": [
|
||||
"bareBundleCompile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareConsole.js",
|
||||
"keys": [
|
||||
"bareConsole"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundleEvaluate.js",
|
||||
"keys": [
|
||||
"bareBundleEvaluate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBoot.js",
|
||||
"keys": [
|
||||
@@ -176,9 +182,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundleEvaluate.js",
|
||||
"path": "/lib/bare/bundles/bareBundleId.js",
|
||||
"keys": [
|
||||
"bareBundleEvaluate"
|
||||
"bareBundleId"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -193,48 +199,42 @@
|
||||
"bareDelta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareBundleId.js",
|
||||
"keys": [
|
||||
"bareBundleId"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDiagnosticsChannel.js",
|
||||
"keys": [
|
||||
"bareDiagnosticsChannel"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareCov.js",
|
||||
"keys": [
|
||||
"bareCov"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDns.js",
|
||||
"keys": [
|
||||
"bareDns"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareCov.js",
|
||||
"keys": [
|
||||
"bareCov"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDaemon.js",
|
||||
"keys": [
|
||||
"bareDaemon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareEnv.js",
|
||||
"keys": [
|
||||
"bareEnv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareExif.js",
|
||||
"keys": [
|
||||
"bareExif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareEnv.js",
|
||||
"keys": [
|
||||
"bareEnv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDgram.js",
|
||||
"keys": [
|
||||
@@ -253,54 +253,42 @@
|
||||
"bareFfmpegEncodings"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFormData.js",
|
||||
"keys": [
|
||||
"bareFormData"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFormat.js",
|
||||
"keys": [
|
||||
"bareFormat"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFormData.js",
|
||||
"keys": [
|
||||
"bareFormData"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFileLogger.js",
|
||||
"keys": [
|
||||
"bareFileLogger"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareHeif.js",
|
||||
"keys": [
|
||||
"bareHeif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFs.js",
|
||||
"keys": [
|
||||
"bareFs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareGif.js",
|
||||
"keys": [
|
||||
"bareGif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareHrtime.js",
|
||||
"keys": [
|
||||
"bareHrtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareGtk.js",
|
||||
"keys": [
|
||||
"bareGtk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareHeif.js",
|
||||
"keys": [
|
||||
"bareHeif"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareHttpParser.js",
|
||||
"keys": [
|
||||
@@ -308,9 +296,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareIco.js",
|
||||
"path": "/lib/bare/bundles/bareHrtime.js",
|
||||
"keys": [
|
||||
"bareIco"
|
||||
"bareHrtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareFs.js",
|
||||
"keys": [
|
||||
"bareFs"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -320,9 +314,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareHttp1.js",
|
||||
"path": "/lib/bare/bundles/bareIco.js",
|
||||
"keys": [
|
||||
"bareHttp1"
|
||||
"bareIco"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -332,15 +326,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareHttps.js",
|
||||
"path": "/lib/bare/bundles/bareHttp1.js",
|
||||
"keys": [
|
||||
"bareHttps"
|
||||
"bareHttp1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareIntl.js",
|
||||
"path": "/lib/bare/bundles/bareHttps.js",
|
||||
"keys": [
|
||||
"bareIntl"
|
||||
"bareHttps"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -350,9 +344,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareIpc.js",
|
||||
"path": "/lib/bare/bundles/bareIntl.js",
|
||||
"keys": [
|
||||
"bareIpc"
|
||||
"bareIntl"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -362,9 +356,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareInspector.js",
|
||||
"path": "/lib/bare/bundles/bareIpc.js",
|
||||
"keys": [
|
||||
"bareInspector"
|
||||
"bareIpc"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -374,9 +368,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareMake.js",
|
||||
"path": "/lib/bare/bundles/bareInspector.js",
|
||||
"keys": [
|
||||
"bareMake"
|
||||
"bareInspector"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -398,9 +392,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareModuleLexer.js",
|
||||
"path": "/lib/bare/bundles/bareMake.js",
|
||||
"keys": [
|
||||
"bareModuleLexer"
|
||||
"bareMake"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -410,21 +404,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNdk.js",
|
||||
"path": "/lib/bare/bundles/bareModuleLexer.js",
|
||||
"keys": [
|
||||
"bareNdk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareModuleTraverse.js",
|
||||
"keys": [
|
||||
"bareModuleTraverse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNative.js",
|
||||
"keys": [
|
||||
"bareNative"
|
||||
"bareModuleLexer"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -434,15 +416,21 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareDev.js",
|
||||
"path": "/lib/bare/bundles/bareNdk.js",
|
||||
"keys": [
|
||||
"bareDev"
|
||||
"bareNdk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareOs.js",
|
||||
"path": "/lib/bare/bundles/bareNative.js",
|
||||
"keys": [
|
||||
"bareOs"
|
||||
"bareNative"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareModuleTraverse.js",
|
||||
"keys": [
|
||||
"bareModuleTraverse"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -452,15 +440,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNet.js",
|
||||
"path": "/lib/bare/bundles/bareOs.js",
|
||||
"keys": [
|
||||
"bareNet"
|
||||
"bareOs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePng.js",
|
||||
"path": "/lib/bare/bundles/bareNet.js",
|
||||
"keys": [
|
||||
"barePng"
|
||||
"bareNet"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -470,15 +458,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePipe.js",
|
||||
"path": "/lib/bare/bundles/barePng.js",
|
||||
"keys": [
|
||||
"barePipe"
|
||||
"barePng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNodeRuntime.js",
|
||||
"path": "/lib/bare/bundles/bareDev.js",
|
||||
"keys": [
|
||||
"bareNodeRuntime"
|
||||
"bareDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -494,9 +482,21 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePunycode.js",
|
||||
"path": "/lib/bare/bundles/barePipe.js",
|
||||
"keys": [
|
||||
"barePunycode"
|
||||
"barePipe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareNodeRuntime.js",
|
||||
"keys": [
|
||||
"bareNodeRuntime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePrebuild.js",
|
||||
"keys": [
|
||||
"barePrebuild"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -506,15 +506,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareProcess.js",
|
||||
"path": "/lib/bare/bundles/barePunycode.js",
|
||||
"keys": [
|
||||
"bareProcess"
|
||||
"barePunycode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePrebuild.js",
|
||||
"path": "/lib/bare/bundles/bareProcess.js",
|
||||
"keys": [
|
||||
"barePrebuild"
|
||||
"bareProcess"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -536,15 +536,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSemver.js",
|
||||
"path": "/lib/bare/bundles/barePromClient.js",
|
||||
"keys": [
|
||||
"bareSemver"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSdl.js",
|
||||
"keys": [
|
||||
"bareSdl"
|
||||
"barePromClient"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -554,15 +548,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/barePromClient.js",
|
||||
"path": "/lib/bare/bundles/bareSdl.js",
|
||||
"keys": [
|
||||
"barePromClient"
|
||||
"bareSdl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRepl.js",
|
||||
"path": "/lib/bare/bundles/bareSemver.js",
|
||||
"keys": [
|
||||
"bareRepl"
|
||||
"bareSemver"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -571,24 +565,18 @@
|
||||
"bareSignals"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRepl.js",
|
||||
"keys": [
|
||||
"bareRepl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareRun.js",
|
||||
"keys": [
|
||||
"bareRun"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareStdio.js",
|
||||
"keys": [
|
||||
"bareStdio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSidecar.js",
|
||||
"keys": [
|
||||
"bareSidecar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareStringDecoder.js",
|
||||
"keys": [
|
||||
@@ -596,9 +584,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareStorage.js",
|
||||
"path": "/lib/bare/bundles/bareStdio.js",
|
||||
"keys": [
|
||||
"bareStorage"
|
||||
"bareStdio"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -608,15 +596,21 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSvg.js",
|
||||
"path": "/lib/bare/bundles/bareSidecar.js",
|
||||
"keys": [
|
||||
"bareSvg"
|
||||
"bareSidecar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareStructuredClone.js",
|
||||
"path": "/lib/bare/bundles/bareStorage.js",
|
||||
"keys": [
|
||||
"bareStructuredClone"
|
||||
"bareStorage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSvg.js",
|
||||
"keys": [
|
||||
"bareSvg"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -626,9 +620,15 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTap.js",
|
||||
"path": "/lib/bare/bundles/bareStructuredClone.js",
|
||||
"keys": [
|
||||
"bareTap"
|
||||
"bareStructuredClone"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSubprocess.js",
|
||||
"keys": [
|
||||
"bareSubprocess"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -644,15 +644,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTcp.js",
|
||||
"path": "/lib/bare/bundles/bareTap.js",
|
||||
"keys": [
|
||||
"bareTcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareSubprocess.js",
|
||||
"keys": [
|
||||
"bareSubprocess"
|
||||
"bareTap"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -661,24 +655,30 @@
|
||||
"bareTpl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTcp.js",
|
||||
"keys": [
|
||||
"bareTcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareType.js",
|
||||
"keys": [
|
||||
"bareType"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareUiKit.js",
|
||||
"keys": [
|
||||
"bareUiKit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTls.js",
|
||||
"keys": [
|
||||
"bareTls"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareUiKit.js",
|
||||
"keys": [
|
||||
"bareUiKit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareTty.js",
|
||||
"keys": [
|
||||
@@ -691,12 +691,6 @@
|
||||
"bareUnpack"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareThread.js",
|
||||
"keys": [
|
||||
"bareThread"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareV8.js",
|
||||
"keys": [
|
||||
@@ -721,6 +715,18 @@
|
||||
"bareUnionBundle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareV8ToIstanbul.js",
|
||||
"keys": [
|
||||
"bareV8ToIstanbul"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareThread.js",
|
||||
"keys": [
|
||||
"bareThread"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWebKit.js",
|
||||
"keys": [
|
||||
@@ -733,18 +739,6 @@
|
||||
"bareUtils"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWebp.js",
|
||||
"keys": [
|
||||
"bareWebp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareV8ToIstanbul.js",
|
||||
"keys": [
|
||||
"bareV8ToIstanbul"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareWebKitGtk.js",
|
||||
"keys": [
|
||||
@@ -752,9 +746,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareXdiff.js",
|
||||
"path": "/lib/bare/bundles/bareWebp.js",
|
||||
"keys": [
|
||||
"bareXdiff"
|
||||
"bareWebp"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -764,9 +758,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareZlib.js",
|
||||
"path": "/lib/bare/bundles/bareXdiff.js",
|
||||
"keys": [
|
||||
"bareZlib"
|
||||
"bareXdiff"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -793,6 +787,12 @@
|
||||
"bareWorker"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/bareZlib.js",
|
||||
"keys": [
|
||||
"bareZlib"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/lib/bare/bundles/holesail.js",
|
||||
"keys": [
|
||||
@@ -1740,8 +1740,8 @@
|
||||
],
|
||||
"bundleProvenance": {
|
||||
"schemaVersion": 1,
|
||||
"generatedAt": "2026-04-27T02:27:44.588Z",
|
||||
"gitCommit": "a553a4e4a7bab6aa44856f16f510da501209bf07",
|
||||
"generatedAt": "2026-04-27T04:38:37.185Z",
|
||||
"gitCommit": "5647491b08d27ebbbfbbf93fb63bc4a9504b5c98",
|
||||
"nodeVersion": "v20.20.2",
|
||||
"bundleTier": "all",
|
||||
"normativeManifest": "packages/bare-os-booter/lib/bare-module-manifest.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1777258500725,
|
||||
"atMs": 1777264715669,
|
||||
"commands": [
|
||||
"agent",
|
||||
"appctl",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user