2014/04/15

PowerShellはゆるふわな言語ですが、そのゆるふわさがたまによく牙を剥きます。今日はそんなお話。

あえとすさんがこんなツイートをされていました。

直観的には、$xには'A', 'B', 'C'の3要素が格納された配列となるのでLengthは3、$x[2]は最後の要素である'C'が入っていそうです。

さて、何故だかわかりますか。シンキングタイム3分。

…では解説です。

まず、'A' + @('B', 'C')というのは実は3要素の配列を返さず、単一の文字列を返します。というのもPowerShellは+, -等の二項演算子を利用する際、左辺と右辺の型が異なる場合は、まず右辺の型を左辺の型に暗黙の型変換を行ってから演算を行います。この場合だと右辺は@('B', 'C')なので文字列配列(厳密にはobject[])、左辺は文字列型なので、文字列配列が文字列に型変換されるわけです。

さて、ここで配列→文字列の型変換がどうやって行われるかという話なのですが、まず配列要素がそれぞれ文字列型に変換されます。この変換は型によってそれぞれ挙動が違いますが、特にPowerShell上で定義がない場合はToString()されたものが返されます。今回のは配列要素が元々文字列なので変換はありません。

次に、文字列同士がユーザー定義$OFSに格納されている文字列で連結されます。$OFSはデフォルトではnull(定義なし)なのですが、nullの場合は" "(半角スペース)として扱われます。

※ちなみにOFSとはOutput Field Separatorの略です。awkとかPerlとかにも同様の変数があり、PowerShellのはそれらを参考にしたものと思います。

よって、@('B', 'C')が文字列に変換されると、'B'と'C'が$OFSのデフォルトの" "で連結され、'B C'となります。変換の後+演算子が実行されて、'A'と'B C'が連結されるので、'AB C'となります。この値が$xに格納されるわけです。

$xには配列ではなく単一の文字列が格納されているので、Lengthプロパティはstringクラスのものが参照されるので、文字数を返却します。$xの中身はA,B,半角スペース,Cの4文字なので$x.Lengthは4になります。

また文字列変数に数値でのインデックスアクセスをすると、該当文字位置に格納されたchar型の文字が返されるので、$x[2]は$xに格納された3番目の文字(インデックスは0から始まるので)、' '(半角スペース)を返すわけですね。

これであえとすさんの疑問は解消したわけですが、じゃあ本来の目的である、「単一の値と配列を連結して配列を得る」にはどうするか、というと…

となるわけです。こうやって非配列値をあらかじめ@()により要素数1の配列にしておくと、+演算子の左辺と右辺がどちらも配列型となるため型変換は行われず、配列同士の+演算、すなわち配列の連結処理が行われるわけですね。

その1とありますがその2があるかは不明。なお、闇が沢山あるのは事実です。('A`)ヴァー

2012/08/01

シェル操作課題 (cut, sort, uniq などで集計を行う) 設問編 - Yamashiro0217の日記

ログファイルをコマンド使って解析するというこの課題。

え。Windows?まさか???cygwin入れたり、vmでやってみたり、開発サーバーで作業すればいいんじゃないっすか(ホジホジ)

いやいやWindowsでシェルと言えばPowerShellでしょう!というわけでやってみました。出力形式に関しては本筋とは関係ないと思うので、PowerShellのデフォルトのままです。

準備
$logs = Import-Csv hoge.log -Header ServerName,UnixTime,UserId,AccessUrl
問1 このファイルを表示しろ
$logs
問2 このファイルからサーバー名とアクセス先だけ表示しろ
$logs|select ServerName,AccessUrl
問3 このファイルからserver4の行だけ表示しろ
$logs|where {$_.ServerName -eq "server4"}
問4 このファイルの行数を表示しろ
$logs|measure
問5 このファイルをサーバー名、ユーザーIDの昇順で5行だけ表示しろ
$logs|sort ServerName,{[int]$_.UserId}|select -First 5
問6 このファイルには重複行がある。重複行はまとめて数え行数を表示しろ
$logs|sort * -Unique|measure
問7 このログのUU(ユニークユーザー)数を表示しろ
$logs|sort UserId -Unique|measure
問8 このログのアクセス先ごとにアクセス数を数え上位1つを表示しろ
$logs|group AccessUrl -NoElement|sort Count -Descending|select -First 1
問9 このログのserverという文字列をxxxという文字列に変え、サーバー毎のアクセス数を表示しろ
$logs|group {$_.ServerName -replace "server","xxx"} -NoElement|sort Count -Descending
問10 このログのユーザーIDが10以上の人のユニークなユーザーIDをユーザーIDでソートして表示しろ
$logs|where {[int]$_.UserId -ge 10}|select -Expand UserId|sort -Unique

PowerShellはテキスト情報をテキストのまま扱うのではなく、オブジェクトとして扱うところが特徴で、CSVファイルもImport-Csvコマンドレットを用いることで、ヘッダ文字列をプロパティ名として各行をオブジェクトとして読み込んでくれます。

あとは各種オブジェクト処理のコマンドレットに、パイプライン経由でオブジェクトを渡していくだけです。コマンドが何をやっているのかも、列番号ではなくプロパティ名を指定してやるので分かりやすいと思います。この課題に寄せられた回答の中では、シェル操作課題 SQLによる解答例が近いかと思います。

今回はCSVから情報を読み込んでいますが、ソースは別になんであってもいいわけです。たとえばGet-EventLogコマンドレットだとイベントログから持ってくることもできますし、XMLなら$xml=[xml](Get-Content file.xml)みたいな感じです。一旦オブジェクトとして取得できてしまえば、あとの処理方法は基本的に同じなので悩む必要がないのが良いところです。

そしてPowerShellの良いところはやはり、外部処理コマンドを使わずとも、シェル言語と組み込みコマンド(コマンドレット)のみで処理が完結するところです。たとえばbashだけでやるとtsekine's miscellaneous thoughts: シェル操作課題への回答のような大変なことになるようです。sortなど基本的なコマンドは使用するにしても、複雑なフィルタ処理などはawkを組みあわせる等しないと、特に後半の処理は辛いでしょう。PowerShellなら異なる処理系を持ち出す必要がなく、PowerShellのみですべて行えます。

もちろん問題点も色々あります。まず第一に、今回の課題のように、ヘッダ文字列がないCSVの場合は例のようにヘッダー文字列(=プロパティ名)を自分で定義してやる必要が出てきます。そのため可読性はともかく、若干、冗長なところがでてきてます。まあ例のように一旦オブジェクトを作って変数に入れてしまえばあとは使いまわせますが、そこはちょっと厳しい点かもしれません。

あとImport-CsvコマンドレットはCSVの各エントリをすべて文字列型のプロパティとしてオブジェクトに格納するところがちょっと微妙な気がしました。数値は数値型で入れてほしいですよね。おかげで何か所か、[int]にキャストせざるを得ない部分が出てきました。

そして処理速度の問題。おそらくログファイルが巨大だとかなりリソースを食って時間もかかると思います。処理を分ける、バックグラウンドジョブやワークフローで動かす、そもそもログファイルを分割保存するようにしておく、等、実運用上では工夫が必要かと思います。

(5:14追記)
既出でした>< [Power Shell] シェル操作課題への回答 - Pastebin.com(by @usamin5885さん)



Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー

Books

Twitter