しばたテックブログ

気分で書いている技術ブログです。

PowerShellでBOM無しUTF8を簡単に扱う、デフォルト設定を簡単に変える方法

【2017/11/06追記】

現在開発中のPowerShell 6.0からファイル出力に関わるエンコーディングの扱いが変わり、BOM無しUTF-8をより簡単に扱える様になっています。
詳細は以下のエントリを見てください。

blog.shibata.tech

【追記ここまで】


なんとなく思いついて試したら意外といい感じになったので。

はじめに

PowerShellのUTF8はBOM付きUTF8

PowerShellにはOut-File等といったファイルを簡単に扱うためのコマンドレットがいくつか存在します。

たとえばOut-FileでUTF8のファイルを出力する場合は以下の様に-Encodingパラメーターを指定してやればOKです。

"ファイルの内容" | Out-File ".\UTF8なファイル.txt" -Encoding utf8

ただし、PowerShell(とその基盤である.NET Framework)においてUTF8はBOM付きです。

BOM無しのUTF8を扱う場合はOut-Fileなどのコマンドレットは使用できず、以下の様にBOM無しのSystem.Text.UTF8Encodingクラスを生成してやり.NET FrameworkのIO処理を頑張って書いてやる必要があります。(追記あり)

# BOM無しのUTF8エンコーディングクラスを生成
#   コンストラクタの第一引数 = $false でBOM無し
$UTF8woBOM = New-Object "System.Text.UTF8Encoding" -ArgumentList @($false)
# あとは.NET FrameworkのIO処理を頑張って書く
[System.IO.File]::WriteAllLines((Join-Path $PWD "BOMなしUTF8なファイル.txt"), @("ファイルの内容"), $UTF8woBOM)

-Encodingパラメーターで指定可能なエンコーディング

また、先の-Encodingパラメーターは文字列でエンコーディング名を指定しており、コマンドレットによって指定可能なパラメーターは微妙に異なるのですが、おおむね以下となっています。

