はじめに
tr
コマンドで日本語を置換すると文字化けする場合があります。文字化けしない場合もあります。
$ echo あいう | tr あ 漢
漢漄漆
$ echo あいう | tr あ 漢
漢いう
さてその理由は何でしょうか?
記事のタイトルでネタバレてしていますね。そうです。tr
コマンドが日本語を置換すると文字化けするのは GNU 版の tr
コマンドが日本語(正確には UTF-8 を含むマルチバイト文字)に対応していないからです。まだ実装されていないというのが正しいのかもしれませんが、GNU Coreutils 9.1 の man ドキュメントの BUGS に次のように書いてあるため、バグと言っても差し支えないでしょう。
BUGS
Full support is available only for safe single-byte locales, in which every possible input byte represents a single character. The C locale is safe in GNU systems, so you can avoid this issue in the shell by running LC_ALL=C tr instead of plain tr.
tr
コマンドは UTF-8 文字列を 1バイトずつ扱わなければならないという仕様があるわけではなく、文字単位で扱うべきであり、実際に環境によっては文字化けしません。文字化けしない理由は、その tr
コマンドの実装がマルチバイト文字に対応しているからです。
macOS・FreeBSD・Solarisでは文字化けしない
これらの OS の tr
コマンドは日本語に対応しているため文字化けしません。POSIX ではデフォルトでは tr
コマンドはバイトではなく文字として扱うことになっています。したがって POSIX に準拠しているのであれば、文字化けしてはならないということです。
GNU tr の他に文字化けを起こす環境は NetBSD(少なくとも NetBSD 10)と OpenBSD(少なくとも OpenBSD 7.4)です。
UTF-8非対応のtr
はUTF-8文字列を壊す
tr
コマンドで UTF-8 文字列を扱う場合、UTF-8 対応の tr
コマンドが必要です。GNU tr のようにマルチバイト文字非対応の tr
コマンドは文字をバイト単位で処理します。つまり UTF-8 の一文字を一文字として認識しておらずバイト単位で扱っているからです。UTF-8 では文字列は 1 バイトから 4 バイトの可変長です。
もしかしたら UTF-8 非対応の GNU tr で置換しても問題なく置換できるじゃないかと思った人がいるかも知れません。それはおそらく置換対象の文字が 1 バイトの ASCII 文字だからでしょう。
$ echo あaいiうu | tr a A
あAいiうu
1 バイトの ASCII 文字が壊れない理由は、UTF-8 が ASCII 文字と最低限の互換性があるように設計されているからです。バイト列で考えればその理由ははっきりとわかります。
$ printf あaいiうu | od -tx1
0000000 e3 81 82 61 e3 81 84 69 e3 81 86 75 0a
0000014
読みやすいように出力を加工
[あ: e3 81 82] [a: 61] [い: e3 81 84] [i: 69] [う: e3 81 86] [u: 75]
$ printf aA | od -tx1
0000000 61 41
0000002
[a: 61] [A: 41]
ASCII 文字の範囲(文字コードで言えば \x00
から 0x7F
)であれば、ASCII と UTF-8 はバイト列は全く同じです。そして UTF-8 のバイト列は ASCII 文字のバイト列とかぶらないように設計されています。つまり \x80
から \xFF
までしか使いません。したがって ASCII 文字の範囲の文字の置換であれば UTF-8 文字列を壊しません。しかしASCII 文字の範囲外の文字の場合は 1 文字をバイト単位で行うのでマルチバイト文字に対応してない tr
コマンドでは、UTF-8 文字列を壊してしまうのです。
$ printf あ | od -tx1
0000000 e3 81 82
0000003
$ printf 漢 | od -tx1
0000000 e6 bc a2
0000003
$ echo あいう | tr あ 漢 # tr \xe3\x81\x82 \xe6\xbc\xa2
👆 \xe3 → \xe6、\x81 → \xbc、\x82 → \xa2 に置き換えるという意味
❌ \xe3\x81\x82 → \xe6\xbc\xa2 では無い
ちなみに、ひらがなやカタカナでコードポイントとして近い位置にある文字の場合も UTF-8 文字列を壊しませんが、これはたまたまそういうバイト列だったというだけです。次の例を見ると分かるように、頭の2バイト (e3
81
) がすべて同じなので置換しても変わらないだけです。
$ echo あいう | tr あ か
かいう
$ printf あいう | od -tx1
0000000 e3 81 82 e3 81 84 e3 81 86
0000011
[あ: e3 81 82] [い: e3 81 84] [う: e3 81 86]
printf あか | od -tx1
0000000 e3 81 82 e3 81 8b
0000006
[あ: e3 81 82] [か: e3 81 8b]
まとめ
日本語を含むマルチバイト文字を tr
コマンドで置換すると文字化けする理由は GNU 版 の tr
コマンドのバグ(未実装)です。(POSIX の仕様では)日本語も正しく置換できるのが正しい動作です。
OS 標準の Unix/Linux コマンドは長く使われているから、いまさらバグなんて無いと思いましたか? そんなことはありません。未だにバグは見つかりますし環境ごとに動作が完全に同じなんてことはありません。特に Unicode (UTF-8) 関連は他にも問題があります。Unix/Linux コマンドでの日本語を含むテキスト処理をどの環境でも同じように正しく扱うのは大変です。難しい話ですね。