Windows PowerShell 2.0语言开发之脚本块


脚本块是重要的编程结构,是PowerShell重要的摘要和重用代码的机制,学习脚本块的最终目标是掌握各种重用代码的方法,如别名程序提供和脚本文件。这些技术都很重要,因为它们是逐步创建复杂脚本的基础。

定义脚本块

定义脚本块只需要把一些程序语句用花括号({})括起,它不会立即执行,取而代之的是建立和返回一个新的脚本块对象。下面是创建的第1个脚本块:

PS C:\> {Write-Host "This  sentence will not be print out."}
Write-Host "This  sentence will not be print out"
PS C:\>  Write-Host "This  sentence will be print out."
This  sentence will be print out

两个语句的主要区别在于花括号,它使脚本块中的Write-Host语句没有被执行,得到的是这些字符串组成的脚本块对象。如果不为其保存一个引用对象,这个语句没有意义。下例将一个建立的脚本块赋值给一个变量:

PS C:\> $HelloWorldBlock={Write-Host "Hello World from a Script Block"}
PS C:\>

查看脚本块变量的类型:

PS C:\> $HelloWorldBlock.GetType().FullName
System.Management.Automation.ScriptBlock

可以看到的脚本块对象是System.Manage.Automation的实例。作为标准的.NET类,脚本块也是PowerShell基础结构,因此可以如同.NET对象那样来处理它。下例获取脚本块的属性和方法:

PS C:\> $HelloWorldBlock | Get-Member

   TypeName: System.Management.Automation.ScriptBlock

