ChatGPT に便利コマンドラインプログラムを書いてもらう： search_files.py


### **user**:

指定したディレクトリの中の .py ファイルを再帰的に全部走査し、目的の文言を含むファイルとその該当箇所をリストアップする python プログラムを書いてください。

---

### **assistant**:

指定したディレクトリ下の全ての`.py`ファイルを走査し、特定の文字列を含むファイルと該当箇所をリストアップするためのPythonスクリプトは以下のようになります。

```python
import os

def search_files(start_directory, target_string):
    for root, _, files in os.walk(start_directory):
        for file in files:
            if file.endswith(".py"):
                with open(os.path.join(root, file), 'rt') as f:
                    lines = f.readlines()
                    for lineno, line in enumerate(lines, start=1):
                        if target_string in line:
                            print(f"Found in {os.path.join(root, file)}, line {lineno}:")
                            print(line.strip())
                            
search_files('/path/to/start', 'target string')
```

このスクリプトは、指定したディレクトリ（ここでは `'/path/to/start'`）から始めて、再帰的に全てのサブディレクトリを検索します。

そして、`.py` で終わるファイル名を見つけたら、そのファイルを開き、各行に対して指定したターゲットの文字列（ここでは `'target string'`）が含まれているかどうかをチェックします。

このスクリプトを実行すると、ターゲットの文字列を含む各行とその行番号、それが含まれるファイル名が出力されます。

また、このスクリプトではファイルパスと検索したい文字列を事前に定義しています。実際には、このスクリプトを使用する前に、`'/path/to/start'` を実際の検索を開始したいディレクトリパスに、`'target string'` を見つけたい具体的な文字列に置き換えてください。

---

### **user**:

ありがとうございます。上記のコードをコマンドラインから実行できるようにコードを改変してください。

---

### **assistant**:

Pythonの`argparse`モジュールを使用すると、コマンドライン引数を扱い、上記のコードをコマンドラインから実行できるようにすることができます。

以下に、その改変版を示します：

```search_files.py
import os
import argparse

def search_files(start_directory, target_string):
    for root, _, files in os.walk(start_directory):
        for file in files:
            if file.endswith(".py"):
                with open(os.path.join(root, file), 'rt') as f:
                    lines = f.readlines()
                    for lineno, line in enumerate(lines, start=1):
                        if target_string in line:
                            print(f"Found in {os.path.join(root, file)}, line {lineno}:")
                            print(line.strip())

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--directory', help="Directory to start search", required=True)
    parser.add_argument('-s', '--string', help="String to search for", required=True)
    args = parser.parse_args()

    search_files(args.directory, args.string)

if __name__ == "__main__":
    main()
```

このプログラムはコマンドライン引数としてディレクトリと検索文字列を受け取ります。これを実行するためには以下のように引数を指定します：

```shell
python search_files.py -d /path/to/start -s target_string
```

ここで、`search_files.py`はこのスクリプトの名前、`/path/to/start`はスクリプトが探索を開始するディレクトリ、`target_string`は探すべき文字列です。

---

### **user**:

上記コードを改変して、--help でヘルプが出るようにしてください。また型ヒントもつけてください。コード中に日本語でできるだけ詳しいコメントをつけてください。

---

### **assistant**:

了解しました。以下に、ご要望に合うように改変したコードを記載します：

```search_files.py
import os
import argparse
from typing import List

def search_files(start_directory: str, target_string: str) -> None:
    """
    指定ディレクトリから始めて、再帰的に全てのサブディレクトリを検索し、
    '.py'で終わるファイルが見つかったら、そのファイルを開き、各行が特定の文字列を含んでいるかチェックします。
    
    Parameters:
    start_directory (str): 検索開始ディレクトリ
    target_string (str): 検索対象文字列
    """
    
    for root, _, files in os.walk(start_directory):
        for file in files:
            if file.endswith(".py"):
                with open(os.path.join(root, file), 'rt') as f:
                    lines = f.readlines()
                    for lineno, line in enumerate(lines, start=1):
                        if target_string in line:
                            print(f"Found in {os.path.join(root, file)}, line {lineno}:")
                            print(line.strip())
                            
def main() -> None:

    # 引数を解析するパーサーを作成
    parser = argparse.ArgumentParser(description="このプログラムは、与えられたディレクトリ以下の全ての.pyファイルに対し、指定した文字列を探します。")

    # 必要な引数を追加します。help オプションは argparse により自動的に生成されます。
    parser.add_argument('-d', '--directory', help="検索を開始するディレクトリを指定", required=True)
    parser.add_argument('-s', '--string', help="検索したい文字列を指定", required=True)

    # 引数を解析
    args = parser.parse_args()

    # 検索関数を実行
    search_files(args.directory, args.string)


if __name__ == "__main__":
    main()
```

このコードは、`--help`オプションまたは`-h`オプションで以下のようなヘルプメッセージを出力します：

```
usage: search_files.py [-h] -d DIRECTORY -s STRING

このプログラムは、与えられたディレクトリ以下の全ての.pyファイルに対し、指定した文字列を探します。

optional arguments:
  -h, --help            show this help message and exit
  -d DIRECTORY, --directory DIRECTORY
                        検索を開始するディレクトリを指定
  -s STRING, --string STRING
                        検索したい文字列を指定
```

