Ubuntu (Pop!_OS) で ls -l の出力を sort に食わせたらアルファベットの間に記号が来た。壊れてるのは誰だ。

学生に 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 した環境だったのだ。

そしてこの、日本語環境にすると再現しない、というところがポイントで、UbuntuMac の違いはターミナルのロケール設定にあった。

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 *」でいいじゃんという誘惑