diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 18:32:25 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 18:32:25 +0200 |
| commit | b0404b1c9154c3438d0e124d69dc25d9140cfac4 (patch) | |
| tree | b39d11720c803e2586b4b1a06ae3230c9a98dffd /modules/core/core-bundle.js | |
| parent | 87f44c42aeb763e2aaf3da159d60b252dca9aa53 (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.js | 103 |
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'); |