Files
bare-operating-system/packages/bare-os-coreutils/lib/sed-engine.js
T
2026-04-03 23:42:47 -04:00

736 lines
18 KiB
JavaScript

/**
* 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('')
}