aboutsummaryrefslogtreecommitdiff
path: root/modules/core/core-bundle.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 18:32:25 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 18:32:25 +0200
commitf55e3055a4b85cf3920f5df1e3add3696153169c (patch)
tree4edfdbd52739d5ca62278beb752c5e071aaaeee0 /modules/core/core-bundle.js
parente74b3b1d8569c981cc34f1d5078fd19e8de98338 (diff)
fix(newos4): grep, head and tail take every path argument (v4.6.67)
Packet 1's multi-path ask was only honoured by wc in v4.6.58 — grep read only args[ai+1] and head/tail only positional[0], so `grep x a b` searched `a`, printed one file's hits, and exited 0. Same silent drop, three more commands. - grep: loops every path; multiple files print `file:line: text`, `-c` prints one count per file, missing files are reported per file and excluded from the summary count; single-file output unchanged - head: every path, `==> file <==` header when more than one; `head f 40` still works - tail: every path with headers; a numeric second positional is still the legacy `tail <file> <count>`; `-f` warns and follows only the first file instead of printing all of them and claiming otherwise Verified live on v4.6.67: grep "bug_id" a b -> a:2 + b:3 + "2 match(es) in 2 file(s)" grep -c "alpha" a b -> one count per file grep "alpha" a missing -> hits from a, per-file error, "in 1 file(s)" head -n 1 a b -> header + first line for each tail -n 1 a b -> header + last line for each tail a 2 -> legacy count still honoured tail -f a b -> warns, follows a only
Diffstat (limited to 'modules/core/core-bundle.js')
-rw-r--r--modules/core/core-bundle.js103
1 files changed, 71 insertions, 32 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index a5e9a79..e4cd832 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -6641,7 +6641,7 @@ class FileSystemModule {
ai++;
}
const pattern = args[ai]; const target = args[ai + 1];
- if (!pattern) return this.terminal.print('Usage: grep [-n] [-i] [-c] [-l] <pattern> [filename]', 'warning');
+ if (!pattern) return this.terminal.print('Usage: grep [-n] [-i] [-c] [-l] <pattern> [filename ...]', 'warning');
// v4.6.64: an invalid pattern is user input, not a system fault. It used to throw
// out of the command handler into the error ledger, polluting the counter that
// packet §2 says nobody can trust (packet §8).
@@ -6653,20 +6653,36 @@ class FileSystemModule {
return;
}
const hits = (line) => (opt.invert ? !re.test(line) : re.test(line));
- if (target) {
- const c = this.terminal.fs.readFile(target);
- if (c === null || c === undefined) return this.terminal.print('grep: ' + target + ': No such file', 'error');
- const lines = c.split('\n');
- let count = 0;
- lines.forEach((line, i) => {
- if (!hits(line)) return;
- count++;
- if (!opt.count && !opt.files) this.terminal.print((i + 1) + ': ' + line);
+ // v4.6.66: every path argument is searched. `grep x a b` used to read only `a` and
+ // exit clean — a silent false negative (packet §1).
+ const grepFiles = args.slice(ai + 1).filter(Boolean);
+ if (grepFiles.length) {
+ const multi = grepFiles.length > 1;
+ let grand = 0;
+ let readFiles = 0;
+ grepFiles.forEach(file => {
+ const c = this.terminal.fs.readFile(file);
+ if (c === null || c === undefined) {
+ this.terminal.print('grep: ' + file + ': No such file', 'error');
+ return;
+ }
+ readFiles++;
+ let count = 0;
+ c.split('\n').forEach((line, i) => {
+ if (!hits(line)) return;
+ count++;
+ if (opt.count || opt.files) return;
+ this.terminal.print(multi ? (file + ':' + (i + 1) + ': ' + line) : ((i + 1) + ': ' + line));
+ });
+ grand += count;
+ if (opt.files) { if (count) this.terminal.print(file); }
+ else if (opt.count) this.terminal.print(multi ? (file + ':' + count) : String(count));
+ else if (!count && !multi) this.terminal.print('(no matches)', 'info');
});
- if (opt.files && count) this.terminal.print(target);
- if (count === 0) this.terminal.print('(no matches)', 'info');
- else if (opt.count) this.terminal.print(String(count));
- else if (!opt.files) this.terminal.print(count + ' match(es)', 'text-muted');
+ if (!opt.count && !opt.files) {
+ if (grand === 0) this.terminal.print('(no matches)', 'info');
+ else this.terminal.print(grand + ' match(es)' + (multi ? ' in ' + readFiles + ' file(s)' : ''), 'text-muted');
+ }
} else {
const dir = this.terminal.fs.getCurrentDir();
if (!dir) return this.terminal.print('grep: cannot search', 'error');
@@ -6689,18 +6705,25 @@ class FileSystemModule {
};
this.commands['head'] = (args) => {
// 2026-10-21: accept `head -40 f`, `head -n 40 f` and `head f 40`.
- let n = 10, file = null;
+ // v4.6.66: every path argument, not just the first (packet §1).
+ let n = 10;
+ const headFiles = [];
for (let i = 0; i < (args || []).length; i++) {
const a = args[i];
if (/^-\d+$/.test(a)) n = parseInt(a.slice(1), 10);
- else if (a === '-n') { const v = parseInt(args[i + 1], 10); if (!isNaN(v)) n = v; i++; }
+ else if (a === '-n') { const v = parseInt(args[i + 1], 10); if (!isNaN(v)) { n = v; i++; } }
else if (/^-[a-zA-Z]+$/.test(a)) { /* ignore other flags */ }
- else if (!file) file = a;
- }
- if (!file) return this.terminal.print('Usage: head [-n] <filename> [lines]', 'warning');
- const c = this.terminal.fs.readFile(file);
- if (c === null || c === undefined) return this.terminal.print('head: ' + file + ': No such file', 'error');
- this.terminal.print(c.split('\n').slice(0, Math.max(1, n)).join('\n'));
+ else if (headFiles.length && /^\d+$/.test(a)) n = parseInt(a, 10); // head f 40
+ else headFiles.push(a);
+ }
+ if (!headFiles.length) return this.terminal.print('Usage: head [-n] <filename> [...] [lines]', 'warning');
+ const headMulti = headFiles.length > 1;
+ headFiles.forEach(file => {
+ const c = this.terminal.fs.readFile(file);
+ if (c === null || c === undefined) return this.terminal.print('head: ' + file + ': No such file', 'error');
+ if (headMulti) this.terminal.print('==> ' + file + ' <==', 'text-muted');
+ this.terminal.print(c.split('\n').slice(0, Math.max(1, n)).join('\n'));
+ });
};
this.commands['tail'] = (args) => {
const self = this;
@@ -6719,17 +6742,33 @@ class FileSystemModule {
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');
- // 2026-10-21: a trailing newline produced an empty last element, so `tail -n 1`
- // printed a blank line instead of the final line.
- const _all = c.split('\n');
- if (_all.length > 1 && _all[_all.length - 1] === '') _all.pop();
- this.terminal.print(_all.slice(-n).join('\n'));
+ // v4.6.66: every path argument (packet §1). A numeric second positional is still
+ // the legacy `tail <file> <count>`, not a filename.
+ const tailFiles = positional.filter((p, i) => !(i > 0 && /^\d+$/.test(p)));
+ if (!tailFiles.length) 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
+ const tailMulti = tailFiles.length > 1;
+ let shownFiles = tailFiles;
+ if (follow && tailMulti) {
+ this.terminal.print('tail: -f needs a single file — following ' + tailFiles[0] + ' only', 'warning');
+ shownFiles = [tailFiles[0]];
+ }
+ let firstContent = null;
+ shownFiles.forEach(f2 => {
+ const cc = this.terminal.fs.readFile(f2);
+ if (cc === null || cc === undefined) return this.terminal.print('tail: ' + f2 + ': No such file', 'error');
+ if (shownFiles.length > 1) this.terminal.print('==> ' + f2 + ' <==', 'text-muted');
+ // 2026-10-21: a trailing newline produced an empty last element, so `tail -n 1`
+ // printed a blank line instead of the final line.
+ const _all = cc.split('\n');
+ if (_all.length > 1 && _all[_all.length - 1] === '') _all.pop();
+ this.terminal.print(_all.slice(-n).join('\n'));
+ if (firstContent === null) firstContent = cc;
+ });
+ const file = tailFiles[0];
+ const c = firstContent === null ? '' : firstContent;
if (!follow) return;
+ if (c === '') return; // v4.6.66: the only file was unreadable — nothing to follow
// AI tool calls cannot press Ctrl+C — auto-downgrade to a bounded read instead of a live timer
if (this.terminal.security && this.terminal.security._aiToolDepth > 0) {
return this.terminal.print('tail: follow mode (-f) is interactive-only; showing last lines instead (use tail -n ' + Math.max(n, 50) + ' for more)', 'warning');