WindowsコマンドプロンプトにUnicode表示
久々にC++でWindowsのプログラムを書いた。指定したフォルダーに含まれる画像ファイルを、サブフォルダーも含め再帰的に探索して片っ端から表示するGUIアプリケーションである。Visual C++ 2008 Express(以下VCPと略)で「Unicode文字セットを使用する」にしていたが、利用したライブラリーの都合でファイル名をマルチバイト文字、すなわちShift JISに変換して渡すようにしているところで見事に躓いた。
Logo_de_la_République_française.svg[1].png
フランス共和国政府の公式ロゴ画像である。Shift JISは日本語と英語の他、ギリシャ文字とキリル文字をサポートしているが「é」と「ç」は無い。この件は、予めファイルをメモリーに読み込んだバッファーを渡すように変更して落着した。
GUIアプリケーションだったからこれで済んだが、もしコンソールアプリケーションで、コマンドプロンプトに英語、日本語、そしてフランス語のファイル名を正しく表示する仕様だったら、それは可能なのだろうか。
今回はCプログラミング入門の定番である「Hello world」を多言語に拡張したバージョンを使って、コマンドプロンプトとコンソールアプリケーションの文字エンコーディングについて調べてみた。なお記事の内容は調査で判明した事実関係を述べているものであって、取り上げている各ソフトウエア製品の仕様と異なる場合があるかもしれない。また調査に使った環境はWindows Vistaである。試していないがWindows 7および8では同じ結果になると予想している。Windows XPおよびそれ以前のNT系Windowsでも概ね同じになると思うが、3.1~95/98~Me系はWindows自体が一部を除きUnicode対応していないので、全く事情が異なるはずだ。
なお今回の記事の画像は、クリックすれば拡大表示されるようになっている。
2013/01/28追記: 現在設定されているコードページの取得方法が判明した。それに合わせて本文に追記・編集している。
/******* *******/
キーボードから入力できない文字を入力する方法
万が一追試をしてくださる方がいらっしゃった場合に備え、メモを残しておく。
手許にあるファイル名であれば、エクスプローラーでファイル名を変更する手順の途中でコピーすることが可能だ。しかし適当なファイルが手許に無かったり、任意の文字を入力したかったりする場合にはこれは使えない。標準設定の日本語Windowsでは「é」や「ç」をキーボードから入力できないが、標準装備されているIMEパッドを使うと入力可能である。
マウスオーバーで文字コードを表示する機能があり、文字コード表としても使えて便利である。ただしJIS2004で追加された追加漢字面に属する「
(U+20B9F)」などの文字を入力すると、一部のアプリケーションでは最悪クラッシュする場合があるので、無くなっては困るドキュメントは予め保存してから試すようにしてほしい。なおXPのIMEパッドは基本多言語面だけのサポートなので、これが起こるのはVista以降だ。
残念なことにコマンドプロンプトではIMEパッドが使えない。私は入力する文字を一旦メモ帳(notepad.exe)に書き込んで、コピー・ペーストしている。
3ヶ国語+αでこんにちは
「Hello world」を英語、日本語、そしてフランス語に拡張し、更にコマンドラインの引数と標準入力に与えた任煮の文字を表示するようにした。なおコードを簡潔にするため、一般にエラーチェックが必要な場合であっても省略している。これはこの記事に登場する全てのソースコードに共通である。
どの文字変数型を使っているか一目で分るよう「_t」系のマクロは使っていない。こうすればVCPのプロジェクトのプロパティで「文字セット」に何を設定していても関係ないはずだが、念のため「設定なし」にしている。
実はこのプログラム、ビルドする前に警告を受けてしまう。
ディフォルトのShift JISで保存すると「é」と「ç」が文字化けしてしまう、と言う警告である。ここは素直に「はい」を選ぶと、BOM(Byte Order Mark)付UTF-16のソースファイルができる。先に進むとビルドは正常終了したがコンパイルで警告が出ている。
1>h:\helo_uni\mbcs\mbcs.c(11) : warning C4566: ユニバーサル文字名'\u00E9' によって表示されている文字は、現在のコードページ(932) で表示できません
警告の吟味は後回しにして、プログラムを実行してみよう。
日本語コードページ932のエンコーディングはShift JISなので、プログラムが出力しようとしているフランス語は正しく表示できていない。ただしコマンドラインへの入力やechoのような実際の処理はcmd.exeが行っている内部コマンドの出力はUnicodeなので、設定されているコードページとは無関係に、フォントがサポートしている文字は正しく表示される。
次にフランス語をサポートしているはずのコードページ1252で試して見る。
日本語が文字化けするのは当然だが、化け方が2種類ある。文字リテラルと引数で与えられたものは同じように意味不明な化け方をしていて、文字数が2倍になっている。赤い下線を引いたのは「国」に該当する部分だ。それに対して標準入力から与えた日本語は、元の文字数と同じ数の「?」に置き換えられている。
フランス語が正しく表示できているのは、標準入力から与えた文字列だけである。文字リテラルと引数で与えたフランス語はそれぞれ違った化け方をしているが、それぞれを日本語コードページの場合と比べると同じであることが分る。
以上の現象はいろいろな事情が絡み合った結果である。文字列の与え方毎に理由を探ってみよう。
文字リテラルの場合
これはコンパイルの際に出た警告に関連する部分である。
コンパイラーは命令だけでなくデーターもコンパイルする。「"..."」形式の文字リテラルはchar型文字の配列にする必要があるためマルチバイト文字に変換され、最終的にプログラムに埋め込まれる。日本語Windows上でコンパイルすると、標準でShift JISに変換しようとする。しかし「é」と「ç」に該当するShift JISコードが無いので、代わりに「?」に置き換えられているのだ。このことは出来上がったプログラムをメモ帳で開いてみると分る。
ハイライトしているのがプログラムに埋め込まれた問題の文字リテラルで、「"R?publique fran?aise: Bonjour monde. "」になっている。
「日本語Windows上でコンパイルすると標準でShift JIS」と書いたが、正確にはWindowsの設定の一つである「システムロケール」に規定されたエンコーディングに従っている。これはコントロールパネルの「時計、言語、および地域」→「地域と言語のオプション」の「管理」タブで確認・変更可能である。
ただし日本語以外のロケールに設定すると今度は日本語が当該ロケールのエンコーディングに変換できなくなるし、メモ帳などでShift JISエンコーディングのテキストファイルが正常に開けなくなる。
printf関数はプログラム内部のエンコーディングのまま文字列を出力するので、実行するコマンドプロンプトのコードページをビルドしたコンピューターのシステムロケールが規定しているのに合わせないと、文字リテラルを正しく表示できない。
標準入力の場合
cmd.exeは出力をパイプやリダイレクトに渡す場合、文字のエンコーディングを現在設定されているコードページに従って変換しようとする。この時、前節のコンパイラーが直面したのと同じ「é」と「ç」をShift JISに変換できない問題が発生している。コンパイラーはShift JISにできない文字を「?」に置き換えたが、cmd.exeは似た文字に置き換えている。後者が私の使っているWindows Vistaの標準らしく、IMEパッドで表示される「©」と「ç」のShift JISコードは「c」と同じで、「èéêë」はどれも「e」と同じになっている。私としては「?」に置き換えるほうが好ましいと思うのだが・・・。
前節の場合と違うのは、変換する先のエンコーディングを決めているのが実行時に設定されているコードページだと言う事。フランス語をサポートしているコードページに設定されていれば、標準入力から与えられたフランス語は全て正しく表示される。文字リテラルはビルド環境で成否が決まってしまうのと大きな違いだ。
引数の場合
標準入力の場合とほぼ同じだが、変換する先のエンコーディングがシステムロケールで規定されることが異なる。なぜコードページにしなかったのかと言う意味で、これが正しい仕様なのか疑問である。ただしシステムロケールとコマンドプロンプトのディフォルト・コードページは合わせているはずなので、結果的にWindowsの言語バージョン依存であると言うことになるのかもしれない。
素朴なUnicode対応だと日本語が表示できない場合がある
Unicode対応していないプログラムでは、メモリー上に保持したデーターに日本語とフランス語の文字が混在できないことが分った。Unicode対応すれば複数言語の文字が混在できる。しかし標準出力は依然としてUnicodeを受け付けないので、プログラムから出力するときにエンコードする必要が出てくる。この際どういったエンコーディングを行うかプログラムに知らせるため、文字ロケールを設定する必要が出てくる。
以下の例はプログラムで明示的に文字ロケールを設定しない場合にどうなるか調べるために作った。
_wsetlocale関数の第二引数にNULLを指定すると現在設定されているロケールは変更されず、その名称を表す文字列が得られる。
「L"..."」形式の文字リテラルはUnicode(実質的にUTF-16LE)でwchar_t型文字の配列としてプログラムに埋め込まれる。ソースファイルのエンコーディングがUnicodeではない場合は、コンパイラーがUnicodeに変換する。当該文字のコードがUnicodeに存在しないと言う事態は、原理的に発生しない。
プログラムに埋め込まれた文字リテラルを確認するには、メモ帳でプログラムをUnicodeファイルとして開く必要がある。
文字リテラルの開始位置は32ビット境界、または偶数バイト境界に合わせられるようで、1バイトずれることによる文字化けは経験したことがない。
全ての文字リテラルが正しく格納されていることが分る。
プログラムを実行すると、明示的に設定しない場合「"C"」ロケールがディフォルトとして使われることが分る。
本来「"C"」ロケールは7ビットASCIIだったはずだが、以上の結果を見ると8ビットに拡張されているようだ。「"C"」ロケールの下でwprintf関数は以下の様にエンコーディングを行っていると考えられる。
- コードポイントがU+00FF以下なら、下位8ビットをそのまま出力する
- コードポイントがU+00FFを超えていると、不正な文字として「?」に置き換える
- ただし、フォーマット文字列が不正な文字を含む場合、全く何も出力しない
最後の項目は、wprintf(L"日本国:こんにちは世界\n"); は何も出力しないが、wprintf(L"%s\n", L"日本国:こんにちは世界"); なら「???????????[改行]」が出力される、と言う意味である。
文字ロケールは入力にも影響していて、「"C"」ロケールの下でfgetws関数は、cmd.exeがShift JISに変換してパイプに流した各バイトが常にコードポイントU+00FF以下の1文字に該当するとして扱っていると考えられる。そのため日本語が正しく表示された。これは結果オーライだ。その一方、引数はcmd.exeからUnicodeで渡されているようだ。これは起動されるプログラムに引数をwchar_t型文字で渡す必要があることを感知できるかららしい。
Unicode対応で日本語を表示させる標準的処方箋
第二引数に空文字を指定して_wsetlocale関数を呼び出すと、現在システムに設定されているロケールが自動的に設定されると言われている。
こうすれば日本語のロケールが設定され日本語表示がOKになるが、当然ながらフランス語は正しく表示できない。
コードページを切り替えれば反映されるかと期待したが、効果は無かった。
コントロールパネルでシステムロケールを変更しても、頑として"Japanese_Japan.932"のままである。Microsoftの資料によれば
ロケールを既定値に設定します。これはシステムの既定の ANSI コード ページで、オペレーティング システムから取得します。
と、コントロールパネルで設定しているのを参照しそうな書き方だが、まさかビルド時に取得ってオチ?それでは無意味だよね。_wsetlocale関数を呼び出す時の第一引数に依って挙動が違う?それは勘弁して欲しい。
正確な事実関係は不明である。
現在設定されているコードページに合わせる方法(2013/01/28追記)
引数無しでchcpコマンドを実行すれば表示できるのだから、現在設定されているコードページをプログラム的に取得する方法があるはずである。「chcp」をキーワードに含めて検索したら「[Undocumented] CHCP」と言うサイトが見つかった。
GetConsoleCP関数を使えばchcpで設定された現在のコードページが取得できる。コンソール入出力それぞれが別々にコードページを持っていて、chcpは常に同じ値を両方に設定するが、異なる値を設定することも可能だ。それを考えると、今回の目的にはGetConsoleOutputCP関数で取得した値を使った方が安全である。
しかし取得できるのはコードページに対応した数値である。数値でプログラムのコードページを設定できればよいのだが、そういった方法は見つかっていない。ロケールで設定するとなると、取得した値からそのコードページを使っているロケールを探さなくてはならないが、この目的に合った関数やAPIは見つかっていない。最悪switch文などを使って自分で書いてしまうのは可能だろう。あまり気乗りのする話ではないし、ソースコードの分量もかなりのものになるので、今回は実施を見送った。
レガシィロケールを使っている間は無理な注文
元々は日英仏3ヶ国語を混在できるか、という設問だった。Unicodeではないレガシィロケールを使っていたのでは無理なことは始めから明らか、と言ってしまえばそれまでの話である。
しかし探し続ければ見つかるものだ。ロケールやエンコーディングではなく、ファイルストリーム、今回の場合標準入力や標準出力を_setmode関数でUnicodeモードにできることが分った。なおVCP2008の資料にはUnicodeモードの解説が無かったため、リンク先はVCP2010に関する資料である。私が試した限りでは、VCP2008でもUnicodeモードがサポートされている。
それにしてもVCP2010の資料の「解説」にある「注意」の機械翻訳が最悪だ。
データ ファイルのストリームには、明示的にフラッシュ コードを使用して書き込む場合fflushを使用する前に_setmode、モードを変更します。コードをフラッシュする場合は、予期しない動作を得る可能性があります。ストリームにデータを記述した場合、コードをフラッシュする必要はありません。
これは本来「モードを切り替える前に書き込みバッファーに残っているデーターをフラッシュしておかないと期待した通りの動作にならない場合があるから、fflushでフラッシュしてから_setmodeでモードを切替える様にしてね。」と言う趣旨である。「fflushを使用する前に_setmode」や「コードをフラッシュする場合は、予期しない動作を得る可能性があります。」は本来の趣旨と逆の意味に捉えてしまっても不思議ではない。このページには「機械翻訳の免責」の表示が無いようだが、大丈夫なのだろうか。
モードをUnicodeにするアプローチ
ロケールやコードページを意識する必要が全く無く、実にシンプルである。
パイプにUnicode文字列を流し込むには、cmd.exeの新しいインスタンスを/uスイッチ付で起動する必要がある。
実にご機嫌である。ただし出力をリダイレクトするとBOMが付かないUTF-16LEのファイルができる。これはtypeコマンドで表示できない。メモ帳で「文字コード」にUnicodeを指定して開き上書き保存するとBOMが付いたUTF-16になって、typeコマンドでも表示できるようになる。
_O_U16TEXTの代わりに_O_U8TEXTを第二引数に指定して_setmode関数を呼び出せば入出力をUTF-8にすることも可能である。これはコマンドプロンプトに文字を表示する部分はUTF-16と全く差が無い。入力の全て、および出力をパイプやリダイレクトに流した時のエンコーディングがUTF-8になる点が異なる。
おまけ
日英仏3ヶ国語混在の文字列をコマンドプロンプトに表示すると言う元々のお題は完全クリアしたが、新たな課題が一つ出てきた。標準入出力をUnicode(UTF-16LE)にしたバージョンをいじっていて気付いたのだが、cmd.exeはユーザー入力を標準入力にShift JISで渡しているのだ。
「グRépublique=共和国」はユーザー入力のエコーバックで、その下の行をプログラムが出力している。それぞれデーター部分の先頭文字を比べると、Shift JISコード0x834Fの「グ」がU+4F83の「侃」に変わっていることが分る。cmd.exeからパイプにUnicodeを渡す時に/uスイッチが必要だったことから類推すると、これはコマンドプロンプトの仕様上の限界なのかもしれない。
3ヶ国語混在は諦めることになるが、今回のプログラムのように自分で直せるのなら、標準入力はCP932のエンコーディングにしておけば済む話である。しかし自分で直せないプログラムだったらどうするのか?最近はコンソール入出力をUTF-8で行うアプリケーションが増えているようだが、そのような場合の対応だ。今回調べた結果を利用すると、そのようなプログラムとShift JISでコンソール入出力ができるようにするWrapperが比較的簡単に作れそうである。簡単にするためWrapperではなくFilterだが、コンセプトはこんな感じになる。
こうやって使う。
パイプの制御をcmd.exeに任せているので、プログラムを終了させるためにCtrl+Zが必要だったり、不完全な部分がある。本来ならターゲットプログラムをサブプロセス化して、パイプの制御なども全てWrapperで行うようにすべきだ。完璧を期すと難しい部分が出てくるかもしれない。
コンセプトモデルとしたFilterプログラムの手法はかなり使い回しが効きそうである。デコード・エンコードはfgetws関数とwprintf関数に任せたので、特にコード変換のためだけのロジックは存在しない。また他はそのまま_setmode関数の呼び出し方を変えるだけで色々なパターンに対応できる。
_setmode関数で標準出力に_O_U8TEXTを設定したFilterプログラムsjis2u8、標準入力に_O_U8TEXTを設定したu8sjis、そして標準入出力両方に_O_U8TEXTを設定したテストプログラムutf8を使うと、UTF-8でコンソール入出力を行うアプリケーション用WrapperのProof of concept的なモデルができる。
テストプログラムutf8は元々単独でコマンドプロンプトへ全ての文字を正しく表示できるが、引数で与えているフランス語の文字が正しく表示できなくなっているのがu8mbcsを通っていることの証拠である。ユーザー入力のエコーバックがコマンドラインの直後になっているのはパイプのバッファー制御をcmd.exeに任せた影響である。先に述べたようにパイプの管理もWrapperで行えば解決できると思う。またこの仕組みのままでも、標準入力から文字列の取得を行う直前に標準出力のバッファーをフラッシュするよう、テストプログラムutf8を修正すれば違和感の無い挙動にできる。
今回のまとめ
多言語混在の文字列をコマンドプロンプトに表示するには、ロケールやコードページではなく標準出力のモードを変えればよいことが分った。コンソールアプリケーションを様々な文字エンコーディングに対応させるには、文字に関するプログラム内部の処理は全てUnicode対応にし、必要に応じてロケールと標準入出力のモードをうまく組み合わせるのが最もシンプルかつ柔軟性が高い。
(2013/01/28編集)コマンドプロンプトに現在設定されているコードページをプログラム的に取得し、それに合ったロケールを設定する様にしたかったが、結局方法が分らなかった。またsetlocale関数に空文字列を指定した場合、どこで設定しているロケールが使われるのかも不明だ。
コマンドプロンプトに現在設定されているコードページをプログラム的に取得し、それに合ったロケールを設定するのは可能である。しかしコードページとロケールを結び付けようとすると、あまりエレガントではない方法に頼る必要がありそうだ。またsetlocale関数に空文字列を指定した場合、どこで設定しているロケールが使われるのか不明だ。
分らないことが残っているのは気持ちが悪いが、当面レガシィロケールに関してはsetlocale関数に空文字列を指定する方法にしておけば実害は無い。そうすれば「"Japanese_Japan.932"」に設定されることが分っているし、それ以外のレガシィロケールを使わなければならない可能性を心配する必要が今は無いからだ。心配する必要が出てきた時は2番目の不明な点を調べられる環境にアクセスできるようになるだろうから、その時調べればよいだけの話である。
(2013/01/28編集終わり)
| 固定リンク
「IT」カテゴリの記事
- 意外な方法でSearchIndexHost.exeの暴走が止まる(2013.04.12)
- 「WindowsコマンドプロンプトにUnicode表示」の訂正(2013.03.31)
- WindowsコマンドプロンプトにUnicode表示(2013.01.28)
- 「合理的な愚者」って?(2012.06.02)
- 「リアルタイム放射線情報」開設(2011.03.19)
「パソコン・インターネット」カテゴリの記事
- 意外な方法でSearchIndexHost.exeの暴走が止まる(2013.04.12)
- 「WindowsコマンドプロンプトにUnicode表示」の訂正(2013.03.31)
- WindowsコマンドプロンプトにUnicode表示(2013.01.28)
- Ubuntu desktop 12.04のdhclientのバグ?(2012.11.22)
- Ubuntu 12.04 Desktopのdnsmasq(2012.05.03)
コメント