2010年7月22日木曜日

◆スクリプトブロックを引数で渡す

こんな関数があったとする。

function fncHoge{
    Write-Host "Hello World"
    Write-Host "こんにちは みなさん"
    Write-Host "さようなら"
    Write-Host “終わり”
}


ここで、ある時は「さようなら」の代わりに「おやすみなさ」を表示させたい場合、引数でスイッチを渡してif文で切り分けるというのも有りだが、またあとから「こんばんは」も発生したりすると修正が必要になる。

こんな時はスクリプトブロックを引数で渡せるような関数にしておくのも一つの手かもしれない。

function fncHoge([scriptblock]$speak){
    Write-Host "Hello World"
    Write-Host "こんにちは みなさん"
    & $speak
    Write-Host “終わり”
}

fncHoge {Write-Host "さようなら"}
Write-Host
fncHoge {Write-Host "おやすみなさい"}
Write-Host
fncHoge {Write-Host "こんばんは"}


受け取ったスクリプトブロックは実行演算子(&)で実行してあげる。

結果は、こんな感じ。

PS>D:\desktop\fncHoge.ps1
Hello World
こんにちは みなさん
さようなら
終わり 

Hello World
こんにちは みなさん
おやすみなさい
終わり

Hello World
こんにちは みなさん
こんばんは
終わり


これで、最後のあいさつにフランス語が来ようがドイツ語が来ようが修正する必要のない関数が出来上がる。

◆PowershellでDBアクセス

手始めにAccessデータベースを読み込んでみる。

プロバイダーはとりあえずOLEDBを使う。
接続文字列は2007以降ではMicrosoft.ACE.OLEDB.12.0を使うようだ。
(2007より前はMicrosoft.Jet.OLEDB.4.0)

.NETで組む時とほとんど同じ。
結果はGridViewに表示させた。

param(
    [string]$dataSource  ,
    [string]$sqlCommand  ,
    [switch]${??}
)
$comment = @'
#####################################################

MRead-AccessDB.ps1
    Accessデータを読み込む。
param(
    [string]$dataSource  ,
    [string]$sqlCommand  ,
    [switch]${??}
)
例: 
    # Accesデータベースにアクセスする
    MRead-AccessDB.ps1 (Resolve-Path access_test.mdb) -Sql "Select * from Users"
#####################################################
'@
if(${??}) {$comment;return}
if(!$dataSource) {Write-Warning "Please specify a datasource." ; return}
if(!$sqlCommand) {Write-Warning "Please specify a query." ; return}

#接続文字列を準備する
$connectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=$dataSource;"

#データソースに接続し開く
$connection = New-Object System.Data.OleDb.OleDbConnection $connectionString
$command = New-Object System.Data.OleDb.OleDbCommand $sqlCommand,$connection
$connection.Open()

#結果をフェッチし、接続を閉じる
$adapter = New-Object System.Data.OleDb.OleDbDataAdapter $command
$dataset = New-Object System.Data.DataSet
[void]$adapter.Fill($dataset)
$connection.Close()

#クエリーからすべての行を返す
$dataset.Tables | Select-Object -ExpandProperty Rows | Out-GridView

結果はこんな感じ。
20100720152357

Accessがインストールされていない場合は以下からコンポーネントをインストール出来る。

2010年7月20日火曜日

◆$a = $b と $a | set b の違い

PowerShell: ◆XMLファイルを扱うで表題の違いに気付かず戸惑ったが原因が判ったのでメモしておく。
PS>$b = @(1)
PS>$a = $b
PS>$a | set c
PS>$a.ToString()
System.Object[]
PS>$c.ToString()
1

この例をみると明らかに両者が等価でないことが判る。
もともとObject配列だったものがパイプラインを通ることによりInt型に変化している。
配列の個数が1個か複数かで結果の型が変わる例である。
この様に、$a = $b は明らかに等価な代入であるがパイプラインを通した場合は中身が展開されるので必ずしも等価とは限らない。
ちなみに、$aの前にカンマを付けて
,$a | set c
とすると同じ結果を得ることができる。
PowerShell: ◆XMLファイルを扱うの例でいくと、
$xml.SelectNodes($query) | Set-Variable node
とした場合、$xml.SelectNodes($query)はSystem.Xml.XPathNodeList型なのだが、その要素が一つなためnode変数はSystem.Xml.XmlElement型となる。
なので、そのプロパティである”#text”が参照できた。
では、$xml.SelectNodes($query)の方はどうやって参照するかというと
PS>,$xml.SelectNodes($query) | gm
   TypeName: System.Xml.XPathNodeList
