1522 lines
40 KiB
Plaintext
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
|
|
}
|
|
}
|