/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */ /** Shared helpers for drive-resident /bin scripts (prepended before each command). */ function bareStdin(ctx) { return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : '' } /** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */ function bareFormatModeString(mode, type) { const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-' const perm = mode & 0o777 const r = (bit) => (perm & bit ? 'r' : '-') const w = (bit) => (perm & bit ? 'w' : '-') const x = (bit) => (perm & bit ? 'x' : '-') return ( typeChar + r(0o400) + w(0o200) + x(0o100) + r(0o040) + w(0o020) + x(0o010) + r(0o004) + w(0o002) + x(0o001) ) } /** @param {number} mtimeMs @param {number} [nowMs] */ function bareFormatLsMtime(mtimeMs, nowMs) { const now = nowMs != null ? nowMs : Date.now() const d = new Date(mtimeMs) const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] const mon = months[d.getMonth()] const day = String(d.getDate()).padStart(2, ' ') const sixMo = 180 * 24 * 3600 * 1000 if (Math.abs(now - mtimeMs) > sixMo) { const yr = String(d.getFullYear()).padStart(4, ' ') return mon + ' ' + day + ' ' + yr } const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return mon + ' ' + day + ' ' + hh + ':' + mm } /** @param {number} size */ function barePosixBlocks(size) { return Math.ceil(Number(size) / 512) || 0 } /** * Raw stdout for NUL/binary when **`process.stdout.write`** is missing. * If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it. * @param {Record} ctx * @param {string | Uint8Array} chunk * @returns {boolean} */ function bareOsEmitRaw(ctx, chunk) { if (typeof ctx.bareOsBinWrite === 'function') { const b4 = ctx.b4a const u8 = typeof chunk === 'string' ? b4 && typeof b4.from === 'function' ? b4.from(chunk) : new TextEncoder().encode(chunk) : chunk ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8)) return true } const w = globalThis.process?.stdout?.write if (typeof w === 'function') { w.call(globalThis.process.stdout, chunk) return true } return false } /** * POSIX-oriented sed engine for Bare OS /bin/sed (no import; concatenated before src/sed.js). * Covers: -n -e -f -E, addresses (#,$,/re/,n,m,n~s), s///[ngp0-9], y///, d D p P n N, * h H g G x, b t :, q, r w, =, l, a i c (backslash forms), comments, hold space, line continuations. */ /** * @param {string} delim * @param {string} script * @param {number} start * @returns {{ raw: string, end: number } | null} */ function bareSedReadDelimited(delim, script, start) { if (delim === '\n' || delim === '') return null let i = start let out = '' while (i < script.length) { const c = script[i] if (c === '\\' && i + 1 < script.length) { out += script[i + 1] i += 2 continue } if (c === delim) return { raw: out, end: i + 1 } out += c i++ } return null } /** * @param {string} reStr * @param {boolean} extended */ function bareSedCompileRegex(reStr, extended) { let flags = extended ? 'u' : 'u' let body = reStr if (extended) { body = body .replace(/\(\?#[^)]*\)/g, '') .replace(/\(\?:/g, '(') .replace(/\+/g, '{1,}') .replace(/\?/g, '{0,1}') } try { return new RegExp(body, flags) } catch { return /$^/ } } /** * @param {string} repl * @param {string[]} caps * @param {string} match * @param {string} line * @param {number} off */ function bareSedApplyReplacement(repl, caps, match, line, off) { let o = '' for (let i = 0; i < repl.length; i++) { const c = repl[i] if (c === '&') { o += match continue } if (c === '\\' && i + 1 < repl.length) { const n = repl[i + 1] if (n >= '1' && n <= '9') { o += caps[Number(n)] || '' i++ continue } if (n === '&') { o += '&' i++ continue } if (n === '\\') { o += '\\' i++ continue } o += n i++ continue } o += c } return o } function bareSedListLine(s) { let o = '' for (let i = 0; i < s.length; i++) { const code = s.charCodeAt(i) if (code === 10) o += '\\n' else if (code === 9) o += '\\t' else if (code === 92) o += '\\\\' else if (code < 32 || code > 126) o += '\\' + code.toString(8).padStart(3, '0') else o += s[i] } return o + '$' } /** * @typedef {{ type: string, [k: string]: unknown }} BareSedCmd */ /** * @param {string} script * @param {boolean} extended * @returns {BareSedCmd[]} */ function bareSedParseScript(script, extended) { /** @type {BareSedCmd[]} */ const cmds = [] let i = 0 const len = script.length function skipWs() { while (i < len && /[ \t\r]/.test(script[i])) i++ } function skipCommentLine() { while (i < len && script[i] !== '\n') i++ if (i < len && script[i] === '\n') i++ } function readAddr() { skipWs() if (i >= len) return null const c = script[i] if (c === '#') { skipCommentLine() return 'skip' } if (c === '0' && script[i + 1] >= '1' && script[i + 1] <= '9') { /* fall through to number */ } else if (c >= '1' && c <= '9') { let n = 0 while (i < len && script[i] >= '0' && script[i] <= '9') { n = n * 10 + (script[i].charCodeAt(0) - 48) i++ } return { kind: 'num', n } } if (c === '$') { i++ return { kind: 'last' } } if (c === '/' || c === '\\') { let delim = c let start = i + 1 if (c === '\\') { delim = script[i + 1] || '/' start = i + 2 } const got = bareSedReadDelimited(delim, script, start) if (!got) throw new Error('sed: unterminated address regex') i = got.end return { kind: 're', rx: bareSedCompileRegex(got.raw, extended) } } return null } function readAddrPair() { const a = readAddr() if (a === 'skip' || a === null) return a skipWs() if (i < len && script[i] === ',') { i++ const b = readAddr() if (b === 'skip' || b === null) throw new Error('sed: invalid address') return { kind: 'range', a, b } } if (i < len && script[i] === '~') { i++ let step = 0 while (i < len && script[i] >= '0' && script[i] <= '9') { step = step * 10 + (script[i].charCodeAt(0) - 48) i++ } if (step < 1) step = 1 return { kind: 'step', a, step } } return a } while (i < len) { skipWs() if (i >= len) break if (script[i] === '#' || (script[i] === '\n' && (i++, false))) { if (script[i - 1] === '#') skipCommentLine() else continue continue } if (script[i] === ';') { i++ continue } if (script[i] === '\n') { i++ continue } let neg = false if (script[i] === '!') { neg = true i++ skipWs() } const addr1 = readAddrPair() if (addr1 === 'skip') continue skipWs() if (i >= len) break const ch = script[i] if (ch === '#') { skipCommentLine() continue } if (ch === ':') { i++ let lab = '' while (i < len && /[A-Za-z0-9_]/.test(script[i])) lab += script[i++] cmds.push({ type: 'label', name: lab, neg, addr: addr1 }) continue } if (ch === 'b' || ch === 't') { const ty = ch i++ skipWs() let lab = '' while (i < len && /[A-Za-z0-9_]/.test(script[i])) lab += script[i++] cmds.push({ type: ty, label: lab, neg, addr: addr1 }) continue } if (ch === 'r' || ch === 'w') { const ty = ch i++ skipWs() let path = '' while (i < len && script[i] !== '\n' && script[i] !== ';') path += script[i++] path = path.replace(/[ \t]+$/, '') cmds.push({ type: ty === 'r' ? 'readFile' : 'writeFile', path, neg, addr: addr1 }) continue } if (ch === 'a' || ch === 'i' || ch === 'c') { const ty = ch i++ skipWs() if (i < len && script[i] === '\\') i++ let text = '' while (i < len && script[i] !== '\n') text += script[i++] if (i < len && script[i] === '\n') i++ while (i < len && script[i] === '\\') { i++ let cont = '' while (i < len && script[i] !== '\n') cont += script[i++] text += '\n' + cont if (i < len && script[i] === '\n') i++ } cmds.push({ type: ty === 'a' ? 'append' : ty === 'i' ? 'insert' : 'change', text, neg, addr: addr1 }) continue } if (ch === 's') { i++ const delim = script[i++] const pat = bareSedReadDelimited(delim, script, i) if (!pat) throw new Error('sed: unterminated s command') i = pat.end const rep = bareSedReadDelimited(delim, script, i) if (!rep) throw new Error('sed: unterminated s replacement') i = rep.end /** @type {{ g?: boolean, p?: boolean, n?: number }} */ const fl = {} while (i < len && /[gpn0-9]/.test(script[i])) { const f = script[i++] if (f === 'g') fl.g = true else if (f === 'p') fl.p = true else if (f >= '1' && f <= '9') fl.n = Number(f) } cmds.push({ type: 'subst', rx: bareSedCompileRegex(pat.raw, extended), rep: rep.raw, flags: fl, neg, addr: addr1 }) continue } if (ch === 'y') { i++ const delim = script[i++] const from = bareSedReadDelimited(delim, script, i) if (!from) throw new Error('sed: unterminated y') i = from.end const to = bareSedReadDelimited(delim, script, i) if (!to) throw new Error('sed: unterminated y') i = to.end if (from.raw.length !== to.raw.length) throw new Error('sed: y strings must be same length') cmds.push({ type: 'y', from: from.raw, to: to.raw, neg, addr: addr1 }) continue } const map = { d: 'del', D: 'delFirst', p: 'print', P: 'printFirst', n: 'nextLine', N: 'appendNext', h: 'hold', H: 'holdAppend', g: 'get', G: 'getAppend', x: 'swap', q: 'quit', l: 'list', '=': 'lineNum' } const ty = map[ch] if (ty) { i++ let count = 1 if (ty === 'quit' && i < len && script[i] >= '0' && script[i] <= '9') { count = 0 while (i < len && script[i] >= '0' && script[i] <= '9') { count = count * 10 + (script[i].charCodeAt(0) - 48) i++ } } cmds.push({ type: ty, neg, addr: addr1, quitCode: count }) continue } throw new Error('sed: unknown command: ' + ch) } return cmds } /** * @param {unknown} addr * @param {number} lineNo * @param {number} lastLine * @param {string} ps */ function bareSedAddrSimple(addr, lineNo, lastLine, ps) { if (addr == null) return true if (typeof addr === 'object' && addr.kind === 'num') return lineNo === addr.n if (typeof addr === 'object' && addr.kind === 'last') return lineNo === lastLine if (typeof addr === 'object' && addr.kind === 're') { addr.rx.lastIndex = 0 return addr.rx.test(ps) } if (typeof addr === 'object' && addr.kind === 'step') { const an = addr.a if (typeof an === 'object' && an.kind === 'num') { return lineNo >= an.n && (lineNo - an.n) % addr.step === 0 } return false } return false } /** * @param {unknown} addr * @param {number} lineNo * @param {number} lastLine * @param {string} ps * @param {Map} rangeStates * @param {number} cmdIndex */ function bareSedAddrMatchFull( addr, lineNo, lastLine, ps, rangeStates, cmdIndex ) { if (addr == null) return true if (typeof addr === 'object' && addr.kind === 'range') { const a = addr.a const b = addr.b if ( typeof a === 'object' && a.kind === 'num' && typeof b === 'object' && b.kind === 'num' ) { return lineNo >= a.n && lineNo <= b.n } if ( typeof a === 'object' && a.kind === 'num' && typeof b === 'object' && b.kind === 'last' ) { return lineNo >= a.n && lineNo <= lastLine } if ( typeof a === 'object' && a.kind === 'last' && typeof b === 'object' && b.kind === 'num' ) { return lineNo >= lastLine && lineNo <= b.n } if ( typeof a === 'object' && a.kind === 're' && typeof b === 'object' && b.kind === 're' ) { let st = rangeStates.get(cmdIndex) if (!st) { st = { active: false } rangeStates.set(cmdIndex, st) } a.rx.lastIndex = 0 b.rx.lastIndex = 0 const hitA = a.rx.test(ps) const hitB = b.rx.test(ps) if (!st.active && hitA) st.active = true const inRange = st.active if (st.active && hitB) st.active = false return inRange } if ( typeof a === 'object' && a.kind === 'num' && typeof b === 'object' && b.kind === 're' ) { let st = rangeStates.get(cmdIndex) if (!st) { st = { active: false } rangeStates.set(cmdIndex, st) } if (lineNo === a.n) st.active = true b.rx.lastIndex = 0 const hitB = b.rx.test(ps) const inRange = st.active if (st.active && hitB) st.active = false return inRange } if ( typeof a === 'object' && a.kind === 're' && typeof b === 'object' && b.kind === 'num' ) { let st = rangeStates.get(cmdIndex) if (!st) { st = { active: false } rangeStates.set(cmdIndex, st) } a.rx.lastIndex = 0 if (!st.active && a.rx.test(ps)) st.active = true const inRange = st.active if (st.active && lineNo >= b.n) st.active = false return inRange } return false } return bareSedAddrSimple(addr, lineNo, lastLine, ps) } function bareSedMatchAddr( addr, neg, lineNo, lastLine, ps, rangeStates, cmdIndex ) { const m = bareSedAddrMatchFull( addr, lineNo, lastLine, ps, rangeStates, cmdIndex ) return neg ? !m : m } /** * @param {string[]} lines * @param {string[]} scripts * @param {{ silent?: boolean, extended?: boolean, nullData?: boolean, readFile?: (p: string) => string | null, writeFile?: (p: string, chunk: string) => void, lastLineHint?: number }} opts * @returns {string} */ function bareSedRun(lines, scripts, opts) { const silent = !!opts.silent const extended = !!opts.extended const eol = opts.nullData ? '\0' : '\n' const readF = opts.readFile || (() => null) const writeF = opts.writeFile || (() => {}) const fullScript = scripts.join('\n') const cmds = bareSedParseScript(fullScript.replace(/\\\n/g, ''), extended) /** @type {Record} */ const labels = {} for (let ci = 0; ci < cmds.length; ci++) { if (cmds[ci].type === 'label') labels[/** @type {string} */ (cmds[ci].name)] = ci } const lastLine = opts.lastLineHint != null ? opts.lastLineHint : lines.length /** @type {string[]} */ const out = [] let hold = '' let quit = 0 let lastSubst = false /** @type {Map} */ const rangeStates = new Map() function emit(s) { out.push(s) } let lineIdx = 0 while (lineIdx < lines.length && quit === 0) { let ps = lines[lineIdx] const lineNo = lineIdx + 1 let autoPrint = !silent let delLine = false let nextRead = false let ci = 0 while (ci < cmds.length && quit === 0) { const cmd = cmds[ci] if (cmd.type === 'label') { ci++ continue } const addr = cmd.addr if ( !bareSedMatchAddr( addr, !!cmd.neg, lineNo, lastLine, ps, rangeStates, ci ) ) { ci++ continue } switch (cmd.type) { case 'subst': { lastSubst = false const rx = /** @type {RegExp} */ (cmd.rx) const rep = /** @type {string} */ (cmd.rep) const fl = /** @type {{ g?: boolean, p?: boolean, n?: number }} */ ( cmd.flags ) let count = 0 let res = '' let pos = 0 const g = !!fl.g const wantN = fl.n != null ? fl.n : g ? Infinity : 1 let replCount = 0 rx.lastIndex = 0 let m const str = ps while ((m = rx.exec(str)) && replCount < wantN) { res += str.slice(pos, m.index) const caps = m.map((x) => (x == null ? '' : String(x))) res += bareSedApplyReplacement(rep, caps, m[0], str, m.index) pos = m.index + m[0].length count++ replCount++ lastSubst = true if (!g) break if (m[0].length === 0) { rx.lastIndex++ if (rx.lastIndex > str.length) break } } if (count) { ps = res + str.slice(pos) if (fl.p) emit(ps + eol) } break } case 'y': { const from = /** @type {string} */ (cmd.from) const to = /** @type {string} */ (cmd.to) const map = {} for (let j = 0; j < from.length; j++) map[from[j]] = to[j] let ns = '' for (let j = 0; j < ps.length; j++) ns += map[ps[j]] != null ? map[ps[j]] : ps[j] ps = ns break } case 'del': delLine = true autoPrint = false break case 'delFirst': { const nl = ps.indexOf('\n') if (nl === -1) { delLine = true autoPrint = false } else ps = ps.slice(nl + 1) ci = -1 break } case 'print': emit(ps + eol) break case 'printFirst': { const nl = ps.indexOf('\n') emit((nl === -1 ? ps : ps.slice(0, nl)) + eol) break } case 'nextLine': if (autoPrint && !silent) emit(ps + eol) lineIdx++ nextRead = true ci = cmds.length break case 'appendNext': lineIdx++ if (lineIdx < lines.length) ps += '\n' + lines[lineIdx] else delLine = true break case 'hold': hold = ps break case 'holdAppend': hold += (hold ? '\n' : '') + ps break case 'get': ps = hold break case 'getAppend': ps += '\n' + hold break case 'swap': { const t = ps ps = hold hold = t break } case 'quit': if (autoPrint && !silent) emit(ps + eol) quit = /** @type {number} */ (cmd.quitCode) || 0 break case 'list': emit(bareSedListLine(ps) + eol) break case 'lineNum': emit(String(lineNo) + eol) break case 'readFile': { const text = readF(/** @type {string} */ (cmd.path)) if (text) emit(text.endsWith(eol) ? text : text + eol) break } case 'writeFile': writeF(/** @type {string} */ (cmd.path), ps + '\n') break case 'append': emit(/** @type {string} */ (cmd.text) + eol) break case 'insert': /* handled as emit before line — approximated by prepending to output before autoPrint */ out.push(/** @type {string} */ (cmd.text) + '\n') break case 'change': autoPrint = false emit(/** @type {string} */ (cmd.text) + eol) delLine = true break case 'b': { const lab = /** @type {string} */ (cmd.label) if (lab && labels[lab] != null) ci = labels[lab] break } case 't': { if (lastSubst) { const lab = /** @type {string} */ (cmd.label) if (lab && labels[lab] != null) ci = labels[lab] lastSubst = false } break } default: break } ci++ if (delLine) break if (nextRead) break } if (quit) break if (nextRead) continue if (!delLine && autoPrint) emit(ps + eol) lineIdx++ } return out.join('') } async function run(ctx, argv) { let silent = false let extended = false let nullData = false /** @type {string[]} */ const scripts = [] /** @type {string[]} */ const files = [] for (let i = 1; i < argv.length; i++) { const a = argv[i] if (a === '-n' || a === '--quiet' || a === '--silent') { silent = true continue } if (a === '-z' || a === '--null-data') { nullData = true continue } if (a === '-E' || a === '-r') { extended = true continue } if (a === '-e') { scripts.push(argv[++i] || '') continue } if (a === '-f') { const path = argv[++i] if (!path) { ctx.console.error('sed: -f requires a file') ctx.exitCode = 1 return } const buf = await ctx.vfs.readFile(path) if (!buf) { ctx.console.error('sed: cannot read ' + path) ctx.exitCode = 1 return } scripts.push(ctx.b4a.toString(buf)) continue } if (a === '--') { files.push(...argv.slice(i + 1)) break } if (a.startsWith('-') && a.length > 1) { ctx.console.error('sed: unsupported option ' + a) ctx.exitCode = 1 return } if (!scripts.length) { scripts.push(a) files.push(...argv.slice(i + 1)) } else { files.push(...argv.slice(i)) } break } if (!scripts.length) { ctx.console.error('usage: sed [-n] [-E] {-e script | -f file} [file...]') ctx.exitCode = 1 return } function bareSedCollectRPaths(scList) { const set = new Set() const s = scList.join('\n') const m0 = /^\s*r\s+([^\n;]+)/.exec(s) if (m0) set.add(m0[1].trim()) let i = 0 while (i < s.length) { const j = s.indexOf('\nr', i) const k = s.indexOf(';r', i) let hit = -1 if (j >= 0 && (k < 0 || j <= k)) hit = j + 1 else if (k >= 0) hit = k + 1 if (hit < 0) break let p = hit + 1 while (p < s.length && /[ \t]/.test(s[p])) p++ let end = p while (end < s.length && s[end] !== '\n' && s[end] !== ';') end++ const path = s.slice(p, end).trim() if (path) set.add(path) i = end } return [...set] } /** @type {Record} */ const readCache = Object.create(null) for (const rp of bareSedCollectRPaths(scripts)) { const buf = await ctx.vfs.readFile(rp) readCache[rp] = buf ? ctx.b4a.toString(buf) : '' } const maxNull = Number.parseInt( String(ctx.vfs?.env?.BARE_OS_SED_NULL_MAX_RECORDS || '100000'), 10 ) || 100000 /** @type {string[]} */ const lines = [] async function pushFile(path) { const buf = await ctx.vfs.readFile(path) if (!buf) { ctx.console.error('sed: ' + path + ': No such file') ctx.exitCode = 1 return false } const t = ctx.b4a.toString(buf) if (nullData) { const rec = t.split('\0') const room = maxNull - lines.length lines.push(...rec.slice(0, Math.max(0, room))) } else { const ls = t.split(/\r?\n/) if (ls.length && ls[ls.length - 1] === '') ls.pop() lines.push(...ls) } return true } if (!files.length) { const s = bareStdin(ctx) const ls = nullData ? s.split('\0').slice(0, maxNull) : s.split(/\r?\n/) if (!nullData && ls.length && ls[ls.length - 1] === '') ls.pop() lines.push(...ls) } else { for (const f of files) { if (!(await pushFile(f))) return } } /** @type {Record} */ const wAccum = Object.create(null) const out = bareSedRun(lines, scripts, { silent, extended, nullData, readFile: (p) => readCache[p] ?? null, writeFile: (p, chunk) => { wAccum[p] = (wAccum[p] || '') + chunk }, lastLineHint: lines.length }) for (const [p, data] of Object.entries(wAccum)) { try { const prev = await ctx.vfs.readFile(p) const merged = prev ? ctx.b4a.concat([prev, ctx.b4a.from(data)]) : ctx.b4a.from(data) await ctx.vfs.writeFile(p, merged) } catch (e) { ctx.console.error('sed: ' + p + ': ' + ((e && e.message) || e)) ctx.exitCode = 1 } } const trail = nullData ? /\0$/ : /\n$/ const t = out.replace(trail, '') ctx.console.log(t) }