Zenn
🎃

Node.jsでインタラクティブなCLIツールを作る

2025/03/31に公開

👋 はじめに

こんにちは!この記事では、Node.jsを使って、ユーザーと対話しながら設定を進められる「インタラクティブなCLI(コマンドラインインターフェース)ツール」の作り方を、初心者の方にも分かりやすく解説します。

🤔 なぜインタラクティブなCLIツール?

  • 設定が楽になる: 環境変数や設定ファイルを、質問に答えるだけで自動生成できます。
  • ユーザーフレンドリー: 初めてプロジェクトを使う人でも、迷わずに初期設定ができます。
  • 自動化: 面倒なセットアップ手順をスクリプト化できます。

今回は、簡単な質問に答えるだけで、設定ファイル(.envファイル)を生成するシンプルなツールを一緒に作っていきましょう!

🎯 対象読者

  • Node.jsの基本的な書き方(変数、関数、async/await)がわかる方
  • コマンドライン操作に抵抗がない方
  • CLIツール開発に興味がある方

🛠 必要なもの

  • Node.js: v18以上を推奨します。インストールされていなければ、公式サイトからダウンロードしてください。
  • npm (or yarn): Node.jsと一緒にインストールされます。
  • テキストエディタ: VS Codeなどがおすすめです。

🚀 ステップ1: プロジェクトの準備

まずは、作業用のフォルダを作成し、Node.jsプロジェクトを初期化しましょう。

  1. フォルダ作成 & 移動:

    mkdir my-interactive-cli
    cd my-interactive-cli
    
  2. プロジェクト初期化:

    npm init -y
    

    これにより package.json ファイルが作成されます。

  3. TypeScriptの準備 (オプションですが推奨):
    今回は、より安全なコードが書けるTypeScriptを使います。

    npm install -D typescript @types/node ts-node
    
    • typescript: TypeScriptコンパイラ
    • @types/node: Node.jsの型定義ファイル
    • ts-node: TypeScriptファイルを直接実行するツール
  4. TypeScript設定ファイルの作成:
    tsconfig.json という名前のファイルをプロジェクトルートに作成し、以下の内容を記述します。

    tsconfig.json
    {
      "compilerOptions": {
        "target": "ES2016", // または新しいバージョン
        "module": "CommonJS",
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
      },
      "include": ["src/**/*"], // ソースファイルはsrcフォルダに置く
      "exclude": ["node_modules"]
    }
    
  5. ソースフォルダの作成:

    mkdir src
    

    これからのコードは src フォルダ内に作成します。

💬 ステップ2: ユーザーに質問してみよう (readline)

ユーザーからの入力を受け取るには、Node.js標準の readline モジュールが便利です。

  1. src/ask.ts ファイルを作成:

    src/ask.ts
    import readline from 'node:readline/promises'; // promises APIを使うと楽!
    import { stdin as input, stdout as output } from 'node:process';
    
    // readlineインターフェースを作成
    const rl = readline.createInterface({ input, output });
    
    async function askQuestion(query: string): Promise<string> {
      const answer = await rl.question(query);
      return answer;
    }
    
    async function main() {
      console.log('インタラクティブな質問を開始します!');
    
      const name = await askQuestion('あなたの名前は何ですか?: ');
      console.log(`こんにちは、${name}さん!`);
    
      const favoriteFood = await askQuestion('好きな食べ物は何ですか?: ');
      console.log(`${favoriteFood}、美味しいですよね!`);
    
      // 最後にreadlineインターフェースを閉じる
      rl.close();
      console.log('質問は終わりです。');
    }
    
    main().catch(console.error); // エラーハンドリング
    
  2. 実行してみる:
    ターミナルで以下のコマンドを実行します。

    npx ts-node src/ask.ts
    

    実行すると、ターミナルに質問が表示され、入力待ちになります。入力してEnterキーを押すと、次の質問に進みます。

💡 ポイント:

  • readline/promises を使うと async/await で簡単に質問と回答の処理が書けます。
  • createInterface で、どの入力(stdin: 標準入力)と出力(stdout: 標準出力)を使うかを指定します。
  • rl.question(query) で質問を表示し、ユーザーの回答を Promise<string> として受け取ります。
  • 最後に rl.close() を呼んでインターフェースを閉じるのを忘れずに!

⚙️ ステップ3: 外部コマンドを実行してみよう (child_process)

