Node.js:如何从标准输入流中同步读取数据?

72

在node.js中是否可能同步读取标准输入?因为我正在用JavaScript编写一个JavaScript的Brainfuck编译器(只是为了好玩)。Brainfuck支持需要同步实现的读操作。

我尝试了以下代码:

const fs = require('fs');
var c = fs.readSync(0,1,null,'utf-8');
console.log('character: '+c+' ('+c.charCodeAt(0)+')');

但这只会产生这种输出:

fs:189
  var r = binding.read(fd, buffer, offset, length, position);
              ^
Error: EAGAIN, Resource temporarily unavailable
    at Object.readSync (fs:189:19)
    at Object.<anonymous> (/home/.../stdin.js:3:12)
    at Module._compile (module:426:23)
    at Module._loadScriptSync (module:436:8)
    at Module.loadSync (module:306:10)
    at Object.runMain (module:490:22)
    at node.js:254:10

节省时间,使用一个良好维护的npm库来抽象从stdin读取的过程,https://www.npmjs.com/package/get-stdin。 - Gajus
11个回答

69

你尝试过:

fs=require('fs');
console.log(fs.readFileSync('/dev/stdin').toString());

然而,它会等待整个文件被读入,而不像scanf或cin一样在遇到换行符\n时返回。


8
这个答案帮我省了很多重构时间 - 谢谢!看起来它在Windows上不起作用,但我不太担心这个。 - Jesse Hallett
1
@panzi 如果你想让它在每一行都阻塞,你需要实现自己的C++包装器来包装getline()或类似的函数。 - dhruvbird
5
非常方便,但有两个注意事项:(a)该解决方案不适用于 Windows(正如@JesseHallett所述),(b) 与交互式stdin输入展示出非标准的行为readFileSync()调用不像按行处理交互式输入,而是会一直阻塞,直到所有行都被接收(由@dhruvbird的免责声明暗示,但值得明确说明)。 - mklement0
3
注意事项3:似乎只读取了可用输入的前65536个字符。 - Armand
5
在v5+版本中,使用0代替/dev/stdin可以使该方法在Windows上也能够工作。但是,在v9.11.1中存在一个问题:当没有输入或stdin被关闭时会出现错误(bug)。 - mklement0
显示剩余3条评论

30

经过一番摆弄,我找到了答案:

process.stdin.resume();
var fs = require('fs');
var response = fs.readSync(process.stdin.fd, 100, 0, "utf8");
process.stdin.pause();

响应将是一个数组,第一个索引是输入到控制台的数据,第二个索引将是数据长度,包括换行符。

很容易确定当您console.log(process.stdin)时,会枚举所有属性,包括标记为fd的属性,这显然是fs.readSync()的第一个参数的名称。

享受! : D


2
即使在v0.7.5-pre上,它仍然会给出与从STDIN进行的普通fs.readSync相同的“错误:UNKNOWN,未知错误”。那个版本的node和操作系统可以正常工作? - rjp
@rjp 我刚刚仔细检查了代码,在Windows7和v0.6.7上运行良好。我现在正在我的Linux机器上安装0.6.12,安装完成后我会告诉你结果的。 - Marcus Pope
2
@rjp - 是的,看起来底层依赖库中文件读取存在一个 bug... 不是 bug,只是一个未考虑到的注意事项。我真的不是很擅长 C 开发,但看起来 open() 调用 stdin 时会失败,如果已经打开的话。为了解决这个问题,我认为他们需要在句柄为 0 或 1 的情况下 dup() 句柄,并在完成后将句柄 dup2() 回去。但我可能非常错误 :D。我会在 GitHub 上开一个工单,让一些真正的 C 开发人员给你正确的答案。 - Marcus Pope
2
这种方法在原则上仍然有效(有限制),但是自从 node.js v0.10.4 接口发生变化后,本答案中的代码已经无法使用了;请参考我的回答。 - mklement0

24

以下是 Marcus Pope 的回答的更新版本,适用于 node.js v0.10.4:

请注意:

  • 总的来说,node 的流接口仍处于不稳定状态(双关语半意味深长),并且在 node.js v0.10.4 中仍然被归类为 2-不稳定
  • 不同平台的行为略有不同;我看过 OS X 10.8.3Windows 7:主要区别在于:在 Windows 7 上只有通过同步方式读取交互式stdin输入(逐行键入终端)才能正常工作。

