2016/01/11

注:最後に書きましたが、この記事の内容には未解決問題が残っています。

はじめに

前回は、パイプラインを下流の関数内で打ち切る方法について説明しました。

今回は逆に、下流でパイプラインが打ち切られた場合、上流の関数ではどう対応すべきなのか、特にリソース解放処理を焦点にして考察してみます。

パイプラインが打ち切られるケース

前回も説明しましたが、パイプライン内の関数またはコマンドレットで何らかの例外が発生すると、パイプライン処理がその時点で打ち切られます。パイプライン処理が打ち切られると、後続のprocessブロックのみならず、パイプラインに含まれるすべてのendブロックの実行もスキップされてしまいます。

ところでコマンドレットや関数では何かエラーが発生した場合、コマンドレットではWriteErrorメソッド、関数ではWrite-Errorコマンドレットを使ってエラーストリームに、生の例外オブジェクトをラップしたErrorRecordを出力し、呼び出し側のErrorActionの設定にエラー時の処理を委ねるのが基本です。

呼び出し時のErrorAction指定がContinue、SilentlyContinue、Ignoreの場合は、パイプラインが中断することはありませんが、StopやInquireで中断した場合は例外(ActionPreferenceStopException)がthrowされ、パイプラインは打ち切られます。
(ちなみにv3からはStopで中断した場合は、コマンドレットがエラーストリームに書き出したErrorRecordに含まれるExceptionがthrowされる)

また、継続不能エラーが発生(ThrowTerminatingErrorメソッド)したり、.NETの生の例外がそのままthrowされたり(お行儀が悪いですが)、breakステートメント(FlowControlException)が実行されたり、Select-Object -First(StopUpstreamCommandsException)が実行された場合も、同様にパイプラインは打ち切られます。

つまり、下流のコマンドでパイプラインが打ち切られるケースというのは色々あり得るので、endブロックというのは実行が確約されたものでは全くない、ということに留意しておく必要があります。

関数内のリソース解放処理

確実に実行したい後処理というのは色々あると思いますが、特に確保したリソースの解放というのは確実に実行してもらわないと困ります。

しかし、前述のような背景があるので、何らかのリソースを用いる関数を書く際に、beginブロックでリソースを確保し、processブロックでリソースを利用し、endブロックでリソースを解放する、ということは基本はNGということになります。

この点、コマンドレットクラスであれば、IDisposableインターフェースを実装しておけば、コマンドレット終了時にDisposeメソッドをPowerShellが呼んでくれるので、その中にリソース解放処理を記述しておけばOKです。しかし関数ではこの手法が使えないので、代替案を考える必要があります。

パイプライン処理の後始末をしよう - 鷲ノ巣であえとす氏が考案した、beginブロックに後処理用の関数を定義しておく方法では、同じ関数内のprocessブロックで発生したエラーをcatchしてリソース解放処理を走らせることは可能です。が、残念ながら下流で発生した例外をcatchすることはできず、その場合は後処理がスキップされてしまいます。

リソース解放処理を含めた関数

あえとす氏の方法を若干アレンジして、下流で例外が発生した場合も確実にリソース解放の後処理を走らせる方法を考えてみました。以下にコードを示します。

※クラス構文を使ってますが、これは単に、Disposeできるオブジェクトのサンプルだと思ってください。

class SomeResource : IDisposable
{
    [void]Dispose()
    {
        # ここで何らかのリソース解放処理を行ったものとする
    }
}

function Write-OutputWithResource
{
    [CmdletBinding()]
    param([Parameter(ValueFromPipeline)][psobject[]]$InputObject)
    begin
    {
        $resource = New-Object SomeResource # 後で解放する必要のある何らかのリソース確保

        function Clear-Resource
        {
            # リソースの解放処理
            $resource.Dispose()
        }
    }
    process
    {
        try
        {
            $processing = $true
            Write-Verbose "output_start: $InputObject"
            $InputObject # パイプライン出力
            Write-Verbose "output_end: $InputObject"
            $processing = $false
        }
        catch
        {
            # try内で例外が発生した場合はそのまま再スロー
            throw
        }
        finally
        {
            if($processing)
            {
                Write-Verbose "resource_dispose (on error)"
                Clear-Resource
            }
            Write-Verbose "output_finally: $InputObject"
        }
    }
    end
    {
        Write-Verbose "resource_dispose (at end)"
        Clear-Resource
    }
}

processブロック内のtryブロックでパイプライン下流に値を出力したとき、下流で例外が出てもcatchブロックが実行されないので、代わりに、必ず実行されるfinallyブロックから後処理を呼び出すようにしてみました。

ただし、パイプライン下流で例外が発生しなかった場合には、processブロック内ではリソース解放処理はしたくないので、例外発生の有無を$processingという変数の値を見ることで確認しています。もしパイプライン出力したあと下流で例外が出ていれば、$processingの値は$trueのままになるので判断可能です。

パイプラインが中断することなく、最後まで実行される場合は、endブロック内でリソース解放処理を行います。

