Node.js で標準入力から同期的に一行読み取る


前座:既存の手法(非同期 or 入力全行)

主に競技プログラミングなどのシーンで標準入力からデータが受け渡されることは多いです。競技でなくとも、同様の形式でコーディングテストが行われることもあります。

こういったときに Node.js だと少し煩雑な手順を取る必要があります。

readline

const rl = require("readline").createInterface({
  input: process.stdin,
  output: process.stdout,
});

const lines = [];

rl.on("line", (line) => {
  lines.push(line);
  // ... 多分入力処理
});

rl.on("close", () => {
  // 入力が閉じたのでデータは受け取った
  // データを全て受け取った前提の処理がここ
});

何も知らない人から見ると呪文です。
上から順に実行されるのはそうなのですが、rl.on では「イベントに対するコールバックを設定」という処理が行われるだけなので rl.on("line", ...); の直後に処理を書いたとしてもデータが入っているとは限らないのが罠です。
インタラクティブな場面では気持ちがいいかもしれません。

fs.readFileSync

const fs = require("fs");
const wholeInput = fs.readFileSync("/dev/stdin", "utf8");
const inputLines = wholeInput.split("\n");

const n = Number(inputLines[0]);
// ...

こちらは比較的まともです。標準入力からの入力を全て文字列として受け取るものです。

強いて欠点を挙げるとすれば以下のようになるでしょう。

  • インタラクティブな場面には向かない
  • 行を取るには split してインデックスを指定する必要がある

作ってみた

正直上の二つがあれば特に困らないのですが、例えば ruby や python などで簡単に利用できる getsinput() などのような関数が Node.js にもあればなあ、と思い作ってみました。

使う

ローカルにインストールして利用する場合は npm で直接 github 上のリポジトリを指定してください。

(10/28 追記)npm に publish しました。
https://www.npmjs.com/package/node-gets

$ npm install --save node-gets

GitHub にあるリポジトリを指定してインストールすることもできます(<owner>/<repository> と書けば GitHub からインストールできるらしい)。

$ npm install --save cwd-k2/node-gets

(10/28 追記終わり)

以下のような書き心地になります。

const gets = require("node-gets").createGets();

const n = Number(gets().trim());
const a = gets().trim().split(" ").map(Number);
// ...
  • 行入力末尾の改行文字などは付いたまま
  • EOF 的な感じで入力がなくなると undefined を返すようになる

問題点

  • テストがない
  • ベンチマークがない
  • 実装上の問題
    • 一行読むたびに内部のバッファでコピーが発生している(リングバッファにすべきなど色々あると思います)
    • \n 以外の文字での分割に対応していない

最後に

他に問題点があったり、直してやったぞということがあれば issue や PR をお願いします。

追記

9/23 追記(簡単 gets 風味の fs.readFileSync

競プロで利用する程度なら次の簡単なスニペットを持っておくのもいいかもな、というのが最近の結論です。

const line =
  ((l, i = 0) => () => l[i++])
  (require("fs").readFileSync(0, "utf8").replace(/\n$/, '').split(/\n/));

次のように利用することができるので、getsinput() の気持ちにはなれるかも(願望)。

const n = Number(line());
for (let i = 0; i < n; i++) {
  const [name, type] = line().split(' ');
  console.log({ name, type });
}