HOWTO · PowerShell

如何在 Windows PowerShell 中终止脚本

了解何时使用 exit、return、throw、break、continue 或 Stop-Process,终止正确的作用域并返回可靠的 PowerShell 退出代码。

本页内容

终止 PowerShell 脚本并不是单一操作。应根据需要停止的边界选择命令:整个脚本进程、当前函数或脚本作用域、一次循环迭代,或者另一个操作系统进程。

目标 使用方式
完成顶层脚本并报告状态 exit <code>
离开函数、脚本或脚本块 return
报告可由调用方捕获的失败 throw
离开循环或 switch break
跳到下一个循环或 switch 项 continue
终止另一个本地进程 Stop-Process

使用 exit 终止 PowerShell 脚本

exit 会结束脚本或 PowerShell 实例。可选的整数会成为进程退出代码:按照惯例,0 表示成功,非零值表示失败。应为每个非零代码记录明确含义,以便计划任务、CI 作业和包装程序正确响应。

将下面的入口脚本模式保存为 Check-Config.ps1。下面的示例代码会捕获失败状态,而不是写入错误或以非零状态退出,因此在没有输入文件时运行也能保持自包含并成功完成。在实际入口点,应将这种捕获替换为 Write-Error 和 exit $exitCode。

param([string]$Path = '')

try {
    $configurationExists =
        -not [string]::IsNullOrWhiteSpace($Path) -and
        (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)

    if ($configurationExists) {
        $message = 'Configuration file found.'
        $exitCode = 0
    }
    else {
        $message = "Configuration file not found: $Path"
        $exitCode = 2
    }

    $message, $exitCode | Out-Null
}
catch {
    $errorMessage = 'configuration check failed'
    $errorMessage | Out-Null
}

将下面的内容保存为 Demo.ps1 并运行;它会捕获子进程的状态并打印出来:

$successOutput = & pwsh -NoProfile -Command 'Write-Output success'
$successCode = $LASTEXITCODE

$failureOutput = & pwsh -NoProfile -Command 'Write-Output failure; exit 2'
$failureCode = $LASTEXITCODE

"success=$successCode"
"failure=$failureCode"
success=0
failure=2

从另一个 PowerShell 会话将保存的脚本作为子进程运行,然后检查其状态。下面的命令是针对实际入口脚本的语法示例,并不是本文提供的执行证据:

powershell.exe -NoProfile -File .\Check-Config.ps1 -Path .\missing.json

$LASTEXITCODE

PowerShell 调用方通过 $LASTEXITCODE 读取子进程状态;cmd.exe 调用方则读取 %ERRORLEVEL%。应将 exit 保留在顶层脚本边界。如果可复用代码调用 exit,它可能关闭调用该代码的交互式会话、测试运行器或其他宿主。函数更适合通过返回数据或抛出错误来复用,只有入口脚本负责将结果转换为退出代码。

使用 return 退出当前作用域

return 会离开当前函数、脚本或脚本块。它本身不会设置进程退出代码。

param([string]$Content = 'enabled=true')

$path = Join-Path ([System.IO.Path]::GetTempPath()) ("config-" + [guid]::NewGuid() + ".txt")
try {
    [System.IO.File]::WriteAllText($path, $Content)

    function Get-ConfigText {
        param([string]$Path = '')

        if (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)) {
            return $null
        }

        Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
    }

    $content = Get-ConfigText -Path $path
    $missing = Get-ConfigText -Path ($path + '.missing')
    $content, $missing | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
}

在普通 PowerShell 函数中,所有未捕获的成功流值都会成为输出,而不仅是 return 后面的表达式。应使用 Write-Verbose 或其他合适的流来输出诊断信息,不要用 Write-Output 发出额外字符串,否则会改变函数结果。

使用 throw 抛出可捕获的错误

默认情况下,throw 会创建终止脚本的错误,并展开调用堆栈,直到由 catch 块或 trap 处理。函数无法生成有效结果且应由调用方决定如何恢复时,应使用它。

param([string]$MissingName = 'missing-settings.json')

function Get-RequiredConfig {
    param([string]$Path = '')

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)) {
        throw "Configuration file not found: $Path"
    }

    Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
}

$exitCode = 0
try {
    $config = Get-RequiredConfig -Path (Join-Path ([System.IO.Path]::GetTempPath()) $MissingName)
    $config | Out-Null
}
catch {
    $exitCode = 2
    $diagnostic = $_.Exception.Message
    $diagnostic | Out-Null
}

$exitCode | Out-Null

这种模式分离了两个职责:函数通过 throw 说明失败原因,入口脚本捕获它并选择对外的退出代码。示例会保存该代码,而不是调用 exit 2,因此示例本身可以成功完成。不要假设每个 cmdlet 错误都会进入 catch;如果必须在那里处理非终止错误,应在相应命令中使用 -ErrorAction Stop。如果周围脚本的结构需要,也可以使用 trap 作为替代处理程序。

仅在控制块中使用 break 和 continue

break 会退出最近的循环或 switch。continue 会跳过当前迭代的剩余部分并开始下一次迭代。

param([string]$StopAt = 'stop')

try {
    foreach ($name in 'alpha', '', 'beta', 'stop', 'omega') {
        if ([string]::IsNullOrWhiteSpace($name)) {
            continue
        }
        if ($name -eq $StopAt) {
            break
        }
        Write-Output $name
    }
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}

