如何传输数据给管道

当将两条命令串联在一起时,PowerShell必须搞清楚怎样将第一条命令的输出作为第二条命令的输入。如下,我们将第一条命令称为命令A,这条命令会产生某些结果。第二条命令称为命令B,它会接收命令A产生的结果集,然后完成自己的工作。
image.png
例如,创建一个包含计算机名称的文本文件,每行代表一个计算机名称。
image.png
然后将这部分计算机名称作为某些命令的传入数据,以便该命令会在这些计算机上被运行。
image.png
当运行Get-Content命令时,它会将文本文件中的计算机名称放入管道中。之后PowerShell再决定如何将该数据传递给Get-Service命令。但PowerShell一次只能使用单个参数接收传入数据。也就是说,PowerShell必须决定由Get-Service的哪个参数接收Get-Content的输出结果。这个决定的过程就称为管道参数绑定(Pipeline parameter binding)。
PowerShell使用两种方法,将Get-Content的输出结果传入给Get-Service的某个参数。

  • 该Shell尝试使用的第一种方法称为ByValue
  • 如果这种方法行不通,它将会尝试ByPropertyName

    使用ByValue进行管道输入

    当使用ByValue这种方式实现管道参数绑定时,PowerShell会确认命令A产生的数据对象类型,然后查看命令B中哪个参数可以接受经由管道传来对象的类型。

可以采用下面的方法来证明:通过管道将命令A的输出结果发送给Get-Member,然后就可以查到该命令产生的结果的对象类型。之后,查看命令B的详细帮助信息(例如Help Get-Service -Full),确定命令B的哪个参数可以接收ByValue管道传出的数据类型。
image.png
你将会看到Get-Content命令产生的结果对象的类型是System.String(简称为String)。通过查询帮助信息,可以看到Get-Service中的确也存在可以从ByValue管道中,接收String类型数据的参数。检查发现,可以接受String类型数据的参数是-Name,其说明为“指定要检索的服务的名称”。

你可能已经发现一个问题:文本文件中的内容,也就是String对象,是指计算机名称,并不是服务的名称。如果我们执行下面的命令,之后会得到名为SERVER2或者WIN8的服务名称,肯定无法正常执行。
image.png
PowerShell只允许使用一个参数,接收ByValue管道返回的对象类型。也就意味着,由于-Name参数接收了来自ByValue管道返回的String类型数据,那么其他参数就无法再接收该数据。

下面的示例,可以得到我们期望的结果:
image.png
将Get-Process输出结果,绑定到Stop-Service命令的一个参数:
image.png
Get-Process命令会返回类型为System.Diasnostics.Process的对象,Stop-Process命令会使用-InputObject参数接收这些来自ByValue管道的进程对象。从帮助信息中得知,该参数会“停止由指定的进程对象表示的进程”。换句话说,命令A会返回一个或多个进程对象,命令B会停止(或者杀死)这些进程。

大部分情况下,使用相同名词的命令,都可以使用ByValue方式相互之间进行管道传输(比如Get-Process和Stop-Process)。

来看另外一个示例:
image.png
Get-Service返回了ServiceController类型的对象(System.ServiceProcess.ServiceController的简写)。
image.png
通过查看帮助信息,Stop-Process没有一个参数可以接收ServiceController类型的对象。这也就意味着,使用ByValue方式进行处理的方案失败,此时PowerShell会尝试其备选方案ByPropertyName

使用ByPropertyName进行管道传输

ByPropertyName与ByValue稍有不同。通过该方法,命令B的多个参数可以被同时使用。我们再次将命令A的输出结果传递给Get-Member,之后查看命令B的语法。

  • 命令A的输出结果中一个属性的名称,匹配到命令B的一个参数。

image.png
ByPropertyName的原理很简单,仅仅是寻找能够匹配参数名称的属性名称。本例中属性“Name”与参数名称“-Name”相同,Shell会尝试将这两个值进行关联。

首先,它会检查-Name参数是否可以接收来自ByPropertyName管道的输出。通过查看详细帮助信息,就可以确定:-Name参数可以接收来自ByPropertyName管道的输出结果。
image.png
与ByValue管道只能使用一个参数不同,ByPropertyName会将每个匹配的属性与参数进行关联(提供的每个参数都可以接收来自ByPropertyName管道的输出值)。

运行后,可以看到产生了大量的错误。问题在于,Service的名称基本上都类似于ShellHWDetection和SessionEnv,但是服务的可执行文件一般为类似svchost.exe的这种命名规则。Stop-Process只会处理那些可执行文件的名字。虽然Name属性能通过管道关联到-Name参数,但是Name属性中隐藏的属性值并不能被-Name参数所处理,最终也就导致错误。
image.png

下面看一个可以正常运行的示例:使用记事本新建一个以逗号间隔的CSV文件。
image.png
之后回到Shell界面,尝试导入该文件:
image.png
在来查看New-Alias的详细帮助:
image.png
Name和Value属性都可以关联到New-Alias的参数名称。当然,这里是特意实现的(CSV文件的列可以任意命名)。现在检查New-Alias的-Name和-Value参数,是否可以接收来自ByPropertyName管道的输出结果。
image.png
经过查看,两个参数都可以接收管道输入,也就证明下面的语句可以正常工作。
image.png
执行之后,会产生3个新的别名,名为d、sel和go,分别对应Get-ChildItem、Select-Object和Invoke-Command命令。