同じ関数内で例外が発生したときのリソース解放処理についても、$processing = $trueと$processing  = $falseの間に例外が発生する可能性のある処理を記述した上で、catchブロックで再throwすれば、併せて対応できるのではないかと思います。(それ用のtry..catchを記述してもいいですが)

関数の実行例
PS> 1..3 | Write-OutputWithResource -Verbose | Select-Object -First 2
詳細: output_start: 1
1
詳細: output_end: 1
詳細: output_finally: 1
詳細: output_start: 2
2
詳細: resource_dispose (on error)
詳細: output_finally: 2

このように、下流でパイプライン打ち切りがあるとendブロックは実行されませんが、リソース解放処理は、きちんとprocessブロック内のfinallyブロックから呼び出されています。

PS> 1..3 | Write-OutputWithResource -Verbose
詳細: output_start: 1
1
詳細: output_end: 1
詳細: output_finally: 1
詳細: output_start: 2
2
詳細: output_end: 2
詳細: output_finally: 2
詳細: output_start: 3
3
詳細: output_end: 3
詳細: output_finally: 3
詳細: resource_dispose (at end)

もちろんパイプラインが最後まで正常に実行された場合も、ちゃんと最後にendブロックでリソース解放が行えるようになっています。

おわりに

本当に、こうするしかないんですかね…?

PowerShellにもリソース利用のusingステートメント欲しいです、が、begin, process, endにまたがって機能するusingってどういう構文になるんでしょうね。

(14:22追記)と、ここまで書いておいて、この方法では下流で発生した例外には対処できますが、上流で例外が発生した場合はリソース解放が実行されないという問題に気付きました…。どうすればいいんだ…。

2012/12/01

今日から、PowerShell Advent Calendar 2012が始まりました。初日は私が担当させていただきます。お題は旬の話題、PowerShell 3.0の新機能!…ではなく、初心に返って、PowerShellの「関数」ってどう書くのがいいのかというお話をします。PowerShell 3.0どころか、大部分はPowerShell 1.0から変わっていない基本の話です。

これは今までずっと書きたかったネタですがなかなか書く暇がなくて放置してたものです。3.0の話はきっと他の皆さんが書いて下さるはず!私もまた順番が回ってきたら書こうと思います。

PowerShellの関数は従来言語とだいぶ違う

PowerShellを使いこなすようになってくると、他の言語を使う時と同じで、定型処理は関数として一つにまとめたくなってきます。ところが他の言語と同じような感覚で関数を書くと、どうもうまくいかないのです。

たとえば引数にフォルダパスとフォルダ名を指定すると、指定フォルダが存在すればFalseを返し、存在しなければ作成してTrueを返す関数を書いてみました。

function MakeDir($path,$name)
{
    $newDirPath = Join-Path $path $name
    if((Test-Path $newDirPath))
    {
        return $false
    }
    else
    {
        New-Item -ItemType Directory -Path $newDirPath 
        return $true
    }
}

実行は

MakeDir("C:\test","NewFolder")

と、メソッド風に呼び出すことはできないので、コマンドレット風に

MakeDir C:\test NewFolder

と呼び出せばいいんですが(まあ最初はここもつまづきポイントではありますが)、この実行結果は以下のようになります。

    ディレクトリ: C:\test

Mode                LastWriteTime     Length Name 
----                -------------     ------ ----
d----        2012/12/01      7:51            NewFolder
True

フォルダが作成されてTrueが返却されることを想定していたのに、なんか余計な出力が混じってしまっています。なんでしょうこれは?

実はPowerShell関数内で値が出力されると、returnキーワードがついてなくてもすべて呼び出し元に出力されるという仕様なのです。そしてPowerShellにおけるreturnキーワードの効果は「後続処理を打ち切って呼び出し元に戻る。ただしreturnの後に値が指定してあればそれを最後の値として戻す」となります。そのため、呼び出し元に返したくない出力が関数内にある場合は、すべて[void]にキャストしたり|Out-Nullとしてリダイレクトするなどして出力を破棄する必要があるのです。このMakeDir関数の場合はNew-Itemコマンドレットが作成したフォルダのFolderInfoオブジェクトを出力するので、これをNew-Item -ItemType Directory -Path $newDirPath | Out-Null のように破棄してやる必要があるわけです。

パイプラインの動作

先ほどの例を見ると、「いやいやなんでそんな訳のわからない仕様なんだよ、returnあるときだけ値返せよ」とお思いかと思います。しかしこれはPowerShellの特長の一つである、コマンドのパイプラインによる連携を行うための仕様なんです。

ここでコマンドを繋ぐパイプラインがどういう動作をしてるか、おさらいします。

Get-Process | where {$_.Handles -ge 500} | foreach {$_.Path}

これはハンドル数が500以上のプロセスのメインモジュールファイルのパスを取得するというコマンドで、別に何の変哲もありません。ところが、このコマンドがやっている処理を、次のように誤解してませんでしょうか?

@ 稼働中のすべてのプロセスの一覧を配列として取得する。
A @で取得した配列を走査して、Handlesプロパティの値を調べる。Handlesが500以上のオブジェクトだけ抽出した配列を生成する。
B Aで生成した配列を列挙して、{}内のスクリプトをそれぞれ実行する。

しかし、これは間違いです。

正しくは

@ 稼働中の1つのプロセスオブジェクトを取得して次のコマンドへ送る。
A そのプロセスのハンドル数が500以上なら、次のコマンドへ送る。そうでないなら@に戻る。
B そのプロセスに対して{}内のスクリプトを実行する。まだ未取得のプロセスが残っていれば@に戻る。

という動きをしています。つまり、パイプラインの手前で一旦すべての処理を終えてから、出力オブジェクトがまとめて配列という形で次のコマンドに送られるのではなく、オブジェクトがパイプラインの先頭から末尾に向けて1つずつ通過していき、それが先頭コマンドの出力オブジェクト数だけ繰り返される、という動作をしているのです。

これがPowerShellのパイプライン処理が、従来の処理系での関数と決定的に違うところで、パイプラインによって複数のコマンドが、あたかももとからあった単一のコマンドのように密に連携するわけです。

(この処理、.NETのLINQにちょっと似てると思う方もいらっしゃると思います。しかしLINQとは全然違うものです。なんせPowerShellはLINQより先に世に出てますし! しかし類似点も多いのでいずれ比較なんかを書きたいと思ってます)

パイプラインで連携可能な関数の書き方

さて、先ほどのパイプラインの話ではコマンドレットを連携させていました。しかしPowerShellにおいてはコマンドレットも関数も、それが.NETのクラスかPowerShellのスクリプトなのかの違いがあるだけで、基本は同じ「コマンド」です。なので、関数もコマンドレットと同様、適切な記述をおこなえば、パイプラインでコマンド同士を連携させることが可能です。

以下に、Get-Repeatという関数の例を挙げます。この関数は-Textパラメータに文字列を指定し、-Countパラメータに回数を指定すると、指定文字列を指定回数分連結した文字列を出力する、という何の変哲もない関数です。しかしパイプラインからの入力を受け付け、次のパイプラインへ出力することを想定した作りになっています。

function Get-Repeat
{
    param(
        [Parameter(ValueFromPipeline=$true,Mandatory=$true)]
        [string[]]
        $Text,
        
        [int]
        $Count=2
    )

    begin
    {
    }

    process
    {
        foreach($s in $Text)
        {
            $s * $count
        }
    }

    end
    {
    }
}

以下は実行例です。

PS> Get-Repeat -Text ab -Count 2
abab
PS> "ab" | Get-Repeat -Count 2
abab
PS> Get-Repeat -Text ab,cd -Count 2
abab
cdcd
PS> "ab","cd" | Get-Repeat -Count 2
abab
cdcd

このように、パラメータに値を指定してもパイプラインから入力しても、スカラー値(配列ではない単一のオブジェクト)でも配列でも、正しく処理されています。

この関数をポイントごとに見ていきましょう。

PowerShellの正式な関数はparam節、beginブロック、processブロック、endブロックに分かれます。param節にはパラメータを指定します。beginブロックにはパイプラインで連携した際、最初の1回だけ実行される初期化処理、endブロックには最後の1回だけ実行される後始末処理を記述します。beginとendは今回の例では内容を省略しています。processブロックには、パイプラインから入力された1つのオブジェクトに対してその都度実行される処理を記述します。

ちなみに、

コマンド@|コマンドA|コマンドB

とある場合、各コマンドにおけるbegin,process,endブロックは次のような順番で呼び出されます。

コマンド@begin→コマンドAbegin→コマンドBbegin→{コマンド@process→コマンドAprocess→コマンドBprocess→コマンド@process…}→コマンド@end→コマンドAend→コマンドBend

processブロックでの処理は、通常はパイプラインだけではなくパラメータからも値を入力できるようにしておきます。そのためにはparam節に記述するパラメータに「このパラメータはパイプラインから値を入力することもできる」を意味する[Parameter(ValueFromPipeline=$true)]という属性を指定します(この属性はPowerShell 2.0から利用可)。今回のパラメータには「このパラメータは必須である」を意味するMandatory=$trueもあわせて指定しています。

先述の通り、パイプラインから入力される場合は配列ではなくオブジェクトが単体で渡されるのですが、パラメータから入力される場合はスカラー値と配列値、どちらの可能性もあるため、[string[]] のようにパラメータの型を配列型にしておくことで、どちらを指定しても処理できるようにしています。

processブロックではパラメータ経由で配列値が渡された場合に、各要素に対して処理を行うためforeachループを設けています。ちなみにスカラー値が渡された場合もforeachは問題なく処理します。

processブロック内では、returnは記述しません。returnするとその時点で関数が終了してしまうので正しくすべての出力ができなくなってしまいます。

