"BOKU"のITな日常

BOKUが勉強したり、考えたことを頭の整理を兼ねてまとめてます。

ソースコード調査に使うGrep風スクリプトサンプル/Python3

目次

ソースコード調査に使うGrepスクリプトサンプル

たまに、担当システムのソースコードの調査をすることがあります。

例えば。

  • 使用されている関数を調べる
  • ソース内のSQLで使われているテーブルを調べる

などです。

たいていGREPコマンドなどで片付くのですが、もう少し複雑な条件で調べたい場合はPythonスクリプトを拡張GREP的に使うことがあります。

今回は、そのサンプルを書いておきます。

 

サンプルの想定要件

今回のサンプルの想定は以下のようにします。

実際にやったケースの簡易版です。

対象ソースは「.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」する・・って感じでやってます。

 

こんな感じです。

ではでは。