tech.guitarrapc.cóm

Technical updates

第2回チキチキ!シェル芸人養成勉強会をPowerShellでやってみた

UPS友の会 - またシェル芸勉強会やりました で紹介されていた、第2回チキチキ!シェル芸人養成勉強会が楽しそうだったのでPowerShellでやってみました。 なにせ主題が………
端末操作だけで色んな仕事をできるようになってもらいます。
OK、WindowsでシェルならPowerShellだろーー(棒  という思いになったのはPowerShellを愛する者としては、何も間違っていないはず。 原則は大元に従って、かつPowerShell標準のままで、どこまでできるかということを念頭にしています。
マウスも使わず、 プログラムも書かず、 GUIツールを立ち上げる間もなく、 あらゆる調査・計算・ファイル処理をコマンド一撃で終わらす。
また、
シェルスクリプト?そんな大げさなもんいらん。
との事なので、なるべく1ライナーで……敢えて、変数に収めるべきところすら、そのまま利用できるところは読みやすさを犠牲にパイプで繋ぐという制約で  (おい 出題内容は、UPS友の会様をご覧下さい。 ※シェル環境前提なので、なるべくAliasを利用しているのはご了承ください。 ※私はAlias余り好きじゃない派です。 ※PowerShellとBashの大きな違いは | (パイプ)で渡されるのが文字列ではなくオブジェクトということを念頭に…
Get-ChildItem  #ls
Get-Content #cat #gc
Get-Random #random
Foreach-Object #%
Where-Object #?
Measure-Object #measure
Compare-Object #diff
Format-Table #ft
Format-List #fl

問題1: 文字化けしたファイルの削除

以下のファイルを作って、ファイル一覧を表示(ls)
"abc" | out-file -Encoding utf8 -FilePath abc.txt
"DEF" | out-file -Encoding utf8 -FilePath DEF.txt
"㊥" | out-file -Encoding utf7 -FilePath ㊥.txt
ls
文字化けしたファイルの削除。 ※単純に正規表現で処理してます。
Remove-Item *.txt -Exclude [a-zA-Z]* -Confirm

問題2: 計算

ファイルを見てみて
cat num.txt
足し算でforeachを使ったのは怒られるのだろうか… ※文字列を[int[]]配列にキャストしましょう。
$num = cat num.txt | %{ $_ -split " "}
foreach($int in [int[]]$num){$sum += $int}
$sum
[2012/12/17 AM 追記]
@superriver様から素敵な別解が!! 掲載許可をいただき以下に紹介します。 別解1. begin / process / end 部を活用したSclipt Blockによるワンライナー例: ※非常に素敵です。何がって、begin/process/endという、対応comandletが無い場合に応用が利く、大事な考えが自然と活用されていることが素敵なのです。
begin {$sum=0} process { gc num.txt |% { $_ -split ' ' } |% {$sum+=[int]$_ }} end {$sum}
別解2. measureを使ったワンライナーの例: ※measureがいいなぁって呟いたら、見事に拾って下さいましたww 流石です…!
gc .\num.txt |% { $_.split(' ') } | measure -sum | ft sum
[2012/12/28 AM 追記] @superriver様からの例を受けて、設問がMax以外の表示させたくないワンライナー回答ならこうかなと。 ※これならsum結果の55だけ表示します。
(cat num.txt | %{ $_ -split " "} | measure -sum).sum
ftを入れなくても一応measureは表示してくれるので、ただ表示するならこうかなとか。 ※やはり、measureは使えますね!! -Averaveや-Maximumや-Minimumも入れるとww
cat num.txt | %{ $_ -split " "} | measure -sum

問題3: 条件でデータを取り出し

ファイルを見てみて
cat hoge.txt
Import-Csvしてしまって、テキストベースでの扱いではなくオブジェクトベースで扱います。 ※文字列が単純にスペースで処理されていてもImport-Csvで-Delimiterを" "(半角スペース)と指示すれば取り込めます。 あとは、aとbそれぞれを選択してあげれば完了です。
$hoge = Import-Csv hoge.txt -header abc,num -Delimiter " "
$hoge | ?{$_.abc -eq "a"} | sort num -desc | select -first 1
$hoge | ?{$_.abc -eq "b"} | sort num | select -first 1
出題の回答にあったsortしてselect forst 1よりも、Measure-Objectを使えば、最大、最小、平均、合計、個数が得れるのでよりいいかと。
$hoge | ?{$_.abc -eq "a"} | measure num -max -minimum -average -sum -count
$hoge | ?{$_.abc -eq "b"} | measure num -max -minimum -average -sum -count
これまた@superriver様から素敵な別解が!! 掲載許可をいただき以下に紹介します。 別解1. [math]::メソッドによるmax取得でのワンライナー例: ※これが問2のようなmeasureを使えない場合でも、begin/proces/endによるScript Blockの好例ですね…!!素敵過ぎて憧れますww
begin{$m =@{}} process { import-csv .\hoge.txt  -header key,val -Delimiter ' ' |%{ $m[$_.key] = [math]::max([int]$_.val, $m[$_.key]) } } end {$m}
別解2. measureを使ったワンライナーの例: ※こちらもmeasureがいいなぁって呟いたら、見事に拾って下さいました!! Powershellのワンライナーで使える機能をふんだんに利用している具合に目眩を覚える素敵さ…www
import-csv -Header key,val .\hoge.txt -Delimiter ' ' | group key |%{ $k=$_.name; $_.group | measure -max val | ft @{Label="Key"; Expression={$k}}, maximum}
[2012/12/28 AM 追記] @superriver様からの例を受けて、一時変数を利用せずScript Blockで処理してみました。 ※表示幅がFormat-Tableでは広がるので-Autosizeを付けてます。
Import-Csv hoge.txt -header abc,num -Delimiter " " | group abc | select name, @{"label"="max";"expression" = {($_.Group | measure -Maximum num).Maximum}} | ft -Autosize
Format-Listならそんな必要もありません。
Import-Csv hoge.txt -header abc,num -Delimiter " " | group abc | select name, @{"label"="max";"expression" = {($_.Group | measure -Maximum num).Maximum}} | fl
ワンライナーが見にくい? 改行するとこうです。
Import-Csv hoge.txt -header abc,num -Delimiter " " `
    | group abc `
    | select name,
        @{"label"="max";
            "expression" = {($_.Group | measure -Maximum num).Maximum }} `
    | ft -AutoSize

問題4: 計算

ファイルを見てみて
cat num2.txt
文字列を抜き出す時に、今度はSelect-Stringを利用してaとb毎に分けてみました。 挑戦とばかりに、文字列のまま扱ってみましたが辛いですね…。
$num2 = select-string -Path num2.txt -Pattern [a]
$num3 = $num2.line -replace "a","" -split " "
foreach($int2 in [int[]]$num3){$sum2 += $int2}
$sum2

$num4 = select-string -Path num2.txt -Pattern [b]
$num5 = $num4.line -replace "b","" -split " "
foreach($int3 in [int[]]$num5){$sum3 += $int3}
$sum3

問題5: 日付と曜日

ファイルを作ります。..演算子を使うとshellのseqのようにレンジ指定で連番生成できるので楽ですね。
1990..2012 | %{$_.ToString() + "0101"} | Out-File osyogatsu.txt
ここが冗長ですが、[datetime]型は、YYYYMMDDではなくYYYY/MM/DDである必要があったので…くっ! 配列を取り出すとx y zと値が分かれてしまうので、-joinメソッドで結合させています。
([datetime[]](cat .\osyogatsu.txt | %{($_[0..3] -join "") +"/" + ($_[4,5] -join "") + "/" + ($_[6,7] -join "") })).DayOfWeek | sort | group | select count, name
ワンライナーが見にくい?改行するとこうですね…。
([datetime[]](cat .\osyogatsu.txt `
    | %{($_[0..3] -join "") +"/" `
        + ($_[4,5] -join "") + "/" `
        + ($_[6,7] -join "") })).DayOfWeek `
    | sort `
    | group `
    | select count, name
[2012/12/28 AM 追記]
これまた@superriver様から素敵な別解が!! 掲載許可をいただき以下に紹介します。 別解. そもそもファイルを作成するときにdatetimeの書式にしなよ: ※うぅ…ずるいなりよーwww ポイントは-Fと、| %でDayOfWeekプロパティを受けている点ですね。
1990..2012 |% { [datetime]("{0}/01/01" -f $_) } |% { $_.dayofweek} | group | ft count,name
[2012/12/29 AM 追記] OK、ファイル読み取るね!でも日付に書式指定するときは正規表現でしょ? ※と、指摘がwww まさにその通りです~。という訳で、これまた@superriver様からの別解追加
gc .\osyogatsu.txt |% { $_ -replace '(....)(..)(..)', '$1/$2/$3' } |% { [datetime]$_ } | %{$_.DayOfWeek} | group | ft count,name
[2012/12/28 AM 追記] @superriver様からの例を受けて、プロパティを().DayOfWeekでくくっていた所を %で受けました。
[datetime[]](cat .\osyogatsu.txt | %{($_[0..3] -join "") +"/" + ($_[4,5] -join "") + "/" + ($_[6,7] -join "") }) | %{$_.DayOfWeek} | sort | group | select count, name
さらに[datetime[]]での囲みも | %で受ければ
(cat .\osyogatsu.txt | %{($_[0..3] -join "") +"/" + ($_[4,5] -join "") + "/" + ($_[6,7] -join "") }) | %{[datetime]$_}| %{$_.DayOfWeek} | sort | group | select count, name
ワンライナーが見にくい? 改行するとこうです。
(cat .\osyogatsu.txt `
        | %{($_[0..3] -join "")
        + "/"
        + ($_[4,5] -join "")
        + "/"
        + ($_[6,7] -join "") }) `
    | %{[datetime]$_} `
    | %{$_.DayOfWeek} `
    | sort `
    | group `
    | select count, name
[2012/12/29 AM 追記] ご指摘の正規表現で、きっちり数値桁を明示するなら…\dと{}でこうですね。
cat .\osyogatsu.txt |% { $_ -replace '(\d{4})(\d{2})(\d{2})', '$1/$2/$3' } |% { [datetime]$_ } | %{$_.DayOfWeek} | sort | group | ft count,name
ご指摘の通り、 . でいいことに大賛成ですが、まぁ一応正規表現できるよアピールでw
cat .\osyogatsu.txt |% { $_ -replace '(....)(..)(..)', '$1/$2/$3' } |% { [datetime]$_ } | %{$_.DayOfWeek} | sort | group | ft count,name
ちなみに、sortでパイプを受けているのは、曜日順に結果を並べる為です。 sortをつけないと以下になります。
Count Name
----- ----
    4 Monday
    3 Tuesday
    3 Wednesday
    3 Friday
    4 Saturday
    3 Sunday
    3 Thursday
sortをつけると以下になります。
Count Name
----- ----
    3 Sunday
    4 Monday
    3 Tuesday
    3 Wednesday
    3 Thursday
    3 Friday
    4 Saturday

問題6: ダミーデータの作成

ここが問題の意図を読みかねたので、一応2パターンで。 ※ランダムに100以内の数値を出力して、並び替え。
1..100 | %{random $_} | sort
※単純にランダムに100以内の数値を出力。
1..100 | %{random $_}

問題7. 検索

ここも、例題からの出力意図がイマイチだったので、回答例をPowershellで表現してます。 すいませんすいません。
cat words.txt | ?{$_ -match "[a-zA-Z]"} | sort -Unique

問題8. ファイルの比較

これは、Powershellのお得意な所ですね。 PowershellにはCompare-Object(diff)があるので、利用します。
diff (cat file1.txt) (cat file2.txt)

問題9. 形式変換

正直これが一番つらかったです。 ワンライナーに近づけた版と、一度変数に入れて多少見やすくした版の2つを…。 まずは、ファイルを見ます。
cat game.txt
ファイルを、表、裏順にsortかけてから、Hash Tableに入れ込みます。 ※Hash Tableに入れることで、オブジェクト処理できるようになります。あとは、HashTableのキー毎に値を展開します。 ポイント1. $()内部でHash Tableを展開することで、変数にHash Tableを代入しなくても取り出せます。 ※$()を付けずに"$Hash Table"にすると、型名しか出ないのです。 ポイント2. 配列はただ出力すると値ごとに行が分かれます。 ※配列を一行に出力するには、""で囲んでStringと明示してあげます。
$a = cat game.txt | %{$_[2] + " " + $_[0] + " " + $_[4]} | sort -Unique
$h = @{title=($a | %{$_[2]} | sort -Unique);name1=($a | %{$_[0]} | sort -Unique)[0];name2=($a | %{$_[0]} | sort -Unique)[1];value1=$a | %{$i=@()}{$i += $_[4]}{$i[0..$(($a | %{$_[2]}).length / 2 - 1)]};value2=$a | %{$i=@()}{$i += $_[4]}{$i[$(($a | %{$_[2]}).length / 2)..$(($a | %{$_[2]}).length -1)]}}
"   $($h.title)";"$($h.name1) $($h.value1)";"$($h.name2) $($h.value2)"
見にくい?改行するとこうですね…。
$a = cat game.txt | %{$_[2] + " " + $_[0] + " " + $_[4]} | sort -Unique
$h = @{title=($a | %{$_[2]} | sort -Unique);
    name1=($a | %{$_[0]} | sort -Unique)[0];
    name2=($a | %{$_[0]} | sort -Unique)[1];
    value1=$a | %{$i=@()}{$i += $_[4]}{$i[0..$(($a | %{$_[2]}).length / 2 - 1)]};
    value2=$a | %{$i=@()}{$i += $_[4]}{$i[$(($a | %{$_[2]}).length / 2)..$(($a | %{$_[2]}).length -1)]}}
"   $($h.title)";"$($h.name1) $($h.value1)";"$($h.name2) $($h.value2)"
Hash Tableを一度変数に入れると、最後の出力部は若干見やすくなります。
$a = cat game.txt | %{$_[2] + " " + $_[0] + " " + $_[4]} | sort -Unique
$h = @{title=($a | %{$_[2]} | sort -Unique);
    name1=($a | %{$_[0]} | sort -Unique)[0];
    name2=($a | %{$_[0]} | sort -Unique)[1];
    value1=$a | %{$i=@()}{$i += $_[4]}{$i[0..$(($a | %{$_[2]}).length / 2 - 1)]};
    value2=$a | %{$i=@()}{$i += $_[4]}{$i[$(($a | %{$_[2]}).length / 2)..$(($a | %{$_[2]}).length -1)]}}
$title = $h.title
$name1 = $h.name1
$value1 = $h.value1
$name2 = $h.name2
$value2 = $h.value2
"   $title"
"$name1 $value1"
"$name2 $value2"
[2012/12/31 AM 追記]
これまた@superriver様から素敵な別解が!! 掲載許可をいただき以下に紹介します。 別解1. HashKeyを利用した行列変換とbegin end processでのワンライナー例 ※実は29日の内にいただいていたのですがorz さて、見てみましょう。
begin { $m=@{''='  1 2 3 4 5'}} process { import-csv .\game.txt -Encoding Default -header inning,ht,score -Delimiter ' ' |% { $m[$_.ht] += " {0}" -f $_.score }} end { $m.getenumerator() | sort key | ft -auto -HideTableHeaders}
ポイントは、m=@{''=' 1 2 3 4 5'}} でのHash Table作成と、|% { $m[$_.ht] += " {0}" -f $_.score }でのKeyへのArray追加です。 Tash TableへArrayを追加することで、縦から横に並び替えられています。 後は、抜き出すだけですね。
悔しいので、@superriver様が、ズルしちゃったwと可愛く仰っていたbegin句の固定inningラベルを動的に処理します。 さらに、上記ソースでは実行ごとにhtとscoreが追加される問題も解決します。 ※process句ではforeachを回しているのでinningの重複をsort -Uniqueで除外できません。(なんとかできないものか) ※そこで、beginにimport-csvごと持ってきて、Hash Tableの初期化とinningのsort -Uniqueを処理してしまいます。 ※後は一緒ですね。ちなみにftの-HideTableHeadersでテーブルのカラム名を非表示にしています。
begin { $temp= Import-Csv .\game.txt -Encoding Default -Header inning,ht,score -Delimiter ' ' ; $m=@{'' = "  " + $($temp.inning | sort -Unique) }} process { $temp |% { $m[$_.ht] += " {0}" -f $_.score }} end { $m.getenumerator() | sort key | ft -AutoSize -HideTableHeaders}
はい、改行するとこうです。
begin {
    $temp= Import-Csv .\game.txt `
        -Encoding Default `
        -Header inning,ht,score `
        -Delimiter ' ' ;
     $m=@{'' = "  " + $($temp.inning | sort -Unique) }
     } `
process { $temp |% { $m[$_.ht] += " {0}" -f $_.score }} `
end {
    $m.getenumerator() `
        | sort key `
        | ft -AutoSize -HideTableHeaders
    }
