Windows PowerShell一次编写一行脚本

Don Jones

在过去的专栏中,我强调了 Windows PowerShell 是一种外壳这一事实。它应被交互式使用 — 与您可能已经很熟悉的 cmd.exe(或命令提示符)外壳并无不同。PowerShell 仍然支持脚本语言 — 一种比 cmd.exe 的批处理语言更稳定可靠的语言。并且它

同某些语言(如 VBScript)一样功能强大(如果不如其功能强大的话)。但是,Windows PowerShell™ 是一种交互式外壳的事实,使得编写脚本非常容易学习。实际上,您可以在外壳内部交互式地开发脚本,从而可以一次编写一行脚本,并立即看到自己努力的成果。

此迭代的脚本编写技术还使调试变得更加容易。由于您立即看到了脚本的结果,因此您就可以在脚本不能按预期方式运行时快速地对其进行修订。

在此专栏中,我将向您介绍一个在 Windows PowerShell 中进行交互式脚本编写的示例。我将创建一个脚本,该脚本从文本文件读取服务名称并将每个服务的启动模式设置为“Disabled”(禁用)。

我想要您从这次演练中领会到在小段代码中构建 Windows PowerShell 脚本(而不是试图在大段代码中应对整个脚本)的概念。您可以采用需要完成的任何管理任务并将其分成组成自身的多段代码,然后确定使每段代码独立工作的方法。您将看到,Windows PowerShell 提供了将那些段代码联系起来的绝佳方法。并且您将发现,一次处理多个小段代码,您就会更轻松地开发最终脚本。

从文件读取名称

确定在 Windows PowerShell 中读取文本文件的方法可能令人沮丧。如果我运行 Help *file*,那么我只能获得 Out-File cmdlet(用于将文本发送到文件)的帮助,而不会从中进行读取。根本没有帮助!但是,Windows PowerShell cmdlet 名称遵循一种使用时对自己有利的逻辑。Windows PowerShell 进行检索时,cmdlet 名称通常以 Get 开始。因此我在运行 Help Get* 显示出那些 cmdlet 后,就接着滚动列表并看到 Get-Content。看来大有希望!因此我运行 Help Get-Content 以了解更多与 Get-Content 的用途有关的信息(见图 1),并且看来 Get-Content 将能够满足我的需求。

图 1 运行 Help Get-Content 获得更多信息

图 1** 运行 Help Get-Content 获得更多信息 **(单击该图像获得较大视图)

Windows PowerShell 将几乎每件事情都作为对象来处理,文本文件也不例外。文本文件从技术上说是行的集合,文件中的每一行都可充当一种独立对象。因此,如果我已经创建了一个名为 C:\services.txt 的文本文件,并在其中填充了服务名称(在文件内部将每个名称放置在其本身的行上),Windows PowerShell 可以使用 Get-Content cmdlet 逐个读取这些名称。由于此次演练的目的是显示交互式开发脚本的方法,因此我首先只运行 Get-Content,为其赋予我的文本文件的名称,然后再查看发生了什么:

PS C:\> get-content c:\services.txt
messenger
alerter
PS C:\>

正如所料,Windows PowerShell 读取文件并显示名称。当然,只显示这些名称并不是我真正想要的,但现在我知道 Get-Content 正在以我所期望的方式工作。

更改服务

下一件我要做的事就是更改服务的启动模式。同样,我首先试图找到正确的 cmdlet。因此我运行 Help *Service*。此语句返回的列表很短,并且看起来只有 Set-Service cmdlet 才符合我的要求。在尝试将此 cmdlet 合并到脚本中之前,为了确保我了解它的工作方式,我要对其进行测试。运行 Help Set-Service,这将向我显示该 cmdlet 的工作方式,并且可通过快速测试进行确认:

PS C:\> set-service messenger -startuptype
    disabled
PS C:\>

组合各部分

