掌握 PowerShell 错误处理:一本直截了当的指南

你是否厌倦了在 PowerShell 脚本中看到那些恼人的红色错误消息?虽然它们看起来令人畏惧,但适当的错误处理对于构建可靠的 PowerShell 自动化至关重要。在本教程中,你将学习如何在脚本中实现强大的错误处理——从理解错误类型到掌握 try/catch 块。

前提条件

本教程假设你有:

  • 安装了 Windows PowerShell 5.1 或 PowerShell 7+
  • 对 PowerShell 脚本有基本的了解
  • 愿意将错误视为学习机会!

理解 PowerShell 错误类型

在深入处理错误之前,你需要了解 PowerShell 可能抛出的两种主要错误类型:

终止错误

这些是严重错误——完全停止脚本执行的错误。当出现以下情况时,你会遇到终止错误:

  • 你的脚本存在语法错误,导致无法解析
  • 在 .NET 方法调用中发生未处理的异常
  • 你明确指定了 ErrorAction Stop
  • 严重的运行时错误使得无法继续

非终止错误

这些是更常见的操作错误,不会停止你的脚本:

  • 文件未找到错误
  • 权限被拒绝的场景
  • 网络连接问题
  • 无效的参数值

错误操作参数:你的第一道防线

让我们从一个实际示例开始。这里有一个尝试删除早于特定天数的文件的脚本:

param (
    [Parameter(Mandatory)]
    [string]$FolderPath,

    [Parameter(Mandatory)]
    [int]$DaysOld
)

$Now = Get-Date
$LastWrite = $Now.AddDays(-$DaysOld)
$oldFiles = (Get-ChildItem -Path $FolderPath -File -Recurse).Where{$_.LastWriteTime -le $LastWrite}

foreach ($file in $oldFiles) {
    Remove-Item -Path $file.FullName
    Write-Verbose -Message "Successfully removed [$($file.FullName)]."
}

默认情况下,Remove-Item 会生成非终止错误。为了使其生成我们可以捕获的终止错误,请添加-ErrorAction Stop

Remove-Item -Path $file.FullName -ErrorAction Stop

Try/Catch块:您的错误处理瑞士军刀

现在让我们将文件删除包裹在一个try/catch块中:

foreach ($file in $oldFiles) {
    try {
        Remove-Item -Path $file.FullName -ErrorAction Stop
        Write-Verbose -Message "Successfully removed [$($file.FullName)]."
    }
    catch {
        Write-Warning "Failed to remove file: $($file.FullName)"
        Write-Warning "Error: $($_.Exception.Message)"
    }
}

try块包含可能生成错误的代码。如果发生错误,执行将跳转到catch块(如果是终止错误),您可以:

  • 记录错误
  • 采取纠正措施
  • 通知管理员
  • 优雅地继续脚本执行

使用$Error变量进行错误调查

PowerShell在自动变量$Error中维护一个错误对象数组。将$Error视为PowerShell的“黑匣子记录器” – 它会跟踪您的PowerShell会话期间发生的每个错误,使其在故障排除和调试中无价。

以下是您可能希望使用$Error的时间和原因:

  1. 故障排除过去的错误:即使您错过了看到红色错误消息,$Error会维护一个历史记录:

    # 查看最近的错误详情
    $Error[0] | Format-List * -Force
    
    # 查看最后5个错误
    $Error[0..4] | Select-Object CategoryInfo, Exception
    
    # 搜索特定类型的错误
    $Error | Where-Object { $_.Exception -is [System.UnauthorizedAccessException] }
    
  2. 调试脚本: 使用 $Error 来了解出错的原因和位置:

    # 获取发生错误的确切行号和脚本
    $Error[0].InvocationInfo | Select-Object ScriptName, ScriptLineNumber, Line
    
    # 查看完整的错误调用堆栈
    $Error[0].Exception.StackTrace
    
  3. 错误恢复与报告: 非常适合创建详细的错误报告:

    # 创建错误报告
    function Write-ErrorReport {
        param($ErrorRecord = $Error[0])
    [PSCustomObject]@{
        TimeStamp = Get-Date
        ErrorMessage = $ErrorRecord.Exception.Message
        ErrorType = $ErrorRecord.Exception.GetType().Name
        Command = $ErrorRecord.InvocationInfo.MyCommand
        ScriptLine = $ErrorRecord.InvocationInfo.Line
        ErrorLineNumber = $ErrorRecord.InvocationInfo.ScriptLineNumber
        StackTrace = $ErrorRecord.ScriptStackTrace
    }
    

    }

  4. 会话管理: 清理错误或检查错误状态:

    # 清除错误历史(在脚本开始时非常有用)
    $Error.Clear()
    
    # 计算总错误数(适合错误阈值检查)
    if ($Error.Count -gt 10) {
        Write-Warning "检测到高错误数量:$($Error.Count) 个错误"
    }
    

