代码改变世界

Windows PowerShell 2.0创建调用脚本文件

2010-11-30 02:01 @天行健中国元素 阅读(...) 评论(...) 编辑 收藏

在PowerShell中不存在文件和目录的概念,涉及文件和目录的操作总是转换为项(item)处理,即Get-Item、Get-ChildItem和Get-ItemProperty。在PowerShell中的dir命令以Get-ChildItem的别名出现,通过项操作对象的原因是PowerShell作用的任何类型的对象均以项的形式存在。而不像类Unix系统中将所有的对象都抽象成文件,即使这个对象很明显不是文件的情况。

项是包含内容的属性对象,这些对象又可以包含其他项。这个定义可以很容易扩展到文件和目录,即它们是具有各自内容和属性的项。项的概念可以应用于多个系统对象,提供程序(provider)用来创建、找回、修改和移除项。它是官方提供的重要的Shell扩展机制之一,Shell与一些内置的提供程序一起发布给用户,其中提供了提供程序的接口。

1 创建脚本

典型的PowerShell脚本是文本类型的文件,可以使用任何文本编辑器来创建。通常情况下,具有.ps1的文件后缀。最好使用一些语法高亮,甚至是智能补全功能的编辑器,如创建本书中脚本文件的Notepad++编辑器。这是一个功能强大的编辑器,支持多种语言,并且提供语法高亮及关键字补全等;另外可以在PowerShell控制台下使用Set-Content这个cmdlet来创建脚本文件。下面使用该命令创建一个脚本文件:

PS C:\> $code = @"
>> Write-Host "Hello World!!"
>> "@
>>
PS C:\> Set-Content hello-world.ps1 $code
PS C:\> type hello-world.ps1
Write-Host "Hello World!!"

hello-world.ps1以普通的文本文件形式存在,可以使用type命令(以Get-Content命令的别名存在)来查看其内容。

1.1 调用脚本

在PowerShell中调用脚本文件时应在PATH环境变量中包含的路径中搜索,当前目录并不在系统的PATH中。调用当前目录中的脚本文件需要添加前缀,调用当前的脚本文件的方式应该是.\hello-world.ps1。

如果当前目录默认包含在PATH中,则允许攻击者采用恶意代码覆盖同名的默认程序。假设恶意用户在共享目录中写入与内置cmdlet同名的脚本,当管理员访问这个共享目录时并调用了该脚本,则可导致恶意操作。因此在PowerShell和Unix的Shell中需要用户通过在当前路径前增加.\前缀,显式地指明需要执行当前目录中的程序。

调用创建的hello-world.ps1脚本文件提示以下错误:

PS C:\> .\hello-world.ps1
File C:\hello-world.ps1 cannot be 
loaded because the execution of scripts is 
disabled on this system. Please see "get-help 
about_signing" for more details.
At line:1 char:18
+ .\hello-world.ps1 <<<<
    + CategoryInfo          : NotSpecified: (:) [], PSSecurityException
    + FullyQualifiedErrorId : RuntimeException

从提示中可以看到抛出错误是因为当前脚本在使用的默认安全设置为不允许执行,Shell包含多种执行脚本的安全级别和用户权限策略,默认运行在Restricted(受限)策略级别下,既不允许执行脚本。可以通过执行Get-ExecutionPolicy命令查看当前脚本的执行权限:

PS C:\> Get-ExecutionPolicy
Restricted

为了能够执行正常的脚本操作,需要改变当前脚本的执行策略。预定义的脚本执行策略如下。

(1)Restricted:不允许执行任何脚本,而忽略用户是否具有超级用户权限,仍可执行控制台互交式命令。

(2)Allsigned:仅执行已经过受信第三方数字签名的脚本,关于签名脚本文件将在第12章中详细介绍。

(3)RemoteSigned:在执行任何来自Internet的脚本文件时要求其具有受信第三方数字签名;否则拒绝执行,执行本地创建的脚本文件不需要数字签名。

(4)Unrestricted:所有脚本均可执行,无论是否具有签名,或者签名是否合法。

RemoteSigned执行策略兼顾了好的网络安全级别和本地易用性。为了执行前面创建的脚本文件,使用Set-ExecutionPolicy命令调整安全策略级别,是这个cmdlet只能由超级管理员(administrator)来执行。如果是普通用户,则可右击PowerShell的快捷方式,然后选择快捷菜单中的“以管理员方式运行”(Run as administrator)选项,这样就有了以超级用户身份创建的shell线程,从而可以执行命令改变执行策略,如下:

