了解PowerShell管道並創建函數

PowerShell管線是PowerShell殼層和腳本語言中最重要(也是最有用)的功能之一。一旦你理解了它的基本工作原理和能力,你就可以利用它的威力來構建自己的函數。

PowerShell管線允許你鏈接命令以構建一個單一的“管線”,簡化代碼,實現並行處理等。如果你準備學習有關管線的知識並構建自己的函數以利用管線的功能,讓我們開始吧!

先決條件

本帖將是一個教程,全部都是實際操作演示。如果你想跟著進行,你需要安裝PowerShell v3+。本教程將使用Windows PowerShell v5.1

理解PowerShell管線

大多數PowerShell命令通過參數接收一些輸入。該命令接收某個對象作為輸入,並在內部對其執行某些操作。然後,它可以選擇性地通過輸出返回某個對象。

管線中的命令就像接力賽中的人類跑步者一樣。賽中的每個跑步者,除了第一個和最後一個之外,都接收來自前任的接力棒(對象)並將其傳遞給下一個。

例如,Stop-Service cmdlet 有一個名為InputObject的參數。此參數允許您傳遞特定類型的物件給Stop-Service,以表示您想要停止的 Windows 服務。

要使用InputObject參數,您可以通過Get-Service檢索服務物件,然後將該物件傳遞給InputObject參數,如下所示。通過此方法將輸入提供給Stop-Service cmdlet 的InputObject參數非常有效且能完成任務。

$service = Get-Service -Name 'wuauserv'
Stop-Service -InputObject $service

通過此方法將輸入傳遞給Stop-Service命令需要兩個不同的步驟。PowerShell 必須先運行Get-Service,將輸出保存到變量中,然後通過InputObject參數將該值傳遞給Stop-Service

現在,將上面的片段與下面的片段進行對比,它完成相同的工作。這樣做起來簡單得多,因為您不必創建一個$services變量,也不必使用InputObject參數。相反,PowerShell “知道”您打算使用InputObject參數。它通過一個叫做參數繫結的概念實現這一點。

您現在使用了|運算符將命令連接在一起。您創建了一個pipeline

Get-Service -Name 'wuauserv' | Stop-Service

但是,您不必僅使用兩個命令來創建 pipeline;您可以鏈接任意多個命令在一起(如果命令參數支持)。例如,以下代碼片段:

  1. Get-Service 命令返回的所有物件傳遞給 Where-Object 命令。
  2. Where-Object 命令然後查看每個物件的 Status 屬性,然後僅返回具有值 Running 的物件。
  3. 然後,將這些物件中的每個物件發送到 Select-Object,該命令僅返回物件的 NameDisplayName 屬性。
  4. 由於沒有其他命令接受 Select-Object 輸出的物件,因此該命令將物件直接返回到控制台。

Where-ObjectSelect-Object 命令了解如何通過稱為參數綁定的概念來處理管道輸入,下一節將進行討論。

Get-Service | Where-Object Status -eq Running | Select-Object Name, DisplayName

有關管道的其他信息,運行命令 Get-Help about_pipelines

管道參數綁定

乍一看,管道可能似乎微不足道。畢竟,它只是將物件從一個命令傳遞到另一個命令。但實際上,管道要複雜得多。命令僅通過參數接受輸入。即使您沒有明確定義,管道也必須想辦法確定使用哪個參數。

確定在命令通過管道接收輸入時使用哪個參數的任務稱為參數綁定。要成功地將從管道中進入的對象綁定到參數,傳入命令的參數必須支持它。命令參數通過以下兩種方式之一支持管道參數綁定:ByValue和/或ByPropertyName。

ByValue

命令參數接受整個傳入對象作為參數值。 ByValue參數在傳入對象中尋找特定類型的對象。如果該對象類型匹配,PowerShell會假定該對象意味著要綁定到該參數並接受它。

Get-ChildItem cmdlet有一個名為Path的參數,它接受字符串對象類型並通過ByValue進行管道輸入。因此,運行像’C:\Windows’ | Get-ChildItem這樣的命令將返回C:\Windows目錄中的所有文件,因為C:\Windows是一個字符串。

ByPropertyName

命令參數不接受整個對象,而是該對象的單個屬性。它通過查看屬性名稱而不是對象類型來實現這一點。

