PowerShellで進捗表示!プログレスバーを使った自作関数を紹介
PowerShellには標準で Write-Progress
というコマンドレットがありますが、ループ内で毎回パラメーターを計算して指定する必要があり実用性に欠けると思っていました。
そこで、「全体の数」と「現在の位置」を渡すことで、自動で進捗率を計算しプログレスバーを表示する関数を作成しました。
また、少し複雑なロジックになってしまいましたが繰り返し処理をスクリプトブロックで渡すことで、処理本体や進捗表示、プログレスバーの完了フラグまで、ひととおり実行できる関数も作ったので合わせて紹介します。
簡単にプログレスバーを表示できるPowerShell関数「Show-ProgressBar」
この関数は、進捗の計算とプログレスバーの表示を自動で行い、ユーザーに進捗状況を通知するものです。
「Show-ProgressBar」のコード
Write-Progress
ではプログレスバーの親子関係が実現できるオプション「-ParentId
」がありますが、
処理が複雑になるため、今回の関数では使用していません。
必要に応じてカスタマイズしてください!
ここをクリックしてコードを表示
function Show-ProgressBar {
<#
.SYNOPSIS
コレクションやループ処理の進捗状況をプログレスバーで表示します。
.DESCRIPTION
全体のステップ数と現在のステップ数を渡すことで、パーセンテージを自動計算し、
PowerShell標準のWrite-Progressコマンドレットを呼び出します。
進捗率は常に先頭に表示され、その後に詳細なステータスメッセージが続きます。
.PARAMETER TotalCount
処理全体のステップ数、または処理対象のアイテムの総数。
.PARAMETER CurrentStep
現在の処理が全体の何番目かを示す数値。
.PARAMETER Activity
プログレスバーのメインタイトルとなる、実行中のタスク全体の名前。
.PARAMETER Status
現在実行中のサブタスクや、処理中のアイテム名など、進捗率の後に表示される詳細な状況。
.PARAMETER Id
プログレスバーのID。ネストしたプログレスバーを区別するために使用します。
.PARAMETER ShowCount
件数表示を「(現在の件数 / 合計件数)」形式でステータスメッセージに追加します。
.EXAMPLE
# 1. 基本的な使い方 (パーセンテージのみ表示)
# Status: 1%, 10%, 100% のように、右揃えで表示されます。
$total = 100
for ($i = 1; $i -le $total; $i++) {
Show-ProgressBar -TotalCount $total -CurrentStep $i -Activity "テスト処理を実行中"
Start-Sleep -Milliseconds 50
}
Write-Progress -Activity "テスト処理を実行中" -Completed
.EXAMPLE
# 2. Statusを指定して、より詳細な情報を表示する
# Status: 50% 処理中: file_short.txt
# Status: 60% 処理中: a_very_long_file_name.txt
$files = @(
[pscustomobject]@{Name="file_short.txt"},
[pscustomobject]@{Name="a_very_long_file_name_that_should_be_padded.txt"}
)
$totalFiles = $files.Count
$currentFileIndex = 0
foreach ($file in $files) {
$currentFileIndex++
Show-ProgressBar -TotalCount $totalFiles -CurrentStep $currentFileIndex -Activity "システムファイルをスキャン中" -Status "処理中: $($file.Name)"
Start-Sleep -Seconds 1
}
Write-Progress -Activity "システムファイルをスキャン中" -Completed
.EXAMPLE
# 3. -ShowCount を使用して件数を表示する
# Status: 42% (50 / 120) のように表示されます。
$total = 120
for ($i = 1; $i -le $total; $i++) {
Show-ProgressBar -TotalCount $total -CurrentStep $i -Activity "データ処理(件数表示)" -ShowCount
Start-Sleep -Milliseconds 30
}
Write-Progress -Activity "データ処理(件数表示)" -Completed
.EXAMPLE
# 4. -Status と -ShowCount を組み合わせて動的な情報を表示する
# Status: 42% (50 / 119) チェック中: chrome
# Status: 43% (51 / 119) チェック中: Code
$processes = Get-Process | Select-Object -First 150 # 例として150個に制限
$totalProcesses = $processes.Count
$currentProcessIndex = 0
foreach ($process in $processes) {
$currentProcessIndex++
# 現在処理中のプロセス名を動的に-Statusに渡す
$statusMessage = "チェック中: $($process.Name)"
Show-ProgressBar -TotalCount $totalProcesses -CurrentStep $currentProcessIndex `
-Activity "実行中プロセスの仮想チェック" -Status $statusMessage -ShowCount
Start-Sleep -Milliseconds 50
}
Write-Progress -Activity "実行中プロセスの仮想チェック" -Completed
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[int]$TotalCount,
[Parameter(Mandatory = $true)]
[int]$CurrentStep,
[Parameter(Mandatory = $true)]
[string]$Activity,
[Parameter()]
[string]$Status,
[Parameter()]
[int]$Id = 1,
[Parameter()]
[switch]$ShowCount
)
if ($TotalCount -le 0) {
return
}
$percentComplete = [int](($CurrentStep / $TotalCount) * 100)
if ($percentComplete -gt 100) {
$percentComplete = 100
}
# 先頭に表示するパーセンテージ部分を右揃え3桁で整形 (例: " 1%", " 10%", "100%")
$percentText = "{0,3}%" -f $percentComplete
# -ShowCount が指定された場合、件数表示部分を作成
$countText = ""
if ($ShowCount.IsPresent) {
$countText = "( $($CurrentStep) / $($TotalCount) )"
}
# 各パーツを結合して最終的なStatusメッセージを作成
$statusParts = @($percentText)
if (-not [string]::IsNullOrEmpty($countText)) {
$statusParts += $countText
}
if (-not [string]::IsNullOrEmpty($Status)) {
$statusParts += $Status
}
$statusMessage = $statusParts -join " "
Write-Progress -Activity $Activity -Status $statusMessage -PercentComplete $percentComplete -Id $Id
}
「Show-ProgressBar」の使い方
コンソール上に直接定義してから関数を実行するか、PowerShellスクリプトファイル(*.ps1
)として作成して実行する方法の2つがあります。
それぞれの実行方法の詳細は、こちらの記事をご参照ください。
「Show-ProgressBar」の注意事項
注意書きで前述したとおり、この関数の実行後、個別に完了フラグ(-Completed
)を付けた Write-Progress
の実行が必要です。
# 基本
Write-Progress -Activity "<アクティビティ名>" -Id <ID番号> -Completed
# Activityのみで指定可能
Write-Progress -Activity "<アクティビティ名>" -Completed
# IDのみでも指定可能
Write-Progress -Id <ID番号> -Completed
Id
を未指定で Write-Progress
を実行した場合は、規定値「-Id = 0
」が使用されます。
なお、今回自作した関数 Show-ProgressBar
では、引数が未指定の場合は規定値「-Id = 1
」と設定しました。
そのため、Id
を未指定で実行したとしても、
Write-Progress -Activity "<アクティビティ名>" -Id <ID番号> -Completed
で対応します。
前述したとおり「-Activity
」や「-Id
」どちらかを指定する方法でも対応できますが、コードの可読性と意図の明確さを考えると、素直に両方の引数を指定して完了フラグを立てるべきでしょう。
補足情報:Write-Progress -Id の規定値は、公式ドキュメントだと None で実際は 0 です
こちらの公式マニュアルをみてわかるとおり、Id
の規定値は「None
」となっています。
この「None
」はどのように解釈されるか、以下のとおり実際に検証してみた結果、IDを未指定で Write-Progress
を実行した場合は「Id = 0
」として処理されました。
Write-ProgressのIDの規定値が「0」であることを調べた
わたしが知る限り、実行したプログレスバーの状態(IDなど)を確認するコマンドレットはありません。
そのため、実際にIDを未指定でプロセスバーを表示した後に、そのバーを親とするためIDを指定(ParentId = 0
)して子バーを作れるか検証しました。
-
検証で使用した関数「Show-ProgressIdVerification」
Write-ProgressのIDの規定値が「0」であることを調べる関数「Show-ProgressIdVerification」function Show-ProgressIdVerification { <# .SYNOPSIS Write-Progress のデフォルトIDが '0' であることを視覚的に検証します。 .DESCRIPTION ID未指定の親プログレスバーと、ParentId 0 を指定した子プログレスバーを順に表示し、 ID未指定時のデフォルト値が '0' であることを証明します。 モダンターミナルでの表示崩れを防ぐため、プログレスバー表示中のコンソール出力を抑制します。 .PARAMETER PauseSeconds 各ステップで待機する秒数を指定します。 .EXAMPLE Show-ProgressIdVerification -PauseSeconds 5 #> [CmdletBinding()] param ( [int]$PauseSeconds = 3 ) try { Clear-Host Write-Host "Write-ProgressのデフォルトIDが '0' であることを検証します。" -ForegroundColor Yellow Write-Host Write-Host "これから3つのステップでプログレスバーの表示が変わります。" Write-Host " ステップ1. ID未指定の親バー(親タスク1)を表示" Write-Host " ステップ2. 親タスク1の子としてネストされた子バー(子タスク)を表示" Write-Host " ステップ3. 別のIDを指定し独立した親バー(親タスク2)を表示" Write-Host Read-Host -Prompt "Enterキーを押すと検証を開始します..." Write-Host "------------------------------------------------------------" Write-Host # --- 検証フェーズ --- # ステップ1: ID未指定で親バーを表示 (これが暗黙のID '0' になる) Write-Progress -Activity "親タスク1" -Status "ステップ1: ID未指定(規定値によりID = 0)で表示中..." -PercentComplete 50 Start-Sleep -Seconds $PauseSeconds # ステップ2: ParentId 0 を指定して子バーを表示 Write-Progress -Id 1 -Activity "子タスク" -Status "ステップ2: [ID = 0]の子バーとして表示中..." -PercentComplete 75 -ParentId 0 Start-Sleep -Seconds $PauseSeconds # ステップ3: 比較のため、別のIDで独立したバーを表示 Write-Progress -Id 2 -Activity "親タスク2" -Status "ステップ3: [ID = 2]の独立したバーで表示中..." -PercentComplete 20 Start-Sleep -Seconds $PauseSeconds } finally { # 後処理: 表示したすべてのプログレスバーを閉じる Write-Progress -Activity "親タスク2" -Id 2 -Completed Write-Progress -Activity "子タスク" -Id 1 -Completed Write-Progress -Activity "親タスク1" -Id 0 -Completed Clear-Host Write-Host "検証完了。" } }
-
実際に関数を動かした結果
プログレスバーのインデントを見てわかる通り、親をId=0と指定した子バーが想定通り機能しました。
つまり、この検証で「Id
を未指定でWrite-Progressを実行するとIdは[0
]として処理される」ということがわかりました。
以上が、「Write-ProgressのIDの規定値が「0」であることを調べた
」でした。
おそらく、公式ドキュメントの規定値「None
」というのは 設計上の指定の有無 ということであり、実装言語の仕様による結果 の「0
」のことは指していないものと思われます。
すこし、わかりにくいですよね。
以上が、「Write-Progress -Id の規定値は、公式ドキュメントだと None で実際は 0 です
」でした。
「Show-ProgressBar」の使用例
使用例1:シンプルなループ処理
単純に100回のループ処理の進捗を表示したい場合に最適です。
$activityName = "データのエクスポート"
$totalSteps = 100
for ($i = 1; $i -le $totalSteps; $i++) {
# 関数を呼び出してプログレスバーを更新
Show-ProgressBar -TotalCount $totalSteps -CurrentStep $i -Activity $activityName
# ここで実際の処理を行う
Start-Sleep -Milliseconds 20
}
# 【重要】処理完了後、必ずプログレスバーを非表示にする
Write-Progress -Activity $activityName -Completed
Write-Host "エクスポートが完了しました。"
データのエクスポート [ 57% ]
使用例2:ファイル一覧の処理
foreach
ループでファイルのコレクションを処理するような、より実践的なシナリオです。
# 1. 親タスクの対象フォルダパスを設定 (AppData\Localは管理者権限不要)
$basePath = $env:LOCALAPPDATA
Write-Host "スキャン対象: $basePath"
# 2. 親タスクの対象を取得 (AppData\Local直下のサブフォルダを15個に制限)
$folders = Get-ChildItem -Path $basePath -Directory -ErrorAction SilentlyContinue | Select-Object -First 15
$totalFolders = $folders.Count
$folderIndex = 0
if ($totalFolders -eq 0) {
Write-Warning "$basePath にサブフォルダが見つかりませんでした。"
return
}
# 3. 親ループ (各フォルダを処理)
foreach ($folder in $folders) {
$folderIndex++
# 【親プログレスバー (Id=1) の更新】
Show-ProgressBar -TotalCount $totalFolders -CurrentStep $folderIndex `
-Activity "AppDataフォルダのスキャン" -Status "フォルダ: $($folder.Name)" -Id 1 -ShowCount
# 親プログレスバーの表示を認識させるための短い待機
Start-Sleep -Milliseconds 300
# 4. 子タスクの対象を取得 (各フォルダ内の設定/ログファイルを50個に制限)
# - .json, .xml, .txtなど、より一般的に存在するファイル種別を対象にする
$files = Get-ChildItem -Path $folder.FullName -Include "*.json", "*.xml", "*.log", "*.txt" -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 50
$totalFiles = $files.Count
$fileIndex = 0
if ($totalFiles -gt 0) {
# 5. 子ループ (各ファイルを処理)
foreach ($file in $files) {
$fileIndex++
# 【子プログレスバー (Id=2) の更新】
Show-ProgressBar -TotalCount $totalFiles -CurrentStep $fileIndex `
-Activity "ファイルのスキャン" -Status "ファイル: $($file.Name)" -Id 2 -ShowCount
Start-Sleep -Milliseconds 25 # ダミーの処理時間
}
}
# 6. 子プログレスバー (Id=2) を閉じる
# (ファイルが0件の場合も、前のループで表示されたままのバーを消すために実行)
Write-Progress -Activity "ファイルのスキャン" -Id 2 -Completed
}
# 7. 親プログレスバー (Id=1) を閉じる
Write-Progress -Activity "AppDataフォルダのスキャン" -Id 1 -Completed
Write-Host "`nスキャンが完了しました。"
スキャン対象: C:\Users\XXXX\AppData\Local
AppDataフォルダのスキャン [ 67% ( 10 / 15 ) フォルダ: atom ]
ファイルのスキャン [ 92% ( 46 / 50 ) ファイル: help.es.txt ]
「Show-ProgressBar」の総評
この関数を使えば、簡単かつ統一された方法・レイアウトで進捗を表示可能です。
基本的にはループ内で実行して進捗表示するものですが、複雑な条件で進捗表示する際などにも使えるでしょう。
ひとつ課題に感じているは、Show-ProgressBar
を実行した後に明示的に完了フラグを付けた Write-Progress
を実行する必要があるという点です。
基本的に完了フラグの処理をしなくてもプログレスバーは非表示になってくれますが、実行する環境や状況などによって後続処理のレイアウトが崩れるなどの影響がある可能性も。
そして、この完了フラグは自作関数内や自作関数のオプションではなく、個別に Write-Progress
を呼び出す必要があるという仕組みである為、対応漏れが発生するでしょう。
この課題を解決する方法として、自動的に完了フラグを実行する関数を作成してみました。
すこし複雑で扱いにくい面もあると思いますが、個人的に抜け漏れが発生する可能性がある関数で対応するよりは良いと思っています。
完了フラグも自動で対応する関数「Invoke-WithProgressBar」
この Invoke-WithProgressBar
関数は実行時にプログレスバーの設定を行うと同時に繰り返す処理をスクリプトブロック形式で渡します。
それによって関数内で処理本体と進捗表示、完了フラグすべてを完結できるようにしています。
「Invoke-WithProgressBar」のコード
ここをクリックしてコードを表示
function Invoke-WithProgressBar {
<#
.SYNOPSIS
コレクション内の各アイテムに対して処理を実行しながら、プログレスバーを表示・管理します。
.DESCRIPTION
指定されたコレクションの各アイテムに対し、処理スクリプトブロックを実行します。
進捗表示の開始、更新、完了(非表示)が自動的に行われます。
進捗率は常に先頭に表示され、その後に詳細なステータスメッセージが続きます。
.PARAMETER Activity
プログレスバーのメインタイトルとなる、実行中のタスク全体の名前。(例: "ファイルのコピー")
.PARAMETER Collection
処理対象となるアイテムのコレクション。
.PARAMETER ProcessAction
コレクションの各アイテムに対して実行するスクリプトブロック。`param($Item)` で現在のアイテムを受け取ります。
.PARAMETER ItemStatusPrefix
各アイテムの処理時にプログレスバーのStatusに表示されるメッセージの接頭辞。
.PARAMETER Id
プログレスバーのID。ネストしたプログレスバーを区別するために使用します。
.PARAMETER ShowCount
件数表示を「(現在の件数 / 合計件数)」形式でステータスメッセージに追加します。
.EXAMPLE
# 1. デフォルト表示
# Status: 1% スキャン中: file1.dll
# Status: 2% スキャン中: a_very_long_file_name.dll
$files = Get-ChildItem -Path "C:\Windows\System32" -File | Select-Object -First 100
Invoke-WithProgressBar -Activity "システムファイルの仮想スキャン" `
-Collection $files `
-ItemStatusPrefix "スキャン中: " `
-ProcessAction {
param($FileItem)
Start-Sleep -Milliseconds 50
}
.EXAMPLE
# 2. -ShowCount を使用してオブジェクトのコレクションを処理する
# Status: 42% チェック中: chrome (50 / 119)
# 実行中のプロセスを取得して処理する例(管理者権限は不要)
$processes = Get-Process | Select-Object -First 150 # 例として150個に制限
Invoke-WithProgressBar -Activity "実行中プロセスの仮想チェック" `
-Collection $processes `
-ItemStatusPrefix "チェック中: " `
-ShowCount `
-ProcessAction {
param($process)
# ここで各プロセスに対する実際の処理を行う(例: CPU使用率の記録など)
Start-Sleep -Milliseconds (Get-Random -Minimum 10 -Maximum 50)
}
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Activity,
[Parameter(Mandatory = $true)]
[System.Collections.IEnumerable]$Collection,
[Parameter(Mandatory = $true)]
[scriptblock]$ProcessAction,
[Parameter()]
[string]$ItemStatusPrefix = "処理中: ",
[Parameter()]
[int]$Id = 1,
[Parameter()]
[switch]$ShowCount
)
$totalCount = ($Collection | Measure-Object).Count
$currentStep = 0
if ($totalCount -eq 0) {
Write-Warning "コレクションが空のため、処理はスキップされました。"
Write-Progress -Activity $Activity -Id $Id -Completed
return
}
try {
foreach ($item in $Collection) {
$currentStep++
$statusItemName = $null
if ($item -is [string] -or $item -is [int] -or $item -is [long] -or $item -is [double]) {
$statusItemName = $item.ToString()
}
elseif ($item | Get-Member -MemberType Property -Name "Name" -ErrorAction SilentlyContinue) {
$statusItemName = $item.Name
}
else {
$statusItemName = $item.ToString()
}
$percentComplete = [int](($currentStep / $totalCount) * 100)
if ($percentComplete -gt 100) { $percentComplete = 100 }
# 先頭に表示するパーセンテージ部分を右揃え3桁で整形
$percentText = "{0,3}%" -f $percentComplete
# -ShowCount が指定された場合、件数表示部分を作成
$countText = ""
if ($ShowCount.IsPresent) {
$countText = "( $($CurrentStep) / $($TotalCount) )"
}
# アイテム情報部分を作成
$itemInfoText = "$($ItemStatusPrefix)$($statusItemName)"
# 各パーツを結合して最終的なStatusメッセージを作成
$statusParts = @($percentText)
if (-not [string]::IsNullOrEmpty($countText)) {
$statusParts += $countText
}
$statusParts += $itemInfoText
$statusMessage = $statusParts -join " "
Write-Progress -Activity $Activity `
-Status $statusMessage `
-PercentComplete $percentComplete `
-Id $Id
& $ProcessAction $item
}
}
catch {
Write-Error "Invoke-WithProgressBar内でエラーが発生しました: $($_.Exception.Message)"
throw
}
finally {
# 正常終了時、エラー発生時を問わず、必ずプログレスバーを閉じる
Write-Progress -Activity $Activity -Id $Id -Completed
}
}
「Invoke-WithProgressBar」の使い方
コンソール上に直接定義してから関数を実行するか、PowerShellスクリプトファイル(*.ps1
)として作成して実行する方法の2つがあります。
それぞれの実行方法の詳細は、こちらの記事をご参照ください。
「Invoke-WithProgressBar」の注意事項
この関数の引数では、繰り返し処理する内容をスクリプトブロックとして引き渡します。
スクリプトブロックでループ変数を使う場合は「param($Item)
」と指定する事で対応可能です。
「Invoke-WithProgressBar」の使用例
Invoke-WithProgressBar
を使用した九九の計算例は以下のようになります。
「かけられる数(被乗数)」を Invoke-WithProgressBar
の引数で指定し、スクリプトブロック内でループ変数でも param($CurrentNumber)
と定義。
「かける数(乗数)」はスクリプトブロック内で定義して回すという流れです。
# Collectionとして1から9までの数値を定義
$numbersForMultiplication = 1..9
Write-Host "--- 九九の計算を開始します ---`n"
Invoke-WithProgressBar -Activity "九九の計算" `
-Collection $numbersForMultiplication `
-ItemStatusPrefix "計算中: " `
-ProcessAction {
param($CurrentNumber)
# 現在処理している段の数値を表示
Write-Host " $($CurrentNumber) の段:" -ForegroundColor Cyan
# $CurrentNumber と 1から9までの数値を掛け合わせて九九を表示
1..9 | ForEach-Object {
$multiplier = $_ # 1から9までの掛け算の相手の数値
$result = $CurrentNumber * $multiplier # 受け取った引数変数を使用
Write-Host " $($CurrentNumber) × $($multiplier) = $($result)"
}
# 各段の計算の間に少し待機して、プログレスバーの動きを確認しやすくする
Start-Sleep -Milliseconds 1500
}
Write-Host "`n--- 九九の計算が完了しました ---"
Write-Host "すべての段の計算が終了しました。"
1 の段:
1 × 1 = 1
1 × 2 = 2
1 × 3 = 3
1 × 4 = 4
1 × 5 = 5
1 × 6 = 6
1 × 7 = 7
1 × 8 = 8
1 × 9 = 9
2 の段:
2 × 1 = 2
2 × 2 = 4
2 × 3 = 6
2 × 4 = 8
2 × 5 = 10
2 × 6 = 12
2 × 7 = 14
2 × 8 = 16
2 × 9 = 18
3 の段:
~ (省略) ~
7 の段:
7 × 1 = 7
7 × 2 = 14
7 × 3 = 21
7 × 4 = 28
7 × 5 = 35
7 × 6 = 42
7 × 7 = 49
7 × 8 = 56
7 × 9 = 63
九九の計算 [ 78% 計算中: 7 ]
「Invoke-WithProgressBar」の総評
「コレクションを foreach
で回す」といったお決まりのパターンの時に活用することで、進捗表示させたいループ処理を簡潔かつ標準化して対応できます。
とくに終了する際のプログレスバーの消し忘れ(-Completed
フラグで実行)が発生しません。
スクリプトブロック内で複雑な処理をする場合は、その処理自体も関数化することでデバッグが容易になるでしょう。
ただ、開発者にとって Invoke-WithProgressBar
は良いと判断していますが、スクリプトの利用者が運用や保守メンバーの場合は注意が必要です。
内容が複雑なため、問題が発生した場合にトラブルシューティングが困難になるかもしれません。
まとめ
-
foreach
+Show-ProgressBar
の明示的なループを使うケースの代表例- チームで共有され、PowerShellスキルレベルがさまざまなメンバーがメンテナンスする可能性があり、長期間運用される自動化スクリプトなど。
- 厳格なコードレビューがあり、処理の透明性が最優先される環境。
- スクリプトの実行ログを追いながら、第三者が障害調査を行うことが頻繁に想定される環境。
-
Invoke-WithProgressBar
を使うケースの代表例- 個人が使うツールや、短期的な分析スクリプト。
- 開発者が明確で、メンテナンスもその開発者が行うことが保証されている場合。
- スクリプトの利用者が「使うだけ」で、中身を分析する必要がない場合。
関連記事
Discussion