Name                 MemberType Definition
----                 ---------- ----------
Equals               Method     System.Boolean Equals(Object obj)
GetHashCode          Method     System.Int32 GetHashCode()
GetNewClosure        Method     System.Management.Automation.
ScriptBlock GetNewClosure()
GetPowerShell        Method     System.Management.Automation.
PowerShell GetPowerShell(Params Object[] args)
GetSteppablePipeline Method     System.Management.Automation.
SteppablePipeline GetSteppablePipeline()
GetType              Method     System.Type GetType()
Invoke               Method     System.Collections.ObjectModel.
Collection`1[[System.Management.Automation.PSObject, ...
InvokeReturnAsIs  Method  System.
Object InvokeReturnAsIs(Params Object[] args)
ToString             Method     System.String ToString()
File                 Property   System.String File {get;}
IsFilter             Property   System.
Boolean IsFilter {get;set;}
Module      Property   System.Management.Automation.
PSModuleInfo Module {get;}
StartPosition  Property System.Management.Automation.
PSToken StartPosition {get;}

可以通过上面的属性清单了解PowerShell使用脚本块的方法。在执行时PowerShell会解析脚本代码创建脚本块对象,并调用对象的Invoke和InvokeReturnAsIs方法。

可以通过在脚本块名前缀之前添加引用操作符(&)引用脚本块,下例通过脚本块变量来调用脚本:

PS C:\> $HelloWorldBlock={Write-Host "Hello World from a Script Block"}
PS C:\> &$HelloWorldBlock
Hello World from a Script Block

引用操作符不仅可以和变量配合使用,也是一个表达式操作符,可以在脚本块定义时使用。如下面声明定义一个匿名脚本块并且执行:

PS C:\> &{Write-Host "Hello World from a Anonymous Script Block"}
Hello World from a Anonymous Script Block

脚本块是编译后的可以被传递和多次执行的对象,可以指定变量指向内存中的脚本块。下例说明如何多次执行脚本块,并通过不同的变量来访问它:

PS C:\> $Block1={Write-Host "Block executed"}
PS C:\> $Block2=$Block1
PS C:\> for($i=0;$i-lt 3;$i++)
>> {&$Block1
>> &$Block2
>> }
>>
Block executed
Block executed
Block executed
Block executed
Block executed
Block executed

能够看到6条脚本块执行的提示信息,循环体被执行了3次,每次都执行了变量$Block1和$Block2指向的脚本块。两个变量是指向同一个脚本块对象,所以脚本块执行了6遍。

返回值和参数

从脚本块中给出返回值需要输出包含不被cmdlet和其他表达式接收的对象,下例是返回数字的脚本块:

PS C:\> $number = {5}
PS C:\> &$number 
5
PS C:\> 1+ (&$number)
6

可以从上例中看到最后一个命令,返回值可以在一定条件下用于其他表达式,为此需要使用圆括号括起返回值包含。在使用宏的表达式中需要注意括号的使用,不然可能会因为构造的表达式有误而造成错误。在上例中如果没有括号,加号会把&作为其他右侧的操作数,而抛出异常。

PS C:\> 1 + &$number
You must provide a value expression on the right-hand side of the '+' operator.
At line:1 char:4
+ 1 + <<<<  &$number
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ExpectedValueExpression

需要注意的是输出对象不会终止脚本块中后续语句的操作,余下的语句将会继续执行。如在返回数值后在控制台窗口中输出一个字符串:

PS C:\> $numberPrint  = {5;Write-Host "generated a number" }
PS C:\> &$numberPrint
5
generated a number
PS C:\> $result = &$numberPrint
generated a number
PS C:\> $result
5

$numberPrint语句块返回值并向控制台打印字符串,匿名语句块表面上看起来同时输出了赋值的5,还有字符串。但是实际的返回值只是5,打印到控制台的字符串是在赋值时产生的伴生品。如果将$numberPrint调用赋值给其他变量,这个变量就拥有了$numberPrint的返回值,在赋值过程中可以看到已经打印字符串,在赋值后使用$result打印的才是实际的返回值。

为了结束执行并终止脚本块,可以使用return语句,它将终止执行并返回值。下例中使用return语句终止Write-Host命令的输出:

PS C:\> $numberPrint  = {return 5;Write-Host "generated a number" }
PS C:\> $a = &$number
PS C:\> $a
5

这样可以使用return语句直接退出脚本块并终止执行,该语句并不严格要求用户提供返回值。如果省略,脚本块将在不返回任何值的情况下退出。这种情况下,脚本块将会给调用者返回return语句之前的输出内容,如下例所示:

PS C:\> $noReturn = {return; 4}
PS C:\> &$noReturn
PS C:\> $numberReturn = {4;return}
PS C:\> &$numberReturn
4
PS C:\> $writeStringReturn = {Write-Host "output string";return}
PS C:\> &$writeStringReturn
output string
PS C:\> $b = &$writeStringReturn
output string
PS C:\> $b
PS C:\> $stringReturn = {"output string";return}
PS C:\> &$stringReturn
output string
PS C:\> $b = &$stringReturn
PS C:\> $b
output string

调用$noReturn语句块能看出return语句使后面的4未起作用;调用$numberReturn语句块可以看出return语句在没有返回值的情况下,其前面的输出4成为返回值。$writeStringReturn语句块验证了return语句前的Write-Host输出的字符串并不会作为语句块的返回值,Write-Host的作用只是向控制台输出要打印的内容,并不会以赋值的形式传递给其他变量。对比$stringReturn语句块调用和上面的$writeStringReturn语句块,可以看出return语句是将前面的字符串作为返回值输出。

上例中的return语句前面只有一个语句,事实上PowerShell中可以有多个语句作为返回值,这是和其他编程语言不同之一。PowerShell的返回值可以是多个值,如果在循环中多次执行,结果会是返回一个数组,下面是单个脚本块返回多个数字组成的数组实例:

PS C:\> $numbers = {1;2;3;}
PS C:\> &$numbers
1
2
3
PS C:\> (&$numbers).GetType().FullName
System.Object[]

需要强调的是可以不显式地返回一个数组,为此使用逗号作为数字分隔符,而不是使用分号。分号是作为语句的终结符,结果为3个数字组成的数组。

如果脚本块能够对外部有效,则其需要能够从外部获取参数,参数基于其传递并解析的顺序和位置。脚本块拥有被称为“$args”的预定义变量存在,这个变量由PowerShell的外壳自动定义,用来接收用户传递到脚本块的参数的集合。下例演示$args的使用方法:

PS C:\> $greeter = {
>> $firstName = $args[0]
>> $lastName = $args[1]
>> Write-Host "Hello,$firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:\> &$greeter
Hello,  Welcome to the world of PowerShell
PS C:\> &$greeter "Jim" "Green"
Hello,Jim Green Welcome to the world of PowerShell
PS C:\> &$greeter "Liu" "Tao"
Hello,Liu Tao Welcome to the world of PowerShell

通过索引访问参数是个不错的功能,很多语言中均提供该功能。它适合在很简单的应用环境下使用。也可用在定义脚本块时,并不清楚参数数目的应用场景下。但如果传递的参数比较多,这个功能会使得代码块有更多出错的可能性。在绝大多数情况下可以在脚本块内部使用param关键字声明变量,来使用命名参数。下例是前一个例子用命名参数重写后的形式:

PS C:\> $greeter = {
>> param ($firstName,$lastName)
>> Write-Host "Hello,$firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:\> &$greeter "Liu" "Tao"
Hello,Liu Tao Welcome to the world of PowerShell

这样的写法更为清晰,但还是有些不方便,因为是需要记忆参数的顺序。为了解决这个问题,可以使用参数名开关来关联参数名和参数值,如:

PS C:\> &$greeter "Liu" "Tao"
Hello,Liu Tao Welcome to the world of PowerShell
PS C:\> &$greeter -firstName "Liu" -lastName "Tao"
Hello,Liu Tao Welcome to the world of PowerShell
PS C:\> &$greeter -lastName "Tao" -firstName "Liu"
Hello,Liu Tao Welcome to the world of PowerShell

如果定义的参数名很长,这样的方法也不太方便。为此,可以缩写参数名,PowerShell外壳将会自动识别这些参数,如:

PS C:\> &$greeter -last "Tao" -first "Liu"
Hello,Liu Tao Welcome to the world of PowerShell
PS C:\> &$greeter -l "Tao" -f "Liu"
Hello,Liu Tao Welcome to the world of PowerShell

需要强调的是这种省略至少需要参数名的首字母就能够满足要求。如果参数名开头的几个字母相同,则需要提供足够多的参数名字母,直到能让PowerShell的外壳区分体提供的是哪个参数。

可以使用param语句强制将参数转换为需要的类型。如果脚本块调用者提供了可以转换到目标类型的参数,PowerShell外壳将会自动转换。与此同时,还可以在代码块中显式指明当参数类型转换失败时抛出错误。这个特性可以看做是简单的错误处理。下例在两个数相加时使用参数类型定义转换值类型:

PS C:\> $sum = {
>> param([int]$a,[int]$b)
>> $a + $b
>> }
>>
PS C:\> &$sum 4 5
9
PS C:\> &$sum "4" 5
9
PS C:\> &$sum "not a number" 5
param([int]$a,[int]$b)
$a + $b
 : Cannot process argument transformation 
on parameter 'a'. Cannot convert value 
"not a number" to type "System.Int32".
 Error: "Input string was not in a correct format."
At line:1 char:2
+ & <<<< $sum "not a number" 5
    + CategoryInfo          : 
InvalidData: (:) [], ParameterBindin...mationException
    + FullyQualifiedErrorId : 
ParameterArgumentTransformationError

$sum脚本块求两个整型数字的和。传递字符串会触发自动的类型转换。如果不成功,将得到类型不符的错误提示。

当脚本块的调用者没有提供所有的参数时丢失的参数会被初始化为$null,可以用这个特性检查是否已经被传递某个参数。下例在调用者没有提供给$lastName参数值时使用“Unknown”默认值:

PS C:\> $greeter = {
>> param ([string] $firstName,[string]$lastName)
>> if(!$lastName)
>> {
>>    $lastName = "Unknown"
>> }
>> Write-Host "Hello, $firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:\> &$greeter "Liu" "Tao"
Hello, Liu Tao Welcome to the world of PowerShell
PS C:\> &$greeter "Liu"
Hello, Liu Unknown Welcome to the world of PowerShell

上面的方法可以向缺失参数的脚本块调用提供默认值,但这并不是最好的方法。因为如果在脚本块中存在很多参数需要判断参数值是否缺失,则代码中会有大量的重复代码用于判断传递的参数值是否为空。PowerShell提供了参数默认值的简化符号,为参数块赋值意味着如果调用者没有提供参数值,参数会被初始化为默认值。下例会为$greeter脚本块的$lastName赋予默认值:

PS C:\> $greeter = {
>> param ([string] $firstName,[string] $lastName = "Unknown")
>> Write-Host "Hello,$firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:\> &$greeter "Liu" "Tao"
Hello,Liu Tao Welcome to the world of PowerShell
PS C:\> &$greeter "Liu"
Hello,Liu Unknown Welcome to the world of PowerShell

参数默认值能包含任何表达式,可以使用这一特性实现强制参数,强制参数指调用者必须提供给脚本块的参数。为此需要添加抛出异常的表达式,一旦调用者不能正常提供强制参数,就会抛出异常。下例在$greeter中将$firstName参数设置为强制参数:

PS C:\> $greeter = {
>> param ($firstName =$(throw "firstName required!"),$lastName)
>> Write-Host "Hello,$firstName $lastName Welcome to the world of PowerShell"
>> }
>>
PS C:\> &$greeter -lastName "Tao"
firstName required!
At line:2 char:27
+ param ($firstName =$(throw <<<<  "firstName required!"),$lastName)
    + CategoryInfo          : OperationStopped: (firstName required!:String) [], RuntimeExceptio
    + FullyQualifiedErrorId : firstName required!

在这里只需要知道$(throw “Error message”)的格式用来实现强制参数,将会在第11章中详细讨论异常和错误处理。

处理管道输入

在前面的章节中曾经涉及向cmdlet传递脚本块并在管道中的每个元素调用它们。下例演示传递脚本块给ForEach-Object并获取为所有文本文件最后的写入时间:

PS C:\> dir *.txt | ForEach-Object {$_.LastWriteTime.Date}
2009-1-3 10:09:25
2009-1-3 22:52:23
2009-1-6 14:52:45
2009-1-8 08:42:31

每个脚本块包含3个段来做用于管道输入,即begin, process和end。每个段看起来像是嵌套的块,包含零到多个语句。最重要的段是process,它会被管道中的每个对象调用。为了方便,当前的对象被标记为$_变量。下例使用process段过滤数字集合并返回比5大的数字:

PS C:\> $greaterThanFive = {
>> process {
>>          if ($_ -gt 5)
>>             {
>>               return $_
>>             }
>>          }
>> }
>>
PS C:\> 2,3,4,5,6,7 | &$greaterThanFive
6
7

与process类似,可以嵌入begin和end段。它们分别在管道进程开始之前及结束之后执行,主要用于计算集合统计信息或保持用于创建返回值的状态。下例将演示如何创建用于对管道中传入的所有数字求和脚本块:

PS C:\> $sum = {
>> begin {
>>        $total = 0
>>       }
>> process {
>>       $total +=$_
>>       }
>> end {
>>      return $total
>>     }
>> }
>>
PS C:\> 1,3,5,7,8 | &$sum
24

其中的process段将当前值叠加到求和值$total上,返回值的操作在end段中实现。在process段中使用了+=操作符,用于获取右侧的变量值相加到左侧的变量上。计算$total值通过反复叠加当前值,并把$_变量传递给$total变量。

脚本块既能接收参数,也能接收管道输入。在当前作用域和$args集合中命名参数和位置参数有效,而管道对象是被传递到$_变量中。为了示范如何创建参数和管道输入,下例累加管道中的所有元素:

PS C:\> $adder = {
>> param($number)
>> process{
>>         $_ +$number
>>        }
>> }
>>
PS C:\> 1,3,5  | &$adder 10
11
13
15

上例将操作的方法和具体的应用剥离开,便于读者理解。下例计算特定日期和当前目录下文本文件最后修改时间之间的天数:

PS C:\> $dateDiff = {
>> param([datetime]$referenceDate)
>> process {
>>         ($referenceDate - $_.LastWriteTime.Date).Days
>>         }
>> }
>>
PS C:\> dir *.txt | &$dateDiff([datetime]::Today)
5
7
13
22
84

需要强调的是对于[datetime]:Today使用了圆括号。如果没有括号,DateTime对象被转换为字符串后比较时会被再次转换为DateTime对象。这两次转换使得代码效率降低并可能因为datetime字符串在不同语言操作系统中表示方法的不同而使得转换出现异常。

将字符串作为表达式调用

几乎所有的脚本语言都有特殊的函数或操作来将输入的字符串作为代码块编译并执行,PowerShell中的Invoke-Expression的cmdlet实现该功能。字符串可以作为参数传递或者从其他命令中用管道传递,下例演示如何使用Invoke-Expression:

PS C:\> Invoke-Expression "Write-Host 'invoke expression'"
invoke expression

需要强调的是在语句中需要对引号进行转义或者在字符串中使用单引号的,这样可以在运行时操作字符串并且创建高度动态而优雅的脚本。下例生成多个命令并用管道传递给Invoke-Expression执行:

PS C:\> 1..5 | foreach {"Write-Host 'Got $_'"}| Invoke-Expression
Got 1
Got 2
Got 3
Got 4
Got 5

调用表达式与创建并执行脚本块相似,Invoke-Expression的作用形式也是类似的。它会创建并执行新的脚本块,传递参数并通过管道将返回值给下一个命令。需要谨记的是,与脚本块不同的是Invoke-Expression不会创建子变量作用域。它总会在当前的作用域中执行,如下:

PS C:\> $name = "LiuTao"
PS C:\> Invoke-Expression "`$name = 'WangLei'"
PS C:\> $name
WangLei

