コマンドラインでユーザーに入力を求めたい、あるいは大きなファイルを1行ずつ処理したい——Node.jsでは標準のreadlineモジュールでこれらを実現できます。CLIツールでの対話的な入力受け取りや、巨大なログファイルをメモリを圧迫せずに読む処理に使えます。
注意したいのは、対話入力ではrl.close()を呼ばないとプログラムが終了しないこと、そしてターミナルからの入力とパイプ(|)での入力で挙動が変わることです。また、ファイルを1行ずつ読むときはfor awaitを使うと簡潔に書けます。この記事では、実機のNode.jsで確認しながら、readlineの使い方を整理します。
- 対話入力は
readline/promisesのawait rl.question("質問")が簡潔です。 - 古い書き方はコールバック版
rl.question("質問", (answer) => {...})です。 - ファイルを1行ずつ読むなら
for await (const line of rl)が便利です。 - 対話入力では
rl.close()を呼ばないとプログラムが終了しません。 - パイプ入力(
echo ... | node)では、入力の終わり(EOF)で自動的に閉じます。 - 大きなファイルでも、1行ずつ読めばメモリを圧迫しません。
コマンドライン引数の受け取りはprocess完全ガイド、ファイルの読み書き全般はfsでファイルを読み書きする、大量データの高速処理はストリームの使い方もあわせて参考になります。
対話入力(readline/promises)
ユーザーに質問して入力を受け取るには、readline/promisesのquestionが便利です。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.createInterfaceでinput(標準入力)と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()を呼んで明示的に閉じないと、入力が済んでもプログラムが終了しません。
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await rl.question("好きな言語は? ");
console.log(answer);
// rl.close() を呼ばないと…
// → ターミナルからの対話入力では、ここでプログラムが止まったまま終わらない
rl.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もある巨大なファイルでもメモリを圧迫しません。
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);
実機で、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で全部読む
メモリを圧迫します。readline+for awaitで1行ずつ読みます。
コールバック版で質問を入れ子にしすぎる
読みにくくなります。readline/promisesのawaitを使います。
各行を改行付きだと思って処理する
for awaitで得る行に改行は含まれません。そのまま使えます。
よくある質問
readlineモジュールを使います。readline/promisesでconst ans = await rl.question("質問")と書くと、入力を待って受け取れます。最後にrl.close()を呼ぶのを忘れないでください。古いコードではコールバック版のrl.question("質問", (ans) => {...})も使われます。rl.close()を呼んでいない可能性が高いです。readlineは標準入力を開いたまま待つため、対話入力では明示的に閉じないと終了しません。なお、パイプ(echo ... | node)で入力した場合はEOFで自動的に閉じるため、ターミナルでだけ固まる、という違いが起こります。readline.createInterface({ input: fs.createReadStream("file") })を作り、for await (const line of rl)で各行を順に処理します。ファイル全体をメモリに読み込まないため、巨大なファイルでもメモリを圧迫しません。各行に改行は含まれません。readlineはコールバックで入力を受け取る従来のAPI、readline/promisesはawaitで入力を待てる新しいAPI(Node.js 17以降)です。新しく書くなら、コードが読みやすいreadline/promisesがおすすめです。for await (const line of rl)で得られる各lineは、末尾の改行が取り除かれた状態です。そのため、fs.readFileSyncで読んで自分で分割する場合と違い、改行を取り除く処理は不要です。まとめ
- 対話入力は
readline/promisesのawait rl.question("質問")が簡潔です。 - 古いコードはコールバック版
rl.question("質問", (ans) => {...})。 - 対話入力では
rl.close()が必須。呼ばないと終了しません(パイプはEOFで自動)。 - ファイルは
for await (const line of rl)で1行ずつ。改行は含まれません。 - 大きなファイルは全部読まず、1行ずつ流して処理するとメモリを節約できます。
readlineは、CLIツールの対話入力と、大きなファイルの行単位処理という2つの場面で活躍します。「対話ではclose()を忘れない」「大きなファイルはfor awaitで1行ずつ」という2点を押さえれば、入力まわりとファイル処理をすっきり書けます。

