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.aspx2007/07/21
「パイプライン入力を許可する: true (ByPropertyName)」の意味
Get-Helpコマンドレットに-fullオプションを付けると、コマンドレットのパラメータの説明に「必須」、「位置」、「既定値」、「パイプライン入力を許可する」、「ワイルドカード文字を許可する」という項目が追加されます。この中で「パイプライン入力を許可する」がtrueになっている場合は、パイプラインからの入力がそのパラメータに渡されるという意味なのですが、これにはByValueとByPropertyNameの二種類があります(同時に指定されていることも)。
この意味お分かりになられますか?
mixiコミュでいろいろと議論した結果、ようやく分かったのでここでご報告しておきます。
ByValueはオブジェクトがそのまま渡ります。これは特に問題ないでしょう。
ByPropertyNameは、パイプを渡ってきたオブジェクトのプロパティが、パラメータ名と一致した場合、そのプロパティをパラメータとして解釈するという意味です。
具体的にGet-ChildItemコマンドレットを取り上げましょう。
Get-ChildItemコマンドレットは-pathパラメータがtrue (ByValue, ByPropertyName)、-literalPathパラメータ(エイリアスは-PSPath。ちなみにパラメータのエイリアスを調べるには(Get-Command Get-ChildItem).parametersetsのようにするとパラメータの一覧が出ますので、そのAliasを見てください)がtrue (ByPropertyName)です。
よって、入力オブジェクトにPathプロパティがあればその値が-pathパラメータに渡ります。(なければ入力オブジェクトがそのまま-pathパラメータに渡されます)。また、入力オブジェクトにLiteralPathプロパティまたはPSPathプロパティがあれば、-literalPathパラメータにその値が渡ります。これを検証します。
Get-ChildItemコマンドレットの戻り値はファイルシステムプロバイダにおいてはFileInfoオブジェクトとDirectoryInfoオブジェクトを含んだ配列です。これらのオブジェクトにはPSPathプロパティがあるので、この結果をパイプで次のGet-ChildItemコマンドレットに渡すと、そのPSPathプロパティが、-literalPathパラメータに渡ります。すなわちこういうことです。
PS C:\script> Get-ChildItem a*|Get-ChildItem ディレクトリ: Microsoft.PowerShell.Core\FileSystem::C:\script Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2007/07/20 18:33 0 a.txt -a--- 2007/07/21 16:17 8826 about_Alias.help.txt
このコマンドに意味があるかどうかは別にして、そういうことが可能だということです。
もっと分かりやすいと思われる例を示しましょう。$aというオブジェクトを作成し、それにAdd-MemberコマンドレットでPathという名前のNotePropertyを追加します。そして$aをパイプラインを通じてGet-ChildItemコマンドレットに渡すとどうなるかご覧ください。
PS C:\script> $a = New-Object PSObject PS C:\script> $a = $a | Add-Member noteproperty Path "a*" -passthru PS C:\script> $a.path a* PS C:\script> $a|Get-ChildItem ディレクトリ: Microsoft.PowerShell.Core\FileSystem::C:\script Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2007/07/20 18:33 0 a.txt -a--- 2007/07/21 16:17 8826 about_Alias.help.txt
というわけで無事、Pathプロパティが-pathパラメータに渡っていることがお分かりいただけると思います。
元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/07/21/86361.aspx2007/06/10
ネストされたスクリプトブロック内での$_
たとえば、2階層下のフォルダまで(無限階層じゃないのがミソ)のファイル・フォルダのフルパスを取得するスクリプトを考えます。
dir|%{$_.Fullname;if ($_.PSIsContainer){dir $_|%{$_.Fullname}}}
こんな感じでしょうか。
実行例。-recurseパラメータをつけた場合は3階層目までさらいますが、上記のスクリプトは二階層目で止まります。
PS C:\s> dir -recurse |%{$_.fullname}
C:\s\t
C:\s\新規 「サクラ」用音楽テキスト.mml
C:\s\t\u
C:\s\t\新規 Microsoft Office PowerPoint プレゼンテーション.pptx
C:\s\t\u\v
C:\s\t\u\v\新しいビットマップ イメージ.bmp
PS C:\s> dir|%{$_.Fullname;if ($_.PSIsContainer){dir $_|%{$_.Fullname}}}
C:\s\t
C:\s\t\u
C:\s\t\新規 Microsoft Office PowerPoint プレゼンテーション.pptx
C:\s\新規 「サクラ」用音楽テキスト.mml
PS C:\s>
さて、このForEach-Objectコマンドレット(エイリアスは%)はネストされています。最初の$_.Fullnameは最初のdir(Get-ChildItemコマンドレット)が返すFileInfo/DirectoryInfoオブジェクトのFullnameプロパティを見ています。その次の$_.PSIsContainerもそうですね。$_がフォルダだったら次のdirを実行します。dir $_の$_も最初のオブジェクトです。さてここでふたたびForEachループを回します。ここで実行される$_.Fullnameの$_は、直前のdir $_が返したFileInfo/DirectoryInfoオブジェクトを参照しています。
このようにスクリプトブロックをネスト化した場合、$_にはもっとも内側のパイプラインを通ったオブジェクトが格納されます。
さて、では内側のスクリプトブロックから、外側のスクリプトブロックのパイプラインにわたったオブジェクトを参照するにはどうするのでしょう?この場合、たとえば最初から見て1階層下のフォルダC:\s\tのdirを内側のスクリプトブロックで回す際、外側のC:\s\tのフルパスも合わせて表示したいとき、内側から外側の$_を参照する必要がでてきます。このやり方は実は分かりません(汗 できるのかな〜?
一晩寝たら思いつきました。コロンブスの卵です。外側の$_をほかの変数に入れちゃえばOKですね。
PS C:\s> dir|%{$_.Fullname;if ($_.PSIsContainer){$outer=$_;dir $_|%{"[" + $outer .Fullname + "]";$_.Fullname;}}}元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/06/10/80193.aspx
2006/11/07
dir /b に相当するコマンドは?
cmd.exeでdir /bとすると、カレントにあるファイル名・ディレクトリ名だけを出力します。以下、例。
C:\WINDOWS\system32\windowspowershell\v1.0>dir /b certificate.format.ps1xml dotnettypes.format.ps1xml examples filesystem.format.ps1xml help.format.ps1xml ja powershell.exe powershellcore.format.ps1xml powershelltrace.format.ps1xml pwrshmsg.dll pwrshsip.dll registry.format.ps1xml types.ps1xml
同じことをPowerShellにやらせるにはどうすればいいか、考えてみました。
【案1】
PS C:\WINDOWS\system32\WindowsPowerShell\v1.0> ls|format-wide -c 1 ディレクトリ: Microsoft.PowerShell.Core\FileSystem::C:\WINDOWS\system32\Win dowsPowerShell\v1.0 [examples] [ja] certificate.format.ps1xml dotnettypes.format.ps1xml filesystem.format.ps1xml help.format.ps1xml powershell.exe powershellcore.format.ps1xml powershelltrace.format.ps1xml pwrshmsg.dll pwrshsip.dll registry.format.ps1xml types.ps1xml
Format-Wideコマンドレットを使った例。うーんちょっと違う。でもディレクトリに[]が付くのでわかりやすいかも。これはこれで。
【案2】
PS C:\WINDOWS\system32\WindowsPowerShell\v1.0> ls|select name Name ---- examples ja certificate.format.ps1xml dotnettypes.format.ps1xml filesystem.format.ps1xml help.format.ps1xml powershell.exe powershellcore.format.ps1xml powershelltrace.format.ps1xml pwrshmsg.dll pwrshsip.dll registry.format.ps1xml types.ps1xml
Select-Objectコマンドレットを使ってNameプロパティだけをもったPSCustomObjectのArrayを作っているのでこんな感じに出力されます。
Name
----
が邪魔ですね。
【案3】
PS C:\WINDOWS\system32\WindowsPowerShell\v1.0> ls|%{$_.name} examples ja certificate.format.ps1xml dotnettypes.format.ps1xml filesystem.format.ps1xml help.format.ps1xml powershell.exe powershellcore.format.ps1xml powershelltrace.format.ps1xml pwrshmsg.dll pwrshsip.dll registry.format.ps1xml types.ps1xml
これでdir /bと同じ結果になりました。Foreach-ObjectでNameプロパティだけを取り出して出力しているわけです。
【案4】
PS C:\WINDOWS\system32\WindowsPowerShell\v1.0> ls|split-path -leaf examples ja certificate.format.ps1xml dotnettypes.format.ps1xml filesystem.format.ps1xml help.format.ps1xml powershell.exe powershellcore.format.ps1xml powershelltrace.format.ps1xml pwrshmsg.dll pwrshsip.dll registry.format.ps1xml types.ps1xml
Split-Pathコマンドレットを-leafオプション付きで使っても同様の出力が得られました。これ実は何故こうなるのかよくわからないんですけど、たぶんパイプを渡るときに、System.IO.DirectoryInfoオブジェクトやSystem.IO.FileInfoオブジェクトのNameプロパティが渡されてるか、ToString()メソッドが実行されているのでしょうね。ps1xmlファイルの何らかの記述でこうなっているのかもしれません。でもまあ仕組みが分からなくてもこの動作は非常に合理的であります。
【案5】
PS C:\WINDOWS\system32\WindowsPowerShell\v1.0> ls -name examples ja certificate.format.ps1xml dotnettypes.format.ps1xml filesystem.format.ps1xml help.format.ps1xml powershell.exe powershellcore.format.ps1xml powershelltrace.format.ps1xml pwrshmsg.dll pwrshsip.dll registry.format.ps1xml types.ps1xml
ていうかこんなことをしなくても、Get-ChildItemコマンドレットには-nameオプションがあり、dir /bと同じ効果が得られるのでした。ヘルプの見落としでこんな回りくどいことを考えていたのです。すみません、こんなオチで。でもまあ同じことをやるのに色んな手段があるということが分かったので良しとしましょう。
元記事:http://blogs.wankuma.com/mutaguchi/archive/2006/11/07/43902.aspx
Copyright © 2005-2018 Daisuke Mutaguchi All rights reserved
mailto: mutaguchi at roy.hi-ho.ne.jp
プライバシーポリシー