NodeJS 速習チュートリアル

Node.js Readline モジュール

1. Readline モジュールの概要

Readline モジュールは、process.stdin のような Readable ストリームからデータを 1 行ずつ読み取るためのインターフェースを提供する Node.js のコアモジュールです。

主に以下のようなケースで活用されます。

  • 主なユースケース
    • 対話型のコマンドラインアプリケーション
    • 設定ウィザードやセットアップツール
    • コマンドラインゲーム
    • REPL (Read-Eval-Print Loop) 環境
    • 巨大なテキストファイルの 1 行ずつの処理
    • カスタムシェルや CLI の構築
  • 主な機能
    • 行ごとの入力処理
    • カスタマイズ可能なプロンプトとフォーマット
    • タブ補完(Tab Completion)のサポート
    • ヒストリ(履歴)管理
    • イベント駆動型インターフェース
    • Promise ベースの API サポート

メモ: Readline モジュールは Node.js に標準で組み込まれているため、追加のインストールは不要です。コマンドラインを通じてユーザーとやり取りしたり、行単位でテキスト入力を処理したりする必要があるあらゆるアプリケーションに最適です。

2. Readline を使い始める

まずは、Readline モジュールを使用してシンプルな対話型 CLI アプリケーションを作成する例を見てみましょう。

2.1 基本的な対話型プロンプトの例

const readline = require('readline');

// 入出力用のインターフェースを作成
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

// 質問を表示し、レスポンスを処理する
rl.question('あなたの名前は何ですか? ', (name) => {
  console.log(`こんにちは、${name}さん!`);

  // 続けて質問する
  rl.question('年齢はいくつですか? ', (age) => {
    console.log(`5年後、あなたは ${parseInt(age) + 5} 歳になります。`);

    // 完了したらインターフェースを閉じる
    rl.close();
  });
});

// アプリケーションの終了をハンドルする
rl.on('close', () => {
  console.log('さようなら!');
  process.exit(0);
});

3. インポートとセットアップ

プロジェクトのモジュールシステムに合わせて、いくつかの方法でインポートできます。

3.1 CommonJS (Node.js デフォルト)

// 基本的な require
const readline = require('readline');

// 分割代入を使用して特定のメソッドをインポート
const { createInterface } = require('readline');

// デフォルト設定でインターフェースを作成
const rl = createInterface({
  input: process.stdin,
  output: process.stdout
});

3.2 ES Modules (Node.js 12+)

// デフォルトインポート
import readline from 'readline';

// 名前付きインポート
import { createInterface } from 'readline';

// 動的インポート (Node.js 14+)
const { createInterface } = await import('readline');

// インターフェースの作成
const rl = createInterface({
  input: process.stdin,
  output: process.stdout
});

ベストプラクティス: システムリソースを解放し、プログラムを正常に終了させるために、使用が終わったら必ず rl.close() を呼び出してください。

4. Readline インターフェースの作成

createInterface メソッドは、Readline インターフェースを作成する主要な方法です。設定プロパティを含むオプションオブジェクトを引数に取ります。

4.1 基本的なインターフェースの作成

const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,   // 監視する Readable ストリーム
  output: process.stdout, // 書き込み先の Writable ストリーム
  prompt: '> ',           // 表示するプロンプト (デフォルト: '> ')
});

4.2 主要なオプション

  • input: 読み込む Readable ストリーム (デフォルト: process.stdin)
  • output: 書き込む Writable ストリーム (デフォルト: process.stdout)
  • prompt: 使用するプロンプト文字列 (デフォルト: '> ')
  • terminal: true の場合、出力を TTY として扱う (デフォルト: output.isTTY)
  • historySize: 履歴の最大保存数 (デフォルト: 30)
  • removeHistoryDuplicates: true の場合、履歴内の重複を削除する (デフォルト: false)
  • completer: タブ補完用のオプション関数
  • crlfDelay: CR と LF の間の待機時間 (デフォルト: 100ms)

4.3 高度なインターフェースの例

const readline = require('readline');
const fs = require('fs');

const rl = readline.createInterface({
  input: fs.createReadStream('input.txt'), // ファイルから読み込む
  output: process.stdout,
  terminal: false,            // ファイル入力の場合は false に設定
  historySize: 100,
  removeHistoryDuplicates: true,
  prompt: 'CLI> ',
  crlfDelay: Infinity,        // すべての CR/LF を単一の改行として扱う
  escapeCodeTimeout: 200
});

rl.on('line', (line) => {
  console.log(`処理中: ${line}`);
});

rl.on('close', () => {
  console.log('ファイルの処理が完了しました');
});

5. 基本的な Readline メソッド

5.1 rl.question(query, callback)

ユーザーに質問を表示し、ユーザーの回答を引数としてコールバックを呼び出します。

rl.question('好きなプログラミング言語は何ですか? ', (answer) => {
  console.log(`${answer} は素晴らしい選択ですね!`);
  rl.close();
});

5.2 rl.prompt([preserveCursor])

設定済みのプロンプトを出力に書き込み、ユーザー入力を待ちます。preserveCursortrue にすると、カーソル位置が維持されます。

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

rl.prompt();

rl.on('line', (line) => {
  if (line.trim() === 'exit') {
    rl.close();
    return;
  }
  console.log(`入力内容: ${line}`);
  rl.prompt(); // 次の入力を促す
});

5.3 rl.write(data[, key])

