Files
bare-operating-system/packages/bare-os-coreutils/lib/engines/awk-engine.js
T
2026-08-18 18:11:28 -04:00

1458 lines
39 KiB
JavaScript

/**
* POSIX-oriented awk interpreter for Bare OS (no import; concatenated before src/awk.js).
* Supports: BEGIN/END, /re/, line patterns, {}, print/printf, if/else, while, for(;;), for(x in a),
* delete arr[idx], next, exit, break, continue, ++/--, arrays, strings, regex ~ !~, builtins (length, substr, index,
* split, sprintf, sub, gsub, match, int, atan2, cos, sin, exp, log, sqrt, tolower, toupper, rand, srand),
* getline from stdin, files (getline x < \"path\"), unary minus,
* optional fixed-width fields via FIELDWIDTHS when BARE_OS_AWK_FIELDWIDTHS is enabled,
* -F FS, NF, NR, FNR, $0..$n, OFS, ORS, RS, ARGC, ARGV, FILENAME, ENVIRON (read-only mirror).
* GNU-style empty fields: consecutive FS delimiters still advance $1..$NF where the grammar allows.
*/
function bareAwkError(msg) {
const e = new Error('awk: ' + msg)
throw e
}
/** @typedef {{ t: string, v?: unknown, a?: unknown, b?: unknown, c?: unknown, args?: unknown[], name?: string, op?: string }} BareAwkNode */
/**
* @param {string} src
* @param {{ line: number, i: number }} st
*/
function bareAwkSkipWs(src, st) {
while (st.i < src.length && /[ \t\r]/.test(src[st.i])) st.i++
}
/**
* @param {string} src
* @param {{ line: number, i: number }} st
*/
function bareAwkLex(src, st) {
bareAwkSkipWs(src, st)
if (st.i >= src.length) return { kind: 'EOF', line: st.line }
const c = src[st.i]
const line = st.line
if (c === '\n') {
st.i++
st.line++
return { kind: 'NL', line }
}
if (c === '#') {
while (st.i < src.length && src[st.i] !== '\n') st.i++
return bareAwkLex(src, st)
}
if (c === '"' || c === "'") {
const q = c
st.i++
let s = ''
while (st.i < src.length) {
const ch = src[st.i]
if (ch === q) {
st.i++
return { kind: 'STRING', value: s, line }
}
if (ch === '\\' && st.i + 1 < src.length) {
st.i++
const e = src[st.i++]
if (e === 'n') s += '\n'
else if (e === 't') s += '\t'
else if (e === 'r') s += '\r'
else s += e
continue
}
if (ch === '\n') bareAwkError('newline in string at line ' + line)
s += ch
st.i++
}
bareAwkError('unterminated string')
}
if (c >= '0' && c <= '9') {
let n = ''
while (st.i < src.length && /[0-9.]/.test(src[st.i])) n += src[st.i++]
return { kind: 'NUMBER', value: Number(n), line }
}
if (c === '>' && src[st.i + 1] === '>') {
st.i += 2
return { kind: 'OP', value: '>>', line }
}
const two = src.slice(st.i, st.i + 2)
if (
two === '<=' ||
two === '>=' ||
two === '==' ||
two === '!=' ||
two === '++' ||
two === '--' ||
two === '+=' ||
two === '-=' ||
two === '*=' ||
two === '/=' ||
two === '%=' ||
two === '~!' ||
two === '&&' ||
two === '||'
) {
if (two === '~!') {
/* not used */
}
if (
two === '&&' ||
two === '||' ||
two === '==' ||
two === '!=' ||
two === '<=' ||
two === '>='
) {
st.i += 2
return { kind: 'OP', value: two, line }
}
if (two === '++' || two === '--') {
st.i += 2
return { kind: 'OP', value: two, line }
}
if (/^[\+\-\*\/%]=/.test(two)) {
st.i += 2
return { kind: 'OP', value: two, line }
}
}
if (c === '!' && src[st.i + 1] === '~') {
st.i += 2
return { kind: 'OP', value: '!~', line }
}
if (c === '=' && src[st.i + 1] === '=') {
st.i += 2
return { kind: 'OP', value: '==', line }
}
if ('+-*/%(){}[],;?:<>=!~^$'.includes(c)) {
st.i++
return { kind: 'OP', value: c, line }
}
if (/[A-Za-z_]/.test(c)) {
let id = ''
while (st.i < src.length && /[A-Za-z0-9_]/.test(src[st.i]))
id += src[st.i++]
if (
id === 'BEGIN' ||
id === 'END' ||
id === 'if' ||
id === 'else' ||
id === 'while' ||
id === 'for' ||
id === 'in' ||
id === 'do' ||
id === 'break' ||
id === 'continue' ||
id === 'next' ||
id === 'nextfile' ||
id === 'exit' ||
id === 'print' ||
id === 'printf' ||
id === 'return' ||
id === 'function' ||
id === 'getline' ||
id === 'delete'
) {
return { kind: 'KW', value: id, line }
}
return { kind: 'ID', value: id, line }
}
if (c === '/') {
st.i++
let pat = ''
while (st.i < src.length) {
const ch = src[st.i]
if (ch === '/') {
st.i++
let flags = ''
while (st.i < src.length && /[igmsuy]/.test(src[st.i]))
flags += src[st.i++]
return { kind: 'REGEX', value: pat, flags, line }
}
if (ch === '\\' && st.i + 1 < src.length) {
pat += ch + src[st.i + 1]
st.i += 2
continue
}
if (ch === '\n') bareAwkError('newline in regex')
pat += ch
st.i++
}
bareAwkError('unterminated regex')
}
bareAwkError('bad char ' + c + ' at ' + line)
}
/**
* @param {string} src
*/
function bareAwkTokenize(src) {
const st = { line: 1, i: 0 }
/** @type {ReturnType<typeof bareAwkLex>[]} */
const toks = []
while (true) {
const t = bareAwkLex(src, st)
toks.push(t)
if (t.kind === 'EOF') break
}
return toks
}
class BareAwkParser {
/**
* @param {ReturnType<typeof bareAwkLex>[]} toks
*/
constructor(toks) {
this.toks = toks
this.p = 0
}
peek() {
return this.toks[this.p] || { kind: 'EOF' }
}
eat(kind, val) {
const t = this.peek()
if (kind && t.kind !== kind)
bareAwkError('expected ' + kind + ' got ' + t.kind)
if (val != null && t.value !== val) bareAwkError('expected ' + val)
this.p++
return t
}
parseProgram() {
/** @type {{ pattern: BareAwkNode | null, stmts: BareAwkNode[] }[]} */
const rules = []
/** @type {{ name: string, params: string[], body: BareAwkNode[] }[]} */
const funcs = []
while (this.peek().kind !== 'EOF') {
if (this.peek().kind === 'NL') {
this.p++
continue
}
if (this.peek().kind === 'KW' && this.peek().value === 'function') {
funcs.push(this.parseFunction())
continue
}
const pat = this.parsePattern()
if (this.peek().value === '{' && this.peek().kind === 'OP') {
this.eat('OP', '{')
const stmts = this.parseStmtList()
this.eat('OP', '}')
rules.push({ pattern: pat, stmts })
} else bareAwkError('expected { after pattern')
}
return { rules, funcs }
}
parseFunction() {
this.eat('KW', 'function')
const name = this.eat('ID').value
this.eat('OP', '(')
/** @type {string[]} */
const params = []
if (this.peek().kind === 'ID') {
params.push(this.eat('ID').value)
while (this.peek().value === ',') {
this.p++
params.push(this.eat('ID').value)
}
}
this.eat('OP', ')')
this.eat('OP', '{')
const body = this.parseStmtList()
this.eat('OP', '}')
return { name, params, body }
}
/** @returns {BareAwkNode | null} */
parsePattern() {
const t = this.peek()
if (t.kind === 'KW' && (t.value === 'BEGIN' || t.value === 'END')) {
this.p++
return { t: 'pat', k: t.value }
}
if (t.kind === 'REGEX') {
this.p++
return { t: 'patRe', rx: bareAwkMakeRx(t.value, t.flags || '') }
}
if (t.kind === 'OP' && t.value === ',') {
bareAwkError('empty pattern')
}
if (t.kind === 'OP' && t.value === '{') {
return null
}
const e = this.parseExpr()
return { t: 'patExpr', e }
}
parseStmtList() {
/** @type {BareAwkNode[]} */
const out = []
while (
this.peek().kind !== 'EOF' &&
!(this.peek().kind === 'OP' && this.peek().value === '}')
) {
if (this.peek().kind === 'NL') {
this.p++
continue
}
out.push(this.parseStmt())
}
return out
}
/** @returns {BareAwkNode} */
parseStmt() {
const t = this.peek()
if (t.kind === 'KW' && t.value === 'if') {
this.p++
this.eat('OP', '(')
const cond = this.parseExpr()
this.eat('OP', ')')
const thenS = this.parseStmt()
let elseS = null
if (this.peek().kind === 'KW' && this.peek().value === 'else') {
this.p++
elseS = this.parseStmt()
}
return { t: 'if', cond, then: thenS, else: elseS }
}
if (t.kind === 'KW' && t.value === 'while') {
this.p++
this.eat('OP', '(')
const cond = this.parseExpr()
this.eat('OP', ')')
const body = this.parseStmt()
return { t: 'while', cond, body }
}
if (t.kind === 'KW' && t.value === 'for') {
this.p++
this.eat('OP', '(')
const i0 = this.peek()
if (
i0.kind === 'ID' &&
this.toks[this.p + 1]?.kind === 'KW' &&
this.toks[this.p + 1]?.value === 'in'
) {
const iv = this.eat('ID').value
this.eat('KW', 'in')
const arr = this.eat('ID').value
this.eat('OP', ')')
const body = this.parseStmt()
return { t: 'forin', iv, arr, body }
}
let init = null
if (!(this.peek().kind === 'OP' && this.peek().value === ';'))
init = this.parseExpr()
this.eat('OP', ';')
let cond = null
if (!(this.peek().kind === 'OP' && this.peek().value === ';'))
cond = this.parseExpr()
this.eat('OP', ';')
let step = null
if (!(this.peek().kind === 'OP' && this.peek().value === ')'))
step = this.parseExpr()
this.eat('OP', ')')
const body = this.parseStmt()
return { t: 'for', init, cond, step, body }
}
if (
t.kind === 'KW' &&
(t.value === 'next' ||
t.value === 'nextfile' ||
t.value === 'break' ||
t.value === 'continue')
) {
this.p++
this.optSemi()
return { t: t.value }
}
if (t.kind === 'KW' && t.value === 'delete') {
this.p++
const arrTok = this.eat('ID')
this.eat('OP', '[')
const key = this.parseExpr()
this.eat('OP', ']')
this.optSemi()
return { t: 'delete', arr: arrTok.value, key }
}
if (t.kind === 'KW' && t.value === 'exit') {
this.p++
let code = null
if (
!(this.peek().kind === 'OP' && this.peek().value === ';') &&
this.peek().kind !== 'NL'
)
code = this.parseExpr()
this.optSemi()
return { t: 'exit', code }
}
if (t.kind === 'KW' && t.value === 'return') {
this.p++
let e = null
if (
!(this.peek().kind === 'OP' && this.peek().value === ';') &&
this.peek().kind !== 'NL'
)
e = this.parseExpr()
this.optSemi()
return { t: 'return', e }
}
if (t.kind === 'KW' && t.value === 'print') {
this.p++
/** @type {BareAwkNode[]} */
const args = []
if (
!(this.peek().kind === 'OP' && this.peek().value === ';') &&
this.peek().kind !== 'NL'
) {
args.push(this.parseExpr())
while (this.peek().value === ',') {
this.p++
args.push(this.parseExpr())
}
}
let redir = null
if (this.peek().kind === 'OP' && this.peek().value === '>') {
this.p++
redir = { op: '>', file: this.parseExpr() }
} else if (this.peek().kind === 'OP' && this.peek().value === '>>') {
this.p++
redir = { op: '>>', file: this.parseExpr() }
}
this.optSemi()
return { t: 'print', args, redir }
}
if (t.kind === 'KW' && t.value === 'printf') {
this.p++
this.eat('OP', '(')
const fmt = this.parseExpr()
/** @type {BareAwkNode[]} */
const args = []
while (this.peek().value === ',') {
this.p++
args.push(this.parseExpr())
}
this.eat('OP', ')')
let redir = null
if (
this.peek().kind === 'OP' &&
(this.peek().value === '>' || this.peek().value === '>>')
) {
const op = this.peek().value
this.p++
redir = { op, file: this.parseExpr() }
}
this.optSemi()
return { t: 'printf', fmt, args, redir }
}
if (t.kind === 'OP' && t.value === '{') {
this.p++
const block = this.parseStmtList()
this.eat('OP', '}')
return { t: 'block', stmts: block }
}
const e = this.parseExpr()
this.optSemi()
return { t: 'expr', e }
}
optSemi() {
if (this.peek().kind === 'OP' && this.peek().value === ';') this.p++
else if (this.peek().kind === 'NL') this.p++
}
parseExpr() {
return this.parseAssign()
}
parseAssign() {
let n = this.parseCond()
const t = this.peek()
if (
t.kind === 'OP' &&
(t.value === '=' ||
t.value === '+=' ||
t.value === '-=' ||
t.value === '*=' ||
t.value === '/=' ||
t.value === '%=')
) {
const op = t.value
this.p++
if (n.t !== 'var' && n.t !== 'field' && n.t !== 'index')
bareAwkError('bad lvalue')
const rhs = this.parseAssign()
return { t: 'assign', op, left: n, right: rhs }
}
return n
}
parseCond() {
let n = this.parseOr()
if (this.peek().kind === 'OP' && this.peek().value === '?') {
this.p++
const a = this.parseExpr()
this.eat('OP', ':')
const b = this.parseExpr()
return { t: '?:', n, a, b }
}
return n
}
parseOr() {
let n = this.parseAnd()
while (this.peek().value === '||') {
this.p++
n = { t: '||', a: n, b: this.parseAnd() }
}
return n
}
parseAnd() {
let n = this.parseMatch()
while (this.peek().value === '&&') {
this.p++
n = { t: '&&', a: n, b: this.parseMatch() }
}
return n
}
parseMatch() {
let n = this.parseCmp()
while (true) {
const t = this.peek()
if (t.value === '~' || t.value === '!~') {
this.p++
const rhs = this.parseCmp()
n = { t: t.value === '~' ? 'match' : 'nmatch', a: n, b: rhs }
} else break
}
return n
}
parseCmp() {
let n = this.parseAdd()
const ops = ['==', '!=', '<', '>', '<=', '>=']
while (
this.peek().kind === 'OP' &&
ops.includes(String(this.peek().value))
) {
const op = String(this.peek().value)
this.p++
n = { t: 'binop', op, a: n, b: this.parseAdd() }
}
return n
}
parseAdd() {
let n = this.parseMul()
while (this.peek().value === '+' || this.peek().value === '-') {
const op = String(this.peek().value)
this.p++
n = { t: 'binop', op, a: n, b: this.parseMul() }
}
return n
}
parseMul() {
let n = this.parseUnary()
while (
this.peek().value === '*' ||
this.peek().value === '/' ||
this.peek().value === '%'
) {
const op = String(this.peek().value)
this.p++
n = { t: 'binop', op, a: n, b: this.parseUnary() }
}
return n
}
parseUnary() {
if (
this.peek().value === '!' ||
this.peek().value === '+' ||
this.peek().value === '-'
) {
const op = String(this.peek().value)
this.p++
return { t: 'unop', op, a: this.parseUnary() }
}
if (this.peek().value === '++' || this.peek().value === '--') {
const op = String(this.peek().value)
this.p++
const x = this.parsePost()
if (x.t !== 'var' && x.t !== 'field') bareAwkError('bad ++')
return { t: 'pre', op, x }
}
return this.parsePost()
}
parsePost() {
let n = this.parsePrimary()
while (this.peek().value === '++' || this.peek().value === '--') {
const op = String(this.peek().value)
this.p++
if (n.t !== 'var' && n.t !== 'field') bareAwkError('bad post')
n = { t: 'post', op, x: n }
}
return n
}
parsePrimary() {
const t = this.peek()
if (t.kind === 'NUMBER') {
this.p++
return { t: 'num', v: t.value }
}
if (t.kind === 'STRING') {
this.p++
return { t: 'str', v: t.value }
}
if (t.kind === 'REGEX') {
this.p++
return { t: 'rxLit', rx: bareAwkMakeRx(t.value, t.flags || '') }
}
if (t.kind === 'KW' && t.value === 'getline') {
this.p++
let varn = null
if (this.peek().kind === 'ID') {
varn = this.eat('ID').value
}
let from = null
if (this.peek().kind === 'OP' && this.peek().value === '<') {
this.p++
from = this.parsePrimary()
}
return { t: 'getline', var: varn, from }
}
if (t.kind === 'OP' && t.value === '$') {
this.p++
return { t: 'field', e: this.parseUnary() }
}
if (t.kind === 'ID') {
const name = t.value
this.p++
if (this.peek().kind === 'OP' && this.peek().value === '(') {
this.p++
/** @type {BareAwkNode[]} */
const args = []
if (!(this.peek().kind === 'OP' && this.peek().value === ')')) {
args.push(this.parseExpr())
while (this.peek().value === ',') {
this.p++
args.push(this.parseExpr())
}
}
this.eat('OP', ')')
return { t: 'call', name, args }
}
if (this.peek().kind === 'OP' && this.peek().value === '[') {
this.p++
const idx = this.parseExpr()
this.eat('OP', ']')
return { t: 'index', name, idx }
}
return { t: 'var', name }
}
if (t.kind === 'OP' && t.value === '(') {
this.p++
const e = this.parseExpr()
this.eat('OP', ')')
return e
}
bareAwkError('unexpected ' + JSON.stringify(t))
}
}
function bareAwkMakeRx(body, flags) {
let f = 'u'
if (flags.includes('i')) f += 'i'
try {
return new RegExp(body, f)
} catch {
return /$^/u
}
}
/**
* @param {string} prog
*/
function bareAwkParse(prog) {
const toks = bareAwkTokenize(prog)
const p = new BareAwkParser(toks)
return p.parseProgram()
}
class BareAwkRuntime {
/**
* @param {{ rules: { pattern: BareAwkNode | null, stmts: BareAwkNode[] }[], funcs: { name: string, params: string[], body: BareAwkNode[] }[] }} ast
* @param {{ argc: number, argv: string[], environ: Record<string, string>, fs: string, ofmt: string }} opts
* @param {{ print(s: string): void, writeFile(path: string, data: string, append?: boolean): Promise<void>, readFile(path: string): Promise<Uint8Array | null>, stdinLines: string[] }} io
*/
constructor(ast, opts, io) {
this.ast = ast
this.opts = opts
this.io = io
this.NR = 0
this.FNR = 0
this.NF = 0
this.$0 = ''
/** @type {string[]} */
this.fields = []
this.FS = opts.fs || ' '
this.OFS = ' '
this.ORS = '\n'
this.RS = '\n'
this.FILENAME = ''
this.ARGC = opts.argc
this.ARGV = opts.argv
/** @type {Record<string, unknown>} */
this.vars = { OFMT: opts.ofmt || '%.6g' }
/** @type {Record<string, Record<string, unknown>>} */
this.arrays = Object.create(null)
/** @type {Record<string, unknown>} */
this.environ = { ...opts.environ }
/** @type {Map<string, { params: string[], body: BareAwkNode[] }>} */
this.funcs = new Map()
for (const f of ast.funcs) this.funcs.set(f.name, f)
this.exitCode = 0
this.exitPending = false
this.randSeed = Date.now() % 100000
this._stdinIx = 0
/** @type {{ path: string, data: string, append: boolean }[]} */
this.pendingWrites = []
/** @type {Map<string, { lines: string[], i: number }>} */
this._getlineFileState = new Map()
}
splitFields() {
const cap = this.environ.BARE_OS_AWK_FIELDWIDTHS
if (cap === '1' || cap === 'true') {
const fw = String(
this.vars.FIELDWIDTHS != null ? this.vars.FIELDWIDTHS : ''
).trim()
if (fw) {
const nums = fw
.split(/\s+/)
.map((x) => Number.parseInt(x, 10))
.filter((n) => Number.isFinite(n) && n > 0)
if (nums.length) {
/** @type {string[]} */
const parts = []
let pos = 0
const line = this.$0
for (const w of nums) {
parts.push(line.slice(pos, pos + w))
pos += w
}
if (pos < line.length) parts.push(line.slice(pos))
this.fields = parts
this.NF = parts.length
this.vars.NF = this.NF
return
}
}
}
const fs = this.FS
let parts
if (fs === ' ') {
parts = this.$0.trim().split(/\s+/).filter(Boolean)
} else if (fs.length === 1) {
parts = this.$0.split(fs)
} else {
try {
const rx = new RegExp(fs, 'u')
parts = this.$0.split(rx)
} catch {
parts = this.$0.split(fs)
}
}
this.fields = parts
this.NF = parts.length
this.vars.NF = this.NF
}
fieldNum(n) {
const i = Math.trunc(Number(n)) || 0
if (i < 0) return ''
if (i === 0) return this.$0
return this.fields[i - 1] != null ? String(this.fields[i - 1]) : ''
}
setField(n, val) {
const i = Math.trunc(Number(n)) || 0
if (i < 1) return
while (this.fields.length < i) this.fields.push('')
this.fields[i - 1] = String(val)
this.$0 = this.fields.join(this.OFS)
this.vars.$0 = this.$0
this.NF = this.fields.length
this.vars.NF = this.NF
}
/**
* @param {BareAwkNode | null} pat
*/
async patternMatch(pat) {
if (pat == null) return true
if (pat.t === 'pat') return false
if (pat.t === 'patRe') {
pat.rx.lastIndex = 0
return pat.rx.test(this.$0)
}
if (pat.t === 'patExpr') return this.truthy(await this.evalExpr(pat.e))
return false
}
truthy(v) {
if (v == null) return false
if (typeof v === 'number') return v !== 0 && !Number.isNaN(v)
if (typeof v === 'string') return v.length > 0
return true
}
/**
* @param {BareAwkNode} n
*/
async evalExpr(n) {
if (!n) return ''
switch (n.t) {
case 'num':
return n.v
case 'str':
return n.v
case 'var': {
const name = n.name
if (name === 'NR') return this.NR
if (name === 'FNR') return this.FNR
if (name === 'NF') return this.NF
if (name === 'FS') return this.FS
if (name === 'OFS') return this.OFS
if (name === 'ORS') return this.ORS
if (name === 'RS') return this.RS
if (name === 'FILENAME') return this.FILENAME
if (name === 'ARGC') return this.ARGC
if (name in this.environ) return this.environ[name]
return this.vars[name] != null ? this.vars[name] : ''
}
case 'field':
return this.fieldNum(await this.evalExpr(n.e))
case 'index': {
const k = String(await this.evalExpr(n.idx))
if (n.name === 'ENVIRON') {
const v = this.environ[k]
return v != null ? v : ''
}
const arr =
this.arrays[n.name] || (this.arrays[n.name] = Object.create(null))
return arr[k] != null ? arr[k] : ''
}
case 'binop': {
const a = await this.evalExpr(n.a)
const b = await this.evalExpr(n.b)
const an = Number(a)
const bn = Number(b)
switch (n.op) {
case '+':
return (
(Number.isFinite(an) ? an : 0) + (Number.isFinite(bn) ? bn : 0)
)
case '-':
return an - bn
case '*':
return an * bn
case '/':
return bn === 0 ? 0 : an / bn
case '%':
return bn === 0 ? 0 : an % bn
case '<':
return String(a) < String(b) ? 1 : 0
case '>':
return String(a) > String(b) ? 1 : 0
case '<=':
return String(a) <= String(b) ? 1 : 0
case '>=':
return String(a) >= String(b) ? 1 : 0
case '==':
return String(a) === String(b) ? 1 : 0
case '!=':
return String(a) !== String(b) ? 1 : 0
default:
return 0
}
}
case 'unop': {
const a = await this.evalExpr(n.a)
if (n.op === '!') return this.truthy(a) ? 0 : 1
if (n.op === '+') return Number(a) || 0
if (n.op === '-') return -(Number(a) || 0)
return 0
}
case '||':
return this.truthy(await this.evalExpr(n.a))
? 1
: this.truthy(await this.evalExpr(n.b))
? 1
: 0
case '&&':
return this.truthy(await this.evalExpr(n.a)) &&
this.truthy(await this.evalExpr(n.b))
? 1
: 0
case 'match': {
const s = String(await this.evalExpr(n.a))
const rhs = n.b
let rx
if (rhs.t === 'rxLit') rx = rhs.rx
else {
const t = String(await this.evalExpr(rhs))
rx = bareAwkMakeRx(t, '')
}
rx.lastIndex = 0
return rx.test(s) ? 1 : 0
}
case 'nmatch': {
const s = String(await this.evalExpr(n.a))
const rhs = n.b
let rx
if (rhs.t === 'rxLit') rx = rhs.rx
else {
const t = String(await this.evalExpr(rhs))
rx = bareAwkMakeRx(t, '')
}
rx.lastIndex = 0
return rx.test(s) ? 0 : 1
}
case '?:':
return this.truthy(await this.evalExpr(n.n))
? await this.evalExpr(n.a)
: await this.evalExpr(n.b)
case 'assign': {
const v = await this.evalExpr(n.right)
await this.assign(n.left, n.op, v)
return v
}
case 'pre': {
const cur = Number(await this.evalExpr(n.x))
const next = n.op === '++' ? cur + 1 : cur - 1
await this.assignScalar(n.x, next)
return next
}
case 'post': {
const cur = Number(await this.evalExpr(n.x))
const next = n.op === '++' ? cur + 1 : cur - 1
await this.assignScalar(n.x, next)
return cur
}
case 'call':
return await this.callBuiltin(n.name, n.args)
case 'getline':
return await this.doGetline(n)
default:
return ''
}
}
async assignScalar(x, v) {
if (x.t === 'var') {
const name = x.name
if (name === 'FS') this.FS = String(v)
else if (name === 'OFS') this.OFS = String(v)
else if (name === 'ORS') this.ORS = String(v)
else if (name === 'RS') this.RS = String(v)
else this.vars[name] = v
} else if (x.t === 'field') {
this.setField(await this.evalExpr(x.e), v)
}
}
/**
* @param {BareAwkNode} left
* @param {string} op
* @param {unknown} v
*/
async assign(left, op, v) {
let base = v
if (op !== '=') {
const cur = Number(await this.evalExpr(left))
const nv = Number(v)
if (op === '+=') base = cur + nv
else if (op === '-=') base = cur - nv
else if (op === '*=') base = cur * nv
else if (op === '/=') base = nv === 0 ? 0 : cur / nv
else if (op === '%=') base = nv === 0 ? 0 : cur % nv
}
if (left.t === 'index') {
const arr =
this.arrays[left.name] || (this.arrays[left.name] = Object.create(null))
arr[String(await this.evalExpr(left.idx))] = base
return
}
await this.assignScalar(left, base)
}
/**
* @param {string} path
*/
async _ensureGetlineFile(path) {
let st = this._getlineFileState.get(path)
if (st) return st
const buf = await this.io.readFile(path)
const text =
buf && this.io.bytesToString ? this.io.bytesToString(buf) : ''
const lines = text.split(/\r?\n/)
if (lines.length && lines[lines.length - 1] === '') lines.pop()
st = { lines, i: 0 }
this._getlineFileState.set(path, st)
return st
}
/**
* @param {string} name
* @param {BareAwkNode[]} args
*/
async callBuiltin(name, args) {
const a = async (i) =>
args[i] != null ? await this.evalExpr(args[i]) : ''
switch (name) {
case 'length':
if (!args.length) return this.$0.length
return String(await a(0)).length
case 'substr': {
const s = String(await a(0))
const start = Math.max(0, Math.trunc(Number(await a(1))) - 1)
if (args.length < 3) return s.slice(start)
const ln = Math.trunc(Number(await a(2)))
if (!Number.isFinite(ln) || ln < 0) return ''
return s.slice(start, start + ln)
}
case 'index':
return String(await a(0)).indexOf(String(await a(1))) + 1 || 0
case 'split': {
const s = String(await a(0))
const fs = args[1] ? String(await this.evalExpr(args[1])) : this.FS
let rx = fs.length === 1 ? null : bareAwkMakeRx(fs, '')
const parts = rx ? s.split(rx) : s.split(fs)
const aname = /** @type {BareAwkNode} */ (args[2])
if (aname && aname.t === 'var') {
const arr = (this.arrays[aname.name] = Object.create(null))
for (let i = 0; i < parts.length; i++) arr[String(i + 1)] = parts[i]
return parts.length
}
return parts.length
}
case 'sprintf': {
const fmt = String(await a(0))
const rest = []
for (let j = 1; j < args.length; j++)
rest.push(await this.evalExpr(args[j]))
return bareAwkSprintf(fmt, rest)
}
case 'int':
return Math.trunc(Number(await a(0)))
case 'log': {
const x = Number(await a(0))
return x > 0 ? Math.log(x) : Number.NaN
}
case 'sqrt': {
const x = Number(await a(0))
return x >= 0 ? Math.sqrt(x) : Number.NaN
}
case 'sin':
return Math.sin(Number(await a(0)))
case 'cos':
return Math.cos(Number(await a(0)))
case 'exp':
return Math.exp(Number(await a(0)))
case 'atan2':
return Math.atan2(Number(await a(0)), Number(await a(1)))
case 'tolower':
return String(await a(0)).toLowerCase()
case 'toupper':
return String(await a(0)).toUpperCase()
case 'rand': {
this.randSeed = (this.randSeed * 1103515245 + 12345) & 0x7fffffff
return this.randSeed / 0x7fffffff
}
case 'srand': {
const old = this.randSeed
if (args.length) this.randSeed = Math.trunc(Number(a(0))) || 0
else this.randSeed = Date.now() % 100000
return old
}
case 'sub':
case 'gsub': {
const rx =
args[0].t === 'rxLit'
? args[0].rx
: bareAwkMakeRx(String(await a(0)), '')
const rep = String(await a(1))
let target = args[2] ? String(await a(2)) : this.$0
let n = 0
if (name === 'sub') {
rx.lastIndex = 0
const m = rx.exec(target)
if (m) {
target =
target.slice(0, m.index) +
rep +
target.slice(m.index + m[0].length)
n = 1
}
} else {
target = target.replace(rx, () => {
n++
return rep
})
}
if (!args[2]) {
this.$0 = target
this.splitFields()
}
return n
}
case 'match': {
const s = String(await a(0))
const rx =
args[1].t === 'rxLit'
? args[1].rx
: bareAwkMakeRx(String(await a(1)), '')
rx.lastIndex = 0
const m = rx.exec(s)
if (!m) {
this.vars.RSTART = 0
this.vars.RLENGTH = -1
return 0
}
this.vars.RSTART = m.index + 1
this.vars.RLENGTH = m[0].length
return m.index + 1
}
default:
if (this.funcs.has(name)) {
return await this.callUser(name, args)
}
bareAwkError('unknown function ' + name)
}
}
/**
* @param {string} name
* @param {BareAwkNode[]} args
*/
async callUser(name, args) {
const f = this.funcs.get(name)
if (!f) return ''
const frame = { ...this.vars }
for (let i = 0; i < f.params.length; i++) {
frame[f.params[i]] = args[i] ? await this.evalExpr(args[i]) : ''
}
const prev = this.vars
this.vars = frame
let ret = ''
try {
for (const st of f.body) {
const r = await this.execStmt(st)
if (r && r.t === 'return') {
ret = r.v != null ? r.v : ''
break
}
}
} finally {
this.vars = prev
}
return ret
}
/**
* @param {BareAwkNode} n
*/
async doGetline(n) {
let line = null
if (n.from) {
const path = String(await this.evalExpr(n.from))
const st = await this._ensureGetlineFile(path)
if (st.i < st.lines.length) line = st.lines[st.i++]
else line = null
if (line == null) {
if (n.var) this.vars[n.var] = ''
else {
this.$0 = ''
this.splitFields()
}
return 0
}
if (n.var) this.vars[n.var] = line
else {
this.$0 = line
this.splitFields()
}
return 1
}
if (this._stdinIx < this.io.stdinLines.length) {
line = this.io.stdinLines[this._stdinIx++]
} else line = null
if (line == null) {
if (n.var) this.vars[n.var] = ''
else this.$0 = ''
return 0
}
if (n.var) this.vars[n.var] = line
else {
this.$0 = line
this.splitFields()
}
return 1
}
/**
* @param {BareAwkNode} st
* @returns {Promise<{ t: string, v?: unknown } | void>}
*/
async execStmt(st) {
switch (st.t) {
case 'block':
for (const s of st.stmts) {
const r = await this.execStmt(s)
if (r) return r
}
break
case 'if':
if (this.truthy(await this.evalExpr(st.cond)))
return await this.execStmt(st.then)
if (st.else) return await this.execStmt(st.else)
break
case 'while':
while (this.truthy(await this.evalExpr(st.cond))) {
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') continue
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
return r
}
break
case 'for':
if (st.init) await this.evalExpr(st.init)
while (true) {
if (st.cond && !this.truthy(await this.evalExpr(st.cond))) break
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') {
if (st.step) await this.evalExpr(st.step)
continue
}
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
return r
if (st.step) await this.evalExpr(st.step)
}
break
case 'forin': {
const arr = this.arrays[st.arr] || {}
for (const k of Object.keys(arr)) {
this.vars[st.iv] = k
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') continue
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
return r
}
break
}
case 'delete': {
const tbl =
this.arrays[st.arr] || (this.arrays[st.arr] = Object.create(null))
const ky = String(await this.evalExpr(st.key))
delete tbl[ky]
break
}
case 'next':
return { t: 'next' }
case 'nextfile':
return { t: 'nextfile' }
case 'break':
return { t: 'break' }
case 'continue':
return { t: 'continue' }
case 'exit':
this.exitCode =
Math.trunc(Number(await this.evalExpr(st.code))) || 0
this.exitPending = true
return { t: 'exit' }
case 'return':
return {
t: 'return',
v: st.e ? await this.evalExpr(st.e) : ''
}
case 'print':
await this.queuePrint(st.args, st.redir, false)
break
case 'printf':
await this.queuePrint([st.fmt, ...st.args], st.redir, true)
break
case 'expr':
await this.evalExpr(st.e)
break
}
}
/**
* @param {BareAwkNode[]} args
* @param {{ op: string, file: BareAwkNode } | null} redir
* @param {boolean} isPrintf
*/
async queuePrint(args, redir, isPrintf) {
let s = ''
if (isPrintf) {
const fmt = String(await this.evalExpr(args[0]))
const rest = []
for (let j = 1; j < args.length; j++)
rest.push(await this.evalExpr(args[j]))
s = bareAwkSprintf(fmt, rest)
} else if (!args.length) s = this.$0
else {
const parts = []
for (const x of args) parts.push(String(await this.evalExpr(x)))
s = parts.join(this.OFS)
}
s += this.ORS
if (redir) {
const path = String(await this.evalExpr(redir.file))
this.pendingWrites.push({
path,
data: s,
append: redir.op === '>>'
})
} else this.io.print(s)
}
async flushWrites() {
for (const w of this.pendingWrites) {
await this.io.writeFile(w.path, w.data, w.append)
}
this.pendingWrites = []
}
}
function bareAwkSprintf(fmt, args) {
let ai = 0
let o = ''
for (let i = 0; i < fmt.length; i++) {
if (fmt[i] !== '%') {
o += fmt[i]
continue
}
if (fmt[i + 1] === '%') {
o += '%'
i++
continue
}
let j = i + 1
while (j < fmt.length && /[0-9.#\-+ ]/.test(fmt[j])) j++
const spec = fmt[j] || 's'
const arg = args[ai++]
if (spec === 's') o += String(arg)
else if (spec === 'd' || spec === 'i') o += String(Math.trunc(Number(arg)))
else if (spec === 'f' || spec === 'g') o += String(Number(arg))
else if (spec === 'o') o += (Math.trunc(Number(arg)) >>> 0).toString(8)
else if (spec === 'x') o += (Math.trunc(Number(arg)) >>> 0).toString(16)
else if (spec === 'X')
o += (Math.trunc(Number(arg)) >>> 0).toString(16).toUpperCase()
else o += String(arg)
i = j
}
return o
}
/**
* @param {string} program
* @param {{ fs?: string, argc: number, argv: string[], environ: Record<string, string> }} opts
* @param {{ print(s: string): void, writeFile(path: string, data: string, append?: boolean): Promise<void>, readFile(path: string): Promise<Uint8Array | null>, stdinLines: string[] }} io
*/
export async function bareAwkRun(program, opts, io) {
const ast = bareAwkParse(program)
const rt = new BareAwkRuntime(ast, opts, io)
for (const rule of ast.rules) {
if (
rule.pattern &&
rule.pattern.t === 'pat' &&
rule.pattern.k === 'BEGIN'
) {
for (const st of rule.stmts) {
const r = await rt.execStmt(st)
await rt.flushWrites()
if (r?.t === 'exit') return rt.exitCode
}
}
}
if (rt.exitPending) return rt.exitCode
const files = opts.argv.slice(1).filter((x) => x != null && x !== '')
const useStdin = files.length === 0
async function runOnFile(path, lines) {
rt.FILENAME = path
rt.FNR = 0
lines: for (const line of lines) {
if (rt.exitPending) return
rt.NR++
rt.FNR++
rt.$0 = line
rt.vars.NR = rt.NR
rt.vars.FNR = rt.FNR
rt.vars.NF = 0
rt.splitFields()
for (const rule of ast.rules) {
if (rule.pattern && rule.pattern.t === 'pat') continue
if (!(await rt.patternMatch(rule.pattern))) continue
for (const st of rule.stmts) {
const r = await rt.execStmt(st)
await rt.flushWrites()
if (r?.t === 'next') break
if (r?.t === 'nextfile') break lines
if (r?.t === 'exit') return
}
}
}
}
if (useStdin) {
await runOnFile('', io.stdinLines)
} else {
for (const f of files) {
const buf = await io.readFile(f)
const text = buf && io.bytesToString ? io.bytesToString(buf) : ''
const lines = text.split(/\r?\n/)
if (lines.length && lines[lines.length - 1] === '') lines.pop()
await runOnFile(f, lines)
if (rt.exitPending) break
}
}
for (const rule of ast.rules) {
if (rule.pattern && rule.pattern.t === 'pat' && rule.pattern.k === 'END') {
for (const st of rule.stmts) {
await rt.execStmt(st)
await rt.flushWrites()
}
}
}
return rt.exitCode
}