d.sunnyone.org
sunnyone.org

ページ

ラベル PowerShell の投稿を表示しています。 すべての投稿を表示
ラベル PowerShell の投稿を表示しています。 すべての投稿を表示

2015-12-20

PowerShellでスクレイピングする (AngleSharp編)

これは、PowerShell Advent Calendar 2015の12/20分の記事です。Invoke-WebRequestとIEエンジンによるDOM処理とは違うやり方について書きます。

---
PowerShellを使うと、Invoke-WebRequestを使うことで、特にライブラリ等を準備せずともWebページをスクレイピングすることができる(参考:PowerShellでスクレイピング 後編 HTMLをパースする) 。

これはこれで便利なのだが、少し複雑なHTMLになると、Where-ObjectやSelect-Objectがだんだん増えてきてしまう。こんな感じである。



やっぱりDOMツリーをたどるなら使い慣れたCSSセレクタの記法が使いたいよね?ということで、今回はCSSセレクタが使える.NET用HTMLパーサライブラリ「AngleSharp」をPowerShellから使ってみるお話。本当はC#で馴染みのあったCsQueryを使おうと思ったのだけど「Not Actively Maintained」ということでAngleSharpにしてみた。

インストール

例によって適当なフォルダに移動してnuget.exeを使ってダウンロードする。
> nuget install AngleSharp

使い方

初期化

まず、AngleSharp.dllをロードし、loaderが設定されたconfigを使って、BrowsingContextを作っておく。

このBrowsingContextを使っていく。

GETリクエスト

シンプルにGETリクエストを出すには、文字列のURLを使ってOpenAsyncを呼び出す。そして返ってきたものに対して、最初に一致したものだけでよければQuerySelector()を、全て欲しい場合はQuerySelectorAll()を呼び出す。

例として、このブログの右側の「ブログ アーカイブ」の一番上のタイトル(画像参照)を取得するにはこんな感じである。



~Asyncは、流儀通りTask<T>が返ってくる。PowerShell世界では扱いに困るのでResultでひたすらブロックしていく(ひどい)

ほぼ同じであるが、もう一つの例として@mutaguchiさんのブログPowerShell Scripting Weblogの右側にあるMVPのタイトルを取得するにはこんな感じである。


各ブラウザの開発者ツール(F12)のコンソールで、document.querySelector('#ArchiveList')などとして、先に試しておくとよい。

POSTリクエスト

POSTにするには、OpenAsync()の第2引数にリクエストオブジェクトを生成する。POSTリクエストを作る便利メソッドはいくつかあり、例えばPostAsUrlEncodedを使うとこんな感じである。

$docができたら、あとはGETと同様に、QuerySelector()やQuerySelectorAll()を呼び出せばOKである。

感想

これで一応、AngleSharpをPowerShellから呼ぶことはできた。Where-Objectを連発するよりは見通しのよいものが書けると思う。ただし、AngleSharpの呼び出す部分は拡張メソッドだらけになっており、PowerShellから大変に呼びにくいので、うーんという感じ。やはりInvoke-WebRequestが、querySelector / querySelectorAllできれば万事OKだったのに、という感じであった。

2015-08-10

PowerShell と ImageMagick (Magick.NET)で画像を加工する

WindowsでImageMagickを使おうとすると、cmdの引数の扱いがだるいと思っていたのだけど、ImageMagickライブラリの.NETラッパーであるところのMagick.NETをPowerShellから呼んでしまえば、便利に呼べることに気づいた。convertコマンドのパラメータの順序とか考えなくていいので、正直convert使うより楽かもしれない。

基本的にMagick.NETを呼んでいけばOK(気づいたPowerShell固有の罠は以下に書いておく)。本格的に処理するのであればC#でコード書いたほうがいいと思うけど、そこまででもないワンタイムバッチ処理にどうぞ。ただし「Verb-Noun -Arg abc」styleでない、.NETクラス直呼び出しの「なんちゃってPowerShell」なので、がっかりされる前に宣言しておく。コンソールで打って問題ない量だと思うけど。

なお動かしてる環境は、PowerShell v4 / x64。

準備

インストール

適当なフォルダを作り、NuGetでMagick.NETのx64 or x86を環境にあわせてもってくる。
PS C:\> mkdir c:\opt\magick
PS C:\> cd c:\opt\magick
PS C:\opt\magick> Invoke-WebRequest https://nuget.org/nuget.exe -OutFile nuget.exe
PS C:\opt\magick> .\nuget install Magick.NET-Q16-x64
NuGet的にはAnyCPUがあるので、何も考えずにAnyCPUを選びたくなるところではあるが、それはNG。というのも、AnyCPU版はx86とx64のDLLをEmbedded Resourceでかかえる形になっていて、必要に応じてロードしているっぽいのだが、これがAdd-Typeでうまくロードできない。x64やx86はプラットフォーム依存DLLがそのまま置いてあるのでOK。

アセンブリのロード

Add-Typeするだけ。これはさすがにスクリプト化しておいたほうが便利かもね。
PS C:\opt\magick> Add-Type -Path .\Magick.NET-*\lib\net40-client\Magick.NET-x64.dll

イメージの読み書き

イメージの読み込み

とにかくここから始まる。これはMagickImageインスタンスを作ればOK。
PS c:\temp> $image = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
ただし、ワーキングディレクトリはPowerShellのものではないので、フルパスで指定するのが無難。クラス名はNew-O[TAB] MagickI[TAB]で補完できるのでおすすめ。

イメージの書き込み

後述の加工作業を行ってから、.Write(path)で書く。

PS C:\temp> $image.Write('c:\temp\sample-out.jpg')

基本的に「New-Object」「何かして」「Write」の組み合わせ。

フォーマット指示が必要な場合、Writeの前に入れる。
PS C:\temp> $image.Format = 'png'
PS C:\temp> $image.Write('c:\temp\sample-out.png')
とか、
PS C:\temp> $image.Format = 'jpeg'
PS C:\temp> $image.Quality = 99
PS C:\temp> $image.Write('c:\temp\sample-out-q99.jpg')
とか。

