COBOLの固定形式とその処理方法

COBOLのソースコードは固定形式(Fixed Format)と自由形式(Free Format)があります。多くのレガシーCOBOLは固定形式であり、各列には決まった役割があります。

名称内容処理方針
1〜6シーケンス番号行番号(古いシステムでは使用)無視
7指示子*=コメント行 / -=継続行 / D=デバッグ行コメント・デバッグ行をスキップ
8〜11Area ADIVISION・SECTION・段落名PROCEDURE DIVISION を検出
12〜72Area B命令文・SQL主な解析対象
73〜80プログラムIDプログラム識別子切り捨て

前処理:コメント行・継続行の正規化

Python — 前処理モジュール
import re
from pathlib import Path

def normalize_cobol_source(source: str) -> str:
    """
    COBOLの固定形式ソースを正規化する。
    - コメント行(7列目が * または /)を除去
    - デバッグ行(7列目が D)を除去
    - 継続行(7列目が -)を前の行に連結
    - 73〜80列のプログラムID を切り捨て
    """
    lines = source.splitlines()
    result_lines = []

    for raw_line in lines:
        # 80文字に満たない行はパディングしない(後で72文字切り捨て)
        if len(raw_line) < 7:
            result_lines.append(raw_line)
            continue

        indicator = raw_line[6]  # 7列目(0-indexed で 6)

        # コメント行・デバッグ行はスキップ
        if indicator in ('*', '/', 'D', 'd'):
            continue

        # 73〜80列を切り捨て(インデックス 72以降)
        content = raw_line[:72]

        if indicator == '-':
            # 継続行:前の行の末尾に Area B(12〜72列)を連結
            # 継続先の先頭の引用符を考慮して strip
            continuation = content[11:].lstrip()
            if result_lines:
                # 前の行の末尾が引用符で終わる場合は引用符を除いて連結
                prev = result_lines[-1]
                if prev.rstrip().endswith("'"):
                    result_lines[-1] = prev.rstrip()[:-1] + continuation.lstrip("'")
                else:
                    result_lines[-1] = prev.rstrip() + ' ' + continuation
            continue

        result_lines.append(content)

    return '\n'.join(result_lines)

EXEC SQL ブロックの抽出

Python — EXEC SQL ブロック抽出
def extract_exec_sql_blocks(normalized_source: str) -> list[str]:
    """
    正規化済みCOBOLソースから EXEC SQL〜END-EXEC ブロックを抽出する。
    戻り値: SQL 文字列のリスト(ホスト変数 :VAR は ? に置換済み)
    """
    # EXEC SQL から END-EXEC までを抽出(大文字小文字不問、複数行対応)
    pattern = re.compile(
        r'EXEC\s+SQL\s+(.*?)\s+END-EXEC',
        re.IGNORECASE | re.DOTALL
    )

    blocks = []
    for match in pattern.finditer(normalized_source):
        sql_raw = match.group(1).strip()

        # ホスト変数(:WK-XXX 等)を SQL パーサが処理できる ? に置換
        sql_clean = re.sub(r':[A-Z][A-Z0-9-]*', '?', sql_raw, flags=re.IGNORECASE)

        # 空白の正規化
        sql_clean = re.sub(r'\s+', ' ', sql_clean).strip()

        if sql_clean:
            blocks.append(sql_clean)

    return blocks

💡 ホスト変数の置換について

COBOLの埋め込みSQLでは :WK-TOKUI-CD のようなホスト変数(コロン始まり)を使います。JSqlParser はこれをそのままでは解析できないため、?(プレースホルダ)に置換してから渡します。

プログラム名の取得

Python — PROGRAM-ID の取得
def get_program_id(source: str) -> str:
    """
    IDENTIFICATION DIVISION の PROGRAM-ID からプログラム名を取得する。
    見つからない場合はファイル名を代替として使用する。
    """
    pattern = re.compile(
        r'PROGRAM-ID\s*\.\s*([A-Z0-9][A-Z0-9-]*)',
        re.IGNORECASE
    )
    match = pattern.search(source)
    if match:
        return match.group(1).upper()
    return 'UNKNOWN'

ファイル横断スキャンの実装

Python — ファイル横断スキャン
from pathlib import Path
from dataclasses import dataclass, field

@dataclass
class ProgramSqlInfo:
    program_id: str
    file_path: str
    sql_list: list[str] = field(default_factory=list)

def scan_cobol_directory(
    source_dir: str,
    extensions: tuple[str, ...] = ('.cbl', '.cob', '.CBL', '.COB')
) -> list[ProgramSqlInfo]:
    """
    ディレクトリ配下の COBOL ソースファイルをスキャンして
    プログラム名と SQL リストを収集する。
    """
    results = []
    base = Path(source_dir)

    for cbl_file in base.rglob('*'):
        if cbl_file.suffix not in extensions:
            continue

        try:
            # エンコーディングは環境に応じて変更(CP932 / UTF-8 等)
            raw = cbl_file.read_text(encoding='cp932', errors='replace')
        except Exception as e:
            print(f'[WARN] 読み込み失敗: {cbl_file} — {e}')
            continue

        program_id = get_program_id(raw) or cbl_file.stem.upper()
        normalized = normalize_cobol_source(raw)
        sql_list = extract_exec_sql_blocks(normalized)

        if sql_list:
            results.append(ProgramSqlInfo(
                program_id=program_id,
                file_path=str(cbl_file),
                sql_list=sql_list
            ))

    print(f'スキャン完了: {len(results)} プログラムでSQL検出')
    return results

動作確認

Python — 動作確認
if __name__ == '__main__':
    infos = scan_cobol_directory('./cobol_src')
    for info in infos:
        print(f'\n=== {info.program_id} ({info.file_path}) ===')
        for i, sql in enumerate(info.sql_list, 1):
            print(f'  SQL #{i}: {sql[:80]}...' if len(sql) > 80 else f'  SQL #{i}: {sql}')
実行結果(例)
スキャン完了: 3 プログラムでSQL検出

=== URIAGE-TOROKU (./cobol_src/URIAGE-TOROKU.CBL) ===
  SQL #1: SELECT TOKUI-CD, TOKUI-NM INTO ?, ? FROM TOKUI WHERE TOKUI-CD = ?
  SQL #2: INSERT INTO URIAGE (URIAGE-NO, TOKUI-CD, URIAGE-DT, KIN-GAK) VALUES (?, ?, ?, ?)
  SQL #3: INSERT INTO MEISAI (URIAGE-NO, SEQ, SHOHIN-CD, SU, KIN) VALUES (?, ?, ?, ?, ?)

=== URIAGE-SHUKEI (./cobol_src/URIAGE-SHUKEI.CBL) ===
  SQL #1: SELECT SUM(KIN-GAK) INTO ? FROM URIAGE WHERE URIAGE-DT = ?
  SQL #2: SELECT URIAGE-NO, TOKUI-CD, KIN-GAK FROM URIAGE WHERE URIAGE-DT BETWEEN ? AND ?

次の章では…

PART 04 では抽出したSQL文字列を JSqlParser(または正規表現)に渡し、テーブル名と操作種別(C/R/U/D)を判別します。JOIN・サブクエリが絡む複雑なケースも解説します。

→ PART 04 — テーブル名・操作種別の判別へ