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追記)と、ここまで書いておいて、この方法では下流で発生した例外には対処できますが、上流で例外が発生した場合はリソース解放が実行されないという問題に気付きました…。どうすればいいんだ…。
2014/12/22
PowerShellでMMLシーケンサーを作ってみた(その2)
はじめに
この記事はPowerShell Advent Calendar 2014の22日目の記事です。
前回はMMLをパースし、音楽をBeepで再生するところまで作りました。
MMLはテキストデータなので、テキストエディタで入力しても良いのですが、どうせなら、PowerShellでエディタも作ってしまいましょう。
コード
いきなりですが、エディタの本体コードを。
function Enter-MusicEditor { $keyMap=@{ A="<G+4>" Z="<A4>" S="<A+4>" X="<B4>" C="C4" F="C+4" V="D4" G="D+4" B="E4" N="F4" J="F+4" M="G4" K="G+4" ","="A4" L="A+4" "."="B4" "/"=">C4<" ":"=">C+4<" "\"=">D4<" "]"=">D+4<" "R"="R4" } $mml=@() cls while($true) { $k = [System.Console]::ReadKey($true) if($k.Key -eq [System.ConsoleKey]::Escape -or $k.Key -eq [System.ConsoleKey]::Enter) { cls break } elseif($k.Key -eq [System.ConsoleKey]::Backspace) { if($mml.length -eq 1) { $mml=@() } elseif($mml.length -ge 2) { $mml=$mml[0..($mml.length-2)] } cls [console]::write((-join $mml)) } else { $key=$k.KeyChar.ToString().ToUpper() if($keyMap.Contains($key)) { $mml+=$keyMap[$key] [console]::write($keyMap[$key]) Invoke-Mml $keyMap[$key] } } } -join $mml }
使い方
PowerShellコンソール上(ISEは不可)で、前回公開した、ConvertFrom-MML、Invoke-Beep、Invoke-Mmlの3つの関数をまず読み込み、続いて上記のEnter-MusicEditor関数を読み込みます。
この状態でコンソール上でEnter-MusicEditorを実行すると、MMLエディタモードに入ります。
MMLエディタモードでは、PCのキーボードを鍵盤代わりにして入力できます。
たとえば、Cキーを押下すると、「ド」の音がBeepで鳴り、コンソールに"C4"と出力されます。同様にFキー押下で「ド♯」が鳴り"C+4"と出力されます。どのキーがどの音に対応しているかは、上記コードの$keyMap変数に格納された連想配列を参照してください。
(なんとなくですけど、楽器の鍵盤の配置と合わせてあります)
Backspaceキーを押下すると、直前の入力を削除できます。
EnterキーかEscapeキーを押下すると、エディタモードを終了します。
出力したMMLは、コンソール上に表示が残るので、あとはコピーしてInvoke-Mml関数に渡してやると演奏することができます。
また、$mml=Enter-MusicEditorとしてやると、入力したMMLをそのまま変数に格納できます。この場合だとInvoke-Mml $mmlと実行すれば演奏できます。
解説
技術的には全然大したことをしてないですが、一応解説。
まず、[System.Console]::ReadKey()で入力したキーコードを判別します。引数に$trueを指定すると、入力したキー名をそのままコンソールに出力するのを抑制できます。
ReadKeyメソッドはConsoleKeyInfoオブジェクトを返します。アルファベットキーについてはKeyCharプロパティの値、特殊キーについてはConsoleKey列挙体を返すKeyプロパティの値で調べることができます。
あとは入力キーから対応する音名を連想配列$keyMapから取ってきて、その音名を[System.Console]::Write()でコンソール出力すると同時に、Invoke-Mml関数でその音をBeepとして鳴らしているだけです。
まとめ
入力も再生もできるようになったので、これはもう完全にシーケンサーですね! いや、やはり無理があるか…。
しかし、他のMMLコンパイラと併用して、ちょっと演奏しながらMML入力したいな、というときにもしかすると役に立つかもしれません。
ちょっと機能が少なすぎるんで、せめて↑で半音上げ↓で半音下げ、→で伸ばす←で短く、くらいはそのうち実装してみたいですね。
まあ、MML作成にはあんまり役に立たないかもしれませんが、PowerShellでコンソールの入出力を制御するのは、こういう単純なものなら意外と簡単にできるという、サンプルにはなるかと思います。
2013/01/20
FizzBuzz再び
PowerShellでFizzBuzz問題をいかに短く書くかというのは、人類にとっての太古からの命題であり、色々な方がチャレンジしています。
以下は国内でのチャレンジを、 日時、チャレンジャー名、コード文字数(半角スペース消去後)、初出アドレス で時系列にまとめたものです。
2007/11/06 牟田口 89文字 リンク
2007/11/07 囚人さん 86文字 リンク
2007/11/07 よこけんさん 75文字 リンク
2007/11/13 よこけんさん 57文字 リンク
2013/01/19 guitarrapcさん 57文字 リンク
私の現在の最短コードはこれです。PowerShell 3.0でしか動きませんが、51文字です。
1..100|%{($t="fizz"*!($_%3)+"buzz"*!($_%5))+$_[$t]}
PowerShell 2.0でも動くバージョンは以下。54文字です。
1..100|%{($t="fizz"*!($_%3)+"buzz"*!($_%5))+@($_)[$t]}
きっと解説は不要だと思いますが蛇足を承知で少しばかり。
$_%3 は、剰余を求める演算子%を使っているので、$_が3の倍数のとき0を返します。
!(0)とすると、0はboolに型変換され$falseとなり、その論理否定なので!(0)は$trueになります。
”fizz”*$true とすると右辺はintに型変換されるので”fizz”*1が評価され、”fizz”を返します。PowerShellでは「文字列*整数値」で文字列を整数値回繰り返した文字列を返すことを利用しています。
同じことを”buzz”に対しても行い、結果を+で連結します。このとき、”fizz”か”buzz”か”fizzbuzz”か””(空の文字列)のいずれかを返します。得られた値を@とします。
($t=@) とすると$tに@の値を入れつつ、@の値を返します。
@($_)[$t] とすると、$tが””(空の文字列)の場合は型変換され@($_)[0]が評価されます。よって、$tが””のときは@($_)の0番目の要素、$_、すなわち元の数値が取り出されます。最後に””と元の数値を+で連結したものが出力されるので、結果として数値のみが出力されます。
$tが”fizz”か”buzz”か”fizzbuzz”の場合は@($_)[$t]は配列の範囲外なので$nullを返します。よって$t+$null、すなわち$tの文字列がそのまま出力されます。
PowerShell 3.0だと非配列変数でも[]演算子を使用することができます。よって$_[0]は$_と等しく、$_[文字列]は$nullです。これによって@($_)のように配列化する必要がなく、3文字短縮できたわけです。
2011/09/30
スクリプトブロックパラメータのススメ
いきなりですが、PowerShellで「カレントディレクトリに含まれる.txtファイルの拡張子をすべて.logに変更する」方法がぱっと思いつくでしょうか?
コマンドプロンプトなら
ren *.txt *.log
で一発なのですが、PowerShellでrenコマンドに対応するコマンドレットであるRename-Itemコマンドレットを使って
Rename-Item -path *.txt -newName *.log
と書くことはできません。Rename-Itemコマンドレットの-pathパラメータと-newNameパラメータはワイルドカード文字を受け付けないからです。
ではどう書くのか。Get-ChildItemコマンドレットの-pathパラメータはワイルドカード文字を使うことができます(Get-Help Get-ChildItem -fullを調べるとpathパラメータの「ワイルドカード文字を許可する」はfalseになってますが、実際はワイルドカードが使えます)。よってGet-ChildItemでワイルドカードを用いてファイル一覧を取得し、それをRename-Itemコマンドレットにパイプで渡すとよさそうです。Rename-Itemの-pathパラメータは「パイプライン入力を許可する true (ByValue, ByPropertyName)」なので、パイプ経由でオブジェクトを渡すとこのパラメータに値が渡ります。なお、ByValueなどの意味は以前書いたエントリを参考にしてください。では書いてみましょう。
Get-ChildItem *.txt | Rename-Item -newItem *.log
あれ、新しい名前のほうのワイルドカードはどうすればいいんだ?というわけでこれでは駄目で、まだ一工夫が必要です。
素直に考えると、Get-ChildItemの結果(FileInfoオブジェクトの配列)をForEach-Objectで列挙して、その各要素でNameプロパティを元にRename-Itemコマンドレットを実行するというのが思いつきます。
Get-ChildItem *.txt | %{Rename-Item -path $_.Name -newName ($_.Name -replace "\.txt`$",".log")}
注: -replace演算子の右辺配列の最初の要素は正規表現を指定します。なので正規表現における特殊文字「.」は「\」でエスケープする必要があります。さらに拡張子以外の文字が置き換わらないように文字列の末端を表す「$」を使用します。「$」はPowerShellにおいて特殊文字なので「`」でエスケープします。
しかしこれはなんかNameプロパティの値を2回も参照してて冗長ですしあまりやりたくないですね。そもそもせっかくRename-Itemコマンドレットの-pathパラメータにパイプライン経由で直接オブジェクトを流し込める利点を生かせていません。
そこで登場するのが、このエントリのタイトルにもある「スクリプトブロックパラメータ」です。実はPowerShellには任意のコマンドレットパラメータにスクリプトブロックを指定する機能があるのです。コマンドレットパラメータは型が指定されていますが、これが<scriptblock>である必要はなく、<string>でも<int>でも何でもOKです。したがって、冒頭の問題の回答は次のように記述することができます。
Get-ChildItem *.txt | Rename-Item -newName {$_.Name -replace "\.txt`$",".log"}
このように、-newNameパラメータの型は<string>であるにも関わらず、スクリプトブロックを指定することができるのです。このスクリプトブロック内の$_は、パイプラインに渡されたオブジェクト配列の一要素です。つまりここではFileInfoオブジェクトになります。
注:この例だとファイルはカレントディレクトリにあるものが対象になるので、カレントディレクトリ以外で実行する場合はNameプロパティの代わりにFullNameプロパティを使ってフルパスを指定してください。
この機能、マイナーだと思いますが知っているとずいぶん楽になるケースが多いと思うので、ぜひ覚えておくことをお勧めします。しかし実はこの例題、Rename-Itemコマンドレットのヘルプの例4そのままだったりします。私はそこの解説を読んでもいまいち仕組みが分かりませんでした。Flexible pipelining with ScriptBlock Parameters - Windows PowerShell Blog - Site Home - MSDN Blogsという記事を読んでようやくこれがPowerShellの機能だと認識した次第です。
まあ、それでもrenコマンドのお手軽さには負けますけども、柔軟性に関してはもちろんPowerShellのほうが圧倒的に優れているのでそこは我慢するしかないのかなあ、と思います。どうしても簡単に書きたい場合は
cmd /c ren *.txt *.log
とかしてくださいませ。
ちなみにこの機能はユーザーが定義した関数では原則使用できないようです。ただ例外があって、次のような関数定義をしておくと大丈夫でした。
function test { param([parameter(ValueFromPipeline=$true)][string]$str) process { $str } }
ポイントはパラメータにparameter属性を指定して、ValueFromPipelineもしくはValueFromPipelineByPropertyNameを$trueにすることと、型名を指定すること(ここでは<string>)です。こうしておけば
dir|test -str {$_.fullname}
のようにして、コマンドレットの場合と同様にスクリプトブロックパラメータを使うことができます。属性と型指定どちらかが欠けているとスクリプトブロックが展開されずそのまま-strパラメータに渡ってしまうようです。
元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/09/30/203768.aspx
Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー