diffによるunified形式の意味について

最近VCS関係でよくdiffを使用しているのだが、表示される差分の情報を長いことわからないまま放置してきた。
いい加減わからないまま放置するのが気持ち悪くなってきたのでいろいろ調べたところ、差分の表示形式にはいくつかあるらしく、私が普段目にしている表示形式はunified形式という形式だったらしい。しかし、Manpage of diffを参照しても例が示されておらずいまいち理解できない。
当初はググればすぐにわかると思ったのだが、想像していた以上に情報が少なく詳細に説明されている記述が見つからなかったので、備忘録ついでにここへわかったことを記録しておくことにした。

次からのdiff表示の例では、「diffファイル(unified形式)について - 試験運用中なLinux備忘録」の例を元に説明させてもらった。*1

ぱっと見でわかること

次はdiffによりunified形式で表示された差分の例である。

$ diff -u test.txt.orig test.txt
--- test.txt.orig       2007-06-10 22:13:55.718311614 +0900
+++ test.txt    2007-06-10 22:54:23.038008776 +0900
@@ -2,9 +2,10 @@
 かきくけこ
 さしすせそ
 たちつてと
-なにぬねの
+なにぬねのん
 はひふへほ
 まみむめも
 やゆよ
 らりるれろ
 わをん
+がぎぐげご

まず冒頭の---および+++の行だが、それぞれが比較元、比較先のファイル名とそのファイルの日付であることはすぐにわかる。
また、出力中盤の部分が比較しているテキストで、その中の-および+がついている行は二つのファイルの相違点であることもすぐにわかるだろう。さらに冒頭の二行に---および+++が付加されていることから類推すれば、-がついている行は比較元のファイルにのみ存在する行であること、逆に+が付加されている行は比較先にのみ存在する行であることがわかる。頭に何も書かれていない行は両者に共通している行である。

問題「@@行はなんぞや」

ここで唯一わからないのは@@が書かれた行である。Manpage of diffでは次のようにしか書かれていない。

次に来るのは hunk (テキストブロック) である。繰り返し登場することもある。それぞれの hunk はファイルの異なっている 1 つ 1 つの部分を示している。 unified 形式の hunk は以下のようなものである:

@@ FROMFILE-RANGE TOFILE-RANGE @@
LINE-FROM-EITHER-FILE
LINE-FROM-EITHER-FILE...

この説明を「@@ -2,9 +2,10 @@」に当てはめると、FROMFILE-RANGEが-2,9、TOFILE-RANGEが2,10ということになるが、正直これだけではさっぱりである。しかし、正直なところいろいろ探してみてもこれ以上の情報が載っているところがほとんどなく、一時八方塞がりだった。

wikipediaのdiff項にヒント

一度真相解明を放棄してからも時々気が向いたときに検索していたのだが、wikipediaのdiff項を開いたところ、そこにちょうど知りたいことすべてが書かれていた。

ここでは@@行について次のように書かれていた。

@@ -R +R @@

変更箇所の範囲情報は2つの範囲についての情報を含んでいる。一方はマイナス記号が頭に付いた元ファイルにおける変更箇所を示すものであり、もう一方はプラス記号が頭に付いた変更後のファイルにおける変更箇所を示している。範囲を示すRはl,sという形式となり、lは変更箇所の開始行、sはそれぞれのファイルにおける変更箇所の行数を示している。GNU diffの大半のバージョンでは、Rにおいてsがデフォルトの1である場合にはカンマ以降を省略した形式も使える。

ここでやっとコンマの意味がわかった。例えば「@@ -2,9 +2,10 @@」の「2,9」は2行目から変更箇所が始まり、変更箇所が9行に渡ることを示している。
しかし、この情報を元に改めて出力された情報を見ても、この情報と変更箇所が一致していないことに気づいた。上の例では五行目が書き換えられ、十一行目に新たな行が挿入されているので「@@ -5,1 +5,1 @@」や「@@ -11,0 +11,1 @@」ならば理解できるのだが、上で書かれている二行目などに変更箇所などない。ここで非常に頭をひねることになってしまった。

