🕌

MSYS2のPostgreSQLはなぜ文字化けするのか

に公開

msys2環境のpostgresを使っていて、文字化けした話から、Windowsのシステムロケール問題を話します。

今回はポエムなので、物語調にしてみます。

1. MSYS2にPostgreSQLを入れる

MSYS2cygwinに近いUnixライクな操作感をWindowsで実現するための環境です。最近のWindowsはWSL2などもあり影が薄いのですが、VMなども関与せず正真正銘のWindowsアプリを作れるOpenSourceなC/C++環境でもあるので、たまに使います。

インストールは本家のトップページのリンクから直にインストーラーをダウンロードして実行するだけです。私はデフォルト設定で入れてます。インストールできたら、Windowsキー押して「MSYS2」とか打てば候補が並ぶので、MSYS2 UCRT64を選べばOKです。

起動するのはWindows10のコマンドプロンプトみたいな画面です。

1-0. minttyの設定

フォントサイズと言語とエンコーディングの設定だけはしておきます。

  1. ウィンドウの左上からメニューを出し、options>Window>UI Languageをjaに
  2. 一旦apply
  3. 再びウィンドウの左上からメニューを出し、
  4. オプション>テキスト>ロケールをja_JPに
  5. オプション>テキスト>文字セットをUTF-8に
  6. オプション>テキスト>フォントを好みのサイズに
  7. 適用して保存

1-1. パッケージupgrade

$ pacman -Syu

1-2. PostgreSQLパッケージインストール

$ pacman -S mingw-w64-ucrt-x86_64-postgresql

1-3. データディレクトリの初期化

$ mkdir postgres
$ cd postgres
$ initdb ./data

※この方法ではロケールがOSデフォルトのSJISになります。普通は--locale=ja_JP.UTF8とか付けてUTF8にします。

1-4. PostgreSQLサーバーの起動

$ pg_ctl -D ./data -l pg.log start

-lオプションはログファイルの指定なので要りませんが、startをstopやstatusにすると停止や状態確認なども出来ます。

2. PostgreSQLに繋ぐ

PostgreSQLをインストールしてサーバーを起動するところまで出来たので、同じパッケージでインストールされたPostgreSQLクライアントのpsqlから繋いでみます。

2-1. MSYS2上のpsqlから繋ぐ

$ psql postgres
psql (18.0)
"help"でヘルプを表示します。

postgres=# select '日本語';
 ?column?
