2011年12月31日土曜日

◆Write-Hostを使ってみる

言わずと知れたホストに出力するコマンドレット。
簡単に使えて、何の変哲もないといえば無いのだが、実はなんとなく良く分からないコマンド。

コンソールに色つきで出力したい、なんて時はこのコマンドの出番なのだが意外と色々戸惑うことも有ったので少し纏めておく。

PS>Write-Host A
A
PS>Write-Host A B
A B

いままで特に不思議に思わず普通に使ってきたのだが、2つめのコマンドはなぜこんな結果になるのだろう。

通常、「コマンドレット A  B」という形式は位置パラメータの1番目に”A” 、2番目に”B”と解釈されるものと思う。
なので、位置パレメータを1つしか持たない「Write-Host」はエラーになるのが普通ではなかろうか。

実際、「Write-Warning」でためしてみると、予想通りのエラーになる。
image

パラメータの型が<Object>で位置パラメータが1つ、と言ったあたりに何か理由があるのかとも思って調べてみたが結局良く判らなかった。

Trace-Commandで見てみると、パラメータがArrayListとして解釈されているっぽい感じではあるが、それがなぜかまでは判らない。
image

すべてのコマンドを検証したわけではないが、このような動作をするのは他に「Read-Host」くらいしか見つけられなかった。
とりあえず現時点ではそういうものだと思うことにしよう。(^^;

次に、連想配列を「Write-Host」に渡した時の動作を見てみる。

PS>$array = @{a=1;b=2;c=3}
PS>$array

Name                           Value                                     
----                           -----                                     
a                              1                                         
b                              2                                         
c                              3                                         


PS>Write-Host $array
System.Collections.DictionaryEntry System.Collections.DictionaryEntry System.Collections.DictionaryEntry

そのまま出力すれば内容が表示されるのだが、こいつに色を指定しましょうなどと思って「Write-Host」を使うと残念な結果になる。

まずはこの時、「Write-Host」がどのような動作をしているのかを確認してみる。
「DictionaryEntry」が3つ出力されているので中身をループしながらToStringって感じだろうか。

PS>$array.GetEnumerator() | %{$_.ToString()}
System.Collections.DictionaryEntry
System.Collections.DictionaryEntry
System.Collections.DictionaryEntry

内容的には合っているが表示形式がちょっと違う。

こうか。

PS>$array.GetEnumerator() -join " "
System.Collections.DictionaryEntry System.Collections.DictionaryEntry Syst
em.Collections.DictionaryEntry

実際にはこんな感じの条件がついているのかな。

PS>if($array -is [System.collections.IEnumerable]) {$array.GetEnumerator() -join " "}
System.Collections.DictionaryEntry System.Collections.DictionaryEntry Syst
em.Collections.DictionaryEntry

さて、実はここからが本題で、これまでの話とはそれほど関係ないのだが、以下の記事でハッシュテーブルに色をつけて表示するにはどうすればよいかという話が有った。
Using Write-Host to display a hashtable
全体に色をつければ良いようなのでOut-Stringして結果をWrite-Hostするだけの事なのだが通常は特定の行に色を付けたいよなと思って考え始めたところWrite-Hostの仕様がいまひとつ判らなかったという事で前段の調査になった。

以前似たようなことをやった事があるPowerShell: ◆特定のプロセスに色をつけて表示するのだが、若干うまく行かないケースがあるので少しだけ違った方法でやってみた。

今回も今ひとつスマートな方法は思いつかず、以下のような若干ベタな感じ。

001
002
003
004
005
006
007
008

$array = @{a=1;b=2;c=3}
$array.GetEnumerator() | %
{ 
     
Write-host "key`tvalue`r---`t-----"
  }{
     
$para =
 @{}
     
if($_.key -eq "b"){$para.Add("foreground","red"
)}
     
Write-Host $_.key `t $_.value @para
  }

image

やっぱり素直にOut-Stringでやったほうが良さそうかな。

001
002
003
004
005
006

$array = @{a=1;b=2;c=3}
$array | Out-String -Stream | %
{
 
$para =
 @{}
 
if($_ -match '^b\b'){$para.Add("foreground","red"
)}
 
Write-Host $_ @para
}

image

本当はToStringをオーバーライドしたりするとスマートかなと思って書き始めたのだが、そちらはうまく行かなかった。

2011年12月29日木曜日

◆正規表現の基礎<アトミックゼロ幅アサーション>

<アトミックゼロ幅アサーション>

なんか難しい言葉ですが、要はパターンがどのような場所で一致するかを表す指定のようです。


^

文字列の先頭。複数行オプションが有効の場合は行の先頭。

PS>"abc`ndef" -match '^ab'
True
PS>"abc`ndef" -match '^de'
False
PS>"abc`ndef" -match '(?m:^de)'
True


$

文字列の末尾。複数行オプションが有効の場合は行の末尾。

PS>"abc`ndef" -match 'ef$'
True
PS>"abc`ndef" -match 'bc$'
False
PS>"abc`ndef" -match '(?m:bc$)'
True


\A

文字列の先頭でマッチ。 

PS>"abc`ndef" -match '\Aab'
True
PS>"abc`ndef" -match '(?m:\Ade)'
False

 


\Z

文字列の末尾または末尾の改行の前でマッチ。

PS>"abc`ndef" -match 'ef\Z'
True
PS>"abc`ndef" -match '(?m:bc\Z)'
False

 


\z

文字列の末尾でマッチ。

PS>"abcde`n" -match 'de\Z'
True
PS>"abcde`n" -match 'de\z'
False
PS>"abcde" -match 'de\z'
True


\G

先頭から最後まで指定したパターンの連続を表す指定のようです。
マッチしない箇所が出てきたらそこで終わり。
「Match」クラスなどを使って連続してパターンを検索するのに使用する。
以下はMSDNに載っていたサンプルをPowerShellに変換したものです。

連続マッチのサンプル
  1. $str = "capybara,squirrel,chipmunk,porcupin,gopher," +
  2.          "beaver,groundhog,hamster,guinea pig,gerbil," +
  3.          "chinchilla,prairie dog,mouse,rat"
  4. $pattern = '(\G\w+\s?\w*),?'
  5. $match = [Regex]::Match($str, $pattern)
  6. while ($match.Success)
  7. {
  8.     $match.Groups[1].Value
  9.     $match = $match.NextMatch()
  10. }

0または1個のカンマで区切られたワード文字(間にスペースが1つ有っても良い)を検索するパターン。ループしてマッチするものをすべて見つける。
image

途中でマッチしない箇所があるとそこで終了します。

  1. $str = "capybara,squirrel,chipmunk,porcupin,-gopher," +

image

\Gの指定がないときはマッチしない箇所は飛ばして最後まで継続します。


\b

ワード境界でのマッチ。

PS>"Power-Shell(V2)" -match '\bShe'
True
PS>"Power-Shell(V2)" -match '\bhe'
False
PS>"Power-Shell(V2)" -match 'hell\b'
True
PS>"Power-Shell(V2)" -match '\bhell'
False


\B

ワード境界以外でのマッチ。

PS>"Power-Shell(V2)" -match '\BShe'
False
PS>"Power-Shell(V2)" -match '\Bhe'
True

◆パイプライン入力・パラメータ入力対応のGridView出力関数を作る

PowerShell: ◆$inputの使い方PowerShell: ◆関数の作り方?で悩んでいたのですが、色々と試行錯誤の末、なんとなく解決策らしき方法を見つけました。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038

function slo {
 
[CmdletBinding()]
            
 
param
 (            
   
[parameter(ValueFromPipeline=$true)]
 
   
[object[]]$InputObject,
   
   
[parameter(Position=0)]
          
   
[string[]]$firstprop
 
  )
 
begin
  {
   
$steppablePipeline = {Out-GridView}.
GetSteppablePipeline()
   
$steppablePipeline.Begin($true
)
   
$proplist =
 @() 
  }
 
process
 { 
   
$steppablePipeline.Process((select-orderedList
)) 
  }
 
end { $steppablePipeline.
End() }
}


function select-orderedList
{
 
if($proplist.length -eq 0
) 
  {              
   
$proplist = $firstprop
 
   
$Inputobject | Get-Member -MemberType Property |
 
   
foreach
 { 
      
if($proplist -notcontains $_.
Name) 
       { 
          
$proplist += $_.
Name 
       } 
    } 
  } 
 
$InputObject | select-object -property $proplist
}

slo name -InputObject (ps)
ps  | slo name

スクリプトブロックの「GetSteppablePipeline」メソッドで「SteppablePipeline」オブジェクトを作ってあげると、あたかもパイプライン中で使用しているかのように入力オブジェクトを続けて渡せるようになるようです。
「Process」メソッドで1つずつ渡して行き、終わったら「End」メソッドを呼ぶ、といった感じのようです。

だいぶ手探りなので正解かどうかは判りませんが、とりあえずGridViewがウィルスのように湧いて出てくるのは回避できています。(^^;

ちょっと前に、PowerShell: ◆スクリプトブロックを使うを書いた時このメソッドの存在には気づいていたのですが、使い方が今ひとつ判っていませんでした。

これで今年もすっきり終われる、と思ったのですがパラメータで指定した時の結果が、こんな感じになってしまいます。
image

PS> ,(Get-Process) | Out-GridView

のように渡した状態ですね。

「SteppablePipeline」とかの使い方がおかしいのかと色々とまた悩んだのですが、何のことはない、

PS> Out-GridView  -inputobject (ps) 

とやると同じ結果になるじゃないですか・・・。

ん~、どういうことなんでしょう。バグ?

またまた、色々と調べた挙句、よくよくヘルプを見ると、
image

などときっぱり書いてある・・・。
ん~、そりゃ実質パラメータ指定には対応していないってことじゃないですかい・・・。

なんの為にこんなに苦労してきたのか(TT)

悔しいので、とりあえず「Out-File」に変更して、

  1. $steppablePipeline =
  2.     {Out-file -FilePath d:\documents\wk.txt}.GetSteppablePipeline()

こんな感じで確認したところ、一応OKっぽい感じでした。

なんとなく、今ひとつ報われない結果となりましたが、色々と助言を頂きながらパイプライン・関数・スクリプトブロックと少し理解が深まったような気がします。

2011年12月27日火曜日

◆パイプライン中の処理かどうかを判定する(挫折)

関数がパイプラインで使われているのか、どうでないのかを判定する。

調べてみると「$MyInvocation.PipelineLength」あたりを使うのが一般的みたいだ。

こんな感じ。

001
002
003
004
005
006
007
008
009
010
011
012
013

function test
{
 
param($inputobject
)
 
if($MyInvocation.PipelineLength -gt 1
)
  {
   
$input | ogv -Title パイプライン
  }else
{
   
$inputobject | ogv -Title パラメータ
  }
}


ps | 
test
test
 -inputobject (ps)

一見OKなのだが、これではうまく判定できないケースがある。
例えば、

  1. (ps) | test

または、

  1. $temp = ps
  2. $temp | test

などと指定すると、「PipelineLength」は1になってしまうのである。

果たしてこれは正しい結果なのだろうかとちょっと疑問にも思うが、なんにせよこれでは使えない。
色々調べてみたが、他に方法もなさそう。

ん~、パイプラインは難しい。


牟田口さんからヒントを頂き$inputでの判定を検証しました。
判定自体はよさそうです。
ただ、$inputはBeginブロックで判定できないのが若干使いづらいかもしれません。
(Firstブロックみたいな奴があると便利なのですが・・・)
パイプライン入力が1つだけであれば、$PSBoundParametersでその存在を確認する方法で良いかもしれません。

また、ちょっと見落としていたのですが「$myinvocation.ExpectingInput」と言う奴が使えそうな感じにも見えます。

◆関数の作り方?

PowerShell: ◆$inputの使い方の内容でまだ悩んでいる。

色々考えてみたのだが、結局のところ今回の問題は、

  1. Get-Process | select id,name | Out-GridView

こんな処理の、

  1. select id,name | Out-GridView

この部分を纏めたいというところから来ている。

単純に関数にしてもダメだし、関数を入れ子にしても結局同じ問題にぶつかるし・・・。
なんで、PowerShellとあろうものがこの程度のことができないのだろう、と暫く考えもしたのだが、そもそもやろうとしていることがPowerShellの思想にそぐわないのでは無いかと思えてきた。

出力系のコマンド(Out-xxxxやFormat-xxxx)は独立して使うべきものなのではないかと。

  1. Get-Process | select id,name | Out-GridView

これはこのまま使うべきものという方針にしよう。

2011年12月25日日曜日

◆$inputの使い方

Webを眺めていたらこんな記事が目に止まった。
Selecting Property order - Richard Siddaway's Blog

簡単に解釈すると、「Select-Object」するときに、プロパティをすべて表示させたいのだが一部のプロパティだけを先頭に持ってくるにはどうしたら良いかという話のようだ。
全項目指定すれば順番に並べることは可能だがそれは面倒。

  1. ps | select name , *

こんな指定が出来れば良いのだが、プロパティの重複でエラーになる。
そこで以下のような関数を書いてみた。という趣旨だと思う。

一部のプロパティ順を指定する関数
  1. function Select-Order {           
  2. [CmdletBinding()]           
  3. param (           
  4. [parameter(Position=0,           
  5.   ValueFromPipeline=$true)]           
  6.   $InputObject,            
  7.             [string[]]$firstprop
  8. )           
  9.           
  10. PROCESS {           
  11.             $proplist = $firstprop
  12.           
  13. $Inputobject | Get-Member -MemberType Property |           
  14. foreach {           
  15. if ($firstprop -notcontains $_.Name){           
  16.   $proplist += $_.Name           
  17. }           
  18. }           
  19.           
  20.             $InputObject | select -Property $proplist
  21. }}

確かにキー項目のようなものだけ先頭にしたいときはあるなぁと思い、プロファイルに入れることにした。
その際、リスト形式で表示されては見づらいのでGridViewに出力するよう変更しておこうと深く考えずに20行目を以下のように変更し、

  1. $InputObject | select -Property $proplist | ogv

これを実行したらとんでもない目にあった。(1行1GridでGridがウィルスのように湧いて出てきた)

  1. ps | Select-Order name

という訳で、纏めてGridVew出力に変更したのが以下のサンプルである。

Select結果を纏めてGridViewに
  1. function slo {           
  2. [CmdletBinding()]           
  3. param (           
  4.    [parameter(ValueFromPipeline=$true)]           
  5.     $InputObject,            
  6.    [parameter(Position=0)]
  7.                [string[]]$firstprop
  8. )           
  9.           
  10. BEGIN{$out = @()}
  11. PROCESS {           
  12.                $proplist = $firstprop
  13.               
  14.    $Inputobject | Get-Member -MemberType Property |           
  15.    foreach {           
  16.      if ($firstprop -notcontains $_.Name){           
  17.       $proplist += $_.Name           
  18.      }           
  19.    }           
  20.             $out += $InputObject | select -Property $proplist
  21. }
  22. END{
  23.    $out | ogv
  24. }
  25. }
  26.  
  27. ps | slo name

これにて一件落着という所ではあるのだが、なんとなくすっきりしない。

そもそも、このPROCESSブロックは必要なのだろうか。
最初にやったように、単純にOut-GridViewにパイプするととんでもないことになるのは、このPROCESSブロックのせいである。
Out-GridViewを使わないにしても入力パイプライン1件ずつに対してSelectするのは無駄ではないだろうか。
出力にについても、本来以下のように1件ずつ出力されているのがマージされて結果的に纏まって表示されているに過ぎない。
image

そこで、PROCESSブロックを使わずに$input自動変数で入力を纏めて取ってくるよう変更し比較してみようと思う。

$inputを使ったサンプル
  1. function slo {           
  2. [CmdletBinding()]           
  3. param (           
  4.    [parameter(ValueFromPipeline=$true)]           
  5.     $InputObject,            
  6.    [parameter(Position=0)]
  7.                [string[]]$firstprop
  8. )           
  9.           
  10.             $proplist = $firstprop
  11.             
  12. $input | Get-Member -MemberType Property |           
  13. foreach {           
  14.    if ($firstprop -notcontains $_.Name){           
  15.     $proplist += $_.Name           
  16.    }           
  17. }
  18.      $input | select -Property $proplist | ogv
  19. }
  20.  
  21. ps | slo name

こんな感じで良いだろうと思ったのだが、これではうまくいかない。
12行目の$inputは参照できるのだが、不思議なことに18行目では$inputが参照できないのである。(何も返ってこない)

実は$inputは「ArrayListEnumeratorSimple」という型になっていて入力パイプラインのコレクションに対する「列挙」なのである。
なので、一度最後まで読みだすとそのままではもう一度読みだすことが出来ない。
そこで、18行目の前に、

  1. $input.Reset()

を追加してあげる必要があるのだ。

最後に、実際両者の性能はどうなるのか比較してみた。(Grid出力ではなく単純なSelectで比較)

PROCESSブロックを使ったサンプル
  1. function slo {           
  2. [CmdletBinding()]           
  3.     param (           
  4.         [parameter(ValueFromPipeline=$true)]           
  5.        $InputObject,            
  6.         [parameter(Position=0)]
  7.                     [string[]]$firstprop
  8.     )           
  9.           
  10.     PROCESS {           
  11.                     $proplist = $firstprop
  12.                    
  13.         $Inputobject | Get-Member -MemberType Property |           
  14.         foreach {           
  15.             if ($firstprop -notcontains $_.Name){           
  16.              $proplist += $_.Name           
  17.             }           
  18.         }           
  19.                  $InputObject | select -Property $proplist
  20.     }
  21. }
  22.  
  23. measure-command{
  24.     (ps) * 5 | slo name
  25. }

image

$inputを使ったサンプル
  1. function slo {           
  2. [CmdletBinding()]           
  3.     param (           
  4.         [parameter(ValueFromPipeline=$true)]           
  5.        $InputObject,            
  6.         [parameter(Position=0)]
  7.                     [string[]]$firstprop
  8.     )           
  9.           
  10.                 $proplist = $firstprop
  11.                
  12.     $input | Get-Member -MemberType Property |           
  13.     foreach {           
  14.         if ($firstprop -notcontains $_.Name){           
  15.          $proplist += $_.Name           
  16.         }           
  17.     }
  18.     $input.Reset()
  19.          $input | select -Property $proplist
  20. }
  21.  
  22. measure-command{
  23.     (ps) * 5 | slo name
  24. }

image

結果を見ると、予想以上に$inputを使用したサンプルのほうが速いのが判る。

基本的に私はPowerShellで速度を論じるのは好きではないが、これだけの速度差があると看過できない問題に思える。


牟田口さんからコメントを頂きちょっと見直しました。(いつもありがとうございます)

まさにご指摘の通り、本質的に時間がかかっている原因はプロパティの解析を毎回行なっているところにありますね。
時間が違いすぎておかしいなとは思っていたのですが、安易にそのまま掲載してしまいました。(^^;

ご指摘内容を踏まえて、当初の目的であるグリッドビュー出力のサンプルを再掲します。

PROCESSブロックを使ったサンプル
  1. function Select-Order2
  2.             {
  3.    [CmdletBinding()]            
  4.    param (            
  5.        [parameter(ValueFromPipeline=$true)]
  6.        [object[]]$InputObject,   
  7.        [parameter(Position=0)]          
  8.        [string[]]$firstprop
  9.    )            
  10.    begin
  11.    {
  12.       $proplist = @()
  13.        $out = @()
  14.    }
  15.               
  16.    process
  17.    {
  18.        if($proplist.length -eq 0)
  19.        {              
  20.            $proplist = $firstprop
  21.            $Inputobject | Get-Member -MemberType Property |
  22.            foreach {
  23.                if($proplist -notcontains $_.Name)
  24.                {
  25.                    $proplist += $_.Name
  26.                }
  27.            }
  28.        }
  29.        $out += $InputObject | select-object -property $proplist
  30.    }        
  31.    
  32.    end
  33.    {
  34.        $out | ogv
  35.    }
  36. }
  37. measure-command{
  38.    (ps) * 5 | Select-Order2 name
  39. }
$inputを使ったサンプル
  1. function slo {             
  2. [CmdletBinding()]             
  3.    param (             
  4.        [parameter(ValueFromPipeline=$true)]             
  5.        $InputObject,              
  6.        [parameter(Position=0)]
  7.        [string[]]$firstprop
  8.    )             
  9.            
  10.    $proplist = $firstprop
  11.                 
  12.    $input | Get-Member -MemberType Property |             
  13.    foreach {             
  14.        if ($firstprop -notcontains $_.Name){             
  15.         $proplist += $_.Name             
  16.        }             
  17.    }
  18.    $input.Reset()
  19.    $input | select -Property $proplist | ogv
  20. }
  21. measure-command{
  22.    (ps) * 5 | slo name
  23. }

これで両者ほぼ速度的には似たようなものになりました。

ただし、ターンアラウンドタイムは変わらないのですが、レスポンスタイムについては$inputを使ったほうが圧倒的に速いようです。
これは、まさに牟田口さんご指摘のパイプラインの威力なのでしょう。

また、もうひとつご指摘いただいたパラメータを直接指定できなくなることにつきましては私も懸念しておりました。
パラメータ指定の時とパイプライン入力の判定をして処理を振り分けないといけなくなるのでスマートじゃないんですよね。
パイプライン入力を受け付けるにも関わらず、ValueFromPipelineなプロパティが存在しないぞとか思ったりして・・・。