Macでrsyncして文字化けしたファイル名をシェル芸で何とかする

この記事は[武蔵野 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/musashino) 17日目の記事です。
## 検証環境
Mac端末: macOS Sierra (10.12.6)
Linuxサーバ: Ubuntu 16.04.5
(どちらも少し古いですね...)
# 状況設定
ある日、Linuxサーバに保存している一連のファイルを、Mac端末で編集して、再度Linuxサーバに書き戻したいシチュエーションに遭遇しました。
そこで、rsyncコマンドを使って Linux→Macへファイルを転送し

```bash
Mac:foo ishii$ rsync -a LinuxServer:/foo/ /foo
```
Mac上で編集して

```bash
Mac:foo ishii$ echo vim > 武蔵野アドベントカレンダー2018/エディタ系/emacs
Mac:foo ishii$ echo emacs > 武蔵野アドベントカレンダー2018/エディタ系/vim
Mac:foo ishii$ touch 武蔵野アドベントカレンダー2018/ディーーープラーーーニング/ラーメン二郎
```
再び、Mac→Linuxへファイルを転送しました。

```bash
Mac:foo ishii$ rsync -a /foo/ LinuxServer:/foo
```
ところが、Linuxサーバ上でファイル一覧を見てみると

```bash
ishii@LinuxServer:/foo$ tree .
.
├── 武蔵野アドÿベÿントカレンダÿー2018
│   ├── エデÿィタ系
│   │   ├── emacs
│   │   └── vim
│   ├── デÿィーーープÿラーーーニングÿ
│   │   ├── chainer
│   │   ├── tensorflow
│   │   └── ラーメン二郎
│   └── ポÿエム系
│       └── アジÿャイル開発
│           └── デÿジÿタルトランスフォーメーション.md
└── 武蔵野アドベントカレンダー2018
    ├── エディタ系
    │   ├── emacs
    │   └── vim
    ├── ディーーープラーーーニング
    │   ├── chainer
    │   └── tensorflow
    └── ポエム系
        └── アジャイル開発
            └── デジタルトランスフォーメーション.md

10 directories, 11 files
```
似たようなディレクトリツリーが２つできており、片方は何だか文字化けして、ところどころ `ÿ` という文字が見えます。
さて、テキスト処理とファイル操作と言えば、[シェル芸人](https://ja.wikipedia.org/wiki/USP%E5%8F%8B%E3%81%AE%E4%BC%9A#%E3%82%B7%E3%82%A7%E3%83%AB%E8%8A%B8)の大好物です。
(実は便利なコマンド一発で解決できるのですが)ここでは、汎用的なコマンドの組み合わせで、シェル芸的に何とかしてみましょう。

## シェル芸で文字コードを分析する
とりあえず、文字化けっぽいので、２つのディレクトリの違いを見ていきます

```bash
ishii@LinuxServer:/foo$ ls -1
武蔵野アドÿベÿントカレンダÿー2018
武蔵野アドベントカレンダー2018

ishii@LinuxServer:/foo$ ls -1 | nkf -g
UTF-8
```
日本語環境のLinuxサーバなので、ファイル名のエンコードはUTF-8です。
Unicodeのコードポイントを見たいので、UTF-16LEに変換して16進表示します。

```bash
ishii@LinuxServer:/foo$ ls -1 | iconv -f UTF-8 -t UTF-16LE | od -x
0000000 6b66 8535 91ce 30a2 30c8 3099 30d8 3099
0000020 30f3 30c8 30ab 30ec 30f3 30bf 3099 30fc
0000040 0032 0030 0031 0038 000a 6b66 8535 91ce
0000060 30a2 30c9 30d9 30f3 30c8 30ab 30ec 30f3
0000100 30c0 30fc 0032 0030 0031 0038 000a
0000116
```
元の文字列と並べてみます

```bash
ishii@LinuxServer:/foo$ ls -1 | while read l; do echo $l; echo $l | iconv -f UTF-8 -t UTF-16LE | od -x -An | xargs; done
武蔵野アドÿベÿントカレンダÿー2018
6b66 8535 91ce 30a2 30c8 3099 30d8 3099 30f3 30c8 30ab 30ec 30f3 30bf 3099 30fc 0032 0030 0031 0038 000a
武蔵野アドベントカレンダー2018
6b66 8535 91ce 30a2 30c9 30d9 30f3 30c8 30ab 30ec 30f3 30c0 30fc 0032 0030 0031 0038 000a
```
見やすいように、元の文字とコードポイントの表示位置を揃えます。

```bash
ishii@LinuxServer:/foo$ ls -1 | while read l; do (echo $l | sed 's/./& /g'; echo $l | iconv -f UTF-8 -t UTF-16LE | od -x -An | xargs) | column -t; done
武    蔵    野    ア    ト    ゙    ヘ    ゙    ン    ト    カ    レ    ン    タ    ゙    ー    2     0     1     8
6b66  8535  91ce  30a2  30c8  3099  30d8  3099  30f3  30c8  30ab  30ec  30f3  30bf  3099  30fc  0032  0030  0031  0038  000a
武    蔵    野    ア    ド    ベ    ン    ト    カ    レ    ン    ダ    ー    2     0     1     8
6b66  8535  91ce  30a2  30c9  30d9  30f3  30c8  30ab  30ec  30f3  30c0  30fc  0032  0030  0031  0038  000a
```
1行目の方は少しズレてしまいましたが、だいたい見えてきました。
Mac端末から転送した方のディレクトリは、`ド`という文字を U+30c8 U+3099 の組み合わせで表現しているのに対し
元々Linuxサーバにあったディレクトリの方は、U+30c9 一文字だけで表現しているようです。
前者(Mac, HFS+)をNFD(Normalization Form Canonical Decomposition)と呼び、
後者(Linux)をNFC(Normalization Form Canonical Composition)と呼ぶそうです。

## シェル芸でUnicodeテキストをいじる
さっそく、NFD表現されているUnicodeテキストを、NFC表現に変換していきます。
本当は、UnicodeのNormalizationは非常に沼が深く、様々なケースを考慮しなければならないので、専用のツールを使う方が好ましいです。
ただし、今回は「目の前の問題がなるべく早く片付けば、それで良い」の精神で、濁点(U+3099)と半濁点(U+309A)だけに絞って対応してみます。

まず、16進表記されたUnicodeコードポイントを、bashのクオート表現に変換します。

```bash
ishii@LinuxServer:/foo$ find | while read l; do echo $l | iconv -f UTF-8 -t UTF-16LE | od -x -An | xargs | awk '{printf "echo $'\''";for(i=1;i<NF;i++){printf "\\u%s",$i};print "'\''"}'; doneecho $'\u002e'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30a8\u30c6\u3099\u30a3\u30bf\u7cfb'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30a8\u30c6\u3099\u30a3\u30bf\u7cfb\u002f\u0076\u0069\u006d'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30a8\u30c6\u3099\u30a3\u30bf\u7cfb\u002f\u0065\u006d\u0061\u0063\u0073'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30db\u309a\u30a8\u30e0\u7cfb'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30db\u309a\u30a8\u30e0\u7cfb\u002f\u30a2\u30b7\u3099\u30e3\u30a4\u30eb\u958b\u767a'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30db\u309a\u30a8\u30e0\u7cfb\u002f\u30a2\u30b7\u3099\u30e3\u30a4\u30eb\u958b\u767a\u002f\u30c6\u3099\u30b7\u3099\u30bf\u30eb\u30c8\u30e9\u30f3\u30b9\u30d5\u30a9\u30fc\u30e1\u30fc\u30b7\u30e7\u30f3\u002e\u006d\u0064'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30c6\u3099\u30a3\u30fc\u30fc\u30fc\u30d5\u309a\u30e9\u30fc\u30fc\u30fc\u30cb\u30f3\u30af\u3099'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30c6\u3099\u30a3\u30fc\u30fc\u30fc\u30d5\u309a\u30e9\u30fc\u30fc\u30fc\u30cb\u30f3\u30af\u3099\u002f\u30e9\u30fc\u30e1\u30f3\u4e8c\u90ce'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30c6\u3099\u30a3\u30fc\u30fc\u30fc\u30d5\u309a\u30e9\u30fc\u30fc\u30fc\u30cb\u30f3\u30af\u3099\u002f\u0074\u0065\u006e\u0073\u006f\u0072\u0066\u006c\u006f\u0077'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c8\u3099\u30d8\u3099\u30f3\u30c8\u30ab\u30ec\u30f3\u30bf\u3099\u30fc\u0032\u0030\u0031\u0038\u002f\u30c6\u3099\u30a3\u30fc\u30fc\u30fc\u30d5\u309a\u30e9\u30fc\u30fc\u30fc\u30cb\u30f3\u30af\u3099\u002f\u0063\u0068\u0061\u0069\u006e\u0065\u0072'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30a8\u30c7\u30a3\u30bf\u7cfb'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30a8\u30c7\u30a3\u30bf\u7cfb\u002f\u0076\u0069\u006d'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30a8\u30c7\u30a3\u30bf\u7cfb\u002f\u0065\u006d\u0061\u0063\u0073'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30dd\u30a8\u30e0\u7cfb'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30dd\u30a8\u30e0\u7cfb\u002f\u30a2\u30b8\u30e3\u30a4\u30eb\u958b\u767a'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30dd\u30a8\u30e0\u7cfb\u002f\u30a2\u30b8\u30e3\u30a4\u30eb\u958b\u767a\u002f\u30c7\u30b8\u30bf\u30eb\u30c8\u30e9\u30f3\u30b9\u30d5\u30a9\u30fc\u30e1\u30fc\u30b7\u30e7\u30f3\u002e\u006d\u0064'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30c7\u30a3\u30fc\u30fc\u30fc\u30d7\u30e9\u30fc\u30fc\u30fc\u30cb\u30f3\u30b0'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30c7\u30a3\u30fc\u30fc\u30fc\u30d7\u30e9\u30fc\u30fc\u30fc\u30cb\u30f3\u30b0\u002f\u0074\u0065\u006e\u0073\u006f\u0072\u0066\u006c\u006f\u0077'
echo $'\u002e\u002f\u6b66\u8535\u91ce\u30a2\u30c9\u30d9\u30f3\u30c8\u30ab\u30ec\u30f3\u30c0\u30fc\u0032\u0030\u0031\u0038\u002f\u30c7\u30a3\u30fc\u30fc\u30fc\u30d7\u30e9\u30fc\u30fc\u30fc\u30cb\u30f3\u30b0\u002f\u0063\u0068\u0061\u0069\u006e\u0065\u0072'
```
濁点(U+3099)の直前の文字はコードポイントに1を加算し、半濁点(U+309A)の直前の文字には2を加算します。
変換前と変換後のファイルパスを半角スペース区切りで並べます。

```bash
ishii@LinuxServer:/foo$ find | while read l; do echo -n "$l "; echo $l | iconv -f UTF-8 -t UTF-16LE | od -x -An | xargs | awk '{printf "echo $'\''";for(i=1;i<NF;i++){if(i<NF-1&&$(i+1)=="3099"||$(i+1)=="309a"){printf("\\u%x",strtonum("0x" $i)+strtonum("0x" $(i+1))-0x3098);i+=1}else{printf "\\u%s",$i}};print "'\''"}' | bash; done
. .
./武蔵野アドÿベÿントカレンダÿー20./武蔵野アドベントカレンダー2018
./武蔵野アドÿベÿントカレンダÿー2018/エデÿィ ./武蔵野アドベントカレンダー2018/エディタ系
./武蔵野アドÿベÿントカレンダÿー2018/エデÿィタ系/./武蔵野アドベントカレンダー2018/エディタ系/vim
./武蔵野アドÿベÿントカレンダÿー2018/エデÿィタ系/em./武蔵野アドベントカレンダー2018/エディタ系/emacs
./武蔵野アドÿベÿントカレンダÿー2018/ポÿエ ./武蔵野アドベントカレンダー2018/ポエム系
./武蔵野アドÿベÿントカレンダÿー2018/ポÿエム系/アジÿャイル./武蔵野アドベントカレンダー2018/ポエム系/アジャイル開発
./武蔵野アドÿベÿントカレンダÿー2018/ポÿエム系/アジÿャイル開発/デÿジÿタルトランスフォーメーシ ./武蔵野アドベントカレンダー2018/ポエム系/アジャイル開発/デジタルトランスフォーメーション.md
./武蔵野アドÿベÿントカレンダÿー2018/デÿィーーープÿラーーーニ./武蔵野アドベントカレンダー2018/ディーーープラーーーニング
./武蔵野アドÿベÿントカレンダÿー2018/デÿィーーープÿラーーーニングÿ/ÿラーメ ./武蔵野アドベントカレンダー2018/ディーーープラーーーニング/ラーメン二郎
./武蔵野アドÿベÿントカレンダÿー2018/デÿィーーープÿラーーーニングÿ/ÿtenso./武蔵野アドベントカレンダー2018/ディーーープラーーーニング/tensorflow
./武蔵野アドÿベÿントカレンダÿー2018/デÿィーーープÿラーーーニングÿ/ÿch./武蔵野アドベントカレンダー2018/ディーーープラーーーニング/chainer
./武蔵野アドベントカレンダー2018 ./武蔵野アドベントカレンダー2018
./武蔵野アドベントカレンダー2018/エディタ系 ./武蔵野アドベントカレンダー2018/エディタ系
./武蔵野アドベントカレンダー2018/エディタ系/vim ./武蔵野アドベントカレンダー2018/エディタ系/vim
./武蔵野アドベントカレンダー2018/エディタ系/emacs ./武蔵野アドベントカレンダー2018/エディタ系/emacs
./武蔵野アドベントカレンダー2018/ポエム系 ./武蔵野アドベントカレンダー2018/ポエム系
./武蔵野アドベントカレンダー2018/ポエム系/アジャイル開発 ./武蔵野アドベントカレンダー2018/ポエム系/アジャイル開発
./武蔵野アドベントカレンダー2018/ポエム系/アジャイル開発/デジタルトランスフォーメーション.md ./武蔵野アドベントカレンダー2018/ポエム系/アジャイル開発/デジタルトランスフォーメーション.md
./武蔵野アドベントカレンダー2018/ディーーープラーーーニング ./武蔵野アドベントカレンダー2018/ディーーープラーーーニング
./武蔵野アドベントカレンダー2018/ディーーープラーーーニング/tensorflow ./武蔵野アドベントカレンダー2018/ディーーープラーーーニング/tensorflow
./武蔵野アドベントカレンダー2018/ディーーープラーーーニング/chainer ./武蔵野アドベントカレンダー2018/ディーーープラーーーニング/chainer
```

## シェル芸でファイル操作する
変換後(NFC)のファイルパス中のディレクトリ名を取り出し、そのディレクトリを(もし存在しなければ)作成します。

```bash
ishii@LinuxServer:/foo$ find | while read l; do echo -n "$l "; echo $l | iconv -f UTF-8 -t UTF-16LE | od -x -An | xargs | awk '{printf "echo $'\''";for(i=1;i<NF;i++){if(i<NF-1&&$(i+1)=="3099"||$(i+1)=="309a"){printf("\\u%x",strtonum("0x" $i)+strtonum("0x" $(i+1))-0x3098);i+=1}else{printf "\\u%s",$i}};print "'\''"}' | bash; done | while read a b; do mkdir -p $(dirname $b); done
```
配下の通常ファイルのうち、変換前と変換後のパスが異なるファイルを、変換前のパスから変換後のパスへ移動します。

```bash
ishii@LinuxServer:/foo$ find -type f | while read l; do echo -n "$l "; echo $l | iconv -f UTF-8 -t UTF-16LE | od -x -An | xargs | awk '{printf "echo $'\''";for(i=1;i<NF;i++){if(i<NF-1&&$(i+1)=="3099"||$(i+1)=="309a"){printf("\\u%x",strtonum("0x" $i)+strtonum("0x" $(i+1))-0x3098);i+=1}else{printf "\\u%s",$i}};print "'\''"}' | bash; done | awk '$1!=$2' | while read a b; do mv $a $b; done
```
配下のディレクトリのうち、変換前と変換後のパスが異なるディレクトリについて、変換前のパスを削除します。

```bash
ishii@LinuxServer:/foo$ find -type d | while read l; do echo -n "$l "; echo $l | iconv -f UTF-8 -t UTF-16LE | od -x -An | xargs | awk '{printf "echo $'\''";for(i=1;i<NF;i++){if(i<NF-1&&$(i+1)=="3099"||$(i+1)=="309a"){printf("\\u%x",strtonum("0x" $i)+strtonum("0x" $(i+1))-0x3098);i+=1}else{printf "\\u%s",$i}};print "'\''"}' | bash; done | awk '$1!=$2' | while read a b; do rm -r $a 2>/dev/null; done

ishii@LinuxServer:/foo$ tree .
.
└── 武蔵野アドベントカレンダー2018
    ├── エディタ系
    │   ├── emacs
    │   └── vim
    ├── ディーーープラーーーニング
    │   ├── chainer
    │   ├── tensorflow
    │   └── ラーメン二郎
    └── ポエム系
        └── アジャイル開発
            └── デジタルトランスフォーメーション.md

5 directories, 6 files
```
無事に１つのディレクトリに統合することができました。

## 本来の正しいやり方
[Macの濁点ファイルをなんとかする](https://qiita.com/esfpp/items/e360aa290d8da2b3b981) という記事によると `convmv` というコマンドを使えば、一発で解決できるようです。

また、MacからLinuxサーバへrsyncで転送する際に、rsyncのバージョン3.0以降であれば、`--iconv`オプションを指定することで、NFDからNFCへ変換して転送することができるようです。