----------
 □□□{□□
(1 □s)

postgres=# \q

$ 

何か2行ほど文字化けしてますね。

2-2. コマンドプロンプト上のpsqlから繋ぐ

Windowsキー押して、cmdと打ってそのまま[Enter]するとコマンドプロンプトが開きます。そしてそこから同じpsqlを起動すると…

C:\Users\user>c:\msys64\ucrt64\bin\psql postgres
psql (18.0)
"help"でヘルプを表示します。

postgres=# select '日本語';
 ?column?
----------
 日本語
(1 行)


postgres=# \q

C:\Users\user>

これが文字化けのない正しい動作になります。

3. なぜMSYS2環境では文字化けしたのか?

端的に言えばselect '日本語'というSQLの実行結果を表示するため、psqlが内部的に呼び出しているページャーがUTF-8を期待しているのに、シフトJISを食わせているためです。

3-1. ページャーとは?

moreコマンドやlessコマンドが有名ですが、1画面に収まらない出力をページ単位で少しずつ見れるようにする機能を持つコマンドのことです。

3-2. 今回はどこで使われていたのか?

SQLの結果は場合によってはとても長くなるので、PostgreSQLはSQLの結果を出力する際に、デフォルトでページャーをサブプロセスとして起動してパイプで繋いで出力しています。

3-3. 今回は何がページャーに使われていたのか?

MSYS2環境ではデフォルトで、/usr/bin/moreが使われています(実際にはPATH上で最初に見つかるmoreコマンド)。

3-4. /usr/bin/more はなぜUTF-8を期待しているのか?

MSYS2環境の/usr/bin/moreは実質cygwinと似ていて、MS謹製のCライブラリを使わず、Unixライクな環境に近い動作をするからです。

3-5. psqlはなぜページャーにシフトJISで渡していたのか?

日本語Windowsのシステムロケールは一般にシフトJISなので、伝統的なパイプの標準はシフトJISだからです。

3-6. cmdやpowershellで文字化けしなかったのはなぜか?

通常PATHから見つかるmoreコマンドがWindowsに付属する C:\windows\system32\more.com だからです。このmoreコマンドは入力としてシフトJISを期待します。

3-7. MSYS2環境でpsqlにWindows付属のmoreコマンドを使わせるにはどうしたらいいのか?

環境変数PSQL_PAGERにmore.comを指定すればMSYS2のmoreコマンドは呼ばれません(.exeなので)。

$ PSQL_PAGER=more.com psql postgres
psql (18.0)
"help"でヘルプを表示します。

postgres=# select '日本語';
 ?column?
----------
 日本語
(1 行)


postgres=#

これでシフトJISで正しく入力/表示できるようになりました。

4. シフトJISとUTF-8だとどちらが良いのか?

UTF-8です。なのでシフトJISで統一している今回のデフォルト設定は一般にはあまりよくありません。

4-1. シフトJISのダメな点

ほぼ日本語の文字しかないし、絵文字など新しい文字が使えません。また国別に違うエンコーディングを使うより、どの国のどの言語でもUTF-8で統一した方がはるかに良いです。統一してないと常に文字化けのリスクが付きまといます。

以下はselect '日本語😀と入力しようとして入力できなかった例です。

$ PSQL_PAGER=more.com psql postgres
psql (18.0)
"help"でヘルプを表示します。

postgres=# select '日本語??

4-2. 他にもダメなエンコーディングはあるのか?

現在日本語環境ではUTF-8以外のエンコーディングは全て悪だと思います。他の言語では知りません。

4-3. WindowsはなぜUTF-8をシステムロケールのデフォルトにしないのか?

今もシフトJISを前提とした過去の資産は大量にあるので、変更するととても広範な影響があるからだと思います。何年も前からずっとUTF-8にする機能はありますが、ずっとベータと言ってます。

5. WindowsのシステムロケールをシフトJISにしたままpsqlをUTF-8で動かしたいです

頑張ればUTF-8で動かすことはできます。でもあまりinteractiveに動かすことはできません(私は方法を知らない)。以下は直接日本語入力をしない前提のUTF-8対応例です。

5-1. client_encodingをutf8にする

方法が3つあります。

  1. psql内からset cllient_encoding=utf8;
  2. psql内から\encoding utf8;
  3. psql起動時の環境変数PGCLIENTENCODINGをUTF8にする

3番目の方法を使って同様に実行するとこうなります。

$ PGCLIENTENCODING=UTF8 psql postgres
psql (18.0)
"help"でヘルプを表示します。

postgres=# select '日本語';
ERROR:  隨ヲ蜿キ蛹匁婿蠑・UTF8"縺ォ蟇セ縺吶k荳肴ュ」縺ェ繝舌う繝亥・縺ァ縺・ 0x93
postgres=#

これはUTF-8で動かした結果。入力した日本語がシフトJISだったため、エラーになっています。このエラーはシステムロケールがシフトJISである以上避けては通れません。
しかもエラーメッセージも文字化けしています。

5-2. なぜエラーメッセージは文字化けしたのか?

クライアントのエンコーディングをUTF-8に設定したため、エラーメッセージもUTF-8文字列でCライブラリ(UCRT)に渡されたのですが、出力エンコーディングであるシフトJISである端末にそのまま渡されたからです。

5-3. なぜCライブラリは文字列をそのまま端末に渡したのか?

プログラムのロケール設定がシステムロケールであるシフトJISだったからです。Cライブラリ(UCRT)はロケール設定のエンコーディングから端末の出力エンコーディング(これをAPIで機械的に取得できるのはWindows固有の特徴です)に自動変換して渡すのですが、同じシフトJISなので変換の必要がないと判断されたのです。

つまり、psqlのクライアントのエンコーディング設定は、Windowsだとシステムロケールを指定しない限り、正しく端末出力できないのです。

5-4. 端末出力でなく、リダイレクトやパイプならどうか?

リダイレクトやパイプには出力エンコーディングの設定がないので、Cライブラリも自動で変換したりしません。受取先で扱えるエンコーディングであればOKです。

$ PGCLIENTENCODING=UTF8 psql postgres 2>&1 | cat
select '日本語';
ERROR:  符号化方式"UTF8"に対する不正なバイト列です: 0x93
\q

$

上記はパイプにしたことで、そのままcatに流すだけでも正しく出力されていることが分かります。psqlがUTF-8でエラー出力したストリームを、catは(MSYS2付属のコマンドなので)、LANG/LC_CTYPE設定に基づき標準入力をUTF-8として処理するので、正しく動きます。psqlは端末が話し相手でない場合、(バッチ処理させやすいため)あまり色々メッセージやプロンプトを出しません。

5-5. 入力も端末以外にすればUTF-8を渡せるのでは?

そうですね。以下の方法があります。

  1. パイプやリダイレクトで渡す
  2. psql内でSQLスクリプトとしてUTF-8で書かれた外部ファイルを実行する

SQLスクリプトとして実行させてみます。

$ cat hoge.sql
select '日本語😄';

$ PGCLIENTENCODING=UTF8 psql postgres
psql (18.0)
"help"でヘルプを表示します。

postgres=# \i hoge.sql
 ?column?
----------
 日本語�
(1 □s)

postgres=#

ただエラーは出ずに実行されたようですが、2箇所で文字化けしています。

5-6. 絵文字が文字化けしたのはなぜ?

winptyだと絵文字含むサロゲートペアの文字をキャラクタデバイスとしてうまく扱えないからではないかと推測しています。
/ucrt64/bin/psqlを見れば分かるとおり、psqlの実体はシェルスクリプトになっていて、端末実行時はwinptyを挟んでpsql.exeが実行されています。

psqlではなく、psql.exeで直接動かせば端末動作が不安定になりますが、絵文字は正しく表示されます。

$ PGCLIENTENCODING=UTF8 psql.exe postgres
psql (18.0)
"help"でヘルプを表示します。

postgres=# \i hoge.sql
 ?column?
----------
 日本語😄
(1 □s)

postgres=#

5-7. 行が文字化けしたのはなぜ?

UCRTでsetlocale(LC_ALL, "")を呼び出し、ロケールをJapanese_Japan.932(シフトJIS)に設定したため、libintl(GETTEXT)から"(1行)"がシフトJISで取得されたからです。
そのため、ページャー経由で出力されるこの文字列はシフトJIS文字列に対して、さらにUTF-8→シフトJISの変換がかかってしまい、文字化けしていました。

libintlでsetlocale(LC_ALL, "")した場合は、ロケールは環境変数LANGなどの設定に従いja_JP.UTF-8に設定され、libintl(GETTEXT)から"(1行)"がUTF-8で取得されます。
psqlはWindowsの作法に則り、システムロケールのエンコーディングでlibintlを動作させるよう実装しているようです。
なお、libintlのsetlocaleの呼び出しがなくとも、環境変数LANGなどは参照され、エンコーディングではなく言語までであれば変更が可能なようです。

5-8. 行の文字化けを回避できる?

日本語のままだと不可能です。例えば、「"help"でヘルプを表示します。」などの文字列もlibintlを使って取得していて、こちらは直接端末に出力するためシフトJISが必要だからです。ページャー経由とそうでない場合でUTF-8/シフトJISを切り替える方法がないのです。

次善の策として、LANGなど環境変数で言語だけ英語にするという手がありますが、この方法では全てのメッセージ(サーバー含まず)が英語になってしまいます。

$ LANG=C.UTF-8 PGCLIENTENCODING=UTF8 psql.exe postgres
psql (18.0)
Type "help" for help.

postgres=# \i hoge.sql
 ?column?
----------
 日本語😄
(1 row)

postgres=#

6. 文字化けが起こる原因となるポイント

  • 端末の場合は、ロケールの設定とCライブラリ(UCRT)に渡す/から渡される入出力文字列のエンコーディングの想定が不一致の場合
  • パイプ・リダイレクトの場合は、受け取る相手とエンコーディングの想定が不一致の場合

なおUNIX系のOSでは端末からエンコーディングを取得することが出来ないので、この条件以外にも文字化けが起こる場合があります。またWindowsでもCライブラリを経由せずAPIを直に呼び出すなどした場合は文字化けを意図的に起こすことができます。

7. 悪のシフトJISを撲滅するにはどうしたらいいか?

MSがシステムロケールをUTF-8にするのを待つしかありません。

8. MSがシステムロケールをUTF-8にするまでどう実装するべきか?

大きく2つの道があります。

  1. ワイド文字を中心に処理を実装する
  2. マルチバイト文字列を使って実装し、ロケール設定をUTF-8にする

1.で実装すると、システムロケールが何であっても正しく動作させられますが、UNIXライクなOSではワイド文字があまり一般的ではありません(ただし動きます)。
2.で実装すると、単独で動かす分にはシステムロケールに依らず正しく動作させられますが、1.で作られたアプリとパイプなどで繋ぐと必ず化けます。UNIXライクなOSと似た作りで実装できます。

完全なのは1.ですが、MSYS2環境(cygwin環境)は2.なのでどちらを優先するかです。

GitHubで編集を提案

Discussion