这是更新后的代码,以256字节块同步从stdin读取,直到没有更多输入可用:

var fs = require('fs');
var BUFSIZE=256;
var buf = new Buffer(BUFSIZE);
var bytesRead;

while (true) { // Loop as long as stdin input is available.
    bytesRead = 0;
    try {
        bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUFSIZE);
    } catch (e) {
        if (e.code === 'EAGAIN') { // 'resource temporarily unavailable'
            // Happens on OS X 10.8.3 (not Windows 7!), if there's no
            // stdin input - typically when invoking a script without any
            // input (for interactive stdin input).
            // If you were to just continue, you'd create a tight loop.
            throw 'ERROR: interactive stdin input not supported.';
        } else if (e.code === 'EOF') {
            // Happens on Windows 7, but not OS X 10.8.3:
            // simply signals the end of *piped* stdin input.
            break;          
        }
        throw e; // unexpected exception
    }
    if (bytesRead === 0) {
        // No more stdin input available.
        // OS X 10.8.3: regardless of input method, this is how the end 
        //   of input is signaled.
        // Windows 7: this is how the end of input is signaled for
        //   *interactive* stdin input.
        break;
    }
    // Process the chunk read.
    console.log('Bytes read: %s; content:\n%s', bytesRead, buf.toString(null, 0, bytesRead));
}

2
这是我一直以来在输入较长时捕获STDIN全部内容的唯一方法。 - user59278
while(true)break?如果bytesRead === 0是你的条件,为什么要使用break语句? - Sebastian
你的条件不是“true”。你的条件是当标准输入中有字节并且在处理过程中没有错误的情况下。这就是我说的。这是意大利面条式代码。 - Sebastian
1
你不需要为此引入一个变量,只需否定包含“break”语句的if语句即可。您可以引入一个错误变量,当读取时发现任何错误时,该变量为TRUE。是的,它很值得,您的代码就是您的文档。“while(true)”对我来说没有任何意义。“while(bytesRead != 0 && !error)”才有意义。 - Sebastian
2
@Sebastian:退一步说,答案中的代码有很好的注释,并讨论了与手头问题相关的问题。无论您对于意大利面式代码的担忧是否有价值都与问题本身无关,这不是讨论它们的地方:您的评论会给未来的读者带来干扰。我已删除以前的评论以最小化干扰。 - mklement0
1
@Sebastian,也许这些粗糙的边缘会阻止人们直接复制/粘贴答案,这是软件公司法律部门所不喜欢的! :) 答案是为了满足问题而提供的,而不是代码审查! - Darius

19

我不知道这是什么时候出现的,但这是一个有帮助的进展: http://nodejs.org/api/readline.html

var readline = require('readline');

var rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false
});

rl.on('line', function (cmd) {
  console.log('You just typed: '+cmd);
});

现在我可以从标准输入一次读取一行。开心的日子。


3
友情提醒:截至Node.js v0.10.4,readline模块仍被归类为“2-不稳定”。 - mklement0
@mklement0 我认为 2 - 不稳定 意味着 API 不是固定的,可能会发生变化。http://nodejs.org/api/documentation.html#documentation_stability_index。我不确定它对使用稳定性意味着什么。 - Alain O'Dea
@AlainO'Dea:谢谢;引用您链接的页面:“如果合理,将保持向后兼容性。”我理解为:“可能不会改变,但我们保留这样做的权利”,从用户的角度来看,这意味着:“该功能已经存在,很可能使用其现有API,但未来有可能出现破坏性的API更改。” - mklement0
52
这个技术不是同步的。 - Barry Kelly
@BarryKelly rl.prompt 可以被 await;或者它实际上可以被同步化。 - jpaugh
显示剩余3条评论

16

2
为什么这个回答排在这么后面?这是最好的答案!它用一行代码实现了 OP 想要的功能! - schubakah
我发现它很有用!我在几个项目中使用过,可以确认它确实实现了它所说的功能 :) - Nate Ferrero
这个无法使用管道输入。foo | bar其中 bar 使用 readline-sync 将尝试从终端读取,而不是从标准输入读取。 - Barry Kelly

8

以下是使用“async await”实现的内容。在下面的代码中,从标准输入获取输入数据后,通过使用“process.stdin.pause();”停止等待数据的标准输入。