Name          MemberType            Definition
----          ----------            ----------
ToString      CodeMethod            static string XmlNodeList(psobject instance)
Equals        Method                bool Equals(System.Object obj)
GetEnumerator Method                System.Collections.IEnumerator GetEnumerator()
GetHashCode   Method                int GetHashCode()
GetType       Method                type GetType()
Item          Method                System.Xml.XmlNode Item(int index)
ItemOf        ParameterizedProperty System.Xml.XmlNode ItemOf(int i) {get;}
Count         Property              System.Int32 Count {get;}

これで判るように、ItemもしくはItemOfを使えばよさそうだ。
PS>$xml.SelectNodes($query).itemof(0)."#text"
佐藤
PS>$xml.SelectNodes($query).item(0)."#text"
佐藤

2010年7月15日木曜日

◆FilterとPassThruの使い方

以前書いたPowerShell: ◆サブフォルダーの容量を求めると同じことをMSDNのサンプルでやっていたので比較してみる。
PowerShell で which、PowerShell で du ... | 今週の How-To | MSDN

function dusage{
param([String]$tgtDir = (pwd).Path)

filter dusage_filter{
$sum =
(dir -literalpath $_.FullName –r -force -ea silentlycontinue |
measure-object Length -sum).Sum
$result = New-Object Object |
Add-Member NoteProperty Folder $_.FullName -PassThru |
Add-Member NoteProperty Size $sum -PassThru
return $result
}

dir $tgtDir -force -erroraction silentlycontinue |
? {$_.PSIsContainer} | dusage_filter
}

dusage -tgtDir "c:\work" | sort -des size



再帰しながらファイルを読み込みMeasure-ObjectでLengthをサマリーするというのは基本的に同じ。
違いとしては、ファイルを読み込むときForceスイッチを指定して隠しファイルも取得できるようにしている。
目的を考えると確かにそのほうが良いだろう。
また、出力用のObjectを自前で作りAdd-Memberでプロパティを追加している。
まぁ、これはどちらでも良い気がする。

ここで参考にしたいのがAdd-Memberに指定しているPassThruパラメータの使い方。Helpを読んでも今一つ使い道が分かっていなかったが、この例をみるとよくわかる。
Add-Memberは入力パイプラインから入ってきたObjectにプロパティを追加しているのだが、本来こいつは出力を持たないので、PassThruスイッチを指定して、プロパティ追加後のObjectを次のパイプラインへと送っている。

もう一つ参考にしたいのがFilterの使い方。
いままでFilterってFunctionと同じ?と思っていたのだが、若干違うみたい。
Where-Objectで書くような処理で汎用的なものをFilterとして作っておけば便利なのかな。


PassThruについては、Start-ProcessのようにそのままではProcessオブジェクトを返さないが、PassThruスイッチを付けることにより開始したProcessオブジェクトを返してくれるようなコマンドもある。

◆Staticなメンバーを取得する

コンピュータ名を確認するにはhostname.exeを使えば簡単だが、Environmentクラスなんてのも使える。

[Environment]::MachineName

EnvironmentクラスにどんなStaticメンバーがあるのか確認するには、

PS>[Environment] | gm -static -MemberType Property

◆Webページをダウンロードする

WEBページをダウンロードし、文字列解析を行って様々な情報を得たい。
といった事はよくある。
そこでWEBページをダウンロードしてみる。
Bitsではうまくいかない(静的なページであればうまくいくのかもしれないが)ので、WebClientクラスを使う。

$source = "http://www.yahoo.co.jp"
$destination = "d:\desktop\test.html"

$wc = New-Object System.Net.WebClient
$wc.DownloadFile($source,$destination)


ダウンロードしてきたHTMLを表示した結果は、こんな感じ。
20100715111508

いつものYahooのページと若干異なっていることが判る。
これは、Webサイトがアクセスしてきたブラウザに応じて返すHTMLを変えているためであろう。
そこで、WebClientにヘッダー情報を付加してIEに偽装してみる。

$source = "http://www.yahoo.co.jp"
$destination = "d:\desktop\test.html"
$userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2;)"

$wc = New-Object System.Net.WebClient
$wc.Headers.Add("user-agent",$userAgent)
$wc.DownloadFile($source,$destination)


結果はこんな感じ、
20100715112023

いつもの表示になった。


ちなみに、非同期でダウンロードするには、DownloadFileの代わりにDownloadFileAsync、文字列としてダウンロードするには、DownloadStringを使えばよさそうだ。

