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

2011/09/08

PowerShellでクリップボードを読み書きする方法については5年ほど前にクリップボードアクセス(未完)II という記事を書きました。タイトル通り未完でして、このたび完結編を書こうと思いました。

System.Windows.Forms.Clipboardクラスを使う場合は先の記事にあるようにPowerShellをSTAで動作させる必要があります。ver.1の当時はその方法がありませんでしたが、ver.2ではpowershell.exeに-staパラメータが追加され、STAでの実行が可能となっています。

-staパラメータを使用しない場合は相変わらずこの方法は使えません。powershell.exe –staのプロセスを逐次起こして、クリップボードアクセス部分だけSTAの子プロセスでやらせることも不可能ではないですが、冗長ですしパフォーマンスの点で難があります。

ver.2ではAdd-Typeコマンドレットが追加され、任意のC#コードを動的に読み込むことができるようになりました。これを利用してクリップボードアクセスクラスを書くという手もあると思います。その中ではSTAのスレッドを起こしてクリップボードアクセスをすることになります。実際PowerShell Community Extensionsに含まれるOut-Clipboard/Get-Clipboardコマンドレットはそのような実装がされているようです。ですが動的にコードを読み込みコンパイルはやはりパフォーマンス上不利ですし、PSCXをインストールできない場合などもあるでしょう。

ところでSystem.Windows.Formsに含まれるTextBoxクラスはその名の通りテキストボックスコントロールなのですが、このコントロールにはCopy()メソッドとPaste()メソッドがあり、これらを使うとクリップボードに書き込み、クリップボードから読み込みが可能です。TextBoxクラスの良いところは、STAである必要がないところです。この方法を最初に提唱したのはこちらの方かな?

この方法を用いた関数もすでに公開されているのですが、PowerShellの作法に則った使い方ができるように少し改良を加えました。具体的には

Set-ClipBoard:  配列の指定、パイプラインからの入力を可能にした。

Get-ClipBoard: クリップボードの中身が複数行のテキストの場合、文字列配列を返すようにした。(Get-Contentと同様)

という変更を加えています。

function Set-ClipBoard
{
    param([parameter(Mandatory=$true,
            ValueFromPipeline=$true)][object[]]$InputObject)
    begin
    {
        Add-Type -AssemblyName System.Windows.Forms
        $tb = New-Object System.Windows.Forms.TextBox
        $tb.Multiline = $true
        $out=@()
    }
    process
    {
        $out+=$InputObject
    }
    end
    {
        $tb.Text = $out -join "`r`n"
        $tb.SelectAll()
        $tb.Copy()
    }
}

function Get-ClipBoard
{
    Add-Type -AssemblyName System.Windows.Forms
    $tb = New-Object System.Windows.Forms.TextBox
    $tb.Multiline = $true
    $tb.Paste()
    $tb.Text -replace "`r`n","`n" -replace "`r","`n"  -split "`n"
}

使い方例

# ファイルのフルパスをクリップボードに格納
Get-ChildItem|select -expand fullname|Set-ClipBoard

# CSV文字列がクリップボードに入っている場合以下のコマンドを実行すると、
# クリップボードの中身が対応するHTML tableタグに置換される
Get-ClipBoard|ConvertFrom-Csv|ConvertTo-Html -Fragment|Set-ClipBoard

やっぱりパイプで繋ぐのがPowerShellの醍醐味ですよね!

元記事:http://blogs.wankuma.com/mutaguchi/archive/2011/09/08/202547.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

2007/08/30

パイプを多用してもスクリプトの可読性を落とさないには?
http://blogs.wankuma.com/mutaguchi/archive/2007/08/01/88060.aspx

という記事で紹介した方法は、各行にコメントが入れられないという問題点があったので、行継続文字`を使わない方法を推奨します。

Get-ChildItem |                # カレントのファイルを列挙
    ? {$_.Length -gt 100KB} |  # 容量が100KBより大きいものを抽出
    % {$_.FullName}            # FullNameプロパティを参照

演算子関係(+とかでも)では改行してもそのままちゃんと解釈されますね。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/08/30/92713.aspx

2007/08/13

たとえばHKCRの拡張子のキーの(既定)の値を取るのに

Get-ItemProperty registry::HKEY_CLASSES_ROOT\.* -name "(default)"

で取れるには取れるのですが、これを文字列にする(正式な)方法が分かりません。これをパイプでつなげて|%{"$_"."(default)"}とやっても(default)なんてプロパティはないと怒られます。

Get-ItemProperty registry::HKEY_CLASSES_ROOT\.* -name "(default)" -ErrorAction SilentlyContinue|
%{"$_".ToString() -replace "^@\{\(default\)\=(.+)\}$","`$1"} 

というのを考えましたがどう見てもスマートではありません。何かいいアイデアはありませんでしょうか?,、と思ったら使い方が間違っていた。

Get-ItemProperty registry::HKEY_CLASSES_ROOT\.* -ErrorAction SilentlyContinue| 
%{$_."(default)"}

