代码改变世界

Windows PowerShell 2.0语言之函数和过滤器

2010-11-23 01:52 by @天行健中国元素, ... 阅读, ... 评论, 收藏, 编辑

本文讨论了PowerShell提供的业务控制机制函数和过滤器,函数是用来扩展内置shell功能最常见的方法,尽快掌握函数非常重要,会便于对业务进行封装。“分而治之”的策略在PowerShell中重要而有效,用户能够将划分后的子问题使用函数来解决,最后可以通过将多个函数联合起来的方法解决高难度的问题。用户需要逐渐熟悉和习惯将复杂的问题划分成为多个较为简单的小问题,单个小问题划分到相对独立而易于处理的程度后单独处理,这样解决问题的成本会降低。划分问题的模式不是固定的,划分的标准一般是尽量降低模块间的耦合度,不可避免的耦合关系可以通过函数间变量的传递实现数据的交换。这样单个函数的功能较为独立,便于代码的重复使用,也便于出现错误时的调试和排错。

1 定义函数

函数的可执行代码块类似脚本块,不同之处在于脚本块是匿名的,在执行之前会被指定给一个变量;函数在创建时获取名称,函数名会立即生效并可以执行。定义函数使用function关键字,其格式如下:

function <name>(<parameter list>)
{
  <function body>
}

函数名应该以字母开头,包含字母和数字的组合,以及连字符(-)和下画线(_)。下例使用函数输出字符:

PS C:\>function Say-Hello()
>>  {
>>    Write-Host “Hello World from a function in PowerShell.”
>>   }
>>
PS C:\> Say-Hello
PS C:\> Hello World from a function in PowerShell.

参数列表中的圆括号为可选,不接收参数时可以省略,如:

PS C:\>function Say-Hello
>>  {
>>    Write-Host “Hello World from a function in PowerShell.”
>>   }
>>
PS C:\> Say-Hello
PS C:\> Hello World from a function in PowerShell.

定义函数的过程中会在当前作用域中自动创建同名对象,所以在调用函数时使用函数名即可,如:

PS C:\> Say-Hello
PS C:\> Hello World from a function in PowerShell.

1.1 函数体

在上一节的实例中使用了Verb-Noun的形式命名函数,这种动词-名词的形式是PowerShell中为了清楚和易读而约定俗成的命名规则。函数的执行动作类似于内建的cmdlet,所以命名形式也是类似的。

函数注册在全局命令命名空间中,它的调用和其他cmdlet中的调用相同。可以使用Get-Command获取之前定义函数的引用。下例获取之前定义的函数:

PS C:\> $helloFunction = Get-Command Say-Hello -type Function
PS C:\> $helloFunction.GetType().FullName
System.Management.Automation.FunctionInfo

这里获取的是这个函数是个System.Management.Automation.FunctionInfo对象,其中包含的有用属性如下;

PS C:\> $helloFunction | gm -type Property


   TypeName: System.Management.Automation.FunctionInfo

