PowerShellでいろいろな文字コードを扱う

みんな大好きPowerShellのGet-Contentさんですが、日本語使うには文字コードの扱いがイマイチです。
Encodingなしでざっと試験してみると、

  • Shift-JIS(多分、Win CP932)は、そのまま読める(ロケールのおかげか)
  • Unicode系は、BOMありなら読める
  • Unicode系で、BOMなしはダメ
  • EUC-JP/JISはダメ

ま、変なコード体系が多い日本語がいけないんですけどね。
Encodingありは、Out-Fileさんと同じ。
(ってか、ヘルプにEncodingの記述がないのは何の嫌がらせですかね?Get-Commandで調べないと)




Out-Fileさんは、

  • -Encodingを指定しないと、utf16-le-bomあり
  • -Encodingは「unknown、string、unicode、bigendianunicode、utf8、utf7、utf32、ascii、default、oem」で
    • unknown、string、unicode : utf16-le-bom
    • bigendianunicode : utf16-be-bom
    • utf8 : utf8-bom
    • utf7 : utf7(bomなし)
    • utf32 : utf32-le-bom
    • ascii : ascii(日本語不可)
    • default : Shift-JIS
    • oem : Shift-JIS

defaultやoemは日本語環境でやっているからでしょう。


とてもいただけないのが、BOMなしUTF-8(utf8n)を読めないこと。
ま、Encodingを指定すれば良いんですが、そんなもんはお前が判定しろよ、と。
ということで、簡単な文字コード判定方法を調べてみた。
C# から IMultiLanguage2::DetectInputCodepage() を使う方法 - てっく煮ブログ
これを、PowerShellの関数に乗せてみたんだけど、、、
読みたいutf8nのファイルが、iso-8859-1だといわれる、、、。


そんなわけで小細工していろいろそぎ落として完成したのがこれ。
Get-Encoding.ps1

Param(
	$Path,
	[switch]$List,
	[int]$Max = 10
)

$namespaceName = 'Mlang'
$className = 'Encoding'
$classFullName = '{0}.{1}' -f $namespaceName, $className
if(-not ($classFullName -as [type])) {
	Add-Type @"
using System;
using System.Runtime.InteropServices;

namespace $namespaceName
{
    public class $className
    {
        public static DetectEncodingInfo[] Detect(byte[] bytes, int max)
        {
            DetectEncodingInfo[] ret = null;
            IMultiLanguage2 lang = (IMultiLanguage2)new MultiLanguage();
            int len = bytes.Length, scores = max, i;
            DetectEncodingInfo[] infos = new DetectEncodingInfo[scores];
            for (i = 0; i < scores; i++) infos[i] = new DetectEncodingInfo();

            // bytes to IntPtr
            GCHandle hbytes = GCHandle.Alloc(bytes, GCHandleType.Pinned);
            IntPtr pbytes = Marshal.UnsafeAddrOfPinnedArrayElement(bytes, 0);
            GCHandle hinfos = GCHandle.Alloc(infos, GCHandleType.Pinned);
            IntPtr pinfos = Marshal.UnsafeAddrOfPinnedArrayElement(infos, 0);
            try
            {
                if (lang.DetectInputCodepage(0, 0, pbytes, ref len, pinfos, ref scores) == 0 && scores >= 0)
                {
                    ret = new DetectEncodingInfo[scores];
                    for (i = 0; i < scores; i++) ret[i] = infos[i];
                }
            }
            finally
            {
                if (hinfos.IsAllocated) hinfos.Free();
                if (hbytes.IsAllocated) hbytes.Free();
            }

            return ret;
        }
    }

    public struct DetectEncodingInfo
    {
        public UInt32 nLangID;
        public UInt32 nCodePage;
        public Int32 nDocPercent;
        public Int32 nConfidence;
    };

    [ComImport, Guid("275c23e2-3747-11d0-9fea-00aa003f8646")]
    public class MultiLanguage { }

    [Guid("DCCFC164-2B38-11D2-B7EC-00C04F8F5D9A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IMultiLanguage2
    {
        void GetNumberOfCodePageInfo();
        void GetCodePageInfo();
        void GetFamilyCodePage();
        void EnumCodePages();
        void GetCharsetInfo();
        void IsConvertible();
        void ConvertString();
        void ConvertStringToUnicode();
        void ConvertStringFromUnicode();
        void ConvertStringReset();
        void GetRfc1766FromLcid();
        void GetLcidFromRfc1766();
        void EnumRfc1766();
        void GetRfc1766Info();
        void CreateConvertCharset();
        void ConvertStringInIStream();
        void ConvertStringToUnicodeEx();
        void ConvertStringFromUnicodeEx();
        void DetectCodepageInIStream();
        int  DetectInputCodepage(
            [In] UInt32 dwFlag,
            [In] UInt32 dwPrefWinCodePage,
            [In] IntPtr pSrcStr,
            [In, Out] ref Int32 pcSrcSize,
            [In] IntPtr lpEncoding,
            [In, Out] ref Int32 pnScores);
        void ValidateCodePage();
        void GetCodePageDescription();
        void IsCodePageInstallable();
        void SetMimeDBSource();
        void GetNumberOfScripts();
        void EnumScripts();
        void ValidateCodePageEx();
    }
}
"@
}
$encoding = $classFullName -as [type]
if($Path -is [string]) {
	$infos = $encoding::Detect([System.IO.File]::ReadAllBytes($Path), $Max)
} else {
	$infos = $encoding::Detect($Path, $Max)
}
$ret = $null
if($List) {
	$ret = $infos
} else {
	for($i = 0; $i -lt $infos.Count; $i++) {
		$ret = [System.Text.Encoding]::GetEncoding([int]$infos[$i].nCodePage)
		if($infos[$i].nLangID -eq 0xffffffff) { break }
	}
}
return $ret

期待したものが返ってこなかったら、-Listつけて応答見て、最後のfor文の中変えてみて。
、、、もっとも、そもそも選択肢に上がらない場合もあるけどね。
なんか、nDocPercentとかnConfidenceがやたら-1を返してくる、、、。


あ、名前空間やクラス名が気に入らない場合は、$namespaceNameとか$classNameとか変更して。
そこを変えればOKなように作ったのし。


んでまあ、これに判定させてから-Encodingを指定すればOKというすんぽーです。