前言

PowerShell存在的主要意义在于自动化管理,这通常意味着你将会在多个目标上同时执行任务。或许你希望重启多台计算机,重新配置多个服务,修改多个邮箱等。在本文,你将学到3种技术:批处理Cmdlet、WMI方法以及对象枚举,用于完成这些以及其他多目标任务。

“批处理”Cmdlet

很多PowerShell Cmdlet可以接受批量对象,或者称之为对象集合。比如,利用管道将一个Cmdlet产生的结果,传输给另一个Cmdlet。
Set-Service、Stop-Service、Move-ADObject以及Move-Mailbox等,都是接受一个或多个输入对象,并执行其任务或行为的Cmdlet。

范例:改变3个服务的启动模式

  1. Get-Service -Name BITS, Spooler, W32Time | Set-Service -StartupType Automatic

从某种程度来说,Get-Service也是一种批处理Cmdlet。这是由于该命令能够从多台计算机中获取服务。

假设需要变更同样这2台计算机上的服务:

  1. Get-Service -Name BITS, Spooler, W32Time -ComputerName server1, server2 | Set-Service -StartupType Automatic

上述方法中一个潜在的问题在于,执行动作的Cmdlet通常不会返回表示作业状态的结果。值得庆幸的是,这些命令通常会有一个-passThru参数,该参数用于打印出该命令所接受的对象。
当然,也可以使用Set-Service输出其修改的服务,并使用Get-Service重新获取这些服务,以便查看之前的命令是否生效。

范例:使用-PassThru参数
image.png
image.png
该命令将会从3台计算机列表中获取指定的服务,然后通过管道将这些服务传递给Start-Service。该命令不仅会启动服务,而且会将涉及的服务对象打印在屏幕上。并且会通过管道传递给Out-File,将这些被更新对象的信息存储在文本文件中。

CIM/WMI方式:调用方法

总有一些任务无法通过调用Cmdlet完成,而且有一些可以通过Windows管理规范(WMI)可以操控的条目。

比如,WMI中的Win32_NetworkAdapterConfiguration类,该类代表与网卡绑定的配置信息。假如我们的目标是在计算机上所有的Intel网卡上启用DHCP,但不希望启用RAS或其他虚拟网卡的DHCP。可以按如下操作:
image.png

  1. gwmi win32_networkadapterconfiguration -Filter "description like '%Wireless%'"

下一步是查看对象本身是否包含可以启用DHCP的方法,为了找出结果,我们将配置对象通过管道传输给Get-Member(或者其别名Gm)。
image.png
在结果列表的开始部分,可以看到方法EnableDHCP()
image.png
接下来,可以使用Invoke-WmiMethod这个通用Cmdlet,该Cmdlet特别设计用于接受一批WMI对象,比如说我们的Win32_NetworkAdapterConfiguration对象,并调用附加在这些对象上的某个方法。
image.png
Invoke-WmiMethod一次只能接收一种类型的WMI对象。在本例中,我们只发送给Win32_NetworkAdapterConfiguration一种对象,这意味着命令可以如预期产生效果。当然也可以一次发送多个对象(实际上,这是重点),但所有的对象都必须是同一类型。
也可以针对Invoke-WmiMethod加上-WhatIf和-Conifrm参数。但直接由对象调用方法时,无法使用这些参数。

当有一个WMI对象包含可执行的方法时,大多可以使用Invoke-WmiMethod。该命令对于远程计算机同样有效。“如果可以使用Get-WmiObject获取对象,则也能够使用Invoke-WmiObject执行它的方法”。

下面是命令的接替者为Get-CimInstanceInvoke-CimMethod
image.png

请尝试在PowerShell中运行Help Set-NetIPAddress。在较新版本的Windows中,你将会发现这个强大的Cmdlet掩盖了大量底层WMI/CIM的复杂性。我们可以使用该Cmdlet变更IP地址,而无需一大堆WMI/CIM。

枚举对象

我们遇到的一些情况是Invoke-WmiObject无法执行某个方法——执行时不断返回奇怪的错误信息(Invoke-CimMethod更可靠)。还有就是虽然某个Cmdlet可以产生对象,但我们知道并没有可以通过管道接收这些对象并进行操作的批处理Cmdlet。无论是上述哪种情况,你依然可以完成任务,但必须回到传统的VBScript风格的方法,来指挥计算机枚举对象并一次执行一个对象。

此处使用Win32_Service这个WMI类的Change()方法作为示例。这是一个可以一次性变更某个服务中多个元素的复杂方法。下图展示了其在线文档(通过搜索“Win32_Service”并单击Change方法找到)。
image.png
通过阅读该页,发现无须为该方法的每一个参数赋值。可以将希望忽略的参数指定为Null(即$null变量)。

对于本例,我们希望变更服务的启动密码,即第8个参数。为了完成该工作,需要将前7个参数指定为$null
image.png
顺便提一下,无论是Get-Service还是Set-Service,都无法显示或设置某个服务的登录密码。而Get-WmiObject和Get-CimInstance这两个命令都可以完成该功能,此处使用WMI。

此处,可以尝试使用Invoke-WmiMethod。该Cmdlet包含一个参数:-ArgumentList,可以利用该参数为方法指定参数。
image.png

使用ForEach-Object

