fdupesを自作してみた


ファイルの重複チェックに便利なfdupesコマンドですが、入ってない環境でも使えるようにワンライナーを書いてみました。車輪の再発明です。

```bash
find . -type f | xargs md5sum > /tmp/a; grep -f <(awk '{print $1}' /tmp/a | sort | uniq -c | awk '{if($1 > 1) print $2}') /tmp/a | sort | perl -lane 'push @{$a{$F[0]}}, $F[1]; END {print join "\n", @{$a{$_}}, "" for keys %a}'
```

---
## fdupesとは
同一ファイルを探してくれるコマンドです。
重複したファイルがゴミじゃないか？とか、共通化出来ないか？とか、シンボリックリンクに置き換えられないか？などの調査の取っ掛かりに使えます。

```bash
# fdupes -r .
./images/milestone_done.png
./images/task_done.png

./images/arrow_collapsed.png
./images/bullet_arrow_right.png

./help/bg/wiki_syntax_detailed.html
./help/pl/wiki_syntax_detailed.html
./help/sv/wiki_syntax_detailed.html
./help/bs/wiki_syntax_detailed.html
...
```

空行ごとにグルーピングされたもの同士でdiff取ってみれば、それぞれすべて同じということが分かります。

```bash
# diff ./images/milestone_done.png ./images/task_done.png
# diff ./images/arrow_collapsed.png ./images/bullet_arrow_right.png
# diff ./help/bg/wiki_syntax_detailed.html ./help/pl/wiki_syntax_detailed.html
# diff ./help/pl/wiki_syntax_detailed.html ./help/sv/wiki_syntax_detailed.html
```

## md5sum
これを自作するには、findでカレント配下の一覧を取りつつ、md5sumで各ファイルのフィンガープリントを計算します。やりたいことは、これが同じものをグルーピングさせて表示させたい、というだけです。


```bash
# find . -type f | xargs md5sum
12203dd0b04ea085217b93b0871053b5  ./dispatch.fcgi.example
61affd115c8d129c258713445d558301  ./javascripts/datepicker.js
50e838e3ddd98dc7845db32539fe980f  ./javascripts/attachments.js
25e711a474b72694444e46441a45daef  ./javascripts/i18n/datepicker-hr.js
4ec013ff08274062b9affe005d6e0bad  ./javascripts/i18n/datepicker-it.js
a67ea4ea3a1305230d78c663d781aec5  ./javascripts/i18n/datepicker-cs.js
0a30262822d33944bef4f32f2c40dabd  ./javascripts/i18n/datepicker-nl.js
d379d734a97928692297893bf538c503  ./javascripts/i18n/datepicker-eu.js
01d3088ea6837a13205cea656d042b2c  ./javascripts/i18n/datepicker-fa.js
5fca3c9b96c5ef5142521f7e650edf5f  ./javascripts/i18n/datepicker-th.js
...
```

左側のフィンガープリントだけを抽出し、`uniq -c`で数を数えます。(わかりやすくするため、数字順で逆ソートもしてます)

```bash
# find . -type f | xargs md5sum | awk '{print $1}' | sort | uniq -c | sort -nr
     43 8ec587ffbce2b400b7a3abc1087c61f2
     41 150d3ca182103800e898533daac8a93c
      6 c49e65f5c02ababbaa0c9a9968424950
      2 980dcfdb816ce626e4d2df5c2a308549
      2 40c58172e0c52eee4deb5227ec37f0cf
      1 ff96c02d8f18116bb3f005f2c8b86e91
      1 feb0aa630b9a43ca8b40ca6c85038327
      1 fd6a3f960f0d6e1ce7e6466d592453fe
      1 fba036d7348ff28f7ab7dacbc52bc32a
      1 fb00cc94dfc8f319dda707b957aae8a6
...
```

カウント数が2以上のものを抽出し、フィンガープリントだけ表示させます。

```bash
# find . -type f | xargs md5sum | awk '{print $1}' | sort | uniq -c | awk '{if($1>1) print $2}'
150d3ca182103800e898533daac8a93c
40c58172e0c52eee4deb5227ec37f0cf
8ec587ffbce2b400b7a3abc1087c61f2
980dcfdb816ce626e4d2df5c2a308549
c49e65f5c02ababbaa0c9a9968424950
```

これを一旦ファイルに落とし、`grep -f`の材料とします。

```bash
# find . -type f | xargs md5sum | awk '{print $1}' | sort | uniq -c | awk '{if($1>1) print $2}' > /tmp/g
```

元の一覧から`grep -f`します。