正如上例中所示,$name变量被修改,这个字符串在当前作用域中执行。在创建命令字符串时使用了两个引号,所以需要对美元符转义以传递到Invoke-Expression中的字符串包含变量名,而不是被展开的变量值。

使用Invoke-Expression的最大好处是很容易将字符串转换为可执行的命令,下例演示如何使用cmdlet创建允许用户输入表达式的计算程序:

:PS C:\> $expression = Read-Host "Enter expression"
Enter expression: 5*6-4
PS C:\> $result = Invoke-Expression $expression
PS C:\> Write-Host $result
26

由于尚未对输入进行校验,所以任何输入的代码将会被执行,如果用户输入一下表达式4*6;del C:\-recurse –force,则将会丢失系统盘所有的数据。正确的做法是需要验证用户输入,避免Invoke-Expression执行任何尚未过滤危险操作的表达式。

另外一种执行字符串的方法是通过使用全局变量$ExecutionContent,它是System.Management.Automation.EngineIntrinsics类型,其中包含一个属性InvokeCommand,是System.Management.Automation.CommandInvocationIntrinsics类型。可以使用Invoke-Command的InvokeScript方法来实现Invoke-Expression所做的一切:

PS C:\> $ExecutionContext.InvokeCommand.InvokeScript("Write-Host 'invoke'")
invoke