PS C:\> Set-ExecutionPolicy RemoteSigned
PS C:\> Get-ExecutionPolicy
RemoteSigned

下例调用本地脚本文件:

PS C:\> .\hello-world.ps1
Hello World!!

另外脚本块执行操作符(&)也可执行脚本文件:

PS C:\> & .\hello-world.ps1
Hello World!!
PS C:\> & .\hello-world
Hello World!!

1.2 传递参数

脚本能够接收参数,这样可以针对不同对象执行所需操作。脚本文件支持$args变量,用于在执行时接收传递的参数。下例在指定目录中查找文件名符合指定通配符的文件:

$where = $args[0]
$what = $args[1]
if(!$what -match "\.wma$")
{
	$what = $what + ".wma"
}
Get-ChildItem $where $what –Recurse

$what参数用来在通配符不包含.wma扩展名时添加扩展名,从而限制搜索文件的范围只限于.wma文件。代码递归遍历所有文件,并返回符合$what通配符的文件。将以上代码保存为Get-Music.ps1脚本文件,其中查找名为“ A Place Near by.wma”的音乐文件:

PS C:\> .\Get-Music.ps1 'D:\Music' *Near*

    Directory: D:\Music

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---          2009/3/1     20:38    3006849 A Place Near by.wma

脚本文件允许使用param语句定义自己的参数名,以避免$args参数对象要求输入参数的顺序必须与预定义顺序一致。下例用命名参数方式重写前面的脚本文件,并命名为“Get-Music-Param.ps1”:

param ($where,$what)
if(!$what -match "\.wma$")
{
		$what = $what + ".wma"
}
Get-ChildItem $where $what –Recurse

其中的参数允许在不限制输入参数顺序的情况下调用脚本文件,下面调用该脚本文件查找名为“Dont You Forget.wma”的音乐文件:

PS C:\> .\Get-Music-Param.ps1 -where 'D:\Music' -what *You*

    Directory: D:\Music

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---          2009/3/1     21:19    3556057 Dont You Forget.wma

通过定义begin、process和end块来实现在管道命令中输入,下例获取文件序列并过滤掉所有大于指定大小的文件,将代码保存为FileSize-FilterLarge.ps1脚本:

param ($sizeLimit)

begin
{
}

process
{
		if($_.Length -ge $sizeLimit)
		{
			$_
		}
}

end
{
}

脚本文件接收文件大小变量用于后面检测所有管道命令传递项的大小,最后返回文件长度等于大小限制的文件。实例中的所有操作在process块中执行,当调用代码块时会作用于管道输入的所有项。begin和end块未执行任何操作,但是为了代码的完整性和可读性,总是习惯性地包含它们,即使其为空。下例使用脚本文件查找小于13个字节的文件:

PS C:\> dir *.txt | .\FileSize-FilterLarge.ps1 13

    Directory: C:\

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

与函数相似,脚本文件会创建独立的变量作用域。即可读入由当前shell环境及其父作用域中的变量,对同名变量的写入会在当前作用域下覆盖父作用域中的值。下面创建一个访问父作用域的脚本文件:

PS C:\> $name = "LiMing"
PS C:\> Set-Content Variable-Scope.ps1 "Write-Host $name"
PS C:\> .\Variable-Scope.ps1
LiMing

接下来演示如果在脚本文件中修改外部已经初始化的变量,是否会更改该变量的值,将以下代码保存为“Variable-ScopeInnerVariable.ps1”文件:

Write-Host "Variable Within script original: $name"
$name = "XiaoGang" 
Write-Host "Variable Within script modified: $name"

调用这个脚本文件验证原有变量的内容:

PS C:\> $name = "LiMing"
PS C:\> .\Variable-ScopeInnerVariable.ps1
Variable Within script original: LiMing
Variable Within script modified: XiaoGang
PS C:\> Write-Host "Variable Outside of script $name"
Variable Outside of script LiMing

可以看出修改变量的操作只在脚本文件内部起作用,如果需要在脚本文件内部修改父作用域中的变量,则在脚本文件中使用$global作用域前缀或者Get-Variable/Set-Variable这类cmdlet显式要求修改父作用域变量。下例在脚本文件内使用$global关键字修改父作用域变量:

Write-Host "Variable Within script original: $global:name"
$global:name = "XiaoGang" 
Write-Host "Variable Within script modified: $global:name"

将上述代码保存为“Variable-ModifyGloablVariable.ps1”文件,调用该文件时的提示如下:

PS C:\> $name = "LiMing"
PS C:\> .\Variable-ModifyGloablVariable.ps1
Variable Within script original: LiMing
Variable Within script modified: XiaoGang
PS C:\> Write-Host "Variable outside script :$global:name"
Variable outside script :XiaoGang

