PowerShell 腳本的引數設計與模組化 ── 從「能動的腳本」到「能交給別人用的腳本」

· · PowerShell, Windows, 腳本, 自動化, 維運改善, 業務效率化, 既有資產活用, 命令列

「負責人寫的 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 腳本、運維自動化腳本的設計審查、建立公司內部發布與版本控管的機制。即使是從「雖然在動但沒有人能碰」的腳本資產盤點開始,也歡迎諮詢。

參考連結

  1. Microsoft Learn,about_Functions_CmdletBindingAttribute。關於 CmdletBinding 屬性讓函式以和已編譯 Cmdlet 相同的方式運作、共用參數會自動附加、可以使用 $PSCmdlet、未知的參數或不對應的位置引數會導致繫結失敗、SupportsShouldProcess 會加上 Confirm/WhatIf 參數。  2 3 4

  2. 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

  3. Microsoft Learn,about_Comment_Based_Help。關於用 .SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE 等關鍵字撰寫以註解為基礎的說明時,Get-Help 會以和 XML 說明相同的格式顯示,以及在腳本和函式中各自的配置規則。  2 3

  4. Microsoft Learn,Everything you wanted to know about ShouldProcess。關於只要指定 SupportsShouldProcess,-WhatIf/-Confirm 就會自動建立、$PSCmdlet.ShouldProcess() 的分支寫法、不要過度信任 -WhatIf 的傳遞,建議明確傳給內部的指令。  2 3 4

  5. Microsoft Learn,How to Write a PowerShell Script Module。關於只要存成 .psm1 副檔名就會變成腳本模組、要存在和腳本同名的資料夾中、預設所有函式都會被公開而變數不會、建議用 Export-ModuleMember 明確指定要公開的函式。  2 3 4

  6. Microsoft Learn,about_PSModulePath。關於 $env:PSModulePath 是模組搜尋資料夾的清單、PowerShell 7 與 Windows PowerShell 5.1 的 CurrentUser/AllUsers 範圍預設路徑不同、Documents 的位置可能因 OneDrive 或資料夾重新導向而改變。  2 3 4 5

  7. Microsoft Learn,about_Modules。關於 PSModulePath 底下的模組會在指令第一次執行時自動匯入(模組自動載入)、把模組資料夾整個複製過去的手動安裝方式、模組的預設放置位置。  2 3 4

  8. Microsoft Learn,New-ModuleManifest。關於模組資訊清單(.psd1)是描述模組內容・屬性・前提條件的雜湊表,且並非必要、唯一必要的鍵是 ModuleVersion、New-ModuleManifest 會產生可當作範本使用的雛型。  2 3 4 5

  9. Microsoft Learn,about_Execution_Policies。關於 RemoteSigned 允許本機建立的無簽章腳本、對來自網際網路的腳本要求要有簽章、在不區分 UNC 路徑與網際網路路徑的系統中,UNC 路徑上的腳本有時不會被 RemoteSigned 允許執行,以及用 Unblock-File 解除封鎖。  2

  10. Microsoft Learn,about_Functions_Advanced_Methods。關於高度函式中可用的 begin/process/end 輸入處理方法、ShouldProcess 方法要在 process 區塊內呼叫,且需要用 CmdletBinding 屬性宣告。  2

  11. Microsoft Learn,Approved Verbs for PowerShell Commands。關於指令名稱應採用「動詞-名詞(Verb-Noun)」形式、核可動詞清單與用 Get-Verb 確認的方法、匯入含有未核可動詞的模組時會顯示警告。 

  12. Microsoft Learn,about_Scripts。關於腳本預設會在自己的作用範圍中執行,其中建立的函式・變數・別名・磁碟機只存在於腳本作用範圍;在路徑前加上點與空白執行的「點來源」,會在目前的作用範圍中執行,建立的項目在執行後仍會留在工作階段中。 

  13. Microsoft Learn,Export-ModuleMember。關於 Export-ModuleMember 是用來指定腳本模組要匯出哪些成員的 Cmdlet、未指定時函式和別名會被匯出而變數不會、雖可省略但明確表達作者意圖是最佳做法。  2

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

在 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)區域的組態設定。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