こうですね、すみません。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/08/13/90031.aspx

2007/08/01

PowerShellは何かとパイプを多用するので、いわゆるif文とかfor文とかだけを使うときに比べると可読性が落ちる傾向にあるように思います。そこで編み出した?のが次の記法です。カレントにある100KBより大きいファイルのフルパスを列挙するというものです。

Get-ChildItem | `
    ? {$_.Length -gt 100KB} | `
    % {$_.FullName}

ポイントは、

・パイプのあとに`記号(改行継続文字)を入れて改行
・スペースを4つ入れてインデント
・Where-Objectコマンドレットは?、ForEach-Objectコマンドレットは%で記述。
・|.`.?,%の前後にはスペースを入れる。

これだと1行に繋げて書くよりかなり見やすいですし分かりやすいですよね?もちろんコンソールに直で打ち込む時はこんなことしなくてもいいですが、スクリプトとして保存するときはこういうのがお勧めです。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/08/01/88060.aspx

2007/07/21

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.aspx

2007/05/21

.NET Frameworkをバリバリ使ってC#ライクに書けばわりと見やすいスクリプトが書けるが冗長である。コマンドレットをパイプでつないだり各種演算子を使うと見にくいが簡潔に書ける。
これが本題。

しかし前者のようなスクリプトはC#で書いちゃえばいいじゃんという話だ。csファイルに、コンパイルして実行するVBSでも関連付けておけばよいでしょう。js(JScript.NET)ならクラスを書かないでも大丈夫。COMを使うならスクリプトはWSHでもいい。

そう考えるとやはりPowerShellはコマンドレットとパイプを駆使するのが本道のように思う。ただ、どこまで込み入ったものを書くのか。重要なのは可読性と簡潔性のバランスだ。パイプを多用すれば簡潔だが読みにくい。パイプを使わなければ冗長になる。そのあたりをうまく調整していくのがPowerShell使いの課題だろう。

さて、込み入ったスクリプトの究極例は、込み入ったことを一行のスクリプト(ワンライナーという)にすることだ。これには通常のプログラミングとは違い、頭の体操が必要になってくる。しかも後でみると自分でもわからなかったりする。

なので、保存して実行するならワンライナーにする必要はないように思う。冗長でもわかりやすく書くほうがいい。ワンライナーはあくまでコンソール上で手で打ち込むことに意義があるのではないか。

だが、そのスキルを管理者は身につけるべきなのか?

現実的には、コンソール上でコマンドレットを2,3個のパイプで繋ぐ程度が限界なのではないかと思う。ワンライナーをその場で書くのは相当なスキルが要求されるように思う。ただ、幸いなことにPowerShellにはfunctionやfilterを記述したスクリプトファイルをプロファイルとして読み込むことができるので、込み入った処理はあらかじめ関数化、フィルタ化しておくとよいだろう。そのときはワンライナーである必要はないのは先に述べたとおり。

でもPowerShellのワンライナーを極めた人を見てみたい気もする。UNIX系ではいますよね。私?無理ですよw

というわけでPowerShellスクリプトの考察でした。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2007/05/21/77517.aspx

2006/12/18

12/16、わんくま大阪勉強会#4でPowerShellのセッションをさせていただきました。
いろいろと拙いところがありましたが、ご静聴いただきありがとうございました。

いくつかフィードバックやご質問をいただきました。

・PPTに書いてること以外をしゃべってほしい
→資料だけを読まれる方のためにPPTだけを見ても分かるようにしたのですが、せっかく参加された方のためにもそれ以外のことをしゃべったほうがいいですね。今回は時間がなくてなかなか難しかったのですが次回は改善させていただきたいです。

・デモが少ないので増やしてほしい
→これも時間の関係で今回ちょっとしかやれなかったのですが、次回1/12(金)はたっぷりやれると思います。シェルを叩くだけでなくスクリプトをその場で書いていくのもいいですね(今回他の方のセッションを見て思ったのですが)。ネタを仕込んでおかなければ…。

・実際に使える実用的なスクリプトサンプルが見たい
→時間の関係でスクリプトに関してはほとんど取り上げられなかったのですが、これも次回きちっとやります。PowerShellならではのスクリプトを鋭意研究中です。あまり運用系のことがわからないので、「こんなスクリプトがあったら嬉しい」などのご意見をお待ちしております。メールやここのコメント欄などでいただきたいと思います。

・IronPythonとの住み分けは?
→申し訳ありませんが、まだIronPythonを弄るところまでは行っていません。おいおい、研究していきたいと思います。現時点では個人的にはPowerShellは運用系、IronPythonは開発系なのかなと思います。

・安定性は?
→少し重いというのがありますが、安定性はまずまずなんじゃないでしょうか。