$ExecutionContext.InvokeCommand对象很有用,它允许用户把字符串编译为脚本块。可以用来创建稍后执行的动态命令,如:

PS C:\> $cmd = "Write-Host `"`$(`$args[0])`""
PS C:\> $cmdBlock = $ExecutionContext.InvokeCommand.NewScriptBlock($cmd)
PS C:\> &$cmdBlock "test"
test

虽然能够存储字符串并使用Invoke-Expression调用它来执行,但是脚本块的代码只在创建时编译一次,而Invoke-Expression会在每次执行行编译。这样如果脚本内容很多,编译时间会很长;另外,脚本块会创建子变量作用域,如果要在当前作用域中保护变量值,子变量作用即可实现。通常处理嵌套作用域可以将父作用域变量声明为script或global作用域,或在脚本块需要修改变量时用Get-Variable和Set-Variable改变内容。

脚本块作为委托

委托是.NET事件处理机制的重要组成部分。简而言之,如果方法不是静态的,委托即用于存储.NET类、方法引用和对象引用的对象。当调用委托时,它指向对象的方法会被执行。NET中的事件工具指向事件接收对象的委托和方法,传递这些委托到事件触发者。当事件发生时,事件触发者调用事件委托。

脚本块和委托有很多相似点,二者都是指向一些代码并调用执行。PowerShell的类型系统允许将脚本块转换为委托。只有System.EventHandler类型的委托是支持的,即只有带对象和System.EventArgs实例为参数的脚本块可以被转换为委托。下例演示如何声明脚本块并转换为委托:

PS C:\> $handler = {
>> param($sender,[EventArgs] $eventArgs)
>> Write-Host "Event raised!"
>> }
>>
PS C:\> $delegate = [EventHandler] $handler
PS C:\> $delegate.GetType().FullName
System.EventHandler

可以通过使用Invoke()方法调用委托:

PS C:\> $delegate.Invoke(3,[EventArgs]::Empty)
Event raised!

可以创建Windows窗体程序来模仿图形输入框,允许用户输入。为此,需要创建Form对象并在其中增加TextBox和Button控件绑定Button的Click事件处理。Click事件处理将会关闭窗口,并把TextBox中的值存储到全局变量,以下是代码:

PS C:\> $null = [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
PS C:\> $form = New-Object Windows.Forms.Form
PS C:\> $form.Size = New-Object Drawing.Size -arg 300,85
PS C:\> $textBox = New-Object Windows.Forms.textBox
PS C:\> $textBox.Dock = "Fill"
PS C:\> $form.Controls.Add($textBox)
PS C:\> $button = New-Object Windows.Forms.Button
PS C:\> $button.Text = "Done"
PS C:\> $button.Dock = "Bottom"
PS C:\> $button.add_Click(
>> {$global:resultText = $textBox.Text;$form.Close()})
>> $form.Controls.Add($button)
>> [Void] $form.ShowDialog()
>> Write-Host $global:resultText
>>
Hello ,Windows Form in PowerShell!

在第1行中PowerShell没有默认载入System.Windows.Forms的汇编对象。创建Form对象,并设置窗口大小为300像素宽,85像素高。然后添加充满整个窗口宽度的TextBox控件,即水平方向使用所有可用空间。最后添加Button控件,其中包含调用add_Click方法,这个方法把委托作为参数并绑定Click事件。需要强调的是在调用方法时,不需要显式抛出,PowerShell自动完成。代码执行后显示如图1所示的输入框窗口。

在其框中输入“Hello ,Windows Form in PowerShell!”,单击“Done”按钮,能够看到控制台输出“Hello ,Windows Form in PowerShell!”。说明PowerShell已经接受了Form对象的事件委托,即将Form接收的参数传递给PowerShell处理。

赛迪网地址:http://tech.ccidnet.com/art/3539/20100617/2089247_1.html

作者: 付海军

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

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

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

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


发表回复