2010年6月30日水曜日

◆ファイルオープンダイアログを使う

ファイルオープンダイアログを使うときは、Powershell –sta として、シングルスレッドアパートメントモードでPowershellを起動する必要がある。
なぜか、Powershell  .\XXX.ps1  -sta  では駄目で、Powershell  -sta  .\XXX.ps1  とする必要がある。

Add-Type -AssemblyName System.Windows.Forms
$dl = New-Object System.Windows.Forms.OpenFileDialog
$dl.Title = "ファイルの選択"
$dl.InitialDirectory = $home
$ret = $dl.ShowDialog()
if($ret -eq "OK"){
    "[{0}]が選択されました" -f $dl.FileName
}

2010年6月29日火曜日

◆switchステートメント

構文はこんな感じ、(ごく普通)
PS>$text = "bbb"
PS>
PS>switch($text){
>>     aaa{"aaaが見つかりました";break}
>>     bbb{"bbbが見つかりました";break}
>>     ccc{"cccが見つかりました";break}
>>     {$_.length -eq 3}{"3桁の文字が見つかりました";break}
>>     default{"不明な文字が見つかりました"}
>> }
>>
bbbが見つかりました
ワイルドカードも使える。
PS>$text = "bbb"
PS>
PS>switch -wildcard ($text){
>>     aaa{"aaaが見つかりました";break}
>>     b*{"bbbが見つかりました";break}
>>     ccc{"cccが見つかりました";break}
>>     default{"不明な文字が見つかりました"}
>> }
>>
bbbが見つかりました

正規表現も使える。
PS>$text = "bbb"
PS>
PS>switch -regex ($text){
>>     aaa{"aaaが見つかりました";break}
>>     "bbb$"{"bbbが見つかりました";break}
>>     ccc{"cccが見つかりました";break}
>>     default{"不明な文字が見つかりました"}
>> }
>>
bbbが見つかりました
ファイルの中身を直接判定することもできる。
次の様な内容のファイルがあったとすると

bbb
ccc
ddd
aaaa

PS>
PS>switch -file test.txt{
>>     aaa{"aaaが見つかりました";break}
>>     bbb{"bbbが見つかりました";break}
>>     ccc{"cccが見つかりました";break}
>>     {$_.length -eq 3}{"3桁の文字が見つかりました";break}
>>     default{"不明な文字が見つかりました"}
>> }
>>
bbbが見つかりました

あれ、1行目しかヒットしない。
どうやら、breakはswitchステートメント全体をbreakしてしまうようだ。
ファイルの全行を判定するには、
PS>
PS>switch -file test.txt{
>>     aaa{"aaaが見つかりました";continue}
>>     bbb{"bbbが見つかりました";continue}
>>     ccc{"cccが見つかりました";continue}
>>     {$_.length -eq 3}{"3桁の文字が見つかりました";continue}
>>     default{"不明な文字が見つかりました"}
>> }
>>
bbbが見つかりました
cccが見つかりました
3桁の文字が見つかりました
不明な文字が見つかりました

breakとcontinueの使い分けについては、通常のループ処理と同じ感じ。
switchステートメント2

2010年6月28日月曜日

◆オブジェクトにプロパティを追加する(Add-Member)

オブジェクトにプロパティを追加するには、Add-Memberコマンドレットを使う。

サンプルとして、FileオブジェクトにOwnerプロパティを追加してみる。

function Get-ChildItemAddedOwnerProp
{
    $file = Get-ChildItem
    $file | %{
      Add-Member -MemberType NoteProperty -Name Owner -Value $_.GetAccessControl().owner -InputObject $_
    }

    $file
}

Get-ChildItemAddedOwnerProp | select name,owner

結果はこんな感じ、
20100628195213

Add-Memberの代わりに集計プロパティを使っても同様の事ができる。

$file |
  select * ,@{name="Owner";expression={$_.GetAccessControl().Owner}} |
  Set-Variable file

$file | select Name,Owner

2010年6月26日土曜日

◆Get-ChildItemにフォルダーのLength表示を追加する

Dirコマンドでは、ファイルのLengthは表示されるが、フォルダーのLengthは表示されない。
20100626012819

そこで、PowerShell: ◆関数を作るで作った関数を修正し、フォルダーのLengthも表示するDirコマンド(関数)を作ってみた。

function dir3(
    [string]$Path = ".\",
    [switch]${??}){

$comment = @'
#####################################################

dirコマンドにフォルダーのサイズを追加して表示する

param(
    [string]$Path = ".\",
    [switch]${??}
)

例:
    #
    dir3  "C:\Windows"

#####################################################
'@

    Set-PSDebug -Strict

    if(${??}) {$comment;return}

    Get-ChildItem -Path $Path |
        select mode,LastWriteTime,@{
             name="length";expression={
                 if($_.PsIsContainer){
                    ($_ | Get-ChildItem -Recurse -ea silentlycontinue |
                     measure -Sum length).Sum
                  }else{
                    $_.length
                  }
             }
         },name |
        sort @{expression="mode";descending=$true},
             @{expression="name";descending=$false}
    Set-PSDebug -Off
}

あまり変わったところはないが、PsIsContainerプロパティでフォルダーかどうか判定しフォルダーに対してのみlengthを追加している。
(ファイルの場合はそのままlengthを返している)

Sortコマンドでは、昇順と降順の組み合わせを連想配列形式で指定している。

結果はこんな感じ。
20100626015546 
Selectすると表示書式が若干変わってしまう(クラスが変わるので仕方がない)。
また、Cドライブ直下などでやると若干時間がかかる。(だから標準ではlength表示しないのだろう)

結果でちょっと気になるのがPerfLogsフォルダ。
lengthが表示されていない。
中身を見てみるとファイルが存在していないためNullとなっている。
こんな時はif(SumがNullだったら)とかやりたくなるが以下のようにすると簡単に0表示できる。

                     measure -Sum length).Sum  +  0


ん~、Null + 0 は 0 なんですね。
Powershellっておもしろい。

◆関数を作る

PowerShell: ◆サブフォルダーの容量を求めるで作ったスクリプトを関数にしてみた。

function dir2(
    [string]$Path = ".\",
    [switch]${??}){

$comment = @'
#####################################################

指定されたフォルダーのサブフォルダーのディスク容量を表示する
(FunctionLib.ps1)

param(
    [string]$Path = ".\",
    [switch]${??}
)

例:
    #
    MGet-FolderSizes.ps1 -Path "C:\Windows"

#####################################################
'@

    Set-PSDebug -Strict

    if(${??}) {$comment;return}

    Get-ChildItem -Path $Path | ?{$_.PsIsContainer} |
        select name,@{
             name="Size(MB)";expression={
                "{0,10:n2}" -f
                (($_ | Get-ChildItem -Recurse -ea silentlycontinue |
                 measure -Sum length).Sum / 1mb)
             }
         } |
        sort "Size(MB)" -des
    Set-PSDebug -Off
}

