何を間詰めするか
正規化対象のスペースは2種類ある。
- 先頭のインデント — Area A / Area B のインデントは正規化後は不要
- トークン間の連続スペース —
MOVE A TO B→MOVE A TO B
一方、文字列リテラル内のスペース は変更してはいけない。
MOVE ' HELLO ' TO WS-MSG. の ' HELLO ' はスペースが意味を持つ文字列データだ。
単純実装とその問題点
「re.sub(r'\s+', ' ', line).strip() を使えばよい」と思うかもしれないが、
これは文字列リテラル内のスペースも潰してしまう。
import re
line = " MOVE ' HELLO ' TO WS-MSG."
bad = re.sub(r'\s+', ' ', line).strip()
print(bad)
# → "MOVE ' HELLO ' TO WS-MSG." ← リテラル内のスペースが潰れた!
文字列リテラル保護付き実装
文字を1文字ずつ走査し、引用符の中にいるかどうかを in_string フラグで管理する。
リテラル外ではスペースの連続を1つにまとめ、リテラル内ではそのまま出力する。
def normalize_spaces(lines: list[str]) -> list[str]:
"""
各行の連続スペースを1つに間詰めし、先頭・末尾の空白を除去する。
文字列リテラル(シングル/ダブルクォート)内のスペースは変更しない。
Step1〜4(継続行結合まで)適用後に呼ぶ。
"""
result = []
for line in lines:
normalized = _squeeze_spaces(line)
if normalized: # 空行は除外
result.append(normalized)
return result
def _squeeze_spaces(line: str) -> str:
"""
1行のスペースを間詰めする内部関数。
文字列リテラル内のスペースは保護する。
"""
output = []
in_string = False
quote_char = ''
prev_was_space = False # 直前がスペースだったかフラグ
for ch in line:
if in_string:
# リテラル内: そのまま出力
output.append(ch)
if ch == quote_char:
in_string = False
prev_was_space = False
else:
# リテラル外
if ch in ("'", '"'):
# リテラル開始
in_string = True
quote_char = ch
output.append(ch)
prev_was_space = False
elif ch == ' ':
# スペース: 直前がスペースでなければ出力
if not prev_was_space:
output.append(ch)
prev_was_space = True
else:
output.append(ch)
prev_was_space = False
return ''.join(output).strip()
💡 標識列(インデックス0)の扱い
Step 1〜4 後の行ではインデックス0が元の列7(標識列)だが、通常行(空白)・コメント行(既に削除済み)・継続行(既に結合済み)なので、
このステップでは単純に strip() で先頭の空白インデントをまとめて除去できる。
動作確認
lines = [
" MOVE 'HELLO WORLD' TO WS-MSG.",
" MOVE A TO B.",
" IDENTIFICATION DIVISION.",
" IF WS-FLG = ' Y '",
]
result = normalize_spaces(lines)
for line in result:
print(repr(line))
"MOVE 'HELLO WORLD' TO WS-MSG." "MOVE A TO B." "IDENTIFICATION DIVISION." "IF WS-FLG = ' Y '"
'HELLO WORLD' 内の複数スペースは保護され、
リテラル外の MOVE A TO B は正しく間詰めされている。
⚠️ PICTURE句の文字 X() はスペースを含まない
PICTURE 句(例: PIC X(30))の括弧内は文字数定義であり文字列リテラルではない。
X(30) のような記述にスペースが入ることは通常ないため特別処理は不要だが、
万が一 PIC X( 30 ) のように書かれていた場合は間詰めされて PIC X( 30 ) → PIC X( 30 ) と1スペースになる。
これはコンパイラによっては問題になりうるが、実際のソースでは極めてまれだ。
✅ 次の章では…
PART 06 では正規化の最難関である 1行1命令への分割 を実装する。
ピリオドが文字列中・PIC句・数値リテラルにも現れること、
END-IF などのスコープ終端がピリオドなしで命令を区切る場合があることを丁寧に解説する。