しばたテックブログ

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

シェル芸勉強会の問題にPowerShellでチャレンジしてみた

きっかけはコレ。

togetter.com

最初はスルーしていたのですが、ふと良さげな実装を思いついたのでTwitterでこんなつぶやきをしてみました。

で、コレが第25回シェル芸勉強会のお題にもなっているとの事だったので、じゃあこっちもやれるだけやってみようと思いやってみました。

ちなみに勉強会の状況は完全に後追いです。

お題もちょっと考えて行き詰ったらすぐ解答を見ています。
問題を解くことよりPowerShellでどう実現するかに重点を置いています。

お題と解答

お題と解答は以下にあります。

【問題のみ】第25回もう4年もやってんのかシェル芸勉強会 – 上田ブログ

【問題と解答】第25回もう4年もやってんのかシェル芸勉強会 – 上田ブログ

PowerShellでの実装

Q1から順にやっていきます。
実行環境はWindows 10、PowerShell 5.1になります。

Q1. www.usptomo.comのIPアドレス

これは[Net.Dns]::GetHostAddressesメソッドを使えば一瞬です。

[Net.Dns]::GetHostAddresses("www.usptomo.com").IPAddressToString

# もしくは以下の様にしても良い
"www.usptomo.com" | % { [Net.Dns]::GetHostAddresses($_).IPAddressToString }

実行結果)

f:id:stknohg:20161031095518p:plain

シェル芸というには邪道かもしれませんが、PowerShellとしては王道です。


【ちょっと追記】

コマンドレットの使用にこだわるのであれば以下の様にしてもOKです。
この場合も内部的には[Net.Dns]::GetHostAddressesメソッドが使われています。

(Test-Connection "www.usptomo.com" -Count 1).IPV4Address.ToString()

f:id:stknohg:20161031181552p:plain

Q2. ひらけ!ポンキッキ

こちらはTwitterで書いた通りです。

"ひらけ!ポンキッキ" | % {$c,$l=[Char[]]$_,$_.Length;for($i=0;$i-lt$l;$i++){-join $c[($i-$l)..($i-1)]}}

実行結果)

f:id:stknohg:20161031095823p:plain

ちょっとだけ補足すると、このコードはPowerShellの配列インデックスが負値を取れることを利用しています。
ForEach-Object(%)内部のループを展開すると以下の様なイメージで処理が行われています。

# 処理イメージ
$c = [Char[]]"ひらけ!ポンキッキ"
-join $c[-9..-1] # ひらけ!ポンキッキ
-join $c[-8..0]  # らけ!ポンキッキひ
-join $c[-7..1]  # け!ポンキッキひら
# ・・・(中略) ・・・
-join $c[-1..7]  # キひらけ!ポンキッ

Q3. rbash

PowerShellではrbashに相当する機能制限はありません。
この問題はパスで。

Q4. すけふぇにんけん

こちらはPowerShell2.0以降である必要がありますが、以下の様にして可能です。

echo すけふぇにんけん `
    | % { Add-Type -AssemblyName "Microsoft.VisualBasic" } `
        { $w = -join ([Char[]]$_ | % { $_ + "゛" }); [Microsoft.VisualBasic.Strings]::StrConv($w,[Microsoft.VisualBasic.VbStrConv]::Wide) -replace "゛","" }

実行結果)

f:id:stknohg:20161031100709p:plain

解答ではnkfコマンドを使っていますが、Windowsには無いので近い機能としてVB.NETのStrConv関数で代用しています。
PowerShellからVB.NETの機能を使う詳細は、

stknohg.hatenablog.jp

を参考にしてください。
ネタで書いたエントリだったのですが意外なところで役に立ちました。

Q5. 1秒に一つ*が伸びていくアニメーション

コンソールに文字を出すだけという条件であればこんな感じで。

while(!(Start-Sleep 1)){Write-Host "*" -NoNewline}

PowerShellには進捗状況を表示するためのWrite-Progressコマンドレットがありますので、これを使っても良いと思います。

while(!(Start-Sleep 1)){$m+="*";Write-Progress $m}

実行結果)

f:id:stknohg:20161031103701p:plain

Q6. 文字列を復元

こちらはエンコーディングさえわかってしまえばあっさり行くと思います。

# "b730a730eb30b8820a00"
cat ".\crypt" `
    | % { $n = 2; for( $i = 0; $i -lt $_.Length; $i += $n ){ -join $_[$i..$($i+$n-1)] } } `
    | % { $b = @() } `
        { $b += [Convert]::ToByte($_, 16) } `
        { [Text.Encoding]::Unicode.GetString($b) }