ほとんどそのままだ。
使うときは、$profileに書いておくかドットソースで読み込む。

.  FunctionLib.ps1
dir2

◆サブフォルダーの容量を求める

PowerShell: ◆スクリプトファイルを作るで決めた形式でサンプルを一つ作ってみた。

フォルダーの容量を求めたいことはよくあるので、指定されたフォルダーにあるサブフォルダーの容量を求める。

param(
    [string]$Path = ".\",
    [switch]${??}
)

$comment = @'
#####################################################

指定されたフォルダーのサブフォルダーのディスク容量を表示する
  (MGet-FolderSizes.ps1)

param(
    [string]$Path = ".\",
    [switch]${??}
)

例:
    #
    MGet-FolderSizes.ps1 -Path "C:\Windows"

#####################################################
'@

Set-PSDebug -Strict

if(${??}) {$comment;return}

Get-ChildItem -Path $Path | ?{$_.PsIsContainer} |
    select name,@{
         name="Size(MB)";expression={
            "{0,10:n2}" -f
            (($_ | Get-ChildItem -Recurse -ea silentlycontinue |
             measure -Sum length).Sum / 1mb)
         }
     } |
    sort “Size(MB)” -des
Set-PSDebug –Off


指定されたパスの子供を取得。
PsIsContainerプロパティでフォルダーだけを抽出。
NameとSizeプロパティをSelect
Sizeプロパティはもともと存在しないので自分で求める。
  配下のファイルをRecurse指定で全てとってくる。(アクセスできないファイルは無視)
  Measure-Objectコマンドレットでlengthプロパティをサマリ。
  余計なプロパティもくっついてくるのでSumプロパティを使用。
  1mbで割ってMB表示に。
  フォーマット演算子でフォーマット設定。
結果をSizeプロパティで降順にソート

2010年6月25日金曜日

◆スクリプトファイルを作る

汎用的な処理はスクリプトファイルにしておきたい。(関数でも良いが)
適当に作っていくと後でわけがわからなくなるので、ある程度書式を決めておく。

一応こんなひな形を考えてみた、

param(
    [string]$Path = ".\",
    [switch]${??}
)

$comment = @'
#####################################################

指定されたフォルダーのサブフォルダーのディスク容量を表示する
  (MGet-FolderSizes.ps1)

param(
    [string]$Path = "C:\",
    [switch]${??}
)

例:
    #
    MGet-FolderSizes.ps1 -Path "C:\Windows"

#####################################################
'@

Set-PSDebug -Strict

if(${??}) {$comment;return}


パラメータの受け取り方は何種類かあるが、Paramステートメントを使うことにする。(これは先頭に書く必要があるようだ)
次に、簡単な機能とパラメータ及び使用例をコメントとして入れておく。

Set-PSDebug –Strict

これは未設定変数の参照をエラーとする定義。タイプミスを早めに見つけられるので入れておきたい。

if(${??}) {$comment;return}


これはヘルプ表示用。
コマンドレット  -?  でヘルプ表示されるのをまねている。
$?としたいところだが、これは既に自動変数で使われているので$??としてみた。
コマンドレット -?? でもヒットしてしまうが実害はないだろう。
ただし、?は変数に使えない文字なので{}で囲んでいる。
ここでは単純にコメントをそのまま出力している。

もっと正当な書き方があるのかもしれないがこれでも十分だろう。

2010年6月24日木曜日

◆例外処理

PowerShell: ◆ヘルプをメモ帳で表示する でCatchできないエラーがあることが分かった。
調べてみるとエラーには「終了するエラー」と「続行するエラー」があり、Catchできるのは「終了するエラー」だけのようだ。
(エラーの詳細は、about_errorsを参照しろとヘルプにあるが、このトピックは存在しなかった)

そこで、ErrorActionパラメータに「Stop」を指定してCatchできるようにする。

Function gh{

    $path = Join-Path  $env:temp  PSHelp.txt
    try{
        Invoke-Expression "Get-Help $args  -ErrorAction Stop" | Out-File $path -Width 61
    }
    catch{ throw $error[0] }
    notepad $path
}


ErrorActionの規定値はErrorActionPreference自動変数で管理されていて、この初期値がContinueになっている。
よって、この値をStopにして、個別に続行させたいコマンドに対してErrorActionでContinueを指定するのも良いかもしれない。

Function gh{
    $ErrorActionPreference = "Stop"
    $path = Join-Path  $env:temp  PSHelp.txt
    try{
        Invoke-Expression "Get-Help $args "  | Out-File $path -Width 61
    }
    catch{ throw $error[0] }
    notepad $path

}


また、trapステートメントを使って以下のような書き方もできる。

Function gh{
    trap{ throw $error[0] }
    $ErrorActionPreference = "Stop"
    $path = Join-Path  $env:temp  PSHelp.txt
    Invoke-Expression "Get-Help $args "  | Out-File $path -Width 61
    notepad $path
}

$error[0]の代わりに $_  を指定しても良い。

Function gh{
    trap{ throw $_ }
    $ErrorActionPreference = "Stop"
    $path = Join-Path  $env:temp  PSHelp.txt
    Invoke-Expression "Get-Help $args "  | Out-File $path -Width 61
    notepad $path
}


ちなみに、Trapステートメントはどこに書いても有効なようだ(同一スコープ内で)。
VBのOn Error Goto 0 (だっけ?)のように途中で無効にする方法は見つけられなかった。
まぁ、Try~Catch~Finally(Version2から)があるので問題ないだろう。

◆ヘルプをメモ帳で表示する

Get-Help  dir  -Full  |  more

別画面に表示させたほうが便利なこともある。
Get-Help  dir  -Full  |  Out-String  –Stream  |  Out-GridView

通常のコマンドレットはこれでOKだが、about系のHELPはうまく表示されない。(スクロールされない)

メモ帳に出力したほうが良いかも。

Function gh{

    $path = Join-Path  $env:temp  PSHelp.txt
    try{
        Invoke-Expression "Get-Help $args" | Out-File $path -Width 61
    }
    catch{ throw $error[0] }
    notepad $path

}


上記のInvoke-Expressionは、

Get-Help $args  |  Out-File $path -Width 61


でも良さそうだが、こう書いてしまうと
gh  dir  -Full

と入力されたときに、
Get-Help  (dir  -Full)