Name                MemberType Definition
----                ---------- ----------
CmdletBinding       Property   System.Boolean CmdletBinding {get;}
CommandType         Property   System.Management.Automation.
CommandTypes CommandType {get;}
DefaultParameterSet Property   System.String DefaultParameterSet {get;}
Definition          Property   System.String Definition {get;}
Description         Property   System.String Description {get;set;}
Module              Property   System.Management.Automation.
PSModuleInfo Module {get;}
ModuleName          Property   System.String ModuleName {get;}
Name                Property   System.String Name {get;}
Options             Property   System.Management.Automation.
ScopedItemOptions Options {get;set;}
Parameters          Property   System.Collections.Generic.
Dictionary`2[[System.String, mscorlib, Version=2.0.0.0, Cu...
ParameterSets       Property   System.Collections.
ObjectModel.ReadOnlyCollection`1[[System.Management.
Automation.Com...
ScriptBlock         Property   System.Management.
Automation.ScriptBlock ScriptBlock {get;}
Visibility          Property   System.Management.
Automation.SessionStateEntryVisibility Visibility 
{get;set;}

CommandType属性已经是Function,Name属性是Say-Hello。ScriptBlock属性可以在不查看文档的情况下显示在当前函数中包含的功能。下例获取当前函数的信息:

PS C:\> $helloFunction.ScriptBlock
   Write-Host " Hello World from a function in PowerShell."
PS C:\> &$helloFunction.ScriptBlock
Hello World from a function in PowerShell.

上例中使用了调用操作符(&),可以在执行函数之前检查函数脚本块的内容。函数对象的Definition属性以字符串的形式包含函数代码。下例演示如何使用这个属性:

PS C:\> $helloFunction.Definition.GetType().FullName
System.String
PS C:\> $helloFunction.Definition
   Write-Host "Hello World from a function in PowerShell."

Definition属性包含所有有效的可操作代码,甚至可以通过Invoke-Expression执行函数,如:

PS C:\> Invoke-Expression $helloFunction.Definition
Hello World from a function in PowerShell.

看起来上例的执行方式对于函数来说并没有优势,但在某些场合中确实很方便。

1.2 函数参数

为了让函数接收参数,可以在函数定义中指定参数清单。下例接收两个参数并将其和输出到控制台:

PS C:\> function Write-Sum($first,$second)
>> {
>>   $sum = $first + $second
>>   Write-Host "Sum:$sum"
>> }
>>
PS C:\> Write-Sum 6 9
Sum:15

【提示】

大多数编程语言需要用户在括号中提供参数,并用逗号作为分隔符,如Write-Sum(6,9)。在PowerShell中调用函数的语法与调用外部程序和cmdlet相同,并以空格分隔参数列表,或者对命名参数使用基于开关的语法,如Write-Sum –first 6 –second 9。

在PowerShell中用圆括号和逗号的形式提供参数对象方法,如$myObject.DoSometing(4,5)。尽管二者相似,但是却有很大差别。

在函数中也可以使用前面章节中脚本块参数使用的特性,所有在脚本块中对参数执行的操作在函数参数操作中均可使用。可以指定参数的类型,程序解释器会自动转换传递给函数的参数类型。下例在函数中强制类型转换为整形参数:

PS C:\> function Write-Sum([int]$first, [int]$second)
>> {
>>  $sum = $first + $second
>>  Write-Host "Sum: $sum"
>> }
>>
PS C:\> Write-Sum 5 "9"
Sum: 14

如果没有传递某个参数,可以使用程序提供的默认值。下例格式化日期并输出到控制台:

PS C:\> function Format-Date($date,  $format = "{0:M/d/yyyy}")
>> {
>> Write-Host ($format -f $date)
>> }
>>
PS C:\> Format-Date [datetime]::Today
[datetime]::Today
PS C:\> Format-Date ([datetime]::Today)
2/4/2009

默认日期格式是“月/日/年”。如果要使用完整格式的日期输出,可以覆盖上例中的格式参数:

PS C:\> Format-Date ([datetime]::Today) "{0:f}"
2009年2月4日 0:00

可以强制通过抛出异常的形式使用默认值,由函数调用者传递一个参数。下例的输入日期强制要求参数:

PS C:\> function Format-Date($date = $(throw "Date required"),`
>>                       $format = "{0:M/d/yyyy}")
>> {
>>         Write-Host ($format -f $date)
>> }
>>
PS C:\> Format-Date
Date required
At line:1 char:37
+ function Format-Date($date = $(throw <<<<  "Date required"),`
    + CategoryInfo          : OperationStopped: (Date required:String) [], RuntimeException
    + FullyQualifiedErrorId : Date required

可以在定义函数时跳过参数声明,而在函数体中声明。函数体本身以脚本块的形式存在,可以包含param语句。下例中的Format-Date函数在脚本块中声明变量:

PS C:\PowerShell> function Format-Date
>> {
>> param ($date = $(throw "Date required"), $format = "{0:M/d/yyyy}")
>> Write-Host ($format -f $date)
>> }
>>
PS C:\PowerShell> Format-Date (Get-Item C:\autoexec.bat).LastAccessTime
3-26-2009
PS C:\PowerShell>

1.3 通过引用传递参数

用参数传递数据给函数只是单方面的,不能通过为参数赋值而改变参数的原有值。为参数赋新值时,只是简单地在本地创建了同名变量,如:

PS C:\> $name = "LiMing"
PS C:\> function AssignValueToParam($name)
>> {
>> $name = "WangLei"
>> Write-Host "inside function: $name"
>> }
>>
PS C:\> AssignValueToParam $name
inside function: WangLei
PS C:\> Write-Host "outside function: $name"
outside function: LiMing

新创建的变量会在当前作用域中覆盖之前传递的参数,原参数值不变,为改变传递到函数中的参数值,可以使用Get-Variable和Set-Variable在复杂的作用域间更改变量值。下例创建的函数用来交换两个变量值:

PS C:\> function SwapValue($first, $second)
>> {
>>     $firstValue = Get-Variable $first -scope 1 -valueOnly
>>     $secondValue = Get-Variable $second -scope 1 -valueOnly
>>     Set-Variable $first $secondValue -scope 1
>>     Set-Variable $second $firstValue -scope 1
>> }
>>
PS C:\> $a = 5
PS C:\> $b = 8
PS C:\> SwapValue "a" "b"
PS C:\> $a
8
PS C:\> $b
5

上例中虽然达到了预定的功能,但是代码当中是存在局限性的。交换值的变量定义在父作用域中,如果要交换父作用域中的两个变量,则无效。

在PowerShell中可以通过引用PSReference对象的变量,这样可以使用[ref]的类型标记来将任何变量转换为PSReference对象,从而修改Value属性的对象,设置引用对象的value属性将修改原始的变量。下例使用引用方式重写上面的SwapValue函数:

PS C:\> function SwapValue([ref] $first,[ref] $second)
>> {
>> $tmp = $first.Value
>> $first.Value = $second.Value
>> $second.Value = $tmp
>> }
>>
PS C:\> $a = 6
PS C:\> $b = 3
PS C:\> SwapValue ([ref] $a) ([ref]$b)
PS C:\> $a
3
PS C:\> $b
6

在引用变量的过程中并没有限定搜索的作用域,这样代码的使用范围更广。代码中调用SwapValue时使用圆括号使得变量的[ref]强制类型转换在将对象传递给函数之前完成,从而保证了对象类型的有效。

1.4 返回值

可以通过从函数中输出未被任何操作销毁的对象来返回值,输出多个对象将会返回包含多个对象的集合。下面的函数中在循环中输出多个对象:

PS C:\> function Generate-NumberTo($max)
>> {
>>     for($i=0; $i -lt $max; $i++)
>>     {
>>         $i
>>     }
>> }
>>
PS C:\> Generate-NumberTo 4
0
1
2
3

可以使用return语句在退出函数的同时返回值,下例中的函数在集合中搜索对象:

PS C:\> function Find-Object($target, $haystack)
>> {
>>     foreach ($item in $haystack)
>>     {
>>         if($item -eq $target)
>>         {
>>             return $item
>>         }
>>     }
>> }
>>
PS C:\> Find-Object 5 (2..19)
5
PS C:\> Find-Object 5 (7..19)
PS C:\>

需要强调的是输出对象与将对象写到控制台之间的区别,前者是将对象传递到输出流中的动作。可以用变量保存cmdlet返回的对象,或调用命令将对象写到控制台。下例把对象写到控制台:

PS C:\> function Get-TextFiles()
>> {
>>     dir *.txt
>> }
>>
PS C:\> Get-TextFiles

    Directory: C:\

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---          2009/1/3     14:31         12 digit.txt
-a---          2009/1/3      6:06      12100 largetext.txt
-a---          2009/1/3      6:07       6949 smalltext.txt
-ar--          2009/1/3      6:27         13 test.txt
-a---          2009/1/3      6:28          9 test2.txt

输出对象时,PowerShell会传递所有输出对象到管道中的下一个命令。

将对象写到控制台是把对象转换为文本后写到控制台,不会传递值到管道中,它通过Wrist-Host这个cmdlet实现。输出对象和将对象写到控制台看起来相似是因为每个管道会在最后隐式调用Out-Default,这个cmdlet处理所有输入并将其写到控制台。

1.5 作用域规则

函数会创建新的本地作用域,作用域继承变量的可见性,函数可以读取所有其作用域中及其父作用域中定义的变量。

对于命名对象,函数遵循类似变量的作用域规则。可以在任何作用域中声明函数,函数会在其作用域及其子作用域有效,这意味着可以嵌套函数。下例是嵌套函数:

PS C:\> function OuterFunction()
>> {
>>         function InnerFunction()
>>         {
>>             Write-Host "Printed by InnerFunction!"
>>         }
>>         InnerFunction
>> }
>>
PS C:\> OuterFunction
Printed by InnerFunction!

InnerFunction函数对于OuterFunction函数内部的代码可见,这样能有效地保护函数内部特定的逻辑不被外部访问。如果强行在OuterFunction函数外部作用域调用InnerFunction函数,将会报错,如:

PS C:\> InnerFunction
The term 'InnerFunction' is not recognized as a cmdlet, 
function, operable program, or script file. 
Verify the term and
 try again.
At line:1 char:14
+ InnerFunction <<<<
    + CategoryInfo          : ObjectNotFound: 
(InnerFunction:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

类似变量赋值,在子作用域中的函数会覆盖父作用域中的同名函数,下例在当前作用域中覆盖父作用域中的同名函数:

PS C:\> function OuterFunction()
>> {
>>        function Do-Something()
>>         {
>>             Write-Host "Original Do-Something function"
>>          }
>>        function InnerFunction()
>>         {
>>             function Do-Something()
>>             {
>>                 Write-Host "Overriden Do-Something function"
>>             }
>>             Do-Something
>>         }
>>         InnerFunction
>>         Do-Something
>> }
>>
PS C:\> OuterFunction
Overriden Do-Something
Original Do-Something function

OuterFunction函数定义了Do-Something函数,其中嵌套了InnerFunction函数。该函数中定义了与父作用域中的Do-Something函数,覆盖了父作用域中的同名函数。这种覆盖只在InnerFunction函数作用域中有效,一旦退出这个作用域,Do-Something返回原值。

与Get-Variable不同,Get-Command不支持作用域参数。一旦在当前作用域中覆盖,则无法从父作用域中得到函数。

另外,可以在函数名前使用global、script、local或private作用域标识符。下例在global和local作用域中声明同名函数,然后用命名空间前缀区别二者:

PS C:\> function global:Do-Something()
>> {
>>     Write-Host "Global Do-Something"
>> }
>> function InnerScope()
>> {
>>      function local:Do-Something()
>>      {
>>         Write-Host "Local Do-Something"
>>      }
>>     local:Do-Something
>>     global:Do-Something
>> }
>>
PS C:\> InnerScope
Local Do-Something
Global Do-Something

【提示】

用于变量的作用域命名空间前缀对函数有效,如果要调用全局变量,则必须使用$global:myVariable语法。而函数的作用域命名空间不使用美元符,引用全局函数的语法格式是global:myFunction。

当在不同作用域级别重载本地函数后,只要在当前作用域的函数被重载前获得函数的引用,就可以从父作用域调用该函数,如下例:

PS C:\> function OuterFunction()
>> {
>>     function Do-Something()
>>     {
>>         Write-Host "Original Do-Something"
>>     }
>>     function InnerFunction()
>>     {
>>         $original = Get-Command Do-Something -type Function
>>         function Do-Something()
>>         {
>>             Write-Host "Override Do-Something"
>>             Write-Host "Calll original function......"
>>             &$original
>>         }
>>         Do-Something
>>     }
>>     InnerFunction
>> }
>>
PS C:\> OuterFunction
Override Do-Something
Calll original function......
Original Do-Something

2 过滤器

在函数中接收管道输入需要在函数中定义begin、process和end段,当对象传递给函数时这些段就会执行。3个段中只有process是必须的,它作用于传递的每个对象,通常在其中可以使用$_这个特殊变量引用当前对象。begin和end段分别在管道执行前后执行。下例为定义接收文件的管道并计算文件总大小的函数:

PS C:\> function Get-FileSize
>> {
>>     begin
>>     {
>>         $total = 0
>>     }
>>     process
>>      {
>>         Write-Host "processing: $($_.name)"
>>         $total += $_.Length
>>     }
>>     end
>>     {
>>         return $total
>>     }
>> }
>>
PS C:\> dir *.txt | Get-FileSize
processing: digit.txt
processing: largetext.txt
processing: smalltext.txt
processing: test.txt
processing: test2.txt
19083

很多情况下,在处理管道输入时仅需要定义process段,如过滤进程的集合并只显示启动不超过5分钟的进程等。为此需要定义Get-RecentlyStarted函数,在process段中返回StartTime属性值小于5分钟的管道对象:

PS C:\> function Get-RecentlyStarted
>> {
>>     process
>>     {
>>         $start = $_.StartTime
>>         if ($start -ne $null)
>>         {
>>             $now = [datetime]::Now
>>             $diff = $now - $start
>>             if ($diff.TotalMinutes -lt 5)
>>             {
>>                 return $_
>>             }
>>         }
>>     }
>> }
>>
PS C:\> Get-Process | Get-RecentlyStarted

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    291      10    10240      17836   101    19.38   3324 iexplore
     80       4     2740       7252    67     2.73   3400 mspaint
     45       2     1192       3952    54     1.09   1892 notepad
    234       8     3336       5072    51     2.28   2440 svchost

PowerShell不允许函数同时包含在调用时立即执行的任意语句,以及与管道相关的begin、process和end段。PowerShell常规函数和管道处理函数完全不同,管道函数模式经常用于过滤集合。为了便于定义过滤函数,可以引用fliter关键字。下例使用过滤器重新定义上一个实例:

PS C:\> filter Get-RecentlyStarted
>> {
>>     $start = $_.StartTime
>>     if ($start -ne $null)
>>     {
>>         $now = [datetime]::Now
>>         $diff = $now - $Start
>>         if ($diff.TotalMinutes -lt 5)
>>         {
>>             return $_
>>         }
>>     }
>> }
>>
PS C:\> Get-Process |  Get-RecentlyStarted

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    189       8     5452      11076    86     2.63   3368 iexplore
     80       4     2740       7228    67     1.58   2408 mspaint
    229       8     3308       5076    51     2.28   2440 svchost
     82       4     2920       8192    71     2.84   2892 wordpad

使用过滤器替代函数会清除嵌套的复杂度而使代码更为简洁且易读,可以使用Get-Command查看Get-RecentlyStarted的详细信息。PowerShell规定过滤器是函数的特例,因为使用Function和Filter类型查找命令都会返回前面的过滤器,如:

PS C:\> Get-Command Get-RecentlyStarted -type Function

CommandType     Name                                                Definition
-----------     ----                                                ----------
Function        Get-RecentlyStarted                                 ...

PS C:\> Get-Command Get-RecentlyStarted -type Filter

CommandType     Name                                                Definition
-----------     ----                                                ----------
Function        Get-RecentlyStarted                                 ...

上例中的Definition属性被省略,可以用下面的方法获取:

PS C:\> (Get-Command Get-RecentlyStarted -type Filter).ScriptBlock

    $start = $_.StartTime
    if ($start -ne $null)
    {
        $now = [datetime]::Now
        $diff = $now - $Start
        if ($diff.TotalMinutes -lt 5)
        {
            return $_
        }
    }

与管道配合工作的函数与过滤器看起来相似,尽管函数的process块语义等同于过滤器,但是函数在内部以FunctionInfo对象存在;而过滤器以FilterInfo对象存在。

下例中的函数会过滤掉管道中所有的奇数,只保留偶数:

PS C:\> function Even-Function
>> {
>>     process
>>     {
>>         if ($_ % 2 -eq 0)
>>         {
>>             $_
>>         }
>>     }
>> }
>>

下例使用filter实现同样的逻辑:

PS C:\> filter Even-Filter
>> {
>>     if ($_ %2 -eq 0)
>>     {
>>         $_
>>     }
>> }
>>

尽管Even-Function和Even-Filter在查询函数类型的命令时均返回,但是对于CommandType属性,二者分别返回Function和Filter,如:

PS C:\> Get-Command Even-Function -CommandType Function
CommandType    Name                        Definition
-----------         ----                          ----------
Function         Event-Function                process {...
PS C:\> Get-Command Even-Filter -CommandType Function

CommandType    Name                        Definition
-----------         ----                          ----------
Filter              Even-Filter                   ...
另外一种在运行时区别函数和过滤器的方法是检查内部ScriptBlock对象的IsFilter属性,如:
PS C:\> $function = (Get-Command Event-Function -CommandType Function)
PS C:\> $function.ScriptBlock.IsFilter
False
PS C:\> $function = (Get-Command Even-Filter -CommandType Function)
PS C:\> $function.ScriptBlock.IsFilter
True

需要再次重申,这两种函数对象以完全相同的方式工作。

赛迪网地址:http://news.ccidnet.com/art/32857/20100617/2089211_1.html

作者: 付海军
出处:http://fuhj02.cnblogs.com
版权:本文版权归作者和博客园共有
转载:欢迎转载,为了保存作者的创作热情,请按要求【转载】,谢谢
要求:未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
个人网站: http://txj.shell.tor.hu/