了解PowerShell管道并创建函数

PowerShell 管道是 PowerShell shell 和脚本语言中最重要(也是最有用)的功能之一。一旦您了解了它的基本工作原理和能力,就可以利用它的强大功能来编写自己的函数。在本教程中,您将正是这样做!

PowerShell 管道允许您将命令链接在一起,构建一个单一的“管道”,这简化了代码,允许并行处理等等。如果您准备好了解管道并构建自己的函数以利用管道,请开始吧!

先决条件

本文将是一个教程,全部都是实际演示。如果您想跟着做,您需要 PowerShell v3+。本教程将使用 Windows PowerShell v5.1

理解 PowerShell 管道

大多数 PowerShell 命令通过参数接收一些输入。命令通过参数接收一些对象作为输入,然后在内部对其进行操作。然后,它可以选择通过输出返回一些对象。

管道中的命令就像接力赛中的人类接力选手一样。接力赛中的每个选手(对象)都接收前一个选手的接力棒,并将其传递给下一个选手,除了第一个和最后一个选手。

例如,Stop-Service 命令有一个名为 InputObject 的参数。此参数允许您向 Stop-Service 传递表示您想要停止的 Windows 服务的特定类型的对象。

要使用 InputObject 参数,您可以通过 Get-Service 检索服务对象,然后将对象传递给 InputObject 参数,如下所示。通过 InputObject 参数提供输入给 Stop-Service 命令的这种方法非常有效,可以完成任务。

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

通过 InputObject 参数传递输入给 Stop-Service 命令的这种方法需要两个不同的步骤。PowerShell 必须首先运行 Get-Service,将输出保存到变量中,然后通过 InputObject 参数将该值传递给 Stop-Service

现在,将上述片段与下面的片段进行对比,它们完成相同的任务。下面的片段要简单得多,因为您不必创建 $services 变量,甚至根本不使用 InputObject 参数。相反,PowerShell “知道”您打算使用 InputObject 参数。它通过一种称为参数绑定的概念实现了这一点。

您现在使用了 | 运算符将命令链接在一起。您创建了一个管道

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

但是,您不必仅使用两个命令创建管道;如果命令参数支持,您可以将尽可能多的命令链接在一起。例如,下面的代码片段:

  1. Get-Service cmdlet 返回的所有对象传递给Where-Object cmdlet。
  2. Where-Object cmdlet 然后查看每个对象的Status属性,然后仅返回具有值Running的对象。
  3. 然后,将这些对象中的每一个发送到Select-Object,它仅返回对象的NameDisplayName属性。
  4. 由于没有其他 cmdlet 接受Select-Object 输出的对象,因此命令直接将对象返回到控制台。

Where-ObjectSelect-Object cmdlet 了解如何通过一个叫做参数绑定的概念来处理管道输入,下一节将讨论这个概念。

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

有关管道的附加信息,请运行命令Get-Help about_pipelines

管道参数绑定

乍一看,管道可能看起来微不足道。毕竟,它只是从一个命令传递对象到另一个命令。但实际上,管道要复杂得多。命令只通过参数接受输入。即使您没有明确定义它,管道也必须想办法确定使用哪个参数。

确定命令通过管道接收输入时使用哪个参数的任务被称为参数绑定。要成功地将来自管道的对象绑定到参数,传入命令的参数必须支持它。命令参数以两种方式之一支持管道参数绑定:ByValue和/或ByPropertyName

按值

命令参数接受整个传入对象作为参数值。ByValue参数在传入对象中查找特定类型的对象。如果该对象类型匹配,PowerShell假定该对象是要绑定到该参数的,并接受它。

Get-ChildItem cmdlet 有一个名为Path的参数,它接受字符串对象类型和通过ByValue的管道输入。因此,运行类似'C:\Windows' | Get-ChildItem的命令会返回 C:\Windows 目录中的所有文件,因为C:\Windows是一个字符串。

按属性名

命令参数不接受整个对象,而是接受该对象的单个属性。它这样做不是通过查看对象类型,而是通过查看属性名称。

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 cmdlet 支持管道输入,但这并不意味着您不能利用这个功能。幸运的是,您可以构建接受管道输入的函数。

为了演示,让我们从一个名为 Get-ConnectionStatus 的现有函数开始。

  • 该函数有一个单一参数(不接受管道输入),名为 ComputerName,允许您将一个或多个字符串传递给它。您可以通过它没有定义为参数属性([Parameter()])来判断 ComputerName 参数不接受管道输入。
  • 然后,该函数读取这些字符串并对每个字符串运行 Test-Connection cmdlet。
  • 对于传递的每个字符串计算机名称,然后返回一个具有 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 以接受通过属性名的管道输入。

在本例中,将 ValueFromPipeline 参数属性添加到 [Parameter()] 定义的括号内,如下所示。

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

此时,这就是你需要做的一切。 Get-ConnectionStatus 函数现在将任何传递给它的字符串对象绑定到 ComputerName 参数。但是,即使进行了参数绑定,也不意味着函数会对其执行有意义的操作。

步骤 2:添加一个处理块

当你希望 PowerShell 处理从管道中传入的所有对象时,必须接下来添加一个 Process 块。此块告诉 PowerShell 对从管道中传入的每个对象进行处理。

如果没有 Process 块,PowerShell 将仅处理来自管道的第一个对象。 Process 块告诉 PowerShell 继续处理对象。

添加一个Process块,如下所示,通过将函数的所有功能包含在其中来完成。

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

    Process ## 新的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块内发送输出。从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命令,该命令将以青色显示的单个字符串返回一个可读的状态。
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命令已设置为接受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
    )

一旦你启用了 ByPropertyName 的管道支持,就告诉 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/