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不会将该触发器存到某个列表。这还意味着我们需要将触发器传递给某个父命令,而不是先创建它供后续使用。