现在我需要将从文件中读取服务名称的功能同 Set-Service cmdlet 相组合,并且这正是 Windows PowerShell 的功能强大的管道功能起作用之处。使用此管道,一个 cmdlet 的输出可以作为输入传递给第二个 cmdlet。管道传递整个对象。如果将对象的集合放入管道,那么每个对象将单独通过管道。这意味着,Get-Content 的输出(请记住,这是对象的集合)可以通过管道输送到 Set-Service。因为 Get-Content 传递的是集合,所以集合中的每个对象(或文本行)将单独通过管道输送到 Set-Service。其结果是对文本文件中的每一行都要执行一次 Set-Service。命令如下所示:

PS C:\> get-content c:\services.txt | 
 set-service -startuptype disabled
PS C:\>

以下为将发生的情况:

  1. Get-Content cmdlet 执行,从而读取整个文件。将文件中的每一行都作为唯一的对象进行处理,将它们放在一起就是对象的集合。
  2. 将对象的集合通过管道输送到 Set-Service。
  3. 对于每个输入对象,管道都执行一次 Set-Service cmdlet。对于每次执行,都要将输入对象作为 cmdlet 的第一个参数(即服务名称)传递到 Set-Service。
  4. Set-Service 使用其第一个参数的输入对象和指定的任何其他参数(在本例中是 -startuptype 参数)执行。

有趣的是,我实际上在此时已经完成了我的任务,可我甚至还不曾编写脚本。在 Cmd.exe 外壳中将很难实现相同的操作,而且需要若干行 VBScript 代码.但是 Windows PowerShell 可以在一行中处理所有的代码。我的工作仍然没有完全完成。正如您所见,我的命令并没有提供大量的状态输出或反馈。除了没有出错误外,很难看出是否真正发生了什么。既然我已经掌握了完成任务所需的功能,那么我就可以开始通过了解真正的脚本编写来使其更好看。

Michael Murgolo 所做的 Windows PowerShell Prompt Here

“Open Command Window Here”(在此处打开命令窗口)工具是最受欢迎的(也是我最喜爱的)Microsoft® PowerToys for Windows® 之一。“Open Command Window Here”(在此处打开命令窗口)可以作为 Microsoft PowerToys for Windows XP 的一部分或可在 Windows Server® 2003 Resource Kit Tools 中使用,使用它可以在 Windows 资源管理器中右键单击文件夹或驱动器以打开指向所选文件夹的命令窗口。

当我学习 Windows PowerShell 时,发现自己希望它具有相同的功能。因此,我从 Windows Server 2003 Resource Kit Tools 获取“Open Command Window Here”(在此处打开命令窗口)的安装 .inf 文件 cmdhere.inf,然后对其进行修改,以创建 Windows PowerShell Prompt Here 上下文菜单。此 .inf 文件包括在此边栏所基于的原始博客文章(可从 leeholmes.com/blog/PowerShellPromptHerePowerToy.aspx 获得)内。要安装此工具,只需右键单击 .inf 文件并选择“Install”(安装)即可。

创建此工具的 Windows PowerShell 版本时,我发现原来的版本有错误 — 如果卸载“Open Command Window Here”(在此处打开命令窗口),它将留下失效的上下文菜单项。因此,我已经提供了一个 cmdhere.inf 的更新版本(还可以从原始博客文章中获得)。

这两种 PowerToy 都利用了以下的事实:这些上下文菜单项在注册表中与 Directory 和 Drive 对象类型关联的注册表项下配置。执行此操作时采用的方式与上下文菜单操作与文件类型相关联的方式相同。例如,在 Windows 资源管理器中右键单击 .txt 文件后,您就可以在列表顶部获得几种操作(如“打开”,“打印”和“编辑”)。要了解这些项的配置方式,让我们看一下 HKEY_CLASSES_ROOT 注册表配置单元。

如果您打开注册表编辑器并展开了 HKEY_CLASSES_ROOT 分支,您就会看到根据文件类型命名的注册表项(如 .doc、.txt 等等)。如果单击 .txt 注册表项,您就会看到 [“Default”(默认)] 值为 txtfile(见图 A)。这是与 .txt 文件相关联的对象类型。如果向下滚动并展开 txtfile 注册表项,然后在其下展开 shell 注册表项,您将看到根据 .txt 文件的某些上下文菜单项命名的注册表项。(您将不会看到所有的注册表项,因为还有创建上下文菜单的其他方法)。在这些注册表项的每一个下面都有一个命令项。命令项下面的 [“Default”(默认)] 值是命令行,选择该上下文菜单项时由 Windows 执行该命令行。