あとは
PS c:\temp> $image.Dispose()
で後片付け。インタラクティブ操作時はpowershellごと消してもいいと思うけど、ループ時とかは注意。

情報の表示

属性の表示

$imageと打てば、それだけで情報がでてくる。Width, Heightなど。これだけでもわりと便利。ただ、計算するプロパティが含まれているせいか、全部出すと重いのでループするのはおすすめしない。表示はこんな感じ(全部ではない):


EXIF情報の表示

$image.GetExifProfile().Values でOK。
PS C:\temp> $image.GetExifProfile().Values
 DataType                       IsArray                           Tag Value
 --------                       -------                           --- -----
    Ascii                         False              ImageDescription
    Ascii                         False                          Make SONY
    Ascii                         False                         Model NEX-6
    Short                         False                   Orientation 1
 Rational                         False                   XResolution 350
 Rational                         False                   YResolution 350
    Short                         False                ResolutionUnit 2
    Ascii                         False                      Software NEX-6 v1.01
    Ascii                         False                      DateTime 2015:05:24 10:46:23
    Short                         False              YCbCrPositioning 2
Undefined                          True                       Unknown {80, 114, 105, 110...}
 Rational                         False                  ExposureTime 0.01
 Rational                         False                       FNumber 13
    Short                         False               ExposureProgram 2
    Short                         False               ISOSpeedRatings 100
(以下略)
PS C:\temp> $image.GetExifProfile().Values.Where({$_.Tag -eq 'FocalLengthIn35mmFilm'}).Value
24

1ファイル系の処理

1ファイルに対する処理をこの画像を使って行っていく。


回転(変換と保存)

回転はRotate()。

PS c:\temp> $image = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
PS C:\temp> $image.Rotate(90)
PS C:\temp> $image.Write('c:\temp\sample-out.jpg')


(多分)EXIFにしたがって回転してくれるAutoOrient()ってのもある。便利ね。

切り出し

切り出しはCrop()。Gravityはどこからひっぱってくるかの指示。
PS c:\temp> $image = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
PS c:\temp> $image.Crop(200, 200, [ImageMagick.Gravity]::Center)
PS C:\temp> $image.Write('c:\temp\sample-out.jpg')

リサイズ

Resize()かResample()かScale()かThumbnail()。この使い分けは難しいのだけど、とりあえず無難そうなResizeだけ説明。

そのResize()もパラメータをいくつか選べる。
PS C:\temp> $image.Resize

OverloadDefinitions
-------------------
void Resize(int width, int height)
void Resize(ImageMagick.MagickGeometry geometry)
void Resize(ImageMagick.Percentage percentage)
void Resize(ImageMagick.Percentage percentageWidth, ImageMagick.Percentage percentageHeight)

一番簡単なやつがwidth, heightを指定するやつ(New-ObjectとWriteはここの説明では省略)
PS C:\temp> $image.Resize(200, 100)
ただし、アスペクト比を維持するので、(200, 100)にはならない。ここに入る大きさになる。



パーセンテージ指定が次に簡単(書き方がイマイチ...)
PS C:\temp> image.Resize((New-Object ImageMagick.Percentage 50))


で、もっともいろいろできるのが、MagickGeometry指定パターン。ImageMagickをコマンドで使っている人ならおなじみの、例の記法が使える。例えば、アスペクト比を無視する!を付与したスタイル。
PS C:\temp> $image.Resize((New-Object ImageMagick.MagickGeometry '200x200!'))


