From 34a9de67b16177aaa6477f71262a17612e69d125 Mon Sep 17 00:00:00 2001 From: Lellansin Date: Sun, 20 Sep 2026 22:23:10 +0800 Subject: [PATCH 1/2] feat: expose stdin EOF state --- README.md | 24 +++++++++ lib/gets.js | 14 ++++- lib/scanf.d.ts | 6 +++ lib/scanf.js | 7 +++ test/eof.js | 140 +++++++++++++++++++++++++++++++++++++++++++++++++ test/gets.js | 60 +++++++++++++++++++++ 6 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 test/eof.js diff --git a/README.md b/README.md index 7cc048a..c510dd3 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,30 @@ result { } ``` +### Detect end of input + +After calling `scanf()`, use `scanf.isEOF()` to check whether the read reached +the end of standard input. This is different from an empty line or input that +does not match the requested format. + +```javascript +var scanf = require('scanf'); + +while (true) { + var values = scanf('%d %d %d'); + + if (scanf.isEOF()) { + break; + } + + console.log(values[0], values[1], values[2]); +} +``` + +EOF is reported only after a read attempts to go past the available input. In +a terminal, send EOF with `Ctrl+D` on macOS/Linux, or `Ctrl+Z` followed by +Enter on Windows. + ## sscanf REPL diff --git a/lib/gets.js b/lib/gets.js index ebeca8f..36637e3 100644 --- a/lib/gets.js +++ b/lib/gets.js @@ -10,10 +10,12 @@ var bytesRead; var createGets = function(customFs) { var localFs = customFs || fs; + var eof = false; - return function() { + var read = function() { var fd = process.stdin.fd; bytesRead = 0; + eof = false; try { bytesRead = localFs.readSync(fd, buf, 0, BUFSIZE); @@ -29,6 +31,7 @@ var createGets = function(customFs) { } else if (e.code === 'EOF') { // Happens on Windows 7, but not OS X 10.8.3: // simply signals the end of *piped* stdin input. + eof = true; return ''; } throw e; // unexpected exception @@ -40,14 +43,21 @@ var createGets = function(customFs) { // of input is signaled. // Windows 7: this is how the end of input is signaled for // *interactive* stdin input. + eof = true; return ''; } // Process the chunk read. - var content = buf.toString(undefined, 0, bytesRead - 1); + var content = buf.toString(undefined, 0, bytesRead).replace(/\r?\n$/, ''); return content; }; + + read.isEOF = function() { + return eof; + }; + + return read; }; module.exports = createGets(); diff --git a/lib/scanf.d.ts b/lib/scanf.d.ts index 7e347ef..8c7aefa 100644 --- a/lib/scanf.d.ts +++ b/lib/scanf.d.ts @@ -89,6 +89,12 @@ declare namespace __node_scanf { ------------------------------------------------------------ */ namespace scanf { + /** + * Returns whether the most recent scanf operation attempted to read from + * stdin and reached the end of the input stream. + */ + function isEOF(): boolean; + /** *

Reads formatted data from string.

* diff --git a/lib/scanf.js b/lib/scanf.js index 9e344ff..bebf848 100644 --- a/lib/scanf.js +++ b/lib/scanf.js @@ -46,6 +46,13 @@ var scanf = (module.exports = function(format) { scanf.throw = true; +/** + * Returns whether the most recent scanf read reached the end of stdin. + */ +scanf.isEOF = function() { + return stdin_flag && gets.isEOF(); +}; + module.exports.sscanf = function(str, format) { if (typeof str !== 'string' || !str.length) { return null; diff --git a/test/eof.js b/test/eof.js new file mode 100644 index 0000000..22549ee --- /dev/null +++ b/test/eof.js @@ -0,0 +1,140 @@ +var childProcess = require('child_process'); +var path = require('path'); +var should = require('should'); + +describe('scanf', function() { + describe('#isEOF', function() { + var modulePath = path.resolve(__dirname, '..'); + + function runWithInput(input, statements) { + var script = + 'var scanf = require(' + JSON.stringify(modulePath) + ');' + statements; + var result = childProcess.spawnSync(process.execPath, ['-e', script], { + input: input + }); + + if (result.status !== 0) { + throw new Error(result.stderr.toString()); + } + + return JSON.parse(result.stdout.toString()); + } + + it('should be false before stdin is read', function() { + var result = runWithInput( + '', + 'process.stdout.write(JSON.stringify(scanf.isEOF()));' + ); + + should.strictEqual(result, false); + }); + + it('should become true after reading past complete input', function() { + var result = runWithInput( + '1 2 3\n', + 'var values = scanf("%d %d %d");' + + 'var afterValues = scanf.isEOF();' + + 'var eofValues = scanf("%d %d %d");' + + 'process.stdout.write(JSON.stringify({' + + 'values: values,' + + 'afterValues: afterValues,' + + 'eofValues: eofValues,' + + 'afterEOF: scanf.isEOF()' + + '}));' + ); + + should.deepEqual(result, { + values: [1, 2, 3], + afterValues: false, + eofValues: [null, null, null], + afterEOF: true + }); + }); + + it('should preserve the final value when input has no trailing newline', function() { + var result = runWithInput( + '1 2 3', + 'var values = scanf("%d %d %d");' + + 'process.stdout.write(JSON.stringify({' + + 'values: values,' + + 'eof: scanf.isEOF()' + + '}));' + ); + + should.deepEqual(result, { + values: [1, 2, 3], + eof: false + }); + }); + + it('should distinguish a format mismatch from EOF', function() { + var result = runWithInput( + 'not-a-record\n', + 'var value = scanf("value=%d");' + + 'process.stdout.write(JSON.stringify({' + + 'value: value,' + + 'eof: scanf.isEOF()' + + '}));' + ); + + should.deepEqual(result, { + value: null, + eof: false + }); + }); + + it('should report EOF reached while reading an incomplete record', function() { + var result = runWithInput( + '1 2\n', + 'var values = scanf("%d %d %d");' + + 'process.stdout.write(JSON.stringify({' + + 'values: values,' + + 'eof: scanf.isEOF()' + + '}));' + ); + + should.deepEqual(result, { + values: [1, 2, null], + eof: true + }); + }); + + it('should remain true across repeated reads after EOF', function() { + var result = runWithInput( + '', + 'scanf("%d");' + + 'var first = scanf.isEOF();' + + 'scanf("%d");' + + 'process.stdout.write(JSON.stringify({' + + 'first: first,' + + 'second: scanf.isEOF()' + + '}));' + ); + + should.deepEqual(result, { + first: true, + second: true + }); + }); + + it('should not report stdin EOF after sscanf', function() { + var result = runWithInput( + '', + 'scanf("%d");' + + 'var before = scanf.isEOF();' + + 'var value = scanf.sscanf("42", "%d");' + + 'process.stdout.write(JSON.stringify({' + + 'before: before,' + + 'value: value,' + + 'after: scanf.isEOF()' + + '}));' + ); + + should.deepEqual(result, { + before: true, + value: 42, + after: false + }); + }); + }); +}); diff --git a/test/gets.js b/test/gets.js index 7ce5b42..e6f7b8f 100644 --- a/test/gets.js +++ b/test/gets.js @@ -28,6 +28,7 @@ describe('gets', function() { var result = gets(); should.strictEqual(result, ''); + should.strictEqual(gets.isEOF(), true); }); it('should return empty string when EOF is reached', function() { @@ -44,6 +45,7 @@ describe('gets', function() { var result = gets(); should.strictEqual(result, ''); + should.strictEqual(gets.isEOF(), true); }); it('should read and trim newline-terminated input', function() { @@ -59,6 +61,64 @@ describe('gets', function() { var result = gets(); should.strictEqual(result, 'hello'); + should.strictEqual(gets.isEOF(), false); + }); + + it('should not treat an empty line as EOF', function() { + var gets = loadGetsWith({ + readSync: function(fd, buffer) { + buffer.write('\n'); + return 1; + } + }); + + var result = gets(); + should.strictEqual(result, ''); + should.strictEqual(gets.isEOF(), false); + }); + + it('should preserve input that does not end with a newline', function() { + var gets = loadGetsWith({ + readSync: function(fd, buffer) { + buffer.write('hello'); + return 5; + } + }); + + var result = gets(); + should.strictEqual(result, 'hello'); + should.strictEqual(gets.isEOF(), false); + }); + + it('should trim a trailing CRLF sequence', function() { + var gets = loadGetsWith({ + readSync: function(fd, buffer) { + buffer.write('hello\r\n'); + return 7; + } + }); + + var result = gets(); + should.strictEqual(result, 'hello'); + should.strictEqual(gets.isEOF(), false); + }); + + it('should reset EOF state after a successful read', function() { + var reads = 0; + var gets = loadGetsWith({ + readSync: function(fd, buffer) { + reads++; + if (reads === 1) return 0; + + buffer.write('hello\n'); + return 6; + } + }); + + should.strictEqual(gets(), ''); + should.strictEqual(gets.isEOF(), true); + should.strictEqual(gets(), 'hello'); + should.strictEqual(gets.isEOF(), false); }); it('should not call openSync (prevents fd leak #46)', function() { From 8213b98fe6f1f90ea100d9d202b774b41382c8f9 Mon Sep 17 00:00:00 2001 From: Lellansin Date: Sun, 20 Sep 2026 22:49:28 +0800 Subject: [PATCH 2/2] docs: link EOF guide from contents --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c510dd3..553afa0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Do you want a `sscanf` like function to parse format strings? * [`Directly return`](#directly-return) * [`Array return`](#array-return) * [`Json return`](#json-return) + * [`Detect end of input`](#detect-end-of-input) * [`sscanf`](#sscanf) ## Installation