Day 5 我們第一次讓 PowerShell 從「處理一台機器」走到「批次處理多台 Server」。
當時的流程大概是:
Server 清單
↓
foreach
↓
Test-Connection
↓
判斷 Online / Offline
↓
建立 Object
↓
Export-Csv
如果環境非常單純,這支 Script 已經可以用了。
但實際做維運,很快就會遇到另一個問題:
如果執行到一半發生錯誤,怎麼辦?
例如今天要巡檢:
SERVER01
SERVER02
SERVER03
SERVER04
SERVER05
理想狀況當然是:
SERVER01 OK
SERVER02 OK
SERVER03 OK
SERVER04 OK
SERVER05 OK
但實際環境更可能是:
SERVER01 OK
SERVER02 DNS 找不到
SERVER03 OK
SERVER04 Access Denied
SERVER05 OK
如果 Script 遇到 SERVER02 就直接停止,那後面的 Server 都沒有檢查到。
所以今天要處理的是一個很重要的問題:
Script 發生錯誤時,怎麼記錄錯誤,但繼續完成其他工作?
可以執行,不代表可以拿來維運
剛開始學 PowerShell 時,我們通常會先關心:
Script 有沒有跑?
結果對不對?
但真正拿去做維運之後,問題會變成:
執行失敗時發生什麼?
哪一台 Server 失敗?
什麼時間失敗?
失敗原因是什麼?
其他 Server 有沒有繼續執行?
這些其實比單純「正常時可以跑」更重要。
因為系統工程師真正需要處理的,往往就是:
不正常的情況。
PowerShell 發生錯誤時會看到什麼?
先做一個簡單的測試。
Get-Item "C:\ThisFolderDoesNotExist"
如果這個路徑不存在,就會看到錯誤訊息。
例如:
Get-Item : Cannot find path
'C:\ThisFolderDoesNotExist'
because it does not exist.
PowerShell 已經告訴我們:
哪個 Command 出錯
發生什麼事情
哪個 Path 有問題
問題是,如果我們正在寫自動化 Script,不能只靠人在螢幕前看紅字。
我們需要讓程式自己知道:
「這裡出錯了。」
接著決定該怎麼處理。
今天最重要的四個東西
Day 6 主要會使用:
try
catch
finally
-ErrorAction
它們負責的事情可以先簡單理解成:
try
↓
嘗試執行
成功
↓
繼續
失敗
↓
catch
↓
記錄 / 處理錯誤
最後
↓
finally
第一個 try / catch
先看最基本的寫法。
try {
Get-Item "C:\ThisFolderDoesNotExist"
}
catch {
Write-Host "發生錯誤"
}
看起來應該很合理。
但如果實際執行,有時候會發現:
catch 居然沒有執行。
這就要先理解 PowerShell 的兩種錯誤。
PowerShell 錯誤不完全一樣
PowerShell 的錯誤大致可以分成:
Terminating Error
與
Non-Terminating Error
可以先不用記英文。
簡單來說:
Terminating Error
這個錯誤嚴重到目前這個操作無法繼續。
這種錯誤通常可以直接進入:
catch
Non-Terminating Error
指令發生問題,但 PowerShell 覺得:
我還可以繼續執行後面的東西。
這種錯誤預設不一定會進入 catch。
而系統管理 Cmdlet 裡,這種情況其實很常見。
所以需要 -ErrorAction Stop
我們把剛才的指令改成:
try {
Get-Item `
-Path "C:\ThisFolderDoesNotExist" `
-ErrorAction Stop
}
catch {
Write-Host "發生錯誤"
}
這次如果 Path 不存在,就會進到:
catch
這裡的關鍵就是:
-ErrorAction Stop
它的意思可以理解成:
如果這個 Cmdlet 發生錯誤,把它當成需要立即處理的錯誤。
這樣 try / catch 才可以接手。
catch 裡面怎麼知道發生什麼錯?
如果只寫:
catch {
Write-Host "發生錯誤"
}
其實幫助不大。
因為真的出問題時,我們最想知道的是:
到底錯在哪裡?
PowerShell 在 catch 裡可以使用:
$_
沒錯,又看到 $_ 了。
Day 3 裡:
$_
代表 Pipeline 正在處理的 Object。
在:
catch
裡面,$_ 則代表:
目前捕捉到的 Error Record。
例如:
try {
Get-Item `
-Path "C:\ThisFolderDoesNotExist" `
-ErrorAction Stop
}
catch {
Write-Host "Error:"
Write-Host $_
}
就能看到真正的錯誤訊息。
只取得錯誤訊息
如果不想把整個 Error Record 全部輸出,可以:
$_.Exception.Message
例如:
try {
Get-Item `
-Path "C:\ThisFolderDoesNotExist" `
-ErrorAction Stop
}
catch {
Write-Host "Error Message:"
Write-Host $_.Exception.Message
}
可能得到:
Error Message:
Cannot find path 'C:\ThisFolderDoesNotExist'
because it does not exist.
這個就很適合寫進 Log。
把錯誤處理放進 Server 巡檢
回到 Day 5。
我們原本有:
foreach ($Server in $Servers) {
$PingResult = Test-Connection `
-ComputerName $Server `
-Count 1 `
-Quiet
}
現在改成:
foreach ($Server in $Servers) {
try {
$PingResult = Test-Connection `
-ComputerName $Server `
-Count 1 `
-Quiet `
-ErrorAction Stop
}
catch {
Write-Host "$Server 發生錯誤"
Write-Host $_.Exception.Message
}
}
這樣其中一台 Server 發生錯誤時,我們至少可以知道原因。
而不需要讓整支 Script 直接失去控制。
Offline 跟 Error 其實不是同一件事
這裡開始要把狀態分得更清楚。
假設:
Test-Connection
沒有收到 ICMP Response。
可能代表:
Online
Offline
Error
三種狀況最好不要混在一起。
例如:
SERVER01 → Online
SERVER02 → Offline
ABC123 → DNS / Name Resolution Error
SERVER02 可能真的存在,只是沒有回 Ping。
但 ABC123 可能連 Hostname 都不存在。
對維運來說,這兩件事情的排查方向完全不同。
把狀態分成 Online、Offline、Error
我們可以改成:
foreach ($Server in $Servers) {
try {
$PingResult = Test-Connection `
-ComputerName $Server `
-Count 1 `
-Quiet `
-ErrorAction Stop
if ($PingResult) {
$Status = "Online"
$ErrorMessage = ""
}
else {
$Status = "Offline"
$ErrorMessage = ""
}
}
catch {
$Status = "Error"
$ErrorMessage = $_.Exception.Message
}
}
現在就開始有三種結果:
Online
Offline
Error
這比單純:
True
False
更適合實際維運。
把錯誤也放進 PSCustomObject
Day 5 我們使用:
[PSCustomObject]
建立自己的資料。
今天再加入:
ErrorMessage
完整一點:
$Result = [PSCustomObject]@{
Server = $Server
Status = $Status
CheckTime = Get-Date
ErrorMessage = $ErrorMessage
}
結果可能變成:
Server Status CheckTime ErrorMessage
SERVER01 Online 2026/09/14 05:30
SERVER02 Offline 2026/09/14 05:30
WRONGSRV Error 2026/09/14 05:30 No such host is known
現在報表價值就比昨天高很多。
因為不只是說:
失敗。
而是留下:
為什麼失敗。
第一版完整錯誤處理
先組合看看:
$Servers = @(
"SERVER01"
"SERVER02"
"WRONG-SERVER-NAME"
)
$Results = @()
foreach ($Server in $Servers) {
try {
Write-Host "Checking $Server..."
$PingResult = Test-Connection `
-ComputerName $Server `
-Count 1 `
-Quiet `
-ErrorAction Stop
if ($PingResult) {
$Status = "Online"
$ErrorMessage = ""
}
else {
$Status = "Offline"
$ErrorMessage = ""
}
}
catch {
$Status = "Error"
$ErrorMessage = $_.Exception.Message
}
$Result = [PSCustomObject]@{
Server = $Server
Status = $Status
CheckTime = Get-Date
ErrorMessage = $ErrorMessage
}
$Results += $Result
}
最後:
$Results
可能得到:
Server Status CheckTime ErrorMessage
SERVER01 Online 2026/09/14 05:32
SERVER02 Offline 2026/09/14 05:32
WRONG-SERVER-NAME Error 2026/09/14 05:32 No such host...
這就已經比 Day 5 的 Script 健壯很多了。
finally 是做什麼的?
除了:
try
catch
還有:
finally
它比較特別。
不管:
try 成功
還是:
try 失敗 → catch
finally 都會執行。
例如:
try {
Write-Host "開始執行"
Get-Item `
"C:\Test" `
-ErrorAction Stop
}
catch {
Write-Host "發生錯誤"
}
finally {
Write-Host "這段一定會執行"
}
不管 C:\Test 存不存在:
這段一定會執行
都會出現。
finally 什麼時候有用?
例如 Script 開始時建立了一個:
Connection
Session
Temporary File
不管成功或失敗,最後都要:
關閉 Connection
移除 Session
刪除 Temporary File
這種事情就很適合放在:
finally
今天的 Ping Script 其實還沒有非常需要 finally。
所以我不會為了使用它硬塞進去。
但先知道:
需要「不管成功還是失敗都要執行」的清理工作時,可以考慮 finally。
Script 發生錯誤,最好還要有 Log
現在 CSV 已經會留下結果。
但一般維運工具我還會希望有一個:
Log
因為報表通常是給人看的結果。
Log 比較偏:
程式執行過程發生了什麼?
例如:
2026-09-14 05:35:01 [INFO] Starting Server Check
2026-09-14 05:35:02 [INFO] Checking SERVER01
2026-09-14 05:35:02 [OK] SERVER01 Online
2026-09-14 05:35:03 [INFO] Checking SERVER02
2026-09-14 05:35:06 [WARN] SERVER02 Offline
2026-09-14 05:35:07 [ERROR] WRONGSRV - No such host is known
2026-09-14 05:35:07 [INFO] Check Completed
這對排查自己的 Automation Script 非常有幫助。
PowerShell 怎麼寫 Log?
最簡單可以用:
Add-Content
例如:
Add-Content -Path "C:\Temp\ServerCheck.log"
-Value "Server Check Started"
執行後:
C:\Temp\ServerCheck.log
就會多一行:
Server Check Started
但正式一點,我們通常還要加入時間。
加入 Timestamp
例如:
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
然後:
Add-Content -Path "C:\Temp\ServerCheck.log"
-Value "$Time [INFO] Server Check Started"
結果:
2026-09-14 05:40:30 [INFO] Server Check Started
這已經開始有 Log 的樣子了。
不要每一次都重複寫 Add-Content
如果程式裡到處都是:
$Time = Get-Date
Add-Content ...
很快就會很亂。
所以這裡第一次可以稍微接觸:
function
我們建立:
function Write-Log {
param (
$Message,
$Level = "INFO"
)
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content `
-Path $LogFile `
-Value "$Time [$Level] $Message"
}
之後只需要:
Write-Log "Server Check Started"
或:
Write-Log "SERVER01 is Online" "OK"
甚至:
Write-Log "Cannot connect SERVER02" "ERROR"
Log 就會統一格式。
Function 可以先怎麼理解?
今天不用深入 Function。
先把它理解成:
把一段會重複使用的程式包起來,取一個名字。
原本一直寫:
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $LogFile
-Value "$Time [INFO] Something"
現在只要:
Write-Log "Something"
後面我們還會再正式使用 Function。
今天只是因為 Log 非常適合拿來示範。
今天完整版本:Server Check + Error Handling + Log
把 Day 4、Day 5、Day 6 組起來:
$Servers = @(
"SERVER01"
"SERVER02"
"WRONG-SERVER-NAME"
)
$ReportFolder = "C:\Temp"
$Date = Get-Date -Format "yyyyMMdd"
$ReportFile = "$ReportFolder\Server_Status_$Date.csv"
$LogFile = "$ReportFolder\Server_Check_$Date.log"
if (-not (Test-Path $ReportFolder)) {
New-Item `
-Path $ReportFolder `
-ItemType Directory | Out-Null
}
function Write-Log {
param (
$Message,
$Level = "INFO"
)
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content `
-Path $LogFile `
-Value "$Time [$Level] $Message"
}
Write-Log "Server connectivity check started."
$Results = @()
foreach ($Server in $Servers) {
Write-Host "Checking $Server..."
Write-Log "Checking $Server"
try {
$PingResult = Test-Connection `
-ComputerName $Server `
-Count 1 `
-Quiet `
-ErrorAction Stop
if ($PingResult) {
$Status = "Online"
$ErrorMessage = ""
Write-Log "$Server is Online" "OK"
}
else {
$Status = "Offline"
$ErrorMessage = ""
Write-Log "$Server did not respond to ICMP" "WARN"
}
}
catch {
$Status = "Error"
$ErrorMessage = $_.Exception.Message
Write-Log `
"$Server - $ErrorMessage" `
"ERROR"
}
$Result = [PSCustomObject]@{
Server = $Server
Status = $Status
CheckTime = Get-Date
ErrorMessage = $ErrorMessage
}
$Results += $Result
}
$Results |
Export-Csv -Path $ReportFile
-NoTypeInformation `
-Encoding UTF8
Write-Log "Server connectivity check completed."
Write-Host ""
Write-Host "Check completed."
Write-Host "Report : $ReportFile"
Write-Host "Log : $LogFile"
執行後現在會產生兩個檔案
以前 Day 5 只有:
Server_Status_20260914.csv
現在變成:
C:\Temp
│
├── Server_Status_20260914.csv
│
└── Server_Check_20260914.log
CSV 負責:
最終結果
Log 負責:
執行過程
這兩個角色最好分開。
CSV 可能長這樣
Server,Status,CheckTime,ErrorMessage
SERVER01,Online,2026/09/14 05:50:01,
SERVER02,Offline,2026/09/14 05:50:05,
WRONG-SERVER-NAME,Error,2026/09/14 05:50:06,No such host is known
而 Log:
2026-09-14 05:50:00 [INFO] Server connectivity check started.
2026-09-14 05:50:01 [INFO] Checking SERVER01
2026-09-14 05:50:01 [OK] SERVER01 is Online
2026-09-14 05:50:02 [INFO] Checking SERVER02
2026-09-14 05:50:05 [WARN] SERVER02 did not respond to ICMP
2026-09-14 05:50:05 [INFO] Checking WRONG-SERVER-NAME
2026-09-14 05:50:06 [ERROR] WRONG-SERVER-NAME - No such host is known
2026-09-14 05:50:06 [INFO] Server connectivity check completed.
如果三天後有人問:
為什麼這次巡檢少了一台?
我們至少有東西可以往回查。
另一個很好用的東西:$Error
PowerShell 本身也會保存最近發生過的錯誤。
可以輸入:
$Error
查看 Error History。
最新的一筆:
$Error[0]
例如:
$Error[0].Exception.Message
就可以看最新錯誤的 Message。
這在 Debug Script 時很好用。
不過正式 Automation 還是不建議完全依賴:
$Error
因為它比較像目前 PowerShell Session 裡的錯誤歷史。
真正需要長期追蹤,還是應該自己寫:
Log
-ErrorAction 還有哪些模式?
今天主要使用:
-ErrorAction Stop
另外常見的還有:
Continue
SilentlyContinue
Stop
例如:
-ErrorAction SilentlyContinue
意思比較接近:
發生錯誤就不要顯示錯誤訊息,繼續執行。
有時候很好用。
例如:
Get-Process "Notepad" `
-ErrorAction SilentlyContinue
如果 Notepad 不存在,就不會一直跳紅字。
但這個選項也要小心。
因為:
看不到 Error,不代表沒有 Error。
如果 Script 很重要,我反而會比較偏向:
Error 發生
↓
Catch
↓
記錄原因
而不是直接全部:
SilentlyContinue
把錯誤藏起來。
不要用 try / catch 把所有問題吃掉
這也是寫 Automation 很容易發生的另一個問題。
有人可能會寫:
try {
# 一大堆程式
}
catch {
}
catch 裡什麼都不做。
這樣畫面確實很乾淨。
但是如果 Script 執行失敗,我們也完全不知道原因。
這其實比看到紅字更麻煩。
比較好的做法至少留下:
Time
Server
Action
ErrorMessage
這樣未來才有辦法追。
維運 Script 最怕「看起來執行成功」
我覺得這是今天最重要的一件事情。
假設 Script 最後顯示:
Completed.
但中間其實:
50 台 Server
成功 20 台
失敗 30 台
如果沒有 Log、沒有 Status、沒有 Error Message,
那這個:
Completed
其實沒有太大意義。
所以以後看到:
Script 執行完畢
我們最好再問:
是程式跑到最後了,還是工作真的全部成功?
這兩件事情是不一樣的。
我會怎麼定義一支比較能用的維運 Script?
目前我們的 Script 已經開始有這幾層:
Input
↓
Server List
Processing
↓
foreach
Action
↓
Test-Connection
Decision
↓
if
Error Handling
↓
try / catch
Result
↓
PSCustomObject
Report
↓
CSV
Trace
↓
Log
跟 Day 2 的:
Get-Service
相比,已經開始完全不一樣了。
但本質上還是把前面每天學的一點東西慢慢加進來。
Day 6 小結
今天我們沒有增加很多新的系統管理功能。
但我們把 Script 很重要的一塊補起來了:
錯誤處理。
最需要記住的基本結構就是:
try {
# 可能發生錯誤的工作
}
catch {
# 錯誤發生後怎麼處理
}
finally {
# 不管成功失敗都要執行的工作
}
搭配:
-ErrorAction Stop
以及:
$_.Exception.Message
把真正的錯誤原因留下來。
我們的 Server Check 也從:
Online / Offline
進一步變成:
Online
Offline
Error
+
Error Message
+
CSV Report
+
Log
這樣才開始比較接近真正可以拿來長期使用的維運工具。