Windows PowerShell 2.0语言开发之管道对象


本文介绍了通过创建对象管道作用于集合的基础工具,可以把数据转换为用户需要的形式。有了这些操作,可以作用于来自不同来源的对象。每个PowerShell命令均作用于对象集合并返回另外的集合,并且可以在需要情况下将任何单个对象显式地转换为集合。

传统基于文本的管道

链式命令中的每个命令读取文本作为输入,然后生成文本作为输出,因此便于执行并提供传递参数的基本方法。然而用文本来传递数据的缺点主要的是没有标准的数据格式,从而很难应用这些数据。甚至需要解析输入,并从中提取出有意义的数据便于程序开发人员生成便于转换的解析形式,这样可以使得编写的脚本工具更加的实用。当然输出是针对便于机器解析,而不仅是便于用户阅读的。Shell的设计者通常会提供开关和命令行选项,使用这些选项可以控制输出的格式。

大多数的Shell通过提供助手命令解决数据解析问题,这些命令可以通过应用常见操作来从文本中提取关键字,如类似下面的方法。

(1)grep或findstr为命令行进行匹配正则表达式查找。

(2)sed允许用户编辑文件流,如替换特定字符串。

(3)awk将文本作为表格处理,通过特定的分隔符(如逗号或制表符tab)分割单元格。

基于对象的管道

PowerShell中有类流水线操作,它要求任何命令cmdlet以一个满足要求且标准接口的.NET对象出现。建立在.NET之上的特性允许Shell传递.NET对象,而不是文本。这样不再需要生成文本,不必从其他程序中传递文本。管道不再是文本流,而是各种对象的集合,这些对象可以很容易地向外界展现其包含的任何属性、方法等信息。获取命名属性时,不再需要从逗号分隔的3列表格中获取值。

在处理对象时,需要根据一定的标准来过滤集合得到对象属性的子集,甚至添加自己的属性。大多数情况下,需要像数据库表那样处理对象集合,以查询感兴趣的数据。PowerShell引入如下cmdlet来完成这些任务。

(1)ForEach-Object:简写为“foreach”,允许操作管道中的任意项,结果是包含所有操作返回值的集合。

例5-1 将集合中所有的元素与2相乘并返回一个新的集合:

PS C:\> 1,2,3| ForEach-Object{$_ *2}
2
4
6

(2)Where-Object:与where及?相互为别名,过滤集合并返回符合条件的项。命名空间来自于数据库中SQL字句,用特定条件来过滤数据表内容。

例5-2 获取所有以“Error”开头的字符串:

PS C:\> “Error 4”,”Hello world”,”PowerShell”,”Error 7”|Where-Object{$_ -like “Error*”}
Error4
Error7

上述两个操作是操作对象的直观实例,并不一定通过基于文本的命令格式化数字或字符串,因为输入已经被Shell解析为对象集合。

另外PowerShell还提供如下附加的cmdlet使得操作更加简便。

(1)Select-Object:简写为“select”,用于创建包含原始对象属性子集的对象集合,这个命名空间也来自SQL语言,SELECT操作用来定义将表中的哪些列取回。

下例获取WORD进程的属性:

PS C:\>Get-Process winword | Select-Object ProcessName,WS

ProcessName                                                              WS
---------------                                                              --
WINWORD                                                              35413951

(2)Sort-Object:简写为“sort”,有别于基础排序的常见操作,可以按照一个或多个属性排序集合并支持高级的选项。

例5-3 按照法语的习惯比较字符串进行比较

PS C:\> "cote", "côte", "coté", "côté" | Sort-Object -Culture "fr-FR"
cote
côte
coté
côté

(3)Tee-Object:简写为“tee”,在把集合传递给下一个命令之前保存当前管道到文件或变量。可以在管道执行的特定阶段保存集合,并在后面的命令中使用这个集合。

例5-4 将原始未排序的集合在排序之前保存在$unsort变量中:

