【PHP】LLMに読ませるために任意のファイル群をマージするスクリプトの紹介

  • 2025年5月29日
  • 2025年5月29日
  • PHP

 LLMには様々なものがありますが、いずれの場合も読める量に制限があります。このためプログラムの改善のためにLLMと相談する際、任意のファイルだけ抜き出して渡す必要があります。これを楽にするためのファイルやディレクトリを指定して、指定したファイルやディレクトリを1テキストファイルにまとめるPHPスクリプトを紹介します。

 実際のコードが次です。少なくともPHP8.4で動作します。新しい関数や文法を使ってないので多分PHP7.0でも動きます(少なくとも文法エラーはないです)。

<?php

/**
 * LLM用のファイルマージツール
 *
 * 指定されたディレクトリやファイルから、指定拡張子のファイルのみを抽出し、
 * それらを1つのテキストファイルにマージするCLIスクリプトです。
 *
 * 使用例:
 * php llm_merge.php --directories=src,config --output=merged.txt --ignore-pattern='/Test.php$/' --extension=php,ts
 */

// ----------------------------------------
// ヘルプ表示処理(--help または -h が指定された場合)
// ----------------------------------------
if (in_array('--help', $argv) || in_array('-h', $argv)) {
    echo <<<EOL
LLMファイルマージツール

指定されたディレクトリやファイルから、指定拡張子のファイルを読み取り、1つのファイルにマージします。

【使用例】
  php llm_merge.php --directories=src,tests --output=merged.txt --ignore-pattern='/Test.php$/' --extension=php,ts

【オプション】
  --directories       必須。対象ディレクトリまたはファイルをカンマ区切りで指定します。
  --output            出力ファイルパス。省略時は 'tmp.txt' になります。
  --ignore-pattern    無視するファイル名の正規表現(例: '/Test.php$/', '\.spec\.')
  --extension         対象の拡張子。カンマ区切り。デフォルト: php,ts,tsx,json,yaml
  --help, -h          このヘルプを表示します。

EOL;
    exit(0);
}

// ----------------------------------------
// CLIオプションの定義と取得
// ----------------------------------------
// 各オプションを取得。省略可能なものにはデフォルト値を設定
$options = getopt('', [
    'directories:',          // 必須
    'output::',              // 任意(省略可能)
    'ignore-pattern::',      // 任意
    'extension::',           // 任意
]);

// --directories が指定されていない場合はエラー終了
if (empty($options['directories'])) {
    fwrite(STDERR, "エラー: --directories オプションは必須です。\n");
    fwrite(STDERR, "詳細は --help を参照してください。\n");
    exit(1);
}

// オプション値の整理と初期化
$directories       = explode(',', $options['directories']);                         // 対象ディレクトリ
$outputPath        = $options['output'] ?? 'tmp.txt';                               // 出力ファイルパス

$allowedExtensions = explode(',', $options['extension'] ?? 'php,ts,tsx,json,yaml'); // 対象拡張子

// 無視するパターン
// ignore-pattern にデリミタが無い場合、自動で / を追加する
$ignorePatternRaw = $options['ignore-pattern'] ?? '';
$ignorePattern = '';
if (!empty($ignorePatternRaw)) {
    // 先頭と末尾の文字が同じ場合はデリミタありとみなしてそのまま使用
    $firstChar = substr($ignorePatternRaw, 0, 1);
    $lastChar  = substr($ignorePatternRaw, -1);
    if ($firstChar === $lastChar) {
        $ignorePattern = $ignorePatternRaw;
    } else {
        // / が無い場合は自動で追加. /はエスケープもする
        $ignorePattern = '/' . str_replace('/', '\/', $ignorePatternRaw) . '/';
    }
}
// ----------------------------------------
// マージ処理本体
// ----------------------------------------
$mergedContent = ''; // マージ後の出力内容を格納する文字列

// 指定された各ディレクトリまたはファイルを処理
foreach ($directories as $directoryOrFile) {
    // ファイルやディレクトリが存在しない場合は警告表示
    if (!is_dir($directoryOrFile) && !is_file($directoryOrFile)) {
        fwrite(STDERR, "警告: 対象が見つかりません: {$directoryOrFile}\n");
        continue;
    }

    // ファイル指定だった場合の処理
    if (is_file($directoryOrFile)) {
        $extension = pathinfo($directoryOrFile, PATHINFO_EXTENSION);
        if (in_array($extension, $allowedExtensions)) {
            $fileContent = file_get_contents($directoryOrFile);
            $mergedContent .= "===== FILE: {$directoryOrFile} =====\n";
            $mergedContent .= $fileContent . "\n\n";
        }
        continue;
    }

    // ディレクトリ指定だった場合の処理
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($directoryOrFile)
    );

    foreach ($iterator as $file) {
        if ($file->isDir()) {
            continue; // ディレクトリはスキップ
        }

        $filename  = $file->getFilename();
        $extension = $file->getExtension();

        // 無視パターンにマッチする場合はスキップ
        if (!empty($ignorePattern) && preg_match($ignorePattern, $filename)) {
            continue;
        }

        // 対象拡張子であれば内容を読み込んでマージ
        if (in_array($extension, $allowedExtensions)) {
            $filePath     = $file->getPathname();
            $fileContent  = file_get_contents($filePath);

            $mergedContent .= "===== FILE: {$filePath} =====\n";
            $mergedContent .= $fileContent . "\n\n";
        }
    }
}

// ----------------------------------------
// マージ結果の確認と追加メッセージ
// ----------------------------------------
if (empty($mergedContent)) {
    fwrite(STDERR, "警告: マージ対象のファイルが見つかりませんでした。\n");
    exit(1);
}

// ChatGPTとの連携に向けた案内文を末尾に追加
$mergedContent .= "会話とコードのコメントは日本語でお願いします。\n";

// ----------------------------------------
// ファイルへの出力処理
// ----------------------------------------
try {
    file_put_contents($outputPath, $mergedContent); // ファイルに書き込み
    chmod($outputPath, 0644);                       // 権限設定(読み取り可)

    // 成功メッセージ表示
    echo "✅ マージファイルを作成しました: {$outputPath}\n";
    echo "📏 文字数: " . number_format(mb_strlen($mergedContent)) . "\n";
} catch (Exception $e) {
    // 書き込み失敗時のエラーハンドリング
    fwrite(STDERR, "エラー: ファイルの書き込み中に例外が発生しました: " . $e->getMessage() . "\n");
    exit(1);
}

exit(0); // 正常終了


// 使用例:
// app/Commands以下とconfig/commands.phpを読んで merged.txt に出力する
// Test.phpで終わるファイルは無視する
// 拡張子がphpとtsのファイルだけを対象にする
php llm_merge.php --directories=app/Commands,config/commands.php --output=merged.txt --ignore-pattern='/Test.php$/' --extension=php,ts

 やっているのは渡されたディレクトリの配下やファイルを探索し、見つかったファイルの内容をパスと一緒に一つの文字列にまとめ、外部に書きだす、という処理です。こんな感じで特定のファイルだけ抽出してまとめると、LLMが受け入れられる程度のサイズで目的に必要なファイルの内容だけLLMに渡せます。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG