「夜間批次處理明明失敗了,工作排程器上卻顯示成功(0x0),沒有人發現」「明明寫了 try/catch,卻沒有進入 catch」「網路瞬斷導致每個月只掛掉一次」── 只要把 PowerShell 腳本投入實際維運,這類諮詢就必定會出現。在手邊執行、跑得順就滿足的腳本,與無人值守、每晚持續運作的腳本之間,橫著一道錯誤處理與重新執行(重試)設計的高牆。
麻煩的是,PowerShell 的錯誤模型與一般程式語言的例外模型有著微妙的差異。「明明出現了錯誤,處理卻繼續執行下去」「明明應該被 catch 住,卻悄悄溜走了」,這些情況大多不是臭蟲,而是 PowerShell 規格上本來就如此運作,如果不了解機制就直接動手寫,就會大量生產出「把失敗吞掉、卻正常結束的腳本」。
本文以用 PowerShell 將公司內部例行作業自動化的資訊系統負責人與開發者為對象,從終止錯誤與非終止錯誤的區別開始,整理原生指令的成敗判定、能讓工作排程器與監控系統判斷成敗的 exit code 設計,以及能承受暫時性錯誤的重試模式,並附上官方文件作為佐證。
1. 先講結論
- PowerShell 的錯誤分為「非終止錯誤」與「終止錯誤(陳述式終止/腳本終止)」。非終止錯誤只會顯示訊息並讓管線繼續執行,在預設情況下不會進入 try/catch。1
- 由於計算方式有兩種,先在這裡整理清楚。官方文件的分類是「非終止/陳述式終止/腳本終止」這 3 個類別,這是以引擎「要停到什麼範圍」(僅管線/僅該陳述式/整個呼叫堆疊)為軸線的分法。1 另一方面,撰寫者最先想知道的是「會不會進入 try/catch」,以這個軸線來看則分為非終止錯誤與終止錯誤兩種。本文主要採用兩分類的軸線,並在必要處觸及 3 個類別的細節。
- 想用 try/catch 捕捉的指令,加上
-ErrorAction Stop是實務上的定石。Stop 會把非終止錯誤升級為終止錯誤,使其可以在 catch 中處理。也可以在腳本開頭將$ErrorActionPreference(預設為 Continue)設為 Stop。12 -ErrorAction會針對單一指令覆寫$ErrorActionPreference。不過兩者並非完全對稱,-ErrorAction所能控制的僅限於非終止錯誤。1- 原生指令(robocopy、git、外部 EXE)的失敗,在預設情況下不會成為 PowerShell 的錯誤。非零結束代碼會讓
$?變成$false、並存入$LASTEXITCODE,但不會建立 ErrorRecord,也不會進入 catch。成敗要用$LASTEXITCODE來判定。1 - 在 PowerShell 7.4 中,
$PSNativeCommandUseErrorActionPreference已成為正式功能。設為$true時,非零結束代碼會引發非終止錯誤,若與$ErrorActionPreference = 'Stop'搭配,就能以 try/catch 捕捉(預設為$false)。32 - 在 catch 中,
$_就是 ErrorRecord。用$_.Exception可取得例外本體;若是被升級的錯誤,還可以透過$_.Exception.ErrorRecord追溯到原始的錯誤資訊。指定例外型別的 catch 區塊,能只針對預期內的錯誤進行個別處理。14 - 成敗一定要透過 exit code 傳達給外部。用
exit關鍵字設定結束代碼,若以pwsh -File/powershell.exe -File啟動,該值就會成為處理程序的結束代碼。若沒有 exit,正常結束為 0,未處理例外則為 1。56 - 重試的三大原則是:限定於暫時性錯誤、設有上限、冪等(即使同一處理執行多次,結果也不會改變的性質,詳見第 6 章)。不用重試來蒙混業務錯誤、以指數退避拉大間隔、將設計做成即使重新執行也不會造成重複處理。這三點都齊備,才稱得上是「可以重新執行的腳本」。
2. 兩種錯誤 ── 為什麼 try/catch 不起作用
PowerShell 的錯誤,首先分為非終止錯誤與終止錯誤兩種。而終止錯誤又分為陳述式終止錯誤與腳本終止錯誤,因此細分下去共有 3 個類別。非終止錯誤不會停止管線、只會回報;陳述式終止錯誤只停止該陳述式,接著進行下一個陳述式;腳本終止錯誤則會回捲整個呼叫堆疊。1
在實務上容易踩到的陷阱是非終止錯誤。Get-Content、Get-ChildItem 這類 Cmdlet 在處理個別輸入失敗時,發出的多半是非終止錯誤,明明會顯示紅色的錯誤訊息,處理卻照樣繼續,既不會進入 try/catch,也不會進入 trap。1
# 【陷阱】永遠不會進入 catch,會一路顯示到「完成」
try {
Get-Content -Path 'C:\Data\不存在的檔案.txt' # 非終止錯誤
Write-Host '完成' # 會被執行
}
catch {
Write-Host '不會來到這裡'
}
# 【定石】用 -ErrorAction Stop 升級為終止錯誤,讓 catch 可以處理
try {
Get-Content -Path 'C:\Data\不存在的檔案.txt' -ErrorAction Stop
Write-Host '完成' # 發生錯誤時不會執行
}
catch {
Write-Host "捕捉到: $($_.Exception.Message)"
}
當 -ErrorAction Stop 或 $ErrorActionPreference = 'Stop' 生效時,引擎會把非終止錯誤用 ActionPreferenceStopException 包裝起來,升級為終止錯誤。在 try 區塊內,正是這個被升級的錯誤傳到 catch,這才是正確的機制。1
另一方面,也有一開始就是終止錯誤(=什麼都不用做就會進入 catch)的情況。陳述式終止錯誤正是如此,官方文件列舉了以下發生來源。1
- 呼叫了不存在的指令時(
CommandNotFoundException) - 參數繫結失敗時(
ParameterBindingException。例如傳入無法轉換成數值參數的字串等) - .NET 的方法擲回例外時(如
[int]::Parse('abc')) - Cmdlet 或進階函式透過
$PSCmdlet.ThrowTerminatingError()回報「這次呼叫已經無法繼續」時
正如其名,它只會停止「該陳述式」,因此腳本會從下一個陳述式繼續執行。1
# 陳述式終止錯誤:這一行會停止,但下一行仍會執行
[int]::Parse('abc')
Write-Output '這一行會被執行'
# 因為是終止錯誤,即使不加 -ErrorAction Stop 也會進入 catch
try { [int]::Parse('abc') }
catch { Write-Warning "捕捉到: $($_.Exception.Message)" }
麻煩的是,即使是「檔案不存在」這種相同的情況,依 Cmdlet 內部實作的不同,有時是非終止錯誤,有時是陳述式終止錯誤。要撰寫者每次都去分辨是不切實際的,因此實務上的解答,是在想要捕捉的那一行明確加上 -ErrorAction Stop,統一成不論哪一種都能確實傳到 catch 的形式。
「那麼一律把 $ErrorActionPreference = 'Stop' 設起來不就好了嗎」這個想法算是對了一半。在無人執行的腳本中,與其吞下錯誤繼續前進,不如停下來回報失敗來得安全,因此在開頭設為 Stop 是不錯的預設值。不過要意識到,$ErrorActionPreference 會作用於該作用域與子作用域,因此連呼叫的模組或函式的行為都會跟著改變;而對於失敗也無妨的收尾處理(如刪除暫存檔),則需要個別重新加上 -ErrorAction SilentlyContinue。2
3. 在 catch 中該讀取什麼 ── ErrorRecord 的走訪方式
catch 區塊中的 $_ 裝的是 ErrorRecord。應該留存在日誌中的資訊,都可以從這裡取得。14
try {
Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
# 用指定型別的 catch,只個別處理「預期內的失敗」。
# 即使是被升級的錯誤,引擎也會用原本的例外型別來比對
Write-Warning "I/O 錯誤: $($_.Exception.Message)"
}
catch {
# 預期外的情況要連同脈絡一起記錄到日誌,再重新拋出(不要吞掉)
$rec = $_ # $_ 是 ErrorRecord
Write-Warning ('種類: {0} / 位置: {1} / 對象: {2}' -f `
$rec.Exception.GetType().FullName,
$rec.InvocationInfo.PositionMessage,
$rec.TargetObject)
throw # 不帶引數的 throw,將同一個錯誤向上層傳遞
}
finally {
# finally 無論成功、發生錯誤,或被 Ctrl+C 中止,都會執行。收尾處理放在這裡
if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}
有 3 個重點。
$_.Exception就是例外本體。以-ErrorAction Stop升級的錯誤會被包裝在ActionPreferenceStopException中,但在 catch 的型別比對時,引擎會看的是原本的例外型別(例如ItemNotFoundException),因此指定型別的 catch 可以照原樣寫。原始的 ErrorRecord 可以透過$_.Exception.ErrorRecord追溯。1$_.InvocationInfo.PositionMessage裡裝著「是哪個檔案的第幾行、哪個指令」,在無人執行的日誌中,有沒有這項資訊,調查所需的時間會相差一個數量級。- finally 區塊無論 try 成功、發生錯誤,或是被 Ctrl+C 中止,都會執行。關閉連線、刪除暫存檔之類的收尾處理,應該放在 finally 裡。7
在哪一層 catch、在哪裡寫日誌,這個設計上的討論是跨語言共通的。在「應該在哪裡 catch 例外並輸出日誌、進行錯誤處理 - 以實務向整理呼叫階層的邊界與職責」中整理的原則(在邊界處 catch、不要吞掉錯誤、避免重複記錄日誌),同樣可以直接套用在 PowerShell 上。
4. 原生指令的成敗 ── $? 與 $LASTEXITCODE,以及 7.4 的新功能
另一個大陷阱,是 robocopy、git,以及公司內部 EXE 這類原生指令。外部程式並不參與 PowerShell 的錯誤系統,而是用結束代碼來回報失敗。預設的行為如下。1
| 事件 | 行為(預設) |
|---|---|
| 非零結束代碼 | $? 變成 $false,結束代碼存入 $LASTEXITCODE |
| 產生 ErrorRecord | 不會(也不會被加入 $Error) |
| try/catch | 不會進入 |
也就是說,try { robocopy ... } catch { ... }(在預設情況下)什麼都捕捉不到。原生指令的成敗判定,要用 $LASTEXITCODE 來寫。$? 是「前一個操作是否成功」的布林值,對原生指令而言,只有結束代碼為 0 時才會是 $true。1 另外,在 Windows PowerShell 5.1 中,原生指令只要寫入了 stderr,$? 有時就會變成 $false;而 PowerShell 7 已改為只有在非零結束代碼時才會變成 $false。這是一項貼近實際情況的變更 —— 不再把寫入 stderr 視為失敗。8
# 原生指令要用 $LASTEXITCODE 判定
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
# robocopy 的 0~7 屬於成功系(是否有複製等資訊),8 以上才是失敗
throw "robocopy 失敗 (ExitCode=$LASTEXITCODE)"
}
從 PowerShell 7.4 開始,可以使用能改變這種處理方式的 $PSNativeCommandUseErrorActionPreference。它在 7.3 中以實驗性功能新增,並在 7.4 成為正式功能(mainstream)。3 設為 $true 時,結束代碼非零的原生指令會引發標明結束代碼的非終止錯誤,並依循 $ErrorActionPreference。也就是說,只要與 Stop 搭配,外部指令的失敗也能被 try/catch 接住。12
在 5.1 與 7 混用的環境中,請務必先確認版本前提。這項功能只有 PowerShell 7.4 以後才能使用,7.3 中它還是實驗性功能(功能名稱為 PSNativeCommandErrorActionPreference),需要用 Enable-ExperimentalFeature 啟用並重新啟動工作階段才能生效。3 而 Windows PowerShell 5.1 根本不存在這個變數,即使賦值 $true 也不會發生任何事(只是單純建立了一個新變數,也不會出現錯誤,所以很難察覺)。如果同一支腳本有可能同時在 5.1 與 7 上執行,就不要依賴這項功能,包括後面幾章在內,統一以 $LASTEXITCODE 判定會比較安全。
# PowerShell 7.4 以後:外部指令的失敗也用 try/catch 處理(預設為 $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'
try {
git.exe fetch origin
}
catch {
Write-Warning "git 失敗: $($_.Exception.Message)"
throw
}
& {
# 像 robocopy 這種「非零 ≠ 失敗」的指令,在腳本區塊內
# 暫時停用此功能,照舊以 $LASTEXITCODE 判定(離開區塊後即恢復原狀)
$PSNativeCommandUseErrorActionPreference = $false
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
if ($LASTEXITCODE -ge 8) { throw "robocopy 失敗 (ExitCode=$LASTEXITCODE)" }
}
robocopy 的範例正如官方文件所寫的一樣,存在把非零結束代碼當作正常資訊使用的指令,因此若要一次性啟用,就需要設計例外區段。2 如果現場只能使用 Windows PowerShell 5.1,這項功能並不存在,請統一以 $LASTEXITCODE 判定。5.1 與 7 的行為差異很容易在遷移時成為陷阱,也請一併參考「Windows PowerShell 5.1 與 PowerShell 7 的差異 ── 公司內部腳本遷移實務指南」。
5. exit code 設計 ── 讓工作排程器與監控系統能判斷成敗
捕捉到錯誤之後,接下來就是向外部回報。工作排程器與監控工具用來得知腳本成敗的手段,實質上就只有處理程序的結束代碼。以下正確掌握其規格。
- 用
exit <數值>可以明確指定腳本的結束代碼。exit同時也會把該值設定到$LASTEXITCODE。59 - 以
pwsh -File(powershell.exe -File)啟動時,exit 所指定的值會直接成為處理程序的結束代碼。若沒有 exit 陳述式,正常完成為 0,因未處理例外而結束則為 1。56 - 以
-Command啟動腳本時,腳本內像exit 10這樣的結束代碼不會被保留。會依最後一個指令的成敗被四捨五入成 0 或 1(若直接在指令字串中寫exit 10,則會傳回該值)。若維運上要區分使用腳本的結束代碼,用-File啟動才是實務定石。6
把這項規格落實成骨架,無人執行腳本的範本會是這樣。
# Invoke-NightlyExport.ps1 ── 讓工作排程器能判斷成敗的骨架
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop' # 在無人執行中,把「停止並回報」設為預設值
# 把包含標準輸出、錯誤的執行證跡留存為日誌(用 -Append 附加到每日檔案)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append
try {
Export-DailyData # 業務處理本體(呼叫已模組化的函式)
exit 0 # 明確表示成功
}
catch [System.Net.WebException] {
Write-Warning "通訊錯誤: $($_.Exception.Message)"
exit 10 # 暫時性錯誤系列 ── 保留讓工作排程器那側設定重新執行的空間
}
catch {
Write-Warning "未預期的錯誤: $($_.Exception.Message)"
Write-Warning $_.InvocationInfo.PositionMessage
exit 1 # 恆久性錯誤 ── 不重新執行,交給人來查看
}
finally {
Stop-Transcript # 放在 finally,即使透過 exit 離開也會確實關閉證跡
}
Start-Transcript 是把工作階段的輸入與輸出整個記錄成文字檔的 Cmdlet,即使不安排 echo 或重新導向,也能重現「當時畫面上顯示了什麼」。10 它與自訂的日誌函式並不互斥,作為最後一道防線一併使用是有價值的。日誌的設計與避免肥大化的對策,在「PowerShell 腳本應用 ── 安全地自動化日誌調查、封存與報表化」中有討論。
exit code 的分配訣竅是不要弄得太複雜。0=成功、1=恆久性錯誤(交由人查看)、10 幾號=暫時性錯誤(可以重新執行),這種粒度就已經足夠,可以直接對應工作排程器的「上次執行結果」或工作管理工具的成敗判定。工作排程器那一側的設定(失敗時重新執行、確認執行結果的方式)請參考「工作排程器的工作不執行、以 0x1 結束 ── 原因排查與安全的維運設計」。
6. 重試設計 ── 區分暫時性錯誤與業務錯誤
最後是重新執行。重試的價值在於「自動吸收暫時性錯誤,不在半夜把人叫醒」,但若隨便加進去,就會產生「對恆久性失敗沒完沒了地重試」「因重複處理而破壞資料」這類另一種事故。原則有三個。
- 只對暫時性錯誤重試。限定於網路瞬斷、檔案暫時被鎖定、等待相依服務啟動等,時間可以解決的失敗。輸入不正確、權限不足、設定錯誤則要立即失敗,透過 exit code 與日誌交給人處理。
- 設計上限與間隔。決定次數上限,間隔則以指數退避(2 秒、4 秒、8 秒……)逐漸拉大。對正處於故障中的對象以固定間隔持續敲打,只會妨礙它復原。
- 要做到冪等(重新執行也安全)。無論是重試,還是工作排程器的重新執行,意味的都是「同一項處理會再跑一次」。這需要以「輸出先寫入暫存檔再重新命名」「記錄已處理過的 ID,擋掉重複匯入」之類的設計為前提。
歸納成一個型式,會是這樣。
function Invoke-WithRetry {
[CmdletBinding()]
param(
[Parameter(Mandatory)] [scriptblock] $Operation,
# 若傳入 0 以下的值,會一次都不執行就正常結束,因此強制要求 1 以上
[ValidateRange(1, 100)]
[int] $MaxAttempts = 4,
# 負值會在重試時的 Start-Sleep 中變成另一種錯誤,因此在繫結參數的階段就先擋掉
[ValidateRange(0, 3600)]
[int] $BaseDelaySeconds = 2,
# 只列舉值得重試的例外型別(預設為通訊、I/O 系)
[Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
try {
# 輸出先接到變數,成功之後才回傳。若直接 return & $Operation,
# 一旦在輸出到一半時發生例外,部分輸出就會流向呼叫端,
# 導致重試成功時,同一筆資料被重複送達
$output = & $Operation
return $output
}
catch {
$ex = $_.Exception
$isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
throw # 業務錯誤,或已達重試上限 ── 直接讓它失敗
}
# 為指數成長的等待時間設定上限(即使次數較多的組態也不會等太久,
# 也不會超出 Start-Sleep 能接受的範圍)
$delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
Write-Warning "失敗(第 ${attempt} 次): $($ex.Message) ── ${delay} 秒後將重試"
Start-Sleep -Seconds $delay
}
}
}
# 用法:目標處理要先用 -ErrorAction Stop 轉為終止錯誤
Invoke-WithRetry -Operation {
Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}
# 重試 PowerShell 7 的 Invoke-RestMethod / Invoke-WebRequest 時的注意事項:
# 在 7 中,通訊失敗不再是 5.1 時代的 WebException,而是以 HttpRequestException 系傳達,
# 因此在預設狀態下不會被重試。另外,像 404 這種恆久性的 HTTP 錯誤回應也會以同一種型別
# 傳達,因此在收到回應時,要自行依狀態碼判斷「是否值得重試」
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
# 用 -SkipHttpErrorCheck,即使是錯誤回應也不會變成例外,先接住再依代碼分別拋出
$r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
# 只把暫時性的代碼以 HttpRequestException 拋出 → 會被重試
throw [System.Net.Http.HttpRequestException]::new("暫時性 HTTP 錯誤: $($r.StatusCode)")
}
if ($r.StatusCode -ge 400) {
throw "恆久性 HTTP 錯誤: $($r.StatusCode)" # 型別不同,不會被重試
}
$r.Content | ConvertFrom-Json
}
重點在於,用例外的型別明確選定重試對象。如果寫成「只要 catch 到就一律重試」,就連參數寫錯這種恆久性錯誤,也會白白嘗試 4 次、浪費等待時間。實際上線之後,把在真實日誌中觀察到的暫時性錯誤型別逐步加進 $RetryableExceptions,是比較務實的養成方式。
另外,由於 Invoke-WithRetry 本體篇幅較長,請不要每次使用時都貼到腳本裡,而是整個存成 Retry.psm1 這樣的檔案,用 Import-Module 載入。這樣可以避免每次複製貼上時,只有 catch 裡混進舊版本這類事故(模組化的做法請參考「PowerShell 腳本的參數設計與模組化 ── 從「能運作的腳本」到「可以交給別人的腳本」」)。此外,重試與錯誤分支的邏輯,正是值得用 Pester 撰寫測試的部分(參見「用 Pester 整備 PowerShell 測試 ── 讓維運腳本不易損壞的實務做法」)。
7. 實務定石(判斷表)
| 論點 | 選項 | 判斷依據 |
|---|---|---|
| 錯誤的預設行為 | 維持 Continue / 在開頭將 $ErrorActionPreference 設為 ‘Stop’ | 無人執行以「停止並回報」較安全。用於互動式調查的腳本則維持 Continue 即可2 |
| 想要 catch 的地方 | 祈禱 / 明確加上 -ErrorAction Stop | Cmdlet 大多是非終止錯誤。想捕捉的行要明確加上 Stop1 |
| 原生指令的成敗 | 放著不管 / 以 $LASTEXITCODE 判定 / 7.4 的 $PSNativeCommandUseErrorActionPreference | 5.1 混用的環境要統一以 $LASTEXITCODE 判定。若只有 7.4 以後,可用新功能+為 robocopy 等設計例外區段32 |
| 對外回報成敗 | 只靠日誌 / 設計 exit code 並以 -File 啟動 | 日誌給人看,exit code 給機器判斷,兩者都需要。以 -Command 啟動會使結束代碼失真6 |
| 執行證跡 | 只靠自訂日誌 / 併用 Start-Transcript | 作為保險,把自訂日誌接不到的輸出(如外部指令的標準輸出)也一併留存10 |
| 重試 | 所有錯誤都重試 / 限定暫時性錯誤+指數退避+冪等 | 對業務錯誤重試是事故的根源。要靠上限、間隔、冪等這三件套 |
8. 總結
- PowerShell 的錯誤分為非終止錯誤與終止錯誤,非終止錯誤在預設情況下不會進入 try/catch。想捕捉的指令要明確加上
-ErrorAction Stop,這是實務定石。 $ErrorActionPreference的預設值是 Continue。無人執行的腳本應在開頭設為 Stop,從結構上防止「把失敗吞掉、正常結束」的事故。- 原生指令的失敗在預設情況下不會進入 catch。要用
$LASTEXITCODE判定,或是若在 PowerShell 7.4 以後,可善用$PSNativeCommandUseErrorActionPreference。 - 在 catch 中,要從
$_(ErrorRecord)把例外的型別、訊息、位置資訊記錄到日誌,收尾處理則放在 finally。finally 即使遇上 Ctrl+C 或 exit 也會執行。 - 成敗要以 exit code 向外部回報。以
-File啟動的話,exit的值會直接成為結束代碼,讓工作排程器與監控系統能判斷成敗。 - 重試要遵守限定暫時性錯誤、上限指數退避、冪等這三大原則。恆久性錯誤則要立即失敗、交給人處理。
相關文章
- PowerShell 腳本應用 ── 安全地自動化日誌調查、封存與報表化
- 用 Pester 整備 PowerShell 測試 ── 讓維運腳本不易損壞的實務做法
- 工作排程器的工作不執行、以 0x1 結束 ── 原因排查與安全的維運設計
- 應該在哪裡
catch例外並輸出日誌、進行錯誤處理 - 以實務向整理呼叫階層的邊界與職責 - PowerShell 的執行原則與指令碼簽署 ── 從「用 Bypass 蓋住問題」的做法畢業的實務指南
- PowerShell 腳本的引數設計與模組化 ── 從「能動的腳本」到「能交給別人用的腳本」
相關諮詢領域
合同會社小村軟體提供夜間批次處理、例行作業腳本的錯誤處理與重試設計審查,針對「明明失敗了卻被當成成功」「每個月只出一次問題」這類間歇性故障的調查,以及既有腳本資產的維運品質改善。
參考連結
-
Microsoft Learn,about_Error_Handling。說明非終止錯誤、陳述式終止錯誤、腳本終止錯誤這 3 種分類,非終止錯誤在預設情況下不會進入 catch/trap,-ErrorAction Stop 的升級機制(ActionPreferenceStopException 與 $_.Exception.ErrorRecord),指定型別的 catch 會以原本的例外型別比對,$? 與 $LASTEXITCODE 的規格,原生指令的非零結束代碼在預設情況下不會產生 ErrorRecord,以及 $PSNativeCommandUseErrorActionPreference 的行為。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17
-
Microsoft Learn,about_Preference_Variables。說明 $ErrorActionPreference 的預設值為 Continue,-ErrorAction 參數會在個別指令中優先,設定會套用於該作用域與子作用域,$PSNativeCommandUseErrorActionPreference 的預設值為 $false,以及像 robocopy 這種把非零結束代碼當作資訊使用的指令,在腳本區塊內暫時停用該功能的範例。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn,What’s New in PowerShell 7.4。說明實驗性功能 PSNativeCommandErrorActionPreference($PSNativeCommandUseErrorActionPreference)已在 PowerShell 7.4 成為正式功能(mainstream)。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,Everything you wanted to know about exceptions。說明可以在 catch 區塊中透過 $_ 存取例外資訊,加上 -ErrorAction Stop 的指令與 Write-Error 的錯誤都能在 catch 中處理,以及 try/finally 的資源釋放模式。 ↩ ↩2
-
Microsoft Learn,about_Language_Keywords。說明 exit 關鍵字會設定結束代碼,並反映到 $LASTEXITCODE,以 pwsh -File 啟動的腳本會把 exit 的數值引數作為結束代碼傳回,以及沒有 exit 陳述式時,正常完成為 0、未處理例外為 1。 ↩ ↩2 ↩3
-
Microsoft Learn,about_Pwsh。說明以 -File 啟動時結束代碼的決定方式,以及以 -Command 啟動時,0 與 1 以外的結束代碼會被轉換成 1,因此若要保留結束代碼,需要 exit $LASTEXITCODE。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,about_Try_Catch_Finally。說明 try/catch/finally 的語法、指定型別的 catch 區塊與多重 catch,以及 finally 區塊除了成功、發生錯誤時之外,在被 Ctrl+C 中止或 catch 內執行 exit 時也會執行。 ↩
-
Microsoft Learn,Differences between Windows PowerShell 5.1 and PowerShell 7.x。說明在 PowerShell 7 中,原生指令僅寫入 stderr 並不會使 $? 變成 $false,已改為只有非零結束代碼時才會變成 $false 這項變更。 ↩
-
Microsoft Learn,about_Automatic_Variables。說明 $LASTEXITCODE 會保存原生程式或腳本的結束代碼,以及以 pwsh -File 呼叫時,因例外結束會設為 1、正常完成會設為 0,或設為 exit 關鍵字所指定的值。 ↩
-
Microsoft Learn,Start-Transcript。說明把工作階段的指令與主控台輸出記錄到文字檔、以 -Append 附加寫入、預設的儲存位置與檔名,以及以 Stop-Transcript 停止記錄。 ↩ ↩2
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
PowerShell 腳本太慢時該檢查的地方 ── 陣列・管線・比對的訣竅
整理 PowerShell 腳本變慢的常見原因。從實務角度解說陣列 += 造成 O(n^2) 的原因、管線與 foreach 的差異、比對的雜湊表化、檔案 I/O 的改善,以及正確的測量方式。
告別 Write-Host ── PowerShell 的輸出資料流與日誌設計
本文整理 PowerShell 六個輸出資料流的使用區分、Write-Host 所存在的問題與正確用途、函式傳回值被污染的原因、透過 -Verbose 與 -InformationVariable 由呼叫端進行控制的方法,以及結構化日誌的留存方式。
PowerShell 的並行處理 ── ForEach-Object -Parallel 與工作(Job)的選用之道
從實務角度整理 ForEach-Object -Parallel、Start-ThreadJob、Start-Job 的差異與選用方式,$using: 與執行緒安全性,ThrottleLimit 的決定方法,以及反而變慢的情況。
從 PowerShell 正確呼叫外部 exe ── 引數的引號、結束代碼、亂碼陷阱
從 PowerShell 呼叫 robocopy 或公司內部 EXE 時,引數會被破壞、拿不到結束代碼、輸出出現亂碼。本文從實務角度整理 PowerShell 7.3 的引數傳遞變更、停止剖析權杖 --%、以及 Start-Process 的使用時機。
PowerShell 中安全處理認證資訊 ── 把明文密碼逐出腳本
本文整理將 PowerShell 腳本中的明文密碼遷移至安全保存方式的做法,說明 SecureString 的實際樣貌與限制、Export-Clixml 透過 DPAPI 保存的機制,以及 SecretManagement/SecretStore 的適用場合。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
常見問題
整理諮詢這個主題時常見的問題。
- 為什麼在 PowerShell 中寫了 try/catch,卻沒有進入 catch?
- 因為 Cmdlet 所發出的錯誤大多是非終止錯誤(non-terminating error)。try/catch 只會捕捉終止錯誤,非終止錯誤只會顯示訊息並繼續執行,不會進入 catch。實務上的定石做法,是在想要捕捉的指令加上 -ErrorAction Stop(或是在腳本開頭將 $ErrorActionPreference 設為 'Stop')。這樣一來,非終止錯誤就會被升級為終止錯誤,變得可以用 try/catch 處理。
- $? 和 $LASTEXITCODE 該怎麼區分使用?
- $? 是表示前一個操作是否成功的布林值,無論是 Cmdlet 還是原生指令都會設定它。$LASTEXITCODE 則是最後執行的原生程式(或已 exit 的腳本)的結束代碼,Cmdlet 發生錯誤時並不會改變它。要判定 robocopy、git 等外部指令的成敗時,用能確認結束代碼實際意義的 $LASTEXITCODE 來判斷較為確實。請注意,原生指令的非零結束代碼在預設情況下不會進入 catch。
- 要如何讓工作排程器判斷 PowerShell 腳本的成敗?
- 在腳本結尾(以及 catch 區塊)用 exit 關鍵字明確指定結束代碼,工作排程器那一側則以 pwsh -File(或 powershell.exe -File)啟動,監控「上次執行結果」的數值。以 -File 啟動時,exit 所指定的值會直接成為處理程序的結束代碼;若沒有 exit,正常結束為 0,發生未處理例外則為 1。若以 -Command 啟動,0 與 1 以外的結束代碼都會被轉換成 1,因此若要以結束代碼建立維運機制,用 -File 啟動才是實務定石。
- 重試應該針對哪些錯誤進行?
- 應限定於重新嘗試就可能改變結果的暫時性錯誤(網路瞬斷、檔案暫時被鎖定、等待服務啟動等)。輸入資料錯誤、權限不足、設定錯誤等業務錯誤、恆久性錯誤,無論重試幾次都會失敗,因此不應重試,而要立即失敗,透過日誌與 exit code 通知人員。即使要重試,也必須為次數與間隔設定上限,以指數退避拉大間隔,並且前提是要把處理設計成冪等,讓重新執行也不會造成重複處理。
- PowerShell 7.4 的 $PSNativeCommandUseErrorActionPreference 是做什麼用的設定?
- 這是在原生指令以非零結束代碼結束時,觸發 PowerShell 錯誤(非終止錯誤)的設定。它在 PowerShell 7.3 中以實驗性功能新增,並於 7.4 成為正式功能(預設為 $false)。設為 $true 時會依循 $ErrorActionPreference,因此若與 Stop 搭配使用,就能以 try/catch 捕捉外部指令的失敗。不過,像 robocopy 這類會把非零結束代碼當作正常資訊使用的指令也存在,因此需要留意在那個區段內暫時改回 $false 等處理。