991 lines
25 KiB
Plaintext
991 lines
25 KiB
Plaintext
/* 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<string, unknown>} 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<number, { active: boolean }>} rangeStates
|
|
* @param {number} cmdIndex
|
|
*/
|
|
function bareSedAddrMatchFull(
|
|
addr,
|
|
lineNo,
|
|
lastLine,
|
|
ps,
|
|
rangeStates,
|
|
cmdIndex
|
|
) {
|
|
if (addr == null) return true
|
|
if (typeof addr === 'object' && addr.kind === 'range') {
|
|
const a = addr.a
|
|
const b = addr.b
|
|
if (
|
|
typeof a === 'object' &&
|
|
a.kind === 'num' &&
|
|
typeof b === 'object' &&
|
|
b.kind === 'num'
|
|
) {
|
|
return lineNo >= a.n && lineNo <= b.n
|
|
}
|
|
if (
|
|
typeof a === 'object' &&
|
|
a.kind === 'num' &&
|
|
typeof b === 'object' &&
|
|
b.kind === 'last'
|
|
) {
|
|
return lineNo >= a.n && lineNo <= lastLine
|
|
}
|
|
if (
|
|
typeof a === 'object' &&
|
|
a.kind === 'last' &&
|
|
typeof b === 'object' &&
|
|
b.kind === 'num'
|
|
) {
|
|
return lineNo >= lastLine && lineNo <= b.n
|
|
}
|
|
if (
|
|
typeof a === 'object' &&
|
|
a.kind === 're' &&
|
|
typeof b === 'object' &&
|
|
b.kind === 're'
|
|
) {
|
|
let st = rangeStates.get(cmdIndex)
|
|
if (!st) {
|
|
st = { active: false }
|
|
rangeStates.set(cmdIndex, st)
|
|
}
|
|
a.rx.lastIndex = 0
|
|
b.rx.lastIndex = 0
|
|
const hitA = a.rx.test(ps)
|
|
const hitB = b.rx.test(ps)
|
|
if (!st.active && hitA) st.active = true
|
|
const inRange = st.active
|
|
if (st.active && hitB) st.active = false
|
|
return inRange
|
|
}
|
|
if (
|
|
typeof a === 'object' &&
|
|
a.kind === 'num' &&
|
|
typeof b === 'object' &&
|
|
b.kind === 're'
|
|
) {
|
|
let st = rangeStates.get(cmdIndex)
|
|
if (!st) {
|
|
st = { active: false }
|
|
rangeStates.set(cmdIndex, st)
|
|
}
|
|
if (lineNo === a.n) st.active = true
|
|
b.rx.lastIndex = 0
|
|
const hitB = b.rx.test(ps)
|
|
const inRange = st.active
|
|
if (st.active && hitB) st.active = false
|
|
return inRange
|
|
}
|
|
if (
|
|
typeof a === 'object' &&
|
|
a.kind === 're' &&
|
|
typeof b === 'object' &&
|
|
b.kind === 'num'
|
|
) {
|
|
let st = rangeStates.get(cmdIndex)
|
|
if (!st) {
|
|
st = { active: false }
|
|
rangeStates.set(cmdIndex, st)
|
|
}
|
|
a.rx.lastIndex = 0
|
|
if (!st.active && a.rx.test(ps)) st.active = true
|
|
const inRange = st.active
|
|
if (st.active && lineNo >= b.n) st.active = false
|
|
return inRange
|
|
}
|
|
return false
|
|
}
|
|
return bareSedAddrSimple(addr, lineNo, lastLine, ps)
|
|
}
|
|
|
|
function bareSedMatchAddr(
|
|
addr,
|
|
neg,
|
|
lineNo,
|
|
lastLine,
|
|
ps,
|
|
rangeStates,
|
|
cmdIndex
|
|
) {
|
|
const m = bareSedAddrMatchFull(
|
|
addr,
|
|
lineNo,
|
|
lastLine,
|
|
ps,
|
|
rangeStates,
|
|
cmdIndex
|
|
)
|
|
return neg ? !m : m
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} lines
|
|
* @param {string[]} scripts
|
|
* @param {{ silent?: boolean, extended?: boolean, 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<string, number>} */
|
|
const labels = {}
|
|
for (let ci = 0; ci < cmds.length; ci++) {
|
|
if (cmds[ci].type === 'label')
|
|
labels[/** @type {string} */ (cmds[ci].name)] = ci
|
|
}
|
|
|
|
const lastLine = opts.lastLineHint != null ? opts.lastLineHint : lines.length
|
|
/** @type {string[]} */
|
|
const out = []
|
|
let hold = ''
|
|
let quit = 0
|
|
let lastSubst = false
|
|
/** @type {Map<number, { active: boolean }>} */
|
|
const rangeStates = new Map()
|
|
|
|
function emit(s) {
|
|
out.push(s)
|
|
}
|
|
|
|
let lineIdx = 0
|
|
while (lineIdx < lines.length && quit === 0) {
|
|
let ps = lines[lineIdx]
|
|
const lineNo = lineIdx + 1
|
|
let autoPrint = !silent
|
|
let delLine = false
|
|
let nextRead = false
|
|
let ci = 0
|
|
|
|
while (ci < cmds.length && quit === 0) {
|
|
const cmd = cmds[ci]
|
|
if (cmd.type === 'label') {
|
|
ci++
|
|
continue
|
|
}
|
|
const addr = cmd.addr
|
|
if (
|
|
!bareSedMatchAddr(
|
|
addr,
|
|
!!cmd.neg,
|
|
lineNo,
|
|
lastLine,
|
|
ps,
|
|
rangeStates,
|
|
ci
|
|
)
|
|
) {
|
|
ci++
|
|
continue
|
|
}
|
|
|
|
switch (cmd.type) {
|
|
case 'subst': {
|
|
lastSubst = false
|
|
const rx = /** @type {RegExp} */ (cmd.rx)
|
|
const rep = /** @type {string} */ (cmd.rep)
|
|
const fl = /** @type {{ g?: boolean, p?: boolean, n?: number }} */ (
|
|
cmd.flags
|
|
)
|
|
let count = 0
|
|
let res = ''
|
|
let pos = 0
|
|
const g = !!fl.g
|
|
const wantN = fl.n != null ? fl.n : g ? Infinity : 1
|
|
let replCount = 0
|
|
rx.lastIndex = 0
|
|
let m
|
|
const str = ps
|
|
while ((m = rx.exec(str)) && replCount < wantN) {
|
|
res += str.slice(pos, m.index)
|
|
const caps = m.map((x) => (x == null ? '' : String(x)))
|
|
res += bareSedApplyReplacement(rep, caps, m[0], str, m.index)
|
|
pos = m.index + m[0].length
|
|
count++
|
|
replCount++
|
|
lastSubst = true
|
|
if (!g) break
|
|
if (m[0].length === 0) {
|
|
rx.lastIndex++
|
|
if (rx.lastIndex > str.length) break
|
|
}
|
|
}
|
|
if (count) {
|
|
ps = res + str.slice(pos)
|
|
if (fl.p) emit(ps + 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<string, string>} */
|
|
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<string, string>} */
|
|
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)
|
|
}
|