/** * tail — last lines/bytes of files; -f/--follow with vfs.watch or poll. * Test/limit: BARE_OS_TAIL_F_POLL_MS, BARE_OS_TAIL_F_MAX_ROUNDS (empty = unlimited). */ function bareTailSleep(ms) { return new Promise((r) => setTimeout(r, ms)) } function bareTailEnv(ctx) { return (ctx.vfs && ctx.vfs.env) || ctx.env || {} } function bareTailPollMs(env) { const n = Number.parseInt(String(env.BARE_OS_TAIL_F_POLL_MS || '1000'), 10) return Number.isFinite(n) && n >= 0 ? n : 1000 } function bareTailMaxRounds(env) { const raw = env.BARE_OS_TAIL_F_MAX_ROUNDS if (raw == null || raw === '') return Number.POSITIVE_INFINITY const n = Number.parseInt(String(raw), 10) return Number.isFinite(n) && n >= 0 ? n : Number.POSITIVE_INFINITY } function bareTailUseWatch(ctx, vfs) { if (typeof vfs.watch !== 'function') return false const caps = ctx.bareOsRuntimeCaps if (caps && caps.features && caps.features.vfsWatch === false) return false return true } /** @param {string} s */ function bareTailSplitLines(s) { const lines = s.split('\n') if (s.endsWith('\n') && lines.length && lines[lines.length - 1] === '') lines.pop() return lines } /** * @param {Uint8Array} u8 * @param {'end' | 'start'} which * @param {number} n count or 1-based start (+ mode) */ function bareTailBytesSlice(u8, which, n, fromPlus) { if (!u8 || !u8.byteLength) return new Uint8Array(0) if (which === 'end') { if (n <= 0) return new Uint8Array(0) const start = Math.max(0, u8.byteLength - n) return u8.subarray(start) } /* from start (+): GNU tail -c +K starts at byte K (1-based) */ const idx = fromPlus ? Math.max(0, n - 1) : 0 return idx >= u8.byteLength ? new Uint8Array(0) : u8.subarray(idx) } /** * @param {string} s * @param {'end' | 'start'} which * @param {number} n * @param {boolean} fromPlus * @param {*} b4a */ function bareTailLinesText(s, which, n, fromPlus, b4a) { const lines = bareTailSplitLines(s) if (which === 'end') { if (n <= 0) return '' const slice = lines.length <= n ? lines : lines.slice(-n) return slice.join('\n') } /* +N: from line N (1-based) */ const start = fromPlus ? Math.max(0, n - 1) : 0 const slice = lines.slice(start) return slice.join('\n') } /** * Emit follow chunks as full lines via console.log (tests capture log). * @param {string} chunk * @param {{ partial: string }} state */ function bareTailFollowEmit(ctx, chunk, state) { const combined = state.partial + chunk const lines = combined.split('\n') state.partial = lines.pop() || '' for (const ln of lines) ctx.console.log(ln) } function bareTailFollowFlush(ctx, state) { if (state.partial !== '') { ctx.console.log(state.partial) state.partial = '' } } /** * @param {*} ctx * @param {string} path * @param {*} vfs * @param {number} startOffset byte offset after initial read (next byte to emit) */ async function bareTailFollow(ctx, path, vfs, startOffset) { const env = bareTailEnv(ctx) const pollMs = bareTailPollMs(env) const maxRounds = bareTailMaxRounds(env) const state = { partial: '' } let offset = startOffset let rounds = 0 const pumpOnce = async () => { const buf = await vfs.readFile(path) if (!buf) { offset = 0 return } const u8 = buf instanceof Uint8Array ? buf : ctx.b4a.from(buf) if (u8.byteLength < offset) { offset = 0 } if (u8.byteLength > offset) { const piece = u8.subarray(offset) offset = u8.byteLength bareTailFollowEmit(ctx, ctx.b4a.toString(piece), state) } } if ( bareTailUseWatch(ctx, vfs) && maxRounds === Number.POSITIVE_INFINITY ) { let handle = null try { handle = await vfs.watch(path) } catch { handle = null } if (handle && handle.watcher) { try { for await (const _ of handle.watcher) { await pumpOnce() } } finally { if (typeof handle.destroy === 'function') handle.destroy() } bareTailFollowFlush(ctx, state) return } } while (rounds < maxRounds) { await bareTailSleep(pollMs) await pumpOnce() rounds++ } bareTailFollowFlush(ctx, state) } async function run(ctx, argv) { const vfs = ctx.vfs let follow = false /** @type {'lines' | 'bytes'} */ let mode = 'lines' let count = 10 let fromPlus = false let i = 1 while (i < argv.length) { const a = argv[i] if (a === '--') { i++ break } if (a === '-f' || a === '--follow') { follow = true i++ continue } if (a === '-F') { follow = true i++ continue } if (a === '-n' || a === '--lines') { const v = argv[i + 1] if (v == null) { ctx.console.error('tail: option requires an argument -- n') ctx.exitCode = 1 return } i++ mode = 'lines' if (v.startsWith('+')) { fromPlus = true count = Number.parseInt(v.slice(1), 10) || 1 } else { fromPlus = false count = Number.parseInt(v, 10) || 10 } i++ continue } if (a.startsWith('-n') && a.length > 2) { const v = a.slice(2) mode = 'lines' if (v.startsWith('+')) { fromPlus = true count = Number.parseInt(v.slice(1), 10) || 1 } else { fromPlus = false count = Number.parseInt(v, 10) || 10 } i++ continue } if (a === '-c' || a === '--bytes') { const v = argv[i + 1] if (v == null) { ctx.console.error('tail: option requires an argument -- c') ctx.exitCode = 1 return } i++ mode = 'bytes' if (v.startsWith('+')) { fromPlus = true count = Number.parseInt(v.slice(1), 10) || 1 } else { fromPlus = false count = Number.parseInt(v, 10) || 0 } i++ continue } if (a.startsWith('-c') && a.length > 2) { const v = a.slice(2) mode = 'bytes' if (v.startsWith('+')) { fromPlus = true count = Number.parseInt(v.slice(1), 10) || 1 } else { fromPlus = false count = Number.parseInt(v, 10) || 0 } i++ continue } if (a.startsWith('-') && /^-\d+$/.test(a)) { mode = 'lines' fromPlus = false count = Number.parseInt(a.slice(1), 10) || 10 i++ continue } if (a.startsWith('-')) { ctx.console.error('tail: unrecognized option: ' + a) ctx.exitCode = 1 return } break } const files = argv.slice(i).filter((x) => x !== '--') if (follow && !files.length) { ctx.console.error( 'tail: --follow is not supported for stdin on Bare OS (stdin is a captured string)' ) ctx.exitCode = 1 return } if (follow && files.length > 1) { ctx.console.error( 'tail: following multiple files is not supported (use one path)' ) ctx.exitCode = 1 return } if (!files.length) { const s = bareStdin(ctx) if (mode === 'bytes') { const u8 = ctx.b4a.from(s, 'utf8') const slice = bareTailBytesSlice( u8, fromPlus ? 'start' : 'end', count, fromPlus ) const out = ctx.b4a.toString(slice) if (out) ctx.console.log(out) } else { const out = bareTailLinesText( s, fromPlus ? 'start' : 'end', count, fromPlus, ctx.b4a ) if (out) ctx.console.log(out) } return } for (const f of files) { const buf = await vfs.readFile(f) if (!buf) { ctx.console.error('tail: ' + f + ': No such file') continue } if (files.length > 1) ctx.console.log('==> ' + f + ' <==') const u8 = buf instanceof Uint8Array ? buf : ctx.b4a.from(buf) const text = ctx.b4a.toString(u8) if (!follow) { if (mode === 'bytes') { const slice = bareTailBytesSlice( u8, fromPlus ? 'start' : 'end', count, fromPlus ) const out = ctx.b4a.toString(slice) if (out) ctx.console.log(out) } else { const out = bareTailLinesText( text, fromPlus ? 'start' : 'end', count, fromPlus, ctx.b4a ) if (out) ctx.console.log(out) } continue } if (mode === 'bytes') { const slice = bareTailBytesSlice( u8, fromPlus ? 'start' : 'end', count, fromPlus ) const out = ctx.b4a.toString(slice) if (out) ctx.console.log(out) await bareTailFollow(ctx, f, vfs, u8.byteLength) } else { const out = bareTailLinesText( text, fromPlus ? 'start' : 'end', count, fromPlus, ctx.b4a ) if (out) ctx.console.log(out) const startOffset = u8.byteLength await bareTailFollow(ctx, f, vfs, startOffset) } } }