Hey, Scripting Guy!ストップウォッチをウォッチする

Microsoft Scripting Guys

メキシコで働いていたときに私がいつも楽しんでいたことの 1 つに、時間に対する考え方があります。すべての物事は必ず行われますが、決して時間に追われることはありませんでした。たとえば、出勤途中で、同僚の Miguel が新しくできたコーヒー ショップに行ってみようと提案したら、私たちはそうしました。数分遅刻しても、それはたいしたことではありませんでした。私たちはおいしいコーヒーを飲み、一緒に楽しいときを過ごし、美しい日の出を見たのです。たった数分の遅刻なんて、問題にもなりません。

このようなライフスタイルの良さはわかっているのですが、状況が変わり、私の私生活はかなり厳格に管理されるようになりました。午前 6 時に起床し、1 時間運動して、シャワーを浴びます。その後、朝食を食べ、午前 7 時 30 分には出勤しています。1 日に何度も腕時計を見て、予定どおりに行動していることを確認している自分に驚きます。メキシコでは、腕時計を付けたりすることもありませんでした。とにかく、時間が議論の的になることがなかったように思います。

一方、ネットワーク管理者やコンサルタントは、常に時間を気にする必要があります。というのも、このような職種に就いていると、バックアップをスケジュールしたり、サービスの可用性を計算したり、システムの稼働時間、ダウンタイム、応答時間を特定したりする必要があるからです。時間を追跡する場合は、アイコンをクリックし、腕時計を見て、アプリケーションが起動するまでの時間を計測すれば十分かもしれません。しかし、時間の計測中に、アプリケーションの起動が中断された場合はどうでしょうか。皆さんはどうかわかりませんが、私は何かをしているときに別のことに気を取られると、よく物事を忘れてしまいます。

ダイバーウォッチの回転ベゼルを使って開始時刻を記録することもできますが、計測結果を算出するには減算が必要になります。また、作業が中断された場合、その計算結果は当てにならない可能性があります。このような場合に本当に必要なのは、普通の腕時計やダイバーウォッチではなく、動作にかかった時間を示し、前回かかった時間を記憶することができるストップウォッチです。このようなストップウォッチがあれば、パフォーマンスの観点から、現在の結果の真相に関する見通しを立てることができます。

さいわいにも、Microsoft .NET Framework には StopWatch クラスがあります。このクラスを Set-StopWatchRecordValues.ps1 という名前のスクリプトで使用しました。このスクリプトでは、コマンドの実行時間を計り、その結果をレジストリに書き込みます。スクリプトが 1 回以上実行されている場合は、レジストリから前回の値が取得され、現在の時間と前回の時間の両方が示されます。

StopWatch クラスは System.Diagnostics 名前空間のクラスです。このクラスには、StartNew という名前の静的メソッドがあり、このメソッドにより新しい StopWatch オブジェクトが作成されて開始されます。.NET Framework のクラスから静的メソッドを呼び出すには、名前空間のパスとクラス名を角かっこで囲み、その後に 2 つのコロンとメソッド名を続けます。この例では、System.Diagnostics が .NET Framework 名前空間で、StopWatch がクラスです。また、このメソッドから返される System.Diagnostics.StopWatch オブジェクトを保持する変数も必要です。

$sw = [system.diagnostics.stopwatch]::startNew()

この処理を行うと、$sw 変数を使用して、実行中の StopWatch オブジェクトに関する情報を表示できます。たとえば、結果を Format-List コマンドレットにパイプして、このオブジェクトに含まれる全プロパティの値を表示することができます。

PS C:\> $sw | Format-List -Property *

IsRunning           : True
Elapsed             : 00:00:22.5467175
ElapsedMilliseconds : 22546
ElapsedTicks        : 67472757628

StopWatch オブジェクトには、図 1 に示すような、便利なメソッドがたくさんあります。最も重要なのは Stop メソッドです。StartNew 静的メソッドを使用すると、StopWatch オブジェクトは作成されると同時に開始されるので、その場合に行う可能性が最も高いのは、StopWatch オブジェクトを停止して、動作にかかった時間を確認することだからです。