process.stdin.setEncoding('utf8');

// This function reads only one line on console synchronously. After pressing `enter` key the console will stop listening for data.
function readlineSync() {
    return new Promise((resolve, reject) => {
        process.stdin.resume();
        process.stdin.on('data', function (data) {
            process.stdin.pause(); // stops after one line reads
            resolve(data);
        });
    });
}

// entry point
async function main() {
    let inputLine1 = await readlineSync();
    console.log('inputLine1 = ', inputLine1);
    let inputLine2 = await readlineSync();
    console.log('inputLine2 = ', inputLine2);
    console.log('bye');
}

main();

2
请解释您的代码如何运作以及为什么能够运作,以帮助未来遇到类似问题的人们。 - Tom M
1
非常感谢,这正是我所需要的。已确认在Node 10中可行。 - Fr0sT
这也帮助了我。这是一个好回答,但您应该解释一下为什么要这样做,以便任何人都能理解。 - The24thDS
3
此解决方案不是同步的。 - Ian Liu Rodrigues

7

重要提示: 我刚刚从Node.js的贡献者那里得知,.fd是未记录的,仅用于内部调试。因此,代码不应引用它,并应使用fs.open/openSync手动打开文件描述符。

在Node.js 6中,值得注意的是,通过其构造函数使用new创建Buffer实例已被弃用,因为它具有不安全性。应该改用Buffer.alloc代替:

'use strict';

const fs = require('fs');

// small because I'm only reading a few bytes
const BUFFER_LENGTH = 8;

const stdin = fs.openSync('/dev/stdin', 'rs');
const buffer = Buffer.alloc(BUFFER_LENGTH);

fs.readSync(stdin, buffer, 0, BUFFER_LENGTH);
console.log(buffer.toString());
fs.closeSync(stdin);

此外,应该仅在必要时打开和关闭文件描述符;每次想要从stdin读取时都这样做会导致不必要的开销。

1
重要提示:.fd目前已有文档记录 - Rolf

6
function read_stdinSync() {
    var b = new Buffer(1024)
    var data = ''

    while (true) {
        var n = fs.readSync(process.stdin.fd, b, 0, b.length)
        if (!n) break
        data += b.toString(null, 0, n)
    }
    return data
}

2
这真是一份宝藏。感谢分享。我将在我的开源节点模块中实现它。测试后我会回来分享节点模块的链接。谢谢! - Vikas Gautam
这是基于您的想法开发的新模块: line-reader - Vikas Gautam
@VikasGautam 谢谢你,干得好。你是一次性读取整个 stdin 还是逐行产出的? - exebook
line-reader是一个生成器函数,它一次读取64 * 1024字节,直到从文件或stdin中读取所有内容,并在每个.next()调用或迭代中输出单行。请查看index.ts。我认为,我还应该将defaultChunkSize作为用户参数。我即将推送更新。 - Vikas Gautam

6
以下代码从标准输入中读取同步数据。输入一直读取到换行符 / 回车键为止。该函数返回一个已剔除换行符(\n)和回车符(\r)的输入字符串。这应该与Windows、Linux和Mac OSX兼容。添加了对Buffer.alloc的条件调用(new Buffer(size)目前已过时,但一些较旧版本缺少Buffer.alloc)。
function prompt(){
    var fs = require("fs");

    var rtnval = "";

    var buffer = Buffer.alloc ? Buffer.alloc(1) : new Buffer(1);

    for(;;){
        fs.readSync(0, buffer, 0, 1);   //0 is fd for stdin
        if(buffer[0] === 10){   //LF \n   return on line feed
            break;
        }else if(buffer[0] !== 13){     //CR \r   skip carriage return
            rtnval += new String(buffer);
        }
    }

    return rtnval;
}

fs.readSync 调用在读取到文件结尾后会抛出异常,因此请务必将其包装在 try/catch 中。 - Bbrk24

3

我在node 0.10.24/linux上使用了以下解决方法:

var fs = require("fs")
var fd = fs.openSync("/dev/stdin", "rs")
fs.readSync(fd, new Buffer(1), 0, 1)
fs.closeSync(fd)

这段代码等待用户按下回车键。如果在按下回车键之前输入了一个字符,则它会从该行读取一个字符。其他字符将保留在控制台缓冲区中,并将在后续调用readSync时读取。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接