【Node.js】readlineで標準入力・ファイルを1行ずつ読む|question・for await・close

【Node.js】readlineで標準入力・ファイルを1行ずつ読む|question・for await・close Node.js

コマンドラインでユーザーに入力を求めたい、あるいは大きなファイルを1行ずつ処理したい——Node.jsでは標準のreadlineモジュールでこれらを実現できます。CLIツールでの対話的な入力受け取りや、巨大なログファイルをメモリを圧迫せずに読む処理に使えます。

注意したいのは、対話入力ではrl.close()を呼ばないとプログラムが終了しないこと、そしてターミナルからの入力とパイプ(|)での入力で挙動が変わることです。また、ファイルを1行ずつ読むときはfor awaitを使うと簡潔に書けます。この記事では、実機のNode.jsで確認しながら、readlineの使い方を整理します。

先に結論

  • 対話入力はreadline/promisesawait rl.question("質問")が簡潔です。
  • 古い書き方はコールバック版rl.question("質問", (answer) => {...})です。
  • ファイルを1行ずつ読むならfor await (const line of rl)が便利です。
  • 対話入力ではrl.close()を呼ばないとプログラムが終了しません
  • パイプ入力(echo ... | node)では、入力の終わり(EOF)で自動的に閉じます。
  • 大きなファイルでも、1行ずつ読めばメモリを圧迫しません。

コマンドライン引数の受け取りはprocess完全ガイド、ファイルの読み書き全般はfsでファイルを読み書きする、大量データの高速処理はストリームの使い方もあわせて参考になります。

スポンサーリンク

対話入力(readline/promises)

ユーザーに質問して入力を受け取るには、readline/promisesquestionが便利です。awaitで入力を待てるため、上から順に読める素直なコードになります。

await で入力を受け取る
import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

const rl = readline.createInterface({ input, output });

const name = await rl.question("名前は? ");
console.log(`こんにちは ${name} さん`);

rl.close();   // 【重要】これを呼ばないと終了しない

実機でも、await rl.question("名前は? ")田中を入力すると、受け取り: 田中が表示され、プログラムが正常終了することを確認しました。readline.createInterfaceinput(標準入力)とoutput(標準出力)を指定し、await rl.question(...)で入力を待ちます。最後にrl.close()を必ず呼びます(理由は後述)。readline/promisesはNode.js 17以降で使える、現在おすすめの書き方です。

コールバック版のquestion

古いコードでよく見るのがコールバック版です。readline/promisesなし)のquestionは、入力が終わるとコールバック関数で結果を受け取ります。

コールバックで受け取る
const readline = require("node:readline");

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

rl.question("入力してください: ", (answer) => {
  console.log("受け取り:", answer);
  rl.close();   // コールバックの中で閉じる
});

実機でも、コールバック版でhelloを入力すると受け取り: helloが表示されました。コールバック版は入力後の処理を関数の中に書くため、複数の質問を続けると入れ子が深くなりがちです。新しく書くなら、awaitで素直に書けるreadline/promisesのほうが読みやすくおすすめです。どちらの場合も、処理が終わったらrl.close()を呼びます。

rl.close()が必要な理由

readlineでつまずきやすいのが、プログラムが終わらない問題です。readlineは標準入力を開いたまま待ち続けるため、rl.close()を呼んで明示的に閉じないと、入力が済んでもプログラムが終了しません

close を忘れると終わらない
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

const answer = await rl.question("好きな言語は? ");
console.log(answer);

// rl.close() を呼ばないと…
//  → ターミナルからの対話入力では、ここでプログラムが止まったまま終わらない

rl.close();   // 必ず呼ぶ
対話入力ではclose()を忘れずに(パイプとは挙動が違う)

実機で確認したところ、入力の与え方によって挙動が変わります。ターミナルから対話的に入力する場合rl.close()を呼ばないと標準入力が開いたままになり、プログラムが終了せず待ち続けます。一方、echo "値" | node script.jsのようにパイプで入力を渡した場合は、入力の終わり(EOF)に達した時点で自動的に閉じられ、close()が無くてもプログラムは終了しました。つまり「パイプでは動いたのに、ターミナルで実行すると固まる」という現象は、この違いが原因です。対話的に使うことを想定するなら、必ずrl.close()を呼ぶのが鉄則です。readline/promisesでは、最後の質問のあとに1回呼べば十分です。

ファイルを1行ずつ読む(for await)