-Encodingパラメータの値 対応する.NET Frameworkのエンコーディング
unknown System.Text.Encoding.Unicode
string System.Text.Encoding.Unicode
System.Text.Encoding.Default
unicode System.Text.Encoding.Unicode
bigendianunicode System.Text.Encoding.BigEndianUnicode
utf8 System.Text.Encoding.UTF8
utf7 System.Text.Encoding.UTF7
utf32 System.Text.Encoding.UTF32
ascii System.Text.Encoding.ASCII
default System.Text.Encoding.Default
(PowerShell Coreだと System.Text.Encoding.GetEncoding(28591)(PowerShell Core 6.0だとUTF8) *1
oem GetOEMCP()関数で得られるコードページのエンコーディング

これら以外のエンコーディングを扱おうとするとBOM無しUTF8の場合と同様に.NET FrameworkのIO処理を頑張って書くしかありません。

せっかく便利なコマンドレットが提供されているにもかかわらず、これはちょっと不便でなりません。

BOM無しUTF8を簡単に扱う、デフォルト設定を簡単に変える方法

で、実際不便をするケースがあったのでInvoke-CustomEncodingBlockというファンクションを作ってみました。

Invoke-CustomEncodingBlock ファンクション

実装は次で説明しますのでまずは使い方から。

Invoke-CustomEncodingBlockでは引数にスクリプトブロックを指定し、そのスクリプトブロック中でのみutf8defaultパラメーターの挙動を変更する様にしています。

BOM無しUTF8を扱いたい場合は、スクリプトブロックと-UseBOMlessUTF8パラメータを使い以下の様にします。

Invoke-CustomEncodingBlock { "BOMなしUTF8なファイルの内容" | Out-File ".\BOMなしUTF8.txt" -Encoding utf8 } -UseBOMlessUTF8

これでOut-FileからBOM無しUTF8のファイルを出力することができます。

また、-DefaultCodePageパラメーターを使うとdefaultを任意のコードページに差し替えることができます。
例えば以下の様にするとEUC(コードページ51932)のファイルを出力することができます。

Invoke-CustomEncodingBlock { "EUCなファイルの内容" | Out-File ".\UEC_CP51932.txt" -Encoding default } -DefaultCodePage 51932

そしてOut-Fileコマンドレット以外にも利用できますので、たとえば、

Invoke-CustomEncodingBlock { Get-Content -Path ".\UEC_CP51932.txt" -Encoding default } -DefaultCodePage 51932

の様にするとGet-ContentでEUCのファイルを読み込みことができたりもします。

いかがしょうか?
既存のコマンドレットを生かしつつ好きなエンコーディングを扱えるので結構便利だと思います。

実装

実装はGistに上げています。
PowerShell 2.0, 4.0, 5.1, 6.0.0.alpha10 *2 で動作確認をしています。

仕組み

仕組みは極めて単純かつ強引ですw
uft8defaultパラメーターは最終的にSystem.Text.Encoding.UTF8System.Text.Encoding.Defaultに変わるため、リフレクションで強引にこれらのエンコーディングの中身を差し替え、スクリプトブロックを呼び終えたら元に戻すということをやっています。

このため、RunSpaceを使って並列処理をしている場合は予期せぬ問題が起きる可能性がありますが、それ以外の場合はおおむね問題ないはずです。
PowerShellのバージョンによって差し替える必要のあるフィールドに違いがあるため、そのあたりは工夫しています。

最後に

とりあえずこんな感じです。

思いつきで作ったものですが、想像以上に便利で個人的にはかなり気に入っています。

追記 BOM無しUTF8を簡単に扱う別解

【2016/10/03追記】

Add-ContentSet-Contentにバイナリデータを扱う-Encoding Byteパラメーターがあるのを今更ながら知りました...
Byte[]なバイナリデータをそのままファイルに書き込みできるため、BOM無しUTF8は、

"ファイルの内容" `
    | % { [Text.Encoding]::UTF8.GetBytes($_) } `
    | Set-Content -Path ".\BOMなしUTF8なファイル.txt" -Encoding Byte

の様にしてあっさり簡単に書き込むことができます。
自分のPowerShell力の低さがうらめしいです...

【2017/05/18追記】

こちらの方法ですが、状況によってはOut-Stringを使って明示的に改行込みの文字列にしてやらないと改行コードが抜かされて出力される場合があります。

[ファイルの内容を出力するコマンドなど] | `
    | Out-String `
    | % { [Text.Encoding]::UTF8.GetBytes($_) } `
    | Set-Content -Path ".\BOMなしUTF8なファイル.txt" -Encoding Byte

参考)

satob.hatenablog.com

*1:PowerShell Core 6.0 Beta.1よりUTF8を使う様に変更されている 参考 : https://github.com/PowerShell/PowerShell/pull/3467

*2:PowerShell 6.0では-Encodingパラメーターの扱いが変わったのでこの関数を使えない様にしました

符号化処理芸人たちのシェル芸をPowerShellで再現する

元ネタはこちら。

papiro.hatenablog.jp

はじめに

私はシェル芸人ではないので大したことも面白いこともできませんのでご了承ください。

本エントリはシェル芸人たちの匠の技をPowerShellで再現するにはどうするかという点だけに注力しています。
PowerShell(というかWindows)には残念ながら元ネタで使われている各種コマンドが無い*1ため、その部分をPowerShellおよび.NET Frameworkの機能をいかに使って補うかがキモになるかと思っています。

それでは元ネタのお題の順に進めていきます。
特にバージョン依存の処理は書いていないはずですが、Windows 10のPowerShell(5.1)で動作確認をしています。

1. Use Xamarin.

お題

最初はちょまどさんのこちら。

元ネタの回答はこちら。

echo 01010101 01110011 01100101 00100000 01011000 01100001 01101101 01100001 01110010 01101001 01101110 00101110 | tr -d ' ' | sed 's/^/obase=16;ibase=2;/' | bc | xxd -p -r

PowerShell版

PowerShellだとこんな感じで書けます。
長くなるのが嫌だったので適当なところで改行しています。

# PowerShell
-split "01010101 01110011 01100101 00100000 01011000 01100001 01101101 01100001 01110010 01101001 01101110 00101110" `
    | % { $o = "" } `
        { $o += [Char]([Convert]::ToByte($_, 2)) } `
        { $o }

実行結果はこんな感じ。

f:id:stknohg:20160923215425p:plain

解説

簡単に解説を入れていきます。
最初の行の、

-split "01010101 01110011 01100101 00100000 01011000 01100001 01101101 01100001 01110010 01101001 01101110 00101110"

は、お題の文字列がスペース区切りなので-split演算子を使って配列化しています。

2行目以降は配列化された二進表記の文字列をForEach-Object(%)を呼んで、3つのスクリプトブロックを使って変換しています。
ForEach-Object(%)では-Begin-Process-Endのパラメータで最大3つのスクリプトブロックを指定することができ、それぞれ、

  • -Begin : 最初のオブジェクトがパイプされる直前に呼ばれる処理
  • -Process : 各オブジェクトがパイプされる毎に呼ばれる処理
  • -End : 最後のオブジェクトがパイプされた後に呼ばれる処理

となっています。

今回はパイプラインには配列化した各要素("01010101", "01110011", ...)が順に渡されていきます。
最初のスクリプトブロック(-Begin)で最終出力用の変数$oを定義し、二番目のスクリプトロック(-Process)で入力文字列の変換処理を行っています。

[Char]([Convert]::ToByte($_, 2))

で入力文字列$_("01010101"など)をByte型に変換し、Char型にキャストしてASCII文字列に戻しています。
最後のスクリプトブロック(-End)で連結された$oを出力ストリームに出力しています。

ちなみに、

$o

Write-Output $o

は同義です。
今回はそれっぽくするためWrite-Outputを端折ってみました。

2. 届けiOS10

お題

次はぱぴろんさんのこちら。

元ネタの方ではRubyやPerlを使った回答が提示されていました。

PowerShell版

PowerShellだとこんな感じです。

# PowerShell
"111001011011000110001010111000111000000110010001011010010100111101010011001100010011000000001010" `
    | % { $n = 8; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } } `
    | % { $b = @() } `
        { $b += [Convert]::ToByte($_, 2) } `
        { [Text.Encoding]::UTF8.GetString($b) }

実行結果はこんな感じ。

f:id:stknohg:20160923222547p:plain

解説

今回はお題の文字列に区切り文字が無いので、2行目のForEach-Object(%)

% { $n = 8; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } }

で、8文字ごとに区切って後続にパイプしています。
スクリプトブロックを1つだけにした場合は-Processブロックになります。

3行目以降のForEach-Object(%)につては基本的に最初のお題と同じですが、最終的な答えがUTF8の文字列であったため、Byte[]列をUTF8に変換する様になっている部分が異なっています。

3. 焼肉

お題

次はぐれさんさんのこちら。

こちらの回答は自分の環境(Bash on Ubuntu on WindowsおよびVM上のUbuntuのBash)ではうまく動作せず、以下のコードであれば動作しました。

echo $(echo 1302140411021101140213011103110511011103110211 | fold -w 2 | awk '{for(i=1;i<=$2;i++){printf $1}}' FS= | sed 's/^/obase=16;ibase=2;/') | bc | xxd -p -r

シェル芸人ではないので細かいところはよくわかりません...

PowerShell版

PowerShellだとこんな感じです。

# PowerShell
"1302140411021101140213011103110511011103110211" `
    | % { $n = 2; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } } `
    | % { $s = "" } `
        { $s += "".PadRight([string]$_[1], $_[0]) } `
        { $s } `
    | % { $n = 8; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } } `
    | % { $b = @() } `
        { $b += [Convert]::ToByte($_, 2) } `
        { [Text.Encoding]::UTF8.GetString($b) }

実行結果はこちら。

f:id:stknohg:20160923225340p:plain

解説

だいぶつらくなってきました...
文字列をランレングス圧縮しているとの事ですので、そのあたりの処理が入ってきます。

PowerShellには良い感じのコマンドがないのでひたすらForEach-Object(%)のスクリプトブロックで頑張るしかありません。

2行目の

% { $n = 2; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } }

はお題の文字列を2文字区切りにしています。
3~5行目のForEach-Object(%)でそれぞれ二進表記に変換(13111などへ)しています。