有时需要隔离特定的变量或函数,对于特定的脚本文件来说它是全局的。需要让其中定义的所有函数和脚本块能够访问该变量或函数,但用户需要显式强调不能在脚本文件外访问对象。这时不能使用$global作用域前缀,因为这将使该变量或函数暴露在所有代码面,无论它们是否已在脚本文件中定义。解决这个问题的方法是使用$script作用域前缀,为了说明这个问题,建立如下脚本文件代码:

$script:name = "LiuTao"

function Modify-Name()
{
	$script:name = "XiaoGang"
	Write-Host "Variable within script function scope:$script:name"
}

Write-Host "Variable Original script global scope: $script:name"
Modify-Name
Write-Host "Variable Modified script global scope: $script:name"

上述代码中存在脚本全局作用域和Modify-Name函数作用域,在shell可以通过引用$script:name调用脚本全局变量。将以上脚本另存为“Variable-ScriptScope.ps1”文件并调用:

PS C:\> $name = "LiMing"
PS C:\> .\Variable-ScriptScope.ps1
Variable Original script global scope: LiuTao
Variable within script function scope:XiaoGang
Variable Modified script global scope: XiaoGang
PS C:\> Write-Host "Variable outeside of script: $name"
Variable outeside of script: LiMing

能够看到对于脚本文件以外的$name变量并没有受到影响。

与此相对应,通过在脚本文件名前增加点(.)前缀来实现脚本文件访问所有的当前变量和函数,即将脚本文件点源引用(dot-sourcing)到当前shell进程中。点源引用使用点加空格再加上要引用的脚本文件名,将该脚本文件的所有变量、函数、脚本块释放在当前shell中供使用。下例以点源引用的方式来调用Variable-ScriptScope.ps1脚本文件:

PS C:\> $name = "LiMing"
PS C:\> . .\Variable-ScriptScope.ps1
Variable Original script global scope: LiuTao
Variable within script function scope:XiaoGang
Variable Modified script global scope: XiaoGang
PS C:\> Write-Host "Variable outeside of script: $name"
Variable outeside of script: XiaoGang

由于使用了点源引用方式来调用同一个脚本文件,因此结果与之前不同,即修改了全局变量$name的值。

1.3 返回值

在大多数情况下,参数是外部世界传递数据到脚本文件的唯一通信机制。在脚本文件处理数据之后需要将结果返回给外部,在PowerShell中提供了很好的实现方法。

如果没有显式销毁、赋值给变量、管道输出或重定向到其他命令对象,则将会为下个命令输出到管道中,可以通过脚本生成一系列的对象并输出。下例输出3个缓存文件名:

PS C:\> $code = @"
>> "File1.tmp"
>> "File2.tmp"
>> "File3.tmp"
>> "@
>>
PS C:\> Set-Content Generate-TempFiles.ps1 $code
PS C:\> $files = .\Generate-TempFiles.ps1
PS C:\> $files
File1.tmp
File2.tmp
File3.tmp
PS C:\> .\Generate-TempFiles.ps1 | foreach{"File:" + $_ }
File:File1.tmp
File:File2.tmp
File:File3.tmp

既可将对象赋给变量,也可传递到管道中的下一个命令。

下例中的return语句返回现有对象,并终止后续代码:

PS C:\> $code = @"
>> return "File1.tmp"
>> return "File2.tmp"
>> "@
>>
PS C:\> Set-Content Generate-TempFilesReturn.ps1 $code
PS C:\> .\Generate-TempFilesReturn.ps1
File1.tmp

代码在第1个return语句之后终止,因而只输出一个文件对象。

如果是运行在顶层作用域中,return语句会退出脚本文件,但是上述代码中该语句只是退出当前作用域。即如果return语句是在脚本文件中的脚本块或函数中,只是退出该脚本块或函数,而脚本文件本身还将继续执行。为了显式退出脚本文件,需要使用exit语句。下例在脚本文件中的函数中退出整个脚本文件的执行:

function Verify-TextFile($file)
{
		if(!$file.EndsWith(".txt"))
		{
			Write-Host "$file is not a text file"
			exit
		}
}

$files = "Names.txt","Music.wma","Photo.jpg"
foreach($file in $files)
{
		Verify-TextFile $file
}

将文件另存为“Explicit-Exit.ps1”,然后调用如下:

PS C:\> .\Explicit-Exit.ps1
Music.wma is not a text file

可以看到未执行检测Photo.jpg文件的操作,而是在检测Music.wma文件后退出。

