aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-fs.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-09-29 13:10:14 +0200
committerAgando <roduccomm@gmail.com>2026-09-29 13:10:14 +0200
commitd2c4913c17cb7ea8f2c3bd27b8e4b3990f8bc79e (patch)
tree185bb00e0daf95c55dc67d58f9e7c5b94f6e9a43 /modules/core/module-fs.js
parentd4abdf3d7764bf4362a16261d0ebf151a15926f7 (diff)
newOS4: tail -n/-N flags + live follow mode (-f) with test harness
Diffstat (limited to 'modules/core/module-fs.js')
-rw-r--r--modules/core/module-fs.js49
1 files changed, 45 insertions, 4 deletions
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index 25442eb..2fb9e65 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -232,11 +232,52 @@ class FileSystemModule {
this.terminal.print(c.split('\n').slice(0, n).join('\n'));
};
this.commands['tail'] = (args) => {
- if(!args[0]) return this.terminal.print('Usage: tail <filename> [lines]', 'warning');
- const c = this.terminal.fs.readFile(args[0]);
- if(c === null || c === undefined) return this.terminal.print('tail: ' + args[0] + ': No such file', 'error');
- const n = args[1] ? parseInt(args[1]) : 10;
+ const self = this;
+ let follow = false, n = 10;
+ const positional = [];
+ for (let i = 0; i < (args || []).length; i++) {
+ const a = args[i];
+ if (a === '-f' || a === '--follow') follow = true;
+ else if (a === '-n') {
+ const v = args[++i];
+ if (v === undefined || !/^\d+$/.test(v)) return this.terminal.print('tail: -n requires a number', 'error');
+ n = parseInt(v, 10);
+ }
+ else if (/^-\d+$/.test(a)) n = parseInt(a.slice(1), 10);
+ else if (a === '-c' || a === '-q' || a === '-v' || a === '--pid') return this.terminal.print('tail: option ' + a + ' not supported', 'error');
+ else if (a.startsWith('-')) return this.terminal.print('tail: invalid option: ' + a, 'error');
+ else positional.push(a);
+ }
+ const file = positional[0];
+ if (!file) return this.terminal.print('Usage: tail [-f] [-n N] <file> — last N lines; -f follows changes (Ctrl+C or Esc to stop)', 'warning');
+ if (positional[1] && /^\d+$/.test(positional[1])) n = parseInt(positional[1], 10); // legacy back-compat: tail <file> <count>
+ const c = this.terminal.fs.readFile(file);
+ if (c === null || c === undefined) return this.terminal.print('tail: ' + file + ': No such file', 'error');
this.terminal.print(c.split('\n').slice(-n).join('\n'));
+ if (!follow) return;
+ // stop any previous follow before starting a new one
+ if (this.terminal._activeFollow && this.terminal._activeFollow.interval) clearInterval(this.terminal._activeFollow.interval);
+ this.terminal._activeFollow = null;
+ let offset = c.length;
+ const interval = setInterval(() => {
+ const cur = self.terminal.fs.readFile(file);
+ if (cur === null || cur === undefined) {
+ self.terminal.stopFollow('tail: ' + file + ': file removed — follow stopped');
+ return;
+ }
+ if (cur.length < offset) {
+ self.terminal.print('tail: ' + file + ': file truncated — following from start', 'warning');
+ self.terminal.print(cur);
+ offset = cur.length;
+ return;
+ }
+ if (cur.length > offset) {
+ self.terminal.print(cur.slice(offset));
+ offset = cur.length;
+ }
+ }, 1500);
+ this.terminal._activeFollow = { interval, file };
+ this.terminal.print('tail: following ' + file + ' (Ctrl+C or Esc to stop)', 'text-muted');
};
this.commands['sort'] = (args) => {
if(!args[0]) return this.terminal.print('Usage: sort <filename>', 'warning');