「負責人寫的 PowerShell 腳本正在運作,但只有那個人才碰得動」「伺服器名稱和路徑到處直接寫死在程式碼裡,每次環境改變就要改本體」「引數傳錯了也照樣默默動起來,後來才發現」── 在運維自動化的諮詢中,比起腳本本身,「腳本要怎麼交付、怎麼養大」成為問題的案例非常多。
如果只是自己手邊執行,變數直接寫死的「能動的腳本」不會造成困擾。但是一旦要放上工作排程器、交給同事、在多台伺服器上重複使用,引數設計與共用處理的整理就會決定品質。幸運的是,PowerShell 從一開始就以語言功能的形式,準備好了通往這種「能交給別人用的腳本」的工具:param 區塊、驗證屬性、以註解為基礎的說明,以及模組。
本文以中小企業資訊系統(IT)或運維人員手邊已有的「能動的腳本」為起點,依「引數設計 → 輸入驗證 → 說明與 -WhatIf 對應 → .psm1 模組化 → 公司內部共用」的順序,整理逐步提升品質的實務步驟。以 PowerShell 7.x 為基準,同時也會隨時補充只能使用 Windows PowerShell 5.1 的現場需要注意的地方。
1. 先講結論
- 引數要用 param 區塊宣告,並加上 [CmdletBinding()] 把它變成「高度函式(advanced function)」,這是出發點。共用參數(-Verbose、-ErrorAction 等)會自動附加,而且傳入未定義的參數會產生繫結錯誤,因此可以防止打錯字卻被默默忽略的事故。1
- 必要的引數要用 [Parameter(Mandatory)] 宣告,並且一定要加上型別。忘記指定必要引數的呼叫會在執行前就停下來,型別不符的值也會在執行前就被擋下。2
- 格式檢查不要寫在 if 陳述式裡,而要交給驗證屬性(ValidateSet / ValidateRange / ValidateScript / ValidateNotNullOrEmpty)。驗證失敗時函式根本不會被呼叫,可以從結構上消除「執行到一半才壞掉」的問題。原則是讓錯誤儘早在入口處出現。2
- 開/關類型的引數要用 [switch] 宣告。用字串接收 $true 或 $false 這種自創的旗標引數,是呼叫端出事故的根源。2
- 只要寫好以註解為基礎的說明(.SYNOPSIS/.EXAMPLE),Get-Help 對自製指令也能生效。擺脫「用法自己讀程式碼」,是交給別人使用的最低條件。3
- 會造成變更的函式要宣告 SupportsShouldProcess,讓它支援 -WhatIf/-Confirm。讓呼叫端可以在執行前確認影響範圍,是運維腳本的安全裝置中,投資報酬率最高的一項。4
- 多個腳本共用的函式要切出來放進 .psm1 模組,放在 $env:PSModulePath 底下。只要放置的位置正確,就不需要 Import-Module 也會自動載入。psd1 資訊清單只要在「要分發出去的階段」再加上就足夠了。5678
- 模組與腳本要用 Git 做版本控管,透過共用資料夾發布時要注意與執行原則的關係。UNC 路徑上的腳本有時會被 RemoteSigned 拒絕執行。9
2. param 區塊與 [CmdletBinding()] ── 通往「高度函式」的入口
首先,現場常見的典型「能動的腳本」是這樣的。
# 常見範例: 變數直接寫死。每次環境改變都要改本體
$logDir = "D:\Logs\AppA"
$days = 90
Get-ChildItem $logDir -Filter *.log |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$days) }
把這段改寫成 param 區塊與 [CmdletBinding()]。
[CmdletBinding()]
param(
# 必要。忘記指定的呼叫會在執行前就停下來
# (注意: [string] 是表明意圖,而非拒絕。數值等會自動轉換為字串
# 再傳進來,因此想嚴格擋下的條件要寫在後面說明的驗證屬性裡)
[Parameter(Mandatory)]
[string]$LogDir,
# 可省略的引數要給業務上安全的預設值
[int]$Days = 90
)
Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$Days) }
[CmdletBinding()] 是一個宣言,表示「讓這個函式(腳本)以和已編譯 Cmdlet 相同的方式運作」,加上它的函式就會變成高度函式(advanced function)。效果有三個。1
- 共用參數會自動附加。不需要自己實作 -Verbose、-Debug、-ErrorAction、-ErrorVariable 等,呼叫端就能直接使用。腳本內的 Write-Verbose 只在指定 -Verbose 時顯示這種標準行為,也會直接跟著到手。
- 引數的錯誤會在執行前就停下來。在高度函式中,傳入未定義的參數名稱,或是沒有對應位置參數的多餘引數,都會導致參數繫結失敗。像
-Dyas 30這種打錯字卻被默默忽略、以預設值繼續運作的事故就不會發生。1 - 可以使用 $PSCmdlet。這是通往後面會提到的 ShouldProcess 等 Cmdlet 專用功能的入口。10
如果省略了加上 Mandatory 的引數,PowerShell 會在執行前提示輸入。這是防止「明明是必要引數卻忘記傳、就以預設值運作」的第一道安全裝置。2 如果要說有什麼要注意的地方,就是這種「提示輸入」的動作和無人值守執行不合。從工作排程器啟動時如果漏掉了必要引數,工作就可能卡在一個沒有人能回答的提示上動彈不得。在無人值守執行時,加上 -NonInteractive 啟動,讓它以立即出錯的方式失敗、而不是等待提示,是標準做法。
由於「該寫在哪裡」不太容易理解,這裡也附上工作排程器「動作」分頁的填寫範例(以 PowerShell 7 執行的情況)。
| 欄位 | 填寫範例 |
|---|---|
| 程式或腳本 | C:\Program Files\PowerShell\7\pwsh.exe |
| 新增引數(選用) | -NoProfile -NonInteractive -File "C:\Scripts\Remove-OldAppLog.ps1" -LogDir "D:\Logs\AppA" -Days 90 |
| 開始位置(選用) | C:\Scripts |
如果要在 Windows PowerShell 5.1 上執行,程式欄位就是 powershell.exe。要抓住的重點有三個:用 -NoProfile 消除設定檔載入造成的環境差異與啟動延遲;用 -NonInteractive 防止前面提到的等待提示;以及只要腳本路徑或引數的值裡可能含有空白,就要用引號括起來。寫在 -File 之後的內容會當作腳本的引數傳遞,因此腳本本身的參數要排在 -File 之後。如果「開始位置(選用)」留空,工作目錄就會變成預設位置,因此使用相對路徑的腳本務必要指定這個欄位。
另外,要為函式命名並公開時,要採用「動詞-名詞(Verb-Noun)」的形式,動詞要從可以用 Get-Verb 確認的核可動詞中選擇。使用未核可的動詞也能運作,但在匯入模組時會出現警告。11
3. 輸入驗證要在入口處進行 ── 用驗證屬性讓「錯誤儘早出現」
如果把引數的格式檢查寫在函式本體的 if 陳述式裡,很容易混入檢查遺漏,或「副作用先於檢查執行」這類錯誤。在 PowerShell 中,可以用驗證屬性把驗證和參數宣告寫在一起。驗證會在函式被呼叫之前進行,一旦失敗,本體連一行都不會執行。2
[CmdletBinding()]
param(
# 只接受存在的資料夾。$_ 是驗證對象的值
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$LogDir,
# 保留天數限制在 1~3650 天。防止 0 或負數造成「所有檔案都是對象」的事故
[ValidateRange(1, 3650)]
[int]$Days = 90,
# 固定選項。也能讓 Tab 鍵補完生效
[ValidateSet('Zip', 'Move', 'ReportOnly')]
[string]$Mode = 'ReportOnly',
# 擋掉空字串、$null。字串引數的基本配備
[ValidateNotNullOrEmpty()]
[string]$ReportName = 'log-report',
# 開/關用 switch。指定就是 $true,省略就是 $false
[switch]$IncludeSubfolders
)
整理一下使用區分的判斷標準。
| 屬性 | 用途 | 現場中的典型範例 |
|---|---|---|
| ValidateSet | 把值限制在固定的選項內,也能讓 Tab 鍵補完生效2 | 動作模式、環境名稱(Dev/Test/Prod) |
| ValidateRange | 數值、日期的範圍限制2 | 保留天數、重試次數、連接埠號碼 |
| ValidateScript | 用任意腳本驗證。以 $false 或例外表示失敗2 | 路徑是否存在的確認、日期的前後關係 |
| ValidateNotNullOrEmpty | 拒絕 $null、空字串、空集合2 | 幾乎所有的字串引數 |
| ValidatePattern | 用正規表示式做格式檢查2 | 單據編號、主機名稱的命名規則 |
補充兩點。第一,驗證屬性要注意宣告順序。如果把驗證屬性寫在型別之後,可能會驗證到型別轉換前的值,引發預期外的失敗,因此「屬性 → 型別 → 變數名稱」的順序,是官方文件推薦的最佳做法。2 第二,ValidateScript 的 ErrorMessage 引數(自訂錯誤訊息)是 PowerShell 6 以後才有的功能,Windows PowerShell 5.1 無法使用。2 在混用 5.1 的環境中,比較穩妥的做法是在驗證腳本內用 throw 拋出自己的訊息,或是維持使用預設訊息。
一旦開始在 ValidateScript 裡寫複雜的驗證邏輯,那也是該寫測試的訊號。驗證邏輯本身的動作確認,如果套進「用 Pester 整備 PowerShell 測試」中整理的型式,就會比較不容易損壞。
4. 管線輸入的基礎 ── ValueFromPipeline 與 process 區塊
自製函式如果也能像 Get-Content servers.txt | Test-AppServer 這樣用在管線中,就能以和 PowerShell 標準指令相同的感覺來組合使用。需要的只有兩件事:ValueFromPipeline 的宣告,以及 process 區塊。2
function Test-AppServer {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
begin { $results = @() } # 在管線處理之前只執行一次
process {
# 每個從管線流入的元素都會執行一次
foreach ($name in $ComputerName) {
$results += [pscustomobject]@{
ComputerName = $name
# -ComputerName 在 5.1 和 7 都能用(7 新增的 -TargetName 在 5.1 沒有)
Reachable = Test-Connection -ComputerName $name -Count 1 -Quiet
}
}
}
end { $results } # 最後只執行一次
}
該掌握的重點只有一個。如果要接收管線輸入,就要把處理寫在 process 區塊裡。沒有 process 區塊的話,即使在管線中傳入多個值,也只會處理最後一筆,這是典型的臭蟲。10 begin 和 end 可以省略,拿不定主意時,只要記住「本體寫在 process,需要彙總才用 begin/end」,實務上就夠用了。
由於這個陷阱不容易有實際感受,這裡附上一個 NG 範例並列比較。
# NG 範例: 沒有 process 區塊。即使在管線中傳入 3 筆,也只會處理最後 1 筆
function Test-AppServerBad {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
# 如果 begin/process/end 一個都不寫,本體整個會被當作 end 區塊處理,只會「最後動一次」。
# 管線的元素會逐一綁定到參數,因此進到 end 時,留下來的是最後 1 筆
foreach ($name in $ComputerName) {
[pscustomobject]@{ ComputerName = $name }
}
}
'SV01', 'SV02', 'SV03' | Test-AppServerBad # → 只有 SV03 這 1 行。SV01 和 SV02 被默默丟棄
'SV01', 'SV02', 'SV03' | Test-AppServer # → 回傳 3 行(前面附有 process 區塊的版本)
麻煩的地方在於一個錯誤都不會出現。而且如果像 Test-AppServerBad -ComputerName 'SV01','SV02','SV03' 這樣用引數傳入,3 筆都會被正確處理,因此如果只用引數呼叫來做動作確認,根本不會發現問題。寫了接收管線輸入的函式之後,請務必加入一項「流入多筆資料」的確認測試。
5. 讓 Get-Help 與 -WhatIf 生效 ── 交給別人使用的最低條件
5.1. 以註解為基礎的說明
熟悉 PowerShell 的人,遇到不認識的指令時,第一件事就是打 Get-Help。自製函式能不能融入這種文化,取決於有沒有寫以註解為基礎的說明。只要寫好帶有特殊關鍵字的註解,Get-Help 就會用和標準 Cmdlet 相同的格式顯示說明。3
這裡列出常用的關鍵字。不需要全部寫,最低限度是 .SYNOPSIS 和 .EXAMPLE,如果要交給別人使用,再依序加到 .DESCRIPTION 和 .PARAMETER 就足夠了。3
| 關鍵字 | 要寫什麼 |
|---|---|
| .SYNOPSIS | 一行摘要。會顯示在 Get-Help 的最前面 |
| .DESCRIPTION | 詳細說明。前提條件或副作用寫在這裡 |
| .PARAMETER 參數名稱 | 每個參數的說明。關鍵字後面接上參數名稱 |
| .EXAMPLE | 使用範例。第一行寫要執行的指令,接下來的行寫說明。可以寫多個 |
| .INPUTS | 可以透過管線接收的物件型別 |
| .OUTPUTS | 傳回的物件型別 |
| .NOTES | 補充說明。作者、更新日期、已知限制等 |
| .LINK | 相關指令或 URL。第一個 URL 會是 Get-Help -Online 的跳轉目標 |
5.2. SupportsShouldProcess 與 -WhatIf
對於會執行刪除、移動、變更設定的函式,要宣告 [CmdletBinding(SupportsShouldProcess)]。光是這樣,-WhatIf 與 -Confirm 參數就會自動被加上,本體則用 $PSCmdlet.ShouldProcess() 的回傳值,來判斷是否要實際執行變更。4
把兩者都組合進去,可以交給別人使用的完成版函式,會是這樣的形式。
function Remove-OldAppLog {
<#
.SYNOPSIS
從指定資料夾刪除超過保留期限的日誌檔案。
.DESCRIPTION
刪除 LastWriteTime 早於保留天數的 *.log 檔案。
使用 -WhatIf 可以只確認刪除對象,不會實際刪除。
.EXAMPLE
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -Days 90 -WhatIf
只顯示刪除對象,不會實際刪除。
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$LogDir,
[ValidateRange(1, 3650)]
[int]$Days = 90
)
process {
$limit = (Get-Date).AddDays(-$Days)
# 用 -File 排除資料夾(避免誤刪名稱以 ".log" 結尾的資料夾)
Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt $limit } |
ForEach-Object {
# ShouldProcess 回傳 $false 的情況,是 -WhatIf 時,以及被 -Confirm 拒絕時
if ($PSCmdlet.ShouldProcess($_.FullName, "刪除")) {
Remove-Item -LiteralPath $_.FullName
}
}
}
}
只要打 Remove-OldAppLog -LogDir D:\Logs\AppA -WhatIf,就只會顯示「What if: …」的清單,不會刪除任何東西。把會造成變更的腳本交給別人時,要連同用 -WhatIf 做預演的步驟一起交出去── 這是本站反覆推薦的運維型式。實際整合進日誌整理腳本的範例,在「PowerShell 腳本應用 ── 安全地自動化日誌調查、封存與報表化」中有詳細說明。
另外,官方的解說文章中也提醒,不要過度信任 -WhatIf 一定會傳遞到內部呼叫的指令。若要確保萬無一失,可以明確地把 -WhatIf:$WhatIfPreference 傳給內部的 Remove-Item 等指令。4
6. 把共用處理做成 .psm1 模組
6.1. .psm1 與 Export-ModuleMember
隨著函式逐漸成長,就會想從多個腳本呼叫同一個函式。用複製貼上的方式增加,修改就無法波及到所有複本,因此共用函式要集中管理。
在這之前的一個選項是點來源(dot sourcing)。在腳本路徑前面加上一個點和一個空白來執行,該腳本就會在呼叫端的作用範圍中執行,裡面定義的函式和變數會直接留在呼叫端。12
# 載入寫有共用函式的 Common.ps1(開頭的點與空白就是點來源)
. C:\Scripts\Common.ps1
# 可以直接呼叫在 Common.ps1 中定義的函式
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -WhatIf
雖然簡便,但呼叫端必須知道檔案的實體路徑,也無法選擇公開範圍(函式、變數、別名全部都會直接流入)。如果只是把一支腳本拆開,這樣就夠用了,但一旦想從第二支腳本呼叫同一個函式,就是該進到模組化的判斷時機。
模組化的做法簡單到讓人有點洩氣,只要把寫了函式的檔案存成 .psm1 副檔名即可。5
# AppOpsTools.psm1 ── 公司內部運維工具的共用模組
function Remove-OldAppLog { <# 前一章的函式 #> }
function Get-AppLogSummary { <# 彙總函式 #> }
# 內部輔助函式。不對外公開
function ConvertTo-InternalPath { <# ... #> }
# 明確指定要公開的函式。不寫的話所有函式都會被公開
Export-ModuleMember -Function Remove-OldAppLog, Get-AppLogSummary
如果不寫 Export-ModuleMember,模組內的函式和別名全部都會被匯出(變數則不會)。雖然可以省略,但明確指定公開範圍被視為最佳做法。13 把內部輔助函式隱藏起來,之後就能保留自由重構的空間。
6.2. 放置位置 ── $env:PSModulePath 與自動載入
模組要放在 $env:PSModulePath 中列出的資料夾底下,建立一個和模組名稱相同的資料夾來存放(AppOpsTools\AppOpsTools.psm1)。如果資料夾名稱和檔案的基底名稱不一致,就不會被辨識為模組。65 預設的放置位置如下表所示,Windows PowerShell 5.1 和 PowerShell 7 的路徑不同,是現場常見的絆腳石。6
| 範圍 | PowerShell 7 | Windows PowerShell 5.1 |
|---|---|---|
| 自己專用(CurrentUser) | $HOME\Documents\PowerShell\Modules |
$HOME\Documents\WindowsPowerShell\Modules |
| 全體使用者(AllUsers) | $env:ProgramFiles\PowerShell\Modules |
$env:ProgramFiles\WindowsPowerShell\Modules |
與其死背這張表,不如在實際的環境中確認來得確實。一行就能看到。
# 以每行一項的方式確認自己環境的搜尋路徑(Windows 的分隔符號是分號)
$env:PSModulePath -split ';'
# 從環境取得分隔符號的寫法。若在 PowerShell 7 中也會碰 macOS/Linux,用這個
$env:PSModulePath -split [System.IO.Path]::PathSeparator
即使名稱同樣是 $env:PSModulePath,5.1 和 7 的內容其實是不同的東西。「找不到模組」的原因,大多是放置的位置沒有列在該環境的搜尋路徑中,因此請先執行這一行,再開始懷疑其他原因。
只要放在正確的位置,即使不寫 Import-Module,第一次執行模組內的指令時,PowerShell 也會自動匯入(模組自動載入)。7 也就是說,使用者可以「像是這個指令本來就內建」的方式來使用。實務上的節奏,是先用完整路徑 Import-Module 正在試錯中的模組來確認動作,定案之後再放到 PSModulePath 底下。
有兩點與環境相關的注意事項。Documents 資料夾的實體有時會因為 OneDrive 的資料夾重新導向而移動,這種情況下,使用者範圍的模組也會被放在 OneDrive 底下。6 另外,部署到全體使用者範圍需要系統管理員權限。如果要放在伺服器上,建議設為 AllUsers 範圍,並確保工作排程器的執行帳戶也看得到,就能避免「自己的電腦上能動,伺服器上卻不動」的情況。
6.3. psd1 資訊清單留到「要分發的階段」
模組資訊清單(.psd1)是一個雜湊表檔案,用來記述模組的版本、相依關係等中繼資料,並非必須。資訊清單中唯一必要的鍵是 ModuleVersion。8 只在自己團隊內使用的階段,單靠 .psm1 就足夠了,等到要分發給其他部門、或需要嚴格做版本控管的階段,再用 New-ModuleManifest 產生。8
New-ModuleManifest -Path .\AppOpsTools\AppOpsTools.psd1 `
-RootModule 'AppOpsTools.psm1' `
-ModuleVersion '1.0.0' `
-FunctionsToExport 'Remove-OldAppLog', 'Get-AppLogSummary' `
-PowerShellVersion '5.1'
產生出來的 psd1 是帶有註解的範本,因此只需要逐步補齊需要的鍵即可。8
6.4. 公司內部共用與版本控管的要點
- 原始檔要放在 Git 裡。腳本和模組都是文字檔,和 Git 相性很好,「什麼時候、誰、為什麼改的」這種可追溯性,本身就是運維腳本可信度的來源。讓 ModuleVersion 的更新和提交(commit)對應起來,就能更輕鬆地確認伺服器上裝的是哪個版本。
- 發布的基本形式是「從共用資料夾複製到各台機器的 PSModulePath 底下」。只要把模組整個資料夾複製過去,就能完成手動安裝。7 把共用資料夾上的路徑直接加進 PSModulePath 的架構,考量到共用資料夾「可能會慢、可能會斷線、有時候不在」的前提(詳情請見「網路磁碟機與 UNC 路徑的陷阱」),不建議當作常態使用。
- 要注意與執行原則的關係。預設的 RemoteSigned 允許本機建立的無簽章腳本執行,但在不區分 UNC 路徑與網際網路路徑的系統中,UNC 路徑上的腳本有時會被拒絕執行。另外,帶有下載來源標記的檔案會被封鎖,需要用 Unblock-File 解除封鎖或加上簽章。9 如果要正式展開公司內部發布,請在「PowerShell 的執行原則與腳本簽章」中確認與程式碼簽章搭配使用的方式。
7. 實務上的定石(判斷表)
| 論點 | 選項 | 判斷標準 |
|---|---|---|
| 引數的接收方式 | 變數直接寫死 / param 區塊 | 只要用兩次以上、或會有別人使用,就非 param 莫屬。預設值要偏向「安全側」2 |
| [CmdletBinding()] | 不加 / 要加 | 交給別人使用、放上運維流程的一律都要加。打錯字會在執行前就停下來1 |
| 輸入檢查 | 本體的 if 陳述式 / 驗證屬性 | 單一參數的格式檢查交給屬性。只有組合驗證才寫在本體2 |
| 變更類的安全裝置 | 自訂的 -TestMode 引數 / SupportsShouldProcess | 不要自創旗標。要搭上標準的 -WhatIf/-Confirm4 |
| 共用處理的持有方式 | 複製貼上 / 點來源 / .psm1 模組 | 在第二支腳本開始共用的時候就模組化。公開的函式要用 Export-ModuleMember 明確指定513 |
| 模組的放置位置 | 任意資料夾 + Import-Module / PSModulePath 底下 | 常態使用的要放在既定位置,搭上自動載入。伺服器要用 AllUsers 範圍67 |
| psd1 資訊清單 | 一開始就做 / 要分發的階段再做 | 到了要拿出團隊之外、或需要嚴格版本控管的階段,再用 New-ModuleManifest8 |
8. 總結
- 用 param 區塊與 [CmdletBinding()] 把函式變成高度函式,是「能交給別人用的腳本」的出發點。會附加共用參數,引數的錯誤也會在執行前就停下來。
- 用型別指定、[Parameter(Mandatory)]、偏安全側的預設值、驗證屬性,讓錯誤儘早在入口處出現。ValidateSet 還能提升 Tab 鍵補完這種易用性。
- 管線輸入要用 ValueFromPipeline 搭配 process 區塊來接收。忘記寫 process 的話只會處理最後一筆。
- 用以註解為基礎的說明讓 Get-Help 生效,變更類的函式則用 SupportsShouldProcess 支援 -WhatIf。能夠預演,就是運維的安全裝置。
- 共用函式要切出來放進 .psm1,用 Export-ModuleMember 明確指定公開範圍,並放置在 PSModulePath 底下。請注意 5.1 和 7 的路徑不同。
- psd1 資訊清單留到要分發的階段再做。原始檔用 Git 管理,透過共用資料夾發布時,要確認執行原則(UNC 路徑與 RemoteSigned 的關係)。
相關文章
- PowerShell 指令基礎 ── 該先學會的操作與安全使用方式
- PowerShell 腳本應用 ── 安全地自動化日誌調查、封存與報表化
- 用 Pester 整備 PowerShell 測試 ── 讓維運腳本不易損壞的實務做法
- PowerShell 的執行原則與指令碼簽署 ── 從「用 Bypass 蓋住問題」的做法畢業的實務指南
- PowerShell 的錯誤處理與重新執行設計 ── 從 try/catch 失效的陷阱到 exit code、重試的實務定石
- PowerShell 中安全處理認證資訊 ── 把明文密碼逐出腳本
相關諮詢領域
合同會社小村軟體處理的業務包括:整理・模組化因人而異的 PowerShell 腳本、運維自動化腳本的設計審查、建立公司內部發布與版本控管的機制。即使是從「雖然在動但沒有人能碰」的腳本資產盤點開始,也歡迎諮詢。
參考連結
-
Microsoft Learn,about_Functions_CmdletBindingAttribute。關於 CmdletBinding 屬性讓函式以和已編譯 Cmdlet 相同的方式運作、共用參數會自動附加、可以使用 $PSCmdlet、未知的參數或不對應的位置引數會導致繫結失敗、SupportsShouldProcess 會加上 Confirm/WhatIf 參數。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,about_Functions_Advanced_Parameters。關於 Parameter 屬性與 Mandatory、ValueFromPipeline、switch 參數,以及 ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty/ValidatePattern 等驗證屬性的規格、驗證失敗時函式不會被呼叫、把屬性宣告在型別之前是最佳做法、ValidateScript 的 ErrorMessage 引數是 PowerShell 6 以後才有的功能。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15
-
Microsoft Learn,about_Comment_Based_Help。關於用 .SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE 等關鍵字撰寫以註解為基礎的說明時,Get-Help 會以和 XML 說明相同的格式顯示,以及在腳本和函式中各自的配置規則。 ↩ ↩2 ↩3
-
Microsoft Learn,Everything you wanted to know about ShouldProcess。關於只要指定 SupportsShouldProcess,-WhatIf/-Confirm 就會自動建立、$PSCmdlet.ShouldProcess() 的分支寫法、不要過度信任 -WhatIf 的傳遞,建議明確傳給內部的指令。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,How to Write a PowerShell Script Module。關於只要存成 .psm1 副檔名就會變成腳本模組、要存在和腳本同名的資料夾中、預設所有函式都會被公開而變數不會、建議用 Export-ModuleMember 明確指定要公開的函式。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,about_PSModulePath。關於 $env:PSModulePath 是模組搜尋資料夾的清單、PowerShell 7 與 Windows PowerShell 5.1 的 CurrentUser/AllUsers 範圍預設路徑不同、Documents 的位置可能因 OneDrive 或資料夾重新導向而改變。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn,about_Modules。關於 PSModulePath 底下的模組會在指令第一次執行時自動匯入(模組自動載入)、把模組資料夾整個複製過去的手動安裝方式、模組的預設放置位置。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,New-ModuleManifest。關於模組資訊清單(.psd1)是描述模組內容・屬性・前提條件的雜湊表,且並非必要、唯一必要的鍵是 ModuleVersion、New-ModuleManifest 會產生可當作範本使用的雛型。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn,about_Execution_Policies。關於 RemoteSigned 允許本機建立的無簽章腳本、對來自網際網路的腳本要求要有簽章、在不區分 UNC 路徑與網際網路路徑的系統中,UNC 路徑上的腳本有時不會被 RemoteSigned 允許執行,以及用 Unblock-File 解除封鎖。 ↩ ↩2
-
Microsoft Learn,about_Functions_Advanced_Methods。關於高度函式中可用的 begin/process/end 輸入處理方法、ShouldProcess 方法要在 process 區塊內呼叫,且需要用 CmdletBinding 屬性宣告。 ↩ ↩2
-
Microsoft Learn,Approved Verbs for PowerShell Commands。關於指令名稱應採用「動詞-名詞(Verb-Noun)」形式、核可動詞清單與用 Get-Verb 確認的方法、匯入含有未核可動詞的模組時會顯示警告。 ↩
-
Microsoft Learn,about_Scripts。關於腳本預設會在自己的作用範圍中執行,其中建立的函式・變數・別名・磁碟機只存在於腳本作用範圍;在路徑前加上點與空白執行的「點來源」,會在目前的作用範圍中執行,建立的項目在執行後仍會留在工作階段中。 ↩
-
Microsoft Learn,Export-ModuleMember。關於 Export-ModuleMember 是用來指定腳本模組要匯出哪些成員的 Cmdlet、未指定時函式和別名會被匯出而變數不會、雖可省略但明確表達作者意圖是最佳做法。 ↩ ↩2
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
PowerShell 呼叫 COM 與 .NET 的實戰 ── 一口氣擴大腳本能觸及的範圍
從 PowerShell 呼叫 .NET 類別的方法、透過 Add-Type 組入 C# 與 Win32 API、COM 操作、Excel 的處理程序殘留與後續處理、Office 無人執行不受支援的原因,到 5.1 與 7 的差異,皆以實務角度解說。
Windows PowerShell 5.1 與 PowerShell 7 的差異 ── 公司內部腳本遷移實務指南
本文整理 Windows PowerShell 5.1 與 PowerShell 7 的關係(共存與 pwsh.exe)、5.1 不再新增功能的官方方針、編碼差異造成的亂碼問題、以 #Requires 進行防禦,直到更新工作排程器為止的遷移步驟。
用 PowerShell 自動化 Excel・CSV 業務處理 ── 彙總・比對・報表輸出的實務食譜
用 PowerShell 自動化 CSV 彙總・比對與 Excel 報表輸出的實務食譜。解說 Import-Csv/Export-Csv 的字元編碼預設值(5.1 與 7 的差異)、以 Group-Object 進行彙總、用 Compare-Object 與雜湊表進行比對,...
PowerShell 實用指令集錦 ── 累積日常工作常用的小工具
本文整理 PowerShell 日常工作中常用的實用指令,說明 Measure-Object、Group-Object、Select-String、Compare-Object、Tee-Object、Start-Transcript 等指令的使用場景與時機。
用 winget + PowerShell 自動化 PC 配置 ── 讓操作手冊變得可執行
本文整理讓新進員工 PC 的環境建置可重現的方法,涵蓋以 winget 進行應用程式導入與 export/import、WinGet Configuration 的宣告式組態、以 PowerShell 補充的設定,以及無人執行時的注意事項。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
既有資產活用 & 遷移支援
在持續活用 COM / ActiveX / OCX 資產、原生程式碼與 32 位元相依的同時,協助規劃階段性的遷移。
常見問題
整理諮詢這個主題時常見的問題。
- 在 PowerShell 的 param 區塊加上 [CmdletBinding()] 會有什麼變化?
- 函式或腳本會被當作「高度函式(advanced function)」處理,因而獲得和已編譯 Cmdlet 相同的行為。具體來說,-Verbose、-ErrorAction 等共用參數會自動附加,$PSCmdlet 變數變得可用,傳入未定義的參數或多餘的位置引數也會導致繫結失敗。打錯字的引數不會再被默默忽略,因此在運維腳本中,加上它是基本做法。
- 腳本的引數檢查應該用 Validate 屬性還是 if 陳述式來寫?
- 參數的格式檢查交給 ValidateSet、ValidateRange、ValidateScript 等驗證屬性,是標準做法。驗證會在函式本體執行之前進行,值不合法時,處理不會執行任何一行就會出錯,因此可以防止「執行到一半才壞掉」的事故。ValidateSet 還有能讓 Tab 鍵補完生效這項實質好處。另一方面,多個參數的組合驗證,或依賴執行時狀態的檢查,則要在本體端用 if 陳述式進行。
- PowerShell 自製模組(.psm1)應該放在哪裡?
- 要在 $env:PSModulePath 所包含的資料夾底下,建立一個「和模組名稱相同的資料夾」來存放。若只供自己使用,PowerShell 7 的預設位置是 $HOME\Documents\PowerShell\Modules;若要全體使用者共用,則預設是 $env:ProgramFiles\PowerShell\Modules。要注意的是,Windows PowerShell 5.1 的路徑分別是不同的 WindowsPowerShell\Modules。放在這個位置後,即使不寫 Import-Module,指令第一次執行時也會自動載入。
- 模組資訊清單(psd1)一定要建立嗎?
- 並非必須。沒有資訊清單,單靠 .psm1 也能當作模組運作。需要資訊清單的時機是「要分發出去的階段」,當你想附加版本編號、所需的 PowerShell 版本、相依模組、明確指定要匯出的指令等中繼資料時,就用 New-ModuleManifest 產生。資訊清單中必要的鍵只有 ModuleVersion,因此一開始從最小組成著手,之後再依需要逐步擴充就足夠了。
- 放在公司內部共用資料夾的腳本為什麼會被執行原則擋下?
- 預設的 RemoteSigned 原則允許本機建立的腳本即使無簽章也能執行,但對標記為來自網際網路的腳本則要求要有簽章。官方文件也明確指出,在不區分 UNC 路徑與網際網路路徑的系統組態下,共用資料夾上的腳本有時會被 RemoteSigned 拒絕執行。若要正式展開公司內部發布,建議考慮程式碼簽章與 AllSigned 的組合,或是內部網路(Intranet)區域的組態設定。