Get-Process 命令有一個 Name 參數,設置為接受通道輸入 ByPropertyName。 當你將具有 Name 屬性的對象傳遞給 Get-Process 命令時,例如 [pscustomobject]@{Name='firefox'} | Get-Process,PowerShell 會將傳入對象上的 Name 屬性與 Name 參數進行匹配或綁定,並使用該值。

發現支持管道輸入的命令參數

如前所述,並非每個命令都支持管道輸入。 命令作者必須在開發中創建該功能。 該命令必須至少有一個支持管道的參數,使用 ByValueByPropertyName

你怎麼知道哪些命令及其參數支持管道輸入呢? 你可以嘗試通過反覆試驗來嘗試,但使用 PowerShell 幫助系統的 Get-Help 命令有一種更好的方法。

Get-Help <COMMAND> -Parameter <PARAMETER>

舉例來說,查看 Get-ChildItem 命令的 Path 參數。 你可以看到它支持兩種類型的管道輸入。

PowerShell pipeline input is allowed

一旦您知道哪些命令參數支持管道輸入,您就可以利用該功能,如下所示。

# 非管道呼叫
Get-ChildItem -Path 'C:\\Program Files', 'C:\\Windows'

# 管道呼叫
'C:\\Program Files', 'C:\\Windows' | Get-ChildItem

但是,另一方面,Get-Service 上的 DisplayName 參數不支持管道輸入。

PowerShell pipeline input is not allowed

構建您自己的管道函數

即使標準的 PowerShell 命令支持管道輸入,也不意味著您不能利用該功能。幸運的是,您也可以構建接受管道輸入的函數。

為了演示,讓我們從一個名為 Get-ConnectionStatus 的現有函數開始。

  • 這個函數有一個單一的參數(不接受管道輸入)叫做 ComputerName,它允許您將一個或多個字符串傳遞給它。您可以看出 ComputerName 參數不接受管道輸入,因為它未被定義為參數屬性([Parameter()])。
  • 然後,該函數會讀取每個字符串並對每個字符串運行 Test-Connection 命令。
  • 對於傳遞的每個字串電腦名稱,它將返回一個帶有ComputerNameStatus屬性的物件。
function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter()] ## 無管道輸入
        [string[]]$ComputerName
    )

    foreach($c in $ComputerName)
    {
        if(Test-Connection -ComputerName $c -Quiet -Count 1)
        {
            $status = 'Ok'
        }
        else
        {
            $status = 'No Connection'
        }

        [pscustomobject]@{
            ComputerName = $c
            Status = $status
        }
    }
}

然後,通過傳遞ComputerName參數,呼叫Get-ConnectionStatus,傳遞一個或多個主機名稱或IP地址,如下所示。

Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'

步驟1:允許管道輸入

要使此函數接受管道輸入,您必須首先定義適當的參數屬性。此屬性可以是ValueFromPipeline以接受通過值的管道輸入,或者ValueFromPipelineByPropertyName以接受通過屬性名的管道輸入。

在此示例中,在[Parameter()]定義的括號內添加ValueFromPipeline參數屬性,如下所示。

 [Parameter(ValueFromPipeline)]
 [string[]]$ComputerName

在這一點上,那就是技術上您所需要做的。Get-ConnectionStatus函數現在將任何傳遞給它的字串物件繫結到ComputerName參數。但是,即使參數繫結正在發生,也不表示函數將對其執行任何有意義的操作。

步驟2:添加處理區塊

當您希望PowerShell處理來自管道的所有物件時,您必須接著添加一個Process區塊。該區塊告訴PowerShell要處理來自管道的每個物件。

沒有Process區塊,PowerShell將僅處理來自管道的第一個物件。Process區塊告訴PowerShell繼續處理物件。

添加

function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline)]
        [string[]]$ComputerName
    )

    Process ## 新的流程區塊
    {
        foreach($c in $ComputerName)
        {
            if(Test-Connection -ComputerName $c -Quiet -Count 1)
            {
                $status = 'Ok'
            }
            else
            {
                $status = 'No Connection'
            }

            [pscustomobject]@{
                ComputerName = $c
                Status = $status
            }
        }
    } ## 結束流程區塊
}

