Powershell学习笔记——函数和函数库
前段时间公司封闭开发,就在封闭的前一天感冒发烧,为了封闭,一顿猛药下去,烧是退了,却在扁桃附近爆发出来——扁桃发炎加溃疡,搞了十多天才好啊,天天喝稀饭啊……所以请大家原谅这么久没有续上学习笔记。顺便:过两天继续封闭,所以这个笔记更新速度可能不会很快了,我尽力。
函数
函数是Powershell里一个非常重要的东西,与CMD比较起来,这绝对是一个亮点。CMD中只能用“标签”+CALL来模拟函数,而Powershell不仅支持函数,还支持3种类型的函数:普通函数(Function)、过滤器(Filter)和管道函数(Pipeline Function)。除此之外,Powershell的参数解析也是非常智能和强大——当然,参数形式的约定是必不可少的部分。
函数可以为一系列的操作提供一个快捷命令,而且还可以通过参数来改变函数内的流程或运算结果。还是先来一个示例:
- PS C:\Users\james> f:
- PS F:\> function cd~ { cd c:\users\james }
- PS F:\> cd~
- PS C:\users\james>
这里定义了一个名为“cd~”的函数,目的是直接回到用户目录,就像Linux的“cd ~”一样。这里function是定义函数的关键字,cd~是函数名,{}中的部分则是函数体。——这里还没用到参数,这个,后面再说。执行“cd~”,实际是执行了函数体里面的内容。
当然,如果一个函数比较复杂,要一行写完是比较痛苦的。那么也可以分多行来写,比如
- PS C:\users\james> function cd~ {
- >> cd c:\users\james
- >> }
- >>
- PS C:\users\james>
当然,这样写不太方便,因为你在写到最后一行时如果发现前面有错,连修改的机会都没有。不过,如果把一段程序或者函数定义写在脚本文件中,再来执行脚本就会方便得多了——我想,关于脚本文件,在前面已经说过,这里就不用多说了。
函数的参数
为了演示参数,我们用另一个例子。这个例子会将参数转为大写输出:
- PS F:\> function toUpper { $args[0].toUpper() }
- PS F:\> toUpper "james"
- JAMES
- PS F:\>
这个示例中,用到了未命名参数数组$args。只要是没有命名的参数都会按顺便存在在这个数组中。如果是多个未命名参数,我们可以用用一个循环来依次处理。比如:
- PS F:\> function toUpper {
- >> foreach ($a in $args) { $a.toUpper() }
- >> }
- >>
- PS F:\> toupper hello james fancy
- HELLO
- JAMES
- FANCY
- PS F:\>
循环是控制流程的内容,之前的笔记还没提到。这篇笔记主要是记函数,所以也暂时略过不说。
命名参数
不过这里有一个问题却不得不说——未命名参数。顾名思义,未命名参数就是没有名字的参数;而且,既然有未命名参数,就一定有命名参数。那么命名参数又是什么呢?还是看例子(这次的例子是写的脚本文件):
- # sample.ps1
- # 命名参数示例
- function hello($name, $isMale) {
- if ([bool]::parse($isMale)) {
- "Hello Mr. $name"
- } else {
- "Hello Ms. $name"
- }
- }
- hello James true # 参数值为按顺便赋予命名参数
- hello -ismale false Jenny # 指定了名称的参数值会赋给对应的命名参数,其它的按顺序赋给其它命名参数
运行结果如下:
- PS F:\james\Desktop> .\sample.ps1
- Hello Mr. James
- Hello Ms. Jenny
多余的参数
上面的示例中有两个调用函数的示例。第一个没有为参数值指定名称,那么函数调用时会按顺序把参数值赋给参数列表中的命名参数;而第二种调用,为ismale参数指定了值,那么会先将指定了名称的参数值赋给相应的命名参数,其它的参数值再按顺序赋予其它命名参数。现在有一个问题:如果赋予所有命名参数之后还有参数传入,这些参数是否可以通过$args来访问呢?继续做实验,把上面的示例稍做改动:
- # sample.ps1
- # 多余的参数示例
- function hello($name, $isMale) {
- if ([bool]::parse($isMale)) {
- "Hello Mr. $name"
- } else {
- "Hello Ms. $name"
- }
- foreach ($a in $args) {
- "MORE ARG: $a"
- }
- }
- hello James true a b c d e f g
运行结果
- PS F:\james\Desktop> .\sample.ps1
- Hello Mr. James
- MORE ARG: a
- MORE ARG: b
- MORE ARG: c
- MORE ARG: d
- MORE ARG: e
- MORE ARG: f
- MORE ARG: g
默认参数值
其实多做做实验,答案都是显而易见的。继续下一个问题:“[bool]::parse($isMale)”能不能简化?[bool]::parse其实是调用了System.Boolean的静态方法Parse来将字符串解析为布尔类型的值。如果我们直接传入布尔类型的参数不就可以简化了么?就像这样
- # sample.ps1
- function hello($name, $isMale) {
- if ($isMale) {
- "Hello Mr. $name"
- } else {
- "Hello Ms. $name"
- }
- }
- hello James $true
- hello -ismale $false Jenny
其实,还可以更简单,比如对于男士,省略第2个参数……当然,现在这个脚本不行,试试就知道了,可以省略$isMale参数的是女士。再做点改动:
- # sample.ps1
- # 默认参数值示例
- function hello($name, $isMale=$true) {
- if ($isMale) {
- "Hello Mr. $name"
- } else {
- "Hello Ms. $name"
- }
- }
- hello James
- # Hello Mr. James
开关[switch]参数
既然isMale是一个布尔,在控制台脚本里,会很容易让人想起“开关”,即通过一个参数是否存在来表示两种不同的参数值。比如,如果加了“-ismale”参数,则表示男士,否则表示女士——哦,应该换一下,因为不加参数为默认形式,而我们前面约定的默认情况是男士,所以开关参数应该改为“-isFemale”,不过既然是开关,那“is”也可以省了,就是“-female”。示例:
- # sample.ps1
- # [switch]参数值示例
- function hello($name, [switch] $female) {
- if (!$female) {
- "Hello Mr. $name"
- } else {
- "Hello Ms. $name"
- }
- }
- hello James
- hello Jessy -female
- # Hello Mr. James
有注意到定义参数时指定的[switch]标记么?这叫参数类型。当然[switch]只是参数类型中的一种……
指定参数的类型
除此之外,还可以为参数指定类型,这样的话,只要给予的参数值不是指定的类型,或者不能转换为指定的类型,就会抛出错误。当然,指定了类型的参数,在函数内进行处理时,往往可以活力掉类型转换的步骤,比如我们想把年、月、日3个参数拼成一个8位长度的日期字符串,下面哪个函数是可以完成呢?
- # sample.ps1
- # 指定类型的参数示例
- function add1($a, $b, $c) {
- $a + $b + $c
- }
- function add2([string]$a, $b, $c) {
- $a + $b + $c
- }
- add1 2010 10 21 # 输出:2042
- add2 2010 10 21 # 输出:20111021
由于没有指定类型,add1的三个参数都被当作int型进行处理,相加的结果是2042。而add2中,将$a申明为string类型,虽然$b和$c仍然是被当作int型进行处理,但是“+”遇到不同类型的运算时是自动转为其左边的类型进行运算,所以是字符串相连,结果20111021。
函数的返回值
Powershell关于函数返回值这个问题,比较复杂。在其它脚本或者语言中,通常来说,通过return之类的关键返回的才是返回值,而Powershell不同,只有是输出到Output的内容,都是返回值,比如
- # sample.ps1
- function test() {
- write-output "Hello"
- "James Fancy"
- return "OK"
- }
- $a = test
- $a.GetType().FullName
- $a
它的输出:
- PS F:\james\Desktop> .\sample.ps1
- System.Object[]
- Hello
- James Fancy
- OK
可以看出来,test函数返回了包含3个值的一个数组,除了最后的return外,前面两个都是写入管道的。哦,管道……这又是一个复杂的东西,后面再来复习。现在只需要记得write-output输出,直接字面值或者变量值输出以及return都会产生返回值就对了。
很明显,Powershell的函数允许一次返回多个值,这些值都保存在一个数组中。当然,如果函数只返回一个值,那就不需要数组了,比如把上面的示例精简一下:
- # sample.ps1
- function test() {
- "James Fancy"
- }
- $a = test
- $a.GetType().FullName # 输出:System.String
- $a # 输出:James Fancy
Filter,过滤器函数
除了定义函数之外,也可以定义过滤器。过滤器可以对通过管道进来的内容进行过滤,比如下面这个列子就是为了只列出.exe文件:
- # sample.ps1
- filter test() {
- # 只列出.exe扩展名的文件
- if ($_.extension -eq ".exe") { $_ }
- }
- dir | test
运行结果:
- PS E:\james\Desktop> cd c:/windows
- PS C:\windows> E:\james\Desktop\sample.ps1
- 目录: C:\windows
- Mode LastWriteTime Length Name
- ---- ------------- ------ ----
- -a--- 2009-7-14 9:14 65024 bfsvc.exe
- -a--- 2011-6-9 11:10 642240 bjzq.exe
- -a--- 2010-1-31 15:23 2614272 explorer.exe
- -a--- 2009-7-14 9:14 13824 fveupdate.exe
- -a--- 2009-7-14 9:14 497152 HelpPane.exe
- -a--- 2009-7-14 9:14 15360 hh.exe
- -a--- 2011-3-21 23:21 78848 KMSEmulator.exe
- -a--- 2011-3-21 23:26 151552 KMService.exe
- -a--- 2009-11-28 7:52 179712 notepad.exe
- -a--- 2009-7-14 9:14 398336 regedit.exe
- -a--- 2009-6-11 5:41 49680 twunk_16.exe
- -a--- 2009-7-14 9:14 31232 twunk_32.exe
- -a--- 2009-6-11 5:42 256192 winhelp.exe
- -a--- 2009-7-14 9:14 9728 winhlp32.exe
- -a--- 2009-7-14 9:14 9216 write.exe
- -a--- 2011-8-10 12:27 34512 xinstaller.exe
- PS C:\windows>
注意脚本中最后一句“dir | test”,意思就是把dir的输出通过管道传递给test进行处理。再看test函数的内容,管道中传入的每一项都由特殊变量$_引用。test对传入的每一项都进行判断,将扩展名为.exe的文件对象输出,其余的丢弃。
管道函数
其实,过滤器是一种特殊的函数,管道函数的简化版。管道函数也是一种特殊的函数,它包含3个部分,begin、process和end。管道输出在进入管道函数的时候,会首先运行begin区域的脚本,仅运行一次;之后从管道进来的每个对象都会经历process过程;所有项结束之后,会触发end区域的脚本。而过滤器就是只定义了process区的管理函数。还是来看例子:
- # sample.ps1
- function test() {
- begin {
- "处理开始了"
- }
- process {
- if ($_ -like "a*") { $_ }
- }
- end {
- "处理完成了"
- }
- }
- # 这次示例用一个数组来演示
- $("a", "ab", "bac", "b", "bc", "ac") | test
输出:
- PS E:\james\desktop> E:\james\Desktop\sample.ps1
- 处理开始了
- a
- ab
- ac
- 处理完成了
管道函数的3个块都可以省略,包括process块,只不过如果省略了process块之后,这个函数就没啥意义了。不过根据实际情况,begin和end块倒是经常被省略的。
函数库
前面关于运算符的笔记中提到了点号(.)运算符可以用于引入一个脚本,而这个脚本就类似于C/C++中#include的方式被引入到当前位置并执行。那么,如果这个脚本里面只包含函数定义,而不包含其它内容,那么这个脚本就是一个函数库。每次使用该函数库的时候,只需要使用点号运算符引入即可。比如上面的例改稍作改动:
- # sample.ps1
- function test() {
- process {
- if ($_ -like "a*") { $_ }
- }
- }
然后在Powershell控制台运行:
- PS E:\james\desktop> . .\sample.ps1
- PS E:\james\desktop> $("a", "ab", "bac", "b", "bc", "ac") | test
- a
- ab
- ac
- PS E:\james\desktop>
如果有兴趣试试不使用点号,而是直接运行脚本,那么第二条命令就会出错是,因为test未定义。