created: 2024-09-05T03:41:28.224Z

macOS の sort コマンドはマルチバイト文字をきちんと扱えない

人生ではじめて日本語の stackoverflow で質問したものの顛末。

ソートがおかしい

macOS で ↓ の 3 行を LC_COLLATE=ja_JP.UTF-8 でソートすると予期せぬ結果になる。

入力

$ LC_ALL=ja_JP.UTF-8 sort --ignore-nonprinting <<__TXT__
なにわ男子の逆転男子
アニメ おじゃる丸
アニメ「烏は主を選ばない」
__TXT__

ソート結果

アニメ おじゃる丸
なにわ男子の逆転男子
アニメ「烏は主を選ばない」

アニメ というカタカナの prefix をもっている 2 行の間に、なにわというひらがなの行が入っている。

ソートなので先頭の部分が同じもので並べられて欲しい。

実行環境

$ sort --version
2.3-Apple (165.100.8)
$ locale | grep -E '(COLLATE|ALL)'
LC_COLLATE="ja_JP.UTF-8"
LC_ALL=

もちろん LC_COLLATE=C とするとこのソート順は再現しない。

$ LC_ALL=C sort --ignore-nonprinting <<__TXT__
なにわ男子の逆転男子
アニメ おじゃる丸
アニメ「烏は主を選ばない」
__TXT__
なにわ男子の逆転男子
アニメ「烏は主を選ばない」
アニメ おじゃる丸

原因

この不可解な動作は sort コマンドのバグではなくきちんと原因がある。

macOS のデフォルトの比較ルールの設定は LC_COLLATE=ja_JP.UTF-8 となっている。 しかし、なんと ja_JP.UTF-8 という指定は la_LN.US-ASCII のエイリアスになっているらしい。

$ ls -l /usr/share/locale/ja_JP.UTF-8/LC_COLLATE
lrwxr-xr-x  1 root  wheel  28  5  7 16:01 /usr/share/locale/ja_JP.UTF-8/LC_COLLATE -> ../la_LN.US-ASCII/LC_COLLATE

la_LN.US-ASCII の比較ルールはこのようになっている。

  • マルチバイト文字はすべて比較時に等価な扱いになっている
  • ascii をマルチバイト文字と比較すると、ascii のほうが先でマルチバイト文字が後になる

参考

この種明かしはこれらのコメントとブログのおかげでたどり着くことができた。

変なソートがどう発生しているか

たとえば、あざとくて何が悪いのふるカフェ系 ハルさんの休日 を比較するとき、内部的にはこのようになっているという例をあげる。

LC_ALL=ja_JP.UTF-8 sort --ignore-nonprinting <<__TXT__
あざとくて何が悪いの
ふるカフェ系!ハルさんの休日
__TXT__

期待としては のほうが先に来るはずだが、 のほうが先としてソートされる。

ふるカフェ系!ハルさんの休日
あざとくて何が悪いの

ステップごとに追うと

5 文字目までは両方ともマルチバイト文字。 マルチバイト文字は等価 というルールなので、ここまでだとどちらも同じ順位。

ふるカフェ系
あざとくて何

6 文字目は ! の比較となる。

ふるカフェ系!
あざとくて何が

ascii 文字のほうが先でマルチバイト文字 というルールが適用される。 ! は ascii で はマルチバイト文字なので、ここで勝負がつき ふるカフェ系! のほうが先だとされる。

ふるカフェ系!ハルさんの休日
あざとくて何が悪いの

これどうする

マルチバイト文字はすべて比較時に等価な扱いになっている というルールはかなり不便なのでなんとかしたい。

.bashrc で export LC_COLLATE=C することも考えたが、予期せぬところでハマったらイヤなので sort コマンドを使う時だけ照合ルールを変更したい。

alias を設定して、ユニコードの順番でソートされるようにした。

alias sort="LC_COLLATE=C sort"
怒られの作法 ――日本一トラブルに巻き込まれる編集者の人間関係術 (単行本)
[ad] 怒られの作法 ――日本一トラブルに巻き込まれる編集者の人間関係術 (単行本)
草下 シンヤ (単行本(ソフトカバー))