図 1 StopWatch オブジェクトのメソッド
Name 定義
等しい System.Boolean Equals(Object obj)
GetHashCode System.Int32 GetHashCode()
GetType System.Type GetType()
get_Elapsed System.TimeSpan get_Elapsed()
get_ElapsedMilliseconds System.Int64 get_ElapsedMilliseconds()
get_ElapsedTicks System.Int64 get_ElapsedTicks()
get_IsRunning System.Boolean get_IsRunning()
リセット System.Void Reset()
スタート System.Void Start()
停止 System.Void Stop()
ToString System.String ToString()

Set-StopWatchRecordValues.ps1 スクリプトでは、4 つの関数を作成して StopWatch オブジェクトを作成および管理し、経過時間をレジストリ内に記録します。スクリプトの初回実行時には、レジストリ キーが作成され、レジストリに格納されている情報がないことをユーザーに通知します (図 2 参照)。

fig02.gif

図 2 情報が格納されていない新しいレジストリ キー

その後、スクリプトではレジストリ プロパティを作成し、現在の実行時間がレジストリに格納されます (図 3 参照)。それ以降にスクリプトを実行すると、レジストリ データが取得および表示され、データが更新されます (図 4 参照)。

fig03.gif

図 3 現在の実行時間が格納されているレジストリ キー

fig04.gif

図 4 スクリプトを再度実行すると、レジストリ データが取得および表示され、更新される

このスクリプトでは、$debug という名前の 1 つのパラメータを使用します。このパラメータは、スイッチ パラメータで、指定されている場合にのみ処理を実行します。–debug スイッチを指定してスクリプトを実行すると、図 5 のような詳細情報が出力されます。出力される情報には、レジストリ キーの場所、スクリプトで実行する動作、および動作の実行が終了した時刻などがあります。

fig05.gif

図 5 –debug スイッチを指定してスクリプトを実行した結果

コマンド ライン パラメータを作成するには、Param ステートメントを次のように使用します。

Param([switch]$debug)

Param ステートメントに続いて、1 つ目の関数を作成します (Param ステートメントは、スクリプト内で、コメント行ではない最初のコード行である必要があります)。この関数は、Set-StopWatch という名前で、StopWatch オブジェクトを作成して開始したり、停止したりするのに使用します。この関数の入力パラメータである $action に指定する値によって、関数で実行される処理が決まります。この関数の宣言は次のようになります。

Function Set-StopWatch($action)

実行する処理の選択には、Switch ステートメントを使用しています。ここでは、1 つ目のデバッグ ステートメントを出力します。デバッグ情報をコンソール プロンプトに出力するために、Write-Debug コマンドレットを使用しています。Write-Debug コマンドレットのすごいところは、テキストの書式を黄色と黒で自動的に設定できることです (この色は指定することができます)。もう 1 つのすごいところは、指定した場合だけ、情報をコンソールに出力することです。既定では、コンソールには何も出力されません。Set-StopWatch 関数に渡される入力パラメータは、$action 変数です。Switch ステートメントでは $action 変数の値を確認して、実行する動作を決定します。

Write-Debug "Set-StopWatch action is $action
 Switch ($action)

