Edited at

Nodeで対話型プログラム(readlineモジュールのasync iterator対応について)

NodeでCLIなソフトを作るとき、標準モジュールの readline を使うのだけれど「ユーザからの入力を同期的に待ち、それに応じた処理する」という一見楽勝な操作でさえ、煩雑な形の実装になってしまう。JavaScript/Nodeが非同期なAPIを提供することに起因する。

そのため、これまではreadline-syncというサードパーティ製のライブラリを使うことが多かったのだけれど Promise, await, async iterator を手に入れた今、(単純な要件なら)readline-sync を使わなくても 簡単に実現できる。

最近整理したところ (Node v11 から?) readline.createInterface が async iterable を返すようになっていたという公式ドキュメントの記載を見つけたので、スッキリ書けるようになっていることに気づいた。

以下 Node v12で動作確認しており、たぶんv11以降じゃないと動作しない


レシピ1: 2数を行ごとに受け取って和を表示

イテレータを手動管理するパターン

const readline = require('readline');

async function main () {
const rl = readline.createInterface({ input: process.stdin });
const ait = rl[Symbol.asyncIterator]();
const n1 = parseInt((await ait.next()).value, 10);
const n2 = parseInt((await ait.next()).value, 10);
rl.close();
console.log(`answer is ${n1 + n2}`);
}

main();

ユーザからの入力が2行であるとわかっている場合、rl[Symbol.asyncIterator]()のように明示的にイテレータを取得してnextを呼び出すことで値を取得する。rl.close()は明示的に呼び出さないといつまでも入力を待ち続けることになる(Ctrl+Dを押さないとプログラムが終了しなくなる)。


レシピ2: 可変個の数を行ごとに受け取り総和を表示

イテレータ管理は for-await-of に任せるパターン

const readline = require('readline');

async function main () {
const rl = readline.createInterface({ input: process.stdin });
let sum = 0;
for await (const val of rl) {
sum += parseInt(val, 10);
console.log(`accumulated: ${sum}`);
}
console.log(`answer is ${sum}`);
}

main();

入力の終わりを知らせるために Ctrl+D を押下する必要がある。その代わりイテレータの管理をしなくてもよい。forループの内部で何らかの脱出条件の判定をして明示的に rl.close() を呼び出すのもよいアイディアだと思う。