diff options
Diffstat (limited to 'modules/core/module-fs.js')
| -rw-r--r-- | modules/core/module-fs.js | 103 |
1 files changed, 84 insertions, 19 deletions
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js index 2b525f0..686a1a6 100644 --- a/modules/core/module-fs.js +++ b/modules/core/module-fs.js @@ -200,37 +200,72 @@ class FileSystemModule { this.terminal.print('Found ' + results.length + ' match(es)', 'info'); }; this.commands['grep'] = (args) => { - if(!args[0]) return this.terminal.print('Usage: grep <pattern> [filename]', 'warning'); - const pattern = args[0]; const target = args[1]; + // 2026-10-21: accept the common GNU flags instead of treating them as the pattern. + // `grep -n "x" file` used to search for the literal "-n" inside a file named "x" + // (6/6 syntax variants failed for the agent), while plain grep already prints + // "line: text" for every hit. + const opt = { count: false, invert: false, files: false, ignoreCase: true }; + let ai = 0; + while (ai < args.length && /^-[a-zA-Z]+$/.test(args[ai])) { + const f = args[ai].slice(1); + if (f.indexOf('c') !== -1) opt.count = true; + if (f.indexOf('v') !== -1) opt.invert = true; + if (f.indexOf('l') !== -1) opt.files = true; + if (f.indexOf('i') !== -1) opt.ignoreCase = true; + 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'); + const re = new RegExp(pattern, opt.ignoreCase ? 'i' : ''); + 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'); const re = new RegExp(pattern, 'i'); - let count = 0; lines.forEach((line, i) => { if (re.test(line)) { this.terminal.print((i+1) + ': ' + line); count++; } }); + 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); + }); + if (opt.files && count) this.terminal.print(target); if (count === 0) this.terminal.print('(no matches)', 'info'); - else this.terminal.print(count + ' match(es)', 'text-muted'); + else if (opt.count) this.terminal.print(String(count)); + else if (!opt.files) this.terminal.print(count + ' match(es)', 'text-muted'); } else { const dir = this.terminal.fs.getCurrentDir(); if (!dir) return this.terminal.print('grep: cannot search', 'error'); - let total = 0; const re = new RegExp(pattern, 'i'); + let total = 0; Object.keys(dir.content).forEach(name => { const node = dir.content[name]; if (node.type === 'file') { (node.content || '').split('\n').forEach((line, i) => { - if (re.test(line)) { this.terminal.print(name + ':' + (i+1) + ': ' + line); total++; } + if (!hits(line)) return; + total++; + if (!opt.count && !opt.files) this.terminal.print(name + ':' + (i + 1) + ': ' + line); }); } }); + if (opt.files && total) this.terminal.print(dir.name || '.'); if (total === 0) this.terminal.print('(no matches)', 'info'); - else this.terminal.print(total + ' total match(es)', 'text-muted'); + else if (opt.count) this.terminal.print(String(total)); + else if (!opt.files) this.terminal.print(total + ' total match(es)', 'text-muted'); } }; this.commands['head'] = (args) => { - if(!args[0]) return this.terminal.print('Usage: head <filename> [lines]', 'warning'); - const c = this.terminal.fs.readFile(args[0]); - if(c === null || c === undefined) return this.terminal.print('head: ' + args[0] + ': No such file', 'error'); - const n = args[1] ? parseInt(args[1]) : 10; - this.terminal.print(c.split('\n').slice(0, n).join('\n')); + // 2026-10-21: accept `head -40 f`, `head -n 40 f` and `head f 40`. + let n = 10, file = null; + 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-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')); }; this.commands['tail'] = (args) => { const self = this; @@ -254,7 +289,11 @@ class FileSystemModule { 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')); + // 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')); if (!follow) return; // 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) { @@ -308,6 +347,16 @@ class FileSystemModule { return this.terminal.print('sed: missing file operand (no piped input)', 'error'); } let subst = null, delMatch = null, delLine = null; + // 2026-10-21: `sed -n '10,40p' file` and `sed -n '7p' file` had no parser, so the + // most common "show me lines N–M" idiom failed with an unsupported-expression error. + const _range = /^(\d+)(?:,(\d+))?p$/.exec(expr); + if (_range) { + const _a = parseInt(_range[1], 10); + const _b = _range[2] ? parseInt(_range[2], 10) : _a; + const _lines = content.split('\n'); + this.terminal.print(_lines.slice(Math.max(0, _a - 1), Math.max(_a, _b)).join('\n')); + return; + } if (expr.startsWith('s/') && expr.length > 2) { const parts = []; let cur = ''; for (let i = 2; i < expr.length; i++) { @@ -381,12 +430,28 @@ class FileSystemModule { c.split('\n').sort().forEach(l => this.terminal.print(l)); }; this.commands['wc'] = (args) => { - if(!args[0]) return this.terminal.print('Usage: wc <filename>', 'warning'); - const content = this.terminal.fs.readFile(args[0]); - if(content === null || content === undefined) return this.terminal.print('wc: ' + args[0] + ': No such file', 'error'); + // 2026-10-21: accept -l / -w / -c (previously ANY flag was read as a filename, + // so `wc -l file` answered "wc: -l: No such file"). + const want = { l: false, w: false, c: false }; + let file = null; + (args || []).forEach(a => { + if (/^-[lwc]+$/.test(a)) a.slice(1).split('').forEach(k => { want[k] = true; }); + else if (/^-[a-zA-Z]+$/.test(a)) { /* ignore other flags */ } + else if (!file) file = a; + }); + if (!file) return this.terminal.print('Usage: wc [-l] [-w] [-c] <filename>', 'warning'); + const content = this.terminal.fs.readFile(file); + if (content === null || content === undefined) return this.terminal.print('wc: ' + file + ': No such file', 'error'); const lines = content.split('\n').length; const words = content.split(/\s+/).filter(w => w).length; - this.terminal.print(lines + ' ' + words + ' ' + content.length + ' ' + args[0]); + const chars = content.length; + const anyFlag = want.l || want.w || want.c; + const out = []; + if (want.l || !anyFlag) out.push(lines); + if (want.w || !anyFlag) out.push(words); + if (want.c || !anyFlag) out.push(chars); + if (anyFlag && !(want.l && want.w && want.c)) out.push(file); + this.terminal.print(out.join(' ')); }; this.commands['touch'] = (args) => { if(!args[0]) return this.terminal.print('Usage: touch <filename>', 'warning'); |