テキストログを読み、日付 × ログレベルで件数を集計し、ERROR だけ詳細を出します。
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$LogPath,
[int]$Days = 7
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$since = (Get-Date).AddDays(-$Days).Date
$pattern = '^(?<date>\d{4}-\d{2}-\d{2})\s+\S+\s+(?<level>\w+)\s+(?<msg>.*)$'
Write-Verbose "対象: $LogPath ($($since.ToString('yyyy-MM-dd')) 以降)"
# 1行ずつ流す。巨大なログでもメモリを使い切らない
$records = Get-Content -LiteralPath $LogPath -Encoding UTF8 |
ForEach-Object {
if ($_ -match $pattern) {
[PSCustomObject]@{
日付 = [datetime]$matches.date
レベル = $matches.level
内容 = $matches.msg
}
}
} |
Where-Object { $_.日付 -ge $since }
# 日付×レベルで集計
$records |
Group-Object { $_.日付.ToString('yyyy-MM-dd') }, { $_.レベル } |
ForEach-Object {
[PSCustomObject]@{
日付 = $_.Group[0].日付.ToString('yyyy-MM-dd')
レベル = $_.Group[0].レベル
件数 = $_.Count
}
} |
Sort-Object 日付, レベル | Format-Table -AutoSize
# ERROR だけ詳細を出す
$errors = @($records | Where-Object レベル -eq 'ERROR')
if ($errors.Count -gt 0) {
Write-Host "ERROR が $($errors.Count) 件あります:" -ForegroundColor Red
$errors | ForEach-Object { Write-Host " $($_.日付.ToString('yyyy-MM-dd')) $($_.内容)" }
}
PS> .\Get-LogSummary.ps1 -LogPath .\app.log -Days 7 -Verbose
VERBOSE: 対象: .\app.log (2026-08-16 以降)
VERBOSE: 解析できた行: 7 件
日付 レベル 件数
---- ------ ----
2026-08-20 INFO 1
2026-08-20 WARN 1
2026-08-21 ERROR 2
2026-08-21 INFO 1
2026-08-23 ERROR 1
2026-08-23 INFO 1
ERROR が 3 件あります:
2026-08-21 接続に失敗しました host=db01
2026-08-21 接続に失敗しました host=db01
2026-08-23 タイムアウト host=api02
| 要素 | 学んだ章 |
|---|---|
-match と $matches(名前付きキャプチャ) | 04.3 |
[PSCustomObject]@{ } | 04.4 |
Where-Object / Group-Object / Sort-Object | 05.3・05.4 |
Format-Table は最後にだけ置く | 05.1 |
[CmdletBinding()] と Write-Verbose | 06.2・08.4 |
@( ) で包んで .Count を安全に使う | 06.3 |
-Encoding UTF8 を明示 | 07.3 |
(?<date>...) は名前付きキャプチャで、マッチした部分に名前を付ける正規表現の書き方です。$matches.date のように名前で取り出せるので、$matches[1] のような番号指定より読みやすく、パターンを増やしても壊れません。(?<name>...) と同じ記法です。
リネームはやり直しが効きません。必ず「実行前に結果を確認できる」形にします。
-WhatIf の有無だけで切り替える[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Pattern,
[Parameter(Mandatory)][string]$Replacement
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$targets = Get-ChildItem -LiteralPath $Path -File | ForEach-Object {
$new = $_.Name -creplace $Pattern, $Replacement # ← 大小文字を区別する
if ($_.Name -cne $new) {
[PSCustomObject]@{ 元 = $_.Name; 新 = $new; Item = $_ }
}
}
if (-not $targets) { Write-Host '対象がありません'; return }
# 重複チェック(同じ名前になるものがないか)
$dup = $targets | Group-Object 新 | Where-Object Count -gt 1
if ($dup) {
throw "リネーム後の名前が重複します: $($dup.Name -join ', ')"
}
foreach ($t in $targets) {
if ($PSCmdlet.ShouldProcess($t.元, "→ $($t.新)")) {
Rename-Item -LiteralPath $t.Item.FullName -NewName $t.新
}
}
PS> .\Rename-Bulk.ps1 -Path .\photos -Pattern '^IMG_0*(\d+)\.JPG$' -Replacement 'photo-$1.jpg' -WhatIf
What if: Performing the operation "→ photo-1.jpg" on target "IMG_0001.JPG".
What if: Performing the operation "→ photo-2.jpg" on target "IMG_0002.JPG".
What if: Performing the operation "→ photo-3.jpg" on target "IMG_0003.JPG".
readme.html を README.html にしたい場合、-replace と -ne では動きません。
| 判定式 | 結果 | 意味 |
|---|---|---|
| 'readme.html' -ne 'README.html' | False | 誤り「変更なし」と判定されスキップされる |
| 'readme.html' -cne 'README.html' | True | 正しい変更ありと判定される |
-creplace と -cne を使っています。c 付きを既定にすると考えて差し支えありません。Rename-Item での大小文字の変更自体は正しく動きます。問題は「変更が必要か」の判定側にあります。
$PSCmdlet.ShouldProcess() は、-WhatIf が指定されていれば「何をするか」を表示して False を返し、指定がなければ True を返す関数です。if で包むだけで、確認機能が自動的に付きます。-Confirm(1件ずつ確認)にも同時に対応できます。
robocopy に任せるCopy-Item でも書けますが、数万件を超えると robocopy のほうが桁違いに速いです。差分コピー・再試行・ログ出力が組み込まれています。
if ($LASTEXITCODE -ne 0) { throw } と書くと誤検知します。1 が返るためです。-ge 8 で判定してください。08 章で「終了コードの意味はコマンドごとに違う」と書いたのは、この robocopy が代表例です。
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Source,
[Parameter(Mandatory)][string]$Destination,
[string]$LogDir
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# 既定値は param() の外で入れる(5.1 では param() の中で $PSScriptRoot が空になる)
if (-not $LogDir) { $LogDir = Join-Path $PSScriptRoot 'logs' }
if (-not (Test-Path -LiteralPath $Source)) { throw "コピー元がありません: $Source" }
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$logFile = Join-Path $LogDir "backup-$stamp.log"
Write-Host "同期: $Source → $Destination"
# /MIR : ミラーリング(削除も反映される。要注意)
# /R:2 : 再試行 2 回 /W:5 : 待機 5 秒
# /LOG : ログ出力 /NP : 進捗率を出さない(ログが肥大化するため)
robocopy $Source $Destination /MIR /R:2 /W:5 /NP /LOG:$logFile | Out-Null
$code = $LASTEXITCODE
if ($code -ge 8) {
throw "robocopy が失敗しました (終了コード: $code) ログ: $logFile"
}
$label = switch ($code) {
0 { '差分なし' }
1 { 'コピーしました' }
default { "完了(コード $code。ログを確認してください)" }
}
Write-Host "結果: $label" -ForegroundColor Green
Write-Host "ログ: $logFile"
exit 0
$LogDir の既定値を param() の中に書いてはいけません。[CmdletBinding()] が付いていると、5.1 では param() の既定値を評価する時点で $PSScriptRoot がまだ空です。-LogDir を省略した瞬間に Cannot bind argument to parameter 'Path' because it is an empty string. で落ちます。pwsh 7 では起きないため気づかないまま配布してしまいます。詳細は A2.1 を見てください。
/MIR はコピー先の余分なファイルを削除します。/L(一覧のみ、実行しない)を付けて確認してください。-WhatIf の robocopy 版にあたります。/MIR ではなく /E(空フォルダを含めてコピー、削除はしない)を使います。
exit 0 は意図的に書いています。これが無いと、直前の robocopy の終了コード(成功時でも 1)がスクリプト自身の終了コードになり、タスクスケジューラや CI から「失敗」と判定されます。08.3 で扱った内容の実例です。
@echo off
cd /d "%~dp0"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0main.ps1" %*
exit /b %ERRORLEVEL%
exit /b %ERRORLEVEL% で ps1 の終了コードをタスクスケジューラまで返します。これが無いと失敗が伝わりません。
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$logDir = Join-Path $PSScriptRoot 'logs'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$logFile = Join-Path $logDir ("run-{0}.log" -f (Get-Date -Format 'yyyyMMdd'))
function Write-Log {
param([string]$Message, [string]$Level = 'INFO')
$line = '{0} {1,-5} {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message
Add-Content -LiteralPath $logFile -Value $line -Encoding UTF8
Write-Verbose $line
}
try {
Write-Log "開始 (実行ユーザー: $env:USERNAME)"
# ここに本体の処理を書く
Write-Log "処理中..."
Write-Log "正常終了"
exit 0
}
catch {
Write-Log "$($_.Exception.Message) ($($_.InvocationInfo.ScriptLineNumber) 行目)" 'ERROR'
exit 1
}
$action = New-ScheduledTaskAction -Execute 'C:\work\backup\run.cmd'
$trigger = New-ScheduledTaskTrigger -Daily -At 02:00
Register-ScheduledTask -TaskName 'DailyBackup' `
-Action $action -Trigger $trigger `
-Description '毎日 2:00 にバックアップを実行'
Write-Log は 10 行程度ですが、これがあるかないかで障害対応の時間がまったく変わります。処理を書く前にログの仕組みを作るくらいで丁度よいです。
PowerShell と外部プログラム(Node.js・Python・自作 exe)の間でデータをやり取りするなら、標準出力に JSON を吐かせるのが最も確実です。
# Node.js に JSON を吐かせて受け取る
$out = node -e "console.log(JSON.stringify({ok:true, items:[1,2,3]}))"
$obj = $out | ConvertFrom-Json
$obj.ok # → True
$obj.items.Count # → 3
$LASTEXITCODE # → 0
node -e "process.exit(2)"
$LASTEXITCODE # → 2
$? # → False
[CmdletBinding()]
param([Parameter(Mandatory)][string]$ScriptPath)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# 1) 実行ファイルの存在を先に確認する
$node = Get-Command node -ErrorAction SilentlyContinue
if (-not $node) { throw 'node が見つかりません。PATH を確認してください。' }
# 2) 標準エラーを分けて受け取る
$stdout = & $node.Source $ScriptPath 2>&1 | ForEach-Object {
if ($_ -is [System.Management.Automation.ErrorRecord]) {
Write-Warning $_.ToString() # 標準エラーは警告として出す
} else {
$_ # 標準出力だけを通す
}
}
$code = $LASTEXITCODE
# 3) 終了コードで失敗を判定する(例外は飛んでこない)
if ($code -ne 0) { throw "node が失敗しました (終了コード: $code)" }
# 4) JSON として解釈する
$result = $stdout -join "`n" | ConvertFrom-Json
$result
| 手順 | やっていること | 省くとどうなるか |
|---|---|---|
| ① | Get-Command で存在確認 | PATH が違う環境で不親切なエラーになる |
| ② | 標準エラーを分離 | エラー文字列が JSON に混ざり解析に失敗する |
| ③ | $LASTEXITCODE を確認 | 失敗に気づかず先へ進む(08.3) |
| ④ | ConvertFrom-Json | 文字列を自分で切り出すことになる |
& $node.Source のように実行ファイルのフルパスで呼ぶのが安全です。curl の例(5.1 ではエイリアス、7 では curl.exe)と同じ問題です。Get-Command で解決してから .Source を使えば、どれを実行しているかが明確になります。
2>&1 は標準エラーを標準出力に合流させる指定です。合流させた上で、ErrorRecord かどうかで振り分けています。| パターン | 核になる考え方 | 関連章 |
|---|---|---|
| ログ集計 | テキストを早めにオブジェクトに変える | 04.3・05.4 |
| 一括リネーム | -WhatIf で実行前に確認。大小文字は c 付き演算子 | 04.3・06.2 |
| バックアップ | 終了コードは -ge 8 で判定。末尾に exit 0 | 08.3 |
| タスク実行 | カレント・画面・ユーザーが変わる。ログが命綱 | 03.2・07.1 |
| 外部連携 | JSON を挟む。終了コードで失敗を判定 | 07.4・08.3 |
[CmdletBinding(SupportsShouldProcess)] と $PSCmdlet.ShouldProcess() で、-WhatIf が自動的に使えるようになるrobocopy は 8 以上が失敗)$PSScriptRoot 基準でパスを組み立て、末尾に exit 0 を明示する