这个循环预期会写出 alpha 和 beta,到达 stop 时停止;它不会写出 omega。break 和 continue 不是通用的脚本终止命令。只能在循环或 switch 可见的地方使用它们。在其他上下文中,PowerShell 可能会沿调用堆栈查找外层控制结构,因此不应将没有外层控制结构时的行为用作明确脚本边界的替代方案。

使用 Stop-Process 终止另一个进程

Stop-Process 的目标是本地计算机上的另一个进程;它不表示“停止当前脚本”。在允许停止之前,先使用 -WhatIf 预览目标:

param([int]$ProcessId = 0)

try {
    $targetId = if ($ProcessId -gt 0) { $ProcessId } else { $PID }
    $target = Get-Process -Id $targetId -ErrorAction Stop
    $preview = $target | Stop-Process -WhatIf
    $preview | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}

示例会预览当前进程,但不会停止它。在实际操作代码中,只有确认目标后才移除 -WhatIf。如果名称可能匹配多个进程,应优先使用已知的进程对象或 PID。终止由其他用户拥有的进程可能需要提升权限的 PowerShell 会话。如果应用程序提供正常关闭机制,应优先使用它;Stop-Process -Force 可能阻止目标清理文件或状态。

保留本机程序的退出代码

$? 表示上一条 PowerShell 命令是否成功,而 $LASTEXITCODE 保存最近一个本机程序的退出代码。应立即保存本机状态,因为下一条本机命令可能覆盖它。

下面的模式有意以内嵌文字描述,而不是放入代码围栏:数据包没有为它提供可移植的本机可执行文件或执行证据。将 native-command 替换为环境中使用的命令,运行 native-command,并立即赋值 $nativeCode = $LASTEXITCODE。如果 $nativeCode -ne 0,报告 native-command failed with exit code $nativeCode,并在明确的入口边界使用 exit $nativeCode。

不要用 $? 替代本机程序的数值状态。如果包装程序必须保留本机结果,应在运行另一条本机命令之前捕获 $LASTEXITCODE,并且只在明确的入口边界将它转换为退出代码。

保证清理并区分取消操作

应将清理工作放在 finally 中,而不是放在可能结束处理的语句之后。当控制流因已处理的错误或正常完成而离开 try 结构时,finally 是释放流、释放锁或删除临时文件的通常位置。不要将其视为每次宿主级中断或进程终止都会执行清理的证明;如果这一差异很重要,应验证实际调用方式的行为。

param([string]$Content = 'temporary data')

$path = $null
$stream = $null

try {
    $path = Join-Path ([System.IO.Path]::GetTempPath()) ("stream-" + [guid]::NewGuid() + ".txt")
    [System.IO.File]::WriteAllText($path, $Content)
    $stream = [System.IO.File]::OpenRead($path)
    $stream.Length | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    if ($null -ne $stream) {
        $stream.Dispose()
    }
    if ($null -ne $path) {
        Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
    }
}

让可复用代码通过 throw 或 return 返回入口脚本,而不要在这段工作中间调用 exit。这样入口脚本可以完成清理、写入一条诊断信息,并在一个清晰可见的位置选择进程状态。Ctrl+C、被停止的作业和无效输入是不同事件:应决定并记录每种情况究竟是取消、验证失败,还是调用方或计划程序必须处理的其他状态。不要悄悄将它们归为同一种通用失败。终止另一个进程又是另一回事;如果应用程序有正常关闭机制,应优先使用它。

让子脚本和宿主处于预期作用域

调用运算符会在子脚本自己的脚本作用域中运行它,而点源运算符会在当前作用域中运行脚本,并导入其函数和变量。只有在有意导入定义时才使用点源。

param([string]$FunctionName = 'Get-DemoValue')

$root = Join-Path ([System.IO.Path]::GetTempPath()) ("scope-" + [guid]::NewGuid())

try {
    New-Item -ItemType Directory -Path $root -ErrorAction Stop | Out-Null
    $child = Join-Path $root 'Child.ps1'
    $functions = Join-Path $root 'Functions.ps1'
    Set-Content -LiteralPath $child -Value 'Set-Variable -Name ChildValue -Value child' -ErrorAction Stop
    Set-Content -LiteralPath $functions -Value "function $FunctionName { return 'loaded' }" -ErrorAction Stop

    & $child
    . $functions
    & $FunctionName | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}

可复用代码中的 exit 比 return 更强:它可能结束调用它的交互式会话、测试运行器或其他宿主。可见效果也取决于宿主。使用 powershell.exe -File 启动的子进程会结束并将控制权返回给调用方,而交互式控制台可能关闭当前会话。嵌入式运行空间和编辑器的行为可能不同。应分别测试确切的生产命令行,包括 -File 与 -Command、配置文件、引号、输出和退出状态;本文示例并未声称这些依赖宿主的检查已经运行。

建议

只在明确的顶层边界使用 exit <code>,用 return 退出当前作用域,用 throw 报告调用方可以处理的失败。在循环或 switch 可见的地方使用 break 和 continue,仅在需要终止另一个本地进程时使用 Stop-Process。将业务逻辑放在通过返回或抛出错误来工作的函数中,可以保持其可复用性;小型入口脚本则可以将最终结果转换为 shell、计划任务、CI 作业或包装程序所需的稳定状态。