特にこの例の関数のように入力型と出力型が同一の場合は、processブロックでは1オブジェクトの入力に対して、1オブジェクトを出力するようにしておくと、他のコマンドと連携させやすくなります。ただしWhere-Objectコマンドレットのようにフィルタ処理を行う関数の場合は、条件によっては何も出力しないようにします(空の配列とか$nullを返すのではないことに注意)。もちろん入力オブジェクトから何らかの配列値を出力する場合もありえます。

最低限、これらのポイントを押さえて関数を記述すると、他のコマンドとパイプラインで連携しやすい、PowerShellらしい関数を書くことができると思います。

まとめ

PowerShellでは従来言語と同じ感覚で関数を書くと、うまくいかないことが多いです。もっとも単に処理をひとまとめにしたいというニーズだけならばそれでも問題ないのですが、関数同士を組み合わせたいときに問題が顕在化します。

パイプラインの真の動作を理解し、パイプラインの中に組み込んで動作させることを想定した関数を記述すると、他のコマンドレットあるいは自作関数と連携しやすくなり、PowerShellの真の力を解放することができると思います。

PowerShell Advent Calendar 2012の1日目にしてはえらい固いネタかもですが、基本をおさらいするのも大事ですよね。

さて、明日は@jsakamotoさんの番です。よろしくお願いします。

2010/03/04

PowerShell 2.0ではscriptblockにGetNewClosure()メソッドが追加され、クロージャを記述することができるようになりました。

クロージャを作るには、まず関数の中に変数(レキシカル変数)と関数(PowerShellではスクリプトブロック)を定義します。外側の関数内に定義された変数(レキシカル変数)を関数内に定義された内側の関数から操作するようにしておきます。そして外側の関数は内側の関数そのものを返却するようにしておきます。これでクロージャができました。

クロージャを使用するには、まず外部から外側の関数の戻り値(これは内側の関数です)を変数に代入します。そしてこの変数に含まれる関数(内側の関数)を実行すると、レキシカル変数に何らかの変化を及ぼしつつ結果が返却されます。ポイントは、レキシカル変数の値が保持されることです。その結果、内側の関数を実行するたびにその時のレキシカル変数の値に基づいた結果を返却するようにできるわけです。

とまあ書いても何のことかよくわからないかと思いますので実例を示します。よくある例題なのですが、「呼び出すたびに1が加算された結果を返す関数。すなわち、呼び出すと結果が、1,2,3,4…と続いていく関数」を考えます。

function counter()
{
	$x=0
	return {$script:x++;return $x}.GetNewClosure()
}

これがクロージャの本体です。ポイントは、scriptスコープを使用することで内側のスクリプトブロックからレキシカル変数(ここでは$x)の値を変更している点です。

使用法は次のようになります。変数$fにcounter関数の内側のスクリプトブロックを代入し、&演算子で実行しています。

PS C:\Users\daisuke> $f=counter
PS C:\Users\daisuke> &$f
1
PS C:\Users\daisuke> &$f
2
PS C:\Users\daisuke> &$f
3
PS C:\Users\daisuke> &$f
4
PS C:\Users\daisuke>

見事、お題を実現することができました。

さて、GetNewClosure()メソッドが追加されたことで、関数のカリー化も可能になります。カリー化とは、たとえばf(x,y)という関数があった場合、g(x)(y)という、f(x,y)と常に同じ値を返却する関数を作ることです。一般には、複数の引数をとる関数fを、fの最初の引数だけを引数にとり、「fの残りの引数をとり結果を返す関数」が戻り値であるgという関数に変換することです。これまた何のことかわかりにくいですね。

たとえば一番簡単な例。引数同士を加算する関数sumをカリー化してcurried_sumという関数を作ってみます。

function sum([int]$x,[int]$y)
{
	return [int]($x+$y)
}

function curried_sum([int]$x)
{
	return {param([int]$y);return sum $x $y}.GetNewClosure()
}

これでsumのカリー化ができました。実行してみます。

PS C:\Users\daisuke> &$(curried_sum 3) 4
7
PS C:\Users\daisuke> &$(curried_sum -6) 14
8
PS C:\Users\daisuke> &$(curried_sum -2) -3
-5
PS C:\Users\daisuke> $sum5 = curried_sum 5
PS C:\Users\daisuke> &$sum5 11
16
PS C:\Users\daisuke> &$sum5 -4
1
PS C:\Users\daisuke> $sum2 = curried_sum 2
PS C:\Users\daisuke> &$sum2 8
10
PS C:\Users\daisuke> &$sum2 1
3

PowerShellの関数の呼び出し方がわりと特殊であるため、少々分かりにくいですがサブ式$()と呼び出し演算子&をつかってカリー化されたsum関数を実行して目的通りの結果を得ています。たとえば最初の例は3+4を実行していることになり、結果はsum 3 4と同じ7になります。以下同様です。$sum5は、「引数に5を加える関数」になります。

いかがでしたでしょうか。PowerShellでも(なんか文法とか奇妙ですが)クロージャやカリー化ができて楽しいですね。というか私は本記事を書くにあたってクロージャとカリー化がなんぞやということを勉強しました…。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2010/03/04/186768.aspx

2010/02/13