begin process endを使っているのでワンライナー(笑)と言われそうですが、ナカナカこれ以上短く出来ず……orz 我こそはという方をお待ちしています!! コメント欄にS.K.様から、短いコード例が来ましたーww Import-Csvを利用せずに、ソース元と同様にキレイに処理されています。 とても参考になるので是非ご覧ください!

問題10: ファイルの結合

始め、別々のObjectにファイルを取り込んでScript Block使ってやろうとか思い色々迷走しましたが断念orz 最終的には、1つのObjectとして取り込んで、ごにょごにょすることであっさりできました。
$file = Import-Csv -Path file1.txt,file2.txt -Delimiter " " -header num,name -Encoding default
$a = $file | sort name -Unique | group num | sort Name
"$($a.group[1].num) $($a.group[1].name) $($a.group[0].name)";"$($a.group[2].num) $($a.group[3].name) $($a.group[2].name)";"$($a.group[4].num) $($a.group[5].name) $($a.group[4].name)"
これも見にくい?改行するとこうですね…。
$file = Import-Csv `
    -Path file1.txt,file2.txt `
    -Delimiter " " `
    -header num,name `
    -Encoding default
$a = $file `
    | sort name -Unique `
    | group num `
    | sort Name
"$($a.group[1].num) $($a.group[1].name) $($a.group[0].name)"
"$($a.group[2].num) $($a.group[3].name) $($a.group[2].name)"
"$($a.group[4].num) $($a.group[5].name) $($a.group[4].name)"
[2012/12/28 追記] せっかくなのでワンライナーで
Import-Csv -Path file1.txt,file2.txt -Delimiter " " -header num,name -Encoding default | sort name -Unique | group num | sort Name | %{"$($_.group[1].num),$($_.group[1].name),$($_.group[0].name)"}
見にくいですよね。改行します。
Import-Csv `
            -Path file1.txt,file2.txt `
            -Delimiter " " -header num,name `
            -Encoding default `
        | sort name -Unique `
        | group num `
        | sort Name `
| %{"$($_.group[1].num),$($_.group[1].name),$($_.group[0].name)"}

まとめ

以上で、全問回答となります。 MVP for PowerShellのライター牟田口大介先生が行われている「シェル操作課題」をPowerShellでやってみたでも指摘がありますが、一旦オブジェクトとして取り込んでしまえば、PowerShellはごにょごにょ容易に行えます。 とはいえ、問9、問10はごにょごにょするやり方を考えないと辛かったのも確かで…ようは良く考えろってことですね。 基本的には、PowerShellの標準構文でも、おおよその文字列処理は可能です。 WindowsでPerlで行ってきたような処理は、PowerShellに置換できると信じています。 とりあえず、Import-CsvなりImport-*は、-Headerの冗長さはともかく、かなり便利なので活用していくべきところでしょうね。 最後に、いくつか処理スクリプトをPowerShellで書いていますが、処理速度は問題です。 牟田口先生のように、「処理を分ける、バックグラウンドジョブやワークフローで動かす」ことは考慮する必要がありますのでご注意を。

Powershellでの処理参考

「シェル操作課題」をPowerShellでやってみた [Power Shell] シェル操作課題への回答 - Pastebin.com(by @usamin5885さん)