CLIツールでは、他のコマンド(例: git, docker, npm)を実行したい場合があります。これには child_process モジュールを使います。

  1. src/checkGit.ts ファイルを作成:
    Gitがインストールされているかを確認する簡単な例です。

    src/checkGit.ts
    import { exec } from 'node:child_process'; // execをインポート
    import { promisify } from 'node:util'; // コールバックをPromise化する
    
    // execをPromise化する
    const execAsync = promisify(exec);
    
    async function checkGitVersion() {
      console.log('Gitがインストールされているか確認します...');
      try {
        // 'git --version' コマンドを実行
        const { stdout, stderr } = await execAsync('git --version');
    
        if (stderr) {
          // 標準エラー出力があった場合
          console.error('コマンド実行時にエラーが発生しました:', stderr);
          return false;
        }
    
        // 標準出力からバージョン情報を表示
        console.log(`Gitが見つかりました: ${stdout.trim()}`);
        return true;
      } catch (error) {
        // コマンド実行自体に失敗した場合 (gitがインストールされていないなど)
        console.error('Gitの確認に失敗しました。インストールされていない可能性があります。');
        // console.error(error); // 詳細なエラーを見たい場合
        return false;
      }
    }
    
    async function main() {
      const gitExists = await checkGitVersion();
      if (gitExists) {
        console.log('✅ Gitの準備はOKです!');
      } else {
        console.log('❌ Gitが見つかりませんでした。必要に応じてインストールしてください。');
      }
    }
    
    main();
    
  2. 実行してみる:

    npx ts-node src/checkGit.ts
    

    Gitがインストールされていればバージョン情報が表示され、なければエラーメッセージが表示されます。

💡 ポイント:

  • exec(command) はシェルコマンドを実行します。非同期で実行され、完了時にコールバック関数が呼ばれます。
  • util.promisify(exec) を使うことで、execasync/await で扱えるようになります。これは非常に便利です!
  • execAsync{ stdout, stderr } というオブジェクトを返します。stdout が標準出力、stderr が標準エラー出力です。
  • try...catch を使って、コマンドが存在しない場合などのエラーを適切に処理します。

📝 ステップ4: ファイルに書き出してみよう (fs)

ユーザーから得た情報や生成した設定をファイルに保存するには fs (File System) モジュールを使います。

  1. src/createEnv.ts ファイルを作成:
    ユーザーにAPIキーとデータベースURLを尋ね、.env ファイルに書き出す例です。
    src/createEnv.ts
    import { promises as fs } from 'node:fs'; // fs.promisesを使う
    import path from 'node:path';
    import readline from 'node:readline/promises';
    import { stdin as input, stdout as output } from 'node:process';
    
    const rl = readline.createInterface({ input, output });
    
    async function askQuestion(query: string): Promise<string> {
      const answer = await rl.question(query);
      return answer;
    }
    
    async function main() {
      console.log('.env ファイルを作成します。');
    
      const apiKey = await askQuestion('APIキーを入力してください: ');
      const dbUrl = await askQuestion('データベースURLを入力してください: ');
    
      // .env ファイルの内容を作成
      const envContent = `
    

自動生成された設定

API_KEY=${apiKey}
DATABASE_URL=${dbUrl}
`.trim(); // trim()で前後の余分な空白を除去

  // 書き込むファイルパス (プロジェクトルートの .env ファイル)
  const filePath = path.join(process.cwd(), '.env');

  try {
    // ファイルに書き込む (fs.promises.writeFile)
    await fs.writeFile(filePath, envContent);
    console.log(`✅ ${filePath} に設定を書き込みました!`);
  } catch (error) {
    console.error(`❌ ファイルの書き込みに失敗しました: ${filePath}`);
    console.error(error);
  } finally {
    // 必ずreadlineインターフェースを閉じる
    rl.close();
  }
}