PowerShellは.NET Framework 2.0を利用するWindowsのシステム管理用シェルである。シェルであるためコンソールで対話的にコマンドを実行することができるのはもちろん、スクリプトファイル(*.ps1)を記述しバッチ的に実行することも可能である。ここではPowerShellスクリプトで(コンソールでも使用は可能だが)用いることのできる基礎文法を紹介する。なお、PowerShellでは文法上、大文字小文字を区別しない。

※(★2.0)の注釈があるものはPowerShell 2.0で新たに追加された要素である。

1.基礎

表示

コンソールに文字列を表示。

"Hello world" 

コマンドレット(後述)を使用した場合。

Write-Host "Hello world" 

コマンドレット

PowerShellはコマンドレットと呼ばれる100種類以上のコマンドライン・ツール群を単独で、あるいはパイプライン(後述)で連結して使用するのが基本となる。コマンドレットは原則verb-nounという命名規則にしたがっている。パラメータをつける場合は「-パラメータ名」あるいは「-パラメータ名 パラメータ値」を指定する。

# コマンドレットの一覧表示
Get-Command 

# サービスの一覧を表示
Get-Service 

# アプリケーション イベントログの最新15個のエントリを表示
Get-EventLog -logName Application -newest 15

パイプライン

コマンドレットが値を返却する場合、.NET Frameworkのオブジェクトが含まれる配列であることが多い。このオブジェクト配列がパイプラインを渡って後続のコマンドレットに入力される。

# プロセスのリスト(System.Diagnostics.Processオブジェクトの配列)を取得し、
# Where-Objectコマンドレットでハンドル数(handlesプロパティ)の値が500より大きいものだけを取り出し
# Select-Objectコマンドレットで最初の5つのオブジェクトだけを切りだして表示
Get-Process | Where-Object {$_.handles -gt 500} | Select-Object -first 5

# C:\Windows 配下のフォルダ、ファイルの一覧(System.IO.DirectoryInfo,System.IO.FileInfoオブジェクトの配列)を取得し、
# ForEach-Objectコマンドレットで配列を列挙しすべてのオブジェクトのFullNameプロパティ(フルパス)の値を表示
Get-ChildItem C:\Windows | ForEach-Object {$_.FullName} 

# 通常の配列に関してもパイプラインを使用可能。
# 重複を取り除き、ソートをかける
@(3,5,10,1,2,1,1,1,2,6,4,4)|Sort-Object|Get-Unique 

コメント

# コメント 

マルチラインコメント(★2.0)

<#
複数行に渡る
コメントです
#> 

変数の宣言

PowerShellは変数の宣言をしなくても変数を使用可能。以下のようにするとどのような型でも代入可能な変数が作られる。

$a = 1
$a = $b = $c = 1 #複数変数に一度に同じ値を代入する場合
$items = Get-ChildItem # コマンドレットの戻り値を格納 

変数の型を指定することは可能。以下のようにするとint型のみ格納可能な変数が作られる。

[int]$a = 1 

あるいは、コマンドレットを用いて$aという変数を宣言することもできる。この場合変数の型は指定できない。

New-Variable -name a 

変数のスコープ

# どのスコープからも読み書き可能
$global:a = 1

# 現在のスコープからのみ読み書き可能
$private:a = 1 

# 現在のスクリプトからのみ読み書き可能
$script:a = 1 

文法チェック

以下を実行することで未定義の変数を参照するとエラーが出るようになる。

Set-PSDebug -strict 

スクリプトの実行

デフォルトの実行ポリシーではスクリプトの実行は不許可であるため、以下のようにポリシーを変更しておく。(RemoteSignedはローカルにあるスクリプトファイルは無条件で実行可、リモートにあるスクリプトファイルは署名付きのもののみ実行可)

Set-ExecutionPolicy RemoteSigned 

スクリプト/コマンドを実行するにはコマンドラインで次のようにする。

コマンドを実行する

powershell -command {Get-ChildItem C:\} 

ファイルを実行する

powershell  .\script.ps1 

ドットソース(スクリプトの内容をグローバルスコープに読み込む)

powershell  . .\script.ps1 

ファイルを実行する(★2.0)

powershell -file script.ps1 

PowerShellスクリプトから別のスクリプトを実行する場合(関数のインクルードにも用いられる)

.\script.ps1
. .\script.ps1 # ドットソース 

デバッガの起動

Set-PSDebug -trace 2 

ステップ実行

Set-PSDebug -step 

2.数値

数値の表現

PowerShellにおける数値は.NET Frameworkの数値を表す構造体のインスタンスである。数値には整数、浮動小数点があり、変数に代入した段階で適切な型が設定される。

# int型(System.Int32型)
$int = 1

# System.Double型
$double = 1.001

四則演算

# 足し算
$i = 1 + 1

# 引き算
$i = 1 - 1 

# 掛け算
$i = 1 * 1 

# 割り算
$i = 1 / 1 

余りと商の求め方

# 割り算の余り
$mod = 7 % 3 

# 上記の場合の商
$div = (7 - 7 % 3) / 3 

べき乗

