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命令的参数:

可以看到,其恰好接收-NameValue。我们再看这两个参数是否支持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-CustomFormat-Custom在这里暂不介绍。

  • Format-Table(ft)

其常用参数如下:

-AutoSize

强制结果集仅保存足够的列空间,使表格更为紧凑。

-Property

使用你提供的属性列。我们看几个效果:

(好丑)

(这个比第一幅图好看得多)

-GroupBy

每当指定属性值变更时,创建一个具有新列头的结果集。效果如下:

上面的例子中,它实际上把输出给分成了两部分。

-Wrap

默认情况下如果Shell需要把列的信息截断,会在列尾带上(…),如下图:

而加上-Wrap后,它会让信息拐到下一行。像这样:

  • Format-List(fl)

Format-Table相关参数Format-List也有。不过,fl也是除gm外的另一个展示对象属性的方法:

  • Format-Wide(fw)

用于展示一个宽列表。

它仅展示一个属性的值,所以它的-Property只接受一个属性。

与“自定义属性”结合

上一章我们提到“自定义属性”,这一技术在Format-TableFormat-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。另外需要注意的是,ActionTrigger

最终,结合我们之前学习的括号知识,可以成功完成任务,效果如下:

总结

当每次创建触发器时触发器的ID都为0,而不是每次创建触发器都有一个连续递增的触发器ID时,我们可以安全地确认PowerShell不会将该触发器存到某个列表。这还意味着我们需要将触发器传递给某个父命令,而不是先创建它供后续使用。