という感じに解釈されてしまうようだ。
そこで、Get-Expression コマンドを使って、純粋に文字列として命令を実行させている。

また、不正な引数でエラーとなった時にメモ帳が開かないようにtry~catchを使っている。
自分でthrowした場合は、そこで止まってくれるようだ。

Invoke-Expression  “aaa” –ErrorAction  Stop ; notepad

これではメモ帳が開くのを抑止できない。(Invoke-Expression がStopするだけなのかな)

あれ、 gh  dir  -f  はCatchしてくれるけど、gh  hoge はCatchしてくれないなぁ。

ん~。

2010年6月23日水曜日

◆配列関連

PS>Get-Variable arr* | Remove-Variable
PS>
PS>#配列を作る
PS>$arr = @(1,2,3)
PS>$arr.count
3
PS>Write-Host $arr
1 2 3
PS># @( )は省略してもよい
PS>$arr = 1,2,3
PS>Write-Host $arr
1 2 3
PS>#範囲演算子を使った指定
PS>$arr = 1..4
PS>Write-Host $arr
1 2 3 4
PS>#空の配列
PS>$arr = @()
PS>$arr.Count
0
PS>
PS>#型指定した配列を作る
PS>[int[]]$arr = 1,2,3
PS>#これはエラー
PS>$arr += "a"
値 "a" を型 "System.Int32" に変換できません。エラー: "入力文字列の形式
発生場所 行:1 文字:5
+ $arr <<<<  += "a"
    + CategoryInfo          : MetadataError: (:) []、ArgumentTransfor
    + FullyQualifiedErrorId : RuntimeException

PS># 特定サイズの配列を作る
PS>Remove-Variable arr
PS>$arr = New-Object object[] 3
PS>$arr.Count
3
PS>
PS>#配列の加算
PS>$arr1 = 1..3 ; $arr2 = 4..6 ; Write-Host ($arr1 + $arr2)
1 2 3 4 5 6
PS>Write-Host (1..3+4..6)
1 2 3 4 5 6
PS>
PS>#多次元配列
PS>$arrMulti = New-Object "int[,]" 2,3
PS>$arrMulti.Count
6
PS>
PS>#多段階(ジャグ)配列
PS>$arrJag = @((New-Object int[] 2) , (New-Object int[] 3))
PS>$arrJag[1][2] = 5
PS>Write-Host $arrJag
0 0 0 0 5
PS>
PS>#配列要素の削除
PS>$arr = 1,2,3
PS>[Array]::Clear($arr,0,1)
PS>Write-Host $arr
2 3
PS>#配列要素の順番を調べる
PS>[Array]::IndexOf($arr,3)
2
PS>#配列をソートする
PS>$arr = 3,4,2,1,5
PS>[Array]::Sort($arr)
PS>Write-Host $arr
1 2 3 4 5
PS>
PS>#配列要素に対する演算
PS>$a = 1,2,3
PS>$a[2] -= 1
PS>$a
1
2
2
PS>#配列要素の削除
PS>$a = 1,2,3
PS>$a[1] = $null
PS>$a
1
3

PS>#連想配列のソート
PS>$hashtbl = @{k1 = 3;k2 = 1; k3=2}
PS>$hashtbl.GetEnumerator() | sort value

Name                           Value                                     
----                           -----                                     
k2                             1                                         
k3                             2                                         
k1                             3                                         

PS>#配列要素を逆順に
PS>$a = 1,2,3
PS>[array]::Reverse($a)
PS>$a
3
2
1
 

PS>#配列要素の削除
PS>$a = 1,2,3,4
PS>$array = [Collections.ArrayList]$a
PS>$array.RemoveAt(2)
PS>$a = $array.ToArray()
PS>$a.Length
3

2010年6月21日月曜日

◆グリッドビューへの表示(Out-GridView)

Get-Process  |  Get-Member

などとすると、結果の表示が画面サイズで切られてしまうことがよくある。
20100621100541

すべてを表示させるためにわざわざ画面サイズを広げるのも面倒。

こんな時は、Out-GridViewコマンドを使うとGUIなGridViewに表示してくれるので結果の確認が容易になる。

Get-Process  |  Get-Member  |  Out-Gridview

20100621101025

◆Pauseの実装

PowershellにはDosのPauseに相当するコマンドが実装されていない。
同等の機能を関数で実装してみる。
PS>Read-Host  続行するには何かキーを押してください . . .
続行するには何かキーを押してください . . .:

これでも、ほぼ使い物になるとは思うのだが、プロンプト末尾のコロン「 : 」が邪魔なのと、Enterキー以外ではReturnしない。
そこで、ConsoleクラスのReadKeyメソッドを使ってみる。
function Pause
{
    Write-Host "続行するには何かキーを押してください . . ." -NoNewLine
    [Console]::ReadKey($false) | Out-Null
    Write-Host
}

20100621112058
よさそうだ。

<追記>
これも使えそうだ。
$host.UI.RawUI.ReadKey()

<追記>
[Console]::ReadKey($false)のパラメータは$trueのほうが良さそうだ。(ヘルプに騙された)

改良版PowerShell: ◆Pauseの実装2

◆スコープ

Powershellのスコープは若干判りづらい。(気がする)

以下のスクリプトを見て、3つのWrite-Hostがそれぞれ何を出力するか判るだろうか。

function testFunc($a){
    Write-host $a
    $a +=1
    Write-host $a
}

$a = 123
testFunc $a
Write-Host $a


結果は、
123
124
123

testFuncは子スコープになっているので、そこでの変数の更新は親スコープに反映されない。
(参照は可能なところがやっかい。子スコープにコピーされるイメージなのだろうか)

外出しの関数であれば戻り値で受け渡すのが良いと思うが、同一スクリプト内であれば直接更新したいこともあるだろう。
さしあたって引数で渡してみる。

function testFunc($a){
    Write-host $a
    $a +=1
    Write-host $a
}

$a = 123
testFunc $a
Write-Host $a


っが、結果は変わらない。きっと値渡しなのだろう。
参照渡しはっと、

function testFunc([ref]$a){
    Write-host $a.value
    $a.value +=1
    Write-host $a.value
}

$a = 123
testFunc ([ref]$a)
Write-Host $a


こんな感じっぽい。
[ref]をつけるだけなら良いが、やけに面倒。
こんなことなら戻り値もらったほうが・・・。

別の方法を調べていると、どうやら変数にスコープをつけてやれば良いようだ。

function testFunc{
    Write-host $a
    $script:a +=1
    Write-host $a
}

$a = 123
testFunc
Write-Host $a