@@の意味の正解

しばらく上の2,9と、-とも+とも表示されていない行を見比べたのち、やっとこの2,9の意味がわかった。この開始行の2は比較元のファイルで言うと「かきくけこ」に当たり、これは上のunified形式で表示されたdiff情報の最初の行である。つまり、この"変更箇所の開始行"とは、実際の変更箇所の前後を含めた数行分の、一番最初の行を指しており、その次の変更行数とはその前後を含めた行数なのである。
つまり、@@行が示している情報とは次のようなことになる。

あいうえお
かきくけこ   ← 2,9の2(行目) また変更箇所1行目
さしすせそ   ←         変更箇所2行目
たちつてと   ←         変更箇所3行目
なにぬねの   ←         変更箇所4行目
はひふへほ   ←         変更箇所5行目
まみむめも   ←         変更箇所6行目
やゆよ     ←         変更箇所7行目
らりるれろ   ←         変更箇所8行目
わをん     ←         変更箇所9行目、2,9の9(行分)
あいうえお
かきくけこ   ← 2,10の2(行目) また変更箇所1行目
さしすせそ   ←         変更箇所2行目
たちつてと   ←         変更箇所3行目
なにぬねのん  ←         変更箇所4行目
はひふへほ   ←         変更箇所5行目
まみむめも   ←         変更箇所6行目
やゆよ     ←         変更箇所7行目
らりるれろ   ←         変更箇所8行目
わをん     ←         変更箇所9行目
がぎぐげご   ←         変更箇所10行目、2,10の10(行分)

このように、変更箇所の前後を含めているため、@@行の情報は直感的ではなかったらしい。
いままでdiffによって表示される変更箇所の前後の情報は単に人間がわかりやすいように表示しているだけでパッチなどで変更箇所情報を使用するときはそれらの情報はなんの作用もしていないと思っていたのだが、今回の発見によりパッチなどでも前後情報を確認していることがわかった。
これはおそらく修正箇所の前後行の情報とも比べることにより、本当にパッチを適用してよいかなどをチェックするためだと考えられる。実際、この後unified形式のパッチを適用する場面があったのだが、適用先のファイルがパッチ作成時のものと違っていたため、適用できないとエラーが表示された。

蛇足 - 変更箇所の前後行情報の不完全性

上の知識があれば少し考えればすぐにわかることだが、このような適用先のチェックは非常に不完全であり、例えば記述されている変更箇所の行のすぐ一つ上の行を編集しても、通常通りパッチできてしまうことは用意に想像できる。究極的にはファイルのすべての情報を含まなければ完全に安全なパッチというものは不可能だと考えられるが、それはむしろパッチではなくファイルの上書きである。また、パッチの軽量性および変更箇所の可視化という意味でもあまりメリットはない。
まとめると、今のunified形式のパッチは対象ファイルが想定したものであるかを少しはチェックするものの非常に不完全であり、完全には安全性が保証されない*2。唯一といっていい利点は人間が目視したときにわかりやすいことであるが、それも変更箇所の表示方法次第で何とでもなると言える*3。よって、理論的には実際に変更された行とその変更内容のみが記述されているようなdiffファイルを使うのがもっとも効率的であると想像できる。
実際にそのような形式がないか調べて見たところ、diffコマンドにより標準で出力される差分ファイルはそのような最低限の記述のみされている形式のようである。diffの表示形式である標準形式、unified形式、context形式の違いとそれぞれの出力の仕方は「UNIXの部屋 コマンド検索:diff (*BSD/Linux)」が非常に参考になった。

*1:ちなみにこのブログ名、以前一度見たことがあると思ったらwine関係の設定で非常にお世話になったところだった。

*2:私が長いこと@@の意味がわからなかったのは、こういう風に考えており変更箇所の前後行などの情報を含めてもあまり役に立たないと考えていたからである。

*3:例えば、最低限の情報(変更行とその内容のみが記述されているようなdiffファイル)でもその適用前または適用後のファイルがあればそのパッチがどのようなものであるか容易に表示できる。