// modules/test/test-shell-stdin.js — run: node // Pipes: wc/head/tail must read stdin when no file operand is given, and must still read the file // when one is (ignoring a stale pipe buffer). The channel is terminal._pipeInput — the property // the pipe wrapper in modules/ext/shell.js sets before dispatching each stage. const fsMod = require('fs'), path = require('path'); const ROOT = path.join(__dirname, '..'); const src = fsMod.readFileSync(path.join(ROOT, 'core/module-fs.js'), 'utf8'); const FileSystemModule = new Function(src + '\nreturn FileSystemModule;')(); let fails = 0, passes = 0; const ok = (name, cond, detail) => { if (cond) { passes++; return; } fails++; console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); }; // A fake terminal whose fs knows only the files it is handed: there is no __pipebuf__ magic here, // so a handler that quietly searched the cwd instead of reading stdin would fail, not pass. const mk = (files, pipeInput) => { const out = []; const term = { fs: { readFile: (p) => (files && Object.prototype.hasOwnProperty.call(files, p)) ? files[p] : null, getCurrentDir: () => ({ name: 'fake', content: {} }), }, _pipeInput: pipeInput === undefined ? null : pipeInput, print: (msg) => out.push(String(msg)), }; const m = new FileSystemModule(); m.terminal = term; m.registerCommands(); return { m, out }; }; const last = (out) => out.length ? out[out.length - 1] : ''; // (g) grep over a pipe must search the piped text, never the working directory. // Measured 2026-10-27 on v4.6.115: `cat /gdir/in.txt | grep zzz` answered // "other.txt:1: zzz in a cwd file" — a file that was never piped. { const decoy = { readFile: () => null, getCurrentDir: () => ({ name: 'gdir', content: { 'other.txt': { type: 'file', content: 'zzz in a cwd file' }, 'in.txt': { type: 'file', content: 'aaa\nbbb\nccc' }, } }), }; const out = []; const term = { fs: decoy, _pipeInput: 'aaa\nbbb\nccc', print: (m) => out.push(String(m)) }; const m = new FileSystemModule(); m.terminal = term; m.registerCommands(); m.commands.grep(['zzz']); ok('G1 grep over a pipe does not search the cwd', !out.join('|').includes('other.txt'), JSON.stringify(out)); ok('G2 a non-match over a pipe reports no matches', out.join('|').includes('(no matches)'), JSON.stringify(out)); out.length = 0; m.commands.grep(['bbb']); ok('G3 a match prints the line number without a filename', out.length === 1 && out[0] === '2: bbb', JSON.stringify(out)); out.length = 0; m.commands.grep(['-c', 'b']); ok('G4 grep -c over a pipe counts stdin lines', out.length === 1 && out[0] === '1', JSON.stringify(out)); } // (a) wc -l over 3 lines of stdin { const { m, out } = mk({}, 'alpha\nbeta\ngamma'); m.commands.wc(['-l']); ok('A1 wc -l on 3 piped lines prints 3', out.length === 1 && out[0].trim() === '3', JSON.stringify(out)); } // (b) wc -c counts characters, not lines { const { m, out } = mk({}, 'abcdef'); m.commands.wc(['-c']); ok('B1 wc -c on "abcdef" piped prints 6', out.length === 1 && out[0].trim() === '6', JSON.stringify(out)); const w = mk({}, 'abcdef'); w.m.commands.wc(['-w']); ok('B2 wc -w on one piped word prints 1', w.out.length === 1 && w.out[0].trim() === '1', JSON.stringify(w.out)); const l = mk({}, 'abcdef'); l.m.commands.wc(['-l']); ok('B3 wc -l on the same piped text still prints 1 (so -c is not counting lines)', l.out.length === 1 && l.out[0].trim() === '1', JSON.stringify(l.out)); } // (c) head -n 2 returns the first 2 piped lines { const { m, out } = mk({}, 'alpha\nbeta\ngamma'); m.commands.head(['-n', '2']); ok('C1 head -n 2 on piped stdin prints 2 lines', out.length === 1 && out[0] === 'alpha\nbeta', JSON.stringify(out)); const d = mk({}, 'alpha\nbeta\ngamma'); d.m.commands.head([]); ok('C2 head with no operand/number defaults to 10 piped lines', d.out.length === 1 && d.out[0] === 'alpha\nbeta\ngamma', JSON.stringify(d.out)); } // (d) tail -n 1 returns the last piped line { const { m, out } = mk({}, 'alpha\nbeta\ngamma'); m.commands.tail(['-n', '1']); ok('D1 tail -n 1 on piped stdin prints the last line', out.length === 1 && out[0] === 'gamma', JSON.stringify(out)); const t = mk({}, 'alpha\nbeta\ngamma'); t.m.commands.tail(['-2']); ok('D2 tail -2 on piped stdin prints the last 2 lines', t.out.length === 1 && t.out[0] === 'beta\ngamma', JSON.stringify(t.out)); const nl = mk({}, 'alpha\nbeta\ngamma\n'); nl.m.commands.tail(['-n', '1']); ok('D3 tail -n 1 ignores a trailing newline (no blank line)', nl.out.length === 1 && nl.out[0] === 'gamma', JSON.stringify(nl.out)); } // (e) a real file operand still wins: the file is read and the pipe buffer is ignored { const files = { '/probe.txt': 'one\ntwo\nthree\nfour' }; const { m, out } = mk(files, 'PIPED\nJUNK\nHERE'); m.commands.wc(['-l', '/probe.txt']); ok('E1 wc -l counts the file (4), not stdin', out.length === 1 && out[0].trim() === '4 /probe.txt', JSON.stringify(out)); const h = mk(files, 'PIPED\nJUNK\nHERE'); h.m.commands.head(['-n', '1', '/probe.txt']); ok('E2 head -n 1 prints the file\'s first line', h.out.length === 1 && h.out[0] === 'one', JSON.stringify(h.out)); const t = mk(files, 'PIPED\nJUNK\nHERE'); t.m.commands.tail(['-n', '1', '/probe.txt']); ok('E3 tail -n 1 prints the file\'s last line', t.out.length === 1 && t.out[0] === 'four', JSON.stringify(t.out)); const d = mk(files, 'PIPED\nJUNK\nHERE'); d.m.commands.wc(['/probe.txt']); // Real behaviour (verified identical on the pre-change file): with no flag and a single // file, wc prints the bare counts and no name — the name is only appended when there are // several rows or when a flag was given. ok('E4 wc with no flags keeps its long format (l w c, no name for one file)', d.out.length === 1 && d.out[0] === '4 4 18' && d.out[0].indexOf('PIPED') === -1, JSON.stringify(d.out)); } // (f) no operand and no pipe must still be usage, not a crash or a silent empty read { const w = mk({}, null); w.m.commands.wc(['-l']); ok('F1 wc with no file and no stdin prints usage', /^Usage: wc/.test(w.out[0] || ''), JSON.stringify(w.out)); const h = mk({}); h.m.commands.head(['-n', '2']); ok('F2 head with no file and no stdin prints usage', /^Usage: head/.test(h.out[0] || ''), JSON.stringify(h.out)); const t = mk({}, null); t.m.commands.tail(['-n', '1']); ok('F3 tail with no file and no stdin prints usage', /^Usage: tail/.test(t.out[0] || ''), JSON.stringify(t.out)); } console.log(passes + ' passed, ' + fails + ' failed'); if (fails) process.exit(1);