2010年7月10日土曜日

◆Bits転送3

これまでSMBでの転送を試してきたが、今度はHTTPを試してみる。
ドライバーをダウンロードしてするには、こんな感じ。

Start-BitsTransfer http://www.epson.jp/download/printer/driver/win/ink/cl752as.exe   .\


試してみるとSMBと特に違いはないみたい。
若干違いがあるとすればTransfertypeパラメータ。
SMBの時にはUploadを指定してもDownloadを指定しても動作に違いは見られなかったが(気づかないだけで何かあるかもしれないが)、httpのときはさすがにUploadではエラーになった。
Uploadはサーバーを見つけないとテストできないので、またの機会に。

2010年7月8日木曜日

◆Bits転送2

PowerShell: ◆Bits転送(Start-Bitstransfer)では同期転送を試したが、今度は非同期転送を試してみる。
Start-BitstransferコマンドレットにAsynchronousスイッチをつけると非同期となりコマンドは即座にリターンする。
Get-BitstransferコマンドレットでJOBの進捗状況が判る。Transferredになれば終了だ。

ところが、終了はするのだが肝心の転送ファイルがローカルに見当たらない。
探してみると拡張子がTMPの隠しファイルとして存在していることが判った。
どうやらTransferredになったらComplete-Bitstransferコマンドレットを実行してやる必要があるみたい。

Get-BitsTransfer | ?{$_.JobState -eq "Transferred"} | Complete-BitsTransfer


転送中の中途半端なファイルを人目にさらさないという配慮なのだろう。

Complete-Bitstransferを実行するとTMPファイルがリネームされ隠しファイル属性が削除される。
また、Complete-BitstransferによりGet-Bitstransferで表示されていたJOB自体も削除される。

Complete-Bitstransferを実行する前にRemove-Bitstransferを実行するとTMPファイルもJOBも削除される。

実行中のJOB(Transferring)に対してSuspend-Bitstransferでサスペンド、サスペンドJOB(Suspended)に対してResume-Bitstransferで転送を再開することができる。

Start-BitstransferにはPriorityパラメータがあり、転送JOBのプライオリティを指定できる。
プライオリティは、Foreground、High、Normal、Lowの4種類。
High、Normal、Lowはバックグラウンドだ。

また、Foregroundジョブは並列に実行され、Backgroundジョブは優先度に応じた順番で一つずつ直列に実行される。

ローカルでDVD再生しながら試していたところ、やはりForegroundで複数の転送をしていると再生が途切れ途切れになった。
途中でPriorityを変更するには、

Get-Bitstransfer  |  Set-Bitstransfer  -Priority  Normal
とすると良い。

最後にもうひとつ、
一つのJOBで複数のファイルを指定することもできる。
後から追加するにはAdd-BitsFileコマンドを使う。

$job = Start-Bitstransfer  a.txt,b.txt  c:\,c:\  -Suspend
Add-BitsFile  -BitsJob $job  -Source c.txt  -Destination  c:\
Resume-Bitstransfer  -BitsJob  $job

2010年7月7日水曜日

◆Bits転送(Start-Bitstransfer)

PowershellでBits転送してみる。
Bits転送には同期転送と非同期転送があり、同期転送は非常に簡単だ。
Import-ModuleでBits用のコマンドレットを読み込んだ後、Start-BitsTransferで転送することができる。
プロトコルはSMBとHTTPが使えるようだが、HTTPはサーバーを選ぶようなのでとりあえずSMBで試す。

Import-Module Bitstransfer
Start-BitsTransfer -Source \\Test-Server\Downloads\Silverlight.exe -Destination .\


TransferTypeパラメータには「download」と「upload」があり、デフォルトは「download」だが「upload」を指定しても動作に違いは見られなかった。(HTTPでは違いがあるのかもしれない)
また、PriorityパラメータはForegroundとBackgroundの2種類。BackgroundはHigh、Normal、Lowの3種類に分かれる。
デフォルトはForegroundだ。

Bits関係のコマンドレットは他にもいくつかあるが、それらは非同期転送用だ。(と思う)

2010年7月4日日曜日

◆XMLファイルを扱う

スクリプトの設定ファイルをXMLでというのはよくある。
こんな設定ファイルがあったとすると、

<config>
    <param1>1111</param1>
    <param2>2222</param2>
    <param3>3333</param3>
</config>

その読み込みは、

PS>$xml = [xml](Get-Content d:\desktop\config.xml)
PS>$xml.config.param2
2222