2 从其他环境中执行脚本

PowerShell代码与已经存在的解决方案结合,才能发挥最大作用。可以编写所需脚本并在Windows的计划任务中调用,也可以将现有的脚本解决方案逐块转换为用PowerShell开发,或者用命令方式调用现有解决方案。所有自动化环境,包括任务计划服务、软件系统,以及执行外部命令都可以将PowerShell扩展进来。可以调用PowerShell.exe并把脚本块名作为参数:

C:\>PowerShell.exe hello-world.ps1
The term 'hello-world.ps1' is not recognized as a cmdlet, function, operable pr
ogram, or script file. Verify the term and try again.
At line:1 char:16
+ hello-world.ps1 <<<<
    + CategoryInfo          : ObjectNotFound: (hello-world.ps1:String) [], Com
   mandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

提示不能识别hello-world.ps1脚本文件,出现这个错误的原因是PowerShell中的当前目录并不包含在PATH环境变量中。为执行当前目录中的脚本文件,需要在脚本文件前增加.\前缀:

C:\>PowerShell.exe .\hello-world.ps1
Hello World!!

如果在脚本文件名中包含空格或者其他PowerShell不能正确解析的符号,则会出现错误,如下例:

C:\>PowerShell.exe .\hello world.ps1
The term '.\hello' is not recognized as a cmdlet, function, operable program, o
r script file. Verify the term and try again.
At line:1 char:8
+ .\hello <<<<  world.ps1
    + CategoryInfo          : ObjectNotFound: (.\hello:String) [], CommandNotF
   oundException
    + FullyQualifiedErrorId : CommandNotFoundException

外部调用PowerShell.exe和以参数形式执行脚本文件时,路径和文件名中不能包含空格。解析过程中空格前后被截断,只是把hello作为脚本文件名,这显然是错误。可以使用引号包含带空格的路径或文件名,然后使用执行操作符(&)执行脚本:

PS C:\> powershell.exe  "& '.\hello world.ps1'"
Hello World!!

PowerShell允许用户创建由控制台程序返回的退出代码,这样可能通过为退出语句传递退出代码执行退出操作,即执行脚本和传递错误代码均可使程序退出。

3 开发和维护脚本库

复杂的功能会使大量的代码堆砌在一起,杂乱无章,不便于管理。如果需要实现复杂功能,最好将功能分割为相对独立且耦合性比较小的模块,这样形成的脚本文件将更便于重用和管理

3.1 以dot sourcing方式包含库

脚本库是包含有用函数的文件,可以被多次重用。库是不执行操作的普通PowerShell脚本文件,其中只是定义一些函数,让脚本库的客户代码调用。一个最简单的脚本库Library1.ps1的代码如下:

function LibHello()
{
	Write-Host "Hello form Lib"
}
Write-Host "Library1.ps1 script included"

包含或导入脚本文件中的脚本库相当于执行它,下例从脚本文件中点源引用脚本库并执行其中的LibeHello函数:

PS C:\> . .\Library1.ps1
Library1.ps1 script included
PS C:\> LibHello
Hello form Lib

可以看到,除了对Library.ps1脚本块执点源引用操作外没有执行其他操作,但是脚本库中的函数却能够使用。

3.2 库路径

点源引用的缺点是所有和脚本库的相关路径以当前目录相对路径的形式存在,即一旦将脚本库释放在当前shell中,则其对于所有函数都是公开的。如果更换当前目录的位置,以全路径方式执行脚本文件将会报错,如下例:

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

The term 'LibHello' is not recognized as a cmdlet, 
function, operable program, or script file. 
Verify the term and try
again.
At C:\Lib-User-DotSource.ps1:2 char:9
+ LibHello <<<<
    + CategoryInfo          : 
ObjectNotFound: (LibHello:String) [], 
CommandNotFoundException
    + FullyQualifiedErrorId :
 CommandNotFoundException

错误的原因在于对于shell来说Library.ps1在当前目录下,但在当前目录下无法找到这个脚本文件。而在没有对.\Library.ps1执行成功的点源引用的前提下,无法找到LibHello函数。

4 总 结

本文首先介绍了创建和调用脚本文件的基础,然后分别示范了如何传递参数、管道传递对象及从脚本文件返回值。并且对比了脚本文件同函数和脚本块在传递参数、操作管道输入及返回值之间的联系,三者以相同的方式执行。本文还介绍了如何在其他环境下调用PowerShell并使其一起工作,最后说明了创建脚本库的方式。

赛迪网地址:http://news.ccidnet.com/art/32859/20100708/2109359_1.html

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