前言
PowerShell存在的主要意义在于自动化管理,这通常意味着你将会在多个目标上同时执行任务。或许你希望重启多台计算机,重新配置多个服务,修改多个邮箱等。在本文,你将学到3种技术:批处理Cmdlet、WMI方法以及对象枚举,用于完成这些以及其他多目标任务。
“批处理”Cmdlet
很多PowerShell Cmdlet可以接受批量对象,或者称之为对象集合。比如,利用管道将一个Cmdlet产生的结果,传输给另一个Cmdlet。
Set-Service、Stop-Service、Move-ADObject以及Move-Mailbox等,都是接受一个或多个输入对象,并执行其任务或行为的Cmdlet。
范例:改变3个服务的启动模式
Get-Service -Name BITS, Spooler, W32Time | Set-Service -StartupType Automatic
从某种程度来说,Get-Service也是一种批处理Cmdlet。这是由于该命令能够从多台计算机中获取服务。
假设需要变更同样这2台计算机上的服务:
Get-Service -Name BITS, Spooler, W32Time -ComputerName server1, server2 | Set-Service -StartupType Automatic
上述方法中一个潜在的问题在于,执行动作的Cmdlet通常不会返回表示作业状态的结果。值得庆幸的是,这些命令通常会有一个-passThru
参数,该参数用于打印出该命令所接受的对象。
当然,也可以使用Set-Service输出其修改的服务,并使用Get-Service重新获取这些服务,以便查看之前的命令是否生效。
范例:使用-PassThru
参数
该命令将会从3台计算机列表中获取指定的服务,然后通过管道将这些服务传递给Start-Service。该命令不仅会启动服务,而且会将涉及的服务对象打印在屏幕上。并且会通过管道传递给Out-File,将这些被更新对象的信息存储在文本文件中。
CIM/WMI方式:调用方法
总有一些任务无法通过调用Cmdlet完成,而且有一些可以通过Windows管理规范(WMI)可以操控的条目。
比如,WMI中的Win32_NetworkAdapterConfiguration
类,该类代表与网卡绑定的配置信息。假如我们的目标是在计算机上所有的Intel网卡上启用DHCP,但不希望启用RAS或其他虚拟网卡的DHCP。可以按如下操作:
gwmi win32_networkadapterconfiguration -Filter "description like '%Wireless%'"
下一步是查看对象本身是否包含可以启用DHCP的方法,为了找出结果,我们将配置对象通过管道传输给Get-Member(或者其别名Gm)。
在结果列表的开始部分,可以看到方法EnableDHCP()
。
接下来,可以使用Invoke-WmiMethod
这个通用Cmdlet,该Cmdlet特别设计用于接受一批WMI对象,比如说我们的Win32_NetworkAdapterConfiguration对象,并调用附加在这些对象上的某个方法。Invoke-WmiMethod
一次只能接收一种类型的WMI对象。在本例中,我们只发送给Win32_NetworkAdapterConfiguration
一种对象,这意味着命令可以如预期产生效果。当然也可以一次发送多个对象(实际上,这是重点),但所有的对象都必须是同一类型。
也可以针对Invoke-WmiMethod
加上-WhatIf和-Conifrm参数。但直接由对象调用方法时,无法使用这些参数。
当有一个WMI对象包含可执行的方法时,大多可以使用Invoke-WmiMethod。该命令对于远程计算机同样有效。“如果可以使用Get-WmiObject获取对象,则也能够使用Invoke-WmiObject执行它的方法”。
下面是命令的接替者为Get-CimInstance
和Invoke-CimMethod
:
请尝试在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方法找到)。
通过阅读该页,发现无须为该方法的每一个参数赋值。可以将希望忽略的参数指定为Null(即$null变量)。
对于本例,我们希望变更服务的启动密码,即第8个参数。为了完成该工作,需要将前7个参数指定为$null
。
顺便提一下,无论是Get-Service还是Set-Service,都无法显示或设置某个服务的登录密码。而Get-WmiObject和Get-CimInstance这两个命令都可以完成该功能,此处使用WMI。
此处,可以尝试使用Invoke-WmiMethod
。该Cmdlet包含一个参数:-ArgumentList
,可以利用该参数为方法指定参数。
使用ForEach-Object
对于这种情况,就需要尝试其他方式:我们将会要求shell枚举所有服务对象,每次一个,并对每个对象执行Change()方法。即,此处使用ForEach-Object
这个Cmdlet完成这项工作。
PS C:\Windows\system32> gwmi win32_service -Filter "name='BITS'" |
>> ForEach-Object -Process {
>> $_.change($null, $null, $null, $null, $null, $null, $null, "P@ssw0rd")
>> }
__GENUS : 2
__CLASS : __PARAMETERS
__SUPERCLASS :
__DYNASTY : __PARAMETERS
__RELPATH :
__PROPERTY_COUNT : 1
__DERIVATION : {}
__SERVER :
__NAMESPACE :
__PATH :
ReturnValue : 0
PSComputerName :
从返回的结果,发现ReturnValue
为0表示成功。
说明:
- 使用
-Process
参数指定脚本段。其为位置参数,可以省略。 ForEach-Object
将会对于每一个通过管道传输给ForEach-Object的对象执行脚本段。每次脚本段执行后,下一个通过管道传输进来的对象,都会被置于特殊的$_容器。- 通过在
$_
后输入一个“.”,告诉Shell我们需要访问当前对象的属性或方法。 在示例中,访问
Change()
方法。方法的参数以逗号分隔列表方式存在,并被包在括号内。将-filter简写为-fi;
- 使用
**%**
这个别名代替**ForEach-Object**
; - 花括号内的代码段,对于每一个通过管道传入的对象执行一次。
- 在代码段内,
$_
代表通过管道传入的对象之一。 - 即使方法不需要任何参数,方法名称之后也要跟随圆括号。当需要参数时,通过逗号将参数分隔放在括号内。
常见误区
使用术语“批处理Cmdlet”或“行为Cmdlet”指代一个针对一组对象或对象集合操作的Cmdlet。你可以将一组对象发送给Cmdlet并由Cmdlet对循环进行处理,而不是指示计算机“遍历列表中的东西,并对列表中的每一个东西执行某些行为”。
下面所有的命令实现的功能完全相同:
接下来看一下每种方式的工作机制:
- ❶使用批处理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
;如果通过
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方法,那么就可以这么做。
方法文档
PowerShell的内置帮助系统并未记录WMI方法的文档。你需要使用搜索引擎(通常搜索WMI类的名称)来找到WMI方法的指南和示例。
也无法在PowerShell内置的帮助系统中找到非WMI对象的文档。比如说,如果获取一个服务对象的成员列表,你将会发现存在名称为Stop和Start的方法。
如果希望找到该对象的文档,请重点关注TypeName,在本例中也就是System. ServiceProcess.ServiceController
。在搜索引擎中搜索完整的类型名称,你通常可以找到完整的官方开发文档,并可以根据文档找出你所需的特定方法的文档。