クラシック コマンドと PowerShell の間のエンコード設定

設定変数についてだらだら書いても面白味がないので、従来からのクラシックなコンソール コマンドと PowerShell の間のやり取りについて書いてみます。
まぁ、普通にコマンドを使う分には大きな問題はないのですが、従来のシェルではコマンドの入出力は基本的にバイナリのストリームであり、ファイルでした。これは、お互いのコマンドが入出力をどのように扱うかを協力的に扱っていたということでもあります。
PowerShell ではコマンド間の入出力はオブジェクトのストリームになりました。これは、PowerShell とクラシックなコンソール コマンドの間ではオブジェクトとバイナリの間のマーシャリングが必要になるということです。また、PowerShell はコンソール コマンドとのデータのやりとりを文字列ベースで行おうと努力します。

PowerShell からクラシック コマンドへ

XML を扱うコンソール コマンド……たとえば構造的な差を取る XmlDiffPatch コマンドは XML を入力として受け付けるので、その XMLエンコードはドキュメント宣言に書かれた通りである必要があります。つまり、PowerShell のコマンドの出力であるオブジェクトを utf-8 のようなエンコードで出力しなければならないことになります。
オブジェクトがどのように文字列化されるか?については少し色々あるので、問題を簡単にするために string または string の配列で構成されたストリームであることにしましょう。
PowerShell からクラシック コマンドへ送信されるエンコードは設定変数 $OutputEncoding にて指定できます。デフォルトでは US-ASCII が設定されているため、日本語すら受け渡しがうまくいきませんので注意が必要です。

PS > "かきくけこ", "あいうえお" | sort.exe
?????
?????

PS > $OutputEncoding = [Text.Encoding]::Default
PS > "かきくけこ", "あいうえお" | sort.exe
あいうえお
かきくけこ

設定変数はセッション変数ですので、特定のエンコードに依存した処理を関数やスクリプトにすることで、呼び出し前のエンコードなんだっけ?みたいなことにならなくて済みます。
この例で使っている System.Text.Encoding.Default は、OS のログインユーザの既定の言語が取得できるようになっています。このあたりは、.NET Framework の知識や経験がある人ならば即断できることなのですが、PowerShell を使うぞ!と考えて調べものをしているかんじの人にはなかなか見つけられないのが、ちょっとつらいところですね。

クラシックコマンドから PowerShell

設定変数に $InputEncoding というのがあれば一発解決だったのですが、残念ながら $InputEncoding はありません。こちらも .NET Framework の知識が必要になるので中々調べるのが難しいのですが、System.Console.OutputEncoding を設定することで変更できます。PowerShell の入力になるものはコンソールの出力なので、こんなところに設定があるんですね。
System.Console.OutputEncoding のデフォルト値は Default になっていて、日本語を既定としている環境からなら最初から cp932(ShiftJIS) のデータが入力できます。これは、PowerShell を動かしているコンソールウィンドウでキーボードを叩いて入力している文字列もこの設定の影響を受けるからです。たとえば、この値を System.Text.Encoding.ASCII に設定すると IME を使って日本語を入力しても最初の例の出力のようにすべて "?" に置換されてしまうようになります。*1

System.Console.OutputEncoding を設定する場合、2つの注意点があります。
1つめの注意点としては、セッション変数ではないというところです。つまり、元の値を覚えておいたり、その値を再設定するのは PowerShell を使う側や、スクリプトの責任になります。
もう1つの注意点は、ほとんどの人が Windows のコンソール上で PowerShell を使用していると思います。実際に System.Console.OutputEncoding を ASCII に変更してみた人は気が付いたと思いますが、Windows のコンソールはこの処理を検出して自動的にコンソールウィンドウのコードページを追従させます。結果として、ASCII を設定すると見た目が ASCII コードページのフォントに変わり、IME も無効になって日本語の入力ができなくなります。

設定した System.Console.OutputEncoding を戻すには、trap や try-finally, begin-process-end を使うかんじになります。

# 現在のエンコードを保管しておく
$enc = [Console]::OutputEncoding;
try
{
    # コンソールの出力を utf-8 に変更する
    [Console]::OutputEncoding = [Text.Encoding]::UTF8;

    # 2つの XML の差分を取得する
    $xml = [xml] XmlDiffPatch source1.xml source2.xml;

    # 差分を出力
    foreach ($diff in Select-Xml $xml -XPath "//Diff")
    {
       ...
    }
}
finally
{
    # エンコードを元に戻す
    [Console]::OutputEncoding = $enc;
}

こんなかんじですね。

*1:入出力される文字が ? になるのは Encoding.EncoderFallback, Encoding.DecoderFallback に ? が設定されているため、ASCII の範囲にない文字が ? に置換されるためです。