/* 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} 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). */ 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[]} */ const toks = [] while (true) { const t = bareAwkLex(src, st) toks.push(t) if (t.kind === 'EOF') break } return toks } class BareAwkParser { /** * @param {ReturnType[]} 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, fs: string, ofmt: string }} opts * @param {{ print(s: string): void, writeFile(path: string, data: string, append?: boolean): Promise, readFile(path: string): Promise, 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} */ this.vars = { OFMT: opts.ofmt || '%.6g' } /** @type {Record>} */ this.arrays = Object.create(null) /** @type {Record} */ this.environ = { ...opts.environ } /** @type {Map} */ 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 }} opts * @param {{ print(s: string): void, writeFile(path: string, data: string, append?: boolean): Promise, readFile(path: string): Promise, 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 } }