用于 cmd 提示符和 Windows PowerShell 提示符的工具使用此技术来配置“Open Command Window Here”(在此处打开命令窗口)和 Windows PowerShell Prompt Here 上下文菜单项。没有任何文件类型与驱动器和目录相关联,但在 HKEY_CLASSES_ROOT 下却有与这些对象相关联的注册表项“Drive”(驱动器)和“Directory”(目录)。

图 A 在注册表中配置上下文菜单项

图 A** 在注册表中配置上下文菜单项 **(单击该图像获得较大视图)

Michael Murgolo 是 Microsoft Consulting Services 的一名高级基础结构顾问。他关注的领域有操作系统、部署、网络服务、Active Directory、系统管理、自动化和补丁管理。

交互式脚本编写

我需要做的一件事就是针对 C:\services.txt 中列出的每个服务执行多个 cmdlet。这样,我就可以输出自己所喜欢的服务名称和任何其他信息,从而跟踪脚本的进度。

之前,我使用管道将对象从一个 cmdlet 传递到另一个 cmdlet。但是,这一次我要使用的称为 Foreach 构造的技术(我在上个月的专栏中进行过介绍)更像脚本。Foreach 构造可以接受对象的集合,并且它将针对该集合中的每个对象执行多个 cmdlet。我指定了一个变量,该变量表示每次通过循环时的当前对象。例如,构造可能这样开始:

foreach ($service in get-content c:\services.txt)

我仍将执行相同的 Get-Content cmdlet 来检索文本文件的内容。这一次我要求 Foreach 构造遍历由 Get-Content 返回的对象的集合。每个对象执行一次循环,而当前对象则放在变量 $service 中。现在我只需要指定想在循环中执行的代码。我将首先尝试复制原始的单行命令(这将最小化复杂性并确保我不会丢失任何功能):

PS C:\> foreach ($service in get-content c:\services.txt) {
>> set-service $service -startuptype disabled
>> }
>>
PS C:\>

