寫了很多方便自己用的 PowerShell Function 指令後,發現有很多指令功能其實差不多,只有少部分不一樣,想說要來重構他們,但又不希望影響到既有使用方式,也就是 Function 名稱不改變,可以怎麼處理呢?想說能不能使用動態建立 Function 的方式來做,沒想到…還真的可以!

情境

前情提要一下,這樣之後看這篇文章的時候,比較能進入狀況。

假設我有一批 PowerShell Function 長得像這樣:

function Func1($Description) { Write-Output "Result1 - $Description" }
function Func2($Description) { Write-Output "Result2 - $Description" }
function Func3($Description) { Write-Output "Result3 - $Description" }

# 執行方式
# Func1 "Hello World"
# 執行結果
# Result1 - Hello World

這三個 Function 動作長得很像,只有 Function 名稱和輸出的結果有些不同,如何在不影響其他地方的使用方式下,動態建立這些 Function 呢?

動態建立 Function

要動態建立 Function 比我想像中的簡單一些,先看最終用於動態建立 Function 的 Function 程式碼:

function Add-DynamicFunction {
    Param(
        [Parameter(
            Mandatory = $true,
            Position = 0,
            HelpMessage = "Function name"
        )]
        [string]$FuncName,
        [Parameter(
            Mandatory = $true,
            Position = 1,
            HelpMessage = "Function action"
        )]
        [string]$FuncAction
    )

    Set-Variable -name Func -value "function global:$($FuncName)() { $($FuncAction) }"
    Invoke-Expression $Func
}

上面我使用 Param 的方式接收兩個參數,分別會是 Function 名稱,以及執行 Function 時的動作。

然後用 Set-Variable 建立變數的 Cmdlet 將要建立的 Function 用文字的方式組合並設定給 Func 變數,接著執行 Invoke-Expression $Func 即將組合好的文字 Function 拿去給 PowerShell 執行環境執行。

這裡有幾個注意事項:

  • 傳進去的 Function 名稱和動作都是用純文字表示,並且是必要的參數(所以設定 Mandatory = $true
  • 組合的 Function 前面加上 global: 表示是全域使用的 Function,否則之後會找不到此建立的 Function

有了動態建立 Function 的 Function 之後,就可以使用如下的方式,來動態建立 Function:

# 動態建立 Function
Add-DynamicFunction -FuncName 'Get-HelloFromDynamicFunction' -FuncAction 'Write-Output "Hello-Dynamic-Function..."'
# 執行
Get-HelloFromDynamicFunction

批量動態建立

有了基礎之後,就要來大量建立了 😀

在 PowerShell 中,有很多種建立物件的方式,我個人偏好使用 [PSCustomObject] 搭配 HashTable 來建立,這是最快速、畫面最清爽的建立方式。

然後只要把他們用 @() 括起來,就可以建立出陣列裡面包含多個物件的資料格式:

$list = @(
    [PSCustomObject]@{ FuncName="Func1"; Description="3" },
    [PSCustomObject]@{ FuncName="Func2"; Description="3" },
    [PSCustomObject]@{ FuncName="Func3"; Description="3" }
);

這時候你可以用清單變數自帶的 ForEach 方法、ForEach-Object Cmdlet、或用 foreach 語法來遍巡 $list 清單變數,但我建議使用第三種 foreach 語法,因為前面兩者通常會使用 $_ 來取得當前資料,而 $_ 是參考內部的 Scope,比較容易出現不如預期的狀況,相對的第三種 foreach 語法則比較不容易有問題。

如果還是偏好使用前兩個做法,可以先建立一個變數來接收 $_ 資料,例如 $list = $_,這樣也可以避免 Scope 的問題。

$list.ForEach({ Write-Output $_ })
$list | ForEach-Object { Write-Output $_ }

# 建議用這裡的寫法
$list.ForEach({ $l = $_; Write-Output $l; })
$list | ForEach-Object { $l = $_; Write-Output $l; }
foreach ($item in $list) { Write-Output $item }

OK!執行完 ForEach 之後,就完成了動態建立 Function 囉,這種靠資料驅動建立 Function 的感覺挺不錯的 😆


參考資料:


Poy Chang

Trial and Error