この記事は武蔵野 Advent Calendar 2018 17日目の記事です。
検証環境
Mac端末: macOS Sierra (10.12.6)
Linuxサーバ: Ubuntu 16.04.5
(どちらも少し古いですね...)
状況設定
ある日、Linuxサーバに保存している一連のファイルを、Mac端末で編集して、再度Linuxサーバに書き戻したいシチュエーションに遭遇しました。
そこで、rsyncコマンドを使って Linux→Macへファイルを転送し
Mac:foo ishii$ rsync -a LinuxServer:/foo/ /foo
Mac上で編集して
Mac:foo ishii$ echo vim > 武蔵野アドベントカレンダー2018/エディタ系/emacs
Mac:foo ishii$ echo emacs > 武蔵野アドベントカレンダー2018/エディタ系/vim
Mac:foo ishii$ touch 武蔵野アドベントカレンダー2018/ディーーープラーーーニング/ラーメン二郎
再び、Mac→Linuxへファイルを転送しました。
Mac:foo ishii$ rsync -a /foo/ LinuxServer:/foo
ところが、Linuxサーバ上でファイル一覧を見てみると
ishii@LinuxServer:/foo$ tree .
.
├── 武蔵野アドÿベÿントカレンダÿー2018
│ ├── エデÿィタ系
│ │ ├── emacs
│ │ └── vim
│ ├── デÿィーーープÿラーーーニングÿ
│ │ ├── chainer
│ │ ├── tensorflow
│ │ └── ラーメン二郎
│ └── ポÿエム系
│ └── アジÿャイル開発
│ └── デÿジÿタルトランスフォーメーション.md
└── 武蔵野アドベントカレンダー2018
├── エディタ系
│ ├── emacs
│ └── vim
├── ディーーープラーーーニング
│ ├── chainer
│ └── tensorflow
└── ポエム系
└── アジャイル開発
└── デジタルトランスフォーメーション.md
10 directories, 11 files
似たようなディレクトリツリーが2つできており、片方は何だか文字化けして、ところどころ ÿ
という文字が見えます。
さて、テキスト処理とファイル操作と言えば、シェル芸人の大好物です。
(実は便利なコマンド一発で解決できるのですが)ここでは、汎用的なコマンドの組み合わせで、シェル芸的に何とかしてみましょう。
シェル芸で文字コードを分析する
とりあえず、文字化けっぽいので、2つのディレクトリの違いを見ていきます
ishii@LinuxServer:/foo$ ls -1
武蔵野アドÿベÿントカレンダÿー2018
武蔵野アドベントカレンダー2018
ishii@LinuxServer:/foo$ ls -1 | nkf -g
UTF-8
日本語環境のLinuxサーバなので、ファイル名のエンコードはUTF-8です。
Unicodeのコードポイントを見たいので、UTF-16LEに変換して16進表示します。
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
元の文字列と並べてみます
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
見やすいように、元の文字とコードポイントの表示位置を揃えます。
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のクオート表現に変換します。
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を加算します。
変換前と変換後のファイルパスを半角スペース区切りで並べます。
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)のファイルパス中のディレクトリ名を取り出し、そのディレクトリを(もし存在しなければ)作成します。
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
配下の通常ファイルのうち、変換前と変換後のパスが異なるファイルを、変換前のパスから変換後のパスへ移動します。
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
配下のディレクトリのうち、変換前と変換後のパスが異なるディレクトリについて、変換前のパスを削除します。
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
無事に1つのディレクトリに統合することができました。
本来の正しいやり方
Macの濁点ファイルをなんとかする という記事によると convmv
というコマンドを使えば、一発で解決できるようです。
また、MacからLinuxサーバへrsyncで転送する際に、rsyncのバージョン3.0以降であれば、--iconv
オプションを指定することで、NFDからNFCへ変換して転送することができるようです。