main();
```
  1. 実行してみる:
    npx ts-node src/createEnv.ts
    
    質問に答えると、プロジェクトのルートディレクトリに .env ファイルが作成され、入力した内容が書き込まれているはずです。

💡 ポイント:

  • import { promises as fs } from 'node:fs'; で、PromiseベースのファイルシステムAPIを fs としてインポートします。
  • fs.writeFile(filePath, content) で、指定したパスに内容を書き込めます。ファイルが存在しない場合は新規作成され、存在する場合は上書きされます。
  • path.join(process.cwd(), '.env') で、実行中のディレクトリ(プロジェクトルート)からの相対パスで .env ファイルの絶対パスを安全に作成します。
  • ここでも try...catch でファイル書き込みエラーを捕捉し、finally ブロックで rl.close() を確実に実行します。

✨ ステップ5: 組み合わせてインタラクティブなセットアップスクリプトを作る

これまで学んだ readline, child_process, fs を組み合わせれば、冒頭で紹介したような、より実践的なセットアップスクリプトを作成できます!

例: src/setup.ts

src/setup.ts
import { exec } from 'node:child_process';
import { promises as fs } from 'node:fs';
import { promisify } from 'node:util';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import path from 'node:path';

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

async function askQuestion(query: string): Promise<string> {
  return await rl.question(query);
}

// Gitがインストールされているかチェックする関数 (ステップ3から)
async function checkGit(): Promise<boolean> {
  console.log('Step 1: Gitがインストールされているか確認...');
  try {
    await execAsync('git --version');
    console.log('✅ Gitが見つかりました。');
    return true;
  } catch {
    console.error('❌ Gitが見つかりません。セットアップにはGitが必要です。');
    console.log('Gitをインストールしてから再度実行してください: https://git-scm.com/');
    return false;
  }
}

// ユーザーに設定情報を質問する関数 (ステップ2, 4から)
async function getSettings(): Promise<{ apiKey: string; dbUrl: string } | null> {
  console.log('\nStep 2: 設定情報を入力してください...');
  try {
    const apiKey = await askQuestion('APIキーを入力してください: ');
    const dbUrl = await askQuestion('データベースURLを入力してください (例: postgres://user:pass@host:port/db): ');
    return { apiKey, dbUrl };
  } catch (error) {
    console.error('❌ 設定情報の入力中にエラーが発生しました。', error);
    return null;
  }
}

// .envファイルに書き出す関数 (ステップ4から)
async function writeEnvFile(settings: { apiKey: string; dbUrl: string }): Promise<boolean> {
  console.log('\nStep 3: .env ファイルを作成...');
  const envContent = `API_KEY=${settings.apiKey}\nDATABASE_URL=${settings.dbUrl}\n`;
  const filePath = path.join(process.cwd(), '.env');
  try {
    await fs.writeFile(filePath, envContent);
    console.log(`${filePath} に設定を書き込みました!`);
    return true;
  } catch (error) {
    console.error(`❌ ファイルの書き込みに失敗しました: ${filePath}`, error);
    return false;
  }
}

// メインの処理
async function main() {
  console.log('🚀 プロジェクトのセットアップを開始します...');

  // 1. Gitのチェック
  const gitOk = await checkGit();
  if (!gitOk) {
    process.exit(1); // Gitがない場合は終了
  }

  // 2. 設定情報の取得
  const settings = await getSettings();
  if (!settings) {
    process.exit(1); // 設定取得に失敗したら終了
  }

  // 3. .envファイル書き込み
  const writeOk = await writeEnvFile(settings);
  if (!writeOk) {
    process.exit(1); // 書き込み失敗したら終了
  }

  // 完了
  console.log('\n🎉 セットアップが正常に完了しました!');
  console.log('`.env` ファイルに必要な設定が書き込まれました。');
  console.log('次のコマンドでアプリケーションを開始できます: npm run dev (など)');

  rl.close(); // 最後にインターフェースを閉じる
}

// スクリプト実行
main().catch((error) => {
  console.error('\n💥 セットアップ中に予期せぬエラーが発生しました:', error);
  rl.close(); // エラー時も閉じる
  process.exit(1);
});

実行:

npx ts-node src/setup.ts

これで、Gitのチェック、ユーザーへの質問、.env ファイルの作成という一連の流れを実行するインタラクティブなセットアップスクリプトが完成しました!

🎉 まとめ

今回は、Node.jsの標準モジュールである readline, child_process, fs を使って、インタラクティブなCLIツールを作成する方法を学びました。

  • readline: ユーザーに質問し、回答を受け取る。
  • child_process: 外部コマンドを実行する。
  • fs: ファイルの読み書きを行う。

これらの基本的な機能を組み合わせることで、プロジェクトの初期設定を自動化したり、ユーザーが使いやすいツールを作ったりすることができます。

💡 さらに学ぶには?

  • inquirer.js: より高機能な質問(リスト選択、チェックボックスなど)を簡単に実装できる人気のライブラリです。https://github.com/SBoudrias/Inquirer.js/
  • commander.js / yargs: コマンドライン引数やオプションを簡単に扱えるようにするライブラリです。
  • chalk: ターミナル出力に色を付けて見やすくするライブラリです。

Discussion

ログインするとコメントできます