実行結果)

f:id:stknohg:20161031101637p:plain

処理の細かいところは

stknohg.hatenablog.jp

を参考にしてください。

Q7. UNIX時刻で素数

これはPowerShellだとワンライナーで書くのは不可能です...
スクリプトブロックになってしまいますが、こんな感じでご容赦ください。

# 検算できていないので間違いがあるかも...
&{
    # 素数判定
    function IsPrime([int]$n){
        if($n -eq 1){return $false}
        if($n -eq 2){return $true}
        $b = [Math]::Floor([Math]::Sqrt($n));
        for($i = 2; $i -lt $b; $i++){ 
            if ($n % $i -eq 0){return $false}
        }
        return $true
    }
    # 621355968000000000 = ([DateTime]::ParseExact("1970/01/01 00:00:00", "yyyy/MM/dd HH:mm:ss", $null)).Ticks
    $s = [Math]::Floor(([Datetime]::Parse("2016.10.29").Ticks - 621355968000000000 ) / 10000000);
    $s..($s+86399) | ? { IsPrime $_ } | % {(New-Object "Datetime" (($_*10000000 )+621355968000000000 ))}
}

※実行結果は省略

まず、解答ではfactorコマンドを使って素数の判定をしていますが、Windowsにはもちろん存在せず代替できそうなコマンドやライブラリもありません。
仕方ないので私でも書けるエラトステネスの篩で簡単な関数IsPrimeを書いてやります。

そしてUnixTimeは.NET FrameworkのTicksから頑張って変換する必要があります。
UnixTimeは1970/01/01 00:00:00から1秒単位のTicks、.NET FrameworkのTicksは0001/01/01 00:00:00からナノ秒単位なので頑張れば普通に変換できます。

余談

余談ですが、Get-DateコマンドレットにUnix形式の表示書式を指定できる-UFormatがあり、このパラメーターに+%sを指定してやるとUnixTimeの結果を返すことが出来ます。
UnixTimeと書いたのは、Githubの以下のIssueにある様に、

github.com

  • 精度がナノ秒で小数表示
  • UTCではなくローカル時刻の1970/01/01 00:00:00を基準としたTicks

となっているためです。
Issue中に記載されていますが、正しいUnixTimeとするには以下の様に書く必要があります。

[Math]::Floor((Get-Date ([DateTime]::UtcNow) -UFormat +%s))

8. サイン波

流石にお題と完全に同じ答えを出すことができませんでした。
概ね一致する結果であれば以下の様にすれば可能です。

0..19 `
    | % {$l=20;$a=[Array]::CreateInstance([int],$l,$l);for($y=0;$y-lt$l;$y++){$a[([Math]::Ceiling([Math]::Sin(($y+1)/3)*10)+9),$y]=1}} `
        {$x=$_;-join (0..19 | % {if($a[$x,$_]){"* "}else{"  "}})}

実行結果)

f:id:stknohg:20161031103435p:plain

解答ではrsコマンドを使って軸の変換をしていますが、上のコードでは二次元配列の変換で代用しています。
PowerShell標準では二次元配列を生成できないため[Array]::CreateInstanceを使って生成しています。

その他の部分については気合で何とかしています。
ワンライナーにする前のコードは以下の様な感じで、こちらの方が分かりやすいかと思います...

# [int]型の二次元配列を生成
$l=20
$a=[Array]::CreateInstance([int],$l,$l)

# *を描画する要素を1としている
for($y=0;$y-lt$l;$y++){ 
    $a[([Math]::Ceiling([Math]::Sin(($y+1)/3)*10)+9),$y]=1
}

# 縦横変換
0..19 | % { $x=$_;-join (0..19 | % { if($a[$x,$_]){"* "}else{"  "}}) }
補足

補足ですが、私の環境(Bash on Ubuntu on Windows)では

# rs -t 23 でなく rs -t 22 でないと波形がずれる
seq 1 20 | awk '{a=sin($1/3) * 10 + 10;for(i=0;i<a;i++)printf "@ ";printf "* ";for(i=a;i<20;i++)printf "@ ";print ""}' | rs -t 22 | tr @ ' '

としないと綺麗な波形になりませんでした。

f:id:stknohg:20161031113859p:plain

最後に

正直疲れました...

PowerShellでも頑張ればシェル芸をこなすことはできますが、やっぱり用途が違うなぁという印象です。