このあたりの挙動はImageMagickそのものの話なので、詳しくは公式ドキュメント(ImageMagick v6 Examples --
Resize or Scaling (General Techniques)
)を参照。なお、日本語でまとめてくれてる人もいる(ImageMagickでリサイズする方法

複数ファイル系の処理

ここからはこの2つの画像を例に説明。

結合

結合は、MagickImageCollectionを作ってimageを追加し、AppendHorizontally()かAppendVertically()を呼ぶ。
PS C:\temp> $image1 = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
PS C:\temp> $image2 = New-Object ImageMagick.MagickImage c:\temp\sample2.jpg

PS C:\temp> $col = New-Object ImageMagick.MagickImageCollection
PS C:\temp> $col.Add($image1)
PS C:\temp> $col.Add($image2)

PS C:\temp> $image = $col.AppendVertically() # あるいは $col.AppendHorizontally()  
PS C:\temp> $image.Write('c:\temp\sample-out.jpg')

重ねる

結合とだいたい同じノリだけども、重ねることも可能。
PS C:\temp> $image1 = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
PS C:\temp> $image2 = New-Object ImageMagick.MagickImage c:\temp\sample2.jpg

PS C:\temp> $col = New-Object ImageMagick.MagickImageCollection
PS C:\temp> $col.Add($image1)
PS C:\temp> $col.Add($image2)

PS C:\temp> $image = $col.Evaluate([ImageMagick.EvaluateOperator]::Add)
PS C:\temp> $image.Write('c:\temp\sample-out.jpg')

ここまで書いておいて例が悪いな。

etc, etc...

あとはBlurとか、さまざまなフィルタがあるのだけど、きりないからこのへんでやめ。ImageMagickは加工は得意なので、できることはたくさんある。

他のものを知りたかったら、$image.で[TAB]してみたり、MagickNetのドキュメントとか、ImageMagickのコマンドラインオプション(ImageMagick: Command-line Options)を参照。

おまけ: 統計情報の表示

最後に一応、需要はなさそうだけど、これをやるために使い始めたので説明。平均とかの取り方。

Statistics()を呼んで、Composite()あるいはGetChannel(チャンネル)を呼ぶ。
先に
$image.Grayscale([ImageMagick.PixelIntensityMethod]::Lightness)
を呼んでおくのも便利かも。
PS C:\temp> $stat = $image.Statistics()
PS C:\temp> $stat.Composite()

Channel           : Composite
Depth             : 1
Entropy           : 0.966856332282486
Kurtosis          : -1.25816857805541
Maximum           : 65535
Mean              : 27115.482213115
Minimum           : 0
Skewness          : 0.0779930273889325
StandardDeviation : 17685.6919467067
Sum               : 27115.482213115
SumCubed          : 45922010656938.6
SumFourthPower    : 2.14361048522477E+18
SumSquared        : 1049386363.99743
Variance          : 312783699.633806

PS C:\temp> $stat.GetChannel([ImageMagick.PixelChannel]::Blue)

Channel           : Blue
Depth             : 8
Entropy           : 0.967346949241304
Kurtosis          : -1.26933689965117
Maximum           : 65535
Mean              : 28404.2418122061
Minimum           : 0
Skewness          : 0.0995661483530123
StandardDeviation : 18559.3929293196
Sum               : 28404.2418122061
SumCubed          : 52904690206274
SumFourthPower    : 2.59600361552516E+18
SumSquared        : 1151252018.83115
Variance          : 1151252018.83115

2015-04-23

PowerShellからRazorテンプレートエンジンを使ってみた

以前のCottleの記事で書いた通り、去年の7月(!)にPowerShellとRazorの話をしたのですが、資料をアップロードしました。上げてなかったのは、Cottleの記事に書いた通り、向いてなくね、と思ったからなのですが、Razorだからこそうれしいケースがなくもなさそうだったので、とりあえず資料を上げるだけ上げることにしました。



その中使っていたスクリプトがこちら。


DynamicViewBagがModelってナニソレって感じなんですが、このRazorEngineは、Modelクラスを作って渡すことを想定されているので、クラスを作るのが大変なPowerShellからはまっとうな呼び方をするのが難しかったのです。いわば抜け穴。これを探してたのは、LTしませんかと現場で言われてから発表までの話なので、もうちょっと頑張ればいい方法があるかもしれませんが、がんばってません。ASP.NET MVC屋的に気持ち悪いだけで、名前がアレなのを除けば、PowerShellとのつなぎという意味ではそれほど悪くないかなとも思っていますが、このまま行くか頑張って直すかは使われる皆様の判断にお任せします。

ちなみにおすすめはC#からRazorEngineを使う、です。

追記
RazorEngineのソースを見たところ、当時読んでたソースとだいぶ変わっているようです。もしかすると、こんなややこしい呼び方は要らないかもしれません。もし動かないとかあれば、RazorEngineのソースをご確認ください。

追記2
altriveさんが、ModelにPSCustomObjectを渡す例を書いてくれました。一度コンパイルするといいらしいです。要ちぇっく。
https://gist.github.com/altrive/2f048e4c78398c055090

2015-02-11

PowerShellでCottleテンプレートエンジンを使う

Cottleは、テンプレート文字列にしたがってテキストを生成する.NET用のテンプレートエンジン。超ざっくり言うと、{}で囲ったところに変数名を書くと展開されたり、構文を書くと分岐ができたりする(文法の詳細はこちら)。今回はこれをPowerShellから使う話を書く。

1. ダウンロード

CottleのライブラリはNuGetからダウンロードするようになっているので、Installing NuGet のCommand-Line Utilityから「nuget.exe」をダウンロードする。そして、以下のコマンドを実行すれば、実行したディレクトリ配下にパッケージが展開されるのでdllを入手する。
> nuget install Cottle
(面倒だったら https://www.nuget.org/api/v2/package/Cottle/1.3.1 てな感じのURL に直接アクセスして拡張子を.zipにして展開すればdllが得られなくもない)

2. 呼び出す

dllの横にスクリプトを置いて、単にドキュメントの通りのメソッドを呼べばいい。一応関数にするとこんな感じ。
(…ほんとはモジュールにしないとね)


これを実行すると、以下のように出力される。
実行結果
=========
結果: 失敗
ログ:
Failed to write a file.
No space left on device.

ifだけじゃなくいろいろな記法が使えるので、ドキュメントを参照してほしい。

前のPowerShell勉強会で、RazorEngineを使ってRazorテンプレートをPowerShellからレンダリングする話をしたのだけど、PowerShellから呼びやすくもなかったし、何よりRazorがHTML用すぎてPowerShellで使いそうなHTML以外のテキストに向いている文法ではないので、今回はCottleを紹介した。

2014-09-20

WindowsアプリのUI自動操作をUI Automation PowerShell Extensionで行う

UI Automation PowerShell Extensionは、.NET Frameworkに付属するUI オートメーションライブラリのPowerShellラッパーで、UI自動操作(UIオートメーション)をPowerShellで記述できる。これにより、GUIしかないアプリの定型作業をスクリプト化したり、UIテストを行ったりすることができる。だいぶ前に書こうと思ったのだけど、思い出したのでようやく書くことにした。

まずは例として、Windowsに付属する「電卓」を使って1 + 2を計算してみるスクリプトを書くと以下のようになる。

$process = Start-Process calc -PassThru
$window = Get-UiaWindow -ProcessId $process.Id
$window | Get-UiaButton -Name '1' | Invoke-UiaButtonClick | Out-Null
$window | Get-UiaButton -Name '加算' | Invoke-UiaButtonClick | Out-Null
$window | Get-UiaButton -Name '2' | Invoke-UiaButtonClick | Out-Null
$window | Get-UiaButton -Name '等号' | Invoke-UiaButtonClick | Out-Null


結構簡単に操作できることがわかると思う。ではここから使い方を見ていく。

準備

まずは準備として、http://uiautomation.codeplex.com/ に行き、Downloadボタンを押してzipをダウンロード、どこかに展開する。

本当はこの中身をDocuments\WindowsPowerShell\Modules\UIAutomationに入れるのだけど、お試しなのでとりあえず
Import-Module .\UIAutomation.dll
とする。

Get-Moduleして以下のようにでてくればOK。

PS > Get-Module UIA*

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Binary     0.8.7.79   UIAutomation                        {Add-UiaBannerText...


作り方

作り方は、「UI要素を探す」「UI要素をコントロールする」を繰り返すだけ。

UI要素を探す

UIをポイントするとUI要素を表示してくれるSpyツール「UIAutomationSpy.exe」が付属しているので、これを使ってコマンドを生成することができる。

UIAutomationSpy.exeを起動すると、ボタンが2つとテキストボックスがいくつかある画面が起動するので「Start」ボタンを押してから、マウスカーソルに適当なウィンドウに持っていくと、テキストボックスにコマンドが表示される。



欲しい部品が見つかったら、素早くStopボタンを押す。

一番下のテキストボックスにホバーしたUI要素を探すためのコマンドが書かれているが、基本的にはこれをそのまま使うのではなく、不要な部分を削ったり、ちょっと加えたりする。例えばさっきの電卓の例であれば、採取したタイミングでは以下のようになっている。

Get-UiaWindow -Class 'CalcFrame' -Name '電卓' | `
Get-UiaPane -Class 'CalcFrame' | `
Get-UiaPane -Class '#32770' | `
Get-UiaButton -AutomationId '81' -Class 'Button' -Name 'クリア'

しかし、Get-Uiaシリーズは基本的に再帰的に要素を探してくれるので、ウィンドウを取得できたら、あとは特定できるのであれば詳しく書く必要はない。上記の例では、以下でもOK.

Get-UiaWindow -Class 'CalcFrame' | Get-UiaButton -Name 'クリア'

なお、Idというだけあって、通常はAutomationIdを使うのがおすすめ。上記の例では適切なものがついていないが、WPFアプリケーションならx:NameやNameに設定したものがついている。

UI要素をコントロールする

上記のGet-~で得られたオブジェクトを、コントロールするコマンドレットにパイプで渡せばコントロールすることができる。例えば、ボタンをクリックするなら以下の通りになる。

$button | Invoke-UiaButtonClick

コントロールするコマンドレットは自分で探すしかないが、基本的にSet-UiaやInvoke-Uiaがついているので、補完でだいたい探せる。例えば「Invoke-UiaWindowPattern -PatternName Close」や「Set-UiaTextBoxText」などである。

見つからないときは「Get-Command *uia*」とすれば一覧がでてくるので、これから探す。なお、テキストを取得するGet-UiaTextTextなど、取得する系のコマンドレットもあるので、テストなんかには活用されたし。

上記を組みわせていけば自動操作するスクリプトができる。

もっと詳しく

ほとんど上記で行えているが、歯抜けではあるがドキュメントがあるので、こちらも参考にできる。

また、このライブラリはSystem.Windows.Automationのラッパーでしかないので、.NETのUI オートメーションの情報、例えばMSDNのドキュメント(UI オートメーションの概要)や既存の記事なども参考にできる。コマンドレットで不足があれば、.NETのAPIを得られたオブジェクトに対して実行してしまっても動作する。

なお、試してみてはいないが、Metro UIなんて記載もあるので、Windowsストアアプリも操作できそうである。興味があればぜひ。

Tips

UI自動操作は対象によってTips的なものが使うほどにでてくるが、最初から大事そうなものを挙げておく。

ウィンドウをプロセスで絞り込む

ウィンドウはプロセスを決めなくても検索できるが、自分で起動している場合など、限定できる場合は-ProcessIdで絞っておくと安全である。

Get-UIAWindow -ProcessId $proc.Id -AutomationId hogehoge

UI要素をガイドする赤枠を外す

デフォルトでは選択した要素には赤枠がつくようになっている。しかし、通常は邪魔なので以下で外すことができる。

[UIAutomation.Preferences]::Highlight = $false

タイムアウトを変更する

既定でもUI要素が出てくるまである程度待ってくれるが、足りない場合は-Secondsで延長できる。

Get-UIAWindow -AutomationId hogehoge -Seconds 60

以上、Happy UI Automation Lifeを。

2014-06-03

PSCmdletでもasync/awaitを使いたい

PowerShellコマンドレットをC#で作ること自体は簡単で、単にSystem.Management.Automationアセンブリを参照してCmdletかPSCmdletクラスを継承したクラスを含むクラスライブラリを作るだけなのだけど(このへんを参照→How to Write a Simple Cmdlet) うっかりasync/awaitしたあとにWriteObjectでオブジェクトを出力しようとしたりすると、以下のように怒られてしまう。

Get-SampleCmdlet : WriteObject メソッドと WriteError メソッドは、BeginProcessingメソッド、ProcessRecord メソッド、および EndProcessing メソッドの上書きの外側から呼び出すことはできず、同じスレッド内からだけ呼び出すことができます。コマンドレットで呼び出しが正しく作成されていることを確認するか、または Microsoft カスタマー サポート サービスにお問い合わせください。

Microsoft カスタマー サポート サービスにお問い合わせしたらこんなことも教えてくれるの?と思うところだけど、まぁこういう状況ではSynchronizationContextを提供してあげればよくね?ということでオレオレSynchronizationContextを準備して提供するようにしたのがこのAwaitablePSCmdletクラス

使い方は簡単で、PSCmdletクラスの代わりにこのAwaitablePSCmdletクラスを継承して、BeginProcessing/ProcessRecord/EndProcessingの代わりにAsyncをつけたBeginProcessingAsync/ProcessRecordAsync/EndProcessingAsyncを実装すればいいだけ。
例えばこんな感じ。


本当はStopProcessingにCancellationTokenまわりとか実装しないといけないのだろうけど、たぶんあとは微調整できるレベルだと思うので、必要に応じて。

2013-12-22

PowerShellにMSBuildLauncherで簡易GUIをつける

今日のPowerShell勉強会第1回の運営の方々、発表者の方々、そして参加者の方々お疲れ様でした。どれも面白かったり勉強になったりでよかったです。

さて、Lightning Talkで掲題の話をしたのですが、実用するには速すぎてわからなかったと思うので読み物としてわかるようにまとめたいと思います。

概要

MSBuild(.csprojなんかで使われている)はビルドには必要なんだけど、ちょっとコマンドライン叩くの面倒だよね、適当なGUIないよね。
  →MSBuild Launcherの誕生。

あれ、このGUIなら逆にビルドじゃない用途でも(スクリプト実行の用途でも)MSBuildにExec書いて使ったら便利なんじゃね?
  →MSBuild LauncherありきのMSBuildの邪道な使い方の誕生。

というお話です。

前提として、MSBuild Launcherをインストーラでインストール(関連付けオプション有効)を想定しています。

---

1. シンプルな使い方

単純に.ps1ファイルを置いて、それを実行するだけ。これでも十分に便利。一応、コマンドが長くなりすぎちゃうので、プロパティ(※1)としてpowershellコマンドを別出しにしておくのがおすすめ。

※1: パラメータのようなもの。PropertyGroup要素に好きな名前の要素を置くと、それがプロパティになる。



なお、-NonInteractiveは常に入れておくのがおすすめ。発表時も話したけども、例えば必須引数が足りなかったりすると通常のコンソールでは入力待ちで止まるけども、こいつはそんなことできないので、無言で待ちに入る。そして「進まない!」と悩む。なので-NonInteractiveで即座にエラーにする。

対応するスクリプトは好きな処理を書けばいいのだけど、一応例として。



動かすとこんな感じ。


2. プロパティを持たせる使い方

パラメータが欲しいときは、MSBuildにプロパティを持たせるとよい。そしてコマンドの中で$(Name)で使う。
ただし、Exec Command=""の中身の文字列はcmd解釈?のようなので注意。Command=をくくるのは""にして、プロパティを'$(Name)'という感じにくくるのがおすすめ。



プロパティはLTでは話さなかったけども、候補としてConditionを追加してあげると、コンボボックスの選択になるので、選択のときはそうしてあげるとよいかも。

スクリプトはこんな感じ。paramで受けてあげればいい。



動かすとこんな感じ。右側で設定できる。


3. 複数のターゲットを用意する便利な使い方

複数のやりたいことを一つの画面で行うには、もちろんスクリプトを複数用意してもよいのだけど、処理自体は少ない場合は、モジュール(.psm1)に関数を定義して、それをMSBuildから使う方式がおすすめ。

具体的には、PSCmdの中にImport-Moduleまで入れてしまって、Execの中では関数名を書く形にする。



スクリプトはこんな感じ。



動かすとこんな感じ。


4. PowerShellの便利機能を活用する

PowerShellを簡易GUIっぽくしちゃうことで有名なOut-GridViewやShow-Commandはここでも便利。
ただ1点注意があって、Execで実行するとコンソールと違ってホストが残らないので、そのままだとすぐ画面がいなくなってしまう。
なので、以下のように終了を待ってあげるのがいい。


---
今度こそぜひ活用してあげてください。

2013-12-11

PowerDbg でデバッガ操作をオートメーションする

この記事は、PowerShell Advent Calendar 2013用です。本当は、誰も触れてないPowerShell v4.0の新機能について触れる予定だったのですが、新機能以外の部分で挫折したので実用的な内容に方針転換します。

今回の話題は、WinDbgの操作をオートメーションするスクリプト、PowerDbgの使い方です。これはなにかというと、WinDbgのコンソール版、cdb.exeのフロントエンドです。PowerShellがデバッガになるというよりは、windbgをコントロールできる感じです。

インストール方法

http://powerdbg.codeplex.com/ に行き、Downloadボタンを押すと、zipが落ちてくるので開きます。その中に「Install_PowerDbg.bat」というファイルがあるので、実行するとインストールされます。

といっても、横に置いてある「PowerDbg.psm1」を「%USERPROFILE%\Documents\WindowsPowerShell\Modules\PowerDbg」にコピーするだけなので、手でやっても変わらないです(もっと言うと、好きな場所に置いて毎回Import-Module .\PowerDbg.psm1でもいいです)。

あとは、いくつかの方法のどれかでcdb.exeのあるフォルダを指定します。でも一番簡単なのは.psm1の頭、param(...)のあとに以下のように記述することなので、ここではそれだけ書きます(どうせスクリプトは更新されなさそうだし)。
$debuggerRoot = "C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x64"


使い方

「New-DbgSession」コマンドレットで、デバッガを起動できます。-command "コマンド..."で新コマンド起動、-process "プロセス名" でアタッチ、-dump "dumpファイルのパス" でダンプ読み込みです。

PS> New-DbgSession -command "C:\Program Files (x86)\MsbuildLauncher\MsbuildLauncher.exe"

そして、「Invoke-DbgCommand」でデバッガコマンドを送れます。
PS> Invoke-DbgCommand 'bp $exentry'

「g」は「Send-DbgGo」というコマンドレットが用意されているので使えます。
PS> Send-DbgGo
Send-DbgGoすると、止まるまでブロックします。Ctrl+Cで抜けることも可能。

終わりたいときは、「Exit-DbgSession」で終われます。
PS> Exit-DbgSession

.loadby sos clrみたいに拡張をロードしたいときは、Load-DbgExtensionが使えます。
PS> Load-DbgExtension sos clr

Invoke-DbgCommandで何を送るかが肝で、PowerShell固有の操作はほとんどありません。(引数名がcamelCaseなのがいけてない感じですがスルーでおねがいします…)

実用例1: 異常終了したときに.NETのスタックトレース等を表示するスクリプト

前にやった「WinDbgとSOS拡張でVSを使わずに.NETアプリをデバッグ - 異常終了時の調査」を自動でやれます。



実行するとこんな感じ。
PS> > .\PrintExceptionSecondChance.ps1 -Command "C:\temp\MsbuildLauncher-0.1.1\MsbuildLauncher\MsbuildLauncher.exe"
ModLoad: 000007fe`f7200000 000007fe`f7b60000   C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
ntdll!ZwMapViewOfSection+0xa:
00000000`7755153a c3              ret
(1340.8e8): Unknown exception - code 04242420 (first chance)
ModLoad: 000007fe`f5a60000 000007fe`f5b8e000   C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clrjit.dll
ntdll!ZwMapViewOfSection+0xa:
00000000`7755153a c3              ret
(1340.8e8): C++ EH exception - code e06d7363 (first chance)
(1340.8e8): C++ EH exception - code e06d7363 (first chance)
(1340.8e8): C++ EH exception - code e06d7363 (first chance)
(1340.8e8): C++ EH exception - code e06d7363 (first chance)
(1340.8e8): C++ EH exception - code e06d7363 (first chance)
(1340.8e8): CLR exception - code e0434352 (first chance)     ←ここでエディタパスをおかしくして「Edit」ボタンをクリック
(1340.16c0): Unknown exception - code 000006ba (first chance)
(1340.8e8): CLR exception - code e0434352 (first chance)
(1340.8e8): CLR exception - code e0434352 (!!! second chance !!!)
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\system32\KERNELBASE.dll -
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\Microsoft.NET\Framework64\v4.0.3
0319\clr.dll -
KERNELBASE!RaiseException+0x3d:
000007fe`fd6a940d 4881c4c8000000  add     rsp,0C8h
PDB symbol for clr.dll not loaded
Exception object: 0000000002b0eeb8
Exception type:   System.ComponentModel.Win32Exception
Message:          指定されたファイルが見つかりません。
InnerException:   
StackTrace (generated):
(略)


実用例2: .NETなプログラムのプロセスのダンプファイルを読み取ってスタックトレースを出力するスクリプト

実はいつもやってる「clr.dllが読み込まれるまで進める」という処理が必要なければ、New-DbgSessionは-sosというオプションでsos.dllを自動で読んでくれるのです。それを利用すると、これだけ簡潔にできます。




どうでしょうか?今回の例はとくにPowerShellらしさはないですが、特定のブレイクポイントを一気に仕掛けるとか、結果に応じてどうこうするなんて用途には便利かと思います。

蛇足

WDK 8.1についてるWinDbgでSOS拡張をロードして、SOS系のコマンドを打つと、一発目がExceptionになって、二発目以降に成功するようになる。8.0ではそんなことはないので、WDK 8.0についてるWinDbgがおすすめ。






2013-10-14

PSWFWeb (PowerShell Workflow WebConsole) v0.0.1 をリリース

PSWFWeb (PowerShell Workflow WebConsole) version 0.0.1 をリリースしました。こんな感じで、ブラウザからPSWorkflowを走らせたり状態が見られたりするソフトです。


ダウンロードはこちらから。使い方は、READMEを見てください。


途中で挫折しかけてお蔵入りになるところだったのですが、スクリーンショットで反響を頂いたので、仕様を絞って最低限試せるレベルまで作ってみました。とはいえ自分でもあまり試せていないレベルなので、アグレッシブなことをすると、なにが起きるかはわかりません :)

ただでさえPowerShellはユーザーが多くないと思われるのに、PowerShell Workflowを使う人はさらに少ないと思いますが、ピンと来た人はぜひ使ってみてください。


造りとしては、Nancy という.NET向けのSinatraライクなフレームワークを使っています。これは主に、自身でWindowsサービスとなってWebをホストする(self-hosting)ためです。動作ユーザーを好きにさせるためと、将来(があれば)たぶん状態を持ってポーリングとかするだろうなと思ってWindowsサービスにするためにしています。

実際使ってみると、ASP.NET MVC (空)よりずっと依存関係が少なく、Sinatraの感覚がわかる人にはけっこうおすすめで使える感じです。内蔵のSuper Simple View Engineは、かなーりシンプルなのでRazorを持ってきたほうがいいかもしれませんが。

画面のほうは、PureというYahooが出しているWebフレームワークを使ってみました。Emailというレイアウトサンプルをほぼそのままの感じです。

ちなみに、Runtimeをhostingするわけではなく、Get-Job等々をPowerShellで呼び出しています。ほんとに*-Jobのwrapperというかんじ。

2013-06-19

PowerShell での Hyper-V Snapshot操作のまとめ

ポイント(=罠)がいくつかあるのでまとめた。

Snapshotの一覧を得る

Get-VMSnapshot -VMName [machineName]
唯一なにも考えなくてもいいのがこれ。

Snapshotを作成する

Checkpoint-VM -Name [machineName] -SnapshotName [snapshotName]
Checkpointを見つけるのは無理ゲーすぎる。しかもここはマシン名が-Nameなので注意。このverbいらなくない?

Snapshotに戻す (ロールバックする)

Restore-VMSnapshot -VMName [machineName] -Name (SnapshotName) -Confirm:$true
確認されたくないときは-Confirm:$falseをつける。

Snapshotを削除する

Remove-VMSnapshot -VMName [machineName] -Name [snapshotName] -IncludeAllChildSnapshots
-IncludeAllChildSnapshots がポイント。

Get-VMSnapshotしたものをRemove-VMSnapshotで受けたりもできるが、この4つの名前がわかればget-helpで戦えると思う。

2013-02-09

PowerShellを使ってインストールされたファイルのパスからプロダクトを調べる

rpm -qf /usr/bin/hogehogeや、dpkg -S bin/hogehogeなどとしてファイルパスからどのパッケージに入っているのか調べることありますよね?そのWindows版です。

(残念ながら入れないといけないのですが)Windows Installer PowerShell Moduleをインストールして、トップページに書いてある通り、PowerShellから以下のように打てばOKです。
PS C:\> Import-Module msi
PS C:\> Get-MSIComponentInfo | where { $_.path -eq "C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramew
ork\v4.0\System.dll" }  | Get-MSIProductInfo

ProductCode                            ProductVersion      ProductName
-----------                            --------------      -----------
{CFEF48A8-BFB8-3EAC-8BA5-DE4F8AA267CE} 4.0.30319           Microsoft .NET Framework 4 Multi-Targeting Pack

Windows Installer (msi) で入っていないものには使えないですが、参考になるかもしれません。

2012-12-15

PowerShellでApacheのログを集計する

この記事はPowerShell Advent Calendar 2012向けです。PowerShellはまだまだマスターには遠いですが、ありがたみがわかってきたので、書くことにしました。昨日の牟田口さんの記事を見るとAdd-TypeもPSObjectも使わずにPowerShellでクラスを作ってみろと言われているような気がしますが、誘惑を振り切りもともと想定していたネタです。

今日は、PowerShellを使ってApacheのログを集計してみます。Windowsを使っている人だったら、「bashとsedともげもげで十分じゃん」「Perl使うよ」と言わず、いいところがあるので見てください。特にログ集計でshもLLもわずらわしくなって、SQLiteやPostgreSQLにワンタイムのDBを作ってSQLでログ集計したことがある人におすすめです。

メリット

そもそもなにがうれしいの?という部分ですが、PowerShellには「パイプラインをオブジェクトが流れる」という特徴があり、これを利用するとshやPerlやRubyでの処理と比較して以下のようなメリットがあります。
  • (shで処理する場合と比較して)パーサがパイプにテキストではなく、オブジェクト群を流せるので、再利用が容易。きちんとパイプに流れてきたオブジェクトを処理するコマンド(コマンドレット)が用意されている。空白区切りテキストを見て、「うーん、User-Agent何番目かな…1, 2, 3...」と数えなくてよい。
  • (Perl等で処理する場合と比較して)sedやawk, sort等々をつなげて作っていくような考え方で、パイプに流して絞り込んでいく形で処理を記述できるので、試行錯誤で試していくのがラク。
要は、いいとこ取りというわけです。具体的にはこれから見ていきます。

対象ログ

今回対象とするのは、"combined"として定義されているタイプのログです。こんな感じです。


ひとつ正規表現を変えるだけなので、ちょっと違ってても大きな問題はないです。

パーサ(モジュール)の準備

まずは、ログのパーサを準備します。とはいっても、テキストにマッチしてオブジェクトをばんばん返す(流す)関数を書くだけです。
以下のスクリプトを(Win7/Vistaの場合)「C:\Users\(ユーザ名)\Documents\WindowsPowerShell\Modules\ApacheLogParser\ApacheLogParser.psm1」に配置します。



ここはあまりPowerShellっぽくないので、ポイントだけ書きます。
  • 関数名は動詞-名詞にする。使える動詞は「Approved Verbs for Windows PowerShell Commands」を参照。
  • 正規表現のマッチ結果である$matches ハッシュテーブルを活用する。
  • PowerShellの文字列の特殊文字のエスケープは`。 例: `"。
  • 正規表現では、名前つきキャプチャを活用する。 例: (?<Host>.*?)。こうすると、$matchesに指定した名前で入ってくる。
  • パイプラインには、ハッシュをPSObjectにして流す。その際は、「New-Object」の-Propertyを使うと便利。

returnを書いているので、値を返すにはそうしないといけなそうに見えますが、このケースは別に書かなくても平気です。どういうことかというと、値を返すコマンドレットを使ったら、|Out-Nullとか、>$null とかして捨てないと、どんどん「流れ」ていってしまうのです。shでコマンドを実行したらstdout/stderrに出力が出るようなものです。不要なものは捨てるようにします。

Pathを受けとるのではなく、ログ本文を受け取るようにして、この関数自体をパイプで使うのが「ぽい」ような気もしますが、物理ファイルを指定するケースが多そうなのでとりあえずこの形にしました。需要に応じて変更するのがいいと思います。Perlの<>や、RubyのARGFみたいにstdin or 引数のファイルからのInputというのが簡単にできるといいのですが。

作業開始前の準備

使う段になったら、powershellを起動後、「Import-Module」で先程のモジュールをロードします。
PS C:\temp> Import-Module ApacheLogParser

試しに、「Read-ApacheLog」にログを食べさせてみましょう。そのまま出すと出すぎるので、「Select-Object(エイリアス:select)」の-Lastオプションで数を減らしたものが以下です。


各行の「 : 」の左側に書かれたプロパティ名を使って、さまざまな操作をしていくことになります。


実践

先程のパーサを使って、具体的に集計をしてみます。

トータルのアクセス数

もっともシンプルなものとして、全てのアクセス数を数えてみましょう。ここでは、いろいろ数えてくれる「Measure-Object(エイリアス: measure)」を使います。

PS C:\temp> Read-ApacheLog .\access.log | measure


Count    : 281
Average  :
Sum      :
Maximum  :
Minimum  :
Property :
281アクセスあることがわかりました(テスト用に作ったので少なくてごめんなさい)

特定のパスのみのアクセス数

先の「Measure-Object」に加えて、与えた条件を満たすもののみを流す「Where-Object(エイリアス:where、?)」を使います。

PS C:\temp> Read-ApacheLog .\access.log | where { $_.Path -eq "/favicon.ico" } | measure


Count    : 2
Average  :
Sum      :
Maximum  :
Minimum  :
Property :
"/favicon.ico"へのアクセスは2回あるようです。

ステータス別アクセス数

ステータスコードで分類してみましょう。今度は指定したプロパティが同じ値を持つオブジェクトをグループ化する「Group-Object(エイリアス:group)」と、表示順を整えるために「Sort-Object(エイリアス:sort)」を使います。

PS C:\temp> Read-ApacheLog .\access.log | group Status | sort Name

Count Name                      Group
----- ----                      -----
  259 200                       {@{Time=2012/12/14 0:09:47; Host=127.0.0.1; Request=OPTIONS * HTTP/1.0; TimeString=1...
    1 301                       {@{Time=2012/12/14 0:17:35; Host=10.0.2.2; Request=GET /mediawiki/index.php HTTP/1.1...
    1 302                       {@{Time=2012/12/14 0:17:44; Host=10.0.2.2; Request=POST /mediawiki/index.php?title=M...
   11 304                       {@{Time=2012/12/14 0:17:41; Host=10.0.2.2; Request=GET /mediawiki/skins/common/share...
    9 404                       {@{Time=2012/12/14 0:14:26; Host=10.0.2.2; Request=GET /favicon.ico HTTP/1.1; TimeSt...
200が多いですが、3xxや4xxもちらほらあることがわかります。

トップディレクトリごとのアクセス数

今度はトップディレクトリごとのアクセス数を集計してみましょう。「Group-Object」に置換を行うブロックを渡します(パスのファイル名部分と、サブディレクトリ以下をカットします)。

PS C:\temp> Read-ApacheLog .\access.log | group { $_.Path -replace '[^/]*$','' -replace '^(/.+?)/.*','$1' } | sort -Descending Count

Count Name                      Group
----- ----                      -----
  207 /pukiwiki                 {@{Time=2012/12/14 0:23:12; Host=127.0.0.1; Request=GET /pukiwiki/ HTTP/1.1; TimeStr...
   58 /mediawiki                {@{Time=2012/12/14 0:14:30; Host=10.0.2.2; Request=GET /mediawiki/ HTTP/1.1; TimeStr...
   11                           {@{Time=2012/12/14 0:09:47; Host=127.0.0.1; Request=OPTIONS * HTTP/1.0; TimeString=1...
    4 /                         {@{Time=2012/12/14 0:14:26; Host=10.0.2.2; Request=GET / HTTP/1.1; TimeString=14/Dec...
    1 /trac                     {@{Time=2012/12/14 0:17:51; Host=10.0.2.2; Request=GET /trac/ HTTP/1.1; TimeString=1...

"/pukiwiki"にたくさんアクセスされているようです。

時間帯別アクセス数

パーサスクリプトの中で、時刻はSystem.DateTime型に変換しておいたので「Hour」プロパティが使えます。今までと同じように「Group-Object」を使えばOKです。

PS C:\temp> Read-ApacheLog .\access.log | group { $_.Time.Hour }

Count Name                      Group
----- ----                      -----
  227 0                         {@{Time=2012/12/14 0:09:47; Host=127.0.0.1; Request=OPTIONS * HTTP/1.0; TimeString=1...
   54 18                        {@{Time=2012/12/14 18:30:05; Host=127.0.0.1; Request=GET /pukiwiki/index.php?InterWi...

例がとてもよくないのですが、0時台にたくさんアクセスがあり、18時台に少しアクセスがあるようです。他の時間帯には使われていません。

日付/時間帯別アクセス数をCSV出力する

CSV出力することも可能です。「Export-Csv」コマンドレットを使います。
(日付+時間を作るには、若干トリッキーですがDateTime型の「Date」プロパティで日付部をもらい、「AddHours」メソッドを呼び出して時間を付加しています。)

PS C:\temp> Read-ApacheLog .\access.log | group { $_.Time.Date.AddHours($_.Time.Hour) } | select Name,Count | Export-Csv output.csv -Encoding default


平均レスポンスバイト数を調べる

combinedフォーマットには「%b:レスポンスのバイト数」があるので、レスポンスのサイズを集計することも可能です。「Where-Object」「Measure-Object」を使って、/pukiwiki以下の返答の平均バイト数を出してみましょう。

PS C:\temp> Read-ApacheLog .\access.log | where { $_.Path -match "/pukiwiki" } | measure -Property BytesSent -Average


Count    : 207
Average  : 3019.21256038647
Sum      :
Maximum  :
Minimum  :
Property : BytesSent

このようにひとつのログ形式に対して、パーサを1回作ってあげれば、あとは直感的に集計処理が可能になります。量的にどこまで耐えられるかは検証していないのですが、GUIな表計算ソフトでうんぬんするよりはさっくりできると思います。もちろんApacheのログでなくて、テキストのログは同じように処理することが可能ですので、しょっちゅうログを処理するような方は、試してみてはいかがでしょうか。

2012-06-12

PowerShellをMSBuildから呼び出す

MSBuildは、MSの世界のAntのようなもの。そのMSBuildから、PowerShellのスクリプトブロックを呼び出す方法。

きれいな方法を目指すのであれば、PowerShell MSBuild Taskというものがあり、そのTaskを呼び出してPowerShellを実行すればよいのであるが、入っているdllが32bitであった。

32bit版MSBuildから呼べばいい話なのだと思うが、他のツールとの関連が面倒なので、本エントリでは安直にpowershellコマンドを呼ぶ方法を紹介する。

単純に書けば簡単で、Execタスクを利用して、powershell -Command "& { }" を呼び出せばよい(この表記については、PowerShell.exe コンソールのヘルプを参照。)

ただし、生で記述するとごちゃごちゃするので、MSBuildのPropertyGroupの中に、コマンドの始まりと終わりの文字列をPSBegin, PSEndなどとして定義し、ExecのCommand属性で$(PSBegin) $(PSEnd)などのように使うことで、ちょっときれいに書ける。
 
 例は以下のとおり。


上述のように、Invoke-Commandを使ってのリモートの呼び出しを塊にしておけば、リモート呼び出しもさっくり書ける。(リモートからPowerShellを呼ぶ環境を作るのが難しいのはまた別の話。)

参考:http://blog.brianhartsock.com/2009/10/20/using-powershell-scripts-from-msbuild-scheduled-tasks-etc/


まったく余談だが、勝手な要素をどんどん作っていくのがキモチワルイ...


2012-06-11

PowerShellとWPFでシンプルGUIプログラミング

ずっとWindowsでお手軽にGUIを作る方法を探していたが、ついに見つけてしまったかもしれない。

欲しかったのは、こんな感じのことができるもの:
  • なるべく追加ソフトウェアをインストールせずに使えるもの
  • 複雑なレイアウトは別にできなくてもよい
  • UIはなるべくシンプルに書ける
  • プロジェクトなどを必要とせず、1ファイルのスクリプトで表現できる
どのようにするかというと、PowerShellスクリプト上にXAMLをインラインで記述し、WPFのUIを作ればよい。

以下、ボタンを押すとWebサービスを呼び出して、テキストボックスに東京の天気を表示する例である。


PowerShellの仕組み上、.NETでできることはできるだろうとわかっていたので、Windows Formsを使えばいけそうな気はしていたが、コードがごちゃごちゃしそうでやめていた。
しかしWPFという発想はなかった。

なお、「なるべく追加ソフトウェアをインストールせずに使えるもの」という条件を外せば、ShowUIというモジュールがあり、よりPowerShell的に記述できる(らしい)。しかし使う人みんな入れないといけないので、ちょっと微妙だと思っている。

また、蛇足であるが、PowerGUIというエディタが便利。一度走らせると、ダイナミックな言語にも関わらず、ちゃんとVisual Studio的な補完が可能である。

参考:
http://stackoverflow.com/questions/5829787/powershell-wpf-from-xml-findname-problem
http://thepowershellguy.com/blogs/posh/archive/2009/05/15/powershell-v2-get-weather-function-using-a-web-service.aspx
http://www.dougfinke.com/blog/index.php/2011/07/24/extending-powershell-to-the-gui-with-showui/