# 2の8乗
$i = [math]::Pow(2,8) 

インクリメントとデクリメント

# インクリメント
$i++ 

# デクリメント
$i-- 

3.文字列

PowerShellにおける文字列は.NET Frameworkの System.Stringクラスのインスタンスである。

文字列の表現

文字列はシングルクォーテーションかダブルクォーテーションで囲む。ダブルクォーテーションの中では`t(タブ)や`r`n(改行)などの特殊文字が使用でき、変数が展開される。

$str1 = 'abc'
$str2 = "def"
$str3 = "a`tbc`r`n" 

#変数展開(結果は abc def)
$str4 = "$str1 def" 

文字列操作

各種文字列操作

# 結合
$join1 = "aaa" + "bbb"
$join2 = [string]::Join(",",@("aaa","bbb","ccc") )

# 結合(★2.0)
$join2 = @("aaa","bbb","ccc") -join "," 

# 分割
$record1 = "aaa,bbb,ccc".Split(",") 

# 分割(★2.0)
$record2 = "aaa,bbb,ccc" -split "," 

# 長さ
$length = "abcdef".Length 

# 切り出し
$substr = "abcd".SubString(0,2) # ab

正規表現検索

# hitした場合はTrue,しなかった場合はFalse
$result = "abcd" -match "cd"

# 最初に見つかった文字列。添え字の1,2…には()内のサブ式にhitした文字列が格納。
$matches[0] 

正規表現置換

$result = "abc" -replace "c","d" 

4.配列

PowerShellにおける配列は.NET Frameworkの System.Arrayクラスのインスタンスである。

配列の参照と代入

# 5個の要素を持つ配列宣言と代入
$arr1 = @(1,3,5,7,9)
 
# 以下のようにも記述できる
$arr1 = 1,3,5,7,9 

# 型指定する場合
[int[]]$arr1 = @(1,3,5,7,9) 

# 1〜10までの要素を持つ配列宣言と代入
$arr2 = @(1..10) 

# 1要素の配列宣言と代入
$arr3 = @(1) 
$arr3 = ,1 

# 空の配列宣言と代入
$arr4 = @() 

配列の要素の参照と代入

# 4番目の要素を参照 
$ret = $arr2[3] 

# 6〜9番目の要素を含んだ配列を参照
$ret = $arr2[5..8] 

# 1〜4番目と8番目の要素を含んだ配列を参照
$ret = $arr2[0..3+7] 

# 配列の末尾の要素を取り出す
$ret = $arr2[-1] 

# 5番目の要素に値を代入
$arr2[4] = 11 

# 3より小さな要素を含んだ配列を返す
$ret = $arr2 -lt 3 

配列の個数

$arr1_num = $arr1.Length 

配列の操作

$arr1 = @(1,3,5,7,9) 
$arr2 = @(1..10) 

# 配列の末尾に要素を加える(push)
$arr2 += 50 

# 配列を結合し新しい配列を作成
$arr5 = $arr1 + $arr2 

# 配列にある要素が含まれるかどうか(ここではTrue)
$arr2 -contains 2 

5.ハッシュ

PowerShellにおけるハッシュは.NET Frameworkの System.Collections.Hashtableクラスのインスタンスである。

ハッシュ変数の宣言と代入

# 3つの要素を持つハッシュの宣言と代入
$hash1 = @{a=1;b=2;c=3}
 
# 空のハッシュの宣言と代入
$hash2 = @{} 

ハッシュの要素の参照と代入

# 要素の参照
$hash1.a 
$hash1["a"] 

#要素の代入
$hash1.b = 5
$hash1["b"] = 5 

ハッシュの操作

# ハッシュに要素を追加
$hash1.d = 4 
$hash1.Add("e",5)
 
# ハッシュの要素の削除
$hash1.Remove("a") 

# ハッシュのキーの取得
$keys = $hash1.Keys 

# ハッシュの値の取得
$values = $hash1.Values 

# ハッシュの要素を列挙
foreach ($key in $hash1.Keys)
{
    $key + ":" + $hash1[$key]
} 

# キーの存在確認
$hash1.Contains("b") 

6.制御文

if文

if (条件) {

}

if 〜 else文

if (条件) {

}
else{

}

if 〜 elsif 文

if (条件) {

}
elseif (条件) { 

} 

while/do文

while (条件) {

}

do {

} while (条件)

for文

for ($i = 0; $i -lt 5; $i++) {

} 

foreach文

foreach ($item in $items) {

} 

switch文

case を書かないのが特徴的。またスクリプトブロックを条件文に記述できる。

switch ($i) {
    1 {"1";break}
    2 {"2";break}
    {$_ -lt 5} {"5より小さい";break}
    default {"default句";break}
}
# ここで$iに配列を指定すると配列要素すべてに対してswitch文が実行される。 

比較演算子

比較演算子の一覧。PowerShellではPerlの文字列比較演算子のような記述をおこなうが、Perlとは異なり文字列も数値も同じ書式である。

