以下の内容はhttps://let.blog.jp/tag/readlineより取得しました。


Node.js の readline のプロンプト
Node.js で 1 行標準入力から読み取ってなにかの処理をして を繰り返すとき よく使うのが標準モジュールの readline です
簡単に使えますが 単にループで読み取るだけだとプロンプトは自動で出力されません

import readline from "node:readline"

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

for await (const line of rl) {
console.log({ line })
}

これを実行して文字を入力すると エンターを押すたびに { line: "入力値" } のようなのが表示されます

user@DESKTOP03 ~> node rl.js
a
{ line: 'a' }
123
{ line: '123' }

入力するとき何も表示されていないので入力していい状態なのか分かりづらいです

createInterface の prompt の初期値は "> " になっているので紛らわしいのですが 手動で prompt メソッドを呼び出さないとプロンプトは出力されません
また output を標準出力に指定していないと prompt メソッドを呼び出しても画面に表示できません

こうすることで表示されます

import readline from "node:readline"

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

rl.prompt()
for await (const line of rl) {
console.log({ line })
rl.prompt()
}
user@DESKTOP03 ~> node rl.js
> 1
{ line: '1' }
> xx
{ line: 'xx' }
> ⏎

ただこれだと prompt の呼び出しが分かれますし なんか気持ち悪いです
自分でこれを書きたくないし 自動で内部的にやってほしいものなので asyncIterator を置き換えることにしました

[auto-prompt.js]
const org_async_iterator = Symbol("org-async-iterator")

async function* asyncIterator(...args) {
this.prompt()
for await (const item of this[org_async_iterator].call(this, ...args)) {
yield item
this.prompt()
}
}

const autoPrompt = (rl) => {
rl[org_async_iterator] = rl[Symbol.asyncIterator]
rl[Symbol.asyncIterator] = asyncIterator
}

export default autoPrompt
import readline from "node:readline"
import autoPrompt from "./auto-prompt.js"

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

autoPrompt(rl)

for await (const line of rl) {
console.log({ line })
}
user@DESKTOP03 ~> node rl.js
> foo
{ line: 'foo' }
> bar
{ line: 'bar' }
> ⏎

autoPrompt 側は仕方ないとして 普段使う部分ではスッキリと書けます

標準入力を使うならこの使い方で十分そうですが 全インスタンスに反映させるなら Interface の prototype の方を書き換えることもできます

autoPrompt(readline.Interface.prototype)
Node.js でファイルを 1 行ずつ読み取る
ビルトインの readline モジュールはユーザーからの入力受付用かと思っていましたが ファイルの読み取りにも使えるみたいです

const fs = require("fs")
const readline = require("readline")

const fn = async () => {
fs.writeFileSync("test.txt", "foo\nbar\nbaz")

const rl = readline.createInterface({ input: fs.createReadStream("test.txt") })
for await (const line of rl) {
console.log({ line })
}
}

fn()
{ line: 'foo' }
{ line: 'bar' }
{ line: 'baz' }

readline で作成した Interface は async iterator を持ってるので for-await-of で使えます

const readline = require("readline")

const rl = readline.createInterface({
input: { on(){}, resume() {} }
})

console.log(rl[Symbol.iterator])
// undefined
console.log(rl[Symbol.asyncIterator])
// [Function (anonymous)]

Node.js 17 から readline に Promise API が追加されています
これは question みたいな関数がコールバックか Promise かの違いで 行ごとに読み取るだけなら 16 まででも使えます
18 以降でも変わりないです

注意しないといけないのがこういうケースです

const fs = require("fs")
const readline = require("readline")

const fn = async () => {
fs.writeFileSync("test.txt", "foo\nbar\nbaz")

const rl = readline.createInterface({ input: fs.createReadStream("test.txt") })

await new Promise(r => setTimeout(r, 100))

console.log(1)
for await (const line of rl) {
console.log({ line })
}
console.log(2)
}

fn()

readline の Interface を作ってから for-await-of で読むまでに非同期処理を挟む場合です
これを実行すると 1 だけが出力されます
for-await-of では待機したままデータなしになって 解決しない Promise という扱いで それ以降実行できるものがなくプロセスが終了します
その結果 2 を出力する console.log にたどり着かないので出力は 1 だけです

内部で stream の resume が呼び出されるので自動で読み進めてしまうみたいです
非同期処理を挟まず for-await-of を実行すると asyncIterator の作成処理で stream のイベントが起きる前にリスナを設定できます
その結果 正常に line イベントなどを受け取れて期待どおりに動作します
しかし 非同期処理を挟むと先にイベントが起きてしまって stream が close されたあとにリスナをつけることになるのでなんのイベントも起きず解決されない Promise になるということみたいです



以上の内容はhttps://let.blog.jp/tag/readlineより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14