```bash
# find . -type f | xargs md5sum | grep -f /tmp/g
c49e65f5c02ababbaa0c9a9968424950  ./javascripts/jstoolbar/lang/jstoolbar-en.js
c49e65f5c02ababbaa0c9a9968424950  ./javascripts/jstoolbar/lang/jstoolbar-sq.js
c49e65f5c02ababbaa0c9a9968424950  ./javascripts/jstoolbar/lang/jstoolbar-he.js
c49e65f5c02ababbaa0c9a9968424950  ./javascripts/jstoolbar/lang/jstoolbar-uk.js
c49e65f5c02ababbaa0c9a9968424950  ./javascripts/jstoolbar/lang/jstoolbar-en-gb.js
c49e65f5c02ababbaa0c9a9968424950  ./javascripts/jstoolbar/lang/jstoolbar-az.js
150d3ca182103800e898533daac8a93c  ./help/bg/wiki_syntax.html
8ec587ffbce2b400b7a3abc1087c61f2  ./help/bg/wiki_syntax_detailed.html
150d3ca182103800e898533daac8a93c  ./help/sl/wiki_syntax.html
8ec587ffbce2b400b7a3abc1087c61f2  ./help/sl/wiki_syntax_detailed.html
...
```

ソートして同一フィンガープリント同士でグルーピングします。

```bash
# find . -type f | xargs md5sum | grep -f /tmp/g | sort | perl -MData::Dumper -lane 'push @{$a{$F[0]}}, $F[1]; END {print Dumper %a}'
$VAR1 = '150d3ca182103800e898533daac8a93c';
$VAR2 = [
          './help/ar/wiki_syntax.html',
          './help/az/wiki_syntax.html',
          './help/bg/wiki_syntax.html',
          './help/bs/wiki_syntax.html',
          './help/ca/wiki_syntax.html',
          './help/da/wiki_syntax.html',
          './help/el/wiki_syntax.html',
          './help/en-gb/wiki_syntax.html',
(中略)
          './help/zh/wiki_syntax.html'
        ];
$VAR3 = 'c49e65f5c02ababbaa0c9a9968424950';
$VAR4 = [
          './javascripts/jstoolbar/lang/jstoolbar-az.js',
          './javascripts/jstoolbar/lang/jstoolbar-en-gb.js',
          './javascripts/jstoolbar/lang/jstoolbar-en.js',
          './javascripts/jstoolbar/lang/jstoolbar-he.js',
          './javascripts/jstoolbar/lang/jstoolbar-sq.js',
          './javascripts/jstoolbar/lang/jstoolbar-uk.js'
        ];
$VAR5 = '8ec587ffbce2b400b7a3abc1087c61f2';
$VAR6 = [
          './help/ar/wiki_syntax_detailed.html',
          './help/az/wiki_syntax_detailed.html',
...
```

グループごとに空行で区切るように表示を工夫すればほぼ完成です。

```bash
# find . -type f | xargs md5sum | grep -f /tmp/g | sort | perl -lane 'push @{$a{$F[0]}}, $F[1]; END {print join "\n", @{$a{$_}}, "" for keys %a}'
./help/ar/wiki_syntax.html
./help/az/wiki_syntax.html
./help/bg/wiki_syntax.html
./help/bs/wiki_syntax.html
./help/ca/wiki_syntax.html
./help/da/wiki_syntax.html
./help/el/wiki_syntax.html
./help/en-gb/wiki_syntax.html
(中略)
./help/vi/wiki_syntax.html
./help/zh/wiki_syntax.html

./javascripts/jstoolbar/lang/jstoolbar-az.js
./javascripts/jstoolbar/lang/jstoolbar-en-gb.js
./javascripts/jstoolbar/lang/jstoolbar-en.js
./javascripts/jstoolbar/lang/jstoolbar-he.js
./javascripts/jstoolbar/lang/jstoolbar-sq.js
./javascripts/jstoolbar/lang/jstoolbar-uk.js

./help/ar/wiki_syntax_detailed.html
./help/az/wiki_syntax_detailed.html
...
```

### リファクタリング

bashの`<()`を使って`grep -f`用の中間ファイルを消します。

```bash
# find . -type f | xargs md5sum | grep -f <(find . -type f | xargs md5sum | awk '{print $1}' | sort | uniq -c | awk '{if($1 > 1) print $2}') | sort | perl -lane 'push @{$a{$F[0]}}, $F[1]; END {print join "\n", @{$a{$_}}, "" for keys %a}'
```

`find . -type f | xargs md5sum`が重複しているので中間ファイルとして切り出します。(やむなく)

```bash
# find . -type f | xargs md5sum > /tmp/a; grep -f <(awk '{print $1}' /tmp/a | sort | uniq -c | awk '{if($1 > 1) print $2}') /tmp/a | sort | perl -lane 'push @{$a{$F[0]}}, $F[1]; END {print join "\n", @{$a{$_}}, "" for keys %a}'
```

なんとか中間ファイルを作らない方法を試行錯誤しましたが、残念ながら力及ばず。これにて完成とします。
なかなか楽しいエクササイズになりました。