PS C:\> 3,2,1 | Tee-Object –variable unsorted | Sort-Object
1
2
3
PS C:\>$unsorted
3
2
1

(4)Group-Object:简写为“group”,把对象集合按照属性值分为多个分组。

例5-5 将一些文件按照后缀分类:

PS C:\> dir | Group-Object –property Extension
Count  Name                                    Group
------   ------                                    -------
     1    .log                                    {install.log}
     4    .txt                                    {test.txt,text2.txt,text3.txt,test4.txt}

(5)Measure-Object:计算集合的统计值,提供一个简便的方法来获取最小值、最大值及平均值属性。

例5-6 快速统计数组值:

PS C:\>1,2,3 | Measure-Object –Sum –Max –Min –Average

Count      : 3
Average   : 2
Sum        : 6
Maximum   : 3
Minimum   : 1
Property   :

(6)Compare-Object:简写为“diff”,用于比较两个对象或集合并报告其不同,如:

PS C:\>diff(1,2,3)(1,2,4)

InputObject SideIndicator
----------- -------------
4 =>
3 <=

操作对象时需要相应的编程语言提供操作、创建及修改对象的命令,PowerShell允许将这些命令包含在脚本块中。脚本块本身也是对象,类似过程和函数,其中包含一个或多个带不同参数并可多次执行的命令。

使用Foreach-Object处理集合

PowerShell提供了用操作符多次操作集合的语言特性,ForEach-Object命令通过对每个对象和集合运用相同的操作而使重复代码最小化,并返回新的集合。

最简单的情况下,ForEach-Object至少需要两个输入,一是集合,通常由上一个命令传送;二是操作,操作以脚本块的形式来表现。

PS C:\> dir
Directory: Microsoft.PowerShell.Core\FileSystem::C:\l
Mode   LastWriteTime   Length    Name
---- ------------- ------ ----
-a--- 1/17/2009  11:33 PM  4   test.txt
-a--- 1/17/2007  11:34 PM  12  test2.txt
-a--- 1/17/2007  11:33 PM  4   test3.txt
-a--- 1/17/2007  11:34 PM  14  test4.txt
PS C:\> dir | ForEach-Object { $_.Length }
4
12
4
14

需要注意的是$_是个很特殊的变量,它保存一个到当前对象的索引,如同在JavaScript中的this。ForEach-Object将循环作用于所有的对象,用$_变量来代替当前的对象,并调用脚本块。

通常当循环操作开始之前需要进行初始化,操作所有的对象后需要给出结果。开发人员可以通过传递两个脚本块作为begin和end。下例遍历当前目录所有的文件,这里用一个累加计数器置零为开始。在循环体中增加文件数目,并且最后显示和:

PS C:\> dir | ForEach-Object –begin{$sum=0} –process `
        {$sum +=$_.Length} –end {echo “Total size of files: $sum bytes.”}

Total size of files: 34 bytes.

process参数是默认的,尽管很多时候并不是必须的。但是使用begin和end脚本块时,包含process将会使得命令更加的易读。

使用Where-Object过滤集合

内置的cmdlet提供了很多功能,以Get-Process为例,它将会返回所有进程并允许通过使用进程名来过滤未发生的进程。为了能获取所有占用内存超过20 MB的进程:可以将Get-Process的输出作为Where-Object的输入。where需要的参数包括集合及脚本块。脚本块将会在所有的对象上执行,只有返回值为$true的对象将会被包含在结果集合当中。具体的实例如下:

PS C:\> Get-Process | Where-Object {$_.WS –ge 20MB}

Handles  NPM(K)  PM(K)  WS(K)  VM(M)  CPU(s)  Id  ProcessName
------- ------ ----- ----- ----- ------ -- -----------
156      8         99296  58544   191   134.00  2808  dwm
847      39        59112  58472   277   99.48   2852  explorer
431      11        65792  53012   157            1004  svchost
328      17        21780  48908   297   23.19   2600  WINWORD

WS属性的别名为“WorkingSet”,返回进程在当前所占用物理内存的量,-ge是greater-than-or-equal比较操作符。

 

新增或删除指定对象的属性

Select-Object对应于数据库中的select,用于定义在检索中返回的列。Where-Object允许在对象中选择属性的子集,也可以选择表格的列。同样Select-Objectcmdlet允许从结果对象集中移除属性,甚至在其中新增新的属性。

在最简单的情况下,Select-Object要求一个被返回的属性列表。下例只提取当前文件夹中文件的文件名和最后访问时间:

PS C:\> dir | Select-Object Name,LastAccessTime

Name                                         LastAccessTime
------                                         -----------------
test.txt                                       1/17/2009 8:55:12 AM
test2.txt                                      1/17/2009 8:55:12 AM
test3.txt                                      1/17/2009 8:55:12 AM
test4.txt                                      1/17/2009 8:55:12 AM

为对象增加属性也是类似的方法,它遵循这样的规定,即定义一个可计算的属性并传递包含属性名的字典及可计算属性的表达式。下例为文件列表增加LastAccessWeekDay属性:

PS C:\> dir | Select-Object Name,
@{Name=”LastAccessWeekDay”;Expression=
{$_.LastAccessTime.DayOfWeek}}

Name                                         LastAccessWeekDa
------                                         -----------------
test.txt                                       Saturday
test2.txt                                      Saturday
test3.txt                                      Saturday
test4.txt                                      Saturday

需要注意的是Select-Object也可以使用$_来替代当前的对象。

值得一提的是Select-Object具有一种很方便的用法,可以用传递first和last参数选择最前面或是最后面的N列。下例使用这种方法来选择当前文件夹中前面或后面的两个文件:

PS C:\> dir *.txt | select –first 2

Directory: Microsoft.PowerShell.Core\FileSystem::C:\PowerShell
Mode     LastWriteTime        Length      Name
----       -------------           ------       ----
-a---      1/17/2009 11:33 PM   4           other.txt
-a---      1/17/2009 11:34 PM   7           test.txt
PS C:\> dir *.txt | select –last 2

Directory: Microsoft.PowerShell.Core\FileSystem::C:\PowerShell
Mode     LastWriteTime        Length      Name
----       -------------           ------       ----
-a---      1/17/2009 11:36 PM   12        test4.txt
-a---      1/17/2009 11:35 PM   14        test3.txt

如果对象是被排序的,则获取前面或后面几个对象非常有用。这将允许开发人员很容易获取集合中感兴趣的对象,如获取最大的5个文件,或消耗内存最多的3个进程等。

排序集合

如果要根据一些属性值来排序数据,则在PowerShell中通过Sort-Object实现。用户要做的只是为cmdlet传递一个属性列表,Sort-Object自动将集合排序。下例按照占用的内存大小排序进程列表:

PS C:\> Get-Process | Sort-Object WS

Handles   NPM(K)   PM(K)   WS(K)   VM(M)   CPU(s)   Id   ProcessName
-------     ------     -----     -----     -----     ------    --   -----------
0          0         0        16       0                  0      Idle
28         1         248     524      4                  364   smss
713        0         0       532      4                   4      System
...
347        18        22524  49612   312      34.55    2600  WINWORD
431        11        66084  53300   157                1004  svchost
848        39        59116  58532   277      101.20   2852  explorer

如果要某个降序排列属性,则传递-descending参数即可,如:

PS C:\> Get-Process | Sort-Object WS –descending

Handles   NPM(K)   PM(K)   WS(K)   VM(M)   CPU(s)   Id   ProcessName
-------     ------     -----     -----     -----     ------    --   -----------
848        39        59116  58532   277      101.20   2852  explorer
431        11        66084  53300   157                1004  svchost
347        18        22524  49612   312      34.55    2600  WINWORD
...
713        0         0       532      4                   4     System
28         1         248     524      4                  364   smss
0          0         0        16       0                  0      Idle

管道树

建立长命令管道是表示复杂操的有效方法,有时需要获取在管道命令中生成的对象并保存以备后面使用。更常见的情况是在操作对象之前需要存储一系列的对象,如要终止一系列进程,但需要在结束进程之前将进程名写入日志文件中。通常会使用Get-Process和Stop-Process来获取和终止进程:

PS C:\>Get-Process notepad | Stop-Process
PS C:\>

为了把Get-Process获取到的对象写入文件中,需要在管道中使用Tee-Object cmdlet:

PS C:\>Get-Process notepad | Tee-Object –file kill.log |Stop-Process
PS C:\>type kill.log
Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     50       2     1200       4076    54     1.48   2772 notepad
     48       2     1200       4016    54     0.77   2880 notepad

有时并不需要写入磁盘,而将变量作为中间媒介来存储,只需要将-file参数换成-variable参数:

PS C:\>Get-Process notepad | Tee-Object –variable KilledProcesses |Stop-Process
PS C:\>$KilledProcesses

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     48       2     1204       4016    54     0.23   3876 notepad
     48       2     1200       4052    54     0.48   3884 notepad
PS C:\> $KilledProcesses |select ProcessName,WS,HasExited

ProcessName                    WS(K)                              HasExited
-----------                        -------                              ---------
notepad                          4016                               True
notepad                          4052                               True

需要注意的是不必在Tee-Object –variable中的变量前面加美元符($)的前缀,只需要传递变量名。cmdlet定义变量并设置其值,即在使用变量之前需要定义。这里只是传递变量名,并不需要用户定义度量,而是PowerShell外壳自动完成。

分组对象

很多处理对象的过程中需要将对换分成多个分类,PowerShell使用Group-Object的cmdlet使分类操作变得简单。最常见的情况是根据某项属性值来分类集合,下例根据系统服务的状态将其分类:

PS C:\> Get-Service | Group-Object Status

Count      Name                      Group
-----       ----                         -----
50         Running                     
{System.ServiceProcess.ServiceController, 
System.ServiceProcess.ServiceController, S...
60         Stopped                     
{System.ServiceProcess.ServiceController, 
System.ServiceProcess.ServiceController, S...

结果是GroupInfo对象的集合。可以通过访问GroupInfo的Group属性可以逐个获取和查询属于分类的实际对象,如:

PS C:\> (Get-Service | Group-Object Status)[1].Group

Status   Name               DisplayName
------   ----               -----------
Stopped  ALG                Application Layer Gateway Service
Stopped  Appinfo            Application Information
Stopped  AppMgmt            Application Management
Stopped  AudioEndpointBu... Windows Audio Endpoint Builder
Stopped  Audiosrv           Windows Audio
Stopped  Browser            Computer Browser
……

前面的实例用来展示所有状态为“Stopped”的系统服务分类。如果要获取所有启动的系统服务,只需要将索引值1换为0即可获取启动的服务组。需要强调的是前例中的圆括号结束管道命令并将结果作为包含集合的常规变量。

采集对象统计信息

通常,如果需要收集大量对象的统计信息,则需要逐个遍历所有对象,为此使用Measure-Object这个cmdlet。使用这个cmdlet能用最少的代码量来遍历整个集合并计算属性值中最常见的统计信息,如计数、求和、最小值、最大值及平均值。下例收集与所有文件大小相关的信息:

PS C:\>dir –r | Measure-Object –property Length –min –max –average -sum
Count    : 39963
Average  : 164494.456822561
Sum      : 6573691978
Maximum  : 136491745
Minimum  : 0
Property : Length

该例使用dir命令的-r参数来实现递归遍历所有的文件,查询每个文件的长度属性并计算出统计信息。有趣的是Measure-Object完全可以通过为ForEach-Object命令设置合理的参数来替代它,为了方便,一般情况下最好使用Measure-Object,而将ForEach-Object留在更复杂的情况下使用。

检测对象间的变化和不同

所有的脚本语言都会遇上比较两个对象或集合的任务,即是否有相应的对象被增加或删除。PowerShell的Compare-Object会遍历两个对象,并报告之间的不同。传统的diff工具基于文本,而Compare-Object面向对象。下例用其来检测当前的文件夹和昨天备份之间的不同:

PS C:\> diff (dir .\PowerShellBackup) (dir .\PowerShell)

InputObject                                                 SideIndicator
-----------                                                 -------------
kill.log                                                    =>

单侧指示器(side Indicator)=>符号代表两个文件夹中不同的对象kill.log对象是存在于右侧的集合中。与此对应,存在<=符号表示两个集合中存在差异的对象仅在左侧存在。

Compare-Object帮助用户创建强大的脚本,如可以将其输出导入到ForEach-Object而创建简单的备份更新脚本,将新创建的文件从PowerShell文件夹中复制到PowerShellBackup文件夹中。从一个目录中复制所有文件到另外一个目录是个很危险的操作,为了确保没有因覆盖而丢失重要文件,下例传递了-confirm参数给copy命令:

PS C:\> diff (dir PowerShellBackup) (dir PowerShell) | `
>> where {$_.SideIndicator -eq "=>"} | `
>> ForEach {copy -confirm "PowerShell\$($_.InputObject)" `
>> "PowerShellBackup"}
>> 
Confirm
Are you sure you want to perform this action?
Performing operation "Copy File" on Target "Item: 
C:\PowerShell\kill.log Destination: 
C:\PowerShellBackup\kill.log".
[Y] Yes  [A] Yes to All  [N] No  
[L] No to All  [S] Suspend  
[?] Help (default is "Y"): y

