「このリストにしか無い行は、どれだろう」
2つの一覧を突き合わせたいとき、そんな確認をしたくなる場面があります。
とりあえず diff を叩いてみると、返ってくるのは「どこをどう書き換えれば一致するか」という編集手順。
私が知りたかったのは、もっと素朴な仕分けでした。
両方にある行と、片方だけにある行。
この用途にまっすぐ効くのが comm コマンドです。
commは行を「集合」として仕分ける
comm はソート済みの2つのファイルを先頭から突き合わせ、それぞれの行を3つのグループへ振り分けてくれます。
出力は3列に分かれる
まず、比較用の小さなファイルを2つ用意しました。
$ cat a.txt
apple
banana
cherry
date
$ cat b.txt
banana
cherry
fig
grape
オプションを付けずに実行すると、出力はこんな見た目になります。
$ comm a.txt b.txt
apple
banana
cherry
date
fig
grape
左端に寄っているのが a.txt だけにある行。
タブ1つぶん右へずれた行は、b.txt だけにあるものです。
タブ2つぶん下がった行が両方に共通する行。
つまり第1列=左だけ/第2列=右だけ/第3列=両方という並びで固定されています。
diffとは見ている軸が違う
同じ「2ファイルを比べる」でも、diff と comm が返す答えの形は別物です。
diff が返すのは、片方をもう片方へ変形するための編集手順。
comm が返すのは、その行がどちらの集合に属するかという所属情報です。
だから、行の並び順が揃っているかどうかを気にする必要がありません。
インストール済みパッケージの一覧、許可リスト、ファイル名の棚卸し。
「順番はどうでもよくて、有無だけ知りたい」場面では、comm のほうが素直に読めます。
要らない列を -1 -2 -3 で落とす
3列そのままだと目で追いにくいので、普段は列を絞って使うのが実用的です。
数字が指すのは残す列ではなく、消したい列の番号という点に注意してください。
共通行だけ見たいとき
第1列と第2列を捨てれば、両方にある行だけが残ります。
$ comm -12 a.txt b.txt
banana
cherry
「どちらにも登録されているのはどれか」を知りたいときの定番。
片方にしか無い行を見たいとき
a.txt 側だけを知りたいなら、第2列と第3列を落とします。
$ comm -23 a.txt b.txt
apple
date
逆に b.txt 側だけを取り出すなら、-13 の組み合わせ。
$ comm -13 a.txt b.txt
fig
grape
「消えた行」と「増えた行」を別々に取り出せるので、そのまま次の処理へパイプで流し込めます。
共通行を除いた差分だけ見たいとき
-3 だけを付けると、食い違っている行が左右に振り分けられて出てきました。
$ comm -3 a.txt b.txt
apple
date
fig
grape
タブの有無で、どちら側の行なのかが判別できる形になっています。
ソート済みが前提という落とし穴
comm でいちばんつまずくのは、入力が同じ順序に並んでいることを前提にしている点です。
並んでいないと結果が壊れる
試しに、わざと順不同のファイルを食わせてみます。
$ cat c.txt
cherry
apple
banana
$ comm c.txt b.txt
banana
cherry
comm: file 1 is not in sorted order
apple
banana
fig
grape
警告は出るものの、途中まで平然と間違った結果を吐いているところが厄介でした。
終了ステータスは 1 になるので、スクリプトの中なら戻り値で気づけます。
対策はシンプルで、比較の直前に sort を通すだけ。
$ comm <(sort c.txt) <(sort b.txt)
apple
banana
cherry
fig
grape
ロケールで照合順が変わる
もう一つの罠が、LC_COLLATE による照合順の違いです。
sort と comm が別々の基準で並びを判断すると、ソート済みのはずのファイルで不一致が起きてしまいます。
スクリプトに書くなら、両方の頭に LC_ALL=C を付けて基準をそろえておくと安全。
なお comm には、大文字と小文字を無視するオプションが用意されていません。
ケースをそろえたいときは、tr などで事前に変換してから渡す形になります。
コマンドの出力どうしを直接比べる
ファイルに落とさなくても、プロセス置換を使えばコマンドの出力を直接渡せるのです。
ディレクトリの中身を突き合わせる
$ comm -3 <(ls old) <(ls new)
legacy.js
router.js
old にしか無いファイルと、new にしか無いファイルが1行ずつ並びました。
移行作業で「消し忘れ」と「追加漏れ」を同時に洗い出せるので、私はこの形をよく使います。
件数だけ知りたいとき
–total を付けると、末尾に3列ぶんの合計が1行だけ加わりました。
$ comm --total a.txt b.txt | tail -1
2 2 2 total
左だけ2件、右だけ2件、共通2件。
中身を読まずに規模だけ把握したい場面では、この–total オプションが効きます。
最後に
comm は、2つのリストを集合として仕分けるためのコマンドでした。
並び順まで含めて比べたいなら diff、有無だけ知りたいなら comm。
この住み分けを覚えておくだけで、シェルでの棚卸し作業がずいぶん軽くなります。
ソートを通してから渡す、という一手間さえ守れば怖くないのが、このコマンドの良いところ。
細かい仕様はGNU coreutils マニュアルの comm invocationに載っています。
なお本記事の出力例は、GNU coreutils 8.32 と GNU bash 5.2.37 の環境で確かめたものです。
以上です。











コメントを残す