Files
bare-operating-system/packages/bare-os-seeder/kernel/bin/awk
T
2026-04-03 23:21:30 -04:00

1522 lines
40 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 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),
* next, exit, break, continue, ++/--, arrays, strings, regex ~ !~, builtins (length, substr, index,
* split, sprintf, sub, gsub, match, int, tolower, toupper, rand, srand), getline from stdin/files,
* -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 === 'exit' ||
id === 'print' ||
id === 'printf' ||
id === 'return' ||
id === 'function' ||
id === 'getline'
) {
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 === 'break' || t.value === 'continue')
) {
this.p++
this.optSemi()
return { t: t.value }
}
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 === '+') {
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 = []
}
splitFields() {
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
*/
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(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
*/
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(this.evalExpr(n.e))
case 'index': {
const k = String(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 = this.evalExpr(n.a)
const b = 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 = this.evalExpr(n.a)
if (n.op === '!') return this.truthy(a) ? 0 : 1
if (n.op === '+') return Number(a) || 0
return 0
}
case '||':
return this.truthy(this.evalExpr(n.a))
? 1
: this.truthy(this.evalExpr(n.b))
? 1
: 0
case '&&':
return this.truthy(this.evalExpr(n.a)) &&
this.truthy(this.evalExpr(n.b))
? 1
: 0
case 'match': {
const s = String(this.evalExpr(n.a))
const rhs = n.b
let rx
if (rhs.t === 'rxLit') rx = rhs.rx
else {
const t = String(this.evalExpr(rhs))
rx = bareAwkMakeRx(t, '')
}
rx.lastIndex = 0
return rx.test(s) ? 1 : 0
}
case 'nmatch': {
const s = String(this.evalExpr(n.a))
const rhs = n.b
let rx
if (rhs.t === 'rxLit') rx = rhs.rx
else {
const t = String(this.evalExpr(rhs))
rx = bareAwkMakeRx(t, '')
}
rx.lastIndex = 0
return rx.test(s) ? 0 : 1
}
case '?:':
return this.truthy(this.evalExpr(n.n))
? this.evalExpr(n.a)
: this.evalExpr(n.b)
case 'assign': {
const v = this.evalExpr(n.right)
this.assign(n.left, n.op, v)
return v
}
case 'pre': {
const cur = Number(this.evalExpr(n.x))
const next = n.op === '++' ? cur + 1 : cur - 1
this.assignScalar(n.x, next)
return next
}
case 'post': {
const cur = Number(this.evalExpr(n.x))
const next = n.op === '++' ? cur + 1 : cur - 1
this.assignScalar(n.x, next)
return cur
}
case 'call':
return this.callBuiltin(n.name, n.args)
case 'getline':
return this.doGetline(n)
default:
return ''
}
}
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(this.evalExpr(x.e), v)
}
}
/**
* @param {BareAwkNode} left
* @param {string} op
* @param {unknown} v
*/
assign(left, op, v) {
let base = v
if (op !== '=') {
const cur = Number(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(this.evalExpr(left.idx))] = base
return
}
this.assignScalar(left, base)
}
/**
* @param {string} name
* @param {BareAwkNode[]} args
*/
callBuiltin(name, args) {
const a = (i) => (args[i] ? this.evalExpr(args[i]) : '')
switch (name) {
case 'length':
if (!args.length) return this.$0.length
return String(a(0)).length
case 'substr':
return String(a(0)).slice(
Math.max(0, Math.trunc(Number(a(1))) - 1),
Math.max(0, Math.trunc(Number(a(1))) - 1 + Math.trunc(Number(a(2))))
)
case 'index':
return String(a(0)).indexOf(String(a(1))) + 1 || 0
case 'split': {
const s = String(a(0))
const fs = args[1] ? String(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':
return bareAwkSprintf(
String(a(0)),
args.slice(1).map((x) => this.evalExpr(x))
)
case 'int':
return Math.trunc(Number(a(0)))
case 'tolower':
return String(a(0)).toLowerCase()
case 'toupper':
return String(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(a(0)), '')
const rep = String(a(1))
let target = args[2] ? String(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(a(0))
const rx =
args[1].t === 'rxLit' ? args[1].rx : bareAwkMakeRx(String(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 this.callUser(name, args)
}
bareAwkError('unknown function ' + name)
}
}
/**
* @param {string} name
* @param {BareAwkNode[]} args
*/
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] ? this.evalExpr(args[i]) : ''
}
const prev = this.vars
this.vars = frame
let ret = ''
try {
for (const st of f.body) {
const r = this.execStmt(st)
if (r && r.t === 'return') {
ret = r.v != null ? r.v : ''
break
}
}
} finally {
this.vars = prev
}
return ret
}
/**
* @param {BareAwkNode} n
*/
doGetline(n) {
let line = null
if (n.from) {
const path = String(this.evalExpr(n.from))
/* sync read — awk.js should preload or use async wrapper; runtime uses promise in run loop */
return 0
}
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 {{ t: string, v?: unknown } | void}
*/
execStmt(st) {
switch (st.t) {
case 'block':
for (const s of st.stmts) {
const r = this.execStmt(s)
if (r) return r
}
break
case 'if':
if (this.truthy(this.evalExpr(st.cond))) return this.execStmt(st.then)
if (st.else) return this.execStmt(st.else)
break
case 'while':
while (this.truthy(this.evalExpr(st.cond))) {
const r = this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') continue
if (r?.t === 'next' || r?.t === 'exit') return r
}
break
case 'for':
if (st.init) this.evalExpr(st.init)
while (true) {
if (st.cond && !this.truthy(this.evalExpr(st.cond))) break
const r = this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') {
if (st.step) this.evalExpr(st.step)
continue
}
if (r?.t === 'next' || r?.t === 'exit') return r
if (st.step) 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 = this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') continue
if (r?.t === 'next' || r?.t === 'exit') return r
}
break
}
case 'next':
return { t: 'next' }
case 'break':
return { t: 'break' }
case 'continue':
return { t: 'continue' }
case 'exit':
this.exitCode = Math.trunc(Number(this.evalExpr(st.code))) || 0
this.exitPending = true
return { t: 'exit' }
case 'return':
return { t: 'return', v: st.e ? this.evalExpr(st.e) : '' }
case 'print':
this.queuePrint(st.args, st.redir, false)
break
case 'printf':
this.queuePrint([st.fmt, ...st.args], st.redir, true)
break
case 'expr':
this.evalExpr(st.e)
break
}
}
/**
* @param {BareAwkNode[]} args
* @param {{ op: string, file: BareAwkNode } | null} redir
* @param {boolean} isPrintf
*/
queuePrint(args, redir, isPrintf) {
let s = ''
if (isPrintf) {
const fmt = String(this.evalExpr(args[0]))
const rest = args.slice(1).map((x) => this.evalExpr(x))
s = bareAwkSprintf(fmt, rest)
} else if (!args.length) s = this.$0
else s = args.map((x) => String(this.evalExpr(x))).join(this.OFS)
s += this.ORS
if (redir) {
const path = String(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
*/
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 = 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
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 (!rt.patternMatch(rule.pattern)) continue
for (const st of rule.stmts) {
const r = rt.execStmt(st)
await rt.flushWrites()
if (r?.t === 'next') break
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) {
rt.execStmt(st)
await rt.flushWrites()
}
}
}
return rt.exitCode
}
async function run(ctx, argv) {
let fsVal = null
/** @type {string[]} */
const progParts = []
const rest = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-F' || a === '--field-separator') {
fsVal = argv[++i] || ''
continue
}
if (a === '-v') {
const ax = argv[++i] || ''
const eq = ax.indexOf('=')
if (eq > 0) {
const k = ax.slice(0, eq)
const v = ax.slice(eq + 1)
progParts.push('BEGIN { ' + k + ' = ' + JSON.stringify(v) + ' }')
}
continue
}
if (a === '-f') {
const path = argv[++i]
if (!path) {
ctx.console.error('awk: -f needs a file')
ctx.exitCode = 1
return
}
const buf = await ctx.vfs.readFile(path)
if (!buf) {
ctx.console.error('awk: cannot read ' + path)
ctx.exitCode = 1
return
}
progParts.push(ctx.b4a.toString(buf))
continue
}
if (a === '--') {
rest.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-') && a.length > 1) {
ctx.console.error('awk: unsupported option ' + a)
ctx.exitCode = 1
return
}
progParts.push(a)
rest.push(...argv.slice(i + 1))
break
}
const program = progParts.join('\n')
if (!program.trim()) {
ctx.console.error(
'usage: awk [-F fs] [-v k=v] [-f file] program [file ...]'
)
ctx.exitCode = 1
return
}
const awkArgv = ['awk', ...rest]
const stdinLines = (() => {
const s = bareStdin(ctx)
const ls = s.split(/\r?\n/)
if (ls.length && ls[ls.length - 1] === '') ls.pop()
return ls
})()
const io = {
bytesToString(buf) {
return buf ? ctx.b4a.toString(buf, 'utf8') : ''
},
print(s) {
const t = s.replace(/\n$/, '')
ctx.console.log(t)
},
async writeFile(path, data, append) {
const prev = append ? await ctx.vfs.readFile(path) : null
const merged = prev
? ctx.b4a.concat([prev, ctx.b4a.from(data)])
: ctx.b4a.from(data)
await ctx.vfs.writeFile(path, merged)
},
readFile(path) {
return ctx.vfs.readFile(path)
},
stdinLines
}
try {
const code = await bareAwkRun(
program,
{
fs: fsVal != null ? fsVal : ' ',
argc: awkArgv.length,
argv: awkArgv,
environ: { ...ctx.vfs.env }
},
io
)
ctx.exitCode = code || 0
} catch (e) {
ctx.console.error((e && e.message) || String(e))
ctx.exitCode = 1
}
}