$num1 -eq $num2 # $num1は$num2と等しい
$num1 -ne $num2 # $num1は$num2は等しくない
$num1 -lt $num2 # $num1は$num2より小さい
$num1 -gt $num2 # $num1は$num2より大きい
$num1 -le $num2 # $num1は$num2以下
$num1 -ge $num2 # $num1は$num2以上 

論理演算子

# 論理否定
$ret = -not $true
$ret = !$true

# 論理積
$ret = $true -and $false 

# 論理和
$ret = $true -or $false 

# 排他的論理和
$ret = $true -xor $false 

ビット演算子

# ビット単位の否定
$ret = -bnot 0x14F4

# ビット単位の積
$ret = 0x14F4 -band 0xFF00 

# 上記結果を16進数で表示する場合
$ret = (0x14F4 -band 0xFF00).ToString("X") 

# ビット単位の和
$ret = 0x14F4 -bor 0xFF00 

# ビット単位の排他的論理和
$ret = 0x14F4 -bxor 0xFF00 

7.サブルーチン

PowerShellのサブルーチンには関数とフィルタがある。関数とフィルタは呼び出し行の前で宣言する必要がある。 filter構文もfunction構文と並んで独自関数を記述するものだが、filter構文はパイプラインに渡されたオブジェクトをフィルタするのに用いる。 functionとの違いは、パイプラインに渡した配列を一度に処理するか(function)個別に処理するか(filter)

# 関数宣言の基本
function Get-Test {
    return "test"
}
# 注:returnを付けなくても関数内で出力された値はすべて呼び出し元に返却される。返却したくない場合は出力値をを[void]にキャストするか|Out-Nullに渡す。

# 引数を指定する場合
function Get-Test {
    param($param1,$param2)
    return $param1 + $param2
}
 
# 引数を指定する場合の簡易的な記述法
function Get-Test($param1,$param2) {
    return $param1 + $param2
} 

# 引数の型を指定する場合
function Get-Test {
    param([string]$param1,[string]$param2)
    return $param1 + $param2
} 

# 関数の呼び出し方(,区切りではなくスペース区切りであることに注意)
Get-Test "引数1" "引数2"

# 引数の順序はパラメータ名(引数名)を指定すると自由に指定可能
Get-Test -param2 "引数2" -param1 "引数1" 

# フィルタ宣言の基本
filter Get-Odd {
    if($_ % 2 -eq 1){
        return $_ 
    }else{
        return
    }
} 

# フィルタの使用
@(1..10) | Get-Odd

8.テキストファイル入出力

コマンドレットで可能。エンコーディングは日本語環境のデフォルトではShift-JIS。コマンドレット出力のテキストファイルへの書き出しに関してはリダイレクトも可能。この場合エンコーディングはUnicode。

$str1 = "testテスト"
Set-Content test.txt $str1 # 書き込み
Add-Content test.txt "追記" # 追記
$str2 = Get-Content test.txt # 読み込み

Set-Content test.txt $str1 -encoding UTF8 # UTF-8で書き込み

# リダイレクト
Get-Process > test.txt # 書き込み
Get-Process >> test.txt # 追記
Get-Process | Out-File test.txt -encoding UTF8 # エンコーディングを指定する場合

9.例外

PowerShellで例外が発生すると、デフォルトではエラーメッセージを表示し次の行を実行する(シェル変数$ErrorActionPreferenceの設定により挙動の変更可能)。VBでいうとOn Error Resume Nextに近い。エラーが発生すると$Errorにエラー情報の配列が格納され、$?にFalseが格納される。エラーをトラップするには次の構文を使用する。VBでいうとOn Error Goto lineに近い。

# すべてのエラーをトラップ
trap {

}

# エラーの型名を指定してトラップ
trap [System.Management.Automation.CommandNotFoundException] {

} 

# エラーを発生させる
throw "エラー"
throw New-Object NullReferenceException 

構造化例外処理(★2.0)

# 基本
try{

}
catch{

}
finally{

} 

# エラーの型を指定してcatch
try{

}
catch [System.Net.WebException],[System.IO.IOException]{

}

10.知っておいたほうがよい文法

行継続文字

1行にすると長いコードを複数行に書くには行継続文字`を用いる。VBの_。

$items = Get-ChildItem a*,b*,c*,d*,e* `
-force -recurce 