---

### **user**:

STRING に正規表現を使えるように改変できますか？

---

### **assistant**:

もちろんです、Pythonの`re`モジュールを使って、指定した文字列を正規表現として解釈し検索することが可能です。 

以下にその例を示します：

```search_files.py
import os
import re
import argparse
from typing import List

def search_files(start_directory: str, regex: str) -> None:
    """
    指定ディレクトリから始めて、再帰的に全てのサブディレクトリを検索し、
    '.py'で終わるファイルが見つかったら、そのファイルを開き、各行が正規表現にマッチするかチェックします。
    
    Parameters:
    start_directory (str): 検索開始ディレクトリ
    regex (str): 検索対象の正規表現
    """
    
    pattern = re.compile(regex)
    
    for root, _, files in os.walk(start_directory):
        for file in files:
            if file.endswith(".py"):
                with open(os.path.join(root, file), 'rt') as f:
                    lines = f.readlines()
                    for lineno, line in enumerate(lines, start=1):
                        # 文字列ではなく、正規表現パターンとのマッチングを試す
                        if pattern.search(line):
                            print(f"Match in {os.path.join(root, file)}, line {lineno}:")
                            print(line.strip())

def main() -> None:
    # 引数を解析するためのパーサーを作成
    parser = argparse.ArgumentParser(description="このプログラムは、与えられたディレクトリ以下の全ての.pyファイルに対し、指定した正規表現を探します。")

    # 必要な引数を追加します。help オプションは argparse により自動的に生成されます。
    parser.add_argument('-d', '--directory', help="検索を開始するディレクトリを指定します", required=True)
    parser.add_argument('-r', '--regex', help="検索したい正規表現を指定します", required=True)

    # 引数を解析
    args = parser.parse_args()

    # 検索関数を実行
    search_files(args.directory, args.regex)


if __name__ == "__main__":
    main()
```

このコードは、指定した文字列を正規表現として解釈し、各行がその正規表現にマッチするかどうかをチェックします。これにより、単純な文字列検索よりも多様な検索パターンを扱うことができます。

---

### **user**:

上記コードを改変して、文字列マッチでも正規表現マッチでも、どちらでも使えるようにしてください。

---

### **assistant**:

了解です。そのためには、コマンドラインから新たにマッチングタイプを選ぶための引数を追加すれば良いです。

ここでは`-m`または`--mode`オプションを追加し、ユーザが文字列マッチモード`string`または正規表現マッチモード`regex`のいずれかを選べるようにします。モードが明示的に指定されなかった場合は、デフォルトで文字列マッチとしましょう。

```search_files.py
import os
import re
import argparse
from typing import List

def search_files(start_directory: str, target: str, mode: str) -> None:
    """
    指定ディレクトリから始めて、再帰的に全てのサブディレクトリを検索し、
    '.py'で終わるファイルが見つかったら、そのファイルを開き、各行が指定の形式でマッチするかチェックする。

    Parameters:
    start_directory (str): 検索開始ディレクトリ
    target (str): 検索対象文字列または正規表現
    mode (str): 文字列マッチ "string" か、正規表現マッチ "regex"
    """
    
    is_regex_mode = mode == 'regex'
    if is_regex_mode:
        pattern = re.compile(target)
    
    for root, _, files in os.walk(start_directory):
        for file in files:
            if file.endswith(".py"):
                with open(os.path.join(root, file), 'rt') as f:
                    lines = f.readlines()
                    for lineno, line in enumerate(lines, start=1):
                        if (is_regex_mode and pattern.search(line)) or (not is_regex_mode and target in line):
                            print(f"Match in {os.path.join(root, file)}, line {lineno}:")
                            print(line.strip())
                            print()

def main() -> None:
    # 引数を解析するためのパーサーを作成
    parser = argparse.ArgumentParser(description="このプログラムは、与えられたディレクトリ以下の全ての.pyファイルに対し、指定した文字列または正規表現を探すプログラムです。")

    # 必要な引数を追加します。help オプションは argparse により自動的に生成されます。
    parser.add_argument('-d', '--directory', help="検索を開始するディレクトリを指定します", required=True)
    parser.add_argument('-t', '--target', help="検索したい文字列または正規表現を指定します", required=True)
    parser.add_argument('-m', '--mode', choices=['string', 'regex'], default='string',
                        help="検索モードを選択します. 'string' は文字列マッチ, 'regex' は正規表現マッチになります")

    # 引数を解析
    args = parser.parse_args()

    # 検索関数を実行
    search_files(args.directory, args.target, args.mode)

if __name__ == "__main__":
    main()
```

このスクリプトを実行するコマンドは以下のようになります：

```bash
python search_files.py -d /path/to/start -t target_string -m string
python search_files.py -d /path/to/start -t target_regex -m regex
```

最初のコマンドは文字列のマッチング、2番目のコマンドは正規表現のマッチングを行います。`-m`オプションが省略された場合は、文字列のマッチングがデフォルトとなります。

---

### **user**:

ありがとうございます！いつも助かります！

---

### **assistant**:

どういたしまして！いつでもお気軽にご質問ください。喜んでお手伝いさせていただきます。何か他にも質問があればお知らせください！

---