このスコープには以下の種類がある。
global(全体に有効)
script(スクリプト内で有効)
local(現在のスコープとその子スコープで有効)
private(現在のスコープでのみ有効)

globalとscriptの仕様を確認するために以下のようなスクリプトを作ってみた。

#funcScriptGlobalACounuUP.ps1
function test{
    $global:a += 1
    $script:a += 1
}

$script:a = 1
test
$script:a

これを実行するとこんな感じ、

PS>$a = 2
PS>funcScriptGlobalACounuUP.ps1
2
PS>$a
3

これは予想通りの結果。

しかし、このスクリプトをコマンドラインに張り付けて実行(もしくはISEで開いて実行)するとこんな結果になる。

PS>function test{
>>     $global:a += 1
>>     $script:a += 1
>> }
>>
PS>$script:a = 1
PS>test
PS>$script:a
3


先ほどは2と表示された$script:aの値が3と表示されている。

どうやらスクリプトスコープが存在しない環境では$script:aも$global:aも同じものとして扱われるようだ。

このように、スコープとは相対的なもののようなので、その使用に当たっては注意が必要だろう。
特に、ISEでスクリプトを書いて、その場でテストしてOKでも本番環境では違った結果になりうるので必ず本番環境でのテストが必要。

PowerShell: ◆スコープ2

2010年6月20日日曜日

◆プロバイダ

Powershellでは以下のようなファイルシステムへのアクセスと同様な構文で様々な情報にアクセスできる。

Get-ChildItem  D:\PS

レジストリーに対しては

Get-ChildItem HKCU:\software\microsoft\windows

環境変数にたいしては
Get-ChildItem  env:

関数の一覧も
Get-ChildItem  function:

変数にたいしても

PS>$a = "AAA"
PS>gci variable:a

Name                           Value
----                           -----
a                              AAA


Get-Contentで中身も取得できたりする(ものもある)
Get-Content  function:help

20100620205447

PS>Get-Content  env:temp
E:\TEMP


Get-Content  D:\PS\test.txt

中身の取得は$を使った変数構文も使える。

PS>$env:temp
E:\TEMP
PS>$function:help