对于这种情况,就需要尝试其他方式:我们将会要求shell枚举所有服务对象,每次一个,并对每个对象执行Change()方法。即,此处使用ForEach-Object这个Cmdlet完成这项工作。

  1. PS C:\Windows\system32> gwmi win32_service -Filter "name='BITS'" |
  2. >> ForEach-Object -Process {
  3. >> $_.change($null, $null, $null, $null, $null, $null, $null, "P@ssw0rd")
  4. >> }
  5. __GENUS : 2
  6. __CLASS : __PARAMETERS
  7. __SUPERCLASS :
  8. __DYNASTY : __PARAMETERS
  9. __RELPATH :
  10. __PROPERTY_COUNT : 1
  11. __DERIVATION : {}
  12. __SERVER :
  13. __NAMESPACE :
  14. __PATH :
  15. ReturnValue : 0
  16. PSComputerName :

从返回的结果,发现ReturnValue为0表示成功。
image.png
说明:

  • 使用-Process参数指定脚本段。其为位置参数,可以省略。
  • ForEach-Object将会对于每一个通过管道传输给ForEach-Object的对象执行脚本段。每次脚本段执行后,下一个通过管道传输进来的对象,都会被置于特殊的$_容器。
  • 通过在$_后输入一个“.”,告诉Shell我们需要访问当前对象的属性或方法。
  • 在示例中,访问Change()方法。方法的参数以逗号分隔列表方式存在,并被包在括号内。

    • 使用$null作为我们不希望变更的参数传入,并将新密码作为第8个参数。
    • 该方法可以接受更多参数,但由于不需要修改后面三个参数,可以省略不写(也可以指定为$null)。

      分解ForEach-Object

      image.png
      下面以别名方式,执行命令:
      image.png
      说明:
  • 将-filter简写为-fi;

  • 使用**%**这个别名代替**ForEach-Object**
  • 花括号内的代码段,对于每一个通过管道传入的对象执行一次。
  • 在代码段内,$_代表通过管道传入的对象之一。
  • 即使方法不需要任何参数,方法名称之后也要跟随圆括号。当需要参数时,通过逗号将参数分隔放在括号内。

    常见误区

    使用术语“批处理Cmdlet”或“行为Cmdlet”指代一个针对一组对象或对象集合操作的Cmdlet。你可以将一组对象发送给Cmdlet并由Cmdlet对循环进行处理,而不是指示计算机“遍历列表中的东西,并对列表中的每一个东西执行某些行为”。

下面所有的命令实现的功能完全相同:
image.png
接下来看一下每种方式的工作机制:

  • ❶使用批处理Cmdlet。即使用Get-Service获取所有名称包含“B”的服务,并停止这些服务。
  • ❷使用ForEach-Object替代批处理Cmdlet,并要求每个服务执行Stop()方法。
  • ❸使用WMI,而不是Shell的原生管理Cmdlet。将接收到包含字母“B”的服务,通过管道传递给Invoke-WmiMethod。然后调用StopService方法,这是WMI服务对象使用的方法名称。
  • ❹使用ForEach-Object代替Invoke-WmiMethod实现完全相同的工作。
  • ❺直接使用Stop-Service,但其-Name参数(PowerShell v3及以上)接受通配符。

上述的例子,还阐述了使用原生Cmdlet和WMI的重要区别。

  • 原生Cmdlet过滤条件通常使用“*”作为通配符,而WMI过滤使用百分比符号(%)。
  • 原生对象通常和WMI有同样的功能,但语法或许会有不同。
    • 在本例中,由Get-Service产生的ServiceController对象有Stop()方法;
    • 而我们通过WMI的Win32_Service类访问同样的对象时,方法名称变为StopService()
  • 原生过滤通常使用原生的比较操作符,比如说-eq;

    • WMI使用类似编程语言风格的操作符,比如说=或者Like

      WMI方法与Cmdlet对比

      你何时该使用WMI方法或Cmdlet来完成一个任务呢?这个选择十分简单。
  • 如果通过Get-WmiObject获取对象,将需要通过使用WMI方法来执行行为。

    • 即可以使用Invoke-WmiMethod或ForEach-Object方式执行方法。
  • 如果通过非Get-WmiObject的方式获取对象,将需要对获取到的对象使用原生Cmdlet。除非获取到的对象只有方法而没有能够完成任务所需的Cmdlet,你可能会使用ForEach-Object方式执行方法。

无论何时都无法将任何对象通过管道传递给一个方法。你只能利用管道将一个Cmdlet产生的对象传递给另一个Cmdlet。如果完成任务所需的Cmdlet不存在,但存在这样的方法,那么你就可以将其通过管道传递给ForEach-Object并执行对象的方法。

例如,假设你通过Get-Something这个Cmdlet获取到对象,你希望删除这些对象,但不存在Delete-Something或Remove-Something这样的Cmdlet。但该对象包含Delete方法,那么就可以这么做。
image.png

方法文档

PowerShell的内置帮助系统并未记录WMI方法的文档。你需要使用搜索引擎(通常搜索WMI类的名称)来找到WMI方法的指南和示例。
也无法在PowerShell内置的帮助系统中找到非WMI对象的文档。比如说,如果获取一个服务对象的成员列表,你将会发现存在名称为Stop和Start的方法。
image.png
如果希望找到该对象的文档,请重点关注TypeName,在本例中也就是System. ServiceProcess.ServiceController。在搜索引擎中搜索完整的类型名称,你通常可以找到完整的官方开发文档,并可以根据文档找出你所需的特定方法的文档。