出力ストリームにデータを書き込みます。key オプションを使用してキープレス(Ctrl+C など)をシミュレートすることも可能です。

// デフォルト値をプリセットする例
rl.write('デフォルト値');
// 行の先頭にカーソルを移動
rl.write(null, { ctrl: true, name: 'a' });

5.4 rl.close()

インターフェースを閉じ、入出力ストリームの制御を解放します。

5.5 rl.pause() と rl.resume()

これらは入力ストリームを一時停止および再開させるために使用します。時間のかかる処理中にユーザー入力を受け付けたくない場合に有効です。

メソッド説明
rl.question(query, callback)質問を表示して入力を待ち、回答をコールバックに渡す
rl.prompt([preserveCursor])設定されたプロンプトを表示する
rl.write(data[, key])出力ストリームにデータを書き込む。キーイベントのシミュレートも可能
rl.close()インターフェースを閉じ、ストリームの制御を解放する
rl.pause()入力ストリームを一時停止する
rl.resume()入力ストリームを再開する

6. Promise と async/await の活用

コールバックベースの API を Promise でラップすることで、よりモダンな async/await 構文を利用できます。

const readline = require('readline');

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

// Promise ベースの質問関数
function askQuestion(query) {
  return new Promise(resolve => {
    rl.question(query, resolve);
  });
}

async function main() {
  try {
    const name = await askQuestion('名前は? ');
    const location = await askQuestion('どこに住んでいますか? ');
    console.log(`${name}さんは ${location} にお住まいなのですね。`);
  } finally {
    rl.close();
  }
}

main();

7. 対話型 CLI メニューの作成

Readline を使用して、ユーザーが選択肢から選べるメニュー形式の CLI を構築できます。

async function handleMenu() {
  let running = true;
  while (running) {
    console.log('\n--- メインメニュー ---');
    console.log('1: 現在時刻を表示');
    console.log('2: システム情報を表示');
    console.log('3: 終了');

    const choice = await askQuestion('オプションを選択してください: ');

    switch (choice) {
      case '1':
        console.log(`時刻: ${new Date().toLocaleTimeString()}`);
        break;
      case '2':
        console.log(`プラットフォーム: ${process.platform}`);
        break;
      case '3':
        running = false;
        break;
      default:
        console.log('無効な選択です。');
    }
  }
  rl.close();
}

8. ファイルを 1 行ずつ読み込む

Readline は、巨大なテキストファイルを効率的に(メモリを節約しながら)処理するのにも適しています。

const fs = require('fs');
const readline = require('readline');

const rl = readline.createInterface({
  input: fs.createReadStream('large-log-file.txt'),
  crlfDelay: Infinity
});

let count = 0;
rl.on('line', (line) => {
  count++;
  if (line.includes('ERROR')) {
    console.log(`エラー発見 (行 ${count}): ${line}`);
  }
});

rl.on('close', () => {
  console.log('スキャン完了');
});

9. タブ補完の実装

completer 関数を定義することで、コマンドやファイルパスのサジェスト機能を実装できます。

const commands = ['help', 'exit', 'status', 'config'];

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  completer: (line) => {
    const hits = commands.filter((c) => c.startsWith(line));
    // 一致するものがあればそれを返し、なければ全リストを返す
    return [hits.length ? hits : commands, line];
  }
});

10. 複数行入力の処理

テキストエディタのような、複数行にわたる入力を受け取る仕組みも構築可能です。

const lines = [];
console.log('入力を開始してください。".end" と入力すると終了します:');

rl.on('line', (line) => {
  if (line.trim() === '.end') {
    console.log('入力された全内容:', lines.join('\n'));
    rl.close();
    return;
  }
  lines.push(line);
});

11. シンプルな REPL の構築

Readline を使えば、独自の JavaScript 実行環境(REPL)を自作することもできます。

const vm = require('vm');

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

const context = vm.createContext({ console, Math });

rl.prompt();

rl.on('line', (line) => {
  try {
    const result = vm.runInContext(line, context);
    console.log(`=> ${result}`);
  } catch (err) {
    console.error(`エラー: ${err.message}`);
  }
  rl.prompt();
});

12. ベストプラクティス

  • インターフェースを閉じる: リソース解放のため、必ず rl.close() を呼ぶ。
  • エラーハンドリング: 入力待ちや処理中のエラーを適切にハンドルする。
  • Promise の活用: コールバック地獄を避け、async/await でコードをクリーンに保つ。
  • UX の向上: 明確なプロンプトや、タブ補完などのフィードバックを提供する。
  • メモリ管理: 大容量ファイルは fs.createReadStream を併用して 1 行ずつ処理する。

13. 他の入力メソッドとの比較

メソッド長所短所最適な用途
Readline行ごとの処理、ヒストリ、補完複雑な UI には向かないCLI, REPL, ファイル処理
process.stdin低レイヤー、生データへのアクセスバッファ処理が手動で困難バイナリデータ、独自プロトコル
Inquirer.js豊富な UI(リスト、チェックボックス)外部依存が必要複雑なウィザード、アンケート
Commander.js引数解析に強力対話性は低い引数ベースのツール

14. まとめ

Node.js の Readline モジュールは、シンプルながらも強力な CLI アプリケーションを構築するための基盤です。ユーザーとの対話だけでなく、ストリームを利用した効率的なテキスト処理においても非常に価値の高いツールと言えるでしょう。