ただし以下のような場合は`を使用しなくてもよい

$items =
    Get-ChildItem a*,b*,c*,d*,e* -force –recurse
    
Get-Process | 
    Where-Object {$_.handles -gt 500} |
    Select-Object -first 5 

ステートメント分割

ステートメントを分割するには改行コードもしくは;を使用する。VBの:。JavaScriptと同様、文末に;はつけてもつけなくてもよい。

$i = 1; $j = 5; $k = $i + $j 

ヒア文字列

複数行の文字列を記述する方法。

$str = @"
aaaaaa
bbbbb
cccc
ddd
ee
"@ 

.NET Frameworkクラスの利用

.NET Frameworkに含まれているクラスのプロパティやメソッドを使用できる。基本的に完全修飾名を指定しなければいけないが、"System."は省略可能。また、intなど型エイリアスがいくつか定義されている。

# スタティックメンバの使用
[System.Math]::Pow(2,8) 

# インスタンスの生成とメソッドの実行
$arrayList = New-Object System.Collections.ArrayList
$arrayList.Add("a") 

# コンストラクタがある場合。複数ある場合は配列として指定
$message = New-Object System.Net.Mail.Message from@example.com,to@example.com

# COMオブジェクトの生成
$wshShell = New-Object -com WScript.Shell 

# デフォルトで読み込まれていないアセンブリを読み込む
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[System.Windows.Forms.MessageBox]::Show("hello!") 

# クラスにどんなメンバがあるかの確認
# インスタンスメンバ
Get-ChildItem | Get-Member 

# スタティックメンバ
[math] | Get-Member -static 

キャスト

-asを使った場合はキャスト失敗時もエラーにならずNullが格納される。

$dt = [System.DateTime]"2010/02/13"
$dt = "2010/02/13" -as [System.DateTime] 

ユーザー定義オブジェクト

PowerShellにはクラスを定義する構文はないが、空のオブジェクト(PSObject)を生成し、任意のプロパティ(ノートプロパティ)を付加することができる。

$obj = New-Object PSObject
$property = New-Object System.Management.Automation.PSNoteProperty "Name","名前"
$obj.PSObject.Members.Add($property) 

シェル変数

あらかじめ定義されている変数。シェル変数には自動変数(変更不可能)とユーザー定義変数(変更すると挙動を変更することができる)がある。自動変数の例を挙げる。

$_ :現在パイプラインにわたっているオブジェクト
$args :関数やスクリプトに与えられたパラメータの配列
$pshome :PowerShellがインストールされているフォルダのフルパス
$MyInvocation :スクリプトの実行情報。$myInvocation.ScriptNameでスクリプトのフルパス取得(★2.0)。$myInvocation.MyCommand.Path(1.0の場合)
$true :true。
$false :false。
$null :null。

 

サブ式

$()内には複数行のコードが記述できる。

$arr = $(1;2;1+4)

式モードとコマンドモード

PowerShellの構文解析は式モードとコマンドモードがある。式モードは通常のモード。コマンドモードは引用符がなくても文字列を文字列として扱う。コマンドレットのパラメータなどはコマンドモードで扱われる。ただしコマンドモードになるところでも()もしくは$()もしくは@()をつけるとその中身は式モードとして解釈、実行される。

$i = 1 + 1 # 式モード
Write-Host aaa # コマンドモード(表示:aaa)
Write-Host aaa bbb # コマンドモード(表示:aaa bbb)
Write-Host 1+1 # コマンドモード(表示:1+1)
Write-Host (1+1) # 式モード(表示:2)
$itemCount = @(Get-ChildItem).Length # 式モード

実行演算子とスクリプトブロック

&演算子を用いるとスクリプトブロック{}の内容を実行できる。この場合、スクリプトブロック内のコードは別スコープになる。

$script = {$i = 1+6; Write-Host $i}
&$script
& 'C:\Program Files\Internet Explorer\iexplore.exe' # パスにスペースの含まれるファイルを実行したりするのにも使える

フォーマット演算子

-f演算子を使うと、.NET Frameworkのカスタム書式が使用可能。

"{0:#,##0}Bytes" -f 38731362 # 表示:38,731,362Bytes

バイト数の簡易表記

$i = 1KB # 1024が代入される
$i = 1MB # 1048576が代入される
$i = 1GB # 1073741824が代入される

そのほかの基礎文法最速マスターへのリンク

プログラミング基礎文法最速マスターまとめ - ネットサービス研究室
http://d.hatena.ne.jp/seikenn/20100203/programmingMaster

PowerShellの詳しい機能解説についてはこちらの記事を参照してください。
PowerShell的システム管理入門 ―― PowerShell 2.0で始める、これからのWindowsシステム管理術 ―― ─ @IT
進化したPowerShell 2.0 ─ @IT

文法や機能について詳しく学びたい方には書籍もあります
Windows PowerShellポケットリファレンス
PowerShellによるWindowsサーバ管理術

元記事:http://blogs.wankuma.com/mutaguchi/archive/2010/02/13/186034.aspx

2008/02/25

複数の戻り値

Rubyで気に入ったところ

PowerShellはどうなのかというと

function hoge{
	1000
	3000
	"fuga"
	"moge"
}

$a = hoge
$a | % {$_}
$a.GetType().FullName

実行結果

PS D:\script> D:\script\test\test.ps1
1000
3000
fuga
moge
System.Object[]

どうですか?気持ち悪いでしょうw returnすらいらないんですよ(returnも使えますが)。逆に言うと関数内でコンソールに値を出力する行がある場合、それを呼び出し元に返したくない場合は[void]にキャストするか、|Out-Nullにパイプで渡します。結構はまりやすいので注意が必要です。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2008/02/25/124861.aspx

Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

mailto: mutaguchi at roy.hi-ho.ne.jp

Awards

Books

Twitter