SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

Python3でGrep風スクリプト(コマンドより複雑な条件で検索する)/Pythonサンプル

目次

より複雑な検索条件が必要な場合のGrepスクリプト

GREPコマンドなどより、もう少し複雑な条件で調べたいときに、Pythonスクリプトを拡張GREP的に使うサンプルです。

動作確認は、Windows11/Anaconda/Python3.8でしています。

サンプルの想定要件

今回のサンプルは、実際にやったケースの簡易版です。

対象ソースは「.php」です。

拾いだすのは、指定の命名規則に一致する標準関数です。

なので、対象のソース内でユーザ関数として定義されているものは除きます。

指定の命名規則は「小文字オンリーのスネークケース」で3文字以上で、「アルファベットととアンダーバー(_)」のみで構成されているものとしてみました。

サンプルのソース

Pythonのソースです。

パスとかは適当なものに読み替えてください。

 

import re
import pathlib


# 指定拡張子のソースからpatternにマッチする関数を抽出する
# ソース内で宣言されているファンクションと除外指定名を除く


path = "C:\\tmp\\src2\\"
target_suffix = ".php"
pre_pattern = "[\\s\\(\\!\\[]"
after_pattern = "\\("
pattern = "[a-z][a-z_]{2,99}"
function_pattern = "function"

# fileで示すソースからpatternにマッチする関数を抽出する。ただし、except_listに含まれるものとソース内で宣言されている関数は除く


def print_matched_function(file, pattern, except_list=[]):
    lines = []
    # ファイルから行単位でlinesリストに読み込む
    with open(file, "r", encoding="utf-8") as f:
        lines = f.readlines()
        f.close()

    # ソースファイルからパターンにマッチする関数名と使っている行を抽出してマッチリストに追加する
    lines_matched = []
    line_count = 0
    for ln in lines:
        line_count += 1
        next_flag = 0
        work_ln = ln
        while(next_flag == 0):
            m = re.search(
                pre_pattern + "(" + pattern + ")" + after_pattern, work_ln)
            if(m is not None):
                if(m.group(1) not in except_list):
                    lines_matched.append(
                        file.name +
                        "," +
                        m.group(1) +
                        "," +
                        ln.strip() +
                        "," +
                        str(line_count))
                work_ln = work_ln.replace(m.group(1), "XXXXX", 1)
            else:
                next_flag = 1

    # マッチリストを出力する
    for line in lines_matched:
        print(line)


# 除外リストを生成する。
def make_except_list():
    return [
        "array",
        "try",
        "catch",
        "if",
        "else",
        "elseif",
        "case",
        "switch",
        "while",
        "for"]


def add_except_list(file, except_list=[]):
    lines = []
    # ファイルから行単位でlinesリストに読み込む
    with open(file, "r", encoding="utf-8") as f:
        lines = f.readlines()
        f.close()

    # ソースファイル内で宣言されている関数を抽出し、除外リストに追加する
    for ln in lines:
        m = re.search(function_pattern + pre_pattern +
                      "(" + pattern + ")" + after_pattern, ln)
        if(m is not None):
            except_list.append(m.group(1))


except_list = make_except_list()
files = list(pathlib.Path(path).glob("**\\*"))
for file in files:
    if(file.suffix == target_suffix):
        add_except_list(file, except_list)
for file in files:
    if(file.suffix == target_suffix):
        print_matched_function(file, pattern, except_list)

 

サンプルソースの補足説明

ポイントだけ補足します。

対象にしたソースで標準関数が使われるシーンとしては

$a = strlen($b)

のように空白が前にくる場合だけではなくて、「if(strlen・・」や「if(!strcmp・・」もしくは配列の添え字に$a[strlen($b)・・」みたいに使われる場合もありますが、関数名の終わりはほぼ例外なく「(」になります。

なので。

関数名を表す正規表現(pattern)、関数の前(pre_pattern)、関数の後(after_pattern)にわけると。

pre_pattern = "[\\s\\(\\!\\[]"
pattern = "[a-z][a-z_]{2,99}"
after_pattern = "\\("

としています。

とはいえ。

名前の条件に一致しても関数ではないものもあります。

例えば「array」とか「while」「for」なんかです。

これらを省くために、except_listを定義してます。

そして、「対象のソース内で「function」で定義されているものは除きます」の条件をみたすために「add_except_list」で全ソースをチェックして「function」で関数定義されているものを先に「except_list」に追加してます。

工夫が必要だったのは、対象の関数が1行にひとつ・・とは限らないところです。

この「re.search」のオプションで一致するものをすべて引っ張り出すのは無理なので、マッチして出力用配列にappendしたら、それを「XXXX」みたいなマッチしないものに置き換えて、また「re.search」する・・って感じでやってます。

ではでは。