しばたテックブログ

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

符号化処理芸人たちのシェル芸を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にはありますよ