需要强调的是这里首先过滤了diff的结果,仅保留了单侧指示器为=>的对象,最后使用了$_.InputObject的值来代替要复制的文件路径。

管道对象和功能编程

对象管道是PowerShell中改变整个Shell脚本编程范例的特性,因为管道允许用户直接作用于对象,不同命令之间可以很好结合而使得PowerShell比其他Shell的操作更加简易。

传递对象不是唯一最重要的。早期非官方版本的PowerShell的前身被称为“Monad”(即单孢体和单细胞生物),这个叫法源于名为“单元论”的哲学。单元论的核心思想是世界上所有的物质由不可再分的简单单元组成,任何复杂且有价值的物质由简单部分组成。PowerShell架构的指导原则是任何复杂的操作均由简单的命令组合而成。

对象管道在PowerShell中不仅仅指管道中的对象,更应该引起读者注意的是前面的实例中传递给cmdlet的都是脚本块对象,将脚本块应用于对象使得管道的功能更加强大。将可执行代码块作用于其他代码的编程风格拥有更多的功能性编程范例,这种方法能在运行时创建和传递对象。PowerShell中的ForEach-Object和Where-Object一类的cmdlet是实现这一切很强有力的工具。这类cmdlet的基本原理是在集合上循环执行操作并创建另外的集合,其值来自用户针对目标集合定义的操作。如果在循环中没有定义确切功能的脚本块,则需要手工为每个集合的遍历写代码处理。

总 结

最有用的PowerShell脚本来自于命令行,如何得到远程主机上运行进程的列表?如何过滤出不需要的进程?如何将结果根据内存的使用情况排序?如何获取占用内存最多的10个进程?如何杀死效率最低的几个进程?这些实例中的大多数的查询可以简单地使用管道实现。如果能够拆分操作,则更有利于提高其可读性,当然在操作的复杂性和代码的可读性之间寻找一个可以兼顾二者的结合点是最好的。

作为建议,笔者推荐读者学习本文涉及到的命令的别名,用其可以更便捷地在命令行尝试命令操作。作为脚本语言学习的好方法之一,多尝试脚本内部的实现机制可以更快且更好地理解操作。

赛迪网地址:http://news.ccidnet.com/art/32857/20100612/2085433_5.html

作者: 付海军

版权:本文版权归作者所有

转载:欢迎转载,为了保存作者的创作热情,请按要求【转载】,谢谢

要求:未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

个人网站: http://txj.shell.tor.hu/


发表回复