今度は、このステートメントで実行する動作を定義する必要があります。1 つ目の動作は、$action 変数の値が Start の場合に実行されます。この場合は、System.Diagnostic.StopWatch クラスの StartNew 静的メソッドを使用し、このメソッドの実行結果として作成される StopWatch オブジェクトを $sw 変数に格納します。また、タイマを開始することを示すデバッグ ステートメントを用意しましたが、これは、–debug スイッチを指定してスクリプトを開始した場合にのみ表示されることに注意してください。

 {
  "Start" { 

Write-Debug "Starting Timer"
              $script:sw = [system.diagnostics
                 .stopwatch]::StartNew() 
             }

$action 変数の値が Stop の場合に実行する、タイマが停止したことを示すデバッグ ステートメントも記述しました。ここでは、レジストリ キーが存在しているかどうかを確認します。レジストリ キーがある場合は、$swv という名前の変数を作成し、その値を $null に設定します。その後で、別のデバッグ メッセージを出力します。

  "Stop" { 
Write-Debug "Stopping Timer"
               If(Test-Path -path $path)
                  {
                    $swv = $null
Write-Debug "$path was found. Calling Get-StopWatchvalue"

次に、Get-StopWatchvalue 関数を呼び出し、先ほど作成した $swv 変数を渡します。このコードでは、変数を参照渡しで渡しています。つまり、関数によって新しい値が変数に代入されるので、変数に値を代入した関数外で、変数を使用するということです。変数を参照渡しで渡すには、$swv 変数を参照型に変換する必要があります。この変換は [ref] を使用して実行します。存在しない変数は参照型にキャストできないので、$swv 変数はあらかじめ作成しておく必要があることに注意してください。

                 Get-StopWatchvalue([ref]$swv)
Write-Debug "Get-StopWatchvalue swv was: $($swv)"
                  }

レジストリ キーが存在しない場合は、2 つのデバッグ ステートメントを出力して、$swv 変数を作成し、その値を null に設定します。その後、レジストリ キーを作成する New-StopWatchKey 関数を呼び出します。

               Else

                 {
Write-Debug "$path was not found"
                  $swv = $null

Write-Debug "$swv is null. Calling New-StopWatchKey"
                  New-StopWatchKey

New-StopWatchKey 関数の処理が完了したら、別のデバッグ ステートメントを出力して、Get-StopWatchvalue 関数を呼び出し、$swv 変数を参照渡しで渡します ($swv 変数は、後でレジストリから取得したデータを表示する際に使用します)。

Write-Debug "Obtaining default value from get-StopWatchvalue"
                 et-StopWatchvalue([ref]$swv)
                 }

ここで、StopWatch オブジェクトを停止する必要があります。まず、別のデバッグ ステートメントを使用して停止メッセージを出力します。その後、$sw 変数に格納されている StopWatch オブジェクトで Stop メソッドを呼び出します。StopWatch オブジェクトを停止したら、Elapsed プロパティから取得した System.TimeSpan オブジェクトを文字列に変換して、その値をレジストリに保存できるようにする必要があります。そのため、以下のように、TimeSpan オブジェクトを文字列に変換したものを Set-StopWatchValue 関数に渡します。

Write-Debug "Stopping the stop watch"
               $script:sw.Stop() 
Write-Debug "Converting stop watch to string, and calling
 Set-StopWatchValue"
                   Set-StopWatchValue($script:
                      sw.Elapsed.toString())

これで、出力をコンソールに表示する準備ができました。まず、コマンドの実行にかかった時間を通知します。有効範囲がスクリプトである $sw 変数を使用して、Elapsed プロパティの値を取得します。ここでは、Elapsed プロパティの評価で TimeSpan オブジェクトが返されるように、サブ式を使用しています。

Windows PowerShell では 2 種類の文字列があることを理解していないと、これは少し紛らわしく思えるかもしれません。1 つ目はリテラル文字列です。この種類の文字列は単一引用符で囲みます。リテラル文字列では、指定した文字列が、そのままの状態で出力されます。つまり、単一引用符で囲まれている変数は、拡張されないということです。

PS C:\> $a = "this is a string"
PS C:\> "This is what is in $a"
This is what is in $a

もう 1 つは拡張文字列で、この種類の文字列は二重引用符で囲みます。この文字列を使用する場合は、変数の値が拡張されて出力されます。

PS C:\> $a = "this is a string"
PS C:\> "This is what is in $a"
This is what is in this is a string

最初は紛らわしいかもしれませんが、これらの文字列の種類は文字列と変数の連結を防ぐのに活用できます。この 2 種類の文字列について理解したら、アクサン グラーブ文字 (`) を使用し、必要に応じて変数の拡張を抑制することができます。

PS C:\> $a = "this is a string"
PS C:\> "This is what is in "$a: $a"
This is what is in $a: this is a string

拡張文字列を使用しない場合は、次のように出力を連結する必要があります。

PS C:\> $a = "this is a string"
PS C:\> "This is what is in $a: " + $a
This is what is in $a: this is a string

では、この文字列とコードの関連をご説明しましょう。拡張文字列内でオブジェクトが拡張されると、オブジェクトの名前が示されます。オブジェクトの名前を表示する必要がある場合は、オブジェクトをかっこで囲み、その前にドル記号を追加することによってサブ式を作成する必要があります。これで、次に示すように、オブジェクトを評価して、既定のプロパティがコマンド ラインに返されます。

PS C:\> "$sw.Elapsed"
System.Diagnostics.Stopwatch.Elapsed
PS C:\> "$($sw.Elapsed)"
00:00:07.5247519

出力では、コマンドを実行するのにかかった時間を表示するメッセージと $swv 変数に格納されている値が表示されます。

"The command took $($script:sw.Elapsed)"
"Previous command took: " + $swv

Switch ステートメントを使用する場合は、既定の動作を用意することをお勧めします。–debug スイッチを指定してスクリプトを実行していない場合は、$action 変数の値が出力され、ユーザーには Start か Stop のどちらかを選択することを要求するメッセージが表示されます。–debug スイッチを指定してスクリプトを実行している場合は、これ以外に追加で数行の情報が表示されます。どちらの場合でも、次に示すように Exit コマンドを呼び出すことにより、スクリプトは終了します。

   Default {
                 if(!$debug) {"$action is not understood. Specify Start / Stop"} 
Write-Debug "Entering default action. $action was specified."
Write-Debug "Action value must be either Start or Stop"
Write-Debug "Exiting script now"
                  Exit
               }
} #end Switch
} #end Set-StopWatch

次は、New-StopWatchKey 関数です。この関数は、スクリプトで作成するレジストリ キーがまだ作成されていないことを検出した場合に呼び出されます。New-StopWatchKey 関数では、まずデバッグ メッセージを出力します。その後、New-Item コマンドレットを使用してレジストリ キーを作成します。この処理結果を通知するメッセージは、Out-Null コマンドレットにパイプされ、画面に不要な情報が表示されるのを防いでいます。

別のデバッグ メッセージが出力され、前回の実行時間を保持するためのレジストリ プロパティが作成されます。この処理には、New-ItemProperty コマンドレットを使用します。このコマンドレットの処理結果を通知するメッセージも Out-Null コマンドレットにパイプします。その後、デバッグ メッセージをもう 1 つ出力したら、関数の処理は終了です。

Function New-StopWatchKey()
{
 Write-Debug "Inside New-StopWatchKey. Creating new item $path"
New-Item -path $path -force | 
out-null
Write-Debug "Creating new item property $property"
New-ItemProperty -Path $path "
-Name $property -PropertyType string -value "no data" | 
 out-null
Write-Debug "Leaving New-StopWatchkey"
} #end New-StopWatchKey

今度は、Get-StopWatchvalue 関数を作成して、レジストリに格納されたデータを読み取り、そのデータを $swv 参照変数に代入する必要があります。これを行うには、まず 2 つのデバッグ メッセージを出力します。その後、Get-ItemProperty コマンドレットを使用して、$swValue 変数に格納するカスタムの Windows PowerShell オブジェクトを作成します。ご覧のとおり、このオブジェクトにはさまざまなプロパティがあります。

PS C:\> $swValue = Get-ItemProperty -Path HKCU:\Scripting\Stopwatch
PS C:\> $swValue

PSPath          : Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Scripting\Stopwatch
PSParentPath    : Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Scripting
PSChildName     : Stopwatch
PSDrive         : HKCU
PSProvider      : Microsoft.PowerShell.Core\Registry
PreviousCommand : 00:00:00.3153793

前回のコマンド実行時のプロパティ値を取得するのに、中間変数を使用する必要はありません。かっこを使用して、プロパティを直接照会できます。これは少しややこしいかもしれませんが、ご覧のとおり、構文は有効です。

PS C:\> (Get-ItemProperty -Path HKCU:\Scripting\Stopwatch).PreviousCommand
00:00:00.3153793

カスタム オブジェクトを作成したら、PreviousCommand プロパティを照会して、その値を $swv 参照型変数の value プロパティに代入します。この後に 2 つのデバッグ ステートメントを出力したら、Get-StopWatchvalue 関数の処理は終了です。

Function Get-StopWatchvalue([ref]$swv)
{
Write-Debug "Inside Get-StopWatchvalue."
Write-Debug "Obtaining $path\$property"
$swValue = Get-ItemProperty -Path $path "
  -Name $property
$swv.value = $swValue.PreviousCommand
Write-Debug "Value of "$swv is $($swv.value)"
Write-Debug "Leaving Get-StopWatchvalue"
} #end Get-StopWatchvalue

次に、コマンドの実行にかかった時間をレジストリに書き込む必要があるので、Set-StopWatchvalue 関数を作成します。この関数では、$newSwv 変数の値をレジストリに書き込みます。2 つのデバッグ ステートメントを出力してから、Set-ItemProperty コマンドレットを呼び出して、$path 変数に格納されているパス、$property 変数に格納されているレジストリ プロパティの名前、および $newSwv 変数に格納されている値を、このコマンドレットに渡します。その後、デバッグ メッセージをもう 1 つ出力したら、Set-StopWatchValue 関数の処理は終了です。

Function Set-StopWatchvalue($newSwv)
{
Write-Debug "Inside Set-StopWatchvalue"
Write-Debug "setting $path\$property to $newSwv"
Set-ItemProperty -Path $path "
-Name $property -Value $newSwv
Write-Debug "Leaving Set-StopWatchvalue"
} #end Set-StopWatchvalue

これで、スクリプトで必要となる関数はすべてです。スクリプトのエントリ ポイントでは、まず $debug 変数が存在しているかどうかを確認します。$debug 変数がある場合は、–debug スイッチを指定してスクリプトが実行されているということなので、$DebugPreference 自動変数の値を continue に変更します。既定では、$DebugPreference 変数の値は SilentlyContinue に設定されるので、Write-Debug コマンドレットによってデバッグ メッセージが表示されません。

このスクリプトでは、HKEY_Current_User ハイブの Scripting\Stopwatch という名前のレジストリ キーに、StopWatch オブジェクトの時間を格納する必要があります。これを行うには、PreviousCommand という名前のプロパティを使用します。この 2 つの情報は、それぞれ適切な変数に代入します。変数を初期化したら、入力引数に Start を指定して Set-StopWatch 関数を呼び出して、処理を開始します。この関数を実行したら、Get-Process コマンドレットを呼び出します。

if($debug) { $DebugPreference = "continue" }
$path = "HKCU:\Scripting\Stopwatch"
$property = "PreviousCommand"
Set-StopWatch "Start"
Get-Process

このスクリプトでさまざまなコマンドの実行時間を計測する場合は、ここで必要な処理を追加します。この時点で別のスクリプトを呼び出すこともできますし、必要な数だけコマンドを含めることもできます。

$debug 変数が存在しない場合、スクリプトは通常モードで実行されているということになります。この場合は、実行時間を計測することだけが目的なので、Get-Process コマンドレットを呼び出したときの結果を画面上に表示する必要はありません。そのため、not 演算子 (感嘆符、バットとボール、お好きなように呼んでください) を使用して、$debug 変数がない場合には Windows PowerShell の Clear-Host 関数を使用して画面に情報を出力しないことを指示しています。最後に、入力引数に Stop を指定して Set-StopWatch 関数を呼び出して、処理を終了します。

if(!$debug) {Clear-Host}
Set-StopWatch "Stop"

図 6 に、Set-StopWatchRecordValues.ps1 の完全なスクリプトを示します。

図 6 Set-StopWatchRecordValues.ps1

Param([switch]$debug)
Function Set-StopWatch($action)
{
Write-Debug “Set-StopWatch action is $action”
 Switch ($action)
 {
  “Start” {
Write-Debug “Starting Timer”
               $script:sw = [system.diagnostics.stopwatch]::StartNew()
            }
  “Stop” {
Write-Debug “Stopping Timer”
               If(Test-Path -path $path)
                  {
                    $swv = $null
Write-Debug “$path was found. Calling Get-StopWatchvalue”
                    Get-StopWatchvalue([ref]$swv)
Write-Debug “Get-StopWatchvalue swv was: $($swv)”
                   }
               Else
                 {
Write-Debug “$path was not found”
                 $swv = $null
Write-Debug “$swv is null. Calling New-StopWatchKey”
                 New-StopWatchKey
Write-Debug “Obtaining default value from get-StopWatchvalue”
                 Get-StopWatchvalue([ref]$swv)
                }
Write-Debug “Stopping the stop watch”
               $script:sw.Stop()
Write-Debug “Converting stop watch to string, and calling Set-StopWatchValue”
               Set-StopWatchValue($script:sw.Elapsed.toString())
               “The command took $($script:sw.Elapsed)”
               “Previous command took: “ + $swv
              }
      Default {
                  if(!$debug) {“$action is not understood. Specify Start / Stop”}
Write-Debug “Entering default action. $action was specified.”
Write-Debug “Action value must be either Start or Stop”
Write-Debug “Exiting script now”
                 Exit
              }
 } #end Switch
} #end Set-StopWatch

Function New-StopWatchKey()
{
Write-Debug “Inside New-StopWatchKey. Creating new item $path”
 New-Item -path $path -force |
 out-null
Write-Debug “Creating new item property $property”
 New-ItemProperty -Path $path `
 -Name $property -PropertyType string -value “no data” |
 out-null
Write-Debug “Leaving New-StopWatchkey”
} #end New-StopWatchKey

Function Get-StopWatchvalue([ref]$swv)
{
Write-Debug “Inside Get-StopWatchvalue.”
Write-Debug “Obtaining $path\$property”
 $swValue = Get-ItemProperty -Path $path `
  -Name $property
 $swv.value = $swValue.PreviousCommand
Write-Debug “Value of `$swv is $($swv.value)”
Write-Debug “Leaving Get-StopWatchvalue”
} #end Get-StopWatchvalue

Function Set-StopWatchvalue($newSwv)
{
Write-Debug “Inside Set-StopWatchvalue”
Write-Debug “setting $path\$property to $newSwv”
 Set-ItemProperty -Path $path `
 -Name $property -Value $newSwv
Write-Debug “Leaving Set-StopWatchvalue”
} #end Set-StopWatchvalue

# *** entry point ***

if($debug) { $DebugPreference = “continue” }
$path = “HKCU:\Scripting\Stopwatch”
$property = “PreviousCommand”
Set-StopWatch “Start”
Get-Process
if(!$debug) {Clear-Host}
Set-StopWatch “Stop”

StopWatch クラスの説明をお楽しみいただけましたでしょうか。TechNet スクリプト センターにアクセスして、Hey, Scripting Guy! の日刊コラムもご覧ください。

Ed Wilson は有名なスクリプトの専門家です。これまでに 8 冊の書籍 (うち 5 冊は Microsoft Windows スクリプトに関連する書籍) を執筆しており、これらの書籍は Microsoft Press から出版されています。現在は Windows PowerShell のベスト プラクティスに関する書籍を執筆しています。Ed は、マイクロソフト認定システム エンジニア (MCSE) や情報システム セキュリティ プロフェッショナル (CISSP) など、20 種類を超えるこの業界の資格を持っています。また、余暇には、木工作業、水中写真撮影、スキューバ ダイビング、お茶などを楽しんでいます。

Craig Liebendorfer は言葉を巧みに操る、マイクロソフトのベテラン Web 編集者です。Craig は毎日言葉にかかわって給料を受け取ることができる仕事が存在することが、いまだに信じられないと思っています。彼が大好きなことの 1 つは場違いなユーモアなので、まさにこのコラムにうってつけの人物です。彼は自分の立派な娘が、自分の人生における最も偉大な功績であると考えています。