始終在Process區塊內發送輸出。在Process區塊內發送輸出會將對象流到管線,使其他命令能夠從管線接受這些對象。

將對象傳遞到 PowerShell 管線

一旦在上面的函數中定義了Process區塊,您現在可以通過管線傳遞值給ComputerName參數,如下所示。

Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'
## 或者
'127.0.0.1', '192.168.1.100' | Get-ConnectionStatus

此時,您可以充分利用管線的真正威力,開始將更多命令納入其中。例如,也許您有一個文本文件C:\Test\computers.txt,其中包含一行IP地址,透過換行分隔,如下所示。

127.0.0.1
192.168.1.100

然後,您可以使用Get-Content命令讀取文本文件中的每個IP地址,並將它們直接傳遞給Get-ConnectionStatus函數。

Get-Content -Path C:\Test\computers.txt | Get-ConnectionStatus 

將這個設置推進一步,您可以將Get-ConnectionStatus返回的對象直接傳遞給ForEach-Object命令。

代碼如下:

  • 讀取文本檔中的所有計算機名稱並將它們傳遞給Get-ConnectionStatus函數。
  • Get-ConnectionStatus處理每個計算機名稱並返回一個具有ComputerNameStatus屬性的對象。
  • Get-ConnectionStatus然後將每個對象傳遞給ForEach-Object cmdlet,該cmdlet然後返回一個以Cyan顏色顯示的人可讀的狀態字符串。
Get-Content -Path C:\Test\computers.txt |
Get-ConnectionStatus |
ForEach-Object { Write-Host "$($_.ComputerName) connection status is: $($_.Status)" -ForegroundColor Cyan }

如果在ComputerName參數上未啟用管道輸入,或者Get-ConnectionStatusProcess塊中沒有返回對象,PowerShell將不會將任何狀態返回到控制台,直到所有對象(IP地址)都被處理。

通過屬性名稱進行管道綁定

到目前為止,Get-ConnectionStatus cmdlet被設置為接受管道輸入ByValue(ValueFromPipeline),方法是接受一個字符串數組,如'127.0.0.1', '192.168.1.100'。如果從CSV文件而不是IP地址的文本文件接收輸入,這個函數是否也會按預期工作?

也許您有一個CSV文件,看起來像下面這樣:C:\Test\pc-list.csv

ComputerName,Location
127.0.0.1,London
192.168.1.100,Paris

請注意,CSV文件中的ComputerName字段與Get-ConnnectionStatusComputerName參數相同。

如果您嘗試導入CSV並將其通過管道傳遞給Get-ConnectionStatus,該函數將以ComputerName列返回意外結果。

Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus

ComputerName                                  Status       
------------                                  ------       
@{ComputerName=127.0.0.1; Location=London}    No Connection
@{ComputerName=192.168.1.100; Location=Paris} No Connection

你能猜到哪裡出了問題嗎?畢竟,參數名稱確實相符,那麼為什麼 PowerShell 管線沒有將 Import-CSV 返回的輸出綁定到 Get-ConnectionStatus 上的 ComputerName 參數呢?因為你需要使用參數屬性 ValueFromPipelineByPropertyName

截至目前,函數的 ComputerName 參數具有以下參數定義:[Parameter(ValueFromPipeline)]。因此,您必須添加 ValueFromPipelineByPropertyName 來將 ComputerName 參數設置為支持按屬性名稱輸入,如下所示。

    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

一旦您啟用了管道支持按屬性名稱,您告訴 PowerShell 開始查看對象屬性名稱和對象類型。一旦您進行了此更改,您應該會看到預期的輸出。

PS> Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus

ComputerName  Status       
------------  ------       
127.0.0.1     Ok           
192.168.1.100 No Connection

總結

在本教程中,您了解了 PowerShell 管道的工作原理,它如何綁定參數,甚至如何創建支持 PowerShell 管道的自己的函數。

儘管函數可以在沒有管道的情況下工作,但它們不會將對象“流”從一個命令傳送到另一個命令並簡化代碼。

您能想到一個您編寫過的函數,或者即將編寫的函數,可以從使其準備好管道中受益嗎?

Source:
https://adamtheautomator.com/ppowershell-pipeline/