XMLタイプにキャストして参照するだけ。非常に簡単だ。

もうすこしXMLらしいサンプルを。

<Address>
    <Person type="Personal">
        <Name>佐藤</Name>
        <Phone>111-1111</Phone>
    </Person>
    <Person type="Business">
        <Name>吉田</Name>
        <Phone>333-3333</Phone>
    </Person>
</Address>

 

PS>$xml = [xml](Get-Content d:\desktop\sample.xml)
PS>$xml.Address.Person  |  ft -auto

type     Name Phone
----     ---- -----
Personal 佐藤 111-1111
Business 吉田 333-3333

PS>$xml.Address.Person[0].Name
佐藤


属性でクエリーすると

PS>$query = "/Address/Person[@type='Personal']/Name"
PS>$xml.SelectNodes($query)

#text
-----
佐藤


ちなみに、このプロパティは#textなんて名前になっているので、 XXXX.#textでは参照できない。
この場合は、以下のようにすると参照できる。

PS>$xml.SelectNodes($query) | Set-Variable node
PS>$node."#text"
佐藤

ただ、不思議なことに以下のようにすると結果が返ってこない。

PS>$node = $xml.SelectNodes($query)
PS>$node."#text"
PS>


これも駄目だ。

PS>($xml.SelectNodes($query))."#text"
PS>


なぜ?
意味が分からない・・・。
とりあえず逃げ道(Set-Variable)があるので放置(笑)

<追記>
PowerShell: ◆$a = $b と $a | set b の違い
,$node | gm  とやって$node自体のメンバーを確かめてみる。

PS>,$node | gm


   TypeName: System.Xml.XPathNodeList

Name          MemberType            Definition
----          ----------            ----------
ToString      CodeMethod            static string XmlNodeList(psobject instance)
Equals        Method                bool Equals(System.Object obj)
GetEnumerator Method                System.Collections.IEnumerator GetEnumerator()
GetHashCode   Method                int GetHashCode()
GetType       Method                type GetType()
Item          Method                System.Xml.XmlNode Item(int index)
ItemOf        ParameterizedProperty System.Xml.XmlNode ItemOf(int i) {get;}
Count         Property              System.Int32 Count {get;}

どうやらitemOfを使えばよさそうだ。

PS>$node.itemof(0)."#text"
佐藤

<追記おわり>

要素でもクエリーしてみる。

PS>$query = "/Address/Person[Name='吉田']/Phone"
PS>$xml.SelectNodes($query) | Set-Variable node
PS>$node

#text
-----
333-3333

PS>$node."#text"
333-3333

2010年7月3日土曜日

◆一時ファイルを使う

PathクラスのGetTempFilenameメソッドを使うと良いようだ。

[System.IO.Path]::GetTempFileName() | Tee  -Variable Path
"今日はブラジルが負けました"  >  $Path
notepad $path  |  Out-Null
del  $path  -Verbose

2010年7月2日金曜日

◆ループを抜ける

for($counter = 0 ; $counter -lt 4 ; $counter++){
    for($counter2 = 0 ; $counter2 -lt 4 ; $counter2++){
        if($counter2 -eq 2){break}
        Write-Host "ループ回数 $counter $counter2"
    }
}

ループ回数 0 0
ループ回数 0 1
ループ回数 1 0
ループ回数 1 1
ループ回数 2 0
ループ回数 2 1
ループ回数 3 0
ループ回数 3 1

外側のループを抜けるにはラベルを指定する。

:outer for($counter = 0 ; $counter -lt 4 ; $counter++){
    for($counter2 = 0 ; $counter2 -lt 4 ; $counter2++){
        if($counter2 -eq 2){break outer}
        Write-Host "ループ回数 Item $counter $counter2"
    }
}

ループ回数 Item 0 0
ループ回数 Item 0 1

continueを使うとこんな感じ

for($counter = 0 ; $counter -lt 4 ; $counter++){
    for($counter2 = 0 ; $counter2 -lt 4 ; $counter2++){
        if($counter2 -eq 2){continue}
        Write-Host "ループ回数 $counter $counter2"
    }
}

ループ回数 0 0
ループ回数 0 1
ループ回数 0 3
ループ回数 1 0
ループ回数 1 1
ループ回数 1 3
ループ回数 2 0
ループ回数 2 1
ループ回数 2 3
ループ回数 3 0
ループ回数 3 1
ループ回数 3 3