自定义属性名称

这里会使用到属于活动目录中的一个模块New-ADUser。它存在于Windows Server 2008 R2及之后的版本操作系统的域控制器中。另外,你也可以在安装了微软的远程服务器管理工具(Remote Server Administration Tools, RSAT)的客户端电脑上找到该组件。

New-ADUser命令包含一些参数,每个参数用来匹配一个新的活动目录账号的信息:

  • -Name(必要参数)
  • -samAccountName(从语法角度,可以不提供。但是仍然需要提供该参数,使得AD账号可用)
  • -Department
  • -City
  • -Title

以上这些参数,都可以按照ByPropertyName方式接收管道的输出。

范例:处理一个CSV文件
image.png
PowerShell成功导入该CSV文件,最终产生了3个对象,并且每个对象包含4个属性。但是存在一个问题:dept属性与New-ADUser的-Department参数并不吻合;同时Login属性是无意义的,这里并没有包含samAccountName或者Name属性。创建新用户时,必须指定这两个属性。
image.png

哈希表结构

如何解决这个问题?当然,可以直接打开这个CSV文件,然后修改正确的列名。但也可以通过以下方式来解决:

  1. PS C:\> Import-CSV .\NewUsers.CSV |
  2. >> Select-Object -Property *,
  3. >> @{name='samAccountName'; expression={$_.login}},
  4. >> @{label='Name'; expression={$_.login}},
  5. >> @{n='Department'; e={$_.Dept}}
  6. >>

image.png
下面将这部分语法拆开来看。

  • 这里使用了Select-Object命令以及它的-Property参数。
    • 最开始,指定了这个属性。在后面,使用了逗号,也就意味着我们还会输入其他的一些属性列。
  • 之后我们创建一个哈希表,哈希表的结构是以@{为起始,以}为结尾。
    • 哈希表中包含了一个或者多个成对的键-值(Key-Value)数据。
    • 使用Select-Object寻找我们指定的一些特定键。
  • Select-Object需要寻找的第一个键可以是Name、N、Label或L,该键对应的值也就是要创建的属性名称。
    • 在第一个哈希表中,我们指定了sam AccountName,第二个哈希表中为Name,第三个哈希表中指定为Department。
    • 这三个属性的名称,正好可以对应到New-ADUser命令的3个参数。
  • Select-Object需要的第二个键可以是expression或者E。该键对应的值是一个包含在{}中的脚本块。
    • 在脚本块中,使用特定的$_占位符关联到已存在的管道对象(CSV文件中每行的数据)。
    • 通过$_可以读取管道对象的属性列。也就是说,通过这种方法来指定新属性的值。

到现在为止,已完成的步骤包括获取CSV文件的内容(Import-CSV),之后在管道中动态地修改该内容。最后新的数据输出结构能与New-ADUser命令期望的格式一致,这样我们就可以使用下面的命令创建新的AD用户了。
image.png

无法获取管道结果的命令

有的cmdlet无法处理管道的输出结果,比如Get-WMIObject。我们现在先大概看一下它的帮助信息:
image.png
该参数并不能接收来自管道的计算机名称,下述命令无法正常执行。
image.png
Get-Content命令输出的String对象无法匹配到Get-WMIObject命令的-ComputerName参数。那么此时,我们应该怎么做?答案是使用圆括号。
image.png
PowerShell会采用如下顺序来执行这个命令:先执行括号里的命令;第一步命令执行的结果(在本例中,是多个String类型的对象)被传递给Get-WMIObject的参数。由于-Computer Name能够接收String类型的对象,所以此时,整个命令可以正常执行。

提取属性的值:**-ExpandProperty**参数

在很多时候,我们可能不会从一个静态文件中获取计算机名称,比如需要从活动目录中获取某些数据。借助于ActiveDirectory模块,可以查询域控制器(Domain Controller)上所有的信息。
image.png
可以使用括号将上面命令的输出结果传递给Get-Service吗?也就是说,下面的命令可以执行吗?
image.png
上面的命令无法成功运行。查看Get-Service的帮助文件,可以看到-Computer这个参数只能接收String类型的值。但Get-ADComputer命令的输出结果是ADComputer类型的对象,而不是String类型的对象。

但是ADComputer类型的对象包含了一个-Name的属性。接下来要做的是,提取出ADComputer类型对象中的-Name属性值,然后将这些值(也就是计算机名称)传递给-ComputerName参数。

可以使用**Select-Object**命令解决这个问题,因为它包含一个可以接收属性名称的参数**-ExpandProperty**。它会获取对应的属性,提取属性的值,然后返回这些值(作为Select-Object的输出结果)。
image.png
该命令会返回一个包含计算机名称的清单,里面的值可以传递给Get-Service命令的-ComputerName参数(或者其他包含-ComputerName参数的一些Cmdlet)。
image.png

-ExpandProperty-Property的区别:

  1. PS C:\Users\guoruilong> Get-date | select -ExpandProperty DayOfWeek
  2. Tuesday
  3. PS C:\Users\guoruilong> Get-date | select -Property DayOfWeek
  4. DayOfWeek
  5. ---------
  6. Tuesday