programs2single_md.py テクニカルドキュメント

プログラムの動作

programs2single_md.py は、指定されたディレクトリ以下のファイルを検索し、その内容を結合してMarkdown形式のファイルに効率的に出力するPythonスクリプトです。

目的: 大規模なプロジェクトのソースコードや関連ドキュメントなど、複数のテキストファイルを一元的にまとめ、単一または分割されたMarkdownファイルとして出力することを目的としています。これは、AIモデルへの入力準備や、コードベース全体のスナップショット作成などに特に有用です。

主な機能:

  • ディレクトリの再帰的走査: 指定されたルートディレクトリ以下に存在するすべてのサブディレクトリとファイルを探索します。

  • 柔軟なファイルフィルタリング: 1つまたは複数のファイルマスク(例: *.py, *.js;*.txt)を指定し、合致するファイルのみを対象とします。複数のマスクはセミコロン ; で区切ります。

  • Markdown形式での出力: 見つかった各ファイルのフルパスをMarkdownの第三段階見出し (###) として記述し、その直下にファイルの内容を特定の区切り (#=======) で囲んで出力します。

  • 自動ファイル分割: 出力されるMarkdownファイルが指定された最大バイト数(--max_bytes)を超えた場合、自動的に新しい連番のMarkdownファイルを作成し、書き込みを継続します。

  • 既存ファイル名の回避: 出力ファイル名が既に存在する場合、連番を自動的にインクリメントして既存のファイルを上書きすることなく新しいファイルを作成します。

解決する課題: 手動で多数のファイルの内容をコピー&ペーストしてまとめる手間を省き、自動的かつ体系的にファイルを統合します。特に、入力サイズに制限があるシステム(例: 大規模言語モデルのプロンプト)に複数のコードファイルを提供する必要がある場合に、このツールは作業を大幅に簡素化します。

原理

本プログラムは以下の主要な原理に基づいています。

  1. ファイルシステム走査: Pythonの標準ライブラリ os モジュールの os.walk() 関数を利用して、指定された root_dir から開始し、その下のすべてのディレクトリとファイルを再帰的に探索します。これにより、プロジェクト構造全体を網羅的に検索できます。

  2. ファイル名マッチング: Pythonの標準ライブラリ fnmatch モジュールの fnmatch.fnmatch() 関数を使用し、各ファイル名がコマンドライン引数 filemasks で指定されたワイルドカードパターンに合致するかどうかを判定します。複数のマスクが指定された場合は、セミコロン ; で区切られた各マスクに対して順番にマッチングを試み、いずれか一つでも合致すればそのファイルを処理対象とします。

  3. 動的な出力ファイル管理:

    • 出力ファイル名の生成: 出力ファイル名は --prefix 引数で指定されたプレフィックスと4桁の連番、そして .md 拡張子を組み合わせて生成されます(例: output_0001.md)。

    • 既存ファイル名の回避: find_output_filename 関数は、生成しようとしているファイル名 (output_0001.md など) が既にファイルシステムに存在するかどうかを確認します。もし存在する場合、連番をインクリメントし、存在しない一意なファイル名が見つかるまでこのプロセスを繰り返します。これにより、既存のファイルが意図せず上書きされることを防ぎます。

    • バイト数によるファイル分割: 各ファイルの内容をMarkdown形式に変換し、UTF-8エンコードした際のバイト数を計算します。このバイト数が現在の出力ファイルの --max_bytes を超える場合、現在開いているファイルを閉じ、find_output_filename 関数を使用して新しい連番のMarkdownファイルを作成し、書き込みを継続します。これにより、巨大な出力を管理可能なサイズのファイルに自動的に分割できます。

    各ファイルの内容が追加されるたびに、現在のファイルサイズ \(S_{current}\) と新しいコンテンツのバイト数 \(B_{new}\) を比較します。

    \[S_{current} + B_{new} > S_{max}\]

    ここで、\(S_{max}\)--max_bytes で指定された最大バイト数です。この条件が真の場合、新しい出力ファイルが作成されます。

  4. Markdownコンテンツの構造: 処理対象となる各ファイルのコンテンツは、以下のMarkdown形式で出力ファイルに追加されます。

    ### <ファイルのフルパス>
    
    #=======
    <ファイルの内容>
    #=======
    

    これにより、出力されたMarkdownファイル内で各ソースファイルの内容が区別しやすく、整理された状態で提供されます。ファイルのフルパスは ### 見出しとして、ファイルの内容は #======= で囲まれたブロックとして記述されます。

必要な非標準ライブラリとインストール方法

このプログラムは、Pythonの標準ライブラリのみを使用しており、argparseosfnmatch はいずれもPythonのデフォルトインストールに含まれています。

したがって、特別な非標準ライブラリをインストールする必要はありません。

必要な入力ファイル

このプログラムの入力は、検索対象となるファイルシステム上のファイル群です。

  • ルートディレクトリ: コマンドライン引数 root_dir で指定します。プログラムはこのディレクトリ以下を再帰的に走査します。

  • ファイルマスク: コマンドライン引数 filemasks で指定します。これはワイルドカードを含む文字列(例: *.py)で、セミコロン ; で区切ることで複数のマスクを指定できます(例: *.py;*.js;*.ts)。このマスクに合致するファイルのみが処理対象となります。

  • ファイル形式: 基本的にテキストファイル(例: .py, .js, .txt, .md, .json など)を想定しています。プログラムはファイルをUTF-8エンコーディングで読み込もうとします。エンコーディングエラーが発生した場合は、そのファイルはスキップされ、コンソールにメッセージが表示されます。バイナリファイル(例: .jpg, .png, .zip など)は通常、UnicodeDecodeError を発生させるためスキップされます。

期待されるファイル構造の例:

my_project/
├── main.py
├── utils/
│   ├── helper.py
│   └── data_processor.py
├── tests/
│   └── test_main.py
└── docs/
    └── README.md

上記の構造で my_projectroot_dir として指定し、*.pyfilemasks として指定すると、main.py, helper.py, data_processor.py, test_main.py の内容が収集されます。

生成される出力ファイル

プログラムは以下の特性を持つMarkdownファイルを生成します。

  • ファイル名:

    • --prefix 引数で指定された文字列に、4桁の連番と .md 拡張子が付与されます。

    • 例: --prefix output の場合、output_0001.md, output_0002.md など。

    • 生成しようとするファイル名が既に存在する場合、連番を自動的にインクリメントして、既存のファイルを上書きすることなく新しいファイルを作成します。

  • 内容:

    • 各出力ファイルは、フィルタリング条件に合致した複数の入力ファイルの内容を結合したものです。

    • 各入力ファイルの内容は、以下に示すMarkdown形式のブロックとして出力されます。

      ### <入力ファイルの絶対パス>
      
      #=======
      <入力ファイルの内容>
      #=======
      
    • <入力ファイルの絶対パス> には、システム上の完全なファイルパスが記述されます。

    • <入力ファイルの内容> は、読み込んだファイルの実際のテキストデータです。

    • #======= は、各ファイルの内容の開始と終了を示す独自の区切り記号として使用されます。

  • ファイル分割:

    • --max_bytes 引数で指定された最大バイト数(デフォルトは20MB)に達すると、現在の出力ファイルが閉じられ、新しい連番のファイルが自動的に作成されて書き込みが継続されます。これにより、出力ファイルが巨大になりすぎるのを防ぎます。

コマンドラインでの使用例 (Usage)

programs2single_md.py は、以下の基本構文でコマンドラインから実行されます。

python programs2single_md.py <root_dir> <filemasks> [--max_bytes <int>] [--prefix <str>]

引数

デフォルト値

説明

root_dir

str

.

検索を開始するルートディレクトリ。

filemasks

str

*.py

検索対象とするファイルマスク。ワイルドカード使用可能。複数指定する場合はセミコロン ; で区切る (例: *.py;*.js)。

--max_bytes

int

20000000

各Markdownファイルの最大バイト数。デフォルトは20MB。

--prefix

str

output

出力ファイルのベース名。連番と .md 拡張子が追加される。

コマンドラインでの具体的な使用例

1. カレントディレクトリのPythonファイルを全て結合する(デフォルト設定)

現在の作業ディレクトリ (.) とそのサブディレクトリにあるすべての .py ファイルを検索し、output_0001.md というファイルに結合します。

python programs2single_md.py . "*.py"

実行結果の例:

Added /path/to/project/main.py to output_0001.md
Added /path/to/project/utils/helper.py to output_0001.md
Added /path/to/project/tests/test_main.py to output_0001.md

これにより、output_0001.md が作成され、指定されたPythonファイルの内容が上記「生成される出力ファイル」で説明した形式でまとめられます。

2. 特定のディレクトリから複数の種類のファイルを結合し、ファイルサイズ制限を設ける

my_project ディレクトリ以下の .py ファイルと .md ファイルを結合し、各出力ファイルの最大サイズを50KB (50000 バイト) に制限し、出力ファイル名を project_doc_ で開始させます。

python programs2single_md.py my_project "*.py;*.md" --max_bytes 50000 --prefix project_doc

実行結果の例:

Added my_project/main.py to project_doc_0001.md
Added my_project/utils/helper.py to project_doc_0001.md
Added my_project/docs/README.md to project_doc_0001.md
Added my_project/big_script.py to project_doc_0002.md  # 50KBを超えたため新しいファイルに

この例では、my_project/big_script.py が非常に大きいファイルであったため、project_doc_0001.md が50KBの制限に達した後、project_doc_0002.md が自動的に作成され、そこに内容が追記されています。

3. 出力ファイル名が既存の場合の挙動を確認する

もし my_output_0001.md が既に存在する場合に、--prefix my_output で実行すると、my_output_0002.md から始まるファイルが作成されます。

事前準備: my_output_0001.md を手動で作成(例: echo "Existing content" > my_output_0001.md)。

python programs2single_md.py . "*.txt" --prefix my_output

実行結果の例:

Added /path/to/project/file1.txt to my_output_0002.md
Added /path/to/project/file2.txt to my_output_0002.md

この場合、my_output_0001.md は上書きされず、プログラムは自動的に my_output_0002.md を作成してファイルを書き込みます。

4. 存在しないディレクトリを指定した場合

存在しないルートディレクトリを指定すると、ファイルは何も処理されません。

python programs2single_md.py non_existent_dir "*.py"

実行結果:

(特にエラーメッセージはなく、何も出力ファイルが生成されません。)