PowerShell实战指南 Chapter 8-12
很多年 冰山形成以前 鱼曾浮出水面 很多年
Chapter 8 对象:数据的另一个名称
Get-Process
产生的进程表其实是进程对象的集合:
- 对象——表行
- 属性——表列
- 方法
- 集合——表
PowerShell给出的结果是对象的格式化展示,这一点区别于Linux Shell,Linux Shell纯粹是文本。所以在Linux上需要依赖各种文本解析工具来筛选到想要的结果,如awk
/sed
/grep
。相比之下,基于对象的处理方法更为优越,因为你只需指明属性(也就是类的成员),而无需担心输出文本位置改变等不可控因素。
其实,在PowerShell管道中传输的正是对象,而非Linux Shell管道中的文本。GetProcess
在控制台中默认不会把进程对象的属性展示完全,这只是在将要输出时受到了配置文件的限制(最终展示的是表还是列表,也受到配置文件的限制)。但进程对象本身是完整的。如果你导出到文件中,就会发现进程对象的所有属性都被导出了:
Get-Process | ConvertTo-Html | Out-File procs.html
查看类的成员:Get-Member
(gm):
事实上,所有会产生输出的Cmdlet都能够被Get-Member
,比如它本身:
如上,MemberType
有如下的值:
- Method
- Property (.Net中的)
- NoteProperty (PowerShell ETS自动添加的)
- ScriptProperty (PowerShell ETS自动添加的)
- AliasProperty (PowerShell ETS自动添加的)
- PropertySet
- Event
PowerShell中对象属性往往是只读的。
此时再回看Cmdlet的组合:
Get-Process | Sort-Object -Property VM -Descending
全部都是对对象的操作。
另外,Select-Object
用于选择所需属性,而Where-Object
基于筛选条件从管道中移除或过滤对象。
在一个命令行中管道可以包含不同类型的对象。注意下面两幅图:
Sort-Object
从管道中取出进程对象,放入的还是进程对象。而Select-Object
在取出后放入的则是一个自定义对象。
当PowerShell
发现光标到达命令行末尾时,它必须知道如何对文本输出结果进行排版。在Select-Object
后,由于管道中已经是自定义对象,所以它只能尽最大努力排版,所以最后结果不如Get-Process
的输出那么好看。
动手实验
- 找出生成随机数字的Cmdlet
Das ist einfach.
- 找出显示时间和日期的Cmdlet
- 用2中的Cmdlet只显示星期几
- 找出显示已安装hotfix的Cmdlet,按照安装日期排序,并仅显示安装日期、补丁ID和安装用户
首先看一下都有哪些属性:
OK,行动:
- 从安全事件日志中显示最新的50条列表。按时间升序排序,同时也按索引排序。显示索引、时间和来源,把这些内容存入文本文件
Chapter 9 深入理解管道
ByValue & ByPropertyName
首先做一个测试:
在一个文本文件computers.txt
中输入
SERVER2
WIN8
CLIENT17
DONJONE1D96
然后执行
Get-Content .\computers.txt | Get-Service
产生错误。本章我们研究Pipeline parameter binding
,即上一个命令通过管道把内容传递给下一个命令后,PowerShell如何决定由下一条命令的哪个参数去接收这些内容。
抽象出研究模型:
CommandA | CommandB
它会依次尝试下面两种方法:
- ByValue
- ByPropertyName
ByValue
即先确定CommandA
产生的数据对象类型,然后看CommandB
中哪个参数可以接受这个类型。比如:
可以看到传递过来的是System.String
,而CommandB
中也的确存在可以以ByValue
方式接收String
类型的参数-Name
:
但是由于的确没有如computers.txt
内容那样的服务名,所以报错为“找不到服务”。同时,由于PowerShell只允许一个参数去接收ByValue
管道传递的对象类型,而-Name
接收了,所以其他参数无法接收这个数据。
我们之前提到,具有相同名词的命令在大部分情况下都可以直接通过管道传递对象。比如:
Get-Process -Name note* | Stop-Process
这是因为Stop-Process
具有如下参数:
那么什么时候用ByPropertyName
呢?看下面这个例子:
Get-Service -Name s* | Stop-Process
经过比对后发现,Stop-Process
没有一个参数可以接收传过来的对象类型,于是ByValue
失败,尝试ByPropertyName
,它会尝试匹配传递对象的属性名称与后一个命令的参数名称。
我们可以看到,Name
是传递对象和后面命令共有的一个名称,且对于后面的命令来说,其-Name
参数支持ByPropertyName
。PowerShell会尝试把所有能够对应起来的属性名与参数名进行关联。这里只有Name
匹配。
所以我们看到其报错为,“找不到进程”,因为的确没有以服务名称命名的进程。
下面进行另一个测试。将下面的文本保存为Alias.CSV
:
Name,Value
d,Get-ChildItem
sel,Select-Object
go,Invoke-Command
接着我们尝试导入并查看导入的是什么类型的对象:
然后我们看一下New-Alias
命令的参数:
可以看到,其恰好接收-Name
和Value
。我们再看这两个参数是否支持ByPropertyName
:
支持!那么下面这条语句应该可以正常工作:
Import-CSV Alias.csv | New-Alias
果然成功:
这说明,我们只需要为命令提供符合其用法的值,然后就可以用管道把这些连接起来。这有点像拼图或者拼装玩具。
自定义属性
下面通过一个例子,学习数据不对齐时的处理方法:自定义属性。
由于默认环境无相关命令,从这里到本章结束为“Chapter 7 扩展命令”使用的环境。
这个实验的情景是,我们要处理其他对象或者是别人提供给自己的数据(比如,以上文提到的CSV格式)。
我们使用的命令是New-ADUser
(需要预先配置域控制器):
我们需要用到以下参数:
可以发现,这些参数都支持ByPropertyName
,且-Name
是必需的。假设我们是某公司的管理员,公司的HR部门提供了一个如下的CSV文件(他们固执的使用自己的格式):
login, dept, city, title
DonJ, IT, Las Vegas, CTO
Gregs, Custodial, Denver, Janitor
JeffH, IT, Syracuse, Network Engineer
如上,成功导入文件并产生三个对象。但是这些对象的属性与我们前面提到的参数并不完全对应:
dept
并不是-Department
的前缀login
属性完全不存在于前面的参数中(事实上,它应该是-Name
)
那么如何解决这个问题?一个方法是,手动去修改CSV文件。另一个方法是,使用我们提到的自定义属性:
解释:
- 我们使用
Select-Object
及-Property
参数,首先是*
,即选择所有属性列,然后输入逗号,意思是后面还有别的 - 之后创建哈希表,其形式为
@{}
,其中包含一个或多个Key-Value - 哈希表中第一个键是
Name
/N
/Label
/L
其中任意一个均可(即它们是等同的),其对应的值为我们想要创建的属性名称
- 第二个键是
expression
/e
任意均可,其对应的值是一个包含在大括号内的脚本块。$_
指的是已经存在的管道对象(即CSV文件中每行的数据),我们借此来读取管道对象的属性
OK。我们测试一下:
成功,我们查看一下:
Get-ADUser -Filter *
我们可以通过help Select-Object -Examples
看一下官方对这种用法的解释:
括号的使用
当参数不支持管道输入时怎么办?使用括号!例如:
我们看一下帮助:
果然不行。那么就用括号吧:
成功了。报错只是因为没有相关的配置而已。
那么,如果ComputerName
并不是从文件中直接获取,而是需要从其他对象的属性中获取呢?比如下面这个例子:
我们希望提取其中的Name
传给其他命令,比如
Get-Service -ComputerName (Get-ADComputer -Filter * -SearchBase "ou=domain controllers, dc=rambo, dc=com")
这样会报错(当然,其实对于Get-Service
来说,可以使用管道,但这里我们是为了学习括号的用法):
原因很简单,我们之前已经说了,类型不匹配:
我们需要提取其中的Name
属性。这里可以用到Select-Object
的-ExpandProperty
参数。首先注意它与-Property
的区别。它们的作用分别是“提取属性的值并返回”和“返回只包含特定属性的对象”。下图清楚地展示了这些区别:
很明显。这里我们需要的是String
!
Bingo!
(作者不停地说这个技术非常强大,一定要掌握!)
进一步地,我们来设计另一个实验:
创建一个computers.csv
:
hostname, operatingsystem
localhost, windows
由于我的虚拟机环境目前只能访问本机,所以只写了localhost
,但这不影响我们的实验。
如上。我们借用括号技术从CSV文件中获取了属性,并成功读取了相关计算机的进程列表。
我们也可以使用管道(只要参数支持管道,就能用):
当然了,直接搞是不行的:
总结
本章学习了非常有用的概念和方法:
- ByValue
- ByPropertyName
- 自定义属性
- 括号
- ExpandProperty提取属性值
有了这些技术,我们可以获得比Linux Shell强大得多的功能,而不必编写复杂的脚本,只需利用“面向对象的特性”和上面这些技能就可以达到目的。
一个意外的惊喜是 The Computername parameter in Get-WMIObject doesn’t take any pipeline binding.
Chapter 10 格式化及如何正确使用
默认格式化方法
默认的输出格式受配置文件的约束,配置文件如下:
C:\Windows\System32\WindowsPowerShell\v1.0
另外,不要改动文件,因为其末尾有数字签名:
其中DotNetTypes.format.ps1xml
中包含了进程对象的格式化方式,如下:
<View>
<Name>process</Name>
<ViewSelectedBy>
<TypeName>System.Diagnostics.Process</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Handles</Label>
<Width>7</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>NPM(K)</Label>
<Width>7</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PM(K)</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>WS(K)</Label>
<Width>10</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>VM(M)</Label>
<Width>5</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>CPU(s)</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Width>6</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader />
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>HandleCount</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>[int]($_.NPM / 1024)</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>[int]($_.PM / 1024)</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>[int]($_.WS / 1024)</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>[int]($_.VM / 1048576)</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if ($_.CPU -ne $())
{
$_.CPU.ToString("N")
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Id</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>ProcessName</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
可以看到,XML对格式的定义就是我们看到的那样。
当运行Get-Process
时,发生下面的事情:
- Cmdlet把
System.Diagnostics.Process
类型的对象放入管道 - 管道末端有一个名为
Out-Default
的隐藏Cmdlet
,它把需要运行的命令全部放入管道中 Out-Default
把对象传输到Out-Host
(默认即为本地机器的显示屏)- 大部分
Out-Cmdlets
不适合用在普通对象中,而主要用于特定格式化指令。所以Out-Host
看到普通对象会把它们传递给格式化系统 - 格式化系统依赖内部规则检查对象类型,并产生格式化指令,最终传输回
Out-Host
Out-Host
发现格式化指令,于是根据这个指令产生显示到屏幕上的结果
同理,当你Get-Process | Out-File procs.txt
时也会经历上面的几个步骤。只不过Out-Host
被换成了Out-File
。
格式化系统所谓的内部规则做了什么呢?
- 检查对象类型是否能够被预定义视图处理(即
DotNetType.format.ps1xml
中的进程部分) - 如果没有找到对应的预定义视图,则寻找是否有针对这个对象类型的“default display property set”,这部分被定义在
types.ps1xml
中
一个例子是Win32_OperatingSystem
,我们可以在types.ps1xml
对其的定义:
<Type>
<Name>System.Management.ManagementObject#root\cimv2\Win32_OperatingSystem</Name>
<Members>
<PropertySet>
<Name>PSStatus</Name>
<ReferencedProperties>
<Name>Status</Name>
<Name>Name</Name>
</ReferencedProperties>
</PropertySet>
<PropertySet>
<Name>FREE</Name>
<ReferencedProperties>
<Name>FreePhysicalMemory</Name>
<Name>FreeSpaceInPagingFiles</Name>
<Name>FreeVirtualMemory</Name>
<Name>Name</Name>
</ReferencedProperties>
</PropertySet>
<MemberSet>
<Name>PSStandardMembers</Name>
<Members>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>SystemDirectory</Name>
<Name>Organization</Name>
<Name>BuildNumber</Name>
<Name>RegisteredUser</Name>
<Name>SerialNumber</Name>
<Name>Version</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</MemberSet>
</Members>
</Type>
也是一致的。
- 继续。如果上一步中也没有找到相应的结果,那么下一步的决策就会考虑所有对象的属性值
- 决策。如果显示4个及以下的属性,将采用表格。否则,将采用列表(那么为什么
Get-Process
用的是表格呢?因为预定义中文件用的是表格<TableControl>
)
自定义格式化
PowerShell中有4种用于格式化的Cmdlets,分别为Format-Table
/Foramt-List
/Format-Wide
/Format-Custom
。Format-Custom
在这里暂不介绍。
Format-Table
(ft)
其常用参数如下:
-AutoSize
强制结果集仅保存足够的列空间,使表格更为紧凑。
-Property
使用你提供的属性列。我们看几个效果:
(好丑)
(这个比第一幅图好看得多)
-GroupBy
每当指定属性值变更时,创建一个具有新列头的结果集。效果如下:
上面的例子中,它实际上把输出给分成了两部分。
-Wrap
默认情况下如果Shell需要把列的信息截断,会在列尾带上(…),如下图:
而加上-Wrap
后,它会让信息拐到下一行。像这样:
Format-List
(fl)
Format-Table
相关参数Format-List
也有。不过,fl
也是除gm
外的另一个展示对象属性的方法:
Format-Wide
(fw)
用于展示一个宽列表。
它仅展示一个属性的值,所以它的-Property
只接受一个属性。
与“自定义属性”结合
上一章我们提到“自定义属性”,这一技术在Format-Table
和Format-List
中也可以使用:
输出到网格
Out-GridView
完全绕过了格式化子系统,它也不接受Format-Cmdlet
的输出:
常见问题
Format-
命令应该是Out-File
或者Out-Printer
前的最后一个命令,因为只有Out-
相关命令能够处理Format-
产生的结果。如果你直接让Format-
作为命令行的结尾,那么最终会通过Out-Default -> Out-Host
,这样的格式化是非预期的。
上面这条命令结果如下:
另外,一次只输出一种对象。
Get-Process; Get-Service
这种不要做。
练习
使用Get-EventLog
显示所有可用事件日志的列表,并把信息格式化为一个表,日志需要显示名字和保留期限,分别以“LogName”和“RetDays”显示。
Chapter 11 过滤和对比
本章使用“Chapter 7 扩展命令”的环境。
PowerShell提供两种方式缩小结果集:
- 尝试让Cmdlet命令只检索指定内容
- 使用另一个命令进行迭代过滤(类似于grep)
一般来说,能用第一种尽量用第一种。例如:
但是如果你希望基于更为复杂的条件进行过滤,比如只返回正在运行的服务,而不考虑服务名称,只用Get-Service
就无法做到——它没有提供相关参数。
然而,对于微软的活动目录模块相关的命令来说,Get-
基本上都有-Filter
参数。但不建议用-Filter *
,这样会增大域控制器的压力。如下的命令是推荐的:
上述技巧被称为“左过滤”,其优势在于只检索匹配的对象。
左过滤
左过滤的缺点是可能不同的Cmdlet过滤方法不同。比如Get-Service
只能通过Name
过滤,而Get-ADComputer
可以根据任何属性过滤。
对比操作符
注:当对比文本字符串时会忽略大小写。
-eq
/-ne
/-ge
/-le
/-gt
/-lt
如果希望区分字符串的大小写,可以在所有操作符前加c
,如-ceq
:
日期也可以比较:
另外还有-and
/-or
/-not
。
$False
/$True
表示false和true。
对于字符串,还有-like
和-notlike
,即比较可以使用通配符;-match
/-notmatch
则允许使用正则表达式。
可通过查看帮助文件进一步学习:
那么我们可以在哪些地方使用对比操作表达式?一个地方是前面演示过的-Filter
,另一个地方是Where-Object
。
Where-Object
上面的截图也表现了它的优点——Where-Object
是通用的,即使Get-Service
本身并没有上面的过滤功能。往往它也简写为Where
。
迭代过滤
一个例子:我们想要计算正在使用虚拟内存的十大进程占用的虚拟内存总量(排除powershell进程):
Get-Process |
Where-Object -FilterScript {$_.Name -notlike "powershell*"} |
Sort-Object -Property VM |
Select-Object -Last 10 |
Measure-Object -Property VM -Sum |
Select-Object -Property @{l="Sum";e={$_.Sum / 1024 / 1024 -as [int]}}
总结
Where-Object
不是首选。首选是“左过滤”原则。对于一个Cmdlet来说,应该尽可能地使用其参数提供的功能。
Chapter 12 学以致用
本章我们做一个自学实验:添加计划任务。
首先通过Get-Command *task*
找到可能要用的命令,然后发现它们基本上都属于ScheduledTasks
,于是查看该模块下的命令:
发现New-ScheduledTask
可能会有帮助,看一下文档:
发现它并不能自动注册。根据样例,最终还是要用到另一个命令:Register-ScheduledTask
。另外需要注意的是,Action
和Trigger
。
最终,结合我们之前学习的括号知识,可以成功完成任务,效果如下:
总结
当每次创建触发器时触发器的ID都为0,而不是每次创建触发器都有一个连续递增的触发器ID时,我们可以安全地确认PowerShell不会将该触发器存到某个列表。这还意味着我们需要将触发器传递给某个父命令,而不是先创建它供后续使用。