学生に sort コマンドを教えた日の講義録を作っていて妙な現象にぶち当たった。
kmf@pop-os:~$ ls -l /var/log/ | sort
drwx------ 2 root root 4096 2月 19 16:27 private
drwx------ 2 speech-dispatcher root 4096 1月 9 09:53 speech-dispa
(中略)
-rw-rw-r-- 1 root utmp 11520 5月 13 23:02 wtmp
-rw-rw-r-- 1 root utmp 292292 5月 13 23:02 lastlog
total 91028
kmf@pop-os:~$
「あれー? ヘンな並びになってますね〜」とかやってみせるための例だ。
この例ではsortコマンドがlsの出力を受けて最初のパーミッション表示を使ってソートするのだが、肝心のソート内容がおかしい。dで始まるディレクトリが最初に、続いて -rw なファイル群が続き、最後に「total 91028」が表示されているのだ。
すなわち、
d → ハイフン → t
という並びになっている。
記号はアルファベットより前だか後だかに来るはずが、間に入っている。えっ?
なにかヘマでもしてるのかと思い、何度やってみても、また他のディレクトリでやっても同じ現象が起きる。
調査
はじめは、totalの部分が標準エラー出力で後から出てきたりするということなのか? などと思ったけど、2> /dev/null しても出てくるし、なによりファイルに出力を保存して sort しても同じ結果になる。
困り果てて色々やっていたところ、同じマシンにMacからsshで入った端末では再現しない、という現象が出た。
kmf@pop-os:~$ export LANG=ja_JP.UTF-8
kmf@pop-os:~$ LANG=C ls -l /var/log/ | sort
(中略)
-rw-rw-r-- 1 root utmp 292292 5月 15 14:20 lastlog
drwx------ 2 root root 4096 2月 19 16:27 private
(中略)
drwxr-xr-x 2 root root 4096 5月 15 14:08 apt
total 91684
kmf@pop-os:~$
安心の「記号→アルファベット順」である。
現象が安定しないということは、もしや原因は1つではない? などと思ったが、これが緒になった。
実はこのUbuntuマシンでは、日本語manページのインストール確認作業も同時にやっており、Macから入った端末は、その一環で LANG=ja_JP.UTF-8 した環境だったのだ。
そしてこの、日本語環境にすると再現しない、というところがポイントで、Ubuntu と Mac の違いはターミナルのロケール設定にあった。
Pop!_OS で英語設定+日本語単位系とすると、ロケール設定はこのようになる:
kmf@pop-os:~$ locale
LANG=en_US.UTF-8
LANGUAGE=
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC=ja_JP.UTF-8
LC_TIME=ja_JP.UTF-8
LC_COLLATE="en_US.UTF-8"
LC_MONETARY=ja_JP.UTF-8
LC_MESSAGES="en_US.UTF-8"
LC_PAPER=ja_JP.UTF-8
LC_NAME=ja_JP.UTF-8
LC_ADDRESS=ja_JP.UTF-8
LC_TELEPHONE=ja_JP.UTF-8
LC_MEASUREMENT=ja_JP.UTF-8
LC_IDENTIFICATION=ja_JP.UTF-8
LC_ALL=
kmf@pop-os:~$
Macからsshで入ってexport LANG=ja_JP.UTF-8 した環境ではこうなる:
kmf@pop-os:~$ locale
LANG=ja_JP.UTF-8
LANGUAGE=
LC_CTYPE="ja_JP.UTF-8"
LC_NUMERIC=ja_JP.UTF-8
LC_TIME=ja_JP.UTF-8
LC_COLLATE="ja_JP.UTF-8"
LC_MONETARY=ja_JP.UTF-8
LC_MESSAGES="ja_JP.UTF-8"
LC_PAPER=ja_JP.UTF-8
LC_NAME=ja_JP.UTF-8
LC_ADDRESS=ja_JP.UTF-8
LC_TELEPHONE=ja_JP.UTF-8
LC_MEASUREMENT=ja_JP.UTF-8
LC_IDENTIFICATION=ja_JP.UTF-8
LC_ALL=
kmf@pop-os:~$
そしてsortコマンドのソート順は、強調表示した LC_COLLATE に規定されるのだ。
結論
ながながした調査の経緯は省くが(一緒に調査してくださったずけらんさん、ありがとうございました)、「壊れてるのはUnicodeコンソーシアム」が結論だ。
上記の現象は:
- UTF-8の英語でのソート順はスペース記号とハイフン(-)を無視する
- これはUnicodeコンソーシアムの決定であり、バグではない
ためであった。
このソート順だと、「-r」で始まる列は「r」としてソートされる。なるほどそれならd→r→tでアルファベット順になっているでないか。
まとめとしては:
- ソート順は環境変数 LC_COLLATE で制御される
- 西欧言語のUTF-8設定では空白とハイフンは無視される。英語ではイギリス英語(en_GB.UTF-8)はおろかシンガポール英語(en_SG.UTF-8)でも漏れなく起きる。
- ハイフンを記号として扱いたければ LC_COLLATE=C にすれば「伝統的」なソート順(ハイフンを含む記号→アルファベット)になる
- LC_COLLATE=ja_JP.UTF-8 とした場合も、LC_COLLATE=C と同じくハイフンが記号扱いになる
である。
実用的にはLC_COLLATE=C や LC_COLLATE=ja_JP.UTF-8 を使うのが驚き最小だと思うけど、用途によっては空白やハイフンが無視されることを選ぶべきかもしれない。
余談
この現象、日本語や英語を扱っている場合は選択の余地があるけど、フランス語やドイツ語を扱う大変だと思う。
なぜなら、フランス語やドイツ語では単語にアクサンやセディーユ、ウムラウトの付いた文字が入ってくるから。
これらの文字は、LC_COLLATE=C では、素のアルファベットの後ろに回されてしまう。LC_COLLATE=Unicode なら正しく「素の文字→記号付き文字」の順でソートされるのだが、こんどはハイフンが無視されてしまう。
kmf@pop-os:~$ echo 'a á b c f -c d é e' | tr ' ' '\n' | LANG=fr_CA.UTF-8 sort
a
á
b
-c
c
d
e
é
f
kmf@pop-os:~$ echo 'a á b c f -c d é e' | tr ' ' '\n' | LANG=C sort
-c
a
b
c
d
e
f
á
é
kmf@pop-os:~$
オレなら泣く。
とはいえ困るのはハイフンの扱いを選択できないことだけなので、これはsortコマンドの方で(より根本的にはロケール情報を処理するlibcに)選択オプションを設けるべきであろう。
参考
わかってる人が怒りながら返事してて示唆に富んでた。
LC_COLLATEの違いによる記号と文字、.と_の優先順位の問題。もっと良いページあると思うけど、とりあえず。
totalを消して解決するなら「ls -ld *」でいいじゃんという誘惑