Claude CodeでCOBOLのcopybookとJCLを読む — レイアウト表と検算
Claude CodeにCOBOLのcopybookとJCLを読ませ、COMP-3やREDEFINESを含む固定長レコードの項目表と、DD名から見たジョブ間のファイルの流れを作る手順です。実データ1行での検算とiconvの罠も示します。
メインフレームの資産を読むとき、最初に壁になるのはCOBOLの手続きではなく、データの形です。copybook(COPY句で取り込まれるデータ定義)を読み違えると、後の処理はすべてずれます。この記事では、Claude Codeにcopybookを「項目名・開始位置・長さ・型」の表にさせ、実データ1行で検算する手順と、JCLのDD名からジョブ間のファイルの流れを表にする手順を示します。
要約が自然な文章で返ってくるほど、間違いには気づきにくくなります。そこで読解の合格条件を、人が目で読んだ印象ではなく、表の数字が実データと合うことに置きます。移行後の出力を突き合わせる段階はClaude CodeでCOBOLを移すで扱っています。ここはその前段、データ定義とジョブ構成を読み解く段階です。言語を問わない進め方はClaude Codeのコードモダナイゼーションにあります。
用意するもの
- python3(項目表とJCLの表を出す短いスクリプトに使います)
- iconv(文字コードの比較用。
iconv -lでIBM037が載っているか確かめます) - 任意でGnuCOBOL(cobcの記号表と突き合わせる場合)
- 個人情報を含まない検証用の固定長ファイル(本番の先頭行は使わず、同じ長さのダミー値に置き換えたもの)
最初に決める文字コードの扱い — 丸ごとiconvは使えない
メインフレーム由来のファイルはEBCDICで、COMP-3(パック10進)やCOMP(2進)の項目はバイナリのまま入っています。ここで「読めないからファイル全体をiconvでUTF-8にしてから渡す」と考えると、数値項目が壊れます。
手元で検証用の32バイト1件を作り、iconv -f IBM037 -t UTF-8に通して比べました。サンプルは筆者が作った架空のデータで、実在の顧客情報ではありません。
# 元のファイル(EBCDIC のまま)
xxd cust.dat
# 00000010: 0012 3456 7d13 88c3 c1c3 d4f0 f0f0 f4f2
# 丸ごと変換した結果
iconv -f IBM037 -t UTF-8 cust.dat | xxd
# 00000010: 0012 c294 c3ae 2713 6843 4143 4d30 3030文字の項目は000123YAMADAのように読める形になりました。一方、パック10進の34 56 7dはc2 94 c3 ae 27のような別のバイト列に変わり、2進の13 88も13 68に化けています。値を復元する手がかりが、変換で消えています。
分け方は単純です。
- 文字項目(PIC X、符号なしの表示数字)だけを、開始位置と長さで切り出してから変換する
- COMP-3とCOMPの項目は、変換せず16進のまま扱う
- Claudeには、変換後のテキストではなく、
xxdで出した16進ダンプと項目表の両方を渡す
使える文字コード名は環境で違うので、iconv -lでIBM037などが載っているか先に確かめます。IBM037は米国・カナダ向けの英語のコードページで、日本語の環境では別のコードページが使われていることがあります。漢字を含む項目(PIC NやDBCS)は、SO/SI(シフトアウト/シフトイン)で文字種が切り替わる形になりえます。項目ごとに切り出しても単純には変換できないため、コードページは移行元の設定を確認して決めます。
copybookから項目表を作る
検証に使うcopybookです。COMP-3、COMP、REDEFINESを1つずつ含めてあります。
01 CUST-REC.
05 CUST-ID PIC 9(6).
05 CUST-NAME PIC X(10).
05 CUST-BAL PIC S9(7)V99 COMP-3.
05 CUST-LIMIT PIC S9(4) COMP.
05 CUST-TYPE PIC X(1).
05 CUST-DETAIL PIC X(8).
05 CUST-CORP REDEFINES CUST-DETAIL.
10 CORP-CODE PIC X(3).
10 CORP-DEPT PIC 9(5).項目表にするときの計算は4つだけです。
| 型 | 長さの求め方 | この例 |
|---|---|---|
| PIC X(n) | 長さの求め方nバイト | この例CUST-NAMEは10 |
| 符号なしPIC 9(n) | 長さの求め方nバイト(表示数字は1桁1バイト) | この例CUST-IDは6 |
| COMP-3 | 長さの求め方(桁数+2)÷2の切り捨て | この例S9(7)V99は9桁で5 |
| COMP(2-4-8の場合) | 長さの求め方4桁以下は2、9桁以下は4、それ以上は8バイト | この例S9(4)は2 |
COMP-3は1バイトに2桁を詰め、最後のニブル(4ビット)に符号を入れます。符号は正がC、負がD、符号なしはFが標準です。偶数桁のときは先頭に0が1つ入るので、桁数が増えても長さは「桁数+符号1つ」を2で割って切り上げた値になります。V(暗黙の小数点)は桁を取らず、位置の約束としてだけ働きます。
COMPの長さは、コンパイラの設定で変わります。GnuCOBOLでは、-fbinary-sizeで2-4-8、1-2-4-8、1-8のどれかを選べます。上の表は2-4-8の場合で、1-2-4-8ならS9(2)以下は1バイトになり、このスクリプトの結果とずれます。マニュアルの設定例の既定は1-2-4-8なので、既定のままコンパイルした結果とも食い違います。移行元のコンパイラがどの割り当てだったかは、表を作る前にCLAUDE.mdへ書いておきます。
REDEFINESは同じ位置から始まる
CUST-CORP REDEFINES CUST-DETAILは、新しい領域を作りません。同じ8バイトを別の見方で読むだけです。表にすると、開始位置がCUST-DETAILと同じ25になります。レコード長に足してはいけません。
どちらの見方が有効かは、データ側の別項目(この例ではCUST-TYPE)で決まるのが普通です。表には「この項目は何の値のときに有効か」の列を足し、COBOLの手続き部でIF CUST-TYPE = ...と書かれている箇所をClaudeに探させます。
OCCURS DEPENDING ONは可変長になる
COBOLでOCCURS ... DEPENDING ONが現れると、レコード長が可変になります。GnuCOBOLのマニュアルの記号表の例でも、OCCURS 5 TO 5000のサイズ欄には最大の5000が出ます。可変長のレコードは、最大長を表に書くだけでなく、長さを決める項目の値も復号の対象にします。
表を作るスクリプト
Claudeに暗算させず、計算はスクリプトに寄せます。次の短いPythonは、上のcopybookの書き方(固定形式、PIC、COMP、COMP-3、REDEFINES)だけを扱う検証用です。実際のcopybookにはOCCURSやCOPY REPLACINGなどが現れるので、足りない構文はClaudeにこの形を拡張させます。
layout.py(copybook から項目表を出す)
import re, sys
def digits(s):
return sum(int(n) if n else 1 for n in re.findall(r'[9X](?:\((\d+)\))?', s))
def size(pic, usage):
m = re.fullmatch(r'S?([9X()\d]+)(?:V([9()\d]+))?', pic)
d = digits(m.group(1)) + (digits(m.group(2)) if m.group(2) else 0)
if usage == 'COMP-3':
return (d + 2) // 2, 'packed' # 桁数 + 符号1ニブル を 2 で割って切り上げ
if usage == 'COMP':
return (2 if d <= 4 else 4 if d <= 9 else 8), 'binary'
return d, ('char' if 'X' in pic else 'zoned')
def parse(path):
text = ' '.join(l[6:72].strip() for l in open(path) if len(l) > 7 and l[6] != '*')
items, stack, off, saved = [], [], 0, []
for s in [x.strip() for x in text.split('.') if x.strip()]:
t = s.split()
lvl, name = int(t[0]), t[1]
red = t[t.index('REDEFINES') + 1] if 'REDEFINES' in t else None
pic = t[t.index('PIC') + 1] if 'PIC' in t else None
usage = next((u for u in ('COMP-3', 'COMP') if u in t), '')
while stack and stack[-1] >= lvl:
stack.pop()
while saved and saved[-1][0] >= lvl: # REDEFINES の範囲を抜けたら位置を戻す
off = saved.pop()[1]
start = next(i['start'] for i in items if i['name'] == red) if red else off
it = dict(name=name, start=start, red=red, pic=pic, usage=usage, len=0, kind='group')
if red and not pic: # REDEFINES した集団項目: 子は同じ位置から始まる
saved.append((lvl, off)); off = start
if pic:
it['len'], it['kind'] = size(pic, usage)
mv = re.search(r'V([9()\d]+)', pic); it['scale'] = digits(mv.group(1)) if mv else 0
if not red:
off = start + it['len']
items.append(it); stack.append(lvl)
return items, off
if __name__ == '__main__':
items, total = parse(sys.argv[1])
print(f"{'項目':<12}{'開始':>4}{'長さ':>4} 型 PIC / USAGE")
for i in items:
if i['pic']:
print(f"{i['name']:<12}{i['start']+1:>4}{i['len']:>4} {i['kind']:<7} {i['pic']} {i['usage']}")
print('レコード長:', total)python3 layout.py CUSTREC.cpy項目 開始 長さ 型 PIC / USAGE
CUST-ID 1 6 zoned 9(6)
CUST-NAME 7 10 char X(10)
CUST-BAL 17 5 packed S9(7)V99 COMP-3
CUST-LIMIT 22 2 binary S9(4) COMP
CUST-TYPE 24 1 char X(1)
CUST-DETAIL 25 8 char X(8)
CORP-CODE 25 3 char X(3)
CORP-DEPT 28 5 zoned 9(5)
レコード長: 32別の突き合わせ手段として、cobcの出力もあります。-t=fileでリストを出力し、-ftsymbolsを付けるとSIZE、TYPE、LVL、NAME、PICTUREの列を持つ記号表が付きます。マニュアルの例ではPIC 9(08) COMPが4、PIC 9(04) COMPが2、REDEFINESの項目にはRの付いたTYPEが出ています。スクリプトの表と、cobcの記号表のSIZE列が一致するかを見れば、表の計算がコンパイラの解釈と合っているかを確かめられます。COPY句が絡むときは、cobc -Eで展開後のソースを.iファイルに出せます。
実データ1行で検算する
表ができたら、実データの先頭1件を表に当てて復号します。復号の規則は、文字項目をEBCDICとして読み、COMPを符号付きの2バイト整数として読み、COMP-3は最後のニブルを符号、残りを桁として読むだけです。このスクリプトは符号付きの表示数字(ゾーン10進)と、2バイト以外のCOMPには対応していません。該当する項目が出たら、Claudeに同じ形で拡張させます。
decode.py(項目表で 1 件を復号する)
import sys, struct
from layout import parse
items, total = parse(sys.argv[1]); data = open(sys.argv[2], 'rb').read()[:total]
def dec(i, b):
if i['kind'] == 'char': return b.decode('cp037')
if i['kind'] == 'zoned': return b.decode('cp037')
if i['kind'] == 'binary': return struct.unpack('>h', b)[0]
if i['kind'] == 'packed':
h = b.hex(); sign = {'c': 1, 'f': 1, 'd': -1}[h[-1]]
return sign * int(h[:-1]) / 10 ** i['scale']
for i in items:
if i['pic']:
b = data[i['start']:i['start'] + i['len']]
print(f"{i['name']:<12}{b.hex():<22}{dec(i, b)}")python3 decode.py CUSTREC.cpy cust.datCUST-ID f0f0f0f1f2f3 000123
CUST-NAME e8c1d4c1c4c140404040 YAMADA
CUST-BAL 001234567d -12345.67
CUST-LIMIT 1388 5000
CUST-TYPE c3 C
CUST-DETAIL c1c3d4f0f0f0f4f2 ACM00042
CORP-CODE c1c3d4 ACM
CORP-DEPT f0f0f0f4f2 00042001234567dは、桁が001234567、符号がDなので負、V99で小数2桁として−12345.67になります。人が見て「顧客番号が6桁で、名前があり、残高が負になっている」と読める結果が出れば、少なくとも位置と長さは合っています。
合否は、次の4点で判定します。
- 先頭の数件で、ID、日付、区分コードのように意味が分かる項目が、意味の分かる値になっている
- COMP-3の最後のニブルが、すべてC、D、Fのいずれかになっている
- ファイルのバイト数が、レコード長で割り切れる(可変長で長さ情報が付く形式は、この確認が使えません)
- JCLのDD文にLRECLがあれば、表のレコード長と等しい
4つのうち1つでも崩れたら、表を直します。多くの場合は、REDEFINESの二重計上、COMPの割り当て方式、FILLERの見落としのどれかです。
JCLからジョブ間のファイルの流れを作る
JCLは、どのプログラムをどのファイルで動かすかを書いたものです。EXEC文がステップで走らせるプログラムを示し、DD文がプログラム内のファイル記述とディスクなどの実体を結びます。DD名がプログラム側の入出力名で、DSNが実体の名前です。
検証用のJCLです。
//NIGHTLY JOB (ACCT),'CUST BATCH',CLASS=A
//STEP010 EXEC PGM=CUSTEXT
//CUSTIN DD DSN=PROD.CUST.MASTER,DISP=SHR
//CUSTOUT DD DSN=&&TMPCUST,DISP=(NEW,PASS)
//STEP020 EXEC PGM=SORT
//SORTIN DD DSN=&&TMPCUST,DISP=(OLD,DELETE)
//SORTOUT DD DSN=PROD.CUST.SORTED,DISP=(NEW,CATLG,DELETE)
//SYSIN DD *
SORT FIELDS=(1,6,CH,A)
/*
//STEP030 EXEC PGM=CUSTRPT
//CUSTIN DD DSN=PROD.CUST.SORTED,DISP=SHR
//REPORT DD SYSOUT=*DD文のDISPは、ファイルの扱いを示します。DISP=(NEW,CATLG,DELETE)は「正常終了なら新規作成してカタログに登録し、異常終了なら削除する」の意味です。DISP=SHRは他のプログラムが同時に読めることを示します。この性質から、NEWで始まるDDは出力、SHRは入力とみなして流れを組み立てられます。次のスクリプトがその表を出します。
jclflow.py(EXEC / DD を表にする)
import re, sys
step = None; rows = []
for line in open(sys.argv[1]):
if not line.startswith('//') or line.startswith('//*'): continue
m = re.match(r'//(\S*)\s+(EXEC|DD)\s+(.*)', line)
if not m: continue
name, op, rest = m.groups()
if op == 'EXEC':
step = (name, re.search(r'PGM=(\w+)', rest).group(1))
else:
dsn = re.search(r'DSN=([^,\s]+)', rest)
disp = re.search(r'DISP=(\(?[\w,]+\)?)', rest)
sysout = 'SYSOUT' in rest
target = dsn.group(1) if dsn else ('SYSOUT' if sysout else ('*(in-stream)' if rest.startswith('*') else '?'))
d = disp.group(1) if disp else ''
new = d.strip('()').split(',')[0] == 'NEW' or sysout
rows.append((step[0], step[1], name, target, d, 'OUT' if new else 'IN'))
print(f"{'STEP':<8}{'PGM':<9}{'DD':<9}{'データセット':<20}{'DISP':<20}向き")
for r in rows: print(f"{r[0]:<8}{r[1]:<9}{r[2]:<9}{r[3]:<20}{r[4]:<20}{r[5]}")python3 jclflow.py JOB1.jclSTEP PGM DD データセット DISP 向き
STEP010 CUSTEXT CUSTIN PROD.CUST.MASTER SHR IN
STEP010 CUSTEXT CUSTOUT &&TMPCUST (NEW,PASS) OUT
STEP020 SORT SORTIN &&TMPCUST (OLD,DELETE) IN
STEP020 SORT SORTOUT PROD.CUST.SORTED (NEW,CATLG,DELETE) OUT
STEP020 SORT SYSIN *(in-stream) IN
STEP030 CUSTRPT CUSTIN PROD.CUST.SORTED SHR IN
STEP030 CUSTRPT REPORT SYSOUT OUTこの表から、同じDSNが出力として現れたステップと、入力として現れたステップを結べば、流れが決まります。この例では次の1本です。
PROD.CUST.MASTER -> [STEP010 CUSTEXT] -> &&TMPCUST -> [STEP020 SORT] -> PROD.CUST.SORTED -> [STEP030 CUSTRPT] -> SYSOUT&&で始まる名前は、ジョブの中だけで使う一時データセットです。ジョブをまたぐ受け渡しには、カタログ登録されるデータセット(ここではPROD.CUST.SORTED)が使われます。スクリプトの向きは、DISPからの推定にすぎません。DISP=OLDのファイルが読み書き両用のこともあるので、最終的にはCOBOL側のSELECT ... ASSIGN TOと、OPEN INPUTかOPEN OUTPUTかを見て確定します。
DD名とCOBOLの対応を確かめる
COBOLのSELECT CUST-IN ASSIGN TO CUSTINのCUSTINが、JCLのDD名に当たります。表のDD名ごとに、COBOLソースの該当するSELECT文とFDを探し、そのFDが使うcopybookを先の項目表と結びます。これで「どのステップが、どのファイルを、どのレイアウトで読むか」が1枚の関係になります。GnuCOBOLでは、DD名を環境変数DD_<名前>で実ファイルに対応させられるので、移行後の検証にも同じ対応表を使えます。
JCLがEXEC PROC=名前や、名前だけのEXEC文でカタログ化手続きを呼んでいる場合は、手続きの中のDD文が見えません。手続きの定義ファイルも一緒にClaudeへ渡します。
Claude Codeに任せる形にする
ここまでをClaude Codeに任せるときは、検証用の2本のスクリプトを作業ディレクトリに置き、CLAUDE.mdに手順を書きます。公式は、CLAUDE.mdを1ファイル200行以内にするのが目安だとしています。COBOL固有の最小限だけを書きます。
CLAUDE.mdの断片(例)
## COBOL のデータ定義とJCLの読解
- copybook は `copy/`、JCL は `jcl/`、サンプルは `sample/`(個人情報は含めない)
- ファイル全体を iconv で変換しない。COMP / COMP-3 の項目は 16 進のまま扱う
- copybook を読んだら `python3 tools/layout.py <copybook>` で項目表を作り、
手計算でなく表を根拠にする
- COMP の長さは `-fbinary-size=2-4-8` の想定。tools/layout.py はこの割り当てで計算する。移行元が違うときは表の前に報告する
- REDEFINES の項目はレコード長に足さない。有効になる条件を表に書く
- 表ができたら `sample/` の1件を `tools/decode.py` で復号し、意味のある値になるか報告する
- JCL は `python3 tools/jclflow.py <jcl>` で表にし、DD名を COBOL の SELECT と対応づける公式のワークフロー集には、@でファイルやディレクトリを会話に取り込む書き方があります。text形式で256KBを超えるファイルは取り込まれない、とも書かれています。固定長ファイルを大量に渡すのでなく、先頭数件だけを切り出して渡す運用が前提になります。
# 先頭1件(32バイト)だけを切り出して16進で渡す例
head -c 32 sample/cust.dat | xxd > sample/cust.first.hex
claude "@copy/CUSTREC.cpy と @sample/cust.first.hex を、tools/layout.py と tools/decode.py で検算して"複数のcopybookを繰り返し処理するなら、公式の「Pipe Claude into scripts」にある標準入力と標準出力の使い方が使えます。claude -pは非対話で実行する形で、ここでは各copybookの項目表を作る作業を1件ずつ回す用途になります。
落とし穴
実データのサンプルに個人情報が残っている
本番ファイルの先頭行には、氏名や口座番号が入っていることがあります。会話に渡す前に、氏名や番号の項目を同じ長さのダミー値に置き換えた検証用ファイルを作ります。位置と型を調べる目的なので、値が本物である必要はありません。
表が合わないときの切り分け
表と復号結果が合わないときは、上から順に確かめます。
復号がずれるときの順序
- Qファイルのバイト数はレコード長で割り切れますか?答えがいいえのとき答えがはい
可変長で長さ情報が付いているか、FILLERやREDEFINESで長さの計算を誤っています。ファイルの形式を確認します。
- Q文字項目の先頭は読める文字ですか?答えがいいえのとき答えがはい
開始位置がずれているか、コードページが違います。直前の項目の長さを見直します。
- QCOMP-3の最後のニブルはC、D、Fですか?答えがいいえのとき答えがはい
COMP-3の長さを偶数桁として計算しているか、位置が1つずれています。
表は実データと整合しています
この表をレイアウトの基準として保存し、移行後の出力の突き合わせに進みます。
まとめ
読解の合格条件を「表が実データ1件で復号できること」に固定すると、以降の移行作業の基準を同じ表で持てます。AWS Transformのような移行ツールに変換まで任せる場合も、変換前に手元で作った項目表があれば、変換後のデータ定義が元と合っているかを検査する基準になります。ツールの使い方はClaude CodeのAWS Transformプラグインにあります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。