:outer for($counter = 0 ; $counter -lt 4 ; $counter++){
    for($counter2 = 0 ; $counter2 -lt 4 ; $counter2++){
        if($counter2 -eq 2){continue outer}
        Write-Host "ループ回数 Item $counter $counter2"
    }
}

ループ回数 Item 0 0
ループ回数 Item 0 1
ループ回数 Item 1 0
ループ回数 Item 1 1
ループ回数 Item 2 0
ループ回数 Item 2 1
ループ回数 Item 3 0
ループ回数 Item 3 1

2010年7月1日木曜日

◆パス関連(Convert-Path,Resolve-Path,Split-Path,Join-Path)

Convert-PathとResolve-Pathは似たようなコマンド。
PS>Convert-Path .\
D:\temp
PS>Resolve-Path .\ 
Path
----
D:\temp

これを見るとどちらもPathを解決してくれているが、Convert-Pathは文字列、Resolve-PathはPathInfoオブジェクトが返ってきている。

ワイルドカードも使える。
PS>cvpa *.tmp
D:\temp\CR_5BC5.tmp
D:\temp\GUR7F5C.tmp
D:\temp\GUR99ED.tmp
PS>rvpa *.tmp 
Path
----
D:\temp\CR_5BC5.tmp
D:\temp\GUR7F5C.tmp
D:\temp\GUR99ED.tmp


違いが出るのはこんな時、
PS>Convert-Path HKLM:\software\microsoft
HKEY_LOCAL_MACHINE\software\microsoft
PS>Resolve-Path HKLM:\software\microsoft 
Path
----
HKLM:\software\microsoft

Convert-Pathはドライブ表記から通常の表記に変換してくれる。

Split-Pathはパスを様々な場所で分割して取り出してくれる。
PS>rvpa *.tmp | Split-Path -parent
D:\temp
D:\temp
D:\temp
PS>rvpa *.tmp | Split-Path -leaf
CR_5BC5.tmp
GUR7F5C.tmp
GUR99ED.tmp
PS>rvpa *.tmp | Split-Path -Qualifier
D:
D:
D:
PS>rvpa *.tmp | Split-Path -NoQualifier
\temp\CR_5BC5.tmp
\temp\GUR7F5C.tmp
\temp\GUR99ED.tmp

parentがデフォルトのようだ。

Join-Pathはパスの連結。
PS>Join-Path  -Path  c:\windows\  -ChildPath  \system32
c:\windows\system32
PS>Join-Path  -Path  c:\windows\  -ChildPath  \system99
c:\windows\system99

\記号が重複してもよきに計らってくれる。

Resolveパラメータを付けると実存チェックをしてくれる。
PS>Join-Path  -Path  c:\windows\  -ChildPath  \system32 -Resolve
C:\windows\system32
PS>Join-Path  -Path  c:\windows\  -ChildPath  \system99 -Resolve
Join-Path : パス 'C:\windows\system99' が存在しないため検出できません。
発生場所 行:1 文字:10
+ Join-Path <<<<   -Path  c:\windows\  -ChildPath  \system99 -Resolve
    + CategoryInfo          : ObjectNotFound: (C:\windows\system99:String)
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.Jo


単純に実存チェックをするときにはTest-Pathを使う。
PS>Test-Path c:\windows\system32
True
PS>Test-Path c:\windows\system99
False

◆VisualStudioで作ったフォームを使う

込み入ったフォームを使うときは、やはりVisualStudioのデザイナーで作りたい。
そこで、VisualStudioで作ったフォームをPowershellから使ってみる。
$path = "D:\Documents\Visual Studio 2010\Projects\WindowsFormsApplication2\WindowsFormsApplication2"
$source1 = "Form1.cs"
$source2 = "Form1.Designer.cs"
$source = gc (Join-Path $path $source1)
$source += gc (Join-Path $path $source2)
Add-Type -TypeDefinition ($source | out-string) -ReferencedAssemblies System.Windows.Forms,
                                                                      System.Drawing,
                                                                      System.Data,
                                                                      System.Core
$form = New-Object WindowsFormsApplication1.Form1
$form.button1.text = "test"
$form.button1.Add_Click({$form.textBox1.text = "Hello World"})
[Windows.Forms.Application]::Run($form)

VisualStudioのフォームはパーシャルクラスになっているので2つのソースを連結して読み込んでいる。
それ以外は、これまでと違うところは特に無い。
Powershell側で参照するコントロールについては、予めVisualStudioにてPublicスコープに変更しておく必要がある。(上記の場合はbutton1とtextbox1)