readlineのもう1つの便利な使い方が、ファイルを1行ずつ読むことです。fs.createReadStreamと組み合わせ、for await (const line of rl)で各行を順に処理します。ファイル全体を一度に読み込まないため、何GBもある巨大なファイルでもメモリを圧迫しません。

ファイルを1行ずつ処理
import * as readline from "node:readline";
import fs from "node:fs";

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

let count = 0;
// 1行ずつ取り出す(メモリ効率がよい)
for await (const line of rl) {
  count++;
  console.log(`${count}: ${line}`);
}
console.log("総行数:", count);
大きなファイルは1行ずつが正解

実機で、3行のテキストファイルをfor await (const line of rl)で読んだところ、1行目2行目3行目が順に取得でき、総行数も3と正しくカウントできました。この方法の利点は、ファイル全体をメモリに読み込まず、1行ずつ流して処理することです。fs.readFileSyncでファイル全体を読む方法は手軽ですが、巨大なファイルではメモリを使い切ってしまう恐れがあります。ログ解析や大きなCSVの処理など、サイズが大きい・行数が読めないファイルはreadlineで1行ずつ処理するのが安全です。各行には改行が含まれないため、自分でtrim()する必要もありません。

主な使い方まとめ

readlineの使い方をまとめます。

やりたいこと 書き方
対話入力(推奨) await rl.question("質問")(readline/promises)
対話入力(旧) rl.question("質問", (ans) => {...})
ファイルを1行ずつ for await (const line of rl)
入力元の指定 createInterface({ input, output })
終了 rl.close()(対話では必須)

よくある失敗

close()を呼ばずプログラムが終わらない

対話入力ではrl.close()が必須です。入力が済んだら呼びます。

パイプでは動くのにターミナルで固まる

パイプはEOFで自動的に閉じますが、対話入力はclose()が必要です。

大きなファイルをreadFileSyncで全部読む

メモリを圧迫します。readlinefor awaitで1行ずつ読みます。

コールバック版で質問を入れ子にしすぎる

読みにくくなります。readline/promisesawaitを使います。

各行を改行付きだと思って処理する

for awaitで得る行に改行は含まれません。そのまま使えます。

よくある質問

QNode.jsでユーザーの入力を受け取るには?
Areadlineモジュールを使います。readline/promisesconst ans = await rl.question("質問")と書くと、入力を待って受け取れます。最後にrl.close()を呼ぶのを忘れないでください。古いコードではコールバック版のrl.question("質問", (ans) => {...})も使われます。
Qプログラムが終了せず固まります。
Arl.close()を呼んでいない可能性が高いです。readlineは標準入力を開いたまま待つため、対話入力では明示的に閉じないと終了しません。なお、パイプ(echo ... | node)で入力した場合はEOFで自動的に閉じるため、ターミナルでだけ固まる、という違いが起こります。
Q大きなファイルを1行ずつ読むには?
Areadline.createInterface({ input: fs.createReadStream("file") })を作り、for await (const line of rl)で各行を順に処理します。ファイル全体をメモリに読み込まないため、巨大なファイルでもメモリを圧迫しません。各行に改行は含まれません。
Qreadlineとreadline/promisesの違いは?
Areadlineはコールバックで入力を受け取る従来のAPI、readline/promisesawaitで入力を待てる新しいAPI(Node.js 17以降)です。新しく書くなら、コードが読みやすいreadline/promisesがおすすめです。
Qfor awaitで読んだ行に改行は含まれますか?
A含まれません。for await (const line of rl)で得られる各lineは、末尾の改行が取り除かれた状態です。そのため、fs.readFileSyncで読んで自分で分割する場合と違い、改行を取り除く処理は不要です。

まとめ

  • 対話入力はreadline/promisesawait rl.question("質問")が簡潔です。
  • 古いコードはコールバック版rl.question("質問", (ans) => {...})
  • 対話入力ではrl.close()が必須。呼ばないと終了しません(パイプはEOFで自動)。
  • ファイルはfor await (const line of rl)で1行ずつ。改行は含まれません。
  • 大きなファイルは全部読まず、1行ずつ流して処理するとメモリを節約できます。

readlineは、CLIツールの対話入力と、大きなファイルの行単位処理という2つの場面で活躍します。「対話ではclose()を忘れない」「大きなファイルはfor awaitで1行ずつ」という2点を押さえれば、入力まわりとファイル処理をすっきり書けます。