一个月的-Powershell-学习指南-全-

一个月的 Powershell 学习指南(全)

原文:Learn PowerShell in a Month of Lunches

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

前言

坐下来写这篇前言时,我的第一个想法是,“哇,这里有很多东西要解释。”我自己的 PowerShell 之旅始于 2005 年,大约在 2006 年 TechEd 巴塞罗那大会发布 Shell 之前一年。“在一个月的午餐中学习 Windows PowerShell”,这本书的鼻祖,远非我写的第一本 PowerShell 书籍。我和 Jeff Hicks 首先与 SAPIEN Technologies 合作编写了三版《Windows PowerShell: TFM》。在那之后,我实际上做出了一个决定——如果你能想象的话,就是不再写任何 PowerShell 书籍!但很快我就意识到,当时现有的 PowerShell 书籍选择——那时已经超过十几本——缺少了一个主要受众的需求。那时的书籍都是将 PowerShell 作为一种编程语言来教授,目标是当时相当庞大的 VBScript 编程人员群体。但 PowerShell 本身的目标是面向一个更大、更广泛的受众:非程序员。

那时我开始使用在实时 PowerShell 课程中使用的叙述,并开始构建一本新书:一本不会在第三章就直接进入控制流语句的书,一本会真正专注于最佳教学序列,使学习 PowerShell 尽可能容易。我想做出并保持一个承诺:每天给我一个小时,一个月后,我会让你在 PowerShell 中变得功能实用。

《午餐月》作为一套书系,经历了一段坎坷的道路。图书出版商的利润微薄,而推出一套新书需要消耗大量资源。最初,有另一家出版社决定尝试这个系列,但在最后一刻退缩了。Manning 出版社——我希望这对他们来说是一个出色的决定——站了出来,说:“让我们试试吧。”我们开发了一个新的封面艺术概念,这与公司的常规风格截然不同,展示了他们愿意如何创造性地思考这个新系列。

《在一个月的午餐中学习 Windows PowerShell》是一本成功的书籍,成为世界上最畅销的 PowerShell 书籍之一。它已被翻译成多种不同的语言,对许多人来说,这是他们第一次接触 PowerShell。我收到了成千上万的人的来信,讲述了这本书如何帮助他们进入 PowerShell 世界。对许多人来说,这是他们第一次 PowerShell 学习经历。对大多数人来说,这并不是他们的最后一次,我为这么多人信任我和 Jeff 开始他们的 PowerShell 之旅而感到自豪。

当微软最终将 PowerShell 开源(!!!!!)并且跨平台(!!!!!!!!!)时,我们知道是时候推出一本新的《午餐月》书籍了——一本专注于 PowerShell 而不是仅仅 Windows PowerShell 的书籍。

但到了那时,我和 Jeff 对写作都有些厌倦了。我的职业生涯正在向不同的方向发展——我在公司接受了一个副总裁的角色,我知道我将很难跟上 PowerShell 快速变化和扩展的世界。我的最后一本书,《Shell of an Idea: The Untold History of PowerShell》,在许多方面是对社区和产品团队的一封情书,他们已经支持了十多年,并且一直是朋友。它讲述了 PowerShell 仅仅勉强诞生的故事,当我写作时,我知道我将没有时间再写关于 PowerShell 未来的任何东西了。

正因如此,我很高兴这本书的作者们站了出来。PowerShell 社区普遍充满了极其慷慨的人,总是愿意回答问题并帮助你。“退居二线”,对我来说,也意味着从 PowerShell.org 这个我共同创立的网站和背后的非营利组织退出来。它意味着从 PowerShell + DevOps 全球峰会退出来,这是我最初用美国运通卡资助的会议。但 PowerShell 社区像他们几乎总是做的那样站了出来:新人们同意不仅让组织继续运行,而且还要让它增长。这本书的合著者之一 James Petty 就是这些人之一,我永远感激他和他的团队让社区精神保持活力。

这本书在很大程度上是基于我为第一本《午餐月 PowerShell》标题所创作的叙事,以及我和 Jeff Hicks 在三个版本中共同完善的——包括《午餐月 PowerShell 脚本学习》,这本书今天和当初我们写的时候一样相关。但这本书突破了 Windows 操作系统的局限,将 PowerShell 视为一个真正的全球公民:无论你在 Windows、Linux 还是 macOS 上运行 PowerShell,你都会找到适用的例子——这对于这些作者来说是一项巨大的工作,考虑到这些操作系统之间的明显差异。

我对 PowerShell 社区始终怀有无比的感激之情。他们让我感到受欢迎、受尊重和有价值——这是我希望每个人都能在某一点上体验到的。这是一个我鼓励你们去探索的社区,无论是通过 PowerShell.org 还是众多其他由志愿者驱动的网站、GitHub 仓库、Twitter 账户以及其他渠道。你会发现这样做是值得的。

最后,我想让你知道,你在 PowerShell 上的时间投资将带来令人印象深刻的回报。PowerShell 几乎在软件中是独一无二的,它没有寻求重新发明任何东西。相反,它只是想将世界上已经存在的混乱、疯狂、强大的东西变得更为一致和易于使用。例如,PowerShell 尊重 Linux,它没有试图将微软的世界观强加给那个操作系统。相反,PowerShell 只是让 Linux 已经存在的一切变得稍微容易操作一些。

我希望你的 PowerShell 之旅,无论它刚刚开始还是你已经深入其中,都能像我的旅程一样富有成效和令人满意。我希望你能支持这本书的作者,因为他们付出了巨大的努力,将这本书带到你的手中。我还希望你能利用你新获得的知识,找到方法与他人分享,他们可能刚刚开始自己的旅程。无论已经说过或写过的关于 PowerShell 的内容如何,你自己的看法将证明是帮助某人获得“啊哈!”时刻的那个,从而启动他们自己的 PowerShell 成功之旅。

——唐·琼斯

前言

我从未想过有一天我会被要求帮助编写任何技术书籍,更不用说《一个月午餐学会 PowerShell》第四版了——这本书正是多年前开启我旅程的起点。

当我听说特拉维斯·普伦克和泰勒·莱昂哈特加入了梅宁出版社,将撰写该成功书籍的第四版时,我想:“谁比 PowerShell 团队中的这两个人更适合编写下一版呢?”为了保留早期版本中唐·琼斯和杰弗里·希克斯的获胜风格,泰勒和特拉维斯将基于他们原始的章节进行工作。然而,由于 PowerShell 现在可在 Linux 和 macOS 上使用,因此这本书将专注于这两个操作系统,并展示 PowerShell 7 的开源/跨平台能力。这本书的名称也将改为《一个月午餐学会 PowerShell》,而不是经典书籍第一、第二和第三版的标题《一个月午餐学会 Windows PowerShell》。我很高兴看到这个扩展和更新的版本,我购买了 MEAP,并随着章节的发布而阅读。

快进一年。PowerShell 7 发布了。此外,本书的读者和早期审稿人明确表示,Windows 仍然是大多数管理员花费时间最多的操作系统,因此他们希望第四版包括 Windows 以及 Linux 和 macOS。因此,我被邀请加入团队来完成这本书,并更新它以涵盖最新的 PowerShell 和 Windows 操作系统版本。我继承了 Tyler 和 Travis 的成果,确保三个操作系统都得到了代表。这本书最终确实有点偏重 Windows,这是可以预料的,因为,再次强调,PowerShell 在 Windows 上仍然具有更多的功能。我来自企业环境,每天使用 PowerShell 来支持 Windows 服务器。

对于我来说,从阅读第一本书到帮助编写这一版,这是一段令人惊叹的旅程。无论你是刚开始接触 PowerShell,还是经验丰富的管理员在寻找最新的技巧和窍门,我都希望你喜欢这本书。

——詹姆斯·佩蒂

致谢

我要向我的妻子 Kacie 表示衷心的感谢,感谢她在整个项目期间的支持。我还要感谢 Don Gannon-Jones 的支持、指导以及鼓励我加入这个项目。对我来说,从阅读 Don 的第一本关于 PowerShell 的《午餐月》到帮助编写这一版,这是一次令人难以置信的完整旅程。

我还要感谢 Manning 出版社的员工:Deirdre Hiam,我的项目编辑;Carrie Andrews,我的校对编辑;Katie Tennant,我的校对员;以及 Shawn Bolan,我的技术校对员。

致所有审稿人:Aldo Solis Zenteno、Birnou Sebarte、Brad Hysmith、Bruce Bergman、Foster Haines、Giuliano Latini、James Matlock、Jan Vinterberg、Jane Noesgaard Larsen、Jean-Sebastien Gervais、Kamesh Ganesan、Marcel van den Brink、Max Almonte、Michel Klomp、Oliver Korten、Paul Love、Peter Brown、Ranjit Sahai、Ray Booysen、Richard Michaels、Roman Levchenko、Shawn Bolan、Simon Seyag、Stefan Turalski、Stephen Goodman、Thein Than Htun 和 Vincent Delcoigne,感谢你们。你们的建议帮助使这本书变得更好。

关于本书

我们很高兴你决定加入我们这个为期一个月的旅程!一个月看起来时间很长,但我们保证这将是值得的。

适合阅读本书的人群

本书面向广泛的读者群体;然而,主要受众是刚开始接触 PowerShell 的人。工作职能可能包括帮助台或服务器管理员。

你所需的大部分初步信息都在第一章中介绍,但这里有一些我们应该提前说明的事情。首先,我们强烈建议你跟随书中的示例进行操作。为了获得最佳体验,我们建议你在虚拟机上运行所有内容。我们已经尽力确保示例是跨平台的,但正如你将看到的,有几章是针对 Windows 特定的。

其次,准备好从头到尾阅读这本书,按顺序覆盖每一章。再次强调,我们将在第一章中更详细地解释这一点,但理念是每一章都会介绍一些你将在后续章节中需要的新内容。你真的不应该试图一口气读完整本书——坚持每天一章的方法。人类大脑一次只能吸收有限的信息,通过小块小块地学习 PowerShell,你实际上会学得更快、更彻底。

关于代码

本书包含大量的代码片段。大多数都很短,你应该能够轻松地输入它们。实际上,我们建议你亲自输入它们,因为这样做将有助于加强一个重要的 PowerShell 技能:准确的输入!较长的代码片段在列表中给出,并且可以从出版社网站上的本书网页下载,网址为www.manning.com/books/learn-powershell-in-a-month-of-lunches

话虽如此,你应该注意一些约定。代码总是以特殊字体显示,就像这个例子一样:

Get-CimInstance –class Win32_OperatingSystem 
➥ –computerName SRV-01

那个例子也说明了本书中使用的行续字符。它表示这两行应该作为一行在 PowerShell 中输入。换句话说,在输入 Win32_OperatingSystem 后不要按 Enter 或 Return 键——继续输入。PowerShell 允许长行,但本书的页面只能容纳这么多。

有时代码也会加粗以突出显示与章节中先前步骤不同的代码,例如当新功能添加到现有代码行时。有时你也会在文本本身中看到这种代码字体,例如当我们写 Get-Command 时。这只是为了让你知道你正在查看一个命令、参数或其他你会在 shell 中输入的元素。

第四,你将看到我们在几个章节中都会提到的元素:反引号字符(`)。以下是一个例子:

Invoke-Command –scriptblock { Get-ChildItem } `
   -computerName SRV-01,localhost,DC02

第一行末尾的字符不是一个多余的墨迹——它是一个真正的字符,你需要输入。在美国键盘上,反引号(或重音符号)通常位于左上角,在 Esc 键下方,与波浪符字符(~)在同一键上。当你看到代码列表中的反引号时,要按原样输入。此外,当它在行末出现,如前面的例子所示时,确保它是那一行的最后一个字符。如果你在它之后允许出现任何空格或制表符,反引号将无法正常工作,代码示例也将无法正常工作。

你可以从这本书的 liveBook(在线)版本中获取可执行的代码片段,网址为 livebook.manning.com/book/learn-powershell-in-a-month-of-lunches。书中示例的完整代码可以从 Manning 网站下载,网址为 www.manning.com/books/learn-powershell-in-a-month-of-lunches,以及 GitHub 上 github.com/psjamesp/Learn-PowerShell-in-a-Month-of-Lunches-4th-Edition

liveBook 讨论论坛

购买《Learn PowerShell in a Month of Lunches,第四版》包括对 liveBook 的免费访问,Manning 的在线阅读平台。使用 liveBook 的独家讨论功能,您可以在全球范围内或针对特定章节或段落附加评论。为自己做笔记、提问和回答技术问题,以及从作者和其他用户那里获得帮助都非常简单。要访问论坛,请访问 livebook.manning.com/book/learn-powershell-in-a-month-of-lunches/discussion。您还可以在 livebook.manning.com/discussion 了解更多关于 Manning 论坛和行为准则的信息。

Manning 对读者的承诺是提供一个平台,让读者之间以及读者与作者之间可以进行有意义的对话。这并不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们鼓励您向他们提出一些挑战性的问题,以免他们的兴趣偏离!只要书籍在印刷中,论坛和先前讨论的存档将可通过出版社的网站访问。

关于作者

James Petty 是 DevOps Collective 的总裁兼首席执行官,该组织运营 PowerShell.org,同时也是微软云和数据中心 MVP。他组织了每年四月举办的 PowerShell + DevOps 全球峰会以及秋季举办的 DevOps + Automation 峰会,并帮助在美国各地举办了多次 PowerShell 周六活动。他是《PowerShell 会议手册》第一卷的撰稿人,也是 Chattanooga PowerShell 用户组的共同创始人,以及田纳西州查塔努加举办的为期两天的 PowerShell 会议“PowerShell on the River”的联合主席。

Travis Plunk 自 2013 年以来一直在多个 PowerShell 团队担任软件工程师,自 1999 年以来在微软工作。他参与了 PowerShell 的开源,并在 PowerShell 成为开源后不久就将核心 PowerShell 引擎迁移了过来。

Tyler Leonhard 在 PowerShell 团队担任软件工程师大约两年,在微软工作了近三年。他是 VS Code 中 PowerShell 扩展的核心维护者,同时也是 PowerShell 团队成员,活跃在社交媒体(Twitter、Twitch 直播、LinkedIn)上。

杰弗里·希克斯是一位拥有 30 年以上经验的 IT 老兵,其中大部分时间都在担任 IT 基础设施顾问,专注于微软服务器技术,强调自动化和效率。他是多年微软 MVP 奖的获得者。如今,他作为一名独立作家、教师和顾问工作。杰夫向全球的 IT 专业人士传授了 PowerShell 和自动化的好处。他撰写并合著了多本书籍,为众多在线网站和印刷出版物撰稿,是 Pluralsight 的作者,并在技术会议和用户组中频繁演讲。您可以在 Twitter 上关注杰夫(twitter.com/JeffHicks)。您可以在jdhitsolutions.com/blogjeffhicks.substack.com找到他的在线作品。

1 在开始之前

PowerShell 刚刚满 15 岁(截至 2021 年 11 月 14 日)。很难相信它已经存在这么长时间了,但仍有大量 IT 人员尚未使用它。我们理解——一天中只有这么多时间,你已经习惯了按照一贯的方式做事。或者也许你的网络安全官不允许你开启 PowerShell,因为它只能被坏人使用。无论如何,我们很高兴你能加入我们的冒险。我们已经在使用 PowerShell 很长时间了。实际上,我们中的两位,James 和 Tyler,实际上是从这本书的早期版本中学习 PowerShell 的。

大约在 2009 年,整个行业发生了巨大的转变,当时人们对 PowerShell 有了新的认识。它既不是一种脚本语言,也不是一种编程语言,因此我们教授 PowerShell 的方式也需要改变。实际上,PowerShell 是一个命令行外壳,在这里你可以运行命令行实用程序。像所有好的外壳一样,它具有脚本功能,但你不必使用它们,而且你当然也不必它们开始。

这本书的前几版是那种文化转变的结果,我们今天仍然保持同样的心态。这是我们迄今为止为那些可能没有脚本背景的人教授 PowerShell 的最佳方法(尽管如果你有脚本背景当然更好)。但在我们开始教学之前,让我们为你设定好舞台。

1.1 为什么你不能再忽视 PowerShell

批处理。KiXtart。VBScript。让我们面对现实,PowerShell 并非微软(或任何其他人)在为 Windows 管理员提供自动化能力方面的第一次努力。我们认为了解为什么你应该关注 PowerShell 是很有价值的——当你这样做时,你投入学习 PowerShell 的时间将会得到回报。让我们先回顾一下 PowerShell 出现之前的生活是什么样的,并看看使用这个外壳的一些优点。

1.1.1 没有 PowerShell 的生活

Windows 管理员总是乐于在图形用户界面(GUI)中点击来完成任务。毕竟,GUI 大部分是 Windows 的全部意义所在——操作系统毕竟不是叫 Text。GUI 很好,因为它们能让你发现你能做什么。你还记得第一次打开 Active Directory 用户和计算机吗?也许你悬停在图标上阅读工具提示,下拉菜单,右键点击东西,所有这些只是为了看看有什么可用。GUI 让学习工具变得更容易。不幸的是,GUI 对投资回报为零。如果你花 5 分钟在 Active Directory 中创建一个新用户(假设你填写了很多字段,这是一个合理的估计),你将永远不会比这更快。一百个用户将需要 500 分钟——除非你学会更快地打字和点击,否则这个过程不会变得更快。

微软试图以某种随意的方式处理这个问题,VBScript 可能是其最成功的尝试。你可能需要一个小时来编写一个 VBScript 脚本,可以从 CSV 文件中导入新用户,但一旦你投入了一个小时,未来创建用户只需几秒钟。VBScript 的问题在于微软并没有全力以赴地支持它。微软必须记得让事情对 VBScript 可访问,而当开发者忘记(或没有时间)时,你就会陷入困境。想通过 VBScript 更改网络适配器的 IP 地址?好吧,你可以。想检查其链路速度?你不能,因为没有人在 VBScript 可以访问的方式中将其连接起来。抱歉。Windows PowerShell 的架构师 Jeffrey Snover 将这称为“最后一公里”。你可以用 VBScript(以及其他类似技术)做很多事情,但它往往会让你在某一点上失望,永远无法到达终点线。

Windows PowerShell 是微软为了做得更好并帮助用户跨越最后一公里而做出的快速尝试。到目前为止,这是一个成功的尝试。微软内部的数十个产品组已经采用了 PowerShell,一个广泛的第三方生态系统依赖于它,一个全球的专家和爱好者社区每天都在推动 PowerShell 的发展。

1.1.2 使用 PowerShell 生活的体验

微软对 Windows PowerShell 的目标是构建产品 100% 的管理功能。微软继续构建 GUI 控制台,但这些控制台在幕后执行 PowerShell 命令。这种方法迫使公司确保你可以通过 PowerShell 访问产品上可以做的每一件事。如果你需要自动化重复性任务或创建 GUI 无法很好地实现的过程,你可以进入 PowerShell 并完全控制它。

几年来,包括 Exchange、SharePoint、System Center 产品、Microsoft 365、Azure 以及不要忘记 Windows Admin Center 在内的几个微软产品已经采用了这种方法。非微软产品,包括亚马逊网络服务(AWS)和 VMware,也对 PowerShell 表示了浓厚的兴趣。

Windows Server 2012,PowerShell v3 最初引入的地方,以及更高版本几乎完全由 PowerShell 或 PowerShell 之上的 GUI 来管理。这就是为什么你不能忽视 PowerShell:在过去的几年里,PowerShell 已经成为更多和更多管理的基础。它已经成为包括所需状态配置(DSC)在内的许多高级技术的基石。PowerShell 到处都是!

自问一下这个问题:如果我是负责一支 IT 管理员团队(也许你就是)的人,我会希望谁担任高级、薪酬更高的职位?是每次需要执行任务时需要花费几分钟点击图形用户界面(GUI)的管理员,还是那些在自动化任务后只需几秒钟就能完成任务的管理员?我们从 IT 界的几乎所有其他部分都知道答案。问问思科管理员、AS/400 操作员或 UNIX 管理员。答案是,“我更愿意有一个人能够从命令行更高效地运行事物。”展望未来,Windows 世界将开始分为两组:能够使用 PowerShell 和不能使用 PowerShell 的管理员。我们最喜欢的来自微软 2010 年 TechEd 大会上的 Don Gannon-Jones 的一句话是,“你的选择是 学习 PowerShell,还是 想要搭配薯条吗?”我们很高兴你决定加入我们,与我们一起学习 PowerShell!

1.2 Windows、Linux 和 macOS,哦,我的天

在 2016 年中旬,微软做出了前所未有的决定,开源 PowerShell 版本 6(当时称为 PowerShell Core)。同时,它发布了不带 Windows 后缀的 PowerShell 版本,以及针对 macOS 和多个 Linux 版本的构建。太棒了!现在,以对象为中心的 shell 可在许多操作系统上使用,并且可以由全球社区进行发展和改进。因此,对于这本书的这一版,我们尽力展示了 PowerShell 的多平台使用,并包括了 macOS 和 Linux 环境的示例。我们仍然认为 PowerShell 的最大受众将是 Windows 用户,但我们还想要确保你了解它在其他操作系统上的工作方式。

我们尽力使这本书中的所有内容都具备跨平台兼容性。然而,截至本书编写时,Linux 和 macOS 上可用的命令仅有 200 多个,因此我们想要展示的内容并不都能工作。考虑到这一点,我们特别指出第十九章和第二十章,因为它们完全专注于 Windows。

1.3 这本书适合你吗?

这本书并不试图满足所有人的需求。微软的 PowerShell 团队大致定义了三个使用 PowerShell 的受众群体:

  • 主要运行命令并使用他人编写的工具的管理员(无论操作系统)

  • 将命令和工具组合成更复杂的过程,并可能将这些过程打包成经验较少的管理员可以使用的工具的管理员(无论操作系统)

  • 管理员(无论操作系统)和创建可重用工具和应用的开发者

这本书主要面向第一个受众群体。我们认为,即使是开发者,了解 PowerShell 如何运行命令也是有价值的。毕竟,如果你要创建自己的工具和命令,你应该了解 PowerShell 使用的模式,因为它们允许你创建在 PowerShell 中运行得尽可能好的工具和命令。

如果你感兴趣于创建脚本来自动化复杂的过程,例如新用户配置,那么你将在本书的结尾看到如何做到这一点。你甚至将看到如何开始创建其他管理员可以使用的命令。但本书不会深入探讨 PowerShell 可能做到的所有事情。我们的目标是让你开始使用 PowerShell,并在生产环境中有效地使用它。

我们还将向你展示几种使用 PowerShell 连接到外部管理技术的方法;远程操作和与通用信息模型(CIM)类以及正则表达式交互是两个很快就能想到的例子。大部分情况下,我们将只介绍这些技术,并专注于 PowerShell 如何连接到它们。这些主题值得有它们自己的书籍(并且已经有了),所以我们只专注于 PowerShell 这一方面。如果你想要自己探索这些技术,我们会提供一些建议。简而言之,这本书并不是你用来学习 PowerShell 的最后一本书,而是设计为非常好的第一步。

1.4 如何使用本书

这本书的核心理念是每天阅读一章。你不必在午餐时阅读,但每个章节应该只需要大约 40 分钟来阅读,这样你就有额外的 20 分钟来吃完三明治并练习本章展示的内容。

1.4.1 章节内容

在本书的章节中,第二章到第二十六章包含主要内容,为你提供了 25 天的午餐期待。你可以在大约一个月内完成本书的主要内容。尽可能坚持这个计划,不要觉得有必要在给定的一天阅读额外的章节。更重要的是,你花些时间练习每一章展示的内容,因为使用 PowerShell 将有助于巩固你所学的知识。不是每个章节都需要整整一个小时,所以有时你可以在回到工作之前有额外的时间练习(以及享用午餐)。我们发现,当人们每天只专注于一个章节时,他们学得更快,因为这给了你的大脑时间去思考新想法,并给你时间去自己练习。不要急于求成,你可能会发现自己比想象中进步得更快。第二十七章提供了在 PowerShell 之旅中下一步去哪里的一些想法。最后,我们包括附录“PowerShell 速查表”,这是本书正文中提到的所有“陷阱”的汇编;当你想要找到某样东西但记不起在哪里找时,请以此作为参考。

1.4.2 实践实验室

大多数主要内容章节都包含一个供你完成的简短实验室。你将获得指示,也许还有一两个提示。这些实验室的答案出现在每个章节的末尾。但请尽量在没有查看答案的情况下完成每个实验室。

1.4.3 补充材料

我们制作了一个与本书相关的视频:Tyler 的“如何在 PowerShell 中导航帮助系统”;它可以在 Manning 的免费内容中心找到(mng.bz/enYP)。

我们还建议访问由 James 运营的 PowerShell.org,以及它的 YouTube 频道 YouTube.com/powershellorg,其中包含大量的视频内容。你可以找到 PowerShell + DevOps 全球峰会活动的记录会议、在线社区网络研讨会以及更多内容。全部免费!

1.4.4 进一步探索

本书中的几章只是简要介绍了某些酷炫的技术,我们在这些章节的结尾提出了建议,让你自己探索这些技术。我们指出了额外的资源,包括你可以根据需要使用的免费资源,以扩展你的技能集。

1.4.5 超越

在学习 PowerShell 的过程中,我们经常想要走一些弯路,探索为什么某些事情会以这种方式工作。我们并没有通过这种方式学到很多额外的实用技能,但我们确实对 PowerShell 是什么以及它是如何工作的有了更深入的理解。我们在书中的一些“超越”部分包含了这些旁路信息。这些内容不会让你花超过几分钟的时间去阅读,但如果你是那种喜欢知道为什么某事会以这种方式工作的人,它们可以提供一些有趣的额外事实。如果你觉得这些部分可能会让你分心,第一次阅读时可以忽略它们。你总是可以在掌握章节的主要材料之后回来探索它们。

1.5 设置你的实验室环境

在本书中,你将要在 PowerShell 中进行大量的实践,因此你需要一个实验室环境来进行工作。请勿在公司生产环境中进行实践。

要运行本书中的大多数示例以及完成所有实验室,你只需要安装了 PowerShell 7.1 或更高版本的 Windows 的一份副本。我们建议使用 Windows 10 或更高版本,或者 Windows Server 2016 或更高版本,这两者都自带 PowerShell v5.1。如果你要玩 PowerShell,你将不得不投资于一个包含 PowerShell 的 Windows 版本。对于大多数实验室,我们为你的 Linux 环境提供了额外的说明。

注意:你必须单独下载和安装 PowerShell 7,因为它与预装在 Windows 中的 Windows PowerShell 5.1 并行运行。然而,大多数这些实验室都可以在 Windows PowerShell 中运行。有关如何安装 PowerShell 7 的说明可以在 mng.bz/p2R2 找到。

我们还将使用带有最新稳定版本的 PowerShell 扩展的 Visual Studio Code (VS Code)。这个扩展可以从市场安装。如果你使用的是非 Windows 版本的 PowerShell,你将需要考虑的选项会更少。只需从 github.com/PowerShell/PowerShell 获取适合你版本 macOS 或 Linux(或其他)的正确构建版本,你应该就可以正常使用了。然而,请注意,我们将在示例中使用的大量 功能 是 Windows 独有的。例如,在 Linux 上无法获取服务列表,因为 Linux 没有服务(它有守护进程,与它们类似),但我们将尽力使用跨平台示例(例如 Get-Process)。

小贴士 你应该能够使用单台运行 PowerShell 的计算机完成这本书中的所有内容,尽管有些内容如果你有两三台计算机,且它们都在同一个域中,会更有趣。

1.6 安装 PowerShell

如果你目前还没有安装 PowerShell 7,没关系。我们将在下一章中讲解如何进行安装。如果你想检查 PowerShell 的最新可用版本或下载它,请访问 docs.microsoft.com/en-us/powershell。这个官方的 PowerShell 主页提供了最新发布版本及其安装方法。

小贴士 你应该检查你的 PowerShell 版本:打开 PowerShell 控制台,输入 $PSVersionTable 并按 Enter。

在继续之前,请花几分钟时间自定义 PowerShell。如果你使用的是基于文本的控制台宿主,我们强烈建议你将默认控制台字体更改为 Lucida fixed-width 字体。默认字体使得区分 PowerShell 使用的某些特殊标点符号变得困难。按照以下步骤自定义字体:

  1. 点击控制框(即控制台窗口左上角的 PowerShell 图标)并从菜单中选择属性。

  2. 在出现的对话框中,浏览各个选项卡以更改字体、窗口颜色、窗口大小和位置等。

小贴士 确保窗口大小和屏幕缓冲区具有相同的宽度值。

你的更改将应用于默认控制台,这意味着当你打开新窗口时,这些更改将保留。当然,所有这些仅适用于 Windows:在非 Windows 操作系统上,你通常会安装 PowerShell,打开你的操作系统的命令行(例如,一个 Bash shell),然后运行 powershell。你的控制台窗口将决定你的颜色、屏幕布局等,因此请根据你的喜好进行调整。

1.7 联系我们

我们热衷于帮助像你这样的人学习 Windows PowerShell,并尽力提供尽可能多的资源。我们也感谢你的反馈,因为这有助于我们提出新的资源想法,我们可以将这些资源添加到网站上,以及改进本书未来版本的方法。在 Twitter 上,你可以联系 Travis (@TravisPlunk),Tyler (@TylerLeonhardt) 和 James (@PsJamesP)。如果你有 PowerShell 的问题,我们还在 forums.powershell.org 的论坛上活跃。另一个获取更多资源的绝佳地方是 powershell.org,它包括免费的电子书、现场会议、免费网络研讨会以及更多内容。James 帮助管理这个组织,我们强烈推荐它作为你在完成本书后继续 PowerShell 学习的地方。

1.8 使用 PowerShell 立即生效

立即 生效 是我们为这本书设定的主要目标。尽可能多,每一章都聚焦于你可以在实际生产环境中立即使用的内容。这意味着我们有时会跳过一些初始细节,但必要时我们承诺会在适当的时候回顾并覆盖这些细节。在许多情况下,我们不得不在用 20 页的理论知识开场,还是直接深入并完成某事而不解释所有细微差别、注意事项和细节之间做出选择。当这些选择出现时,我们几乎总是选择直接深入,目标是让你 立即生效。但所有这些重要细节和细微差别都将在本书的后续部分进行解释。

好了,背景介绍就到这里。现在是时候开始立即生效了。你的第一堂午餐课程正在等待。

2 探索 PowerShell

本章全部关于让您熟悉环境,并帮助您决定您将使用哪个 PowerShell 界面(是的,您有选择)。如果您之前使用过 PowerShell,这些材料可能看起来有些重复,所以请随意浏览本章——您可能仍然会在这里和那里找到一些有用的信息。

此外,本章仅适用于 Windows、macOS 和 Ubuntu 18.04 上的 PowerShell。其他 Linux 发行版有类似的设置,但本章不会涉及。对于其他安装说明,您可以直接从 PowerShell 的 GitHub 页面github.com/PowerShell/PowerShell#获取。

有用术语

我们应该定义一些在本章中会大量使用的术语。

PowerShell—指的是您已安装的 7.x 版本。

Shell Shell 基本上是一个可以接受基于文本的命令的应用程序,通常用于通过脚本或终端等交互式体验与您的计算机或其他机器进行交互。Shell 的例子包括 Bash、fish 或 PowerShell。

Terminal—终端是一个可以在其中运行 shell 应用程序的应用程序,这样用户就可以以可视化的方式与 shell 进行交互。终端是 shell 无关的,因此您可以在任何终端中运行任何 shell。

Windows PowerShell—指的是预装在您的 Windows 10 设备上的 PowerShell 5.1。

2.1 Windows 上的 PowerShell

PowerShell 自 Windows 7(以及 Server 2008)以来就预装在 Windows PC 上。需要注意的是,Windows 上 PowerShell 7 的进程名已更改。它不再是powershell.exe,而是pwsh.exe。PowerShell 7 是并行安装,这意味着 Windows PowerShell(5.1)仍然默认安装(这就是为什么进程名必须更改)。

让我们先安装 PowerShell 7。安装此软件有多种方式(例如,从 Microsoft Store、winget、Chocolatey),因此您可以选择您喜欢的任何方法,但在这本书中,我们采用直接的方法,即从 PowerShell 的 GitHub 仓库下载 MSI:PowerShell/PowerShell。请确保您下载的是稳定版本,因为这是 PowerShell 团队发布的最新 GA(通用可用)版本(如图 2.1 所示)。

图 2.1 这显示了可用于 PowerShell 的不同安装方式,其中 MSI 用于 Windows 安装。

按照 MSI 向导进行操作,接受默认设置,然后您就完成了。启动 PowerShell 有多种方式(如图 2.2 所示)。安装后,您可以在任务栏中搜索它。这也是指出图标也略有变化的好时机。

图 2.2 Windows 10 的开始菜单显示了 PowerShell 7 和 PowerShell 5.1 的并行安装

如果你点击 PowerShell 7 图标(我们建议你将其也设置为任务栏图标),这将启动 PowerShell 控制台。如果你熟悉 Windows PowerShell,你将注意到它在外观上的明显差异。这是因为背景色是黑色而不是蓝色。为了这本书的目的,我们已经更改了控制台颜色,以便更容易阅读。

当你在没有安装 GUI 终端的服务器上运行 PowerShell 时,PowerShell 控制台应用程序是你的唯一选择:

  • 控制台应用程序非常小巧。它加载速度快,且内存占用很少。

  • 它不需要比 PowerShell 本身更多的 .NET Framework 支持。

  • 你可以将颜色设置为绿色文本和黑色背景,假装你在使用 1970 年代的巨型计算机。

如果你决定使用控制台应用程序,我们有一些建议来配置它。你可以通过点击窗口左上角的控制框并选择属性来设置所有这些配置。如图 2.3 所示的对话框。在 Windows 10 中,它看起来略有不同,因为它增加了一些新选项,但基本原理是相同的。

图 2.3 配置控制台应用程序属性

在选项选项卡上,你可以增加命令历史缓冲区的大小。这个缓冲区使控制台能够记住你输入过的命令,并允许你通过使用键盘上的上箭头和下箭头来回忆它们。

在字体选项卡上,选择比默认的 12 点字体稍大一些的字体。求你了。我们不在乎你是否有 20/10 的视力;稍微增加字体大小。PowerShell 需要你能够快速区分许多相似外观的字符——例如 '(撇号或单引号)和 `(反引号或重音符号)——而小字体无助于此。

在布局选项卡上,将两个宽度大小设置为相同的数字,并确保生成的窗口适合你的屏幕。未能做到这一点可能会导致窗口底部出现水平滚动条,这可能导致一些 PowerShell 输出被包裹在窗口的右侧,你永远看不到。这很烦人。

最后,在颜色选项卡上,不要太过分。保持高对比度和易于阅读。如果你真的想的话,你可以将颜色设置为与你的 Windows PowerShell 终端匹配。

需要记住的一点是:这个控制台应用程序不是 PowerShell;它只是你与 PowerShell 交互的途径。

注意:在我们的旅程中,我们将不会使用 Windows PowerShell 或 ISE。ISE 不支持 PowerShell 7。我们将改用 Visual Studio Code,这在本章稍后会有所介绍。

2.2 macOS 上的 PowerShell

如果你使用的是 Mac,这一部分是为你准备的。我们将讨论如何在 macOS 上安装和运行 PowerShell。这本书假设你知道如何打开终端——macOS 的默认终端应用程序。如果你有其他终端,你也可以使用,但我们将在这本书中坚持使用默认的终端。好,让我们安装 PowerShell!

2.2.1 在 macOS 上的安装

今天,PowerShell 并没有包含在 macOS 中。也许有一天会发生这种情况,但在此之前,我们必须自己安装它。幸运的是,安装很简单,有很多种方法可以做到。我们将介绍在 macOS 上安装 PowerShell 的最简单方法,即通过 Homebrew——macOS 上的首选包管理器。Homebrew 提供了通过终端安装 PowerShell 的能力,无需点击鼠标。

注意,Homebrew 也没有包含在 macOS 中,所以如果你还没有安装它,你可以访问 Homebrew 的网站 (brew.sh) 获取安装说明。去安装它吧。我们等你回来!

一旦你安装了 Homebrew 并准备就绪,你就可以安装 PowerShell。你只需要一个 终端 实例,所以请打开你的 Mac 上的终端。利用 Homebrew,你将使用一条命令安装 PowerShell:

brew cask install powershell

将该命令输入到 终端 并按 回车键。然后你会看到 Homebrew 正在安装 PowerShell(图 2.4)。

图片

图 2.4 Homebrew 安装 PowerShell

你已经设置好了!让我们运行它。我们要运行什么?这是一个好问题。要运行 PowerShell,你只需运行命令 pwsh,这将在你的终端中启动 PowerShell。你应该会看到以下输出:

~ pwsh
PowerShell 7.1.3
Copyright (c) Microsoft Corporation. All rights reserved.

https://aka.ms/pscore6-docs
Type 'help' to get help.

PS /Users/steve>

现在我们已经在 macOS 的终端应用程序中运行 PowerShell 了!做得好。这是在 macOS 上与 PowerShell 交互的主要方式之一。我们稍后会介绍另一种主要方式,但首先我们需要解决那些使用 Linux 作为操作系统的人的问题。

2.3 PowerShell 在 Linux(Ubuntu 18.04)上

这是我们要告诉你 PowerShell 非常棒,能够在极其广泛的 Linux 发行版上运行的部分。这也是我们要告诉你,如果我们逐一介绍了所有这些发行版的安装过程,我们的出版商会想知道为什么这本书变成了百万页。我们将介绍如何在 Ubuntu 18.04 上安装 PowerShell,因为它是写作时的最新 LTS 版本。如果你使用的是其他类型的机器,不用担心!有关如何在所有支持的 Linux 发行版上安装 PowerShell 的所有文档都可以在 PowerShell 文档中找到,具体是关于这个主题的文章:mng.bz/YgnK

好吧,现在让我们谈谈安装。我们还应该提到……这本书假设你知道如何在 Ubuntu 18.04 上打开终端应用程序。你可以使用任何终端进行这些步骤,但我们将坚持使用默认的终端。

2.3.1 在 Ubuntu 18.04 上的安装

Ubuntu 18.04 预装了 Canonical 自家的包管理器,称为 snap。这使我们能够通过单个命令安装 PowerShell。首先,请打开一个终端实例并输入以下命令:

snap install powershell –-classic

完成上述操作后,按 ENTER 运行。你可能需要输入密码,如果是这样,请输入。这是因为 snap 需要以 root 权限运行才能安装 PowerShell。你看到的输出应该看起来像这样:

PowerShell 7.1.3 from Microsoft PowerShell✓ installed

注意:我们在命令中添加了 --classic,因为 PowerShell 被视为“经典 snap 包”。经典 snap 包移除了对 snap 包的限制,允许 PowerShell 完全与操作系统交互。

你已经准备好了!让我们运行它。我们要运行什么?这是一个好问题。要运行 PowerShell,你只需运行命令 pwsh,它将在你的终端中启动 PowerShell。你应该会看到以下输出:

~ pwsh
PowerShell 7.1.3
Copyright (c) Microsoft Corporation. All rights reserved.

https://aka.ms/pscore6-docs
Type 'help' to get help.

PS /Users/tyleonha>

我们现在已经在 Ubuntu 18.04 的终端中运行了 PowerShell!做得好。这是在 Ubuntu 18.04 上与 PowerShell 交互的主要方式之一。现在我们已经让它在终端中运行,让我们让其他 PowerShell 界面也运行起来。

2.4 Visual Studio Code 和 PowerShell 扩展

等等!别急着走。我们知道这听起来像是我们要你安装所有 C# 开发者朋友都在用的那个应用程序,但这并不准确!让我们来解释一下。

微软提供了两个名称非常相似但完全不同的产品(“技术领域有两个难题:缓存失效、命名事物和 off-by-1 错误”这句话是正确的)。第一个产品是你可能听说过的:Visual Studio。它是一个功能齐全的集成开发环境(IDE)。它通常被 C# 和 F# 开发者使用。另一方面,Visual Studio Code 是一个完全不同的应用程序。它是一个轻量级的文本编辑器,与 Sublime Text 或 Notepad++ 等其他文本编辑器有些相似之处,但它增加了一些功能来增强用户体验。

其中一个新增功能是可扩展性。人们可以为 Visual Studio Code 编写扩展并将其放在 Visual Studio Code 的市场上供其他人使用。PowerShell 团队在市场上提供了一个 PowerShell 扩展,它带来了一系列有助于你学习 PowerShell 的优秀功能。带有 PowerShell 扩展的 Visual Studio Code 是推荐用于 PowerShell 的编辑体验,就像 PowerShell 本身一样,它们都是开源的并且跨平台工作。你可以在以下位置找到源代码:

这也是我们向大家说明的一个好机会,如果你对那些产品有任何问题,请在它们各自的 GitHub 页面上打开一个 issue。这是提供反馈和报告问题的最佳方式。好的,让我们进入安装步骤。

注意:在后续章节中学习编写脚本时,Visual Studio Code 和 PowerShell 扩展将更有价值。你将会达到那里。我们保证。

那 PowerShell ISE 呢?

如果你已经对 PowerShell 有所了解,并且熟悉 PowerShell ISE,你可能想知道为什么它没有被提及。PowerShell ISE 不与 PowerShell 一起工作,目前只处于支持模式,这意味着它只会接收安全相关的更新。团队的关注点已经转移到带有 PowerShell 扩展的 Visual Studio Code 上。

2.4.1 安装 Visual Studio Code 和 PowerShell 扩展

如果你已经做到了这一步,你已经在你的操作系统上安装了 PowerShell。要安装 Visual Studio Code,你可以使用相同的步骤。对于 Windows、macOS 或 Linux,请访问code.visualstudio.com/Download并下载并运行安装程序(图 2.5)。

  • 要添加 PowerShell 扩展,启动 VS Code 并转到市场。

  • 搜索 PowerShell 并点击安装。

图 2.5 这显示了 VS Code 中 PowerShell 7 扩展的扩展图标和安装按钮。

对于那些喜欢命令行的人来说,你也可以通过终端安装 VS Code 和 PowerShell 扩展:

  • macOS:打开Terminal并运行brew cask install vscode

  • Ubuntu 18.04:打开Terminal并运行snap install code --classic

你已经掌握了这个!如果你正确地做了,在终端中运行code命令应该会打开一个 Visual Studio Code 的实例。如果不起作用,关闭所有终端窗口,打开一个新的,然后再次尝试运行code命令。一旦安装完成,你需要安装 PowerShell 扩展。由于我们喜欢在 PowerShell 的世界中打字,让我们用一条命令安装扩展。你可以使用code命令安装扩展,如下所示:

code --install-extension ms-vscode.powershell

这将给出以下输出:

~ code --install-extension ms-vscode.powershell
Installing extensions...
Installing extension 'ms-vscode.powershell' v2019.9.0..
Extension 'ms-vscode.powershell' v2019.9.0 was successfully installed.

让我们看看清单:

PowerShell installed ✔
Visual Studio Code installed ✔
PowerShell extension installed ✔

我们准备好看看这一切能提供什么。如果你还没有做,请运行终端中的code命令来打开 Visual Studio Code。

2.4.2 熟悉 Visual Studio Code

从现在开始,无论你运行什么操作系统,体验都将相同。这里我们有 Visual Studio Code。一开始可能看起来有些吓人,但经过一点练习,你将能够利用它的力量来帮助你编写一些出色的 PowerShell 脚本。打开 Visual Studio Code 后,我们应该准备好让它与 PowerShell 一起工作。首先,点击左侧靠近其他奇怪图标的小 PowerShell 模板图标。它在图 2.6 中被突出显示。

图 2.6 Visual Studio Code 启动屏幕

点击 PowerShell 图标后,会出现几个东西。让我们来看看我们看到的内容(图 2.7):

  • 命令探索器(A)——可供您运行的命令列表。当您悬停在其中一个上时,它会为您提供几个不同的操作。您可以再次点击 PowerShell 图标来隐藏它。

  • 脚本编辑面板(B)——我们将在本书的结尾使用它,但这是您的脚本将在不同的标签中出现的部分。

  • 集成控制台(C)——这是魔法发生的地方。这是 PowerShell。您可以在这里运行命令,就像您在终端应用程序中运行的 PowerShell 一样。

图 2.7 带有 PowerShell 扩展的 Visual Studio Code 分解图

在集成控制台的右上角,我们可以看到几个不同的操作。让我们从右侧开始。首先我们看到一个“x”图标。这个图标会隐藏集成控制台和整个终端面板。如果您想再次打开它,请按 Ctrl+。之后,您会看到一个光标(^`)图标。这个图标会隐藏脚本面板并最大化终端面板。然后我们有一个垃圾桶图标。这个图标会关闭终端。请跟着我们重复: “我保证永远永远永远不关闭 PowerShell 集成控制台。” 集成控制台是 PowerShell 扩展的核心,以及所有其功能,如果您关闭它,那么扩展将停止工作——所以,请,请不要删除集成控制台。

PowerShell 集成控制台与普通终端相比?

如我们之前提到的,PowerShell 集成控制台是 PowerShell 扩展的核心。您认为命令探索器中的命令是从哪里来的?是的,没错——集成控制台。扩展中有许多功能依赖于集成控制台,但只需知道只有一个。任何其他生成的终端,即使它运行 PowerShell,也不是“集成”的。记住:不要删除集成控制台。

接下来是分割终端按钮和加号按钮。这些按钮会生成额外的终端,可以在它们旁边的下拉菜单中看到。需要注意的是,Visual Studio Code 默认为这些终端选择 Bash,因为 Bash 默认安装。您可以在设置中轻松配置此选项,但我们可以稍后再讨论。在此期间,如果您在 Visual Studio Code 中打开 Bash 终端,您可以像在终端应用程序中一样输入pwsh,您就会得到 PowerShell。

使用 PowerShell 的 Visual Studio Code 体验主要针对编写 PowerShell 脚本和模块,而终端应用程序中的 PowerShell 则是一个更适用于运行几个快速命令或长时间运行任务的体验。它们都各司其职,我们将在本书中看到更多关于它们的内容。

2.4.3 自定义 Visual Studio Code 和 PowerShell 扩展

正如我们之前所说的,可扩展性对 Visual Studio Code 来说非常重要。因此,很容易根据您的喜好自定义 Visual Studio Code 和 PowerShell 扩展。我们将介绍您可以执行的一些操作——有些很有用,有些只是为了娱乐!

首先,让我们从 Visual Studio Code 的设置页面开始。我们将能够配置我们想要的几乎所有内容。转到文件 > 预设 > 设置以打开设置页面(图 2.8)。从这里您可以在搜索框中搜索任何您想要的内容,或者简单地滚动浏览所有内容。有很多可以配置的!如果您对 PowerShell 扩展提供的设置感兴趣,只需搜索 powershell,您就会看到所有设置。

图片

图 2.8 Visual Studio Code 的设置页面。我们已概述了查看设置 JSON 版本的位置。

您可能会注意到我们在截图中突出显示了一个按钮。如果您点击此按钮,您将获得您已设置的设置的 JavaScript 对象表示法(JSON)。如果您不熟悉 JSON,不要担心。您可以使用常规设置窗口执行 JSON 视图可以做的几乎所有操作。

表 2.1 显示了一组您可以直接粘贴到搜索框中并根据您的喜好配置的常用设置。

表 2.1 推荐设置

设置 描述
Tab Completion Tab 完成设置有助于复制您在常规终端中从 PowerShell 获得的经验。您将在稍后了解更多关于这个概念的信息,但您可能会发现这个设置很有用。
Terminal.Integrated.Shell.WindowsTerminal.Integrated.Shell.OSX Terminal.Integrated.Shell.Linux 如果您记得在本章前面的内容,当我们按下 Visual Studio Code 终端部分的“+”号时,它会打开 Bash。这是因为 macOS 和 Linux 的默认终端是 Bash。您可以通过将此设置更改为 pwsh 来将其更改为 PowerShell。
Files.Default Language 当您在 Visual Studio Code 中打开新文件时,它假定它是纯文本。您可以通过更改默认语言设置来更改此行为。将此更改为 powershell 将确保新文件将是 PowerShell 文件,并为您提供所有 PowerShell 扩展功能。

您还可以更改关于 Visual Studio Code 的另一件事,那就是颜色主题。默认的深色主题很漂亮,但如果您想寻找一个完美匹配的主题,那么您有很多选择。更改起来很简单——我们只需要打开命令面板。为此,在 macOS 上按 CMD+SHIFT+P,在 Windows/Linux 上按 CTRL+SHIFT+P(或者您可以在任一平台上按 F1)。

图片

图 2.9 Visual Studio Code 的命令面板。搜索您要执行的操作。

命令面板(图 2.9)是 Visual Studio Code 最有用的功能之一,因为它允许你搜索可以执行的操作。我们想要执行的操作是“更改颜色主题”,所以让我们在命令面板中搜索theme。你应该会看到一个名为“首选项:颜色主题”的选项——点击它。这将给你一个可用的主题选择列表(图 2.10)。使用箭头键浏览主题;你会注意到 Visual Studio Code 的主题会自动更新,这样你可以在提交之前看到你将得到什么。

图片

图 2.10 Visual Studio Code 中的主题选择

列表中的大多数是 Visual Studio Code 自带的默认主题;然而,PowerShell ISE 主题是随着 PowerShell 扩展一起提供的。你可以在扩展市场中搜索更多酷炫的主题(我们个人喜欢 Horizon 主题,但那只是我们个人喜好!),通过在列表中选择“安装更多颜色主题”项。

现在试试看对于本书的剩余部分,我们将假设你在使用带有 PowerShell 扩展的 Visual Studio Code,而不是其他脚本编辑器来编写或检查脚本。如果你愿意,可以配置设置和你的颜色主题。如果你决定在终端应用程序中使用 PowerShell,你也会做得很好——本书中的大多数内容仍然有效。如果有什么是仅控制台或仅编辑器特有的,我们会告诉你。

2.5 再次是打字课

PowerShell 是一个命令行界面,这意味着你将进行大量的输入。输入会留下出错的空间——打字错误。幸运的是,PowerShell 应用程序都提供了帮助最小化打字错误的方法。

现在试试看以下示例在书中无法展示,但看到它们在实际操作中的效果是很有趣的。考虑在你的 shell 副本中跟随操作。

控制台应用程序支持在四个区域进行 Tab 补全:

  • 输入Get-P并多次按 Tab 键。你会注意到一个可能的补全列表。随着你输入更多,这个列表会缩小,当 PowerShell 可以猜测它必须是一个特定的命令时,它会为你完成它。

  • 输入Dir,然后一个空格,然后/,再按 Tab 键。PowerShell 会显示你可以从该目录深入查看的文件和文件夹。

  • 输入Get-Proc并按 Tab 键。然后输入一个空格和一个连字符(-)。开始按 Tab 键以查看 PowerShell 为此参数提供的可能补全。你也可以输入参数名称的一部分(例如,-E),然后按 Tab 键两次以查看匹配的参数。按 Esc 键以清除命令行。

  • 输入New-I并按 Tab 键。输入一个空格,然后-I,再按 Tab 键。PowerShell 会显示该参数的有效值。这仅适用于具有预定义允许值集的参数(该集合称为枚举)。再次按 Esc 键以清除命令行;你还不希望运行该命令。

使用 PowerShell 扩展的 Visual Studio Code 提供的编辑器面板类似于,甚至优于 tab 完成功能:IntelliSense。这个功能在所有四种与 tab 完成相同的情况下运行,除了你将得到一个酷炫的小弹出菜单,就像图 2.11 中所示的那样。使用你的箭头键向上或向下滚动,找到你想要的项,按 Tab 或 Enter 键选择它,然后继续输入。

警告 在 PowerShell 中输入时,非常、非常、非常、非常、非常重要的是要非常、非常、非常、非常准确。在某些情况下,一个空格、引号,甚至回车符的错误都可能导致一切失败。如果你遇到错误,请仔细检查你输入的内容。

图片

图 2.11 IntelliSense 在 Visual Studio Code 中使用 PowerShell 扩展时,就像 tab 完成一样工作。它还会显示你正在完成的项的相关信息(如果有的话)。

2.6 这是哪个版本?

使用 PowerShell,有一个简单的方法来检查你的版本。输入 $PSVersionTable 并按 Enter 键:

PS /Users/steve> $PSVersionTable
Name                           Value
----                           ----- 
PSVersion                      7.1.3 
PSEdition                      Core
GitCommitId                    7.1.3
OS                             Linux 4.18.0-20-generic #21~18.04.1-Ubuntu...
Platform                       Unix
WSManStackVersion              3.0 
SerializationVersion           1.1.0.1 
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...} 
PSRemotingProtocolVersion      2.3

你将立即看到与 PowerShell 相关的每一项技术的版本号,包括 PowerShell 本身。如果这不起作用,或者它没有显示 PSVersion 为 7.0 或更高版本,那么你使用的 PowerShell 版本不适合这本书。请参考本章前面的部分(2.2、2.3 和 2.4,取决于你的操作系统)以获取获取最新版本 PowerShell 的说明。

现在试试看 不要再等待,开始使用 PowerShell。首先检查你的版本号,确保它至少是 7.1。如果不是,请不要继续,直到你安装了至少 v7.1。

2.7 实验室

因为这是本书的第一个实验,我们将花一点时间来描述这些实验应该如何工作。对于每个实验,我们给你一些你可以尝试自己完成的任务。有时我们会提供一两个提示,帮助你找到正确的方向。从那里开始,你就自由发挥了。

我们绝对保证,完成每个实验所需了解的一切要么在同一章中,要么在前面的章节中介绍(之前介绍的信息是我们最有可能给你提示的内容)。我们并不是说答案显而易见;通常,一个章节会教你如何自己发现某些东西,你必须通过这个发现过程来找到答案。这可能看起来很令人沮丧,但强迫自己这样做将绝对使你在长期使用 PowerShell 时更加成功。我们保证。

请记住,您可以在每个章节的末尾找到示例答案。我们的答案可能不会与您的答案完全一致,而且随着我们转向更复杂的内容,这一点将变得越来越明显。您经常会发现 PowerShell 提供了六种或更多方式来完成几乎任何任务。我们将向您展示我们最常用的方法,但如果您找到了不同的方法,您并没有做错。任何能完成任务的方法都是正确的。

注意:对于这个实验,只要您安装了 PowerShell 7.1 或更高版本,您就可以在任何运行 Windows 10、macOS 或 Linux 的机器上完成它。

我们将从简单开始:我们只想确保您已经将控制台和带有 PowerShell 扩展的 Visual Studio Code 设置好,以满足您的需求。请按照以下五个步骤操作:

  1. 如果您还没有下载和安装所有内容,请先完成这一步。

  2. 在您的终端应用程序中(您可能需要查找一下!)和在 Visual Studio Code 中(提示提示……这是一个设置!)配置字体和文本大小。

  3. 在 Visual Studio Code 中,最大化控制台面板;根据您的意愿删除或保留命令资源管理器。

  4. 在这两个应用程序中,输入一个单引号 (') 和一个反引号 (`),并确保您能轻松地区分它们。在美式键盘上(至少是这样),反引号键与波浪号 (~) 字符在同一键上,位于 Esc 键下方。

  5. 还请输入括号 ( )、方括号 [ ]、尖括号 < > 和花括号 {},以确保您选择的字体和大小显示良好,这样所有这些符号都可以立即区分开来。如果对哪些符号是哪些符号存在视觉上的混淆,请更改字体或选择更大的字体大小。

我们已经向您介绍了如何完成这些步骤中的大多数,所以您不需要检查这个实验的任何答案,只需确保您已经完成了所有五个步骤。

3 使用帮助系统

在第一章中,我们提到可发现性是使图形用户界面(GUIs)更容易学习和使用的关键特性,而像 PowerShell 这样的命令行界面(CLIs)通常更难,因为它们缺乏这些可发现性特性。事实上,PowerShell 具有出色的可发现性特性——但它们并不那么明显。其中一个主要的可发现性特性是其帮助系统。

3.1 帮助系统:如何发现命令

请耐心等待一分钟,让我们站在肥皂箱上向你们布道。我们从事的是一个不太重视阅读的行业,尽管我们确实有一个我们巧妙地传递给用户的缩写,当我们希望他们能够阅读友好的手册时——RTFM。大多数管理员倾向于直接跳入,依赖诸如工具提示、上下文菜单等——那些GUI可发现性工具——来弄清楚如何做某事。我们通常就是这样工作的,我们想象你们也是这样做的。但让我们明确一点:

如果你不愿意阅读 PowerShell 的帮助文件,你将无法有效地使用 PowerShell。你将无法学会如何使用它;你将无法学会如何使用它来管理其他服务,如 Azure、AWS、Microsoft 365 等;你不妨就坚持使用 GUI。

这就是我们所能做到的最清晰的表达。这是一个直截了当的陈述,但它绝对是真实的。想象一下,在没有工具提示、菜单和上下文菜单的帮助下尝试了解 Azure 虚拟机或任何其他管理门户。试图在不阅读和理解帮助文件的情况下学习使用 PowerShell 是一回事。这就像试图在没有阅读手册的情况下组装来自百货商店的 DIY 家具一样。你的体验将会是令人沮丧、困惑且无效的。那么,为什么还要这样做呢?

如果你需要执行一个任务但不知道使用什么命令,帮助系统就是你要找到那个命令的方式。在前往你最喜欢的搜索引擎之前,先从帮助系统开始。

如果你运行一个命令并遇到错误,帮助系统将向你展示如何正确运行该命令,以免出错。如果你想将多个命令链接起来以执行复杂任务,帮助系统将告诉你每个命令如何与其他命令连接。你不需要在 Google 或 Bing 上搜索示例;你需要学会如何使用命令本身,这样你就可以创建自己的示例和解决方案。

我们意识到我们的布道可能有点过于强硬,但 90%我们在论坛上看到用户遇到的问题,如果这些人能抽出几分钟时间坐下来,深呼吸,阅读帮助文件,这些问题中的许多都可以解决。然后阅读这一章,它全部关于帮助你理解你在 PowerShell 中阅读的帮助。

从现在开始,我们鼓励你阅读帮助文件还有更多原因:

  • 尽管我们在示例中展示了许多命令,但我们几乎从不完全展示每个命令的功能、选项和能力,以便更容易理解概念。您应该阅读我们展示的每个命令的帮助,以便熟悉每个命令可以完成的额外操作。

  • 在实验室中,我们可能会给您一些关于使用哪个命令来完成任务的提示,但不会给您关于语法的提示。您需要使用帮助系统自行发现这些语法,以便完成实验室的任务。

  • 我们向您承诺,掌握帮助系统是成为 PowerShell 专家的关键。不,您在那里不会找到每一个细节,而且很多超级高级的材料并没有在帮助系统中记录,而是在帮助系统之外,但就成为一个有效的日常管理员而言,您需要掌握帮助系统。这本书将使该系统易于理解,并且它将教授帮助中省略的概念,但只会与内置的帮助结合进行。

现在停止说教。

命令与 cmdlet

PowerShell 包含许多类型的可执行命令。有些被称为 cmdlets(我们将在下一章中介绍 cmdlets),有些被称为 函数,等等。总的来说,它们都是 命令,帮助系统与它们都兼容。cmdlet 是 PowerShell 独有的,您运行的许多命令将是 cmdlets。但当我们谈论更通用的可执行实用工具类别时,我们会尽量使用“命令”这个词。

3.2 可更新帮助

当您第一次在 PowerShell 中启动帮助时,可能会感到惊讶,因为,嗯,那里没有多少内容。但等等,我们可以解释。

Microsoft 从 PowerShell v3 开始引入了一个新功能,称为 可更新帮助。PowerShell 可以从互联网上下载更新、纠正和扩展的帮助。最初,当您请求命令的帮助时,您会得到一个简化的、自动生成的帮助版本,以及如何更新帮助文件的说明,可能如下所示:

PS /User/travisp> help Get-Process
NAME
    Get-Process

SYNTAX
    Get-Process [[-Name] <string[]>] [-Module] [-FileVersionInfo]
    [<CommonParameters>]

    Get-Process [[-Name] <string[]>] -IncludeUserName [<CommonParameters>]

    Get-Process -Id <int[]> -IncludeUserName [<CommonParameters>]

    Get-Process -Id <int[]> [-Module] [-FileVersionInfo] [<CommonParameters>]

    Get-Process -InputObject <Process[]> [-Module] [-FileVersionInfo]
    [<CommonParameters>]

    Get-Process -InputObject <Process[]> -IncludeUserName 
  ➥ [<CommonParameters>]

ALIASES
    gps

REMARKS
    Get-Help cannot find the Help files for this cmdlet on this computer. It
    is displaying only partial help.
        -- To download and install Help files for the module that includes
    this cmdlet, use Update-Help.
        -- To view the Help topic for this cmdlet online, type: "Get-Help
    Get-Process -Online" or
           go to https://go.microsoft.com/fwlink/?LinkID=113324.

TIP 您可能无法忽视这样一个事实,即您没有安装本地帮助。第一次您请求帮助时,PowerShell 将提示您更新帮助内容。

更新 PowerShell 的帮助应该是您的首要任务。在 Windows PowerShell 中,您需要以“管理员”或“root”等价身份更新帮助。在 PowerShell 6 及更高版本中,您现在可以以当前用户身份更新帮助。打开 PowerShell 并运行 Update-Help,几分钟后您就可以顺利完成了。

TIP 如果您没有运行 en-us 文化,您可能需要指定 -UICulture en-US 以使 Update-Help 能够工作。

每个月左右更新帮助文档的习惯非常重要。PowerShell 甚至可以下载非 Microsoft 命令的更新帮助,前提是这些命令的模块位于正确的位置,并且它们已经被编码以包含更新帮助的在线位置(模块是命令添加到 PowerShell 的方式,并在第七章中进行了解释)。

你有未连接到互联网的计算机吗?没问题:去一个已连接的计算机上,使用Save-Help获取帮助的本地副本。将其放在文件服务器或网络其他部分可访问的地方。然后使用带有-Source参数的Update-Help运行,将其指向下载的帮助副本。这样,你的网络上的任何计算机都可以从该中央位置获取更新帮助,而不是从互联网上获取。

帮助是开源的

微软的 PowerShell 帮助文件是开源材料,可在github.com/MicrosoftDocs/PowerShell-Docs找到。这是一个查看最新源代码的好地方,这些源代码可能尚未编译成 PowerShell 可以下载和显示的帮助文件。

3.3 求助

PowerShell 提供了一个名为Get-Help的 cmdlet,用于访问帮助系统。你可能在网上看到过一些示例,显示人们使用Help关键字。实际上,Help关键字根本不是一个原生的 cmdlet;它是一个函数,是核心Get-Help cmdlet 的包装器。

macOS/Linux 上的帮助

当在 macOS 和 Linux 上查看帮助文件时,使用的是操作系统传统的man(手册)功能,它通常会“接管”屏幕来显示帮助,完成时返回到你的正常屏幕。

Help的工作方式与基本的Get-Help非常相似,但它将Help的输出管道传输到less,允许你以分页视图的方式查看,而不是一次性看到所有帮助信息。运行Help Get-ContentGet-Help Get-Content会产生相同的结果,但前者是逐页显示。你可以运行Get-Help Get-Content | less来产生分页显示,但这需要输入更多的内容。我们通常只使用Help,但我们想让你明白,在幕后有一些技巧在发挥作用。

顺便说一下,有时分页显示可能会让人感到烦恼,因为你已经有了所需的信息,但它仍然要求你按空格键来显示剩余的信息。如果你遇到这种情况,请按q取消命令并返回到 shell 提示符。当使用less时,q始终意味着退出

帮助系统的两个主要目标是:帮助你找到执行特定任务的命令,并在找到这些命令后帮助你学习如何使用它们。

3.4 使用帮助查找命令

从技术上来说,帮助系统并不知道 shell 中存在哪些命令。它只知道有哪些帮助主题可用,并且命令可能没有帮助文件,在这种情况下,帮助系统将不知道命令的存在。幸运的是,Microsoft 为它生产的几乎所有 cmdlet 都提供了一个帮助主题,这意味着您通常不会发现差异。此外,帮助系统可以访问与特定 cmdlet 无关的信息,包括背景概念和其他一般信息。

与大多数命令一样,Get-Help(因此,Help)有几个参数。其中之一——可能是最重要的一个——是-Name。此参数指定您想要访问的帮助主题的名称,它是一个位置参数,因此您不必输入-Name;您只需提供您正在寻找的名称即可。它还接受通配符,这使得帮助系统在发现命令时非常有用。

例如,假设您想对.NET 对象上的事件进行一些操作。您不知道可能有哪些命令可用,您决定搜索涵盖事件的帮助主题。您可能运行以下两个命令中的任何一个:

Help *event*
Help *object*

第一个命令会在您的计算机上返回如下列表:

       Name                 Category ModuleName

       Get-Event            Cmdlet   Microsoft.PowerShell.Utility
       Get-EventSubscriber  Cmdlet   Microsoft.PowerShell.Utility
       New-Event            Cmdlet   Microsoft.PowerShell.Utility
       Register-EngineEvent Cmdlet   Microsoft.PowerShell.Utility
       Register-ObjectEvent Cmdlet   Microsoft.PowerShell.Utility
       Remove-Event         Cmdlet   Microsoft.PowerShell.Utility
       Unregister-Event     Cmdlet   Microsoft.PowerShell.Utility
       Wait-Event           Cmdlet   Microsoft.PowerShell.Utility

注意:在您从其他来源安装了一些模块之后,您可能会注意到命令帮助列表中包括了来自Az.EventGridAz.EventHub等模块的命令(和函数)。即使您还没有将这些模块加载到内存中,帮助系统也会显示所有这些命令,这有助于您发现您可能否则会忽略的计算机上的命令。它将发现安装在正确位置的任何模块中的命令,我们将在第七章中讨论这一点。

前面的列表中的许多 cmdlet 似乎都与事件有关。在您的环境中,您可能还有其他与之无关的命令,或者您可能有“关于”主题,这些主题提供了背景信息(在第 3.6 节中详细讨论)。当您使用帮助系统查找 PowerShell 命令时,尽量使用最广泛的术语进行搜索——*event**object*而不是*objectevent*——因为您将获得尽可能多的结果。

当您有一个认为可以完成工作的 cmdlet(例如,在示例中Register-ObjectEvent看起来像是您所追求的合适候选)时,您可以对该特定主题请求帮助:

Help Register-ObjectEvent

不要忘记使用 Tab 键自动完成!作为提醒,它允许您输入命令名称的一部分并按 Tab 键,shell 将使用最接近的匹配项完成您输入的内容。您可以继续按 Tab 键以获取替代匹配项的列表。

现在尝试一下:输入Help Register-并按 Tab 键。这将匹配几个命令但不会完成。在 Windows 机器上,当您第二次按 Tab 键时,它将不断滚动显示可用的命令。在非 Windows 机器上,如果您按 Tab 键,第二个标签将显示可用的命令列表。

你也可以在 Help 命令中使用通配符——主要是 * 通配符,它可以代表零个或多个字符。如果 PowerShell 只找到与你输入的匹配的一个结果,它不会显示该单个项目的主题列表。相反,它会显示该项目的具体内容。

现在尝试运行 Help Get-EventS*,你应该会看到 Get-EventSubscriber 的帮助文件,而不是匹配的帮助主题列表。

如果你一直在 shell 中跟随,你现在应该正在查看 Get-EventSubscriber 的帮助文件。这个文件被称为 概要帮助,它旨在是命令的简短描述和语法的提醒。当你需要快速刷新对命令用法的记忆时,这些信息很有用,这也是我们将开始解释帮助文件本身的地方。

除此之外

有时候我们想要分享一些信息,虽然很好,但并不是理解 shell 的关键。我们将这些信息放入一个“除此之外”的侧边栏中,就像这样。如果你跳过这些,你会没事;如果你阅读它们,你通常会了解做某事的另一种方法,或者对 PowerShell 有更深入的了解。

我们提到 Help 命令不搜索 cmdlet;它搜索帮助主题。因为每个 cmdlet 都有一个帮助文件,所以我们可以说这个搜索会检索相同的结果。但你也可以直接使用 Get-Command 命令(或其别名 gcm)来直接搜索 cmdlet。

Help 命令一样,Get-Command 也接受通配符——所以你可以运行 gcm *get* 来查看所有名称中包含 get 的命令。不管好坏,这个列表将包括 cmdlet,还包括像 wget 这样的外部命令,这可能并不有用。

更好的方法是使用 -Noun-Verb 参数。因为只有命令名才有名词和动词,所以结果将仅限于 cmdlet。Get-Command -Noun *event* 返回处理事件的 cmdlet 列表;Get-Command -Verb Get 返回所有能够检索事物的 cmdlet。你还可以使用 -CommandType 参数,指定 cmdlet 类型:Get-Command *event* -Type cmdlet 显示所有名称中包含事件的 cmdlet 列表,并且该列表不会包括任何外部应用程序或命令。

3.5 解释帮助

PowerShell 的 cmdlet 帮助文件有一套特定的约定。学会理解你所看到的内容是提取这些文件最大信息量和更有效地使用 cmdlet 的关键。

3.5.1 参数集和常用参数

大多数命令可以根据需要以多种方式工作。例如,这是 Get-Item 帮助的语法部分:

SYNTAX
    Get-Item [-Stream <String[]>] [-Credential <PSCredential>] [-Exclude 
  ➥ <String[]>] [-Filter <String>] [-Force] [-Include
    <String[]>] -LiteralPath <String[]> [<CommonParameters>]

    Get-Item [-Path] <String[]> [-Stream <String[]>] [-Credential 
  ➥ <PSCredential>] [-Exclude <String[]>] [-Filter <String>] [-Force]
    [-Include <String[]>] [<CommonParameters>]

注意到上一个语法中的命令被列出了两次,这表明该命令支持两个 参数集;你可以以两种不同的方式使用该命令。一些参数将在两个集合之间共享。例如,你会发现两个参数集都包括 -Filter 参数。但两个参数集将始终至少有一个唯一的参数,该参数仅存在于该参数集中。在这种情况下,第一个集合支持 -LiteralPath,它不包括在第二个集合中;第二个集合包含 -Path 参数,它不包括在第一个集合中,但两者都可能包含额外的未共享参数。

这就是这样工作的:如果你使用仅包含在一个集合中的参数,你将锁定在该集合中,并且只能使用该集合中出现的附加参数。如果你选择使用 -LiteralPath,你不能使用来自其他集合的参数,在这个例子中是 -Path,因为它不在第一个参数集中。这意味着 -Path-LiteralPath互斥的——你永远不会同时使用它们,因为它们位于不同的参数集中。

有时可以使用仅在多个集合之间共享的参数来运行命令。在这些情况下,shell 通常会选择第一个列出的参数集。因为每个参数集都意味着不同的行为,了解你正在运行哪个参数集是很重要的。

你会注意到每个 PowerShell 命令的每个参数集都以 [<CommonParameters>] 结尾。这指的是一组(在撰写本文时为 11 个)参数,这些参数在每个命令中都是可用的,无论你如何使用该命令。我们将在本书的后面讨论一些这些常见参数,当我们使用它们来完成实际任务时。然而,在本章的后面,我们将向你展示如何了解更多关于这些常见参数的信息,如果你感兴趣的话。

注意,敏锐的读者现在应该已经注意到了我们一些示例中的变化。读者会注意到 Get-Item 的帮助布局因 PowerShell 的版本而异。你甚至可能会看到一些新的参数。但我们在解释的基本原理和概念并没有改变。不要纠结于你所看到的帮助可能与我们书中展示的不同。

3.5.2 可选和必选参数

你不需要每个参数来使命令运行。PowerShell 的帮助列表以方括号列出可选参数。例如,[-Credential <PSCredential>] 表示整个 -Credential 参数是可选的。你根本不需要使用它;如果你不使用此参数指定替代凭据,命令可能会默认使用当前用户的默认凭据。这也是为什么 [<-Common-Parameters>] 在方括号中的原因:你可以不使用任何常见参数来运行命令。

几乎每个 cmdlet 至少有一个可选参数。你可能永远不需要使用其中的一些参数,而可能每天都会使用其他参数。请记住,当你选择使用一个参数时,你只需输入参数名称的一部分,以便 PowerShell 可以明确地确定你指的是哪个参数。例如,-F 对于 -Force 来说是不够的,因为 -F 也可以表示 -Filter。但 -Fo 将是 -Force 的合法缩写,因为没有其他参数以 -Fo 开头。

如果你尝试运行一个命令却忘记了其中一个必选参数怎么办?以 Get-Item 的帮助为例,你会看到 -Path 参数是必选的。你可以通过参数的整个名称及其值没有被方括号包围来判断。这意味着可选参数可以通过整个参数及其值被方括号包围来识别。尝试运行 Get-Item 而不指定文件路径(图 3.1)。

图 3.1 这是 Get-Item 的帮助信息,显示路径变量接受由方括号 [ ] 指示的字符串数组。

现在试试看。通过运行 Get-Item 而不带任何参数来跟随这个例子。

PowerShell 应该会提示你输入必选的 -Path 参数。如果你输入类似 ~./ 并按 Enter 键,命令将正确运行。你也可以按 Ctrl-C 来终止命令。

3.5.3 位置参数

PowerShell 的设计者知道某些参数会被频繁使用,以至于你不想不断地输入参数名称。这些常用参数通常是 位置参数:你可以在不输入参数名称的情况下提供值,只要你把那个值放在正确的位置。你可以通过两种方式识别位置参数:通过语法摘要或完整帮助。

在语法摘要中查找位置参数

你可以在语法摘要中找到第一种方法:参数名称——只有名称——将被方括号包围。例如,看看 Get-Item 第二组参数中的前两个参数:

[-Path] <String[]> [-Stream <String[]>]...[-Filter <String>]

第一个参数 -Path 不是可选的。你可以通过参数的整个名称及其值没有被方括号包围来判断。但是参数名称被方括号包围,使其成为位置参数——你可以提供日志名称而不必输入 -Path。而且因为该参数在帮助文件中的第一个位置,你知道日志名称是你必须提供的第一个参数。

第二个参数 -Stream 是可选的;它及其值都被方括号包围。在这些方括号内,-Stream 本身不包含在另一组方括号中,这表明这不是位置参数。如果是位置参数,它看起来应该是 [[-Stream] <string[]>]。所以,你需要使用参数名称来提供值。

-Filter 参数(在语法中稍后出现;运行 Help Get-Item 并找到它)是可选的,因为它完全被方括号包围。-Filter 名称在方括号中,这告诉你,如果你选择使用该参数,你必须输入参数名称(或者至少是它的一部分)。使用位置参数有一些技巧:

  • 混合使用位置参数和需要其名称的参数是可以的。位置参数必须始终位于正确的位置。例如,Get-Item ~ -Filter * 是合法的:~ 将被传递给 -Path 参数,因为该值位于第一个位置,而 * 将与 -Filter 参数一起使用,因为使用了参数名称。

  • 指定参数名称总是合法的,当你这样做时,你输入它们的顺序并不重要。Get-Item -Filter * -Pa * 是合法的,因为我们使用了参数名称(在 -Path 的情况下,我们对其进行了缩写)。

注意:一些命令,如 Get-ChildItem,有多个位置参数。第一个是 -Path,然后是 -Filter。如果你使用多个位置参数,不要失去它们的顺序。Get-ChildItem ~ Down* 将工作,其中 ~ 将附加到 -Path,而 Down* 将附加到 -FilterGet-ChildItem Down* ~ 不会得到任何结果,因为 ~ 将附加到 -Filter,而且很可能没有项目匹配。

我们将提供一个最佳实践:在你对某个 cmdlet 感到舒适并厌倦了反复输入常用参数名称之前,使用参数名称。之后,使用位置参数来节省你的输入。当需要将命令粘贴到文本文件中以方便重用时,始终使用完整的 cmdlet 名称并输入完整的参数名称——不要使用位置参数和缩写参数名称。这样做使得该文件在未来更容易阅读和理解,而且因为你不必反复输入参数名称(毕竟,你将命令粘贴到文件中是为了这个目的),你不会为自己创造额外的打字工作。

在完整帮助中查找位置参数

我们提到你可以通过两种方式定位位置参数。第二种方式要求你通过使用 Help 命令的 -Full 参数来打开帮助文件。

现在试试看。运行 Help Get-Item -Full。记住,使用空格键一次查看一页帮助文件,如果你想在到达文件末尾之前停止查看,请按 Ctrl-C。现在,翻阅整个文件,这样你可以滚动回来看它。此外,尝试使用 –Online 参数,它应该在任何带有浏览器的客户端计算机或服务器上工作。请注意,使用 –Online 的成功取决于底层帮助文件的质量。如果文件格式不正确,你可能看不到所有内容。

滚动页面,直到您看到 -Path 参数的帮助条目。它应该看起来像图 3.2。

图片

图 3.2 Get-Item 命令的帮助中关于 -path 变量必需性的片段

在前面的示例中,您可以看到这是一个位置参数,它位于第一个位置,紧随 cmdlet 名称之后,基于 0 位置索引。

我们总是鼓励学生在开始使用 cmdlet 时,专注于阅读完整的帮助文档,而不仅仅是简短的语法提示。阅读帮助文档可以揭示更多细节,包括参数使用的描述。您还可以看到该参数确实接受通配符,这意味着您可以提供一个像 Down* 这样的值。您不需要输入项目的名称,例如下载文件夹。

3.5.4 参数值

帮助文件还提供了关于每个参数接受输入类型的线索。大多数参数期望某种类型的输入值,这些值将始终跟在参数名称之后,并且与参数名称之间用空格分隔(不是冒号、等号或任何其他字符,尽管有时可能会遇到例外)。在简写语法中,期望的输入类型用尖括号表示,如 < >

-Filter <String>

在完整的语法中也是同样的方式显示:

    -Filter <String>
        Specifies a filter in the format or language of the provider. 
        The value of this parameter qualifies the Path parameter.

        The syntax of the filter, including the use of wildcard characters, 
        depends on the provider. Filters are more efficient than
        other parameters, because the provider applies them when the cmdlet 
        gets the objects rather than having PowerShell filter
        the objects after they are retrieved.

        Required?                    false
        Position?                    named
        Default value                None
        Accept pipeline input?       False
        Accept wildcard characters?  true

让我们看看一些常见的输入类型:

  • String—一系列字母和数字。这些有时可以包括空格,但如果有,整个字符串必须用引号括起来。例如,像 /usr/bin 这样的字符串值不需要用引号括起来,但 ~/book samples 需要,因为它中间有空格。目前,您可以使用单引号或双引号互换使用,但最好坚持使用单引号。

  • IntInt32Int64—一个整数(没有小数部分的整数)。

  • DateTime—通常,一个可以基于您的计算机区域设置解释为日期的字符串。在美国,这通常类似于 10-10-2010,包括月份、日期和年份。

我们将在遇到它们时讨论其他更专业的类型。您还会注意到一些具有更多方括号的值:

-Path <String[]>

String 之后并排的方括号并不表示某项是可选的。相反,String[] 表示该参数可以接受一个 数组、一个 集合 或一个 字符串列表。在这些情况下,提供单个值总是合法的:

Get-Item -Path ~

但指定多个值也是合法的。一个简单的方法是提供一个以逗号分隔的列表。PowerShell 将所有以逗号分隔的列表视为值数组:

Get-Item -Path ~, ~/Downloads

再次强调,任何包含空格的单独值都必须用引号括起来。但整个列表不需要用引号括起来;重要的是只有单独的值需要用引号。以下示例是合法的:

Get-Item -Path '~', '~/Downloads'

尽管这两个值都不需要用引号括起来,但如果您想使用引号,那也是可以的。但以下是不正确的:

Get-Item -Path '~, ~/Downloads'

在这种情况下,cmdlet 将寻找名为 ~, ~/Downloads 的文件,这可能不是您想要的。

您还可以以几种其他方式将值列表提供给参数,包括从文件中读取计算机名称或使用其他 cmdlet。然而,这些技术稍微复杂一些,所以我们将在您学习到一些需要使技巧生效的 cmdlet 之后,在后面的章节中介绍它们。

另一种为参数指定多个值(前提是该参数是必填参数)的方法是根本不指定该参数。与所有必填参数一样,PowerShell 将提示您输入参数值。对于可以接受多个值的参数,您可以输入第一个值并按 Enter 键。然后 PowerShell 将提示您输入第二个值,您可以输入并按 Enter 键完成。继续这样做,直到完成,然后在空白提示符上按 Enter 键,让 PowerShell 知道您已完成。始终可以按 Ctrl-C 键终止命令,如果您不想被提示输入条目。

其他参数,称为 开关,根本不需要任何输入值。在缩写语法中,它们看起来如下:

[-Force]

在完整语法中,它们看起来如下:

    -Force [<SwitchParameter>]
        Indicates that this cmdlet gets items that cannot otherwise be accessed, 
        such as hidden items. Implementation varies from
        provider to provider. For more information, see about_Providers 
        (../Microsoft.PowerShell.Core/About/about_Providers.md).
        Even using the Force parameter, the cmdlet cannot override security restrictions.

        Required?                    false
        Position?                    named
        Default value                False
        Accept pipeline input?       False
        Accept wildcard characters?  false

[<SwitchParameter>] 这一部分确认这是一个开关,并且它不需要输入值。开关永远不会定位;您始终必须输入参数名称(或至少是其缩写版本)。开关始终是可选的,这给了您选择是否使用它们的自由。

例如,Get-Item .* 不会显示任何文件,但 Get-Item .* -Force 将列出以 . 开头的文件列表,因为以 . 开头的文件被认为是隐藏的,而 -Force 告诉命令包括隐藏文件。

3.5.5 查找命令示例

我们倾向于通过例子来学习,这就是为什么我们试图在这本书中尽可能多地包含例子。PowerShell 的设计者知道大多数管理员都喜欢有例子,所以他们将很多例子都内置到了帮助文件中。如果您滚动到 Get-Item 帮助文件的末尾,您可能注意到几乎有十种使用该 cmdlet 的例子。

让我们看看一种更简单的方法来获取那些例子,如果您只想看到这些例子的话。使用 Help 命令的 -Example 参数,而不是 -Full 参数:

Help Get-Item -Example

现在尝试一下:使用这个新参数来获取 cmdlet 的示例。

注意:由于 PowerShell 的起源是 Windows,因此许多例子都使用了 Windows 路径。您应该知道您可以使用 macOS 或 Linux 路径。实际上,PowerShell 不关心您在两个平台上使用 /\ 作为目录分隔符。

我们喜欢这些示例,尽管其中一些可能很复杂。如果一个示例看起来对你来说太复杂,忽略它,现在检查其他示例。或者(始终在非生产计算机上)进行一些实验,看看你是否能弄清楚示例做了什么以及为什么。

3.6 访问“关于”主题

在本章前面,我们提到 PowerShell 的帮助系统包括背景主题以及特定 cmdlet 的帮助。这些背景主题通常被称为“关于”主题,因为它们的文件名都以about_开头。你也许还记得本章前面提到,所有 cmdlet 都支持一组通用参数。你认为你如何了解更多关于这些通用参数的信息?

现在试试 在继续阅读之前,看看你是否可以使用帮助系统列出通用参数。

你可以从使用通配符开始。由于本书中反复使用了单词common,这可能是开始的好关键词:

Help *common*

这是一个如此好的关键词,实际上它只匹配一个帮助主题:关于 _ 通用 _ 参数。该主题会自动显示,因为它是最匹配的。浏览一下文件,你会找到以下 11 个(当本文撰写时)通用参数列表:

-Verbose
-Debug
-WarningAction
-WarningVariable
-ErrorAction
-ErrorVariable
-OutVariable
-OutBuffer
-InformationAction
-InformationVariable
-PipelineVaribale

文件指出 PowerShell 有两个额外的风险缓解参数,但并非每个 cmdlet 都支持这些参数。帮助系统中的关于主题非常重要,但鉴于它们与特定的 cmdlet 无关,它们很容易被忽视。如果你运行help about*来列出所有这些主题,你可能会对隐藏在 shell 中的额外文档量感到惊讶。

现在试试 Run the command get-help about_* 并查看所有关于主题。现在运行 get-help about_Updateable_Help

3.7 访问在线帮助

PowerShell 的帮助文件是由普通人编写的,这意味着它们不是没有错误的。除了通过运行Update-Help更新帮助文件外,Microsoft 还会在其网站上发布帮助信息。PowerShell 帮助命令的-Online参数将尝试打开基于 Web 的帮助信息——即使在 macOS 或 Linux 上!——对于特定的命令:

Help Get-Item -Online

Microsoft Docs 网站托管了帮助信息,它通常比 PowerShell 本身安装的帮助信息更新。如果你认为你在示例或语法中发现了错误,请尝试查看帮助信息的在线版本。并不是宇宙中的每个 cmdlet 都有在线帮助;这取决于每个产品团队(例如提供 VM 功能的 Azure 计算团队、Azure 存储团队等等)是否提供该帮助。但是当它可用时,它是一个很好的内置功能的补充。

我们喜欢在线帮助,因为它允许我们在 PowerShell 中输入文本时(在浏览器中,帮助信息也格式化得很好)阅读文本。

我们很重要的一点是要指出,自 2016 年 4 月起,Microsoft 的 PowerShell 团队已经开源了他们所有的帮助文件。任何人都可以添加示例、纠正错误,并通常帮助改进帮助文件。这个在线开源项目位于github.com/MicrosoftDocs/Powershell-Docs,通常只包括 PowerShell 团队拥有的文档;它不一定包括其他团队生产的 PowerShell 命令的文档。你可以直接联系这些团队,要求他们开源他们的文档!

3.8 实验室

注意:对于这个实验室,你需要任何运行 PowerShell 7 的计算机。

我们希望这一章已经传达了掌握 PowerShell 帮助系统的重要性。现在,是时候通过完成以下任务来磨练你的技能了。请记住,以下任务中包含斜体的单词,你可以用它们作为完成任务时的线索:

  1. 运行Update-Help,并确保它无错误地完成,以便你在本地计算机上有一个帮助文件的副本。你需要一个互联网连接。

  2. 你能找到任何可以将其他 cmdlet 的输出转换为HTML的 cmdlet 吗?

  3. 有没有可以将输出重定向到文件的 cmdlet?

  4. 有多少 cmdlet 可以用于处理进程?(提示:记住,所有的 cmdlet 都使用单数名词。)

  5. 你可能会使用哪个 cmdlet 来设置PowerShell 断点?(提示:PowerShell 特定的名词通常以PS为前缀。)

  6. 你已经了解到别名是 cmdlet 的昵称。有哪些 cmdlet 可以创建、修改、导出或导入别名

  7. 有没有一种方法可以记录你在 shell 中输入的所有内容,并将该记录保存到文本文件中?

  8. 获取所有进程可能会让人感到不知所措。你如何通过进程名称获取进程?

  9. 有没有一种方法可以告诉Get-Process告诉你启动进程的用户?

  10. 有没有一种方法可以在远程主机上运行一个命令?(提示:现在运行某物的动词是Invoke。)

  11. 检查Out-File cmdlet 的帮助文件。此 cmdlet 创建的文件默认宽度是多少字符?有没有一个参数可以让你改变这个宽度?

  12. 默认情况下,Out-File会覆盖任何与指定文件名相同的现有文件。有没有一个参数可以防止 cmdlet 覆盖现有文件?

  13. 你如何查看在 PowerShell 中定义的所有别名的列表?

  14. 使用别名和缩写参数名称,你能输入的最短的命令行来检索名称中包含process命令列表是什么?

  15. 有多少 cmdlet 可以处理通用对象?(提示:记住要使用单数名词object而不是复数名词objects。)

  16. 本章简要提到了数组。哪个帮助主题可以告诉你更多关于它们的信息?

3.9 实验室答案

  1. Update-Help

    或者如果你在一天内运行了多次,可以使用以下命令:

    更新帮助 –强制

  2. 帮助 html

    或者,您可以尝试使用获取命令

    获取命令 –名词 html

  3. 获取命令 -名词 文件,打印机

  4. 获取命令 –名词 进程

    或者

    帮助 *进程*

  5. 获取命令 -动词 设置 -名词 psbreakpoint

    或者如果您不确定名词,可以使用通配符:

    帮助 *断点*

    或者

    帮助 *中断*

  6. 帮助 *别名*

    或者

    获取命令 –名词 别名

  7. 帮助 转录

  8. 帮助 获取进程 –参数 名称

  9. 帮助 获取进程 –参数 包含用户名

  10. 在 SSH 上执行的命令是

    帮助 执行命令 –参数 主机名

    或者,通过传统的 Windows 协议执行的命令是

    帮助 执行命令 –参数 计算机名

  11. 帮助 输出文件 –完整

    或者

    帮助 输出文件 –参数 宽度

    应该显示 80 个字符作为 PowerShell 控制台的默认值。您也可以使用此参数来更改它。

  12. 如果您运行帮助 输出文件 –完整并查看参数,您应该看到-NoClobber

  13. 获取别名

  14. Gcm -na *process*

  15. 获取命令 –名词 对象

  16. 帮助 关于数组

    或者,您可以使用通配符:

    帮助 *数组*

4 运行命令

当你开始在网上查看 PowerShell 示例时,很容易产生一种印象,即 PowerShell 是一种基于 .NET 的脚本或编程语言。我们的微软最有价值专家(MVP)获奖者和数百名其他 PowerShell 用户都是非常严肃的极客,他们喜欢深入研究 shell,看看我们能让它做什么。但几乎我们所有人都是从本章开始的地方开始的:运行命令。这就是你将在本章中做的事情:不是脚本编写,也不是编程,而是运行命令和命令行实用程序。

4.1 让我们谈谈安全

好的,是时候谈谈房间里的大象了。PowerShell 很棒,PowerShell 真是太棒了。但是,坏人同样喜欢 PowerShell,就像我们一样。确保生产环境的安全是每个人的首要任务。到现在为止,你可能已经开始感受到 PowerShell 的强大之处——你可能想知道所有这些力量是否可能成为安全问题。可能会。本节的目标是帮助您了解 PowerShell 如何影响您环境中的安全,以及如何配置 PowerShell 以提供您所需的精确的安全与权力的平衡。

首先,PowerShell 不会在其接触的任何事物上应用任何额外的权限层。PowerShell 使你只能做你已经有权做的事情。如果你不能通过图形控制台在 Active Directory 中创建新用户,你也不会在 PowerShell 中做到。PowerShell 是一种行使你已有权限的另一种方式。

PowerShell 也不是绕过任何现有权限的方法。假设你想将一个脚本部署给你的用户,并且你想让这个脚本执行你的用户通常没有权限执行的操作。这个脚本对他们来说将不起作用。如果你想让你的用户做某事,你需要给他们执行这项操作的权限。PowerShell 只能完成运行命令或脚本的人已经有权执行的事情。

PowerShell 的安全系统并不是为了阻止任何人输入并运行他们有权执行的任何命令。其理念是,欺骗用户输入一个长而复杂的命令有些困难,因此 PowerShell 不会在用户现有权限之外应用任何安全措施。但我们从以往的经验中知道,很容易欺骗用户运行脚本,而这些脚本可能包含恶意命令。这就是为什么 PowerShell 的大部分安全设计都是为了防止用户无意中运行脚本。无意中这部分很重要:PowerShell 的安全中没有任何东西是为了阻止一个决心运行脚本的用户。其理念是仅阻止用户被欺骗运行来自不受信任来源的脚本。

4.1.1 执行策略

PowerShell 包含的第一项安全措施是 执行策略。这个全局设置控制 PowerShell 将执行的脚本。Windows 10 的默认设置是受限。在 Windows 服务器上,默认是 RemotedSigned,而在非 Windows 设备上的执行策略不被强制执行。Windows 10 设备上的受限设置阻止所有脚本执行。没错:默认情况下,您可以使用 PowerShell 交互式运行命令,但您不能使用它来运行脚本。让我们假设您从互联网上下载了一个脚本。如果您尝试运行它,您将收到以下错误消息:

File C:\Scripts\Get-DiskInventory.ps1 cannot be loaded because the execution 
➥ of scripts is disabled on this system. Please see "get-help about_signing"
➥ for more details.

通过运行 Get-ExecutionPolicy 查看当前的执行策略。您可以通过以下三种方式之一更改执行策略:

  • 通过运行 Set-ExecutionPolicy 命令—这会更改 Windows 注册表的 HKEY_LOCAL_MACHINE 部分的设置,通常必须由管理员运行,因为普通用户没有权限写入该部分的注册表。

  • 通过使用组策略对象(GPO**)—从 Windows Server 2008 R2 开始,包括了对 PowerShell 相关设置的支持。图 4.1 中显示的 PowerShell 设置位于计算机配置 > 策略 > 管理模板 > Windows 组件 > Windows PowerShell 下。图 4.2 显示了策略设置已启用。通过 GPO 配置时,组策略中的设置将覆盖任何本地设置。实际上,如果您尝试运行 Set-ExecutionPolicy,它将生效,但警告消息将告诉您,由于组策略覆盖,您的新设置没有效果。

  • 通过手动运行 PowerShell.exe 并使用其 -ExecutionPolicy 命令行开关—以这种方式运行时,指定的执行策略将覆盖任何本地设置以及任何组策略定义的设置。如图 4.1 所示。

图 4.1 在组策略对象中查找 Windows PowerShell 设置

图 4.2 在组策略对象中更改 Windows PowerShell 执行策略

您可以将执行策略设置为以下五种设置之一(请注意,组策略对象仅提供以下列表中间三个设置):

  • Restricted—这是默认设置,脚本不会执行。唯一的例外是几个由 Microsoft 提供的脚本,用于设置 PowerShell 的默认配置设置。这些脚本带有 Microsoft 数字签名,如果被修改则不会执行。

  • AllSigned—PowerShell 将执行任何由受信任的认证机构(CA)签发的代码签名证书数字签名的脚本。

  • RemoteSigned—PowerShell 将执行任何本地脚本,如果远程脚本由受信任的 CA 签发的代码签名证书进行数字签名,它也将执行远程脚本。"远程脚本"是指存在于远程计算机上的脚本,通常通过通用命名约定(UNC)路径访问。标记为来自互联网的脚本也被视为远程脚本。Edge、Chrome、Firefox 和 Outlook 都将下载标记为来自互联网。

  • Unrestricted—所有脚本都将运行。

  • Bypass—这个特殊设置旨在供将 PowerShell 嵌入其应用程序的应用程序开发人员使用。此设置绕过配置的执行策略,并且仅在托管应用程序提供自己的脚本安全层时使用。你实际上是在告诉 PowerShell,“别担心,我已经有安全措施了。”

等等,什么?

你注意到你可以通过组策略对象设置执行策略,但也可以通过PowerShell.exe的参数来覆盖它吗?如果人们可以轻松覆盖 GPO 控制的设置,那么这种设置有什么好处?这强调了执行策略的目的是仅为了保护那些不知情的用户免受无意中运行匿名脚本的风险。

执行策略的目的是不让一个知情用户有意地做任何事情。它不是那种类型的安全设置。

事实上,一个聪明的恶意软件编写者可以同样容易地直接访问.NET Framework 功能,而无需麻烦地使用 PowerShell 作为中间人。或者换句话说,如果一个未经授权的用户拥有你的计算机的管理权限并且可以运行任意代码,那么你已经在麻烦之中了。

微软建议你在需要运行脚本时使用RemoteSigned,并且只在必须执行脚本的计算机上使用它。根据微软的说法,所有其他计算机应保持为Restricted。他们说,RemoteSigned在安全性和便利性之间提供了良好的平衡。"AllSigned"更严格,但要求所有脚本都必须进行数字签名。PowerShell 社区整体上意见分歧,对良好的执行策略有各种看法。现在,我们将遵循微软的建议,并让你自己进一步探索这个话题,如果你愿意的话。

注意:许多专家,包括微软自己的“脚本侠”,建议使用ExecutionPolicyUnrestricted设置。他们的观点是,这个功能并不提供一层安全保护,你不应该对自己产生错误的信心,认为它在保护你免受任何威胁。

4.2 不编写脚本,而是运行命令

如其名称所示,PowerShell 是一个shell。你可能使用过或至少听说过其他 shell,包括 cmd.exe、Bash、Zsh、fish 和 ksh。PowerShell 不仅是一个 shell,还是一个脚本语言——但并非像 JavaScript 或 Python 那样。

使用这些语言,就像大多数编程语言一样,你会在文本编辑器或集成开发环境(IDE)前坐下,输入一系列关键字来形成一个脚本。你保存该文件,也许双击它来测试它。PowerShell 可以这样做,但这并不是 PowerShell 的主要使用模式,尤其是当你刚开始的时候。使用 PowerShell,你输入一个命令,添加一些参数来定制命令的行为,按 Enter 键,然后立即看到你的结果。

最终,你会厌倦一遍又一遍地输入相同的命令(及其参数),所以你会把它全部复制粘贴到一个文本文件中。给这个文件一个 .ps1 文件扩展名,你突然就有了 PowerShell 脚本。现在,你不再需要一遍又一遍地输入命令,而是运行那个脚本,它会执行里面的任何命令。这通常比用完整的编程语言编写程序要简单得多。实际上,它是一种与 UNIX 管理员多年来使用的模式相似的模板。常见的 UNIX/Linux shell,如 Bash,也有类似的方法:运行命令直到你做对为止,然后将它们粘贴到文本文件中,并称之为 脚本

请不要误解我们:你可以用 PowerShell 实现你需要的任何复杂程度。它确实支持与 Python 和其他脚本或编程语言相同的用法模式。PowerShell 让你可以访问 .NET Core 的全部底层功能,我们见过几乎与在 Visual Studio 中编写的 C# 程序无法区分的 PowerShell “脚本”。PowerShell 支持这些不同的用法模式,因为它旨在对广泛的受众都有用。重点是,尽管它 支持 那种复杂程度,但这并不意味着你 必须 在那个级别使用它,也不意味着你不能用更少的复杂性达到极高的效率。

这里有一个类比。你可能开车。如果你像我们一样,更换机油是你用汽车做的最复杂的机械任务。我们不是汽车爱好者,不能重新组装发动机。我们也不能做那些你在电影中看到的高速 J 转弯。你永远不会看到我们在汽车商业广告中在封闭赛道上开车。但不是专业特技驾驶员的事实并不会阻止我们在更简单的层面上成为极其有效的驾驶员。总有一天我们可能会决定把特技驾驶作为一项爱好(我们的保险公司会非常高兴),到那时我们需要学习更多关于汽车如何工作的知识,掌握一些新技能,等等。这个选择总是存在的,让我们有机会成长。但到目前为止,我们对我们作为普通驾驶员所能完成的事情感到满意。

现在,我们将坚持作为普通的“PowerShell 驾驶员”,在较低复杂性的层面上操作 shell。信不信由你,处于这个级别的用户是 PowerShell 的主要目标受众,你会发现你可以在不超出这个级别的情况下做很多令人难以置信的事情。你所需要做的就是掌握在 shell 中运行命令的能力,然后你就上路了。

4.3 命令的结构

图 4.3 显示了一个复杂 PowerShell 命令的基本结构。我们称这为命令的 完整形式 语法。这里我们展示了一个相对复杂的命令,这样你可以看到所有可能出现的元素。

图片

图 4.3 PowerShell 命令的结构

为了确保你完全熟悉 PowerShell 的规则,让我们更详细地介绍前一个图中的每个元素:

  • cmdlet 名称是 Get-Command。PowerShell cmdlet 总是采用这种动词-名词的命名格式。我们将在下一节中更详细地解释 cmdlet。

  • 第一个参数名称是 -Verb,并且被赋予的值是 Get。因为该值不包含任何空格或标点符号,所以它不需要用引号括起来。

  • 第二个参数名称是 -Module,并且被赋予两个值:PSReadLinePowerShellGet。这些值以逗号分隔,并且因为这两个值都不包含空格或标点符号,所以这两个值都不需要用引号括起来。

  • 最后一个参数 -Syntax 是一个切换参数。这意味着它不需要值;指定参数就足够了。

  • 注意,命令名称和第一个参数之间必须有一个强制性的空格。

  • 参数名称始终以破折号 (-) 开头。

  • 参数名称之后和参数的值与下一个参数名称之间必须有一个强制性的空格。

  • 在参数名称前缀的破折号 (-) 和参数名称本身之间没有空格。

  • 这里没有任何内容是大小写敏感的。

习惯这些规则。开始对准确、整洁的输入敏感。注意空格、破折号和其他规则将最大限度地减少 PowerShell 向你抛出的愚蠢错误。

4.4 cmdlet 命名约定

首先,让我们讨论一些术语。据我们所知,我们是在日常对话中唯一使用这些术语的人,但我们使用得很一致,所以我们不妨解释一下:

  • 一个 cmdlet 是一个本地的 PowerShell 命令行实用程序。这些只存在于 PowerShell 中,并且是用如 C# 这样的 .NET Core 语言编写的。cmdlet 这个词是 PowerShell 独有的,所以如果你在你的首选搜索引擎中添加它作为搜索关键词,你得到的结果将主要是与 PowerShell 相关的。这个词的发音是 command-let

  • 一个 函数 可以与 cmdlet 类似,但与用 .NET 语言编写不同,函数是用 PowerShell 自己的脚本语言编写的。

  • 一个 应用程序 是任何类型的可执行外部程序,包括如 pingipconfig 这样的命令行实用程序。

  • 命令 是我们用来指代上述任何或所有术语的通用术语。

微软为 cmdlet 建立了一个命名约定。同样的命名约定 应该 也用于函数,尽管微软无法强迫除其员工之外的人遵守该规则。

规则是这样的:名称以标准动词开头,例如 GetSetNewPause。你可以运行 Get-Verb 来查看批准的动词列表(你会看到大约 100 个,尽管只有大约 12 个是常见的)。动词之后是一个破折号,后面跟着一个单数名词,例如 JobProcessItem。开发者可以自己创造名词,因此没有 Get-Noun cmdlet 来显示它们。

这条规则有什么大不了的?好吧,假设我们告诉你有一些名为 Start-JobGet-JobGet-ProcessStop-Process 等的 cmdlet。你能猜出在您的机器上启动新进程的命令是什么吗?你能猜出修改 Azure 虚拟机 (VM) 的命令是什么吗?如果你猜的是 Start-Process,那么第一个就猜对了。如果你猜的是 Set-VM,你很接近了:它是 Set-AzVM,你可以在 Az.Compute 模块中找到这个命令(我们将在第七章中介绍模块)。所有的 Azure 命令都使用相同的 Az 前缀,后面跟着命令所操作的名词。重点是,通过使用有限动词集的这种一致命名约定,你可以猜测命令名称,然后你可以使用 HelpGet-Command,结合通配符,来验证你的猜测。这样,你就可以更容易地找出所需的命令名称,而无需每次都跑到 Google 或 Bing 上去。

注意:并非所有所谓的动词都是动词。尽管微软官方使用术语 verb-noun 命名约定,但你也会看到像 NewWhere 这样的“动词”。你会习惯它们的。

4.5 别名:命令的昵称

尽管 PowerShell 命令名称可以很优雅且一致,但它们也可以很长。像 Remove-AzStorageTableStoredAccessPolicy 这样的命令名称要输入很多,即使有自动补全功能也是如此。尽管命令名称很清晰——看一眼,你大概能猜出它做什么——但它要输入的字符实在太多了。

这就是 PowerShell 别名的作用所在。别名不过是命令的一个昵称。厌倦了输入 Get-Process?试试这个:

PS /Users/james> Get-Alias -Definition "Get-Process"
Capability      Name
----------      ----
Cmdlet          gps -> Get-Process

现在你知道了 gpsGet-Process 的别名。

当使用别名时,命令的工作方式相同。参数相同;一切都是相同的——只是命令名称更短。

如果你正在查看一个别名(互联网上的人倾向于像我们都记住了数百个内置别名一样使用它们),却无法弄清楚它是什么,请寻求帮助:

PS /Users/james> help gps
NAME
    Get-Process
SYNOPSIS
    Gets the processes that are running on the local computer.
SYNTAX
    Get-Process [[-Name] <String[]>] [-FileVersionInfo] [-Module]
    [<CommonParameters>]
    Get-Process [-FileVersionInfo] -Id <Int32[]> [-Module] 
    [<CommonParameters>]
    Get-Process [-FileVersionInfo] -InputObject <Process[]> [-Module] 
    [<CommonParameters>]
    Get-Process -Id <Int32[]> -IncludeUserName [<CommonParameters>]
    Get-Process [[-Name] <String[]>] -IncludeUserName [<CommonParameters>]
    Get-Process -IncludeUserName -InputObject <Process[]>
  ➥ [<CommonParameters>]

当需要关于别名帮助时,帮助系统将始终显示完整命令的帮助,这包括命令的完整名称。

不仅如此

您可以使用 New-Alias 创建自己的别名,使用 Export-Alias 导出别名列表,或者甚至使用 Import-Alias 导入之前创建的别名列表。当您创建一个别名时,它只持续到您的当前 shell 会话。一旦关闭窗口,它就会消失。这就是为什么您可能想要导出它们,以便在另一个 PowerShell 会话中使用。

我们倾向于避免创建和使用自定义别名,因为它们只对创建它们的人可用。如果某人无法查找 xtd 做什么,我们就会造成混淆和不兼容。

xtd 什么也不做。这是我们编造的一个假别名。

我们必须指出,由于 PowerShell 现在可在非 Windows 操作系统上使用,因此其关于别名的概念与 Bash 中的别名略有不同。在 Bash 中,别名可以是运行包含大量参数的命令的一种快捷方式。PowerShell 的行为并非如此。别名是命令名的昵称,别名不能包含任何预定义的参数。

4.6 使用快捷方式

这就是 PowerShell 变得复杂的地方。我们很乐意告诉您,到目前为止我们所展示的只是做事的唯一方法,但我们会撒谎。不幸的是,您将不得不在互联网上寻找(好吧,重新利用)别人的例子,您需要知道您在查看什么。

除了别名,即命令名的简短版本之外,您还可以使用参数来缩短操作。您有三种方法来做这件事,每种方法都可能比前一种更令人困惑。

4.6.1 截断参数名称

PowerShell 不会强迫您输入完整的参数名称。如您在第三章中可能记得的,例如,您可以使用 -comp 而不是 -ComputerName。规则是您必须输入足够多的名称,以便 PowerShell 能够区分它。如果有 -ComputerName 参数、-Common 参数和 -Composite 参数,您至少需要输入 -compu-comm-compo,因为这是唯一识别每个参数所需的最小字母数。

如果您必须使用快捷方式,这是一个不错的选择,如果您记得在输入最小长度的参数后按 Tab 键,以便 PowerShell 为您完成剩余的输入。

4.6.2 使用参数名称别名

参数也可以有自己的别名,尽管它们可能非常难以找到,因为它们没有在帮助文件或其他方便的地方显示。例如,Get-Process 命令有 -ErrorAction 参数。要发现其别名,您运行以下命令:

PS /Users/james> (get-command get-process | select -Expand 
➥ parameters).erroraction.aliases

我们已经加粗了命令和参数名称;用您好奇的任何命令和参数替换这些。在这种情况下,输出显示 -ea-ErrorAction 的别名,因此您可以运行以下命令:

PS /Users/james> Get-Process -ea Stop

完成提示将显示 -ea 别名;如果您输入 Get-Process -e 并开始按 Tab 键,它就会出现。但该命令的帮助文档根本不显示 -ea,完成提示也不表明 -ea-ErrorAction 是同一件事。

注意:这些被称为 通用参数。您可以通过运行命令 Get-Help about_CommonParameters 来了解更多关于它们的信息。

4.6.3 使用位置参数

当您在帮助文件中查看命令的语法时,您可以轻松地识别位置参数:

SYNTAX
    Get-ChildItem [[-Path] <string[]>] [[-Filter] <string>] [-Include 
  ➥ <string[]>] [-Exclude <string[]>] [-Recurse] [-De
    pth <uint>] [-Force] [-Name] [-Attributes {ReadOnly | Hidden | System | 
  ➥ Directory | Archive | Device | Normal | Tem
    porary | SparseFile | ReparsePoint | Compressed | Offline | 
  ➥ NotContentIndexed | Encrypted | IntegrityStream | NoScr
    ubData}] [-FollowSymlink] [-Directory] [-File] [-Hidden] [-ReadOnly] 
[-System] [<CommonParameters>]

在这里,-Path-Filter 都是位置参数,您知道这一点是因为参数名称和接受的输入包含在方括号内。更详细的解释可以在完整帮助文档中找到(在这种情况下是 help Get-ChildItem -Full),其内容如下:

-Path <String[]>
    Specifies a path to one or more locations. Wildcards are
    permitted. The default location is the current directory (.).
    Required?                    false
    Position?                    0
    Default value                Current directory
    Accept pipeline input?       true (ByValue, ByPropertyName)
    Accept wildcard characters?  True

这是一个明确的指示,说明 -Path 参数位于位置 0,这意味着它是 cmdlet 后的第一个参数。对于位置参数,您不需要输入参数名称;您可以在正确的位置提供其值。例如:

PS /Users/james> Get-ChildItem /Users
    Directory: /Users
Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----         3/27/2016  11:20 AM            james
d-r--         2/18/2016   2:06 AM            Shared

这与以下内容相同:

PS /Users/james> Get-ChildItem -Path /Users
    Directory: /Users
Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d-----         3/27/2019  11:20 AM            james
d-----         2/18/2019   2:06 AM            Shared

位置参数的问题在于您需要记住每个参数的位置。在您能够添加任何命名(非位置)参数之前,您必须首先以正确的顺序输入所有位置参数。如果您搞错了参数顺序,命令就会失败。对于像 Dir 这样的简单命令,您可能已经使用了多年,输入 -Path 会感觉有些奇怪,而且几乎没有人这样做。但对于更复杂的命令,可能连续有三个或四个位置参数,记住每个参数的位置可能会很困难。

例如,这个版本有点难以阅读和理解:

PS /Users/james> move file.txt /Users/james/

使用完整 cmdlet 名称和参数名称的这个版本更容易理解:

PS /Users/james> move-item -Path /tmp/file.txt -Destination /Users/james/

当您使用参数名称时,这种将参数按不同顺序排列的版本是允许的:

PS /Users/james> move -Destination /Users/james/ -Path /tmp/file.txt

我们倾向于不建议使用位置(未命名)参数,除非您在命令行上快速地执行一些粗略的工作。在所有将持久存在的操作中,例如 PowerShell 脚本文件或博客文章,都应包含所有参数名称。在这本书中,我们尽可能这样做,但在一些必须缩短命令行以适应打印页面的情况下,我们会这样做。

4.7 对外部命令的支持

到目前为止,您在 shell 中运行的(至少是我们建议您运行的)所有命令都是内置 cmdlet。在您的 Windows 机器上,PowerShell 内置了超过 2,900 个 cmdlet,在您的 Linux 或 macOS 机器上则超过 200 个。您可以添加更多——例如 Azure PowerShell、AWS PowerShell 和 SQL Server 等产品都附带包含数百个额外 cmdlet 的插件。

但你不仅限于 PowerShell cmdlets。你也可以使用你可能已经使用了多年的相同外部命令行工具,包括pingnslookupifconfigipconfig等等。因为这些不是本地的 PowerShell cmdlets,所以你使用它们的方式和你一直使用的方式相同。现在就试试几个老牌的喜欢的工具吧。

在非 Windows 操作系统上也是同样的情况。你可以使用grepbashsedawkping以及其他你可能拥有的现有命令行工具。它们将正常运行,PowerShell 将以你的旧 shell(例如 Bash)相同的方式显示它们的结果。

现在试试运行一些之前使用过的外部命令行工具。它们是否按预期工作?是否有任何失败的情况?

这一部分说明了重要的一课:使用 PowerShell 时,Microsoft(可能是有史以来第一次)并没有说,“你必须从头开始,重新学习一切。”相反,Microsoft 说的是,“如果你已经知道如何做某事,就继续那样做。我们将努力为你提供更好、更完整的工具,但你所知道的东西仍然会有效。”

在某些情况下,Microsoft 提供的工具比一些现有的、较旧的工具更好。例如,本地的Test-Connection cmdlet 提供了比旧的、外部的ping命令更多的选项和更灵活的输出。但如果你知道如何使用ping,并且它解决了你的需求,那就继续使用它。在 PowerShell 内部它也能正常工作。

话虽如此,我们不得不传达一个残酷的事实:并非每个外部命令都能在 PowerShell 内部完美运行,至少不是不需要你稍作调整。这是因为 PowerShell 的解析器——读取你输入的内容并试图弄清楚你想要 shell 做什么的那部分 shell——并不总是猜得正确。有时你会输入一个外部命令,PowerShell 会出错,开始输出错误信息,并且通常无法正常工作。

例如,当外部命令有很多参数时,事情可能会变得复杂——这就是你看到 PowerShell 出错最多的地方。我们不会深入探讨为什么它会这样工作,但这里有一个运行命令的方法,可以确保其参数能够正确工作:

$exe = "func"
$action = "new"
$language = "powershell"
$template = "HttpTrigger"
$name = "myFunc"
& $exe $action -l $language -t $template -n $name

这假设你有一个名为func的外部命令。(这个现实生活中的命令行工具用于与 Azure Functions 交互。)如果你从未使用过它或者没有它,那没关系;大多数老式的命令行工具工作方式相同,所以这仍然是一个很好的教学示例。它接受几个参数:

  • "new"在这里是指你想要执行的操作,而–newinitstartlogs是选项。

  • -l是指你希望函数使用的语言。

  • -t是指你想要使用的模板。

  • -n是指函数的名称。

我们所做的是将所有各种元素——可执行路径和名称,以及所有参数值——放入占位符中,这些占位符以 $ 字符开始。这迫使 PowerShell 将这些值视为单个单元,而不是尝试解析它们以查看是否包含命令或特殊字符。然后我们使用了调用运算符 (&),传递给它可执行名称、所有参数以及参数的值。这种模式几乎适用于任何在 PowerShell 中运行时脾气不好的命令行工具。

听起来很复杂吗?好吧,这里有一些好消息:在 PowerShell v3 及以后的版本中,你不必那么麻烦。只需在前面加上两个连字符和一个百分号,PowerShell 就不会尝试解析它;它将直接传递到你正在使用的命令行工具。为了绝对清楚,这意味着你将无法将变量作为参数值传递。

这里有一个快速示例,说明什么会失败:

PS /Users/james> $name = "MyFunctionApp"
PS /Users/james> func azure functionapp list-functions --% $name

Can't find app with name "$name"

我们尝试运行命令行工具 func 来列出所有名为 "MyFunctionApp" 的 Azure 函数,但如果我们明确指出我们想要什么,PowerShell 将将所有参数传递给底层命令,而不会尝试对它们做任何事情:

PS /Users/james> func new -t HttpTrigger -n --% "MyFunc"
Select a template: HttpTrigger
Function name: [HttpTrigger] Writing /Users/tyler/MyFuncApp/MyFunc/run.ps1
Writing /Users/tyler/MyFuncApp/MyFunc/function.json
The function "MyFunc" was created successfully from the "HttpTrigger" 
➥ template.
PS /Users/james>

希望这不会是你要经常做的事情。

4.8 处理错误

在开始使用 PowerShell 的时候,你不可避免地会看到一些难看的红色文字——甚至在你成为专家级 shell 用户之后,可能时不时也会遇到。我们都有过这种情况。但不要让红色文字让你感到压力。(就我个人而言,它让我想起了高中英语课和写得不好的作文,所以“压力”这个词用得有点轻了。)

除了令人警觉的红色文字外,PowerShell 的错误消息在多年中得到了极大的改进(这很大程度上要归功于错误消息也是开源的)。例如,如图 4.4 所示,它们试图显示 PowerShell 碰到的确切问题。

图 4.4 解释 PowerShell 错误消息

大多数错误消息都很容易理解。在图 4.4 中,一开始就说了,“你输入了 get,我不知道那是什么意思。”那是因为我们输入了错误的命令名:它应该是 Get-Command,而不是 Get Command。哎呀。图 4.5 呢?

图 4.5 什么是“第二个路径片段”?

图 4.5 中的错误消息“第二个路径片段不能是驱动器或 UNC 名称”令人困惑。什么第二个路径?我们没有输入第二个路径。我们输入了一个路径,C:\Users\James 和一个命令行参数,\s。对吗?

嗯,不是这样。解决这类问题最简单的方法之一是阅读帮助文档,并完整地输入命令。如果我们输入了 Get-ChildItem -Path C:\Users\james,我们会意识到 \s 不是一个正确的语法。我们实际上想要的是 -Recurse。有时错误信息可能看起来并不有帮助——如果你觉得你和使用 PowerShell 说着不同的语言,那么你可能是错的。显然,PowerShell 不会改变它的语言,所以你可能做错了,查阅帮助文档并完整地拼写整个命令(包括参数),通常是解决问题的最快方式。

4.9 常见混淆点

每当适当的时候,我们都会在每个章节的末尾添加一个简短的章节,涵盖一些我们常见的错误。目的是帮助你看到最常使其他管理员(就像你一样)感到困惑的事情,并避免这些问题——或者至少在你开始使用外壳时能够找到解决方案。

4.9.1 输入 cmdlet 名称

首先,是 cmdlet 名称的输入。它们总是动词-名词的形式,比如 Get-Content。所有这些选项都是新入门者可能会尝试的,但它们都不会工作:

  • 获取内容

  • GetContent

  • Get=Content

  • Get_Content

部分问题来自拼写错误(例如,= 代替 -),部分来自口头上的懒惰。我们都把命令读作 获取内容,口头省略了连字符。但你需要输入连字符。

4.9.2 输入参数

参数也是一致书写的。例如,参数 -Recurse 在其名称前有一个连字符。如果参数有值,参数名称和其值之间会有一个空格。你需要确保 cmdlet 名称与其参数之间、参数之间都有空格分隔。以下都是正确的:

  • 123

  • Dir -rec(缩写参数名称是可以的)

  • New-PSDrive -name DEMO -psprovider FileSystem -root \\Server\Share

但这些示例都是错误的:

  • Dir-rec(别名和参数之间没有空格)

  • New-PSDrive -nameDEMO(参数名称和值之间没有空格)

  • New-PSDrive -name DEMO-psprovider FileSystem(第一个参数的值和第二个参数的名称之间没有空格)

PowerShell 通常不区分大小写,这意味着 dirDIR 是相同的,同样 -RECURSE-recurse 以及 -Recurse 也是相同的。但外壳确实对那些空格和连字符很挑剔。

4.10 实验

注意:对于这个实验,您需要在 Windows、macOS 或 Linux 上运行 PowerShell v7 或更高版本。

使用本章以及上一章中关于使用帮助系统的内容,在 PowerShell 中完成以下任务:

  1. 显示正在运行的进程列表。

  2. 不使用像 ping 这样的外部命令测试到 google.com 或 bing.com 的连接。

  3. 显示所有命令列表,这些命令属于 cmdlet 类型。(这有点棘手——我们已经向你展示了 Get-Command,但你需要阅读帮助文档以了解如何缩小列表,正如我们要求的那样。)

  4. 显示所有别名的列表。

  5. 创建一个新的别名,这样你就可以从 PowerShell 提示符运行 ntst 来运行 netstat

  6. 显示以字母 p 开头的进程列表。再次,阅读必要命令的帮助信息——并且不要忘记在 PowerShell 中星号 (*) 是一个几乎通用的通配符。

  7. 使用 New-Item 命令创建一个名为 MyFolder1 的新文件夹(即目录)。然后再次创建一个名为 MyFolder2 的文件夹。如果你不熟悉 New-Item,请使用 Help

  8. 使用单个命令删除步骤 7 中的文件夹。使用 Get-Command 查找一个与步骤 7 中使用的类似 cmdlet——并且不要忘记在 PowerShell 中星号 (*) 是一个几乎通用的通配符。

我们希望这些任务对你来说看起来很简单。如果是这样——太好了。你正在利用你现有的命令行技能,让 PowerShell 为你执行一些实际的任务。如果你是命令行世界的初学者,这些任务将是这本书其余部分将要介绍内容的良好入门。

4.11 实验答案

  1. Get-Process

  2. Test-Connection google.com

  3. Get-Command -Type cmdlet

  4. Get-Alias

  5. New-Alias -Name ntst -Value netstat

  6. Get-Process -Name p*

  7. New-Item -Name MyFolder1 -Path c:\scripts -Type Directory;

    New-Item -Name MyFolder2 -Path c:\scripts -Type Directory

  8. Remove-item C:\Scripts\MyFolder*

5 使用提供程序

PowerShell 中更可能让人感到困惑的方面之一是其对提供程序的使用。一个提供程序提供了访问专用数据存储的权限,以便更容易地查看和管理。这些数据在 PowerShell 中显示为驱动器。

我们警告您,本章中的一些内容可能对您来说有点基础。我们假设您熟悉文件系统,例如,您可能知道从 shell 管理文件系统所需的所有命令。但请耐心等待:我们将以特定的方式指出这些内容,以便我们可以利用您对文件系统的现有熟悉程度来帮助使提供程序的概念更容易理解。此外,请记住 PowerShell 不是 Bash。您可能会在本章中看到一些看起来熟悉的内容,但我们向您保证,它们正在做与您习惯的完全不同的事情。

5.1 提供程序是什么?

PowerShell 提供程序,或称为 PSProvider,是一个适配器。它旨在将某种数据存储(如 Windows 注册表、Active Directory 或甚至本地文件系统)转换为看起来像磁盘驱动器的东西。您可以直接在 shell 中查看已安装的 PowerShell 提供程序列表:

PS C:\Scripts\ > Get-PSProvider
Name                 Capabilities                              Drives
----                 ------------                              ------
Alias                ShouldProcess                             {Alias}
Environment          ShouldProcess                             {Env}
FileSystem           Filter, ShouldProcess, Credentials        {/}
Function             ShouldProcess                             {Function}
Variable             ShouldProcess                             {Variable}

提供程序也可以添加到 shell 中,通常与模块一起,这是 PowerShell 可以扩展的两种方式。(我们将在本书的后面部分介绍这些扩展。)有时,启用某些 PowerShell 功能可能会创建一个新的 PSProvider。例如,您可以使用 Environment 提供程序来操作环境变量,我们将在第 5.5 节中介绍,您也可以在这里看到:

PS C:\Scripts> Get-PSProvider
Name                 Capabilities                              Drives
----                 ------------                              ------
Alias                ShouldProcess                             {Alias}
Environment          ShouldProcess                             {Env}
FileSystem           Filter, ShouldProcess, Credentials        {/}
Function             ShouldProcess                             {Function}
Variable             ShouldProcess                             {Variable}

注意,每个提供程序都有不同的功能。这很重要,因为它会影响您可以使用每个提供程序的方式。以下是一些您会看到的常见功能:

  • ShouldProcess—提供程序支持使用 -WhatIf-Confirm 参数,使您能够在做出承诺之前“测试”某些操作。

  • Filter—提供程序支持在操作提供程序内容的 cmdlet 上使用 -Filter 参数。

  • Credentials—提供程序允许您在连接到数据存储时指定备用凭据。为此有一个 -Credential 参数。

您使用提供程序来创建 PSDrive。PSDrive 使用单个提供程序来连接到数据存储。您正在创建驱动器映射,多亏了提供程序,PSDrive 能够连接到比磁盘多得多的东西。运行以下命令以查看当前连接的驱动器列表:

PS C:\Scripts> Get-PSDrive

Name           Used (GB)     Free (GB) Provider      Root
----           ---------     --------- --------      ----
/                 159.55        306.11 FileSystem    /
Alias                                  Alias
Env                                    Environment
Function                               Function
Variable                               Variable

在前面的列表中,您可以看到我们有一个使用 FileSystem 提供程序的驱动器,一个使用 Env 提供程序的驱动器,等等。PSProvider 适配数据存储,PSDrive 使其可访问。您使用一组 cmdlet 来查看和操作每个 PSDrive 暴露的数据。就大多数情况而言,您与 PSDrive 一起使用的 cmdlet 的名词中都有 Item 这个词:

PS C:\Scripts> Get-Command -Noun *item*
Capability      Name
----------      ----
Cmdlet          Clear-Item
Cmdlet          Clear-ItemProperty
Cmdlet          Copy-Item
Cmdlet          Copy-ItemProperty
Cmdlet          Get-ChildItem
Cmdlet          Get-Item
Cmdlet          Get-ItemProperty
Cmdlet          Invoke-Item
Cmdlet          Move-Item
Cmdlet          Move-ItemProperty
Cmdlet          New-Item
Cmdlet          New-ItemProperty
Cmdlet          Remove-Item
Cmdlet          Remove-ItemProperty
Cmdlet          Rename-Item
Cmdlet          Rename-ItemProperty
Cmdlet          Set-Item
Cmdlet          Set-ItemProperty

我们将使用这些 cmdlet 及其别名来开始处理我们系统上的提供器。因为它可能是你最熟悉的一个,所以我们将从文件系统——FileSystem PSProvider 开始。

5.2 理解文件系统的组织结构

文件系统是围绕两种主要类型的对象组织起来的——文件夹和文件。文件夹也是一种容器,能够包含文件和其他文件夹。文件不是一种容器类型;它们更像是端点对象。

你可能最熟悉在 macOS 上的 Finder、Linux 上的文件浏览器或 Windows 设备上的资源管理器(图 5.1)中查看文件系统,在这些工具中,驱动器、文件夹和文件的层次结构在视觉上非常明显。

图片

图 5.1 在 Finder 和 Windows 资源管理器中查看文件、文件夹和驱动器

PowerShell 的术语与文件系统的术语略有不同。因为 PSDrive 可能不指向文件系统——例如,PSDrive 可以映射到环境、注册表,甚至 SCCM 端点,这显然不是文件系统——PowerShell 不使用 文件文件夹 这些术语。相反,它使用更通用的术语 物品 来指代这些对象。文件和文件夹都被视为物品,尽管它们显然是不同类型的物品。这就是为什么我们之前展示的 cmdlet 名称都使用 Item 作为名词的原因。

物品可以,并且通常确实具有属性。例如,一个文件项可能具有包括最后写入时间、是否为只读等属性。一些物品,如文件夹,可以具有子项,这些是包含在该物品内的物品。了解这些事实应该有助于你理解我们之前展示的命令列表中的动词和名词:

  • ClearCopyGetMoveNewRemoveRenameSet 等动词都可以应用于物品(例如文件和文件夹)和物品属性(例如物品最后写入的日期或它是否为只读)。

  • Item 这个名词指的是单个对象,例如文件和文件夹。

  • ItemProperty 这个名词指的是物品的属性,例如只读、创建时间、长度等。

  • ChildItem 这个名词指的是包含在某个物品(例如文件夹)内的物品(例如文件和子文件夹)。

请记住,这些 cmdlet 是故意设计成通用的,因为它们旨在与各种数据存储一起工作。一些 cmdlet 的功能在某些情况下可能没有意义。例如,由于 FileSystem 提供器不支持 Transactions 功能,因此没有任何 -UseTransaction 参数可以与文件系统驱动器中的项目一起使用。

一些 PSProviders 不支持物品属性。例如,Environment PSProvider 是用于在 PowerShell 中使 ENV 驱动器可用的工具。此驱动器提供对环境变量的访问,但如下例所示,它们没有物品属性:

PS C:\Scripts> Get-ItemProperty -Path Env:\PSModulePath
Get-ItemProperty : Cannot use interface. The IPropertyCmdletProvider
interface is not supported by this provider.

事实上,并非每个 PSProvider 都是相同的,这可能是使提供者对 PowerShell 新手来说如此令人困惑的原因。你必须考虑每个提供者为你提供了什么访问权限,并理解即使命令知道如何执行某项操作,这并不意味着你正在与之工作的特定提供者支持该操作。

5.3 导航文件系统

当与提供者一起工作时,你需要了解的另一个命令是Set-Location。这是你用来将 shell 的当前位置更改为不同容器类型项目(如文件夹)的命令:

Linux / macOS
PS /Users/tplunk> Set-Location -Path /
PS />

Windows
PS C:\Scripts > Set-Location -Path /
PS />

你可能更熟悉这个命令的别名cd,它对应于 Bash 中的Change Directory命令。在这里,我们使用别名并将所需的路径作为位置参数传递:

Linux / macOS
PS /Users/tplunk> cd /usr/bin
PS /usr/bin>

Windows
PS C:\Scripts\> cd C:\Users\tplunk
PS C:\Users\tplunk>

非 Windows 操作系统的驱动器

macOS 和 Linux 不使用驱动器来引用离散的附加存储设备。相反,整个操作系统有一个单一的根目录,用斜杠(在 PowerShell 中,反斜杠也被接受)表示。但 PowerShell 仍然在非 Windows 操作系统中为其他提供者提供 PSDrives。尝试运行Get-PSDrive以查看可用内容。

在 PowerShell 中,创建新项目是较为棘手的一项任务。例如,如何创建一个新的目录?尝试运行New-Item,你会得到一个意外的提示:

PS C:\Users\tplunk\Documents> New-Item testFolder
Type:

记住,New-Item命令是通用的——它不知道你想要创建文件夹。它可以创建文件夹、文件以及更多内容,但你必须告诉它你想要创建的项目类型:

PS C:\Users\tplunk\Documents> New-Item testFolder -ItemType Directory

    Directory: C:\Users\tplunk\Documents

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----           5/26/19 11:56 AM                testFolder         

Windows PowerShell 确实包含了一个mkdir命令,大多数人认为它是New-Item的别名。但使用mkdir不需要你指定目录-ItemType。由于与内置的mkdir命令冲突,mkdir函数在 PowerShell Core 的非 Windows 平台上被移除。

5.4 使用通配符和字面路径

大多数提供者允许你使用Item命令以两种方式指定路径。本节将讨论这两种指定路径的方式。Item命令包括-Path参数,默认情况下该参数接受通配符。例如,查看Get-ChildItem的完整帮助,可以发现以下内容:

-Path <String[]>
    Specifies a path to one or more locations. Wildcards are
permitted. The default location is the current directory (.).
    Required?                    false
    Position?                    1
    Default value                Current directory
    Accept pipeline input?       true (ByValue, ByPropertyName)
    Accept wildcard characters?  True

*通配符代表零个或多个字符,而?通配符代表任何单个字符。你无疑已经多次使用过这个,可能使用的是Dir别名来代替Get-ChildItem

PS C:\Scripts > dir y*

    Directory: C:\Scripts

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
--r---            5/4/19 12:03 AM          70192 yaa
--r---            5/4/19 12:02 AM          18288 yacc
--r---            5/4/19 12:03 AM          17808 yes

在 Linux 和 macOS 中,大多数这些通配符都可以作为文件系统中的项目名称以及大多数其他存储中的部分。例如,在环境变量中,你会找到一些包含?的名称的值。这提出了一个问题:当你使用*?作为路径的一部分时,PowerShell 应该将其视为通配符字符还是作为字面字符?如果你寻找名为variable?的项目,你是想要名为variable?的项目,还是想要将?视为通配符,从而得到如variable7variable8之类的项目?

PowerShell 的解决方案是提供一个替代的 -LiteralPath 参数。此参数不接受通配符:

-LiteralPath <String[]>
    Specifies a path to one or more locations. Unlike the Path
    parameter, the value of the LiteralPath parameter is used exactly
    as it is typed. No characters are interpreted as wildcards. If
    the path includes escape characters, enclose it in single
    quotation marks. Single quotation marks tell PowerShell
    not to interpret any characters as escape sequences.
    Required?                    true
    Position?                    named
    Default value
    Accept pipeline input?       true (ByValue, ByPropertyName)
    Accept wildcard characters?  False

当你想将 *? 字面理解时,使用 -LiteralPath 而不是 -Path 参数。请注意,-LiteralPath 不是位置参数;如果您打算使用它,您必须键入 -LiteralPath。如果您在第一个位置提供了一个路径(例如,我们第一个例子中的 y*),它将被解释为 -Path 参数。通配符也被这样处理。

5.5 与其他提供者一起工作

了解这些其他提供者以及各种 item cmdlet 的工作方式的一个最好的方法是通过玩一个不是文件系统的 PSDrive。在 PowerShell 内置的提供者中,环境可能是最好的例子(部分原因是因为它在每个系统上都是可用的)。

我们将创建一个环境变量。请注意,我们在这个练习中使用的是 Ubuntu 终端,但无论您是在 Windows 还是 macOS 机器上,都可以同样跟随(跨平台的奇妙之处)。首先列出所有环境变量:

PS /Users/tplunk> Get-ChildItem env:*

Name                           Value
----                           -----
XPC_FLAGS                      0x0
LANG                           en_US.UTF-8
TERM                           xterm-256color
HOME                           /Users/tplunk
USER                           tplunk
PSModulePath                   /Users/tplunk/.local/share/powershell/Modu...
HOMEBREW_EDITOR                code
PWD                            /Users/tplunk
COLORTERM                      truecolor
XPC_SERVICE_NAME               0

接下来,将环境变量 A 设置为值 1

PS /Users/tplunk> Set-Item -Path Env:/A -Value 1
PS /Users/tplunk> Get-ChildItem Env:/A*

Name                           Value
----                           -----
A                              1

5.5.1 Windows 注册表

在 Windows 机器上,我们还可以查看的另一个提供者是注册表。让我们首先切换到注册表的 HKEY_CURRENT_USER 部分,由 HKCU: 驱动器暴露:

PS C:\> set-location -Path hkcu:

注意:您可能需要以管理员身份启动 PowerShell。

接下来,导航到注册表的正确部分:

PS HKCU:\> set-location -Path software
PS HKCU:\software> get-childitem
    Hive: HKEY_CURRENT_USER\software
Name                           Property
----                           --------
7-Zip                          Path64 : C:\Program Files\7-Zip\
                               Path   : C:\Program Files\7-Zip\
Adobe
Amazon
AppDataLow
AutomatedLab
BranchIO
ChangeTracker
Chromium
Clients

PS HKCU:\software> set-location microsoft
PS HKCU:\software\microsoft> Get-ChildItem
    Hive: HKEY_CURRENT_USER\software\microsoft
Name                           Property
----                           --------
Accessibility
Active Setup
ActiveMovie
ActiveSync
AppV
Assistance
AuthCookies
Avalon.Graphics
Clipboard                      ShellHotKeyUsed : 1
Common
CommsAPHost
ComPstUI
Connection Manager
CTF
Device Association Framework
DeviceDirectory                LastUserRegistrationTimestamp : {230, 198, 218, 150...}
Edge                           UsageStatsInSample                 : 1
                               EdgeUwpDataRemoverResult           : 2
                               EdgeUwpDataRemoverResultDbh        : 1
                               EdgeUwpDataRemoverResultRoaming    : 0
                               EdgeUwpDataRemoverResultData       : 1
                               EdgeUwpDataRemoverResultBackupData : 1
EdgeUpdate                     LastLogonTime-Machine : 132798161806442449
EdgeWebView                    UsageStatsInSample : 1
EventSystem
Exchange
F12
Fax

你几乎完成了。你会注意到我们坚持使用完整的 cmdlet 名称,而不是使用别名来强调 cmdlet 本身:

PS HKCU:\software\microsoft> Set-Location .\Windows
PS HKCU:\software\microsoft\Windows> Get-ChildItem
    Hive: HKEY_CURRENT_USER\software\microsoft\Windows
Name                           Property
----                           --------
AssignedAccessConfiguration
CurrentVersion
DWM                            Composition                  : 1
                               ColorPrevalence              : 0
                               ColorizationColor            : 3288334336
                               ColorizationColorBalance     : 89
                               ColorizationAfterglow        : 3288334336
                               ColorizationAfterglowBalance : 10
                               ColorizationBlurBalance      : 1
                               EnableWindowColorization     : 0
                               ColorizationGlassAttribute   : 1
                               AccentColor                  : 4278190080
                               EnableAeroPeek               : 1
Shell
TabletPC
Windows Error Reporting        LastRateLimitedDumpGenerationTime : 
➥ 132809598562003780
Winlogon

注意 EnableAeroPeek 注册表值。让我们将其更改为 0

PS HKCU:\software\microsoft\Windows> Set-ItemProperty -Path dwm -PSProperty
EnableAeroPeek -Value 0

你也可以使用 –Name 参数而不是 –PSProperty。让我们再次检查以确保更改“生效”:

PS HKCU:\software\microsoft\Windows> Get-ChildItem
    Hive: HKEY_CURRENT_USER\software\microsoft\Windows
Name                           Property
----                           --------
AssignedAccessConfiguration
CurrentVersion
DWM                            Composition                  : 1
                               ColorPrevalence              : 0
                               ColorizationColor            : 3288334336
                               ColorizationColorBalance     : 89
                               ColorizationAfterglow        : 3288334336
                               ColorizationAfterglowBalance : 10
                               ColorizationBlurBalance      : 1
                               EnableWindowColorization     : 0
                               ColorizationGlassAttribute   : 1
                               AccentColor                  : 4278190080
                               EnableAeroPeek               : 0
Shell
TabletPC
Windows Error Reporting        LastRateLimitedDumpGenerationTime : 
➥ 132809598562003780
Winlogon

任务完成!使用这些相同的技巧,你应该能够处理任何你遇到的提供者。

5.6 实验室

注意:对于这个实验,您需要任何运行 PowerShell v7.1 或更高版本的计算机。

从 PowerShell 提示符中完成以下任务:

  1. 创建一个名为 Labs 的新目录。

  2. 创建一个名为 /Labs/Test.txt 的零长度文件(使用 New-Item)。

  3. 使用 Set-Item 将 /Labs/Test.txt 的内容更改为 -TESTING 是否可能?或者你会得到一个错误?如果你得到一个错误,为什么?

  4. 使用环境提供者,显示系统环境变量 PATH 的值。

  5. 使用帮助来确定 Get-ChildItem-Filter-Include-Exclude 参数之间的区别。

5.7 实验答案

  1. New-Item -Path ~/Labs -ItemType Directory

  2. New-Item –Path ~/labs -Name test.txt -ItemType file

  3. FileSystem 提供者不支持此操作。

  4. 以下任一命令都有效:

    Get-Item env:PATH

    Dir env:PATH

  5. -Include-Exclude 必须与 –Recurse 或如果您正在查询一个容器一起使用。Filter 使用 PSProvider 的过滤功能,并非所有提供者都支持。例如,您可以在文件系统中使用 DIR –filter

除此之外

你在任务 4 中遇到任何问题了吗?在 Windows 机器上,PowerShell 是不区分大小写的,这意味着大写和小写字母并不重要。PATHpath是相同的。然而,在 Linux 或 macOS 机器上,大小写很重要:PATHpath是不同的。

6 管道:连接命令

在第四章中,你了解到在 PowerShell 中运行命令与在任何其他 shell 中运行命令相同:你输入一个 cmdlet 名称,给它参数,然后按 Enter。使 PowerShell 特殊的地方不在于它运行命令的方式,而在于它允许将多个命令以强大的单行序列连接在一起的方式。

6.1 将一个命令连接到另一个命令:为你减少工作量

PowerShell 通过使用管道将命令连接在一起。管道提供了一种方式,允许一个命令将其输出传递或管道到另一个命令,使第二个命令有东西可以处理。这可以通过两个 cmdlet 之间的垂直线|(图 6.1)看到。

图 6.1 展示了命令中的管道|

你已经在Dir | more等命令中看到了这个功能。你正在将Dir命令的输出管道传输到more命令;more命令接收那个目录列表并逐页显示。PowerShell 采用相同的管道概念并将其扩展到更有效果。

PowerShell 对管道的使用可能一开始看起来与 UNIX 和 Linux shell 的工作方式相似。但不要被误导。正如你将在接下来的几章中意识到的那样,PowerShell 的管道实现要丰富得多,也更现代。

6.2 导出到文件

PowerShell 提供了几种强大的方式将数据导出为有用的格式,例如 TXT、CSV、JSON 和 XML(可扩展标记语言)。在你的工作流程中,你可能需要从 Azure Active Directory 或从云存储中导出数据。在本章中,我们将介绍管道数据的过程。我们将从获取一些简单的内置命令的数据开始,以简化过程,但概念是相同的。

让我们从运行几个简单的命令开始。然后我们将学习如何将它们连接起来。以下是我们可以使用的一些命令:

  • Get-Process(或gps

  • Get-Command(或gcm)

  • Get-History -count 10(或h

我们选择这些命令是因为它们简单直接。我们在括号中给出了Get-ProcessGet-Command的别名。对于Get-History,我们指定了-count参数的值为10,这样我们只获取最后 10 个历史条目。

现在试试看 选择你想要使用的命令。我们使用Get-Process作为以下示例;你可以坚持我们列出的其中之一,或者在这三者之间切换以查看结果的不同。

你看到了什么?当我们运行Get-Process时,一个包含多个信息列的表格出现在屏幕上(图 6.2)。

图 6.2 Get-Process的输出是一个包含多个信息列的表格。

在屏幕上显示这些信息是很好的,但你可能还想做更多的事情。例如,如果你想制作内存和 CPU 利用率的图表和图形,你可能想将信息导出到一个 CSV 文件中,该文件可以被导入到应用程序中进行进一步的数据处理。

6.2.1 导出到 CSV

将信息导出到文件时,管道和第二个命令很有用:

Get-Process | Export-CSV procs.CSV

与将Dir通过管道传递到more类似,我们已经将我们的进程通过管道传递到Export-CSV。第二个 cmdlet 有一个必须的位置参数(在第三章中讨论),我们已使用它来指定输出文件名。因为Export-CSV是一个本机 PowerShell cmdlet,它知道如何将Get-Process生成的输出转换为标准 CSV 文件。

继续在 Visual Studio Code 中打开文件,查看结果,如图 6.3 所示。

code ./procs.CSV

图片

图 6.3 在 Windows 记事本中查看导出的 CSV 文件

文件的第一行包含列标题,随后的行列出计算机上运行的各个进程的信息。你可以将几乎任何Get- cmdlet 的输出通过管道传递到Export-CSV,以获得出色的结果。你可能还会注意到,CSV 文件包含的信息比通常在屏幕上显示的要多得多。这是故意的。Shell 知道它不可能在屏幕上显示所有这些信息,因此它使用由 Microsoft 提供的配置来选择屏幕显示的最重要信息。在后面的章节中,我们将向您展示如何覆盖该配置以显示您想要的内容。

一旦信息被保存到 CSV 文件中,你可以轻松地通过电子邮件发送给同事,并要求他们在 PowerShell 中查看它。为此,他们需要导入该文件:

Import-CSV procs.CSV

Shell 会读取 CSV 文件并显示进程信息。它不会基于实时信息,但会是从你创建 CSV 文件的精确时间点的快照。

6.2.2 导出到 JSON

假设你想导出进程信息并包含线程信息。线程信息是在process对象上称为嵌套属性的内容。让我们看一下(图 6.4)。请注意,Select-Object Threads告诉 PowerShell 只显示Threads属性。我们将在第八章更深入地介绍Select-Object

图片

图 6.4 我们展示了两种不同的显示Threads属性的方法。

如果你尝试使用ConvertTo-CSV导出进程,Threads属性将具有值System.Diagnostics.ProcessThreadCollection。因此,如果我们想导出Threads属性下的嵌套属性,我们需要另一种导出数据的方法。

PowerShell 还有一个ConvertTo-Json命令,它创建一个 JSON 文件,允许存储这些嵌套属性。大多数语言都有理解 JSON 的库。你还将有一个匹配的ConvertFrom-Json命令。ConvertFromConvertTo命令(如ConvertFrom-CSVConvertTo-CSV)要么在管道上生成字符串,要么消耗字符串。这是将进程转换为 JSON 的命令,使用Out-File将结果保存到文件中:

PS C:\Scripts\> Get-Process | ConvertTo-Json | Out-File procs.json

你可以通过运行以下命令来获取数据:

PS C:\Scripts\> Get-Content ./procs.json | ConvertFrom-Json

如果你运行此命令,你会注意到数据格式与运行Get-Process命令时的格式非常不同。我们将在下一节中向你展示如何处理这种情况。图 6.5 展示了导出 JSON 中Threads属性的一个摘录。

图 6.5 展示Threads属性在 JSON 格式下的样子

6.2.3 导出为 XML

在上一节中,你注意到ConvertFrom-Json返回的数据与从原始命令获取的数据显示方式大不相同。这是因为对象类型不同(我们将在第八章中介绍对象)。有一个命令可以导出数据并获取原始对象。

PowerShell 有一个Export-Clixml命令,它创建一个通用的 CLI XML 文件,允许 PowerShell 重建原始对象(或非常接近)。Clixml是 PowerShell 特有的,尽管任何程序在技术上都可以理解它产生的 XML,但最好是在结果被 PowerShell 使用时使用。你还将有一个匹配的Import-Clixml命令。导入和导出命令(如Import-CSVExport-CSV)都期望一个文件名作为必选参数。

何时使用 Export-Clixml

如果获取原始对象更好,为什么不总是使用它呢?有几个缺点:

  • 格式通常要大得多。

  • 格式是 PowerShell 特有的,可能在其他语言中难以阅读。

  • 在 Windows 上,PowerShell 将加密文件的安全相关部分,这意味着只有创建文件的用户或机器才能解密该文件。

现在尝试一下 尝试将进程、命令等导出到 CLIXML 文件。确保你可以重新导入文件,并尝试在你的系统上的 Visual Studio Code 或其他文本编辑器中打开生成的文件,以查看每个应用程序如何显示信息。

PowerShell 是否包含其他导入或导出命令?你可以通过使用Get-Command命令并指定带有ImportExport-Verb参数来找出答案。

现在尝试一下 看看 PowerShell 是否包含其他导入或导出命令。你可能想在将新命令加载到 shell 中之后重复此检查——你将在下一章中这样做。

6.2.4 Out-File

我们已经讨论了 CSV、JSON 和 XML 文件,但如果你只是想用一个扁平文件来存储你的数据怎么办?让我们看看 Out-File 命令。它将管道中的数据导出到一个扁平文件中——在我们的例子中是在一个文本文件下面:

Get-ChildItem | Select-Object Name | Out-File process.txt

6.2.5 比较文件

CSV 和 CLIXML 文件都可以用于持久化信息快照,与他人共享这些快照,并在以后的时间审查这些快照。实际上,Compare-Object 有一种很好的使用它们的方法。

首先,运行 help Compare-Object 并阅读这个 cmdlet 的帮助信息。我们希望你特别注意三个参数:-ReferenceObject-DifferenceObject-Property

Compare-Object 是设计用来接受两组信息并将它们相互比较的。例如,想象一下你在并排放置的两个计算机上运行 Get-Process。配置得正好符合你要求的计算机在左边,是参考计算机。右边的计算机可能相同,也可能有些不同;这个是差异计算机。在每台计算机上运行命令后,你会看到两张信息表,你的任务是确定这两者之间是否存在任何差异。

因为这些是你正在查看的进程,所以你总是会看到 CPU 和内存利用率等数据上的差异,所以我们将忽略这些列。实际上,关注名称列,因为我们想看看差异计算机是否包含比参考计算机更多的或更少的进程。你可能需要花一些时间来比较两张表中的所有进程名称,但你不一定需要——这正是 Compare-Object 会为你做的。假设你坐在参考计算机前并运行以下命令:

Get-Process | Export-CliXML reference.xml

我们更倾向于使用 CLIXML 而不是 CSV 进行此类比较,因为 CLIXML 可以比扁平的 CSV 文件存储更多信息。然后,您将那个 XML 文件传输到差异计算机上并运行以下命令:

Compare-Object -Reference (Import-Clixml reference.xml) 
➥-Difference (Get-Process) -Property Name

因为前面的步骤有点棘手,我们将解释正在发生的事情:

  • 就像数学一样,PowerShell 中的括号控制着执行顺序。在上一个例子中,它们强制 Import-ClixmlGet-ProcessCompare-Object 运行之前执行。Import-Clixml 的输出被馈送到 -Reference 参数,而 Get-Process 的输出被馈送到 -Difference 参数。

    参数名称是 -ReferenceObject-DifferenceObject。请记住,你可以通过输入足够多的参数名称来缩写参数名称,以便 shell 能够确定你想要哪一个。在这种情况下,-Reference-Difference 已经足够独特地识别这些参数了。我们可能还可以进一步缩短它们,例如缩短为 -ref-diff,而命令仍然可以正常工作。

  • 与比较两个完整的表格不同,Compare-Object专注于Name,因为我们给了它-Property参数。如果我们没有这样做,它就会认为每个进程都不同,因为像VMCPUPM这样的列的值总是会有所不同。

  • 结果是一个表格,告诉你有什么不同。在参考集中但不在差异集中的每个进程都将有一个<=指示符(表示该进程仅在左侧存在)。如果一个进程在差异计算机上但不在参考计算机上,它将有一个=>指示符。在两个集合中都匹配的进程不包括在Compare-Object输出中。

现在试试看。如果你没有两台计算机,可以从导出你的当前进程到一个 CLIXML 文件开始,就像上一个例子中展示的那样。然后启动一些额外的进程,比如另一个pwsh,例如 Visual Studio Code、nano(一个命令行编辑器)、浏览器或一个游戏。你的计算机将成为差异计算机(在右侧),而 CLIXML 文件仍然是参考计算机(在左侧)。

这里是我们测试的输出结果:

PS C:\Scripts>Compare-Object -ReferenceObject (Import-Clixml ./procs.xml) 
 ➥ -DifferenceObject (Get-Process) -Property name

name            SideIndicator
----            -------------
nano            =>
pwsh            =>

这是一个有用的管理技巧。如果你把那些参考 CLIXML 文件视为配置基线,你就可以将任何当前计算机与该基线进行比较,并获取差异报告。在这本书中,你将发现更多可以检索管理信息的 cmdlets,所有这些都可以被管道输入到 CLIXML 文件中,成为基线。你可以快速构建一组基线文件,用于服务、进程、操作系统配置、用户和组等,然后随时使用这些文件将系统的当前状态与基线进行比较。

现在试试看。为了好玩,再次运行Compare-Object命令,但完全省略-Property参数。看到了吗?每个进程都被列出,因为像PMVM等值都发生了变化,尽管它们是相同的进程。输出也不太有用,因为它显示了每个进程的类型名称和进程名称。

顺便说一下,你应该知道Compare-Object在比较文本文件方面通常表现不佳。尽管其他操作系统和 shell 有一个明确用于比较文本文件的Compare-Object命令,但 PowerShell 的Compare-Object命令的工作方式不同。你将在本章的实验中看到它有多么不同。

注意:如果你经常使用Get-ProcessGet-Command,那是有意为之的。我们保证你可以访问这些 cmdlets,因为它们是 PowerShell 的本地命令,不需要像 Azure PowerShell 或 AWS Tools for PowerShell 这样的插件。话虽如此,你正在学习的技能适用于你将需要运行的每个 cmdlet,包括那些与 Azure 计算、Azure 存储、Azure 虚拟网络和 Azure PowerShell 模块一起提供的 cmdlet。

6.3 将输出重定向到文件

每当你有格式良好的输出时——例如,由 Get-CommandGet-Process 生成的表格——你可能希望将其保存在文件中,甚至打印在纸上。通常,cmdlet 输出会被定向到屏幕,PowerShell 将其称为 host,但你也可以改变输出去向。我们已经向你展示了其中一种方法:

Dir > DirectoryList.txt

> 字符是 PowerShell 添加的一个快捷方式,以提供与 Bash shell 的语法兼容性。实际上,当你运行那个命令时,PowerShell 在幕后会做以下操作:

Dir | Out-File DirectoryList.txt

你可以在自己的机器上运行相同的命令,而不是使用 > 语法。你为什么这么做呢?因为 Out-File 还提供了额外的参数,让你可以指定替代字符编码(如 UTF-8 或 Unicode),将内容追加到现有文件,等等。默认情况下,Out-File 创建的文件宽度为 80 列,这意味着有时 PowerShell 可能会调整命令输出以适应 80 个字符。这种调整可能会使文件内容看起来与你在屏幕上运行相同命令时不同。阅读 Out-File 的帮助文件,看看你是否能找到一个参数,可以让你将输出文件宽度更改为 80 个字符以外的值。

现在尝试一下——不要在这里寻找答案——打开那个帮助文件,看看你能找到什么。我们保证你会在几分钟内找到正确的参数。

PowerShell 有许多 Out- 命令。其中一个叫做 Out-Default,当你不指定不同的 Out- 命令时,shell 会使用它。如果你运行

Dir

你实际上正在运行

Dir | Out-Default

即使你没有意识到。Out-Default 除了将内容直接定向到 Out-Host 外,没有做更多的事情,这意味着你正在运行

Dir | Out-Default | Out-Host

在没有意识到的情况下。Out-Host 在屏幕上显示信息。你能找到其他哪些 Out- 命令?

现在尝试一下——现在是调查其他 Out- 命令的时候了。为了开始,尝试使用 Help 命令和通配符,例如 Help Out*。另一个选项是使用 Get-Command 的相同方式,例如 Get-Command Out*。或者,你可以指定 -Verb 参数 Get-Command -Verb Out。你得到了什么?

Out-NullOut-String 有其特定的用途,我们现在不会深入探讨,但您随时可以阅读它们的相关帮助文件,并查看那些文件中包含的示例。

6.4 转换为 HTML

想要生成 HTML 报告?将你的命令通过管道传递给 ConvertTo-Html。这个命令生成格式良好的通用 HTML,可以在任何网页浏览器中显示。它看起来很朴素,但如果你需要的话,可以引用一个级联样式表(CSS)文件来指定更吸引人的格式。请注意,这个命令不需要指定文件名:

Get-Process -Id $PID | ConvertTo-Html

现在尝试一下——确保你自己运行那个命令——我们希望你在继续之前看到它做了什么。

在 PowerShell 世界中,动词 Export 暗示你正在获取数据,将其转换为其他格式,并将该格式保存到某种存储中,例如文件。动词 ConvertTo 仅暗示该过程的一部分:将数据转换为不同格式,但不将其保存到文件中。当你运行前面的命令时,你得到了一屏幕的 HTML,这可能不是你想要的。停一下;你能想到如何将那个 HTML 保存到磁盘上的文本文件中吗?

现在试试看。如果你能想到一种方法,请在继续阅读之前尝试一下。

这个命令可以解决问题:

Get-Process | ConvertTo-Html | Out-File processes.html

看看连接越来越多的命令如何让你拥有越来越强大的命令行?每个命令处理过程的一个步骤,整个命令行作为一个整体完成一个有用的任务。

PowerShell 随带其他 ConvertTo- 命令,包括 ConvertTo-CSVConvertTo-Xml。与 ConvertTo-Html 类似,这些命令不会在磁盘上创建文件;它们分别将命令输出转换为 CSV 或 XML。你可以将转换后的输出通过管道传递给 Out-File 命令,然后将其保存到磁盘上,尽管使用 Export-CSVExport-Clixml 会更简洁,因为这两个命令同时完成转换和保存。

除此之外

现在是时候提供更多无用的背景信息了,尽管在这种情况下,这是许多学生经常向我们提出的问题的答案:为什么微软会同时提供 Export-CSVConvertTo-CSV,以及两个几乎相同的 XML 命令?

在某些高级场景中,你可能不希望将数据保存到磁盘上的文件中。例如,你可能想要将数据转换为 XML,然后将其传输到 Web 服务或其他目的地。通过拥有不保存到文件的独立 ConvertTo- 命令,你就有灵活性去做你想做的事情。

6.5 使用修改系统的命令:终止进程

导出和转换并不是连接两个命令的唯一原因。例如,考虑一下——但请别运行——这个命令:

Get-Process | Stop-Process

你能想象这个命令会做什么吗?我们将告诉你:你可以终止关键进程。它会检索每个进程,然后尝试终止每个进程。它会到达一个关键进程,例如 macOS 上的 /usr/sbin/coreaudiod,然后你的电脑将无法再播放声音。如果你在虚拟机中运行 PowerShell 并想找点乐子,请尝试运行该命令。

重点是具有相同名词(在这种情况下,Process)的 cmdlet 可以经常在彼此之间传递信息。通常,你会指定特定进程的名称,而不是尝试停止所有进程:

Get-Process -Name bash | Stop-Process

作业提供类似的功能:Get-Job 的输出可以被传递到 Stop-JobReceive-JobWait-Job 等命令。我们将在第十四章详细介绍作业。

正如你所期望的,特定的规则限制了哪些命令可以相互连接。例如,如果你查看一个如Get-Process | New-Alias的命令序列,你可能不会期望它做任何有意义的事情(尽管它可能确实做了某些无意义的事情)。在第七章中,我们将深入探讨控制命令如何相互连接的规则。

我们还希望你了解关于Stop-JobStop-Process等 cmdlet 的另一个信息。这些 cmdlet 以某种方式修改系统,所有修改系统的 cmdlet 都有一个内部定义的影响级别。cmdlet 的创建者设置这个影响级别,并且不能更改。shell 有一个相应的$Confirm-Preference设置,默认设置为High。输入以下设置名称以查看你的 shell 设置:

PS /Users/jsnover> $ConfirmPreference
High

这就是这样工作的:当 cmdlet 的内部影响级别等于或高于 shell 的$ConfirmPreference设置时,shell 会自动询问,“你确定吗?”当 cmdlet 执行它试图做的事情时。如果你使用虚拟机尝试我们之前提到的会导致电脑崩溃的命令,你可能对每个进程都被问到了“你确定吗?”当 cmdlet 的内部影响级别低于 shell 的$ConfirmPreference设置时,你不会自动得到“你确定吗?”提示。但你可以强制 shell 询问你是否确定:

Get-Process | Stop-Process -Confirm

你只需将-Confirm参数添加到 cmdlet 中。任何对系统进行某种更改的 cmdlet 都应该支持它,如果它支持,它将在 cmdlet 的帮助文件中显示。

类似的参数是-WhatIf。任何支持-Confirm的 cmdlet 都支持-WhatIf参数。默认情况下,-WhatIf参数不会被触发,但你可以随时指定它:

PS C:\Scripts > Get-Process | Stop-Process –WhatIf
What if: Performing operation "Stop-Process" on Target "conhost (1920)
".
What if: Performing operation "Stop-Process" on Target "conhost (1960)
".
What if: Performing operation "Stop-Process" on Target "conhost (2460)
".
What if: Performing operation "Stop-Process" on Target "csrss (316)".

这会告诉你 cmdlet 会做什么,而不让 cmdlet 实际执行。这是一种有用的方式来预览一个可能危险的 cmdlet 会对你的电脑造成什么影响,以确保你确实想要这样做。

6.6 常见混淆点

在 PowerShell 中,一个常见的混淆点围绕着Export-CSVExport-Clixml命令。从技术上讲,这两个命令都创建文本文件。这两个命令的输出可以在 Visual Studio Code 中查看,如图 6.3 所示。但你必须承认,文本肯定是一种特殊的格式——要么是 CSV,要么是 XML。

当有人被要求将这些文件读回到 shell 时,混淆往往会出现。你是使用Get-Content(或其别名type)吗?例如,假设你这样做:

PS C:\Scripts>Get-Process | Select-Object -First 5 | export-CSV processes.CSV 
➥ -IncludeTypeInformation 

注意到-IncludeTypeInformation开关;我们稍后会回到它。现在,尝试使用Get-Content读取它:

PS C:\Scripts>Get-Content ./processes.CSV                                    
#TYPE System.Diagnostics.Process "Name","SI","Handles","VM","WS","PM","NPM","Path",
"Parent","Company","CPU","FileVersion","ProductVersion","Description","Product","__NounName","SafeHandle",
"Handle","BasePriority","ExitCode","HasExited","StartTime","ExitTime","Id","MachineName","MaxWorkingSet",
"MinWorkingSet","Modules","NonpagedSystemMemorySize64","NonpagedSystemMemorySize","PagedMemorySize64",
"PagedMemorySize","PagedSystemMemorySize64","PagedSystemMemorySize","PeakPagedMemorySize64",
"PeakPagedMemorySize","PeakWorkingSet64","PeakWorkingSet","PeakVirtualMemorySize64","PeakVirtualMemorySize",
"PriorityBoostEnabled","PriorityClass","PrivateMemorySize64","PrivateMemorySize","ProcessName",
"ProcessorAffinity","SessionId","StartInfo","Threads","HandleCount","VirtualMemorySize64","VirtualMemorySize",
"EnableRaisingEvents","StandardInput","StandardOutput","StandardError","WorkingSet64","WorkingSet",
"SynchronizingObject","MainModule","MainWindowHandle","MainWindowTitle","Responding",
"PrivilegedProcessorTime","TotalProcessorTime","UserProcessorTime","Site","Container"
"","87628","0","0","0","0","0",,,,,,,,,"Process",
"Microsoft.Win32.SafeHandles.SafeProcessHandle","0","0",,"False",,,"0",".",,,"System.Diagnostics.ProcessModuleCollection",
"0","0","0","0","0","0","0","0","0","0","0","0","False",
"Normal","0","0","",,"87628",,"System.Diagnostics.ProcessThreadCollection","0","0","0",
"False",,,,"0","0",,,"0","","True",,,,,

我们截断了前面的输出,但还有很多类似的输出。看起来像是垃圾,对吧?你正在查看原始 CSV 数据。命令根本就没有尝试解释,或解析数据。与Import-CSV的结果进行对比:

PS C:\Scripts>Import-CSV ./processes.CSV
 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00       0.00       0.00       0 ...28 
      0     0.00       0.00       0.00       1   1 
      0     0.00       0.00       0.00      43  43 
      0     0.00       0.00       0.00      44  44 
      0     0.00       0.00       0.00      47  47

更好一些,对吧?Import- cmdlets 会注意文件中的内容,尝试进行解释,并创建一个看起来更像原始命令(在这种情况下是 Get-Process)输出的显示。为了使用 Export-CSV 来做到这一点,你必须使用 -IncludeTypeInformation 开关。通常,如果你使用 Export-CSV 创建文件,你会通过使用 Import-CSV 来读取它。如果你使用 Export-Clixml 创建它,你通常通过使用 Import-Clixml 来读取它。通过使用这些命令成对使用,你可以获得更好的结果。仅当你在读取文本文件且不希望 PowerShell 尝试解析数据时(即当你想处理原始文本时),才使用 Get-Content

6.7 实验室

我们将本章的文本略微缩短,因为一些示例可能花费了你更多的时间来完成,并且我们希望你能花更多的时间来完成以下动手实验。如果你还没有完成本章中所有的“现在试试”任务,我们强烈建议你在尝试这些任务之前完成它们:

  1. 创建两个相似但不同的文本文件。尝试使用 Compare-Object 来比较它们。运行类似以下命令:Compare-Object -Reference (Get-Content File1.txt) -Difference (Get-Content File2.txt)。如果文件只有一行文本不同,该命令应该可以工作。

  2. 如果你从控制台运行 Get-Command | Export-CSV commands.CSV | Out-File,会发生什么?为什么会发生这种情况?

  3. 除了获取一个或多个作业并将它们通过管道传递给 Stop-Job 以外,Stop-Job 还提供了哪些方法让你指定要停止的作业或作业?是否可以在不使用 Get-Job 的情况下停止作业?

  4. 如果你想要创建一个以管道分隔符分隔的文件而不是 CSV 文件,你仍然会使用 Export-CSV 命令,但你需要指定哪些参数?

  5. 你如何在导出的 CSV 文件的顶部注释行 # 中包含类型信息?

  6. Export-ClixmlExport-CSV 都会修改系统,因为它们可以创建和覆盖文件。哪个参数可以防止它们覆盖现有文件?哪个参数会在写入输出文件之前询问你是否确定?

  7. 操作系统维护了几个区域设置,其中包括默认的分隔符列表。在美国系统上,该分隔符是逗号。你如何告诉 Export-CSV 使用系统的默认分隔符而不是逗号?

6.8 实验室答案

  1. PS C:\Scripts > "I am the walrus" | Out-File file1.txt

    PS C:\Scripts > "I'm a believer" | Out-File file2.txt

    PS C:\Scripts > $f1 = Get-Content .\file1.txt

    PS C:\Scripts > $f2 = Get-Content .\file2.txt

    PS C:\Scripts > Compare-Object $f1 $f2

    InputObject                             SideIndicator

    -----------                             -------------

    I'm a believer                          =>

    I am the walrus                         <=

  2. 如果您没有使用 Out-File 指定文件名,您将收到错误。但即使指定了,Out-File 也不会执行任何操作,因为文件是由 Export-CSV 创建的。

  3. Stop-Job 可以接受一个或多个作业名称作为 –Name 参数的参数值。例如,您可以运行以下命令:

    Stop-job jobName

  4. get-Command | Export-CSV commands.CSV -Delimiter "|"

  5. Export-CSV 中使用 –IncludeTypeInformation 参数。

  6. Get-Command | Export-CSV services.CSV –NoClobber

    Get-Command | Export-CSV services.CSV -Confirm

  7. Get-Command | Export-CSV services.CSV -UseCulture

7 添加命令

PowerShell 的主要优势之一是其可扩展性。随着 Microsoft 继续投资 PowerShell,它为 Azure 计算(虚拟机)、Azure SQL、Azure 虚拟网络、Azure DNS 等产品开发了越来越多的命令。您通常通过 Azure 门户来管理这些命令。我们将在本章后面讨论如何安装 Azure PowerShell 模块。

7.1 一个 shell 如何做到一切

一个 shell 如何做到一切?让我们思考一下你的智能手机。你是如何在不升级操作系统的情况下给你的手机添加功能?你安装了一个应用程序。

当你安装一个应用程序时,它可以添加小部件,甚至添加你可以对语音助手说的命令。向语音助手添加命令可能与 PowerShell 的扩展模型最相似。PowerShell 提供了添加命令的方式,你可以使用。

假设你安装了一个名为 Ride Share 的应用程序。该应用程序可能添加了一个语音命令,让你可以说,“用 Ride Share 为我预订去工作的车。”手机找到你的工作地址并将命令发送到应用程序。

PowerShell 以类似的方式工作。PowerShell 将其扩展称为模块。没有小部件,但可以添加命令。我们将在下一节中介绍如何安装模块。

7.2 扩展:查找和安装模块

在 PowerShell 6.0 之前,有两种类型的扩展:模块和插件。PowerShell v6 及更高版本支持一种称为模块的扩展类型。模块旨在更加独立且易于分发。

Microsoft 引入了一个名为 PowerShellGet 的模块,它使得从在线仓库中搜索、下载、安装和更新模块变得更加容易。PowerShellGet 与 Linux 管理员非常喜欢的包管理器类似——rpm、yum、apt-get 等。Microsoft 甚至运行一个在线画廊或仓库,称为 PowerShell Gallery (powershellgallery.com)。

WARning Microsoft runs并不意味着Microsoft 生产、验证和认可。PowerShell Gallery 包含社区贡献的代码,在您的环境中运行他人的代码之前,您应谨慎行事。

你可以在powershellgallery.com/上搜索模块,就像大多数搜索引擎一样。Azure 的模块称为Az。图 7.1 展示了搜索该模块的示例。

图 7.1 展示了在 PowerShell Gallery 中搜索 Az 的过程

如果你点击 Az 模块名称,它将带你到有关该模块的更多详细信息。在“包详细信息”>“PSEditions”下,你可以检查作者是否已使用 PowerShell Core 测试了该模块(图 7.2)。

图 7.2 展示模块与 Core 兼容

然后查看安装选项(图 7.3)。

图 7.3 展示通过 PowerShell Gallery 可用的安装命令

注意它指出至少需要 PowerShell 5.1 才能运行该模块,并提供了安装模块的说明。如果我们运行命令 Install-Module -Name Az,我们可以看到发生了什么:

PS C:\Scripts> Install-Module az 

Untrusted repository
You are installing the modules from an untrusted repository. If you trust this 
repository, change its InstallationPolicy value by running the Set-PSRepository
 cmdlet. Are you sure you want to install the modules from 'PSGallery'?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help 
(default is "N"):y

它会提示你询问你是否信任从库中安装,如果你说“是”,那么它就会安装模块。你可以运行 Get-Module 命令来验证是否已安装模块,但需要 -ListAvailable 参数,因为模块尚未加载:

PS C:\Scripts> Get-Module az -ListAvailable

    Directory: 
    C:\Users\Tyler\Documents\powershell\Modules

ModuleType Version    Name
---------- -------    ----                               
Script     6.3.0      Az

路径和版本可能因你而异,但输出应该是类似的。

更多关于从互联网获取模块的信息

PowerShellGet 模块允许从 PowerShellGallery.com 安装模块。使用 PowerShellGet 很简单,甚至可能很有趣:

  • 运行 Register-PSRepository 命令来添加存储库的 URL。PowerShellGallery.com 通常默认设置,但也可以在内部设置自己的“库”用于私人用途,并且你会使用 Register-PSRepository 来指向它。

  • 使用 Find-Module 命令在存储库中查找模块。你可以在名称中使用通配符 (*),指定标签,并有许多其他选项来缩小搜索结果。

  • 使用 Install-Module 命令在找到模块后下载并安装它。

  • 使用 Update-Module 命令确保你的模块本地副本是最新的版本,如果不是,则下载并安装最新版本。

PowerShellGet 包含其他几个命令(PowerShellGallery.com 链接到文档),但你将开始使用的是这些命令。例如,尝试从 PowerShell Gallery 安装 Azure PowerShell 模块或 Jeff Hicks 的 PSScriptTools 模块。

7.3 扩展:查找和添加模块

PowerShell 会自动在一系列路径中查找模块。PSModulePath 环境变量定义了 PowerShell 预期模块存在的路径:

PS /Users/Tyler> (Get-Content Env:/PSModulePath) -split ':'
C/Users/Tyler.local/share/powershell/Modules
/usr/local/share/powershell/Modules
/usr/local/microsoft/powershell/7/Modules 

现在试试看。前面的命令是在 macOS 设备上运行的。运行命令 (Get-Content Env:/PSModulePath) -split ':'` 并查看你的结果。请注意,这取决于你使用的操作系统,它们会有所不同。

如此例所示,有三个默认位置:一个在 PowerShell 安装文件夹中,系统模块就存储在这里;一个在 local/share/ 文件夹中,你可以放置所有用户共享的模块;还有一个在 .local 文件夹中,你可以添加任何个人模块。如果你运行的是 PowerShell 的更晚版本,你可能会看到 Microsoft 现在使用的其他位置。你也可以从任何其他位置添加模块,只要你知道它的完整路径。在你的 Windows 机器上,你会看到类似的布局,模块就是在这里安装的:

$env:PSModulePath -split ';'

C:\Users\Administrator\Documents\PowerShell\7\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\Windows\system32\PowerShell\7\Modules

在 PowerShell 中,路径非常重要。如果您有位于其他位置的模块,您应该将它们的路径添加到 PSModulePath 环境变量中。您可以在您的配置文件中使用以下命令完成此操作(我们将在本章后面介绍如何设置配置文件):

PS C:\Scripts> $env:PSModulePath += [System.IO.Path]::PathSeparator + 
➥ 'C:\Scripts/myModules' 

注意:在上面的示例中,我们还没有提到一些事情。但没关系。我们承诺我们将会提到它们。

为什么 PSModulePath 如此重要?因为它允许 PowerShell 自动定位您计算机上的所有模块。在找到您的模块后,PowerShell 会自动发现它们。对您来说,这就像您的所有模块一直都被加载一样。请求模块的帮助,您就会得到它,而无需加载它。运行您找到的任何命令,PowerShell 会自动加载包含该命令的模块。PowerShell 的 Update-Help 命令也使用 PSModulePath 来发现您拥有的模块,然后为每个模块寻找更新的帮助文件。

例如,运行 Get-Module | Remove-Module 来移除任何已加载的模块。这将移除当前会话中的几乎所有命令,因此如果您尝试这样做,请关闭并重新打开 PowerShell。然后运行以下命令(您的结果可能略有不同,具体取决于您的操作系统和您安装的模块):

PS C:\Scripts> help *storaget*
       Name                                    Category ModuleName

       Get-AzStorageTable                      Cmdlet   Az.Storage
       Get-AzStorageTableStoredAccessPolicy    Cmdlet   Az.Storage
       New-AzStorageTable                      Cmdlet   Az.Storage
       New-AzStorageTableSASToken              Cmdlet   Az.Storage
       New-AzStorageTableStoredAccessPolicy    Cmdlet   Az.Storage
       Remove-AzStorageTable                   Cmdlet   Az.Storage
       Remove-AzStorageTableStoredAccessPolicy Cmdlet   Az.Storage
       Set-AzStorageTableStoredAccessPolicy    Cmdlet   Az.Storage 

如您所见,PowerShell 发现了几个命令(属于 Cmdlet 类型),它们的名称中包含单词 storage(我在示例中使用了 storaget 以简化结果)。您可以对其中之一请求帮助,即使您还没有加载该模块:

PS C:\Scripts> Get-Help Get-AzStorageTable           
       NAME
       Get-AzStorageTable
SYNOPSIS
    Lists the storage tables.

SYNTAX
    Get-AzStorageTable [[-Name] <System.String>] [-Context
<Microsoft.Azure.Commands.Common.Authentication.Abstractions.IStorageContext>]
➥ [-DefaultProfile

<Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core
➥ .IAzureContextContainer>] [<CommonParameters]

如果您愿意,甚至可以运行命令,PowerShell 会确保为您加载模块。这种自动发现和自动加载功能非常有用,可以帮助您找到并使用在您启动 shell 时甚至不存在的命令。

PowerShell 的模块自动发现功能使得 shell 能够完成命令名称(在控制台中使用 Tab 键或 Visual Studio Code 中的 IntelliSense),显示帮助,并运行命令,即使对于您没有明确加载到内存中的模块也是如此。这些功能使得保持 PSModulePath 尽可能精简(即不要在其中放置很多不同的位置)并保持模块更新变得值得努力。

如果模块不在 PSModulePath 引用的路径之一中,您需要运行 Import-Module 并指定模块的完整路径,例如 C:\Scripts\myModules\myModule

模块还可以添加 PowerShell 提供程序。运行 Get-PSProvider 将会列出提供程序列表:

PS /Users/James> get-psprovider

Name                 Capabilities                  Drives
----                 ------------                  ------
Alias                ShouldProcess                 {Alias}
Environment          ShouldProcess                 {Env}
FileSystem           Filter, ShouldProcess, Crede... {/, Temp}
Function             ShouldProcess                 {Function}
Variable             ShouldProcess                 {Variable} 

安装 Google Cloud 命令

安装和添加 Google Cloud 命令有些不同,因为它们打破了规则——它们在第一次尝试使用模块时需要输入。你首先像其他模块一样安装它们的命令:Install-Module -Name GoogleCloud。但如果你尝试查找命令,它将会失败。因此,你需要运行 Import-Module GoogleCloud -Force-Force 是以防 PowerShell 认为模块已加载;它将尝试重新加载它。现在模块将提示你完成安装(假设它与我们编写本书时的设计相同)。现在我们将运行处理 Google Cloud SQL 实例的命令。

PS C:\Scripts> Get-Command -Name *-gcSqlinstance 

CommandType     Name
-----------     ----                                             
Cmdlet          Add-GcSqlInstance                                
Cmdlet          ConvertTo-GcSqlInstance                          
Cmdlet          Export-GcSqlInstance                             
Cmdlet          Get-GcSqlInstance                                
Cmdlet          Import-GcSqlInstance                             
Cmdlet          Remove-GcSqlInstance                             
Cmdlet          Restart-GcSqlInstance                            
Cmdlet          Update-GcSqlInstance

7.4 命令冲突和移除扩展

仔细看看我们为 Google Cloud SQL 实例和 Azure 表存储添加的命令。你注意到命令名称有什么特别之处吗?

大多数 PowerShell 扩展(Amazon Web Services 是一个显著的例外)会在其命令名称的名词部分添加一个简短的前缀。例如,Get-GcSqlInstanceGet-AzStorageTable。这些前缀可能看起来有些别扭,但它们的设计目的是为了防止命令冲突。

例如,假设你加载了两个包含 Get-User cmdlet 的模块。当两个具有相同名称的命令同时加载时,当你运行 Get-User 时,PowerShell 将执行哪一个?结果将是最后加载的那个。但具有相同名称的其他命令并不是不可访问的。要专门运行任一命令,你必须使用一种有些别扭的命名约定,该约定需要模块名称和命令名称。如果 Get-User 中的一个来自名为 MyCoolPowerShellModule 的模块,你必须运行这个:

MyCoolPowerShellModule\Get-User

这需要输入很多,这就是为什么 Microsoft 建议为每个命令的名词添加一个产品特定的前缀,例如 AzGc。添加前缀有助于防止冲突,并有助于使命令更容易识别和使用。

注意:Amazon Web Services 模块不使用前缀。

如果你真的遇到了冲突,你总是可以移除其中一个冲突的模块。运行 Remove-Module,连同模块名称一起,以卸载一个模块。

注意:在导入模块时,为任何模块添加自己的前缀。Import-Module ModuleName -Prefix MyPrefixGet-OriginalCmdLet 改为 Get-MyPrefixOriginalCommand

7.5 玩转新模块

让我们运用你新获得的知识。我们希望你能跟随本节中我们展示的命令。更重要的是,我们希望你能跟随我们将要解释的过程和思考,因为这是我们学习如何使用新命令而不急于购买每款产品和每个功能的新书的方法。在本章的实验室中,我们将让你自己重复这个过程,以学习更深入的任务。

我们的目标是将我们电脑上的文件压缩成 zip 归档。我们不知道 PowerShell 是否能做这件事,所以我们首先向帮助系统寻求线索:

PS C:\Scripts> help *-archive                            
Name                              Category  Module                     
----                              --------  ------                     
Compress-Archive                  Function  Microsoft.PowerShell.Arc... 

哈哈!正如你所见,我们电脑上有一个完整的 Microsoft.PowerShell.Archive(全名被截断)模块。前面的列表显示了 Compress-Archive 命令,但我们很好奇还有哪些其他命令可用。为了找出答案,我们手动加载模块并列出其命令:

PS C:\Scripts> get-command -Module Microsoft.PowerShell.Archive

CommandType     Name
-----------     ----                                              
Function        Compress-Archive                                  
Function        Expand-Archive 

注意:我们本可以请求 Compress-Archive 的帮助,甚至直接运行该命令。PowerShell 会为我们后台加载 Microsoft.PowerShell .Archive 模块。但因为我们正在探索,这种方法让我们可以查看模块的完整命令列表。

这个命令列表看起来与之前的列表大致相同。好吧,让我们看看 Compress-Archive 命令的样子:

PS C:\Scripts> Get-Help Compress-Archive

NAME
    Compress-Archive

SYNTAX
    Compress-Archive [-Path] <string[]> [-DestinationPath] 
    <string> [-CompressionLevel {Optimal | NoCompression | 
    Fastest}] [-PassThru] [-WhatIf] [-Confirm] 
    [<CommonParameters>]

这看起来很简单,并且只有 -Path-DestinationPath 是必选参数。让我们尝试创建一个文件并用该命令压缩它:

PS C:\Scripts> 'test lunch' | Out-File chapter7.txt                     
PS C:\Scripts> Compress-Archive -Path .\chapter7.txt -DestinationPath 
➥ .\chapter7.zip 

好吧,没有消息通常是个好消息。然而,看到命令确实做了些事情会更好。让我们试试这个:

PS C:\Scripts> Compress-Archive -Path .\chapter7.txt -DestinationPath .\chapter7.zip -Force -Verbose                   
VERBOSE: Preparing to compress...
VERBOSE: Performing the operation "Compress-Archive" on target 
➥ "C:\Scripts\chapter7.txt".
VERBOSE: Adding 'C:\Scripts/chapter7.txt'.  

-Verbose 开关对所有 cmdlet 和函数都可用,尽管并非所有这些命令都会使用它。在这种情况下,我们得到一条消息,表明正在发生的事情,这告诉我们命令确实已运行。该命令的 -Force 开关表示要覆盖我们第一次创建的 zip 文件。

7.6 常见混淆点

PowerShell 新手在开始使用模块时经常犯一个错误:他们不阅读帮助。具体来说,他们在请求帮助时没有使用 -Example-Full 开关。

坦白说,查看内置示例是学习如何使用命令的最佳方式。是的,浏览数百个命令的列表(例如,Az.* 模块添加了超过 2,000 个新命令)可能会有些令人畏惧,但使用 HelpGet-Command 与通配符应该可以使你更容易缩小到你想找的任何名词。从那里开始,阅读 帮助

7.7 实验室

如往常一样,我们假设你已经在计算机或虚拟机上安装了最新的 PowerShell 版本以进行测试:

  1. 浏览 PowerShell 画廊。找到一些你认为听起来很有趣的模块并安装它们。

  2. 浏览你刚刚下载的模块可用的命令。

  3. 使用 7.2 节中的命令查找和安装(如果需要)Microsoft 为处理包含 Compress-Archive 命令的存档而提供的最新版本模块。

  4. 导入你刚刚安装的模块。

  5. 为下一步创建一个包含 10 个文件的 Tests 文件夹,并将其命名为 ~/TestFolder。

  6. 使用 Compress-Archive 创建 ~/TestFolder 内容的 zip 文件,并将其命名为 TestFolder.zip

  7. 将存档展开到 ~/TestFolder2。

  8. 使用 Compare-ObjectSelect-Object -ExpandProperty Name 来比较文件夹中的文件名,以验证你是否拥有相同的文件。

7.7 实验室答案

这是一种处理方法:

  1. Install-Module moduleyoufound

    • 如果你使用的是 Windows 机器,我们建议使用 import-excel 模块。
  2. Get-Command –module moduleyoufound

    • Get-command -module az
  3. Find-Module -Command Compress-Archive | Install-Module -Force

  4. Import-Module Microsoft.PowerShell.Archive

  5. 1..10 将创建介于 1 和 10 之间的数字集合。如果你用另一种方式做,不要担心。

    • New-Item ~/TestFolder -ItemType Directory

    • 1..10 | ForEach-Object {New-Item "~/TestFolder/$_.txt" -ItemType File -Value $_}

  6. Compress-Archive ~/TestFolder/* -DestinationPath ~/TestFolder.zip

  7. Expand-Archive ~/TestFolder.zip -DestinationPath ~/TestFolder2

  8. 这是一种可能的方式。记住,dirGet-ChildItem 的别名。

    $reference = Get-ChildItem ~/TestFolder| Select-Object -ExpandProperty name

    $difference = Get-ChildItem ~/TestFolder3| Select-Object -ExpandProperty name

    Compare-Object -ReferenceObject $reference -DifferenceObject $difference

8 对象:另一种名称的数据

在本章中,我们将做一些不同的事情。PowerShell 对对象的使用可能是它最令人困惑的元素之一,但与此同时,它也是 shell 中最关键的概念之一,影响着你在 shell 中做的每一件事。多年来,我们尝试了各种解释,并最终选择了几个针对不同受众都效果良好的解释。如果你有编程经验并且对对象的概念感到舒适,我们希望你跳到第 8.2 节。如果你没有编程背景,之前也没有使用对象进行编程或脚本编写,请从第 8.1 节开始,并直接阅读整章。

8.1 什么是对象?

请花点时间在 PowerShell 中运行Get-Process。你应该会看到一个包含多个列的表格,但这些列只是触及了关于进程的丰富信息的表面。每个进程对象还有一个机器名、主窗口句柄、最大工作集大小、退出代码和时间、处理器亲和力信息等等。你会发现与进程相关联的信息超过 60 条。为什么 PowerShell 只显示这么少的信息呢?

事实简单来说就是,PowerShell 可以访问的大多数事物提供的信息比屏幕上能舒适显示的还要多。当你运行任何命令,例如Get-ProcessGet-AzVmGet-AzStorageBlob时,PowerShell 完全在内存中构建一个包含那些项目所有信息的表格。对于Get-Process,这个表格包含大约 67 列,每列对应你电脑上运行的一个进程。每一列包含一些信息,例如虚拟内存、CPU 利用率、进程名称、进程 ID 等等。然后 PowerShell 会查看你是否指定了想要查看的列。如果你没有指定,shell 会查找由 Microsoft 提供的配置文件,并仅显示 Microsoft 认为你想要看到的表格列。

一种查看所有列的方法是使用ConvertTo-Html

Get-Process | ConvertTo-Html | Out-File processes.html

该 cmdlet 不会过滤列。相反,它生成一个包含所有列的 HTML 文件。这是查看整个表格的一种方法。

除了所有这些信息列之外,每一行表格都与一些操作相关联。这些操作包括操作系统可以对表格中列出的进程执行或与之相关的操作。例如,操作系统可以关闭进程、终止它、刷新其信息或等待进程退出等等。

无论何时你运行产生输出的命令,该输出都以内存中的表格形式存在。当你将一个命令的输出通过管道传递到另一个命令时,就像这样

Get-Process | ConvertTo-Html 

整个表格都会通过管道传递。表格不会过滤到更少的列,直到每个命令都运行完毕。

现在有一些术语上的变化。PowerShell 不会将这个内存中的表格称为表格。相反,它使用以下术语:

  • 对象——这是我们所说的表格行。它代表单个事物,例如单个进程或单个存储账户。

  • 属性——这是我们所说的表格列。它代表关于一个对象的信息的一部分,例如进程名称、进程 ID 或虚拟机的运行状态。

  • 方法——这是我们所说的动作。方法与单个对象相关联,并使该对象执行某些操作——例如,终止进程或启动虚拟机。

  • 集合——这是整个对象集合,或者我们称之为表格。

如果你发现以下关于对象的讨论令人困惑,请参考以下四点清单。始终想象一个集合的对象是一个大型的内存中的信息表,其中属性是列,而单个对象是行(图 8.1)。

图 8.1 展示对象(文件)具有多个属性,如AuthorFileType

8.2 理解 PowerShell 使用对象的原因

PowerShell 使用对象来表示数据的一个原因是,毕竟,你必须以某种方式表示数据,对吧?PowerShell 本可以将数据存储为 XML 格式,或者也许它的创造者可能决定使用纯文本表格。但他们有特定的原因不选择这些路径。

第一个原因是由于 PowerShell 之前的历史,它曾经是仅限 Windows 使用的。Windows 本身是一个面向对象的操作系统——至少,在 Windows 上运行的大多数软件都是面向对象的。选择将数据结构化为对象集合是很容易的,因为操作系统的大部分功能都适合这些结构。实际上,我们可以将面向对象的思想应用到其他操作系统,甚至应用到云和 DevOps 等其他范式。

使用对象的另一个原因是它们最终使你的工作变得更简单,并赋予你更多的权力和灵活性。暂时假设 PowerShell 的命令不会以对象的形式输出。相反,它以简单的文本表格的形式输出,这可能是你最初认为它正在做的事情。当你运行Get-Process这样的命令时,你得到的是格式化的文本输出:

PS /Users/travis> Get-Process
Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     39       5     1876       4340    52    11.33   1920 Code
     31       4      792       2260    22     0.00   2460 Code
     29       4      828       2284    41     0.25   3192 Code
    574      12     1864       3896    43     1.30    316 pwsh
    181      13     5892       6348    59     9.14    356 ShipIt
    306      29    13936      18312   139     4.36   1300 storeaccountd
    125      15     2528       6048    37     0.17   1756 WifiAgent
   5159    7329    85052      86436   118     1.80   1356 WifiProxy

如果你想用这些信息做其他事情呢?也许你想要更改所有正在运行的Code进程。为此,你需要对列表进行一些筛选。在 UNIX 或 Linux 系统上,你可能尝试使用grep命令(顺便说一句,你可以在 PowerShell 中运行它!),告诉它,“为我查看这个文本列表。只保留那些第 58 至 64 列包含字符Code的行。删除所有其他行。”结果列表中只包含你指定的进程:

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
     39       5     1876       4340    52    11.33   1920 Code
     31       4      792       2260    22     0.00   2460 Code
     29       4      828       2284    41     0.25   3192 Code

然后,你将文本传递给另一个命令,可能告诉它从列表中提取进程 ID。“遍历这个列表,从第 52 到第 56 列获取字符,但丢弃前两行(标题行)。”结果可能如下所示:

1920
2460
3192

最后,你将那个文本传递给另一个命令,要求它终止由那些 ID 号表示的进程(或你试图做的其他任何事情)。

这正是使用bash的 IT 专业人士的工作方式。他们花费大量时间学习如何更好地解析文本;使用grepawksed等工具;并精通正则表达式的使用。通过这个过程的学习,他们更容易定义他们想要计算机查找的文本模式。在 PowerShell 跨平台之前的日子里,UNIX 和 Linux IT 专业人士会依赖于像 Perl 和 Python 这样的脚本语言,这些语言在文本解析方面提供了更多的功能。但这种方法确实存在一些问题:

  • 你可能会花更多的时间在文本上,而不是做你的本职工作。

  • 如果命令的输出发生了变化——比如说,将 ProcessName 列移动到表格的开始位置——那么你必须重写所有的命令,因为它们都依赖于诸如列位置之类的因素。

  • 你必须精通解析文本的语言和工具——不是因为你的工作涉及解析文本,而是因为解析文本是实现目标的一种手段。

  • 像 Perl 和 Python 这样的语言是可靠的脚本语言……但它们并不是 shell。

PowerShell 使用对象有助于消除所有这些文本操作的开销。因为对象在内存中像表格一样工作,你不必告诉 PowerShell 信息所在的文本列。相反,你只需告诉它列名,PowerShell 就能准确地知道去哪里获取那些数据。无论你如何安排最终在屏幕或文件中的输出,内存中的表格总是相同的,所以你永远不需要因为列的移动而重写命令。你将花费更少的时间在开销任务上,更多的时间专注于你想要完成的事情。

的确,你必须学习一些语法元素,以便正确地指导 PowerShell,但你必须学习的比在纯文本 shell 中工作时要少得多。

不要生气 顺便说一下,前面提到的内容并不是在贬低 Bash、Perl 或 Python。每个工具都有其优缺点。Python 是一种伟大的通用编程语言,甚至已经进入机器学习和人工智能领域——但这并不是你阅读这本书的原因。你是在寻找能够提升你作为 IT 专业人士技能的东西,而 PowerShell 正是这样的工具。

8.3 发现对象:Get-Member

如果对象在内存中就像一个巨大的表格,而 PowerShell 只在屏幕上显示表格的一部分,你怎么能看到你还有哪些可以操作的呢?如果你认为你应该使用 Get-Help 命令,我们很高兴,因为我们确实在前面几章中一直在推荐这个命令。但不幸的是,你会错的。

帮助系统仅记录背景概念(以 about 主题的形式)和命令语法。要了解更多关于对象的信息,你使用不同的命令:Get-Member。你应该习惯使用这个命令——如此习惯,以至于你开始寻找更简短的输入方式。我们现在就给你:别名 gm

你可以在任何通常产生输出的 cmdlet 后面使用 gm。例如,你已经知道运行 Get-Process 会在屏幕上产生输出。你可以将其管道化到 gm

Get-Process | gm

每当 cmdlet 产生一系列对象,就像 Get-Process 所做的那样,整个集合在管道的末端之前都是可访问的。只有在每个命令都运行之后,PowerShell 才会过滤要显示的信息列并创建你看到的最终文本输出。因此,在上面的例子中,gm 对所有进程对象的属性和方法都有完全的访问权限,因为它们还没有被过滤以供显示。gm 会查看每个对象并构建一个包含对象属性和方法的列表。它看起来像这样:

PS C:\> Get-Process | gm
   TypeName: System.Diagnostics.Process
Name                       MemberType     Definition
----                       ----------     ----------
Handles                    AliasProperty  Handles = Handlecount
Name                       AliasProperty  Name = ProcessName
NPM                        AliasProperty  NPM = NonpagedSystemMemo...
PM                         AliasProperty  PM = PagedMemorySize
VM                         AliasProperty  VM = VirtualMemorySize
WS                         AliasProperty  WS = WorkingSet
Disposed                   Event          System.EventHandler Disp...
ErrorDataReceived          Event          System.Diagnostics.DataR...
Exited                     Event          System.EventHandler Exit...
OutputDataReceived         Event          System.Diagnostics.DataR...
BeginErrorReadLine         Method         System.Void BeginErrorRe...
BeginOutputReadLine        Method         System.Void BeginOutputR...
CancelErrorRead            Method         System.Void CancelErrorR...
CancelOutputRead           Method         System.Void CancelOutput...

我们已经缩减了前面的列表,因为它很长,但希望你能理解这个概念。

现在就试试 不要只听我们的话。现在是跟随我们运行相同的命令并查看它们完整输出的完美时机。

顺便说一下,你可能想知道,所有附加到对象上的属性、方法和其他东西统称为该对象的 members,就像对象本身是一个俱乐部,所有这些属性和方法都属于这个俱乐部。这就是 Get-Member 命令名称的由来——它正在获取对象成员的列表。但请记住,因为 PowerShell 习惯于使用单数名词,所以 cmdlet 名称是 Get-Member,而不是 Get-Members

重要事项 它很容易被忽视,但请注意 Get-Member 命令输出的第一行。它是 TypeName,这是分配给该特定类型对象的唯一名称。现在可能看起来并不重要——毕竟,谁在乎它叫什么名字呢?但它在下一章中将会变得至关重要。

8.4 使用对象属性,或属性

当你检查 gm 的输出时,你会注意到几种不同的属性:

  • ScriptProperty

  • Property

  • NoteProperty

  • AliasProperty

除此之外

通常情况下,.NET 中的对象——所有 PowerShell 对象都来自这里——只有属性。PowerShell 动态添加其他内容:ScriptPropertyNotePropertyAliasProperty 等等。如果你在微软的文档中查找一个对象类型(你可以将对象的 TypeName 插入你喜欢的搜索引擎以找到 docs.microsoft.com 页面),你不会看到这些额外的属性。

PowerShell 有一个可扩展的类型系统(ETS),它负责添加这些最后的属性。为什么它会这样做呢?在某些情况下,是为了使对象更加一致,例如为只具有类似 ProcessName(这就是 AliasProperty 的用途)的对象添加一个 Name 属性。有时是为了暴露对象中深深隐藏的信息(进程对象有几个 ScriptProperties 用于此目的)。

一旦你进入 PowerShell,这些属性的行为方式都是相同的。但当你发现它们没有出现在官方文档页面上时,不要感到惊讶:shell 添加了这些额外的功能,通常是为了使你的生活更轻松。

对于你的用途来说,这些属性都是相同的。唯一的区别在于属性最初是如何创建的,但这不是你需要担心的事情。对你来说,它们都是属性,你将以相同的方式使用它们。

属性总是包含一个值。例如,进程对象的 ID 属性的值可能是 1234,而该对象的 Name 属性的值可能是 Code。属性描述了关于对象的一些信息:其状态、其 ID、其名称等等。在 PowerShell 中,属性通常是只读的,这意味着你不能通过为其 Name 属性分配新值来更改服务的名称。但你可以通过读取其 Name 属性来检索服务的名称。我们估计,你将在 PowerShell 中做的 90% 的工作都涉及到属性。

8.5 使用对象操作或方法

许多对象支持一个或多个方法,正如我们之前提到的,这些方法是你可以指示对象执行的操作。进程对象有一个 Kill 方法,它可以终止进程。一些方法需要一个或多个输入参数,这些参数为该特定操作提供额外的详细信息,但在这个 PowerShell 教育的早期阶段,你不会遇到这些。你可能会花几个月甚至几年时间使用 PowerShell,但可能永远不需要执行单个对象方法。这是因为许多这些操作也由 cmdlet 提供。

例如,如果你需要终止一个进程,你有三种方法可以做到。一种方法是通过检索对象然后以某种方式执行其 Kill 方法。另一种方法是使用几个 cmdlet:

Get-Process -Name Code | Stop-Process

你也可以通过使用单个 cmdlet 来完成这个操作:

Stop-Process -Name Code

本书的主要关注点是使用 PowerShell cmdlets 完成任务。它们提供了最简单、最以 IT 专业人士为中心、最以任务为导向的方式来实现目标。使用方法开始逐渐涉及 .NET 编程,这可能更复杂,可能需要更多的背景信息。因此,你很少——如果有的话——会看到我们在本书中执行对象方法。我们现在的普遍哲学是,“如果你不能用 cmdlet 完成,就回去使用 GUI。”我们承诺,你不会在整个职业生涯中都这样感觉,但现在这是一个保持对“PowerShell 方式”做事的关注的好方法。

超越

在你的 PowerShell 教育的这个阶段,你不需要了解它们,但除了属性和方法之外,对象还可以有事件。事件是对象通知你发生了某种事情的方式。例如,进程对象可以在进程结束时触发其 Exited 事件。你可以将你自己的命令附加到这些事件上,例如,当进程退出时发送电子邮件。以这种方式处理事件是一个高级主题,超出了本书的范围。

8.6 对象排序

大多数 PowerShell cmdlets 以确定性的方式生成对象,这意味着它们倾向于在每次运行命令时以相同的顺序生成对象。例如,Azure VM 和进程都是按名称的字母顺序列出的。如果我们想改变这一点怎么办?

假设我们想要显示一个进程列表,将 CPU 消耗最大的进程放在列表顶部,最小的消耗者放在底部。我们需要根据 CPU 属性重新排序这个对象列表。PowerShell 提供了一个简单的 cmdlet,Sort-Object,它正好可以做到这一点:

Get-Process | Sort-Object -Property CPU 

现在尝试一下 我们希望你能跟随并运行本章中的命令。我们不会将输出粘贴到书中,因为这些表格很长。

这个命令并不完全符合我们的要求。它确实按 CPU 排序,但它是以升序排序的,最大的值在列表底部。阅读 Sort-Object 的帮助,我们看到它有一个 -Descending 参数,可以反转排序顺序。我们还注意到 -Property 参数是位置参数,因此我们不需要输入参数名。

我们将 -Descending 缩写为 -desc,我们得到了我们想要的结果。-Property 参数接受多个值(我们确信如果你查看了帮助文件,你会看到)。

如果两个进程使用了相同数量的虚拟内存,我们希望它们按进程 ID 排序,以下命令可以完成这个任务:

Get-Process | Sort-Object CPU,ID -desc

和往常一样,逗号分隔的列表是传递多个值给任何支持它们的参数的方式。

8.7 选择你想要的属性

另一个有用的 cmdlet 是Select-Object。它接受来自管道的对象,你可以指定你想要显示的属性。这使你能够访问通常由 PowerShell 的配置规则过滤掉的性质,或者将列表缩减到你感兴趣的一两个属性。这在将对象传递给ConvertTo-HTML时非常有用,因为该 cmdlet 通常构建包含每个属性的表格。比较以下两个命令的结果:

Get-Process | ConvertTo-HTML | Out-File test1.html
Get-Process | Select-Object -Property Name,ID,CPU,PM | ConvertTo-Html | 
➥ Out-File test2.html

现在就试试吧,分别运行这些命令,然后在网页浏览器中查看生成的 HTML 文件以查看差异。

查看关于Select-Object的帮助(或者你可以使用它的别名Select)。-Property参数是位置参数,这意味着我们可以缩短最后一个命令:

Get-Process | Select Name,ID,CPU,PM | ConvertTo-HTML | Out-File test3.html

花些时间实验Select-Object。尝试以下命令的变体,它允许输出显示在屏幕上:

Get-Process | Select Name,ID,CPU,PM

尝试从该列表中添加和删除不同的进程对象属性,并查看结果。你能指定多少个属性仍然得到表格作为输出?有多少个属性会强制 PowerShell 将输出格式化为列表而不是表格?

除此之外

Select-Object也有-First-Last参数,这让你可以保留管道中的对象子集。例如,Get-Process | Select -First 10保留前 10 个对象。这里没有涉及任何标准,例如保留某些进程;它只是获取前(或后)10 个。

警告:人们经常混淆两个 PowerShell 命令:Select-ObjectWhere-Object,你还没有看到过。Select-Object用于选择你想要看到的属性(或列),它还可以选择输出行的任意子集(使用-First-Last)。Where-Object根据你指定的标准从管道中删除或过滤对象。

8.8 对象直到结束

PowerShell 管道在最后一个命令执行之前始终包含对象。那时,PowerShell 会查看管道中的对象,然后查看其各种配置文件以确定使用哪些属性来构建屏幕显示。它还会根据内部规则和配置文件决定该显示是表格还是列表。(我们将在第十章中解释更多关于这些规则和配置,以及如何修改它们。)

一个重要的事实是,管道可以在单个命令行中包含许多种类的对象。在接下来的几个例子中,我们将取一个单独的命令行并实际输入,这样只有一条命令出现在文本的一行上。这将使解释我们谈论的内容更容易一些。这是第一个例子:

Get-Process |                 
Sort-Object CPU -Descending |  
Out-File c:\procs.txt

在这个例子中,我们首先运行 Get-Process,将进程对象放入管道。下一个命令是 Sort-Object。这不会改变管道中的内容;它只改变对象的顺序,所以在 Sort-Object 的末尾,管道仍然包含进程。最后一个命令是 Out-File。在这里,PowerShell 必须生成输出,所以它会将管道中的内容——进程——格式化为其内部规则集。结果被放入指定的文件。接下来是一个更复杂的例子:

Get-Process |                 
Sort-Object CPU -Descending |  
Select-Object Name,ID,CPU      

这是从相同的方式开始的。Get-Process 将进程对象放入管道。这些对象进入 Sort-Object,对其进行排序并将相同的进程对象放入管道。但 Select-Object 的工作方式略有不同。进程对象始终具有完全相同的成员。为了缩减属性列表,Select-Object 不能删除您不想要的属性,因为结果将不再是进程对象。相反,Select-Object 创建了一种新的自定义对象,称为 PSObject。它从进程复制您想要的属性,从而在管道中放置一个自定义对象。

现在尝试运行这个三个 cmdlet 的命令行,记住您应该在一行中输入整个命令。注意输出与 Get-Process 的正常输出有何不同?

当 PowerShell 发现它已经到达了命令行的末尾,它必须决定如何布局文本输出。因为管道中不再有任何进程对象,PowerShell 不会使用适用于进程对象的默认规则和配置。相反,它会寻找适用于 PSObject 的规则和配置,这正是管道现在所包含的内容。Microsoft 没有为 PSObjects 提供任何规则或配置,因为它们旨在用于自定义输出。相反,PowerShell 会做出最佳猜测,并生成一个表格,基于这样的理论:这三条信息可能仍然适合放入表格中。然而,这个表格的布局并不像 Get-Process 的正常输出那样整齐,因为外壳缺少制作更美观表格所需的额外配置信息。

您可以使用 gm 来查看最终进入管道的对象。记住,您可以在任何产生输出的 cmdlet 后添加 gm

Get-Process | Sort-Object CPU -Descending | gm
Get-Process | Sort-Object CPU -Descending | Select Name,ID,CPU | gm

现在尝试分别运行这两个命令行,并注意输出的差异。

注意,作为 gm 输出的一部分,PowerShell 会向您显示它在管道中看到的对象的类型名称。在第一种情况下,这是一个 System.Diagnostics.Process 对象,但在第二种情况下,管道包含不同类型的对象。这些新的 选定 对象仅包含指定的三个属性——NameIDCPU——以及一些系统生成的成员。

即使gm也会产生对象并将它们放入管道中。运行gm后,管道中不再包含进程或选定的对象;它包含gm产生的对象类型:Microsoft.PowerShell.Commands.MemberDefinition。你可以通过将gm的输出管道传输到gm本身来证明这一点:

Get-Process | gm | gm

现在尝试一下:你肯定会想尝试这个,并且要深思熟虑,确保它对你来说是有意义的。你从Get-Process开始,它将进程对象放入管道。这些对象进入gm,它分析它们并产生自己的MemberDefinition对象。然后这些对象被管道传输到gm,它分析它们并产生输出,列出每个Member-Definition对象的成员。

掌握 PowerShell 的关键在于学会在任何给定时刻跟踪管道中对象的类型。虽然gm可以帮助你做到这一点,但退后一步,通过口头解释命令行也是一项很好的练习,可以帮助消除困惑。

8.9 常见混淆点

新手在开始使用 PowerShell 时往往会犯一些常见的错误。大多数这些错误随着经验的积累会消失,但我们通过以下列表将你的注意力引向它们,以便你在开始走错路时有机会纠正自己。

  • 记住,PowerShell 的帮助文件不包含关于对象属性的信息。你需要将对象管道传输到gmGet-Member)以查看属性列表。

  • 记住,你可以在通常会产生结果的任何管道末尾添加gm。例如,像Get-Process -Name Code | Stop-Process这样的命令行通常不会产生结果,所以将| gm附加到末尾也不会产生任何结果。

  • 注意整洁的打字。在每一个管道字符的两侧都加上一个空格,因为你的命令行应该读作Get-Process | gm而不是Get-Process|gm。那个空格键之所以特别大,是有原因的——请使用它。

  • 记住,管道在每一步都可能包含各种类型的对象。思考一下管道中是什么类型的对象,并关注下一个命令将对这种类型的对象做什么。

8.10 实验室

注意:对于这个实验,你需要任何运行 PowerShell v7 或更高版本的计算机。

本章可能涵盖了比迄今为止任何章节都多、难度更大的新概念。我们希望你能理解所有这些内容,并且希望这些练习能帮助你巩固所学知识。实验室可能比之前的实验室更具挑战性,但我们希望你能养成找出要使用哪些命令的习惯——依靠get-command和帮助,而不是依靠我们,来找到正确的命令。毕竟,一旦你开始在工作中使用 PowerShell 并遇到书中未涉及的各种情况,你将需要这样做。其中一些任务依赖于你在前面章节中学到的技能,以帮助你刷新记忆并保持敏锐:

  1. 识别一个产生随机数的 cmdlet。

  2. 识别一个显示当前日期和时间的 cmdlet。

  3. 任务 2 中的 cmdlet 产生什么类型的对象?(cmdlet 产生的对象的TypeName是什么?)

  4. 使用任务 2 中的 cmdlet 和Select-Object,以如下表格形式显示当前星期几(注意:输出将右对齐,所以请确保你的 PowerShell 窗口没有水平滚动条):

    DayOfWeek
    ---------
       Monday
    
  5. 识别一个可以显示目录中所有时间的 cmdlet。

  6. 使用任务 5 中的 cmdlet,显示你选择的目录中的所有时间。然后扩展表达式,按创建时间对列表进行排序,并仅显示文件名和创建日期。记住,命令默认输出中显示的列标题不一定是真正的属性名——你需要查找真正的属性名来确保。

  7. 重复任务 6,但这次按最后写入时间对项目进行排序;然后显示文件名、创建时间和最后写入时间。将此保存为 CSV 文件和 HTML 文件。

8.11 实验答案

  1. Get-Random

  2. Get-Date

  3. System.DateTime

  4. Get-Date | select DayofWeek

  5. Get-ChildItem

  6. Get-ChildItem | Sort-Object CreationTime | Select-Object

    ➥ 名称,创建时间

  7. Get-ChildItem | Sort-Object LastWritetime | Select-Object

    ➥ 名称,最后写入时间,创建时间 | Export-CSV files.csv

    Get-ChildItem | Sort-Object LastWritetime | Select-Object

    ➥ 名称,最后写入时间,创建时间 | Out-file files.html

9 实践插曲

是时候将你的新知识付诸实践了。在本章中,我们不会教你任何新东西。相反,我们将通过一个详细的例子来引导你,使用你所学到的知识。这是一个绝对的现实世界例子:我们将给自己设定一个任务,然后让你跟随我们的思维过程,了解我们如何完成它。这一章是本书主题的精髓,因为本书不是仅仅给你一个如何做某事的答案,而是帮助你意识到你可以自学

9.1 定义任务

首先,我们假设你正在使用运行 PowerShell 7.1 或更高版本的任何操作系统。我们将要处理的例子可能非常适用于早期版本的 Windows PowerShell,但我们没有测试这一点。

在 DevOps 的世界里,除了 PowerShell 之外,几乎总是会出现一种特定的语言——当然,还有 YAML。这在 IT 专业人士和 DevOps 工程师中是一个相当有争议的语言——人们要么爱它,要么恨它。有什么猜测吗?如果你猜到了 YAML,你就对了!YAML 代表“YAML ain’t markup language”(它是一个递归缩写,当缩写包含缩写时),尽管它说它不是,但在很多方面它类似于简单的标记语言——换句话说,它只是一个具有特定结构的文件,就像 CSV 和 JSON 也有特定的结构一样。由于我们在 DevOps 世界中看到了很多 YAML,因此我们拥有与之交互的工具是很重要的。

9.2 查找命令

解决任何任务的第一个步骤是找出哪些命令可以为你完成它。你的结果可能与我们不同,这取决于你安装了什么,但重要的是我们正在经历的过程。因为我们知道我们想要管理一些虚拟机,所以我们将以 YAML 作为关键词开始:

PS C:\Scripts\ > Get-Help *YAML*
PS C:\Scripts\ >

嗯。这没有帮助。没有显示任何内容。好吧,让我们尝试另一种方法——这次,我们关注命令而不是帮助文件:

PS C:\Scripts\ > get-command -noun *YAML*
PS C:\Scripts\ >

好吧,没有命令的名称包含YAML。令人失望!所以现在我们必须看看在线 PowerShell Gallery 中可能有什么:

PS C:\Scripts\ > find-module *YAML* | format-table -auto
Version Name            Repository Description
------- ----            ---------- -----------
0.4.0   powershell-yaml PSGallery  Powershell module for serializing...
1.0.3   FXPSYaml        PSGallery  PowerShell module used to...
0.2.0   Gainz-Yaml      PSGallery  Gainz: Yaml...
0.1.0   Gz-Yaml         PSGallery  # Gz-Yaml...

这看起来更有希望!所以让我们安装第一个模块:

PS C:\Scripts\ > install-module powershell-yaml 
You are installing the module(s) from an untrusted repository. If you
trust this repository, change its InstallationPolicy value by
running the Set-PSRepository cmdlet.
Are you sure you want to install software from
'https://go.microsoft.com/fwlink/?LinkID=397631&clcid=0x409'?
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend
[?] Help(default is "N"): y

现在,在这个时候,你必须小心。虽然 Microsoft 运行 PowerShell Gallery,但它不会验证其他人发布的任何代码。因此,我们在我们的过程中暂停了一下,审查了我们刚刚安装的代码,并在继续之前确保我们对此感到舒适。这也得益于这个模块的作者是一个共同的价值专家(MVP),Boe Prox,我们信任他。现在让我们看看我们刚刚获得的命令:

PS C:\Scripts\ > get-command -module powershell-yaml | format-table -auto
CommandType Name              Version Source
----------- ----              ------- ------
Function    ConvertFrom-Yaml 0.4.0   powershell-yaml
Function    ConvertTo-Yaml   0.4.0   powershell-yaml 

好吧,这些看起来很简单。ConvertToConvertFrom——听起来我们需要的就这些。

9.3 学习使用命令

希望作者在他们的模块中包含了帮助信息。如果你有一天编写了一个模块,请始终记住,如果其他人将要使用它,你很可能——不,你 应该——包含帮助信息,这样模块的消费者就知道如何使用它。如果你没有这样做,那就好比在没有说明书的情况下运送宜家家具——不要这样做!还有另一本书叫做 Learn PowerShell Toolmaking in a Month of Lunches(Manning,2012),其中两位作者 Don Jones 和 Jeffery Hicks 讲述了如何编写模块以及如何在其中添加帮助信息——添加帮助信息是正确的事情。让我们看看作者是否做了正确的事情:

PS C:\Scripts\ > help ConvertFrom-Yaml
NAME
    ConvertFrom-Yaml

SYNTAX
    ConvertFrom-Yaml [[-Yaml] <string>] [-AllDocuments] [-Ordered] 
  ➥ [-UseMergingParser] [<CommonParameters>]

PARAMETERS
    -AllDocuments     Add-Privilege

Drat! 没有帮助。嗯,在这个例子中,情况并不太糟糕,因为即使作者没有编写任何帮助信息,PowerShell 仍然提供了帮助。PowerShell 仍然会给你语法、参数、输出和别名——对于这个简单的命令来说,这些信息已经足够了。所以我们需要一个样本 YAML 文件……嗯,使用 PowerShell GitHub 仓库中的实时示例是值得的:

raw.githubusercontent.com/PowerShell/PowerShell/master/.vsts-ci/templates/credscan.yml

这是 PowerShell 团队使用的 Azure Pipelines YAML 文件,用于运行 CredScan 工具——一个用于扫描代码中是否意外添加了机密或凭证的工具。PowerShell 团队将其设置为每次有人向 PowerShell GitHub 仓库发送拉取请求(即代码更改)时运行,以便可以立即捕获。在拉取请求期间运行任务是一种常见的做法,称为 持续集成(CI)。

好吧,先下载这个文件,然后让我们从 PowerShell 中读取这个文件:

PS C:\Scripts\ > Get-Content -Raw /Users/travis/Downloads/credscan.yml
parameters:
  pool: 'Hosted VS2017'
  jobName: 'credscan'
  displayName: Secret Scan

jobs:
- job: ${{ parameters.jobName }}
  pool:
    name: ${{ parameters.pool }}

  displayName: ${{ parameters.displayName }}

  steps:
  - task: securedevelopmentteam.vss-secure-development-tools.build-task
  ➥ -credscan.CredScan@2
    displayName: 'Scan for Secrets'
    inputs:
      suppressionsFile: tools/credScan/suppress.json
      debugMode: false

  - task: securedevelopmentteam.vss-secure-development-tools.build-task
  ➥ -publishsecurityanalysislogs.PublishSecurityAnalysisLogs@2
    displayName: 'Publish Secret Scan Logs to Build Artifacts'
    continueOnError: true

  - task: securedevelopmentteam.vss-secure-development-tools.build-task
  ➥ -postanalysis.PostAnalysis@1
    displayName: 'Check for Failures'
    inputs:
      CredScan: true
      ToolLogsNotFoundAction: Error

好极了,所以我们已经弄清楚如何读取 YAML 文件。接下来,让我们将其转换为更容易处理的东西:

PS C:\Scripts\ > Get-Content -Raw /Users/travis/Downloads/credscan.yml | 
 ➥ ConvertFrom-Yaml

Name                           Value
----                           -----
parameters                     {pool, jobName, displayName}
jobs                           {${{ parameters.displayName }}} 

嗯,那就简单了。让我们看看我们有什么类型的对象:

PS C:\Scripts\ > Get-Content -Raw /Users/travis/Downloads/credscan.yml | 
 ➥ ConvertFrom-Yaml | gm

   TypeName: System.Collections.Hashtable
Name              MemberType            Definition
----              ----------            ----------
Add               Method                void Add(System.Object key...
Clear             Method                void Clear(), void IDictionary.Clear()
Clone             Method                System.Object Clone(), ...
Contains          Method                bool Contains(System.Object key)...
ContainsKey       Method                bool ContainsKey(System.Object key)
ContainsValue     Method                bool ContainsValue(System.Object...
CopyTo            Method                void CopyTo(array array, int...
Equals            Method                bool Equals(System.Object obj)
GetEnumerator     Method                System.Collections.IDictionary...
GetHashCode       Method                int GetHashCode()
GetObjectData     Method                void GetObjectData(System.Runtim... GetType
       Method                type GetType()
OnDeserialization Method                void OnDeserialization(System.Object...Remove
       Method                void Remove(System.Object key), voi... ToString
       Method                string ToString()
Item              ParameterizedProperty System.Object Item(System.Object 
                ➥ key...
Count             Property              int Count {get;}
IsFixedSize       Property              bool IsFixedSize {get;}
IsReadOnly        Property              bool IsReadOnly {get;}
IsSynchronized    Property              bool IsSynchronized {get;}
Keys              Property              System.Collections.ICollection K...
SyncRoot          Property              System.Object SyncRoot {get;}
Values            Property              System.Collections.ICollection Value...

好吧,所以这是一个散列表。散列表只是各种东西的集合。你会在 PowerShell 的旅程中看到很多这样的散列表。它们很棒,因为它们可以轻松地转换为其他格式。让我们尝试将我们在 YAML 中得到的内容转换为 DevOps 中另一个非常重要的数据结构——JSON。让我们看看我们有什么可以工作的:

PS C:\Scripts\ > Get-Help *json*

Name             Category Module                       Synopsis
----             -------- ------                       --------
ConvertFrom-Json Cmdlet   Microsoft.PowerShell.Utility...
ConvertTo-Json   Cmdlet   Microsoft.PowerShell.Utility...
Test-Json        Cmdlet   Microsoft.PowerShell.Utility...

Bingo,一个 ConvertTo-Json 命令。让我们使用管道将 YAML 转换为 JSON。我们需要使用 ConvertTo-JsonDepth 参数(你可以在帮助中阅读有关此信息),这允许我们指定命令应该深入到什么程度来尝试创建 JSON 结构。对于我们正在做的事情,100 是一个安全的选择。好吧,让我们把它放在一起:

PS C:\Scripts\ > Get-Content -Raw /Users/travis/Downloads/credscan.yml | 
 ➥ ConvertFrom-Yaml | ConvertTo-Json -Depth 100

{
  "parameters": {
    "pool": "Hosted VS2017",
    "jobName": "credscan",
    "displayName": "Secret Scan"
  },
  "jobs": [
    {
      "job": "${{ parameters.jobName }}",
      "pool": {
        "name": "${{ parameters.pool }}"
      },
      "steps": [
        {
          "task": "securedevelopmentteam.vss-secure-development-tools.build
        ➥ -task-credscan.CredSca          "inputs": {
            "debugMode": false,
            "suppressionsFile": "tools/credScan/suppress.json"
          },
          "displayName": "Scan for Secrets"
        },
        {
nalysislogs.PublishSecurityAnalysisLogs@2",
          "continueOnError": true,
          "displayName": "Publish Secret Scan Logs to Build Artifacts"
        },
        {
          "task": "securedevelopmentteam.vss-secure-development-tools.build
         ➥ -task-postanalysis.PostAnalysis@1",
          "inputs": {
            "CredScan": true,
            "ToolLogsNotFoundAction": "Error"
          },
          "displayName": "Check for Failures"
        }
      ],
      "displayName": "${{ parameters.displayName }}"
    }
  ]
}

它成功了!我们现在根据 YAML 文件生成了一些 JSON。这是一个有用的练习,因为野外有许多不同的 DevOps 工具接受 YAML 或 JSON(例如 AutoRest、Kubernetes),因此你可能更喜欢 YAML,但你的同事可能更喜欢 JSON。现在你有一个简单的方法可以通过这种方式相互分享。

现在,我们坦白承认,这并不是一个复杂的任务。但任务本身并不是本章的重点。重点是我们是如何找到答案的。我们做了什么?

  1. 我们首先在本地帮助文件中搜索包含特定关键词的任何文件。当我们的搜索词与命令名称不匹配时,PowerShell 会对所有帮助文件的内容进行全文搜索。这很有用,因为如果某个文件甚至提到了 YAML,我们就能找到它。

  2. 我们继续搜索特定的命令名称。这将帮助我们找到没有安装帮助文件的命令。理想情况下,命令应该总是有帮助文件,但我们并不生活在一个理想的世界,所以我们总是采取这一额外步骤。

  3. 在本地找不到任何东西后,我们在 PowerShell Gallery 中搜索,并找到了一个看起来很有希望的模块。我们安装了该模块并审查了其命令。

  4. 即使模块作者没有提供帮助,PowerShell 也帮助我们找到了如何运行命令以将数据转换为 YAML 的方法。这有助于我们了解命令数据的结构以及命令期望的值类型。

  5. 使用我们到那时收集到的信息,我们能够实现我们想要的更改。

9.4 自学小贴士

再次强调,这本书的真正目的是教你如何自学——这正是本章所展示的。这里有一些小贴士:

  • 不要害怕寻求帮助,并且一定要阅读示例。我们反复这么说,但好像没有人相信我们。我们仍然看到新来的新手,就在我们眼前,偷偷地去谷歌寻找示例。帮助文件有什么可怕的呢?如果你愿意阅读某人的博客,为什么不先尝试一下帮助文件中的示例呢?

  • 注意。屏幕上的每一信息都可能很重要——不要在心理上跳过你立即寻找的东西。这很容易做到,但不要这么做。相反,看看每一件事,并试图弄清楚它的用途以及你可以从中得到的信息。

  • 不要害怕失败。希望你有可以玩耍的虚拟机——那么就使用它。新手们不断地向我们提出问题,比如,“嘿,如果我这样做,会发生什么?”我们开始回答,“不知道——试试看。”实验是好的。在虚拟机中,最坏的情况是你不得不回滚到快照,对吧?所以试试看,无论你在做什么。

  • 如果一件事不起作用,不要头撞南墙——尝试其他方法。

随着时间的推移、耐心和练习,一切都会变得容易——但一定要确保你在思考

9.5 实验室

注意:对于这个实验,你可以使用你喜欢的任何操作系统(如果你想的话,可以在多个操作系统上尝试),并确保你正在使用 PowerShell 7.1 或更高版本。

现在轮到你了。我们假设你在一个虚拟机或其他机器上工作,这个机器在学习的过程中可以稍微出点问题。请不要在生产环境中的关键计算机上这样做!

这个练习将涉及秘密管理。DevOps 工程师应该非常熟悉这个概念。想法很简单:我们有一堆敏感信息(密码、连接字符串等),这些信息需要在我们的命令中使用,但我们需要将这些秘密保存在一个安全的地方。我们可能还想与其他团队成员分享这些秘密——电子邮件并不足够安全,朋友们!

PowerShell 团队最近一直在开发一个名为“秘密管理”的模块,专门用于执行这项任务。这是一个通用的模块,可以与支持它的任何秘密存储进行交互。一些将是本地秘密存储,如 macOS Keychain,而其他将是云服务,如 Azure Key Vault 和 HashiCorp Vault。你的目标是获取这个模块,在你的选择秘密存储中存储一个秘密,然后检索它。如果你使用基于云的秘密存储,尝试从不同的机器检索秘密,作为最终测试。

9.6 实验答案

我们确信你们期待我们给出一个完整的命令列表来完成这个实验练习。然而,这一章的全部内容都是关于自己找出这些事情。秘密管理模块有很好的文档记录。如果你一直跟着我们学习,那么你在这个实验中不会遇到任何问题。

10 管道,更深入

到目前为止,你已经学会了如何有效地使用 PowerShell 的管道。运行命令(例如,Get-Process | Sort-Object VM -desc | ConvertTo-Html | Out-File procs.html)非常强大,一行就能完成过去需要几行脚本才能完成的工作。但你可以做得更好。在本章中,我们将更深入地探讨管道,并揭示其一些最强大的功能,这些功能允许你以更少的努力正确地在命令之间传递数据。

10.1 管道:通过更少的输入实现强大功能

我们非常喜欢 PowerShell 的一个原因是可以让我们成为更有效的管理员,而无需编写复杂的脚本,就像我们过去在 Bash 中必须做的那样。强大的一行命令的关键在于 PowerShell 管道的工作方式。

让我们明确一下:你可以跳过这一章,仍然有效地使用 PowerShell,但在大多数情况下,你将不得不求助于 Bash 风格的脚本和程序。尽管 PowerShell 的管道功能可能很复杂,但它们可能比更复杂的编程技能更容易学习。通过学习如何操作管道,你可以更加高效,而无需编写脚本。

这里整个想法是让 shell 为你做更多的工作,尽可能少地输入。我们认为你会对 shell 能做得有多好感到惊讶!

10.2 PowerShell 如何将数据传递到管道中

每当你将两个命令串联起来时,PowerShell 必须找出如何将第一个命令的输出传递到第二个命令的输入。在即将到来的示例中,我们将第一个命令称为命令 A。这是产生内容的命令。第二个命令是命令 B,它需要接受命令 A 的输出,然后执行自己的操作:

CommandA | CommandB

例如,假设你有一个文本文件,每行包含一个模块名称,如图 10.1 所示。

图片

图 10.1 在 VS Code 中创建包含模块名称的文本文件,每行一个名称

你可能希望使用那些模块名称作为命令的输入,告诉该命令你希望它针对哪个模块运行。考虑以下示例:

Get-Content.\modules.txt | Get-Command

Get-Content运行时,它将模块名称放入管道。PowerShell 随后必须决定如何将这些内容传递给Get-Command命令。PowerShell 的技巧是命令只能通过参数接受输入。PowerShell 必须找出Get-Command的哪个参数可以接受Get-Content的输出。这个找出过程被称为pipeline parameter binding,这是我们本章要介绍的内容。PowerShell 有两种方法将Get-Content的输出传递到Get-Command的参数上。shell 将尝试的第一种方法称为ByValue;如果这不起作用,它将尝试ByPropertyName

10.3 计划 A:Pipeline 输入 ByValue

使用这种管道参数绑定方法,PowerShell 会查看命令 A 生成的对象类型,并尝试查看命令 B 的任何参数是否可以接受来自管道的该类型对象。你可以自己确定这一点:首先将命令 A 的输出通过管道传递到 Get-Member 以查看命令 A 生成的对象类型。然后检查命令 B 的完整帮助(例如,Get-Help Get-Command -Full)以查看是否有任何参数接受来自管道 ByValue 的该类型数据。图 10.2 展示了你可能会发现的内容。

图片

图 10.2 比较 Get-Content 的输出与 Get-Command 的输入参数

你会发现 Get-Content 生成 System.String 类型的对象(或简称为 String)。你还会发现 Get-Command 确实有一个参数可以接受来自管道 ByValueString。问题是它是 -Name 参数,根据帮助信息,“指定了一个名称数组。此 cmdlet 仅获取具有指定名称的命令。”这不是我们想要的——我们的文本文件,因此我们的 String 对象是模块名称,而不是命令名称。如果我们运行

Get-Content .\modules.txt | Get-Command

我们将尝试检索名为 Microsoft.PowerShell.Archive 等的命令,这可能不会成功。

如果多个参数接受来自管道的相同类型,则所有参数都将接收相同的值。因为 -Name 参数接受来自管道 ByValueString,所以从实际应用的角度来看,没有其他参数可以这样做。这打破了我们尝试将模块名称从我们的文本文件通过管道传递到 Get-Command 的希望。

在这种情况下,管道输入正在工作,但它并没有达到我们希望的结果。让我们考虑一个不同的例子,其中我们确实得到了我们想要的结果。以下是命令行:

Get-Content ./modules.txt | Get-Module

让我们将命令 A 的输出通过管道传递到 Get-Member 并检查命令 B 的完整帮助。图 10.3 展示了你将发现的内容。

图片

图 10.3 将 Get-Content 的输出绑定到 Get-Module 的参数

Get-Content 生成 String 类型的对象。Get-Module 可以从管道 ByValue 接受这些 string 对象;它在 -Name 参数上这样做。根据帮助信息,该参数“指定了 cmdlet 获取的模块的名称或名称模式。”换句话说,命令 A 获取一个或多个 String 对象,而命令 B 尝试找到字符串中的名称对应的模块。

提示:大多数情况下,具有相同名词(如 Get-ProcessStop-Process)的命令通常可以相互通过 ByValue 管道传递。花点时间看看你是否可以将 Get-Process 的输出通过管道传递到 Stop-Process

让我们再举一个例子:

Get-ChildItem -File | Stop-Process -WhatIf

表面上看,这可能没有意义。但让我们通过将命令 A 的输出通过管道传递到 Get-Member 并重新检查命令 B 的帮助来探究这个问题。图 10.4 展示了你应该发现的内容。

图片

图 10.4 检查Get-ChildItem的输出和Stop-Process的输入参数

Get-ChildItem产生FileInfo类型的对象(技术上,是System.IO.FileInfo,但你通常可以将TypeName的最后部分作为快捷方式)。不幸的是,Stop-Process没有单个参数可以接受FileInfo对象。ByValue方法失败了,PowerShell 将尝试其备份计划:ByPropertyName

10.4 计划 B:通过属性名管道输入

使用这种方法,你仍然希望将命令 A 的输出连接到命令 B 的参数。但ByPropertyNameByValue略有不同。在这个备份方法中,命令 B 的多个参数可能都会被涉及。再次将命令 A 的输出通过管道连接到Get-Member,然后查看命令 B 的语法。图 10.5 显示了你应该找到的内容:命令 A 有一个属性名称与命令 B 上的参数相对应。

图 10.5 将属性映射到参数

许多人对这里发生的事情想得太多,所以让我们明确一下 shell 是如何简单的:它正在寻找与参数名称匹配的属性名称。就是这样。因为属性Name的拼写与参数-Name相同,shell 试图将两者连接起来。

但它不能立即这样做;首先它需要查看-Name参数是否会接受来自管道ByPropertyName的输入。需要查看完整的帮助信息,如图 10.6 所示,以做出这个判断。

图 10.6 检查Stop-Process命令的-Name参数是否接受管道输入ByPropertyName

在这种情况下,-Name确实接受来自管道ByPropertyName的输入,因此这个连接是有效的。现在,这里有个技巧:与只涉及一个参数的ByValue不同,ByPropertyName将每个匹配的属性和参数(假设每个参数都设计为接受管道输入ByPropertyName)连接起来。在我们的当前示例中,只有Name-Name匹配。结果如何?请查看图 10.7。

图 10.7 尝试将Get-ChildItem管道连接到Stop-Process

我们看到了一堆错误信息。问题是,文件名通常是像 chapter7.zip 和 computers.txt 这样的东西,而进程的可执行文件可能是像pwsh这样的东西。Stop-Process只处理这些可执行文件名。尽管Name属性通过管道连接到-Name参数,但Name属性内的值对-Name参数来说是没有意义的,这导致了错误。

让我们看看一个更成功的例子。在 Visual Studio Code 中创建一个简单的 CSV 文件,使用图 10.8 中的示例。

图 10.8 在 Visual Studio Code 中创建此 CSV 文件。

将文件保存为 aliases.txt。现在,回到 shell 中,尝试导入它,如图 10.9 所示。你还应该将Import-Csv的输出通过管道传递给Get-Member,这样你就可以检查输出成员。

图 10.9 导入 CSV 文件并检查其成员

你可以清楚地看到 CSV 文件的列变成了属性,CSV 文件中的每一行数据变成了一个对象。现在,检查New-Alias的帮助信息,如图 10.10 所示。

图 10.10 将属性与参数名称匹配

这两个属性(NameValue)对应于New-Alias的参数名称。显然,这是故意的——当你创建 CSV 文件时,你可以为这些列命名任何你想要的名称。现在,检查-Name-Value是否接受管道输入ByPropertyName,如图 10.11 所示。

图 10.11 检查NameValue是否接受管道输入ByPropertyName的参数

这两个参数都接受,这意味着这个技巧是有效的。尝试运行以下命令

Import-Csv .\aliases.txt | New-Alias

结果是三个新的别名,分别命名为dselgo,分别指向Get-ChildItemSelect-ObjectInvoke-Command命令。这是一种强大的技术,可以将数据从一个命令传递到另一个命令,并在最少的命令数内完成复杂任务。

10.5 当事情不对齐时:自定义属性

CSV 示例很酷,但当你从头开始创建输入时,使属性和参数名称对齐相当容易。当你被迫处理为你创建的对象或他人生成的数据时,事情就变得复杂了。

对于下一个示例,让我们玩一个新的命令:New-ADUser。它是 Active Directory 模块的一部分。你可以在客户端计算机上通过安装 Microsoft 的远程服务器管理工具(RSAT)来获取该模块。但就现在而言,不要担心运行该命令;跟随示例进行。

New-ADUser具有设计用于接受有关新 Active Directory 用户信息的参数。以下是一些示例:

  • -Name(必需)

  • -samAccountName(技术上不是必需的,但你必须提供它才能使账户可用)

  • -Department

  • -City

  • -Title

我们可以涵盖其他内容,但让我们处理这些。所有这些都接受管道输入ByPropertyName

对于这个示例,你将再次假设你正在获取一个 CSV 文件,但它来自你公司的人力资源或人事部门。你已经给他们提供了你想要的文件格式十几次,但他们仍然坚持给你一些接近但并不完全正确的东西,如图 10.12 所示。

图 10.12 使用人力资源部门提供的 CSV 文件

如图 10.12 所示,外壳可以很好地导入 CSV 文件,结果是有三个对象,每个对象有四个属性。问题是 dept 属性不会与 New-ADUser-Department 参数对齐,login 属性没有意义,你没有 samAccountNameName 属性——这两个属性都是运行此命令创建新用户所必需的:

PS C:\> import-csv .\newusers.csv | new-aduser

你该如何解决这个问题?你可以打开 CSV 文件并修复它,但这需要大量的手动工作,而 PowerShell 的整个目的就是减少手动劳动。为什么不设置外壳来修复它呢?看看下面的例子:

PS C:\> import-csv .\newusers.csv |
>> select-object -property *,
>>  @{name='samAccountName';expression={$_.login}},
>>  @{label='Name';expression={$_.login}},
>>  @{n='Department';e={$_.Dept}}
>>
login          : TylerL
dept           : IT
city           : Seattle
title          : IT Engineer
samAccountName : TylerL
Name           : TylerL
Department     : IT
login          : JamesP
dept           : IT
city           : Chattanooga
title          : CTO
samAccountName : JamesP
Name           : Jamesp
Department     : IT
login          : RobinL
dept           : Custodial
city           : Denver
title          : Janitor
samAccountName : RobinL
Name           : RobinL
Department     : Custodial

这是一种相当奇怪的语法,所以让我们来分解一下:

  • 我们使用 Select-Object 和其 -Property 参数。我们首先指定属性 *,这意味着“所有现有属性”。请注意,* 后面跟着一个逗号,这意味着我们正在继续属性列表。

  • 然后我们创建一个哈希表,这是一个以 @{ 开始并以 } 结束的结构。哈希表由一个或多个键值对组成,Select-Object 已经被编程为查找特定的键,我们将提供给它。

  • Select-Object 想要的第一个键可以是 NameNLabelL,该键的值是我们想要创建的属性的名称。在第一个哈希表中,我们指定了 samAccountName;,在第二个中,Name;,在第三个中,Department。这些对应于 New-ADUser 的参数名称。

  • Select-Object 需要的第二个键可以是 ExpressionE。该键的值是一个脚本块,包含在大括号 {} 内。在这个脚本块中,你使用特殊的 $_ 占位符来引用现有的管道输入对象(CSV 文件的原始行数据)后跟一个点。占位符 $_ 允许你访问管道输入对象的某个属性,或 CSV 文件的某一列。这指定了新属性的内容。

现在试试看。创建图 10.12 所示的 CSV 文件。然后尝试运行我们之前所做的确切命令——你可以按显示的完全一样地输入它。

我们所做的是取 CSV 文件的内容——Import-CSV 的输出——并在管道中动态地修改它。我们的新输出与 New-ADUser 想要看到的内容相匹配,因此我们现在可以通过运行此命令来创建新用户:

PS C:\> import-csv .\newusers.csv |
>> select-object -property *,
>>  @{name='samAccountName';expression={$_.login}},
>>  @{label='Name';expression={$_.login}},
>>  @{n='Department';e={$_.Dept}} |
>> new-aduser
>>

语法可能有点丑陋,但这项技术非常强大。它也可以在 PowerShell 的许多其他地方使用,你将在接下来的章节中再次看到它。你甚至会在 PowerShell 帮助文件中的示例中看到它;运行 Help Select -Example 并亲自查看。

10.6 使用 Azure PowerShell

在本章的剩余部分,我们将假设你已经设置了 Azure PowerShell。所以,让我们让它工作起来。如果你没有订阅,你可以在这里注册试用:azure.microsoft.com/en-us/free/。如果这个链接已过时,请搜索 Azure 免费试用。

一旦你有了订阅,确保你已经安装了 Az 模块。回顾第七章,但命令是

Install-Module az

现在,你已经安装了 Az,运行 Connect-AzAccount 并按照说明操作;目前,它要求你打开浏览器并输入代码。它应该会告诉你你已经连接,通过打印你的电子邮件、订阅名称和其他一些信息。

如果你与账户关联了多个订阅,你可能会连接到错误的订阅。如果是这样,请确保你选择了正确的订阅。如果你的订阅名称是 Visual Studio Enterprise,则命令将是 Select-AzSubscription -SubscriptionName 'Visual Studio Enterprise'

10.7 括号命令

有时候,无论你多么努力,你都无法使管道输入工作。例如,考虑 Get-Command。查看其 -Module 参数的帮助,如图 10.13 所示。

图 10.13 读取 Get-CommandModule 参数帮助

尽管此参数接受来自管道的模块名称,但它通过属性名称来执行。有时命令可能根本不接受管道输入。在这种情况下,我们一直在讨论的方法更容易。以下将不起作用:

Get-Content .\modules.txt | Get-Command

Get-Content 生成的 String 对象不会匹配 Get-Command-Module 参数,而是应该使用 -Name。我们该怎么办?使用括号:

PS /Users/tylerl> Get-Command -Module (Get-Content ./modules.txt)

回想一下高中代数课,你会记得括号意味着“先做这个”。这就是 PowerShell 所做的:它首先运行括号内的命令。该命令的结果——在本例中是一系列 String 对象——被传递到参数。因为 -Module 恰好需要一系列 String 对象,所以命令可以正常工作。

现在试试看。使用 Get-Module -ListAvailable 获取一些模块名称进行测试;然后继续尝试那个命令。将正确的模块名称放入你自己的 modules.txt 文件中。

括号命令技巧很强大,因为它完全不依赖于管道参数绑定——它直接将对象放入参数中。但如果你的括号命令没有生成参数期望的确切类型的对象,那么有时你将不得不稍微操作一下。让我们看看如何操作。

10.8 从单个属性中提取值

在第 10.7 节中,我们展示了使用括号执行 Get-Content 的示例,将其输出传递给另一个 cmdlet 的参数:

Get-Command -Module (Get-Content ./modules.txt) 

让我们来探索括号的其他用法。有一个命令用于创建一个名为 New-AzStorageAccount 的存储账户。假设你想创建一个存储账户并将其放入一个已经存在于 Azure 位置的资源组中。与其从现有的文本文件中获取资源组名称,你可能会想从 Azure 查询现有的资源组名称。使用 Az.Storage 模块(该模块包含在我们第 10.6 节中安装的 Az 模块中),你可以查询一个位置中的所有资源组:

Get-AzResourceGroup -Location westus2

你可以使用相同的括号技巧将资源组名称提供给 New-AzStorageAccount 吗?例如,以下命令会起作用吗?

PS /Users/tylerl> New-AzStorageAccount -ResourceGroupName 
➥ (Get-AzResourceGroup -Location westus2| Select-Object -First 1) 
➥ -Name test0719 -SkuName Standard_ZRS -Location westus2

很遗憾,它不会。查看 New-AzStorageAccount 的帮助,你会看到 -ResourceGroupName 参数期望 String 类型的值。注意,添加了 Select-Object -First 1 以获取第一个资源组,因为 -ResourceGroupName 只接受一个字符串,而不是字符串数组。

替换为以下命令:

Get-AzResourceGroup | Get-Member

Get-Member 显示 Get-AzResourceGroup 生成的是 PSResourceGroup 类型的对象。这些不是 String 对象,所以 -ResourceGroupName 不会知道如何处理它们。但是,PSResourceGroup 对象确实有一个 ResourceGroupName 属性。你需要做的是提取对象的 ResourceGroupName 属性的值,并将这些值(即资源组名称)提供给 -ResourceGroupName 参数。

小贴士:这是关于 PowerShell 的一个重要事实,如果你现在有点困惑,请停止并重新阅读前面的段落。《Get-AzResourceGroup》生成 PSResourceGroup 类型的对象;Get-Member 证明了这一点。《New-AzStorageAccount》的 -ResourceGroupName 参数不能接受 PSResourceGroup 对象;它只接受 String 对象,正如其帮助文件所示。因此,那个括号内的命令不能按原样工作。

再次强调,Select-Object 命令可以救你,因为它包括 -ExpandProperty 参数,该参数接受一个属性名称。该命令获取该属性,提取其值,并将这些值作为 Select-Object 的输出返回。考虑以下示例:

Get-AzResourceGroup -Location westus2 | Select-Object -First 1 
➥ -ExpandProperty ResourceGroupName

你应该得到一个资源组名称。它可以被提供给 New-AzStorageAccount-ResourceGroupName 参数(或任何具有 -ResourceGroupName 参数的其他命令):

New-AzStorageAccount -ResourceGroupName (Get-AzResourceGroup -Location 
➥ westus2 | Select-Object -First 1 -ExpandProperty ResourceGroupName) 
➥ -Name downloads -SkuName Standard_LRS -Location westus2

小贴士:再次强调,这是一个重要的概念。通常,像 Select-Object -Property Name 这样的命令会产生只有 Name 属性的对象,因为这是我们指定的所有内容。-ComputerName 参数不想要具有 Name 属性的随机对象;它想要一个 String,这是一个更简单的值。-ExpandProperty Name 进入 Name 属性并提取其值,从而从命令返回简单的字符串。

再次强调,这是一个很酷的技巧,它使得将更多种类的命令组合在一起成为可能,节省了你的输入,并让 PowerShell 做更多的工作。

在我们继续之前,让我们来了解一下Select-Object上的-Property。尝试将括号中的命令更改为

Get-AzResourceGroup -Location westus2 | Select-Object -First 1 
➥ -Property ResourceGroupName

现在将其管道传递给Get-Member。它仍然是一个PSResourceGroup。PowerShell 创建了一个新的自定义对象,只包含你选择的属性。因此,在New-AzStorageAccount上的-ResourceGroupName仍然不会接受这个对象。让我们看看 PowerShell 是如何做到这一点的。运行以下命令:

(Get-AzResourceGroup -Location westus2 | Select-Object -First 1 
➥ -Property ResourceGroupName).GetType().Name

输出是PSCustomObject。这是 PowerShell 用来暴露你选择的属性的包装类型。

让我们回顾一下我们所学的内容。这是一个强大的技术。一开始可能有点难以理解,但理解属性就像一个盒子可能有助于理解。使用Select-Object -Property,你是在决定你想要的盒子,但你仍然有盒子。使用Select-Object -ExpandProperty,你是在提取盒子的内容并完全丢弃盒子。你留下的只是内容。

10.9 实验室

再次强调,我们在短时间内覆盖了许多重要的概念。巩固新知识的最佳方式是将所学知识立即付诸实践。我们建议按照以下顺序完成任务,因为它们相互关联,有助于提醒你所学的内容,并帮助你找到实际应用这些知识的方法。

为了使这个问题更具挑战性,我们将迫使你考虑如何使用 Az.Accounts 模块(该模块包含在我们第 10.6 节中安装的 Az 模块中)。这应该在任何 macOS 或 Ubuntu 机器上都能工作:

  • Get-AzSubscription命令有-SubscriptionName参数;运行Get-AzSubscription -SubscriptionName MySubscriptionName会从你的账户中检索名为MySubscriptionName的订阅。

  • Select-AZSubscription命令有-Subscription参数;运行Select-AzSubscription -Subscription MySubscriptionName会将订阅设置在 Az.*模块中大多数命令使用的上下文中,以确定要使用的订阅。

  • Get-AzContext命令可以用来确定哪个订阅被选中。

这就是你需要知道的所有内容。考虑到这一点,完成以下任务。

注意:你不需要运行这些命令。这更像是一种心理练习。相反,你被要求判断这些命令是否能够正常工作以及为什么。

  1. 以下命令能否用来从当前机器上以 Microsoft.*开头的模块中检索命令列表?为什么或为什么不?写一个解释,类似于我们在本章前面提供的那些。

    Get-Command -Module (Get-Module -ListAvailable -Name Microsoft.* | 
    Select-Object -ExpandProperty name)
    
  2. 这个替代命令能否用来从同一模块检索命令列表?为什么或为什么不?写一个解释,类似于我们在本章前面提供的那些。

    Get-Module -ListAvailable -Name Microsoft.* | Get-Command
    
  3. 这会设置 Azure 上下文中的订阅吗?考虑一下Get-AzSubscription是否检索多个订阅。

    Get-AzSubscription | Select-AzSubscription
    
  4. 编写一个命令,使用管道参数绑定来检索第一个订阅并将它设置在 Azure 上下文中。不要使用括号。

  5. 编写一个使用管道参数绑定的命令来检索第一个订阅并将其设置在 Azure 上下文中。不要使用管道输入;相反,使用括号命令(括号中的命令)。

  6. 有时候有人会忘记在 cmdlet 中添加管道参数绑定。例如,以下命令能否在 Azure 上下文中设置订阅?请写一个类似于我们本章前面提供的解释。

    'mySubscriptionName' | Select-AzSubscription
    

10.10 实验答案

  1. 这应该会起作用,因为嵌套的Get-Module表达式将返回一个模块名称的集合,并且-Module参数可以接受一个值数组。

  2. 这不会起作用,因为Get-Command不接受模块参数的值。它将接受-Name的值,但这只是命令名称,而不是模块对象。

  3. 从技术上讲,这确实设置了订阅,但如果存在多个账户,最后处理的那个账户将被设置。它之所以有效,是因为第一个 cmdlet 返回一个PSAzureSubscription,而Select-AzSubscription-SubscriptionObject,它可以从管道ByValue接受该类型。

  4. Get-AzSubscription | Select-Object -First 1 | Select-AzSubscription

  5. Select-AzSubscription -SubscriptionObject (Get-AzSubscription | Select-Object -First 1)

  6. 这将不会起作用。Select-AzSubscription中的Subscription参数不接受任何管道绑定。

10.11 进一步探索

我们发现许多学生难以接受这个管道输入的概念,主要是因为它太抽象了。不幸的是,这些内容对于理解 shell 也是至关重要的。如果你需要,请重新阅读本章,重新运行我们提供的示例命令,并仔细查看输出以了解管道是如何工作的。例如,为什么这个命令的输出

Get-Date | Select –Property DayOfWeek

略有不同?

Get-Date | Select –ExpandProperty DayOfWeek

如果你仍然不确定,请在livebook.manning.com/book/learn-powershell-in-a-month-of-lunches-linux-and-macos-edition/discussion论坛上给我们留言。

11 格式化:为什么是在右边进行

让我们快速回顾一下。你知道 PowerShell 命令会产生对象,并且这些对象通常包含比 PowerShell 默认显示的更多属性。你知道如何使用 gm 来获取一个对象的所有属性的列表,也知道如何使用 Select-Object 来指定你想要看到的属性。在本书的这一部分,你一直依赖于 PowerShell 的默认配置和规则来确定最终输出在屏幕(或文件、或硬拷贝形式)上的显示方式。在本章中,你将学习如何覆盖这些默认设置,并为你的命令输出创建自己的格式。

11.1 格式化:让你的显示更美观

我们不想给人留下印象,认为 PowerShell 是一个完整的管理报告工具,因为它不是。但 PowerShell 在收集信息方面具有很好的能力,并且,通过正确的输出,你当然可以使用这些信息生成报告。关键是获取正确的输出,这正是格式化的全部内容。

表面上看,PowerShell 的格式化系统可能看起来很容易使用——大部分情况下确实如此。但是,格式化系统也包含了一些整个 shell 中最棘手的“陷阱”,因此我们想确保你理解它是如何工作的以及为什么它会这样做。我们不仅会向你展示一些新的命令;相反,我们将解释整个系统是如何工作的,你如何与之交互,以及你可能会遇到什么限制。

11.2 使用默认格式化

再次运行我们的老朋友 Get-Process,并特别注意列标题。注意,它们并不完全匹配属性名称。相反,每个标题都有特定的宽度、对齐方式等等。所有这些配置信息必须来自某个地方,对吧?你可以在与 PowerShell 一起安装的 .format.ps1xml 文件中找到它。具体来说,进程对象的格式化方向在 DotNetTypes.format.ps1xml 中。

现在尝试一下 你绝对需要打开 PowerShell,以便你可以跟随我们即将展示的内容。这将帮助你理解格式化系统在幕后是如何工作的。

我们将首先切换到 PowerShell 安装文件夹,特别是 PSReadLine 的位置,并打开 PSReadLine.format.ps1xml。PSReadLine 是一个 PowerShell 模块,它提供了在 PowerShell 控制台中输入时的体验。它添加了许多花哨的键盘快捷键和语法高亮,并且是可定制的。请小心不要保存对任何更改到这个文件。它是数字签名的,你保存的任何更改——即使是文件中添加的单个换行符或空格——都会破坏签名并阻止 PowerShell 使用该文件。

PS /Users/jamesp/> cd $pshome/Modules/PSReadLine
PS /Users/jamesp/> code PSReadLine.format.ps1xml

提示:您可能会收到一个警告代码:“term 'code' is not recognized as a name of a cmdlet, function, script file, or executable program.”要修复此问题,请打开命令面板并运行以下 shell 命令:“Shell Command:Install 'code' command in PATH”。

接下来,找出 Get-PSReadLineKeyHandler 返回的确切对象类型:

PS /Users/jamesp/> Get-PSReadLineKeyHandler | get-member

现在,按照以下步骤操作:

  1. 将完整的类型名称 Microsoft.PowerShell.KeyHandler 复制并粘贴到剪贴板。

  2. 切换到 Visual Studio Code 并按 Cmd-F(或在 Windows 上按 Ctrl-F)打开搜索对话框。

  3. 在搜索对话框中,粘贴您复制到剪贴板中的类型名称。按 Enter 键。

  4. 您应该在文件中看到 Microsoft.PowerShell.KeyHandler。图 11.1 展示了您应该找到的内容。

图片

图 11.1 在 Visual Studio Code 中定位键处理器视图

您现在在 Visual Studio Code 中看到的是一组指导如何默认显示键处理器的指令。向下滚动,您将看到 表格视图 的定义,这是您应该预期的,因为您已经知道键处理器以多列表格的形式显示。您将看到熟悉的列名,如果您再向下滚动一点,您将看到文件指定了哪些属性将在每个列中显示。您还将看到列宽和对齐的定义。浏览完毕后,请小心关闭 Visual Studio Code,确保不要保存您可能意外修改的任何文件,然后返回 PowerShell。

现在尝试一下:您也可以通过运行以下命令来获取此格式数据。您可以玩弄返回的对象,但我们不会专注于它。

PS /Users/jamesp/> Get-FormatData -PowerShellVersion 7.1 -TypeName 
➥ Microsoft.PowerShell.KeyHandler

当您运行 Get-PSReadLineKeyHandler 时,shell 中会发生以下情况:

  1. 该命令将 Microsoft.PowerShell.KeyHandler 类型的对象放入管道。

  2. 在管道末尾是一个不可见的命令 Out-Default。它始终存在,其任务是收集在所有命令运行之后管道中的所有对象。

  3. Out-Default 将对象传递给 Out-Host,因为 PowerShell 控制台被设计为使用屏幕(称为 host)作为其默认的输出形式。从理论上讲,有人可以编写一个使用文件或打印机作为默认输出的 shell,但我们所知没有人这样做。

  4. 大多数 Out- 命令无法与标准对象一起工作。相反,它们被设计为与特殊的格式化指令一起工作。因此,当 Out-Host 发现它被传递了标准对象时,它会将它们传递给格式化系统。

  5. 格式化系统查看对象的类型,并遵循一组内部格式化规则(我们将在稍后介绍这些规则)。它使用这些规则来生成格式化指令,并将这些指令传递回 Out-Host

  6. 一旦 Out-Host 看到有格式化指令,它会遵循这些指令来构建屏幕显示。

所有这些都会在你手动指定一个Out-命令时发生。例如,运行Get-Process | Out-File procs.txtOut-File会看到你发送了一些普通对象。它会将这些对象传递给格式化系统,该系统创建格式化指令并将它们传递回Out-File。然后Out-File根据这些指令构建文本文件。所以,格式化系统在任何需要将对象转换为人类可读的文本输出时都会介入。

格式化系统在步骤 5 中遵循哪些规则?对于第一条格式化规则,系统会查看它所处理的对象类型是否有预定义的视图。这就是你在 PSReadLine.format.ps1xml 中看到的:一个针对KeyHandler对象的预定义视图。一些其他的.format.ps1xml 文件也随 PowerShell 一起安装,并且当 shell 启动时默认加载。你也可以创建自己的预定义视图,尽管这样做超出了这本书的范围。

格式化系统会寻找针对它所处理的特定对象类型的预定义视图。在这个例子中,它正在寻找处理Microsoft .PowerShell.KeyHandler对象的视图。

如果没有预定义的视图会怎样?让我们使用System.Uri类型来找出答案,它没有在 format.ps1xml 文件中找到条目(我们保证!)。尝试运行以下命令:

[Uri]"https://github.com"

这就是使用一个叫做“铸造”的概念,我们说,“嘿,PowerShell,我有一个看起来像 URI 的字符串。你能把它当作System.Uri类型来处理吗?”PowerShell 回答,“没问题!”然后给你一个Uri对象。你可能注意到,我们运行的那一行并没有指定System。这是因为如果 PowerShell 找不到只叫做Uri的类型,它就会在前面加上System。聪明的 PowerShell!无论如何,输出的结果是一个长长的属性列表,如下所示:

AbsolutePath   : /
AbsoluteUri    : https://github.com/
LocalPath      : /
Authority      : github.com
HostNameType   : Dns
IsDefaultPort  : True
IsFile         : False
IsLoopback     : False
PathAndQuery   : /
Segments       : {/}
IsUnc          : False
Host           : github.com
Port           : 443
Query          :
Fragment       :
Scheme         : https
OriginalString : https://github.com
DnsSafeHost    : github.com
IdnHost        : github.com
IsAbsoluteUri  : True
UserEscaped    : False
UserInfo       :

对于没有特殊格式化的东西来说,格式化并不太差。这是因为 PowerShell 会查看类型的属性,并以友好的视图显示它们。我们可以通过为该类型引入一个 format .ps1xml 来控制我们在这里看到哪些属性,或者我们可以允许格式化系统进行下一步,或者我们称之为第二个格式化规则:它会查看是否有人为该类型的对象声明了一个默认显示属性集。你可以在不同的配置文件 types.ps1xml 中找到这些。由于我们不会深入探讨编写自己的格式和类型文件,我们将给你一个要加载的文件,并看看它如何影响输出。首先,让我们在 Visual Studio Code 中创建并打开一个名为 Uri.Types.ps1xml 的新文件:

PS /Users/jamesp/> code /tmp/Uri.Types.ps1xml  

现在,粘贴以下内容并保存文件:

<?xml version="1.0" encoding="utf-8" ?>
<Types>
  <Type>
    <Name>System.Uri</Name>
    <Members>
      <MemberSet>
        <Name>PSStandardMembers</Name>
        <Members>
          <PropertySet>
            <Name>DefaultDisplayPropertySet</Name>
            <ReferencedProperties>
              <Name>Scheme</Name>
              <Name>Host</Name>
              <Name>Port</Name>
              <Name>AbsoluteUri</Name>
              <Name>IsFile</Name>
            </ReferencedProperties>
          </PropertySet>
        </Members>
      </MemberSet>
    </Members>
  </Type>
</Types>

太好了,现在,看到DefaultDisplayPropertySet了吗?记下那里列出的五个属性。然后回到 PowerShell 中运行以下命令:

PS /Users/jamesp/> Update-TypeData -Path /tmp/Uri.Types.ps1xml

我们刚刚加载了我们刚刚创建的 Types.ps1xml 文件。现在,让我们再次运行原始行,看看它会得到什么结果:

PS /Users/jamesp/> [Uri]"https://github.com"

Scheme      : https
Host        : github.com
Port        : 443
AbsoluteUri : https://github.com/
IsFile      : False

结果看起来熟悉吗?应该是的——你看到的属性仅仅是因为它们在 Types.ps1xml 中列为默认值。如果格式化系统找到一个默认显示属性集,它将使用该属性集进行下一个决定。如果没有找到,下一个决定将考虑对象的所有属性。

那个接下来的决定——第三个格式化规则——是关于要创建的输出类型。如果格式化系统显示四个或更少的属性,它将使用表格。如果有五个或更多的属性,它将使用列表。这就是为什么System.Uri对象没有以表格形式显示:它的五个属性触发了列表。理论上是说,超过四个属性可能不适合没有截断信息的临时表格。

现在你已经知道了默认格式化是如何工作的。你还知道大多数Out- cmdlet 会自动触发格式化系统,以便它们可以获取所需的格式化指令。接下来,让我们看看如何自己控制该格式化系统并覆盖默认设置。

哦,顺便说一下,格式化系统是 PowerShell 有时似乎“撒谎”的原因。例如,运行Get-Process并查看列标题。看到标记为PM(K)的那个吗?嗯,那是一种谎言,因为不存在名为PM(K)的属性。有一个名为PM的属性。这里的教训是,格式化的列标题只是列标题。它们不一定与底层属性名称相同。查看属性名称的唯一安全方法是使用Get-Member

11.3 表格格式化

PowerShell 有四个格式化 cmdlet,我们将使用提供最多日常格式化能力的三个(第四个在章节末尾的“超越”侧边栏中简要讨论)。首先是Format-Table,它有一个别名,ft

如果你阅读了Format-Table的帮助文件,你会注意到它有几个参数。以下是一些最有用的参数,以及如何使用它们的示例:

  • -Property——此参数接受一个以逗号分隔的属性列表,这些属性应包含在表格中。这些属性不区分大小写,但 shell 将使用你输入的内容作为列标题,因此你可以通过正确的大小写属性名称(例如,CPU而不是cpu)来获得更美观的输出。此参数接受通配符,这意味着你可以指定*以包含表格中的所有属性,或者像c*这样的东西以包含以c开头的所有属性。请注意,shell 仍然只会显示它可以在表格中容纳的属性,因此你指定的并非每个属性都会显示。此参数是位置参数,因此如果你将属性列表放在第一个位置,你不必输入参数名称。尝试以下示例(以下第二个示例来自Format-Table的帮助文件):

    Get-Process | Format-Table -Property *
    Get-Process | Format-Table -Property ID,Name,Responding
    Get-Process | Format-Table *
       Id Name            Responding
       -- ----            ----------
    20921 XprotectService       True
     1242 WiFiVelocityAge       True
      434 WiFiAgent             True
    89048 VTDecoderXPCSer       True
    27019 VTDecoderXPCSer       True
      506 ViewBridgeAuxil       True
      428 usernoted             True
      407 UserEventAgent        True
      544 useractivityd         True
      710 USBAgent              True
     1244 UsageTrackingAg       True
      416 universalaccess       True
      468 TrustedPeersHel       True
      412 trustd                True
    24703 transparencyd         True
     1264 TextInputMenuAg       True
    38115 Telegram              True
      425 tccd                  True
      504 talagent              True
     1219 SystemUIServer        True
    
  • -GroupBy参数会在指定的属性值更改时生成一组新的列标题。这仅在首先按该属性对对象进行排序时才有效。以下是一个示例(此示例将根据虚拟机是运行还是停止来对 Azure VM 进行分组):

    PS /Users/jamesp/> Get-AzVM -Status | Sort-Object PowerState | 
    ➥ ft -Property Name,Location,ResourceGroupName -GroupBy PowerState 
    
       PowerState: VM running
    Name       Location ResourceGroupName
    ----       -------- -----------------
    MyUbuntuVM eastus2  MYUBUNTUVM
    
       PowerState: VM deallocated
    Name        Location ResourceGroupName
    ----        -------- -----------------
    MyUbuntuVM2 eastus2  MYUBUNTUVM
    WinTestVM2  westus2  WINTESTVM2
    
  • -Wrap参数——如果 shell 需要在列中截断信息,它将以省略号(. . .)结束该列,以视觉上表示信息已被抑制。此参数使 shell 能够包装信息,这使得表格更长,但保留了你想显示的所有信息。以下是一个示例:

    PS /Users/jamesp/> Get-Command | Select-Object Name,Source | ft -Wrap
    
    Name                                       Source
    ----                                       ------
    Compress-Archive                           Microsoft.P
                                               owerShell.A
                                               rchive
    Configuration                              PSDesiredSt
                                               ateConfigur
                                               ation
    Expand-Archive                             Microsoft.P
                                               owerShell.A
                                               rchive
    Expand-GitCommand                          posh-git
    Find-Command                               PowerShellG
                                               et
    Find-DscResource                           PowerShellG
                                               et
    Find-Module                                PowerShellG
                                               et
    Find-RoleCapability                        PowerShellG
                                               et
    

现在试试看 你应该在 shell 中运行所有这些示例,并且可以自由混合和匹配这些技术。实验以查看哪些有效,以及你可以创建什么样的输出。这些命令仅在你已经连接到 Azure 帐户并且你有 Azure 中的现有虚拟机时才有效。

11.4 格式化列表

有时你需要显示比表格水平宽度更多的信息,这可以使列表变得有用。Format-List是你要使用的 cmdlet,或者你可以使用它的别名fl

此 cmdlet 支持与Format-Table相同的某些参数,包括-Property。实际上,fl是显示对象属性的另一种方式。与gm不同,fl还会显示这些属性的值,以便你可以看到每个属性包含的信息类型:

Get-Verb | Fl *
...
Verb        : Remove
AliasPrefix : r
Group       : Common
Description : Deletes a resource from a container

Verb        : Rename
AliasPrefix : rn
Group       : Common
Description : Changes the name of a resource

Verb        : Reset
AliasPrefix : rs
Group       : Common
Description : Sets a resource back to its original state

Verb        : Resize
AliasPrefix : rz
Group       : Common
Description : Changes the size of a resource

Verb        : Search
AliasPrefix : sr
Group       : Common
Description : Creates a reference to a resource in a container

Verb        : Select
AliasPrefix : sc
Group       : Common
Description : Locates a resource in a container
...

我们经常使用fl作为发现对象属性的一种替代方式。

现在试试看 Read the help for Format-List,并尝试使用其参数进行实验。

11.5 格式化宽列表

最后一个 cmdlet 是Format-Wide(或其别名fw),它显示一个更宽、多列的列表。它只能显示单个属性的值,因此其-Property参数只接受一个属性名称,不接受列表,也不接受通配符。

默认情况下,Format-Wide查找对象的Name属性,因为Name是一个常用的属性,通常包含有用的信息。显示通常默认为两列,但可以使用-Columns参数指定更多列:

Get-Process | Format-Wide name -col 4

iTerm2        java          keyboardserv... Keychain Ci...
knowledge-ag... LastPass      LocationMenu  lockoutagent
loginwindow   lsd           Magnet        mapspushd
mdworker      mdworker_sha... mdworker_sha... mdworker_sh...
mdworker_sha... mdworker_sha... mdworker_sha... mdworker_sh...
mdworker_sha... mdworker_sha... mdworker_sha... mdwrite
media-indexer mediaremotea... Microsoft Ed... Microsoft E...
Microsoft Ed... Microsoft Ed... Microsoft Ed... Microsoft E...
Microsoft Ed... Microsoft Ed... Microsoft Ed... Microsoft E...

现在试试看 Read the help for Format-Wide,并尝试使用其参数进行实验。

11.6 创建自定义列和列表条目

回到上一章,复习第 10.5 节。在该节中,我们向您展示了如何使用散列表结构向对象添加自定义属性。Format-TableFormat-List都可以使用这些相同的结构来创建自定义表列或自定义列表条目。

你可能这样做是为了提供一个与显示的属性名称不同的列标题:

Get-AzStorageAccount | Format-Table @{name='Name';expression=
  {$_.StorageAccountName}},Location,ResourceGroupName

Name                     Location       ResourceGroupName
----                     --------       -----------------
myubuntuvmdiag           eastus2        MyUbuntuVM
ismtrainierout           westus         ismtrainierout
cs461353efc2db7x45cbxa2d westus         cloud-shell-storage...
mtnbotbmyhfk             westus         mtnbot
pssafuncapp              westus         pssafuncapp 

注意:这仅当已经存在 Azure 连接和存储帐户时才有效。

或者,你可能需要放置一个更复杂的数学表达式:

Get-Process | Format-Table Name, @{name='VM(MB)';expression={$_.VM / 1MB 
➥ -as [int]}}

我们承认,我们通过引入一些尚未讨论的内容而有点作弊。我们不妨现在就谈谈:

  • 显然,我们是从Get-Process开始,这是一个你现在非常熟悉的 cmdlet。如果你运行Get-Process | fl *,你会看到VM属性以字节为单位,尽管默认的表格视图并不是这样显示的。

  • 我们告诉Format-Table从进程的Name属性开始。

  • 接下来,我们使用一个特殊的哈希表来创建一个自定义列,该列将被标记为VM(MB)。这是分号之前的第一个部分,它是一个分隔符。第二个部分通过将对象的正常VM属性除以1 MB来定义该列的值或表达式。斜杠是 PowerShell 的除法运算符,PowerShell 识别KBMBGBTBPB作为千字节、兆字节、吉字节、太字节和拍字节的代表。

  • 那个除法操作的结果将有一个我们不想看到的十进制部分。-as运算符使我们能够将那个结果的数据类型从浮点值更改为,在这种情况下,一个整数值(由[int]指定)。在执行转换时,shell 将根据需要向上或向下舍入。结果是没有任何分数部分的整数:

    Name            VM(MB)
    ----            ------
    USBAgent          4206
    useractivityd     4236
    UserEventAgent    4235
    usernoted         4242
    ViewBridgeAuxil   4233
    VTDecoderXPCSer   4234
    VTDecoderXPCSer   4234
    WiFiAgent         4255
    WiFiVelocityAge   4232
    XprotectService   4244
    

我们向你展示这个小小的除法和转换技巧,因为它在创建更美观的输出时可能很有用。在这本书中,我们不会花太多时间在这些操作上(尽管我们会告诉你*用于乘法,而且正如你所预期的那样,+-分别用于加法和减法)。

除此之外

尝试重复这个例子:

Get-Process |
Format-Table Name,
@{name='VM(MB)';expression={$_.VM / 1MB -as [int]}} -AutoSize

但这次不要在一行中输入所有内容。按照书中所示,在总共三行中输入。你会注意到在输入以管道字符结尾的第一行之后,PowerShell 会改变其提示符。这是因为你以管道结束了 shell,shell 知道还有更多的命令要来。如果你没有正确关闭所有花括号、大括号、引号和括号就按 Enter 键,shell 会进入相同的“等待你完成”模式。

如果你不是故意进入扩展输入模式,请按 Ctrl-C 来终止,然后重新开始。在这种情况下,你可以输入文本的第二行并按 Enter 键,然后输入第三行并按 Enter 键。在这个模式下,你将不得不在空白行上按最后一次 Enter 键,以告诉 shell 你已经完成。当你这样做时,它将像在单行上输入一样执行命令。

与只能接受NameExpression键(尽管它们也会接受NLLabel作为Name,以及接受E作为Expression)的Select-Object不同,Format-命令可以处理额外的键,这些键旨在控制视觉显示。这些额外的键与Format-Table一起最有用:

  • FormatString 指定一个格式化代码,导致数据根据指定的格式显示。这主要用于数值和日期数据。请访问格式化类型的文档 mng.bz/XWy1,以查看标准数值和日期格式以及自定义数值和日期格式的可用代码。

  • Width 指定所需的列宽度。

  • Alignment 指定所需的列对齐方式,可以是 LeftRight

使用这些额外的键可以更容易地实现前面的示例结果,甚至可以改进它们:

Get-Process |
➥ Format-Table Name,
➥ @{name='VM(MB)';expression={$_.VM};formatstring='F2';align='right'} 
➥ -AutoSize

现在我们不需要进行除法操作,因为 PowerShell 会将数字格式化为具有两位小数的定点值,并将结果右对齐。

11.7 输出到:文件或主机

一旦格式化完成,您必须决定它将去往何处。如果命令行以 Format- cmdlet 结尾,则 Format- cmdlet 创建的格式化指令将发送到 Out-Default,它将它们转发到 Out-Host,在屏幕上显示:

Get-ChildItem | Format-Wide

您还可以手动将格式化指令传递给 Out-Host,这会完成完全相同的事情:

Get-ChildItem | Format-Wide | Out-Host

或者,您可以将格式化指令通过管道传递给 Out-File,以将格式化后的输出直接指向文件。正如您将在第 11.9 节中读到的,命令行中只能有一个 Out- cmdlet 随后跟一个 Format- cmdlet。

请记住,Out-File 默认使用特定的字符宽度进行输出,这意味着文本文件可能看起来与屏幕显示不同。该 cmdlet 有一个 -Width 参数,允许您根据需要更改输出宽度,以适应更宽的表格。

11.8 另一种输出:GridViews

在 Windows PowerShell 的早期版本中,有一个名为 Out-GridView 的内置 cmdlet,它提供了一种有用的输出形式——图形用户界面(GUI)。对于 PowerShell 6 及以上版本,创建了一个跨平台的版本,但它以模块的形式存在于 PowerShell Gallery 中。您可以通过运行以下命令来安装此 cmdlet:

Install-Module Microsoft.PowerShell.GraphicalTools

注意,Out-GridView 并非技术上的格式化;实际上,Out-GridView 完全绕过了格式化子系统。没有调用 Format- cmdlet,没有生成格式化指令,也没有在控制台窗口中显示文本输出。Out-GridView 不能接收 Format- cmdlet 的输出——它只能接收其他 cmdlet 输出的常规对象。

图 11.2 展示了当我们运行 Get-Process | Out-GridView 命令时会发生什么。

图片

图 11.2 Out-GridView cmdlet 的结果

11.9 常见混淆点

正如我们在本章开头提到的,格式化系统具有大多数会让 PowerShell 新手绊倒的陷阱。他们往往会遇到两个问题,所以我们将尽力帮助您避免这些问题。

11.9.1 总是右对齐格式化

非常重要,你需要记住本章的一个规则:格式正确。你的 Format- 命令应该是在命令行上的最后一项,只有 Out-File 是例外。这个规则的原因是 Format- 命令生成格式化指令,只有 Out- 命令才能正确消费这些指令。如果一个 Format- 命令是命令行上的最后一项,指令将会发送到 Out-Default(它总是在管道的末尾),然后它会将指令转发到 Out-HostOut-Host 很乐意处理格式化指令。尝试运行以下命令来展示这个规则的需求:

Get-History | Format-Table | gm

   TypeName: Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
Name                                    MemberType Definition
----                                    ---------- ----------
Equals                                  Method     bool Equals(System.Object  
                                                 ➥ obj)
GetHashCode                             Method     int GetHashCode()
GetType                                 Method     type GetType()
ToString                                Method     string ToString()
autosizeInfo                            Property   Microsoft.PowerShell.Commands.Internal.Format.AutosizeInfo, 
   ➥ System.Management.Automation, Version=7.0.0.0,...
ClassId2e4f51ef21dd47e99d3c952918aff9cd Property   string 
➥ ClassId2e4f51ef21dd47e99d3c952918aff9cd {get;}
groupingEntry                           Property   Microsoft.PowerShell.Commands.Internal.Format.GroupingEntry, 
   ➥ System.Management.Automation, Version=7.0.0.0...
pageFooterEntry                         Property   Microsoft.PowerShell.Commands.Internal.Format.PageFooterEntry, 
   ➥ System.Management.Automation, Version=7.0.0...
pageHeaderEntry                         Property   Microsoft.PowerShell.Commands.Internal.Format.PageHeaderEntry, 
   ➥ System.Management.Automation, Version=7.0.0...
shapeInfo                               Property   Microsoft.PowerShell.Commands.Internal.Format.ShapeInfo, 
   ➥ System.Management.Automation, Version=7.0.0.0, Cu...

   TypeName: Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
Name                                    MemberType Definition
----                                    ---------- ----------
Equals                                  Method     bool Equals(System.Object 
                                                 ➥ obj)
GetHashCode                             Method     int GetHashCode()
GetType                                 Method     type GetType()
ToString                                Method     string ToString()
ClassId2e4f51ef21dd47e99d3c952918aff9cd Property   string ClassId2e4f51ef21dd47e99d3c952918aff9cd {get;}
groupingEntry                           Property   Microsoft.PowerShell.Commands.Internal.Format.GroupingEntry, 
   ➥ System.Management.Automation, Version=7.0.0.0...
shapeInfo                               Property   
➥ Microsoft.PowerShell.Commands.Internal.Format.ShapeInfo, 
➥ System.Management.Automation, Version=7.0.0.0, Cu...

你会注意到 gm 并没有显示关于你的历史对象的信息,因为 Format-Table 命令不会输出历史对象。它消耗了你通过管道传递的历史对象,并输出格式化指令——这是 gm 看到的并报告的内容。现在尝试这个命令:

Get-History | Select-Object Id,Duration,CommandLine | Format-Table | 
ConvertTo-Html | Out-File history.html

在浏览器中打开 history.html,你会看到一些疯狂的结果。你没有将历史对象通过 ConvertTo-Html 进行管道传递;你传递了格式化指令,所以这就是被转换成 HTML 的内容。这说明了为什么如果你使用 Format- 命令,它必须是命令行上的最后一项或者倒数第二项,最后一项命令是 Out-File

还要知道,Out-GridView 对于一个 Out- 命令来说是不寻常的(至少是),它不会接受格式化指令,而只会接受标准对象。尝试运行以下两个命令来查看差异:

PS C:\>Get-Process | Out-GridView
PS C:\>Get-Process | Format-Table | Out-GridView

正是因为这个原因,我们明确指出 Out-File 是唯一应该跟在 Format- 命令后面的命令(技术上,Out-Host 也可以跟在 Format- 命令后面,但这是不必要的,因为以 Format- 命令结束命令行的话,输出还是会到达 Out-Host)。

11.9.2 请一次只处理一种类型的对象

下一个要避免的事情是将多种类型的对象放入管道中。格式化系统会查看管道中的第一个对象,并使用该对象的类型来确定要生成哪种格式。如果管道包含两种或更多种类的对象,输出可能不会总是完整或有用。

例如,运行以下命令:

PS /Users/jamesp/> Get-Process; Get-History

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
...
      0     0.00       1.74       0.25    1244   1 UsageTrackingAg
      0     0.00       0.68       0.19     710   1 USBAgent
      0     0.00       4.12       6.37     544   1 useractivityd
      0     0.00       5.44       8.00     407   1 UserEventAgent
      0     0.00       7.50       3.43     428   1 usernoted
      0     0.00       3.44       8.71     506   1 ViewBridgeAuxil
      0     0.00       5.91       0.08   27019 ...19 VTDecoderXPCSer
      0     0.00       5.92       0.07   89048 ...48 VTDecoderXPCSer
      0     0.00      10.79      50.02     434   1 WiFiAgent
      0     0.00       1.11       0.20    1242   1 WiFiVelocityAge
      0     0.00      10.28       4.30   20921 ...21 XprotectService

Id                 : 1
CommandLine        : Update-TypeData -Path /tmp/Uri.Types.ps1xml
ExecutionStatus    : Completed
StartExecutionTime : 9/21/2019 12:20:03 PM
EndExecutionTime   : 9/21/2019 12:20:03 PM
Duration           : 00:00:00.0688690

Id                 : 2
CommandLine        : Update-TypeData -Path /tmp/Uri.Types.ps1xml
ExecutionStatus    : Completed
StartExecutionTime : 9/21/2019 12:21:07 PM
EndExecutionTime   : 9/21/2019 12:21:07 PM
Duration           : 00:00:00.0125330eyp

那个分号允许我们将两个命令放在同一个命令行上,而不需要将第一个命令的输出通过管道传递给第二个命令。这意味着两个命令都是独立运行的,但它们将输出放入同一个管道中。正如你所看到的,输出一开始是好的,显示了进程对象。但当需要显示历史对象时,输出就崩溃了。PowerShell 并没有设计成能够处理多种类型的对象,并尽可能使结果看起来更吸引人。

如果你想将来自两个(或更多)地方的信息合并成单一形式的输出,你绝对可以,而且你可以以一种格式化系统可以很好地处理的方式做到这一点。但这是一个高级主题,我们在这本书中不会涉及。

除此之外

从技术上讲,格式化系统可以处理多种类型的对象——如果你告诉它如何处理。运行Dir | gm,你会注意到管道中包含DirectoryInfoFileInfo对象(gm没有问题处理包含多种对象的管道,并将显示所有对象的成员信息。)当你单独运行Dir时,输出是完全可以读的。这是因为微软为DirectoryInfoFileInfo对象提供了预定义的定制格式化视图,该视图由Format-Custom cmdlet 处理。

Format-Custom主要用于显示各种预定义的定制视图。技术上你可以创建自己的预定义定制视图,但必要的 XML 语法很复杂,目前没有公开文档,因此定制视图仅限于微软提供的。

尽管如此,微软的定制视图确实得到了很多使用。例如,PowerShell 的帮助信息以对象的形式存储,你在屏幕上看到的格式化帮助文件就是将这些对象输入到定制视图的结果。

11.10 实验室

注意:对于这个实验室,你需要任何运行 PowerShell 7.1 或更高版本的计算机。

看看你是否能完成以下任务:

  1. 显示一个仅包含进程名称、ID 和它们是否响应 Windows(Responding属性包含这些信息)的进程表。表格应尽可能占用较少的水平空间,但不要允许任何信息被截断。

  2. 显示一个包含进程名称和 ID 的进程表。还包括虚拟和物理内存使用情况的列,这些值以兆字节(MB)为单位表示。

  3. 使用Get-Module获取已加载模块的列表。以表格形式格式化输出,按此顺序包括模块名称和版本。列标题必须是ModuleNameModuleVersion

  4. 使用Get-AzStorageAccountGet-AzStorageContainer显示所有存储容器,以便为可公开访问的存储容器和不可公开访问的存储容器分别显示一个表格。(提示:管道是你的朋友……使用-GroupBy参数。)

  5. 显示主目录中所有目录的四列宽列表。

  6. 创建一个格式化的列表,列出$pshome中的所有.dll 文件,显示名称、版本信息和文件大小。PowerShell 使用Length属性,但为了更清晰,你的输出应显示Size

11.11 实验室答案

  1. Get-Process | Format-Table Name,ID,Responding -Wrap

  2. Get-Process | Format-Table Name,ID,

    @{l='VirtualMB';e={$_.vm/1MB}},

    @{l='PhysicalMB';e={$_.workingset/1MB}}

  3. Get-Module| Format-Table @{l='ModuleName';e={$_.Name }},

    @{l='ModuleVersion';e={$_.Version}}

  4. 获取-AzStorageAccount | 获取-AzStorageContainer | ft -GroupBy 公共访问组

  5. gci ~ -Directory | 格式化宽 -列 4

  6. gci $pshome/*.dll |

    格式化列表名称,版本信息,@{名称="大小";表达式={$_.length}}

11.12 进一步探索

这正是尝试格式化系统的完美时机。尝试使用三个主要的 Format- 命令来创建不同形式的输出。在接下来的章节中,实验室通常会要求你使用特定的格式化,所以你不妨用这些命令磨练你的技能,并开始记忆本章中涵盖的更常用参数。

12 过滤和比较

到目前为止,你一直在处理 shell 提供的任何输出:所有进程、文件系统对象和各种 Azure 命令。但这类输出并不总是你想要的。通常,你希望将结果缩小到几个特定感兴趣的项目,例如获取匹配特定模式的进程或文件。这就是你将在本章中学到的内容。

12.1 让 shell 只给你你需要的内容

shell 提供了两种广泛的结果缩小模型,它们都被称为 过滤。在第一种模型中,你试图指示为你检索信息的 cmdlet 只检索你指定的内容。在第二种模型(在第 12.5 节中讨论),它采用迭代方法,你接受 cmdlet 提供的所有内容,并使用第二个 cmdlet 过滤掉你不需要的内容。

理想情况下,你应尽可能多地使用我们称之为 过滤左侧 的第一种模型。这可能只是告诉 cmdlet 你想要什么。例如,使用 Get-Process,你可以告诉它你想要哪些进程名称:

Get-Process -Name p*,*s*

但如果你想让 Get-Process 只返回内存超过 1 GB 的进程,而不考虑它们的名称,你不能告诉 cmdlet 为你完成这个任务,因为它没有提供任何参数来指定该信息。

同样,如果你使用的是 Get-ChildItem,它包括 -Path 参数,支持通配符。虽然你可以获取所有文件并使用 Where-Object 进行过滤,但我们不推荐这样做。再次强调,这种技术之所以理想,是因为 cmdlet 只需要检索匹配的对象。我们称之为 过滤左侧 技术或称为 早期过滤 技术。

12.2 过滤左侧

过滤左侧 意味着尽可能将过滤条件放在命令行的左侧或开头。你越早过滤掉不需要的对象,剩余的命令行上的 cmdlet 就需要做的工作就越少,并且可能需要传输到计算机的网络上的不必要信息就越少。

过滤左侧技术的缺点是每个单独的 cmdlet 都可以实现自己的指定过滤方式,并且每个 cmdlet 在执行过滤方面的能力都不同。例如,使用 Get-Process,你只能根据进程的 NameId 属性进行过滤。

当你无法让 cmdlet 完成所有需要的过滤时,你可以转向一个名为 Where-Object 的 PowerShell Core cmdlet(别名为 where)。它使用通用语法,你可以在将对象检索并放入管道后使用它来过滤任何类型的对象。

要使用 Where-Object,你需要学习如何告诉 shell 你想要过滤的内容,这涉及到使用 shell 的比较运算符。

12.3 使用比较运算符

在计算机中,一个 比较 总是涉及两个对象或值,并测试它们之间的关系。你可能正在测试它们是否相等,或者一个是否大于另一个,或者它们是否匹配某种文本模式。你通过使用 比较运算符 来表示你想要测试的关系类型。简单操作中的测试结果会产生一个布尔值:TrueFalse。换句话说,测试的关系要么是你指定的那样,要么不是。

PowerShell 使用以下比较运算符。请注意,当比较文本字符串时,这些运算符不是大小写敏感的;大写字母被视为与小写字母相等:

  • -eq—相等,例如 5 -eq 5(这是 True)或 "hello" -eq "help"(这是 False

  • -ne—不等于,例如 10 -ne 5(这是 True)或 "help" -ne "help"(这是 False,因为实际上它们是相等的,而我们正在测试它们是否不相等)

  • -ge-le—大于或等于,以及小于或等于,例如 10 -ge 5(这是 True)或 (Get-Date) -le '2020-12-02'(这取决于你何时运行它,并展示了如何比较日期)

  • -gt-lt—大于和小于,例如 10 -lt 10(这是 False)或 100 -gt 10(这是 True

对于字符串比较,如果需要,你也可以使用一组单独的大小写敏感运算符:-ceq-cne-cgt-clt-cge-cle

如果你想要同时比较多个事物,你可以使用逻辑运算符 -and-or。每个运算符都包含其一边的一个子表达式,我们通常将它们括起来以使行更容易阅读:

  • (5 -gt 10) -and (10 -gt 100)False,因为一个或两个子表达式是 False

  • (5 -gt 10) -or (10 -lt 100)True,因为至少有一个子表达式是 True

此外,逻辑 -not 运算符会反转 TrueFalse。当你处理一个已经包含 TrueFalse 的变量或属性,并想要测试相反条件时,这可能很有用。例如,如果你想测试一个进程是否没有响应,你可以这样做(你将使用 $_ 作为进程对象的占位符):

$_.Responding -eq $False

PowerShell 定义了 $False$True 来表示 FalseTrue 布尔值。另一种写这种比较的方法如下:

-not $_.Responding

因为 Responding 通常包含 TrueFalse,所以 -notFalse 反转为 True。如果进程没有响应(意味着 RespondingFalse),你的比较将返回 True,表示进程“没有响应”。我们更喜欢第二种技术,因为它在英语中读起来更像我们正在测试的内容:“我想看看进程是否没有响应。”你有时会看到 -not 运算符被缩写为感叹号 (!)。

当你需要比较文本字符串时,一些其他的比较运算符也很有用:

  • -like——接受*, ?,[]作为通配符,因此你可以比较以查看"Hello" -like "*ll*"(这将返回True)。相反的是-notlike,两者都不区分大小写;使用-clike-cnotlike进行大小写敏感的比较。你可以在about_Wildcards帮助文件中找到其他可用的通配符。

  • -match——在文本字符串和正则表达式模式之间进行比较。它的逻辑对立面是-notmatch,正如你所期望的,-cmatch-cnotmatch提供了大小写敏感的版本。正则表达式将在本书的后续章节中介绍。

关于壳(shell)的整洁之处在于,你几乎可以在命令行中直接运行所有这些测试(例外是使用$_占位符的测试——它单独使用时不会工作,但你在下一节中会看到它在哪里会工作)。

现在尝试一下 好吧,尝试这些比较中的任何一个——或者全部——。在一行上输入它们——例如,5 -eq 5——按 Enter 键,看看你得到什么。

你可以在about_Comparison_Operators帮助文件中找到其他可用的比较运算符,你将在第二十五章中了解其中的一些。

12.4 从管道中过滤对象

一旦你编写了一个比较,你将在哪里使用它?嗯,你可以使用 shell 的通用过滤 cmdlet,Where-Object

例如,你想删除所有进程,但保留使用超过 100 MB 内存(WorkingSet)的进程吗?

Get-Process | Where-Object -FilterScript {$_.WorkingSet -gt 100MB} 

-FilterScript参数是位置参数,这意味着你通常会看到它没有指定就输入:

Get-Process | Where-Object {$_.WorkingSet -gt 100MB} 

如果你习惯于大声朗读这些内容,听起来是有道理的:“WorkingSet大于 100 MB 的地方。”这是它的工作原理:当你将对象通过管道传递给Where-Object时,它会使用其过滤器检查每一个对象。它一次将一个对象放入$_占位符中,然后运行比较以查看它是True还是False。如果是False,对象将从管道中丢弃。如果比较是True,对象将从Where-Object中流出,传递到管道中的下一个 cmdlet。在这种情况下,下一个 cmdlet 是Out-Default,它总是位于管道的末尾(正如我们在第十一章中讨论的那样),并且它启动了格式化过程以显示你的输出。

那个$_占位符是一个特殊的实体:你之前已经见过它被使用过(在第十章中),你还会在另一个或两个上下文中看到它。你只能在使用 PowerShell 查找它的特定位置使用此占位符,这恰好是那些位置之一。正如你在第十章中学到的,点号告诉 shell 你并不是在比较整个对象,而是在比较它的一个属性,WorkingSet

我们希望你能开始看到 Get-Member 的用途。它为你提供了一个快速简单的方式来发现对象属性,这让你可以转过来在比较中使用这些属性,就像这样。始终记住,PowerShell 最终输出中的列标题并不总是反映属性名称。例如,运行 Get-Process,你会看到一个像 PM(MB) 这样的列。运行 Get-Process | Get-Member,你会看到实际的属性名是 PM。这是一个重要的区别:始终通过使用 Get-Member 来验证属性名称;不要使用 Format- 命令。

超越

PowerShell v3 引入了一种新的“简化”语法用于 Where-Object。你只能在执行单个比较时使用它;如果你需要比较多个项目,你仍然必须使用原始语法,这就是你在本节中看到的语法。

人们争论这种简化的语法是否有帮助。它看起来可能像这样:

Get-Process | where WorkingSet -gt 100MB

显然,这更容易阅读:它省略了花括号 {},并且不需要使用看起来尴尬的 $_ 占位符。但这个新语法并不意味着你可以忘记旧语法,你仍然需要它来进行更复杂的比较:

Get-Process | Where-Object {$_.WorkingSet -gt 100MB -and $_.CPU -gt 100}

更重要的是,互联网上有数年的示例,所有这些示例都使用旧语法,这意味着你必须知道它才能使用它们。你还必须知道新语法,因为现在它将开始在开发者的示例中出现。需要知道两套语法并不完全是“简化”,但至少你知道是什么。

12.5 使用迭代命令行模型

现在我们想和你简要地谈谈我们所说的 PowerShell 迭代命令行模型。这个模型背后的想法是,你不需要一次性从头开始构建这些大型、复杂的命令行。从小处着手。

假设你想测量使用虚拟内存最多的 10 个进程的虚拟内存量。但如果 PowerShell 本身是这些进程之一,你不想将其包括在计算中。让我们快速盘点一下你需要做什么:

  1. 获取进程。

  2. 移除所有 PowerShell 相关的内容。

  3. 按虚拟内存对进程进行排序。

  4. 根据排序结果,只保留前 10 个或后 10 个。

  5. 将剩余的虚拟内存加起来。

我们相信你知道如何完成前三个步骤。第四步是通过使用你的老朋友 Select-Object 来实现的。

现在尝试一下。花点时间阅读 Select-Object 的帮助。你能找到任何参数,使你能够只保留集合中的第一个或最后一个对象吗?

我们希望你已经找到了答案。最后,你需要将虚拟内存加起来。这是你需要找到一个新命令的地方,可能通过使用 Get-CommandHelp 进行通配符搜索。你可以尝试使用 Add 关键字,或者 Sum 关键字,甚至 Measure 关键字。

现在尝试一下。看看你是否能找到一个可以测量类似虚拟内存这样的数值属性的命令。使用 HelpGet-Command* 通配符。

当你尝试这些小任务(并且不提前阅读答案)时,你正在将自己变成 PowerShell 专家。一旦你认为你找到了答案,你可能会开始尝试迭代方法。

首先,你需要获取进程。这很简单:

Get-Process

现在尝试一下。在 shell 中跟随并运行这些命令。在每个命令之后,检查输出,看看你是否可以预测出你需要在下一个命令迭代中更改的内容。

接下来,你需要过滤掉你不需要的内容。记住,filter left 意味着你希望将过滤器尽可能靠近命令行的开头。在这种情况下,你将使用 Where-Object 来进行过滤,因为你希望它是管道中的下一个 cmdlet。这不如在第一个 cmdlet 上进行过滤好,但比在管道的后面过滤要好。

在 shell 中,按键盘上的上箭头键来回忆你的上一个命令,然后添加下一个命令:

Get-Process | Where-Object { $_.Name -notlike 'pwsh*' }

你不确定是 pwsh 还是 pwsh.exe,所以你使用通配符比较来覆盖所有可能性。任何不与这些名称相似的进程都将保留在管道中。

运行这个命令来测试它,然后再次按上箭头键来添加下一个部分:

Get-Process | Where-Object { $_.Name -notlike 'pwsh*' } |
Sort-Object VM -Descending

按下 Enter 键让你检查你的工作,上箭头键让你添加下一个部分:

Get-Process | Where-Object  { $_.Name -notlike 'pwsh*' } |
Sort-Object VM -Descending | Select -First 10

如果你按默认的升序排序,你会在添加这部分之前保留 -last 10

Get-Process | Where-Object { $_.Name -notlike 'pwsh*' } |
Sort-Object VM -Descending | Select -First 10 | 
Measure-Object -Property VM -Sum

我们希望你能至少找出那个最后一个 cmdlet 的名称,如果不是这里使用的确切语法。

这种模型——运行命令、检查结果、回忆它,并修改它以进行另一次尝试——是 PowerShell 与更传统的脚本语言区别开来的地方。因为 PowerShell 是一个命令行 shell,你可以立即获得结果,以及如果结果不是你所期望的,可以快速轻松地修改你的命令。你也应该看到,当你将你在本书中到目前为止学到的 cmdlet 中的几个 cmdlet 结合起来时,你拥有的力量。

12.6 常见混淆点

任何时候我们在类中引入 Where-Object,我们通常会遇到两个主要难点。我们试图在前面的讨论中深入探讨这些概念,但如果你有任何疑问,我们现在会澄清。

12.6.1 请左过滤

你希望你的过滤条件尽可能靠近命令行的开头。如果你可以在第一个 cmdlet 中完成所需的过滤,就那样做;如果不能,尝试在第二个 cmdlet 中过滤,以便后续的 cmdlet 有尽可能少的工作要做。

此外,尽量在数据源附近进行过滤。例如,如果你从远程计算机查询进程并需要使用 Where-Object——就像我们在本章的一个示例中所做的那样——考虑使用 PowerShell 远程,以便在远程计算机上进行过滤,而不是将所有对象带到你的计算机上并在那里过滤它们。你将在第十三章处理远程,我们将在那里再次提到在源处过滤的想法。

12.6.2 当 $_ 被允许时

特殊的 $_ 占位符仅在 PowerShell 知道查找它的位置有效。当它有效时,它每次包含从被管道传输到该 cmdlet 的对象中的一个。请记住,管道中的内容可以在管道的各个阶段发生变化,因为各种 cmdlet 执行并产生输出。

还要注意嵌套管道——那些发生在括号命令内部的管道。例如,以下内容可能难以理解:

Get-Process -Name (Get-Content c:\names.txt |
Where-Object -filter { $_ -notlike '*daemon' }) |
Where-Object -filter { $_.WorkingSet -gt 128KB }

让我们一步步来:

  1. 你从 Get-Process 开始,但这不是第一个要执行的命令。由于括号的存在,Get-Content 将首先执行。

  2. Get-Content 正在将输出——由简单的 String 对象组成——传输到 Where-Object。那个 Where-Object 在括号内,在其过滤器中,$_ 代表从 Get-Content 管道传输进来的 String 对象。只有那些不以 daemon 结尾的字符串将被保留并由 Where-Object 输出。

  3. Where-Object 的输出成为括号命令的结果,因为 Where-Object 是括号内的最后一个 cmdlet。因此,所有不以 daemon 结尾的名称都将发送到 Get-Process-Name 参数。

  4. 现在 Get-Process 执行,它产生的 Process 对象将被传输到 Where-Object。那个 Where-Object 实例将每次将一个服务放入其 $_ 占位符中,并且它只保留那些 WorkingSet 属性大于 128KB 的服务。

有时我们觉得眼睛都交叉了,因为有所有的花括号、大括号、句点和括号,但这就是 PowerShell 的工作方式,如果你能训练自己仔细地遍历命令,你就能弄清楚它在做什么。

12.7 实验室

记住,Where-Object 不是过滤的唯一方式,甚至也不是你应该首先考虑的方式。我们保持这一章节简短,以便你有更多时间来实际操作。考虑到 filter left 原则,尝试完成以下任务:

  1. PSReadLine 模块获取命令。

  2. PSReadLine 模块获取使用动词 Get 的命令。

  3. 显示 /usr/bin 下所有大于 5 MB 的文件。

  4. 查找 PowerShell 画廊中以 PS 开头且作者以 Microsoft 开头的所有模块。

  5. 获取当前目录中 LastWriteTime 在上周的文件。(提示:(Get-Date).AddDays(-7)将给出上周的日期。)

  6. 显示所有以名称 pwsh 或名称 bash 运行的进程列表。

12.8 实验答案

  1. Get-Command -Module PSReadLine

  2. Get-Command Get-* -Module PSReadLine

  3. Get-ChildItem /usr/bin/* | Where-Object {$_.length –gt 5MB}

  4. Find-Module -Name PS* | Where-Object {$_.Author -like 'Microsoft*'}

  5. Get-ChildItem | where-object LastWriteTime -ge (get-date).AddDays(-7)

  6. Get-Process -Name pwsh,bash

12.9 进一步探索

熟能生巧,所以尝试过滤你已经学习过的 cmdlet 的输出,例如 Get-ChildItemGet-Process,甚至是 Get-Command。例如,你可能尝试过滤 Get-Command 的输出,只显示 cmdlet。或者使用 Test-Connection 来 ping 几个计算机或网站(如 google.com 或 facebook.com),并只显示未响应的计算机的结果。我们并不是建议你在每种情况下都需要使用 Where-Object,但你应该在适当的时候练习使用它。

13 远程控制:一对一和一对多

让我们来看看 Invoke-ScriptBlock 命令。注意它有一个 -ComputerName 参数。嗯嗯……这难道意味着它也可以在其它主机上运行命令吗?经过一番实验,你会发现这正是它的功能。有多少其他命令有连接到远程机器的能力呢?虽然没有办法获得一个具体的数字来回答这个问题,但确实有很多。

我们意识到的是,PowerShell 的创造者有点懒惰——这其实是个好事。因为他们不想为每个 cmdlet 编写 -HostName 参数,所以他们创建了一个跨 shell 的系统,称为 远程。这个系统使你能够在远程计算机上运行任何 cmdlet。实际上,你甚至可以运行存在于远程计算机上但不存在于你自己的计算机上的命令——这意味着你不必总是在你工作站上安装每个管理 cmdlet。这个远程系统非常强大,并提供了有趣的行政管理能力。

注意:远程是一个庞大而复杂的技术。我们在本章中向您介绍它,并涵盖了您将遇到的 80% 到 90% 的使用场景。但我们无法涵盖所有内容,因此在本章末尾的“进一步探索”部分,我们指向了一个必须的资源,该资源涵盖了远程配置选项。

13.1 远程 PowerShell 的理念

Remote PowerShell 的工作方式与 Telnet 和其他古老的远程控制技术有些相似。当你运行一个命令时,它是在远程计算机上运行的——只有该命令的结果会返回到你的计算机。

13.1.1 Windows 设备上的远程

PowerShell 使用一种名为 Web 服务管理 (WSMan) 的通信协议。WSMan 完全通过 HTTP 或 HTTPS(默认为 HTTP)运行,如果需要的话,可以轻松地通过防火墙(因为每个协议都使用单个端口进行通信)。Microsoft 对 WSMan 的实现形式是一个后台服务,Windows 远程管理 (WinRM)。WinRM 默认安装在 Windows 10 设备和 Server 2012 及以上版本上。默认情况下,这些服务是禁用的,但可以很容易地通过组策略单独或成组启用。

13.1.2 macOS 和 Linux 设备上的远程

如你所猜,WSMan 和 WinRM 是仅适用于 Windows 的服务。因此,为了让 PowerShell 具有远程功能,团队决定最好使用行业标准的安全外壳 (SSH)。SSH 如果需要的话可以轻松地通过防火墙(因为该协议使用单个端口进行通信)并且几十年来一直被 Linux 专业人士使用。Microsoft 已经将 OpenSSH 移植到 Windows 上,因此你甚至可以使用它来远程连接到 Windows。

在 Windows 上通过 SSH 设置 PSRP

你可能想在安装了 PowerShell Core 的任何 Windows 机器上设置 PowerShell 远程协议(PSRP)通过 SSH。我们不会详细介绍如何设置,但 Microsoft 的文档中有可用的说明:mng.bz/laPd

13.1.3 跨平台远程操作

你已经了解到 PowerShell cmdlet 的所有输出都是对象。当你运行远程命令时,其输出对象需要被转换成可以在网络上轻松传输的形式。结果证明,XML 是完成这一任务的一种极好的方式,因此 PowerShell 自动将这些输出对象序列化为 XML。XML 通过网络传输,然后在你的计算机上反序列化,回到 PowerShell 内部你可以操作的对象。序列化和反序列化实际上只是格式转换的一种形式:从对象到 XML(序列化),以及从 XML 到对象(反序列化)。

你为什么应该关心这种输出是如何返回的?因为这些序列化然后反序列化的对象只是某种形式的快照;它们不会持续更新。例如,如果你要获取表示远程计算机上正在运行的过程的对象,你得到的结果将只准确反映那些对象生成时的确切时间点。像内存使用和 CPU 利用率这样的值不会更新以反映随后的条件。此外,你不能告诉反序列化的对象做任何事情——你不能指示它们停止自己,例如。

这些是远程操作的基本限制,但它们并不会阻止你做一些令人惊叹的事情。实际上,你可以告诉远程进程停止自己,但你必须足够聪明。我们将在本章后面展示如何做到这一点。为了使远程操作生效,你需要满足两个基本要求:

  • 你想要发送命令的计算机和你自己的计算机都必须运行 PowerShell v7.1 或更高版本。

  • 理想情况下,两台计算机都需要是同一域的成员,或者信任/被信任的域的成员。在域外使远程操作生效是可能的,但这更复杂,本章中我们不涉及这一点。要了解更多关于这种情况的信息,请打开 PowerShell 并运行help about_remote_troubleshooting

现在试试看 我们希望您能够跟随本章的一些示例。为了参与,理想情况下您将有一个第二台测试计算机(或虚拟机),该计算机与您到目前为止使用的测试计算机位于同一 Active Directory 域中。只要在该第二台计算机上安装了 PowerShell v7.1 或更高版本,您就可以在该计算机上运行任何版本的 Windows。如果您使用了两台 Windows 设备,如果它们属于同一域,这将使您的生活变得更加容易。如果您无法设置额外的计算机或虚拟机,请使用 localhost 来创建到您当前计算机的远程连接。尽管您仍在使用远程操作,但坐在您面前的计算机进行“远程控制”并不那么令人兴奋。

13.2 在 SSH 上设置 PSRP

让我们花些时间在您的环境中设置 SSH。

13.2.1 macOS 和 Linux

在计算机上,确保已安装 SSH 服务器和客户端。在 Ubuntu 上,这些是相应的说明:

sudo apt install openssh-client
sudo apt install openssh-server

对于 macOS,客户端默认已安装。以下是启用服务器的命令:

sudo systemsetup -setremotelogin on

接下来,我们需要安装启用 SSH 上 PSRP 的模块:

Install-Module EnableSSHRemoting

然后,运行命令以启用 SSH 上的 PSRP:

sudo pwsh -c Enable-SSHRemoting

接下来,您需要重新启动 OpenSSH 服务。在 Ubuntu 上,这是重新启动服务的命令:

sudo service sshd restart

在 macOS 上,这是相应的命令:

sudo launchctl stop com.openssh.sshd
sudo launchctl start com.openssh.sshd

13.2.2 在 Windows 上设置 SSH

SSH 也可以在 Windows 桌面和服务器上运行。实际上,如果您真的想禁用 WinRM,您也可以这样做(我们不建议这样做)。如果您在 Windows 设备上使用 SSH 进行远程操作,您可能是在 Linux 或 macOS 设备上远程到或从这些设备远程。

安装 OpenSSH 客户端和服务器:

Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0

Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

这里是 SSH 服务器的初始配置:

Start-Service sshd

Set-Service -Name sshd -StartupType 'Automatic'

确认防火墙规则已配置。它应该由设置自动创建:

Get-NetFirewallRule -Name *ssh*

应该有一个名为 OpenSSH-Server-In-TCP 的防火墙规则,该规则应该被启用。配置并编辑位于目标机器 $env:ProgramData\ssh 上的 sshd_config 文件 (图 13.1)

图 13.1 这是添加 PowerShell 变更后的 sshd_config 文件的外观。

通过移除 # 符号来验证密码认证是否已启用:

PasswordAuthentication yes

添加 PowerShell 的 Subsystem。您可以看到我们正在使用包含空格的文件路径的 8.3 短名称。

Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -NoLogo 
➥ -NoProfile

Windows 中 Program Files 文件夹的 8.3 短名称通常是 Progra~1。但是,您可以使用以下命令来确保这一点:

Get-CimInstance Win32_Directory -Filter 'Name="C:\\Program Files"' | 
➥ Select-Object EightDotThreeFileName

可选的启用密钥认证是

PubkeyAuthentication yes

重新启动 OpenSSH 服务:

Restart-Service sshd

确保保护您的 SSH 服务器

您应该研究当前用于保护 OpenSSH 的标准。在撰写本文时,基本做法是仅启用私钥认证。此外,请确保保护您的私钥。以下是针对主要平台如何执行此操作的链接:

macOS: mng.bz/Bxyw

Ubuntu: mng.bz/do9g

13.3 SSH 上 PSRP 概述

让我们谈谈 SSH,因为你将需要配置它才能使用远程功能。再一次,你需要配置 PSRP 通过 SSH——以及 PowerShell 远程——只在这些将 接收 传入命令的计算机上。在我们工作过的多数环境中,管理员已经在每台计算机上启用了远程功能。这样做可以让你在后台远程到客户端桌面和笔记本电脑(这意味着那些计算机的用户不会知道你在这样做),这可以非常有用。

SSH 允许多个子系统注册。这允许不同的协议在同一个端口上工作。当你启用 SSH 远程时,PowerShell 会注册为一个子系统,来自 PSRP 的传入连接会被路由到该子系统。图 13.2 展示了这些部分是如何组合在一起的。

图 13.2 OpenSSH 和 PowerShell 之间的关系

如所示,你可以在系统中拥有数十个甚至数百个 sshd 子系统。每个端点可以指向不同的应用程序。

图 13.2 还展示了 sshd 监听器sshd 作为监听器,坐着等待传入的网络流量——有点像网站服务器在等待传入请求。监听器“监听”在特定的端口和特定的 IP 地址上。

现在试试看。在你的第二台计算机上(或者如果你只有一台计算机,就在那台计算机上)启用远程功能。如果你在启用远程功能时收到错误消息,请停止并找出原因。

13.4 WinRM 概述

让我们谈谈 WinRM,因为你将需要配置它才能使用远程功能。再一次,你需要配置 WinRM——以及 PowerShell 远程——只在这些将 接收 传入命令的计算机上。在我们工作过的多数环境中,管理员已经在基于 Windows 的每台计算机上启用了远程功能(请记住,PowerShell 和远程功能一直支持到 Windows XP)。这样做可以让你在后台远程到客户端桌面和笔记本电脑(这意味着那些计算机的用户不会知道你在这样做),这可以非常有用。

WinRM 并非 PowerShell 独有。微软开始越来越多地使用它来进行管理通信——甚至包括今天使用其他协议的事情。考虑到这一点,微软使 WinRM 能够将流量路由到多个管理应用程序——不仅仅是 PowerShell。WinRM 作为调度器:当流量进入时,WinRM 会决定哪个应用程序需要处理该流量。所有 WinRM 流量都会被标记为接收应用程序的名称,并且这些应用程序必须作为 端点 在 WinRM 上注册,以便 WinRM 代表它们监听传入的流量。这意味着你不仅需要启用 WinRM,还需要告诉 PowerShell 将自己注册为 WinRM 的端点。图 13.3 展示了这些部分是如何组合在一起的。

图 13.3 WinRM、WSMan、端点和 PowerShell 之间的关系

如所示,你可以在系统中拥有数十个甚至数百个 WinRM 端点(PowerShell 称其为会话配置)。每个端点可以指向不同的应用程序,你甚至可以拥有指向相同应用程序但提供不同权限和功能的端点。例如,你可以创建一个只允许一个或两个命令的 PowerShell 端点,并将其提供给环境中特定的用户。我们在这章中不会深入探讨远程操作,但在本书的后续章节中会进行讨论。

图 13.3 还说明了 WinRM监听器,在图中是 HTTP 类型的。监听器代表 WinRM 静候传入的网络流量——有点像网站服务器在等待传入请求。监听器“监听”在特定的端口和特定的 IP 地址上,尽管由Enable-PSRemoting创建的默认监听器会在所有本地 IP 地址上监听。

监听器连接到定义的端点。创建端点的一种方法是在管理员权限下打开 PowerShell 的一个副本——确保你以管理员身份运行它——并运行Enable-PSRemoting命令。你有时可能会看到对另一个名为Set-WSManQuickConfig的命令的引用。你不需要运行那个命令;Enable-PSRemoting会为你调用它,并且Enable-PSRemoting会执行一些额外的步骤,这些步骤对于远程操作能够启动和运行是必要的。总的来说,该命令将启动 WinRM 服务,将其配置为自动启动,将 PowerShell 注册为端点,甚至设置 Windows 防火墙异常以允许传入的 WinRM 流量。

现在尝试一下 前往你的第二台计算机(或者如果你只有一台要工作的话,就是第一台)上启用远程操作。如果你在 Windows 设备上,确保以管理员身份运行 PowerShell(窗口标题栏应显示为管理员)。如果不是,关闭外壳,在开始菜单中右键单击 PowerShell 图标,并从上下文菜单中选择以管理员身份运行。

你最可能遇到的错误是“由于此机器上的网络连接类型之一设置为公共,WinRM 防火墙异常将无法工作。”任何设置为Public的网络连接都不能有 Windows 防火墙异常,所以当Enable-PSRemoting尝试创建一个时,它会失败。唯一的解决方案是进入 Windows 并修改网络适配器设置,将你所在的网络设置为工作或家庭。但如果你连接到公共网络(例如,公共无线热点),请不要这样做,因为你将关闭一些宝贵的安全保护措施。

注意 你在服务器操作系统上不必过多担心 PowerShell 远程操作和公共网络,因为它们在操作系统上没有相同的限制。

如果您对不得不跑遍每台计算机以启用远程操作不感兴趣,请不要担心:您也可以使用组策略对象(GPO)来完成此操作。必要的 GPO 设置已内置到您的域控制器中(您可以从 www.microsoft.com/en-us/download 下载 ADM 模板,以将这些 GPO 设置添加到较旧域的域控制器)。打开一个 GPO,然后在计算机配置 > 管理模板 > Windows 组件下查找。在列表底部附近,您将找到远程外壳和 Windows 远程管理。现在,我们假设您将在您想要配置的计算机上运行 Enable-PSRemoting,因为到目前为止,您可能只是在玩一个或两个虚拟机。

注意 PowerShell 的 about_remote_troubleshooting 帮助主题提供了更多关于使用 GPO 的信息。在该帮助主题中查找“如何在企业中启用远程操作”和“如何使用组策略启用监听器”部分。

13.5 使用 Enter-PSSession 和 Exit-PSSession 进行一对一远程操作

PowerShell 以两种不同的方式使用远程操作。第一种是一对一,或 1:1,远程操作。第二种是一对多,或 1:N,远程操作(您将在下一节中看到)。在一对一远程操作中,您正在访问单个远程计算机上的命令提示符。您运行的任何命令都将直接在该计算机上运行,您将在命令窗口中看到结果。这与使用 SSH 或远程桌面连接有些类似,但您被限制在 Windows PowerShell 的命令行环境中。这种远程操作也只使用远程桌面所需资源的一小部分,因此对您的服务器造成的开销要小得多。

在我们能够连接到远程计算机之前,我们需要您了解 -hostname-computername 参数之间的区别:

  • -hostname—使用此选项使用 SSH。

  • -computername—使用此选项通过 WinRM 进行连接。

PowerShell 无法知道您正在尝试使用哪种协议,因此您必须告诉它。要建立与远程计算机的一对一连接,请运行以下命令:

Enter-PSSession -HostName Ubuntu1 -UserName tylerl
Enter-PSSession -ComputerName SRV2 -UserName contoso\tylerl

或者,您可以使用以下语法:

Enter-PSSession -HostName tylerl@Ubuntu1

(您需要提供正确的计算机名,而不是 SRV2Ubuntu1。)

假设您已在远程计算机上启用了远程操作,并且您都在同一个域中,并且您的网络运行正常,您应该能够建立连接。PowerShell 会通过更改命令提示符来通知您已成功连接:

[Ubuntu1] PS /home/tylerl>

[SRV2] PS C:\>

命令提示符会告诉您,您正在进行的所有操作都在Ubunut1(或您连接到的任何服务器)上。您可以运行您喜欢的任何命令。您甚至可以导入模块。

现在尝试:尝试创建到您的第二台计算机或虚拟机的远程连接。如果您还没有这样做,您在尝试连接之前还需要在该计算机上启用远程操作。请注意,您需要知道主机名或 IP 地址。

你在远程计算机上运行的任何命令都将使用你用于认证的凭据执行,所以你将能够做你通常有权限做的事情。这就像你登录到该计算机的控制台并直接使用其 PowerShell 版本一样。

即使你在远程计算机上有一个 PowerShell 配置文件脚本,当你使用远程连接时它也不会运行。我们还没有完全介绍配置文件脚本(它们在第二十六章中),但简单来说,它们是一组在每次打开外壳时自动运行的命令。人们使用它们来自动加载外壳扩展和模块等。当你远程连接到计算机时,这种情况不会发生,所以请注意这一点。

除了这个相当小的警告之外,你应该没问题。但是等等——当你完成在远程计算机上运行命令后,你该怎么办?许多 PowerShell 命令都是成对的,一个命令执行某个操作,另一个执行相反的操作。在这种情况下,如果 Enter-PSSession 让你 进入 远程计算机,你能猜到什么会让你 离开 远程计算机吗?如果你猜对了 Exit-PSSession,给自己发个奖。该命令不需要任何参数;运行它,你的外壳提示符将恢复正常,远程连接将自动关闭。

现在试试看:如果你创建了一个远程会话,请退出它。我们现在暂时完成了它。

如果你忘记运行 Exit-PSSession 而是关闭 PowerShell 窗口怎么办?别担心。PowerShell 足够智能,能够弄清楚你做了什么,远程连接会自动关闭。

我们确实有一个警告要提供:当你远程连接到计算机时,除非你完全理解你在做什么,否则不要从该计算机上运行 Enter-PSSession。假设你在运行 Ubuntu 的计算机 A 上工作,并远程连接到 SRV2。然后,在 PowerShell 提示符下,你运行以下命令:

[Ubuntu1] PS /home/tylerl>
Enter-PSSession -computername SRV2 -UserName contsco\tylerl

这会导致 Ubuntu1 维持与 SRV2 的开放连接,这可能会开始创建一个难以追踪的 远程链,并且会给你的服务器带来不必要的开销。有时你可能 必须这样做——我们主要考虑的是当 SRV2 这样的计算机位于防火墙后面,你不能直接访问它时,你使用 SRV1 作为中间人跳转到 Server-DC4。但是,作为一般规则,尽量避免远程链。PowerShell 团队有一篇关于在 PowerShell 远程中实现第二次跳转的优秀文章,请参阅 mng.bz/AxXe

警告:有些人将远程链称为 第二次跳转,这是 PowerShell 中的一个主要陷阱。我们提供一个提示:如果 PowerShell 提示符显示计算机名,那么你就完成了。在你退出该会话并“回来”到你的计算机之前,你无法发出任何更多的远程控制命令。

当你使用这种一对一远程时,你不需要担心对象被序列化和反序列化。对你来说,你就像是在远程计算机的控制台上直接输入。如果你检索一个进程并将其管道传输到 Stop-Process,它将停止运行,就像你预期的那样。

13.6 使用 Invoke-ScriptBlock 进行一对多远程

下一个技巧——坦白说,这是 PowerShell 中最酷的事情之一——是同时向 多台远程计算机发送命令。没错,这就是全规模的分布式计算。每台计算机将独立执行命令并将结果发送回您。这一切都是通过 Invoke-ScriptBlock 命令来完成的,这被称为 一对多,或 1:N 远程。命令看起来是这样的:

Invoke-ScriptBlock -ComputerName SRV2,DC3,SRV4
-ScriptBlock { Get-Process pwsh } -UserName tylerl

现在试试看。运行这个命令。用你的远程计算机(或计算机)的名称替换我们放置的三台服务器名称,以及用户名。

那些花括号 {} 中的所有内容都会被传输到远程计算机——所有三台。默认情况下,PowerShell 一次最多与 32 台计算机通信;如果你指定了超过这个数量,它将排队等待,一旦一台计算机完成,下一台就会开始。如果你有一个很棒的网络和强大的计算机,你可以通过指定 Invoke-ScriptBlock-throttleLimit 参数来提高这个数字。更多信息请阅读命令的帮助。

注意标点符号

我们需要进一步考虑一对多远程示例的语法,因为在这种情况下 PowerShell 的标点符号可能会让人困惑。这种困惑可能会让你在开始自己构建这些命令行时做错事情。

这里有一个例子供你考虑:

Invoke-ScriptBlock -HostName SRV2,DC3,SRV4
-ScriptBlock { Get-Process pwsh  | 
Where-Object {$_.Parent.ProcessName -like '*term*'}} -UserName

在这个例子中有两个命令使用了花括号:Invoke-ScriptBlockWhere-ObjectWhere-Object 完全嵌套在外层花括号中。最外层的一组花括号包含了要发送到远程计算机执行的所有内容:

Get-Process pwsh  | Where-Object {$_.Parent.ProcessName -like '*term*'}

那些命令的嵌套可能会很困难,尤其是在像这本书这样的书中,物理页面宽度使得必须在多行文本中显示命令。

确保你能识别出发送到远程计算机的确切命令,并且理解每个匹配的花括号组的使用。

如果你仔细阅读 Invoke-ScriptBlock 的帮助信息(看我们是如何继续推动这些帮助文件的?),你也会注意到一个参数,允许你指定一个脚本文件,而不是命令。这个参数允许你将整个脚本从你的本地计算机发送到远程计算机——这意味着你可以自动化一些复杂的任务,并让每台计算机完成它自己的部分工作。

现在试试看。确保你能识别出 Invoke-ScriptBlock 帮助中的 -ScriptBlock 参数,并且能找到允许你指定文件路径和名称而不是脚本块的参数。

我们想回到本章开头提到的 -HostName 参数。当我们第一次使用 Invoke-ScriptBlock 时,我们输入了一个以逗号分隔的主机名列表,就像上一个例子中做的那样。但我们处理了很多计算机,我们不想每次都要输入它们。我们为一些常见的计算机类别(如 Web 服务器和域控制器)保留文本文件。每个文本文件每行包含一个计算机名,仅此而已——没有逗号,没有引号,什么都没有。PowerShell 让我们很容易使用这些文件:

Invoke-ScriptBlock -ScriptBlock { dir } 
-HostName (Get-Content webservers.txt) -UserName tylerl

这里的括号迫使 PowerShell 首先执行 Get-Content——就像数学中的括号一样工作。然后,Get-ScriptBlock 的结果被固定到 -HostName 参数中,该参数针对文件中列出的每台计算机工作。

13.7 远程命令与本地命令之间的差异

我们希望通过使用 Invoke-ScriptBlock 运行命令和本地运行相同命令之间的差异,以及远程连接与其他远程连接形式之间的差异来解释这些差异。为了这次讨论,我们将使用此命令作为我们的示例:

Invoke-ScriptBlock -HostName SRV2,DC3,SRV4
-ScriptBlock { Get-Process pwsh -UserName tylerl  | 
Where-Object {$_.Parent.ProcessName -like '*term*'}}

13.7.1 反序列化对象

关于远程连接,还需要注意的一个注意事项是,返回到你的计算机上的对象并不完全可用。在大多数情况下,它们缺少方法,因为它们不再“附加”到“实时”软件上。

例如,在你的本地计算机上运行此命令,你会注意到一个 System .Diagnostics.Process 对象与它关联了许多方法:

PS > Get-Process | Get-Member

   TypeName: System.Diagnostics.Process

Name                       MemberType     Definition
----                       ----------     ----------
Handles                    AliasProperty  Handles = Handlecount
Name                       AliasProperty  Name = ProcessName
NPM                        AliasProperty  NPM = NonpagedSystemMemory...
PM                         AliasProperty  PM = PagedMemorySize64
SI                         AliasProperty  SI = SessionId
VM                         AliasProperty  VM = VirtualMemorySize64
WS                         AliasProperty  WS = WorkingSet64
Parent                     CodeProperty   System.Object Parent{get=G...
Disposed                   Event          System.EventHandler Dispos...
ErrorDataReceived          Event          System.Diagnostics.DataRec...
Exited                     Event          System.EventHandler Exited...
OutputDataReceived         Event          System.Diagnostics.DataRec...
BeginErrorReadLine         Method         void BeginErrorReadLine()
BeginOutputReadLine        Method         void BeginOutputReadLine()
CancelErrorRead            Method         void CancelErrorRead()
CancelOutputRead           Method         void CancelOutputRead()
Close                      Method         void Close()
CloseMainWindow            Method         bool CloseMainWindow()
Dispose                    Method         void Dispose(), void IDisp...
Equals                     Method         bool Equals(System.Object ...
GetHashCode                Method         int GetHashCode()
GetLifetimeService         Method         System.Object GetLifetimeS...
GetType                    Method         type GetType()
InitializeLifetimeService  Method         System.Object InitializeLi...
Kill                       Method         void Kill(), void Kill(boo...
Refresh                    Method         void Refresh()
Start                      Method         bool Start()
ToString                   Method         string ToString()
WaitForExit                Method         void WaitForExit(), bool W...
WaitForInputIdle           Method         bool WaitForInputIdle(), b...
__NounName                 NoteProperty   string __NounName=Process

现在,通过远程连接获取一些相同的对象:

PS > Invoke-ScriptBlock {Get-Process} -HostName localhost -UserName tylerl | 
   ➥ Get-Member

   TypeName: Deserialized.System.Diagnostics.Process

Name                       MemberType   Definition
----                       ----------   ----------
GetType                    Method       type GetType()
ToString                   Method       string ToString(), string To...
Company                    NoteProperty object Company=null
CPU                        NoteProperty object CPU=null
Description                NoteProperty object Description=null
FileVersion                NoteProperty object FileVersion=null
Handles                    NoteProperty int Handles=0
Name                       NoteProperty string Name=
NPM                        NoteProperty long NPM=0
Parent                     NoteProperty object Parent=null
Path                       NoteProperty object Path=null
PM                         NoteProperty long PM=0
Product                    NoteProperty object Product=null
ProductVersion             NoteProperty object ProductVersion=null
PSComputerName             NoteProperty string PSComputerName=localh...
PSShowComputerName         NoteProperty bool PSShowComputerName=True
RunspaceId                 NoteProperty guid RunspaceId=26297051-1cb...
SI                         NoteProperty int SI=53860
VM                         NoteProperty long VM=0
WS                         NoteProperty long WS=0
__NounName                 NoteProperty string __NounName=Process
BasePriority               Property     System.Int32 {get;set;}
Container                  Property      {get;set;}
EnableRaisingEvents        Property     System.Boolean {get;set;}              

方法——除了所有对象都通用的 ToString()GetType() 方法之外——都不存在了。这是一个只读的对象副本;你不能让它执行停止、暂停、恢复等操作。因此,你想要命令执行的结果应该包含在发送到远程计算机的脚本块中;这样,对象仍然是活跃的,并且包含所有的方法。

13.7.2 本地与远程处理

我们将再次引用我们的原始示例:

Invoke-ScriptBlock -HostName SRV2,DC3,SRV4
-ScriptBlock { Get-Process pwsh -UserName tylerl  | 
Where-Object {$_.Parent.ProcessName -like '*term*'}}

这里发生的事情是这样的:

  • 计算机是并行接触的,这意味着命令可以更快地完成。

  • 每台计算机都会查询记录并本地过滤它们 locally。网络中传输的唯一数据是过滤的结果,这意味着只有我们关心的记录会被传输。

  • 在传输之前,每台计算机将其输出序列化为 XML。我们的计算机接收那个 XML 并将其反序列化回类似对象的东西。但它们不是真实的事件日志对象,这可能会限制我们在计算机上对它们能做的事情。

现在,比较一下这个替代方案:

Invoke-ScriptBlock -HostName SRV2,DC3,SRV4
-ScriptBlock { Get-Process pwsh } -UserName tylerl  | 
Where-Object {$_.Parent.ProcessName -like '*term*'}

差异是微妙的。嗯,我们只看到一处差异:我们移动了一个大括号。

在第二个版本中,只有 Get-Process 命令是在远程计算机上执行的。Get-Process 生成的所有结果都被序列化并发送到我们的计算机,在那里它们被反序列化为对象,然后通过管道传递给 Where 并进行过滤。第二个版本的命令效率较低,因为大量的不必要数据被传输到网络上,而且我们的计算机需要过滤来自三台计算机的结果,而不是让这三台计算机为我们过滤它们自己的结果。因此,第二个版本不是一个好主意。

让我们看看另一个命令的两个版本,先从以下内容开始:

Invoke-ScriptBlock -ComputerName SRV2 
-ScriptBlock { Get-Process -name pwsh } -UserName tylerl |
Stop-Process

现在,让我们看看第二个版本:

Invoke-ScriptBlock -ComputerName SRV2
-ScriptBlock { Get-Process -name pwsh } -UserName tylerl |
Stop-Process }

再次强调,这两个版本之间的唯一区别是花括号的位置。但在本例中,第一个版本的命令将无法正常工作。

仔细看:我们正在向远程计算机发送 Get-Process -name pwsh 命令。远程计算机检索指定的进程,将其序列化为 XML 格式,并通过网络发送给我们。我们的计算机接收那个 XML,将其反序列化为对象,然后通过管道传递给 Stop-Process。问题是反序列化的 XML 没有包含足够的信息让我们的计算机意识到该进程来自一个 远程机器。相反,我们的计算机将尝试停止本地运行的 pwsh 进程,而这根本不是我们想要的。

这个故事告诉我们,应该尽可能在远程计算机上完成你的处理工作。你唯一应该期望用 Invoke-ScriptBlock 的结果去做的事情是显示它们或将其存储为报告、数据文件等。我们的命令的第二个版本遵循了这一建议:发送到远程计算机的是 Get-Process -name pwsh | Stop-Process,因此整个命令——获取进程和停止进程——都在远程计算机上执行。因为 Stop-Process 通常不会产生任何输出,所以不会有对象被序列化并发送给我们,因此我们本地控制台将不会显示任何内容。但命令会完成我们想要的事情:在远程计算机上停止 pwsh 进程,而不是在我们的本地机器上。

每次我们使用 Invoke-ScriptBlock 时,我们都会查看其后的命令。如果我们看到用于格式化或导出数据的命令,我们就可以放心,因为用 Invoke-ScriptBlock 的结果来做这些事情是可以的。但如果 Invoke-ScriptBlock 后面跟着的是执行命令——那些开始、停止、设置、更改或执行其他操作的命令——那么我们就退后一步,试图思考我们在做什么。理想情况下,我们希望所有这些操作都在远程计算机上执行,而不是在我们的本地计算机上。

13.8 但是等等,还有更多

之前的示例都使用了临时的远程操作连接,这意味着我们指定了主机名。如果你打算在短时间内多次重新连接到同一台(或几台)计算机,你可以创建可重复使用、持久的连接来代替。我们在第十八章中介绍了这种技术。

我们还应该承认,并不是每家公司都会允许启用 PowerShell 远程操作——至少,不是立即允许。例如,具有极其严格的安全策略的公司可能在所有客户端和服务器计算机上都有防火墙,这将阻止远程操作连接。如果你的公司是这类公司之一,看看是否为 SSH 或 WinRM 设置了例外。我们发现这是一个常见的例外,因为管理员显然需要一些远程连接到服务器。如果允许 SSH 或 WinRM,那么你可以使用 SSH 上的 PowerShell 远程操作。

13.9 常见混淆点

对于初学者使用远程操作,在一天的过程中可能会出现一些常见问题:

  • 远程操作设计为基本可以自动配置。如果所有涉及的计算机都在同一个域中,并且你的用户名相同,通常一切都会运行得很好。如果不是这样,你需要运行 help about_remote_troubleshooting 并深入了解细节。

  • 当你调用一个命令时,你是在要求远程计算机启动 PowerShell,运行你的命令,然后关闭 PowerShell。你在同一远程计算机上调用的下一个命令将从头开始——第一次调用中运行的任何内容都将不再有效。如果你需要运行一系列相关的命令,请将它们全部放入同一个调用中。

13.10 实验室

注意:对于这个实验室,你需要一台运行 PowerShell v7 或更高版本的计算机。理想情况下,你应该有两个在同一网络上的计算机,并且启用了远程操作。

是时候将你关于远程操作的知识与你之前章节中学到的知识结合起来。看看你是否能完成以下任务:

  1. 与远程计算机(或如果你只有一台计算机,则与 localhost)建立一对一的连接。启动你喜欢的文本编辑器。会发生什么?

  2. 使用 Invoke-ScriptBlock 从一台或两台远程计算机(如果你只有一台计算机,则可以使用 localhost 两次)检索当前正在运行的进程列表。将结果格式化为宽列表。(提示:检索结果并在你的计算机上进行格式化是可以的——不要在远程调用的命令中包含 Format- cmdlet。)

  3. 使用 Invoke-ScriptBlock 获取虚拟内存(VM)使用量最高的前 10 个进程列表。如果可能的话,针对一台或两台远程计算机;如果你只有一台计算机,则针对 localhost 两次。

  4. 创建一个包含三个计算机名称的文本文件,每行一个名称。如果你只有一台计算机,可以使用相同的计算机名称或 localhost 重复三次。然后使用 Invoke-ScriptBlock 从主目录(~)检索最新的 10 个文件。

  5. 使用 Invoke-ScriptBlock,查询一个或多个远程计算机以显示 $PSVersionTable 变量中的属性 PSVersion。(提示:这需要你获取一个项目的属性。)

13.11 实验答案

  1. Enter-PSSession Server01

    [Ubuntu1] /home/tylerl> nano

    nano 进程将启动,但无论是本地还是远程都不会有任何交互式进程。实际上,以这种方式运行,提示符不会返回,直到 nano 进程结束——尽管启动它的替代命令是 Start-Process nano

    [SRV2] PS C:\Users\Administrator\Documents> Notepad

    记事本进程将启动,但无论是本地还是远程都不会有任何交互式进程。实际上,以这种方式运行,提示符不会返回,直到记事本进程结束——尽管启动它的替代命令是 Start-Process Notepad

  2. Invoke-ScriptBlock –scriptblock {Get-Process } -HostName

    ➥ Server01,Server02 -UserName yourUser | Format-Wide -Column 4

  3. Invoke-ScriptBlock -scriptblock {get-process | sort VM -Descending |

    ➥ Select-first 10} –HostName Server01,Server02 -UserN

  4. Invoke-ScriptBlock -scriptblock { Get-ChildItem ~/* | Sort-Object

    ➥ -Property LastWriteTime -Descending | Select-Object -First 10}

    ➥ -HostName (Get-Content computers.txt) -UserName yourUser

  5. Invoke-ScriptBlock –scriptblock $ -Server01,Server02 -UserName yourUser

13.12 进一步探索

我们可以关于 PowerShell 的远程处理讲很多——足够让你在午餐时间读上一个月。不幸的是,其中一些比较复杂的部分并没有很好地记录。我们建议访问 PowerShell.org,特别是他们的电子书资源,在那里 Don 和 MVP 同事 Dr. Tobias Weltner 为你准备了一本全面(且免费!)的 PowerShell 远程处理秘密 短小电子书(见 leanpub.com/secretsofpowershellremoting)。指南重新梳理了你在本章中学到的一些基础知识,但它主要关注详细的、分步的说明(带有彩色截图),展示了如何配置各种远程处理场景。指南还深入探讨了协议和故障排除的一些更粗糙的细节,甚至还有一个简短的关于如何与信息安全人员讨论远程处理的章节。指南会定期更新,所以每隔几个月检查一下,确保你有最新的版本。

14 使用后台作业进行多任务处理

每个人总是告诉你要“多任务处理”,对吧?为什么 PowerShell 不能通过一次做几件事来帮助你呢?事实证明,PowerShell 确实可以这样做,特别是对于可能涉及多个目标计算机的长时间运行的任务。在深入本章之前,请确保你已经阅读了第十三章,因为我们将进一步探讨那些远程概念。

注意:在本章中,我们将使用大量的 Az 命令,这需要有效的 Azure 订阅。这些只是我们选择突出显示的示例。

14.1 让 PowerShell 同时执行多项任务

你应该将 PowerShell 视为一个单线程应用程序,这意味着它一次只能做一件事。你输入一个命令,按下 Enter,外壳等待该命令执行。你无法在第一个命令完成之前运行第二个命令。

但凭借其后台作业功能,PowerShell 有能力将命令移动到单独的后台线程或单独的后台 PowerShell 进程。这使命令能够在后台运行,同时你继续使用外壳执行其他任务。在运行命令之前,你必须做出这个决定;按下 Enter 后,你不能决定将长时间运行的命令移入后台。

命令进入后台后,PowerShell 提供了检查其状态、检索任何结果等机制。

14.2 同步与异步

让我们先澄清一些术语。PowerShell 以同步方式运行正常命令,这意味着你按下 Enter 然后等待命令完成。将作业移动到后台允许它异步运行,这意味着你可以在命令完成时继续使用外壳执行其他任务。让我们看看以这两种方式运行命令的一些重要区别:

  • 当你同步运行一个命令时,你可以响应输入请求。当你后台运行命令时,没有机会看到输入请求——实际上,它们会阻止命令运行。

  • 同步命令在出错时会产生错误消息。后台命令会产生错误,但你不会立即看到它们。如果需要,你必须做出安排来捕获它们。(第二十四章讨论了如何做到这一点。)

  • 如果你省略了同步命令上的必需参数,PowerShell 可以提示你输入缺失的信息。在后台命令上,它不能这样做,因此命令将失败。

  • 同步命令的结果一旦可用就开始显示。对于后台命令,你等待命令运行完成,然后检索缓存的成果。

我们通常同步运行命令以测试它们并确保它们正常工作,只有在我们知道它们已经完全调试并且按预期工作后,才在后台运行它们。我们采取这些措施以确保命令可以无问题运行,并且有最大的机会在后台完成。PowerShell 将后台命令称为 作业。你可以通过多种方式创建作业,并且可以使用多个命令来管理它们。

超越

从技术上来说,本章所讨论的工作岗位只是你可能会遇到的一小部分工作。工作(Job)是 PowerShell 的一个扩展点,这意味着任何人(无论是微软内部人员还是第三方)都可以创建其他被称为工作(Job)的东西,这些工作看起来和运作方式与本章所描述的略有不同。当你为了各种目的扩展 shell 时,可能会遇到其他类型的工作。我们希望你能理解这个细节,并知道你在本章所学的内容仅适用于 PowerShell 自带的、常规的工作岗位。

14.3 创建进程工作(Process Job)

我们首先介绍的工作类型可能是最简单的:进程工作(Process Job)。这是一个在机器上的另一个 PowerShell 进程中后台运行的命令。

要启动这些作业之一,你使用 Start-Job 命令。一个 -ScriptBlock 参数允许你指定要运行的命令(或命令序列)。PowerShell 会自动生成一个默认作业名称(Job1Job2 等),或者你可以使用 -Name 参数指定一个自定义作业名称。你不必指定脚本块,也可以指定 -FilePath 参数,让作业执行一个包含多个命令的整个脚本文件。以下是一个简单的示例:

PS /Users/travisp/> start-job -scriptblock { gci }
Id   Name  PSJobTypeName  State    HasMoreData     Location    Command
--   ----  -------------  -----    -----------     --------    -------
1    Job1  BackgroundJob  Running  True            localhost    gci

该命令创建作业对象,正如前面的示例所示,作业立即开始运行。作业还分配了一个顺序作业 ID 号,如表中所示。

该命令还有一个 -WorkingDirectory 参数,允许你更改工作在文件系统中的起始位置。默认情况下,它始终从主目录开始。绝不要在后台作业中假设文件路径:使用绝对路径以确保你可以引用作业命令可能需要的任何文件,或者使用 -WorkingDirectory 参数。以下是一个示例:

PS /Users/travisp/> start-job -scriptblock { gci } -WorkingDirectory /tmp
Id   Name  PSJobTypeName  State    HasMoreData     Location    Command
--   ----  -------------  -----    -----------     --------    -------
3    Job3  BackgroundJob  Running  True            localhost    gci

留意细节的读者会注意到,我们创建的第一个作业被命名为 Job1 并分配了 ID 1,但第二个作业是 Job3,ID 为 3。实际上,每个作业至少有一个 子作业,第一个子作业(Job1 的子作业)被命名为 Job2 并分配了 ID 2。我们将在本章后面讨论子作业。

这里有一个需要注意的事项:尽管进程工作在本地运行,但它们确实需要启用 PowerShell 远程,这在第十三章中已经介绍过。

14.4 创建线程工作(Thread Job)

我们还想讨论 PowerShell 中作为其一部分提供的第二种作业类型。它被称为 线程作业。线程作业不会在完全不同的 PowerShell 进程中运行,而是在 同一 进程中启动另一个线程。以下是一个示例:

PS /Users/travisp/> start-threadjob -scriptblock { gci }
Id   Name  PSJobTypeName  State    HasMoreData     Location    Command
--   ----  -------------  -----    -----------     --------    -------
1    Job1  ThreadJob      Running  False           PowerShell   gci

看起来与上一个作业输出非常相似,对吧?只有两个区别——PSJobTypeName,它是 ThreadJob,以及 Location,它是 PowerShell。这告诉我们这个作业是在我们目前正在使用的进程中运行的,但在不同的线程中。

由于启动新线程的开销比启动新进程快得多,因此线程作业非常适合短期脚本和您希望快速启动并在后台运行的命令。相反,您可以使用进程作业在您的机器上运行长时间运行的脚本。

注意:尽管线程作业启动得更快,但请记住,在进程开始变慢之前,一个进程只能同时运行这么多线程。PowerShell 内置了一个“节流限制”为 10,以帮助您防止 PowerShell 过度负载。这意味着一次只能运行 10 个线程作业。如果您想提高限制,可以这样做。只需指定 -ThrottleLimit 参数并传递您想要使用的新限制。如果您一次启动 50、100 或 200 个线程作业,您最终会看到收益递减。请记住这一点。

14.5 作为作业的远程

让我们回顾一下您可以使用来创建新作业的最终技术:PowerShell 的远程功能,您在第十三章中已经学过。有一个重要的区别:您在 -scriptblock(或 -command,它是同一参数的别名)中指定的任何命令都将并行传输到您指定的每台计算机。一次最多可以联系 32 台计算机(除非您修改 -throttleLimit 参数以允许更多或更少的计算机),因此如果您指定了超过 32 台计算机的名称,只有前 32 台将启动。其余的将在第一组开始完成后启动,并且顶级作业将在所有计算机完成后显示完成状态。

与启动作业的其他两种方式不同,这种技术要求您在每个目标计算机上安装 PowerShell v6 或更高版本,并在每个目标计算机上的 PowerShell 中启用 SSH 远程。因为命令在每台远程计算机上物理执行,所以您正在分配计算工作负载,这可以帮助提高复杂或长时间运行的命令的性能。结果将返回到您的计算机,并在您准备好查看之前与作业一起存储。

在以下示例中,您还将看到 -JobName 参数,它允许您指定除无聊的默认值以外的作业名称:

PS C:\> invoke-command -command { get-process } 
-hostname (get-content .\allservers.txt ) 
-asjob -jobname MyRemoteJob
WARNING: column "Command" does not fit into the display and was removed.
Id              Name            State      HasMoreData     Location
--              ----            -----      -----------     --------
8               MyRemoteJob     Running    True            server-r2,lo...

14.6 野外的作业

我们想用这个部分来展示一个 PowerShell 模块,该模块公开了自己的 PSJobs,这样您可以在 PowerShell 的旅程中寻找这种模式。以命令 New-AzVm 为例:

PS /Users/travisp/> gcm New-AzVM -Syntax

New-AzVM -Name <string> -Credential <pscredential> [-ResourceGroupName <string>]
 [-Location <string>] [-Zone <string[]>] [-VirtualNetworkName <string>] [-AddressPrefix <string>]
 [-SubnetName <string>] [-SubnetAddressPrefix <string>] [-PublicIpAddressName <string>]
 [-DomainNameLabel <string>] [-AllocationMethod <string>] [-SecurityGroupName <string>]
 [-OpenPorts <int[]>] [-Image <string>] [-Size <string>] [-AvailabilitySetName <string>]
 [-SystemAssignedIdentity] [-UserAssignedIdentity <string>] [-AsJob] [-DataDiskSizeInGb <int[]>]
 [-EnableUltraSSD] [-ProximityPlacementGroup <string>] [-HostId <string>]
 [-DefaultProfile <IAzureContextContainer>] [-WhatIf] [-Confirm] [<CommonParameters>]

New-AzVM [-ResourceGroupName] <string> [-Location] <string> [-VM] <PSVirtualMachine>
 [[-Zone] <string[]>] [-DisableBginfoExtension] [-Tag <hashtable>] [-LicenseType <string>]
 [-AsJob] [-DefaultProfile <IAzureContextContainer>] [-WhatIf] [-Confirm] [<CommonParameters>]

New-AzVM -Name <string> -DiskFile <string> [-ResourceGroupName <string>]
 [-Location <string>] [-VirtualNetworkName <string>] [-AddressPrefix <string>]
 [-SubnetName <string>] [-SubnetAddressPrefix <string>] [-PublicIpAddressName <string>]
 [-DomainNameLabel <string>] [-AllocationMethod <string>] [-SecurityGroupName <string>]
 [-OpenPorts <int[]>] [-Linux] [-Size <string>] [-AvailabilitySetName <string>]
 [-SystemAssignedIdentity] [-UserAssignedIdentity <string>] [-AsJob]
 [-DataDiskSizeInGb <int[]>] [-EnableUltraSSD] [-ProximityPlacementGroup <string>]
 [-HostId <string>] [-DefaultProfile <IAzureContextContainer>] [-WhatIf] [-Confirm] [<CommonParameters>]

注意到一个熟悉的参数吗?-AsJob!让我们看看它在命令中做了什么:

PS /Users/travisp/> Get-Help New-AzVM -Parameter AsJob

-AsJob <System.Management.Automation.SwitchParameter>
    Run cmdlet in the background and return a Job to track progress.

    Required?                    false
    Position?                    named
    Default value                False
    Accept pipeline input?       False
    Accept wildcard characters?  false

此参数告诉New-AzVM返回一个Job。如果我们执行此命令,并在为虚拟机输入用户名和密码后,我们会看到我们得到了一个Job

PS /Users/travisp/> New-AzVm -Name myawesomevm -Image UbuntuLTS  -AsJob

cmdlet New-AzVM at command pipeline position 1
Supply values for the following parameters:
Credential
User: azureuser
Password for user azureuser: ***********

Id Name            PSJobTypeName   State   HasMoreData  Location  Command
-- ----            -------------   -----   -----------  --------  -------
8  Long Running O... AzureLongRunni... Running True     localhost New-AzVM 

这之所以如此出色,是因为您可以像管理从Start-JobStart-ThreadJob返回的作业一样管理这些作业。您将在稍后看到我们如何管理作业,但这是一个自定义作业可能出现的示例。寻找-AsJob参数!

14.7 获取作业结果

在启动作业后,您可能首先想要做的是检查作业是否已完成。Get-Job命令检索系统当前定义的每个作业,并显示每个作业的状态:

PS /Users/travisp/> get-job
Id Name            PSJobTypeName   State     HasMoreData  Location  Command
-- ----            -------------   -----     -----------  --------  -------
1  Job1            BackgroundJob   Completed True         localhost  gci
3  Job3            BackgroundJob   Completed True         localhost  gci
5  Job5            ThreadJob       Completed True         PowerShell gci
8  Job8            BackgroundJob   Completed True         server-r2, lo...
11 MyRemoteJob     BackgroundJob   Completed True         server-r2, lo...
13 Long Running O... AzureLongRunni... Running   True     localhost  New-AzVM

您也可以通过其 ID 或其名称检索特定作业。我们建议您这样做,并将结果管道到Format-List *,因为您已经收集了一些有价值的信息:

PS /Users/travisp/> get-job -id 1 | format-list *
State         : Completed
HasMoreData   : True
StatusMessage :
Location      : localhost
Command       :  gci
JobStateInfo  : Completed
Finished      : System.Threading.ManualResetEvent
InstanceId    : e1ddde9e-81e7-4b18-93c4-4c1d2a5c372c
Id            : 1
Name          : Job1
ChildJobs     : {Job2}
PSBeginTime   : 12/12/2019 7:18:58 PM
PSEndTime     : 12/12/2019 7:18:58 PM
PSJobTypeName : BackgroundJob
Output        : {}
Error         : {}
Progress      : {}
Verbose       : {}
Debug         : {}
Warning       : {}
Information   : {}

现在尝试一下 如果您正在跟随,请记住,您的作业 ID 和名称可能与我们的不同。关注Get-Job的输出以检索您的作业 ID 和名称,并在示例中替换您的信息。此外,请记住,Microsoft 在过去的几个 PowerShell 版本中扩展了作业对象,因此您查看所有属性时的输出可能不同。

ChildJobs属性是最重要的信息之一,我们将在稍后讨论。要从作业中检索结果,请使用Receive-Job。但在运行此命令之前,您需要了解一些事情:

  • 您必须指定您想要接收结果的作业。您可以通过作业 ID 或作业名称来做到这一点,或者通过使用Get-Job并使用管道将它们传递到Receive-Job来获取作业。

  • 如果您接收父作业的结果,这些结果将包括所有子作业的所有输出。或者,您可以选择从一个或多个子作业中获取结果。

  • 通常,从作业中接收结果会将其从作业输出缓存中清除,因此您无法再次获取它们。指定-keep以在内存中保留结果的副本。或者,如果您想保留一个副本以供工作使用,可以将结果输出到 CLIXML 文件。

  • 作业结果可能是反序列化对象,您在第十三章中了解过。这些是从它们生成时的快照,它们可能没有任何您可以执行的方法。但根据需要,您可以将作业结果直接管道到Sort-Object-Format-ListExport-CSVConvertTo-HTMLOut-File等命令。

这里有一个例子:

PS /Users/travisp/> receive-job -id 1

    Directory: /Users/travisp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          11/24/2019 10:53 PM                Code
d----          11/18/2019 11:23 PM                Desktop
d----           9/15/2019  9:12 AM                Documents
d----           12/8/2019 11:04 AM                Downloads
d----           9/15/2019  7:07 PM                Movies
d----           9/15/2019  9:12 AM                Music
d----           9/15/2019  6:51 PM                Pictures
d----           9/15/2019  9:12 AM                Public

上述输出显示了一组有趣的结果。这是启动此作业的命令的快速提醒:

PS /Users/travisp/> start-job -scriptblock { gci }

当我们从Job1接收结果时,我们没有指定-keep。如果我们尝试再次获取相同的结果,我们将一无所获,因为结果不再与作业一起缓存:

PS /Users/travisp/> receive-job -id 1

这是强制结果保留在内存中的方法:

PS /Users/travisp/> receive-job -id 3 –keep

    Directory: /Users/travisp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          11/24/2019 10:53 PM                Code
d----          11/18/2019 11:23 PM                Desktop
d----           9/15/2019  9:12 AM                Documents
d----           12/8/2019 11:04 AM                Downloads
d----           9/15/2019  7:07 PM                Movies
d----           9/15/2019  9:12 AM                Music
d----           9/15/2019  6:51 PM                Pictures
d----           9/15/2019  9:12 AM                Public  

你最终会想要释放用于缓存作业结果的内存,我们稍后会讨论这一点。但首先,让我们看看一个将作业结果直接管道传输到另一个 cmdlet 的快速示例:

PS /Users/travisp> receive-job -name myremotejob | sort-object PSComputerName 
 ➥ | Format-Table -groupby PSComputerName
   PSComputerName: localhost
NPM(K)    PM(M)      WS(M) CPU(s)     Id ProcessName PSComputerName
------    -----      ----- ------     -- ----------- --------------
     0        0      56.92   0.70    484 pwsh        localhost
     0        0     369.20  70.17   1244 Code        localhost
     0        0      71.92   0.20   3492 pwsh        localhost
     0        0     288.96  15.31    476 iTerm2      localhost

这是使用 Invoke-Command 启动的作业。该 cmdlet 添加了 PSComputerName 属性,这样我们就可以跟踪哪个对象来自哪个计算机。因为我们从顶级作业中检索了结果,这包括我们指定的所有计算机,这使得这个命令可以根据计算机名称对它们进行排序,并为每个计算机创建一个单独的表格组。Get-Job 也可以让你了解哪些作业还有剩余结果:

PS /Users/travisp> get-job
Id Name            PSJobTypeName   State     HasMoreData  Location  Command
-- ----            -------------   -----     -----------  --------  -------
1  Job1            BackgroundJob   Completed False        localhost  gci
3  Job3            BackgroundJob   Completed True         localhost  gci
5  Job5            ThreadJob       Completed True         PowerShell gci
8  Job8            BackgroundJob   Completed True         server-r2, lo...
11 MyRemoteJob     BackgroundJob   Completed False        server-r2, lo...
13 Long Running O... AzureLongRunni... Running   True     localhost  New-AzVM

当没有输出与该作业一起缓存时,HasMoreData 列将显示为 False。在 Job1MyRemoteJob 的情况下,我们已经收到了那些结果,当时没有指定 -keep

14.8 与子作业一起工作

我们之前提到,大多数作业由一个顶级父作业和至少一个子作业组成。让我们再次看看一个作业:

PS /Users/travisp> get-job -id 1 | format-list *
State         : Completed
HasMoreData   : True
StatusMessage :
Location      : localhost
Command       :  dir
JobStateInfo  : Completed
Finished      : System.Threading.ManualResetEvent
InstanceId    : e1ddde9e-81e7-4b18-93c4-4c1d2a5c372c
Id            : 1
Name          : Job1
ChildJobs     : {Job2}
PSBeginTime   : 12/27/2019 2:34:25 PM
PSEndTime     : 12/27/2019 2:34:29 PM
PSJobTypeName : BackgroundJob
Output        : {}
Error         : {}
Progress      : {}
Verbose       : {}
Debug         : {}
Warning       : {}
Information   : {}

现在尝试一下 不要跟随这一部分,因为如果你到现在一直跟着做,你已经收到了 Job1 的结果。如果你想尝试这个,通过运行 Start-Job -script { dir } 开始一个新的作业,并使用那个新作业的 ID 而不是我们示例中使用的 ID 号 1。

你可以看到 Job1 有一个子作业,Job2。现在你知道它的名字,你可以直接获取它:

PS /Users/travisp> get-job -name job2 | format-list *
State         : Completed
StatusMessage :
HasMoreData   : True
Location      : localhost
Runspace      : System.Management.Automation.RemoteRunspace
Debugger      : System.Management.Automation.RemotingJobDebugger
IsAsync       : True
Command       :  dir
JobStateInfo  : Completed
Finished      : System.Threading.ManualResetEvent
InstanceId    : a21a91e7-549b-4be6-979d-2a896683313c
Id            : 2
Name          : Job2
ChildJobs     : {}
PSBeginTime   : 12/27/2019 2:34:25 PM
PSEndTime     : 12/27/2019 2:34:29 PM
PSJobTypeName :
Output        : {Applications, Code, Desktop, Documents, Downloads, Movies,            
            ➥ Music...}
Error         : {}
Progress      : {}
Verbose       : {}
Debug         : {}
Warning       : {}
Information   : {}

有时候一个作业有太多的子作业,无法以那种形式列出,因此你可能想以不同的方式列出它们,如下所示:

PS /Users/travisp> get-job -id 1 | select-object -expand childjobs
Id Name            PSJobTypeName   State     HasMoreData  Location  Command
-- ----            -------------   -----     -----------  --------  -------
2  Job2                            Completed True         localhost  gci

这个技术为作业 ID 1 创建了一个子作业表,表可以扩展到任何长度,以列出所有子作业。你可以通过指定其名称或 ID 来接收任何单个子作业的结果,使用 Receive-Job

14.9 管理作业的命令

作业还使用三个额外的命令。对于这些命令中的每一个,你可以通过提供其 ID、提供其名称或获取作业并将其管道传输到这些 cmdlet 之一来指定一个作业:

  • Remove-Job——这个命令会从内存中删除一个作业及其任何仍然缓存的输出。

  • Stop-Job——如果一个作业看起来卡住了,这个命令会终止它。你仍然可以接收到目前为止生成的任何结果。

  • Wait-Job——如果脚本将要启动一个作业或多个作业,而你又想让脚本仅在作业完成后继续执行,这个命令非常有用。这个命令会强制 shell 停止并等待作业(或多个作业)完成,然后允许 shell 继续执行。

例如,为了删除我们已经收到输出的作业,我们会使用以下命令:

PS /Users/travisp> get-job | where { -not $_.HasMoreData } | remove-job
PS /Users/travisp> get-job
Id Name            PSJobTypeName   State     HasMoreData  Location  Command
-- ----            -------------   -----     -----------  --------  -------
3  Job3            BackgroundJob   Completed True         localhost  gci
5  Job5            ThreadJob       Completed True         PowerShell gci
8  Job8            BackgroundJob   Completed True         server-r2, lo...
13 Long Running O... AzureLongRunni... Completed True     localhost New-AzVM

作业也可能失败,这意味着它们的执行过程中出现了问题。考虑以下示例:

PS /Users/travisp> invoke-command -command { nothing } -hostname notonline
    -asjob -jobname ThisWillFail
Id Name            PSJobTypeName   State     HasMoreData  Location  Command
-- ----            -------------   -----     -----------  --------  -------
11 ThisWillFail    BackgroundJob   Failed    False        notonline  nothing

在这里,我们使用一个无效的命令并针对一个不存在的计算机启动了一个作业。作业立即失败,如其状态所示。此时我们不需要使用 Stop-Job;作业没有在运行。但我们可以获取其子作业的列表:

PS /Users/travisp> get-job -id 11 | format-list *
State         : Failed
HasMoreData   : False
StatusMessage :
Location      : notonline
Command       :  nothing
JobStateInfo  : Failed
Finished      : System.Threading.ManualResetEvent
InstanceId    : d5f47bf7-53db-458d-8a08-07969305820e
Id            : 11
Name          : ThisWillFail
ChildJobs     : {Job12}
PSBeginTime   : 12/27/2019 2:45:12 PM
PSEndTime     : 12/27/2019 2:45:14 PM
PSJobTypeName : BackgroundJob
Output        : {}
Error         : {}
Progress      : {}
Verbose       : {}
Debug         : {}
Warning       : {}
Information   : {}

然后,我们可以获取那个子作业:

PS /Users/travisp> get-job -name job12
Id Name  PSJobTypeName   State     HasMoreData  Location  Command
-- ----  -------------   -----     -----------  --------  -------
12 Job12                Failed     False        notonline  nothing

正如你所见,这个任务没有创建任何输出,所以你不会有任何结果可以检索。但任务中的错误存储在结果中,你可以通过使用 Receive-Job 来获取它们:

PS /Users/travisp> receive-job -name job12
OpenError: [notonline] The background process reported an error with the 
➥ following message: The SSH client session has ended with error message: 
➥ ssh: Could not resolve hostname notonline: nodename nor servname provided, 
➥ or not known.

完整的错误信息要长得多;我们在这里截断以节省空间。你会注意到错误中包含了错误来源的主机名 [notonline]。如果只有一台计算机无法连接会发生什么?让我们试试:

PS /Users/travisp> invoke-command -command { nothing } 
-computer notonline,server-r2 -asjob -jobname ThisWilLFail
Id Name         PSJobTypeName   State    HasMoreData  Location        Command
-- ----         -------------   -----    ---------    --------        -------
13 ThisWillFail BackgroundJob   Running  True         notonline,lo... nothing

稍微等待一下,我们运行以下命令:

PS /Users/travisp> get-job 13
Id Name         PSJobTypeName   State    HasMoreData  Location        Command
-- ----         -------------   -----    -----------  --------        -------
13 ThisWillFail BackgroundJob   Failed   False        notonline,lo... nothing

任务仍然失败,但让我们看看单个子任务:

PS /Users/travisp> get-job -id 13 | select -expand childjobs
Id Name  PSJobTypeName   State     HasMoreData  Location        Command
-- ----  -------------   -----     -----------  --------        -------
14 Job14                 Failed    False        notonline       nothing
15 Job15                 Failed    False        localhost       nothing

好吧,它们两个都失败了。我们感觉我们知道为什么 Job14 不工作,但 Job15 有什么问题?

PS /Users/travisp> receive-job -name job15
Receive-Job : The term 'nothing' is not recognized as the name of a cmdlet
, function, script file, or operable program. Check the spelling of the na
me, or if a path was included, verify that the path is correct and try aga
in.

哎,没错,我们告诉它运行一个虚假命令。正如你所见,每个子任务可能会因为不同的原因而失败,而 PowerShell 会单独跟踪每一个。

14.10 常见混淆点

任务通常很简单,但我们见过有人做了一件事导致混淆。不要这样做:

PS /Users/travisp> invoke-command -command { Start-Job -scriptblock { dir } } 
-hostname Server-R2

这样做会启动到 Server-R2 的临时连接并启动一个本地任务。不幸的是,这个连接立即终止,所以你无法重新连接并检索该任务。总的来说,不要混合使用启动任务的三种方式。以下也是一个坏主意:

PS /Users/travisp> start-job -scriptblock { invoke-command -command { dir }
-hostname SERVER-R2 }

这完全是多余的;保留 Invoke-Command 部分,并使用 -AsJob 参数使其在后台运行。

更少混淆但同样有趣的是,新用户经常询问关于任务的问题。其中最重要的问题可能是,“我们能看到别人启动的任务吗?”答案是不了。任务和线程任务完全包含在 PowerShell 进程中,尽管你可以看到另一个用户正在运行 PowerShell,但你无法看到那个进程内部。就像任何其他应用程序一样:你可以看到另一个用户正在运行 Microsoft Word,例如,但你无法看到该用户正在编辑哪些文档,因为那些文档完全存在于 Word 的进程内部。

任务只在你打开 PowerShell 会话期间存在。在你关闭它之后,其中定义的任何任务都会消失。任务不在 PowerShell 之外定义,因此它们依赖于其进程继续运行以维持自身。

14.11 实验室

以下练习应该有助于你了解如何在 PowerShell 中处理各种类型的任务和作业。在完成这些练习时,不要觉得你必须写出一个单行解决方案。有时将事情分解成单独的步骤更容易。

  1. 创建一个一次性线程任务来查找文件系统中的所有文本文件(*.txt)。任何可能需要很长时间才能完成的任务都是使用任务的绝佳候选。

  2. 你会意识到识别你服务器上的一些文本文件将是有帮助的。你将如何从任务 1 在一组远程计算机上运行相同的命令?

  3. 你会用哪个 cmdlet 来获取任务的结果,以及你将如何将结果保存在任务队列中?

14.12 实验答案

  1. Start-ThreadJob {gci / -recurse –filter '*.txt'}

  2. Invoke-Command –scriptblock {gci / -recurse –filter *.txt}

    –computername (get-content computers.txt) -asjob

  3. Receive-Job –id 1 –keep

    当然,你会使用适用的作业 ID 或作业名称。

15 逐个处理多个对象

PowerShell 的全部目的是自动化管理,这通常意味着你可能想要对多个目标执行一些任务。你可能想要启动几个虚拟机,向几个 blob 存储推送,修改多个用户的权限,等等。在本章中,你将学习两种不同的技术来完成这些和其他多个目标任务:批处理 cmdlet 和对象枚举。这里的概念和技术无论你使用什么操作系统都是相同的。

注意:这是一章极其困难的章节,可能会让你感到沮丧。请对自己和我们保持耐心,并相信我们最终会解释清楚所有内容。

15.1 优先方法:“批处理”cmdlet

正如你在前面几个章节中学到的,许多 PowerShell cmdlet 可以接受批处理,或 集合,的对象来处理。例如,在第六章中,你学习了对象可以被从一个 cmdlet 传递到另一个 cmdlet,如下所示(请勿在任何系统上运行此命令,除非你真的想有一个糟糕的一天):

Get-Service | Stop-Service

这是一个使用 cmdlet 进行批处理管理的例子。在这种情况下,Stop-Process 是专门设计用来从管道接受一个进程对象,然后停止它。Set-ServiceStart-ProcessMove-ItemMove-AdObject 都是接受一个或多个输入对象并针对每个对象执行任务或操作的 cmdlet 的例子。PowerShell 知道如何处理对象批处理,并且可以使用相对简单的语法为你处理它们。

这些 批处理 cmdlet(这是我们给它们的名称——它不是一个官方术语)是我们执行任务的首选方式。例如,假设我们需要更改多个服务的启动类型:

Get-Service BITS,Spooler | Set-Service -startuptype Automatic

这种方法的潜在缺点是,执行操作的 cmdlet 往往不会产生任何输出,以表明它们已经完成了工作。你将不会从前面提到的任何命令中获得任何可视输出,这可能会让人感到不安。但那些 cmdlet 通常有一个 -PassThru 参数,它告诉它们输出它们接受的任何输入对象。你可以让 Set-Service 输出它修改过的相同服务,这样你可以验证它们已经被修改。以下是一个使用 -passThru 的不同 cmdlet 的例子:

Get-ChildItem .\ | Copy-Item -Destination C:\Drivers -PassThru

此命令检索当前目录中的所有项目。然后这些对象被传递到 Copy-Item,然后它会将项目复制到 C:\Drivers 目录。因为我们把 -PassThru 参数放在了最后,所以它会在屏幕上显示它所做的工作。如果我们没有这样做,那么一旦它完成,它就会简单地回到我们的 PowerShell 提示符。

现在试试:从一个目录复制几个文件或文件夹到另一个目录。尝试使用和未使用 -PassThru 参数的情况,并注意区别。

15.2 CIM 方法:调用方法

在我们开始之前,有两件事你必须知道:

  • Windows Management Instrumentation (WMI) 与 PowerShell 7 不兼容。您必须使用通用信息模型 (CIM) 命令,它们的工作方式大致相同。

  • 15.2 节仅适用于使用 Windows 的用户。我们尽力确保本书中我们做的所有事情都是跨平台的。但有些情况下这是不可能的,这就是其中之一。

很遗憾,我们并不总是拥有能够执行所需任何操作的 cmdlet,这在通过 CIM 操作的项目中尤为如此。例如,考虑一下 Win32_NetworkAdapterConfiguration CIM 类。这个类代表绑定到网络适配器的配置(适配器可以有多个配置,但为了简单起见,现在让我们假设每个适配器只有一个配置,这在客户端计算机上很常见)。假设我们的目标是启用所有计算机的英特尔网络适配器的 DHCP——我们不想启用任何 RAS 或其他虚拟适配器。

注意:我们将向您展示一个简短的故事线,旨在帮助您体验人们如何使用 PowerShell。有些事情可能看起来有些重复,但请耐心等待——体验本身是有价值的。

我们可能首先尝试查询所需的适配器配置,这将允许我们得到如下输出:

DHCPEnabled      : False
IPAddress        : {192.168.10.10, fe80::ec31:bd61:d42b:66f}
DefaultIPGateway :
DNSDomain        :
ServiceName      : E1G60
Description      : Intel(R) PRO/1000 MT Network Connection
Index            : 7
DHCPEnabled      : True
IPAddress        :
DefaultIPGateway :
DNSDomain        :
ServiceName      : E1G60
Description      : Intel(R) PRO/1000 MT Network Connection
Index            : 12

要实现这种输出,我们需要查询适当的 CIM 对象,并过滤它以仅包括描述中包含 Intel 的配置。以下命令做到了这一点(注意,在 WMI 过滤器语法中,% 作为通配符使用):

PS C:\> Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | 
-Filter "description like '%intel%'" | Format-List

现在尝试:您欢迎跟随本节中运行的命令。您可能需要稍微调整命令才能使其工作。例如,如果您的计算机没有英特尔制造的任何网络适配器,您需要相应地更改过滤标准。

一旦我们有了这些配置对象在管道中,我们希望在它们上启用 DHCP(您可以看到我们的适配器之一还没有启用 DHCP)。我们可能首先寻找一个名为 Enable-DHCP 的 cmdlet。不幸的是,我们找不到它,因为没有这样的东西。没有能够直接批量处理 CIM 对象的 cmdlet。

现在尝试:基于您到目前为止所学的内容,您会使用什么命令来搜索名称中包含 DHCP 的 cmdlet?

我们下一步是查看对象本身是否有能够启用 DHCP 的方法。为了找出,我们运行 Get-CimClass 命令并展开到 CimClassMethods 属性:

PS C:\> (Get-CimClass Win32_NetworkAdapterConfiguration).CimClassMethods 

在顶部,我们将看到一个名为 EnableDHCP 的方法(图 15.1)。

图 15.1 显示了可用的方法

下一个步骤,许多 PowerShell 新手都会尝试,是将配置对象通过管道传递到方法:

PS C:\> Get-CimInstance win32_networkadapterconfiguration -filter 
"description like '%intel%'" | EnableDHCP

很遗憾,这不会起作用。你不能将对象管道传输到方法中;只能传输到 cmdlet 中。EnableDHCP不是一个 PowerShell cmdlet。相反,它是一个直接附加到配置对象本身的行为。

虽然没有名为Enable-DHCP的“批量”cmdlet,但你可以使用一个名为Invoke-CimMethod的通用 cmdlet。这个 cmdlet 专门设计用来接受一批 CIM 对象,例如我们的Win32_NetworkAdapterConfiguration对象,并调用这些对象上附加的方法。以下是我们要运行的命令:

PS C:\> Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -filter
➥ "description like '%intel%'" | Invoke-CimMethod -MethodName EnableDHCP

你有几件事情需要记住:

  • 方法名称后面不跟括号。

  • 方法名称不区分大小写。

  • Invoke-CimMethod一次只能接受一种 WMI 对象。在这种情况下,我们只发送Win32_NetworkAdapterConfiguration对象,这意味着它将按预期工作。发送多个对象是可以的(实际上这正是目的所在),但所有对象都必须是同一类型。

  • 你可以使用-WhatIf-ConfirmInvoke-CimMethod一起使用。但是,当你直接从对象调用方法时,不能使用这些选项。

Invoke-CimMethod的输出非常简单易懂。它给你两样东西:一个返回值和它在哪台计算机上运行(如果计算机名称为空,则表示在localhost上运行)。

ReturnValue PSComputerName
----------- --------------
         84

ReturnValue数字告诉我们操作的结果。通过你最喜欢的搜索引擎对Win32_NetworkAdapterConfiguration进行快速搜索,可以找到文档页面,然后我们可以点击进入EnableDHCP方法,查看可能的返回值及其含义。图 15.2 显示了我们的发现。

图 15.2 查找 WMI 方法结果的返回值

值为0表示成功,而84表示该适配器配置上未启用 IP,DHCP 无法启用。但是输出中的哪个位与我们的两个网络适配器配置中的哪一个相对应呢?很难判断,因为输出没有告诉你哪个具体的配置对象产生了它。这是不幸的,但这就是 CIM 的工作方式。

Invoke-CimMethod适用于大多数你有 CIM 对象并且想要执行其方法的情况。当从远程计算机查询 CIM 对象时,它也工作得很好。我们的基本规则是,“如果你可以通过Get-CIMInstance访问到某个东西,那么Invoke-CimMethod就可以执行其方法。”

15.3 备份计划:枚举对象

不幸的是,我们遇到了一些情况,其中我们有一个可以生成对象的 cmdlet,但我们不知道可以将这些对象管道化到哪个批处理 cmdlet 中以执行某种操作。我们也遇到过 cmdlet 不从管道中获取任何输入的情况。在两种情况下,你仍然可以执行你想要执行的任何任务,但你将不得不退回到一种更冗长的风格,即指示计算机枚举对象,并逐个对象执行你的任务。PowerShell 提供了两种实现此目的的方法:一种是通过 cmdlet,另一种是通过脚本结构。我们在这章中关注第一种技术,因为它是最简单的。你应该始终尝试使用 cmdlet 而不是自己尝试脚本化它。 我们将第二种方法留到第十九章,该章深入探讨了 PowerShell 的内置脚本语言。

对于我们的示例,因为我们一直在本章中讨论进程,所以我们将讨论 cmdlet。让我们看看语法:

Get-Help Get-Process -Full

这将给我们带来以下所有内容……但是快速浏览一下名为“Id.”的部分。你会注意到一些参数说明它们接受管道输入,但在括号中写着 ByPropertyName。这意味着如果我们向这个 cmdlet 管道一个对象,并且它有一个名为 Id 的属性,例如,这个 cmdlet 将使用那个:

-Id <System.Int32[]>
        Specifies one or more processes by process ID (PID). To specify 
      ➥ multiple IDs, use commas to separate the IDs.
        To find the PID of a process, type 'Get-Process'.

        Required?                    true
        Position?                    named
        Default value                None
        Accept pipeline input?       True (ByPropertyName)
        Accept wildcard characters?  false

    -IncludeUserName <System.Management.Automation.SwitchParameter>
        Indicates that the UserName value of the Process object is returned 
      ➥ with results of the command.

        Required?                    true
        Position?                    named
        Default value                False
        Accept pipeline input?       False
        Accept wildcard characters?  false

然而,如果我们只想管道输入一个字符串列表,这些字符串是我们想要创建的进程的名称呢?我们无法做到这一点,因为 Name 参数不支持其他类型的管道:ByValue。试试看。让我们看看 New-AzKeyVault 命令。我们将把我们的值放入一个数组中,并将其管道化到 New-AzKeyVault 命令中:

@( "vaultInt1", "vaultProd1", "vaultInt2", "vaultProd2" ) | New-AzKeyVault

这会导致出现以下不太理想的红色文本:

New-AzKeyVault: The input object cannot be bound to any parameters for the 
➥ command either because the command does not take pipeline input or the 
➥ input and its properties do not match any of the parameters that take 
➥ pipeline input.

让我们更深入地探讨一下,即使 cmdlet 无法支持我们想要执行的操作,我们仍然如何实现我们的目标。

15.3.1 使 cmdlet 为您服务

到目前为止,我们必须做出决定。有可能我们运行命令的方式不正确,所以我们必须决定我们是否愿意花大量时间来解决这个问题。也有可能 New-AzKeyVault 实际上不支持我们想要做的事情,在这种情况下,我们可能会花大量时间尝试修复我们无法控制的事情。

我们需要创建一个包含我们想要创建的保险库名称的文本文件。我们的 vaultsToCreate.txt 看起来像这样:

vaultInt1
vaultInt2
vaultProd1
vaultProd2

在这种情况下,我们通常的选择是尝试不同的方法。我们将要求计算机(好吧,是外壳)逐个枚举对象(在我们的情况下,是字符串),因为 New-AzKeyVault 命令一次只能接受一个对象,并在对象上执行 New-AzKeyVault。为此,我们使用 ForEach-Object cmdlet:

Get-Content -Path vaultsToCreate.txt | ForEach-Object { New-AzKeyVault 
-ResourceGroupName manning -Location 'UK South' -Name $_ }

对于我们创建的四个资源,我们得到四个看起来像这样的结果(这里只显示了部分输出,因为结果可能相当长):

Vault Name                       : vaultInt1
Resource Group Name              : manning
Location                         : Australia Central
Resource ID                      : /subscriptions/*****/resourceGroups/manning/providers/Microsoft.KeyVault
   ➥ /vaults/vaultInt1
Vault URI                        : https://vaultint1.vault.azure.net/
Tenant ID                        : *********
SKU                              : Standard
Enabled For Deployment?          : False
Enabled For Template Deployment? : False
Enabled For Disk Encryption?     : False
Soft Delete Enabled?             :

在文档中,我们发现如果得到这样的响应,这意味着成功,这意味着我们已经实现了我们的目标。但让我们更详细地看看那个命令。

Get-Content -Path vaultsToCreate.txt | 
 ForEach-Object -Process {
   New-AzKeyVault -ResourceGroupName manning -Location 'UK South' -Name $_
}

这个命令有很多功能。第一行应该是合理的:我们使用 Get-Content 来检索我们放入文本文件中的保险库名称。我们将这些 string 对象通过管道传递给 ForEach-Object cmdlet:

  • 首先,你看到的是 cmdlet 名称:ForEach-Object

  • 接下来,我们使用 -Process 参数指定一个脚本块。我们最初没有输入 -Process 参数名称,因为它是一个位置参数。但那个脚本块——大括号内包含的所有内容——是 -Process 参数的值。我们在重新格式化命令以方便阅读时包括了参数名称。

  • ForEach-Object 对每个被传递到 ForEach-Object 的对象执行其脚本块一次。每次脚本块执行时,下一个被传递的对象将被放置到特殊的 $_ 占位符中,你可以在 New-AzKeyVault 中的 Name 参数中看到它被传递。

15.4 让我们加快速度

在前面的章节中,我们讨论了使用 PowerShell 作业并行运行命令以节省时间。为了简化这种节省时间的功能,PowerShell 7 在 ForEach-Object 上引入了一个新参数:-Parallel。通过一个例子可以更好地理解它,那就是著名的 Measure-Command cmdlet,它允许你测量各种事物,但我们将用它来测量脚本块运行的时间。它看起来是这样的:

Measure-Command {  <# The script we want to time #> }

那么,让我们试试看。首先,我们将使用常规的 ForEach-Object 尝试做一些简单的事情:

Get-Content -Path vaultsToCreate.txt | ForEach-Object -Process {
  Write-Output $_ 
  Start-Sleep 1
}

这所有的一切只是将文件的每一行打印出来,然后每行睡眠一秒。如果我们文件中有五行,你可能能猜出这需要多长时间运行,但让我们使用 Measure-Command

Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -Process {
     Write-Output $_
     Start-Sleep 1
   }
}

当我们运行它时,我们得到以下输出:

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 5
Milliseconds      : 244
Ticks             : 52441549
TotalDays         : 6.06962372685185E-05
TotalHours        : 0.00145670969444444
TotalMinutes      : 0.0874025816666667
TotalSeconds      : 5.2441549
TotalMilliseconds : 5244.1549

让我们具体看看 Seconds 值,它是 5。这说得通,对吧?如果我们文件中有五行,我们逐行处理每一行,并且每行睡眠 1 秒,我们预计该命令将大约运行 5 秒。

现在让我们将相同的命令更改为使用 Parallel 而不是 Process

Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -Parallel {
     Write-Output $_
     Start-Sleep 1
   }
}

有什么猜测吗?让我们运行它:

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 340
Ticks             : 13405417
TotalDays         : 1.55155289351852E-05
TotalHours        : 0.000372372694444444
TotalMinutes      : 0.0223423616666667
TotalSeconds      : 1.3405417
TotalMilliseconds : 1340.5417

一秒钟!这是因为 Parallel 正如其名所示——它并行而不是顺序地运行脚本块。由于我们的文件中有五个项目,并且我们并行运行了所有这些项目,并且我们每次睡眠 1 秒,整个操作只需大约 1 秒。这对于长时间运行的任务或在有很多较小任务需要批量处理的场景非常有用。我们甚至可以使用现有的示例并使用 Parallel ForEach

Get-Content -Path vaultsToCreate.txt | 
 ForEach-Object -Parallel {
   New-AzKeyVault -ResourceGroupName manning -Location 'UK South' -Name $_
}

ParallelForEach 上的一个非常强大的参数,但它确实有一些你应该注意的限制。首先,默认情况下,Parallel ForEach 只会并行运行五个脚本块。这被称为 节流限制,可以通过 ThrottleLimit 参数进行调整。回到我们一直在使用的那个文件,确保它总共有 10 行。这种差异非常明显:

Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -Process {
     Write-Output $_
     Start-Sleep 1
   }
}

没有设置节流限制,我们得到 2 秒:

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 2
Milliseconds      : 255
Ticks             : 22554472
TotalDays         : 2.6104712962963E-05
TotalHours        : 0.000626513111111111
TotalMinutes      : 0.0375907866666667
TotalSeconds      : 2.2554472
TotalMilliseconds : 2255.4472

然而,如果我们把节流限制提高到 10,我们会得到

Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -ThrottleLimit 10 -Process {
     Write-Output $_
     Start-Sleep 1
   }
}

命令在 1 秒内完成!

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 255
Ticks             : 12553654
TotalDays         : 1.45296921296296E-05
TotalHours        : 0.000348712611111111
TotalMinutes      : 0.0209227566666667
TotalSeconds      : 1.2553654
TotalMilliseconds : 1255.3654

Parallel ForEach 是 PowerShell 的一个非常强大的功能。如果你能正确利用它,你会节省很多时间。

15.5 常见混淆点

本章中的技术是 PowerShell 中最困难的之一,它们经常引起最多的困惑和挫折。让我们看看新来者容易遇到的一些问题。我们提供了一些替代解释,这将帮助你避免遇到相同的问题。

15.5.1 哪种方法是正确的?

我们使用术语 批处理 cmdlet操作 cmdlet 来指代任何对一组或集合中的对象执行操作的 cmdlet。你不需要指示计算机“遍历这个列表中的事物,并对每个事物执行这个操作”,你可以将整个组发送到 cmdlet,然后由 cmdlet 处理。

微软正在努力在其产品中提供这类 cmdlet,但它的覆盖范围还不是 100%(可能因为存在许多复杂的微软产品,所以可能很多年都不会达到 100%)。但是,当 cmdlet 存在时,我们更喜欢使用它。话虽如此,其他 PowerShell 开发者更喜欢根据他们最初学习的内容和最容易记住的内容选择其他方法。以下所有方法都有相同的结果:

Get-Process -name *B* | Stop-Process                                  ❶
Get-Process -name *B* | ForEach-Object { $_.Kill()}                   ❷
Get-Process -Name *B* | ForEach-Object -Parallel { Stop-Process $_ }  ❸

❶ 批处理 cmdlet

❷ 调用 Kill() 方法的 ForEach-Object

❸ ForEach-Object 调用 Stop-Process

让我们看看每种方法是如何工作的:

  • 批处理 cmdlet。在这里,我们使用 Get-Process 来检索所有名称中带有 B 的进程,然后停止它们。

  • ForEach-Object 调用 Kill() 方法。这种方法与批处理 cmdlet 类似,但不是使用批处理 cmdlet,而是将进程通过管道发送到 ForEach-Object,并要求它执行每个服务的 Kill()

  • 使用 -ParallelForEach-Object 调用 Stop-Process

事实上,甚至还有第四种方法——使用 PowerShell 的脚本语言来完成相同的事情。你会在 PowerShell 中找到很多方法来完成几乎所有的事情,而且没有哪一种是错误的。有些方法比其他方法更容易学习、记忆和重复,这就是为什么我们专注于我们已有的技术,按照我们使用的顺序。你应该使用哪种方法?这无关紧要,因为没有一种绝对正确的方法。你可能甚至最终会根据具体情况和 shell 为你提供的任务能力,使用这些方法的混合。

15.5.2 Parallel ForEach 的递减回报

还记得我们的 Parallel ForEach 示例吗?它看起来是这样的:

Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -Parallel {
     Write-Output $_
     Start-Sleep 1
   }
}

现在假设 vaultsToCreate.txt 有 100 行。我们应该尝试将 ThrottleLimit 设置为 100 以使操作在 1 秒内完成吗?让我们试试:

Measure-Command {
   Get-Content -Path vaultsToCreate.txt |
   ForEach-Object -ThrottleLimit 100 -Parallel {
     Write-Output $_
     Start-Sleep 1
   }
}

这给出了 3 秒的输出。这很奇怪:

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 3
Milliseconds      : 525
Ticks             : 35250040
TotalDays         : 4.07986574074074E-05
TotalHours        : 0.000979167777777778
TotalMinutes      : 0.0587500666666667
TotalSeconds      : 3.525004
TotalMilliseconds : 3525.004

为什么这么慢?好吧,结果是瓶颈是你的机器,它在开始变慢之前只能并行运行这么多事情。这就像我们在第十四章中看到的 Start-ThreadJob。单个进程在开始比串行运行慢之前只能并行做这么多事情。

这是一个奇怪的概念,但想象一下如果你同时处理一堆任务。你将不得不不断在各个任务之间进行上下文切换,以同时在这所有任务上取得进展。在某些情况下,如果你简单地等待完成其他正在进行的任务后再开始一个任务,你可能会更有效率。我们通常称这种现象为“收益递减”,意味着随着你尝试并行做更多的事情,它变得不那么值得,如果不小心,甚至可能以负面方式影响结果。

15.5.3 方法文档

总是记住,将对象管道传输到 Get-Member 会显示方法:

Get-Process | Get-Member

PowerShell 的内置帮助系统没有记录方法对象。例如,如果你为进程对象获取成员列表,你可以看到存在名为 KillStart 的方法:

TypeName: System.Diagnostics.Process
Name                       MemberType     Definition
----                       ----------     ----------
BeginErrorReadLine         Method         void BeginErrorReadLine()
BeginOutputReadLine        Method         void BeginOutputReadLine()
CancelErrorRead            Method         void CancelErrorRead()
CancelOutputRead           Method         void CancelOutputRead()
Close                      Method         void Close()
CloseMainWindow            Method         bool CloseMainWindow()
Dispose                    Method         void Dispose(), void IDisposable.Dispose()
Equals                     Method         bool Equals(System.Object obj)
GetHashCode                Method         int GetHashCode()
GetLifetimeService         Method         System.Object GetLifetimeService()
GetType                    Method         type GetType()
InitializeLifetimeService  Method         System.Object InitializeLifetimeService()
Kill                       Method         void Kill(), void Kill(bool entireProcessTree)
Refresh                    Method         void Refresh()
Start                      Method         bool Start()
ToString                   Method         string ToString()
WaitForExit                Method         void WaitForExit(), bool WaitForExit(int milliseconds)
WaitForInputIdle           Method         bool WaitForInputIdle(), bool 
➥ WaitForInputIdle(int milliseconds)

要找到这些文档,请关注 TypeName,在这个例子中是 System.Diagnostics.Process。在搜索引擎中搜索这个完整的类型名称,你通常会找到该类型的官方开发者文档,这将引导你找到你想要的特定方法的文档。

15.5.4 ForEach-Object 混淆

ForEach-Object cmdlet 的语法中有很多标点符号,加上方法自己的语法可能会创建一个丑陋的命令行。我们整理了一些打破任何心理障碍的建议:

  • 尽量使用完整的 cmdlet 名称,而不是它的 %ForEach 别名。完整的名称可能更容易阅读。如果你在使用别人的示例,请将别名替换为完整的 cmdlet 名称。

  • 花括号内的脚本块会为每个被管道传输到 cmdlet 的对象执行一次。

  • 在脚本块内部,$_ 代表管道中的当前对象。

  • 使用 $_ 自身来处理你管道传输的整个对象;在 $_ 后面加一个点来处理单个方法或属性。

  • 方法名称总是后面跟着括号,即使该方法不需要任何参数。当需要参数时,它们通过逗号分隔并包含在括号内。

15.6 实验

注意:对于这个实验,你需要一台安装了 PowerShell 7 或更高版本的机器。

尝试回答以下问题并完成指定的任务。这是一个重要的实验,因为它利用了你之前许多章节中学到的技能,你应该在阅读本书的剩余部分时继续使用和加强这些技能:

  1. DirectoryInfo 对象(由 Get-ChildItem 产生)的哪种方法可以删除目录?

  2. Process 对象(由 Get-Process 产生)的哪种方法可以终止指定的进程?

  3. 编写三个命令,用于删除所有名称中包含 deleteme 的文件和目录,假设有多个文件和目录包含这个名称。

  4. 假设你有一个包含计算机名称的文本列表,但你想将它们显示为大写。你可以使用什么 PowerShell 表达式?

15.7 实验答案

  1. 找到类似的方法:

    Get-ChildItem | Get-Member -MemberType Method

    你应该看到一个 Delete() 方法。

  2. 找到类似的方法:

    get-process | Get-Member -MemberType Method

    你应该看到一个 Kill() 方法。你可以通过检查该进程对象的 MSDN 文档来验证这一点。当然,你不需要调用该方法,因为有一个 cmdlet,Stop-Process,可以为你完成这项工作。

  3. Get-ChildItem *deleteme* | Remove-Item -Recurse -Force

    Remove-Item *deleteme* -Recurse -Force

    Get-ChildItem *deleteme* | foreach {$_.Delete()}

  4. Get-content computers.txt | foreach {$_.ToUpper()}

16 变量:存储你的东西的地方

我们已经提到 PowerShell 包含一种脚本语言,在接下来的几章中,我们将开始使用它。但一旦你开始编写脚本,你可能希望将你的对象作为变量存储起来以供以后使用,所以我们将在这个章节中介绍这些内容。你可以在许多地方使用变量,而不仅仅是长而复杂的脚本中,所以我们也将在这个章节中展示一些实际使用变量的方法。

16.1 变量的介绍

变量想象成计算机内存中的一个带有名称的盒子是一种简单的方法。你可以把任何东西放进盒子里:单个计算机名、一组服务、一个 XML 文档等等。你可以通过使用其名称来访问盒子,在访问时,你可以往里面放东西或从中检索东西。这些东西会留在盒子里,让你可以反复检索。

PowerShell 对变量没有太多的正式要求。例如,在使用变量之前,你不必明确宣布或声明你打算使用变量。你还可以更改变量的内容类型或对象:这一刻你可能在里面有一个单个进程,下一刻你可以在里面存储一个计算机名数组。变量甚至可以包含多个不同的事物,例如一组服务和一组进程(尽管我们承认在这些情况下,使用变量的内容可能会很棘手)。

16.2 在变量中存储值

在 PowerShell 中——我们确实是指一切——都被视为对象。即使是像计算机名这样的简单字符字符串也被视为对象。例如,将字符串管道到Get-Member(或其别名gm)会显示该对象是System.String类型,并且它有许多你可以与之交互的方法(我们截断以下列表以节省空间):

PS > "SRV-02" | Get-Member

这为你提供了:

   TypeName: System.String

Name                 MemberType            Definition
----                 ----------            ----------
Clone                Method                System.Object Clone(), System.O...
CompareTo            Method                int CompareTo(System.Object val...
Contains             Method                bool Contains(string value), bo...
CopyTo               Method                void CopyTo(int sourceIndex, ch...
EndsWith             Method                bool EndsWith(string value), bo...
EnumerateRunes       Method                System.Text.StringRuneEnumerato...
Equals               Method                bool Equals(System.Object obj),...
GetEnumerator        Method                System.CharEnumerator GetEnumer...
GetHashCode          Method                int GetHashCode(), int GetHashC...
GetPinnableReference Method                System.Char&, System.Private.Co...
GetType              Method                type GetType()

现在试试 Run this same command in PowerShell to see if you get the complete list of methods—and even a property—that comes with a System.String object.

虽然从技术上讲,这个字符串是一个对象,但你可能会发现人们倾向于像在 shell 中的其他所有东西一样,将其称为一个简单的值。这是因为,在大多数情况下,你关心的是字符串本身——例如前一个例子中的"SRV-02"——而你不太关心从属性中检索信息。这与一个整个进程对象是一个大型的抽象数据结构的过程不同,你通常处理的是单个属性,如VMPMNameCPUID等等。String是一个对象,但它比Process这样的对象要简单得多。

PowerShell 允许你将这些简单的值存储在变量中。为此,指定变量,并使用等号运算符——即赋值运算符——后跟你想放入变量中的任何内容。以下是一个示例:

$var = "SRV-02"

现在试试看。跟随这些例子,因为这样你就能复制我们展示的结果。你应该使用你的测试服务器的名称,而不是 SRV-02.

重要的是要注意,美元符号 ($) 不是变量名的一部分。在我们的例子中,变量名是 var。美元符号是 shell 的一个提示,表明接下来的是变量名,并且我们想要访问该变量的内容。在这种情况下,我们正在设置变量的内容。

让我们看看关于变量及其名称的一些关键点:

  • 变量名通常包含字母、数字和下划线,并且它们通常以字母或下划线开头。

  • 变量名可以包含空格,但名称必须用大括号括起来。例如,${My Variable} 表示一个名为 My Variable 的变量。个人来说,我们不喜欢包含空格的变量名,因为它们需要更多的输入,而且更难阅读。

  • 变量在 shell 会话之间不会持久存在。当你关闭 shell 时,你创建的任何变量都会消失。

  • 变量名可以相当长——长到你不必担心长度。尽量使变量名合理。例如,如果你要将计算机名放入变量中,使用 computername 作为变量名。如果一个变量将包含多个进程,那么 processes 是一个好的变量名。

  • 一些有其他脚本语言经验的用户可能习惯于使用前缀来指示变量中存储的内容。例如,strComputerName 是一种常见的变量名,意味着该变量包含一个字符串(str 部分)。PowerShell 并不关心你是否这样做,但 PowerShell 社区不再认为这是一种可取的做法。

要检索变量的内容,请使用美元符号后跟变量名,如下面的示例所示。同样,美元符号告诉 shell 你想要访问变量的 内容;跟在它后面的是变量名告诉 shell 你要访问哪个变量:

$var

这会输出

SRV-02

你几乎可以在任何情况下使用变量代替值,例如,当使用 Get-Process ID 时。命令可能看起来像这样:

Get-Process -Id 13481

这会输出

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      86.21       4.12   13481 ...80 pwsh

你可以用变量替换任何值:

$var = "13481"

Get-Process -Id $var

这会给你

NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      86.21       4.12   13481 ...80 pwsh

顺便说一下,我们意识到 var 是一个相当通用的变量名。我们通常会使用 processId,但在这个特定的情况下,我们计划在几种情况下重用 $var,所以我们决定保持其通用性。不要让这个例子阻止你在现实生活中使用更合理的变量名。我们最初可能已经将一个字符串放入 $var 中,但我们可以随时更改它:

PS > $var = 5
PS > $var | get-member
   TypeName: System.Int32
Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     int CompareTo(System.Object value), int CompareT...
Equals      Method     bool Equals(System.Object obj), bool Equals(int ...
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()

在前面的例子中,我们将一个整数放入 $var 中,然后我们将 $var 管道到 Get-Member。你可以看到,shell 将 $var 的内容识别为 System .Int32,即 32 位整数。

16.3 使用变量:引号中的趣味技巧

因为我们在谈论变量,这是一个介绍一个整洁的 PowerShell 特性的好时机。到目前为止,这本书中我们建议你通常用单引号括起字符串。这样做的原因是 PowerShell 将单引号内的一切视为字面量字符串。

考虑以下示例:

PS > $var = 'What does $var contain?'
PS > $var
What does $var contain?

在这里,你可以看到单引号内的$var被视为一个字面量。但在双引号中,情况并非如此。看看下面的技巧:

PS > $computername = 'SRV-02'
PS > $phrase = "The computer name is $computername"
PS > $phrase
The computer name is SRV-02

我们从将SRV-02存储在变量$computername中开始我们的示例。接下来,我们将"The computer name is $computername"存储在变量$phrase中。当我们这样做时,我们使用双引号。PowerShell 会自动在双引号内寻找美元符号,并用它找到的任何变量替换它们的内容。因为我们显示$phrase的内容,所以$computername变量被替换为SRV-02

这种替换动作仅在 shell 最初解析字符串时发生。此时,$phrase包含"The computer name is SRV-02"——它不包含"$computername"字符串。我们可以通过尝试更改$computername的内容来测试这一点,看看$phrase是否会更新自己:

PS > $computername = 'SERVER1'
PS > $phrase
The computer name is SRV-02

如你所见,$phrase变量保持不变。

双引号技巧的另一个方面是 PowerShell 的转义字符。这个字符是反引号(`),在美国键盘上,它位于左上角的一个键上,通常位于 Esc 键下方,有时与波浪号(~)字符在同一个键上。问题是,在某些字体中,它与单引号几乎无法区分。事实上,我们通常配置我们的 shell 使用 Consolas 字体,因为这样比使用 Lucida Console 或 Raster 字体更容易区分反引号。

让我们看看这个转义字符的作用。它移除可能与它后面的字符关联的任何特殊含义,或者在某些情况下,它给后面的字符添加特殊含义。我们有一个第一个使用的例子:

PS > $computername = 'SRV-02'
PS > $phrase = "`$computername contains $computername"
PS > $phrase
$computername contains SRV-02

当我们将字符串赋值给$phrase时,我们使用了$computername两次。第一次,我们在美元符号前加了一个反引号。这样做消除了美元符号作为变量指示符的特殊含义,使其成为一个字面量美元符号。你可以在前面的输出中看到,在最后一行,$computername被存储在变量中。我们没有在第二次使用反引号,所以$computername被替换为该变量的内容。现在让我们看看反引号可以以第二种方式工作的例子:

PS > $phrase = "`$computername`ncontains`n$computername"
PS > $phrase
$computername
contains
SRV-02

仔细观察,你会发现我们在短语中使用了`n两次——一次在第一个$computername之后,一次在contains之后。在示例中,反引号添加了特殊含义。通常,n是一个字母,但反引号在它前面时,它变成了回车和换行(想想n代表新行)。

运行 help about_escape 获取更多信息,包括其他特殊转义字符的列表。例如,你可以使用转义的 t 来插入制表符,或者使用转义的 a 来使计算机发出蜂鸣声(想想 a 代表 alert)。

16.4 在变量中存储多个对象

到目前为止,我们一直在处理包含单个对象的变量,而这些对象都是简单值。我们直接与对象本身而不是它们的属性或方法一起工作。现在让我们尝试将多个对象放入一个变量中。

做这件事的一种方法是用逗号分隔的列表,因为 PowerShell 识别这些列表为对象的集合:

PS > $computers = 'SRV-02','SERVER1','localhost'
PS > $computers
SRV-02
SERVER1
Localhost

注意,在这个例子中,我们小心地将逗号放在引号外面。如果我们把它们放在里面,我们会得到一个包含逗号和三个计算机名称的单个对象。使用我们的方法,我们得到三个不同的对象,它们都是 String 类型。正如你所看到的,当我们检查变量的内容时,PowerShell 会单独显示每个对象。

16.4.1 在变量中处理单个对象

你还可以逐个访问变量中的单个元素。为此,指定你想要的对象的索引号,用方括号表示。第一个对象始终在索引号 0,第二个在索引号 1,依此类推。你还可以使用索引 -1 来访问最后一个对象,-2 用于倒数第二个对象,依此类推。以下是一个例子:

PS > $computers[0]
SRV-02
PS > $computers[1]
SERVER1
PS > $computers[-1]
localhost
PS > $computers[-2]
SERVER1

变量本身有一个属性,可以让你看到里面有多少个对象:

$computers.count

这会导致

3

你也可以像访问变量的属性和方法一样访问变量内部对象的属性和方法。一开始,这在一个只包含单个对象的变量中更容易看到:

PS > $computername.length  
6
PS > $computername.toupper()  
SRV-02
PS > $computername.tolower()
srv-02
PS > $computername.replace('02','2020') 
SRV-2020
PS > $computername
SRV-02  

在这个例子中,我们使用本章前面创建的 $computername 变量。你可能还记得,这个变量包含一个 System.String 类型的对象,当你将字符串管道传输到 16.2 节中的 Get-Member 时,你应该已经看到了该类型的完整属性和方法列表。我们使用 Length 属性以及 ToUpper()ToLower()Replace() 方法。在每种情况下,我们都必须在方法名后跟括号,即使 ToUpper()ToLower() 不需要括号内的任何参数。此外,这些方法中的任何一个都不会改变变量中的内容——你可以在最后一行看到这一点。相反,每个方法都会根据原始对象创建一个新的 String,该对象由方法修改。

如果你想改变变量的内容?你可以很容易地为变量分配一个新的值:

PS > $computers = "SRV-02"
PS > $computers
SRV-02

PS > $computers = "SRV-03"
PS > $computers
SRV-03

16.4.2 在变量中处理多个对象

当一个变量包含多个对象时,步骤可能会变得复杂。即使变量内的每个对象都是同一类型,就像我们的 $computers 变量一样,并且你可以对每个对象调用方法,但这可能不是你想要做的。你可能想要指定变量中你想要的对象,然后访问该特定对象的属性或执行方法:

PS > $computers[0].tolower()
SRV-02
PS > $computers[1].replace('SERVER','CLIENT')
CLIENT1

再次强调,这些方法是在生成新的字符串,而不是改变变量内部的字符串。你可以通过检查变量的内容来测试这一点:

PS > $computers
SRV-02
SERVER1
Localhost

如果你想要改变变量的内容?你给现有的对象之一分配一个新值:

PS > $computers[1] = $computers[1].replace('SERVER','CLIENT')
PS > $computers
SRV-02
CLIENT1
Localhost

你可以看到在这个例子中,我们改变了变量中的第二个对象,而不是生成一个新的字符串。

16.4.3 其他处理多个对象的方法

我们想向您展示两种处理变量中包含的一组对象的属性和方法的其他选项。前面的示例仅对变量中的单个对象执行了方法。如果你想在变量中的每个对象上运行 ToLower() 方法,并将结果存储回变量,你可以这样做:

PS > $computers = $computers | ForEach-Object { $_.ToLower()}
PS > $computers
srv-02
client1
localhost

这个例子有点复杂,所以让我们在图 16.1 中分解它。我们以 $computers = 开始管道,这意味着管道的结果将存储在那个变量中。这些结果覆盖了变量中之前的内容。

图片

图 16.1 使用 ForEach-Object 对变量中包含的每个对象执行方法

管道从 $computers 被管道传输到 ForEach-Object 开始。该命令枚举管道中的每个对象(我们有三个计算机名称,它们是 string 对象)并为每个对象执行其脚本块。在脚本块中,$_ 占位符每次包含一个管道输入的对象,并且我们在每个对象上执行 ToLower() 方法。由 ToLower() 产生的新 String 对象被放入管道——以及 $computers 变量中。

你可以使用 Select-Object 来做类似的事情。这个例子选择了传递给该命令的每个对象的 Length 属性:

$computers | select-object length

这给你

Length
------
     6
     7
     9

因为属性是数字,PowerShell 将输出右对齐。

16.4.4 在 PowerShell 中展开属性和方法

你可以使用包含多个对象的变量来访问属性和方法:

$processes = Get-Process
$processes.Name 

在底层,PowerShell “看到”你试图在示例中访问属性。它还看到 $processes 中的集合没有 Name 属性——但是集合中的每个单独的对象都有。因此,它隐式枚举或展开对象,并获取每个对象的 Name 属性。这相当于以下内容:

Get-Process | ForEach-Object { $_.Name }

这也等同于以下操作:

Get-Process | Select-Object –ExpandProperty Name

同样的情况也适用于方法:

$objects = Get-ChildItem ./*.txt -File
$objects.Refresh()

16.5 双引号的一些更多技巧

我们还有一个你可以使用双引号使用的酷炫技巧,这是对变量替换技巧的一种概念上的扩展。例如,假设你已经将一些进程放入了 $processes 变量中。现在你只想将第一个进程的名称放入一个字符串中:

$processes = Get-Process
$firstname = "$processes[0].name"
$firstname

这导致:

System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process System.Diagnostics.Process 
System.Diagnostics.Process System.Diagnostics.Process[0].name

哎,哎呀。示例中 $processes 后面的 [ 通常不是一个变量名中的合法字符,这会导致 PowerShell 尝试替换 $processes。这样做会将每个服务的名称都挤入你的字符串中。[0].name 这部分根本不会被替换。解决方案是将所有这些放入一个表达式中:

$processes = Get-Process | where-object {$_.Name}
$firstname = "The first name is $($processes[0].name)"
$firstname

这导致

The first name is AccountProfileR

$() 中的所有内容都被评估为正常的 PowerShell 命令,并将结果放入字符串中,替换掉任何已经存在的内容。再次强调,这仅在双引号中有效。这个 $() 构造被称为 子表达式

我们在 PowerShell 中还有一个你可以做的酷炫技巧。有时你可能会想要将一些更复杂的东西放入变量中,然后显示该变量的内容,在引号内。在 PowerShell 中,外壳足够聪明,即使你只引用单个属性或方法,也能枚举集合中的所有对象,只要集合中的所有对象都是同一类型。例如,我们将检索进程列表并将它们放入 $processes 变量中,然后只包括进程名称在双引号中:

$processes = Get-Process | where-object {$_.Name}
$var = "Process names are $processes.name"
$var

这导致

Process names are System.Diagnostics.Process (AccountProfileR) 
System.Diagnostics.Process (accountsd) System.Diagnostics.Process (adprivacyd) 
System.Diagnostics.Process (AdvertisingExte) System.Diagnostics.Process (AirPlayUIAgent) 
System.Diagnostics.Process (akd) System.Diagnostics.Process (AMPArtworkAgent) 
System.Diagnostics.Process (AMPDeviceDiscov) System.Diagnostics.Process (AMPLibraryAgent) 
System.Diagnostics.Process (amsaccountsd) System.Diagnostics.Process (APFSUserAgent) 
System.Diagnostics.Process (AppleSpell) System.Diagnostics.Process (AppSSOAgent) 
System.Diagnostics.Process (appstoreagent) System.Diagnostics.Process (askpermissiond) 
System.Diagnostics.Process (AssetCacheLocat) System.Diagnostics.Process (assistantd) 
System.Diagnostics.Process (atsd) System.Diagnostics.Process (AudioComponentR) 
System.Diagnostics.Process (backgroundtaskm) System.Diagnostics.Process (bird)

我们截断了前面的输出以节省空间,但我们希望你能理解这个概念。显然,这可能不是你想要的精确输出,但通过这个技术和我们在本节中之前展示的子表达式技术,你应该能够得到你想要的精确结果。

16.6 声明变量的类型

到目前为止,我们已经将对象放入变量中,让 PowerShell 确定我们使用的是哪种类型的对象。PowerShell 不在乎你把什么类型的对象放入盒子中。但你可能在乎。

例如,假设你有一个你期望包含数字的变量。你计划用这个数字做一些算术运算,并要求用户输入这个数字。让我们看看一个例子,你可以直接在命令行中输入:

PS > $number = Read-Host "Enter a number"
Enter a number: 100
PS > $number = $number * 10
PS > $number
100100100100100100100100100100

现在尝试一下 我们还没有向你展示 Read-Host——我们将其留到下一章,但如果你跟随这个例子,它的操作应该是显而易见的。

真的是什么情况?100 乘以 10 怎么会是 100100100100100100100100100100?那是哪种疯狂的新数学?

如果你眼尖,你可能已经发现了正在发生的事情。PowerShell 不将我们的输入视为数字;它将其视为字符串。而不是将 100 乘以 10,PowerShell 重复了字符串 "100" 10 次。因此,结果是字符串 100,连续列出 10 次。哎呀。

我们可以验证,外壳实际上是将输入作为字符串处理的:

PS > $number = Read-Host "Enter a number"
Enter a number: 100
PS > $number | Get-Member
   TypeName: System.String
Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object valu...
Contains         Method                bool Contains(string value)

是的,将 $number 传递给 Get-Member 可以确认,shell 将其视为 System.String,而不是 System.Int32。处理这个问题的方法有几个,我们将向您展示最简单的一个。

首先,我们告诉 shell $number 变量应该包含一个整数,这将迫使 shell 尝试将任何输入转换为实数。我们在以下示例中通过在变量首次使用之前立即指定所需的数据类型 int,在方括号中完成这一点:

PS > [int]$number = Read-Host "Enter a number"         ❶
Enter a number: 100
PS > $number | Get-Member
   TypeName: System.Int32                              ❷
Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     int CompareTo(System.Object value), int CompareT...
Equals      Method     bool Equals(System.Object obj), bool Equals(int ...
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()
ToString    Method     string ToString(), string ToString(string format...
PS > $number = $number * 10
PS > $number
1000                                                   ❸

❶ 强制变量为 [int]

❷ 确认变量是 Int32

❸ 变量被当作数字处理。

在这个例子中,我们使用 [int] 来强制 $number 只包含整数。在输入我们的输入后,我们将 $number 传递给 Get-Member 以确认它确实是一个整数而不是字符串。最后,您可以看到变量被当作数字处理,并且发生了乘法操作。

使用这种技术的另一个好处是,如果 shell 无法将输入转换为数字,它将抛出一个错误,因为 $number 只能存储整数:

PS > [int]$number = Read-Host "Enter a number"
Enter a number: Hello
MetadataError: Cannot convert value "Hello" to type "System.Int32". Error: 
➥ "Input string was not in a correct format."

这是一个很好的例子,说明了如何防止未来的问题,因为您可以确保 $number 将包含您期望的确切数据类型。

您可以使用许多对象类型来代替 [int],但以下列表包括您最常使用的一些类型:

  • [int]—整数

  • [single][double]—单精度和双精度浮点数(有小数部分的数字)

  • [string]—字符字符串

  • [char]—恰好一个字符(例如,[char]$c = 'X'

  • [xml]—XML 文档;分配给此的任何字符串都将被解析以确保它包含有效的 XML 标记(例如,[xml]$doc = Get-Content MyXML.xml

为变量指定对象类型是防止更复杂脚本中某些棘手的逻辑错误的好方法。以下示例显示,一旦指定了对象类型,PowerShell 就会强制执行它,直到您明确地重新类型化变量:

PS > [int]$x = 5                                          ❶
PS > $x = 'Hello'                                         ❷
MetadataError: Cannot convert value "Hello" to type "System.Int32".
➥ Error: "Input string was not in a correct format."
PS > [string]$x = 'Hello'                                 ❸
PS > $x | Get-Member
   TypeName: System.String                                ❹
Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object valu...

❶ 声明 $x 为整数

❷ 将字符串放入 $x 中创建错误

❸ 将 $x 重新类型化为字符串

❹ 确认 $x 的新类型

您可以看到我们首先声明 $x 为整数并将一个整数放入其中。当我们尝试放入一个字符串时,PowerShell 会抛出一个错误,因为它无法将那个特定的字符串转换为数字。后来我们将 $x 重新类型化为字符串,我们能够放入一个字符串。我们通过将变量传递给 Get-Member 并检查其类型名称来确认这一点。

16.7 变量操作命令

到目前为止,我们已经开始使用变量,但没有正式声明我们的意图。PowerShell 不需要高级变量声明,你不能强迫它进行声明。(有些人可能正在寻找类似 Option Explicit 的东西,但会感到失望;PowerShell 有一个叫做 Set-StrictMode 的东西,但它并不完全相同。)但是,shell 包含以下用于处理变量的命令:

  • New-Variable

  • Set-Variable

  • Remove-Variable

  • Get-Variable

  • Clear-Variable

你不需要使用这些中的任何一项,除非可能是 Remove-Variable,这对于永久删除变量很有用(你还可以在 VARIABLE: 驱动器中使用 Remove-Item 命令来删除变量)。你可以通过使用本章中到目前为止我们所使用的临时语法来执行每个其他功能——创建新变量、读取变量和设置变量。在大多数情况下,使用这些 cmdlet 没有特定的优势,因为你是在运行脚本之前强制变量赋值。这可能会对提供即时补全的工具(如 Visual Studio Code)造成问题。如果你使用正常的赋值运算符,这些复杂情况将更加准确,因为 PowerShell 可以查看你的脚本并预测变量值的样式。

如果你确实决定使用这些 cmdlet,你需要将变量名提供给 cmdlet 的 -name 参数。这仅是变量名——它不包括美元符号。你可能会想使用这些 cmdlet 的情况之一是处理所谓的超出作用域的变量。玩弄超出作用域的变量是一种不良做法,我们在这本书中没有涉及(或关于作用域的更多内容),但你可以在 shell 中运行 help about_scope 来了解更多信息。

16.8 变量最佳实践

我们已经提到了这些实践中的大部分,但这是一个快速复习它们的好时机:

  • 保持变量名既有意义又简洁。例如,$computername 是一个很好的变量名,因为它既清晰又简洁,而 $c 则是一个糟糕的名字,因为它包含的内容并不明确。变量名 $computer_to_query_for_data 对于我们的口味来说有点长。当然,它是有意义的,但你真的想一遍又一遍地输入它吗?

  • 不要在变量名中使用空格。我们知道你可以这样做,但这是一种丑陋的语法。

  • 如果一个变量只包含一种类型的对象,那么在第一次使用变量时就应该声明这一点。这有助于防止逻辑错误。假设你在一个商业脚本开发环境中工作(例如 Visual Studio Code)。在这种情况下,编辑软件可以在你告诉它变量将包含的对象类型时提供代码提示功能。

16.9 常见混淆点

我们看到的新学生最常遇到的一个困惑点是变量名。我们希望我们已经在本章中很好地解释了它,但请始终记住,美元符号不是变量名的一部分。它是向 shell 发出的一个信号,表明你想要访问变量的内容;美元符号之后的内容被视为变量名。shell 有两个解析规则,允许它捕获变量名:

  • 如果美元符号后面的字符是一个字母、数字或下划线,则变量名由美元符号之后直到下一个空白字符(可能是空格、制表符或回车符)的所有字符组成。

  • 如果美元符号后面的字符是一个开括号 {,则变量名由该括号之后直到但不包括闭括号 } 的所有内容组成。

16.10 实验项目

  1. 创建一个后台作业,从两台计算机上获取以 pwsh 开头的所有进程(如果你只有一个计算机进行实验,请使用 localhost 两次)。

  2. 当作业运行完成后,将作业的结果接收到一个变量中。

  3. 显示该变量的内容。

  4. 将变量的内容导出到 CLIXML 文件中。

  5. 获取你本地机器上当前运行的所有服务的列表,并将其保存到变量 $processes 中。

  6. $processes 替换为仅包含 bits 和打印打印机的服务。

  7. 显示 $processes 的内容。

  8. $processes 导出为 CSV 文件。

16.11 实验答案

  1. Invoke-Command {Get-Process pwsh} –computernamelocalhost,$env:computername –asjob

  2. $results = Receive-Job 4 –Keep

  3. $results

  4. $results | Export-CliXml processes.xml

  5. $processes = get-service

  6. $processes = get-service -name bits,spooler

  7. $processes

  8. $processes | export-csv -path c:\services.csv

16.12 进一步探索

抽空浏览一下本书的一些前面的章节。鉴于变量主要是为了存储你可能需要多次使用的东西而设计的,你能否在我们的前几章的主题中找到变量的用途?

例如,在第十三章中,你学习了如何创建到远程计算机的连接。在那个章节中,你几乎是一步一步地创建了、使用了并关闭了连接。将连接创建、存储在变量中并用于多个命令不是很有用吗?这仅仅是变量可以派上用场的一个例子(我们将在第二十章中向你展示如何做到这一点)。看看你是否能找到更多的例子。

17 输入和输出

到本书的这一部分,我们主要依赖 PowerShell 的原生能力来输出表格和列表。当你开始将命令组合成更复杂的脚本时,你可能希望对显示的内容有更精确的控制。你可能还需要提示用户输入。在本章中,你将学习如何收集输入以及如何显示你可能需要的任何输出。

然而,我们想指出,本章的内容仅适用于与人类眼睛和手指交互的脚本。对于无人值守运行的脚本,这些技术并不适用,因为没有人在场进行交互。

17.1 提示和显示信息

PowerShell 显示和提示信息的方式取决于它的运行方式。你知道,PowerShell 是作为一种底层的引擎构建的。

你与之交互的应用程序称为 宿主应用程序。当你在一个终端应用程序中运行 PowerShell 可执行文件时看到的命令行控制台通常被称为 控制台宿主。另一个常见的宿主称为 集成宿主,它由 Visual Studio Code 的 PowerShell 扩展提供的 PowerShell 集成控制台表示。其他非微软应用程序也可以托管 shell 的引擎。换句话说,作为用户,你与宿主应用程序交互,然后它将你的命令传递给引擎。宿主应用程序显示引擎产生的结果。

注意 另一个著名的宿主是 PowerShell worker for Azure Functions。Azure Functions 是微软 Azure 的无服务器服务,这听起来像是允许你在云中运行任意 PowerShell 脚本而不需要管理脚本运行在其中的底层环境的一种服务。这个宿主很有趣——因为它是不受监督运行的,所以这个宿主没有交互元素,与控制台或集成宿主不同。

图 17.1 阐述了引擎与各种宿主应用程序之间的关系。每个宿主应用程序负责物理显示引擎产生的任何输出,并物理收集引擎请求的任何输入。这意味着 PowerShell 可以以不同的方式显示输出和管理输入。

图 17.1 不同的应用程序能够托管 PowerShell 引擎。

我们想指出这些差异,因为有时对于新手来说可能会感到困惑。为什么一个命令在命令行窗口中表现一种方式,但在 Azure Functions 等地方表现不同?这是因为宿主应用程序决定了你与 shell 交互的方式,而不是 PowerShell 引擎。我们即将展示的命令根据你运行它们的位置会表现出略微不同的行为。

17.2 Read-Host

PowerShell 的 Read-Host 命令用于显示文本提示并从用户那里收集文本输入。由于你在上一章中第一次看到我们使用它,所以语法可能看起来很熟悉:

PS C:\> read-host "Enter a computer name"
Enter a computer name: SERVER-UBUNTU
SERVER-UBUNTU

这个例子突出了关于该命令的两个重要事实:

  • 在文本行的末尾附加一个冒号。

  • 用户输入的任何内容都将作为命令的结果返回(技术上,它被放入管道中,但稍后我们会详细介绍这一点)。

你通常会捕获输入到一个变量中,它看起来像这样:

PS C:\> $computername = read-host "Enter a computer name"
Enter a computer name: SERVER-UBUNTU

现在尝试一下 是时候开始跟进了。在这个时候,你应该在 $computername 变量中有一个有效的计算机名。除非你正在工作的计算机的名称是 SERVER-UBUNTU,否则不要使用 SERVER-UBUNTU。

17.3 Write-Host

现在你能够收集输入,你可能需要一种显示输出的方式。Write-Host 命令就是这样一种方式。它并不总是最好的方式,但它可供你使用,并且了解它是如何工作的是非常重要的。

正如图 17.2 所示,Write-Host 与其他任何命令一样在管道中运行,但它不会将任何内容放入管道。相反,它做了两件事:将一条记录写入“信息流”(别担心,我们稍后会介绍这一点!)并直接写入宿主应用程序的屏幕。

图片

图 17.2 Write-Host 跳过管道并直接写入宿主应用程序的显示。

现在,因为 Write-Host 直接写入宿主应用程序的屏幕,所以它能够通过其 -ForegroundColor-BackgroundColor 命令行参数使用交替的前景色和背景色。你可以通过运行 get-help -command write-host 来查看所有可用的颜色。

现在尝试一下 运行 Get-Help Write-HostForegroundColorBackgroundColor 参数有哪些可用的颜色?现在我们知道了有哪些颜色可用,让我们来点乐趣。

PS C:\> write-host "COLORFUL!" -Foreground yellow -BackgroundColor magenta
COLORFUL!

现在尝试一下 你需要自己运行这个命令来查看多彩的结果。

注意 并非所有支持 PowerShell 的宿主应用程序都支持交替的文本颜色,也并非所有应用程序都支持完整的颜色集。当你尝试在这样一个应用程序中设置颜色时,它通常会忽略它不喜欢或无法显示的任何颜色。这也是我们倾向于完全不依赖特殊颜色的一个原因。

Write-Host 命令有一个坏名声,因为在 PowerShell 的早期版本中,它并没有做什么。它作为一个机制,通过控制台向用户显示信息,并且不会弄脏任何流(是的,我们知道,我们一直在谈论这些讨厌的东西,我们承诺会涉及到它们)。但是从 PowerShell 5 开始,Write-Host 命令被重新设计。现在它是一个 Write-Information 命令的包装器,因为它需要向后兼容。它仍然会将文本输出到您的屏幕上,但也会将您的文本放入信息流中,以便您稍后使用。但是 Write-Host 确实有其局限性,并且可能并不总是完成这项工作的正确 cmdlet。

例如,您永远不应该使用 Write-Host 来手动格式化表格。您可以使用更好的方法来生成输出,使用能够使 PowerShell 本身处理格式的技术。我们不会在本书中深入探讨这些技术,因为它们更适合重型脚本和工具制作领域。然而,您可以查阅 Don Jones 和 Jeffery Hicks 所著的 《一个月午餐时间学习 PowerShell 脚本编程》(Manning,2017),以全面了解这些输出技术。

Write-Host 也不是产生错误消息、警告、调试消息等最佳方式——再次强调,您可以找到更多具体的方法来做这些事情,我们将在本章中介绍这些方法。您真正会使用 Write-Host 的情况是,如果您想在屏幕上显示带有花哨颜色的消息。

注意:我们经常看到人们使用 Write-Host 来显示我们所说的“温馨舒适”的消息——比如“现在连接到 SERVER2”和“测试文件夹”。我们建议您使用 Write-Verbose 消息。我们建议这样做的原因是,输出被发送到 Verbose 流(可以抑制),而不是 Information 流。

超越

我们将在第二十章中更深入地探讨 Write-Verbose 和其他 Write cmdlet。但是,如果您现在尝试 Write-Verbose,您可能会失望地发现它没有产生任何输出。好吧,默认情况下没有。

如果您计划使用 Write cmdlet,技巧是首先将其打开。例如,设置 $VerbosePreference="Continue" 以启用 Write-Verbose,以及 $Verbose-Preference="SilentlyContinue" 以抑制其输出。您会发现类似“偏好”变量用于 Write-Debug ($DebugPreference) 和 Write-Warning ($WarningPreference)。

第二十章介绍了一种更酷的 Write-Verbose 使用方法。

使用 Write-Host 可能看起来更容易,如果您愿意,您可以使用它。但请记住,通过使用其他 cmdlet,例如 Write-Verbose,您将更紧密地遵循 PowerShell 的自身模式,从而获得更一致的经验。

17.4 Write-Output

Write-Host 不同,Write-Output 可以将对象发送到管道。因为它不是直接写入显示,所以不允许你指定替代颜色或其他任何内容。实际上,Write-Output(或其别名 Write)在技术上并不是为了显示输出而设计的。正如我们所说的,它将对象发送到管道——是管道本身最终显示那些对象。图 17.3 展示了这是如何工作的。

图片

图 17.3 Write-Output 将对象放入管道,在某些情况下最终会导致这些对象被显示。

参考第十一章快速回顾对象如何从管道到屏幕的过程。让我们看看基本过程:

  1. Write-OutputString 对象 Hello 放入管道。

  2. 因为管道中没有其他内容,Hello 会移动到管道的末端,而 Out-Default 总是坐在那里。

  3. Out-Default 将对象传递给 Out-Host

  4. Out-Host 要求 PowerShell 的格式化系统格式化对象。因为在这个例子中它是一个简单的 String,格式化系统返回字符串的文本。

  5. Out-Host 将格式化后的结果放置到屏幕上。

结果与使用 Write-Host 得到的结果相似,但对象到达那里的路径不同。这个路径很重要,因为管道中可能包含其他内容。例如,考虑以下命令(你可以尝试一下):

PS C:\> write-output "Hello" | where-object { $_.length -gt 10 }

你看不到这个命令的任何输出,图 17.4 展示了原因。Hello 被放入管道。但在到达 Out-Default 之前,它必须通过 Where-Object,该对象会过滤掉任何具有小于或等于 10Length 属性的对象,在这个例子中包括我们可怜的 Hello。我们的 Hello 被从管道中移除,因为管道中已经没有其他内容留给 Out-Default,所以没有内容传递给 Out-Host,因此没有显示。将此命令与以下命令进行对比:

PS C:\> write-host "Hello" | where-object { $_.length -gt 10 }
Hello

图片

图 17.4 将对象放入管道意味着它们可以在显示之前被过滤掉。

我们所做的只是将 Write-Output 替换为 Write-Host。这次,Hello 直接显示在屏幕上,而不是进入管道。Where-Object 没有输入也不产生输出,所以 Out-DefaultOut-Host 都没有显示任何内容。但是因为 Hello 已经直接写入屏幕,所以我们仍然看到了它。

Write-Output 可能看起来很新,但事实上你一直在使用它。它是 shell 的默认 cmdlet。当你告诉 shell 做一些不是命令的事情时,shell 会将你输入的内容传递给 Write-Output

17.5 其他写入方式

PowerShell 有几种其他产生输出的方式。它们都不像 Write-Output 那样写入管道;它们的工作方式更类似于 Write-Host。但它们都以可以抑制的方式产生输出。

Shell 为这些替代输出方法中的每一个都内置了配置变量。当配置变量设置为 Continue 时,我们即将向您展示的命令确实会产生输出。当配置变量设置为 SilentlyContinue 时,相关的输出命令不会产生任何输出。表 17.1 包含了 cmdlet 的列表。

表 17.1 替代输出 cmdlet

Cmdlet 目的 配置变量
Write-Warning 默认以黄色显示警告文本,并在其前加上标签 WARNING: $WarningPreference (默认为 Continue)
Write-Verbose 显示额外的信息性文本,默认以黄色显示,并在其前加上标签 VERBOSE: $VerbosePreference (默认为 SilentlyContinue)
Write-Debug 默认以黄色显示调试文本,并在其前加上标签 DEBUG: $DebugPreference (默认为 SilentlyContinue)
Write-Error 生成错误消息 $ErrorActionPreference (默认为 Continue)
Write-Information 显示信息性消息,并允许结构化数据写入信息流 $InformationPreference (默认为 SilentlyContinue)

注意 Write-Host 在底层使用 Write-Information,这意味着 Write-Host 消息除了发送到宿主应用外,还会发送到信息流。这使得我们能够通过控制 $InformationPreference 等其他我们可以对 PowerShell 流做的事情,来使用 Write-Host 做更多的事情。

Write-Error 的工作方式略有不同,因为它会将错误写入 PowerShell 的错误流。PowerShell 还有一个 Write-Progress cmdlet 可以显示进度条,但它的工作方式完全不同。你可以自由地阅读其帮助文档以获取更多信息,以及示例;我们在这本书中不会涉及它。

要使用这些 cmdlet 中的任何一个,首先确保其相关的配置变量设置为 Continue。(如果设置为 SilentlyContinue,这是其中一些的默认设置,你将看不到任何输出。)然后使用 cmdlet 输出一个消息。

注意某些 PowerShell 宿主应用可能会将这些 cmdlet 的输出显示在不同的位置。例如,在 Azure Functions 中,调试文本被写入到 Application Insights(一个 Azure 日志报告服务)中的日志,而不是终端窗口,因为在无服务器环境中,你不会看到终端;PowerShell 脚本正在云中的某个地方运行。这样做是为了便于调试脚本,并且你可以看到输出。

17.6 实验

注意对于这个实验,你需要一台运行你选择的操作系统且安装了 PowerShell v7 或更高版本的计算机。

Write-HostWrite-Output 在使用时可能有点棘手。看看你能完成多少这些任务,如果你卡住了,查看本章末尾提供的示例答案是可以的。

  1. 使用 Write-Output 显示 100 乘以 10 的结果。

  2. 使用 Write-Host 显示 100 乘以 10 的结果。

  3. 提示用户输入一个名字,然后以黄色文本显示该名字。

  4. 提示用户输入一个名字,然后仅当该名字的长度超过五个字符时显示该名字。使用单个 PowerShell 表达式完成所有这些操作——不要使用变量。

这个实验就到这里。因为这些 cmdlet 都很简单,我们希望你在自己身上花更多的时间进行实验。一定要这样做——我们将在第 17.8 节中提供一些想法。

现在尝试一下 完成这个实验后,尝试完成附录中的复习实验 3。

17.7 实验答案

  1. write-output (100*10)

    或者直接输入公式:100*10

  2. 这些方法中的任何一种都适用:

    $a= 100*10

    Write-Host $a

    Write-Host "The value of 100*10 is $a"

    Write-Host (100*10)

  3. $name = Read-Host "Enter a name"

    Write-host $name -ForegroundColor Yellow

  4. Read-Host "Enter a name" | where {$_.length -gt 5}

17.8 进一步探索

花些时间熟悉本章中所有的 cmdlet。确保你能显示 Verbose 输出,并接受输入。从现在开始,你将经常使用本章中的命令,所以你应该阅读它们的帮助文件,并甚至为将来参考写下快速语法提示。

18 会话:减少工作量进行远程控制

在第十三章中,我们向您介绍了 PowerShell 的远程功能。在那个章节中,您使用了两个主要的命令——Invoke-CommandEnter-PSSession——来访问一对一和多对一的远程控制。这两个命令通过创建一个新的远程连接,执行您指定的任何工作,然后关闭该连接来工作。

这种方法没有问题,但不断地指定计算机名称、凭据、替代端口号等可能会让人感到疲倦。在本章中,你将了解一种更简单、更可重用的方法来处理远程控制。你还将了解使用远程控制的第三种方法,称为隐式远程控制,这将允许你通过将远程机器上的模块导入到你的远程会话中,添加代理命令。

无论何时你需要连接到远程计算机,无论是使用 Invoke-Command 还是 Enter-PSSession,你至少需要指定计算机的名称(或者名称列表,如果你在多台计算机上执行命令)。根据你的环境,你可能还需要指定替代凭据,这意味着需要输入密码。你可能还需要指定替代端口或身份验证机制,这取决于你的组织如何配置远程访问。

虽然指定这些内容并不困难,但重复这个过程可能会很繁琐。幸运的是,我们知道有一种更好的方法:可重用会话。

注意:本章中的示例只能在您有另一台计算机可以连接并且已启用 PS 远程访问的情况下完成。有关更多信息,请参考第十三章。

18.1 创建和使用可重用会话

会话是您的 PowerShell 副本与远程 PowerShell 副本之间的一种持久连接。当会话处于活动状态时,您的计算机和远程机器都会分配一小部分内存和处理器时间来维护连接。然而,在连接中涉及的网络流量很少。PowerShell 维护一个您已打开的所有会话的列表,您可以使用这些会话来调用命令或进入远程外壳。

要创建一个新的会话,请使用 New-PSSession 命令。指定计算机名称或主机名(或名称列表),如果需要,指定替代用户名、端口、身份验证机制等。我们也不应忘记,我们可以通过使用 -hostname 参数来使用 SSH 而不是 WinRM。无论如何,结果都将是一个会话对象,该对象存储在 PowerShell 的内存中:

PS C:\> new-pssession -computername srv02,dc17,print99

PS C:\> new-pssession -hostname LinuxWeb01,srv03

要检索这些会话,请运行 Get-PSSession

PS C:\> get-pssession

提示:如第十三章所述,当使用 -computername 参数时,我们使用的是 WinRM(HTTP/HTTPS)协议。当我们使用 -hostname 参数时,我们指定使用 SSH 作为我们的通信协议。

虽然这可行,但我们更喜欢创建会话并立即将它们存储在变量中以供以后访问。例如,朱莉有多个网络服务器,她通常通过使用 Invoke-Command 来定期重新配置它们。为了使过程更容易,她将这些会话存储在特定的变量中:

PS C:\> $iis_servers = new-pssession -computername web01,web02,web03          
 ➥ -credential WebAdmin

PS C:\> $web_servers = new-pssession -hostname web04,web05,web06              
 ➥ -username WebAdmin

永远不要忘记那些会话会消耗资源。如果你关闭外壳,它们会自动关闭,但如果你没有积极使用它们,即使你打算继续使用外壳进行其他任务,手动关闭它们也是一个好主意,这样你就不必在你的机器或远程机器上占用资源。

要关闭会话,请使用 Remove-PSSession 命令。例如,要仅关闭 IIS 会话,请使用以下命令:

PS C:\> $iis_servers | remove-pssession

或者,如果你想关闭所有打开的会话,请使用以下命令:

PS C:\> get-pssession | remove-pssession

这很简单。

但是一旦你启动了一些会话,你会如何使用它们?在接下来的几节中,我们假设你已经创建了一个名为 $sessions 的变量,它包含至少两个会话。我们将使用 localhostSRV02(你应该指定你自己的计算机名称)。使用 localhost 并不是作弊:PowerShell 会启动一个与自身另一个副本的真实远程会话。请记住,这只有在您已启用所有连接的计算机的远程访问时才会工作,所以如果您还没有启用远程访问,请回顾第十三章。

现在试试看 开始跟随并运行这些命令,并确保使用有效的计算机名称。如果你只有一台计算机,请使用其名称和 localhost。希望你也会有一台运行 macOS 或 Linux 的机器可以跟随。

超越

有一种酷炫的语法允许你使用一条命令创建多个会话,并且每个会话都分配给一个唯一的变量(而不是像我们之前那样将它们全部合并到一个变量中):

$s_server1,$s_server2 = new-pssession -computer SRV02,dc01

这种语法将 SRV02 的会话放入 $s_server1,将 DC01 的会话放入 $s_server2,这可以使得独立使用这些会话变得更容易。

但要小心:我们见过会话并不是按照你指定的顺序创建的情况,所以 $s_server1 可能最终包含 DC01 的会话而不是 SRV02。你可以显示变量的内容来查看它连接到哪台计算机。

这是我们如何启动会话的方法:

PS C:\> $session01 = New-PSSession -computername SRV02,localhost

PS C:\> $session02 = New-PSSession -hostname linux01,linux02 -keyfilepath 
 ➥ {path to key file} 

记住我们已经在这些计算机上启用了远程访问,Windows 机器都在同一个域中。再次提醒,如果你想要复习如何启用远程访问,请回顾第十三章。

18.2 使用会话对象进入-PSSession

好的,现在你已经了解了使用会话的所有原因,让我们看看如何具体使用它们。正如我们希望你能从第十三章回忆起来,Enter-PSSession cmdlet 是你用来与单个远程计算机建立一对一远程交互式 shell 的命令。而不是使用该 cmdlet 指定计算机名或主机名,你可以指定一个会话对象。因为我们的$session01$session02变量包含多个会话对象,我们必须使用索引(你是在第十六章首次学习如何使用索引)来指定其中一个:

PS C:\> enter-pssession -session $session010]
[SRV02]: PS C:\Users\Administrator\Documents>

你可以看到,我们的提示符已经改变,表明我们现在正在控制一台远程计算机。Exit-PSSession将我们返回到本地提示符,但会话仍然保持打开状态以供进一步使用:

[SRV02]: PS C:\Users\Administrator\Documents> exit-pssession
PS C:\>

如果你有多会话而忘记了特定会话的索引号怎么办?你可以将会话变量通过管道传递给Get-Member并检查会话对象的属性。例如,当我们通过管道将$session02传递给Get-Member时,我们得到以下输出:

PS C:\> $session01 | gm
   TypeName: System.Management.Automation.Runspaces.PSSession

Name                   MemberType     Definition
----                   ----------     ----------
Equals                 Method         bool Equals(System.Object obj)
GetHashCode            Method         int GetHashCode()
GetType                Method         type GetType()
ToString               Method         string ToString()
ApplicationPrivateData Property       psprimitivedictionary App...
Availability           Property       System.Management.Automat...
ComputerName           Property       string ComputerName {get;}
ComputerType           Property       System.Management.Automat...
ConfigurationName      Property       string ConfigurationName {get;}
ContainerId            Property       string ContainerId {get;}
Id                     Property       int Id {get;}
InstanceId             Property       guid InstanceId {get;}
Name                   Property       string Name {get;set;}
Runspace               Property       runspace Runspace {get;}
Transport              Property       string Transport {get;}
VMId                   Property       System.Nullable[guid] VMId {get;}
VMName                 Property       string VMName {get;}
DisconnectedOn         ScriptProperty System.Object DisconnectedOn...
ExpiresOn              ScriptProperty System.Object ExpiresOn {get...
IdleTimeout            ScriptProperty System.Object IdleTimeout {get=$t... 
State                  ScriptProperty System.Object State {get=$this...

在前面的输出中,你可以看到会话对象有一个ComputerName属性,这意味着你可以针对该会话进行筛选:

PS C:\> enter-pssession -session ($sessions | where { $_.computername -eq 
 ➥ 'SRV02' })
[SRV02]: PS C:\Users\Administrator\Documents>

虽然这种语法有些尴尬。如果你需要从一个变量中使用单个会话,而你又记不起哪个索引号对应哪个会话,可能更容易忘记使用变量。

即使你将会话对象存储在变量中,它们也仍然存储在 PowerShell 的开放会话主列表中。你可以通过使用Get-PSSession来访问它们:

PS C:\> enter-pssession -session (get-pssession -computer SRV02)

Get-PSSession检索名为SRV02的会话并将其传递给Enter-PSSession-session参数。

当我们第一次弄懂那种技术时,我们感到印象深刻,但它也引导我们进一步深入挖掘。我们调出了Enter-PSSession的全局帮助,并更仔细地阅读了关于-session参数的内容。以下是我们的观察结果:

-Session <System.Management.Automation.Runspaces.PSSession>
        Specifies a PowerShell session ( PSSession ) to use for the    interactive session. This parameter takes a
        session object. You can also use the Name , InstanceID , or ID parameters to specify a PSSession .

        Enter a variable that contains a session object or a command that creates or gets a session object, such as a
        `New-PSSession` or `Get-PSSession` command. You can also pipe a session object to `Enter-PSSession`. You can
        submit only one PSSession by using this parameter. If you enter a variable that contains more than one
        PSSession , the command fails.

        When you use `Exit-PSSession` or the EXIT keyword, the interactive session ends, but the PSSession that you
        created remains open and available for use.

如果你回想起第九章,你会在帮助文档的末尾发现一些有趣的管道输入信息。它告诉我们-session参数可以接受来自管道的PSSession对象。我们知道Get-PSSession会产生PSSession对象,所以以下语法也应该有效:

PS C:\> Get-PSSession -ComputerName SRV02 | Enter-PSSession
[SRV02]: PS C:\Users\Administrator\Documents>

它确实有效。我们认为这是一种检索单个会话的更优雅的方式,即使你将它们全部存储在变量中。

小贴士:将会话存储在变量中作为便利性是可行的。但请记住,PowerShell 已经存储了所有开放会话的列表。将它们存储在变量中只有在你想一次性引用多个会话时才有用,正如你将在下一节中看到的。

18.3 使用会话对象的 Invoke-Command

会话通过Invoke-Command展示了它们的有用性,你可能记得,这是用来向多个远程计算机并行发送命令(或整个脚本)的。在我们的会话存储在$session01变量中时,我们可以轻松地使用以下命令来针对它们:

PS C:\> invoke-command -command { Get-Process } -session $session01

Invoke-Command-session参数也可以接收一个括号内的命令,就像我们在前面的章节中处理计算机名时所做的那样。例如,以下命令会向列出的每个计算机的会话发送命令:

PS C:\> invoke-command -command { get-process bits } -session (get-pssession 
 ➥ –computername server1,server2,server3)

你可能期望Invoke-Command能够从管道接收会话对象,就像你知道Enter-PSSession可以那样。但查看Invoke-Command的完整帮助文档显示,它不能执行那个特定的管道技巧。真遗憾,但前面使用括号表达式提供的功能没有太复杂的语法。

18.4 隐式远程操作:导入会话

对于我们来说,隐式远程操作是我们认为最酷和最有用的——可能是*最酷和最有用的——功能,这是一个命令行界面在任何操作系统上都有过,而且几乎在 PowerShell 中几乎没有文档记录。当然,必要的命令有很好的文档记录,但它们如何组合形成这种令人难以置信的能力并没有提到。幸运的是,我们在这方面为你提供了覆盖。

让我们回顾一下场景:你已经知道微软正在将越来越多的模块与 Windows Server 和其他产品一起发货,但有时由于各种原因,你无法在本地计算机上安装这些模块。ActiveDirectory模块,首次随 Windows Server 2008 R2 一起发货,是一个完美的例子:它只存在于域控制器以及安装了远程服务器管理工具(RSAT)的服务器/客户端上。让我们通过一个单独的例子来看整个过程:

PS C:\> $session = new-pssession -comp SRV02                                   ❶
PS C:\> invoke-command -command { import-module activedirectory }              ❷
        session $session
PS C:\> import-pssession -session $session -module activedirectory -prefix rem ❸

ModuleType Name                      ExportedCommands                          ❹
---------- ----                      ----------------
Script     tmp_2b9451dc-b973-495d... {Set-ADOrganizationalUnit, Get-ADD...

❶ 建立连接

❷ 加载远程模块

❸ 导入远程命令

❹ 检查临时本地模块

下面是这个例子中发生的情况:

  1. 我们首先与安装了 Active Directory 模块的远程计算机建立会话。

  2. 我们告诉远程计算机导入其本地的 Active Directory 模块。这只是其中一个例子;我们本可以选择加载任何模块。因为会话仍然打开,模块会保留在远程计算机上。

  3. 然后我们告诉我们的计算机从那个远程会话中导入命令。我们只想导入 Active Directory 模块中的命令,并且当它们被导入时,我们希望为每个命令的名词添加一个rem前缀。这使我们能够更容易地跟踪远程命令。这也意味着命令不会与已经加载到我们的 shell 中的任何同名的命令冲突。

  4. PowerShell 在我们计算机上创建了一个临时模块,代表远程命令。命令并没有被复制过来;相反,PowerShell 为它们创建了快捷方式,而这些快捷方式指向远程机器。

现在,我们可以运行 Active Directory 模块命令或甚至请求帮助。我们不是运行New-ADUser,而是运行New-remADUser,因为我们已经将rem前缀添加到命令的名词中。这些命令在我们关闭 shell 或关闭与远程计算机的会话之前都可用。当我们打开一个新的 shell 时,我们必须重复此过程才能重新获得对远程命令的访问权限。

当我们运行这些命令时,它们不会在我们的本地计算机上执行。相反,它们被隐式地远程到远程计算机上。它为我们执行它们并将结果发送到我们的计算机。

我们可以想象一个不再需要在我们的计算机上安装管理工具的世界。我们将会避免多少麻烦。今天,您需要能够在您的计算机操作系统上运行并与您试图管理的任何远程服务器通信的工具——让所有这些匹配起来可能是无法实现的。在未来,您将不会这样做。您将使用隐式远程。服务器将通过 Windows PowerShell 提供他们的管理功能作为另一个服务。

现在来说说坏消息:通过隐式远程传递到您计算机上的结果都是反序列化的,这意味着对象的属性被复制到一个 XML 文件中,以便在网络中传输。通过这种方式接收到的对象没有任何方法。在大多数情况下,这不会成为问题,但有些模块和插件生成的对象是您打算以更程序化的方式使用的,而这些对象不适合隐式远程。我们希望您很少(如果有的话)会遇到具有这种限制的对象,因为依赖于方法违反了一些 PowerShell 设计原则。如果您确实遇到了这样的对象,您将无法通过隐式远程使用它们。

18.5 使用断开连接的会话

PowerShell v3 对其远程控制能力引入了两个改进。首先,会话变得更加稳固,这意味着它们可以承受短暂的网络中断和其他短暂的干扰。即使您没有明确使用会话对象,您也能获得这种好处。即使您已经使用了Enter-PSSession及其-ComputerName参数,技术上您仍然在使用底层的会话,因此您获得了更稳健的连接性。

v3 版本中引入的另一个新特性是您必须显式使用的:断开连接的会话。假设您坐在COMPUTER1上,以Admin1(他是域管理员组的成员)的身份登录,并创建到COMPUTER2的新连接:

PS C:\> New-PSSession -ComputerName COMPUTER2
Id Name              ComputerName  State
-- ----------------- ------------- -----
 4 Session4          COMPUTER2     Opened

然后,您可以断开该会话。您仍然在您所在的COMPUTER1上这样做,它会断开两个计算机之间的连接,但会保留在COMPUTER2上运行的 PowerShell 副本。请注意,您通过指定会话的 ID 号来完成此操作,该 ID 号在您首次创建会话时显示:

PS C:\> Disconnect-PSSession -Id 4
Id Name              ComputerName  State
-- ----------------- ------------- -----
 4 Session4          COMPUTER2     Disconnected

这显然是你需要考虑的事情——你正在COMPUTER2上运行 PowerShell 的一个副本。分配有用的空闲超时时间等变得很重要。在 PowerShell 的早期版本中,你断开连接的会话会消失,所以你没有清理工作要做。从 v3 开始,你可以在环境中随意放置正在运行的会话,这意味着你必须承担更多的责任。

但这里有个酷的地方:我们将以同一域管理员Admin1的身份登录到另一台计算机COMPUTER3,并检索在COMPUTER2上运行的会话列表:

PS C:\> Get-PSSession -computerName COMPUTER2
Id Name              ComputerName  State
-- ----------------- ------------- -----
 4 Session4          COMPUTER2     Disconnected

真是整洁,对吧?如果你以不同的用户身份登录,即使是另一位管理员,你也看不到这个会话;你只能看到你在COMPUTER2上创建的会话。但现在,既然你已经看到了它,你可以重新连接它。这将允许你重新连接到一个你故意或无意中断的会话,并且你将能够从你离开的地方继续你的会话:

PS C:\> Get-PSSession -computerName COMPUTER2 | Connect-PSSession
Id Name              ComputerName  State
-- ----------------- ------------- -----
 4 Session4          COMPUTER2     Open

让我们花些时间来谈谈管理这些会话。在 PowerShell 的 WSMan 驱动器中,你可以找到可以帮助你控制断开连接会话的设置。你还可以通过组策略集中配置这些设置中的大多数。要查找的关键设置包括以下内容:

  • 在 WSMan:\localhost\Shell:

    • IdleTimeout—指定会话在自动关闭之前可以空闲的时间。默认值约为 2,000 小时(以秒为单位),或约 84 天。

    • MaxConcurrentUsers—指定一次可以打开会话的用户数量。

    • MaxShellRunTime—确定会话可以打开的最大时间。从所有实际目的来看,默认值是无限的。请注意,如果 shell 处于空闲状态,IdleTimeout可以覆盖此设置,而不是运行命令。

    • MaxShellsPerUser—设置单个用户一次可以打开的会话数量上限。将此值乘以MaxConcurrentUsers,可以计算出所有用户在计算机上可能的最大会话数量。

  • 在 WSMan:\localhost\Service:

    • MaxConnections—设置整个远程基础设施的传入连接的上限。即使你允许每个用户或最大用户数有更多的 shell,MaxConnections也是传入连接的绝对限制。

作为管理员,你显然比标准用户有更高的责任。跟踪你的会话是你的责任,尤其是如果你会断开和重新连接。合理的时间超时设置可以帮助确保 shell 会话不会长时间空闲。

18.6 实验室

注意:对于这个实验室,你需要一台运行 PowerShell v7 或更高版本的 Windows Server 2016、macOS 或 Linux 机器。如果你只能访问客户端计算机(运行 Windows 10 或更高版本),你将无法完成这个实验室的第 6 项至第 9 项任务。

要完成这个实验,你应该有两台计算机:一台用于远程连接,另一台用于远程到。如果你只有一台计算机,请使用其计算机名来远程连接到它。这样你将获得类似的经验:

  1. 关闭你在 shell 中打开的所有会话。

  2. 建立到远程计算机的会话。将会话保存在名为 $session 的变量中。

  3. 使用 $session 变量与远程计算机建立一对一的远程 shell 会话。显示进程列表然后退出。

  4. 使用 $session 变量与 Invoke-Command 列出远程机器的时间区域。

  5. 如果你是在 Windows 客户端,使用 Get-PSSessionInvoke-Command 从远程计算机获取最近的 20 条安全事件日志条目列表。

  6. 如果你在 macOS 或 Linux 客户端,计算 /var 目录中的项目数量。任务 7-10 只能在 Windows 机器上执行。

  7. 使用 Invoke-Command$session 变量在远程计算机上加载 ServerManager 模块。

  8. 从远程计算机导入 ServerManager 模块的命令到你的计算机。给导入命令的名词添加前缀 rem

  9. 运行导入的 Get-WindowsFeature 命令。

  10. 关闭 $session 变量中的会话。

18.7 实验答案

  1. get-pssession | Remove-PSSession

  2. $session=new-pssession –computername localhost

  3. enter-pssession $session

    Get-Process

    Exit

  4. invoke-command -ScriptBlock { get-timezone } -Session $session

  5. Invoke-Command -ScriptBlock {get-eventlog -LogName System

    -Newest 20} -Session (Get-PSSession)

    Get-ChildItem -Path /var | Measure-Object | select count

  6. Invoke-Command -ScriptBlock {Import-Module ServerManager}

    -Session $session

  7. Import-PSSession -Session $session -Prefix rem

    -Module ServerManager

  8. Get-RemWindowsFeature

  9. Remove-PSSession -Session $session

18.8 进一步探索

快速盘点你的环境:你有哪些 PowerShell 启用的产品?Exchange 服务器?SharePoint 服务器?VMware vSphere?System Center 虚拟机管理器?这些和其他产品都包含 PowerShell 模块,其中许多可以通过 PowerShell 远程访问。

19 你这是在调用脚本吗?

到目前为止,您可以通过使用 PowerShell 的命令行界面完成这本书中的所有内容。您不需要编写任何脚本。这对我们来说是个大问题,因为我们看到很多管理员最初会回避脚本编写,正确地认为它是一种编程,并且正确地感觉到学习它有时可能比它的价值花费更多的时间。希望您已经看到了在 PowerShell 中不成为程序员也能完成多少事情。

但在这个时候,您可能也开始感觉到不断重新输入相同的命令将会变得相当乏味。您是对的,所以在本章中,我们将深入探讨 PowerShell 脚本编写——但我们仍然不会进行编程。相反,我们将专注于脚本,将其视为一种节省我们手指不必要的重复输入的方法。

19.1 不是编程,更像是批处理文件

大多数系统管理员在某个时候都创建过命令行批处理文件(通常具有 .bat、.cmd 或 .sh 文件扩展名)。这些文件不过是简单的文本文件(您可以使用文本编辑器,如 vi 进行编辑),其中包含要按特定顺序执行的命令列表。技术上,您将这些命令称为 脚本,因为就像好莱坞剧本一样,它们会告诉表演者(您的计算机)确切要做什么和说什么,以及按什么顺序做和说。但批处理文件很少看起来像编程,部分原因是因为 cmd.exe 壳有有限的语言,不允许编写极其复杂的脚本。

PowerShell 脚本的工作方式与 Bash 或 sh 脚本类似。列出您想要运行的命令,然后 shell 将按指定顺序执行这些命令。您可以通过从主机窗口复制命令并将其粘贴到文本编辑器中来创建脚本。我们预计您会更喜欢使用 VS Code PowerShell 扩展或您选择的第三方编辑器编写脚本。

实际上,VS Code 使脚本编写几乎与交互式使用 shell 无可区分。当使用 VS Code 时,您输入想要运行的命令或命令,然后点击工具栏中的运行按钮来执行这些命令。点击保存,您就创建了一个脚本,而无需复制和粘贴任何内容。

Heads UP 只是一个提醒,就示例而言,这一章非常侧重于 Windows 系统。

19.2 使命令可重复

PowerShell 脚本背后的理念首先是使重复运行给定命令变得更容易,而不必每次都手动重新输入。既然如此,我们需要想出一个您会想反复运行的命令,并在本章中用这个例子。我们希望使这个例子相当复杂,所以我们将从 CIM 开始,然后添加一些过滤、排序和其他内容。

到目前为止,我们将切换到使用 VS Code 而不是正常的控制台窗口,因为 VS Code 将使我们更容易将我们的命令迁移到脚本中。坦白说,VS Code 使得输入复杂的命令更容易,因为你得到的是一个全屏编辑器,而不是在控制台宿主中工作在单行内。这是我们的命令:

Get-CimInstance -class Win32_LogicalDisk -computername localhost `
-filter "drivetype=3" | Sort-Object -property DeviceID |
Format-Table -property DeviceID,
@{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
@{label='Size(GB)';expression={$_.Size / 1GB -as [int]}},
@{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

小贴士 记住,你可以使用name代替label,两者都可以缩写为单个字符,nl。但小写的L看起来可能像数字 1,所以请小心!

图 19.1 展示了我们如何在 VS Code 中输入这个命令。注意,我们通过使用布局选择最右侧的工具栏按钮来选择双栏布局。也请注意,我们格式化我们的命令,使得每行以管道字符或逗号结束。通过这样做,我们迫使 shell 将这些多行识别为单个单行命令。你可以在控制台宿主中做同样的事情,但这种格式化特别有效,因为它使得命令更容易阅读。也请注意,我们使用完整的 cmdlet 名称和参数名称,并且我们指定了每个参数名称而不是使用位置参数。所有这些都会使我们的脚本更容易阅读和跟踪,无论是对于别人还是在我们可能已经忘记了原始意图的将来。

图片

图 19.1 在 VS Code 中使用双栏布局输入和运行命令

我们通过点击运行工具栏图标(你也可以按 F5 键)来运行命令进行测试,我们的输出显示它运行得非常完美。在 VS Code 中有一个小技巧:你可以突出显示命令的一部分并按 F8 键来只运行突出显示的部分。因为我们已经格式化命令,使得每行只有一个明确的命令,这使得我们可以逐个测试我们的命令。我们可以独立地突出显示并运行第一行。如果我们对输出满意,我们可以突出显示第一行和第二行并运行它们。如果一切按预期工作,我们可以运行整个命令。

到目前为止,我们可以保存这个命令——现在我们可以开始称它为脚本了。我们将它保存为 Get-DiskInventory.ps1。我们喜欢给脚本起类似于 cmdlet 风格的动词-名词名称。你可以看到这个脚本开始看起来和运行得越来越像 cmdlet,所以给它一个 cmdlet 风格的名称是有意义的。

19.3 命令参数化

当你考虑反复运行一个命令时,你可能会意识到命令的一部分可能需要不时地更改。例如,假设你想将 Get-DiskInventory.ps1 分发给一些不太熟悉 PowerShell 的同事。这是一个复杂且难以输入的命令,他们可能会欣赏将其打包到一个更容易运行的脚本中。但是,按照目前的编写方式,脚本只针对本地计算机运行。你可以想象,你的同事中有些人可能想要从一台或多台远程计算机获取磁盘库存。

一个选择是让他们打开脚本并更改 -computername 参数的值。但是,他们可能完全不愿意这样做,而且有可能他们会更改其他内容,从而完全破坏脚本。更好的做法是提供一个正式的方式来让他们传递不同的计算机名称(或一组名称)。在这个阶段,你需要确定在运行命令时可能需要更改的事物,并将这些事物替换为变量。

我们现在将计算机名称变量设置为静态值,以便我们仍然可以测试脚本。以下是我们的修改后的脚本。

列表 19.1 Get-DiskInventory.ps1,带有参数化命令(仅限 Windows)

$computername = 'localhost                      ❶
Get-CimInstance -class Win32_LogicalDisk `      ❷
 -computername  $computername `                 ❸
 -filter "drivetype=3" |
 Sort-Object -property DeviceID |
 Format-Table -property DeviceID,
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB)';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

❶ 设置一个新变量

❷ 使用反引号断行

❸ 使用变量

我们在这里做了三件事,其中两件是功能性的,一件是纯粹的美观性的:

  • 我们添加了一个变量 $computername,并将其设置为 localhost。我们注意到大多数接受计算机名称的 PowerShell 命令使用参数名称 -computername,我们希望重复使用这个约定,这就是我们选择这个变量名称的原因。

  • 我们将 -computername 参数的值替换为我们的变量。目前,脚本应该与之前完全相同(我们已经测试过以确保如此),因为我们把 localhost 放入了 $computername 变量中。

  • -computername 参数及其值之后添加了一个反引号。这会取消,或去除,行尾换行符的特殊含义。这告诉 PowerShell 下一个物理行是同一命令的一部分。当行以管道字符或逗号结束时,你不需要这样做,但为了将代码放入本书中,我们需要在管道字符之前断行。这将仅当反引号字符是行上的最后一个字符时才有效!

列表 19.2 Get-FilePath.ps1,带有参数化命令(跨平台)

$filePath = '/usr/bin/'                           ❶
get-childitem -path $filepath | get-filehash |    ❷
Sort-Object hash | Select-Object -first 10

❶ 设置一个新变量

❷ 在管道符号后断行并使用变量

我们在这里做了三件事,其中两件是功能性的,一件是纯粹的美观性的:

  • 我们添加了一个变量 $filepath,并将其设置为 /usr/bin。我们注意到 Get-ChildItem 命令接受一个路径参数名称 -path,我们希望重复使用这个约定,这就是我们选择这个变量名称的原因。

  • 我们将 -path 参数的值替换为我们自己的变量。目前,脚本应该与之前运行完全相同(我们已经测试过以确保如此),因为我们留空了 path 参数,它将在当前工作目录中运行。

  • 如果您需要将命令分成多行,最好的方法是在管道符号后放置一个换行符。PowerShell 知道,如果管道符号旁边没有内容,则下一行代码将是上一行的延续。如果您有一个非常长的管道,这可能会非常有帮助。

Get-Process | Sort-Object        ❶

Get-Process |                    ❷
Sort-Object

Get-Process `                    ❸
 | Sort-Object                  Starting the line with a pipeline symbol

❶ 显示我们的原始命令

❷ 在管道符号处中断命令

❸ 在反引号处中断命令

TIP 在您做出任何更改后,运行您的脚本以验证它仍然可以正常工作。我们总是在做出任何类型的更改后这样做,以确保我们没有引入随机的打字错误或其他错误。

19.4 创建参数化脚本

现在我们已经确定了脚本中可能随时间变化的部分,我们需要提供一个方法让其他人可以指定这些元素的新值。我们需要将硬编码的 $computername 变量转换为输入参数。PowerShell 使这变得很容易。

列表 19.3 Get-DiskInventory.ps1,带有输入参数

param (
  $computername = 'localhost'      ❶
)
Get-CimInstance -class Win32_LogicalDisk -computername $computername `
 -filter "drivetype=3" |
 Sort-Object -property DeviceID |
 Format-Table -property DeviceID,
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

❶ 使用 param 块

我们所做的只是在我们变量的声明周围添加了一个 Param() 块。这定义了 $computername 为一个参数,并指定如果脚本在没有指定计算机名称的情况下运行,则使用 localhost 作为默认值。您不必提供默认值,但我们喜欢在可以想到合理值的情况下这样做。

以这种方式声明的所有参数都是命名和位置参数,这意味着我们现在可以从命令行以以下任何一种方式运行脚本:

PS C:\> .\Get-DiskInventory.ps1 SRV-02
PS C:\> .\Get-DiskInventory.ps1 -computername SRV02
PS C:\> .\Get-DiskInventory.ps1 -comp SRV02

在第一种情况下,我们以位置方式使用参数,提供了值但没有参数名称。在第二种和第三种情况下,我们指定了参数名称,但在第三种情况下,我们根据 PowerShell 的正常参数名称缩写规则缩短了该名称。请注意,在这三种情况下,我们必须指定脚本的路径(.\,即当前文件夹),因为外壳不会自动搜索当前目录以查找脚本。

您可以通过用逗号分隔来指定所需的所有参数。例如,假设我们还想参数化筛选条件。目前,它只检索类型为 3 的逻辑磁盘,这代表固定磁盘。我们可以将其更改为参数,如下所示。

列表 19.4 Get-DiskInventory.ps1,带有附加参数

param (
  $computername = 'localhost',
  $drivetype = 3                                         ❶
)
Get-CimInstance -class Win32_LogicalDisk -computername $computername `
 -filter "drivetype=$drivetype" |                        ❷
 Sort-Object -property DeviceID |
 Format-Table -property DeviceID,
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

❶ 指定附加参数

❷ 使用参数

注意,我们利用了 PowerShell 在双引号内替换变量的值的能力(你可以在第十六章中了解到这个技巧)。我们可以用这三种原始方式中的任何一种来运行这个脚本,尽管我们也可以省略任何一个参数,如果我们想使用它的默认值。以下是一些排列组合:

PS C:\> .\Get-DiskInventory.ps1 SRV1 3
PS C:\> .\Get-DiskInventory.ps1 -ComputerName SRV1 -drive 3
PS C:\> .\Get-DiskInventory.ps1 SRV1
PS C:\> .\Get-DiskInventory.ps1 -drive 3

在第一种情况下,我们按参数在 Param() 块中声明的顺序位置指定了两个参数。在第二种情况下,我们为两个参数指定了缩写参数名称。第三次,我们完全省略了 -drivetype,使用默认值 3。在最后一种情况下,我们省略了 -computername,使用默认值 localhost

19.5 记录你的脚本

只有真正自私的人才会创建一个有用的脚本却不告诉任何人如何使用它。幸运的是,PowerShell 通过使用注释使将帮助信息添加到脚本变得容易。你可以在脚本中添加典型的编程风格注释,但如果你使用完整的 cmdlet 和参数名称,有时脚本的操作就会很明显。然而,通过使用特殊的注释语法,你可以提供类似于 PowerShell 自身帮助文件的帮助信息。以下列表显示了我们对脚本所做的添加。

列表 19.5 向 Get-DiskInventory.ps1 添加帮助

<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses CIM to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -computername SRV02 -drivetype 3
#>
param (
  $computername = 'localhost',
  $drivetype = 3
)
Get-CimInstance -class Win32_LogicalDisk -computername $computername `
 -filter "drivetype=$drivetype" |
 Sort-Object -property DeviceID |
 Format-Table -property DeviceID,
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

PowerShell 忽略任何跟随 # 符号的行,这意味着 # 将一行标记为注释。我们还可以使用 <# #> 块注释语法,因为我们有多个注释行,并且想避免每行都使用单独的 # 字符。

现在我们可以切换到正常的控制台宿主,并通过运行 help .\Get-DiskInventory.ps1 来请求帮助(再次提醒,你必须提供路径,因为这是一个脚本而不是内置的 cmdlet)。图 19.2 展示了结果,这证明了 PowerShell 正在读取这些注释并创建标准的帮助显示。

图片

图 19.2 使用正常的 help 命令查看帮助

我们甚至可以运行 help .\Get-DiskInventory -full 来获取完整的帮助,包括我们示例中的参数信息。

这些特殊的注释被称为 基于注释的帮助。除了 .DESCRIPTION.SYNOPSIS 以及我们之前使用的其他关键字之外,还有一些其他关键字。要获取完整的列表,请在 PowerShell 中运行 help about_comment_based_help

19.6 一个脚本,一个管道

我们通常告诉人们,脚本中的任何内容都会像你手动输入到 shell 中一样运行,或者如果你将脚本复制到剪贴板并将其粘贴到 shell 中。但这并不完全正确。考虑这个简单的脚本:

Get-Process
Get-UpTime

只需两个命令。但如果你手动将这些命令输入到 shell 中,并在每个命令后按 Enter 键会发生什么?

现在尝试一下 在你的机器上运行这些命令以查看结果。它们会生成相当长的输出,不适合在这个书中展示,甚至不适合在屏幕截图上展示。

当你单独运行命令时,你为每个命令创建一个新的管道。在每个管道的末尾,PowerShell 会查看需要格式化的内容并创建你无疑已经看到的表格。关键是每个命令都在单独的管道中运行。图 19.3 说明了这一点:两个完全独立的命令、两个单独的管道、两个格式化过程和两组不同的结果。

图 19.3 单个控制窗口中的两个命令、两个管道和两组输出

你可能会认为我们花这么多时间解释显然的事情很疯狂,但这是很重要的。当你单独运行这两个命令时,会发生以下情况:

  1. 你运行Get-Process

  2. 命令将Process对象放入管道。

  3. 管道以Out-Default结束,它拾取对象。

  4. Out-Default将对象传递给Out-Host,它调用格式化系统以产生文本输出(你已在第十一章中学过)。

  5. 文本输出显示在屏幕上。

  6. 你运行Get-UpTime

  7. 命令将TimeSpan对象放入管道。

  8. 管道以Out-Default结束,它拾取对象。

  9. Out-Default将对象传递给Out-Host,它调用格式化系统以产生文本输出。

  10. 文本输出显示在屏幕上。

所以你现在看到的是一个包含两个命令结果的屏幕。我们希望你将这两个命令放入一个脚本文件中。命名为 Test.ps1 或其他简单的名字。在你运行脚本之前,将这些两个命令复制到剪贴板。在你的编辑器中,你可以突出显示这两行文本并按 Ctrl-C 将它们复制到剪贴板。

在剪贴板上有了这些命令后,转到 PowerShell 控制台宿主并按 Enter。这将从剪贴板粘贴命令到 shell 中。它们应该以完全相同的方式执行,因为换行符也被粘贴了。再次强调,你正在两个不同的管道中运行两个不同的命令。

现在回到你的编辑器并运行脚本。结果不同,对吧?为什么?

在 PowerShell 中,每个命令都在单个管道中执行,包括脚本。在脚本内部,任何产生管道输出的命令都将写入单个管道:脚本本身运行的管道。看看图 19.4。

图 19.4 脚本中的所有命令都在该脚本的单个管道中运行。

我们将尝试解释发生了什么:

  1. 脚本运行Get-Process

  2. 命令将Process对象放入管道。

  3. 脚本运行Get-UpTime

  4. 命令将TimeSpan对象放入管道。

  5. 管道以Out-Default结束,它拾取两种类型的对象。

  6. Out-Default将对象传递给Out-Host,它调用格式化系统以产生文本输出。

  7. 因为 Process 对象优先,shell 的格式化系统会选择适合进程的格式。这就是为什么它们看起来很正常。但随后 shell 遇到了 TimeSpan 对象。此时它无法生成一个全新的表格,所以最终它生成了一个列表。

  8. 文本输出会显示在屏幕上。

这种不同的输出发生是因为脚本将两种类型的对象写入单个管道。这是将命令放入脚本和手动运行它们之间的重要区别:在脚本中,你只有一个管道可以操作。通常,你的脚本应该努力输出一种类型的对象,以便 PowerShell 可以生成合理的文本输出。

19.7 快速了解作用域

我们需要讨论的最后一个主题是 作用域。作用域是某些 PowerShell 元素(主要是别名、变量和函数)的一种容器形式。

shell 本身是顶级作用域,被称为 全局作用域。当你运行一个脚本时,会围绕该脚本创建一个新的作用域,这被称为 脚本作用域。脚本作用域是全局作用域的子级——或者称为 子作用域。函数也有它们自己的 私有作用域,我们将在本书后面的章节中介绍。

图 19.5 展示了这些作用域关系,其中全局作用域包含其子级,这些子级又包含它们自己的子级,依此类推。

图 19.5 全局、脚本和函数(私有)作用域

作用域只存在于执行作用域内内容的需要时间内。全局作用域只存在于 PowerShell 运行期间,脚本作用域只存在于脚本运行期间,依此类推。当任何内容停止运行时,作用域就会消失,其中的一切也随之消失。PowerShell 对作用域元素(如别名、变量和函数)有特定的——有时是令人困惑的——规则,但主要规则是这样的:如果你尝试访问一个作用域元素,PowerShell 会查看它是否存在于当前作用域中。如果不存在,PowerShell 会查看它是否存在于当前作用域的父级中。它会继续向上遍历关系树,直到到达全局作用域。

提示:为了获得正确的结果,仔细且精确地遵循这些步骤非常重要。

让我们看看实际操作。按照以下步骤进行:

  1. 关闭你可能打开的任何 PowerShell 或 PowerShell 编辑器窗口,以便你可以从头开始。

  2. 打开一个新的 PowerShell 窗口和一个新的 VS Code 窗口。

  3. 在 VS Code 中创建一个包含一行代码的脚本:Write $x

  4. 将脚本保存为 C:\Scope.ps1。

  5. 在常规 PowerShell 窗口中,运行脚本 C:\Scope.ps1。你不应该看到任何输出。当脚本运行时,会为它创建一个新的作用域。$x 变量在那个作用域中不存在,所以 PowerShell 会去父级作用域——全局作用域——查看 $x 是否存在那里。它也不存在那里,所以 PowerShell 决定 $x 是空的,并将这个(意味着,没有内容)作为输出写入。

  6. 在正常的 PowerShell 窗口中,运行 $x = 4。然后再次运行 C:\Scope.ps1。这次,你应该看到输出为 4。变量 $x 仍然没有在脚本作用域中定义,但 PowerShell 能够在全球作用域中找到它,所以脚本使用了那个值。

  7. 在 VS Code 中,将 $x = 10 添加到脚本顶部(在现有的 Write 命令之前),并保存脚本。

  8. 在正常的 PowerShell 窗口中,再次运行 C:\Scope.ps1。这次,你会看到输出为 10。这是因为 $x 在脚本作用域内定义,shell 不需要在全球作用域中查找。现在在 shell 中运行 $x。你会看到 4,这证明了脚本作用域内 $x 的值不会影响全局作用域内 $x 的值。

这里的一个重要概念是,当一个作用域定义了一个变量、别名或函数时,该作用域将失去对父作用域中具有相同名称的任何变量、别名或函数的访问。本地定义的元素总是 PowerShell 使用的。例如,如果你在脚本中放入 New-Alias Dir Get-Service,那么在这个脚本中,别名 Dir 将运行 Get-Service 而不是通常的 Get-ChildItem。(实际上,shell 可能不会让你这样做,因为它保护内置别名不被重新定义。)通过在脚本的作用域中定义别名,你防止 shell 前往父作用域并找到正常的默认 Dir。当然,脚本对 Dir 的重新定义只会持续到该脚本的执行,全局作用域中定义的默认 Dir 将保持不受影响。

很容易让这种作用域的东西让你感到困惑。你可以通过从不依赖当前作用域之外的任何内容来避免困惑。所以在你尝试在脚本中访问一个变量之前,确保你已经在相同的作用域内给它赋了一个值。Param() 块中的参数是这样做的一种方式,还有许多其他方式可以将值和对象放入变量中。

19.8 实验室

注意:对于这个实验室,你需要任何运行 Windows 10 或 Server 2019 且具有 PowerShell v7 或更高版本的计算机。

以下命令是你要添加到脚本中的。你应该首先确定任何应该参数化的元素,例如计算机名。你的最终脚本应该定义参数,并在脚本中创建基于注释的帮助。运行你的脚本以测试它,并使用 Help 命令确保你的基于注释的帮助正常工作。不要忘记阅读本章中引用的帮助文件以获取更多信息。以下是命令:

Get-CimInstance -classname Win32_LogicalDisk -filter "drivetype=3" |
Where { ($_.FreeSpace / $_.Size) -lt .1 } |
Select -Property DeviceID,FreeSpace,Size

这里有一个提示:至少需要参数化两块信息。这个命令的目的是列出所有小于给定空闲磁盘空间的驱动器。显然,你并不总是想针对localhost(在我们的例子中,我们使用 PowerShell 的%computername%),你可能不想将 10%(即.1)作为你的空闲空间阈值。你也可以选择参数化驱动器类型(在这里是 3),但在这个实验中,请将其硬编码为值3

19.9 实验室答案

<#
.Synopsis
Get drives based on percentage free space
.Description
This command will get all local drives that have less than the specified 
➥ percentage of free space available.
.Parameter Computername
The name of the computer to check. The default is localhost.
.Parameter MinimumPercentFree
The minimum percent free diskspace. This is the threshold. The default value 
➥ is 10\. Enter a number between 1 and 100.
.Example
PS C:\> Get-DiskSize -minimum 20
Find all disks on the local computer with less than 20% free space.
.Example
PS C:\> Get-DiskSize -Computername SRV02 -minimum 25
Find all local disks on SRV02 with less than 25% free space.
#>
Param (
    $Computername = 'localhost',
    $MinimumPercentFree = 10
)
#Convert minimum percent free
$minpercent = $MinimumPercentFree / 100
Get-CimInstance -classname Win32_LogicalDisk –computername $computername `
    -filter "drivetype=3" |
Where { $_.FreeSpace / $_.Size –lt $minpercent } |
Select –Property DeviceID, FreeSpace, Size

20 提高你的参数化脚本

在上一章中,我们给你留下了一个相当酷的脚本,它已经被参数化了。参数化脚本的想法是,其他人可以运行脚本而无需担心或修改其内容。脚本用户通过指定的接口——参数——提供输入,而且他们只能更改这些。在这一章中,我们将更进一步。

注意:仅就示例而言,本章非常侧重于 Windows。

20.1 起点

为了确保我们处于同一页面上,让我们同意以列表 20.1 作为起点。这个脚本具有基于注释的帮助、两个输入参数以及使用这些输入参数的命令。自从上一章以来,我们做了一处小的改动:我们将输出改为选择对象,而不是我们在第 19.4 节中使用的格式化表格。

列表 20.1 起点:Get-DiskInventory.ps1

<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses CIM to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -ComputerName SRV02 -drivetype 3
#>
param (
  $computername = 'localhost',
  $drivetype = 3
)
Get-CimInstance -class Win32_LogicalDisk -ComputerName $computername `
 -filter "drivetype=$drivetype" |
 Sort-Object -property DeviceID |
 Select-Object -property DeviceID,                                       ❶
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB)';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

❶ 注意到我们使用了 Select-Object 而不是在第十九章中使用的 Format-Table。

为什么我们切换到 Select-Object 而不是 Format-Table?我们通常认为编写生成预格式化输出的脚本是一个坏主意。毕竟,如果有人需要将数据放在 CSV 文件中,而脚本输出的是格式化表格,那么这个人就会很不幸。通过这次修订,我们可以这样运行我们的脚本来获取格式化表格:

PS C:\> .\Get-DiskInventory | Format-Table

或者我们可以这样运行它来获取那个 CSV 文件:

PS C:\> .\Get-DiskInventory | Export-CSV disks.csv

重点是输出对象(Select-Object 所做的),而不是格式化显示,从长远来看,使我们的脚本更加灵活。

20.2 让 PowerShell 做艰苦的工作

我们将通过在脚本中添加一行来开启一些 PowerShell 魔法。从技术上讲,这使我们的脚本变成了一个 高级脚本,从而启用了一系列有用的 PowerShell 功能。以下列表显示了修订版。

列表 20.2 将 Get-DiskInventory.ps1 转换为高级脚本

<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses WMI to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -ComputerName SRV02 -drivetype 3
#>
[CmdletBinding()]        ❶
param (
  $computername = 'localhost',
  $drivetype = 3
)
Get-CimInstance -class Win32_LogicalDisk -ComputerName $computername `
 -filter "drivetype=$drivetype" |
 Sort-Object -property DeviceID |
 Select-Object -property DeviceID,
     @{name='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{name='Size(GB)';expression={$_.Size / 1GB -as [int]}},
     @{name='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

❶ [CmdletBinding()] 必须是注释帮助之后的第一个行;PowerShell 知道在这里寻找它。

正如所提到的,确保 [CmdletBinding()] 指令是脚本中注释帮助之后的第一个行是非常重要的。PowerShell 只知道在那里寻找它。通过这个改动,脚本将继续正常运行,但我们已经启用了一些很酷的功能,我们将在下一部分进行探索。

20.3 使参数成为强制性的

从这里,我们可以说我们已经完成了,但这现在不会很有趣,对吧?我们的脚本之所以以现有的形式存在,是因为它为 -ComputerName 参数提供了一个默认值——我们不确定是否真的需要它。我们更愿意提示输入该值,而不是依赖于硬编码的默认值。幸运的是,PowerShell 使这变得很容易——再次,只需添加一行即可,如下一列表所示。

列表 20.3 给 Get-DiskInventory.ps1 添加强制参数

<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses WMI to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -ComputerName SRV02 -drivetype 3
#>
[CmdletBinding()]
param (
  [Parameter(Mandatory=$True)]   
  [string]$computername,        ❶
  [int]$drivetype = 3
)
Get-CimInstance -class Win32_LogicalDisk -ComputerName $computername `
 -filter "drivetype=$drivetype" |
 Sort-Object -property DeviceID |
 Select-Object -property DeviceID,
     @{name='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{name='Size(GB)';expression={$_.Size / 1GB -as [int]}},
     @{name='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

[Parameter(Mandatory=$True)]装饰器将使 PowerShell 在运行此脚本的人忘记提供计算机名称时提示输入。

除此之外

当某人运行你的脚本但没有提供必需的参数时,PowerShell 将提示他们提供该参数。有两种方法可以使 PowerShell 的提示对用户更有意义。

首先,使用一个好的参数名称。提示某人填写comp并不像提示他们提供computerName那样有帮助,因此尽量使用描述性且与其他 PowerShell 命令一致的参数名称。

您还可以添加一条帮助信息:

[Parameter(Mandatory=$True,HelpMessage="Enter a computer name to query")

一些 PowerShell 宿主将帮助信息作为提示的一部分显示,这使得对用户来说更加清晰,但并非每个宿主应用程序都会使用此属性,所以如果你在测试时并不总是看到它,请不要沮丧。我们仍然喜欢在编写供其他人使用的内容时包含它。这永远不会有害。但为了简洁,我们将在本章的运行示例中省略HelpMessage

就这样一个装饰器[Parameter(Mandatory=$True)],将使 PowerShell 在运行此脚本的人忘记提供计算机名称时提示输入。为了进一步帮助 PowerShell,我们给我们的两个参数都指定了数据类型:-ComputerName[string]-drivetype[int](表示整数)。

将这些类型的属性添加到参数中可能会变得令人困惑,因此让我们更仔细地检查Param()块的语法——参见图 20.1。

图片

图 20.1 解构Param()块语法

这里有一些需要注意的重要事项:

  • 所有参数都包含在Param()块的括号内。

  • 单个参数可以包含多个装饰器,这些装饰器可以放在一行上,也可以像我们在图 20.1 中那样放在不同的行上。我们认为多行更易于阅读——但重要的是它们都是一起的。在这里,Mandatory属性仅修改-ComputerName;它对-drivetype没有任何影响。

  • 除了最后一个参数名之外,每个参数名后面都跟着一个逗号。

  • 为了提高可读性,我们喜欢在参数之间添加一个空行。我们认为这有助于更好地在视觉上分离它们,使Param()块不那么令人困惑。

  • 我们将每个参数定义为一个变量$computername$drivetype,但运行此脚本的人将把它们当作正常的 PowerShell 命令行参数,如-ComputerName-drivetype

现在试试看 Try 将脚本保存到列表 20.3 中,并在 shell 中运行它。不要指定-ComputerName参数,看看 PowerShell 如何提示你提供该信息。

20.4 添加参数别名

当你想到计算机名时,computername 是第一个出现在你脑海中的吗?可能不是。我们使用 -ComputerName 作为参数名,因为它与其他 PowerShell 命令的书写方式保持一致。看看 Get-ServiceGet-CimInstanceGet-Process 等,你会在它们上面看到 -ComputerName 参数。所以我们选择了这个。

但是,如果你更倾向于使用 -host 这样的名称,你可以将其添加为参数的另一个名称或别名。它只是另一个装饰器,如以下列表所示。然而,不要使用 -hostname,因为在使用 PowerShell 远程时,它会指示 SSH 连接。

列表 20.4 向 Get-DiskInventory.ps1 添加参数别名

<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses WMI to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -ComputerName SRV02 -drivetype 3
#>
[CmdletBinding()]
param (
  [Parameter(Mandatory=$True)]
  [Alias('host')]               ❶
  [string]$computername,
  [int]$drivetype = 3
)
Get-CimInstance -class Win32_LogicalDisk -ComputerName $computername `
 -filter "drivetype=$drivetype" |
 Sort-Object -property DeviceID |
 Select-Object -property DeviceID,
     @{name='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{name='Size(GB)';expression={$_.Size / 1GB -as [int]}},
     @{name='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

❶ 这个添加是 -ComputerName 参数的一部分;它对 -drivetype 没有影响。

通过这个小的改动,我们现在可以运行以下命令:

PS C:\> .\Get-DiskInventory -host SRV02

注意 记住,你只需要输入足够多的参数名,以便 PowerShell 能够理解你指的是哪个参数。在这个例子中,-host 已经足够 PowerShell 识别 -hostname。我们也可以输入完整的名称。

再次强调,这个新的添加是 -ComputerName 参数的一部分;它对 -drivetype 没有影响。现在 -ComputerName 参数的定义占据了三行文本,尽管我们也可以将所有内容放在一行中:

[Parameter(Mandatory=$True)][Alias('hostname')][string]$computername,

我们只是觉得这样读起来更困难。

20.5 验证参数输入

让我们稍微玩一下 -drivetype 参数。根据 Win32_LogicalDisk WMI 类的 MSDN 文档(搜索类名,其中一个顶部结果将是文档),驱动器类型 3 是本地硬盘。类型 2 是可移动磁盘,它应该也有大小和可用空间测量。驱动器类型 1、4、5 和 6 没有那么有趣(还有谁现在还在使用 RAM 驱动器,类型 6?),在某些情况下,它们可能没有可用空间(类型 5,对于光盘)。因此,我们希望阻止任何人在运行我们的脚本时使用这些类型。这个列表显示了我们需要做的微小改动。

列表 20.5 向 Get-DiskInventory.ps1 添加参数验证

<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses WMI to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -ComputerName SRV02 -drivetype 3
#>
[CmdletBinding()]
param (
  [Parameter(Mandatory=$True)]
  [Alias('hostname')]   
  [string]$computername,
  [ValidateSet(2,3)]       ❶
  [int]$drivetype = 3
)
Get-CimInstance -class Win32_LogicalDisk -ComputerName $computername `
 -filter "drivetype=$drivetype" |
 Sort-Object -property DeviceID |
 Select-Object -property DeviceID,
     @{name='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{name='Size(GB)';expression={$_.Size / 1GB -as [int]}},
     @{name='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

❶ 我们在脚本中添加了 [ValidateSet(2,3)] 来告诉 PowerShell,只有两个值,2 和 3,是被我们的 -drivetype 参数接受的,并且 3 是默认值。

你可以添加许多其他的验证技术到一个参数中,并且当这样做有意义时,你可以向同一个参数添加多个。运行 help about_functions_advanced_parameters 获取完整的列表。我们现在继续使用 ValidateSet()

现在尝试一下 保存这个脚本并再次运行。尝试指定 -drivetype 5 并看看 PowerShell 会做什么。

20.6 通过冗长的输出添加温暖和舒适感

在第十七章中,我们提到了我们更喜欢使用Write-Verbose而不是Write-Host来生成一些喜欢看到脚本产生的逐步进度信息的用户的信息。现在是一个真正的例子的时候了。我们在下面的列表中添加了一些详细输出的消息。

列表 20.6 向 Get-DiskInventory.ps1 添加详细输出

<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses WMI to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -ComputerName SRV02 -drivetype 3
#>
[CmdletBinding()]
param (
  [Parameter(Mandatory=$True)]
  [Alias('hostname')]   
  [string]$computername,
  [ValidateSet(2,3)]
  [int]$drivetype = 3
)
Write-Verbose "Connecting to $computername"              ❶
Write-Verbose "Looking for drive type $drivetype"        ❶
Get-CimInstance -class Win32_LogicalDisk -ComputerName $computername `
 -filter "drivetype=$drivetype" |
 Sort-Object -property DeviceID |
 Select-Object -property DeviceID,
     @{name='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{name='Size(GB)';expression={$_.Size / 1GB -as [int]}},
     @{name='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}
Write-Verbose "Finished running command"                 ❶

❶ 添加了三个详细输出消息

现在以两种方式尝试运行此脚本。第一次尝试不应该显示任何详细输出:

PS C:\> .\Get-DiskInventory -ComputerName localhost

现在是第二次尝试,我们希望显示详细输出:

PS C:\> .\Get-DiskInventory -ComputerName localhost -verbose

现在试试看 这当你亲自看到的时候会更酷。按照我们在这里展示的方式运行脚本,亲自看看差异。

这有多酷?当你想要详细的输出(如代码列表 20.6 所示),你可以得到它——而且你根本不需要编写-Verbose参数!当你添加[Cmdlet-Binding()]时,它会免费提供。而且一个真正酷的地方是,它还会激活脚本中每个命令的详细输出!所以任何设计用于产生详细输出的命令都将“自动”这样做。这种技术使得开启和关闭详细输出变得很容易,比Write-Host更加灵活。而且你不需要与$VerbosePreference变量纠缠,以使输出显示在屏幕上。

注意,在详细输出中,我们如何利用 PowerShell 的双引号技巧:通过在双引号内包含一个变量($computername),输出能够包含变量的内容,这样我们就可以看到 PowerShell 在做什么。

20.7 实验室

这个实验室要求你回忆一下第十九章中学到的一些内容,因为你将使用以下命令,对其进行参数化,并将其转换为脚本——就像你在第十九章的实验室中所做的那样。但这次我们还想让你将-ComputerName参数设置为必填项,并给它一个host别名。让脚本在运行此命令前后显示详细输出。记住,你必须参数化计算机名——但在这个情况下,你只需要参数化这一项。

在开始修改之前,确保按原样运行命令,以确保它在你的系统上工作:

Get-CimInstance win32_networkadapter -ComputerName localhost |
 where { $_.PhysicalAdapter } |
 select MACAddress,AdapterType,DeviceID,Name,Speed

重申一下,这是你的完整任务列表:

  • 在修改之前确保命令按原样运行。

  • 参数化计算机名。

  • 使计算机名参数成为必填项。

  • 给计算机名参数一个别名,hostname

  • 添加基于注释的帮助,至少包含一个如何使用脚本的示例。

  • 在修改命令前后添加详细输出。

  • 将脚本保存为 Get-PhysicalAdapters.ps1。

20.8 实验室答案

<#
.Synopsis
Get physical network adapters
.Description
Display all physical adapters from the Win32_NetworkAdapter class.
.Parameter Computername
The name of the computer to check.
.Example
PS C:\> c:\scripts\Get-PhysicalAdapters -computer SERVER01
#>
[cmdletbinding()]
Param (
[Parameter(Mandatory=$True,HelpMessage="Enter a computername to query")]
[alias('host')]
[string]$Computername
)
Write-Verbose "Getting physical network adapters from $computername"
Get-CimInstance -class win32_networkadapter –computername $computername |
 where { $_.PhysicalAdapter } |
 select MACAddress,AdapterType,DeviceID,Name,Speed
Write-Verbose "Script finished."

除此之外

将你到目前为止学到的知识应用到我们在第十九章中制作的脚本 Get-FilePath.ps1(列表 19.2)中,

  • 使其成为一个高级函数。

  • 添加必填参数。

  • 添加详细输出。

  • 调整格式以使导出到 CSV 文件更容易。

21 使用正则表达式解析文本文件

正则表达式是那些尴尬的话题之一。我们经常有学生要求我们解释它们,结果发现——在谈话进行到一半时——他们根本不需要正则表达式。正则表达式,有时也称为正则表达式,在文本解析中很有用,这是您在 UNIX 和 Linux 操作系统中经常会做的事情。在 PowerShell 中,您通常进行较少的文本解析——您通常不太需要正则表达式。话虽如此,我们当然知道在 PowerShell 中,您需要解析文本内容,例如日志文件的情况。这就是我们本章介绍正则表达式的方式:作为一个解析文本文件的工具。

请不要误解:您可以用正则表达式做很多事情,我们将在本章末尾介绍其中的一些。但为了确保您有一个良好的预期,让我们明确指出,我们在这本书中并没有全面或详尽地介绍正则表达式。正则表达式可以变得非常复杂。它们是一项完整的技术。我们将帮助您入门,并尝试以立即适用于许多生产环境的方式做到这一点,然后如果您需要,我们将为您提供一些深入学习的指导。

我们编写本章的目的是以简化的方式向您介绍正则表达式语法,并展示 PowerShell 如何使用正则表达式。如果您想自己学习更复杂的表达式,欢迎您这样做,您将知道如何在 shell 中使用它们。

21.1 正则表达式的作用

正则表达式是用一种特定的语言编写的,其目的是定义文本模式。例如,IPv4 地址由一到三个数字、一个点、一到三个更多的数字、一个点等等组成。正则表达式可以定义这种模式,尽管它也会接受一个无效的地址,如 211.193.299.299。这就是识别文本模式和检查数据有效性的区别。

正则表达式最大的用途之一——我们本章要介绍的就是在较大的文本文件中检测特定的文本模式,例如日志文件。例如,您可能编写一个正则表达式来查找表示 Web 服务器日志文件中 HTTP 500 错误的特定文本,或者查找 SMTP 服务器日志文件中的电子邮件地址。除了检测文本模式外,您可能还会使用正则表达式来捕获匹配的文本,从而让您能够从日志文件中提取这些电子邮件地址。

21.2 正则表达式语法入门

最简单的正则表达式是您想要匹配的确切文本字符串。例如,Car在技术上是一个正则表达式,在 PowerShell 中它将匹配CARcarCarCaR等等;PowerShell 的默认匹配是不区分大小写的。

然而,某些字符在正则表达式中具有特殊含义,它们使您能够检测可变文本的模式。以下是一些示例:

  • \w 匹配“单词字符”,意味着字母、数字和下划线,但不包括标点符号和空白。正则表达式 \won 会匹配 DonRonton,其中 \w 代表任何单个字母、数字或下划线。

  • \W 匹配 \w 的对立面(因此这是 PowerShell 对大小写敏感的一个例子),意味着它匹配空白和标点符号——“非单词字符”。

  • \d 匹配从 09 的任何数字。

  • \D 匹配任何非数字。

  • \s 匹配任何空白字符,包括制表符、空格或换行符。

  • \S 匹配任何非空白字符。

  • .(一个点)代表任何单个字符。

  • [abcde] 匹配该集合中的任何字符。正则表达式 c[aeiou]r 会匹配 carcur,但不会匹配 cauncoir

  • [a-z] 匹配该范围内的一个或多个字符。你可以指定多个范围,用逗号分隔的列表,例如 [a-f,m-z]

  • [^abcde] 匹配不在该集合中的一个或多个字符,意味着正则表达式 d[^aeiou] 会匹配 dns 但不会匹配 don

  • ? 后跟另一个字面量或特殊字符,并匹配该字符的精确一个实例。因此,正则表达式 ca?r 会匹配 car 但不会匹配 coir。它也会匹配 ca,因为 ? 也可以匹配前面字符的零个实例。

  • * 匹配前面字符的任意数量实例。正则表达式 ca*r 会匹配 caircar。它也会匹配 ca,因为 * 也可以匹配前面字符的零个实例。

  • + 匹配前面字符的一个或多个实例。你会在括号中看到很多这种用法,括号创建了一种子表达式。例如,正则表达式 (ca)+r 会匹配 cacacacar,因为它匹配 ca 子表达式的重复实例。

  • \(反斜杠)是正则表达式的转义字符。在正则表达式语法中通常有特殊意义的字符之前使用它,使该字符成为字面量。例如,正则表达式 \. 会匹配一个字面量的点字符,而不是像通常那样允许点代表任何单个字符。要匹配字面量的反斜杠,用反斜杠转义它:\\

  • {2} 匹配前面字符的精确数量。例如,\d{1} 会匹配一个数字。使用 {2,} 来匹配两个或更多,使用 {1,3} 来匹配至少一个但不超过三个。

  • ^ 匹配字符串的开始。例如,正则表达式 c.r 会匹配 car 以及 pteranocar。但正则表达式 ^c.r 只会匹配 car,而不会匹配 pteranocar,因为 ^ 使得匹配发生在字符串的开始处。这与前一个示例中的用法不同,在那里它和方括号 [] 一起使用,表示负匹配。

  • $ 匹配字符串的末尾。例如,正则表达式 .icks 会匹配 hickssticks(在这个例子中,匹配实际上是 ticks),还会匹配 Dickson。但正则表达式 .icks$ 不会匹配 Dickson,因为 $ 表示字符串应该在 s 之后结束。

这就是我们所看到的——对基本正则表达式语法的快速浏览。正如我们之前所写的,还有很多其他内容,但这已经足够做一些基本工作了。让我们看看一些示例正则表达式:

  • \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} 匹配 IPv4 地址的模式,尽管它会接受非法数据,如 432.567.875.000,以及合法数据,如 192.169.15.12

  • \\\\\w+(\\\w+)+ 匹配通用命名约定(UNC)路径。所有的反斜杠使得这个正则表达式难以阅读——这也是为什么在将正则表达式用于生产任务之前测试和调整它们很重要的原因之一。

  • \w{1}\.\w+@company\.com 匹配特定类型的电子邮件地址:首字母,一个点,姓氏,然后是 @company.com。例如,sam.smith@company .com 将是一个有效的匹配。在使用这些时确实需要小心。例如,Samuel.smith@company.com.orgSmith@company.com.net 也会是有效的匹配。正则表达式对匹配部分前后有额外文本的情况没有问题。这就是在许多情况下 ^$ 锚点发挥作用的地方。

注意 你可以通过在 PowerShell 中运行 help about_regular _expressions 来了解更多关于基本正则表达式语法的信息。在本章末尾,我们提供了一些额外的资源,供进一步探索。

21.3 使用 -Match 与正则表达式

PowerShell 包含一个比较运算符 -Match 和一个大小写敏感的类似项 -CMatch,它们与正则表达式一起工作。以下是一些示例:

PS C:\> "car" -match "c[aeiou]r"
True
PS C:\> "caaar" -match "c[aeiou]r"
False
PS C:\> "caaar" -match "c[aeiou]+r"
True
PS C:\> "cjinr" -match "c[aeiou]+r"
False
PS C:\> "cear" -match "c[aeiou]r"
False

虽然它有很多用途,但我们主要将依赖于 -Match 来测试正则表达式并确保它们工作正常。正如你所看到的,它的左操作数是你正在测试的字符串,右操作数是正则表达式。如果存在匹配,它输出 True;如果没有,你得到 False

现在试试看 这是一个从阅读中休息一下并尝试使用 -Match 操作符的好时机。运行我们刚才提到的几个示例,并确保你在 shell 中使用 -Match 操作符时感到舒适。

21.4 使用 Select-String 与正则表达式

现在我们来到了本章的真正重点。我们将使用一些网络服务器日志文件作为示例,因为它们正是正则表达式设计用来处理的那种纯文本文件。如果我们能够以面向对象的方式将这些日志读入 PowerShell 那将很棒,但,嗯,我们做不到。所以我们就用正则表达式吧。

让我们先扫描日志文件,寻找任何 40x错误。这些错误通常是文件未找到等,我们希望能够为我们组织的网络开发者生成一个坏文件的报告。日志文件包含每个 HTTP 请求的单行,并且每行被分割成空格分隔的字段。我们有一些文件,它们的文件名中包含 401 等,例如,error401.html,我们不希望这些文件包含在我们的结果中。我们指定一个正则表达式,如\s40[0-9]\s,因为它指定了 40x错误代码两边的空格。它应该找到从 400 到 409 的所有错误。以下是我们的命令:

PS C:\logfiles> get-childitem -filter *.log -recurse | 
 select-string -pattern "\s40[0-9]\s" | 
 format-table Filename,LineNumber,Line -wrap

注意,我们切换到 C:\LogFiles 目录来运行这个命令。我们首先让 PowerShell 获取所有匹配*.log 文件名模式的文件,并递归子目录。这确保了所有日志文件都包含在输出中。然后我们使用Select-String并给它我们的正则表达式作为模式。命令的结果是一个MatchInfo对象;我们使用Format-Table创建一个显示,包括文件名、行号和包含匹配的文本行。这可以很容易地重定向到文件并交给我们的网络开发者。

注意:你可能已经注意到我们使用了Format-Table。我们这样做有两个原因。第一个原因是我们想将屏幕上的文本换行,第二个原因是我们只是让屏幕看起来更整洁,并且我们没有输出任何信息。

接下来,我们想要扫描所有基于 Gecko 的 Web 浏览器的访问文件。我们的开发人员告诉我们,他们的一些客户在使用这些浏览器访问网站时遇到了一些问题,他们想查看哪些特定的文件被请求。他们认为问题已经缩小到在 Windows NT 10.0 下运行的浏览器,这意味着我们正在寻找看起来像这样的用户代理字符串:

(Windows+NT+10.0;+WOW64;+rv:11.0)+Gecko

我们的开发人员强调,64 位不是特定的,所以他们不希望日志结果仅限于WOW64用户代理字符串。我们提出了这个正则表达式:10.0;[\w\W]+\+Gecko。让我们分解一下:

  • 10.0;—这是一个 10.0。请注意,我们转义了点号,使其成为一个字面字符,而不是点号通常表示的单字符通配符。

  • [\w\W]+—这是一个或多个单词或非单词字符(换句话说,任何东西)。

  • \+Gecko—这是一个字面的+,然后是Gecko

以下是从日志文件中查找匹配行的命令,以及输出的前几行:

PS C:\logfiles> get-childitem -filter *.log -recurse | 
Select-string -pattern "10\.0;[\w\W]+\+Gecko"
W3SVC1\u_ex120420.log:14:2012-04-20 21:45:04 10.211.55.30 GET /MyApp1/Testpage.asp 
    - 80 - 10.211.55.29 Mozilla/5.0+(Windows+NT+10.0;+WOW64;+rv:11.0)+Gecko/20100101+Firefox/11.0 200 0 0 1125
W3SVC1\u_ex120420.log:15:2012-04-20 21:45:04 10.211.55.30 GET /TestPage.asp 
    - 80 - 10.211.55.29 Mozilla/5.0+(Windows+NT+10.0;+WOW64;+rv:11.0)+Gecko/20100101+Firefox/11.0 200 0 0 1 109

我们这次保留了输出的默认格式,而没有将其发送到格式化命令。

作为最后的例子,让我们从 IIS 日志文件转换到 Windows 安全日志。事件日志条目包括一个 Message 属性,其中包含有关事件的详细信息。不幸的是,这些信息是为方便人类阅读而格式化的,而不是为方便基于计算机的解析。我们希望查找所有 ID 为 4624 的事件,这表示账户登录(这个数字在不同的 Windows 版本中可能不同;我们的例子来自 Windows Server 2008 R2)。但我们只想看到与以 WIN 开头的账户登录相关的事件,这关系到我们域中的计算机账户,并且账户名称以 TM20$ 通过 TM40$ 结尾,这是我们感兴趣的特定计算机。这个正则表达式可能看起来像 WIN[\W\w]+TM[234][0-9]\$。注意我们为什么需要转义最后的美元符号,以免它被解释为字符串结束锚点。我们需要包含 [\W\w](非单词和单词字符),因为我们的账户名称可能包含连字符,这不会与 \w 单词字符类匹配。以下是我们的命令:

PS C:\> get-eventlog -LogName security | where { $_.eventid -eq 4624 } | 
select -ExpandProperty message | select-string -pattern 
"WIN[\W\w]+TM[234][0-9]\$"

我们首先使用 Where-Object 来保留具有 ID 4624 的事件。然后我们将 Message 属性的内容展开为普通字符串,并将其管道到 Select-String。请注意,这将输出匹配的消息文本;如果我们的目标是输出整个匹配的事件,我们将采取不同的方法:

PS C:\> get-eventlog -LogName security | where { $_.eventid -eq 4624 -and
➥ $_.message -match "WIN[\W\w]+TM[234][0-9]\$" }

在这里,我们不是输出 Message 属性的内容,而是简单地查找 Message 属性包含与我们的正则表达式匹配的文本的记录,然后输出整个事件对象。这完全取决于你想要的输出内容。

21.5 实验答案

注意:对于这个实验,你需要任何运行 PowerShell v7 或更高版本的计算机。

不要误解,正则表达式可能会让你头晕,所以不要一开始就尝试创建复杂的正则表达式——从简单开始。这里有一些练习可以帮助你入门。使用正则表达式和运算符来完成以下任务:

  1. 获取你 Windows 或 /usr 目录下所有名称中包含两位数字的文件。

  2. 在你的计算机上查找所有来自微软的已加载模块,并显示名称、版本号、作者和公司名称。(提示:将 Get-module 管道到 Get-Member 以发现属性名称。)

  3. 在 Windows 更新日志中,你只想显示代理开始安装文件的行。你可能需要打开记事本来找出你需要选择的字符串。你可能需要运行 Get-WindowsUpdateLog,相应的日志将被放置在你的桌面上。

    对于 Linux,找到你的历史记录日志并显示你安装软件包的行。

  4. 使用 Get-DNSClientCache 命令,显示所有 Data 属性是 IPv4 地址的列表。

  5. 如果你在一台 Linux(或 Windows)机器上,找到包含 IPV4 地址的 HOSTS 文件中的行。

21.6 实验答案

  1. Get-ChildItem c:\windows | where {$_.name -match "\d{2}"}

    Get-ChildItem /usr | where {$_.name -match "\d{2}"}

  2. get-module | where {$_.companyname -match "^Microsoft"} |

    Select Name,Version,Author,Company

  3. get-content C:\Windows\WindowsUpdate.log |

    Select-string "[\w+\W+]Installing Update"

    Get-content ./apt/history.log | select-string "[\w+\W+]Installing"

  4. 你可以使用一个以一到三个数字开头,后跟一个实际点的模式来开始,例如:

    get-dnsclientcache | where { $_.data -match "^\d{1,3}\."}

    或者,你可以匹配整个 IPv4 地址字符串:

    get-dnsclientcache | where

    { $_.data -match "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"}

  5. gc /etc/hosts | where {$_ -match "^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"}

21.7 进一步探索

你会在 PowerShell 的其他地方找到正则表达式,其中许多涉及我们在本书中没有涵盖的 shell 元素。以下是一些示例:

  • Switch 脚本结构包括一个参数,允许它将一个值与一个或多个正则表达式进行比较。

  • 高级脚本和函数(脚本命令)可以利用基于正则表达式的输入验证工具来帮助防止无效的参数值。

  • -Match 操作符(我们在本章中简要介绍过)用于测试字符串与正则表达式的匹配,并且——我们之前没有分享过——将匹配的字符串捕获到自动的 $matches 集合中。

PowerShell 使用行业标准正则表达式语法,如果你有兴趣了解更多,我们推荐 Jeffrey E. F. Friedl 的《精通正则表达式》(O’Reilly,2006 年)。市面上还有无数的正则表达式书籍,其中一些是针对 Windows 和.NET(以及 PowerShell)的,有些专注于为特定情况构建正则表达式,等等。浏览你最喜欢的在线书店,看看是否有任何书籍看起来适合你和你特定的需求。

我们还使用了一个免费的在线正则表达式存储库,RegExLib.com,它包含各种目的的正则表达式示例(电话号码、电子邮件地址、IP 地址等)。我们还发现自己在使用 RegExTester.com,这是一个允许你交互式测试正则表达式的网站,以精确地得到你需要的方式。

22 使用他人的脚本

尽管我们希望你能从头开始构建自己的 PowerShell 命令和脚本,但我们也意识到你将严重依赖互联网上的示例。无论你是从某人的博客中重新利用示例,还是调整你在在线脚本存储库中找到的脚本,能够重用他人的 PowerShell 脚本是一项重要的核心技能。在本章中,我们将带你了解我们理解他人脚本并将其变为己用的过程。

感谢感谢归功于 Brett Miller,他为我们提供了本章中使用的脚本。我们故意要求他提供一个不太完美的脚本,这个脚本不一定反映我们通常喜欢看到的最佳实践。在某些情况下,我们甚至 恶化 了此脚本,以便使本章更好地反映现实世界。我们真正感谢他对这个学习练习的贡献!

注意,我们之所以特别选择这些脚本,是因为它们使用了我们尚未教授你的高级 PowerShell 功能。再次强调,我们认为这是现实的:你可能会遇到看起来不熟悉的东西,而这个练习的一部分就是如何快速弄清楚脚本在做什么,即使你对脚本使用的每个技术并不完全训练有素。

22.1 脚本

这是一个真实的世界场景,我们的大多数学生都经历过。他们遇到问题,上网,找到一个能完成他们所需工作的脚本。理解正在发生的事情非常重要。以下列表显示了完整的脚本,其标题为 Get-AdExistence.ps1。此脚本旨在与 Microsoft 的 AD cmdlets 一起工作。这只能在基于 Windows 的计算机上工作。如果你没有访问安装了 Active Directory 的 Windows 机器,你仍然可以跟随我们,因为我们将逐个分析这个脚本。

列表 22.1 Get-AdExistence.Ps1

<#
.Synopsis
   Checks if computer account exists for computer names provided
.DESCRIPTION
   Checks if computer account exists for computer names provided
.EXAMPLE
   Get-ADExistence $computers
.EXAMPLE
   Get-ADExistence "computer1","computer2"
#>
function Get-ADExistence{
    [CmdletBinding()]
    Param(
        # single or array of machine names
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   HelpMessage="Enter one or multiple computer names")]
        [String[]]$Computers
     )
    Begin{}
    Process {
        foreach ($computer in $computers) {
            try {
                $comp = get-adcomputer $computer -ErrorAction stop
                $properties = @{computername = $computer
                                Enabled = $comp.enabled
                                InAD = 'Yes'}
            } 
            catch {
                $properties = @{computername = $computer
                                Enabled = 'Fat Chance'
                                InAD = 'No'}
            } 
            finally {
                $obj = New-Object -TypeName psobject -Property $properties
                Write-Output $obj
            }
        } #End foreach

    } #End Process
    End{}
} #End Function

22.1.1 参数块

首先是参数块,你已经在第十九章中学到了如何创建它:

Param(
        # single or array of machine names
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
        [String[]]$Computers
     )

这个参数块看起来有些不同,但它似乎是在定义一个可以接受数组并强制性的 -Computers 参数。这是合理的。当你运行它时,你需要提供这些信息。接下来的几行更加神秘:

Begin{}
Process

我们还没有介绍过程块,但现在只需知道这是脚本的主要内容所在。我们将在 Learn Scripting in a Month of Lunches(Manning,2017)中更详细地介绍这一点。

22.1.2 过程块

我们还没有介绍 Try Catch,但很快就会介绍,不用担心。现在,只需知道你会尝试做某事,如果那不起作用,你将 CATCH 到它抛出的错误。接下来我们看到两个变量,$comp$properties

foreach ($computer in $computers) {
            try {
                $comp = get-adcomputer $computer
                $properties = @{computername = $computer
                                Enabled = $comp.enabled
                                InAD = 'Yes'}
            } 
            catch {
                $properties = @{computername = $computer
                                Enabled = 'Fat Chance'
                                InAD = 'No'}
            }

$Comp正在运行一个 Active Directory 命令来查看计算机是否存在,如果存在,它将 AD 信息存储在$comp变量中。$Properties是我们创建的一个哈希表,它存储了一些我们需要的信息,包括ComputerNameEnabled以及它是否在AD中。

我们脚本的其余部分将我们创建的哈希表转换成 PS 自定义对象,然后使用Write-Output将其写入屏幕。

finally {
           $obj = New-Object -TypeName psobject -Property $properties
           Write-Output $obj
         }

除此之外

我们需要更改什么才能将此写入文本文件或 CSV 文件?

22.2 这是对每一行的检查

上一节的过程是对脚本的逐行分析,这是我们建议你遵循的过程。随着你逐行前进,请执行以下操作:

  • 识别变量,尝试弄清楚它们将包含什么内容,并将这些内容写在一张纸上。因为变量通常会被传递给命令参数,所以拥有一个关于你认为每个变量包含什么的便捷参考可以帮助你预测每个命令将做什么。

  • 当你遇到新的命令时,阅读它们的帮助并尝试理解它们在做什么。对于Get-命令,尝试运行它们——将脚本传递给变量的任何值插入参数中——以查看产生的输出。

  • 当你遇到不熟悉的内容,如if[environment]时,考虑在虚拟机中运行简短的代码片段以查看这些片段做什么(使用虚拟机有助于保护你的生产环境)。在帮助中搜索这些关键字(使用通配符)以了解更多信息。

最重要的是,不要跳过任何一行。不要想,“嗯,我不知道那是什么,所以我继续。”停下来弄清楚每一行做什么,或者你认为它做什么。这有助于你确定你需要调整脚本以适应你的特定需求。

22.3 实验

列表 22.2 显示了一个完整的脚本。看看你是否能弄清楚它做什么以及如何使用它。你能预测出任何可能导致的错误吗?为了在你的环境中使用它,你可能需要做什么?

注意,这个脚本应该直接运行(你可能需要以管理员身份运行以访问安全日志),但如果它在你的系统上无法运行,你能追踪到问题的原因吗?记住,你已经看到了大多数这些命令,而对于你没有看到的命令,有 PowerShell 的帮助文件。这些文件中的示例包括脚本中展示的每个技术。

列表 22.2 Get-LastOn.ps1

function get-LastOn {
    <#
    .DESCRIPTION
    Tell me the most recent event log entries for logon or logoff.
    .BUGS
    Blank 'computer' column
    .EXAMPLE
    get-LastOn -computername server1 | Sort-Object time -Descending | 
    Sort-Object id -unique | format-table -AutoSize -Wrap
    ID              Domain       Computer Time                
    --              ------       -------- ----                
    LOCAL SERVICE   NT AUTHORITY          4/3/2020 11:16:39 AM
    NETWORK SERVICE NT AUTHORITY          4/3/2020 11:16:39 AM
    SYSTEM          NT AUTHORITY          4/3/2020 11:16:02 AM
    Sorting -unique will ensure only one line per user ID, the most recent.
    Needs more testing
    .EXAMPLE
    PS C:\Users\administrator> get-LastOn -computername server1 -newest 10000
     -maxIDs 10000 | Sort-Object time -Descending |
     Sort-Object id -unique | format-table -AutoSize -Wrap
    ID              Domain       Computer Time
    --              ------       -------- ----
    Administrator   USS                   4/11/2020 10:44:57 PM
    ANONYMOUS LOGON NT AUTHORITY          4/3/2020 8:19:07 AM
    LOCAL SERVICE   NT AUTHORITY          10/19/2019 10:17:22 AM
    NETWORK SERVICE NT AUTHORITY          4/4/2020 8:24:09 AM
    student         WIN7                  4/11/2020 4:16:55 PM
    SYSTEM          NT AUTHORITY          10/18/2019 7:53:56 PM
    USSDC$          USS                   4/11/2020 9:38:05 AM
    WIN7$           USS                   10/19/2019 3:25:30 AM
    PS C:\Users\administrator>
    .EXAMPLE
    get-LastOn -newest 1000 -maxIDs 20 
    Only examines the last 1000 lines of the event log
    .EXAMPLE
    get-LastOn -computername server1| Sort-Object time -Descending | 
    Sort-Object id -unique | format-table -AutoSize -Wrap
    #>
    param (
            [string]$ComputerName = 'localhost',
            [int]$MaxEvents = 5000,
            [int]$maxIDs = 5,
            [int]$logonEventNum = 4624,
            [int]$logoffEventNum = 4647
        )
        $eventsAndIDs = Get-WinEvent -LogName security -MaxEvents $MaxEvents 
      ➥ -ComputerName $ComputerName | 
        Where-Object {$_.id -eq $logonEventNum -or `
        $_.instanceid -eq  $logoffEventNum} | 
        Select-Object -Last $maxIDs -Property TimeCreated,MachineName,Message
        foreach ($event in $eventsAndIDs) {
            $id = ($event | 
            parseEventLogMessage | 
            where-Object {$_.fieldName -eq "Account Name"}  | 
            Select-Object -last 1).fieldValue
            $domain = ($event | 
            parseEventLogMessage | 
            where-Object {$_.fieldName -eq "Account Domain"}  | 
            Select-Object -last 1).fieldValue
            $props = @{'Time'=$event.TimeCreated;
                'Computer'=$ComputerName;
                'ID'=$id
                'Domain'=$domain}
            $output_obj = New-Object -TypeName PSObject -Property $props
            write-output $output_obj
        }  
    }
    function parseEventLogMessage()
    {
        [CmdletBinding()]
        param (
            [parameter(ValueFromPipeline=$True,Mandatory=$True)]
            [string]$Message 
        )    
        $eachLineArray = $Message -split "`n"
        foreach ($oneLine in $eachLineArray) {
            write-verbose "line:_$oneLine_"
            $fieldName,$fieldValue = $oneLine -split ":", 2
                try {
                    $fieldName = $fieldName.trim() 
                    $fieldValue = $fieldValue.trim() 
                }
                catch {
                    $fieldName = ""
                }
                if ($fieldName -ne "" -and $fieldValue -ne "" ) 
                {
                $props = @{'fieldName'="$fieldName";
                        'fieldValue'=$fieldValue}
                $output_obj = New-Object -TypeName PSObject -Property $props
                Write-Output $output_obj
                }
        }
    }
Get-LastOn

22.4 实验答案

脚本文件似乎定义了两个函数,这些函数在调用之前不会做任何事情。在脚本末尾有一个命令Get-LastOn,它与其中一个函数的名称相同,所以我们可以假设这就是执行的内容。查看该函数,你可以看到它有多个参数默认值,这解释了为什么不需要调用其他内容。基于注释的帮助也解释了该函数的功能。这个函数的第一部分使用Get-WinEvent

$eventsAndIDs = Get-WinEvent -LogName security -MaxEvents $MaxEvents | 
  Where-Object { $_.id -eq $logonEventNum -or $_.id -eq $logoffEventNum } | 
  Select-Object -Last $maxIDs -Property TimeCreated, MachineName, Message

如果这是一个新的 cmdlet,我们会查看帮助和示例。表达式似乎返回一个用户定义的事件最大值。在查看Get-WinEvent的帮助后,我们看到参数-MaxEvents将返回从最新到最旧的排序的最大事件数。因此,我们的变量$MaxEvents来自一个参数,默认值为5000。这些事件日志随后通过Where-Object进行过滤,寻找两个事件日志值(事件 ID 为46274647),也来自参数。

接下来,看起来在foreach循环中对每个事件日志都进行了某种处理。这里有一个潜在的陷阱:在foreach循环中,看起来其他变量正在被设置。第一个是将事件对象传递到名为parseEventmessage的某个东西。这看起来不像是一个 cmdlet 名称,但我们确实看到它被用作一个函数。跳转到它,我们可以看到它接受一个消息作为参数,并将每个消息分割成一个数组。我们可能需要研究-Split运算符。

数组中的每一行都通过另一个foreach循环进行处理。看起来行再次被分割,并且有一个try/catch块来处理错误。同样,我们可能需要阅读有关它的内容以了解它是如何工作的。最后,有一个if语句,看起来如果分割后的字符串不为空,则创建一个名为$props的变量作为散列表或关联数组。如果作者包含一些注释,这个函数将更容易理解。无论如何,解析函数通过调用New-Object(另一个需要了解的 cmdlet)结束。

这个函数的输出随后传递给调用函数。看起来重复了同样的过程以获取$domain

哦,看,另一个散列表和New-Object,但到现在我们应该理解这个函数在做什么。这是函数的最终输出,因此是脚本。

23 添加逻辑和循环

循环(或逐个遍历对象列表)是任何语言中的基本概念,PowerShell 也不例外。总会有需要多次执行代码块的时候。PowerShell 已经准备好为你处理这个问题。

23.1 Foreach 和 Foreach-Object

这一节可能有点令人困惑,因为 ForeachForeach-Object 之间有一个区别。请参阅图 23.1 以了解 Foreach 的工作原理的视觉表示。

图 23.1 Foreach 的工作原理图

23.1.1 Foreach

最常见的循环形式可能是 Foreach 命令。Foreach 允许你遍历集合中一系列值,例如数组。Foreach 命令的语法是

Foreach (p
temporary variable IN collection object)
{Do Something}

过程块(被 {} 包围的部分)将根据集合对象的数量执行多次。让我们看看以下命令并对其进行分解:

PS C:\Scripts> $array = 1..10
PS C:\Scripts> foreach ($a in $array) {Write-output $a}

首先,我们创建了一个名为 $array 的变量,它将包含从 1 到 10 的数字数组。接下来,我们创建了一个临时变量($a)并将其分配给我们在处理的集合中的当前项。该变量仅在脚本块内部可用,并且在我们遍历数组时将发生变化。

最后,由大括号 {} 表示的脚本块将输出 $a 到屏幕上(图 23.2)。

图 23.2 使用 foreach 编写数组输出

23.1.2 Foreach-Object

Foreach-Object cmdlet 在输入集合对象中的每个项目上执行在脚本块中定义的操作。最常见的是,Foreach-Object 通过管道调用。

提示:如果你正在遍历多个对象,请使用 Foreach;如果你在管道中使用它,请使用 Foreach-Object

让我们看看命令 Get-ChildItem | ForEach-Object {$_.name}。首先,我们运行 Get-ChildItem 命令,并将对象通过管道发送到 Foreach-Object cmdlet。

接下来,我们说对于从 Get-ChildItem 收到的每个项目,运行命令 $_.name(图 23.3)。如果你还记得文本前面的内容,$_ 简单地是管道中的当前对象。通过使用 $_.Name,我们从对象中获取 name 属性并将其显示在屏幕上。

图 23.3 展示了如何使用 foreach-object 与管道结合。

对于 ForeachForeach-Object cmdlet,命令是顺序执行的,这意味着它将依次执行 item[0],然后运行你指定的命令,接着是 item[1],依此类推,直到输入集合为空。通常这不会成问题,但最终,如果你在过程块中有许多命令或你的输入集合非常大,你就可以看到逐个执行这些命令会对脚本的运行时间产生影响。

希望在你开始深入研究本章之前,你已经使用了帮助功能来查看所有可用的Foreach-Object参数。

现在试试看;运行get-help Foreach-Object并查看结果。

除此之外

%也是ForEach-Object命令的别名。之前的命令可以写成

Get-ChildItem | %{$_.name}

这将产生相同的结果。但让我们记住,始终使用完整的 cmdlet 名称是最好的。

23.1.3 Foreach-Object -Parallel

正如我们之前提到的,Foreach-Object命令的主要缺点是它是顺序运行的。有几个由社区驱动的模块旨在帮助为Foreach-Object命令启用并行功能。随着 PowerShell 7(预览版 3)的引入,Foreach-Object命令中添加了一个新的-Parallel参数。现在,我们可以在大多数或所有输入对象上同时运行相同的命令,而不是顺序运行命令。例如,假设你正在 Active Directory 中创建 1,000 个新用户。你可以运行以下命令

import-csv c:\scripts\newusers.csv | 
ForEach-Object {New-aduser -Name $_.Name }

这将依次运行New-Aduser命令 1,000 次。或者,你可以使用Parallel参数来运行该命令:

import-csv c:\scripts\newusers.csv | 
ForEach-Object -Parallel {New-aduser -Name $_.Name }

以下命令接受一个数字数组(1-5),将其通过管道传递到传统的Foreach-Object命令,将输出写入屏幕,并暂停 2 秒(图 23.4)。

1..5 | ForEach-Object {Write-Output $_; start-sleep -Seconds 2}

图 23.4 接受一个数组,通过管道传递到Foreach-Object,然后运行第二个命令

通过使用measure-command cmdlet,我们可以看到这将需要 10 秒才能完成。

PS C:\Scripts> measure-command {1..5 | ForEach-Object {Write-Output "$_"; 
➥ start-sleep -Seconds 2}}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 10
Milliseconds      : 47
Ticks             : 100471368
TotalDays         : 0.000116286305555556
TotalHours        : 0.00279087133333333 
TotalMinutes      : 0.16745228
TotalSeconds      : 10.0471368
TotalMilliseconds : 10047.1368

当我们添加-parallel参数时,我们将一次性在数组中的所有数字上执行命令块内的内容。

1..5 | ForEach-Object -parallel {Write-Output "$_"; start-sleep -Seconds 2}

通过使用parallel参数,我们将运行时间从 10 秒减少到 2 秒。

PS C:\Scripts> measure-command {1..5 | ForEach-Object -parallel {Write-Output 
➥ "$_"; start-sleep -Seconds 2}}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 2
Milliseconds      : 70
Ticks             : 20702383
TotalDays         : 2.39610914351852E-05
TotalHours        : 0.000575066194444444
TotalMinutes      : 0.0345039716666667
TotalSeconds      : 2.0702383
TotalMilliseconds : 2070.2383

因为每个脚本块都是同时运行的,所以返回到屏幕上的结果顺序无法保证。同时,还有一个节流限制,即一次可以并行运行的脚本块的最大数量,我们需要确保你知道这一点——默认值是 5。在我们的例子中,我们的输入集合中只有 5 个项目,所以所有 5 个脚本块都是同时运行的。然而,如果我们把我们的例子从 5 个项目改为 10 个项目,我们会注意到运行时间从 2 秒变为 4 秒。但是,我们可以通过使用-throttlelimit参数来将节流限制提高到更高的值。

1..10 | ForEach-Object -parallel {Write-Output "$_"; start-sleep -Seconds 2} 
➥ -ThrottleLimit 10 

现在试试看;将数组改为 10 个项目;然后使用measure-command cmdlet 来查看执行所需的时间。

然而,parallel功能有一个限制。为了同时运行每个脚本块,会创建一个新的运行空间。如果你的脚本块是资源密集型的,这可能会导致性能显著下降。

23.3 当

如果你之前做过任何类型的脚本或编程,那么 while 循环应该对你来说不是一个新概念。while 循环是一个迭代循环,它将一直运行,直到满足终止条件。就像我们刚才提到的 Foreach 循环一样,while 循环有一个脚本块,你可以在这里放置要执行的命令(图 23.5)。基本语法如下:While (condition) {commands}。

图 23.5 展示了 while 循环的工作原理

  • 条件—一个布尔 ($True$False) 表达式。循环将在条件为 True 时执行,并在条件为 False 时终止。例如:While ($n -ne 10)

  • 命令—在条件为 True 时要执行的简单或复杂命令。

这里有一个快速示例:

$n=1
While ($n -le 10){Write-Output $n; $n++}

我们还可以开始将逻辑运算符如 -and-or 添加到我们的条件语句中:

While ($date.day -ne 25 -and $date.month -ne 12)
{Write-Host “Its not Christmas Yet”}

提示:如果你运行上述命令,它将无限期地运行,除非你恰好是在 25-December 运行的。使用 Ctrl-C 来中断执行。

23.3 Do While 循环

正如我们之前提到的,while 循环只有在条件为 true 时才会执行。但如果你想要至少执行一次循环,无论条件是否为 true,那该怎么办?这就是 Do While 循环发挥作用的地方。

使用 Do {commands} While (condition),请注意,脚本块和条件块是相反的。这将允许我们至少执行一次脚本块,然后评估我们的条件以确定是否需要重复循环:

$date = get-date

do {
    Write-Output "Checking if the month is December"
    $date = $date.AddMonths(1)
} while ($date.Month -ne 12 )

23.4 实验内容

  1. 在包含大量项目的目录中查找。使用 Foreach 循环并计算每个文件名的字符数。

    • 同样操作,但这次使用 -parallel 参数。
  2. 启动记事本进程(或你选择的文本编辑器);然后编写一个 do while 循环,直到进程关闭时显示以下文本:$process is open

23.5 实验答案

  1. $items = Get-ChildItem SOMEWHERE YOU |CHOSE

    foreach ($i in $items){Write-Output "The character length of $i is

    ➥ "($i).Length"

  2. start-process notepad

    $Process = "notepad"

    do {

        Write-Host "$process is open"

    } while ((get-process).name -contains "notepad")

24 处理错误

在本章中,我们将重点介绍如何捕获、处理、记录以及以其他方式处理工具可能遇到的错误。

注意PowerShell.org提供了一个名为《PowerShell 错误处理大全书》的免费电子书,它从更技术性的参考角度深入探讨了这一主题,请访问devopscollective.org/ebooks/。我们建议在完成这个以教程为重点的章节后查看它。

在我们开始之前,有两个变量我们需要熟悉。第一个是$Error自动化变量。它包含了一个数组,其中包含了当前会话中发生的错误对象,最新的错误对象显示在$Error[0]。默认情况下,所有错误都会放入这个变量中。你可以通过设置ErrorAction常见参数为Ignore来改变这种行为。你可以通过运行get-help about_automatic_variables来获取有关自动变量的更多信息。

你可以使用的第二个内置变量是常见的参数变量ErrorVariable。这是一个你可以发送错误的对象,因此如果需要的话,你可以在稍后使用它们(例如,写入日志文件):

New-PsSession -ComputerName SRV01 -ErrorVariable a

ErrorVariable将只保留最近的错误,除非你在它前面添加一个+(加号):

New-PsSession -ComputerName SRV01 -ErrorVariable +a

注意:我们没有在错误变量前使用$符号,因为在这里不需要。

24.1 理解错误和异常

PowerShell 定义了两种广泛的错误情况:一个错误和一个异常。因为大多数 PowerShell 命令都是设计来同时处理多个事物的,而且在很多情况下,一个事物的问题并不意味着你想停止处理其他所有事物,所以 PowerShell 试图在“继续进行”这一边犯错。因此,当命令中发生错误时,PowerShell 通常会发出一个错误并继续运行(图 24.1)。例如:

Get-Service -Name BITS,Nobody,WinRM

图片

图 24.1 Get-Service与一个不存在的服务

服务Nobody不存在,所以 PowerShell 会在第二个项目上发出一个错误。但默认情况下,PowerShell 会继续运行并处理列表中的第三个项目。当 PowerShell 处于这种继续运行的模式时,你无法让代码响应问题条件。如果你想对问题采取行动,你必须改变 PowerShell 对这种非终止错误的默认响应。

在全球范围内,PowerShell 定义了一个$ErrorActionPreference变量,它告诉 PowerShell 在发生非终止错误时应该做什么——也就是说,这个变量告诉 PowerShell 在出现问题时应该做什么,但 PowerShell 仍然能够继续运行。这个变量的默认值是Continue。以下是可用的选项:

  • Break——当发生错误或抛出异常时进入调试器。

  • Continue(默认)——显示错误消息并继续执行。

  • Ignore—抑制错误信息并继续执行命令。Ignore值旨在用于每个命令,而不是用作保存的偏好。Ignore不是$ErrorActionPreference变量的有效值。

  • Inquire—显示错误信息并询问你是否想继续。

  • SilentlyContinue—无效果。错误信息不会显示,执行将继续而不会中断。

  • Stop—显示错误信息并停止执行。除了生成的错误外,Stop值还会向错误流生成一个ActionPreferenceStopException对象。

  • Suspend—自动挂起工作流作业以允许进一步调查。调查后,工作流可以继续。Suspend值旨在用于每个命令,而不是用作保存的偏好。Suspend不是$ErrorActionPreference变量的有效值。

而不是全局更改$ErrorActionPreference,你通常会想为每个命令指定一个行为。你可以使用存在于每个 PowerShell 命令中的-ErrorAction通用参数来完成此操作——即使是你自己编写的包含[CmdletBinding()]的命令。例如,尝试运行这些命令,并注意它们的不同行为:

Get-Service -Name Foo,BITS,Nobody,WinRM -ErrorAction Continue
Get-Service -Name BITS,Nobody,WinRM -ErrorAction SilentlyContinue
Get-Service -Name BITS,Nobody,WinRM -ErrorAction Inquire
Get-Service -Name BITS,Nobody,WinRM -ErrorAction Ignore
Get-Service -Name BITS,Nobody,WinRM -ErrorAction Stop

要记住的是,除非 PowerShell 实际生成异常,否则你无法在代码中处理异常。大多数命令不会生成异常,除非你使用Stop错误操作运行它们。人们犯的最大错误之一就是忘记在想要处理问题的命令中添加-EA Stop-EA-ErrorAction的缩写)。

24.2 不良处理

我们看到人们参与两种根本性的不良做法。这些做法并不总是是错误的,但它们通常是错误的,所以我们想引起你的注意。

首先,是在脚本或函数的最顶部全局设置偏好变量:

$ErrorActionPreference='SilentlyContinue' 

在 VBScript 的古老时代,人们使用On Error Resume Next。这本质上是在说,“我不想知道我的代码是否有任何问题。”人们这样做是为了错误地抑制他们知道不会造成影响的可能错误。例如,尝试删除一个不存在的文件将导致错误——但你可能并不在乎,因为任务无论如何都完成了,对吧?但为了抑制这个不想要的错误,你应该在Remove-Item命令中使用-EA SilentlyContinue,而不是全局抑制脚本中的所有错误。

另一种不良做法更为微妙,可能会出现在相同的情况下。假设你确实使用了-EA SilentlyContinue来运行Remove-Item,然后假设你尝试删除一个确实存在但你没有权限删除的文件。你会抑制错误并想知道为什么文件仍然存在。

在开始抑制错误之前,请确保你已经仔细考虑过。没有什么比花费数小时调试脚本更令人沮丧了,因为你抑制了一个本可以告诉你问题所在位置的错误消息。我们无法告诉你这在论坛问题中出现的频率有多高。

24.3 异常处理的两个原因

在你的代码中处理异常有两个主要原因。(注意,我们使用它们的官方名称异常来区分我们之前提到的不可处理的错误。)

第一个原因是,你计划在你的视线之外运行你的工具。这可能是一个计划任务,或者你可能正在编写将被远程客户使用的工具。在两种情况下,你都想确保你有任何发生问题的证据,以帮助你进行调试。在这种情况下,你可以在脚本顶部全局设置$ErrorActionPreferenceStop,并将整个脚本包裹在错误处理结构中。这样,任何错误,即使是未预见的错误,都可以被捕获并记录以供诊断。尽管这是一个有效的情况,但我们将不会在本书中重点关注这种情况。

我们将重点关注第二个原因——你正在运行一个你可以预见到可能会出现某种问题的命令,并且你想要积极处理这个问题。这可能是无法连接到计算机,无法登录到某个系统,或者类似的情况。让我们深入探讨一下。

24.4 处理异常

假设你正在构建一个连接到远程机器的脚本。你可以预见到New-PSSession命令可能会遇到问题:计算机可能离线或不存在,或者计算机可能不支持你选择的协议。你想要捕获这些条件,并根据你运行的参数,将失败的计算机名称记录到文本文件中,并尝试使用其他协议再次运行。你将从关注可能引发问题的命令开始,并确保它在遇到麻烦时能够生成一个终止异常。更改如下:

$computer = 'Srv01'
Write-Verbose "Connecting to $computer"
$session = New-PSSession -ComputerName $computer

到这里:

$computer = 'Srv01'
Write-Verbose "Connecting to $computer"
$session = New-PSSession -ComputerName $computer -ErrorAction Stop

但如果我们想在多台计算机上运行这个命令怎么办?我们有两种选择。第一种选择是将多个计算机名称放入$computer变量中。毕竟,它接受字符串数组。

$computer = 'Srv01','DC01','Web02'
Write-Verbose "Connecting to $computer"
$session = New-PSSession -ComputerName $computer -ErrorAction Stop

这里是你需要做出一些个人决定的地方。当发生错误时,你是想让你的脚本继续运行并捕获错误以供后续使用,还是想让脚本立即停止运行?这很大程度上取决于你想要达成的目标。如果你试图连接到五台远程计算机来运行一个命令,如果只有四台运行成功,而你记录了第五台计算机无法连接的错误,这是否可以接受,或者你需要命令在所有五台或没有任何一台计算机上运行?

这里你有两种选择。第一种选择是将你的命令包裹在一个 foreach 循环中。这样每次执行命令时都会设置 ErrorAction。如果你有一个失败,其余的会话仍然会被创建。然而,这却否定了 New-PSSession computername 参数可以接受对象数组作为其输入的事实:

foreach ($computer in $computername) {
          Write-Verbose "Connecting to $computer"
     $session = New-PSSession -ComputerName $Computer -ErrorAction Stop
        }

第二种选择是告诉 PowerShell 继续执行并将错误放入 ErrorVariable 公共参数中(别忘了将 + 符号附加到现有变量数据上):

$computer = 'Srv01','DC01','Web02'
   $session = New-PSSession -ComputerName $Computer -ErrorVariable a

确保你理解为什么这个设计原则如此重要!正如我们之前提到的,如果我们能帮助的话,我们不想抑制有用的错误。

现在试试看 使用你在本章和前几章中学到的知识,获取 spooler 服务和 print 服务的状态。确保记录你的错误。

只是将错误操作更改为 Stop 是不够的。你还需要将你的代码包裹在 Try/Catch 构造中。如果在 Try 块中发生异常,那么 Try 块中随后的所有代码都将被跳过,然后执行 Catch 块:

try { blahfoo }
catch { Write-Warning “Warning: An error occurred." }

这里发生的事情是:在 Catch 块中,你有机会为用户的利益编写一条警告信息。他们可以通过在运行命令时添加 -Warning-Action SilentlyContinue 来抑制警告。这是一些复杂的逻辑——多看几遍,确保你理解它!

24.5 处理非命令的异常

如果你在运行某些东西——比如一个没有 -ErrorAction 参数的 .NET Framework 方法——会发生什么?在 大多数 情况下,你可以直接在 Try 块中运行它,因为 大多数 这些方法在出错时都会抛出可捕获的、终止的异常。非终止异常的情况是 PowerShell 命令(如函数和 cmdlets)特有的。

但你仍然可能遇到需要这样做的情况:

Try {
    $ErrorActionPreference = "Stop"
    # run something that doesn't have -ErrorAction
    $ErrorActionPreference = "Continue"
} Catch {
    # ...
}

这是你的最后一种错误处理方法。基本上,你只是暂时修改 $ErrorActionPreference,以便在你想捕获异常的单个命令(或任何其他操作)期间使用。在我们经验中,这种情况并不常见,但我们认为我们应该指出这一点。

24.6 进一步学习异常处理

在给定的 Try 块之后,可以有多重 Catch 块,每个 Catch 块处理特定类型的异常。例如,如果文件删除失败,你可以针对“文件未找到”或“访问被拒绝”的情况采取不同的反应。为此,你需要知道你想要单独调用的每个异常的 .NET Framework 类型名称。《PowerShell 错误处理大全书》 列出了常见的一些类型,并提供了如何确定这些类型的建议(例如,在自己的实验中生成错误,然后确定异常类型名称)。总的来说,语法看起来像这样:

Try {
    # something here generates an exception
} Catch [Exception.Type.One] {
    # deal with that exception here
} Catch [Exception.Type.Two] {
    # deal with the other exception here
} Catch {
    # deal with anything else here
} Finally {
    # run something else
}

在那个示例中还展示了可选的 Finally 块,它将在 TryCatch 之后始终运行,无论是否发生异常。

已废弃的异常处理

在你的网络旅行中,你可能会在 PowerShell 中遇到一个 Trap 构造。这可以追溯到 v1,当时 PowerShell 团队坦白地说没有时间让 Try/Catch 工作起来,而 Trap 是他们能想出的最好的短期解决方案。Trap 已被弃用,这意味着它被保留在产品中以保持向后兼容性,但你不应该在新编写的代码中使用它。因此,我们在这里不讨论它。在全局的,“我想捕获并记录任何可能的错误”的情况下,它确实有一些用途,但 Try/Catch 被认为是一种更结构化、更专业的异常处理方法,我们建议你坚持使用它。

24.7 实验室

使用你迄今为止学到的知识,做以下事情:

  • 创建一个函数,用于获取远程机器的运行时间。确保你使用的是 PowerShell 7 的内置命令,而不是 .NET 方法。

  • 确保函数可以接受多个机器的输入。

  • 包含本章中讨论的错误处理方法,如 Try/Catch 和错误操作。

超越和超越

将你迄今为止关于远程操作学到的知识应用到你的函数中,使其能够在任何操作系统上工作。这里有一个提示:有三个内置变量可能很有用:

$IsMacOS
$IsLinux
$IsWindows

这里有一些关键事项需要记住:

  • $Error 包含你会话中的所有错误消息。

  • ErrorVariable 也可以用来存储错误(向其添加 + 符号)。

  • Try/Catch 是你的朋友,但仅限于非终止错误。

24.8 实验室答案

Function Get-PCUpTime {
    param (
        [string[]]$ComputerName = 'localhost'
    )
    try {
        foreach ($computer in $computerName) {
            If ($computer -eq "localhost") {
                Get-Uptime
            }
            Else { Invoke-command -ComputerName $computer -ScriptBlock 
          ➥ { Get-Uptime } -ErrorAction Stop}
        }
    }
    catch {
        Write-Error "Cannot connect To $computer"
    }
}

25 调试技术

在上一章中,我们讨论了如何处理不良情况(错误和异常),但特别是你预料到的不良情况。随着你的脚本变得越来越复杂,还可能发生另一种不良情况。我们已经提到过这些情况,它们被称为错误。这些是长时间编写脚本或早上咖啡不足的副作用。换句话说,它们是我们作为人类的副作用。我们都会犯错误,而本章将专注于一些查找和消除脚本中错误的技术。

注意:我们将深入探讨 PowerShell 扩展为 Visual Studio Code 提供的某些功能,所以如果你需要复习如何设置,请确保回到第二章并遵循那里的步骤。

25.1 输出一切

不深入探讨 Azure Pipelines 的概念,列表 25.1 中的脚本将获取我们关心的已发布工件的相关详细信息,并将它们下载到 Temp 驱动器。如果你以前从未听说过工件这个术语,它基本上是一个在其他工具可以下载的地方发布的文件。此外,你还会在脚本中注意到一些环境变量(以$env:开头)。这是因为脚本是为在存在这些工件的地方运行的 Azure Pipelines 而编写的。

让我们从第十七章中熟悉的内容开始,在第十七章中我们讨论了不同的输出流。不同的流是你的工具箱中的工具,用于理解你的代码在做什么以及何时在做这件事。仔细放置Write-*语句可以使你轻松找到脚本中的错误并回到正轨。我们不会过多地涉及这个主题,因为我们已经将第十七章专门用于输入和输出,但以下列表提供了一个示例,说明何时像Write-Debug这样的功能会派上用场。

列表 25.1:为学习目的修改的 VS Code 发布脚本的一部分

$BUILDS_API_URL = 
➥ "$env:SYSTEM_COLLECTIONURI$env:SYSTEM_TEAMPROJECT/_apis/build/builds/
➥ $env:BUILD_BUILDID"

function Get-PipelineArtifact {
    param($Name)
    try {
        Write-Debug "Getting pipeline artifact for: $Name"
        $res = Invoke-RestMethod "$BUILDS_API_URL)artifacts?api-version=6.0" 
      ➥ -Headers @{
            Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN"
        } -MaximumRetryCount 5 -RetryIntervalSec 1

        if (!$res) {
            Write-Debug 'We did not receive a response from the Azure 
          ➥ Pipelines builds API.'
            return
        }

        $res.value | Where-Object { $_.name -Like $Name }
    } catch {
        Write-Warning $_
    }
}

*# Determine which stages we care about*
$stages = @(
    if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' }
    if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' }
    if ($env:VSCODE_BUILD_STAGE_OSX -eq 'True') { 'macOS' }
)
Write-Debug "Running on the following stages: $stages"

Write-Host 'Starting...' -ForegroundColor Green
$stages | ForEach-Object {
    $artifacts = Get-PipelineArtifact -Name "vscode-$_"

    foreach ($artifact in $artifacts) {
        $artifactName = $artifact.name
        $artifactUrl = $artifact.resource.downloadUrl
        Write-Debug "Downloading artifact from $artifactUrl to Temp:/$artifactName.zip"
        Invoke-RestMethod $artifactUrl -OutFile "Temp:/$artifactName.zip" 
      ➥ -Headers @{
            Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN"
        } -MaximumRetryCount 5 -RetryIntervalSec 1  | Out-Null

        Expand-Archive -Path "Temp:/$artifactName.zip" -DestinationPath 
      ➥ 'Temp:/' | Out-Null
    }
}
Write-Host 'Done!' -ForegroundColor Green

现在,假设你运行这个脚本,但它没有按照你预期的那样工作。调试脚本的最简单方法之一是运行脚本,要求它显示调试流。让我们进行比较。

正常运行脚本会产生

PS > ./publishing.ps1
Starting...
Done!
PS >

这并不是很有信息量。然而,我们只需要将我们的调试首选项设置为Continue,我们就可以看到调试流的全部内容:

PS > $DebugPreference = 'Continue'
PS > ./publishing.ps1
Starting...
DEBUG: Running on the following stages: Windows Linux
DEBUG: Getting pipeline artifact for: vscode-Windows
DEBUG: Downloading artifact from <redacted> to Temp:/vscode-windows-
➥ release.zip
DEBUG: Getting pipeline artifact for: vscode-Linux
DEBUG: Downloading artifact from <redacted> to Temp:/vscode-linux-release.zip
Done!

这是有用的信息。它在 Windows 和 Linux 上运行……但是等等,它不是也应该在 macOS 上运行吗?

$stages = @(
    if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' }
    if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' }
    if ($env:VSCODE_BUILD_STAGE_OSX -eq 'True') { 'macOS' }
)

你看到错误了吗?我会等一会儿。明白了吗?几年前,苹果公司将他们的操作系统名称从 OSX 更改为 macOS,看起来脚本并没有完全正确更新,因为它仍然引用VSCODE_BUILD_STAGE_OSX而不是VSCODE_BUILD_STAGE_MACOS。第一个调试语句表明它只运行 Windows 和 Linux,所以这是我们的线索,表明那里可能有问题。

这种调试方式常用于无法进行交互的环境。Azure Pipelines 和 GitHub Actions 是这种环境的绝佳例子,你无法远程连接到你的脚本正在运行的容器或 VM,因此你唯一的调试选项是利用 PowerShell 的流来尽可能多地提供信息。如果你有在本地机器或你能够访问的容器/VM 上运行脚本的能力,这种类型的调试也很有用,但这里也有互补的解决方案,我们现在将探讨。

25.2 逐行运行

使用 PowerShell 的流进行调试被称为“调试过去”,因为你正在查看已经发生的事情。这种方式的调试很有用,但可能会很繁琐,因为你必须等待看到这些流中显示的内容,然后才能采取行动。如果你在调试流中没有足够的信息,你必须进行更改以添加更多信息到调试流中,这需要你反复运行你的脚本。如果你正在尝试调试脚本运行 30 分钟后发生的问题,这意味着你做的任何更改(即使只是获取更多信息)也需要 30 分钟来验证。幸运的是,PowerShell 团队有几种方法可以减少调试所需的时间。这些策略中的第一个就是我们喜欢称之为 F8 调试逐行调试

前提很简单。让我们拿一个大的脚本,并在我们的控制台中逐行运行它。听起来可能很麻烦,需要复制粘贴每一行,但 VS Code 的 PowerShell 扩展简化了这一过程。让我们从一个基本的脚本开始,以演示:

Write-Host 'hello'
$processName = 'pwsh'
Get-Process $processName

现在创建一个名为 test.ps1 的文件,并将上面的代码放入其中,然后在 VS Code 中打开它。接下来,点击第一行(Write-Host),使光标位于第 1 行(图 25.1)。

图片

图 25.1 Visual Studio Code 与我们的脚本。右上角的“运行”和“运行选择”按钮被突出显示。

我们在屏幕右上角突出显示了两个按钮。如果你将鼠标悬停在这些按钮上,它们分别显示为“运行”和“运行选择(F8)”。运行按钮将运行你的整个脚本,但我们会回来解释为什么这个按钮是特殊的。现在,让我们专注于另一个按钮。实际上,当光标位于第 1 行时,让我们点击“运行选择(F8)”按钮,看看会发生什么。PowerShell 扩展将取当前光标所在的行,并在图 25.2 中的 PowerShell 集成控制台 中运行该片段。

图片

图 25.2 Visual Studio Code 与我们的脚本。它只运行高亮显示的部分。

“运行选择”按钮将运行你选择的任何片段,如图 25.2 所示,或者如果你没有选择任何内容,它将运行当前行。选择一些行并点击“运行选择”按钮。你会注意到它将运行你选择的 exactly 内容。

现在试试看。如果你还没有这样做,运行最后两行(可以通过选择它们或逐行选择),你将看到以下类似输出:

PS > $processName = 'pwsh'
Get-Process $processName

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      40.48      17.77    5286 ...85 pwsh
      0     0.00      11.27      11.49   29257 ...57 pwsh
      0     0.00      13.94       3.32   32501 ...01 pwsh
      0     0.00     131.63     461.71   35051 ...51 pwsh
      0     0.00     121.53      19.31   35996 ...96 pwsh

这里开始变得有趣。在 PowerShell 集成控制台中点击,然后运行 $processName。你会看到我们在脚本中设置的值已经在 PowerShell 集成控制台中持续存在。这意味着我们可以逐行运行脚本并看到脚本在执行过程中的整个状态,这让我们能够更清楚地了解脚本正在做什么。这意味着我们可以更快地调试脚本,因为我们能够实时获得正在发生的事情及其发生时间的概览。

注意:我们称之为“F8 调试”,因为“运行选择”在 VS Code 中绑定到 F8 键,所以你只需按 F8 而不必点击右上角的按钮。

你能够在你的控制台中看到变量 $processName 的值,但你可以更进一步,在你想要的时候将其值设置为其他内容。例如,在你的控制台中(图 25.3)将 $processName 设置为 code*,然后使用“运行选择”来运行第 3 行(Get-Process)。

图片

图 25.3 将 $processName 设置为 code* 并运行脚本的第 3 行

注意,输出不再是 pwsh 的结果,而是 code* 的结果。这模糊了编辑器中的脚本和你的控制台之间的界限,这在你想检查脚本是否正确处理不同的输入时非常有用。话虽如此,请记录你做的任何更改,因为你不希望手动设置的变量在脚本中引起其他问题。如果你的 PowerShell 集成控制台处于不良状态并且你想重新启动它,请使用 Windows 或 Linux 上的 Ctrl+Shift+P 或 macOS 上的 Cmd+Shift+P 打开命令面板,并输入 PowerShell: Restart Current Session 并运行它。这将为你提供一个全新的起点(一个新的 PowerShell 实例),你可以使用它。

上述示例很简单,策略本身也很简单。这是我们通常使用的常规工作流程:

  1. 如果你的脚本有参数,请提前在 PowerShell 集成控制台中设置这些值以模拟使用这些参数值运行脚本。

  2. 选择你相当确定其中没有问题的第一行或脚本的一部分;然后按 F8。

  3. 挖掘一下。在控制台中运行重要变量以查看它们的值。你还可以在控制台中运行你使用的函数,以查看它们返回的内容。

  4. 选择下一行并按 F8。重复 3 次或转到 5。

  5. 找到看起来不对的地方了吗?对你认为需要修改的脚本进行更改,然后回到步骤 1。

通过采用这种策略,你将对自己的 PowerShell 脚本以及他人的脚本进行调试时获得信心(正如我们在第二十三章中讨论的)。在任何工作场所,这都是一项必要的技能,因为当事情变得棘手时,你将会有脚本崩溃,你必须卷起袖子来修复它们。

25.3 嘿,脚本,就在这里停下来……使用断点

F8 调试足以进行脚本的交互式调试,你可以在本章到此为止,做得很好。但是,我们真的想为你准备你可能会在现实世界中看到的内容。为了做到这一点,我们想谈谈第三种调试类型,我们称之为断点调试。断点调试是由我们的朋友,软件开发者/工程师推广的。他们已经使用这种类型的调试很多年了,PowerShell 团队使得在 PowerShell 中使用断点调试成为可能——这对于一个 shell 来说是一个独特的功能(Bash、cmd、Zsh 等都没有断点调试功能)。

那么,这一切都是关于什么呢?好吧,从高层次来看,断点调试的工作原理是这样的:你将以“调试”模式运行你的脚本(VS Code 术语);这告诉 VS Code 你想要在脚本中的哪些行停下来进行进一步检查(这些停止点被称为断点)。我们想明确指出:在上一节中,你已经通过 F8 调试做过这件事了,当时你运行了一个选择,直到你想要调查的部分,但这次它甚至更深入地集成到了 VS Code 中。好的,让我们看看如何在 VS Code 中设置断点以及它是如何工作的。

如图 25.4 所示,当你将光标放在行号上(让我们以行号 3 为例),在 VS Code 的“页边空白”中会出现一个淡红色的点(它总是在行号和左侧的活动栏之间)。如果你点击那个红色点,它就会变成实心,并且当你将鼠标移开时不会再消失。恭喜你,你刚刚设置了你的第一个断点!让我们来测试一下这个断点。还记得旁边的运行按钮吗?那个按钮以“调试”模式运行你的脚本,这意味着如果设置了断点,它们将会停止。让我们试试。点击右上角的运行按钮,或者按 F5 键,它被绑定到运行。

图 25.4 通过点击行号旁边的淡红色点来放置断点。

图 25.5 概述了当你开始调试脚本时你会看到的内容。在屏幕顶部,你会看到一组按钮,这些按钮控制你在调试过程中的下一步操作。

图 25.5 当你的脚本在断点处停止时,VS Code 会显示用于调试脚本的有用信息。这包括指示你在脚本中的停止位置,所有断点的列表,当前设置的变量的列表等等。

下面是每个按钮的功能:

继续—点击脚本上的“播放”按钮以继续运行脚本
单步执行—运行当前高亮的行并在下一行停止
重启—停止运行脚本并从开始处重新开始
停止—停止运行脚本并退出调试体验

目前不用担心 按钮。它们是调用栈概念的一部分,这比我们在这本书中想要达到的层次要高一些。这也意味着我们不会介绍调用栈视图。我们也不会介绍监视视图,因为它不是学习(坦白说我们很少使用这个功能)所必需的,所以我们将它留给你作为研究练习。

现在就试试吧!这是一个让你尝试我们之前提到的所有不同 UI 元素的绝佳机会。运行(带有调试)一个简单的脚本,设置一些变量并运行几个简单的 cmdlets,如 Get-ProcessGet-ChildItem

在本书接下来的内容中,继续利用 F8 和断点调试来提高你的技能。我们保证这就像骑自行车一样。一旦你掌握了技巧,你将永远记住这些知识。你发现脚本中问题的能力以及迭代脚本的能力将比没有这种基础知识的人要好得多。

25.4 实验室

调试方面,熟能生巧。本章的实验室有两个部分。回到你在第二十二章实验室中查看的脚本。对其进行修改并添加更好的日志记录,这样你可以更容易地理解脚本在做什么。

确保你添加的日志只在你想要看到调试日志时显示在屏幕上,否则如果不想看到调试日志,则不会污染屏幕。回顾你在本书中迄今为止编写的每一个脚本。尝试使用

  • F8 调试

  • 断点调试

26 个技巧、窍门和技术

你的午餐月即将结束,因此我们想分享一些随机的额外技巧和技术,以完善你的学习。

26.1 配置文件、提示和颜色:定制外壳

每个 PowerShell 会话都以相同的方式开始:相同的别名、相同的 PSDrives、相同的颜色等等。为什么不稍微定制一下外壳呢?

26.1.1 PowerShell 配置文件

我们之前已经解释过,PowerShell 宿主应用程序和 PowerShell 引擎本身之间是有区别的。宿主应用程序,如控制台或 VS Code,是你向 PowerShell 引擎发送命令的一种方式。引擎执行你的命令,宿主应用程序负责显示结果。宿主应用程序还负责在每次启动外壳时加载和运行 配置文件脚本

这些配置文件脚本可以用来通过加载模块、更改到不同的起始目录、定义你想要使用的函数等方式来定制 PowerShell 环境。例如,以下是莎拉在她的电脑上使用的配置文件脚本:

Import-Module ActiveDirectory
Import-Module DBATools
cd c:\

该配置文件加载莎拉使用最多的两个模块,并将其更改到她的 C: 驱动器的根目录,这是莎拉喜欢开始工作的位置。你可以将任何你喜欢的命令放入你的配置文件中。

注意:你可能认为没有必要加载 Active Directory 模块,因为当莎拉尝试使用该模块中的任何命令时,PowerShell 会隐式加载它。但该特定模块还映射了一个 AD: PSDrive,莎拉喜欢在外壳启动时就可用它。

没有默认的配置文件,你创建的确切配置文件脚本将取决于你希望它如何工作。如果你运行 help about_profiles,可以找到详细信息,但你主要需要考虑你是否将在多个不同的宿主应用程序中工作。例如,我们经常在常规控制台、Windows Terminal 和 VS Code 之间切换。我们希望所有三个都运行相同的配置文件,因此我们必须小心地在正确的位置创建正确的配置文件脚本文件。我们还要注意配置文件中包含的内容,因为一些调整控制台特定设置(如颜色)的命令可能会在 VS Code 或 Windows Terminal 中引起错误。以下是控制台宿主尝试加载的文件及其尝试加载的顺序:

  1. $pshome\profile.ps1—无论用户使用哪个宿主,此脚本都将为计算机上的所有用户执行(记住 $pshome 在 PowerShell 中是预定义的,包含 PowerShell 安装文件夹的路径)。

  2. $pshome\Microsoft.PowerShell_profile.ps1—如果计算机上的用户正在使用控制台宿主,则此脚本将执行。

  3. $pshome/Microsoft.VSCode_profile.ps1—如果你使用带有 PowerShell 扩展的 VS Code,则此脚本将执行。

  4. $home\Documents\WindowsPowerShell\profile.ps1—这个配置文件只会为当前用户执行(因为它位于用户的家目录下),无论他们使用哪个主机。

  5. $home\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1—如果当前用户使用控制台宿主,这个配置文件将会执行。如果他们使用带有 PowerShell 扩展的 VS Code,则将执行 $home\Documents\WindowsPowerShell\Microsoft.VSCode_profile.ps1 脚本。

如果一个或多个这些脚本不存在,没有问题。宿主应用将简单地跳过它并继续下一个。

在 64 位系统上,32 位和 64 位的脚本都有变体,因为 PowerShell 本身就有 32 位和 64 位版本。你并不总是希望 64 位 shell 中运行的命令与 32 位 shell 中运行的命令相同——也就是说,某些模块和其他扩展只适用于一个或另一个架构,因此你不会希望 32 位配置文件尝试将 64 位模块加载到 32 位 shell 中,因为这是不会工作的。

现在尝试一下 运行 $Profile | Format-List -force 并列出所有配置文件。

注意,about_profiles 中的文档与这里列出的不同,并且我们的经验是前面的列表是正确的。以下是关于该列表的几个更多要点:

  • $pshome 是一个内置的 PowerShell 变量,包含 PowerShell 本身的安装文件夹;在大多数系统上,它位于 C:\Program Files\PowerShell\7。

  • $home 是另一个内置变量,指向当前用户的配置文件文件夹(例如 C:\Users\Sarah)。

  • 我们用“Documents”来指代“文档”文件夹,但在 Windows 的一些版本中,它将是“我的文档”。

  • 我们写了“无论他们使用哪个宿主”,但从技术上讲,这并不完全正确。对于由微软编写的宿主应用(例如,VS Code)来说,这是正确的,但无法强迫非微软宿主应用的作者遵循这些规则。

因为我们希望无论使用控制台宿主还是VS Code,都能加载相同的 shell 扩展,所以我们选择自定义 $home\Documents\WindowsPowerShell\ profile.ps1,因为这个配置文件适用于微软提供的两个宿主应用。

现在尝试一下 通过为自己创建一个或多个配置文件脚本来测试你的配置文件。即使你只放入一个简单的消息,例如 Write-Output "It Worked",这也是观察不同文件在行动中的好方法。记住,你必须关闭 shell(或 VS Code)并重新打开它才能看到配置文件脚本运行。

请记住,配置文件脚本也是脚本,并受你的 shell 当前执行策略的影响。如果你的执行策略是 Restricted,则配置文件不会运行;如果你的策略是 AllSigned,则配置文件必须经过签名。第四章讨论了执行策略。

小贴士:在 VS Code 中,你可以运行命令code $profile,它将打开 VS Code 配置文件。同样,在控制台中,你可以运行notepad $profile,它将打开你的控制台特定配置文件。

26.1.2 自定义提示

PowerShell 提示——你在本书的大部分内容中看到的PS C:\>——是由一个名为Prompt的内置函数生成的。如果你想自定义提示,你可以替换这个函数。在配置文件脚本中定义一个新的Prompt函数是一种可以做到的事情,这样每次打开 shell 时你的更改就会生效。以下是默认提示:

function prompt
{
    $(if (test-path variable:/PSDebugContext) { '[DBG]: ' }
    else { '' }) + 'PS ' + $(Get-Location) ` 
    + $(if ($nestedpromptlevel -ge 1) { '>>' }) + '> '
}

这个提示首先检查 shell 的VARIABLE:驱动器中是否定义了$DebugContext变量。如果是,这个函数将在提示的开始处添加[DBG]:。如果不是,提示将定义为PS以及当前位置,这是由Get-Location命令返回的。如果 shell 处于嵌套提示中,如内置的$nestedpromptlevel变量定义的,提示将添加>>

这里有一个替代的提示函数。你可以直接将其输入到任何配置文件脚本中,使其成为你的 shell 会话的标准提示:

function prompt {
 $time = (Get-Date).ToShortTimeString()
 "$time [$env:COMPUTERNAME]:> "
}

这个替代提示首先显示当前时间,然后是当前计算机名(位于方括号内):

6:07 PM [CLIENT01]:>

注意,这使用了 PowerShell 的双引号特殊行为,其中 shell 将变量(如$time)替换为其内容。

你可以添加到配置文件中最有用的代码之一是更改 PowerShell 窗口的标题栏:

$host.UI.RawUI.WindowTitle = "$env:username" 

26.1.3 调整颜色

在前面的章节中,我们提到了当一系列长错误消息在 shell 中滚动时,我们可能会多么紧张。Sarah 小时候在英语课上总是很吃力,看到所有红色的文字让她想起了从汉森小姐那里收到的论文,上面用红笔做了很多标记。真恶心。幸运的是,PowerShell 让你能够修改它使用的默认颜色的大部分(如图 26.1)。

图 26.1 配置默认 shell 屏幕颜色

默认的文本前景和背景颜色可以通过点击 PowerShell 窗口右上角的控制框进行修改。从那里,选择属性,然后选择颜色选项卡。

修改错误、警告和其他消息的颜色稍微复杂一些,需要运行一个命令。但是,你可以将这个命令放入你的配置文件中,以便每次打开 shell 时执行。以下是将错误消息的前景色更改为绿色的方法,我们发现这要舒适得多:

(Get-Host).PrivateData.ErrorForegroundColor = "green"

你可以更改以下设置的颜色:

  • ErrorForegroundColor

  • ErrorBackgroundColor

  • WarningForegroundColor

  • WarningBackgroundColor

  • DebugForegroundColor

  • DebugBackgroundColor

  • VerboseForegroundColor

  • VerboseBackgroundColor

  • ProgressForegroundColor

  • ProgressBackgroundColor

下面是一些你可以选择的颜色:

  • Red

  • Yellow

  • 黑色

  • 白色

  • 绿色

  • 青色

  • 洋红色

  • 蓝色

大多数这些颜色也有深色版本:深红色深黄色深绿色深青色深蓝色等等。

26.2 操作符:-as, -is, -replace, -join, -split, -contains, -in

这些额外的操作符在多种情况下都很有用。它们让你能够处理数据类型、集合和字符串。

26.2.1 -as 和 -is

-as 操作符试图将现有对象转换成另一种类型,从而生成一个新的对象。例如,如果你有一个包含小数的数字(可能是除法操作的结果),你可以通过转换,或者说是强制类型转换,这个数字到整数来去除小数部分:

1000 / 3 -as [int]

要转换的对象首先出现,然后是 -as 操作符,接着是在方括号中,你想要转换到的类型。类型可以包括 [string][xml][int][single][double][datetime] 等,尽管你可能最常使用的是这些类型。技术上,这个将转换为整数的例子会将分数四舍五入到整数,而不是仅仅截断数字的小数部分。

-is 操作符的工作方式类似:它被设计用来返回 TrueFalse,以指示一个对象是否为特定类型。以下是一些单行示例:

123.45 -is [int]
"SRV02" -is [string]
$True -is [bool]
(Get-Date) -is [datetime]

现在尝试运行这些单行命令以查看结果。

26.2.2 -replace

-replace 操作符使用正则表达式,并设计用来定位另一个字符串中所有出现的特定字符串,并用第三个字符串来替换这些出现:

PS C:\> "192.168.34.12" -replace "34","15"
192.168.15.12

源字符串首先出现,然后是 -replace 操作符。然后你提供要在源字符串中搜索的字符串,后面跟着一个逗号和用于替换搜索字符串的字符串。在上面的例子中,我们将 34 替换为 15

这与 string replace() 方法不同,后者是静态文本替换。虽然它们的工作方式相似,但它们非常不同。

26.2.3 -join 和 -split

-join-split 操作符被设计用来将数组转换为分隔列表,反之亦然。例如,假设你创建了一个包含五个元素的数组:

PS C:\> $array = "one","two","three","four","five"
PS C:\> $array
one
two
three
four
five

这之所以有效,是因为 PowerShell 会自动将逗号分隔的列表视为数组。现在,假设你想要将这个数组连接成一个以管道符分隔的字符串。你可以使用 -join 操作符来完成这个操作:

PS C:\> $array -join "|"
one|two|three|four|five

将结果保存到变量中可以让你重用它,甚至可以将它输出到文件:

PS C:\> $string = $array -join "|"
PS C:\> $string
one|two|three|four|five
PS C:\> $string | out-file data.dat

-split 操作符做的是相反的操作:它将分隔字符串转换为一个数组。例如,假设你有一个包含一行和四列的制表符分隔的文件。显示文件内容可能看起来像这样:

PS C:\> gc computers.tdf
Server1 Windows East    Managed

请记住,gcGet-Content 的别名。

你可以使用 -split 操作符将字符串拆分为四个单独的数组元素:

PS C:\> $array = (gc computers.tdf) -split "`t"
PS C:\> $array
Server1
Windows
East
Managed

注意到使用了转义字符,反引号,以及一个 t (`t) 来定义制表符字符。这必须放在双引号中,以便识别转义字符。结果数组有四个元素,你可以通过它们的索引号单独访问它们:

PS C:\> $array[0]
Server1

26.2.4 - 包含和 - 在

-contains 操作符常常让 PowerShell 新手感到困惑。你会看到人们尝试这样做:

PS C:\> 'this' -contains '*his*'
False

事实上,它们意味着要使用 -like 操作符:

. PS C:\> 'this' -like '*his*'
True

-like 操作符旨在进行通配符字符串比较。-contains 操作符用于测试给定对象是否存在于集合中。例如,创建一个字符串对象集合,然后测试给定字符串是否在该集合中:

PS C:\> $collection = 'abc','def','ghi','jkl'
PS C:\> $collection -contains 'abc'
True
PS C:\> $collection -contains 'xyz'
False

-in 操作符做同样的事情,但它颠倒了操作数的顺序,使得集合在右边,测试对象在左边:

PS C:\> $collection = 'abc','def','ghi','jkl'
PS C:\> 'abc' -in $collection
True
PS C:\> 'xyz' -in $collection
False

26.3 字符串操作

假设你有一段文本字符串,你需要将其转换为全部大写字母。或者你可能需要从字符串中获取最后三个字符。你将如何操作?

在 PowerShell 中,字符串是对象,并且它们附带了许多方法。记住,方法是一种告诉对象做什么的方式,通常是对它自己,而且你可以通过将对象管道到 gm 来发现可用的方法:

PS C:\> "Hello" | get-member
   TypeName: System.String
Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object value...
Contains         Method                bool Contains(string value)
CopyTo           Method                System.Void CopyTo(int sourceInde...
EndsWith         Method                bool EndsWith(string value), bool...
Equals           Method                bool Equals(System.Object obj), b...
GetEnumerator    Method                System.CharEnumerator GetEnumerat...
GetHashCode      Method                int GetHashCode()
GetType          Method                type GetType()
GetTypeCode      Method                System.TypeCode GetTypeCode()
IndexOf          Method                int IndexOf(char value), int Inde...
IndexOfAny       Method                int IndexOfAny(char[] anyOf), int...
Insert           Method                string Insert(int startIndex, str...
IsNormalized     Method                bool IsNormalized(), bool IsNorma...
LastIndexOf      Method                int LastIndexOf(char value), int ...
LastIndexOfAny   Method                int LastIndexOfAny(char[] anyOf),...
Normalize        Method                string Normalize(), string Normal...
PadLeft          Method                string PadLeft(int totalWidth), s...
PadRight         Method                string PadRight(int totalWidth), ...
Remove           Method                string Remove(int startIndex, int...
Replace          Method                string Replace(char oldChar, char...
Split            Method                string[] Split(Params char[] sepa...
StartsWith       Method                bool StartsWith(string value), bo...
Substring        Method                string Substring(int startIndex),...
ToCharArray      Method                char[] ToCharArray(), char[] ToCh...
ToLower          Method                string ToLower(), string ToLower(...
ToLowerInvariant Method                string ToLowerInvariant()
ToString         Method                string ToString(), string ToStrin...
ToUpper          Method                string ToUpper(), string ToUpper(...
ToUpperInvariant Method                string ToUpperInvariant()
Trim             Method                string Trim(Params char[] trimCha...
TrimEnd          Method                string TrimEnd(Params char[] trim...
TrimStart        Method                string TrimStart(Params char[] tr...
Chars            ParameterizedProperty char Chars(int index) {get;}
Length           Property              System.Int32 Length {get;}

一些更有用的 String 方法包括以下内容:

  • IndexOf() — 告诉你给定字符在字符串中的位置:

    PS C:\> "SRV02".IndexOf("-")
    6
    
  • Split()Join()Replace() — 操作类似于我们在上一节中描述的 -split-join-replace 操作符。我们倾向于使用 PowerShell 操作符而不是 String 方法。

  • ToLower()ToUpper() — 转换字符串的大小写:

    PS C:\> $computername = "SERVER17"
    PS C:\> $computername.tolower()
    server17
    
  • Trim() — 从字符串的两端删除空白。

  • TrimStart()TrimEnd() — 分别从字符串的开始或结束处删除空白:

    PS C:\> $username = "    Sarah "
    PS C:\> $username.Trim()
    Sarah
    

所有这些 String 方法都是操纵和修改 String 对象的极好方式。请注意,所有这些方法都可以与包含字符串的变量一起使用——如在 ToLower()Trim() 示例中所示——或者可以直接与静态字符串一起使用,如在 IndexOf() 示例中所示。

26.4 日期操作

String 对象一样,Date(或如果你更喜欢,DateTime)对象也附带了许多方法,允许日期和时间的操作和计算:

PS C:\> get-date | get-member
   TypeName: System.DateTime
Name                 MemberType     Definition
----                 ----------     ----------
Add                  Method         System.DateTime Add(System.TimeSpan ...
AddDays              Method         System.DateTime AddDays(double value)
AddHours             Method         System.DateTime AddHours(double value)
AddMilliseconds      Method         System.DateTime AddMilliseconds(doub...
AddMinutes           Method         System.DateTime AddMinutes(double va...
AddMonths            Method         System.DateTime AddMonths(int months)
AddSeconds           Method         System.DateTime AddSeconds(double va...
AddTicks             Method         System.DateTime AddTicks(long value)
AddYears             Method         System.DateTime AddYears(int value)
CompareTo            Method         int CompareTo(System.Object value), ...
Equals               Method         bool Equals(System.Object value), bo...
GetDateTimeFormats   Method         string[] GetDateTimeFormats(), strin...
GetHashCode          Method         int GetHashCode()
GetType              Method         type GetType()
GetTypeCode          Method         System.TypeCode GetTypeCode()
IsDaylightSavingTime Method         bool IsDaylightSavingTime()
Subtract             Method         System.TimeSpan Subtract(System.Date...
ToBinary             Method         long ToBinary()
ToFileTime           Method         long ToFileTime()
ToFileTimeUtc        Method         long ToFileTimeUtc()
ToLocalTime          Method         System.DateTime ToLocalTime()
ToLongDateString     Method         string ToLongDateString()
ToLongTimeString     Method         string ToLongTimeString()
ToOADate             Method         double ToOADate()
ToShortDateString    Method         string ToShortDateString()
ToShortTimeString    Method         string ToShortTimeString()
ToString             Method         string ToString(), string ToString(s...
ToUniversalTime      Method         System.DateTime ToUniversalTime()
DisplayHint          NoteProperty   Microsoft.PowerShell.Commands.Displa...
Date                 Property       System.DateTime Date {get;}
Day                  Property       System.Int32 Day {get;}
DayOfWeek            Property       System.DayOfWeek DayOfWeek {get;}
DayOfYear            Property       System.Int32 DayOfYear {get;}
Hour                 Property       System.Int32 Hour {get;}
Kind                 Property       System.DateTimeKind Kind {get;}
Millisecond          Property       System.Int32 Millisecond {get;}
Minute               Property       System.Int32 Minute {get;}
Month                Property       System.Int32 Month {get;}
Second               Property       System.Int32 Second {get;}
Ticks                Property       System.Int64 Ticks {get;}
TimeOfDay            Property       System.TimeSpan TimeOfDay {get;}
Year                 Property       System.Int32 Year {get;}
DateTime             ScriptProperty System.Object DateTime {get=if ((& {...

注意,这些属性使你能够访问 DateTime 的一部分,例如日期、年份或月份:

PS C:\> (get-date).month
10

这些方法使两件事成为可能:计算和转换为其他格式。例如,为了获取 90 天前的日期,我们喜欢使用 AddDays() 并传入一个负数:

PS C:\> $today = get-date
PS C:\> $90daysago = $today.adddays(-90)
PS C:\> $90daysago
Saturday, March 13, 2021 11:26:08 AM

To 开头的方法旨在以不同的格式提供日期和时间,例如短日期字符串:

PS C:\> $90daysago.toshortdatestring()
3/13/2021

这些方法都使用你电脑的当前区域设置来确定正确的时间日期格式化方式。

26.5 处理 WMI 日期

虽然 WMI 在 PowerShell 7 中不可用,但我们知道有些人仍在使用 Windows PowerShell 5.1,因此我们想分享一些关于 WMI 倾向于以难以使用的字符串存储日期和时间信息的知识。例如,Win32_OperatingSystem类跟踪计算机上次启动的时间,日期和时间信息如下所示:

PS C:\> get-wmiobject win32_operatingsystem | select lastbootuptime
lastbootuptime
--------------
20101021210207.793534-420

PowerShell 的设计者知道你不太可能轻松地使用这些信息,因此他们为每个 WMI 对象添加了一对转换方法。将任何WMI对象通过gm管道传输,你可以在末尾或附近看到这些方法:

PS C:\> get-wmiobject win32_operatingsystem | gm
   TypeName: System.Management.ManagementObject#root\cimv2\Win32_OperatingS
ystem
Name                                      MemberType   Definition
----                                      ----------   ----------
Reboot                                    Method       System.Management...
SetDateTime                               Method       System.Management...
Shutdown                                  Method       System.Management...
Win32Shutdown                             Method       System.Management...
Win32ShutdownTracker                      Method       System.Management...
BootDevice                                Property     System.String Boo...
...
PSStatus                                  PropertySet  PSStatus {Status,...
ConvertFromDateTime                       ScriptMethod System.Object Con...
ConvertToDateTime                         ScriptMethod System.Object Con...

我们删除了大部分中间输出,以便你可以轻松找到ConvertFromDateTime``()ConvertToDateTime()方法。在这种情况下,你开始的是一个WMI日期和时间,你想要将其转换为正常的日期和时间,所以你这样做:

PS C:\> $os = get-wmiobject win32_operatingsystem
PS C:\> $os.ConvertToDateTime($os.lastbootuptime)
Thursday, October 20, 2015 9:02:07 PM

如果你想要将日期和时间信息作为正常表格的一部分,你可以使用Select-ObjectFormat-Table来创建自定义的计算列和属性:

PS C:\> get-wmiobject win32_operatingsystem | select BuildNumber,__SERVER,
[CA]@{l='LastBootTime';e={$_.ConvertToDateTime($_.LastBootupTime)}}
BuildNumber               __SERVER                 LastBootTime
-----------               --------                 ------------
7600                      SRV02                10/20/2015 9:02:07 PM

如果你使用的是CIM命令,日期的处理会简单得多,因为它们会自动将大多数日期/时间值转换为人类可读的格式。

26.6 设置默认参数值

大多数 PowerShell 命令至少包含几个具有默认值的参数。例如,只运行Dir,它默认为当前路径,无需指定-Path参数。

默认值存储在一个名为$PSDefaultParameterValues的特殊内置变量中。每次你打开一个新的 shell 窗口时,该变量都是空的,并且它的目的是用哈希表(你可以在配置文件脚本中这样做,以便始终有效)填充。

例如,假设你想创建一个新的包含用户名和密码的凭据对象,并让该凭据自动应用于所有具有-Credential参数的命令:

PS C:\> $credential = Get-Credential -UserName Administrator 
-Message "Enter Admin credential"
PS C:\> $PSDefaultParameterValues.Add('*:Credential',$credential)

或者,你可能只想在每次运行时强制Invoke-Command cmdlet 提示输入凭据。在这种情况下,而不是分配一个默认值,你会分配一个执行Get-Credential命令的脚本块:

PS C:\> $PSDefaultParameterValues.Add('Invoke-Command:Credential',
(Get-Credential -Message 'Enter administrator credential' 
-UserName Administrator})

你可以看到Add()方法第一个参数的基本格式是<-cmdlet> :<parameter>,其中<cmdlet>可以接受通配符,如*Add()方法的第二个参数是你想要设置为默认值的值,或者是一个执行另一个或多个命令的脚本块。

你可以始终检查$PSDefaultParameterValues以查看它包含的内容:

PS C:\> $PSDefaultParameterValues
Name                           Value
----                           -----
*:Credential                   System.Management.Automation.PSCredenti
Invoke-Command:Credential      Get-Credential -Message 'Enter administ

你可以通过阅读 shell 的about_parameters_default_values帮助文件来了解更多关于这个功能的信息。

超越

PowerShell 变量受称为作用域的东西控制。我们在第十六章中简要介绍了作用域,它与这些默认参数值有关。

如果你通过命令行设置$PSDefaultParameterValues,它将应用于该 shell 会话中运行的 所有脚本和命令。但如果你在脚本中设置$-PSDefaultParameterValues,它将只应用于该脚本所做的事情。这是一个有用的技巧,因为它意味着你可以用一个脚本开始,并设置很多默认值,这些值不会应用于其他脚本,或 shell 本身。

“脚本中发生的事情留在脚本中”这个概念是作用域的核心。如果你想进一步探索,可以阅读 shell 的about_scope帮助文件。

26.7 玩转脚本块

脚本块是 PowerShell 的关键部分,你已经使用它们很多了:

  • Where-Object-FilterScript参数接受脚本块。

  • ForEach-Object-Process参数接受脚本块。

  • 用于使用Select-Object创建自定义属性或使用Format-Table创建自定义列的哈希表接受脚本块作为EExpression键的值。

  • 如前所述,默认参数值可以设置为脚本块。

  • 一些远程和作业相关的命令,包括Invoke-CommandStart-Job,接受脚本块作为-ScriptBlock参数。

那么什么是脚本块呢?一般来说,它是由大括号 {} 包围的任何东西,除了哈希表,哈希表使用大括号,但前面有@符号。你甚至可以直接从命令行输入脚本块并将其分配给变量。然后你可以使用调用运算符&来运行该块:

PS C:\> $block = {
Get-process | sort -Property vm -Descending | select -first 10 }
PS C:\> &$block
Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    680      42    14772      13576  1387     3.84    404 svchost
    454      26    68368      75116   626     1.28   1912 powershell
    396      37   179136      99252   623     8.45   2700 powershell
    497      29    15104       6048   615     0.41   2500 SearchIndexer
    260      20     4088       8328   356     0.08   3044 taskhost
    550      47    16716      13180   344     1.25   1128 svchost
   1091      55    19712      35036   311     1.81   3056 explorer
    454      31    56660      15216   182    45.94   1596 MsMpEng
    163      17    62808      27132   162     0.94   2692 dwm
    584      29     7752       8832   159     1.27    892 svchost

你可以用脚本块做很多事情。如果你想自己探索可能性,请阅读 shell 的about_script_blocks帮助文件。

26.8 更多技巧、窍门和技术

正如我们在本章开头所说的,这是一个概述,我们想向你展示一些随机的小东西,但它们并没有很好地融入前面的任何一章。当然,随着你对 shell 了解得更多,以及你获得更多经验,你将继续从中获得技巧和窍门。

你还可以查看我们的 Twitter 动态——@TylerLeonhardt, @TravisPlunk, 和 @PSJamesP——在那里我们定期分享我们发现的和觉得有用的技巧和技术。别忘了 PowerShell.org 上的论坛。有时候,一点一滴地学习可以是一个更容易成为技术专家的方法,所以请将这些以及其他你遇到的任何来源视为逐步和持续提高你的 PowerShell 技能的方式。

27 永无止境

我们几乎到了这本书的结尾,但你的 PowerShell 探索之旅远远没有结束。在 Shell 中还有更多东西要学习,根据你在本书中学到的知识,你将能够自学其中很大一部分。这一简短章节将帮助你找到正确的方向。

27.1 进一步探索的想法

首先,如果你是一名数据库管理员,无论是职业还是偶然,我们强烈建议你调查 dbatools,这是一个包含超过 500 个命令的免费 PowerShell 模块,可以安全快速地自动化你所有需要经常做的任务。像 PowerShell 社区一样,dbatools 社区也是友好和吸引人的;你可以在dbatools.io了解更多关于它们和该模块的信息。dbatools 背后的团队还写了《一个月午餐学会 dbatools》一书,你可以在mng.bz/4jED免费阅读该书的一章。

本书重点介绍了你需要成为有效的 PowerShell 工具用户的技能和技术。换句话说,你应该能够开始使用所有可用的数千个 PowerShell 命令来完成任务,无论你的需求是关于 Windows、Microsoft 365、SharePoint 还是其他什么。

你的下一步是开始组合命令以创建自动化、多步骤的过程,并以一种产生打包、可供他人使用的工具的方式来做。我们称之为工具制作,尽管它更像是一个非常长的脚本或函数,而且这是它自己的完整书籍的主题,即 Don Jones 和 Jeffery Hicks 所著的《一个月午餐学会 PowerShell 脚本编写》(Manning,2017)。但即使你在这本书中学到了,你也可以生成包含完成任务所需的所有命令的参数化脚本——这是工具制作的开始。工具制作还涉及哪些内容?

  • PowerShell 的简化脚本语言

  • 范围

  • 函数,以及将多个工具构建到单个脚本文件中的能力

  • 错误处理

  • 编写帮助

  • 调试

  • 自定义格式化视图

  • 自定义类型扩展

  • 脚本和清单模块

  • 使用数据库

  • 工作流

  • 管道故障排除

  • 复杂的对象层次结构

  • 全球化和本地化

  • 代理函数

  • 限制性远程操作和委托管理

  • 使用.NET

还有更多。如果你对此感兴趣并且具备正确的背景技能,你甚至可能是 PowerShell 的第三大受众:软件开发者。围绕为 PowerShell 开发、在开发中使用 PowerShell 以及更多内容存在一套技术和技术。这是一个庞大的产品!

27.2 “现在我读完了这本书,我应该从哪里开始?”

现在最好的做法是选择一个任务。选择你在生产环境中个人认为重复性高的任务,并使用 shell 自动化它。是的,学习如何编写脚本可能需要更长的时间,但当你第二次需要它时,你的工作已经为你准备好了。你几乎肯定会遇到不知道如何操作的事情,这正是开始学习的完美地方。以下是我们看到其他管理员解决的问题中的一些:

  • 编写一个脚本,更改服务使用的登录密码,并使其针对运行该服务的多台计算机。(你可以用一条命令完成这个操作。)

  • 编写一个脚本,自动化新用户配置,包括创建用户账户、邮箱和主目录。

  • 编写一个脚本,以某种方式管理 Exchange 或 M635 邮箱——比如获取最大邮箱的报告,或者根据邮箱大小创建费用报告。

最重要的是要记住不要过度思考。一位 PowerShell 开发者曾经遇到一位管理员,他花了数周时间在 PowerShell 中编写一个健壮的文件复制脚本,以便能够在 Web 服务器群集中部署内容。“为什么不直接使用 xcopy 或 robocopy 呢?”他问道。管理员盯着他看了一分钟,然后笑了。他太专注于“在 PowerShell 中完成它”,以至于忘记了 PowerShell 可以使用所有已经存在的优秀工具。

27.3 你会越来越喜欢的其他资源

我们在 PowerShell 上花费了大量时间,撰写关于 PowerShell 的文章,并教授 PowerShell。问问我们的家人——有时我们几乎无法停止谈论它,以至于吃饭的时间都不够。这意味着我们已经积累了大量我们每天使用并推荐给所有学生的在线资源。希望它们也能为你提供一个良好的起点:

  • powershell.org—这应该是你的第一站。在这里,你可以找到从问答论坛到免费电子书、免费网络研讨会、现场教育活动等一切内容。它是 PowerShell 社区的一个中心聚集地,包括已经运行多年多年的播客。

  • youtube.com/powershellorg—PowerShell.org 的 YouTube 频道有大量的免费 PowerShell 视频,包括在 PowerShell + DevOps 全球峰会记录的会议。

  • jdhitsolutions.com—这是 Jeff Hick 的多功能脚本和 PowerShell 博客。

  • devopscollective.org—这是 PowerShell.org 的母组织,专注于 IT 管理的更大图景 DevOps 方法。

学生们经常询问我们是否推荐其他 PowerShell 书籍。两本推荐的书是 Don Jones 和 Jeffery Hicks 所著的 Learn PowerShell Scripting in a Month of Lunches(Manning, 2017)以及 Don Jones、Jeffery Hicks 和 Richard Siddaway 所著的 PowerShell in Depth, Second Edition(Manning, 2014)。Windows PowerShell in Action, Third Edition(Manning, 2017)是由该语言的设计者之一 Bruce Payette 以及 Microsoft MVP Richard Siddaway 撰写的一部全面介绍该语言的书籍。我们还推荐 Jeffery Hicks、Richard Siddaway、Oisin Grehan 和 Aleksandar Nikolic 所著的 PowerShell Deep Dives(Manning, 2013),这是一本由 PowerShell MVPs 撰写的深入技术文章集(本书的收益将用于 Save the Children 慈善机构,所以请购买三本)。最后,如果你是视频培训的粉丝,Pluralsight 网站上有大量的 PowerShell 视频课程。Pluralsight.com。Tyler 还有一个关于 PowerShell 的视频介绍,“如何导航 PowerShell 帮助系统”,最初是为 Twitch 录制的,现在可以在 mng.bz/QW6R 上免费观看。

附录。PowerShell 速查表

这是我们将许多小陷阱集中在一个地方的机会。如果你曾经遇到困难,记不起某个东西是什么或做什么,首先翻到这个附录。

A.1 标点符号

PowerShell 充满了标点符号,其中许多在帮助文件中的含义与在 shell 本身中的含义不同。以下是它们在 shell 中的含义:

  • 反引号 (``` ``)——PowerShell 的转义字符。它移除了其后任何字符的特殊含义。例如,空格通常是一个分隔符,这就是为什么cd c:\Program Files会生成错误。转义空格,cd c:\Program Files`,移除了这种特殊含义并强制空格被视为一个字面量,因此命令可以正常工作。

  • 波浪号 (~)——当波浪号用作路径的一部分时,它代表当前用户的家目录,如UserProfile环境变量中定义的那样。

  • 括号 ( )——它们有几种用法:

    • 就像数学一样,括号定义了执行顺序。PowerShell 首先执行括号内的命令,从最内层的括号到最外层的括号。这是一种运行命令并将输出传递给另一个命令的参数的好方法:Get-Service -computerName (Get-Content c:\computernames.txt)

    • 括号也包含了方法的参数,即使方法不需要任何参数也必须包含它们:例如,$mystring.replace('ship','spaceship')Delete()

  • 方括号 []——在 shell 中有两个主要用途:

    • 当你想要引用数组或集合中的单个对象时,它们包含索引号:$services[2]$services获取第三个对象(索引始终从 0 开始)。

    • 当你将数据转换为特定类型时,它们包含数据类型。例如,$myresult / 3 -as [int]将结果转换为整数(整数),而[xml]$data = Get-Content data.xml将读取Data.xml的内容并尝试将其解析为有效的 XML 文档。

  • 大括号 {}——也称为大括号,它们有三个用途:

    • 它们包含可执行代码块或命令,称为脚本块。这些通常被提供给期望脚本块或过滤器块的参数:例如,Get-Service | Where-Object { $_.Status -eq 'Running' }

    • 它们包含组成新哈希表的键值对。开括号总是由一个@符号 precedes。在以下示例中,我们使用大括号来包围哈希表的键值对(有两个),以及包围一个表达式脚本块,这是第二个键e: $hashtable = @{l='Label';e={expression}}的值。

    • 当变量名包含空格或其他在变量名中通常非法的字符时,必须用大括号包围名称:${My Variable}

  • 单引号 (')——它们包含字符串值。PowerShell 在单引号内不寻找转义字符,也不寻找变量。

  • 双引号 (")—这些包含字符串值。PowerShell 在双引号内查找转义字符和$字符。转义字符被处理,跟随$符号(字母数字字符)的字符被视为变量名,并替换该变量的内容。例如,如果变量$one包含值World,那么$two = "Hello $one n"将包含Hello World和一个换行符( `n ``是换行符)。

  • 美元符号 ($)—这告诉 shell,接下来的字母数字字符代表一个变量名。当与处理变量的 cmdlet 一起工作时,这可能会很棘手。假设$one包含值two,那么New-Variable -name $one -value 'Hello'将创建一个名为two的新变量,其值为Hello,因为美元符号告诉 shell 你想使用$one的内容。相比之下,New-Variable -name one -value 'Hello'将创建一个新变量$one

  • 百分号 (%)—这是ForEach-Object cmdlet 的别名。它也是取模运算符,返回除法操作的余数。

  • 问号 (?)—这是Where-Object cmdlet 的别名。

  • 右尖括号 (>)—这是一种Out-File cmdlet 的别名。它不是真正的别名,但它确实提供了 cmd.exe 风格的文件重定向:dir > files.txt

  • 数学运算符 (+, -, *, /, 和 %)—这些作为标准算术运算符。请注意,+也用于字符串连接。

  • 破折号或连字符 (-)—它位于参数名称和许多运算符之前,例如-computerName-eq。它还分隔 cmdlet 名称的动词和名词部分,如Get-Content,并作为减法算术运算符。

  • at 符号 (@)—在 shell 中有四种用途:

    • 它位于哈希表开括号之前(参见此列表中的括号)。

    • 当在括号之前使用时,它包围一个由逗号分隔的值列表,这些值形成一个数组:$array = @(1,2,3,4)@符号和括号都是可选的,因为 shell 通常将任何由逗号分隔的列表视为数组。

    • 它表示一个 here-string,即一组字面字符串文本。here-string 以@"开始,以"@"结束,并且结束标记必须位于新行的开头。运行help about_quoting_rules获取更多信息及示例。here-string 也可以使用单引号定义。

    • 它是 PowerShell 的 splat 运算符。如果你构建一个键与参数名称匹配的哈希表,并且那些值的键是参数的值,那么你可以将哈希表 splat 到 cmdlet 中。运行help about_splatting了解更多信息。

  • 与号 (&)—这是 PowerShell 的调用运算符,指示 shell 将某物视为命令并运行它。例如,$a = "Dir"将字符串Dir放入变量$a中。然后& $a将运行Dir命令。

  • 分号 (;)—这用于分隔单行上包含的两个独立的 PowerShell 命令:Dir ; Get-Process 将运行 Dir 然后运行 Get-Process。结果被发送到单个管道,但 Dir 的结果不会传递到 Get-Process

  • 井号,或哈希标签 (#)—这用作注释字符。任何跟在 # 后面,直到下一个换行符的字符都将被 shell 忽略。尖括号 (<>) 用作定义块注释的标签的一部分:使用 <# 开始一个块注释,使用 #> 结束一个。块注释中的所有内容都将被 shell 忽略。

  • 等号 (=)—这是赋值运算符,用于将值赋给变量:$one = 1。它不用于质量比较;请使用 -eq。请注意,等号可以与数学运算符一起使用:$var +=55 添加到 $var 当前包含的内容中。

  • 管道 (|)—这用于将一个 cmdlet 的输出传递到另一个 cmdlet 的输入。第二个 cmdlet(接收输出的 cmdlet)使用管道参数绑定来确定哪些参数或参数将接收管道中的对象。第六章和第十章有关于此过程的讨论。

  • 正斜杠或反斜杠 (/, \)—这些在数学表达式中用作除法运算符;无论是正斜杠 (/) 还是反斜杠 (\) 都可以用作文件路径中的路径分隔符:C:\Windows 与 C:\Windows 相同。反斜杠还用作 WMI 过滤条件中的转义字符和在正则表达式中的转义字符。

  • 句号 (.)—它有三个主要用途:

    • 它用于表示你想访问一个成员,例如属性或方法,或一个对象:$_.Status 将访问 $_ 占位符中任何对象的 Status 属性。

    • 它用于 dot-source 一个脚本,意味着该脚本将在当前作用域内运行,并且在该脚本完成后定义的任何内容都将保持定义:. c:\myscript.ps1.

    • 两个点 (..) 形成范围运算符,这在附录的后面部分有讨论。你还会看到两个点用于在文件系统中引用父文件夹,例如在 ..\ 路径中。

  • 逗号 (,)—在引号之外,逗号用于分隔列表或数组中的项:"One",2,"Three",4。它可以用来向可以接受它们的参数传递多个静态值:Get-Process -computername Server1, Server2,Server3

  • 冒号 (: )—冒号(技术上,是两个冒号)用于访问类的静态成员;这涉及到 .NET 框架编程概念;[-datetime]::now 是一个例子(尽管你可以通过运行 Get-Date 完成相同的任务)。

  • 感叹号 (!)—这是 -not 布尔运算符的别名。

我们认为,在美式键盘上,PowerShell 没有积极使用的唯一标点符号是插入符 (^),尽管这些在正则表达式中确实被使用。

A.2 帮助文件

帮助文件中的标点符号具有略微不同的含义:

  • 方括号 []—当方括号包围任何文本时,表示该文本是可选的。这可能包括整个参数([-Name <string>]),或者它可能表示参数是位置性的,名称是可选的([-Name] <string>)。它还可以表示参数是可选的,如果使用,则可以按位置使用([[-Name] <string>])。如果你有任何疑问,使用参数名称总是合法的。

  • 相邻方括号 []—这表示参数可以接受多个值(<string[]>而不是<string>)。

  • 尖括号 < >—这些包围数据类型,表示参数期望的值或对象类型:<string><int><process>等等。

总是花时间阅读完整的帮助信息(在help命令中添加-full),因为它提供了最大细节,以及在大多数情况下,使用示例。

A.3 操作符

PowerShell 不使用大多数编程语言中找到的传统比较运算符。相反,它使用这些:

  • -eq—相等(对于大小写敏感的字符串比较,使用-ceq)。

  • -ne—不等式(对于大小写敏感的字符串比较,使用-cne)。

  • -ge—大于或等于(对于大小写敏感的字符串比较,使用-cge)。

  • -le—小于或等于(对于大小写敏感的字符串比较,使用-cle)。

  • -gt—大于(对于大小写敏感的字符串比较,使用-cgt)。

  • -lt—小于(对于大小写敏感的字符串比较,使用-clt)。

  • -contains—如果指定的集合包含指定的对象,则返回True$collection -contains $object);-notcontains是相反的。

  • -in—如果指定的对象在指定的集合中,则返回True$object -in $collection);-notin是相反的。

逻辑运算符用于组合多个比较:

  • -not—反转TrueFalse!符号是该操作符的别名)。

  • -and—整个表达式为True,如果两个子表达式都必须为True

  • -or—整个表达式为True,如果任一子表达式为True

此外,还有一些执行特定功能的操作符:

  • -join—将数组的元素连接为一个分隔的字符串。

  • -split—将分隔的字符串拆分为数组。

  • -replace—将一个字符串的 occurrence 替换为另一个字符串。

  • -is—如果项目是指定的类型,则返回True$one -is [int])。

  • -as—将项目转换为指定的类型($one -as [int])。

  • ..—是范围操作符;1..10返回 10 个对象,从 1 到 10。

  • -f—是格式化操作符,用值替换占位符:"{0}, {1}" -f "Hello","World"

A.4 自定义属性和列语法

在几个章节中,我们向您展示了如何使用Select-Object定义自定义属性,或者使用Format-Table-Format-List分别定义自定义列和列表条目。以下是该散列表语法——您为每个自定义属性或列这样做:

@{label='Column_or_Property_Name';expression={Value_expression}}

两个键,LabelExpression,可以分别缩写为le(务必输入小写的L,而不是数字 1;您也可以使用n代替小写的L):

@{n='Column_or_Property_Name';e={Value_expression}}

在表达式内部,可以使用$_占位符来引用当前对象(例如当前表行或您要添加自定义属性的对象):

@{n='ComputerName';e={$_.Name}}

Select-ObjectFormat- cmdlet 都寻找n(或namelabell)键和e键;Format- cmdlet 还可以使用widthalign(这些仅用于-Format-Table)和formatstring。阅读Format-Table的帮助以获取示例。

A.5 管道参数输入

在第十章中,您了解到有两种类型的参数绑定:ByValueByPropertyNameByValue首先发生,而ByPropertyName只有在ByValue不起作用时才会发生。

对于ByValue,shell 会查看管道输入对象的类型。您可以通过将对象管道到gm来发现该类型名称。然后 shell 会检查是否有任何 cmdlet 的参数接受这种类型的输入,并配置为接受管道输入ByValue。在这种情况下,一个 cmdlet 不可能有两个参数绑定相同的数据类型。换句话说,您不应该看到一个 cmdlet 有两个参数,每个参数都接受<string>输入,并且都接受管道输入ByValue

如果ByValue不起作用,shell 将切换到ByPropertyName。在这里,它会查看管道输入对象的属性,并尝试找到具有完全相同名称的参数,这些参数可以接受管道输入ByPropertyName。如果管道输入对象具有NameStatusID属性,shell 将检查 cmdlet 是否有名为NameStatusID的参数。这些参数还必须标记为接受管道输入ByPropertyName,您可以在阅读完整帮助时看到这一点(在help命令中添加-full)。

让我们看看 PowerShell 是如何做到这一点的。对于这个例子,我们将参考第一个 cmdlet 和第二个 cmdlet,假设您有一个类似于Get-Service | Stop-ServiceGet-Service | Stop-Process的命令。PowerShell 遵循以下过程:

  1. 第一个 cmdlet 产生的对象的TypeName是什么?您可以将 cmdlet 的结果管道到Get-Member来查看这一点。对于多部分类型名称,如System.Diagnostics.Process,只需记住最后那部分:Process

  2. 第二个 cmdlet 的任何参数接受第一个 cmdlet 产生的对象类型吗(阅读第二个 cmdlet 的完整帮助以确定这一点:help <cmdlet name> -full)?如果是这样,它们也使用ByValue技术从管道接受该输入吗?这可以在每个参数的帮助文件的详细信息中看到。

  3. 如果步骤 2 的回答是肯定的,那么第一个 cmdlet 产生的整个对象将被附加到步骤 2 中指定的参数。你已经完成了——不要继续到步骤 4。但如果步骤 2 的回答是否定的,继续到步骤 4。

  4. 考虑第一个 cmdlet 产生的对象。这些对象有哪些属性?你可以通过将第一个 cmdlet 的输出通过管道传递到Get-Member来查看这一点。

  5. 考虑第二个 cmdlet 的参数(你需要再次阅读完整的帮助)。是否有任何参数(a)与步骤 4 中的某个属性同名,并且(b)使用ByPropertyName技术接受管道输入?

如果任何参数符合步骤 5 中的标准,属性值将被附加到同名参数,第二个 cmdlet 将运行。如果属性名称与ByPropertyName启用的参数之间没有匹配,第二个 cmdlet 将不带管道输入运行。

请记住,你可以在任何命令中手动输入参数和值。这样做将防止该参数以任何方式接受管道输入,即使它通常可以这样做。

A.6 何时使用$_

这可能是关于壳(shell)最令人困惑的事情之一:何时允许使用$_占位符?正如我们之前所学的,$_是管道中下一个对象的占位符。

这个占位符仅在 shell 明确寻找它并准备用某物填充它时才有效。一般来说,这种情况只发生在处理管道输入的脚本块中,在这种情况下,$_占位符每次将包含一个管道输入对象。你会在几个地方遇到这种情况:

  • Where-Object使用的过滤脚本块中:

    Get-Service | Where-Object {$_.Status -eq 'Running' }
    
  • 在传递给ForEach-Object的脚本块中,例如通常与 cmdlet 一起使用的默认Process脚本块:

    Get-CimInstance -class Win32_Service -filter "name='mssqlserver'" |
    ForEach-Object -process { $_.ChangeStartMode('Automatic') }
    
  • 在过滤函数或高级函数的Process脚本块中。Don Jones 和 Jeffery Hicks 的《一个月午餐中的 PowerShell 工具制作》(Manning, 2012)讨论了这些内容。

  • 在用于创建自定义属性或表列的哈希表表达式中。

在所有这些情况下,$_仅在脚本块的括号内出现。这是一个很好的规则,可以帮助你确定何时可以使用$_

posted @ 2025-11-24 09:14  绝不原创的飞龙  阅读(11)  评论(0)    收藏  举报