・仕組みは?
→現時点では正直なところ、なぜ動作するのか?なぜ.NETのクラスが動的に呼び出せるのか?というところまでは追い切れていません。
Windows PowerShell in Actionという書籍に詳しい解説があるそうなので、これから読んで、みなさんにお伝えできたらいいなと思っています(購入してるのですが積読状態です(汗)。

あと中さんからエイリアスを使う上では注意が必要というコメントをいただきました。これまでのコマンドプロンプトと同名のエイリアスがあるが、それらを書くときは、オプションなどは従来とは異なるということを言っておかないと混乱する…ということですよね?>中さん
エイリアスはシェルを叩く際はデフォルトで定義されている分には積極的に使っていってもいいと思いますが(コマンドプロンプトと混同しないように、との前提が必要ですが)、スクリプトで使うときはあえてエイリアスを使うこともないかなぁと思います。
?や%などはエイリアスというか文法チックなので、こちらのほうが分かりやすいような気もします。
エイリアスとは少し離れますが、パイプを多用したスクリプトの分かりやすい記法というのも研究中です。PowerShellはやはりオブジェクトが渡るパイプが肝なので、スクリプトでも活用していきたいのですが、多用するとどうしても読みにくくなってしまいます。そのあたりをうまく解消できればいいですね。

次回(1/12(金))はまたよろしくお願いします。まだまだ勉強中なのでなかなか講演、執筆ともに拙かったり進捗が遅かったりして申し訳ありません。取り上げてほしいこと、スクリプトのネタなどがありましたらぜひフィードバックをお寄せください。

元記事:http://blogs.wankuma.com/mutaguchi/archive/2006/12/18/51695.aspx

2006/12/08

コマンドレットオンラインヘルプ作成
http://blogs.wankuma.com/mutaguchi/archive/2006/11/07/43965.aspx
の続編です。本文中のコマンドレットにリンクを張るようにしました。その他いろいろ改善。
旧バージョンをお使いの方は一度*.htmlを削除してください。ファイル名が変わりました。

function Sanitize{
    #サニタイズ処理
    param ([string]$strSource)
    return ($strSource.Replace("&","&").Replace("<","<").Replace(">",">"))
}
 
function MakeLink{
    #用語にリンクを張る
    param ([string]$strSource)
    foreach ($key in $keys){
        $strSource = $strSource -replace "(\s)($key)(\s)",
        ("`$1`$2`$3")
    }
    return ($strSource);
}
 
#キーワードを格納するArrayList
$keys = new-object System.Collections.Arraylist
get-help -category all | ?{"Cmdlet","Provider","HelpFile" -contains $_.category}|
%{if ($_.Name -ne $null) {[void] $keys.Add($_.Name)}}
 
# "Cmdlet","Provider","HelpFile"のカテゴリを持つヘルプをHTML化
get-help -category all | ?{"Cmdlet","Provider","HelpFile" -contains $_.category}|
%{"
" + 
$(MakeLink $(Sanitize (get-help $_.Name -detail|out-string))).Replace("`r`n","
") + "戻る
"| out-file($_.name + ".html")} $temp="" # ヘルプのHTMLのインデックスを作成する # Aliasのヘルプ get-help -category Alias |sort name| %{$temp+=""} # Cmdlet,Provider,HelpFileの各ヘルプ get-help -category all | ?{"Cmdlet","Provider","HelpFile" -contains $_.category} | sort category,name| %{$temp+=""} $temp+="
名前種類簡易説明
" + $_.name + "" + $_.category + "" + $_.synopsis + "
" + $_.name + "" + $_.category + "" + $_.synopsis + "
" out-file index.html -inputobject $temp

コマンドレットとパイプを多用するスクリプトの見やすい記法ってなんかないですかね?自分で読んでもよくわかりませんなこれw

あと気づいたこと。メソッドを呼び出すときはmethodname (parameter)のようにメソッド名と引数の間にスペースを入れてはいけない!VBSとかになれているとはまります。

関数の呼び出しはfuncname param1 param2のようにする(引数は,で区切るのではない!)。関数の戻り値を読むときは$(funcname param1 param2)のようにする。関数中で何か値が返されるとそれがそのまま関数の戻り値になる(returnは明示しなくてもいいということ。複数の値を返すと戻り値は[object[]]の配列になる。逆に値を返したくない場合は[void]にキャスト)

-replace演算子でサブマッチ文字列は$1,$2..に格納される。$は変数の頭文字なので ` (アクサン グラーブ)でエスケープし、`$1とする。もしくは''(シングルクォーテーション)でくくり'$1'のようにする。

そうそう、
get-help -category all | ?{"Cmdlet","Provider","HelpFile" -contains $_.category}|
は、なんで
get-help -category Cmdlet,Provider,HelpFile
にしなかったかというと、このようにしてもなぜかAliasが含まれてしまうからです。Cmdletを指定するとAliasが含まれる仕様のようです。
でも-containsの使い方がわかってよかったです(ちょっとトリッキー?)

元記事:http://blogs.wankuma.com/mutaguchi/archive/2006/12/08/49387.aspx

次のページへ

Copyright © 2005-2016 Daisuke Mutaguchi All rights reserved

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

Awards

Books

Twitter