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 儲存最近一個原生程式的結束代碼。請立即儲存原生狀態,因為下一個原生命令可能會覆寫它。

下列模式刻意以行內文字說明,而不是放入 fenced code block:封包沒有提供可攜式的原生可執行檔或其執行證據。請將 native-command 替換成環境使用的命令,執行 native-command,並立即指派 $nativeCode = $LASTEXITCODE。如果 $nativeCode -ne 0,請回報 native-command failed with exit code $nativeCode,並在刻意設定的進入點邊界使用 exit $nativeCode。

不要用 $? 取代原生程式的數值狀態。如果包裝程式必須保留原生結果,請在執行另一個原生命令前擷取 $LASTEXITCODE,並只在刻意設定的進入點邊界進行轉換。

確保清理並區分取消作業

請將清理工作放在 finally 中,而不是放在可能結束處理的行之後。當控制流程因已處理的錯誤或正常完成而離開 try 建構時,這是處置資料流、釋放鎖定或移除暫存檔的正常位置。不要把它視為每種主機層級中斷或處理程序終止時都一定會執行的證明;需要區分時,請確認實際呼叫方式的行為。

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 啟動的子處理程序會結束並將控制權交還呼叫端,而互動式主控台可能關閉目前工作階段。內嵌 runspace 和編輯器的行為可能不同。請分別測試確切的生產命令列,包括 -File 與 -Command、設定檔、引號、輸出和結束狀態;本文範例不宣稱這些依主機而定的檢查已經執行。

建議

只在刻意設定的最上層邊界使用 exit <code>,以 return 離開目前範圍,並以 throw 回報呼叫端可處理的失敗。在迴圈或 switch 可見的地方使用 break 和 continue,而且只用 Stop-Process 終止另一個本機處理程序。將商業邏輯放在會傳回資料或擲回錯誤的函式中,可以保持可重複使用;小型進入點腳本則能將最終結果轉換為 shell、排程器、CI 工作或包裝程式可穩定使用的狀態。