コンソール アプリケーションの作り方 ≪ ++C++; // 未確認飛行 C ブログ より
PowerShell って .NET Framework 4 で作ったアセンブリ読み込めないんですよねぇ・・・。アップデートして欲しい・・・
確か CLR のバージョンって指定できたなぁと思い調べてみたらできました。
これで .NET 4.0 の新しいクラスを使用したり、アセンブリを読み込んだりすることができます。
[手順]
PowerShell のインストールフォルダ (%windir%\system32\WindowsPowerShell\v1.0) に次のファイルを作成する。
powershell.exe.config
<?xml version ="1.0"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true" >
<supportedRuntime version="v4.0.30319" />
</startup>
</configuration>
僕もまだ読み途中ではありますが、この本を簡単に紹介したいと思います。
本書は、PowerShell 開発メンバーの方が執筆され、MS のエバンジェリストチームの方々が監訳された本です。
260 に及ぶ「レシピ」を紹介していて、ページ数も 600 ページとボリューム満点です。
大きく分けると 3 つの部から構成されていて、第一部が「PowerShell の基本」、第二部が「一般的なタスク」、第三部が「管理者タスク」となってます。
本書を通じて、様々なレシピを身に付けることができます。本書で紹介されている豊富なレシピは初級者から上級者まで幅広い層の方々の役に立つことと思います。
そして、本書を一通り読み終えた後は、リファレンス本としても重宝することと思います。
ではここで、僕が本書を読んですぐに profile.ps1 ファイルに追加した、一つの関数をご紹介したいと思います。
function Prompt
{
$Host.UI.RawUI.WindowTitle = "$(Get-Location)";
$id = 1;
$historyItem = Get-History -Count 1;
if ($historyItem)
{
$id = $historyItem.Id + 1;
}
return "PS {0:00000}> " -f $id;
}
Prompt 関数を定義すると、PowerShell のプロンプトをカスタマイズすることができます。上記の Prompt 関数では、プロンプトにセッション履歴番号を表示するようにしています。それだけだと現在のパスがわからなくなってしまうので、現在のパスをウィンドウのタイトルに表示するようにもしています。
セッション履歴番号が一目でわかるようになったことで、Invoke-History コマンドレットがとてつもなく使いやすいものになります。例えばこんな感じです。
PS 00001> 1+1
2
PS 00002> 2+2
4
PS 00003> Invoke-History 2
2+2
4
PS 00004>
この関数は、「レシピ 1.3 シェル、プロファイル、プロンプトを管理する」にて紹介されていた関数をアレンジしたものです。アレンジと言っても、文字色を付けないようにしたり (オリジナルでは Write-Host コマンドレットを使って文字色を付けています)、セッション履歴番号をゼロパディングするようにした、といった程度です。
Prompt 関数を定義することでプロンプトをカスタマイズできることは元々知っていましたが、それを利用してセッション履歴番号を可視化するという発想は持っていませんでした。本書は、このような気付きも与えてくれます。
PowerShell from Japan!! - ここが日本のPowerShell情報発信基地
立ち上げたのは HIRO's .NET の HIRO さんです。
このサイトは、複数の Author がブログ形式で PowerShell の情報を発信するサイトで、今後、日本の PoSHer (PowerShell 好きな人々) に欠かせないサイトになると予想されます。
ちなみに、僕も Author として参加しています。
「PowerShell from Japan!!」では、Author 募集中です。
PowerShell の情報を発信したいという方、是非 Author として参加してみてください。
参加をご希望の方はこちらのフォームからその旨ご連絡お願いします。
# フォームから送信した内容は HIRO さんの元へ届きます。
# CodeZine では、一定期間を過ぎた記事の一部が、会員でないと閲覧できなくなりますので注意してください。
# 会員登録はこちらから無料で行えます。
Windows PowerShell 入門(1)-基本操作編:CodeZine
Windows PowerShell 入門(2)-基本操作編 2:CodeZine
Windows PowerShell 入門(3)-スクリプト編:CodeZine
Windows PowerShell 入門(4)-変数と演算子:CodeZine
Windows PowerShell 入門(5)-制御構文:CodeZine
Windows PowerShell 入門(6)-関数編1:CodeZine
Windows PowerShell 入門(7)-関数編2:CodeZine
で、本日掲載された第7回目の記事で、当ブログのプロファイル活用という記事を紹介して頂きました。
このテクニックは僕自身気に入ってまして、是非多くの方に知ってもらいたいと思っていました。HIRO さん、ありがとうございます m( _ _ )m
ところで、これまたご存知の方も多いかと思いますが、HIRO さんのサイトとブログでは、PowerShell の Tips がたくさん紹介されています。
PowerShell に興味のある方、CodeZine の連載と併せてチェックしておいた方がいいと思いますよ (^ω^ )
しかし (毎度のことながら)、PowerShell の柔軟さを持ってすれば、クロージャを実現することだって可能です。
今回は closure という名前の関数を作りました。この関数の引数に、クロージャとして機能させたいスクリプトブロックを渡せば、スクリプトブロックをクロージャ化できます。
例えば次のような使い方ができます。 (Wikipedia に掲載されている JavaScript のクロージャサンプルを移植)
function NewCounter
{
$i = 0;
return closure {
$i++;
return $i;
};
}
$counter = NewCounter;
&$counter; # 1
&$counter; # 2
&$counter; # 3
次のような使い方もできます。
function NewDecorator
{
param ([string]$decoration)
return closure {
param ([string]$text)
return $decoration + $text + $decoration;
};
}
$sharpDecorator = NewDecorator '#';
&$sharpDecorator "hoge"; # #hoge#
&$sharpDecorator "fuga"; # #fuga#
&$sharpDecorator "piyo"; # #piyo#
[ コード ]
クロージャを実現する closure 関数は次のようになっています。
この関数は更に InvokeClosureScript という関数を使用します。(正確には closure 関数が生成するスクリプトブロックの内部で使用します。)
closure 関数 + InvokeClosureScript 関数
function global:closure
{
param ([ScriptBlock]$private:script)
trap { break; }
# 引数の妥当性検証
if ($() -eq $script) { throw '引数 script が null です。' }
# 全てのクロージャを保存するクロージャストアを作成 (ハッシュテーブル)
if ($() -eq $global:ClosureStore)
{
Set-Variable 'ClosureStore' @{} -Scope 'global' -Option 'Constant, AllScope';
}
# GC に回収されているクロージャ (を格納しているハッシュテーブル要素) は、クロージャストアから削除
($ClosureStore.GetEnumerator() | ? { !$_.Value.IsAlive; }) | ? { $() -ne $_; } | % { $ClosureStore.Remove($_.Key); };
# 子スコープで環境 (自動変数を除く全ての変数) を取得し保存
$autoVariableNames =
@(
'$', '?', '^', '_', 'args', 'ConfirmPreference', 'ConsoleFileName', 'DebugPreference', 'Error', 'ErrorActionPreference',
'ErrorView', 'ExecutionContext', 'false', 'FormatEnumerationLimit', 'foreach', 'HOME', 'Host', 'input', 'LASTEXITCODE', 'lastWord',
'line', 'Matches', 'MaximumAliasCount', 'MaximumDriveCount', 'MaximumErrorCount', 'MaximumFunctionCount', 'MaximumHistoryCount', 'MaximumVariableCount', 'MyInvocation', 'NestedPromptLevel',
'null', 'OutputEncoding', 'PID', 'PROFILE', 'ProgressPreference', 'PSHOME', 'PWD', 'ReportErrorShowExceptionClass', 'ReportErrorShowInnerException', 'ReportErrorShowSource',
'ReportErrorShowStackTrace', 'ShellId', 'StackTrace', 'switch', 'true', 'VerbosePreference', 'WarningPreference', 'WhatIfPreference'
);
$private:environment = & { return Get-Variable | ? { $autoVariableNames -notcontains $_.Name }; };
# スクリプトと環境を組み合わせてクロージャを表す。
$private:closure =
New-Object 'PSObject' |
Add-Member 'Script' $script -MemberType 'NoteProperty' -PassThru |
Add-Member 'Environment' $environment -MemberType 'NoteProperty' -PassThru;
# GUID をキー、クロージャの弱参照を値とし、クロージャストアに保存
$private:closureId = [Guid]::NewGuid();
$ClosureStore.Add($closureId, [WeakReference]$closure);
# クロージャを実行するスクリプトを動的な文字列操作で構築 (スクリプトに GUID を埋め込むため)
$private:invokerText = "InvokeClosureScript `"$closureId`" `$Args;";
# テキストからスクリプトへ変換し、更に PSObject 化する
$private:invoker = [PSObject](Invoke-Expression "{ $invokerText }");
# 環境をスクリプトに結びつけることでスクリプトと環境の寿命を同一化する
Add-Member -InputObject $invoker -Name 'Closure' -Value $closure -MemberType 'NoteProperty';
return $invoker;
}
function global:InvokeClosureScript
{
param ([Guid]$private:closureId, [Array]$private:Args_)
# 指定した GUID に関連付いているクロージャをクロージャストアから取得 (クロージャは弱参照を使用して格納されている)
$private:closure = $ClosureStore[$closureId].Target;
# Null なら例外
if ($() -eq $closure) { throw '指定した ID に関連付けられたクロージャは存在しません。'; }
# 関数呼び出しを動的な文字列操作で構築 (param キーワードによる引数の受け取りが正常に機能するように)
$private:invokerText = 'param ([ScriptBlock]$private:script, [Array]$private:Args_) .$script';
for ($private:i = 0; $i -lt $Args_.Length; $i++) { $invokerText += " `$Args_[$i]"; }
# テキストからスクリプトへ変換
$private:invoker = Invoke-Expression "{ $invokerText }";
# 環境のロードとクロージャの実行は変数宣言を最小限にした子スコープで
$private:result =
&{
# 環境をロード
$Args[1].Environment | % { trap { continue; } $ExecutionContext.SessionState.PSVariable.Set($_); };
# クロージャを実行
return .$Args[0] $Args[1].Script $Args[2];
} $invoker $closure $Args_;
return $result;
}
[ closure 関数について ]
closure 関数は、受け取ったスクリプトブロックと環境 (自動変数を除く全ての変数) の組み合わせを一つのクロージャとして、クロージャストア (グローバルなハッシュテーブル) に保存します。クロージャストアのキーには、ランダムに生成された GUID を使用します。そして、この GUID を引数として InvokeClosureScript 関数を呼び出すスクリプト (インボーカー) を生成します。closure 関数はこのインボーカーを呼び出し元へと返します。
[ InvokeClosureScript 関数について ]
InvokeClosureScript 関数は、受け取った GUID を元にクロージャストアからクロージャ (スクリプトブロックと環境) を取得します。そして、環境をロードしてスクリプトブロックを実行するのですが、スクリプトブロックは環境がロードされるスコープと同一スコープで実行されます。これにより、本来親スコープで宣言されたはずの変数でもスクリプトブロックから変更することが可能となります。例えば冒頭の一つ目のサンプルコードでは、クロージャ (となるスクリプトブロック) の親スコープで $i が宣言されていますが、クロージャ内から $i を変更 (インクリメント) しています。
(なお、環境のロードとスクリプトブロックの実行は子スコープで行われますが、これは単に、InvokeClosureScript 関数内の変数がスクリプトブロックの実行に影響を与えないようにするためです。)
[ クロージャの寿命について ]
クロージャ (スクリプトブロックと環境) をクロージャストアに保存する際には、弱い参照を保存しています。これにより、クロージャは不要になると GC の対象になることができます。クロージャが GC に回収されただけでは、まだクロージャストアにエントリが残っていますが、次に closure 関数が呼び出された際にエントリも削除されます。
またクロージャは、インボーカーのノートプロパティとしても保存されています。これにより、インボーカーがどこからか参照されている限りは、クロージャは GC の対象になりません。
ただし、一つ注意点があります。インボーカーを変数に保持する代わりに次のように関数として保持してしまうと、(PSObject ではなく生のオブジェクトが保持されるため) ノートプロパティが失われてしまうのです。
$function:func1 = closure {}; # クロージャが GC の対象になってしまう
ノートプロパティが失われてしまうということは、クロージャが GC の対象になってしまうということです。一度変数に保持してから更に関数として保持すればノートプロパティは失われませんが、これも管理が複雑になるのでお勧めしません。
[ インボーカーのノートプロパティについて ]
インボーカーのノートプロパティにクロージャが保存されている理由はクロージャの寿命の制御のためですが、これは嬉しい副作用をもたらします。次のように、インボーカーのノートプロパティを通してスクリプトブロックと環境を確認することができるのです。
$closure1 = closure {};
$closure1.Closure.Script; # スクリプトブロックを確認
$closure1.Closure.Environment; # 環境を確認
【ダウンロード】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。
YokoKen.PowerShell.Scripts
[ パラメータ ]
target
対象のスクリプトブロック。
paramName
取得するパラメータの名前。
省略した場合は全てのパラメータを取得。
[ 戻り値 ]
スクリプトブロックのパラメータ情報。
[ Get-Parameter ]
function Get-Parameter
{
param ([ScriptBlock]$target, [string]$paramName)
trap { break; }
if ($() -eq $target) { throw New-Object "ArgumentException" @("target"); }
$parameterMetadataProperty = [ScriptBlock].GetProperty("ParameterMetadata", [System.Reflection.BindingFlags]"NonPublic, Instance");
$parameterMetadata = $parameterMetadataProperty.GetValue($target, $());
$bindableParametersProperty = $parameterMetadata.GetType().GetProperty("BindableParameters", [System.Reflection.BindingFlags]"NonPublic, Instance");
$bindableParameters = $bindableParametersProperty.GetValue($parameterMetadata, $());
$compiledCommandParameterType = [Type]::GetType("System.Management.Automation.CompiledCommandParameter");
$typeProperty = $compiledCommandParameterType.GetProperty("Type", [System.Reflection.BindingFlags]"NonPublic, Instance");
$result =
$bindableParameters.GetEnumerator() |
? { (("$paramName" -eq "") -or ($_.Key -eq $paramName)); } |
% {
$paramInfo = New-Object "PSObject";
Add-Member -InputObject $paramInfo -Name "Name" -Value $_.Key -MemberType "NoteProperty";
Add-Member -InputObject $paramInfo -Name "Type" -Value $typeProperty.GetValue($_.Value, $()) -MemberType "NoteProperty";
return $paramInfo;
};
return $result;
}
[ 使用例 ]
Get-Parameter ${function:Get-Parameter};
Get-Parameter ${function:Get-Parameter} "paramName";
【 ダウンロード 】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。
YokoKen.PowerShell.Scripts
[Object].GetMethods("Public, NonPublic, Instance") | % { $_.ToString(); };
これは知らないと損だなぁ。
Windows PowerShell Get-Enjoy コンテスト結果発表
で、僕の作品が選考委員特別賞を頂きました!
プロトタイプチェーンというのが僕の作品です。マニアックな作品と、お褒めの言葉を頂きました (笑)
ダウンロードはこちらからです。
Prototype.zip
実は、コンテスト向けの作品は当初全く違う作品を作ろうとしていました。
スクリプトを書いている内にクラスが使いたくなってきて、Add-Member コマンドレットを使って疑似的にクラスのようなものを書き始めました。
気付いたら、疑似的なクラスを作るための機構に凝りだしてしまいました。
で、作品の路線を変更することにしました。
プロトタイプチェーンこそ備わっていませんが、オブジェクトへ動的にメンバーを追加したりできる PowerShell は、プロトタイプベースオブジェクト指向の流れを汲んでいるのだと思います。
ならば、プロトタイプチェーンを実装しよう。PowerShell にできないことは (あまり) ないはずだ!とその時考えたわけです。
そういえば、僕が初めて PowerShell に触った時、真っ先にスクリプトブロックに Prototype プロパティが備わっているかどうかを試しました。
JavaScript でプロトタイプベースオブジェクト指向を知った僕は、PowerShell にも当然のようにプロトタイプチェーンが備わっているだろうと (勝手に) 思い込んでいたので、備わっていないとわかった時には結構テンションが下がりました。
あの時の落胆をバネに、この作品を完成させることができたような気がしないでもないです。
NUnit を使用して記述されたユニットテストを実行する関数。
この関数は内部で Invoke-Process 関数を使用する。
[ パラメータ ]
testAssemblyPaths
ユニットテストアセンブリのパスの配列。
xmlOutputDirectoryPath
XML 出力ファイルの出力先ディレクトリのパス。
version
NUnit のバージョン。
timeoutMilliseconds
プロセスの実行時間に対するタイムアウト値 (単位:ミリ秒)。
無制限に待機する場合、-1 または Int32.MaxValue を指定。
省略した場合は -1。
[ 戻り値 ]
戻り値については Invoke-Process 関数を参照。
[ Invoke-NUnit ]
function global:Invoke-NUnit
{
param ([string[]] $testAssemblyPaths, [string]$xmlOutputDirectoryPath, [string]$version, [int]$timeoutMilliseconds = [System.Threading.Timeout]::Infinite)
trap { break; }
if ("$testAssemblyPaths".Length -eq 0) { throw "引数 testAssemblyPaths が null または空の配列です。"; }
if ([string]::IsNullOrEmpty($xmlOutputDirectoryPath)) { throw "引数 xmlOutputDirectoryPath が null または空の文字列です。"; }
if ([string]::IsNullOrEmpty($version)) { throw "引数 version が null または空の文字列です。"; }
$testAssemblyPathsText = [string]::Empty;
$testAssemblyPaths | % { $testAssemblyPathsText += "`"$_`" "; };
$nunitConsolePath = "$Env:ProgramFiles\NUnit $version\bin\nunit-console.exe";
$nunitConsoleArgs = "$testAssemblyPathsText /xml=`"$xmlOutputDirectoryPath\TestResult.xml`" /nologo /nodots";
return Invoke-Process $nunitConsolePath $nunitConsoleArgs $timeoutMilliseconds;
}
[ 使用例 ]
$testAssemblyPaths =
@(
"C:\work\Hoge\Project1.Test\bin\Release\Project1.Test.dll",
"C:\work\Hoge\Project2.Test\bin\Release\Project2.Test.dll"
);
$xmlOutputDirectoryPath = "C:\work\Hoge";
$result = Invoke-NUnit $testAssemblyPaths $xmlOutputDirectoryPath "2.4.6";
if (!$result.IsSuccess)
{
Write-Host "ユニットテストが失敗しました。" -ForegroundColor "Red";
}
【 ダウンロード 】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。
YokoKen.PowerShell.Scripts
PowerShell でプロセスを実行する関数。
[ パラメータ ]
processPath
プロセス (実行可能ファイル) のパス。
processArgs
プロセスを実行する際に渡す引数。
省略可能。
timeoutMilliseconds
プロセスの実行時間に対するタイムアウト値 (単位:ミリ秒)。
無制限に待機する場合、-1 または Int32.MaxValue を指定。
省略した場合は -1。
[ 戻り値 ]
プロセスの実行結果に関する情報を格納するハッシュテーブル。
各キーの説明を以下に記述する。
IsSuccess
プロセスが正常終了した場合は true、それ以外の場合は false。
プロセスが正常終了したかどうかは、プロセスの終了コードを元に判断される。
IsComplete
プロセスがタイムアウトせずに完了した場合は true、それ以外の場合は false。
Message
プロセスが実行中に出力したメッセージ。
ErrorMessage
プロセスが実行中に出力したエラーメッセージ。
[ Invoke-Process 関数 ]
function global:Invoke-Process
{
param ([string] $processPath, [string]$processArgs, [int]$timeoutMilliseconds = [System.Threading.Timeout]::Infinite)
trap
{
if ($() -ne $process)
{
$process.Dispose();
}
break;
}
if ([String]::IsNullOrEmpty($processPath)) { throw "引数 processPath が null または空の文字列です。"; }
$process = New-Object "System.Diagnostics.Process";
$process.StartInfo = New-Object "System.Diagnostics.ProcessStartInfo" @($processPath, $processArgs);
$process.StartInfo.WorkingDirectory = (Get-Location).Path;
$process.StartInfo.RedirectStandardOutput = $True;
$process.StartInfo.RedirectStandardError = $True;
$process.StartInfo.UseShellExecute = $False;
$process.Start() | Out-Null;
$message = $process.StandardOutput.ReadToEnd();
$errorMessage = $process.StandardError.ReadToEnd();
$complete = $process.WaitForExit($timeoutMilliseconds);
if (!$complete)
{
$process.Kill();
}
$result =
@{
"IsSuccess" = ($process.ExitCode -eq 0);
"IsComplete" = $complete;
"Message" = $message;
"ErrorMessage" = $errorMessage;
};
$process.Dispose();
return $result;
}
[ 使用例 ]
$result = Invoke-Process "$Env:ProgramFiles\Microsoft Visual Studio 9.0\Common7\IDE\devenv.com" "/rebuild Release `"C:\work\Hoge\Hoge.sln`"";
Write-Host $result.Message;
if (!$result.IsSuccess)
{
Write-Host "ソリューションのリビルドに失敗しました。" -ForegroundColor "Red";
}
【 ダウンロード 】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。
YokoKen.PowerShell.Scripts