% { $s = "" } `
  { $s += "".PadRight([string]$_[1], $_[0]) } `
  { $s } `

6行目の

% { $n = 8; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } }

で二進表記の文字列を8文字区切りにし、7~9行目の、

% { $b = @() } `
  { $b += [Convert]::ToByte($_, 2) } `
  { [Text.Encoding]::UTF8.GetString($b) }

でUTF8の文字列に変換しています。

4. YES

最後のお題です。

元ネタの回答はこちら。

echo 'In48FEBACHw8CEACCEBCCH48' | base64 --decode | xxd -b -c 3 | awk '$1="";$NF="";1' | sed 'y/01/ y/'

PowerShell版

PowerShellだとこんな感じ。

# PowerShell
"In48FEBACHw8CEACCEBCCH48" `
    | % { [Convert]::FromBase64String($_) } `
    | % { $s=@(); $map=@{ "0"=" "; "1"="y" } } `
        { $s += ([Regex]($map.Keys -join "|")).Replace([Convert]::ToString($_, 2).PadLeft(8, "0"), {param($m) $map[$m.Value] }) } `
        { for( $i = 0; $i -lt $s.Length; $i+=3 ){ -join $s[$i..($i+2)] } }

実行結果はこちら。

f:id:stknohg:20160923230555p:plain

解説

PowerShellでBASE64を処理するのは[Convert]::FromBase64String()メソッドを呼ぶだけですので楽勝です。
3~4行目の、

% { $s=@(); $map=@{ "0"=" "; "1"="y" } } `
  { $s += ([Regex]($map.Keys -join "|")).Replace([Convert]::ToString($_, 2).PadLeft(8, "0"), {param($m) $map[$m.Value] }) }

の部分でBASE64文字列をデコードしたByte値を二進表記に変換し、加えて0→" "1→"y"への置換を行っています。
文字列の置換には[Regex]クラスを使用しています。
最後の5行目

  { for( $i = 0; $i -lt $s.Length; $i+=3 ){ -join $s[$i..($i+2)] } }

の部分で、3Byteごとに区切っています。

まとめ

とりあえずこんな感じです。

シェル芸に対してPowerShellの圧倒的なコマンド不足が露呈しつつも、.NET Frameworkのもつ強力な機能のおかげでかろうじて何とかなっている感じといったところです。
良くも悪くも「コマンドが無いならスクリプトブロックで何とかするしかないじゃん!」といった体になってしまいました。

とはいえ、実際の業務でPowerShellを利用する場合であれば、足りないコマンドは自作の関数で補うことができますのでそこまで苦労することは無いかと思います。

割と思いつくままにコードを書きましたので、もっと良い方法は他にもたくさんあると思います。
より良い方法がありましたらフィードバック頂けると嬉しいです。

*1:いちおう補足しておくとBash on Ubuntu on Windowsにはありますよ

PowerShell on Linuxに普通にPSRemotingしてみる - その2

以前のエントリ、

stknohg.hatenablog.jp

でソースからのビルドは面倒だからやらないと言ったのですが、気が変わりました。
というのも、

github.com

のIssueがCloseされmasterブランチに取り込まれたとのことで、次のバージョンのリリース前にその結果を確認したくなったからです。

動作環境

今回は普段使い慣れてるCentOS 7.2(1511)でやります。
Virtualbox上の仮想マシン環境です。

PowerShell on LinuxにPSRemotingしてみる

今回は非rootユーザーで試しているので必要に応じてsudoしています。

1. PowerShellのインストール

https://github.com/PowerShell/PowerShell/blob/master/docs/installation/linux.md#centos-7github.com

GitHubからPowerShellのrpmファイルをインストールするだけです。

# Install Powershell on Linux
sudo yum install -y https://github.com/PowerShell/PowerShell/releases/download/v6.0.0-alpha.10/powershell-6.0.0_alpha.10-1.el7.centos.x86_64.rpm

2. OMI Serverのインストール

github.com

こちらもrpmをインストールするだけです。

# Install OMI
sudo yum install -y https://github.com/Microsoft/omi/releases/download/v1.1.0-0/omi-1.1.0.ssl_100.x64.rpm

3. PowerShell on Linux OMI Providerのビルド