此时发生了一些有趣的事情。请注意,我使用一个左花括号结束了 Foreach 构造的第一行。左花括号和右花括号中的所有内容都被认为在 Foreach 循环内,并且针对输入集合中的每个对象都将执行一次该内容。在我键入 { 并按下 Enter 之后,请注意,Windows PowerShell 将其提示符更改为 >>(见图 2)。这表示它知道我已开始进行某种构造,并且正在等待我完成构造。接下来我键入我的 Set-Service cmdlet。这一次,我使用 $service 作为第一个参数,这是由于 $service 表示的是从我的文本文件读取的当前服务名称。在下一行,我使用右花括号结束构造。按 Enter 两次后,Windows PowerShell 立即执行我的代码。

图 2 Windows Powershell 知道已启动某构造

图 2** Windows Powershell 知道已启动某构造 **(单击该图像获得较大视图)

没错,立即执行。我键入的内容看起来像脚本,但 Windows PowerShell 实际上正在实时执行它,而且它并没有存储在文本文件中的任何地方。现在我将重新键入全部内容,添加一行代码以输出当前服务名称:

PS C:\> foreach ($service in get-content c:\services.txt) {
>> set-service $service -startuptype disabled
>> "Disabling $service"
>> }
>>
Disabling messenger
Disabling alerter
PS C:\>

请注意,我正在要求它显示内容,并且我已经将要显示的内容括在双引号内。引号告诉 Windows PowerShell,这是一个文本的字符串,而不是另一个命令。但是,当您使用双引号(而不是单引号)时,Windows PowerShell 会扫描文本字符串中的任何变量。如果它找到了任何变量,它会将变量名替换为变量的实际值。因此,执行此代码时,您可以看到正在显示当前服务名称。

这仍不是脚本!

到现在为止,我一直在交互式地使用 Windows PowerShell,这是立即看到我所编写的内容的结果的好方法。但是,在某些时候,重新键入这些代码行将变得十分单调乏味。这是 Windows PowerShell 也可以运行脚本的原因。实际上,Windows PowerShell 脚本遵循的是脚本一词最为文字化的定义:Windows PowerShell 基本上只在脚本文本文件中进行读取,然后“键入”它所发现的每一行,看来与手动键入行极其相似。这意味着到现在为止我所执行的每项内容都可以粘贴到脚本文件中。因此,我将使用记事本创建名为 disableservices.ps1 的文件,然后在其中粘贴以下内容:

foreach ($service in get-content c:\services.txt) {
 set-service $service -startuptype disabled
 "Disabling $service"
 }

我已将此文件放在名为 C:\test 的文件夹中。现在我将试图从 Windows PowerShell 内部运行此文件:

PS C:\test> disableservices
'disableservices' is not recognized as a cmdlet, function, operable program, or
<script file.
At line:1 char:15
+ disableservices <<<<
PS C:\test>

哎哟。出什么问题啦?Windows PowerShell 现在显示位于 C:\test 文件夹,但它并没有找到我的脚本。这是为什么?由于安全约束,Windows PowerShell 设计为不能运行来自当前文件夹的任何脚本,以防止任何脚本劫持操作系统命令。例如,我无法创建一个名为 dir.ps1 的脚本并用它覆盖正常的 dir 命令。如果我需要运行当前文件夹中的脚本,就必须指定相对路径如下:

PS C:\test> ./disableservices
The file C:\test\disableservices.ps1 cannot be loaded. 
The execution of scripts is disabled on this system. 
Please see "get-help about_signing" for more details.
At line:1 char:17
+ ./disableservices <<<<
PS C:\test>

现在要做什么?它仍然不正常工作。我已经指定了正确的路径,但 Windows PowerShell 却说它无法运行脚本。那是因为,默认情况下,Windows PowerShell 无法运行脚本。同样,这个安全措施旨在防止发生我们使用 VBScript 时已经遇到的问题。默认情况下,恶意脚本无法在 Windows PowerShell 下执行,这是因为在默认情况下任何脚本都不能执行!为了运行脚本,我需要显式更改执行策略:

PS C:\test> set-executionpolicy remotesigned

使用 RemoteSigned 执行策略,未签名的脚本可以从本地计算机执行(下载的脚本仍然必须签名后才可运行)。更好的策略是 AllSigned,该策略仅执行已使用受信任发布方所颁发的证书进行过数字签名的脚本。但是,我手边没有证书,因此我无法对我的脚本进行签名,这种情况下 RemoteSigned 就是一种不错的选择。现在我再次试图运行我的脚本:

PS C:\test> ./disableservices
Disabling messenger
Disabling alerter
PS C:\test>

我要指出的是,我们正在使用的 RemoteSigned 执行策略并不是一个极佳的选择,它只是一个较好的选择而已。但是还有一个更好的解决方案。获取代码签名的证书,使用 Windows PowerShell Set-AuthenticodeSignature cmdlet 来对我的脚本进行签名,然后将执行策略设置为更加安全的 AllSigned 策略,这样做将安全得多。

还有另一种策略 Unrestricted(您一定要避免使用)。使用此策略,所有的脚本(甚至来自远程位置的恶意脚本)可以不受限制地在您的计算机上运行,这会将您置于很危险的境地。因此,我建议无论如何都不要使用 Unrestricted 策略。

即时结果

使用 Windows PowerShell 中的交互式脚本编写功能,您可以快速地建立脚本的原型或甚至只是小段脚本的原型。您获得了即时结果,因此可以轻松地调整您的脚本以获得真正想要的结果。操作结束时,您可以将代码移动到 .ps1 文件中,使其成为永久代码并且将来也容易找到。还要记住:理想情况下,您应该对那些 .ps1 文件进行数字签名,从而您可以将 Windows PowerShell 设置为 AllSigned 执行策略,这是允许脚本执行的最安全的策略。

Don Jones 是 SAPIEN Technologies 的首席脚本权威和《Windows PowerShell: TFM》的合著者(网址为 www.SAPIENPress.com)。请通过 don@sapien.com 与 Don 联系。

© 2008 Microsoft Corporation 与 CMP Media, LLC.保留所有权利;不得对全文或部分内容进行复制.