<#
.FORWARDHELPTARGETNAME Get-Help
.FORWARDHELPCATEGORY Cmdlet
#>
[CmdletBinding(DefaultParameterSetName='AllUsersView')]
param(
    [Parameter(Position=0, ValueFromPipelineByPropertyName=$true)]
    [System.String]
    ${Name},
    ・
    ・

D:\PS\text.txt は変数に使えない文字を含んでいるため { } で囲んで、

${D:\PS\test.txt}

で、中身が取得できる。

◆パイプ処理の排他を回避する

以下のようなコマンドで、テキストファイルの文字列置換を行おうとすると、

Get-Content test2.csv | %{$_ -replace "2010/06","2010/07"} | Set-Content test2.csv

こんなエラーメッセージが出る。

Set-Content : 別のプロセスで使用されているため、プロセスはファイル 'D:\test2.csv' にアクセスできません。
発生場所 行:1 文字:61
+ gc test2.csv | %{$_ -replace "2010/06/16","2011/06/16"} | sc <<<<  test2.csv
    + CategoryInfo          : NotSpecified: (:) [Set-Content]、IOException
    + FullyQualifiedErrorId : System.IO.IOException,Microsoft.PowerShell.Commands.SetContentCommand

テキストを1行ずつ読み取り、順次パイプラインに流し込むためだろう。
Get-ContentはReadCountパラメータがあって、ゼロを指定するとすべてを一度に読み込むとある。
これを使えばOKかと思ったがなぜかうまくいかない。
以下のように括弧で括ってあげる必要がありそうだ。

(Get-Content test2.csv) | %{$_ -replace "2010/06","2010/07"} | Set-Content test2.csv

ファイルサイズが大きい場合は一旦一時ファイルに出力なんてことをしたほうが良いかも。

◆グループ化して最終データを生かす

以下のようなCSVデータを日付でグループ化して、値はグループごとの最後のデータを生かしたいということがあった。
日付,値
2010/06/16,11
2010/06/16,26
2010/06/16,10
2010/06/17,64
2010/06/17,25
2010/06/18,13
2010/06/18,35
2010/06/18,21
まずは、Import-csvで読み込んでみる。
PS>Import-Csv d:\test.csv
���t                                                        �l
----                                                        --
2010/06/16                                                  11
2010/06/16                                                  26
2010/06/16                                                  10
2010/06/17                                                  64
2010/06/17                                                  25
2010/06/18                                                  13
2010/06/18                                                  35
2010/06/18                                                  21

ん~、日本語が文字化けしている。(いきなりつまずく・・・・)
どうやらUNICODE前提のようだ。
Encoding指定もできないやん。(Powershellの文字コード関係はなんかいまいち)
Import-CsvはExport-Csvしたデータ専用なのかな。
仕方がないのでGet-Contentした後、Convrtする。
PS>gc d:\test.csv | ConvertFrom-Csv
日付                                                        値
----                                                        --
2010/06/16                                                  11
2010/06/16                                                  26
2010/06/16                                                  10
2010/06/17                                                  64
2010/06/17                                                  25
2010/06/18                                                  13
2010/06/18                                                  35
2010/06/18                                                  21

いい感じ。
で、これを日付でグループ化。
20100619232946
Groupプロパティは連想配列のコレクションのようなので、グループごとの先頭なら以下の通り。
PS>gc d:\test.csv | ConvertFrom-Csv | Group 日付 | %{$_.group[0]}
日付                                                        値
----                                                        --
2010/06/16                                                  11
2010/06/17                                                  64
2010/06/18                                                  13

最終は、
gc d:\test.csv | ConvertFrom-Csv | Group 日付 | %{$_.group[-1]}

と思いきや、何も返ってこない。
どうも[-1]で最後が返ってくるのは配列の場合で、コレクションでは実装されていないようだ。
そこで、コレクションを配列に突っ込んでから[-1]で最終データを参照する。
(普通にコレクションのカウントプロパティでも良いとは思うが)

PS>gc d:\test.csv | ConvertFrom-Csv | Group 日付 | %{@($_.group)[-1]}
日付                                                        値
----                                                        --
2010/06/16                                                  10
2010/06/17                                                  25
2010/06/18                                                  21

とりあえずはOKっぽい。
欲が出てきて、グループ集計もやってみたくなった。
ちょっと考えてみたが単純にはいかないかも。
変にこねくり回すより、自前でブレーク集計したほうが簡単かなぁ。
あとでやってみよ。

2010年6月19日土曜日

◆サイズの大きなファイルを抽出する

DISKを圧迫しているファイルを調べたいことはよくある。
そこで、一定サイズ以上のファイルを抽出するスクリプトを書いてみる。

Get-ChildItem C:\windows\system32 -Recurse |
    ?{$_.length -gt 10mb} |
    sort -Descending length |
    Format-Table length , fullname –AutoSize


C:\windows\system32 にはアクセスできないファイルがあるため以下のようなメッセージが出る。

20100619195912

実害はないのだが、邪魔なのでGet-ChildItemのErrorActionパラメータにSilentlyContinueを指定してこれを抑止する。

Get-ChildItem C:\windows\system32 -Recurse -ea SilentlyContinue |
    ?{$_.length -gt 10mb} |
    sort -Descending length |
    Format-Table length , fullname -AutoSize

結果はこんな感じ。

20100619200213

これで目的は達成しているのだが、見た目が今一つ。
Format-Tableは書式を連想配列形式で指定できるので、これを使って書式設定する。

$path = "C:\windows\system32"
Get-ChildItem $path -Recurse -ea SilentlyContinue |
    ?{$_.length -gt 10mb} |
    sort -Descending length |
    Format-Table @{name = "サイズ(MB)" ;
        expression={($_.length / 1mb).ToString("0.00")}} ,
        @{name = "パス($path)" ;
        expression={$_.fullname.Replace($path,"")}} –auto


結果はこう。

20100619203105

良い感じ。

◆プロンプトをカスタマイズする

Promptという名前の関数を定義するとプロンプトを表示するごとに実行してくれる。
標準ではカレントパスがプロンプト表示されているが、この機能を使って独自のプロンプト表示が可能になる。
ドットソースで動的に読み込ませても良いが、$profileに定義しておくと便利。
Function Prompt{
    #タイトルバーにパスを表示
    $host.UI.RawUI.WindowTitle = Get-Location

    #
    #入力されたコマンドにエイリアスがあれば表示する
    #

    #入力された直近のコマンドを取得  
    $lastCommand = Get-History -Count 1
    if($lastCommand){
        $suggestions = @() 
        #エイリアスが定義されているコマンドレット(関数を除く)を取得      
        $aliases = Get-Alias | ?{$_.Definition -match "\w\-\w"}
        foreach($alias in $aliases){
            #ワード境界で区切られたコマンドレットがあるか検索          
            if($lastCommand -match ("\b" + [System.Text.RegularExpressions.Regex]::Escape($alias.Definition) + "\b")){
                $suggestions += "エイリアスがあります $($alias.Definition) is $($alias.Name)"
            }
        }
        if($suggestions){
            $suggestions | Write-Host  -ForegroundColor Cyan
            Write-Host
        }
    }
    "PS>"
}

プロンプトが長いと若干鬱陶しいので「PS>」とシンプルに。
代わりに$host自動変数を使ってタイトルバーにパスを表示させている。
役立つ情報を表示させたいので、入力されたコマンドにエイリアスが定義されていたら、それを表示している。
結果はこんな感じ。
20100619192908
個別にエイリアスを調べるには
Get-Alias  -def  Get-ChildItem

◆空白を含むパスのスクリプトを実行する

"C:\Program Files\test.ps1"を実行するには実行演算子「&」を使う。

"C:\Program Files\test.ps1" の中身が以下だとすると

dir $args[0]

実行するにはこんな感じ

&  ‘C:\Program Files\test.ps1’  C:\


結果は

20100619120252

DOSのバッチファイルなどから実行する場合は、

powershell "& 'C:\Program Files\test.ps1' 'c:\'"

◆WindowsUpdateエラー対策

理由はよくわからないがWindowsUpdateがエラーになることがよくある。

C:\windows\SoftwareDistribution配下のファイルを削除するとうまくいく。
これらのファイルはWindowsUpdateが掴んでいるのでサービスを止めて削除する必要がある。
IEのキャッシュもクリアするとベター。

たびたびエラーになるのでスクリプトにしてみた。

#IEキャッシュのクリア(一時ファイル、Cookies、閲覧履歴)
RunDll32.exe InetCpl.cpl,ClearMyTracksByProcess 11 | Out-Null

$wupd = "windows update"
Stop-Service $wupd

del C:\windows\SoftwareDistribution\* -Recurse 
Start-Service $wupd

Write-Host "Windows Update 再起動完了"

キャッシュのクリアは一時ファイルだけでよいのかもしれない。
その場合は引数11の代わりに8を指定する。

本当にこの対策で良いのかは判らないが、今のところこれで100%解決している。

2010年6月18日金曜日

◆プレーンテキストから資格情報を作る

コマンドレットの中には資格情報(Credentialパラメータ)をとるものが多くある。
Get-Credentialと入力すると資格情報入力用のダイアログが表示され、そこにユーザーIDとパスワードを指定すると資格情報を返してくれる。

$cred = Get-Credential

しかし、処理の中には対話処理が許されない場合もある。
本来は上記の$credをDISKに保存しておき、実行時にそれを使うのが筋だろうが、こいつはログオンユーザーが変わると復元できない。
というわけで資格情報をプレーンテキストから作る方法も押さえておきたい。
New-Credentialなんてコマンドがあると良いのだがそんなことはなさそう。
そこで、$cred  |  Get-Member  としてみると、

   TypeName: System.Management.Automation.PSCredential
Name                 MemberType Definition
----                 ---------- ----------
Equals               Method     bool Equals(System.Object obj)
GetHashCode          Method     int GetHashCode()
GetNetworkCredential Method     System.Net.NetworkCredential GetNetworkCredential()
GetType              Method     type GetType()
ToString             Method     string ToString()
Password             Property   System.Security.SecureString Password {get;}
UserName             Property   System.String UserName {get;}

となっているので、このPSCredentialを作ってやればよい。
こいつのコンストラクタをみるとUserNameとPasswordを指定することが判る。
UserNameはStringなので問題なし。PasswordはSecureStringクラスとなっている。
幸いSecureStringクラスはConvertTo-SecureStringコマンドレットで作ることができる。

$password = ConvertTo-SecureString "PassHoge" -asplaintext -force
$cred = New-Object System.Management.Automation.PsCredential "UserHoge",$password

◆メールを送る(Send-MailMessage)

Send-MailMessage    -To "一郎<ichiro@hoge.co.jp>"                                    `
                    -From "花子 <hanako@hoge.co.jp>"                                          `
                    -Subject "Test mail"                                                                     `
                    -SmtpServer "MailServer.hoge.co.jp"                                          `
                    -Body "メール送信のテストです"                                                  `
                    -Encoding  ([System.Text.Encoding]::Default)

構文自体あまり難しいところはないが、Encodingが気に入らない。
通常のコマンドレットのEncodingパラメータは<string>となっているのに、ここでは<Encoding>となっている。
Stringで指定して自動的にキャストもしてくれない。しかもデフォルトがASCIIだと。
仕方がないのでEncodingクラスのスタティックプロパティを指定する。

コマンドプロンプトで[System.Text.Encoding]::Defaultと打つとわかるがシステムのDefaultは通常SJIS。
こんな指定が必要ならSmtpClientクラスを使うのと大差無い。

$smtpClient = new-Object Net.Mail.SmtpClient "MailServer.hoge.co.jp"
$smtpClient.Send('hanako@hoge.co.jp','ichiro@hoge.co.jp','Test Mail',’メール送信のテストです。’)

ちなみに、Powershellからのメール送信はウィルス対策ソフトにブロックされたりするので要注意。
会社などで設定変更できない場合は、Powershell.exeをコピーしてメジャーなメールソフトの名前にリネームして使うと送れたりする。
(それが許されるかどうかは会社のポリシー次第だろう)

2010年6月17日木曜日

◆項目の削除(Clear-Item)

Clear-Itemコマンドレットは「変数、エイリアス、レジストリーエントリー」を削除することができる。
変数は値のクリアで、変数自体が削除されるわけではない。
変数自体を削除するには、Remove-Variableを使う。
#変数の設定
Set-Variable -Name a -Value "aaa"
$b = "bbb"
New-Variable -Name c -Value "ccc"
Write-Warning "変数a,b,cを設定しました" ; sleep 2
Write-Host ("変数a={0}" -f $a)
Write-Host ("変数b={0}" -f $b)
Write-Host ("変数c={0}" -f $c)

#変数のクリア
Clear-Item variable:a
Clear-Variable b
$c = $null
Write-Warning "変数a,b,cをクリアしました" ; sleep 2
Write-Host ("変数a={0}" -f $(if($a -eq $null){"NULL"}))
Write-Host ("変数b={0}" -f $(if($b -eq $null) {"NULL"}))
Write-Host ("変数c={0}" -f $(if($c -eq $null) {"NULL"}))

#変数の削除
Remove-Variable a , b ,c
Write-Warning "変数a,b,cを削除しました" ; sleep 2
Write-Warning "変数aの表示(エラーになる)" ; sleep 2
Get-Variable a

#エイリアスの設定
New-Alias -Name np -Value c:\windows\notepad.exe
Set-Alias -Name np2 -Value c:\windows\notepad.exe
Write-Warning "エイリアスを設定しました" ; sleep 2

#エイリアスの実行
Write-Warning "メモ帳が開くので閉じる" ; sleep 2
"Alias:np = C:\Windows\Notepad.exe" > a.txt
np a.txt | Out-Null
"Alias:np2 = C:\Windows\Notepad.exe" > b.txt
np2 b.txt | Out-Null

#エイリアスの削除
Clear-Item -Path Alias:np

Remove-Item -Path Alias:np2
Write-Warning "エイリアスを削除しました" ; sleep 2
Write-Warning "Alias np の実行、存在しないのでエラーになる" ; sleep 2
np a.txt

del a.txt , b.txt -Verbose
20100617230319
Set-VariableとNew-Variable、Set-AliasとNew-Aliasの違いはNewのほうは既に存在するとエラーになる。
Write-Warningは手軽に黄色表示ができるので使っているだけ。Write-Hostでも色の指定はできる。
Write-Host ("変数a={0}" -f $(if($a -eq $null){"NULL"}))
通常フォーマット演算子( –f )の右にはオブジェクトが来る。
ここではifで条件判定した結果を渡したいので、
$(  ) の形式(部分式演算子)を使っている。
これはPowershellに対して式の評価を強制するもので、以下のように文字列の中などでも使える。
PS E:\temp\test> "(dir)"
(dir)
PS E:\temp\test> "$(dir)"
a.txt b.txt

dirの結果表示が通常と違っているが、部分式演算子ではコマンド出力がOut-Stringされないようだ。
(コマンドラインから入力された場合は自動的に Out-Stringされる。)
自分でOut-Stringしてあげると通常の形式で一覧表示される。
PS E:\temp\test> "$(dir | Out-String)"
    ディレクトリ: E:\temp\test
Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        2010/06/17     22:43          8 a.txt
-a---        2010/06/17     22:43          8 b.txt

エイリアスを使ってメモ帳を起動している部分では、閉じられるのを待つためにOut-Nullにパイプしている。
np a.txt | Out-Null

最後にファイルを削除している部分では、
del a.txt , b.txt -Verbose
-Verboseスイッチを指定して動作の詳細情報を表示させている。
ほとんどの動作系コマンドレットでは、このスイッチが指定できるようだ。

◆C#のクラスをPowershellから使う(Add-Type)

「Add-Type]は、PowerShell: ◆汎用フォームを作る4でも使った通りアセンブリーのロードにも使えるがC#のソースをコンパイルしてからロードするのにも使えるようだ。
こんな感じ、
$source=@"
using System.Windows.Forms;
public class TestClass{
    public void SayHello(string Name)
    {
        MessageBox.Show("★Hello " + Name);
    }
    public static string SayHelloStatic(string Name)
    {
        return "★こんにちは" + Name + "さん";
    }
}
"@
Add-Type -Language CSharp -TypeDefinition $Source -ReferencedAssemblies System.Windows.Forms
[TestClass]::SayHelloStatic("世界")
$testObj = New-Object TestClass

ヒア文字列でソースを定義し、Add-Typeに渡してやる。
参照設定は「ReferencedAssemblies」で。 言語はC#がデフォルトなので省略しても良い。
込み入った処理や、長い処理などは最初にVisualStudioで作ってから持ってくると良いかも・・・。
Add-Typeには1つ問題があって、一度コンパイルが通ったのちにソースを修正して再度実行すると、
Add-Type : 型を追加できません。型名 'TestClass' は既に存在しています。
発生場所 D:\PS\M06VBを利用する.ps1:16 文字:9
+ Add-Type <<<<  -Language CSharp -TypeDefinition $Source -ReferencedAssemblies System.Windows.Forms
    + CategoryInfo          : InvalidOperation: (TestClass:String) [Add-Type]、Exception
    + FullyQualifiedErrorId : TYPE_ALREADY_EXISTS,Microsoft.PowerShell.Commands.AddTypeCommand
ってなエラーになる。(セッションを開きなおせば良いのだが、都度やるのは面倒)
同じ悩みの人は見つけたが解決には至ってなさそう。
コマンドラインから子セッションを開いてそこで実行するしかないのかな。

◆コマンドの実行時間を計測する(Measure-Command)

前回、PowerShell: ◆ファイルにテキストを設定する(Set-Conten,Add-Content)で2通りの方法でテキストファイルを連結したが、どちらの方法がベターか計測してみる。
Powershellには計測用のコマンドとしてMeasure-Commandが用意されている。
Measure-Command{Get-Content a.txt , b.txt  |  Set-Content  c.txt }
Measure-Command{Set-Content  c.txt  -Value (Get-Content  a.txt , b.txt)}

20100617001627

何度か計測してみたが速度的にはばらつきがあり、どちらが速いとも判断しかねる。
ただし、メモリー使用量をみると大きな違いがある。
最大使用量は大差が無いが、パイプを使ったほうは実行後メモリー使用量が下がるのに、パイプを使わないほうは実行後もメモリーを消費したまま。
パイプのほうはすべての読み込みを待たずに次の処理へ順次データを渡すからだと思う。
以下のように括弧で囲むとすべて読み込んだ後にパイプに渡すので両者の処理は同じになるのではないだろうか。
(Get-Content a.txt , b.txt)  | Set-Content c.txt
どちらにしても、ファイルサイズに比べてやけにメモリー使用量が大きいようなので、このような処理を行うときはセッションを区切ったほうが良さそうだ。
コマンドラインから実行するときは、Powershellと打つと同じ画面でもう一つセッションを開始できるので、その中で実行すると良さそう。
Dosコマンドで、
Copy  a.txt+b.txt  c.txt
としたほうが有利かも?
ってか圧倒的に有利(速度、メモリーとも)。PowershellからDosコマンドを実行するには
cmd  /c “Copy  a.txt+b.txt  c.txt”
Powershellでも
Copy-Item -Path a.txt , b.txt  -Destination c.txt
と書けるのだが、結果が思った通りにならない。(b.txtの内容がc.txtにセットされるだけ)
Pathの配列指定ってどう使うんだろ・・・。

あぁ、コピー先がディレクトリーの時か・・・。

2010年6月16日水曜日

◆ファイルにテキストを設定する(Set-Content,Add-Content)

Set-Content –Path  Test.txt  -Value  “Text”
文字コードはSJIS、指定はできないようだ。
<追記>ヘルプには載っていませんがEncoding指定が出来るようになってますね。

これは、リダイレクト
“Text”  >  Test.txt
に同じ。
ただし、こちらはUnicode 。エンコードを指定するには(その他オプションも)、Out-Fileコマンドレットを使う。
"Text" | Out-File Test.txt -Encoding ASCII
Set-Contentとの違いは良く分からない。

追加するときは、「Add-Content」もしくは「>>」を使う。
複数行のテキストを追加するには、改行コードを特殊文字で指定して
Set-Content –Path  Test.txt  -Value  “Text`r`nText”
ってな感じ。

複数のファイルに同じテキストを追加することもできる。
Add-Content  -Path  a.txt , b.txt  -Value “Text”

ワイルドカードを使っても良い。
Add-Content  -Path  *.txt  -Value “Text”

2つのファイルを連結するには
Get-Content a.txt , b.txt  |  Set-Content  c.txt
もしくは
Set-Content  c.txt  -Value (Get-Content  a.txt , b.txt)
もしくはリダイレクトを使って
Get-Content a.txt , b.txt  |  Out-File  c.txt

◆コンピューターをドメインに追加する(Add-Computer,Remove-Computer)

コンピューターをドメインに追加するには、

Add-Computer -DomainName ドメイン名

通常は資格情報が必要になるので、
Add-Computer -DomainName ドメイン名 -Credential Get-Credential

としておくと、資格情報入力用のダイアログが表示され、ユーザーIDとパスワードを指定できる。
通常はドメインの一般ユーザーで追加できるはずだ。

コンピューターアカウントを追加するOUを指定することもできる。

Add-Computer -domainname ドメイン名 -OUPath OU=hogeOU,DC=domain,DC=co,DC=jp

ワークグループに名の変更もAdd-Computerでできるようだ。

Add-computer -workgroupname ワークグループ名


ドメインを抜けるには、

Remove-Computer –Credential Get-Credential

ヘルプによるとRemove-Computerでワークグループを抜けることもできるとあるが・・・、うそっぽい。
(抜けたらどこへ行くかわからんでしょ)

Powershellとは関係ないが、ドメインに参加するには事前にしかるべきDNSを指定しておく必要がある。
また、一般ユーザーで追加できるコンピューターアカウントは10個までとの事(多分)

<追記>
資格情報ダイアログを表示させたくない場合は以下を参考にして資格情報を指定してあげれば良いかも。
PowerShell: ◆プレーンテキストから資格情報を作る

2010年6月15日火曜日

◆汎用フォームを作る4

これまで、汎用フォームのテストにはPowershell_iseを使っていたのだが、作ったスクリプトをコマンドラインから実行すると動かないことに気がついた。
どうやらコマンドラインではSystem.windows.forms.dllがロードされないみたい。
Add-Type -AssemblyName System.windows.forms
でロードしてあげる必要がある。

Powershell1.0では以下の構文で。
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

ISEではISE自体が各種表示用にFormを使っているからデフォルトでロードされているのかなぁ。
なんか、ちょっといやらしいな・・・・・。(変数のスコープなんかも気をつける必要がありそうだし)

◆汎用フォームを作る3

汎用フォームを使ってマスクドテキストボックスをテストしてみる。

Set-PSDebug -Strict

. (Join-Path (Split-Path $myInvocation.MyCommand.path) M00Func.ps1)  #共通関数の読み込み

$arr = New-Object System.Collections.ArrayList[] 4
0..3 | %{$arr[$_] = New-Object System.Collections.ArrayList}

$maskText = New-Object System.Windows.Forms.MaskedTextBox
$maskText.Mask = "0000年90月90日"
$maskText.ValidatingType = [DateTime]
$maskText.add_TextChanged({
    if($maskText.ValidateText()){
        $maskLabel.Text = "チェックOK({0})" -f $maskText.ValidateText()
    }else{
        $maskLabel.Text = "チェックNG"
    }
})

[void]$arr[0].Add($maskText)

$maskLabel = New-Object System.Windows.Forms.Label
[void]$arr[1].Add($maskLabel)

$button1 = New-Object System.Windows.Forms.Button
$button1.Text = "閉じる"
$button1.Add_Click({[void] [System.Windows.Forms.MessageBox]::show("フォームを閉じます");$form.Close()})
[void]$arr[3].Add($button1)

$form = New-Object System.Windows.Forms.Form
MSet-Control $arr $form  #M00Func.ps1の関数(汎用的にフォームにコントロールを配置する)
$maskLabel.Width = 200
[void]$form.ShowDialog()


ValidatingTypeに[DateTime]型を指定しておくと、ValidateText()メソッドでパース後の値を取ってこれる。
この値がNullの時はエラー(DateTime)に変換できなかったという事。
ちなみに、条件判定(IF)ではNull , 空の文字列 , ゼロ 等の型の初期値をFalseと判定するようだ。
また、

$maskLabel.Text = "チェックOK({0})" -f $maskText.ValidateText()

この 「-f」 はフォーマット演算子。
.NETのFormatと同じ使い方ができる。

2010年6月14日月曜日

◆Excelサンプル

Excelへ書き込むサンプル。
Get-Processの結果をシートに設定してみる。

$excel = New-Object -Com Excel.Application
$excelBook = $excel.Workbooks.Add()
$excelSheet = $excelBook.WorkSheets.Item(1)
$excelCells = $excelSheet.Cells
Get-Process | %{$i = 1}{
    $excelCells.Item($i,1) = $_.Id
    $excelCells.Item($i,2) = $_.ProcessName
    $i++
}
$xlOpenXMLWorkbook = 51 #Excel2007形式
$OutputPath = "D:\test.xlsx"
$excelBook.SaveAs($OutputPath,$xlOpenXMLWorkbook)
$excel.Quit()

Foreach-Object(Aliasは%)はスクリプトブロックの配列が指定できる。

%{前処理}{ループ処理}{後処理}の形式。

Excelの保存形式はEnumの値を指定する。Excel2007形式は「51」。

これで問題なさそうだが、Comオブジェクトは参照を解放しないとプロセスが残ってしまう。
(ps Excelで確認できる)
乱暴に、kill –name Excel とする方法もあるが、もう少しスマートに。(まっとうに)

[System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($excel)

とすると開放してくれるようだ。

これを各変数に対して行うと、
foreach($obj in $excel,$excelBook,$excelSheet,$excelCells){
    [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($obj)
}

もう少し汎用的にしてみる。

Get-Variable excel* | %{
    [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($_.value)
}

戻り値がうっとうしいときは、

Get-Variable excel* | %{
    [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($_.value)  | Out-Null
}

voidにキャストしても良い。

Get-Variable excel* | %{
    [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($_.value)
}

2010年6月13日日曜日

◆汎用フォームを作る2

PowerShell: ◆汎用フォームをつくるをもう少し汎用的にしてみる。
関数部分を外出しに。

M00Func.ps1
#
#control[0..2]がコントロール配列、control[3]がボタン配列 ##########################################
#
Function MSet-Control($control,$form){
$CWidth,$CHeight = 100,30  #コントロールのサイズ
$BWidth,$BHeight = 50,30   #ボタンのサイズ
$XMargin,$YMargin = 10,10  #コントロールマージン
$ButtonMaxCount = 6        #ボタンの最大数
#Formサイズの設定
$maxControlCount = [Math]::Max([Math]::Max($control[0].Count,$control[1].Count),$control[2].Count)
$formHeight = $maxControlCount * ($CHeight + $XMargin) + ($BHeight + $XMargin)
$formWidth = ($BWidth + $XMargin) * $ButtonMaxCount
$size = New-Object System.Drawing.Size $formWidth,$formHeight
$form.ClientSize = $size
#Formへコントロールの配置
0..2 | %{$XPos = $XMargin}{
$YPos = $XMargin
foreach($cl in $control[$_]){
[System.Windows.Forms.Control]$ctrl = $cl
$ctrl.Left = $XPos
$ctrl.Top = $YPos ; $YPos += ($CHeight + $XMargin)
$ctrl.Width = $CWidth
$ctrl.Height = $CHeight
$form.Controls.Add($ctrl)
}
$XPos += ($CWidth + $XMargin)
}
#Formへボタンの配置
$buttonCount = 1
foreach($button in $control[3]){
$button.Top = $size.Height - $BHeight - $YMargin
$button.Left = $size.Width - ($buttonCount * ($BWidth + $XMargin))
$button.Width = $BWidth
$form.Controls.Add($button)
$buttonCount++
}
}

これは、そのまま抜き出しただけ。
notepad $profile
に登録しても良いかも。

使う側は、
M53汎用Formサンプル.ps1


Set-PSDebug -Strict
 
. (Join-Path (Split-Path $myInvocation.MyCommand.path) M00Func.ps1)  #共通関数の読み込み
 
#[0..2]はコントロール用、3はボタン用
$arr = New-Object System.Collections.ArrayList[] 4
0..3 | %{$arr[$_] = New-Object System.Collections.ArrayList}
 
 
$test1 = New-Object System.Windows.Forms.Label 
$test1.text = "テスト"
[void]$arr[0].Add($test1)
$test2 = New-Object System.Windows.Forms.TextBox
[void]$arr[0].Add($test2)
$test3 = New-Object System.Windows.Forms.TextBox
[void]$arr[0].Add($test3)
$test4 = New-Object System.Windows.Forms.TextBox
[void]$arr[1].Add($test4)
$test5 = New-Object System.Windows.Forms.ComboBox
$test5.Items.AddRange(@("test1","test2","test3"))
[void]$arr[2].Add($test5)
 
$button1 = New-Object System.Windows.Forms.Button
$button1.Text = "閉じる"
$button1.Add_Click({[void] [System.Windows.Forms.MessageBox]::show("フォームを閉じます");$form.Close()})
[void]$arr[3].Add($button1)
 
$form = New-Object System.Windows.Forms.Form
MSet-Control $arr $form  #M00Func.ps1の関数(汎用的にフォームにコントロールを配置する)
 
[void]$form.ShowDialog()

関数を使うためにドットソースで読み込みの指定をする。
. (Join-Path (Split-Path $myInvocation.MyCommand.path) M00Func.ps1)

$myInvocationは自動変数、$myInvocation.MyCommand.pathで自スクリプトのパスが取れる。
共通関数スクリプトは同一フォルダに置く前提。

ArrayListの4つの配列を作るのに、
0..3 | %{$arr[$_] += New-Object System.Collections.ArrayList}

でよさそうに思ったのだが、これではうまくいかない。
$arr.Countが0のまま。
予め定義しておく必要があるみたい。
$arr = New-Object System.Collections.ArrayList[] 4
0..3 | %{$arr[$_] = New-Object System.Collections.ArrayList}

イベントプロシジャは、
$button1.Add_Click({スクリプトブロック})
のように、Add_イベント名 の形式で登録できる。