COBOLの固定形式とその処理方法
COBOLのソースコードは固定形式(Fixed Format)と自由形式(Free Format)があります。多くのレガシーCOBOLは固定形式であり、各列には決まった役割があります。
| 列 | 名称 | 内容 | 処理方針 |
|---|---|---|---|
| 1〜6 | シーケンス番号 | 行番号(古いシステムでは使用) | 無視 |
| 7 | 指示子 | *=コメント行 / -=継続行 / D=デバッグ行 | コメント・デバッグ行をスキップ |
| 8〜11 | Area A | DIVISION・SECTION・段落名 | PROCEDURE DIVISION を検出 |
| 12〜72 | Area B | 命令文・SQL | 主な解析対象 |
| 73〜80 | プログラムID | プログラム識別子 | 切り捨て |
前処理:コメント行・継続行の正規化
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 ブロックの抽出
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 はこれをそのままでは解析できないため、?(プレースホルダ)に置換してから渡します。
プログラム名の取得
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'
ファイル横断スキャンの実装
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
動作確認
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・サブクエリが絡む複雑なケースも解説します。