结合这些概念的现实世界示例:

function Test-DatabaseConnections {
    $Error.Clear()  # Start fresh

    try {
        # Attempt database operations...
    }
    catch {
        # If something fails, analyze recent errors
        $dbErrors = $Error | Where-Object {
            $_.Exception.Message -like "*SQL*" -or
            $_.Exception.Message -like "*connection*"
        }

        if ($dbErrors) {
            Write-ErrorReport $dbErrors[0] |
                Export-Csv -Path "C:\\Logs\\DatabaseErrors.csv" -Append
        }
    }
}

专业提示:

  • $Error 每个 PowerShell 会话都有
  • 它的默认容量为 256 个错误(由 $MaximumErrorCount 控制)
  • 它是一个固定大小的数组 – 当满时,新错误会推出旧错误
  • 始终首先检查 $Error[0] – 这是最近的错误
  • 考虑在重要脚本开始时清除 $Error 以进行清洁的错误跟踪

多个捕获块: 针对性错误处理

就像你不会对每个家庭维修工作使用相同的工具一样,你不应该以相同的方式处理每个 PowerShell 错误。多个捕获块让您对不同类型的错误做出不同的响应。

下面是它的工作原理:

try {
    Remove-Item -Path $file.FullName -ErrorAction Stop
}
catch [System.UnauthorizedAccessException] {
    # This catches permission-related errors
    Write-Warning "Access denied to file: $($file.FullName)"
    Request-ElevatedPermissions -Path $file.FullName  # Custom function
}
catch [System.IO.IOException] {
    # This catches file-in-use errors
    Write-Warning "File in use: $($file.FullName)"
    Add-ToRetryQueue -Path $file.FullName  # Custom function
}
catch [System.Management.Automation.ItemNotFoundException] {
    # This catches file-not-found errors
    Write-Warning "File not found: $($file.FullName)"
    Update-FileInventory -RemovePath $file.FullName  # Custom function
}
catch {
    # This catches any other errors
    Write-Warning "Unexpected error: $_"
    Write-EventLog -LogName Application -Source "MyScript" -EntryType Error -EventId 1001 -Message $_
}

您将遇到的常见错误类型:

  • [System.UnauthorizedAccessException] – 拒绝访问
  • [System.IO.IOException] – 文件被锁定/正在使用中
  • [System.Management.Automation.ItemNotFoundException] – 文件/路径未找到
  • [System.ArgumentException] – 无效参数
  • [System.Net.WebException] – 网络/网络问题

以下是将这实践的现实示例:

function Remove-StaleFiles {
    [CmdletBinding()]
    param(
        [string]$Path,
        [int]$RetryCount = 3,
        [int]$RetryDelaySeconds = 30
    )

    $retryQueue = @()

    foreach ($file in (Get-ChildItem -Path $Path -File)) {
        $attempt = 0
        do {
            $attempt++
            try {
                Remove-Item -Path $file.FullName -ErrorAction Stop
                Write-Verbose "Successfully removed $($file.FullName)"
                break  # Exit the retry loop on success
            }
            catch [System.UnauthorizedAccessException] {
                if ($attempt -eq $RetryCount) {
                    # Log to event log and notify admin
                    $message = "Permission denied after $RetryCount attempts: $($file.FullName)"
                    Write-EventLog -LogName Application -Source "FileCleanup" -EntryType Error -EventId 1001 -Message $message
                    Send-AdminNotification -Message $message  # Custom function
                }
                else {
                    # Request elevated permissions and retry
                    Request-ElevatedAccess -Path $file.FullName  # Custom function
                    Start-Sleep -Seconds $RetryDelaySeconds
                }
            }
            catch [System.IO.IOException] {
                if ($attempt -eq $RetryCount) {
                    # Add to retry queue for later
                    $retryQueue += $file.FullName
                    Write-Warning "File locked, added to retry queue: $($file.FullName)"
                }
                else {
                    # Wait and retry
                    Write-Verbose "File in use, attempt $attempt of $RetryCount"
                    Start-Sleep -Seconds $RetryDelaySeconds
                }
            }
            catch {
                # Unexpected error - log and move on
                $message = "Unexpected error with $($file.FullName): $_"
                Write-EventLog -LogName Application -Source "FileCleanup" -EntryType Error -EventId 1002 -Message $message
                break  # Exit retry loop for unexpected errors
            }
        } while ($attempt -lt $RetryCount)
    }

    # Return retry queue for further processing
    if ($retryQueue) {
        return $retryQueue
    }
}

多个捕获块的专业提示:

  1. 顺序很重要 – 首先放置更具体的异常
  2. 使用自定义函数一致处理每种错误类型
  3. 考虑瞬态错误的重试逻辑
  4. 将不同错误类型记录到不同位置
  5. 尽可能使用最具体的异常类型
  6. 通过故意引发每种错误类型来测试每个catch块

使用Finally块:自我清理

finally块是您的清理工具组 – 它始终执行,无论是否发生错误。这使其非常适合:

  • 关闭文件句柄
  • 从数据库断开连接
  • 释放系统资源
  • 恢复原始设置

这里有一个实际示例:

try {
    $stream = [System.IO.File]::OpenRead($file.FullName)
    # Process file contents here...
}
catch {
    Write-Warning "Error processing file: $_"
}
finally {
    # This runs even if an error occurred
    if ($stream) {
        $stream.Dispose()
        Write-Verbose "File handle released"
    }
}

把finally看作负责任的露营者的规则:“无论在旅行期间发生了什么,都要在离开前清理好营地。”

错误处理最佳实践

  1. 明确指定错误操作
    在需要捕获错误的命令中,不要使用通用的ErrorAction Stop,而是有选择地使用。

  2. 使用错误变量

    Remove-Item $path -ErrorVariable removeError
    if ($removeError) {
        Write-Warning "Failed to remove item: $($removeError[0].Exception.Message)"
    }
    
  3. 适当记录错误

    • 对可恢复错误使用Write-Warning
    • 对严重问题使用Write-Error
    • 考虑将关键故障写入Windows事件日志
  4. 清理资源
    始终使用finally块来清理资源,如文件句柄和网络连接。

  5. 测试错误处理
    有意触发错误以验证您的错误处理是否按预期工作。

将所有内容整合

以下是一个完整示例,包括这些最佳实践:

function Remove-OldFiles {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$FolderPath,

        [Parameter(Mandatory)]
        [int]$DaysOld,

        [string]$LogPath = "C:\\Logs\\file-cleanup.log"
    )

    try {
        # Validate input
        if (-not (Test-Path -Path $FolderPath)) {
            throw "Folder path '$FolderPath' does not exist"
        }

        $Now = Get-Date
        $LastWrite = $Now.AddDays(-$DaysOld)

        # Find old files
        $oldFiles = Get-ChildItem -Path $FolderPath -File -Recurse |
                    Where-Object {$_.LastWriteTime -le $LastWrite}

        foreach ($file in $oldFiles) {
            try {
                Remove-Item -Path $file.FullName -ErrorAction Stop
                Write-Verbose -Message "Successfully removed [$($file.FullName)]"

                # Log success
                "$(Get-Date) - Removed file: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch [System.UnauthorizedAccessException] {
                Write-Warning "Access denied to file: $($file.FullName)"
                "$ErrorActionPreference - Access denied: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch [System.IO.IOException] {
                Write-Warning "File in use: $($file.FullName)"
                "$(Get-Date) - File in use: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch {
                Write-Warning "Unexpected error removing file: $_"
                "$(Get-Date) - Error: $_ - File: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
        }
    }
    catch {
        Write-Error "Critical error in Remove-OldFiles: $_"
        "$(Get-Date) - Critical Error: $_" |
            Add-Content -Path $LogPath
        throw  # Re-throw error to calling script
    }
}

该实现:

  • 验证输入参数
  • 对常见错误使用特定的catch块
  • 记录成功和失败
  • 提供详细的输出以进行故障排除
  • 将关键错误重新引发到调用脚本

结论

适当的错误处理对于可靠的 PowerShell 脚本至关重要。通过理解错误类型并有效使用 try/catch 块,您可以构建能够优雅处理故障并提供有意义反馈的脚本。请记住彻底测试您的错误处理 – 当您在生产环境中排除问题时,未来的您会感谢自己!

现在去捕捉那些错误吧!只需记住 – 唯一糟糕的错误是未处理的错误。

Source:
https://adamtheautomator.com/powershell-error-handling/