ここからが今回独自の作業になります。

基本的な手順はGitHubに記載されています。
PowerShell on Linux OMI ProviderはC++製ですのでmakeするための環境と、pam-developenssl-devel*1をインストールします。

そして最新のソースをクCloneしてビルドスクリプトbuild.shを実行します。
今回は/tmp/omiディレクトリを作成し、そこにCloneしてみました。
(ビルドスクリプトは/tmp/omi/psl-omi-provider/build.shに存在します)

# Build latest psrp-omi-provider
sudo yum install -y cmake make gcc gcc-c++ git
sudo yum install -y pam-devel openssl-devel
# 
mkdir /tmp/omi
cd /tmp/omi/
git clone --recursive https://github.com/PowerShell/psl-omi-provider.git
cd psl-omi-provider/
./build.sh

エラー無くビルドが完了しlibpsrpomiprov.soが作成されれば成功です。

ちなみにビルドが完了した状態でbuild.shと同じディレクトリにあるrun.shを実行するとOMI Serverをデバッグ実行しコンソールにログを表示させることができます。*2

これでもPSRemotingすることができますが、今回はさらにRPMパッケージの作成まで行っていきます。

4. installBuilder(pal)のインストール

GitHub上の手順

Current OMI (Open Management Infrastructure) products use the InstallBuilder tool to create installation packages.

とある様にパッケージの作成にはInstallBuilderのインストールが必要です。
InstallBuilderと言っておきながらリンク先はPlatform Abstraction Layer(pal)というライブラリを指しており、最初はわけがわからなかったのですが、どうやらビルド環境の判定(OSの種類やパッケージツールの有無など)のためにこのライブラリの一部機能を利用しているということがわかりました。

まずはrpmパッケージを作るということでrpm-develrpm-buildをインストールしておきます。*3

sudo yum install -y rpm-devel rpm-build

続けて先ほどのPlatform Abstraction Layer(pal)のソースをCloneします。
Clone先は必ずPowerShell on Linux OMI Providerと同じディレクトリ(今回であれば/tmp/omi/)にしてください。

そしてpal/build/ディレクトリに移動し、configureコマンドを実行します。
このconfigureでビルド環境の判定を行い、同じディレクトリにconfig.makファイルを作成します。

# Install installBuilder(pal)
cd /tmp/omi/
git clone https://github.com/Microsoft/pal.git
cd pal/build/
# Make config.mak
./configure

config.makファイルが次の手順で必要になります。

5. rpmパッケージの作成とインストール

最後にrpmパッケージを作成してインストールまで行います。

PowerShell on Linux OMI Providerのinstallbuilderディレクトリに移動してmakeを実行します。
先ほどの手順で正しくconfig.makファイルが作成されてれば、omi/Unix/output/release/ディレクトリにRPMファイルが作成されます。

# Build rpm package
cd ../../psl-omi-provider/installbuilder/
make

作成されたrpmファイルをインストールすれば完了です。

# Install rpm package
cd ../omi/Unix/output/release/
sudo rpm -i ./psrp-1.0.0-0.universal.x64.rpm

6. PSRemotingしてみる

後の作業は前回と同じでNew-PSSessionOptionEnter-PSSessionしてやればOKです。

# サーバーのIPが 192.168.33.209 の場合
$o = New-PSSessionOption -SkipCACheck -SkipRevocationCheck -SkipCNCheck
Enter-PSSession -ComputerName 192.168.33.209 -Credential root -Authentication basic -UseSSL -SessionOption $o

下図の様にCentOSでもエラー無くPSRemotingできました。

f:id:stknohg:20160921002702p:plain

これで完了です。

当たり前ですが作成したrpmファイルは再利用できますのでバックアップしておくと次回以降楽ができるでしょう。

補足

今回の一連の手順は以下のGistにまとめていますのでこちらも参考にしてください。

最新のPowerShell on Linux OMI Providerをインストールするサンプル(CentOS7) · GitHub

*1:apt-getだとlibpam0g-dev、libssl-dev

*2:OMI Providerと同時にOMI Serverもビルドしているため

*3:rpm-buildがインストールされているか次で判定しているため必ずこの手順を先にしてください