在PowerShell脚本开发中,有效的错误处理是确保脚本健壮性和可靠性的关键。本文将介绍一系列高级错误处理技术,帮助您编写更专业的PowerShell脚本。
基础错误处理
首先,让我们回顾PowerShell中的基本错误处理机制:
1 2 3 4 5 6 7 8 9 10 11 12 13
| try { Get-Content -Path "C:\NonExistentFile.txt" -ErrorAction Stop } catch { Write-Host "发生错误: $($_.Exception.Message)" -ForegroundColor Red } finally { Write-Host "操作完成" -ForegroundColor Yellow }
|
使用$ErrorActionPreference
PowerShell的$ErrorActionPreference
变量控制脚本遇到错误时的默认行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $ErrorActionPreference = 'Stop'
$originalEAP = $ErrorActionPreference $ErrorActionPreference = 'SilentlyContinue' try { Get-Process -Name "NonExistentProcess" } finally { $ErrorActionPreference = $originalEAP }
|
创建自定义错误记录器
下面是一个自定义错误记录函数,可帮助您以统一格式记录错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| function Write-ErrorLog { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorRecord]$ErrorRecord, [Parameter()] [string]$LogPath = "C:\Logs\PowerShell_Errors.log", [Parameter()] [switch]$PassThru ) $logDir = Split-Path -Path $LogPath -Parent if (-not (Test-Path -Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $errorMessage = $ErrorRecord.Exception.Message $errorLine = $ErrorRecord.InvocationInfo.Line.Trim() $errorPosition = $ErrorRecord.InvocationInfo.PositionMessage $errorCategory = $ErrorRecord.CategoryInfo.Category $errorType = $ErrorRecord.Exception.GetType().FullName $logEntry = @" [${timestamp}] ERROR TYPE: $errorType CATEGORY: $errorCategory MESSAGE: $errorMessage LINE: $errorLine POSITION: $errorPosition STACK TRACE: $($ErrorRecord.ScriptStackTrace) ==================== "@ Add-Content -Path $LogPath -Value $logEntry if ($PassThru) { return $ErrorRecord } }
try { 1/0 } catch { Write-ErrorLog -ErrorRecord $_ Write-Host "发生计算错误,详情已记录到日志文件" -ForegroundColor Red }
|
使用trap语句进行错误处理
trap
语句是另一种捕获和处理错误的机制,特别适用于需要统一处理整个脚本中错误的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13
| trap { Write-Host "捕获到错误: $($_.Exception.Message)" -ForegroundColor Red continue }
Get-Process -Name "NonExistentProcess" Write-Host "这行仍会执行,因为我们在trap中使用了continue"
|
不同作用域的trap
您可以在不同的作用域中设置多个trap来处理不同类型的错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| trap { Write-Host "默认trap: $($_.Exception.Message)" -ForegroundColor Red continue }
trap [System.DivideByZeroException] { Write-Host "除零错误trap: $($_.Exception.Message)" -ForegroundColor Yellow continue }
trap [System.Management.Automation.CommandNotFoundException] { Write-Host "命令未找到trap: $($_.Exception.Message)" -ForegroundColor Cyan continue }
1/0 Get-NonExistentCommand [System.IO.File]::ReadAllText("C:\NonExistentFile.txt")
|
创建高级错误处理函数
下面是一个更全面的错误处理函数,它结合了记录、通知和重试功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
| function Invoke-WithErrorHandling { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [scriptblock]$ScriptBlock, [Parameter()] [string]$ErrorMessage = "执行脚本块时发生错误", [Parameter()] [string]$LogPath, [Parameter()] [switch]$SuppressOutput, [Parameter()] [int]$RetryCount = 0, [Parameter()] [int]$RetryDelaySeconds = 5, [Parameter()] [scriptblock]$OnErrorAction, [Parameter()] [scriptblock]$FinallyAction ) $attempt = 0 $maxAttempts = $RetryCount + 1 $success = $false $result = $null do { $attempt++ try { Write-Verbose "执行脚本块 (尝试 $attempt/$maxAttempts)" $result = & $ScriptBlock $success = $true break } catch { $currentError = $_ $detailedError = @{ Message = $currentError.Exception.Message Type = $currentError.Exception.GetType().FullName ScriptStackTrace = $currentError.ScriptStackTrace PositionMessage = $currentError.InvocationInfo.PositionMessage Line = $currentError.InvocationInfo.Line.Trim() Time = Get-Date Attempt = $attempt } if ($LogPath) { $logEntry = "[$($detailedError.Time.ToString('yyyy-MM-dd HH:mm:ss'))] 尝试 $($detailedError.Attempt)/$maxAttempts`n" $logEntry += "错误: $($detailedError.Message)`n" $logEntry += "类型: $($detailedError.Type)`n" $logEntry += "位置: $($detailedError.PositionMessage)`n" $logEntry += "堆栈跟踪: $($detailedError.ScriptStackTrace)`n" $logEntry += "====================`n" $logDir = Split-Path -Path $LogPath -Parent if (-not (Test-Path -Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } Add-Content -Path $LogPath -Value $logEntry } if ($OnErrorAction) { & $OnErrorAction -ErrorInfo $detailedError } if ($attempt -lt $maxAttempts) { Write-Verbose "将在 $RetryDelaySeconds 秒后重试..." Start-Sleep -Seconds $RetryDelaySeconds } else { if (-not $SuppressOutput) { Write-Error "$ErrorMessage`n$($detailedError.Message)" } } } finally { if ($FinallyAction) { & $FinallyAction } } } while ($attempt -lt $maxAttempts) if ($success) { return $result } }
$result = Invoke-WithErrorHandling -ScriptBlock { if ((Get-Random -Minimum 1 -Maximum 4) -eq 1) { return "操作成功" } else { throw "随机故障发生" } } -ErrorMessage "执行关键操作时失败" -RetryCount 3 -RetryDelaySeconds 2 -LogPath "C:\Logs\Retry.log" -Verbose -OnErrorAction { param($ErrorInfo) Write-Host "发生错误,尝试 $($ErrorInfo.Attempt),错误消息: $($ErrorInfo.Message)" -ForegroundColor Yellow }
if ($result) { Write-Host "最终结果: $result" -ForegroundColor Green }
|
在函数中处理管道输入错误
当编写接受管道输入的函数时,错误处理需要特别注意:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| function Process-Items { [CmdletBinding()] param( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [object]$InputObject ) begin { Write-Verbose "开始处理项目..." $errorCount = 0 $successCount = 0 } process { try { Write-Verbose "正在处理: $InputObject" if ($InputObject -eq "bad") { throw "发现坏项目!" } $successCount++ Write-Host "成功处理: $InputObject" -ForegroundColor Green } catch { $errorCount++ Write-Host "处理项目 '$InputObject' 时出错: $($_.Exception.Message)" -ForegroundColor Red } } end { Write-Verbose "处理完成。成功: $successCount, 失败: $errorCount" [PSCustomObject]@{ SuccessCount = $successCount ErrorCount = $errorCount TotalCount = $successCount + $errorCount } } }
"item1", "bad", "item3" | Process-Items -Verbose
|
错误过滤器和自定义错误类
您可以创建自定义错误类型和错误过滤器来更好地组织错误处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| class CustomValidationError : System.Exception { [string]$Reason [object]$Value CustomValidationError([string]$message, [string]$reason, [object]$value) : base($message) { $this.Reason = $reason $this.Value = $value } }
function Test-PositiveNumber { param( [Parameter(Mandatory = $true)] [object]$Value ) if (-not [double]::TryParse($Value, [ref]$null)) { throw [CustomValidationError]::new( "值 '$Value' 不是有效的数字", "InvalidFormat", $Value ) } if ([double]$Value -le 0) { throw [CustomValidationError]::new( "值 '$Value' 不是正数", "NotPositive", $Value ) } return $true }
try { $number = "-5" Test-PositiveNumber -Value $number } catch [CustomValidationError] { $error = $_ Write-Host "验证错误: $($error.Exception.Message)" -ForegroundColor Red Write-Host "原因: $($error.Exception.Reason)" -ForegroundColor Yellow Write-Host "提供的值: $($error.Exception.Value)" -ForegroundColor Yellow } catch { Write-Host "其他错误: $($_.Exception.Message)" -ForegroundColor Red }
|
创建一个完整的错误处理框架
最后,让我们创建一个全面的错误处理框架,可以在多个脚本中重用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
| function Initialize-ErrorHandling { [CmdletBinding()] param( [Parameter()] [string]$LogPath = "$env:TEMP\PowerShell_Errors.log", [Parameter()] [ValidateSet('Continue', 'Stop', 'SilentlyContinue', 'Inquire', 'Ignore')] [string]$DefaultAction = 'Continue', [Parameter()] [switch]$EnableGlobalErrorLogging, [Parameter()] [switch]$EnableGlobalErrorTrapping, [Parameter()] [scriptblock]$GlobalErrorAction ) $script:OriginalErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = $DefaultAction if (-not [string]::IsNullOrWhiteSpace($LogPath)) { $logDir = Split-Path -Path $LogPath -Parent if (-not (Test-Path -Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } $script:ErrorLogPath = $LogPath } if ($EnableGlobalErrorLogging) { $ExecutionContext.SessionState.PSVariable.Set('ErrorClearOriginal', ${function:Clear-Error}) function global:Clear-Error { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" foreach ($err in $global:Error) { $errorEntry = "[$timestamp] ERROR: $($err.Exception.Message)`nCATEGORY: $($err.CategoryInfo.Category)`nFULL: $($err | Out-String)" Add-Content -Path $script:ErrorLogPath -Value $errorEntry } & $function:ErrorClearOriginal } } if ($EnableGlobalErrorTrapping) { trap { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $errorEntry = @" [$timestamp] TRAPPED ERROR MESSAGE: $($_.Exception.Message) TYPE: $($_.Exception.GetType().FullName) SCRIPT: $($_.InvocationInfo.ScriptName) LINE NUMBER: $($_.InvocationInfo.ScriptLineNumber) LINE: $($_.InvocationInfo.Line.Trim()) POSITION: $($_.InvocationInfo.PositionMessage) STACK TRACE: $($_.ScriptStackTrace) ==================== "@ Add-Content -Path $script:ErrorLogPath -Value $errorEntry if ($GlobalErrorAction) { & $GlobalErrorAction -ErrorRecord $_ } continue } } Write-Verbose "已初始化错误处理框架 (日志路径: $script:ErrorLogPath)" }
function Reset-ErrorHandling { [CmdletBinding()] param() if ($script:OriginalErrorActionPreference) { $ErrorActionPreference = $script:OriginalErrorActionPreference } Write-Verbose "已重置错误处理配置" }
function Write-DetailedError { [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [System.Management.Automation.ErrorRecord]$ErrorRecord, [Parameter()] [string]$LogPath = $script:ErrorLogPath, [Parameter()] [switch]$PassThru, [Parameter()] [ValidateSet('Verbose', 'Warning', 'Error', 'Host', 'None')] [string]$OutputType = 'Host' ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $formattedError = @" [$timestamp] ERROR DETAILS MESSAGE: $($ErrorRecord.Exception.Message) TYPE: $($ErrorRecord.Exception.GetType().FullName) SCRIPT: $($ErrorRecord.InvocationInfo.ScriptName) LINE NUMBER: $($ErrorRecord.InvocationInfo.ScriptLineNumber) LINE: $($ErrorRecord.InvocationInfo.Line.Trim()) POSITION: $($ErrorRecord.InvocationInfo.PositionMessage) STACK TRACE: $($ErrorRecord.ScriptStackTrace) CATEGORY: $($ErrorRecord.CategoryInfo.Category) REASON: $($ErrorRecord.CategoryInfo.Reason) TARGET: $($ErrorRecord.CategoryInfo.TargetName) FULL ERROR: $($ErrorRecord | Out-String) ==================== "@ if (-not [string]::IsNullOrWhiteSpace($LogPath)) { Add-Content -Path $LogPath -Value $formattedError } switch ($OutputType) { 'Verbose' { Write-Verbose $formattedError } 'Warning' { Write-Warning $ErrorRecord.Exception.Message } 'Error' { Write-Error $ErrorRecord.Exception.Message } 'Host' { Write-Host "ERROR: $($ErrorRecord.Exception.Message)" -ForegroundColor Red Write-Host "DETAILS: Type=$($ErrorRecord.Exception.GetType().Name), Script=$($ErrorRecord.InvocationInfo.ScriptName)" -ForegroundColor DarkRed } 'None' { } } if ($PassThru) { return $ErrorRecord } }
function Invoke-WithRetry { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [scriptblock]$ScriptBlock, [Parameter()] [int]$RetryCount = 3, [Parameter()] [int]$RetryIntervalSeconds = 5, [Parameter()] [scriptblock]$RetryCondition = { $true }, [Parameter()] [string]$LogPath = $script:ErrorLogPath, [Parameter()] [scriptblock]$OnRetry ) $attempt = 0 $maxAttempts = $RetryCount + 1 do { $attempt++ $lastError = $null try { Write-Verbose "执行代码块 (尝试 $attempt/$maxAttempts)" return & $ScriptBlock } catch { $lastError = $_ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $retryLog = @" [$timestamp] RETRY ATTEMPT $attempt/$maxAttempts ERROR: $($lastError.Exception.Message) TYPE: $($lastError.Exception.GetType().FullName) ==================== "@ if (-not [string]::IsNullOrWhiteSpace($LogPath)) { Add-Content -Path $LogPath -Value $retryLog } if ($OnRetry) { & $OnRetry -ErrorRecord $lastError -Attempt $attempt -MaxAttempts $maxAttempts } $shouldRetry = & $RetryCondition -ErrorRecord $lastError if ($shouldRetry -and $attempt -lt $maxAttempts) { Write-Verbose "在 $RetryIntervalSeconds 秒后重试..." Start-Sleep -Seconds $RetryIntervalSeconds } else { throw $lastError } } } while ($attempt -lt $maxAttempts) }
Export-ModuleMember -Function Initialize-ErrorHandling, Reset-ErrorHandling, Write-DetailedError, Invoke-WithRetry
|
使用错误处理框架的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Import-Module ErrorHandling.psm1
Initialize-ErrorHandling -LogPath "C:\Logs\MyScript.log" -DefaultAction "Stop" -EnableGlobalErrorLogging -Verbose
try { $result = Invoke-WithRetry -ScriptBlock { if ((Get-Random -Minimum 1 -Maximum 5) -lt 3) { throw "临时错误,可重试" } return "操作成功" } -RetryCount 5 -RetryIntervalSeconds 2 -OnRetry { param($ErrorRecord, $Attempt, $MaxAttempts) Write-Host "重试 $Attempt/$MaxAttempts..." -ForegroundColor Yellow } -Verbose Write-Host "最终结果: $result" -ForegroundColor Green } catch { Write-DetailedError -ErrorRecord $_ -OutputType "Host" Write-Host "执行错误后清理..." -ForegroundColor Cyan } finally { Reset-ErrorHandling }
|
最佳实践总结
- 预见错误:识别脚本中可能发生错误的区域,并相应地进行处理。
- 使用try/catch/finally:对于可能失败的关键操作,始终使用try/catch块。
- 使用-ErrorAction参数:在单个命令级别控制错误行为。
- 记录错误:将错误详细信息记录到日志文件,以便后续分析。
- 实现重试逻辑:对于网络或其他间歇性操作,实现自动重试。
- 提供有意义的错误消息:确保错误消息清晰、具有描述性,并包含足够的上下文信息。
- 使用自定义错误类型:对于复杂应用程序,考虑创建自定义错误类型。
- 测试错误处理:专门测试错误路径,确保它们按预期工作。
通过实施这些高级错误处理技术,您的PowerShell脚本将更加健壮,更易于调试和维护。良好的错误处理不仅能提高脚本质量,还能降低运营风险,特别是在自动化关键业务流程时。