Powershell7-研讨会-全-

Powershell7 研讨会(全)

原文:annas-archive.org/md5/a7c5d98eb6dd2beb9d7fc74516c7725b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

PowerShell 是一种免费的、强大且易于学习的编程语言。最初它作为 Windows 的脚本和管理工具编写,现在作为一种开源资源,几乎可以在所有笔记本和台式机上安装。我在过去十年里一直在向同事们教授 PowerShell,并且在空闲时间里,我还在本地学校教孩子们编程,主要是 Python。为什么不通过 PowerShell 来教编程呢?

许多关于 PowerShell 的书籍和课程假设读者能够访问多台机器、Active Directory 域以及各种其他企业环境。它们通常也会低估 PowerShell 中的传统编码元素。本书既不做这些假设,也尝试用类似于我们教授 Python 编程的方式来教授 PowerShell 编程。我受到了密歇根大学 Chuck Severance 博士的杰出工作的启发——如果你想学习 Python,他的 Python for Everybody 课程在 py4e.org 上非常出色。

本书分为三部分。第一部分,我们介绍传统的编码理论;从 PowerShell 作为一种语言的工作原理开始,探讨语言的基本构件,然后讨论如何将这些构件结合在一起形成程序流程。

在第二部分,我们开始将我们学到的原则整合到脚本和模块中,这样我们就可以共享和重用它们。

在本书的最后部分,我们将探讨 PowerShell 在不同环境中的工作原理,最后以一章关于如何访问 PowerShell 所基于的底层框架作结。

我在书中包含了许多有趣且多样的例子和练习。为了最大限度地从中受益,我鼓励你亲自输入代码,而不仅仅是阅读它;打字的实际行为能比单纯浏览它带来更深的参与感。尝试一下问题和活动,在跳到答案之前,好好思考这些问题。如果你需要稍微动脑筋,你会从练习中获得更多的收获。

我非常想听听你的看法,以及你对如何改进本书的任何建议。

本书适合谁阅读

本书是为那些想学习编写代码并希望使用 PowerShell 学习的人准备的。这可能是想尝试不同的学校学生、想提升技能的 IT 工程师、爱好者、创客……所有人都适用。如果你是一个有经验的程序员,想将 PowerShell 加入你的技能清单,那么这本书可能不适合你;如果你已经会编写 Java、C++ 或 C#,那么你可能更适合参考 Chris Dent 所著的 Mastering PowerShell Scripting,由 Packt 出版。

本书涵盖的内容

第一章PowerShell 介绍 – 它是什么以及如何获取它,解释了 PowerShell 7 的定义,描述了它的用途以及与 Windows PowerShell 的区别。它描述了如何获取它、如何安装它以及如何运行它,解释了用户模式和管理员模式的区别。它还介绍了如何运行 cmdlet,以及如何在 PowerShell 中获取帮助。

第二章探索 PowerShell Cmdlet 和语法,重点介绍了 PowerShell cmdlet 的工作原理、批准的动词、参数、如何通过 PowerShell Gallery 以及互联网其他地方查找新的 cmdlet,最后介绍如何与 PowerShell 进行交互式操作。

第三章PowerShell 管道 – 如何串联 Cmdlet,介绍了 PowerShell 管道是 PowerShell 中最重要的概念之一,与 Bash 和命令提示符中的管道工作方式有很大不同。本章将向你展示如何成功地将 cmdlet 串联在管道中以产生有用的结果。它将讨论过滤、输出、管道的工作原理,以及管道为什么有时无法正常工作!

第四章PowerShell 变量和数据结构,介绍了变量以及它们可能的不同类型,包括整数、浮动数,以及它们如何都是对象。我们将探索对象的概念及其重要性。我们将查看数据结构作为对象的集合,然后是数组和哈希表,最后介绍 splatting。

第五章PowerShell 控制流 – 条件语句和循环,介绍了条件流(*IF* 这个,*THEN* 那个)和循环,包括 foreachwhile 循环。通常,你不希望以线性方式处理 cmdlet——你只会在另一个条件成立时才执行某些操作,或者对管道中的所有对象执行某些操作。控制流正是实现这一点的方式。本章还将带领我们从执行交互式 cmdlet 转向在 VS Code 中编写非常简单的脚本。

第六章PowerShell 和文件 – 读取、写入和操作数据,教你如何从常见文件类型(如 CSV 和 TXT 文件)中提取数据并进行操作,以及如何将输出发送到文件,从而避免必须从屏幕上读取输出并不断输入的乏味。我们还将讨论如何输出到 HTML,这对于创建格式化报告和在网页托管的仪表板中显示实时数据非常有用。

第七章PowerShell 与 Web – HTTP、REST 和 JSON,探索了 PowerShell 与 Web 的关系。文件很好,但许多云端管理需要处理来自互联网的数据;为此,我们需要能够操作最常见的互联网数据类型——JSON。我们还需要操作云服务,因此需要能够使用 REST API 调用。本章将详细讲解这一部分内容。

第八章编写我们的第一个脚本 – 将简单的 Cmdlet 转化为可重用的代码,重点讲解了如何将几行代码转化为我们可以保存并反复运行的脚本。我们已经了解了如何在 IDE 中编写几行代码。接下来,如何将这些代码转化为我们希望反复运行的脚本,并使其对其他人有用?

第九章不要重复自己 – 函数与脚本块,介绍了 PowerShell 中的函数以及脚本块和 Lambda 表达式。当编写脚本时,我们经常会遇到需要多次运行相同几行代码的情况。将它们转化为脚本中的函数意味着我们只需要编写一次,之后每次需要时只需调用即可。

第十章错误处理 – 哎呀!出错了!,涵盖了我们可能遇到的两种主要错误类型——代码执行中遇到的问题和代码本身的问题。在本章的第一部分,我们将定义什么是错误,如何设置 PowerShell 来优雅地处理错误,以及如何理解这些错误。在第二部分,我们将探讨如何识别代码中的问题,并使用 VS Code 进行调试。

第十一章创建我们的第一个模块,探讨了如何将函数和脚本转化为可重用的模块,方便分发并能够整合到其他脚本中。现在我们已经有了一个包含一组函数的脚本,下一步是将它转化为其他人也能使用的工具。

第十二章保护 PowerShell,深入探讨了如何保护我们的 PowerShell 脚本和模块,并以安全的方式运行它们。PowerShell 是一个非常强大的工具,强大的力量伴随着巨大的责任。本章将涵盖脚本执行策略、代码签名、AppLocker 和其他一些安全功能。

第十三章在 PowerShell 7 和 Windows 上工作,探讨了如何在 Windows 上使用 PowerShell 7,我们何时需要使用 PowerShell 5.1,如何使用 WinRM 与远程机器交互,如何通过 CIM 管理机器,以及与 Windows 特性(如存储)的基本交互。PowerShell 起源于 Windows,PowerShell 7 的目标是最终取代 Windows PowerShell,但目前我们还没有完全过渡到那一步。

第十四章PowerShell 7 for Linux and macOS,解释了如何在 Linux 上安装 PowerShell,它与在 Windows 上运行 PowerShell 的区别,以及如何在 Linux 上使用 VS Code。它还解释了如何使用 OpenSSH 进行远程操作,如何运行脚本以及一些常见的管理任务。最后,它介绍了如何在 macOS 上安装和运行 PowerShell 及 VS Code。

第十五章PowerShell 7 与 Raspberry Pi,探讨了如何在 Raspberry Pi 上开始使用 PowerShell,让我们能够进行家庭自动化、创客项目等。它包括 PowerShell 和 VS Code 的安装,连接 Pi,以及运行脚本。Raspberry Pi 是大家最喜爱的单板计算机,我们可以将 PowerShell 技能转移到我们的 Pi 设备上。

第十六章与 PowerShell 和.NET 的工作,深入探讨了.NET,这是 PowerShell 7 构建的平台;它是免费的、开源的,并且与 VS Code 兼容。仅使用 PowerShell 我们无法轻松完成许多任务,但我们可以借助.NET 来实现这些目标。熟悉.NET 是每个高级 PowerShell 程序员的必备技能,本章将帮助你掌握这项技能。

如何从本书中获得最大收益

本书假设你有一台客户端计算机,可能是笔记本电脑或 PC,运行的是 Windows、Linux 或 macOS。然而,如果条件紧张,你也可以几乎用 Raspberry Pi 完成所有任务。它还假设你有互联网连接。大部分练习将在 Windows、Linux 和 macOS 上运行;如果有不兼容的地方,会特别标出。阅读第一章,它包含了 Windows 的安装说明,之后根据 Linux/macOS 或 Raspberry Pi 章节中的安装指南操作。

本书涵盖的软件/硬件 操作系统要求
PowerShell 7.x Windows、macOS 或 Linux

如果你使用的是本书的电子版,我建议你自己输入代码。错误是学习的一部分,当我 意识到 自己做错了什么时,那种成就感是你不想错过的。

使用的约定

本书中使用了一些文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:“应用程序默认安装在C:\Users\<username>\AppData\Local\Programs\Microsoft VS Code,并且仅对安装该应用程序的用户可用。”

一块代码如下所示:

$x = 5
if ($X -gt 4) {
    Write-Output '$x is bigger than 4'
}

当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会以粗体显示:

$Array  = 1,2,3,4,5
switch ($Array) {
    1 {Write-Output '$Array contains 1'}
    3 {Write-Output '$Array contains 3'}
    6 {Write-Output '$Array contains 6'}
}

任何命令行输入或输出均如下所示:

sudo apt update
sudo apt install ./<filename>.deb

粗体:表示新术语、重要词汇,或您在屏幕上看到的词语。例如,菜单或对话框中的词语会以粗体显示。以下是一个例子:“在选择附加任务对话框中,选择是否需要桌面图标以及文件和目录上下文菜单选项,这样您就能直接在 VS Code 中打开文件和文件夹。”

提示或重要事项

以这种方式显示。

联系我们

我们总是欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请通过电子邮件联系我们:customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已尽力确保内容的准确性,但错误难免发生。如果您在本书中发现错误,我们将非常感激您能报告给我们。请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上发现任何我们作品的非法版本,我们将感激您提供该地址或网站名称。请通过 copyright@packt.com 与我们联系并提供该资料的链接。

如果您有兴趣成为作者:如果您在某个领域具有专业知识,并且有意为书籍写作或贡献内容,请访问authors.packtpub.com

分享您的想法

阅读完PowerShell 7 Workshop后,我们很乐意听取您的想法!请点击这里直接进入亚马逊评论页面分享您的反馈。

您的评论对我们和技术社区非常重要,它将帮助我们确保提供高质量的内容。

下载本书的免费 PDF 版本

感谢您购买本书!

您是否喜欢随时随地阅读,但又无法随身携带印刷书籍?

您的电子书购买是否与您选择的设备不兼容?

不用担心,现在购买每本 Packt 书籍时,您都可以免费获得该书的无 DRM PDF 版本。

在任何地方、任何设备上阅读。搜索、复制并粘贴您最喜爱的技术书籍中的代码,直接将其应用到您的程序中。

福利不仅仅止步于此,您还可以独享折扣、新闻通讯和每日发送到邮箱的精彩免费内容。

按照以下简单步骤获取这些福利:

  1. 扫描二维码或访问以下链接

packt.link/free-ebook/9781801812986

  1. 提交您的购买凭证

  2. 就是这样!我们会将您的免费 PDF 和其他福利直接发送到您的邮箱

第一部分:PowerShell 基础

在这一部分,我们将学习 PowerShell 的基础知识,包括语言语法如何运作,如何通过管道将一个操作的结果直接传递到另一个操作,PowerShell 如何使用对象、变量和数据结构,如何创建分支和循环代码,最后,我们如何读写存储文件。

本部分包含以下章节:

  • 第一章PowerShell 入门 – 它是什么以及如何获取它

  • 第二章探索 PowerShell Cmdlet 和语法

  • 第三章PowerShell 管道 – 如何将 Cmdlet 串联起来

  • 第四章PowerShell 变量和数据结构

  • 第五章PowerShell 控制流 – 条件语句与循环

  • 第六章PowerShell 与文件 – 读取、写入和操作数据

  • 第七章PowerShell 与 Web – HTTP、REST 和 JSON

第一章:PowerShell 7 介绍 – 它是什么以及如何获取它

简单来说,PowerShell 是一台时光机。不是那种科幻电影里的时光机,你可以回到过去见到自己的祖父,而是一台真正的、实用的时光机。如果你投入少量时间,那么 PowerShell 就像任何简单的机器一样,能够起到倍增作用;它会为你节省大量的时间。用个比喻,它是一把时间锤子,你投入学习 PowerShell 的时间将为你节省数十倍甚至上百倍的时间,一旦你开始将这些知识付诸实践。

本章是 PowerShell 7 的概述介绍。它将为你提供 PowerShell 的背景知识,并帮助你启动和运行。你将学习如何使用 PowerShell 以及一些典型的使用案例。我们将安装 PowerShell,你将选择一种或多种方式进行安装。安装完成后,我们将介绍如何运行命令(称为cmdlet),以及如何查找可以运行的 cmdlet。最后,也非常重要的是,我们将介绍如何获取帮助,包括获取 cmdlet 以及 PowerShell 主题和概念的帮助。

本章将涵盖以下主要内容:

  • PowerShell 7 是什么?

  • PowerShell 7 用于什么?

  • 获取 PowerShell 7

  • 运行 PowerShell 7

  • 获取帮助

技术要求

为了跟上本章的内容,你需要一个互联网连接和操作系统。如果你使用的是 Linux 或 macOS,安装说明可以在第十四章**, Linux 和 macOS 上的 PowerShell 7 中找到,因此跳过本章 如何获取 PowerShell 7 部分的详细安装说明。

本章假设你将使用运行在标准 64 位 x86 架构上的 Windows 10(版本 1709 或更高版本)。如果你不确定自己的系统是否符合这个要求,不用担心,应该是的。如果你是那种总是担心的人,可以打开 Windows 搜索框,输入 msinfo32,然后按 Enter系统信息应用程序将打开,在系统摘要下,你会看到三行相关信息:

  • 操作系统名称:希望是某种版本的微软 Windows 10;PowerShell 7.3 可在所有当前支持的 Windows 版本上使用。

  • 版本:你需要一个比 16299 更高的版本号。

  • 系统类型:可能是基于 x64 的 PC

以下截图显示了在系统摘要下的样子:

图 1.1 – 系统信息应用程序(msinfo32)中的典型信息

图 1.1 – 系统信息应用程序(msinfo32)中的典型信息

如果你使用的是 Windows 11,那么恭喜你;你不需要做一些我们将讨论的事情,因为 Windows 11 提供了一些额外的功能。

PowerShell 7 是什么?

PowerShell 是一种脚本语言,是命令行接口的替代工具。PowerShell 是一个自动化工具,至少由三部分组成:

  • 一个 shell,类似于 Windows 中的命令提示符或 Linux 或 macOS 中的终端

  • 一个脚本语言

  • 一个名为 Desired State Configuration (DSC) 的配置管理框架

实际上,当我们谈论 PowerShell 时,通常是指脚本语言。正如我们所看到的,Shell 的使用对用户来说是直观的,尽管我们稍后会讨论 DSC,但根据我的经验,大多数人并没有像应该的那样充分使用它。

PowerShell 的第一个版本源自一个名为Monad的项目,这是 Jeffrey Snover 试图在 Windows 上复制 Unix 工具的尝试。他意识到 Unix 工具的一个基本缺点是它们输出的是字节流(通常是文本),因此在你能够对输出结果进行操作之前,很多努力都浪费在搜索、格式化和提取命令输出上。Monad 被写成输出对象,这些对象可以直接输入到另一个命令中。我们将在 第四章,《PowerShell 变量和数据结构》中详细讨论这一点。PowerShell 1.0 于 2006 年发布,但依我看,直到 PowerShell 2.0 在 2009 年发布,且微软开始重新设计主要软件(如 Exchange Server 2010)的管理界面以便利用 PowerShell 时,它才真正起步。其他看法也是存在的。

在撰写本文时,PowerShell 主要有两种版本Windows PowerShell,它随 Windows 的服务器版和桌面版捆绑在一起,以及PowerShell 7,它需要下载并安装。Windows PowerShell 的最新(且 allegedly 最终)版本 v5.1 构建在 .NET Framework 4.5 上,这是与 Windows 一起捆绑的专有软件框架,并支撑了微软的许多产品。PowerShell 7.0 是基于 .NET Core 3.1 构建的,这是一个简化版的、开源实现的 .NET。然而,自从 7.2 版本起,PowerShell 已经基于 .NET 6.0 构建。这个统一版本的 .NET 代替了 .NET Framework 和 .NET Core,并在 2020 年 11 月发布。

由于 Windows PowerShell 5.1 和 PowerShell 7.x 之间的根本差异,它们在 Windows 平台上的工作方式可能会有所不同。我们将在 第十三章,《在 PowerShell 7 和 Windows 中工作》中讨论这些差异。

我们将总结一些关键差异,并在以下表格中列出:

参数 Windows PowerShell PowerShell 7.2
平台 仅限 x64, x86 x64, x86, arm32, arm64
操作系统 Windows Windows、Linux、macOS
.NET 版本 .NET Framework 4.5 .NET 6.0
许可证类型 专有 开源
本地命令数量 1588(在原生 Windows 10 中) 1574(在原生 Windows 10 中)290(在 Ubuntu 20.04 中)

表 1.1 – Windows PowerShell 和 PowerShell 7 之间的一些差异

在这一部分中,我们已经介绍了 PowerShell 是什么,以及它与 Windows PowerShell 的区别。在下一部分中,我们将探讨 PowerShell 7 的存在意义,并看看它有什么特别之处。

PowerShell 7 用于什么?

PowerShell 是为了快速完成任务。它适用于当你需要一个相对较短的代码片段,并且可以轻松地重用和重新设计来完成其他任务时。它适用于当你不想花几个月时间学习一门语言,再花更多的时间编写成千上万行代码时。这个语言至少可以有四种使用方式:

  • 你可以像在 Windows 命令提示符或 Linux 终端中一样,在 shell 中输入单行代码。如果你需要检查一个值、执行一个单一任务(如重启远程计算机)或抓取日志文件时,这非常有用。

  • 你可以编写一个脚本,比如在 Linux 中的 Bash 脚本或 Windows 中的批处理文件,用来完成多个子任务,例如从几台机器中收集事件日志和性能信息,并将它们汇总成一个单一的 HTML 报告。

  • 如果你编写了很多脚本或需要完成一些更复杂的任务,你可以将 PowerShell 用作一种过程式编程语言,使用多个封装的脚本,每个脚本描述一个单一的功能,并由主脚本调用。

  • 你可以将它作为一种面向对象的编程语言,封装一个完整的应用程序,该程序可以重新分发并由任何安装了 PowerShell 的人运行。

本书将重点讲解脚本和过程式编程,因为这是大多数人使用 PowerShell 的方式。这两者非常相似;不同之处在于,在脚本中,你使用的是已经为你编写的 cmdlet,而在过程式编程中,你正在创建自己的 cmdlet,可能是从现有的 cmdlet 中创建,或使用系统编程语言 C# 来创建。

脚本语言与系统编程语言

PowerShell 语言是一种脚本语言。它用于快速且轻松地将其他应用程序粘合在一起——有点像编程版的乐高。它依赖于一个底层的解释器:PowerShell 程序。如果没有安装 PowerShell,PowerShell 脚本无法运行。这与其他解释型语言(如 Python)类似,并且与系统编程语言(如 C 或 C++)形成对比,后者被编译成可执行文件。当你编译一个 C++ 程序时,它理论上可以在任何兼容的机器上运行。还有其他差异——以下是一些主要的区别:

  • 解释型语言比编译型语言效率低,因为每一行代码都必须在运行之前进行解释。这意味着它们比编译程序要慢。虽然有一些编程技巧可以加速,但在解释型语言中执行任务通常会比在编译型语言中执行要慢。

  • 解释语言在开发中比编译语言更有效率。它们可以用更少的代码完成相同的任务。这意味着编写、调试和重用这些代码会更快。它们也更容易学习。

  • 解释语言可以在多种架构上运行。正如本书中所示,使用 PowerShell 编写的代码可以在 Windows、Linux 或 macOS 上运行,只需进行最少的调整。而用 C++ 编写的程序只能在 Windows 上运行,或者在具有 Windows 模拟的机器上运行。它需要重新编写和重新编译以在不同平台上运行。

  • 解释语言生成协作可重用的程序。使用 PowerShell(或 Python),您可以生成人类可读且可编辑的代码。而使用编译语言,您生成的是无法轻易反编译为源代码以便重用的二进制文件。这意味着其他人可以为自己的目的重用您的代码。像 GitHub 这样的平台可以用来分发您的代码,其他人可以为其做出贡献、改进它、将其重用于其程序,并以一种普遍的社区方式行事。

归根结底是这样的:如果您想编写一个具有壮观图形的超快第一人称射击游戏,那么 PowerShell 可能不适合您。如果您想要自动化一些任务,无论是简单还是复杂,PowerShell 都是一个不错的选择。

获取 PowerShell 7

在本节中,我们将探讨将 PowerShell 安装到计算机上的一些方法、安装位置及其原因,以及如何控制安装的各个方面。本章仅涵盖 Windows 上的安装;有关 Linux、macOS 和 ARM 系统的详细安装,请参阅第十四章Linux 和 macOS 上的 PowerShell 7,或第十五章PowerShell 7 和树莓派,然后返回本章的后两节。

您的计算机上可能同时运行多个 PowerShell 版本 - 我通常一次运行三个版本:Windows PowerShell、PowerShell 7(当前版本)和 PowerShell 7 预览版。这不仅适用于我写书时使用 - 我们需要确保我们编写的脚本可以在不同的环境中运行,并在必要时重新编写它们。当您计划在尚未安装 PowerShell 的远程计算机上运行 PowerShell 时,控制安装也非常有用。Windows PowerShell 包含在 Windows 操作系统中,并安装在 \Windows\system32 文件夹中;它的存在位置不可更改。相比之下,PowerShell 7 可以根据需要安装在任何地方。我们将介绍安装的三种最常见方法:

  • 使用 Windows 安装程序从 .msi 文件安装

  • .``zip 文件安装

  • 使用 winget 安装

我们将简要介绍另外两种方法:从 Microsoft Store 安装和安装为 .NET 全局工具。

如果你想做些实验,并且你有 Windows 10 专业版或企业版,那么可以在 控制面板 | 程序和功能 | 启用或关闭 Windows 功能 中启用 Windows Sandbox 功能。

这将为你提供一个完全空白、安全的 Windows 环境供你尝试。请小心——当你关闭它时,它就会永久消失。下次启动时,你所有的更改将会丢失:

图 1.2 – 开启 Windows Sandbox

图 1.2 – 开启 Windows Sandbox

有关运行 Windows Sandbox 的详细要求,请参见此链接:docs.microsoft.com/en-us/Windows/security/threat-protection/Windows-sandbox/Windows-sandbox-overview

让我们开始吧。请确保你已经满足章节开始部分列出的技术要求。

从 .msi 文件进行安装

所有官方的 PowerShell 发行版都可以在 PowerShell 的 GitHub 页面找到:github.com/PowerShell/PowerShell

图 1.3 – 从 GitHub 页面获取 PowerShell

图 1.3 – 从 GitHub 页面获取 PowerShell

如你所见,对于大多数操作系统和平台,有三种版本发布类型:LTS稳定版预览版LTS 代表 长期支持。LTS 版本的发布速度较慢,旨在确保在风险规避的环境中保持稳定,它们通常仅包含关键的安全更新和软件修复,而不会加入新功能。PowerShell 的 LTS 版本基于 .NET 的 LTS 版本。预览版是 PowerShell 的下一个版本,可能会有令人兴奋的新特性,但也可能不稳定,存在一些缺陷。稳定版每月更新一次,可能包含新功能,以及软件修复和安全更新。每个版本在发布下一个版本后,支持六个月。

让我们继续安装最常见的版本——适用于 Windows x64 的稳定版:

  1. 在这里浏览 PowerShell 的 GitHub 发布页面:github.com/PowerShell/PowerShell

  2. 点击下载适用于 Windows x64 的稳定 .msi 安装包。

  3. 在你的 Downloads 文件夹中找到 .msi 文件并运行它,这将启动安装向导。

  4. 你需要做的第一个选择是安装位置。默认情况下,它会安装到 C:\Program Files\PowerShell 下的一个编号文件夹中,其中编号与主版本号相匹配——在我们这个例子中是 7。如果你安装的是预览版,那么文件夹名会带有 -preview 后缀。这是一个相当不错的位置,但如果你同时运行多个版本的 PowerShell,可能会希望将其安装到其他位置。此次就接受默认设置吧:

图 1.4 – 默认安装位置

图 1.4 – 默认安装位置

  1. 现在我们进入了可选操作菜单:

图 1.5 – 可选操作

图 1.5 – 可选操作

这里有五个选项:

  • 使用 pwsh.exe 来运行此命令。

  • 注册 Windows 事件日志清单:您也应该启用此选项。这将创建一个新的 Windows 事件日志,名为PowerShell Core,并开始记录 PowerShell 事件。

  • 启用 PowerShell 远程功能:启用 PowerShell 远程功能将使计算机监听来自 PowerShell 会话的传入连接。显然,这在安全上存在一些漏洞,因此只有在需要时并且您的计算机处于私有网络中时才应启用它。您不需要启用它来连接到其他计算机的远程会话。

  • 在资源管理器中添加“在此打开”上下文菜单:这将允许您在文件资源管理器中的文件夹中打开 PowerShell 会话——PowerShell 会话将打开,并且路径会设置为您选择的文件夹。

  • 为 PowerShell 文件添加“使用 PowerShell 7 运行”上下文菜单:这将允许您右键单击文件并使用 PowerShell 7 打开它。由于后面我们将看到的原因,这可能并不总是可取的。

  1. 可选操作之后,我们进入了Microsoft 更新选项。您可以使用 Microsoft 更新来保持 PowerShell 的最新状态;强烈建议启用此选项,因为它可以自动为您下载安全补丁并根据现有的更新计划应用它们。请注意,如果您在域环境中工作,此设置可能会被组策略覆盖。有两个复选框,第一个用于启用 PowerShell 更新,第二个用于启用系统上的 Microsoft 更新。请注意,取消选中此框只会禁用 Microsoft 更新;如果您的环境使用了如Windows 软件更新服务WSUS)或系统中心配置管理器SCCM)等配置管理工具,它们仍会继续工作。

  2. 最后,我们准备通过点击安装按钮来安装。这个过程很短,应该在一两分钟内完成。点击完成,我们就完成了安装。

有一种替代方法,不使用 GUI。您可以通过命令行使用 msiexec.exe 运行 .msi 文件,具体文档见此处:docs.microsoft.com/en-gb/powershell/scripting/install/installing-powershell-on-Windows?view=powershell-7.2#install-the-msi-package-from-the-command-line

如您刚刚所示,要在 Windows 沙盒中静默安装 PowerShell,您可以运行以下命令:

msiexec.exe /package c:\Users\WDAGUtilityAccount\Downloads\PowerShell-7.2.1-win-x64.msi /quiet REGISTER_MANIFEST=1 USE_MU=1 ENABLE_MU=1

请注意,命令行没有启用或禁用 .msi 文件的属性,因此 PowerShell 会自动添加。由于我们使用了 /quiet 开关,此命令不会有输出,但如果成功,您将会在开始菜单中看到 PowerShell。

活动 1

如何在使用命令行从 .msi 文件安装 PowerShell 时启用文件上下文菜单?(提示:查看前面段落中的链接了解详情。)

.zip 文件安装

另一种流行的安装 PowerShell 的方式是通过 .zip 文件。使用这种方法,我们只是将二进制文件和相关文件提取到合适的文件夹中。缺点是,你将失去 .msi 安装时的前置条件检查和选项;例如,你不能自动将 PowerShell 添加到 PATH 环境变量中或启用 PowerShell 远程功能。优点是,它使得在 DevOps 或基础设施即代码管道中脚本化安装 PowerShell 变得更容易,并且可以在脚本中启用其他功能。

在 Windows 中,没有原生的方式通过脚本从互联网安装文件。你需要已经安装了 PowerShell(在 Windows 机器上,Windows PowerShell 会自动安装),或者安装一个像 curl 这样的工具。

这是你用 Windows PowerShell 执行的方式:

Invoke-WebRequest https://github.com/PowerShell/PowerShell/releases/download/v7.2.1/PowerShell-7.2.1-win-x64.zip

如果你运行前面的 cmdlet,你应该会看到如下输出。注意,这是一个 HTTP 响应,因此 StatusCode 结果为 200 是好的:

图 1.6 – 使用 Windows PowerShell 下载 PowerShell 7

图 1.6 – 使用 Windows PowerShell 下载 PowerShell 7

你可以像这样通过四行代码运行整个过程:

New-Item -Path c:\scratch\myPowershell\7.2 -ItemType Directory
Invoke-WebRequest https://github.com/PowerShell/PowerShell/releases/download/v7.2.1/PowerShell-7.2.1-win-x64.zip -OutFile C:\scratch\myPowershell\7.2\PowerShell-7.2.1-win-x64.zip
Expand-Archive C:\scratch\myPowershell\7.2\PowerShell-7.2.1-win-x64.zip -DestinationPath C:\scratch\myPowershell\7.2\
Remove-Item C:\scratch\myPowershell\7.2\PowerShell-7.2.1-win-x64.zip

不要太担心前面的命令——我们稍后会逐一讲解。总的来说,第一行创建一个新文件夹,第二行从 GitHub 下载 .zip 包到你新建的文件夹中,第三行解压所有文件,准备好运行,第四行则删除下载的包。

你可能会遇到两个错误。首先,你可能会看到一个红色的错误信息:

Invoke-WebRequest : The request was aborted: Could not create SSL/TLS secure channel"

这是因为,默认情况下,Windows PowerShell 会使用 TLS v1.0,而许多网站不再接受此协议。如果你看到这个错误,运行以下 .NET 代码并再试一次:

[Net.ServicePointManager]::SecurityProtocol = "Tls, Tls11, Tls12, Ssl3"

另一个你可能会看到的错误信息是:

Invoke-WebRequest : The response content cannot be parsed because the Internet Explorer engine is not available, or Internet Explorer's first-launch configuration is not complete. Specify the UseBasicParsing parameter and try again

在这种情况下,使用 –``UseBasicParsing 参数运行 Invoke-WebRequest cmdlet:

Invoke-WebRequest https://github.com/PowerShell/PowerShell/releases/download/v7.2.1/PowerShell-7.2.1-win-x64.zip -UseBasicParsing

将脚本中的第二行替换为这一行。它与之前的完全相同,但增加了 –``UseBasicParsing 参数。

使用 winget 安装

.exe.msi.msix 包 – 你不能用它来安装 .zip 版本。当你运行 winget 时,它会搜索、下载并安装你选择的 PowerShell .msi 版本。操作如下:

  1. 首先,运行 PowerShell 包的搜索。从 Windows PowerShell 命令提示符下,运行以下命令:

    winget search microsoft.powershell
    

这将返回 winget 可用的 PowerShell 版本。

  1. 然后,你需要安装一个包。我选择通过运行以下命令来安装预览版:

    winget install --id microsoft.powershell.preview --source winget
    

就是这样。这里有几点需要注意。首先,你正在安装 .msi 文件,因此除非你抑制它们,否则会看到多个图形界面消息。你可以使用 --silent 开关来抑制这些消息。除非你接受默认选择,否则你还需要一种方法来向调用的 .msi 文件传递参数。你可以使用 --override 开关,然后传递我们之前查看过的 .msi 包的命令行开关。其次,如果你启用了用户访问控制(UAC),你需要允许 PowerShell 安装。如果你使用 --silent 开关,那么你不会看到这个提示。如果你想进行静默安装,你需要以管理员权限运行 Windows PowerShell。

如果你以管理员权限从 Windows PowerShell 命令行运行安装命令,整个安装过程如下所示:

图 1.7 – 使用 winget 静默安装 PowerShell

图 1.7 – 使用 winget 静默安装 PowerShell

winget 的主要优势是它有自己的社区创建的包管理库;任何人都可以通过编写清单并上传来打包一个应用。该库使用微软的 SmartScreen 技术加以保护,以防恶意代码进入库中。关于 winget 还有很多内容,请参考这里:

docs.microsoft.com/zh-cn/Windows/package-manager/winget/.

总结来说,你通过 winget 所做的事情其实与之前通过运行 msiexec.exe 所做的没有什么不同,只是它稍微新一些、更酷,并且拥有一个有用的包管理库,使用起来也稍微容易一些。几年后,我们会想,曾经没有它时我们是怎么做的,尤其是如果它在 Windows 服务器上也可用的话。

其他安装方式

我们应该讨论的还有另外两种安装 PowerShell 的方式。两者都不太可能适用于我们。首先,如果你已经安装了 .NET 软件开发工具包SDK),那么我们可以使用它将 PowerShell 安装为一个全局工具。这个方法实际上只对软件开发人员有用,而且仅为了 PowerShell 安装 SDK 并不太有意义。

另一种在 Windows 上安装 PowerShell 的方式是通过微软商店作为应用安装。此方法的主要缺点是商店应用运行在一个沙盒环境中,这限制了对应用根文件夹的访问。这意味着一些 PowerShell 功能将无法正常工作。

注意

沙盒 不一定等同于 Windows 沙盒。术语“沙盒”通常指的是一个具有独立资源的安全计算环境,这意味着运行在其中的任何东西都不能干扰沙盒外的内容。Windows 沙盒是沙盒的一种特定实现。

运行 PowerShell 7

每个人运行 PowerShell 的第一种方式是通过捆绑的控制台(记住,PowerShell 不仅仅是一种语言,它还是一个 shell 和配置管理框架)。假设你使用之前的 .msi 方法进行安装,并将 PowerShell 添加到了 PATH 环境变量中。如果你已经这么做了,那么启动 PowerShell 只需要在 Windows 搜索框中输入 pwsh 并点击应用程序。或者,你也可以右键点击 开始 菜单,在 运行 框中输入 pwsh。或者,你也可以按住 Windows 键并按 R,这也会打开 运行 框。

如果你没有将 PowerShell 添加到路径中,你将需要输入完整的可执行文件路径 pwsh,例如 C:\program files\PowerShell\7\pwsh

如果你关注了前面的截图,或者跟随步骤操作,你首先会注意到控制台窗口的背景是黑色的。这将其与 Windows PowerShell 区分开来:

图 1.8 – 两种不同版本的 PowerShell

图 1.8 – 两种不同版本的 PowerShell

如果你在 Linux 或 macOS 上安装了 PowerShell,那么请打开终端并输入 pwsh —— 终端会切换到 PowerShell 提示符。

在大多数编程书籍中,传统的第一步是让你的应用程序在屏幕上显示 "Hello World" 字样,我们也不例外。请在控制台中输入以下内容并按 Enter 键:

Write-Host "Hello World"

你应该会看到类似这样的内容:

图 1.9 – 你好,自己

图 1.9 – 你好,自己

如果你完成了,恭喜你!你刚刚运行了第一个 PowerShell 命令,或者叫做 cmdlet。

注意,控制台会自动为它识别或期望的内容着色;cmdlet 是黄色的,字符串是蓝色的。它也非常宽容——如果你忘记了引号,"Hello World" 将不会是蓝色的,但 PowerShell 仍然会正确地解析它。

注意

小心使用这一点;虽然 PowerShell 非常智能,但它并不总是按你希望的方式解读输入。最好明确告诉它你正在输入的是什么类型的内容。稍后会详细讲解。

错误最常见的原因是你拼写错了 cmdlet 或者没有关闭引号,如下图所示。你会看到一个有帮助的红色错误信息,告诉你哪里出错了,并建议你如何修复它。我已经开始珍惜这些错误信息了:

图 1.10 – 三种错误方式

图 1.10 – 三种错误方式

在第三次尝试时,我没有关闭引号,因此 PowerShell 期望更多输入。它通过下面一行的 >> 告诉我们这一点。它还通过将命令提示符中的 > 标记为红色,告诉我们它认为命令行不会按你写的那样运行。

请注意,不像某些环境中那样,这里大小写不重要;write-hostWrite-Host 在功能上是相同的。

以管理员权限运行 PowerShell

默认情况下,PowerShell 会在启动它的帐户下运行,但它只会拥有标准用户权限。如果你需要访问通常需要管理员权限的本地机器,那么 PowerShell 会无法运行某些 cmdlet,或者会弹出 用户帐户控制UAC)提示。为了避免这种情况,你需要以管理员权限启动 PowerShell。这里有很多方法来实现,但以下是两种最常见的方法。

首先,你可以打开搜索栏,输入pwsh,然后右键点击 PowerShell 7 图标并选择以管理员身份运行

图 1.11 – 以管理员身份启动 PowerShell

图 1.11 – 以管理员身份启动 PowerShell

第二种稍微更强大的方法是按下Windows键 + R 打开 pwsh,然后按住 Ctrl + Shift + Enter,这将以管理员身份启动 PowerShell。

PowerShell 会清楚地在窗口标题中显示它是否以管理员权限运行。以下是两个 PowerShell 会话运行相同 cmdlet 的情况。下方的窗口是以管理员权限运行的:

图 1.12 – 有时你需要是管理员

图 1.12 – 有时你需要是管理员

如果你经常需要使用管理员模式,最简单的办法是右键点击正在运行的 PowerShell 图标并选择固定到任务栏。然后,你可以在需要时右键点击固定的图标并选择以管理员身份运行

自动补全

到现在为止,你可能已经有些厌倦了不断输入大量长且不熟悉的 cmdlet。让我向你展示一个非常棒的 shell 功能:自动补全。试试这个——在你的 shell 中,输入以下内容,不要按 Enter

Stop-s

现在按下 Tab 键。酷吧?不过这还不是我们想要的 cmdlet。再按一次 Tab 键。此时你应该看到 Stop-Service cmdlet 完整地输入了。现在,添加一个空格,输入 -,然后再次按 Tab 键。继续按 Tab 键,直到你浏览完 Stop-Service cmdlet 所有可能的参数。完成后按 Esc 键。

这是一个很好的方法,可以避免输入大量字母,但它也是一个检查你所做操作是否有效的好方式。如果自动补全不起作用,那很可能是你想要的 cmdlet、参数或选项在这台机器上不可用。

在下一章,我们将探讨一些启动和使用 PowerShell 的其他方法,但现在,你已经准备好所需的一切了。

获取帮助

现在你已经安装了 PowerShell 并可以启动它,你需要用它来做一些事情。你肯定需要一些帮助。幸运的是,PowerShell 内置了三个非常有用的 cmdlet:Get-CommandGet-HelpGet-Member。这些 cmdlet 将告诉你一些有用的内容,并提供指导。我们先从 Get-Command 开始。

Get-Command

Get-Command 会给你一个 cmdlet 列表。如果你直接输入它,它会列出大约 1,500 个 cmdlet。当你开始安装和编写模块时,这个列表会大大增加。浏览成千上万的 cmdlet 以寻找可能的命令并不是很高效。你需要做的是搜索这个列表。

假设你需要调查正在你客户端上运行的某个特定进程。一个执行此操作的 cmdlet 很可能会在某个地方包含 process 这个词。试着在你的 shell 中输入以下内容:

Get-Command *process

你应该会看到类似这样的内容:

图 1.13 – 搜索相关 cmdlet

图 1.13 – 搜索相关 cmdlet

该 cmdlet 会将 *process 解释为一个字符串,并搜索以 process 结尾的 cmdlet。* 是一个通配符字符。尝试像这样运行它:

Get-Command process

你可能会看到一个红色的错误。

这些 cmdlet 中有些看起来有点复杂,但也有一些特别突出——尤其是 Get-Process。试着运行它。你应该会看到一长串进程以及关于它们的一些信息。让我们看看一个我知道你正在运行的进程:pwsh。输入以下内容:

Get-Process pwsh

你应该看到有关你 PowerShell 进程的信息:

图 1.14 – 我的 PowerShell 进程

图 1.14 – 我的 PowerShell 进程

这很不错,但它到底意味着什么呢?让我们来看一下我们的三个有用 cmdlet 之一:Get-Help

Get-Help

运行 Get-Help cmdlet 很简单;输入 Get-Help 后跟你希望获得帮助的 cmdlet 名称:

Get-Help Get-Process

然后你应该会看到类似这样的内容:

图 1.15 – 第一次运行 Get-Help

图 1.15 – 第一次运行 Get-Help

这看起来不太有帮助。然而,如果你阅读 REMARKS 部分,会有一个解释。PowerShell 并没有自带完整的帮助文件;你需要下载并更新它们。要更新帮助文件,运行以下命令:

Update-Help

运行过程会稍微花一点时间,如果你安装了某些模块,可能并非所有模块的帮助文件都能在线获取,所以你会看到红色的错误信息,但经过一两分钟后,应该就能完成,接着你可以尝试再次获取 Get-Process 的帮助。

Get-Help Get-Process

PowerShell 相当偏向于 en-US 文化。这里的文化是指 .NET 和 PowerShell 等相关程序中的一个特定含义;它相当于 en-US,因此它可能不会下载所有相关的帮助文件。如果你发现没有获取到所有内容,试着运行以下命令:

Update-Help -UICulture en-US

然后,重新尝试。这特别影响 Linux 安装。

你应该能看到更多信息,包括一行的简介和详细描述。如果这还不够,那么在REMARKS部分,你将会看到一些获取更多 cmdlet 信息的其他方法。试试运行这个命令:

Get-Help Get-Process -Detailed

你将看到更详细的信息,包括如何使用该 cmdlet 的示例。要查看所有可用的信息,可以使用以下命令:

Get-Help Get-Process -Full

你将看到帮助文件中的所有内容,包括非常有用的NOTES部分,对于这个 cmdlet,它将告诉你如何解释输出中的一些值。

还有一种有用的方法可以使用Get-Help来查看 cmdlet 的帮助信息,方法是使用-online参数:

Get-Help Get-Process -Online

这将会在你的默认浏览器中打开该 cmdlet 的网页;它提供的信息与使用-Full参数时相同。

关于文件

Get-Help不仅帮助你了解 cmdlets,还能通过一套特殊的文件叫做ABOUT TOPICS提供许多关于 PowerShell 概念的有用信息。在撰写本文时,已经有超过 140 个此类文件。这些文件包含大量关于编程概念、构造以及常见查询的信息,例如 Windows 和非 Windows 环境的日志记录。通过运行以下命令,你可以自行查看:

Get-Help about*

我们来看一下其中一个文件:

Get-Help about_Variables

你应该能看到很多关于 PowerShell 中变量使用的有趣信息。

你也可以使用全文搜索来查询Get-Help。如果你查找的词不在帮助文件的文件名中,那么将会搜索文件的内容。这个过程稍微慢一些,但通常是值得的。试试输入以下命令:

Get-Help *certificate*

记住你得到的结果。现在,尝试输入certificates,复数形式:

Get-Help *certificates*

你将会得到一组不同的结果。第一组结果会找到文件名中包含certificate的帮助文件。当Get-Help产生第二组结果时,它找不到任何包含certificates的文件名,因此会执行全文搜索。请注意,如果搜索词出现在文件名中,那么全文搜索将不会执行。

我发现这些文件的唯一缺点是,假设你对 PowerShell 中的其他内容都很了解,只除非是当前讨论的主题。例如,ABOUT_VARIABLES在开头几段中提到了scope变量。尽管如此,如果你需要快速了解某个事物的工作原理,那么这些文件仍然是一个很好的资源。

Get-Member

本章我们将要看的一最后一个有用的 cmdlet 是Get-Member。在本章之前,我们讨论了 PowerShell 是如何生成对象,而不是像某些 shell 和脚本语言那样生成文本输出的。Get-Member允许你查看这些对象的成员、属性以及可用于它们的方法。通过展示而非描述来讲解会更容易理解,所以请继续在你的 shell 中输入以下命令:

Get-Process | Get-Member

注意

两个 cmdlet 之间的竖线称为管道字符 |。它不是小写字母 L——在我的 en-GB 标准 PC 键盘上,它位于左下角,靠近 Z 键;在标准的 en-US 键盘上,它位于 EnterBackspace 键之间。如果你的键盘没有实心的竖线 (|),那么破折竖线 (¦) 也可以使用。

你在这里做的是将 Get-Process cmdlet 的输出通过管道传递给下一个 cmdlet 作为输入,在这个例子中是 Get-Member。在后续的章节中,我们将进行大量关于管道的工作。Get-Member 会告诉你你提供的对象类型,在这种情况下是 System.Diagnostics.Process 对象,以及与该对象相关的方法、属性、别名属性和事件,如下所示:

图 1.16 – System.Diagnostics.Process 的一些成员

图 1.16 – System.Diagnostics.Process 的一些成员

几页之前,在图 1.14中,我们查看了在您的机器上运行的 pwsh 进程的属性。这些是列出的属性:NPM(K)PM(M)WS(M)CPU(s)IDSIProcessName。如您现在所见,这些是 Non-Paged Memory (K)Paged Memory (M)Working Set (M)Session ID,它们都是别名,以便能够在屏幕上整齐地显示在表格中。CPU(s) 别名的衍生方式稍有不同——它不是在对象上设置的。ID 和进程名称不是别名。M 和 K 分别是 兆字节千字节 的缩写。这只是该对象上所有属性的一个非常小的子集。如您所见,还有可用于对对象执行操作的方法。

活动 2

看看这些方法。你可能会使用什么方法来强制立即停止一个进程?如果你卡住了,可以查看这里的这些方法:docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process

在本书的剩余部分,我们将多次回到 Get-Member,因为它是一个非常有用的 cmdlet。

总结

在本章中,我们做了很多工作。我们讨论了 PowerShell 是什么以及它适合用于什么,比如快速简便地生成小段自动化代码。我们已经以几种不同的方式下载并安装了它,具体来说,通过从 .msi 文件安装和从 .zip 文件提取来安装它。

我们尝试了使用内置 shell 启动它的几种不同方法,最后,我们查看了三个有用的 cmdlet:Get-Command,用于查找我们可能使用的 cmdlet,Get-Help,用于了解如何使用它们,以及 Get-Member,用于理解这些 cmdlet 输出的内容。

在下一章,我们将探索 cmdlet 如何工作,探索参数和语法,并查看一个有用的应用程序,用于与 PowerShell 进行交互式操作:Windows Terminal。

练习

  1. 你会使用哪个 cmdlet 来生成一个随机数?

  2. 你如何生成一个 1 到 10 之间的随机数?

  3. 你会使用哪个 cmdlet 来列出文件夹的内容?

  4. 如何同时获取子文件夹的内容?

  5. 你会使用哪个 cmdlet 来创建一个新文件夹?

  6. 哪个 cmdlet 可以告诉你计算机已经开机多久?

  7. 哪个 cmdlet 可以将输出重定向到文件?

  8. 你可以使用 Get-Credential cmdlet 做什么?

  9. 你如何使用 ConvertTo-HTML cmdlet?

进一步阅读

第二章:探索 PowerShell Cmdlet 和语法

现在我们已经安装了 PowerShell,是时候开始使用它了。PowerShell 的强大之处在于它提供了大量的 cmdlet。在本章中,我们将学习这些 cmdlet。首先,我们将了解命名约定如何工作,然后学习如何通过参数控制它们的行为。

我们将深入探讨语法,以便完全理解每个 cmdlet 可用的参数以及这些参数需要什么信息才能正常工作。然后我们将学习如何从本地计算机和外部资源获取更多的 cmdlet 进行使用。

最后,我们将讨论如何与 PowerShell 进行交互式操作,并将为 Windows 安装一个令人兴奋的新应用程序,帮助我们实现这一点。到本章结束时,您将能够自信地找到并使用不熟悉的 cmdlet,并能够使用多功能的 Windows Terminal 应用程序。

本章将涵盖以下主要内容:

  • 什么是 cmdlet?

  • 理解 cmdlet 语法

  • 如何查找更多的 cmdlet

  • 与 PowerShell 进行交互式操作

技术要求

本章假设您已经拥有客户端机器、互联网连接,并且安装了最新的稳定版 PowerShell 7。本章的最后两节—— Windows Terminal – 另一种终端模拟器从 Windows Store 安装 Windows Terminal——是专为 Windows 用户准备的。使用 Linux 和 macOS 的我们可以放心,因为我们不需要做这些部分,这些操作系统已经包含了多标签终端。

什么是 cmdlet?

Cmdlet(发音为 command-lets)是我们在 PowerShell 中使用的命令。它们可以通过终端应用程序或脚本输入。它们可能是 基于脚本 的,由其他 cmdlet 构建,或者可能是用 C# 编写的 二进制 cmdlet。默认安装的 cmdlet 通常是二进制文件。通常,我们自己编写或从互联网上下载的 cmdlet 将是基于脚本的。Cmdlet 从管道中获取 输入对象,对其执行操作,并通常返回一个对象以供进一步处理。我们将在 第三章中详细描述这一过程,PowerShell 管道 – 如何将 cmdlet 串联起来

探索 cmdlet 结构

我们已经使用了一些 cmdlet,您可能已经注意到它们有一个共同的结构:Get-HelpGet-CommandGet-Member。这是一个 动词-名词 结构,旨在帮助我们理解每个 cmdlet 的功能。开始的 动词 告诉我们,在每种情况下,这些 cmdlet 都是要 获取 某些东西:帮助、命令列表或成员列表。这里有一个批准的动词列表:docs.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands?view=powershell-7.2

为什么我们需要一个批准的动词列表?为了清晰起见;PowerShell 的主要目的之一是易于理解。通过限制动词的数量并明确定义每个动词的含义及其使用场景,我们可以立即了解一个 cmdlet 的作用。例如,一个名为 Delete-Item 的 cmdlet 可能和 Remove-Item 一样直观,但如果是 Eliminate-Item 呢?这可能意味着它会做一些不太愉快且具有终结性质的操作。大多数写得好的 cmdlet 都会遵循这个列表;希望我们编写的代码也能符合这一点。

请注意,列表中的并非所有动词在英语中都是实际的动词。New 不是一个动词,但为了 cmdlet 的目的,我们将其当作动词使用。

活动 1

我们什么时候使用 New,什么时候使用 Add?请查看前面的链接了解详细信息。

cmdlet 的第二部分是 Get-Process,而不是 Get-Processes。第二条规则是它们应该具有描述性;即,它们应该是立即可以理解的。这可能会导致一些较长的 cmdlet 名称,如 Get-NetAdapterAdvancedProperty。这虽然有点拗口,但很容易理解。我们在 第一章 中已经看过的自动完成功能,PowerShell 7 简介——它是什么以及如何获取,使得输入长 cmdlet 更加容易。只需输入名词的第一部分,按 Tab 键,再输入下一部分,再按 Tab 键,以此类推,直到完整输入 cmdlet。试试 Get-NetAdapterAdvancedProperty。我们只需输入 Get-NetA,然后按两次 Tab 键就能得到正确的 cmdlet。在 Windows 机器上,反复按 Tab 键会在合适的 cmdlet 之间循环,或者在 Linux 和 macOS 上呈现选项列表。

关于大小写也有一些标准;驼峰命名法是首选,即名词中的每个单词都要大写。这不仅使我们从屏幕上阅读更容易,而且还意味着屏幕阅读器等辅助工具可以正确处理它们。

正确使用别名

有一种方法可以避免繁琐的 cmdlet 名称:将 Get-Alias 输入命令行。你应该会看到一个相当长的别名列表,列出常用 cmdlet 的别名。例如,你应该能看到 manGet-Help 的别名。试试它;输入以下代码并查看发生了什么:

man Get-Command

有两种类型的别名,分别用于不同的目的。让我们在这里仔细看一下它们:

  • gciGet-ChildItem 的别名。这些别名通常要求你已经知道正确的 cmdlet,并且可能非常简短且不直观。它们是为那些懂 PowerShell 并希望节省时间的人准备的。

  • man 是一个 Unix 命令,调用的是 lsdir,它们都是 Get-ChildItem 的别名,并生成看起来像你熟悉的 ls Unix 命令或 dir Windows 命令的输出。这些是为了像我这样的老年人,以便不用重新学习多年的肌肉记忆。

注意

尽管我们可以在 PowerShell 中输入 dir 并获得相关输出,但我们并没有得到来自 dir 命令的输出。我们在 dir 中可能熟悉的命令选项在 PowerShell 中不起作用。我们实际上是在调用一个具有类似功能的 cmdlet。

我们还可以使用 Set-Alias cmdlet 为我们经常使用的 cmdlet 定义自己的别名。我们可以这样做:

Set-Alias -Name adapters -Value Get-NetAdapter

尝试一下。输入前面的 cmdlet,然后输入 adapters。你应该能看到类似这样的输出:

图 2.1 – 设置别名

图 2.1 – 设置别名

这样做的缺点是,你设置的别名仅在当前的 PowerShell 会话中有效。当你关闭窗口时,所有自定义的别名都会丢失。你可以将它们保存在 PowerShell 配置文件中,但它们仍然仅在本地有效。我避免使用自定义别名,而是依赖自动补全。将别名用于脚本中也是不好的做法,因为这会影响代码的可读性。

现在我们了解了什么是 cmdlet,让我们看看使用它们时需要遵循的语法。

理解 cmdlet 语法

我们已经看到,你可以向 cmdlet 传递信息作为输入,或者修改输出。例如,在上一节中,我们输入了以下内容:

Set-Alias -Name adapters -Value Get-NetAdapter

这个 cmdlet 的名称是 Set-Alias,但后面有两部分信息:-Name-Value。这两部分被称为 -,它告诉 PowerShell 后面的字符直到下一个 空格 字符表示的是指令,而不是值。在前面的参数中,我们传递了一个字符串——我们告诉 Set-Alias-Name 的值是 adapters,而 -Value 的值是 Get-NetAdapter。现在,当我们在命令提示符下输入 adapters 时,PowerShell 会知道用 Get-NetAdapter 来替换它。

太好了,但我们如何知道一个 cmdlet 会接受哪些参数呢?好吧,我们将再次回到我们的朋友 Get-Help,它出现在 第一章,《PowerShell 7 入门——它是什么以及如何获取它》中。打开一个 PowerShell 提示符,输入以下内容:

Get-Help Get-Random

这将调出Get-Random的帮助文件。我们关注的是下面的SYNTAX部分:

图 2.2 – Get-Random 语法

图 2.2 – Get-Random 语法

好的,这很棒,但这到底是什么意思呢?帮助文件告诉我们,Get-Random可以有三种不同的操作方式;每种方式都有一组参数。让我们专注于第一个参数集。记住,参数总是以短横线(-)开始,因此我们可以看到第一个参数集中有四个参数:-Maximum-Minimum-Count-SetSeed。正如DESCRIPTION部分所说:你可以使用 Get-Random 的参数来指定最小值和最大值、从集合中返回的对象数量,或者一个种子数字。希望你已经完成了《第一章》中的练习部分,PowerShell 7 简介–它是什么以及如何获取,因此你之前应该见过这个。让我们来看第二个参数集。那里有三个参数:-InputObject-Count-SetSeed。使用这些参数会让Get-Random做不同的事情。它将不再返回一个随机数字,而是返回你在第一个参数中给定的列表中的一个随机对象。让我们试试看。在 PowerShell 提示符下,输入以下内容:

Get-Random -InputObject "apples", "pears", "bananas"

希望Get-Random能够从这个列表中返回一个随机的水果项。逗号字符告诉 PowerShell 列表中还有更多项。

以下截图展示了你可能会犯的一些错误:

图 2.3 – 错误传递项目列表

图 2.3 – 错误传递项目列表

在第一个示例中,我只传递了一个包含大量双引号的单一项。

在第二个示例中,我只传递了一个项给-InputObject参数,PowerShell 将第二个字符串pears解释为另一个参数的输入,结果产生了混淆。

在第三个示例中,PowerShell 正在等待列表中的下一个项,用>>符号表示。如果你遇到这种情况而不确定接下来应该输入什么,按Ctrl + C可以中断操作。

SYNTAX部分告诉我们的不仅仅是参数的名称和集合。它还告诉我们哪些参数是必需的,哪些参数可以接受多个项,哪些参数需要显式指定名称,以及每个参数接受的值类型。请保持专注——接下来可能会有点复杂。让我们再次查看Get-Random的帮助文件中的SYNTAX部分,见下图。请留意方括号:

图 2.4 – Get-Random 语法

图 2.4 – Get-Random 语法

我们的第一个参数集以[[-Maximum] <System.Object>]开始。-Maximum是参数名称,而<System.Object>-Maximum参数将接受的输入类型。

如果你输入 Get-Help Get-Random -Full,你会看到它只接受整数、浮动点数,或可以解释为整数的对象,例如字符串 two,如图 2.5所示。外部的方括号告诉我们 -Maximum 参数是可选的;我们不需要包括它就能得到一个随机数。参数名称周围的内部方括号 [-Maximum] 告诉我们,我们不需要包含参数名称来为 cmdlet 传递最大值。在以下截图中,我们可以看到该参数的位置信息是 0——这意味着第一个未命名的参数会被解释为属于 -Maximum 参数:

图 2.5 –  和  参数的详细信息

图 2.5 – -Maximum-Minimum 参数的详细信息

我们可以看到,对于 [-Minimum <System.Object>] 参数,内部没有方括号;这意味着如果我们想使用 -Minimum 参数,它必须始终被命名;我们必须实际输入 -Minimum。对于 -Minimum-Maximum 参数,它们的 <System.Object> 周围没有方括号。这意味着如果我们使用这些参数,我们必须向它们传递一个 System.Object 类型的参数(具体来说,是一个可以被解释为数字的整数、浮动点数或字符串)。

我们来看第二组参数。它以 [-InputObject] <System.Object[]> 开头;这意味着如果我们想使用第二组参数,我们必须提供一些输入。[-InputObject] 周围的方括号告诉我们这个参数是可选的,不过,PowerShell 会将它接收到的第一个参数解释为输入。这和第一组参数有什么不同呢?仔细看看 <System.Object[]> 参数——它的末尾有一对方括号。这表示它可以包含多个值,用逗号分隔。试试看,输入以下内容:

Get-Random 10, 20, 30

希望你能得到其中一个值的返回。PowerShell 知道它已经接收到多个值,并且知道不会将这些值集合解释为 -Maximum 的参数,因为 -Maximum 只能包含一个单一的值。

活动 2

你怎么给 -InputObject 参数提供一个单一的数值?

现在我们来看第三组参数。乍一看,它们和第二组参数看起来一样。它们的开头是相同的,但注意结尾有一个参数:-Shuffle。它没有方括号,也没有参数。这是一个 switch 参数。如果我们使用它,我们就自动使用了第三组参数;这意味着 -Count 参数对我们不可用,因为它不在第三组参数中。它不接受任何参数,因为它告诉 PowerShell 以随机顺序返回整个列表。

每个参数集以 [<CommonParameters>] 结尾。这是一组可用于任何 PowerShell cmdlet 的参数。你可以阅读 about_CommonParameters 帮助文件获取更多信息。它们包括控制 PowerShell 在发生错误时采取的操作的变量,或从 cmdlet 输出更多内容以帮助故障排除。有关更多信息,请参见第十章《错误处理 – 哎呀!它出错了!》。

让我们总结一下 cmdlet 的语法。接下来列出了六种类型的参数:

  • Start-Service 有一个必填参数:-DisplayName

  • Get-Random 的参数 -InputObject 就是一个例子。

  • Get-Random 的参数 -Count 就是一个例子。

  • -MaximumGet-Random cmdlet 中的一个例子。整个参数被方括号包围,然后有一对第二个方括号仅包围参数名称。

  • -Shuffle 是一个强制性的开关参数的好例子。

  • 常见参数:这些是所有 PowerShell cmdlet 都可以使用的参数,允许你控制输出或错误行为。

参数组织成 Get-Random 可以返回一个随机数字、从列表中随机选取一个项目,或者按随机顺序返回整个列表。

现在我们已经理解了什么是 cmdlet 和参数,你可能会想知道它们从哪里来。下载 PowerShell 时会包含很多,但这远远不够。接下来我们将探索如何获取更多的 cmdlet。

如何找到更多的 cmdlet

Cmdlet 通常被捆绑成一个名为模块的包。我们将在第十一章《创建我们的第一个模块》中详细讲解模块结构,但现在知道模块是一个包含具有共同主题的 cmdlet 集合就足够了,主题可能是与某个特定应用程序互动或执行一组类似的功能。一旦模块被安装和导入,cmdlet 就会在 shell 或脚本中可用。

在你的机器上查找模块和 cmdlet

我们已经有了很多可用的模块。试试看;在命令行中输入 Get-Module。根据 PowerShell 的安装时间、会话的开启时长和我们正在使用的平台,应该会看到一个相对较短的列表,类似这样:

图 2.6 – 已导入模块列表

图 2.6 – 已导入模块列表

这是一个当前会话中已导入模块的列表。但这并不是我们现在可以使用的所有模块。尝试像这样再次运行 cmdlet:

Get-Module -ListAvailable

你应该会看到更多;如果你使用的是 Windows 系统,你将看到更多。

cmdlet 的输出将根据模块所在的目录进行拆分,如下图所示:

图 2.7 – 模块及其目录

图 2.7 – 模块及其目录

如你所见,在我的机器上,该 cmdlet 找到了来自 PowerShell 7 Preview 和 Windows PowerShell 的模块,以及其他许多模块。PowerShell 使用 PSModulePath 环境变量来知道在你的环境中在哪里查找模块。我们可以通过键入以下内容来检查该变量中包含哪些位置:

 $env:PSModulePath -split ";"

如果你在 Linux 或 macOS 设备上工作,使用冒号是必要的。

每次启动 PowerShell 时,这些位置会被放入该变量中,但它们也可以由应用程序或手动添加。你可以在下面的屏幕截图中看到我的机器上的结果。注意,最后一项是 Microsoft Message Analyzer;这是我安装该应用程序时添加的:

图 2.8 – PSModulePath 变量

图 2.8 – PSModulePath 变量

虽然我们可以手动添加路径,但对于大多数情况来说,最好确保将模块安装到默认路径。有关 PSModulePath 变量的更多信息,请参阅 about_PSModulePath 帮助文件。

现在我们知道了在哪里可以找到计算机上的模块,那么如何知道它们包含哪些 cmdlet 呢?请注意在图 2.7 中,ExportedCommands 表中有属性;这些就是我们导入模块时可用的 cmdlet。可能有一些 cmdlet 没有导出,只能在模块内部使用;我们无法直接键入这些 cmdlet 进行使用。我们可以通过运行 Get-Command –Module,后跟模块名称,来查看仅导出的 cmdlet。

让我们尝试一下。在第一章《PowerShell 7 简介——它是什么以及如何获取它》中,我们使用了 Get-Command cmdlet 来查找 cmdlet。该 cmdlet 会搜索所有可用的模块中的 cmdlet,或者我们可以告诉它只搜索已导入的模块。假设我们对操作模块的 cmdlet 感兴趣。我们可以键入以下内容,以获取包含 module 这个词的 cmdlet 列表:

Get-Command *module*

然而,如果我们键入以下内容,我们将只会得到已经导入到当前会话中的模块的 cmdlet:

 Get-Command *module* -ListImported

这显示了这两个 cmdlet 在我的机器上的表现:

图 2.9 – 已导入和已安装的 cmdlet

图 2.9 – 已导入和已安装的 cmdlet

如何使用尚未导入的模块中的 cmdlet?有两种方法,如下所示:

  • 首先,我们可以使用 Import-Module cmdlet 将模块导入会话,然后使用我们需要的 cmdlet。

  • 或者,我们也可以直接使用它,并允许 PowerShell 隐式地导入它。

尝试一下。在图 2.6 中,有一个当前会话已导入模块的列表。它不包含 PowerShellGet 模块。很可能你的当前会话也没有导入该模块,因此让我们现在导入它。键入以下内容:

 Get-InstalledModule

您会注意到,当 PowerShell 导入 PowerShellGet 模块时会有一个小小的暂停,然后如果有已安装的模块,您会看到它们的列表。那很好,但聪明的地方在于:再次输入 Get-Module 命令,来获取已导入模块的列表。您应该会看到 PowerShellGet 已经在后台被导入,如下所示:

图 2.10 – 仿佛魔法般……模块出现了

图 2.10 – 仿佛魔法般……模块出现了

这引出了两个问题。第一个问题是:为什么还要导入和安装?为什么不从一开始就将所有 cmdlet 都可用呢?简短的答案是 空间。每个导入的模块都需要占用内存空间;如果我们导入了数百个模块和成千上万的 cmdlet,那么每次启动 PowerShell 时,它将变得极其笨重和缓慢,因此我们只在需要时导入我们需要的模块和 cmdlet。

另一个问题是:如果我们可以隐式获取 cmdlet,为什么还需要 Import-Module cmdlet 呢?我们需要 Import-Module 有两个原因:首先,您可能没有将模块安装到 PSModulePath 中的默认路径之一,因此隐式导入将不可用。其次,您可能希望控制模块的导入方式。例如,您可能不想导入与已导入 cmdlet 同名的模块,这时您应该使用带有 -NoClobber 参数的 Import-Module,而不是隐式导入。

活动 3

我们如何导入 cmdlet,但改变它们的名称,以便我们知道它们来自哪个模块呢?

查找新的模块和 cmdlet

到目前为止,我们已经查看了系统上已有的模块,但我们刚刚导入了 PowerShellGet,它非常有用,因为它可以帮助我们查找远程存储的模块,默认情况下,PowerShellGet 会连接到远程位置。

让我们首先来看一下如何找到我们感兴趣的 PowerShell Gallery 中的模块。尝试运行以下命令:

Get-Command -Module PowerShellGet

这将为我们提供模块中可用的 cmdlet 列表。我们可以看到它们分为两类——用于查找和安装资源(如模块和脚本)的 cmdlet,以及用于管理仓库的 cmdlet。我们来看看一个模块吧。尝试运行以下命令:

Find-Module -name *math*

我的结果如下所示:

图 2.11 – PowerShell Gallery 中与数学相关的模块

图 2.11 – PowerShell Gallery 中与数学相关的模块

其中一些看起来或多或少有用——我不确定我是否需要 PokerMath,但你可能需要。我们怎么知道这些模块中有哪些 cmdlet 呢?我们不能使用 Get-Command,因为这些模块不在本地计算机上,但是 PowerShellGet 包括了 Find-Command cmdlet。如果我们将某个特定模块的名称传递给 Find-Command,它会列出该模块中的 cmdlet,像这样:

Find-Command -ModuleName psmath

如果我们运行它,我们可以看到psmath包含整个范围的数学函数,从相当明显的——如Get-Math.sqrt——到一些更为深奥的人工智能(AI)和统计学函数。要了解如何使用这些函数,我们需要安装模块并查看帮助文件。我们可以通过输入以下内容来执行此操作:

Install-Module psmath

您可能会看到一个警告,说明仓库是不受信任的,就像下面的屏幕截图一样。这是预期的,因为默认情况下,没有仓库是受信任的。我们需要使用Set-PSrepository cmdlet 显式信任它们:

图 2.12 – 你信任这个仓库吗?

图 2.12 – 你信任这个仓库吗?

输入Y以确认。几秒钟后,进度条消失,命令提示符将重新出现。现在模块已安装,但尚未导入。尽管我们使用PowerShellGet安装了模块,但它已安装到一个位置,这不是隐式导入它的正确路径。但我们可以使用Import-Module cmdlet 显式导入它,就像这样:

Import-Module psmath

如果我们现在输入以下内容,我们将看到社区提供的模块的一个缺点:

Get-Help Get-Math.Sqrt

许多作者更喜欢编写他们的软件而不是为其撰写文档,所以帮助内容相当稀少。然而,我们可以看到Get-Math.Sqrt接受两个参数:要么是值,要么是输入对象。-Values参数是位置参数。有趣的是,它还可以接受值的列表,而不仅仅是单个值。您可以使用输入对象来提供另一个 cmdlet 或变量的输出。尝试这个:

Get-Math.Sqrt -InputObject (Get-Random)

这将生成一个随机数,然后计算其平方根。

为什么PowerShellGet不能将模块安装到正确的位置?这与作用域有关。默认情况下,PowerShellGet会仅为当前用户安装模块,如果您使用 Windows,则会安装到您的Documents文件夹中的路径,或者在 Linux 上安装到您的主目录。我们可以通过使用-Scope参数和AllUsers参数来更改此设置,但我们需要以管理员权限运行才能执行此操作。在下面的屏幕截图中,您可以看到在这种情况下出现的错误消息。另一种方法是将位置添加到PATH环境变量中:

图 2.13 – 我恐怕不能这样做,戴夫

图 2.13 – 我恐怕不能这样做,戴夫

我们也可以在线查看 PowerShell Gallery,网址为www.powershellgallery.com/。在网站上搜索math将会得到更多结果,因为它不仅会搜索模块名称,还会搜索任何关联的标签。我们可以通过使用-Filter参数和Find-Module获取类似的结果。

虽然 PowerShell Gallery 很棒,但也有其他仓库。你可能在工作中有一个内部仓库,或者你可能决定以后为自己建立一个。仓库的优点是版本控制和它所允许的自动化。缺点是维护以及潜在的被利用风险。如果你需要使用除 PowerShell Gallery 外的其他仓库,那么 PowerShellGet 包含了一些用于与它们交互的 cmdlet,如 Get-PSRepositorySet-PSRepositoryRegister-PSRepository

其他来源

PowerShell Gallery 的主要替代方案是 GitHub,网址为 github.com,以及类似的在线源代码管理工具,如 GitLab。这些平台不仅限于 PowerShell,还包含许多其他语言编写的代码。绝大多数代码以某种形式是开源的。GitHub 平台由微软拥有,但并未由微软管理,包含从完全官方的 PowerShell 仓库(包括微软自己的代码),到未经维护的、不完整的脚本片段和恶意软件。除非你绝对信任仓库所有者,否则一定在下载之前阅读代码并理解它的作用。

我们可以在 GitHub 上搜索 PowerShell 模块,通过在 GitHub 网站上输入搜索词并添加 language:PowerShell,如下图所示:

图 2.14 – GitHub 网站上的 49 个仓库,其中一些可能有用

图 2.14 – GitHub 网站上的 49 个仓库,其中一些可能有用

我们也可以在互联网上找到大量 PowerShell 脚本;尽管我们不太可能找到完整的模块,但许多有用的网站在源代码管理平台之外发布了完全可用的脚本。然而,警告依然存在,甚至更加重要。即使你信任作者,也要在使用之前阅读代码,并理解它的作用。永远不要盲目运行某些东西并指望它没问题。PowerShell 功能非常强大,您应该始终安全地运行它。关于这一点,我们将在 第十二章保护 PowerShell 中详细讨论。

获取 cmdlet、模块和脚本的最终方式是自己编写。这比你想象的要容易,我们将在本书的第二部分中介绍,从 第八章编写我们的第一个脚本 – 将简单的 cmdlet 转化为可重用的代码

现在我们应该对可以找到 PowerShell 资源的众多地方有了较好的了解,并且理解它们的相对价值。在下一部分,我们将讨论与 PowerShell 互动的最基本方式,Windows 用户可以享受一些乐趣。

与 PowerShell 进行交互式工作

在我看来,PowerShell 是一种不太寻常的编程语言,因为其庞大的 cmdlet 数量使得它特别适合互动使用;打开终端,输入一行代码,就能看到一些令人兴奋的结果。在其他解释性语言中很难做到这一点,比如 Python,因为 Python 自带的命令不多,并且将库导入到交互式会话中也很困难。因此,Python 用户很快就会转向编写脚本。我在使用 PowerShell 的 10 年里,发现很多同事实际上并没有真正从交互式 PowerShell 进展到脚本编写,这也没问题。在本章的其余部分,我们将回顾一下我们是如何使用 PowerShell 的,Windows 用户将能安装一个非常有用的工具,叫做 Windows Terminal,它能为他们提供与 Linux 和 macOS 用户默认拥有的相同的多标签终端体验。

每当我们输入一行代码时,我们就调用了一个 cmdlet。每个 cmdlet 都是一个迷你脚本或程序。这类似于 shell 脚本;Windows 中的批处理脚本或 Linux 中的 Bash 脚本,其中我们输入的每一行代码都会调用一个定义好的程序。

PowerShell 中的大部分脚本都是以相同的方式编写的——脚本可以像 cmdlet 一样运行,带有参数来修改行为和操作。这意味着我们也可以互动使用它们,从而将我们的努力与那些技术上不太擅长的同事和朋友分享。然而,在我们开始编写脚本之前,最好先设置一个方便的方式来互动使用 PowerShell。PowerShell 7 提供了改进的 shell,包括高亮显示,使我们更容易看清我们正在输入的内容,以及改进的复制粘贴功能。然而,我更喜欢另一个工具——Windows Terminal。

Windows Terminal——一个替代终端模拟器

如果你使用的是 Linux 或 macOS,你不需要做这一部分,因为你们已经有了多标签的终端,真是幸运的人。对于 Windows 用户,我们以前需要打开多个应用程序;过去,我可能同时打开了 Windows Console(命令提示符)、Windows PowerShell、PowerShell 7、Azure Cloud Shell、PuTTY、Git Bash 和 Python,作为不同的应用程序运行。然而,自 2020 年以来,有了一个更好的选择——Windows Terminal。它可以在不同的标签页中运行任何命令行程序的多个实例,这就足以让我信服,但它还支持表情符号和字形、分屏功能,以及一个据说很有趣的新字体叫做 Cascadia Code,另外它是开源的。如果你想了解更多细节,可以参考这篇博客:devblogs.microsoft.com/commandline/introducing-windows-terminal/

Windows Terminal 托管在 GitHub 上,网址是 github.com/Microsoft/Terminal,你可以从那里下载并安装 .msixbundle 文件。你也可以如果之前安装了 Winget,或者使用 Windows 11,则可以通过 Winget 安装它。然而,推荐的方式是通过 Microsoft Store 安装,这样可以让应用自动更新——由于它是开源软件,更新频繁且通常是必要的,因为它包含了 bug 修复和改进。

从 Microsoft Store 安装 Windows Terminal

从 Windows Store 安装非常简单。以下是安装步骤:

  1. 在 Windows 的搜索框中输入 store,然后启动 Microsoft Store 应用。

  2. 在 Store 应用的搜索框中输入 Windows Terminal

  3. 点击 获取,如以下截图所示:

图 2.15 – 获取 Windows Terminal:不适合幼儿

图 2.15 – 获取 Windows Terminal:不适合幼儿

  1. 几分钟后,Terminal 会出现在你的 开始 菜单中。

就这样。Windows Terminal 现在会自动更新。接下来,我们开始为我们的用途进行配置。因为我们已经安装了 PowerShell 7,所以当你打开它时,Terminal 会默认使用 PowerShell 7;如果没有安装,它将默认使用 Windows PowerShell。根据你在客户端安装的其他应用程序,它可能会自动识别并提供可用的选项。点击工具栏中的 向下 图标,查看已提供的应用,并选择一个非默认的应用,如以下截图所示:

图 2.16 – Windows Terminal 中可用的基本应用

图 2.16 – Windows Terminal 中可用的基本应用

这很好,但它还远远不够涵盖你可能拥有的所有命令行应用程序。让我们看看如何做到这一点。我在我的机器上安装了 Python,因为我常常喜欢换个视角。如果你想跟着一起操作,且没有安装 Python,可以从这里下载:www.python.org/downloads/。

默认情况下,Python 安装在 C:\Users\<yourname>\AppData\Local\Programs\Python\<version number>,如下图所示。你需要记下这个路径,并在文件资源管理器中启用查看隐藏文件:

图 2.17 – Python 的位置

图 2.17 – Python 的位置

要配置 Windows Terminal 以访问 Python,你需要设置一个新的配置文件。以下是操作步骤:

  1. 启动 Windows Terminal。

  2. 点击工具栏中的 向下 按钮,然后选择 设置

  3. 在左侧面板中选择 添加新配置文件,然后点击 新建空配置文件,如以下截图所示:

图 2.18 – 创建新配置文件

图 2.18 – 创建新配置文件

  1. 填入名称——在我的情况下,我使用的是Python 3.10,这样我就知道它会启动哪个版本的 Python。

  2. 填入 python.exe 可执行文件的路径。

  3. 选择一个起始目录,如果你喜欢的话。我喜欢把所有的东西都放在一个地方。

  4. 我喜欢使用图标。你可以在这里找到 Python 图标:C:\Users\<用户名>\AppData\Local\Programs\Python\Python310\Lib\test\imghdrdata

  5. 点击保存

你可以在以下截图中看到这个概览:

图 2.19 – 创建新配置文件,完成

图 2.19 – 创建新配置文件,完成

现在我们完成了。当你点击工具栏中的下拉按钮时,你将看到Python 3.10作为一个选项。为什么我在这里使用 Python 作为示例?为什么不使用 PowerShell 7 预览版呢?因为 Windows Terminal 非常智能。如果你在安装 Windows Terminal 后安装了 PowerShell 7 预览版,只需重新启动 Windows Terminal,瞧!PowerShell 7 预览版就会成为一个选项。酷吧,嗯?

这里,你可以看到 Python 在 Windows Terminal 中运行的画面:

图 2.20 – 在 Windows Terminal 中运行 Python

图 2.20 – 在 Windows Terminal 中运行 Python

还有一件事我们必须做。我们已经讨论了能够以管理员身份启动 PowerShell 的重要性;我们也需要为 Windows Terminal 做同样的事情。最简单的方法是将 Terminal 应用程序固定到任务栏。如果你右键点击任务栏上的图标,我们可以右键点击弹出菜单中的Terminal图标,并选择以管理员身份运行,如下所示:

图 2.21 – 以管理员身份运行 Windows Terminal

图 2.21 – 以管理员身份运行 Windows Terminal

如果你在 Windows Terminal 中打开一个配置文件的设置,你可以仅为该配置文件设置以管理员身份运行。Windows Terminal 还有很多其他功能可以使用。设置可以通过用户界面UI)进行访问,或者你也可以直接编辑一个方便的JavaScript 对象表示法JSON)设置文件。

总结

在本章中,我们已经相当彻底地探讨了 cmdlet。到现在为止,我们应该已经了解了命名规则、cmdlet 使用的语法、如何找出 cmdlet 所需的参数以及如何填写它们。接着,我们探索了在本地机器和 PowerShell Gallery 上发现新 cmdlet 和模块的方法。最后,我们讨论了如何与 PowerShell 进行交互式工作,并介绍了一个适用于 Windows 用户的全新应用程序——Windows Terminal。

在下一章中,我们将深入研究管道;它是如何工作的,如何将 cmdlet 连接在一起,如何理解发生了什么错误,以及如何解决它。我们还将探讨另一个适用于 PowerShell 的优秀应用程序,而且这次每个人都可以安装,而不仅仅是 Windows 用户。

练习

  1. 在 PowerShell 中,获取文件内容的正确命令是 Get-Content 还是 Read-Content

  2. 如果你在 shell 中输入 "alive alive" | oh,会发生什么?为什么?

  3. Get-ChildItem 命令有多少个参数集?哪个参数决定我们将使用哪个参数集?

  4. 如果你看到 Get-ChildItem c:\foo *.exe 命令,可以看出 c:\foo 是传递给 -Path 参数的一个参数。那么 *.exe 被传递给哪个参数?

  5. 如果不实际尝试,Get-ChildItem c:\foo -Filter *.exe, *.txt 命令会运行吗?如果不会,为什么?

  6. 如何找到与 Amazon Web Services (AWS) 相关的 cmdlet?

  7. 如何找到与 AliCloud 相关的 cmdlet?

  8. 如何在 Windows Terminal 中更改文本大小?

第三章:PowerShell 管道——如何将 cmdlet 串联起来

几乎所有的 操作系统 (OSs) 都有 管道 的概念,它允许一个进程的输出被传递到下一个进程的输入中。这个概念归功于 Douglas McIlroy,在 1973 年他在贝尔实验室工作时开发了 Unix 第 3 版。最初的实现设想将每个命令的输出视为一种类似文件的结构,下一条命令可以在此结构上操作。

本章将解释 PowerShell 如何遵循这一愿景并与之有所不同。我们将从探讨管道的概念开始,然后介绍一些操作管道内容的基本方法,再深入分析 PowerShell 中管道的工作原理,以及当管道出现问题时如何进行故障排除。

到本章结束时,我们将理解信息如何从一个 cmdlet 传递到下一个 cmdlet,如何操作这些信息以便只处理我们需要的部分,以及当出现错误信息时,如何找出问题所在。

本章将涵盖以下主题:

  • 如何将 cmdlet 组合在一起——管道

  • 选择和排序对象

  • 过滤对象

  • 枚举对象

  • 格式化对象

  • 管道是如何工作的——参数绑定

  • 故障排除管道——管道跟踪

如何将 cmdlet 组合在一起——管道

自 1970 年代的 Unix 和 C 编程语言以来,操作系统已将计算机的输入和输出抽象为 Read-Host cmdlet。stdout 流是 cmdlet 成功输出的结果。stderr 流包含程序产生的任何错误信息,并被发送到一个单独的流,以避免干扰任何成功的输出。

PowerShell 对这些流进行了扩展,拥有六个输出流,而不是两个。每个流都可以通过显式的 PowerShell cmdlet 捕获,或者在运行 cmdlet 时指定一个常用参数,如下表所示:

流 # 描述 Cmdlet 常用参数
1 成功 Write-Output 无——这是默认的输出
2 错误 Write-Error -ErrorAction-ErrorVariable
3 警告 Write-Warning -WarningAction-WarningVariable
4 详细信息 Write-Verbose -Verbose
5 调试 Write-Debug -Debug
6 信息 Write-Information -InformationAction-InformationVariable

表 3.1 – PowerShell 流

流 1 相当于标准输出(stdout),而流 2 相当于标准错误输出(stderr)。PowerShell 管道将流 1 的内容(即成功)从一个 cmdlet 传递到管道中的下一个 cmdlet。当我们看到屏幕上的红色错误信息时,它们并不是输出在流 1 中——它们输出在流 2 中。原因是我们不希望错误信息(或者详细信息或任何除输出对象之外的信息)被传递到下一个 cmdlet 中,导致另一个错误。毕竟,第二个 cmdlet 没有办法解释这些信息。

管道可能由一个或多个 PowerShell cmdlet 组成,使用管道符号 (|) 分隔。每个 PowerShell cmdlet 都是管道的一部分,即使它只是一个单独的 cmdlet。在每个管道的末尾都有一对隐式的 cmdlet,Out-Default | Out-Host,确保 cmdlet 在流 1 中的输出被格式化并写入屏幕。有些 cmdlet 没有流 1 输出,因此在运行它们后我们不会看到任何屏幕上的输出。例如,在 第二章探索 PowerShell Cmdlet 和语法 中,我们运行了以下 cmdlet:

Set-Alias -Name processes -Value Get-Process

一旦 cmdlet 执行完毕,我们会返回到提示符。如果我们查看 Set-Alias 的帮助文件,会看到它默认没有输出,因此在成功运行时我们不会看到屏幕上的任何内容。尽管如此,Set-Alias 仍然是一个管道;Out-Default 仍然运行,只是没有接收到任何输出。

Cmdlet 从左到右执行,左侧 cmdlet 的输出对象会传递到管道右侧的下一个 cmdlet。为了便于阅读(和输入),管道符号可以用作交互式工作时的换行符。尝试在管道符号后按 returnEnter

Get-process |
Get-member

在按下 return/Enter 键后,你应该看到一个继续提示符(>>),如下所示:

图 3.1 – 使用管道符号作为换行符

图 3.1 – 使用管道符号作为换行符

我们一直在提到对象——描述我们具体指的是什么将会很有帮助。我们将在下一节中做这件事。

什么是对象?

当我们在 Linux 和 Windows 控制台中运行命令时,命令将字节流输出到标准输出(stdout);这被解释为一个文本文件,保存在内存中。当我们想要操作这个输出的内容时,我们必须使用与查找和操作文本相同的工具;这些工具可能是 Perl、sed、AWK 或其他一些工具。

这意味着我们变得擅长文本操作;我的桌子上大约有七本 Perl 书籍,最早的是上世纪九十年代出版的。PowerShell cmdlet 不产生类似文本的字节流;相反,它们生成对象和对象集合,这些对象是以表格结构保存在内存中的,在我们运行 Get-Process 时产生,如下所示:

图 3.2 – 获取进程对象集合

图 3.2 – 获取进程对象集合

表格中的每一行都是一个对象。每一列是表中对象的一个属性。整个表格是对象的集合。从图 3.1中,我们知道每个对象都是System.Diagnostics.Process类型,并且具有与该对象类型相关联的属性和方法列表。通过管道,我们可以将这个集合发送到另一个 cmdlet 来提取更多信息,或者仅仅调用我们感兴趣的特定属性。如果我们想知道一个默认未显示的属性值,例如一个特定进程在其生命周期中消耗的特权处理器时间,我们可以输入以下命令:

(Get-Process -id 4514).PrivilegedProcessorTime

那个4514是从哪里来的?它是图 3.2中某个pwsh进程的Id属性。从这里,我可以看到我的pwsh进程在特权模式下消耗了 11.75 秒。我知道——我们可能不想知道像pwsh这样简单的进程的这些信息,但如果我们在调查数据库服务器的存储性能问题时,可能会对其他进程的这些值感兴趣。运行以下代码:

Set-Alias -Name processes -Value Get-Process
(get-process -id (processes).id).privilegedprocessortime

在这里,我们将获取当前在客户端上运行的所有进程的特权处理时间,使用我们为Get-Process设置的别名。

并不是所有 cmdlet 都会生成单一类型的对象。一些 cmdlet 可能会生成多个对象,我们需要小心如何处理这些对象并将它们的输出传递到管道中。例如,考虑Get-ChildItem cmdlet。它获取目录或文件夹的内容。一个目录可能包含两种基本类型的项目——即文件和子目录。这两种类型会有不同的属性——例如,我们不能将子目录嵌套在文件中。如果一个管道已经设置为操作文件对象,那么如果也传递了目录对象,它可能会失败。让我们来看看,输入以下命令:

(Get-ChildItem -Path c:\scratch\ | Get-Member).TypeName | Select-Object -Unique

在这里,我们可以看到我 Windows 机器上的C:\scratch目录包含了目录和文件,如下图所示:

图 3.3 – 检查目录中对象的类型

图 3.3 – 检查目录中对象的类型

我们在这里做了什么?看起来有点复杂。好吧,我们将Get-ChildItem C:\scratch cmdlet 的输出传递给Get-Member。我们只对TypeName属性感兴趣,所以我们把管道放在括号中,这样我们就可以轻松访问我们需要的那个属性。一旦得到所有TypeName实例的集合,我们就把它传递到第二个管道,传给Select-Object,并使用-unique参数告诉它只返回唯一的值。聪明吧?

在接下来的几个章节中,我们将探讨操作这些对象的基本方法。让我们从选择和排序对象开始。

选择和排序对象

我们运行的许多 cmdlet 会产生大量输出,而且其中很多内容可能并不有趣。因此,能够仅选择我们需要的部分并将其排序成有意义的顺序是非常有用的。为此,有两个 cmdlet:Select-ObjectSort-Object。我们通常会看到它们的别名——selectsort

使用 Select-Object

我们在 什么是对象? 部分中使用了 Select-Object 来选择集合中对象的唯一属性。然而,我们可以用它做更多的事情。通过运行以下命令,查看 Select-Object 的帮助文件:

Get-Help Select-Object

在这里,我们可以看到有四组参数集,它们的工作方式有两种——我们可以使用 cmdlet 操作集合的一个或多个属性,或者我们可以用它来选择集合中的一部分对象。让我们尝试第一种方法,输入以下内容:

Get-Process | Select-Object Name, Id

在这里,我们将看到一个包含两个属性的对象集合——NameId。现在,让我们在 Get-Member 中运行它,如下所示:

Get-Process | Select-Object Name, Id | Get-Member

在这里,我们可以看到我们已将那组 System.Diagnostics.Process 对象转变为 Selected.System.Diagnostics.Process 对象——这些对象只有两个属性——我们在 Select-Object cmdlet 中使用的 NameId 属性:

图 3.4 – 新对象的属性

图 3.4 – 新对象的属性

我们现在仍然有相同数量的对象,但它们只包含我们感兴趣的属性。这有两个好处;首先,PowerShell 在处理这些较小的对象时会更快,其次,PowerShell 将需要更少的内存。缺点是,我们不再能访问管道中原始对象的那些选择的属性。

我们可以使用 Select-Object 的第二种方式是从集合中选择一部分对象。实现这一点的参数在第一个参数集中;-first-last-skip。每个参数都需要一个整数作为参数。-first 5 会选择管道中的前五个对象,而 -last 2 会选择管道中的最后两个对象,如下所示:

图 3.5 – 选择对象的子集

图 3.5 – 选择对象的子集

我们可以使用 -skip 参数来跳过开始或结尾的值,如下所示:

1,2,3,4,5,6,7,8 | Select-Object -First 2 -skip 1

这将返回整数 23,它们是跳过第一个后得到的前两个元素。

活动 1

我们如何从这个数组中返回 23478

以这种方式运行Select-Object的问题在于,除非我们能够控制集合中对象的顺序,否则我们只是在随机抓取对象。这引出了下一个 cmdlet,Sort-Object

使用 Sort-Object 排序对象

当我们运行Get-Process时,进程会按进程名称的字母顺序返回。这是由 PowerShell 源代码决定的。然而,我们可以通过使用Sort-Object cmdlet 来更改对象的显示顺序(从而间接地重新排序管道中的对象)。

Sort-Object可以根据一个或多个属性对对象集合进行排序。我们不需要指定任何参数来运行它;如果我们没有指定排序的属性,它将根据管道中第一个对象的默认排序属性来对集合进行排序,该属性深藏在 PowerShell 的源代码中,并且很难找到。

这意味着什么?记住,Get-ChildItem会生成两种类型的输出。默认情况下,当你运行Get-ChildItem时,你会首先得到所有第一类型对象(System.IO.DirectoryInfo,即目录)的列表,然后是所有第二类型对象(System.IO.FileInfo,即文件)的列表,如以下截图中的第一个示例所示:

图 3.6 – 运行 Select-Object 没有参数的效果

图 3.6 – 运行 Select-Object 没有参数的效果

在第二个示例中,Get-ChildItem -Path C:\Scratch\ | Sort-Object,我们得到了一个按字母顺序排列并混合的所有对象列表;它忽略了对象类型。

我们可以添加一个属性名来根据该属性对我们的集合进行排序。例如,我们可以运行Get-Process并按工作集大小进行排序,像这样:

Get-Process | Sort-Object -Property ws

这很好。不过,它们已经按照默认的升序进行了排序,因此我们最感兴趣的进程——那些消耗内存最多的——排在了表格的底部。我们可以通过添加另一个参数-Descending来解决这个问题:

Get-Process | Sort-Object -Property WS -Descending

这会产生一个更有用的输出,如下所示:

图 3.7 – 使用 Sort-Object 按降序排序

图 3.7 – 使用 Sort-Object 按降序排序

我们甚至可以一次按多个属性进行排序。例如,我们可以尝试以下操作:

Get-Process | Sort-Object -Property SI, WS -Descending

这将按会话 IDSI)排序,然后按工作集WS)排序。

让我们来看一下帮助文件。在这里,我们可以看到Sort-Object有三个参数集,它们的工作方式大致相同;唯一的区别是-top-bottom-stable参数。-top-bottom参数比较容易理解,但-stable就不那么直观了。当我们运行Sort-Object时,如果存在相等值的对象,它会根据其内部逻辑输出对象的顺序,而不一定是它们被接收的顺序。-stable参数(以及-top-bottom)会在排序的属性相等时,保持Sort-Object接收对象的顺序。

我们现在可以看到如何将这两个 cmdlet,Sort-ObjectSelect-Object,结合起来生成有意义的信息集合。例如,我们可以键入以下内容:

Get-Process | Sort-Object CPU -Descending | Select-Object -First 5

这将获取我们当前 CPU 占用最多的五个进程,如下所示:

图 3.8 – 结合 Sort-Object 和 Select-Object

图 3.8 – 结合 Sort-Object 和 Select-Object

那么,如果我们不想要前五名呢?如果我们想要所有使用 大量 CPU 的进程呢?这就是过滤的作用。

过滤对象

我们可以使用 Where-Object cmdlet 以更复杂的方式过滤对象。Where-Object 也查看管道中对象的属性,但它还可以做出决定,输出哪些对象,丢弃哪些对象。尝试以下操作:

Get-Process | Where-Object -Property CPU -gt -Value 1

这将返回一个进程列表,其中 CPU 属性的值大于 1。在实际操作中,我们很少看到人们为参数包含 -Property-Value 名称,因为它们是位置参数。更常见的写法如下:

Get-Process | where CPU -gt 1

等一下,-gt 是什么?-gt 参数是一个 比较运算符,这是编程中的一个重要概念。

理解比较运算符

比较运算符在使用 Where-Object cmdlet 时作为开关参数表示,导致帮助文档成为一个冗长且复杂的文档,包含许多参数集,因为每次只能使用一个比较运算符。基本的比较运算符如下表所示:

比较 运算符 区分大小写的运算符
相等 -``eq -``ceq
不等式 -``ne -``cne
大于 -``gt -``cgt
小于 -``lt -``clt
大于或等于 -``ge -``cge
小于或等于 -``le -``cle
通配符相等 -``like -``clike

表 3.2 – 基本比较运算符

默认情况下,运算符是不区分大小写的,因此 -eq top-eq TOP 在功能上是相同的。也有一些 NOT 运算符可以获得相反的结果,例如 -NotLike。此外,我们还有一些更高级的比较运算符,如下所示:

  • 使用 -match 根据正则表达式获取值。

  • 使用 -in 获取属性值在指定数组中的对象。我们将在 第四章 中讨论数组,PowerShell 变量和 数据结构

  • 使用 -contains 获取对象,其中指定的值可能在包含数组的属性中,而不是单一值。

让我们探索这些操作的一些工作方式。尝试运行以下命令,以获取正在运行的 PowerShell 进程列表:

Get-Process | Where-Object ProcessName -eq pwsh
Get-Process | Where-Object ProcessName -like pwsh
Get-Process | Where-Object ProcessName -like *pwsh
Get-Process | Where-Object ProcessName -like *wsh
Get-Process | Where-Object ProcessName -contains pwsh
Get-Process | Where-Object ProcessName -in "pwsh", "bash"

最后一条能正常工作,是因为我们给 Where-Object 提供了一个包含两个项的数组,"pwsh""bash",并要求它返回任何 ProcessName 属性值在该数组中的对象。实际上,数组可能不会像这样是一个字符串列表,而是通过运行另一个 cmdlet 得到的更复杂的东西。

活动 2

为什么 Get-Process | Where-Object ProcessName -contains *wsh 没有输出任何结果?

这都很有意思,但如果我们想要查找更复杂的内容,比如对两个属性进行过滤,或者查找某个范围内的值,会发生什么呢?

理解 Where-Object 高级语法

到目前为止,我们一直在使用Where-Object与所谓的-FilterScript参数。这个参数允许我们将一个简短的脚本对象传递给 cmdlet,然后该 cmdlet 在管道中的每个项目上运行。

过滤器脚本有-and-or-not。让我们看看之前的一个例子,如何在高级语法中使用。我们在本节早些时候输入了以下内容:

Get-Process | Where-Object ProcessName -eq pwsh

这让我们得到了所有正在运行的pwsh进程的列表,使用的是基本语法。

使用高级语法编写相同命令的形式如下:

Get-Process | Where-Object -FilterScript {$PSItem.ProcessName -eq 'pwsh'}

过滤器脚本是被花括号包围的部分——也就是$PSItem.ProcessName -eq 'pwsh'。让我们来解析一下。-eq 'pwsh'对我们来说很熟悉,因为我们之前使用过,但$PSItem.ProcessName又是什么呢?这是一个结构,允许我们访问当前正在处理的对象的ProcessName属性。$PSItem是课本之外的一个$PSItem;这个变量通常写作$_美元符号下划线);例如,$_.ProcessName -eq 'pwsh'。在基本语法中,我们不需要对pwsh加引号,但在高级语法中,我们需要加引号,以便脚本知道我们传递的是一个字符串值,像这样:

图 3.9 – 使用 Where-Object 过滤的三种方式,其中一种是错误的

图 3.9 – 使用 Where-Object 过滤的三种方式,其中一种是错误的

没有引号时,cmdlet 会将pwsh解释为下一个 cmdlet。如果你仔细查看错误,你会发现它并没有走到那一步,因为-eq缺少了一个值。如果在这里使用单引号或双引号并没有太大关系,但最佳实践是使用单引号,除非你需要双引号的一些特殊功能,我们将在第四章中讨论,PowerShell 变量和 数据结构

高级语法中的多个过滤器

现在我们理解了语法,可以开始使用它来组合过滤器,以生成更复杂的结果。试试这个:

Get-Process | Where-Object -FilterScript {$PSItem.ProcessName -eq 'pwsh' -and $PSItem.CPU -gt 1}

这应该给你一个pwsh进程的列表,其中CPU值大于 1。现在,如果你将CPU值改为更高的数字,你应该会看到输出发生变化:

图 3.10 – 使用 Where-Object 高级语法组合过滤器

图 3.10 – 使用 Where-Object 高级语法组合过滤器

要注意,脚本块语法是严格的。除非我们小心且准确地输入,否则我们将得不到期望的结果。例如,假设我们输入以下内容:

Get-Process | Where-Object -FilterScript {$PSItem.ProcessName -eq 'pwsh' -and CPU -gt 25}

在这里,我们会得到一个错误,提示You must provide a value expression following the '-and' operator。因为我们能看到这个错误,我们可以通过将CPU替换为$PSItem.CPU来修正它。然而,假设我们只想要名为pwshbash的进程,并输入如下:

Get-Process | Where-Object -FilterScript {$PSItem.ProcessName -eq 'pwsh' -or 'bash'}

在这里,我们不会得到错误,只会得到错误的结果,如下图所示。正确的语法在下图的第二个示例中展示:

图 3.11 – 小心语法,尤金

图 3.11 – 小心语法,尤金

我们还可以使用高级语法来访问属性的属性。让我们运行以下命令:

Get-Process | Get-Member

在这里,我们可以看到ProcessName属性是一个字符串,因此它具有字符串对象的属性。这意味着我们可以像这样运行:

Get-Process | Where-Object -FilterScript {$_.ProcessName.Length -lt 5}

在这里,我们要查找所有机器上ProcessName少于5个字符的进程。我们还使用了更常见的$_来代替$PSItem。你必须习惯这个。

过滤优化

考虑以下两个 cmdlet:

Get-Process | Sort-Object -Property CPU -Descending | Where-Object CPU -gt 1
Get-Process | Where-Object CPU -gt 1 | Sort-Object -Property CPU -Descending

它们产生相同的结果(至少在我的机器上是这样)。然而,在我的客户端,第一种方法需要 29 毫秒,而第二种方法只需要 20 毫秒。你可以自己试试,使用Measure-Command cmdlet,像这样:

Measure-Command {Get-Process | Sort-Object -Property CPU -Descending | Where-Object CPU -gt 1}
Measure-Command {Get-Process | Where-Object CPU -gt 1 | Sort-Object -Property CPU -Descending}

有时候,由于它们都是非常短的管道,你可能会得到一个意外的结果,但如果你连续运行它们 10 次,第二个 cmdlet 几乎每次都会在某些方面比第一个更快。这种变化是由于在你的客户端上有其他程序在运行,它们与 PowerShell 争夺资源。

本章前面我们讨论了如何减少 PowerShell 生成结果所需的处理和内存量。过滤优化是实现这一点的好方法。我们应该尽早在管道中进行过滤,以减少 PowerShell 需要处理的对象数量。这里有一个基本规则:左侧过滤

我们不仅仅有Where-Object cmdlet 用于过滤。许多 cmdlet 也具有过滤参数,这些参数要么是显式的,参数名为-Filter,要么是执行常见过滤任务的参数。例如,Get-ChildItem cmdlet 有-File-Directory参数,可以将输出限制为这两种对象类型中的一种。尽可能使用 cmdlet 的内建参数来过滤对象,再将它们传递到管道中进行进一步处理。

活动 3

如何找到具有-Filter参数的 cmdlet 列表?

现在,我们已经相当了解如何将管道中的对象限制为我们感兴趣的对象。接下来,我们将看看如何对这些对象执行操作。

枚举对象

我们经常需要对正在处理的对象执行某些操作。大多数时候,会有相应的 cmdlet 来执行这个操作,但有时也没有。例如,假设我们想输出文件夹中某些项的文件名和路径。没有一个便捷的属性可以仅输出文件名和路径;有像 pspath 这样的属性,可以获取我们想要的内容,但会包含一些额外信息,并没有完全符合我们需求的属性。然而,对于 Get-ChildItem 返回的对象,有一个方法可以实现这一点:tostring()。我们可以通过枚举每个项来执行这个方法,如下所示:

Get-ChildItem myfiles | Foreach-Object -MemberName tostring

这将产生我想要的完全输出,如下所示:

图 3.12 – 基本枚举

图 3.12 – 基本枚举

这是一个非常简单的示例。就像 Where-Object 一样,Foreach-Object 有基本语法和高级语法,高级语法与我们在前面部分看到的非常相似。你必须将一个脚本块提供给 ForEach-Object-Process 参数。要使用高级语法运行最后一个 cmdlet,我们需要输入以下内容:

Get-ChildItem myfiles | ForEach-Object -Process {$_.tostring()}

如下图所示,输出是相同的。请注意,当使用脚本块时,方法名 tostring 后面必须跟着一对括号:

图 3.13 – 高级枚举

图 3.13 – 高级枚举

如果方法需要参数,那么我们需要将它们放在括号内,并以逗号分隔,如下所示:

('Powerhell').Insert(5, 'S')

这将通过在原始字符串的第 5 个位置插入 'S' 字符串来修正拼写。我们不再像以前那样经常看到交互式枚举,因为通常,cmdlet 会被编写来执行我们可能想要交互式枚举的大多数操作。然而,这在脚本编写中是一个重要的概念,我们将在 第五章,“PowerShell 控制流 – 条件语句与循环”中看到。不过,这里有一个我们可以使用它的有用技巧 —— 重复执行某个过程指定次数。试试看:

1..10 | Foreach-Object {Get-Random}

所以,在管道的第一部分,我们使用范围操作符(..)创建一个包含从 1 到 10 的 10 个整数的数组。在第二个 cmdlet 中,我们没有使用 $PSItem 管道变量 —— 我们只是指示它对管道中的每个项运行一次。正如你所看到的,我们不仅可以在脚本块中放入对象方法;我们也可以将 cmdlet 和脚本放入其中。

并行枚举

枚举的一个问题是,当对象数量很多,或者过程很复杂时,它可能需要很长时间。在 PowerShell 7 中,我们获得了并行运行 ForEach-Object 进程的能力。尝试运行以下代码,它会输出从 1 到 10 的数字:

1..10 | ForEach-Object {
$_
Start-sleep 1}

当你在每行后按 Enter 时,应该会看到一个继续提示,直到你关闭大括号。慢吧?打印 10 个数字要 10 秒钟。现在,让我们尝试使用并行处理:

1..10 | ForEach-Object -Parallel {
$_
Start-Sleep 1}

现在,你应该看到数字按每五个一组打印出来。我们可以通过-ThrottleLimit参数来改变并行处理的数量。

现在,我们已经探索了一些用于操作管道的有用 cmdlet,并且体验了第一次脚本编写(是的,这就是你刚才做的),接下来我们将看看管道是如何工作的。

管道如何工作 – 参数绑定

PowerShell cmdlet 输出与其他通用 shell 的主要区别在于,它的输出不是类似文件的内容,而是一个对象,具有类型、属性和方法。那么,一个 cmdlet 生成的对象是如何传递给另一个 cmdlet 的呢?

Cmdlet 只能通过它们的参数接受输入。没有其他方式,因此输出对象必须通过管道传递给下一个 cmdlet 的参数。考虑以下 cmdlet 管道:

Get-Process | Sort-Object -Property CPU

这里我们只看到了一个参数,-property,它被赋予了CPU的值。那么,发生了什么呢?Sort-Object被赋予了两个参数,但我们看不见其中一个。这就是所谓的管道 参数绑定

PowerShell 将第一个 cmdlet Get-Process的输出传递到第二个 cmdlet,并且必须对其进行处理,因此它会查找第二个 cmdlet 中可以接受 PowerShell 当前持有的对象的参数。这可以通过两种方式发生:ByValueByPropertyName。让我们详细看看这两种方式。

ByValue是默认方法,PowerShell 总是首先尝试这个方法,所以我们从这个开始。

理解ByValue参数绑定

我们可以通过输入以下命令查看Sort-Object的帮助文件:

Get-Help Sort-Object -Full

看一下参数。你会看到,只有一个参数可以接受来自管道的对象:-InputObject。帮助文件对它有如下描述:

-InputObject <System.Management.Automation.PSObject>
        To sort objects, send them down the pipeline to `Sort-Object`. If you use the InputObject parameter to submit a collection of items, `Sort-Object` receives one object that represents the collection. Because one object cannot be sorted, `Sort-Object` returns the entire collection unchanged.
        Required?                    false
        Position?                    named
        Default value                None
        Accept pipeline input?       True (ByValue)
        Accept wildcard characters?  false

这里,我们可以看到它只接受ByValue输入,并且只接受PSObject类型的输入。PSObject是非常宽泛的,它意味着 PowerShell 中的任何对象。所以,我们可以用它来排序一个数字数组,因为它们是System.Int32类型的对象,如下图所示。注意,正如帮助文件中所描述的那样,我们不能直接将数组传递给-InputObject参数;它必须通过管道。如果我们尝试通过参数显式传递数组,它会将整个数组作为单个对象返回,并且不会排序。我们需要让它通过管道,一次传递一个项:

图 3.14 – 正确与错误使用参数

图 3.14 – 正确与错误使用-InputObject参数

让我们再看一个。我们可以从Get-ChildItem的帮助文件中看到,它有一个参数-path,可以接受ByValue管道输入,并且接受字符串对象。这意味着我们可以做类似这样的事情,把myfiles字符串放入管道中:

'myfiles' | Get-ChildItem

在这里,我们将得到一个有意义的输出——myfiles 目录中所有项目的列表。如果我们有一个输出路径作为字符串的 cmdlet 管道,我们可以将其传递给 Get-ChildItem 来获取内容。使用 ByValue 时需要记住的重要一点是,你传递到管道中的对象类型必须与下一个接受管道输入的 cmdlet 的参数所要求的对象类型匹配。

Get-ChildItem 很有意思,因为接受管道输入的参数不是 -InputObject 参数,而是 -path。如果你尝试将一个字符串传递给 Get-ChildItem,同时又显式指定了 -path 参数,会发生什么?你将会得到一个错误,如下所示:

图 3.15 – 管道破坏

图 3.15 – 管道破坏

前面的错误表示没有参数可以接受管道输入,尽管我们知道有这个参数。这是因为我们在开始处理管道中的对象之前,将一个值绑定到 Get-ChildItem,从而有效地将该参数从可用的参数列表中移除。如果我们看到这个错误,通常需要检查是否已经使用过该参数,而不是在沮丧中将笔记本电脑扔向墙壁。

让我们来看一下另一种将管道内容绑定到参数的方法:ByPropertyName

ByPropertyName 参数绑定

PowerShell 会首先尝试按 ByValue 绑定参数。如果 ByValue 不可用,它才会尝试使用 ByPropertyName 来强行绑定管道对象。如果你的第一个 cmdlet 生成的对象类型不适合下一个 cmdlet 的 pipeline-accepting 参数会发生什么?PowerShell 会查看第二个 cmdlet 是否有接受管道输入的参数,并且该参数的属性名与对象匹配——通常是 -Id-Name

不出所料,Stop-Process 是一个停止进程的 cmdlet。如果我们查看帮助文件,我们会看到三个参数接受管道输入:-InputObject,它接受 ByValue 对象,以及 -Id-Name,它们接受 ByPropertyName。现在,让我们输入以下内容:

Get-Random | Stop-Process

在这里,我们会遇到一个错误——与图 3.15中显示的错误相同。我们知道 Stop-Process 有三个接受管道输入的参数,因此这不是第一个原因。我们也没有显式地将任何内容绑定到参数上,因此错误一定是因为管道中的对象类型不正确。如果我们使用 Get-Member 来确定 Get-Random 生成的对象类型,然后查阅 Stop-Process 的帮助文件,我们会发现 Get-Random 生成的是 System.Int32 类型的对象,而 Stop-Process 需要的是 System.Diagnostics.Process 类型的对象。所以,如果管道中没有正确的对象类型,为什么 PowerShell 没有尝试使用 ByPropertyName 呢?其实它是尝试过的,但 Get-Random 输出的对象的属性名称并没有与 Stop-Process 中的 -Id-Name 参数匹配。让我们来玩点有趣的。输入以下内容:

New-Object -TypeName PSObject -Property @{'Id' = (Get-Random)} | Stop-Process -WhatIf

我们在这里做什么?我们使用 New-Object cmdlet 创建一个通用的 PowerShell 对象(-TypeName PSObject),并添加一个属性 Id,该属性的值由运行 Get-Random cmdlet 生成的随机数填充。如果我们将该 cmdlet 的输出传递给 Get-Member,就可以看到这个属性:

图 3.16 – 创建自定义对象

图 3.16 – 创建自定义对象

一旦我们创建了这个新的自定义对象,我们就可以将它传递给 Stop-Process。由于对象类型不正确,它不能绑定到 -InputObject 参数,但该对象有一个与 -Id 参数匹配的属性名称,因此它会绑定到这个参数。最后,因为我们不想玩得太过火,所以我们使用了 -WhatIf 参数,以防 Get-Random 给我们提供一个有效的进程 ID。-WhatIf 是大多数 PowerShell cmdlet 中常见的参数之一,它告诉我们如果运行该 cmdlet 而不实际更改任何内容,会发生什么。

括号命令

几次之前,我们已经运行了括号中的 cmdlet,就像我们之前所做的那样。括号是一种覆盖 PowerShell 执行顺序的方式。就像数学中一样,括号是一个指令,表示优先执行。当我们在 PowerShell 中使用它们时,括号中的内容必须在该管道段的其他任何内容之前完成处理。这为我们提供了另一种直接将输入传递给参数的方式。

在前面的示例中,我们尝试运行以下内容:

Get-Random | Stop-Process

这并没有成功。尽管对象类型(System.Int32)对于 -Id 参数来说是正确的,但 PropertyName 值是错误的。使用括号时,我们可以显式地将内容传递给 -Id 参数,像这样:

Stop-Process -Id (Get-Random)

首先,PowerShell 会生成一个随机数,然后将其传递给 -Id 参数。我们将在本书中看到更多括号的有用示例。

故障排除管道 – 管道追踪

在这一章中,我们已经做了很多工作,现在是时候放松一下,和Trace-Command玩得开心了。至少,我觉得这很有趣;其他意见另当别论。然而,这个 cmdlet 确实让我们深入了解 PowerShell 的工作原理,从而能够看到它的实际操作。

运行以下代码:

Trace-Command -Name ParameterBinding -Expression {New-Object -TypeName PSObject -Property @{'Id' = (Get-Random)} | Stop-Process -WhatIf} -PSHost

在这里,我们运行 Trace-Command 并请求它记录 ParameterBinding 事件。我们将之前运行的 cmdlet 作为脚本块中的表达式传给它,然后通过 -PSHost,我们告诉它将输出显示到屏幕,而不是其默认的调试流,正如我们在本章一开始谈论流时所看到的。

现在,我们屏幕上满是黄色文字,显得很乱;我们需要仔细看看其中的内容。我们感兴趣的问题是:

  • 自定义对象绑定到哪里了?

  • 自定义对象是如何绑定的?

这是我的输出,整理后的版本,每一行下方有注释:

  1. DEBUG: 绑定管道对象到 参数:[Stop-Process]

在这一行中,我们开始绑定到 Stop-Process 的参数。

  1. DEBUG: 管道对象类型 = [``System.Management.Automation.PSCustomObject]

这告诉我们管道中的对象类型是什么。

  1. DEBUG: 参数 [InputObject] 管道输入 ValueFromPipeline NO COERCION

这告诉我们,-InputObject 只接受 ByValue 对象

  1. DEBUG: 绑定参数 [@{Id=1241688337}] 到 参数 [InputObject]

arg [1241688337] 是生成的随机数。

  1. DEBUG: 绑定集合参数 InputObject:参数类型 [PSObject],参数类型 [System.Diagnostics.Process[]],集合类型 Array,元素类型 [System.Diagnostics.Process], 无强制元素类型

这显示我们对象类型不匹配。

  1. DEBUG: 绑定参数 [@{Id=1241688337}] 到参数 [``InputObject] 被跳过

在这里,我们跳过了对 -InputObject 的绑定。

  1. DEBUG: 参数 [Id] 管道输入 ValueFromPipelineByPropertyName NO COERCION

这一行显示 -Id 参数接受 ByPropertyName 作为输入。

  1. DEBUG: 绑定参数 [1241688337] 到 参数 [Id]

DEBUG: 绑定集合参数 Id:参数类型 [Int32],参数类型 [System.Int32[]],集合类型 Array,元素类型 [System.Int32], 无强制元素类型

这显示我们对象类型匹配。

  1. DEBUG: 绑定参数 [System.Int32[]] 到参数 [``Id] 成功

在这里,我们得知我们已经成功绑定到 –``Id 参数。

所以,我们得到了问题的答案——管道中的对象绑定到了 -Id 参数,ByPropertyName

这只是对 Trace-Command 的一个快速介绍。如果你的管道失败了,且你确信对象类型匹配,或者你确保了属性名称匹配,并且没有明确绑定到唯一接受管道输入的参数,那么这个 cmdlet 是你了解发生了什么的最佳希望。

总结

在本章中,我们讨论了一些非常有趣且相当技术性的主题。我们首先描述了管道的作用,然后探讨了选择和排序对象的技巧。接着,我们讨论了过滤对象,并谈到了使用过滤功能的重要性,以便让 PowerShell 能够高效地工作。

从这里开始,我们引入了一个之后会很重要的主题:枚举,并探讨了 PowerShell 7 的一项新特性——并行枚举。在本章的最后部分,我们深入了解了管道如何实现其魔力,并研究了两种参数绑定方法:ByValueByPropertyName。最后,我们玩了一下一个可以让我们深入了解管道工作原理的 cmdlet:Trace-Command

大多数时候,管道只需正常工作。然而,对于那些不工作的情况,本章为我们提供了必要的知识,帮助我们理解管道的工作原理,并且希望能帮助我们修复问题。有时候,cmdlet 作者没有为其 cmdlet 提供接受管道输入的方式。本章向我们展示了如何发现这一点,并给我们提供了一种解决方法。

本章结束了本书关于 PowerShell 工作机制的介绍部分。在下一章,我们将开始编写一些代码,重点讲解变量和数据结构。请继续关注。

练习

以下是一些练习,帮助你巩固本章的知识:

  1. 我们如何使用 PowerShell 仅显示今天是星期几?

  2. 我们需要获取 CPU 使用情况和所有正在运行的进程的路径位置,并且我们不想要过多的无关信息。我们该如何操作?

  3. 现在我们有了列表,如何按字母顺序反向排列路径名?

  4. 这里有很多内容。我们如何确保它只列出 CPU 使用率大于 5 的进程?

  5. 获取我们主驱动器中只读文件的最有效方法是什么?

  6. 我们需要获取我们主目录下所有文件的大小。我们只需要文件名和字节大小。

  7. 我们有一个包含进程名称列表的文件,名为 processes.txt。我们需要使用它来发现本地计算机上进程的信息,因此我们需要找到一个可以从文件中获取内容的 cmdlet。

  8. 如果不实际运行 cmdlet,在 Windows 主机上如果我们在没有 -WhatIf 的情况下运行它会发生什么?

    'bobscomputer' | Stop-Computer -WhatIf
    

如果它不正确,那么正确的 cmdlet 应该是什么?

如果我们在 Linux 主机上运行它,会发生什么?提示:思考一下这个问题。不要尝试它,特别是在没有 -WhatIf 的情况下。

进一步阅读

随着我年龄的增长,我发现计算机历史变得越来越迷人;在我二十多岁时被认为是前沿的概念和设备,如今已经成为布满灰尘的旧遗物。然而,这些旧遗物在某种程度上帮助解释了我们现在所处的位置。Unix 口述历史项目中有一部分内容讲解了管道的概念:dspinellis.github.io/oral-history-of-unix/frs122/unixhist/finalhis.htm

第四章:PowerShell 变量和数据结构

是时候真正理解我们谈论变量时的含义了。变量是计算机科学和数学中的常见概念,因此,理解它们是什么以及它们如何在 PowerShell 中使用非常重要。

我们将从探索变量的字面意义和隐喻意义开始。我们将研究如何在 PowerShell 中使用它们,并将 PowerShell 的工作方式与其他语言进行对比。我们将探索基本数据类型的概念,它们是数据的基本构建块,然后再讨论 PowerShell 使用的常见数据结构。最后,我们将通过解包这一重要且有用的技术来简化 cmdlet,来增加一些乐趣。

在本章中,我们将涵盖以下主要主题。

  • 理解 PowerShell 变量

  • 探索对象类型

  • 发现值类型

  • 输入解释

  • 导航引用类型

  • 解包 – 用哈希表来获取乐趣和利润

理解 PowerShell 变量

变量就像一个盒子。我们可以把东西放进去。我们可以把这些东西拿出来,再放进去别的东西。这个盒子可能包含一件东西,或者可能包含很多东西。它可以包含同一种类型的多个东西,例如,30 双袜子,或者它可以像我的厨房抽屉一样,包含各种不同的东西,包括线。我们可以以不同的方式组织它包含的东西,或者像我的厨房抽屉一样不组织它们。它可能什么也不包含。

变量实际上是内存区域的标签。它仅仅是一个名称和内存中的地址。当你告诉 PowerShell 使用变量的内容时,你是在告诉它去那个内存区域并使用那里的内容。使用标签给我们带来两个优势;首先,它节省了大量输入,尤其是当变量包含多个对象时。其次,它允许我们赋予变量含义,这样我们和其他读代码的人就能明白变量的目的,并且有一个线索了解它可能包含的内容。这比它现在看起来的要重要和有用得多。

图 4.1 – 这不是一个变量。向马格里特致歉

图 4.1 – 这不是一个变量。向马格里特致歉

PowerShell 的设计目的是易于使用,因此变量可以动态创建,这与一些语言(如 Java)不同,在 Java 中我们必须先声明变量,然后才能给它赋值。我的意思是什么?考虑这行代码;不用打出来,只需思考一下:

$MyVariable = 'some stuff'

我在那里做什么?我正在动态创建一个包含some stuff的变量。

如果我要以更像 Java 风格的方式做这件事,我会这样写:

New-Variable -Name MyVariable
Set-Variable -Name MyVariable -Value 'some stuff'

这将创建一个变量,给它一个名字,然后我们将一个值放入其中。在实际操作中,这种情况非常罕见。大多数人在大多数情况下,都是动态创建变量的。

如果你仔细阅读这些代码行,你会看到第一个示例中包含了$MyVariable,而第二个示例中则是MyVariable,没有$符号。让我们来讨论一下为什么。

变量不是它们的内容

我们很少需要操作一个变量。回到盒子比喻,除非我们五岁,否则通常更感兴趣的是盒子里的内容,而不是盒子本身。MyVariable是分配给变量的名称,是我们用来引用它的标签。$MyVariable则指的是变量的内容。这就是我们感兴趣的东西。让我们来演示一下。

图 4.2 – MyVariable 不是 $MyVariable

图 4.2 – MyVariable 不是 $MyVariable

图 4.2的第一行,我们动态地创建了一个名为MyVariable的变量,并将some stuff放入其中:

$MyVariable = "some stuff"

在第二行,我们通过输入$MyVariable来请求 PowerShell 获取MyVariable的内容,结果返回了some stuff

在第三行,我们只输入了变量的名称MyVariable,但是 PowerShell 并没有理解我们想要什么。

在第四行,我们显式地通过Get-Variable命令请求MyVariable,再次得到了some stuff字符串,但我们还得到了其他一些不属于字符串的内容;Get-Variable命令返回了一个PSVariable对象,而不是变量的内容,即some stuff字符串。我们可以使用Get-Member来确认它是什么类型的变量。输入以下内容:

Get-Variable MyVariable | Get-Member

在第五行,PowerShell 很合理地告诉我们并没有一个名为some stuff的变量——我们这里传递的是MyVariable的内容,而不是变量的名称。

在第六行,看起来我们像是将一个变量传递给Write-Output,但实际上并不是。我们传递的是一个值为MyVariable的字符串,而不是MyVariable的内容。

第七行正确地将MyVariable的内容传递给Write-Output,通过引用$MyVariable

实际上,如果我们总是以$符号开头命名变量,几乎总是会是正确的。这就引出了名字的问题:什么是好的变量名,什么是不好的变量名?

变量命名

在命名变量时,有两个因素需要考虑。首先是我们可以使用的名称,其次是我们应该使用的名称。

我们可以(和不能)使用的变量名

变量名可以包含字母、数字、问号和下划线符号_的任意组合。它们不区分大小写。如果我们将变量名放入花括号{}中,还可以使用其他字符,但我们并不这么做。真的——这样会让生活变得更复杂,而且几乎没有必要。下面是一些好坏变量名的例子:

合法的 变量名 非法的 变量名
Z54 z54!
ComputerName `
--- ---
Z54 z54!
ComputerName
Computer_Name Computer-Name
{``Computer Name} Computer Name
ComputerName? {``Computer Name{}}

表 4.1 – 我们可以使用和不能使用的名字

现在我们知道了可以使用什么样的变量名,那么我们应该使用什么样的名字呢?

我们应该(和不应该)使用的名字

使用变量的一个目的就是赋予其意义。我们通过使变量名具有意义来实现这一点;一个变量名应该给出一些提示,说明变量的内容或用途是什么。虽然MyVariable在我们输入一两行代码时是一个完全合法且合适的名字,但当我们编写脚本时,它并没有提供任何关于它包含的内容或我们希望如何使用它的线索;它只告诉我们它是一个变量,并且它是我的

我的日常工作常常涉及调试别人写的代码。我曾经做过因为脚本中有 20 个或 30 个变量,名字叫aIxyiix3agtd等而做噩梦的事情。我根本不知道它们是什么意思,而且我敢打赌,原作者也不记得了。在命名变量时,我们应该明白,我们的代码将比编写它时被读取的次数要多得多,通常是我们自己读取,有时甚至是在几年后。为了自己着想,给变量起个有意义的名字吧。

我们还应该使用一致的命名规则,例如ComputerName1ComputerName2ComputerName3,而不是ComputerName1computer_name2ComputerNameThree。PowerShell 最佳实践指南建议广泛使用 Pascal 命名法,其中每个单词的首字母都大写,因为这种方式易于阅读,并且与屏幕阅读器兼容。其他语言,如 Python,建议变量名应全部小写,并且单词之间使用下划线分隔:computer_name。无论我们选择哪种方式,保持一致性会使我们的工作更加轻松。

我们也不使用问号。这会显得凌乱,而且可能会导致一些复杂的问题。

最后,我们应该避免尝试使用自动变量或偏好变量的名称。等一下,这是什么?

三种常见的 PowerShell 变量类型

到目前为止,我们一直在讨论一种特定类型的 PowerShell 变量——用户创建的变量。还有两种其他类型:自动变量偏好变量。用户创建的变量只在生成它们的会话或脚本运行时存在;一旦我们关闭 PowerShell 窗口,这些变量就会被销毁。这被称为作用域,我们将在第八章《编写我们的第一个脚本——将简单的 Cmdlet 转化为可重用代码》中详细介绍。自动变量和偏好变量则会在每个会话或脚本中存在。

自动变量

这些是 PowerShell 内部使用的变量。我们已经使用过一个:$PSItem,或$_,它指的是管道中的当前对象。如果我们愿意的话,可以给它赋值,但 PowerShell 会在当前运行的管道完成时清除它,如图 4.3所示:

图 4.3 – 不要使用自动变量的名称

图 4.3 – 不要使用自动变量的名称

在将$PSItem设置为"some stuff"之后,管道完成,变量被清除,丢失了我们试图存储的信息。幸运的是,大多数自动变量是受保护的,如图 4.4所示:

图 4.4 – 你不能覆盖一些自动变量

图 4.4 – 你不能覆盖一些自动变量

你可以在帮助主题about_Automatic_Variables中查看自动变量的列表,或者可以在网上查看它们:docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables

偏好变量

偏好变量存储关于 PowerShell 如何工作的相关信息,使我们能够自定义 PowerShell 的行为。例如,我们可以使用一个偏好变量来确保默认情况下所有我们运行的(支持的)cmdlet 都应用-WhatIf参数。让我们看看它是如何在图 4.5中工作的:

图 4.5 – 使用偏好变量

图 4.5 – 使用偏好变量

在第一行中,我们检查了WhatIfPreference变量的值;它是False,默认值。这是合理的,因为我们刚刚开始了 PowerShell 会话。在第二行中,我们通过设置$whatIfPreference = $``true来赋值True

然后我们再次检查其值。果然,现在它是True。让我们看看现在的情况如何。我们运行Get-Process来获取一个合适进程的Id参数;在这个例子中,我将获取pwsh进程的 ID,407。现在,当我们运行Stop-Process -Id 407时,通常会期待pwsh会话结束;但它没有,因为默认情况下,现在所有进程都以-WhatIf参数设置为True运行。

通过这种方式更改偏好变量仅在当前会话或脚本运行时有效。如果你停止并重新启动 PowerShell,它们会恢复为默认值。如果你需要持久化一个偏好,你可以更改你的 PowerShell 配置文件,这是一个 PowerShell 脚本,可以通过查询PROFILE变量并键入$PROFILE来找到。你将学习如何编辑脚本,在第八章,“编写我们的第一个脚本——将简单的 Cmdlet 转换为可重用代码”中。

关于每个偏好变量的完整解释可以在帮助主题About_Preference_Variables中找到,或者你可以在线阅读:docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables

最后,我们可以通过键入Get-Variable(不带参数)来查看当前所有变量中的值。

现在我们了解了有关变量(框)的基本情况,我们可以开始看看可以放入其中的内容。让我们来看一下对象类型。

探索对象类型

在上一节中,我们讨论了变量的类型。现在,我们要讨论对象类型——即放入盒子里的东西。变量中对象的类型告诉计算机如何处理它——它有什么属性,以及我们可以对它做什么。输入以下内容:

$MyVariable = "some stuff"
$MyVariable | Get-Member

我们应该能看到类似于图 4.6中的输出:

图 4.6 – 它是一个字符串

图 4.6 – 它是一个字符串

我们已经把一个字符串放进去,我们知道这一点,因为它被告诉了我们——TypeNameSystem.String。此时,MyVariable包含一个字符串。我们可以通过赋予其他值来改变其类型。试着输入以下内容,不加引号:

$MyVariable = 4.2

然后,使用Get-Member检查内容。现在我们得到的是一个System.Double对象类型,表示一个浮动点数。

我们可以做更好的事情。输入以下内容,并使用Get-Member检查变量的内容:

$MyVariable = (Get-Process)
$MyVariable | Get-Member

现在,我们已经将Get-Process的输出保存在我们的变量中,并且内容的类型是System.Diagnostics.Process

变量可以保存任何类型的对象。记住,PowerShell 中的一切都是对象。我们在第三章《PowerShell 管道——如何将 Cmdlet 连接起来》中介绍了一些 PowerShell 对象的细节,但从更通用的编程角度回顾它们是个好主意。

什么是对象?——redux

想象一辆自行车。它是什么颜色的?有什么样的车把?轮径是多少?我的自行车有猿式车把,15 英寸的轮子,表面有划痕的红色闪光漆。你的自行车可能是一个更实用的物品,配有升降把手、20 英寸的轮子,颜色是可用的黑色。我的儿子汤姆的自行车有追逐把手、22 英寸的轮子,颜色是白色。

那里有三个对象,属于(不存在的)TypeName Imaginary.Bike类型。我们可以在表 4.2中列出这些对象。所有这些对象都有相同的一组属性:名称、车把、轮径(英寸)和颜色属性。在每个实例中,它们的属性值不同。其他属性可能包括篮子、车灯等等。有些属性可能是可选的,但有些不是;自行车总会有两个固定直径的轮子和一个颜色。

名称 我的自行车 你的自行车 汤姆的自行车
车把 猿式把手 升降把手 追逐把手
轮径(英寸) 15 20 22
颜色 红色 黑色 白色

表 4.2 – 我们的自行车

我们可以在 PowerShell 中创建这些自行车,列出每辆车的属性,并且我们可以通过使用Get-Member看到它们是Imaginary.Bike类型的对象:

图 4.7 – 三辆假想自行车

图 4.7 – 三辆假想自行车

我们的自行车也有一组共同的操作方式。我们可以踩踏板来加速它们。我们可以使用刹车来减速它们。这些在第一章中的 Get-Member 是 PowerShell 7 的简介——它是什么及如何获取它。我们期望同一类型的对象具有相同的方法。

这个对象的概念适用于大多数现代编程语言,并且大多数编程语言以类似的方式处理它们。我们刚刚进行的想象练习在 Python 中和在 PowerShell 中同样适用。以下是 Python 中同样的三辆想象中的自行车:

图 4.8 – 另一种语言中的三辆想象中的自行车

图 4.8 – 另一种语言中的三辆想象中的自行车

在 PowerShell 中的区别在于一切都是对象。考虑到这一点,让我们探索一些我们希望放入变量中的常见对象类型。让我们从值类型开始。

发现值类型

在 .NET 中的 System.Object 类。总体而言,PowerShell 中的值类型类似于(但并非完全相同于)原始类型。保存值类型对象的内存位置保存实际数据。还有引用类型,我们稍后会看到,它们保存对实际数据的引用;数据保存在其他地方,可能在多个位置。值类型数据是固定大小的,以位为单位。值类型具有单个值。为了说明,让我们看一些值类型。我们将从最简单的值类型,布尔类型,开始。

内存位置——计算机科学 101 提示!

数据存储在内存中的两个不同位置:。栈用于静态分配(不变的事物),而堆则是动态的。堆存储全局信息,其结构就像它的名字一样;这只是一个数据堆,具有树状结构。堆的大小不固定,允许随机访问。对数据的访问可能非常慢,并且随着时间推移,堆会变得分散。

栈是一种有序的固定大小的内存空间。它具有线性结构,最后放入栈的数据位于首位,被移除时也是首先移除的。与堆相比,信息访问要快得多。然而,由于空间有限,我们偶尔会遇到栈溢出,即尝试将更多数据放入栈中而它无法容纳的情况——这相当于计算机科学中的交叉流(《捉鬼敢死队》的参考)。每个运行中应用的线程都有自己的栈,但它们共享应用堆。

当我们创建一个变量时,它被放入栈中。变量引用的数据可能在栈中(对于值类型变量)或者在堆中(对于引用类型变量),但变量本身总是在栈中。

布尔值

布尔类型的变量只能保存两个值之一:true 或 false。就是这么简单。它被称为System.Boolean[bool][bool]是一个简写,避免我们输入更长的类名;它是System.Boolean类型的别名。布尔值非常容易赋值,我们可以使用自动变量$True$False。试试下面的操作:

$MyBoolean = $True
$MyBoolean | Get-Member

我们可以看到类型是System.Boolean。这种类型的变量在脚本中非常有用,正如我们在第五章中将看到的,PowerShell 控制流 – 条件语句和循环。在那一章中,我们还会看到一个相似但略有不同的值类型——[switch]类型——它也只能是 true 或 false,但具有不同的成员集合。

整数

整数是没有小数点的整数。常见的三种整数类型为[int32][int64][byte][int32]是默认类型,它是一个有符号的 32 位数值,表示它可以存储介于-2,147,483,648 和+2,147,483,647 之间的整数。如果我们创建一个变量来存储这些值范围内的整数,那么 PowerShell 会为其分配[int32]类型:

$MyNumber = 42
$MyNumber | Get-Member

如果我们尝试赋值超出该范围的数字,PowerShell 会将其类型设为[int64],它是 64 位长,可以存储介于-9,223,372,036,854,775,808 和+9,223,372,036,854,775,807 之间的数字。

我们有时会在书籍和网页上看到[int][long],而不是[int32][int64]——这两个术语可以互换使用。

[byte]是一种特殊的整数类型;它没有符号,并且是一个介于 0 和 255 之间的整数,长度为 8 位——即 1 字节。

活动一

为什么不使用单一的整数类型[int64]呢?为什么要搞得这么复杂?

实数

在数学中,实数是存在的数字——它们可以是整数(42)、小数(3.1)、分数(1/3)或无理数(π、√2)。例如,-1 的平方根这样的虚数并不是实数,尽管它们在很多方面非常有用。在 PowerShell 中,整数使用我们刚刚讨论的整数类型表示,而常见的无理数则通过[math]类型表示。我们大多数时候使用实数类型来表示浮点数。

有三种常见的变量类型用于处理实数:[single](或[float])、[double](或[long])和[decimal]。[single]类型是 32 位,精度为 7 位数字(即小数点右侧的数字)。[double]类型是 64 位,精度为 15 位或 16 位数字。默认情况下,当你使用小数时,PowerShell 会创建[double]类型的变量:

$MyRealNumber = 42.0
$MyRealNumber | Get-Member

[Decimal]类型是 128 位长,精度可以达到 28 位数字。这在科学和金融计算中使用。

字符

[char] 类型描述了一个单一字符,是我们当前使用的 UTF-16 字符集中的一个成员。它是一个 16 位的值,对应于当前字符映射中的一个符号。试试以下操作:

[char]$MyChar = 24
$MyChar

在我的机器上,我看到一个上箭头 。你可能看到不同的东西。但你不会看到的是 24。我们做了一些新的尝试;我们在变量前面加了一个类型加速器。为了理解为什么,我们需要讨论类型。

类型说明

现在我们理解了一些基本类型,接下来需要讨论的是类型声明以及语言,特别是 PowerShell,如何操作变量中对象的类型。计算机是如何知道一个对象的类型的呢?编程语言可以分为两类:一种是只支持 静态类型,其中变量能容纳的对象类型在变量创建时就已声明,并且不会改变;另一种是支持 动态类型,其中变量的内容决定了它的类型。在动态类型语言中,变量的类型是可以改变的。试试以下操作:

$MyVariable = 'some stuff'
$MyVariable | Get-Member
$MyVariable = 42
$MyVariable | Get-Member

我们可以看到,$MyVariable 的对象类型会根据其中的内容变化,这使得 PowerShell 成为一种动态类型语言。事实上,Bruce Payette 在《PowerShell in Action》中将 PowerShell 描述为 类型随意,因为它会尽力让我们放入变量中的任何东西都能按照我们想要的方式运作。只要变量中的对象具有正确的属性,PowerShell 就不关心它的类型是什么。我们来试试。

动态类型与静态类型

如果我们输入 $x = '4',我们得到 [string]。如果我们输入 $x = 4,没有引号,我们得到 [int32]

现在,让我们看看如果输入以下内容会发生什么:

$x = '4' + 4

试试看,并将 $x 传输给 Get-Member 来确认类型。PowerShell 会尽力解释你想要的内容。它选择将第二个 4 作为字符串处理,并将两个字符串拼接在一起,结果是 44。惊人吧?试试反过来:

$x = 4 + '4'

现在它将第二个 '4' 视为整数,并返回 8。在大多数语言中这行不通:

图 4.9 – Python 不是随便的

图 4.9 – Python 不是随便的

在 Python 中,我们可以将字符串连接在一起,这叫做 连接,我们也可以将整数相加(称为 加法),Python 会动态地根据变量内容的不同将其类型定义为字符串或整数。但是,我们不能将字符串和整数混合在一起;Python 会抛出错误。

这通常是一个好事,但有时也可能是双刃剑。因为 PowerShell 会尽力做我们想要的事情,它有时会做出我们不希望的行为。假设我们始终希望内容是字符串类型,但某个数据输入错误或不当操作将整数放了进去。这会影响我们之后对该变量的操作,因为它已经将对象类型更改为整数。在不太宽容的语言中,我们会得到一个明确指出问题所在的错误,但 PowerShell 可能会给我们一些意外的结果,若幸运的话可能需要很长时间来调试,若不幸则可能导致灾难性后果。

变量类型转换

幸运的是,我们可以通过在创建变量时使用加速器[int32]来让 PowerShell 更像静态类型语言:

[int32]$MyNewVariable = '42'
$MyNewVariable | Get-Member

即使我们给它的是字符串,它也是一个[int32]类型。请注意,如果我们给它一个不容易解释为整数的内容,它将无法正常工作:

[int32]$MyNewVariable = 'Forty Two'

这会抛出一个错误。重要的是,即使我们只输入以下内容,它也会抛出错误:

$MyNewVariable = 'Forty Two'

因为当我们在几行前创建MyNewVariable时,我们将其定义为[Int32]类型,所以现在它只能保存这种类型的内容。这也可能导致一些令人困惑的结果。

当我们尝试将一个浮动点数放入MyNewVariable时,可能会期望此命令抛出一个错误:

$MyNewVariable = 4.2

但是不行。PowerShell 只是选择最接近的整数,并使用它:

图 4.10 – PowerShell 按照指示操作

图 4.10 – PowerShell 按照指示操作

不过,我们可以将变量转换为新的类型,如下所示:

[single]$MyNewVariable = 4.2

现在,我们已经得到了正确的值。类型转换是一项非常有用的技术,稍后我们将会大量使用,特别是在第七章,“PowerShell 和 Web – HTTP、REST 和 JSON”中。它允许我们将一系列字符串转换成可以与基于 Web 的应用程序交互的 XML 和 JSON 对象。

有几种常见的方法可以使用类型转换来改变变量内容的类型。首先,我们可以通过将内容作为已定义的类型复制来创建一个新变量。让我们创建一个我们知道包含整数的变量:

[int32]$MyVariable = 42

我们可以将其转换为新变量,并作为字符串使用:

$MyString = [string]$MyVariable
$MyString | Get-Member

我们也可以反过来操作:

[string]$MyOtherString = $MyVariable

活动二

$myString$MyOtherString 有什么区别?

提示:我们接下来可以将哪些类型的对象放入每个变量中?

其次,我们可以在不创建新变量的情况下进行转换;我们可以在代码中使用[string]$MyVariable,如果可能,PowerShell 会将其内容视为正确类型的对象。

这就是我们需要了解的简单值类型,以及 PowerShell 如何使用动态和静态类型。接下来,我们需要查看更复杂的复合变量类型——引用类型。

导航引用类型

现在我们对类型有了一些了解,也知道了值类型是如何工作的,我们可以讨论另一种主要的对象类型——引用类型。在前一节发现值类型中,我们将值类型对象与其他语言中的基本数据类型进行了比较。引用类型对象相当于数据结构。引用类型对象只在堆栈中保存指向堆中更多数据的引用;这一点很重要,因为引用类型对象没有固定大小。一般来说,引用类型最多可以包含 2 GB 的数据。为了证明这一点,我通过从古腾堡计划下载文本文件,将莎士比亚的戏剧《哈姆雷特》转化为字符串:

图 4.11 – 字符串化哈姆雷特

图 4.11 – 字符串化哈姆雷特

如我们所见,整个字符串大约有 197,000 个字符。

我在这里做了两件有趣的事;我使用了字符串类型的 length 属性来查看我的字符串有多长(以字符为单位),但我也使用了 GetType() 方法来检查对象类型,而不是使用管道将其传递给 Get-Member。在我们进一步讨论属性、方法以及可以用字符串做的其他有趣事情之前,我想先从一个简单的引用类型对象——数组开始。

数组

数组是一种固定长度的数据结构,包含一组对象。数组中的对象不一定是同一类型,也不一定是排序或有序的。一个数组可以包含零个或多个对象。我们可以通过隐式使用逗号字符 , 或显式使用 [array] 来告诉 PowerShell 创建一个数组。试试下面的操作:

$MyArray = 1,2,3
$MyArray.GetType()

你试过了吗?你真的应该试一下。随着我们继续这个部分,我会回到我们在这里创建的数组。如果你到了部分末尾才发现没创建数组,然后又得回去从头开始,那会很糟糕。

要创建一个单元素数组,你可以这样做:

$number = ,1
[Array]$MyTinyArray = 1

输出可以在以下截图中看到:

图 4.12 – 两个单元素数组

图 4.12 – 两个单元素数组

我们可以看到,我们每次都会创建一个 Object[] 类型的 BaseType,它是 System.Array

我们还可以使用范围运算符(..)来创建数组。尝试以下操作:

$NewArray = 1..10

我们应该得到一个包含从 1 到 10 所有数字(包括 10)的数组。

最后,我们可以使用数组运算符 @()

$AnotherArray = @(1..5; "some stuff")

注意,当我们分隔不同类型的对象并使用表达式时,必须使用分号,而不是逗号。当我们开始编写脚本并使用展开技巧时,这一点特别有用,这将在本章最后一节中讲解。

数组基础

数组中的每个对象称为元素。我们可以通过调用整个数组($NewArray)来列出元素,和调用其他变量一样:

图 4.13 – 列出数组的元素

图 4.13 – 列出数组的元素

我们可以选择通过传递索引来调用数组中的单个元素。这将获取第一个元素:

$NewArray[0]

这将获取倒数第二个元素:

$NewArray[-2]

我们可以调用多个元素:

$NewArray[0,2,5,7]

我们还可以调用一系列元素:

$NewArray[6..9]

我们可以通过直接赋值来改变数组元素的值:

$NewArray[5] = 5

我们可以通过使用+=运算符向数组中添加元素:

$NewArray += 11

但是从数组中删除一个元素是有点棘手的;相反,创建一个只包含你想要的元素的新数组。

默认情况下,数组可以包含多种类型的对象:

$ScruffyArray = 1, 'socks', (Get-Process)

我们可以限制数组中对象的类型,使其只能包含某一类型的对象:

[Int32[]]$IntArray = 1..5

现在尝试以下操作:

$IntArray += 'socks'

哎呀,我们遇到了错误:

图 4.14 – 你不能把它留在里面

图 4.14 – 你不能把它留在里面

如果我们看一下,可以看到IntArray对象的类型是[int32[]],其基础类型是System.Array,而元素的类型是[Int32]。我们在第二章《探索 PowerShell Cmdlet 和语法》中看到了这个语法,当时我们在查看Get-Help cmdlet 时使用过它。一个空的方括号表示一个可以包含多个值的参数;换句话说,一个数组:

图 4.15 – System.String 接受一个数组

图 4.15 – System.String 接受一个数组

最后,我们可以将数组合并在一起:

$BigArray  = $NewArray + $IntArray

基本内容已经覆盖。让我们来看一下我们可以使用的数组属性和方法。

数组的属性和方法

使用Get-Member来查看数组的属性和方法是很困难的。PowerShell 不会将数组传递到管道中;它按顺序传递数组中的每个元素。通过输入以下内容来尝试:

$NewArray | Get-Member

我们得到的只是[Int32]类型的属性和方法。

要查看数组类型的属性和方法,你需要访问帮助主题about_Arrays,或者在线查看:docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays

让我们简单看一下其中一些更重要的内容。

Count、Length 和 Rank

这些是数组的常见属性。CountLength的别名。它们都告诉我们数组中有多少个元素。$NewArray.Count$NewArray.Length是相同的。这又回到了老管理员的肌肉记忆问题。你会在 PowerShell 文献中看到这两种说法。

Rank 对于处理数据集时很有趣。我们一直在使用的数组是 $ScruffyArray,它包含 Get-Process 的输出。在 PowerShell 中,你处理的大多数数组可能是单维数组。然而,在数据科学中,我们经常需要使用多维数组;它们比较复杂,我还没看到一个好的 PowerShell 模块来操作它们。如果你真的感兴趣,可以看看 about_Arrays 帮助主题。只要知道,如果你需要操作多维数组,这完全是可能的。

Clear、ForEach 和 Where

这些是数组的常见方法。当数组的元素支持Clear方法时,你可以在数组上使用它来清除所有元素。如果元素不支持该方法,你会得到一个错误。

图 4.16 – 有些方法比其他方法更有用

图 4.16 – 有些方法比其他方法更有用

ForEach方法允许我们遍历数组中的每个元素,并对其执行操作。输入以下代码可以得到数组中数字的平方:

@(0..3).ForEach({$_ * $_})

个人而言,我认为这种代码高尔夫(代码简化)故意复杂化,违背了 PowerShell 的最佳实践。如果只处理几行代码,这没问题,但我不会把它放在脚本中。我们将在第五章中讨论如何以更易读的方式做这件事,PowerShell 控制流 – 条件语句 和循环

Where 方法类似,虽然在单行表达式中更有用。要获取 $NewArray 中大于 5 的元素,我们可以输入以下代码:

$NewArray.where({$_ -gt 5})

它在功能上与这个相同:

$NewArray | Where-Object {$_ -gt 5}

可能更不易阅读。我们在第三章中已经讨论过 Where-ObjectPowerShell 管道 – 如何串联 cmdlet。记住,PowerShell 会依次将数组中的每个成员输出到管道中。那么,为什么这些方法还存在呢?如果我们正在处理非常大的数组,那么直接在数组上使用方法,而不是通过管道,会更快。以下 cmdlet 在我的笔记本电脑上大约需要 300 毫秒来完成:

Measure-Command {@(0..100000).ForEach({$_ * $_})}

以下 cmdlet 大约需要 400 毫秒:

Measure-Command {@(0..100000) | ForEach-Object{$_ * $_}}

所以,这取决于我们想要节省时间的地方。如果最快的代码是我们的目标,那么就对数组使用该方法。如果我们希望代码在回顾时能快速理解,那么使用管道。

数组性能

然而,值得节省时间的一个地方是我们对数组的操作。虽然看起来我们可以通过添加元素来改变数组,但实际情况并非如此。数组的大小是固定的。每次添加一个元素时,我们都会创建一个新数组。假设我们看到类似下面的代码:

$SlowArray = @()
1..10000 | Foreach-Object { $Slowarray += $_ }

我们实际上是在创建和丢弃 10,000 个数组。许多文档会告诉我们,替代方案是使用 .NET 类型而不是数组——例如 [System.Collections.ArrayList],像这样:

$ArrayList = [System.Collections.ArrayList]@()
1..10000 | ForEach-Object { $Null = $ArrayList.Add($_) }

这里我们有相同的编程风格,我们先创建数组变量,然后向其添加元素,但我们必须调用非 PowerShell 类型,并使用 Add() 方法。它有效,但相当复杂,尤其是当我们刚刚开始学习时。

另一种选择是通过巧妙的方式创建数组,并像这样构造它:

$AutoArray = (1..10000 | ForEach-Object {$_})

这会生成与其他两个方法相同的数组,速度与 ArrayList 对象类型相同,但使用全 PowerShell 代码的简单性。我在 Measure-Command 中运行了这三个 cmdlet,你可以看到它们的区别:

图 4.17 – 三种创建相同数组的方法

图 4.17 – 三种创建相同数组的方法

我第一次看到这个观察是在 Tobias Weltner 的精彩博客中,PowerShell.one。我强烈推荐阅读他的文章。

复制数组

现在我们对数组有了一个相当清晰的了解,可以看看值类型和引用类型之间的一个关键区别:复制变量的方式。试试看:

$a = 42
$b = $a
$a = 'socks'

在不再输入任何内容的情况下——$b 中的值是什么?如果你说是 42,那你说对了。当然是的。$a 的值保存在栈中,所以当我们创建 $b 时,我们将 42 复制到栈顶,并称之为 $b(如果你收到错误信息 'socks' isn't of type [Int32],请关闭 PowerShell 会话并重新启动一个新的会话)。

复制数组并不像那样工作。试试以下操作:

$Array1 = 1,2,3
$Array2 = $Array1
$Array1[2] = 5
$Array2

啊!发生了什么?Array2 包含了更新后的值!这是因为当我们将 Array1 复制到 Array2 时,我们只是复制了数据在堆中的引用,即进程存储区域。当我们修改元素时,并没有创建新数组;我们只是修改了旧位置的数据。引用保持不变,因此 Array1 的所有副本也会引用新数据。

我们如何解决这个问题呢?很简单。至少,算是简单的。试试看,按照以下步骤操作:

$Array1 = 1,2,3
$Array2 = $Array1 | ForEach-Object {$_}

然后,瞧,我们现在可以对 Array1 进行修改,而不影响 Array2

$Array1[2] = 5
$Array2

我们让 PowerShell 将 Array1 中的每个元素放入管道中,并将其传输到 Array2,从而创建了一个独立的数据数组。这有点慢且笨拙,但它有效,而且简单。

目前关于数组的内容就讲到这里。接下来,我们会在本书的后续内容中继续深入探讨它们。现在,让我们来看第二种引用类型:字符串。

字符串

字符串是一个按顺序排列的[char]类型对象集合——它就是文本。正如我们在本节关于引用类型的开始所看到的,字符串是一种引用类型,并且是不可变的——只读的。如果你想改变字符串,你需要创建一个新的字符串并销毁旧的那个。

字符串有两个属性,CharsLengthChars 返回给定索引处的[char]对象:

$String = "PowerShell"
$String.Chars(0)

Chars 是一个带参数的属性——我们必须为其提供一个值,这个值是一个 [int32];否则会抛出错误。不过,我们可以传递一个包含单个 [int32] 的变量。Length 属性返回字符串对象的字符长度:

$String.Length

请查看下面的截图,了解这一切是如何显示的:

图 4.18 – 字符串的属性

图 4.18 – 字符串的属性

字符串有很多方法。大多数值类型的方法涉及改变对象类型,而字符串有许多方法可以改变格式。看看这个:

$String | Get-Member -MemberType Method | Measure-Object

我们将看到字符串上有 52 个可用方法。我们不会在这里覆盖所有方法,但我们可以尝试一些常见的方法。记住,每次使用这些方法时,我们都在操作字符串的输出,而不是字符串的内容。

要将字符串转换为全大写:

$String.ToUpper()

要将字符串转换为全小写:

$String.ToLower()

要将字符串输出为单个字符的数组:

$String.ToCharArray()

要替换字符串中的字符:

$String.Replace('o','X')

完成所有操作后,键入并查看变量的实际内容没有改变:

$String

Replace()Join()Split() 一起,也是 PowerShell 字符串操作符:

$List = 'one,two,three,four'
$List.Split(',')

这也可以写成以下形式:

$List -Split ','

这在下面的截图中显示:

图 4.19 – 两种分割方式

图 4.19 – 两种分割方式

我们经常发现我们有一个日期,但它是以文本格式存在的,最终变成了一个字符串。ToDateTime 方法允许我们将该字符串捕获为 [DateTime] 类型,但我们需要在方法中提供我们想要的文化信息。我们可以像这样操作:

$Culture = Get-Culture
[String]$DateString = '1/5/2024'
$ImportantDate = $DateString.ToDateTime($Culture)

我们可以在下面的截图中看到,$ImportantDate 是一个 [DateTime] 对象:

图 4.20 – 将字符串转换为 DateTime 对象

图 4.20 – 将字符串转换为 DateTime 对象

然而,如果有机会,使用 [DateTime] 值类型加速器并将变量转换为正确类型的对象会更简单:

[DateTime]$Anniversary = '1/5/2024'

单引号和双引号

在 PowerShell 中,单引号和双引号的行为不同。它们都可以用来定义字符串,但它们的用途不同。试试这个:

$MyName = 'Nick'
Write-Output 'My Name is $MyName'
Write-Output "My Name is $MyName"

我们可以在下面的截图中看到,输出是非常不同的:

图 4.21 – 单引号和双引号

图 4.21 – 单引号和双引号

双引号告诉 PowerShell 在处理之前扩展它找到的变量。我们使用双引号时,Shell 会通过将变量显示为绿色来提醒我们它将要执行的操作。

当打印变量的内容并紧跟另一个字符时,将变量名括在大括号中:

"${MyName}: Engineer"

要打印变量本身,而不是变量的内容,使用反引号字符 `

Write-Output "The value of `$MyName is $MyName"

在本书中,我们将会经常处理字符串,但现在我们应该看一下另一个重要的引用类型——哈希表。

哈希表

哈希表是 PowerShell 实现的一种数据结构,叫做字典。它们由键值对组成。我们使用哈希表来查找给定键的值,或检查哪个键包含给定值;它们基本上是查找表。让我们创建一个哈希表并进行操作:

$Hash = @{}

我们使用@{}来创建一个哈希表。我们不应该将它与@()混淆,后者会创建一个数组。现在我们已经有了哈希表,我们应该往里面添加一些内容。我们可以在创建哈希表时这么做,像这样:

$MyBike = @{HandleBar = "ApeHanger"; Color = "Red"; Wheel = 15}

注意,我们使用分号来分隔键值对,而不是像在数组中那样使用逗号:

图 4.22 – 创建和填充哈希表

图 4.22 – 创建和填充哈希表

我们可以使用add方法向哈希表中添加键值对,如下所示:

$MyBike.Add('Bell', $True)

add方法需要两个参数,用逗号分隔;第一个是键,必须用引号括起来,第二个是值。如果值是字符串,它也必须用引号括起来:

$MyBike.Add('Condition','Poor')

注意,不像数组,PowerShell 将哈希表视为单个对象:

$MyBike | Measure-Object

将其与以下内容进行比较:

$NewList = @(1,2,3,4)
$NewList | Measure-Object

然而,我们可以使用Count属性来返回键值对的数量:

$MyBike.Count

现在我们有了哈希表,我们可以用它做什么呢?显而易见的用途是查找值:

$MyBike.condition

它返回值poor。我们也可以像使用数组索引一样使用键:

$MyBike['Wheel']

我们也可以使用这种方法来添加键值对:

$MyBike['Gears'] = 'Fixed'

使用方括号的一个大优点是,它允许我们传递一个键的数组并返回对应的值:

$MyBike['HandleBar', 'Condition', 'Gears']

或者,我们可以使用一个变量来保存这个数组:

$BikeDetails = 'HandleBar', 'Condition', 'Gears'
$MyBike[$BikeDetails]

我们可以使用GetEnumerator()方法逐个列出哈希表中的所有键值对:

$MyBike.GetEnumerator()

等等。我们难道不能直接输入$MyBike吗?不行。如果我们尝试这样做,整个哈希表会作为一个单独的对象返回。它看起来像是键值对,但不会像一组独立的键值对一样工作。试试下面的操作:

$MyBike | ForEach-Object {[Array]$BikeProperties += $_}
$BikeProperties.Count
$MyBike.GetEnumerator() | ForEach-Object {[Array]$NewBikeProperties += $_}
$NewBikeProperties.Count

我们可以看到,在第一行中,只有一个对象通过了管道。

我们也可以使用remove方法删除键值对:

$MyBike.Remove('Gears')

我们可以测试某个键是否存在:

$MyBike.ContainsKey('HandleBar')

然后我们可以测试值是否存在:

$MyBike.ContainsValue('poor')

有序哈希表

默认情况下,哈希表是无序的,这其实并不重要,因为我们只是寻找它们包含的键值对中的值。然而,有时候我们希望哈希表以特定的方式排列。我们可以使用[ordered]关键字来实现。注意,这个关键字不放在变量前面,而是放在右侧:

$OrderedHash = [ordered]@{a=10;b=20;c=30}

你可以在下图中看到它们的区别:

图 4.23 – 有序哈希表

图 4.23 – 有序哈希表

注意,现在它变成了一个不同的类型——OrderedDictionary。我们仍然可以通过传递键来获取值,但由于它是有序的,我们现在也可以直接传递键值对的索引:

$OrderedHash.c
$OrderedHash[1]

最后,您可能注意到我们又回到了自行车的话题。那是因为我们可以对哈希表做一些很酷的事情——我们可以将它们转换为对象:

$MyImaginaryBike = [PSCustomObject]$MyBike

从以下屏幕截图中可以看到,我们现在有一个名为 $MyImaginaryBikePSCustomObject,它具有一组与原始哈希表键值对匹配的属性:

图 4.24 – 将哈希表转换为对象

图 4.24 – 将哈希表转换为对象

酷吧?我们将在本书的其余部分中经常使用哈希表,因为它们是非常有用的类型。

还有其他引用类型,涉及其他类型的数据结构,如队列和栈,但我们通常不常使用它们,至少在日常的 PowerShell 中不会使用。在本章结束之前,我们将介绍哈希表的最终用法:splatting。

Splatting – 哈希表的一个酷用法

一些 PowerShell cmdlet 接受大量参数,逐个在同一行中输入这些参数可能会让人感到困惑。这时,哈希表派上了用场。试试以下方法:

$Colors = @{
ForegroundColor = 'red'
BackgroundColor = 'white'
}
Write-Host 'all the pretty colors' @Colors

注意我们没有使用 $Colors;我们使用的是 @Colors。同时,注意它的顺序没有关系:

Write-Host @Colors 'OK, just red and white, then'

这样也能工作,因为我们在哈希表中明确指定了参数:

图 4.25 – 基本的 splatting 示例

图 4.25 – 基本的 splatting 示例

我们可以使用数组,但这只对位置参数有效;数组中的第一个值将是第一个位置参数,第二个值是第二个,依此类推。因为大多数 cmdlet 不会有超过一两个位置参数,所以这并不像哈希表那样有用。

我们在接下来的章节中将会经常使用 splatting,但对于这一章来说,内容就到这里。现在是总结我们所学的内容的时候了。

总结

这一章内容比较长,但我们学到了很多东西。我们首先了解了变量及其在 PowerShell 中的使用方式。接着,我们从计算机科学的角度重新审视了对象,学习了它们的属性和方法。这为我们提供了探索对象类型的基础,我们查看了一些值类型,它们相当于计算机科学中的基本类型。接下来,我们看到值类型如何被分组到数据结构或引用类型中。最后,为了增加一些趣味性,我们了解了 splatting 如何帮助我们节省时间和精力。

在下一章中,我们将研究 PowerShell 中的流程控制,包括 ifelse 等条件语句,以及使用 ForEachWhile 的循环。我们还会安装更多的软件,因为那才是有趣的部分,对吧?

练习

  1. My Variable 这个变量有什么问题?

  2. 不尝试代码的话,这个 cmdlet 会返回什么 TypeName

    New-Variable -Name MyVariable -Value "somestuff"
    Get-Variable MyVariable | Get-Member
    
  3. 我们如何改变 PowerShell 显示错误的视图?

  4. 我们可以使用什么自动变量来清空数组或哈希表的内容?

  5. 我们如何比较两个整数?

  6. 这里的 MyVariable 对象类型是什么?

    $MyVariable = ,1
    
  7. 我们如何将字符串中的每个字符放入数组中?

  8. 这里会出什么问题,为什么?

    Write-Output 'My Name is $MyName'
    
  9. 我们如何创建一个类型为 OrderedDictionary 的对象?

进一步阅读

本章有很多内容要阅读,因为我们只是略微触及了这个主题的表面。我们确实应该阅读与本章内容相关的帮助主题,以及一些官方的 PowerShell 语言文档:

第五章:PowerShell 控制流 – 条件语句与循环

到目前为止,我们所做的一切都像阿诺德·汤因比所说的那样,一个接一个。这没问题,但它并没有反映出我们希望在现实世界中发生的情况。大多数时候,我们希望我们的代码根据不同的情况执行不同的操作,有时我们希望它执行某些操作若干次。这就是需要控制流的地方。在本章中,我们将探讨如何让 PowerShell 根据发现的内容在脚本中执行不同的操作——这些称为条件语句。接下来,我们将探讨如何让 PowerShell 重复执行某个过程,不管是预定的次数还是不定的次数。由于不定次数可能会是无限的,我们将讨论如何中断和继续循环行为。

高效地完成这些操作需要编写多行代码,因此我们首先将讨论集成开发环境IDEs),然后安装推荐用于 PowerShell 7 的编辑器,Visual Studio Code。

在本章中,我们将涵盖以下主要内容:

  • IDE 和 VS Code 简介

  • 条件控制 – ifelseelseifswitch

  • 循环 – foreachdo whiledo untilwhilefor

  • 中断与继续

  • 玩个游戏

IDE 和 VS Code 简介

控制台非常适合运行一两行代码,或者快速检查某个想法是否可行,但它无法保存我们的代码,也不能编辑它。我们需要一个编辑器。最简单的编辑器就是我们机器上安装的任何文本编辑器,例如 Windows 上的记事本或 Linux 上的 Vi。它们能做的最基本的事情就是让我们写代码、保存代码并返回编辑。但是它们仅能做到这些,还有许多更好的选择可用。

IDE 将高亮显示命令和关键字,并进行一些语法检查,好的 IDE 通常还会内置控制台,让我们通过运行短小的代码片段(有时是一行或者一次运行整个程序)来测试代码。它们通常还具有某种调试功能,允许我们在特定位置停止代码,并检查变量的内容。大多数编程语言都有某种形式的 IDE,例如 Python 有IDLE,即集成开发与学习环境,但我不知道有多少人会用它来工作,而 Windows PowerShell 有集成脚本环境ISE),它非常优秀,但未被移植到 PowerShell 7 中。因此,推荐用于 PowerShell 7 的 IDE 是Visual Studio CodeVS Code),这是一个由微软开发的基于 JavaScript 的开源环境。

VS Code 是高度可扩展的(意思是——很多人已经为它编写了插件),并且可以支持多种语言。我们需要做的就是下载我们选择的语言模块。还有一些插件可以检查语法,允许我们使用代码片段,连接到代码仓库,如 Git,集成测试,并且做任何我们在更昂贵的 IDE(如 Eclipse)中能做的事。不如不再多说,让我们开始安装吧。

安装 VS Code

VS Code 可以安装在 Windows、Linux、macOS 和 Raspberry Pi 上——没错,这里有 Raspbian 安装。其实很简单。访问 VS Code 下载页面:code.visualstudio.com/download,然后点击适合我们正在使用的机器和操作系统的相关安装包;对于 Raspberry Pi,我们选择.deb ARM 下载。下载完成后,切换到保存包的文件夹或目录。

Linux

VS Code 是一个图形化工具,所以我们需要在 Linux 上使用桌面环境。对于 Ubuntu 及其他基于 Debian 的版本,我们只需要运行以下命令:

sudo apt update
sudo apt install ./<filename>.deb

对于 Raspbian,过程更简单:

sudo apt update
sudo apt install code

这些命令会完成它。

对于 Red Hat 发行版,事情稍微复杂一些。最简单的方法可能是使用 snapd,在这种情况下,以下命令会有效:

sudo snap install code –classic

code.visualstudio.com/docs/setup/linux上可以找到更详细的 Linux 安装 VS Code 的说明。

macOS

在 macOS 上安装 VS Code,下载相关的 Mac 安装包.zip文件并解压其内容。将Visual Studio Code.app拖到应用程序文件夹,这样 VS Code 就能在Launchpad中使用了。

Windows

在 Windows 中,我们可以选择用户安装或系统安装。如果选择用户安装,应用程序默认安装在C:\Users\<username>\AppData\Local\Programs\Microsoft VS Code,仅对安装它的用户可用。如果使用系统安装程序,则会安装在C:\Program Files\Microsoft VS Code,并且需要管理员权限来安装。如果我们安装的是用户安装包,那么自动更新会更容易一些,所以我们就下载那个包吧:

  1. 下载完成后,浏览到下载位置并双击.exe文件。

  2. 接受最终用户许可协议EULA)并点击下一步

  3. 如果需要,可以更改安装位置,然后点击下一步

  4. 决定是否需要一个开始菜单文件夹,并点击下一步

  5. 设置PATH环境变量,并将 code 注册为支持的文件类型的编辑器。点击下一步

图 5.1 – 选择 VS Code 安装的附加任务

图 5.1 – 选择 VS Code 安装的附加任务

  1. 审查安装选项并点击安装

  2. 点击完成,然后欣赏你刚刚安装的 VS Code。

Windows 用户的快速安装方式

如果我们安装了PowerShellGet模块,我们就可以运行以下命令:

Install-Script Install-VSCode -Scope CurrentUser ; Install-VSCode.PS1

Y表示同意Yes,就这样。这个方法非常适合我们已经熟悉 VS Code 的使用,但如果这是我们第一次使用它,我们可以手动操作,以便准确了解每个步骤的发生。

配置 VS Code 以支持 PowerShell

现在我们已经安装了 VS Code,需要告诉它识别 PowerShell 语言。在安装时,VS Code 并不支持 PowerShell。我们需要安装一个扩展。打开 VS Code,如果它尚未打开,可以在搜索栏中输入Code并从搜索结果中选择该应用程序。在主窗口中,点击窗口最左边的扩展图标——它看起来像一堆盒子:

图 5.2 – 查找 VS Code 的 PowerShell 扩展

图 5.2 – 查找 VS Code 的 PowerShell 扩展

在扩展搜索栏中输入powershell。我们需要的是由 Microsoft 提供的 PowerShell 扩展,它是第一个搜索结果,如下图所示。点击它,然后在主面板中点击蓝色的安装按钮:

图 5.3 – 安装 VS Code 的 PowerShell 扩展

图 5.3 – 安装 VS Code 的 PowerShell 扩展

开始吧。按Ctrl + N来打开一个新文件。系统会要求我们选择语言。我们不必这样做,但这样做意味着从一开始就能得到帮助;我们可以不指定语言进行输入,但在将文件保存为 PowerShell 脚本并使用.ps1文件扩展名时,语法检查和高亮显示才会启用。点击下拉菜单选择PowerShell。我们可以随时通过点击右下角的语言名称来更改语言;如果我们没有选择语言,它会显示纯文本

我们首先看到的应该是 PowerShell 集成终端在屏幕底部启动了。我们可以在其中运行 PowerShell cmdlet,并且当我们从顶部窗口运行代码时,结果会显示在终端中。尝试这样做——在顶部窗口中输入write-outp

你应该会看到 VS Code 提供一个有用的自动补全选项,像这样:

图 5.4 – 在 VS Code 中自动补全

图 5.4 – 在 VS Code 中自动补全

它不仅会为你提供自动补全,还会提示如何使用 cmdlet;如果你将鼠标悬停在建议上,你会看到一个.ps1;我使用的是HelloWorld.ps1。现在点击窗口右上角的播放图标(箭头)来运行代码。它应该是这样的:

图 5.5 – 在 VS Code 中运行 PowerShell

图 5.5 – 在 VS Code 中运行 PowerShell

如果你看到的输出像前面的截图,恭喜你!你已经成功运行了第一个脚本!

切换终端环境的技巧

如果你使用的是 Windows 系统,你可能至少安装了两个不同版本的 PowerShell——PowerShell 7 和 Windows PowerShell。你如何在它们之间切换呢?点击大括号(PATH环境变量)。你可以使用$PSVersionTable变量来检查你正在运行的版本。

VS Code 功能强大,拥有数百个功能和扩展,这意味着我们可以花费大量时间使用它,但也有几个需要注意的缺点:

  • 由于它是开源并且经常更新,互联网上的博客文章和教程(或者说,天哪,PowerShell 编程书籍)可能会迅速过时。我们需要仔细研究如何使用某个功能,或者理解为什么某些东西不起作用,并且要小心任何超过一两年历史的资料来源。

  • 由于它是开源且可扩展的,因此学习曲线可能相当陡峭。在本书中,我们会介绍一些常见的使用案例和功能,但我们几乎只触及了使用该软件的表面。官方的 Microsoft 文档是最新的,但不太适合用户使用。我们应该通过可信的文章和有用的网站来补充我们的阅读,比如 Stack Overflow 和 Spiceworks。

  • 由于它是开源且可扩展的,任何人都可以为其编写扩展。这意味着有时扩展会非常棒,但有时则不太有用。也可能是两年前一个非常出色的扩展,现在因为作者将其当作一个有趣的项目编写,而现在集中精力做其他事情,导致它已被搁置。

尽管有这些缺点,VS Code 仍然非常有用。当我们遇到让我们烦恼的事情时,我们应该暂停一下,保持耐心,并找到解决办法。更好的做法是,修复它;这正是开源软件的乐趣所在。

现在我们已经安装好了 VS Code 并准备好使用,让我们进入本章的实际主题——PowerShell 控制流程。我们将从条件语句开始。

条件控制——if, else, elseif 和 switch

条件控制流程基于一个简单的语句。如果这个条件为真,那么执行某个操作。第一部分是一个布尔表达式。第二部分是基于布尔表达式结果的一个动作。布尔表达式会解析为两种值之一:真或假。我们在第三章PowerShell 管道——如何将命令结合在一起,以及理解 Where-Object 高级语法章节中讨论了布尔运算符。考虑以下两个语句:

1 -eq 1
1 -eq 2

第一个解析为布尔类型的真值。第二个解析为假值。如果你不相信,试试看。虽然我们不常检查 1 是否等于 2,但我们经常需要比较变量:

图 5.6 – 只是检查

图 5.6 – 只是检查

这涵盖了我们逻辑的第一部分;接下来,我们需要根据测试结果执行某个操作。让我们为一些有趣的操作准备好 VS Code。现在先关闭 VS Code。

转到 PowerShell 提示符(不是 VS Code 中的提示符),并输入以下内容以创建工作目录,移动到该目录,并在其中启动 VS Code:

New-Item -Path C:\Temp\Poshbook -ItemType Directory
Set-Location -Path C:\Temp\Poshbook
Code .

Code 后面的句点很重要。不要漏掉它,否则你会在错误的位置工作。VS Code 启动后,会询问是否信任该文件夹中的文件作者。我们大概会信任,因为我们将成为作者。我们需要创建一个新的文件来进行工作,所以按 Ctrl + N 创建一个新文件并选择 Conditionals.ps1。我们有了空白画布。现在开始工作。

if 语句

我们将首先查看最简单的条件语句——if 语句。我们可以根据表达式是否为真来执行某个操作。请在脚本窗格中输入以下内容:

$x = 5
if ($X -gt 4) {
    Write-Output '$x is bigger than 4'
}

括号中包含需要被评估的表达式。大括号中包含一个脚本块,当表达式为 true 时,它会运行。现在,选中你的代码行并按 F8。代码会运行,你应该能在终端窗口看到输出:

图 5.7 – if 语句

图 5.7 – if 语句

小心使用单引号;请查看 第四章PowerShell 变量与数据结构,以了解原因。如果在操作中使用双引号,则会看到输出 5 is bigger than 4,因为你扩展了$``x变量。

提示

在输入 if 后,停顿一下,让 VS Code 赶上进度。它会为你提供 cmdlet 和关键字的选择。点击列表中的 if,它会为你构建该语句,如 图 5.7 中第 7 行到第 9 行所示。你需要做的只是填入条件和操作。

查看第 3 行和第 8 行的绿色文本。它们是注释。我们使用它们来使代码更易读,并澄清我们的操作。单行注释以 # 开头;# 后面的内容会被 PowerShell 忽略。多行注释以 <# 开头,以 #> 结束。这些注释是 VS Code 自动生成的,我们可以保留或删除它们,这对代码的运行没有影响。

我们也可以测试一个语句是否为假。在第 9 行下输入此内容:

if (!($x -lt 4)) {
    Write-Output ' $x is bigger than 4'
}

我们在这里使用了!作为-not运算符。我们可以将第 11 行改写为如下:

for (-not($x -lt 4)) {

但你不会经常看到像那样写出的-not运算符——大多数人使用!。如果你选中这段代码并按 F8,你会再次看到输出 $x is bigger than 4

图 5.8 – 使用 -not 运算符反转条件

图 5.8 – 使用 -not 运算符反转条件

如果 $x 小于 4,我们将不会得到任何输出,脚本会继续执行 if 语句后的下一行——我们不需要做其他任何事。如果我们希望脚本在某个语句为真时执行一件事,而在语句为假时执行另一件事,会发生什么呢?

else 语句

我们可以使用 else 语句来指定当 if 语句的测试条件不为真时的第二个动作。从第 15 行开始输入这个内容——使用自动完成功能来补全 else 语句,并注意我们在条件中使用的是 -lt,而不是 -gt

if ($x -lt 4) {
    Write-Output '$x is smaller than 4'
}
else {
    <# Action when all if and elseif conditions are false #>
    Write-Output '$x is not smaller than 4'
}

现在选择第 15 到 22 行并按 F8

图 5.9 – 使用 else 语句

图 5.9 – 使用 else 语句

注意第 20 行自动完成 else 语句时的备注:当所有 if 和 elseif 条件为假时的操作else 语句是当没有条件为真时的兜底方案。但我们如何测试多个条件呢?我们可以一个接一个地写多个 if 语句,但每个语句都会执行,不管前一个语句的结果如何。我们也可以把 if 语句嵌套在一起,但那样会非常繁琐。

elseif 语句

我们可以使用 elseif 来在单个 if 语句内指定多个条件进行测试。在第 24 行输入这个内容。使用自动完成功能来确保正确:

if ($x -lt 5) {
    Write-Output '$x is less than 5'
}
elseif ($x -gt 5) {
    Write-Output '$x is bigger than 5'
}
else {
    Write-Output '$x is 5'
}

高亮显示你刚刚输入的代码并按 F8。你应该看到如下输出:

图 5.10 – 使用 elseif 语句

图 5.10 – 使用 elseif 语句

你可以根据需要添加任意数量的 elseif 语句,但一旦某个语句为真,父级的 if 语句将会退出。试试这个:

if ($x -lt 5) {
    Write-Output '$x is less than 5'
}
elseif ($x -lt 6) {
    Write-Output '$x is less than 6'
}
else {
    Write-Output '$x is 5'
}

我们得到的输出是 $x is less than 6else 语句没有执行。

三元运算符

PowerShell 7 引入了一个新运算符,允许我们在单行中构造简单的 if/else 语句对。在我们代码的第 15 行,有如下示例:

if ($x -lt 4) {
    Write-Output ' $x is smaller than 4'
}
else {
    <# Action when all if and elseif conditions are false #>
    Write-Output '$x is not smaller than 4'
}

我们可以像这样用三元运算符重写它:

($x -lt 4) ? '$x is smaller than 4' : '$x is not smaller than 4'

这样很棒,可以节省很多输入,但也会使代码的可读性降低。问号字符 (?) 也可以作为 Where-Object cmdlet 的别名,但在这里并没有以这种方式使用。请记住,我们的代码会被阅读的次数远远超过编写的次数。

如果多个语句为真,会发生什么?让我们看看如何处理这种情况。

switch 语句

我们可以使用多个 if 语句来测试多个条件,但这样可能会变得复杂,因此有一个特殊的语句可以用来测试多个为真的条件——switchswitch 语句会测试每个条件,并为该条件执行相关的脚本块。每个为真的条件都会被执行。让我们看看它是如何工作的。在新的一行中,输入以下内容:

$Array  = 1,2,3,4,5
switch ($Array) {
    1 {Write-Output '$Array contains 1'}
    3 {Write-Output '$Array contains 3'}
    6 {Write-Output '$Array contains 6'}
}

如果你选择代码并按 F8,你应该会看到以下输出:

图 5.11 – 使用 switch 语句

图 5.11 – 使用 switch 语句

使用 switch 语句可以做很多事情。请考虑以下示例:

$String = "Powershell 7"
Switch -Wildcard -CaseSensitive ($String) {
    7 { Write-Output 'contains 7' }
    "Pow*" { Write-Output 'contains Pow'}
    'pow*' { Write-Output 'contains pow'}
    '*she*' { Write-Output 'contains she'}
    {$String.Length -gt 7} { Write-Output "long string"}
    Default {Write-Output "No powershell here"}
}

在这里,我们正在遍历一个字符串。我们使用了 -Wildcard-CaseSensitive 参数来检查字符串中匹配的部分,并确保大小写也匹配。我们评估的是字符串的属性,而不仅仅是内容,最后,我们设置了一个默认输出:

图 5.12 – Switch 选项

图 5.12 – Switch 选项

让我们来看看截图中的代码行:

  • 在第 75 行,我们为 switch 语句设置了参数——请注意,它们必须在要评估的表达式之前,并且该表达式需要用括号括起来。

  • 在第 76 行,我们在检查字符串是否包含整数 7。在第 77 行和第 78 行,我们演示了 -CaseSensitive 参数的使用。

  • 在第 80 行,我们在评估字符串的一个属性,它的长度。请注意,这需要放在一个脚本块中,因此它需要使用大括号,而不是括号。

  • 最后,在第 81 行,我们设置了一个默认输出——只有在 switch 语句中的其他条件没有匹配时,才会输出这个结果。

活动 1

为什么输出没有产生 contains 7 这一行?

我们还可以使用 -Regex 参数来与正则表达式进行匹配。这不能与 -Wildcard-Exact 参数一起使用。-Exact 参数与 -Wildcard 参数相反,并且是默认值。

循环 – foreach、do while、do until、while、for

自动化理论告诉我们,机器在执行重复任务时比人类更擅长;它们可以以完全相同的方式重复执行任务任意次。当我们编写代码时,我们称之为迭代或循环。接下来我们将查看 PowerShell 中四种常见的循环示例。我们将从 foreach 循环开始,然后再看看 do while 及其对等的 do until。接着,我们会讨论更通用的 while 语句,并最后讨论 for 语句,看看它与 foreach 的不同之处。让我们开始吧,首先创建一个新文件,将其设置为 PowerShell 文件,并保存为 loops.ps1

foreach 循环语句

这是最常见的循环语句。我们之前在 第三章《PowerShell 管道——如何将 cmdlet 连接在一起》中提到过 foreach,当时我们讨论了 Foreach-Object cmdlet。它们的功能相似,但并不完全相同,并且语法差异很大。foreach 循环具有以下语法:

foreach ( $element in <expression> ) {<scriptblock>}

如果 foreach 出现在语句的开头,PowerShell 会将其视为 foreach 循环语句,而不是 ForEach-Object 的别名。我们可以在以下截图中看到这种情况:

图 5.13 – foreach 语句和 foreach 作为 ForEach-Object 的别名

图 5.13 – foreach 语句和 foreach 作为 ForEach-Object 的别名

在第 1 行,foreach出现在行首或语句前,而在第 2 行,它出现在管道符后面。PSScriptAnalyzer,作为 VS Code 中 PowerShell 扩展的一部分,已识别出第 2 行中的foreachForEach-Object的别名,并为它加上了波浪形的黄色下划线,表示这不是好的做法,但第 1 行则没有。如果我们将鼠标悬停在第 2 行的foreach上,可以看到它发出了警告,因为我们使用了别名,事实上这是正确的。那张图中还有什么信息呢?

在第 1 行,我们有一个foreach语句 —— 它由foreach关键字、一个括号中的分组表达式和一个大括号中的脚本块组成。分组表达式的格式是($element in [expression]),它生成一个对象集合。一旦集合完成,脚本块会依次作用于每个对象。在这个例子中,我们正在收集当前目录中所有以.ps1结尾的对象。然后我们对它们执行操作——获取它们的长度($f.length)并通过复合运算符+=将其加到$l变量中。无论我们用什么变量来表示集合中的元素,只要它不是自动变量或我们在其他地方使用的变量,都没关系。

在第二行,我们使用ForEach-Object cmdlet 来做完全相同的事情。有什么不同呢?

当我们使用foreach关键字创建一个循环时,表达式会被运行以生成一个数组,并将其保存在内存中。然后每个元素会一次性进入脚本块。如果你的表达式生成了一个非常大的数组,可能会出现内存问题。

当我们将ForEach-Object cmdlet 作为管道的一部分使用时,由Get-ChildItem *.ps1生成的数组中的每个元素都会一次性通过整个管道;这意味着它使用的内存要少得多,因为整个数组并不存在于内存中,只有单个元素。缺点是它可能需要更长的时间来处理。

让我们自己尝试一个foreach循环。输入以下内容:

$l = 0 ; foreach ($f in Get-ChildItem *.ps1) {$l += $f.length} ; $l

这与图 5.13中的那行代码不完全相同。我们使用了分号(;)作为语句分隔符;这使我们能够将多个语句写在一行中,而不是将它们分开写成多行。这虽然输入更容易,但不太容易阅读。我们来逐一分析这个命令;

  • 我们的第一个语句是$l = 0 ;。这创建了变量$l并将其设置为0。这意味着每次我们运行整行代码时,$l都会被重置为0

  • 第二个语句是Foreach ($i in Get-ChildItem *.ps1) {$l += $i.length} ;。在这里我们创建了数组并遍历其中的每个元素。

  • 我们的最后一个语句只是$l —— 它返回$l在循环结束时的值。

我们可以通过以下截图看到它的样子:

图 5.14 – foreach 循环和语句分隔符

图 5.14 – foreach 循环和语句分隔符

我们将经常使用foreach循环,因为它是最常见的循环类型。接下来我们来看看do whiledo until循环。

do while 和 do until 循环语句

do whiledo until循环共享相同的语法,先写脚本块:

do {<scriptblock>} until/while (<condition> is true)

因为条件放在最后,所以它们会至少执行一次脚本块。

尝试一下,输入以下内容:

$number = 0
do { $number ++
Write-Output "The number is $number"
} while ($number -ne 5)

我们来仔细看看刚刚做的事情:

  • 在第一行,我们创建了一个$number变量,并将其设置为0

  • 在第二行,我们使用do关键字开始一个do while循环,用大括号{打开一个脚本块,并使用++递增操作符将$number变量增加 1。

  • 在第三行,我们执行了另一个操作,将输出写到屏幕上。

  • 在最后一行,我们用大括号}关闭脚本块,然后使用while关键字来提供条件,条件放在括号中。在这种情况下,条件是$number不等于5

你的最后一行输出是什么?对我来说,是The number is 5。这是因为脚本块在条件评估之前就被运行。试试这个:

$number = 0
Do {Write-Host "The number is not Zero"}
While ($number -ne 0)

正如我们在下面的截图中看到的,脚本块即使条件为假也已经执行。

图 5.15 – 计算机在骗你

图 5.15 – 计算机在骗你

do until循环的工作方式与此相同,只是直到条件成立时:

$number = 0
do { $number ++
Write-Host "The number is $number"
} until ($number -eq 5)

我们来看看这两种循环放在一起的样子:

图 5.16 – do while 和 do until

图 5.16 – do while 和 do until

一般来说,当测试条件预计为正时,我们使用do until循环,而当测试条件预计为负时,我们使用do while循环。并不是whileuntil决定真假,而是与之关联的条件决定。你可以轻松地在until中使用“不等于”(负)并在while中使用“等于”(正)。不过请记住,当条件不满足时,脚本块会执行第一次。那么我们该怎么处理呢?

while 循环

while循环把do while循环反过来,并将条件放在脚本块之前:

while (<condition> is true) {<scriptblock>}

这意味着如果条件不成立,那么脚本块将永远不会执行。没有until语句。

尝试一下,输入如下内容:

$number = 0
while ( $number -ne 5) { $number ++
Write-Host "The number is $number"}

活动 2

这与我们的do while示例产生了相同的输出。为什么?你怎么证明当条件成立时脚本块没有执行?

接下来我们来看最后一种循环:for循环。

for 循环

尽管名为for循环,但它更像while循环而非foreach循环。它是一个计数循环,依赖于条件为真,就像while,但它更灵活。语法也更复杂:

for (<Iterator> ; <condition> ; <iteration>) { <scriptblock> }

迭代器是我们将要迭代的变量,类似于我们之前在研究while循环时使用的$number变量。条件与while循环的条件相同,迭代是在每次循环时对迭代器执行的操作。试试这个:

for ($i = 0 ; $i -lt 5 ; $i ++) {Write-Host $i}

这并不特别令人兴奋;它看起来只是以一种复杂的方式做了和之前的while循环一样的事情。然而,for循环让我们能够索引循环中的元素,并根据该索引进行移动。试试这个:

$fruits = @('banana', 'apple', 'pear', 'plum')
for ($i = 1 ; $i -lt ($fruits.length) ; $i ++) {
    Write-Host $fruits[$i] "is after"  $fruits[$i-1]
}

你应该看到以下截图中的输出:

图 5.17 – 使用 for 循环排列水果

图 5.17 – 使用 for 循环排列水果

这是我们可以使用for循环做的另一件特别的事情。我们可以改变数组的内容。试试这个:

$fruits = @('banana', 'apple', 'pear', 'plum')
foreach ($fruit in $fruits) {$fruit = "tasty $fruit"}
Write-Host $fruits

好吧,这并没有成功。我们可能希望通过改变$fruit来影响原始数组,但并没有实现,因为foreach循环操作的是数组元素的副本。不过,我们可以使用for循环来实现这一点:

$fruits = @('banana', 'apple', 'pear', 'plum')
for ($i = 0 ; $i -lt ($fruits.length) ; $i ++) {
    $fruits[$i] = "tasty $($fruits[$i])"
}
Write-Host $fruits

然后我们应该看到如下的输出:

图 5.18 – 使用 for 循环让水果变得美味

图 5.18 – 使用 for 循环让水果变得美味

在第 50 行,我们做了一些新的事情。我们使用了$(),这是子表达式符号,确保 PowerShell 正确解释我们的意图。虽然我们可以在双引号中展开简单的变量,但更复杂的表达式需要更多的操作。如果不将$fruits[$i]用一对括号包裹并加上美元符号,PowerShell 将无法正确解包它。

我们快完成对循环的介绍了。我们已经看过了foreach循环,它对数组中的每个元素执行操作。我们看过do whilewhile,它们在条件为真时重复执行操作。接着我们看了do until,它直到条件为真才执行操作。最后,我们看了更复杂的for循环,它会重复执行操作预定的次数。接下来我们需要的是控制循环的方式。如果while循环中的条件永远不变为假,会发生什么呢?让我们看看使用breakcontinue来控制循环。

中断和继续

breakcontinue语句是相关的,它们允许我们控制循环的行为。我们也可以在switch语句中使用它们。重要的是不要在循环或switch语句之外使用它们,因为这样会导致不可预测的行为。break语句可以用来完全停止循环的迭代,而continue语句可以用来停止当前的迭代并跳到下一个迭代。我们先来看break语句。

break 语句

让我们来玩一下可靠的while循环。输入以下内容:

$number = 0
while ( $number -ne 5) { $number ++
if ($number -eq 3) {
    break
    }
    Write-Host "The number is $number"
}

希望你会看到最后打印出的数字是2。条件语句if会在$number3时停止循环。break语句只会作用于它嵌套的循环。

continue 语句

continue语句会停止当前循环的迭代,而不是整个循环,然后进入下一次迭代。试试这个:

$number = 0
while ( $number -ne 5) { $number ++
if ($number -eq 3) {
    continue
    }
    Write-Host "The number is $number"
}

这次,循环会输出数字15,但会省略3。它在到达Write-Host语句之前就会跳出当前迭代,并开始下一个迭代。你应该能在下图中看到结果:

图 5.19 – 使用 continue

图 5.19 – 使用 continue

我们也可以在switch语句中使用breakcontinue,就像这样:

$String = "Powershell 7", "python"
switch -Wildcard -CaseSensitive ($String) {
    {$_} {Write-output "processing $_ :"}
    '*7' { Write-Output 'contains 7'}
    "Pow*" { Write-Output 'contains Pow'}
    'pow*' {Write-Output 'contains pow'}
    '*she*' { Write-Output 'contains she'; continue}
    {$String.Length -gt 7} { Write-Output "long string"}
    {$String.Length -lt 7} {Write-Output "No powershell here"}
}

这与我们在本章开关语句部分早些时候使用的switch块类似。不同之处在于,现在我们传递的是一个字符串数组,而不是单个字符串。第 7 行的continue语句意味着long stringNo powershell here的输出永远不会被执行;相反,switch语句会继续处理数组中的下一个元素python。现在将continue替换为break。我们可以看到整个switch语句会停止——python永远不会被处理。

目前关于循环的内容就讲到这里。我们将在本书的后续部分频繁使用它们。现在,我们来点轻松的内容吧。

让我们来玩个游戏

我们都很熟悉猜数字游戏。让我们运用本章所学来编写一个 PowerShell 游戏。这是许多编程教程中的常见挑战,我们也不例外。在英国,这个游戏有时被称为 Brucie 游戏,因为它与流行的英国电视游戏节目《Bruce Forsyth’s Play Your Cards Right》类似。这里的 Brucie 奖金就是我们可以学到一些东西。

程序会生成一个 1 到 100 之间的随机整数。接着我们需要获取用户输入的另一个整数。我们将猜测值与隐藏的数字进行比较,并判断是否正确。如果正确,我们将结束程序。如果不正确,我们需要判断它是太高还是太低,并输出相应的消息,然后返回继续让用户猜测。我们可以将其表示为一个流程图,这在编写代码时会很有帮助:

图 5.20 – Brucie 游戏的流程图

图 5.20 – Brucie 游戏的流程图

我们需要做的第一件事是打开一个新文件;我的文件名为Brucie.ps1。让我们开始工作吧。

第一个框要求生成一个 1 到 100 之间的随机整数。这很简单:

[int]$Hidden = Get-Random -Minimum 1 -Maximum 101

现在我们需要告诉玩家该做什么,并获取他们的输入:

Write-Host "Let's play the Brucie Game! Guess the hidden number between 1 and 100\. give me a number below"
$guess = [int] (Read-Host)

现在我们有了两个变量$Hidden$guess,存储了我们的两个数字。我们需要比较它们。最好的方法是使用if-else语句。如果数字不对,我们做一件事,否则打印You Win!

[int]$Hidden = Get-Random -Minimum 1 -Maximum 101
Write-Host "Let's play the Brucie Game! Guess the hidden number between 1 and 100\. give me a number below"
$guess = [int] (Read-Host)
if ($guess -ne $hidden) {
    <# Action to perform if the condition is true #>
} else {Write-Host "You Win"}

我们需要执行什么操作?我们需要查看数字是更大还是更小。我们需要再加一个if-else语句:

if ($guess -ne $hidden) {
    <# Action to perform if the condition is true #>
    if ($guess -lt $hidden) {
        Write-Host "Higher!"
    } else { Write-Host "Lower!"}
} else {Write-Host "You Win!"}

就是这样,等一下,不对。我们需要给他们多个猜测机会。我们需要一个循环。我们可以在这里使用各种类型的循环,但可能最简单的是while循环。在继续往下看我怎么做之前,先思考一下你可能怎么做。

不要偷看,思考一下。

好的,这是我的解决方案:

[int]$Hidden = Get-Random -Minimum 1 -Maximum 101
Write-Host "Let's play the Brucie Game! Guess the hidden number between 1 and 100.
give me a number below"
$guess = 0
while ($guess -ne $hidden) {
    $guess = [int] (Read-Host)
    if ($guess -lt $hidden) {
        Write-Host "Higher!"
    } elseif ( $guess -gt $hidden) { Write-Host "Lower!"}
}
Write-Host "You Win"

这是完整的实现:

图 5.21 – 布鲁西游戏

图 5.21 – 布鲁西游戏

在第 4 行,我将$guess初始化为0,这样循环就有了可以操作的对象。

在第 5 行,我把第一个if语句改成了while语句。我还用大括号结束了循环,并且将Write-Host "You Win"作为程序的最后一条语句。

在第 6 行,我将获取输入的语句放进了循环中。你可以试着把它放在外面。那样很快就会显得乏味,因为我们永远不会改变$guess的值,从 0 开始。

我做的唯一其他更改是在第 9 行,我用elseif语句替代了else,否则即便我们猜对了,循环也会输出Lower!

就这些。确保你保存一份游戏的工作副本,以便我们以后继续使用。我们在本章中覆盖了很多内容,让我们来总结一下。

总结

我们从本章开始时先了解了 IDEs,并安装了 PowerShell 7 推荐的环境:VS Code。VS Code 是一个庞大且不断变化的应用程序。我们需要花时间熟悉它,并且随着功能和能力的频繁变化,保持更新。我们已经看到它非常易于使用且功能强大。

然后我们看了控制程序流程的两种主要方式:条件语句和循环语句。我们花了很多时间在ifelseelseif条件语句上,接着又看了相关的switch语句。

接下来,我们看了循环。我们从一个遍历数组所有元素的循环——foreach 循环开始,探讨了它与 ForEach-Object cmdlet 的区别。我们还了解了 do whiledo until 循环,它们会在条件为真或为假时重复执行循环,接着我们看了 while 循环,它将条件和脚本块的位置互换。在本节中我们最后探讨了一个计数循环——for

最后,我们通过编写一个简单的数字猜谜游戏——布鲁西游戏,来结束这一章。我希望你在做这个练习时有所收获,因为这种练习会让你更加扎实地掌握如何编写 PowerShell 代码。

在下一章中,我们将会看看如何使用 PowerShell 与文件进行交互。我们将要读取内容、写入内容,并进行一些操作。

练习

  1. $x = 4 ; IF ($x -gt 4) {Write-Host '$x is larger than 4'} 这段代码会产生什么输出?

  2. 写一个语句,当$x不大于 4 时,产生输出$x = 4 ; IF ($x -gt 4) {Write-Host '$x is larger than 4'}

  3. 写一个语句,当$x = 4 ; IF ($x -gt 4) {Write-Host '$x is larger than 4'}时,$x正好等于 4 时,产生输出。

  4. 写一个语句,当$x = 4 ; IF ($x -gt 4) {Write-Host '$x is larger than 4'}时,如果$x不大于 4(如问题 2 所示),但是这次使用ternary运算符时产生输出。

  5. 这里出了什么问题?

    $processes = Get-process | foreach ($process in $processes) {$process.name}
    
  6. 重写$number = 0 ; Do {$number ++ ; Write-Host "Number is $number"} While ($number -eq 5)使其正常工作,但不使用do until循环。

  7. 使这个语句工作:For ($i = 0 ; $i -lt 5 ) {``Write-Host $i}

  8. 我们应该仅在循环和另一个地方使用breakcontinue语句。那是什么地方?

  9. 你会怎么做才能让 Brucie 游戏限制用户的猜测次数?

进一步阅读

这是一个大章节,因此这里有很多进一步阅读的内容:

第六章:PowerShell 和文件 – 读取、写入和操作数据

到目前为止,我们的 PowerShell 之旅都在屏幕上进行。如果能够将 cmdlet 的输出写入某个文件,以便稍后保存或发送给其他人,那该多好呢?这就是我们在本章要做的。首先,我们将查看如何格式化屏幕上的输出,以便我们可以只专注于感兴趣的内容,或以更有用的方式呈现输出。接下来,我们将探讨如何使用 Out-File 将输出写入文本文件。我每天都会做这件事,它是一个非常有用的技巧,但它有很大的局限性,因此我们还将讨论如何创建逗号分隔值CSV)文件,以供其他程序(例如 Microsoft Excel)使用,以及如何创建 HTML 文件,以便我们能够以网页的形式展示输出。

一旦我们熟悉了输出,我们将深入了解一个简短的部分,帮助我们理解 PowerShell 如何与文件系统交互,之后再看看我们如何使用 PowerShell 读取和操作常见文件类型中的数据,如文本文件和 CSV 文件。最后,我们将通过一个有趣项目的简短演练来结束本章,并留给你一个挑战。

在本章中,我们将覆盖以下主题:

  • 理解格式化

  • 写入文本文件

  • 使用 ConvertTo-Export- cmdlet

  • 处理文件

  • 操作文件

  • 让我们来点乐趣吧!

理解格式化

让我们来想想我们的老朋友 Get-Process。我们知道,当我们运行它时,它会显示我们机器上正在运行的进程列表。对于每个进程,它会显示一些属性。我们通过运行 Get-Process | Get-Member 知道,还有许多属性不会显示,除非我们明确请求显示。这几乎是每个 PowerShell cmdlet 的情况;我们看到的输出通常不是管道中对象所有属性的完整输出。这是怎么发生的呢?欢迎来到默认格式

PowerShell 根据管道中对象的 TypeName 来决定显示输出的格式。如果 TypeName 有关联的 PSCustomObject,那么 shell 会判断是否有默认属性集,如果有,就会显示这些属性。如果默认属性少于五个,它们将以表格形式显示;如果超过五个,它们将以列表形式显示。最后,如果对象既没有默认格式化视图,也没有默认属性集,那么所有属性将会显示。

我们可以在以下截图中看到 Get-Process 的默认格式化视图的一部分:

图 6.1 – System.Diagnostics.Process 对象的默认格式化视图的一部分

图 6.1 – System.Diagnostics.Process 对象的默认格式化视图的一部分

可以重新定义默认显示的属性,但我从未觉得有必要这么做,因此超出了本书的讨论范围。如果你觉得自己可能需要这么做,可以通过运行以下命令查看FormatData命令的帮助文件:

Get-Help *FormatData

不过,我们并不需要像更新FormatData那样进行剧烈的修改来改变输出格式。在本节剩余部分,我们将讨论三种常见的使用Format命令更改数据展示方式的方法。首先,让我们来看一下我最喜欢的Format-List

Format-List

我们使用的大多数命令会以表格形式显示对象名称及几个精选的属性。Format-List允许我们以列表形式显示对象的属性和值。请注意,Format-List会显示默认属性集中的属性,而不是默认格式视图中的属性(如果有视图的话)。查看以下截图以了解我们所指:

图 6.2 – 默认格式和列表格式

图 6.2 – 默认格式和列表格式

我们可以看到,单独运行Get-Process所得到的信息与将其通过管道传递给Format-List后得到的信息有很大不同。实际上,Format-List看起来并不好,因此我为什么会这么常用它呢?试着获取某个进程的信息,并结合通配符操作符(*)使用Format-List,如下所示:

Get-Process pwsh | Format-List *

看!很多信息。我们也可以通过传递以逗号分隔的属性列表给Format-List-Property参数来选择只显示特定的属性,如下所示:

Get-Process pwsh | Format-List -Property Name, Id, CPU, Responding

Format-List有一个别名fl,所以当我在故障排除服务器时,经常要求别人提供的许多信息都会在末尾加上|fl *

Format-Table

如果我们想要获取多个对象的数据子集,那么表格格式非常方便。如果我们希望不同于默认格式视图的数据展示,则可以使用Format-Table。试试这个:

Get-Process | Format-Table -Property Name, Id, CPU, Responding

这将以方便的表格格式为每个进程获取一部分属性。

当我们运行Format-Table时,往往不会看到我们请求的所有数据;相反,输出会被省略号()截断。幸运的是,Format-Table有两个其他参数可以帮助我们。首先,我们可以使用-AutoSize参数去除列间的空白,并将每列设置为其最大条目的宽度。如果仍然无法显示所有内容,我们可以使用-Wrap参数将每个条目显示为多行。试试这个,看看效果的不同:

Get-Process | Format-Table -Property Name, Id, CPU, Path, Modules
Get-Process | Format-Table -Property Name, Id, CPU, Path, Modules -Wrap

这非常方便。接下来我们来看一下最后一个格式化命令Format-Wide

Format-Wide

Format-Wide cmdlet 允许我们以宽列表格式显示数据,这使得在屏幕上阅读更加方便。它类似于标准的 Linux 列表输出(ls),但没有使列表命令如此有用的颜色规则。它只会显示每个对象的一个属性,默认显示 Name 属性,除非我们使用 -Property 参数指定其他属性。它有两个参数可以控制显示方式——-Column,指定列数,以及 -AutoSize,尽可能不截断数据地显示更多内容。试试看:

Get-ChildItem | Format-Wide
Get-ChildItem | Format-Wide -Column 5
Get-ChildItem | Format-Wide -AutoSize

就个人而言,我发现自己并没有像应该那样多使用 Format-Wide cmdlet。它对于简洁地查看对象列表非常有用,可以避免上下滚动寻找某个项目。

格式化陷阱

第三章PowerShell 管道 – 如何将 cmdlet 串联在一起,我们讨论了一个非常重要的规则:Format- cmdlet 会在屏幕上输出可读文本,这是因为它们生成的格式化数据对象由 shell 处理。除了将输出传递给一些专用的 Out- cmdlet(例如,Out-Default | Out-Host,它隐式地出现在每个 PowerShell 管道的末尾)之外,我们实际上无法在管道的其他地方使用 Format- cmdlet 的输出。

然而,我们还有一个非常有用的 Out- cmdlet 尚未介绍,我们将在下一节讲解它。

写入文本文件

我们在本章开始时承诺会讲解如何将 PowerShell 输出写入文件。现在我们来实现这个目标。我们已经了解到,我们可以将 Format- cmdlet 的输出传递给一个 Out- cmdlet,而用于将数据写入文件的特定 cmdlet 是 Out-File

Out-File 会将任何 cmdlet 的输出写入文本文件。如果我们想的话,可以先使用 Format- cmdlet 格式化输出,但也不一定需要这么做;如果不格式化,我们将得到 cmdlet 的默认输出。我们来试试看:

Get-Process | Out-File -FilePath C:\temp\poshbook\procsRaw.txt
Get-Process | Format-Table -Property Name, Id, CPU, Path, Modules | Out-File -FilePath C:\temp\poshbook\procsNoWrap.txt
Get-Process | Format-Table -Property Name, Id, CPU, Path, Modules -Wrap | Out-File -FilePath C:\temp\poshbook\procsWrap.txt

正如我们所看到的,文本文件的内容与屏幕输出完全相同;我们所做的只是将输出重定向到文件,而不是屏幕。

我们可以与 Out-File 一起使用的一些参数如下:

  • -FilePath 指定我们要创建的文件的路径和名称。默认情况下,Out-File 会覆盖任何具有相同名称的已存在文件,除非该文件是只读的。

  • -Append 将输出添加到现有文件中,再次提醒,如果文件不是只读的。

  • -NoClobber 将检查文件是否已存在,并防止其被覆盖。如果文件已经存在,我们将看到一个错误信息。

  • -Force 将允许我们忽略现有文件的只读属性。

  • -Encoding 可以将文件的默认编码从 ASCII 更改为其他多种格式;一种常用的格式是 UTF-8。

Out-File 还有几个别名,我们可能经常看到。大于符号(>)是 Out-File -FilePath 的别名,我们只需要在后面输入路径和文件名。双大于符号(>>)是 Out-File -Append -FilePath 的别名,同样只需要路径和文件名。我们不能使用其他参数与这些别名一起使用。例如,Get-Process > C:\temp\processes.txt 会将 Get-Process 的输出保存到 C:\temp\processes.txt,如果我们接着运行 Get-Date >> C:\temp\processes.txt,它会将 Get-Date 的输出追加到同一个文件中。

Out-File 只会生成一个文件。它不会将任何对象放入管道,因此必须是管道中的最后一个 cmdlet。

首先要记住的一个重要事情是,写入文件的内容是完全与我们在屏幕上看到的一样的。

另一个重要的事情是,我们用 Out-File 创建的文件是文本文件。我们可以把它称作 CSV 文件,但它并不是。数据不会被转换,也不会插入逗号。我们来演示一下:

Get-Process | Format-Table -Property Name, Id, CPU, Path, Modules -Wrap | Out-File C:\temp\poshbook\procsWrap.csv

正如预期的那样,上一行创建的 CSV 文件的内容看起来非常糟糕。如果我尝试在 MS Excel 中打开它,这是我的常用应用程序来处理 CSV 文件,它看起来就是这样:

图 6.3 – 这不是一个 CSV 文件

图 6.3 – 这不是一个 CSV 文件

接下来我们将看看如何使用 ConvertTo-Export- cmdlet 生成不同类型的文件。

使用 ConvertTo- 和 Export- cmdlet

如果我们希望我们的 PowerShell 对象以文本以外的格式出现,我们需要以某种方式处理它们。这里有两个 cmdlet 组可以让我们做到这一点。ConvertTo- cmdlet 会将我们管道中的对象转换为某种特定类型的数据,比如 CSV 或 HTML 数据。Export- cmdlet 则将等效的 ConvertTo- cmdlet 与 Out-File 功能结合,生成相关类型的文件。在本章中,我们将介绍三种常见的文件类型:CSV、XML 和 HTML。

CSV

让我们首先看看 ConvertTo-CSV cmdlet。通过这个 cmdlet,我们可以将另一个 cmdlet 的输出转换成 CSV 数据流。试试看:

Get-Process | ConvertTo-Csv

你会看到类似下面的截图:

图 6.4 – 丑陋

图 6.4 – 丑陋

呜,糟糕吧?不过,我们可以将其通过管道传递给Out-File,如下所示:

Get-Process w* | ConvertTo-Csv | Out-File C:\temp\poshbook\ProcessesConvertTo.csv

它会生成一个真实的 CSV 文件,包含所有以 w 开头的进程,可以在支持 CSV 格式的应用程序中打开,比如 MS Excel 或 Google Sheets。不仅如此,它还会忽略默认的格式信息,直接将所有内容输出。

更好的是,有一个 cmdlet 可以在一个操作中完成整个过程——Export-Csv

Get-Process | Export-Csv C:\temp\poshbook\ProcessesExport.csv

让我们再仔细看看。

ConvertTo-Csv

这个 cmdlet 有一些我们可能感兴趣的参数:

  • -NoTypeInformation:我们在互联网上看到的很多脚本会包含此参数,但现在不再需要它。早于 PowerShell 6.0 时,ConvertTo-Csv cmdlet 默认会包含一行介绍,详细说明已转换的对象类型,因此为旧版本 PowerShell 编写的脚本几乎总是会包含此参数来移除该行。自 PowerShell 6.0 起,默认设置是不包括类型信息。包含此参数不会造成问题,但在 PowerShell 7.x 中我们不再需要它。

  • -IncludeTypeInformation:如果你确实希望在 CSV 流的第一行了解对象类型,可以使用此参数。

  • -Delimiter:我们通常希望 CSV 文件使用逗号作为字段分隔符字符,但有时可能需要使用 `t,或者使用冒号或分号。我们可以使用此参数来指定不同的分隔符字符—例如,在以下情况下:

    DateTime objects, which in some "DateTime"; "Monday, August 6, 2022 17:55".
    
  • -UseQuotes:此参数控制 cmdlet 如何应用双引号字符。默认情况下,每个值都会被双引号包围,将其转化为字符串,但有时我们可能希望将值保留为整数或浮动点数。我们可以使用此参数来控制引用行为。

  • -QuoteFields:如果我们需要更精细的控制引用行为,可以指定要双引号包围的字段。CSV 文件的第一行是字段名称,可能需要一些试验才能正确设置。

一旦我们将对象转换为 CSV 数据流,可以将其通过管道传递给 Out-File 或以其他方式使用;例如,我们可能希望创建多个 CSV 对象,然后聚合和操作它们,最后输出一个单一的文件。

Export-Csv

Export-Csv cmdlet 将 ConvertTo-CsvOut-File 合并为一个 cmdlet。它具有我们已经讨论过的与这些 cmdlet 相匹配的参数。以下是与 Out-File 匹配的参数:

  • -Path(代替 -FilePath

  • -``Force

  • -``NoClobber

  • -``编码

  • -``Append

这是 ConvertTo-Csv 的参数:

  • -``Delimiter

  • -``NoTypeInformation

  • -``IncludeTypeInformation

  • -``QuoteFields

  • -``UseQuotes

Export-Csv 的参数与 ConvertTo-CsvOut-File 中的对应参数一样,Export-Csv 没有别名。

XML

ConvertTo-XmlExport-Clixml

ConvertTo-Xml

此 cmdlet 只有少量的参数:

  • -As:此参数允许我们指定输出格式;无论是单个字符串、字符串数组,还是 XmlDocument 对象。

  • -Depth:我们可以使用此参数限制文档可以包含的层级和子层级的数量。默认情况下,未指定此参数。

  • -NoTypeInformation:这会阻止对象类型信息被写入。

Export-Clixml

Export-Clixml cmdlet 非常简单。它有一个 ConvertTo-Xml 参数 -Depth,然后是以下 Out-File 参数:

  • -Path (而不是 -FilePath)

  • -``Force

  • -``NoClobber

  • -``编码

让我们创建一个可以稍后使用的 XML 文件:

Get-Date |Export-Clixml -Path C:\temp\poshbook\date.xml

尝试在记事本或 VS Code 中打开它。你应该会看到类似以下内容:

图 6.5 – 作为 XML 表示的日期

图 6.5 – 作为 XML 表示的日期

关键部分是第 3 行。那才是实际的数据,其余的是格式信息和对象类型的信息。保留这个文件,稍后我们将在本章中使用它。

HTML

能够将我们的 PowerShell 输出转换为 HTML 非常有用,因为我们可以在网页浏览器中显示它。这是系统管理中编写报告的流行技术。我们可以用它来生成仪表板和其他在线报告。虽然有更好的方法可以创建可以被 Web API 使用的数据,但我们将在下一章中讨论这些方法。创建 HTML 代码的 cmdlet 只有一个;ConvertTo-Html。如果我们想将其写入文件,我们需要使用Out-File。试试这个:

Get-Date | ConvertTo-Html | Out-File -FilePath C:\temp\poshbook\date.html

如果你在浏览器中打开Date.html,你应该会看到类似这样的内容:

图 6.6 – 作为 HTML 的日期

图 6.6 – 作为 HTML 的日期

这并不特别有启发性。让我们仔细看看这个 cmdlet,看看是否能让它更有趣。

ConvertTo-Html

ConvertTo-Html cmdlet 有几个有趣的参数,我们可以用来格式化输出:

  • -As:这个参数允许我们将输出格式化为表格或列表。默认情况下,我们得到的是表格。

  • -Body:这允许我们在打开的<body>标签后添加文本。这是任何网页的主体部分。我们这里不会解释 HTML 的工作原理——如果你想复习 HTML,可以参考www.w3schools.com/这个优秀且免费的在线资源。

  • -Head:这允许我们在<head>部分写入文本。

  • -Title:这允许我们为页面指定一个标题。与-Body-Head不同,它只接受一个字符串。

  • -PreContent:这允许我们在创建的表格之前写入文本。

  • -PostContent:这允许我们在表格后添加文本。

  • -Meta:这允许我们向<head>部分添加元标签。

  • -Fragment:这会省略<head><body>标签,仅编写 HTML 以生成表格。如果我们将多个片段合并为一个网页,这非常有用。

  • -CssUri:这允许我们指定一个 CSS 文件,以提供额外的格式设置,如不同的字体、背景颜色等。我建议你访问www.csszengarden.com/来看看我们可以用 CSS 做的一些惊人事情。

让我们来玩一下。打开 VS Code 中新建一个文件,选择 CSS 作为语言,并严格按照以下内容输入:

Table {
    color: blue ;
    text-align: center;
    background-color: bisque;
}
Body {
    background-color: cadetblue;
    font-family: 'Trebuchet MS';
    color: yellow;
}

将它保存为style.css,存放在你的工作目录C:\temp\poshbook中。

在同一目录下创建一个新的 PowerShell 文件并试试这个:

$params = @{
    As = 'List'
    Title = 'My Date Page'
    Body = 'Here is my Date'
    CssUri = 'Style.css'
}
Get-Date | ConvertTo-Html @params | Out-File C:\temp\poshbook\FancyDate.Html
C:\temp\poshbook\FancyDate.html

如果你选择并运行你的代码,你应该会看到以下输出:

图 6.7 – 一个漂亮的 HTML 日期

图 6.7 – 一个漂亮的 HTML 日期

这里有一篇关于如何在 PowerShell 中编写 HTML 文档的精彩文章:leanpub.com/creatinghtmlreportsinwindowspowershell/read

这是创建文件的一个良好开端。接下来,我们简要介绍一下 PSProvidersPSDrivesNew-Item cmdlet。

处理文件

在我们能处理文件之前,我们需要能够找到它们,并了解 PowerShell 如何处理诸如文件系统之类的层次结构数据结构。

关于 PSProviders 和 PSDrives 的简要说明

PowerShell 通过名为 提供程序 的软件连接到数据结构。这些提供程序使 PowerShell 能够将数据结构呈现为文件系统(包括文件系统本身)。在 Windows 上,这使得我们可以像操作文件一样搜索和操作注册表和证书存储中的对象。可以说,在 Linux 上它的用途较小。让我们来看一下。运行以下命令:

Get-PSProvider

你应该能看到提供程序及其关联驱动器的列表。我们可以使用 Set-Location cmdlet 连接到关联驱动器:

Set-Location Env:

请注意,我们必须使用驱动器的名称,而不是提供程序的名称,并且需要在后面加上冒号。

一旦进入数据结构,我们可以使用 Get-ChildItem 来搜索对象,如下图所示,这列出了环境变量,就像它们是文件系统对象一样:

图 6.8 – 环境提供程序

图 6.8 – 环境提供程序

提供程序是动态的;也就是说,它们不需要在连接之前知道存储中的内容,因此它们作为提供访问数据结构的方式非常流行。虽然默认情况下 PowerShell 只包含少数几个提供程序,但你会发现一些模块会添加新的提供程序,特别是在 Windows 上。例如,如果我们安装了 ActiveDirectory 模块,那么我们将可以访问一个包含 Active Directory 数据的 AD: 驱动器。

请注意,仅仅安装提供程序是不够的。我们还需要有一个驱动器对象才能浏览它。驱动器是我们工作的对象,而提供程序是让我们访问它的软件。大多数模块会创建所需的驱动器并安装提供程序。我们可以非常容易地创建自己的驱动器。在我的 Ubuntu 机器的主目录中,我有一个名为 MyFiles 的目录。我可以通过运行以下命令将其创建为驱动器:

New-PsDrive -Name MyFiles -Root /home/nickp -PSProvider FileSystem

然后,我可以像这样浏览 MyFiles: 驱动器:

图 6.9 – 创建一个新的驱动器

图 6.9 – 创建一个新的驱动器

项目 cmdlet

我们使用 -Item-Childitem cmdlet 在文件系统中移动并查找项。这里不做详细介绍,因为它们是自解释的。阅读相关帮助文件以了解它们简单的语法:

  • Get-ChildItem 列出位置中找到的项目:

    Copy-Item lets us make a copy of an item in a different location:
    
    

    Move-Item 允许我们将项目移动到不同的位置:

    Rename-Item lets us change the name of an item:
    
    

    Remove-Item 允许我们删除一个项目:

    Get-Item lists the item and its value; it doesn’t get the contents. Most of the time, we want to use Get-ChildItem instead when we’re working in the filesystem, but if we wanted to know the value of an environment variable, then we would use this cmdlet. Try this. Type the following:
    
    

    Get-Item 返回了 C:\temp\poshbook 文件夹的属性,而 Get-ChildItem 返回了该文件夹及其内容。

    
    
    
    
    
    
  • Set-Item:同样,在文件系统中这不是特别有用。然而,如果我们想要更改环境变量或别名的值,则需要使用此 cmdlet。

  • New-Item:我们可以用它来创建文件和文件夹,以及创建新的别名和变量:

    Invoke-Item: Performs the default action on an item. For instance, if it’s an executable file, it will run it. If it is a TXT file, it will open the file in the default text editor:
    
    

    Invoke-Item -Path C:\temp\poshbook\FancyDate.html

    
    

还有两个 cmdlet 用于在驱动器之间和驱动器内部移动;Get-LocationSet-Location。许多这些 cmdlet 都有与之关联的别名,模拟等效的 bash 和 Windows 命令——例如,Get-ChildItem 可以通过 lsdir 调用。不过请注意,参数仍然是 PowerShell 的,而不是 bash 或 Windows 命令的。例如,Dir /p 并不会生成分页目录列表,而是会产生一个错误。

现在我们知道如何找到文件并移动它们了,让我们看看文件里面有什么内容。

操作文件

使用 PowerShell 读取文件内容并操作数据是一个常见任务。在这一部分中,我们将介绍三个 cmdlet。首先,我们来了解一下通用的 Get-Content cmdlet。

Get-Content

Get-Content cmdlet 可以获取文件中的任何类型数据,并将其存储为字符串或字节的数组(字节流)。默认情况下,文件中的每一行将被解释为单个字符串。只有当文件可以被解释为字符串数组时,我们才能获得有意义的内容;否则,我们必须将其作为字节流获取。这在我们获取可执行文件的内容时非常有用。它有几个参数,下面是一些较为重要的:

  • -Path 指定了我们想要的项目。它接受一个字符串数组,这样我们就可以将多个文件连接成一个单一的数组。

  • -TotalCount 是从文件中读取的总行数。默认值是 -1,表示读取到最后一行,但我们可以只检查文件的前几行,以确保文件内容符合预期,而不需要加载整个文件。

  • -ReadCount 是每次通过管道发送的行数。

  • -AsByteStream 允许我们将内容作为字节而非字符串获取。

  • -Credential 允许我们提供替代凭据以打开文件。

  • -Delimiter 允许我们指定用于分隔字符串的字符。默认情况下,它是换行符(`n),但我们可以使用任何字符。

  • -Encoding 可用于指定内容使用与默认格式不同的编码;没有字节顺序标记的 UTF-8 编码 (utf8NoBOM)。

  • -Raw 将返回整个内容作为一个单一字符串。

  • -Tail 类似于 -TotalCount,但它是从文件的末尾向回读取行。不能与 -TotalCount 一起使用。

Get-Content 的功能类似于 Linux 中的 cat 命令,毫不意外的是,cat 是它的别名。

Get-content 是将数据导入 PowerShell 以便我们操作的最常见方法之一,但它的灵活性较差。例如,如果我们尝试获取一个 CSV 文件,它会将该文件作为文本文件解释,而不是作为一个特殊格式化的文件。让我们看看如何以结构化的方式导入信息。

Import- cmdlet

Export- cmdlet 一样,Import- cmdlet 同时执行两个操作。它们将 Get-Content 与专门的解析器结合,解析器会解释信息并根据原始文件类型正确地格式化它,使我们能够操作看起来像原始对象的内容。让我们先来看看 Import-Csv

Import-Csv

Import-Csv cmdlet 只有几个参数:

  • -Delimiter:以防文件使用除逗号以外的其他字符分隔值。

  • -Path:用于标识要导入的文件。

  • -Header:如果你的 CSV 文件没有列名,可以手动指定列名信息。

  • -Encoding:如果文件来自一个没有输出为 utf8NoBOM 的系统。

该 cmdlet 期望文件包含字符串对象,并输出带有与原始文件中列标题匹配的注释属性的 PSCustomObjects。我们来试试看:

Get-Process | Export-Csv processes.csv
$procs = Import-Csv processes.csv
$procs.name

这将输出数组中每个对象的名称值。接下来的命令将展示我们没有重建进程对象,而是创建了新的 PSCustomObjects

$procs | Get-Member

以下命令将检索数组中的第二个元素。由于没有默认视图或默认属性列表,所有属性将会显示:

$procs[1]

你应该会看到类似这样的内容:

图 6.10 – 导入 CSV 文件

图 6.10 – 导入 CSV 文件

将 CSV 文件导入 PowerShell 非常方便,例如,当你有一份用户列表,并且需要通过循环获取他们的主驱动器大小,或者你想创建一个查找表时。

Import-Clixml

如果你有一个 PowerShell 生成的 XML 文件,你可以使用 Import-Clixml cmdlet 来重建看起来更像原始对象的内容。这个 cmdlet 并没有太多参数。以下是一些最有趣的参数:

  • -Path 指定要导入的文件。

  • -Skip 允许我们跳过文件中的指定数量的对象。

  • -First 只获取文件开头指定数量的对象。Clixml 文件可能非常大,因此如果我们想检查文件中的内容,这个参数非常有用。

  • -IncludeTotalCount 告诉我们文件中有多少个对象。有趣的是,它有一个精度属性,告诉我们估算值的可靠性。

让我们玩一下。试试这个:

Get-Date | Export-Clixml -Path date.xml
Notepad date.xml
$date = (Import-Clixml -Path date.xml)
$date | Get-Member
$date

我们可以看到返回的是一个日期对象。再试一个例子:

Get-Process | Export-Clixml processes.xml
$XmlProcs =Import-Clixml processes.xml

如果我们调用 $XmlProcs,可以看到输出显示与我们期望的 Get-Process cmdlet 完全一致:

图 6.11 – 使用 Import-Clixml 重建对象

图 6.11 – 使用 Import-Clixml 重新创建对象

然而,它们并不是我们导出的对象。让我们运行以下命令:

$XmlProcs | Get-Member

我们可以看到它们是Deserialized.System.Diagnostics.Process对象。这告诉我们它们已经被转化为数据对象,并从文件中恢复,它们不是实时对象。

这个方法最常见的用途之一是安全地存储凭证。试试看:

$cred = Get-Credential

我们被要求输入用户名和密码。

我们可以将凭证变量传送到 XML 文件中:

$cred | Export-Clixml Credential.xml

该文件现在可以存储在某个磁盘上。它是通过Export-Clixml cmdlet 加密的。

然后我们可以使用Import-Clixml将其导入:

$newcred = Import-Clixml Credential.xml

我们得到一个凭证对象。你可以在以下截图中看到 XML 文件的样子。密码是加密后的十六进制字符串:

图 6.12 – 存储加密凭证

图 6.12 – 存储加密凭证

请注意,加密使用了我们加密文件时所在机器的登录凭证。其他用户无法轻易解密这些文件,而且我们无法在另一台机器上解密这些凭证。

我们不能用Import-Clixml导入任何 XML 文件。如果我们尝试导入一个没有正确格式化的文件,会出现如下错误:

图 6.13 – 我们不能导入任何东西

图 6.13 – 我们不能导入任何东西

在这种情况下,XML 文件的架构对Import-Clixml cmdlet 是错误的;正如我们从错误信息中看到的,缺少了重要的行。不要尝试运行这段代码——你可能没有 cmdlet 中提到的 DISM 文件。

本章的内容大致就是这些。不过,在总结之前,让我们做些练习,巩固一下本章以及之前章节的学习内容。

让我们玩得开心一点 – 测量文本文件中最频繁的单词

我最喜欢的一本编程书是 Cristina Videira Lopes 的《编程风格练习》。它的灵感来源于 Raymond Queneau1947 年的书《风格练习》,在这本书中,作者以 99 种不同的风格讲述同一个故事。Lopes 的书包含了 41 个 Python 程序,用不同的编程风格完成相同的任务。这本书真的是开阔了我的思维,彻底改变了我对编写代码的看法;它和任何一种创造性写作一样,都是一种艺术。虽然这本书本身不便宜,但所有程序都可以在 GitHub 上找到:github.com/crista/exercises-in-programming-style。即使你不了解 Python,我也鼓励你快速浏览一下。

每个程序解决的问题是确定文本文件中单词的频率,并按降序排序;这是一个词频问题,通常是计算机科学学生常见的练习。让我们试试看:

  1. 首先,在 VS Code 中创建一个新文件夹,然后在该文件夹中创建一个新的 PowerShell 文件,文件名为wordcountBasic.ps1

    我将使用的文件是弗朗茨·卡夫卡经典作品《审判》的英文翻译,但任何大型文本文件都可以。我是从这里下载的:www.gutenberg.org/ebooks/7849.txt.utf-8

    继续下载并将其保存到新文件夹中,文件名为thetrial.txt

  2. 现在我们有了数据,接下来的任务是将文件内容放入一个变量中。这很容易。我们需要将其作为一个巨大的单个字符串,而不是一个字符串数组:

    $TheTrial  =  Get-Content -Path .\thetrial.txt -Raw$TheTrial = get-content -path .\thetrial.txt -Raw
    
  3. 现在,我们可以使用字符串对象中可用的某种方法,将文本拆分成单个单词的数组:

    $TrialWords = $TheTrial.Split(" ")
    

    请注意,双引号内有一个空格。

  4. 让我们看看我们有多少个单词:

    $TrialWords.Length
    

    我得到了 79,978 个单词。此代码可能需要一些时间才能运行。

  5. 现在...接下来的部分有点棘手。我们需要计算每个单词在文本中出现的次数。这是 Shell 真正展现其强大功能的地方。在 Python 中做下一步需要几行字符串处理代码,但 PowerShell 自带一个内置 cmdlet,Group-Object

    $GroupedWords = $TrialWords | Group-Object
    
  6. 那一行的问题在于,它会按字母顺序对对象进行分组,而这不是我们想要的。我们希望按频率分组,因此我们还需要在管道中使用Sort-Object cmdlet:

    $GroupedWords = $TrialWords | Group-Object | Sort-Object Count
    
  7. 我们也不想看到所有的单词,只想看到最常出现的单词。我们来获取最常出现的 10 个单词。我们可以使用[-1..-10]的公式从数组中提取它们。我们使用[-1..-10]是因为默认情况下,排序顺序是升序的,而我们希望它是降序的。我们可以运行$GroupedWords[-1..-10]

  8. 就这样!这是我整个程序和输出:

图 6.14 – 任何英文书籍(由男性作者写的)中最常见的十个单词

图 6.14 – 任何英文书籍(由男性作者写的)中最常见的十个单词

我们在这里取得了一些成果,尽管这可能不是我们想要的结果。在我看来,这就像是任何一本英文书籍中最常出现的 10 个单词,都是由男性写的。我们能对此做些什么呢?

计算机科学领域的自然语言处理涉及停止词的概念。这些是语言中常见的词汇,在分析文本时不应被计算在内。网上有许多免费的停止词列表。我们需要做的是构建一个循环,将数组中的每个单词与停止词列表进行比较,并且仅在单词不在列表中时,将其添加到新数组中。

活动

使用循环重写程序,将$TrialWords数组中的每个单词与停止词列表中的单词进行比较。使用raw.githubusercontent.com/stopwords-iso/stopwords-en/master/stopwords-en.txt中的停止词文件。

提示 1:

你不会想用 PowerShell 数组来做这个 —— 为什么?最好使用一个数组列表。可以这样构建它:

$Words = [****System.Collections.ArrayList]@()

提示 2:

你需要将数组按多个字符进行拆分,而不仅仅是空格,因为有些单词后面紧跟着句号和其他标点符号。对于某些字符,你需要使用转义字符——反引号(`)——才能正确解析它们。我发现将前面代码中的第二行替换为这一行是有效的:

$TestWords = $TheTrial.Split(“ “, “t”, “n”, “,”,”`””,”.”, [****System.StringSplitOptions]::RemoveEmptyEntries)

RemoveEmptyEntries 防止空字符串被计算在内。

't 是制表符。

`n 是换行符。

`" 允许我们使用双引号作为分隔符。

提示 3:

Thatthat 是同一个词吗?你需要确保你的单词中所有的字符都使用相同的大小写。

玩得开心!我在最后写了一种做法。

总结

这一章内容较长,实际上只是一个介绍。我们会在后续的章节中进一步探讨如何使用 PowerShell 导入数据。虽然我们已经走了很长一段路,但仍有很多内容要学习。

我们从学习如何使用三种常见的格式化 cmdlet 来格式化屏幕输出开始:Format-ListFormat-TableFormat-Wide。接着我们学习了如何使用 Out-File 将格式化的数据输出到文本文件。我们花了一些时间理解这种方法的局限性,然后才探索了两类 cmdlet:ConvertTo-Export-

我们深入研究了处理 CSV 文件的 cmdlet:ConvertTo-CsvExport-Csv,并理解了 ExportTo-Csv cmdlet 如何结合 ConvertTo-CsvOut-File cmdlet。接着我们研究了 ConvertTo-XmlExport-Clixml,最后,我们看了 ConvertTo-Html 并尝试如何利用它通过引用 CSS 文件来生成格式有趣的文档。

接着我们简要讨论了 PSProvidersPSdrives,并理解了它们在 Windows 环境下更为有用,但在 Linux 和 macOS 下仍然相关。

我们研究了 Get-Content,这是将数据导入 PowerShell 的最常见方法,并了解到它会生成一个字符串或字节的数组——为了以更结构化的方式导入数据,我们需要使用其他 cmdlet:Import-。我们研究了 Import-CsvImport-Clixml,并看到了它们如何用于从结构化数据构建 PowerShell 对象。

最后,我们进行了一些编程,研究了如何使用 PowerShell 分析文本文件。

在下一章中,我们将学习如何使用 PowerShell 与互联网上的系统进行交互,以及我们将需要的常见文件格式。

练习

  1. 如何在你的临时目录中生成一个包含三列的文件名广泛列表?

  2. 如果我们运行这段代码,会发生什么?

    "I love PowerShell" string to a new file in the directory you are working in.
    
  3. 问题 3中的文件添加一个字符串“Sooo much”。

  4. 写一条语句,将工作目录中的所有项目创建为一个 CSV 文件,但用分号而非逗号分隔。

  5. 在你的 PowerShell 会话中定义了多少个函数?

  6. 如何使用空格作为字符串分隔符来导入问题 4中的文本文件?

  7. 如何导入你在问题 5中创建的文件,你会得到什么类型的对象?

  8. 以下错误告诉我们什么?

    Import-Clixml: Element 'Objs' with namespace name 'http://schemas.microsoft.com/powershell/2004/04' was not found.
    

进一步阅读

我们真的应该阅读本章中使用的所有 cmdlet 的帮助文件。我们知道如何找到它们,因此这里不再列出。

第七章:PowerShell 与 Web——HTTP、REST 和 JSON

直到现在,我们一直在一个非常小的空间里工作——我们的客户端机器。虽然这样很方便,但世界并不是这样的。几乎我们使用的每个设备都与其他机器连接,并且通常连接到互联网。设备需要能够与通过网络提供的服务进行交互,以下载数据、与云应用程序互动并玩游戏。在本章中,我们将探讨如何使用 PowerShell 与我们盒子外部的对象进行交互。

我们将首先简要讨论 Web 服务,然后介绍与之交互的基本 PowerShell 工具:Invoke-WebRequest。接下来,我们将介绍 Invoke-RestMethod,并探讨一些使用 JavaScript 对象表示法JSON)与 Web 服务交换信息的方法,以及我们用来将 JSON 数据转换为 PowerShell 对象的工具,反之亦然。最后,我们将进行一个简短的练习。

本章将为我们提供一些在后续章节中所需的基本技术,并建立在我们上一章所做工作的基础上。我们将看到,这些技术同样适用于网页上的数据。

本章我们将涵盖以下主要内容:

  • 使用 HTTP

  • 理解 API

  • 使用 REST

  • 使用 JSON

使用 HTTP

我们所生活的世界有一个主导哲学,一套概念和实践,基本上定义了过去 30 年的信息技术;即客户端/服务器范式。也有其他选择;如通过傻终端进行的集中式计算(例如大型机计算),使用 Citrix 等应用程序的瘦客户机计算,或者像 BitTorrent 或区块链应用程序中可能看到的点对点计算。蒂姆·伯纳斯-李(Tim Berners-Lee)设想的万维网是一个点对点网络,但它实际上并没有保持那样。客户端/服务器模型是普遍存在的。一般来说,我们桌面或手中使用的设备是客户端,它通过远程服务器接收或处理信息。与傻终端的区别在于,部分处理是在客户端完成的,部分处理则在服务器上进行。

在本章中,我们将探讨如何通过互联网使用万维网的基础协议——超文本传输协议HTTP)——在客户端和服务器之间交换信息。这就是我们用来浏览网站的协议。

在 PowerShell 中,处理 HTTP 的基本 cmdlet 是 Invoke-WebRequest,让我们看看如何使用 Invoke-WebRequest cmdlet 来处理网页上的数据。

Invoke-WebRequest cmdlet 是一个功能极其强大的工具,允许我们创建 HTTP 请求并提交到 Web 服务。由于我们可能发出的请求范围非常广泛,这个 cmdlet 相当复杂,有超过三十个参数。让我们从简单的开始,获取一个相关新闻主题的超链接列表。在 Web 浏览器中,访问此地址:neuters.de。这是一个基于文本的网站,聚合了最新的路透社新闻文章。它简单而清晰。

现在,打开 PowerShell 并输入以下内容:

$News = Invoke-WebRequest https://neuters.de

这将向该地址发送GET请求,并将响应存储在$``News变量中。

现在让我们通过调用$News的内容来看我们得到了什么。输入以下内容:

$News

你应该能看到类似于以下图形的内容:

图 7.1 –  获取 HTTP 响应

图 7.1 – Invoke-WebRequest获取 HTTP 响应

这很好,但不太美观。如果我们稍微仔细看一下输出,可以看到其中有一部分叫做Links。我们可以使用这个来仅通过调用它来获取页面上的超链接,方法如下:

$News.Links

不幸的是,它们都是相对链接,但我们应该通过添加根位置将它们转换为绝对链接,这我们可以很容易做到。如果我们将$News.Links的输出管道到Get-Member,我们可以看到它有一个名为href的属性,其中包含链接,如下所示:

图 7.2 – href 属性

图 7.2 – href 属性

我们可以将其与根域名连接。输入以下代码:

$News.Links | Foreach {"https://neuters.de" + $_.href}

你应该会看到一列绝对链接,如下图所示:

图 7.3 – 从 neuters.de 网站抓取链接

图 7.3 – 从 neuters.de 网站抓取链接

如果你得到了类似的结果,恭喜你,你刚刚用 PowerShell 抓取了你第一个网站。抓取是自动化从网站获取数据的过程。

Invoke-WebRequest只有一个必需的参数;-Uri。它是位置参数,因此紧跟在 cmdlet 后的任何字符串将被视为此参数的输入。–Uri参数接受源的 URL。

URI 与 URL

我们可能都很熟悉缩写 http:https:,所以绝对 URL 就是neuters.de,包含了方案(https:)和地址(neuters.de),并且两者之间由双斜杠(//)分隔。

那么 URN 呢?URN 由方案(urn:)和一个或多个标识符(例如 urn:ietf:rfc:1149)组成,标识了我最喜欢的协议定义。

其中的关键是,-Uri 参数仅支持 http:https: 方案,因此只能接受 URL。然而,根据万维网联盟(W3C),即国际网络标准化机构,URI 是比 URL 更精确、更技术性正确的术语。

让我们更仔细地看一下可以与Invoke-WebRequest一起使用的一些参数:

  • -Method 将接受任何标准的 HTTP 方法:GETPUTPOSTDELETE 等。如果我们省略此项,则默认方法为 GET。如果我们需要使用自定义方法,则可以使用 -CustomMethod 参数。我们不能在同一个 cmdlet 中同时使用 -Method-CustomMethod。此参数允许我们将信息发送到 Web 地址。

  • -OutFile 指定一个路径和名称,将输出写入文件。请注意,这只会将响应 HTML 写入文件,而不会写入诸如响应代码之类的内容。此参数会阻止输出被放入管道中。如果我们希望输出既可以进入管道,又可以写入文件,则需要同时使用 -PassThru 参数。

  • -Headers 可以用于提交特定的头部信息作为 Web 请求的一部分。信息必须以哈希表或字典的形式提供。

  • -Body 可以提交特定的主体内容,如查询。我们不能在同一个 cmdlet 中同时使用 -Body-Form

  • -Form-Form 参数用于向目标地址的 HTML 表单提交信息。我们可以在网上找到许多关于如何使用 -Form 参数快速且轻松地通过 Windows PowerShell 登录网站的资料。不幸的是,由于 HTML 解析方式不同,大多数方法无法在 PowerShell 7 中工作。稍后我们将了解原因。

  • -Proxy 允许我们指定一个替代的代理服务器,而不是客户端上设置的代理。通常与 -ProxyUseDefaultCredentials 参数一起使用,后者将当前用户凭据传递给代理,或者与 -ProxyCredentials 参数一起使用,后者允许我们指定替代的凭据。

  • -NoProxy 让我们完全绕过客户端代理。我们不能在与 -Proxy 的同一 cmdlet 中使用此项。

  • -SessionVariable 可用于连接到有状态的 Web 服务;我们可以在第一次运行 Invoke-WebRequest 时使用 -SessionVariable,然后在每个后续连接中使用相同的值和 -WebSession 参数,以确保会话状态的持续性。如果我们需要先登录 Web 服务再使用它,这个功能很有用。请注意,我们将字符串传递给 -SessionVariable,但之后将该字符串作为变量传递给 -WebSession,如下所示:

    Invoke-WebRequest -Uri https://reddit.com -SessionVariable sv
    -SkipCertificateCheck will ignore SSL/TLS certificate problems. For obvious reasons, we shouldn’t use this parameter unless we are sure that the site we are connecting to is secure, and any certificate problems we see are not a sign of a problem. Sometimes, a certificate problem is fairly innocuous and may be down to a harried admin who hasn’t renewed their certificate, and sometimes it’s malicious.
    
  • -SkipHttpErrorCheck:默认情况下,如果 Invoke-WebRequest 收到 HTTP 错误代码作为响应,它将把错误以红色报告出来,而不是作为输出。如果我们想捕捉错误响应并处理它,那么可以使用此参数。有关示例,请参见下图:

图 7.4 – 使用 -SkipHttpErrorCheck

图 7.4 – 使用 -SkipHttpErrorCheck

在这里的图示中,第一个 cmdlet(红框中的部分)没有使用 -SkipHttpErrorCheck 参数,因此 PowerShell 将响应视为错误。在第二个 cmdlet(绿框中的部分)中,我们使用了该参数,可以看到服务器实际发送的 HTTP 响应。

还有另外三十个可以与 Invoke-WebRequest 一起使用的参数,但这些是最常用的参数。让我们来看看为什么 -Form 参数在 PowerShell 7 中不那么有用。

为什么在 PowerShell 7 中看不到 Forms 信息?

如我们所提到的,-Form 参数在 PowerShell 7 中的效果不如在 Windows PowerShell 中那样好。如果我们使用 Get-Member 查看 Invoke-WebRequest 返回的对象,我们在 PowerShell 7 中看不到 Forms 属性,而在 Windows PowerShell 中可以看到。值得仔细研究并了解其中的原因。Invoke-WebRequest cmdlet 在 PowerShell 7 和 Windows PowerShell 中返回的是不同类型的对象。我们可以在以下图示中看到输出的差异。在上方的蓝色框中,我们看到 Windows PowerShell 返回的对象 TypeNameHtmlWebResponseObject,并且可以看到该类型的属性中包括 Forms

图 7.5 – Windows PowerShell 中的 Invoke-WebRequest

图 7.5 – Windows PowerShell 中的 Invoke-WebRequest

在 PowerShell 7 中,接下来我们看到的是 BasicHtmlWebResponseObject,它没有 Forms 属性:

图 7.6 – PowerShell 7 中的 Invoke-WebRequest

图 7.6 – PowerShell 7 中的 Invoke-WebRequest

这是因为 PowerShell 7 默认使用基本解析。

在 Windows PowerShell 中,我们可以使用网页中 Forms 属性包含的信息来创建一个变量,该变量的属性与表单字段匹配,然后使用 -Form 参数将该变量作为 cmdlet 的一部分提交。由于 Invoke-WebRequest 返回的对象没有名为 Forms 的属性,许多网上关于如何使用该 cmdlet 的说明在 PowerShell 7 中将无法正常工作。

这并不意味着我们不能使用 Invoke-WebRequest 来填写表单。我们只需要提前了解表单字段是什么;单靠 PowerShell 7,我们可能难以弄清楚。我们还需要注意,许多网上的示例是为 Windows PowerShell 版本的 Invoke-WebRequest cmdlet 编写的。

认证

许多网站使用某种形式的认证,因此认证是 Invoke-WebRequest 的一个重要部分。首先要做的是设置使用的认证类型。我们可以使用 -UseDefaultCredentials 参数尝试使用当前环境的登录凭据进行认证,但如果我们访问的是外部地址而非我们组织内部的地址,那么我们可能需要提供特定于该站点的信息。-Authentication 参数接受以下选项:

  • None:这是默认选项。如果没有填写 -Authentication 参数,则 Invoke-WebRequest 将不会使用任何认证。

  • Basic:这将发送一组 Base64 编码的凭证。希望我们连接的是 HTTPS 网址,因为 Base64 编码与加密不同。这不是一种非常安全的发送凭证的方式。我们还需要使用-Credential参数来提供用户名和密码。

  • Bearer:这要求提供一个承载令牌,并通过-Token参数以安全字符串的形式传递。令牌基本上是一个长字符串,用于标识请求的发送者。承载身份验证是一种 HTTP 身份验证方案,是 OAuth 2.0(开放授权标准)的一部分。然而,并非所有承载身份验证都是 OAuth。由于我们传递的是由远程服务器分配的令牌,它应该只在 HTTPS 上使用,以确保加密。

  • Oauth:这是一个用于访问委托的开放标准。许多大互联网公司,如亚马逊、谷歌和微软,使用它来提供授权和身份验证。通常,它会使用一个承载令牌,正如之前提到的那样。

让我们看看这是如何工作的。在我的客户端,我安装了 PowerShell Universal,这是 Ironman Software 的好心人提供的一个模块(https://blog.ironmansoftware.com/)。它允许我在客户端运行一个轻量级服务器并创建端点。

不要在你的客户端尝试这样做

它需要相当多的配置,这本书中不会涉及,且需要订阅才能使用身份验证功能。我在这里提及它是为了让我们了解整个过程是如何工作的。

我在http://localhost:5000/me创建了一个端点,需要身份验证令牌才能访问。我已在服务器上创建了令牌,并将其存储为一个安全字符串,如下所示:

$apptoken = ConvertTo-SecureString -String <mytoken> -AsPlainText

令牌并不是真的<mytoken>;它是一个更长的随机字符串。现在,我可以通过提供令牌并使用$apptoken变量来访问该端点:

Invoke-WebRequest -Uri http://localhost:5000/me -Authentication Bearer -Token $apptoken

如下图所示,在没有提供身份验证的情况下,我会收到一个401错误;我没有权限。当我在第二个命令中提供身份验证时,我得到了页面内容Hello World

图 7.7 – 使用 -Authentication 和 -Token 参数提供身份验证

图 7.7 – 使用 -Authentication 和 -Token 参数提供身份验证

然而,通常情况下,我们需要通过-Headers参数提供身份验证。这比使用-Authentication稍微复杂一些。我们需要将令牌作为 Web 请求的一部分提供,但需要指向正确的头部。在我的本地测试端点的情况下,我可以像这样操作:

Invoke-WebRequest -Uri http://localhost:5000/me -Headers @{Authorization = "Bearer $admtoken"}

请注意,如果我们在头部提供令牌,我们必须使用ConvertTo-SecureString命令进行编码。我已经将未编码的令牌保存为$admtoken,(而不是$apptoken),如下所示:

图 7.8 – 使用 -Headers 参数传递身份验证数据

图 7.8 – 使用 -Headers 参数传递身份验证数据

在第一行,我试图传递存储在$apptoken变量中的编码令牌。在第二行,我传递存储在$``admtoken变量中的未编码令牌。

根据微软的说法,SecureString对象类型提供了一种安全性措施,但它并不是真正安全的。详细内容请参阅:learn.microsoft.com/en-us/dotnet/api/system.security.securestring

现在我们已经了解了如何发起基本请求,接下来让我们看看如何更程序化地与网站互动。

掌握 API

现代大多数系统通过 API 在客户端和服务器之间进行通信。这是两者之间约定的请求和响应列表。这听起来很复杂,但其实很简单。如果我们将random.dog/woof.json网址放入浏览器,我们会得到来自狗狗图片数据库的随机狗狗的 URL。我们还会得到文件的大小(以字节为单位)。

我们的浏览器是客户端,它向服务器上的 API 端点(/woof.json)发送 HTTP GET请求,该端点位于random.dog网址。作为对这个请求的响应,服务器发送包含 URL 的消息给我们,如下图中的第一个框架所示。我们可以查看下面第二个框架中的头信息,看到内容类型是JSON。然后我们可以在浏览器中显示该 URL,查看一只可爱的狗狗的图片。请注意,我这里使用的是 Firefox,它可以让我们查看头信息和内容。其他浏览器只会显示 JSON 内容。

图 7.9 – 通过 API 获取的狗狗

图 7.9 – 通过 API 获取的狗狗

服务器并没有发送一个网页供浏览器显示;它只是发送包含 URL 的数据,每次我们请求页面时,它都会发送不同的 URL。我们将在本章稍后的“处理 JSON”部分详细介绍这些数据,所以现在不用太担心它。

一般常用的 API 类型有四种:

  • 远程过程调用RPC)API

  • SOAP API

  • REST API

  • WebSocket API

它们分为两类 —— 有状态,客户端和服务器之间的连接在多个请求中保持不变;和无状态,客户端的每个请求都被视为一个独立事件,并不会将任何信息保留到下一个请求中。让我们深入看看。

RPC API

RPC API 调用服务器上软件中的函数——它们通过特定的代码让服务器执行某个操作,服务器会返回一个可能包含数据的输出。它们可能不通过 HTTP 执行;有一个名为 RPC 的独立协议,通常在局域网内使用。我们在本章中不讨论 RPC API,但它们在互联网上正经历着一些复兴,特别是在区块链应用中。RPC API 在历史上是有状态的,但这种模型的现代实现通常是无状态的。

SOAP API

SOAP API 使用简单对象访问协议(Simple Object Access Protocol)通过 XML 交换消息。我们通常在使用运行 Microsoft Internet Information Server(IIS)的 Windows 服务器时遇到这些 API,而不是在通常运行 Apache 或 NGINX 软件的 Linux 服务器上。如我们在 第六章 中所见,PowerShell 和文件——读取、写入及数据操作,XML 语言并不是最容易使用的,而 Windows 作为 Web 服务器的操作系统不如 Linux 常见,因此 SOAP API 并不是最受欢迎的。SOAP 最常见的是无状态的。

REST API

REST 是一种用于客户端-服务器、机器对机器通信的软件架构风格。REST API 符合这种风格。它们灵活且轻量,通常基于 HTTP 协议。REST API 是无状态的,能够以多种形式接收输出,包括 HTML 和 XML。然而,JSON 是最常见的输出形式。REST API 通常是最简单的 API,也无疑是互联网上最常见的 API。

WebSocket API

WebSocket API 使用 JSON 在客户端和服务器之间传输数据。与 REST API 类似,它们基于 HTTP,但与 REST API 不同的是,WebSocket API 还使用它们自己的协议,即 WebSocket 协议,这是 HTTP 的扩展,允许进行更多种类的操作。它们是有状态的和双向的;服务器可以主动与连接的客户端进行通信。这使得它们非常强大,但也更难使用。

让我们更详细地了解如何使用最常见的 API 类型——REST API。

使用 REST

我们将使用的最常见 API 是 REST API。当我们使用Web 应用程序时,通常会遇到 REST API。Web 应用程序通常是分层的客户端/服务器应用程序。一个典型的应用程序通常包括三个层级:

  • 展示层 —— 客户端设备上的 Web 浏览器或应用程序

  • 应用层 —— Web 服务器

  • 存储层 —— 通常是运行在 Web 服务器上或独立服务器上的数据库

我们使用 REST API 来在展示层(浏览器)和应用层(Web 服务器)之间进行通信;应用层如何与存储层(数据库)进行通信由应用开发者决定,但通常会使用 Python 或 PHP。

REST API 通常使用 HTTP 实现,这意味着它们使用一组熟悉的 HTTP 命令,如 GETPUTPOST。由于 Web 应用程序的设计方式,REST API 通常与数据库操作相关联;创建、读取、更新和删除CRUD)。下表总结了这些命令如何映射到操作。

数据库操作 REST API 请求 示例
创建 POST 创建一个新用户
读取 GET 获取一张狗狗的图片
更新 PUT 修改一个地址
删除 DELETE 删除一个聊天室帖子

表 7.1 – REST 与 CRUD 的关系

我们目前看到的网站都使用了 API,并且我们已经通过 Invoke-WebRequest 与它们进行过交互。现在让我们看看另一种可以使用的 cmdlet。

Invoke-RestMethod

Invoke-RestMethod cmdlet 可以用于查询一个 REST API 端点,例如 http://random.dog/woof.jsonInvoke-RestMethodInvoke-WebRequest 有什么不同呢?让我们使用两者分别查看 random.dog/woof.json API 端点,以比较输出:

图 7.10 – 比较 Invoke-WebRequest 和 Invoke-RestMethod

图 7.10 – 比较 Invoke-WebRequest 和 Invoke-RestMethod

在第一个示例中,红框中的部分,我使用了 Invoke-WebRequest。在第二个示例中,绿框中的部分,我使用了 Invoke-RestMethod。这两个 cmdlet 都能正确解析响应,但它们执行的操作不同。Invoke-WebRequest 显示了来自端点的 HTTP 响应,包括头部和内容。Invoke-RestMethod 只关注内容,并将其显示为一个自定义对象,包含与内容中的字典中名称对应的属性。等等,什么?字典从哪里来?记住,Invoke-RestMethod 是与 REST API 端点交互的。REST API 提供的输出通常是 JSON,或者不常见的是 XML。JSON 和 XML 输出通常由一组名称-值对组成;也就是字典。如果我们使用 Invoke-RestMethod 查询一个没有以 JSON 或 XML 输出的 HTML 页面,那么我们会得到一个单一对象,包含页面的原始 HTML。我们将在本章的下一节中更详细地介绍 JSON,与 JSON 一起工作。让我们更仔细地看看访问 API 端点时获得的输出。

如果我们将 Invoke-RestMethod 传递给 Get-Member,可以看到我们得到了一个 System.Management.Automation.PSCustomObject 对象,其中包含两个属性:fileSizeBytesurl

Invoke-RestMethod -Uri https://random.dog/woof.json | Get-Member

Invoke-RestMethod 的参数与 Invoke-WebRequest 相似。我们可以在 表 7.2 中总结它们。如我们所见,Invoke-WebRequest-HttpVersion 参数,该参数在 PowerShell 7.3 中引入,而 Invoke-RestMethod 没有;Invoke-RestMethod 有一些处理相对链接的参数(-FollowRelLink-MaximumFollowRelLink)以及 -StatusCodeVariable,该参数可以将 HTTP 响应状态码分配给一个单独的变量。当与 -SkipHttpErrorCheck 参数结合使用时非常有用:

表 7.2 – 比较 Invoke-WebRequest 和 Invoke-RestMethod 的参数

有些参数之间有一些小的差异,例如,Invoke-RestMethod -Uri 参数也可以接受 file:ftp: 协议,以及 http:https: 协议。

要真正掌握 Invoke-RestMethod,我们需要了解使用它时检索的内容。为了做到这一点,让我们仔细看看最常见的数据格式,JSON。

处理 JSON

什么是 JSON?首先,它不是一种语言;它是一种数据格式。虽然它的名字中有 JavaScript,但许多现代语言都使用它来生成、解析和交换数据。它还旨在便于人类阅读;我们只需要知道如何读取它。JSON 以类似字典的格式存储数据,使用键值对。第一个术语是键,第二个术语是值。键是字符串,值可以是另一个字符串、布尔值、数字、数组或 JSON 对象。JSON 对象由一个或多个键值对组成,因此因为值可以是另一个对象,JSON 对象可以是嵌套的。让我们输入 cmdlet:

PS C:\Users\nickp> (Invoke-WebRequest random.dog/woof.json).content

然后,我们将得到类似以下的 JSON 响应。

{"fileSizeBytes":176601,"url":"https://random.dog/6b41dccd-90ca-4ce8-a0e2-800e9ab92aa7.jpg"}

这由两个键值对组成:

"fileSizeBytes":176601
"url":"https://random.dog/6b41dccd-90ca-4ce8-a0e2-800e9ab92aa7.jpg"

在第一个键值对中,fileSizeBytes 是键,值是 176601,显然是文件的大小,单位是字节。第二个键值对的键是 url,值是随后的 URL。键和值由冒号 (:) 分隔。

两个键值对都被包围在一对大括号 ({}) 中。大括号告诉我们这是一个由所包含的键值对组成的单一对象。对象中的键值对由逗号 (,) 分隔。对象中的最后一对键值对后没有逗号。如果我们想包含一个数组,则需要将其用方括号括起来。接下来,让我们编写一个描述著名电视角色的 JSON。打开 VS Code,创建一个新文件,并将其保存为适当的文件名,例如 C:\temp\poshbook\ch7\enterprise.json。然后,输入以下内容:

{
"Name": "Enterprise",
"Designation": "NCC-1701",
"Captain": {
    "FirstName": "James",
    "Initial": "T",
    "LastName": "Kirk"
    },
"BridgeCrew": ["Uhura", "Spock", "Sulu", "Chekhov", "Riley"],
"PhotonTorpedoes": 240000000,
"JediName": null,
"IsAwesome": true
}

下图显示了在 VS Code 中的显示效果。这是一个有效的 JSON 文件,展示了有效的 JSON 数据类型和语法。

图 7.11 – 大胆展示 JSON

图 7.11 – 大胆展示 JSON

NameDesignation 包含字符串值,Captain 包含另一个 JSON 对象,而 BridgeCrew 包含一个字符串数组(但它也可以是其他有效数据类型的数组,甚至是更多 JSON 对象的数组)。显然,它没有 JediName,并且它确实非常棒。

如我们所见,VS Code 可以解析 JSON,并通过颜色编码帮助我们确保语法正确。

还有一些其他要记住的事项。首先,元素之间的空白会被忽略;"Initial": "T""Initial": "T" 同样有效。不过,建议你保持一致地使用空白字符。其次,没有特定的数字格式。240000000 既不是整数,也不是浮动点数,它只是一个数字。最后,JSON 中不允许有注释;这应该鼓励我们编写清晰和描述性的代码。

现在我们对 JSON 有了基本的理解,那么我们如何在 PowerShell 中使用它呢?PowerShell 处理 JSON 的方式不同于处理 XML,因此我们需要将 JSON 数据转换为自定义 PowerShell 对象,并将 PowerShell 对象转换为 JSON。有一对 cmdlet 可以帮我们完成这项工作,ConvertFrom-JsonConvertTo-Json。让我们先看看 ConvertFrom-Json

ConvertFrom-Json

ConvertFrom-Json 会解析来自某个位置的 JSON 内容,并将其转换为自定义 PSObject。让我们看看它是如何工作的。

打开 PowerShell 控制台并尝试使用以下命令获取内容:

$starship = Get-Content 'C:\temp\poshbook\ch7\enterprise.json'

从下图中可以看到,我们可以正常导入内容,但如果使用 Get-Member,我们可以看到我们将内容作为字符串导入:

图 7.12 – 使用 Get-Content 单独导入 JSON

图 7.12 – 使用 Get-Content 单独导入 JSON

将 JSON 文件中保存的数据转换为字符串会使得其使用和操作变得困难。现在,让我们尝试使用 ConvertFrom-Json

$starship = (Get-Content 'C:\temp\poshbook\ch7\enterprise.json' | ConvertFrom-Json)

这一次,我们已经将其导入为一个自定义 PSObject,这样更有用。通过这种方式创建的对象具有与 JSON 中的键值对相对应的属性,我们可以像访问任何其他 PowerShell 对象的属性一样访问它们,如下图所示:

图 7.13 – 使用 ConvertFrom-Json 导入 JSON

图 7.13 – 使用 ConvertFrom-Json 导入 JSON

让我们更详细地看看 ConvertFrom-Json cmdlet。

ConvertFrom-Json 是一个简单的 cmdlet,它隐藏了许多复杂的工作。正如我们所看到的,我们可以使用它将 JSON 字符串转换为自定义 PSObject。我们还可以使用它从 JSON 字符串创建有序的哈希表;这是必要的,因为 JSON 允许重复的键名,其中只有字符串的大小写可能不同。由于 PowerShell 对大小写不敏感,因此只有最后一个键值对会被转换;否则,ConvertFrom-Json 会抛出一个错误。另一个原因是 JSON 允许键为空字符串;这将导致一个属性名为空字符串的 PSObject,这是不允许的。请参见下图中的这些错误示例:

图 7.14 – 使用  参数的原因

图 7.14 – 使用 -AsHashtable 参数的原因

我们来看一下这些参数:

  • -AsHashtable 会将 JSON 字符串转换为有序哈希表,从而保留 JSON 键的顺序。我们在 第四章PowerShell 变量与数据结构 中讨论了有序哈希表。尽管它不像 PSObject 那样有用,但有序哈希表比字符串更易于操作,在某些情况下,处理速度也比 PSObject 更快。

  • -Depth 允许我们设置要处理的最大嵌套深度;正如我们在本节开始时所看到的,JSON 的键值对可以包含 JSON 对象,而这些 JSON 对象又可以包含进一步的 JSON 对象。-Depth 的默认值是 1024

  • -InputObject 只接受字符串;可以是字符串本身、包含字符串的变量,或者是生成字符串的表达式,正如我们之前在 enterprise.json 示例中所看到的。我们不能直接将文件传递给它;我们需要先使用 Get-Content 获取文件内容。显然,它接受管道输入。

  • -NoEnumerate 会将一个字符串数组读取为单个字符串,从而产生一个单一的输出对象。请看以下图示的示例:

图 7.15 – 使用  参数

图 7.15 – 使用 -NoEnumerate 参数

在第一个 cmdlet 中,我们得到三个独立的对象。当我们包含 -NoEnumerate 参数时,我们得到一个单一对象,fish cat dog

现在我们已经了解了如何将 JSON 转换为 PowerShell 容易处理的格式,让我们来看一下如何将 PowerShell 对象转换为 JSON。

ConvertTo-Json

ConvertTo-Json 会将任何 PowerShell 对象转换为 JSON 格式的字符串。它通过将对象的属性转换为键值对来实现这一点,其中属性名是键,并且会丢弃对象的任何方法。我们来看看它是如何工作的。如果你之前没有将 $starship 变量创建为 PSObject,请现在创建:

$starship = (Get-Content 'C:\temp\poshbook\ch7\enterprise.json' | ConvertFrom-Json)

现在,假设我们经历了一场战斗,需要更新我们的光子鱼雷数量。我们可以输入以下命令:

$starship.PhotonTorpedoes = 1900000000

这将更新 PSObject,然后我们使用ConvertTo-Json生成一个 JSON 格式的对象来替换我们导入的原始对象:

$starship | ConvertTo-Json | Out-File "C:\temp\poshbook\ch7\klingonattack.json"

如果我们在 VS Code 中打开它,我们可以看到它是一个格式正确的 JSON 文件,并且更新了鱼雷的数量:

图 7.16 – 克林贡攻击后

图 7.16 – 克林贡攻击后

我们可以看到在第 16 行,鱼雷的数量发生了变化。VS Code 仍然认为这是有效的 JSON,且没有任何错误。我们可以看到格式与原始文件有所不同;BridgeCrew数组的每个元素都被放在了各自的行上,这是因为ConvertTo-Json使用的格式化规则,但除此之外,它与原始的enterprise.json文件是一样的。然后我们可以将这个 JSON 字符串传递给 API 来更新服务器上的信息。

ConvertFrom-Json类似,ConvertTo-Json是一个看似简单的 cmdlet,它背后隐藏了很多工作,虽然它的参数列表很短。让我们来看一下这些参数:

  • -AsArray会无条件地将 PSObject 转换为 JSON 数组。请参考下图中的以下示例。

图 7.17 – -AsArray 参数

图 7.17 – -AsArray 参数

  • 在第一行,我将两个字符串转换为 JSON。ConvertTo-Json会自动将它们当作数组处理,并将其放入方括号中,因为它们是两个独立的对象。在第二行,我仅传入一个字符串到管道中,ConvertTo-Json将其视为一个单独的字符串。但如果我想将它格式化为单一成员的数组呢?这时,我使用-AsArray参数,就可以得到我的方括号。

  • -Compress会从 JSON 输出中移除空白字符和缩进格式。输出将呈现为单行。

  • -Depth指定 JSON 输出中可以包含的嵌套对象的级别。我们可以有零到一百级的嵌套,但默认情况下只有两个级别,这可能会让人困惑。如果输出的嵌套层级超过这个限制,我们将收到警告。请参考下图中的 JSON:

图 7.18 – nesting.json

图 7.18 – nesting.json

如果我们将其加载到一个变量中,然后再转换回 JSON,接着会看到如下图所示的警告。请注意,第三层嵌套(从"really?"开始的那一行)没有被转换成 JSON 对象,而是转换成了一个表示哈希表的字符串:

图 7.19 – 我们需要增加 -Depth

图 7.19 – 我们需要增加 -Depth

  • -EnumsAsStrings会转换DateTime对象的DayOfWeek属性的所有值。当我们输入(Get-Date).DayOfWeek时,它返回一个字符串Saturday,但实际值是作为介于 1 到 7 之间的整数存储的。请看下图中的示例:

图 7.20 - 使用 -EnumAsStrings 参数

图 7.20 - 使用 -EnumAsStrings 参数

  • -EscapeHandling 控制某些字符的转义方式,如换行符(`n)。它有三种可能的设置:Default,只有控制字符被转义;EscapeNonAscii,所有非 ASCII 和控制字符都会被转义;EscapeHtml,特殊的 HTML 字符如<>?&'" 会被转义。

  • -InputObject 接受任何类型的 PowerShell 对象,无论是显式传递、通过管道传递,作为表达式,还是作为变量。

这些大多数参数都是格式控制,旨在让我们更容易地与 API 进行交互,因为 API 可能对我们提供的输入有不同的期望。还有一个命令可以帮助我们将数据转换为 API 能够处理的格式;Test-Json

Test-Json

Test-Json cmdlet 将测试一个字符串是否为有效的 JSON 对象。这在编写与 API 交互的脚本时非常有用,可以确保我们的数据可以被正确地处理。尤其当我们考虑到并非所有通过ConvertTo-Json生成的内容都是合格的 JSON 时,它就显得特别有用。请参阅下图中的示例:

图 7.21 – 使用 ConvertTo-Json 时出现错误

图 7.21 – 使用 ConvertTo-Json 时出现错误

在第一行,我们创建了一个名为$date的变量,并将当前日期放入其中。它是一个DateTime类型的对象。毫不奇怪,当我们在第二行尝试使用 cmdlet 时,Test-Json不满意。第三行中,我们使用ConvertTo-Json将表达式转换为 JSON,然后在第四行再次测试它。糟糕!虽然它已经成功转换,但仍然不是符合规范的 JSON。在第五行,我们可以尝试仅测试第三行的字符串输出,这样我们就能看到问题所在。虽然ConvertTo-Json已经从$date变量中获取了值,但它并没有正确地将其格式化为字符串。当我们在第六行纠正格式后,我们可以看到Test-Json现在满意了。聪明的做法当然是这样的:

图 7.22 – 正确的做法

图 7.22 – 正确的做法

Test-Json 有几个参数允许我们定义自定义的 JSON 模式,如果我们需要为特定系统生成专门或定制的 JSON,但这里不需要详细讨论它们。这个 cmdlet 有一个有趣的怪癖,即几乎所有 cmdlet 都有的-InputObject参数在这里叫做-Json,但功能上是一样的;它接受一个字符串,既可以显式传递,也可以通过管道传递。

让我们来点乐趣 – 谁在国际空间站上?

从小到大,我一直对太空充满了兴趣。我的其中一个最早的记忆是和爸爸妈妈一起坐着看阿波罗月球登陆的画面,那时的电视是老旧的黑白电视。那不是阿波罗 11 号,我还没那么老。作为一个练习,看看我们能否找出当前在国际空间站上的宇航员,并将这些数据展示在一个网页上。为了完成这个任务,我们需要参考上一章的内容;第六章PowerShell 与文件——读取、写入和操作数据,以及我们在本章中学到的内容。我们不会手把手讲解——试着自己完成这个任务。有很多种方法可以实现,我把我的解答放在了答案部分。不过这里有一些提示。

活动

我们可以将任务分解为两个部分:

任务 1 – 使用 API 查看当前谁在国际空间站

任务 2 – 在 HTML 文件中展示这些数据

我们所需的数据可以通过一个 API 获取,链接是api.open-notify.org/astros.json

你可以使用Invoke-WebRequest,但使用Invoke-RestMethod可能会更简单

你可能会希望使用ConvertTo-Html来生成网页。你可能需要查阅这个 cmdlet 的帮助文件,了解一些格式化选项。

这是我的最终尝试:

图 7.23 – 真是美丽

图 7.23 – 真是美丽

这将稍微挑战我们,但我们拥有完成这个任务所需的所有知识,并且可以在这个过程中享受乐趣。

摘要

在本章中,我们已经走了一段旅程,并开始与我们本地计算机之外的世界进行互动。我们所讨论的技巧将在全书中使用,因此我们将有充足的机会充分熟悉它们。

我们首先查看了如何使用Invoke-WebRequest cmdlet 来处理通过 HTTP 传输的 HTML 数据。我们发现这个 cmdlet 非常复杂,有很多选项,我们讲解了常用的参数。我们特别关注了身份验证,因为这是获取和发布数据时必须掌握的关键技巧。我们还发现,通过这个 cmdlet 处理获取的数据非常困难,因为它是基于文本的。

然后我们讨论了从服务器通过网络获取数据的一种更简单的方法——使用 API。我们讨论了常见的 API 类型,尤其是最常见的 REST API。

接着,我们了解了用于处理 REST 的 PowerShell cmdlet——Invoke-RestMethod。我们发现这个 cmdlet 与Invoke-WebRequest非常相似,但它输出的是结构化的数据,而不是 HTML 页面。

我们接着探讨了这种数据最常见的格式——JSON。我们看了 PowerShell 中三个用于处理 JSON 数据的 cmdlet:ConvertFrom-JsonConvertTo-JsonTest-Json

最后,我们利用新学的知识,制作了一个 HTML 网页,展示了当前在国际空间站上航天员的名字。

本章标志着本书编码基础部分的结束;我们已经涵盖了数据结构、流程控制、文件操作和连接互联网。在下一章中,我们将开始学习如何将我们杂乱无章的代码转化为脚本和工具,这些脚本和工具可以与他人共享。一定会很有趣。

练习

  1. 我们如何向以下 URL 发送删除请求:https://httpbin.org/delete

  2. 在有状态端点上使用Invoke-WebRequest时,我们需要哪些参数?

  3. 我们尝试连接一个网站并看到以下错误:

    Invoke-WebRequest?
    
  4. 哪种类型的 API 是有状态的?这是什么意思?

  5. 我们注册一个 Web 服务并获得认证令牌。我们使用ConvertTo-SecureString对令牌进行编码,将其存储在名为$token的变量中,然后使用该变量创建一个 Web 请求,如下所示:

    Invoke-WebRequest -Uri 'https://webservice.com/endpoint' -Headers @{Authorization = "Bearer $token"}
    

    我们遇到了认证错误。我们做错了什么,还是令牌有问题?

  6. 获取国际空间站(ISS)当前的纬度和经度。你可以使用api.open-notify.org/iss-now.json

  7. 英国有多少所大学的名字中含有字母‘x’?使用universities.hipolabs.com/search?country=United+Kingdom的 API 来查找答案。

  8. 我们如何使用Test-Json来验证我们生成的 JSON 是否符合自定义模式?

进一步阅读

第二部分:脚本编写与工具制作

本部分将引导你将一组 Cmdlet 转换为脚本,介绍函数式编程,展示如何将脚本转化为模块,以及如何使用 GitHub 和 GitLab 与同事和他人共享这些模块。它还包括一个关于 PowerShell 安全性的章节,确保你不会无意中分发不够安全的代码。

本部分包括以下章节:

  • 第八章编写我们的第一个脚本 – 将简单的 Cmdlet 转换为可重用的代码

  • 第九章不要重复自己 – 函数和脚本块

  • 第十章错误处理 – 哎呀!出错了!

  • 第十一章创建我们的第一个模块

  • 第十二章确保 PowerShell 安全

第八章:编写我们的第一个脚本 – 将简单的 Cmdlet 转换为可重用代码

从本章开始,我们将学习如何将我们学到的基本概念结合起来,编写可以重用、调整和分发给他人的脚本和工具。在接下来的章节中,我们将讨论如何创建函数和脚本块、错误处理和调试、创建模块使我们能够将代码作为工具分发,最后,我们还会讨论如何保护 PowerShell。但在本章中,我们将从基础开始:将一小部分 cmdlet 转换为脚本。

我们将从讨论脚本的一般概念以及为什么我们可能想要编写脚本开始。之后,我们将简要了解我们可以在哪里在线找到 PowerShell 脚本,然后我们将讨论如何运行脚本。

一旦我们理解了如何使用他人的脚本,我们就会开始编写自己的脚本。首先,我们将学习如何在工作中的 cmdlet 管道中识别变化的值,接着我们会学习如何将这些值转化为参数,这样我们就能在运行脚本时将这些值传递给 cmdlets,而不需要每次都编辑脚本。我们将涵盖如何使参数成为必填项、如何从管道中获取值以及如何创建切换参数,之后我们将学习如何帮助自己和他人使用我们的脚本。

本章的第二部分,我们将讨论如何在脚本中添加注释和注释块,解释我们在每个部分尝试做的事情,并帮助我们和他人编辑和调整脚本。接下来,我们将探讨如何编写帮助信息,使其可以通过 Get-Help cmdlet 从脚本外部访问。之后,我们将讨论一个脚本构造,它可以帮助我们理解当脚本产生意外输出时发生了什么——Write-Verbose cmdlet。最后,我们将讲解如何为必填参数添加帮助信息,提示用户如何使用该参数。

在本章中,我们将覆盖以下主要内容:

  • 脚本编写简介

  • 编写脚本

  • 识别变化的值

  • 使用参数

  • 为我们的脚本提供帮助

脚本编写简介

脚本是一系列指令,用人类可读的形式编写,供计算机执行。它们通常使用脚本语言编写,例如 PowerShell、Python 或 JavaScript。我们在第一章PowerShell 7 简介 – 它是什么以及如何获取中讨论了脚本语言和编程语言之间的区别;需要记住的关键点是,脚本语言是解释型的,因此需要一个程序(如 PowerShell)在客户端运行来执行脚本,而编程语言是编译型的;它们会直接在操作系统中运行。

对我来说,脚本编写是一种艺术形式。这可能是最真实的控制论艺术形式;我们在脚本中编写的内容必须对人类和机器都有意义。在第六章PowerShell 和文件 – 读取、写入和操作数据,我们谈到了 Cristina Videira Lopes 的编程风格练习,在其中以几十种不同的方式解决了术语频率任务。这同样适用于 PowerShell 中的脚本。没有一种编写脚本的唯一方法,对一个人来说看起来美好的东西对另一个人来说可能非常丑陋;我非常厌恶代码高尔夫,即尽可能少的行数和字符编写脚本,但这是个人偏好 – 其他人却喜欢。简单、干净和功能性代码 – 这些东西是美丽的,编写它们需要创造性和富有创造力的头脑。在本章中,我们将讨论我如何编写脚本的方法。

为什么我们想要编写脚本呢?

基本上,因为我们懒惰。脚本允许我们捕捉一系列可能复杂或耗时的步骤,并轻松地重复执行而不需要额外工作。其次,因为我们容易出错和容易分心。任务越复杂越长,我们犯错的可能性就越大。编写脚本意味着任务每次都会以相同的方式完成。如果我们能够做对一次并捕捉该过程,那么我们几乎永远不需要再次做对。我们可以去其他地方犯错误。最后,因为我们根本不想做这件事;如果我们编写一个脚本,我们可以把它交给别人去做。

脚本编写是工具制造的一个例子,我们创建一些东西来使任务更容易完成。一些脚本编写可能是适当的自动化,但一般来说,自动化包括反馈循环的概念,即工具在没有用户干预的情况下响应外部刺激。中央供暖锅炉就是一个工具;它使得加热房屋比在每个房间里生火要容易得多。恒温器是自动化的一种;当温度过低时,它打开锅炉,而当温度过高时,它关闭锅炉。如果我们设置正确,那么我们就不会太热或太冷。

有一些围绕工具制造和自动化的理论值得考虑,因为它可以帮助我们决定是否要编写脚本。让我们快速了解自动化的三个基本原则:

  • 补偿原则:基于这样一个概念,即机器在某些任务上比人类更擅长,而人类在某些任务上又比机器更擅长。基于这一原则的自动化和工具根据人类和机器的优势和劣势来分配工作。

  • 互补性原理:不断使用工具完成困难的任务可能会削弱人类操作员的技能;我们会忘记如何做事。然后,当我们真的需要接管时,当事情出错时,我们无法修复问题。我们使用这个原理,要求人类操作员必须保持自动化替代的技能。飞机自动驾驶仪就是一个很好的例子;操作员并不是一直使用它们,或者可能只使用其中的一部分,以便在自动驾驶仪因为天气或机械故障无法着陆时,操作员能保持所需的技能。

  • 剩余原理:这是我们最常与脚本编写相关联的原理。它的想法是我们自动化所有能自动化的任务,然后手动完成剩下的部分。

当然,这比听起来要复杂一点。有时任务太难以自动化,或者我们做得很少,以至于不值得麻烦。通过编写脚本我们并不会节省时间或精力。一个经验法则是,如果你有一个任务,每天都要做,且每次花费 5 分钟,而写脚本需要 4 小时,那么在 3 年内,这个任务将花费 62 小时。通过花费 4 小时编写脚本,你将在 3 年内为自己节省一周的工作。让我们从了解如何获取别人编写的脚本开始。

获取脚本

我们在 第二章《探索 PowerShell Cmdlet 和语法》中讨论了获取 PowerShell 模块和 Cmdlet 的不同途径。我们也可以利用这些途径来查找 PowerShell 脚本。让我们回顾一下这些资源:

  • PowerShell 仓库:无论是官方的 PowerShell 仓库 www.powershellgallery.com/,还是 Microsoft Learn 仓库 learn.microsoft.com/en-us/samples/browse/,或者内部的仓库,都是寻找脚本的好地方。一般来说,PowerShell Gallery 中的脚本经过了一些基本验证,会提供关于作者、版本和使用许可的信息。

  • GitHub:这是另一个很好的 PowerShell 脚本来源,但这些脚本通常没有经过验证,可能不完整或不可用。

  • 其他在线资源:网上有很多很好的脚本。例如,Practical 365 网站(practical365.com)有一些非常有用的 Microsoft 365 脚本。然而,在线脚本的质量可能会有所不同。

每当我们从不认识或不信任的作者那里获取脚本时,应该小心理解脚本的作用和运行方式,再在我们关注的环境中运行它。当然,即使我们信任作者,仍然值得先在沙箱或虚拟机中测试脚本。

运行脚本

在我们运行脚本之前,有两件事需要注意。第一,PowerShell 脚本的扩展名始终是 .ps1。具有此扩展名的文件不会被注册为可执行文件,因此在 Windows 资源管理器中双击它们将会在记事本或其他编辑器中打开,而不会运行脚本。具有此扩展名的文件还需要一个相对路径或绝对路径才能从 PowerShell 终端运行,因此无论是 PS C:\myscripts> .\MySuperScript.ps1(相对路径)还是 PS C:\> C:\myscripts\MySuperScript.ps1(绝对路径),如果没有 .\,脚本都无法运行。

其次,PowerShell 对运行脚本有执行策略,但仅限于 Windows 计算机。默认情况下,在 Windows 客户端上,执行策略是 Restricted,这意味着脚本无法运行。幸运的是,我们可以通过运行以下 cmdlet 为当前用户更改此设置:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

这将允许当前用户运行本地计算机上编写的脚本,或从互联网下载并包含代码签名的脚本。我们将在 第十二章PowerShell 安全》中详细讨论执行策略和代码签名,但现在请运行此 cmdlet,以便我们继续编写脚本。

如果我们使用的是 Linux 或 Mac 客户端,我们无需执行此操作。由于 Linux 和 macOS 操作系统在安全性实现上的差异,这些计算机的策略实际上是 Unrestricted

这就够了,接下来我们开始进入正题:编写脚本。

编写脚本

我们该如何编写脚本呢?几乎每一个我从零开始编写的脚本,都是从列出完成任务所需的步骤和 cmdlet 开始的。一旦我能够仅凭列表中的信息完成任务,且所有 cmdlet 都能正常工作,我就知道我准备好了。让我们从一个有效的命令开始我们的脚本。作为示例,考虑以下 cmdlet:

Invoke-RestMethod -Uri 'https://api.weatherapi.com/v1/current.json?q=London&aqi=no' -Method GET -Headers @{"key" = "fp3eofkf3-0ef-2kdwpoepwoe03eper30r"}| ConvertTo-Html | Out-File "c:\temp\poshbook\ch8\WeatherData.html"

这个 cmdlet 使用个人密钥从天气 API 服务中获取我的位置的天气数据,并将其写入 HTML 文件以进行显示。显然,我没有在前面的 cmdlet 中放入我的真实个人密钥,因此如果我们运行那段代码,系统会因为 API 密钥错误而失败。让我们考虑如何将这个 cmdlet 转换成一个脚本。接下来,我们需要获取我们自己的个人密钥。

获取天气 API 服务的个人密钥

要完成本章内容,我们需要为天气 API 服务获取个人密钥:

1. 访问 www.weatherapi.com,点击 注册 按钮。

2. 输入电子邮件地址(两次)和新密码(两次),接受条款和条件,完成验证码,我们将收到一封验证电子邮件。

3. 点击验证电子邮件中的链接将打开一个网页,告诉我们已验证成功,并邀请我们登录。

4. 通过点击登录链接并输入我们刚刚创建的电子邮件地址和密码来完成此操作。下一页的顶部是我们的个人 API 密钥。这就是我们将用来运行此 cmdlet 的密钥。

5. 复制它并保存在某个安全的文本文件中。你可以随时通过登录服务再次找到它。

现在我们有了密钥,让我们打开 VS Code 并创建一个新文件。命名为weatherdata.ps1。输入以下内容,使用我们刚刚从天气 API 生成的密钥,而不是<our new key here>

Invoke-RestMethod -Uri 'https://api.weatherapi.com/v1/current.json?q=London&aqi=no' -Method GET -Headers @{"key" = "<our new key here>"}| ConvertTo-Html | Out-File "c:\temp\poshbook\ch8\WeatherData.html"

让我们思考如何整理这个 cmdlet,以使其更加可读。这个 cmdlet 做了两件事:它从 API 获取信息,然后将这些信息输出到文件中,所以我们可以将这两部分拆开:

$response = Invoke-RestMethod -Uri 'https://api.weatherapi.com/v1/current.json?q=London&aqi=no' -Method GET -Headers @{"key" = "<our new key here>"}
$response | Convertto-Html | Out-File "c:\temp\poshbook\ch8\WeatherData.html"

现在,我们将响应存储在一个变量中,然后在第二行输出它。

我们还可以如何使这个代码更具可读性?第一个 cmdlet 非常长。我们可以将头部和 URI 分开,以使其更具可读性,正如以下图所示:

图 8.1 – 可读代码

图 8.1 – 可读代码

如果我们保存weatherdata.ps1,现在可以从 PowerShell 控制台运行它,只需切换到我们保存它的目录,并在提示符下运行以下命令:

.\weatherdata.ps1

虽然代码是可读的,但它并不太灵活。它做一件事,如果我们希望它做一些稍微不同的事情(例如,获取不同城市的数据),或者如果我们想与其他人共享它,我们需要做一些更改。让我们看看从哪里开始做这些更改。

识别变化的值

让我们思考一下在这段代码中可能需要更改的内容。我们希望它从网络获取天气信息并将其写入 HTML 文档。这意味着我们不太可能想要更改Invoke-RestMethodConvertto-HtmlOut-File这些 cmdlet。想一想在我们继续之前可能会更改的值。

准备好了吗?很好。

这些是我们可能希望在运行脚本时能够更改其值的一些内容:

  • 我们可能想要更改城市。

  • 我们可能想要更改 HTML 输出的保存位置。

  • 我们可能想要更改 API 密钥。

  • 我们可能根本不想在脚本中硬编码 API 密钥。

我们可以通过在脚本中将它们设置为参数来传递这些值。下一节将专门讲解如何将脚本参数化。

使用参数

参数是脚本变量的值,可以在我们运行脚本时传递,而不是硬编码到脚本中。正如我们在上一节中所看到的,我们以与运行其他 cmdlet 相同的方式运行脚本,并且,像 cmdlet 一样,我们可以向脚本传递参数值。让我们看看如何做到这一点。

我们需要的第一件事是CmdletBinding特性。特性是告诉 PowerShell 我们希望它如何处理脚本元素的方式。我们之前在第四章中使用过它们,PowerShell 变量和数据结构,当时我们学习了如何转换变量。CmdletBinding特性告诉 PowerShell 我们希望它将脚本视为 cmdlet。这样做的主要好处之一是我们的脚本可以访问常用的参数,如-Verbose,以及位置绑定。让我们添加它。在脚本的顶部创建一行,并添加以下内容:

[CmdletBinding()]

接下来,我们需要创建一个Param()块,用来保存我们希望作为参数使用的变量。在CmdletBinding特性下方的行中,输入以下内容:

Param()

Param()块必须位于我们要编写的所有代码之前。我们将在括号内添加参数,所以最好按Enter键几次,给括号内部留出一些空间,以便添加它们:

[CmdletBinding()]
Param(
)

现在,我们准备开始创建参数了。

创建有用的参数

最有用的参数是那些会变化的值。让我们从我们获取天气数据的城市开始。城市嵌入在$uri变量中,这是一个字符串。我们可以轻松创建一个新变量来仅保存城市名称,并将其传递给$uri变量。试试这个:

[CmdletBinding()]
Param(
$City = "London"
)

并将$uri变量更改为如下所示:

$uri = "https://api.weatherapi.com/v1/current.json?q=$($City)&aqi=no"

记住,我们在这里需要使用双引号(")以便扩展$City变量。我们可以通过以下图示来检查我们的代码:

图 8.2 – 添加 CmdletBinding 特性和 param() 块

图 8.2 – 添加 CmdletBinding 特性和 param() 块

这将在我们运行时把$city的值写入$uri的值中。

测试,测试,再测试

这非常重要;如果我们做了两个更改,而脚本停止工作了,我们将不得不找出哪个更改导致脚本中断。每次更改后都要进行测试。在进行下一次更改之前,确保一切仍然有效。

让我们测试一下。从控制台开始,确保我们在保存脚本的目录中,然后输入以下内容:

.\WeatherData.ps1 -City Paris

现在,检查一下WeatherData.html文件。它应该是这样的:

图 8.3 – 欢迎来到美丽的巴黎

图 8.3 – 欢迎来到美丽的巴黎

如果这对你有效,恭喜你!你已经为第一个脚本实现了参数化。让我们看看我们做了什么。

通过将$City变量放入Param()块中,我们使其可以从脚本外部访问,以便在运行脚本时将值传递给它。通过给变量赋值,写成$City = "London",我们设置了一个默认值,这样如果我们没有通过参数传递值,脚本仍然能够运行。

我们可以在Param()块中放入任意数量的变量,但每个变量必须用逗号(,)分隔。让我们再做一个,试试下面的操作。

活动 1

我们怎么传递我们想要保存输出的位置呢?我们又该如何让这件事变得尽可能简单?

提示:如果我们只传递一个文件名,它将把输出保存在与脚本运行位置相同的地方。我们每次都想这样做吗?

让我们看看可以如何使用密钥。密钥是一个长字符串,由随机字符组成,因此直接将其输入参数中并不实际。我们怎样解决这个问题呢?

我们可以将密钥保存到文件中,然后使用Get-Content从文件中提取字符串并以此方式使用它。在第七章中,PowerShell 和 Web – HTTP、REST 和 JSON,我们看到有两种将密钥传递给Invoke-RestMethod的方法。我们可以将其传递给-Key参数,或者像在此脚本中那样将其包含在-Headers参数中。重要的区别在于,在一种情况下密钥必须被编码,而在另一种情况下它必须是纯文本。你能记得是哪种情况吗?回顾上一章找到答案吧。

如果你记得,那就做得好。如果你是从我们当前脚本的工作方式推理出来的,那就更好了。我们已经在头部传递了一个未编码的字符串,因此我们不需要在保存之前对我们的密钥进行编码。

使用你喜欢的文本编辑器打开一个新文本文件,并将其保存为key.txt,放在与脚本相同的文件夹中——在我的情况下是c:\temp\poshbook\ch8。将你的WeatherApi密钥复制粘贴进去并保存。

现在,我们所需要做的就是添加一个指向文件的参数,并添加一行从文件中获取内容的代码。然后,我们可以将其添加到$headers变量中。它可能看起来像这样:

[CmdletBinding()]
Param(
$City = "London",
$KeyFile = "key.txt"
)
$key = Get-Content $KeyFile
$headers = @{"key" = "$key"}

我们已经确定了变化的价值观并创建了我们的参数。

活动 2

根据我们从前面章节中学到的内容,我们怎样重写脚本,使得可以接受-City参数的多个值呢?

提示 1:查看接受多个字符串作为参数的 cmdlet 的帮助文件,看看我们如何为-City参数编写一个属性。Get-Random接受多个对象作为-InputObject

提示 2:我们将希望有一种方法来依次处理每个字符串。记住,传递给参数的多个字符串实际上是一个数组。

提示 3:我们将希望将每个城市发送到不同的输出。

让我们看看如何使用属性来改进它们。

指定类型

我们可以通过指定参数类型来限制错误的可能性,就像我们为变量指定类型一样,在变量前面加上属性。在你的脚本中,将$City = "London"这一行替换为如下代码:

[String]$City = "London"

现在,传入-City参数的任何内容都会被强制转换为字符串。

使参数变为必需

我们可能希望每次运行脚本时都需要显式提供–City参数的值。我们可以通过如下方式修改-City参数来实现:

[CmdletBinding()]
Param(
[Parameter(Mandatory = $true)]
$City = "London"
)

我们在$City参数之前添加了行[Parameter(Mandatory = $true)]。请注意,属性后面没有逗号(,)。现在我们这么做了,脚本就会忽略该参数的默认值("London"),并提示输入城市:

图 8.4 – 强制参数

图 8.4 – 强制参数

在图中,我们可以看到因为我们没有为-City参数提供值,所以系统会提示我们。我们能通过脚本做的最有用的事情之一,就是让它接受来自管道的参数值。让我们看看如何做到这一点。

从管道获取值

我们在第三章PowerShell 管道 – 如何将 Cmdlet 串联起来中讲解了参数如何从管道接受值。在本节中,我们将看到让我们的脚本接受管道值是多么简单。记住,参数可以通过两种方式从管道接受值:ByValueByName。对于我们的脚本来说,最好让-City参数通过值来接受管道输入,因此我们来这样做:

[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline)]
$City = "London",

我们所做的仅仅是在Param()块中添加了行[Parameter(ValueFromPipeline)]。我们可以在下图中看到它带来的区别:

图 8.5 – 从管道接受值

图 8.5 – 从管道接受值

在第一个示例中,我们看到我们创建了一个字符串"cwmbran",并尝试将其通过管道传递给脚本。因为脚本不接受来自管道的值,我们会看到一个红色的错误消息提示我们这一点。如果我们随后更改-City参数来接受来自管道的值,那么在第二个示例中我们就能看到脚本顺利完成,没有错误。

让我们看看如果尝试通过管道向脚本传递两个值会发生什么,如下例所示:

"Manila","Cardiff" | .\weatherdata.ps1

我们只会得到最后一个项的输出。为了让它正确工作,我们需要将要为每个项重复的脚本部分放入process {}块中,像这样:

图 8.6 – 正确处理管道输入

图 8.6 – 正确处理管道输入

我在第44行打开了一个process {}块,然后在第61行关闭它。现在,我可以将多个城市输入到管道中,并为每个城市生成一个 HTML 文件,如下所示:

"Bristol", "Brighton" | .\weatherdata.ps1

请注意,这并不意味着我们可以完全去掉活动 2中的foreach循环。如果我们显式地使用-City参数传递多个城市,我们仍然需要通过循环逐个处理它们。

还有很多其他操作可以在参数上执行,具体内容请参见这篇文章:learn.microsoft.com/en-us/powershell/scripting/lang-spec/chapter-12

它们中的许多可以用于验证参数输入:确保它是正确的类型,或者甚至从接受的值列表中选择输入。

开关参数

在继续之前,我们应该讨论一下参数的最后一个特性:开关参数。我们一直在使用的许多 cmdlet 都有作为开关的参数;它们不需要值,只需存在即可改变 cmdlet 的行为。例如,在Get-Help中使用-Full参数会改变返回的帮助信息量。我们也可以在脚本中做到这一点。试试看——在param()块之后,添加以下行:

If ($Joke.IsPresent) {
Write-Output "Why did the chicken cross the road?"
}

然后,在param()块的最后一行,在闭括号之前添加以下行。记住,你还需要在前一个参数后添加逗号:

[switch] $Joke

以下图示展示了在我的脚本中它的样子,并显示了我们使用新-Joke参数时的输出:

图 8.7 – 只是开个玩笑

图 8.7 – 只是开个玩笑

在第40行,我在参数后添加了一个逗号,这样 PowerShell 就知道接下来会有另一个参数。我在第41行添加了新的-Joke开关参数,然后在第4446行,我添加了一个If语句,告诉 PowerShell 如果开关存在应该做什么。最后,在终端底部调用脚本时,我使用了这个开关,脚本告诉我世界上最棒的笑话。很简单,是吧?

请注意,当我们查看互联网上其他人的脚本时,可能会看到一种不同的开关参数构造。我们常见的写法是If ($Joke -eq $true),而不是使用If ($Joke.IsPresent)。虽然这样也能工作,但这是更旧的做法,可能会引起混淆。使用参数的.IsPresent方法是微软推荐的方式。

在我们继续阅读本书的其余部分时,我们将大量使用参数,但现在,让我们来看看本章的另一个主要话题——为我们的脚本提供帮助。

为我们的脚本提供帮助

编写脚本非常有趣,每次编写脚本时我们都会学到新东西。我们会加入新的技巧或快捷方式,完成脚本,投入使用,然后忘记它。下次回顾时,可能是几个月或甚至几年后,到那时我们可能已经忘记了它是如何工作的,或者为什么我们当初以那种方式编写它。解决这个问题的方法是为脚本编写全面的帮助文档,解释脚本如何工作以及如何使用它。对于我们提供给其他人使用的脚本,这一点尤为重要。

在这一节中,我们将介绍提供帮助的四种方式。首先,我们将简要了解如何通过注释代码来帮助自己和他人。接着,我们将研究如何为Get-Help cmdlet 创建基于注释的帮助。然后,我们将了解Write-Verbose cmdlet,并学习如何使用它来理解脚本的执行情况。最后,我们将看看如何为必填参数提供帮助。

让我们从注释代码开始。

注释

我们在第五章《PowerShell 控制流 – 条件语句和循环》中简要提到过注释。我们看到,可以通过在行首添加井号(#)将一行变成注释,也可以通过用 <# … #> 包围一段文本来创建多行注释。然而,我们并没有深入讨论注释的作用。PowerShell 是一种脚本语言——在我看来,它特别友好。大多数时候,熟悉语言的人可以合理地推测出一行代码的功能,但如果有注释,我们就不需要逐行解析来了解代码的意图。当然,我们写的代码并不总是按我们预期的那样执行。记录我们希望代码执行的注释,可以在我们翻阅四五百行代码时大大简化工作。当脚本被其他人修改时,注释尤为重要。来看下面的示例:

# concatenate the strings to produce an output filename
$Output = "$($OutputPath)\$($item)_$($OutputFile)"

注释行准确告诉我们这些略微难以阅读的代码希望实现的功能。

注释块

注释是为了在某种编辑器中阅读。为了使其更易于阅读,我们希望将长注释拆分成多行。我们可以使用反引号字符(`)来转义手动换行符,但这并不真正使其更易于阅读,尤其是在某些编辑器和字体中。最好使用块注释语法,<#...#>。请看下面的图示:

图 8.8 – 注释块

图 8.8 – 注释块

注释块是绿色的,解释了接下来的代码行的意图。让我们看看编写良好注释的一些注意事项:

  • 注释应单独放在一行;在同一行添加注释会导致行过长或在编辑器中换行。

  • 在你解释的代码行之前添加注释;为读者预告接下来脚本中的内容,而不是已发生的内容。

  • 不要对显而易见的内容进行注释;一般来说,注释过多总比过少好,但不要浪费时间去解释那些不需要解释的内容。最好注释一块简单代码的作用,而不是每一行的作用。

让我们来看一下如何使用Get-Help cmdlet 为脚本提供帮助。

基于注释的帮助

Get-Help 命令读取与 PowerShell 主程序一起提供的 XML 帮助文件,以及具有这些文件的模块和函数。然而,它也可以读取正确格式化的嵌入脚本中的注释。这就是所谓的 基于注释的帮助。我们可以通过在脚本的开始部分添加特殊的帮助注释关键字来编写基于注释的帮助。

关于编写基于注释的帮助文档,有一些规则需要记住:

  • 它必须位于脚本的开始或结束。出于多个原因,包括我们之前讨论的最佳注释实践,我们应该把它放在开始处。

  • 基于注释的帮助必须是连续的。我们不能将其分割成不同的注释块。

  • 每个帮助部分必须以合法的关键字开始。关键字不区分大小写,但必须以点(.)开头。

  • 基于注释的帮助后必须有两行空行。

幸运的是,VS Code 使编写基于注释的帮助变得非常简单。我们来看看它是如何工作的。

在脚本中,在脚本的开始处创建一行,输入 comm。如下面的图所示,如果我们安装了 PowerShell 扩展,VS Code 会提供 comment-help 功能助手。

图 8.9 – VS Code 中的注释帮助功能

图 8.9 – VS Code 中的注释帮助功能

继续选择它。VS Code 会为我们创建一个基于注释的帮助模板,模板中包含最常用的关键字,格式正确,甚至会在块后面自动添加两个空行。

让我们来看看常见的部分:

  • .SYNOPSIS:此部分应为一行,总结脚本的功能。

  • .DESCRIPTION:在这里我们写下完整的描述,包括它是如何工作的,应该用来做什么,如何使用等。

  • .NOTES:在这里,我们应放置关于不兼容性、运行脚本的前提条件(例如,我们需要将 weatherapi 密钥保存到文本文件中)以及任何其他不适合描述部分的有用信息。

  • .LINK:这将为 Get-Help -Online 提供一个链接。就个人而言,我从不使用它,并且会从帮助部分删除它。

  • .EXAMPLE:我们可以在此处放置使用示例,以说明如何使用参数。每个示例应与新行中的 .EXAMPLE 关键字分开。

我承认,第一次做这个时,看到我的脚本帮助文档看起来像真正的帮助文档时,我感到非常兴奋。这种感觉永远不会过时:

图 8.10 – 检索脚本的基于注释的帮助

图 8.10 – 检索脚本的基于注释的帮助

这与 Get-Help 中的其他内容一样工作。要查看注释,请输入 Get-Help .\weatherdata.ps1 -Detailed。要获取所有信息,请使用 -Full 参数。

活动 3

为脚本编写简短的基于注释的帮助文档。通过从控制台调用 Get-Help .\weatherdata.ps1 来测试它。

这就是基于注释的帮助内容。它非常有用,无论我们是在编辑器中查看,还是使用Get-Help cmdlet。我建议我们尽可能多地使用它。现在让我们来看一些其他内容,虽然它不严格属于帮助,但绝对有帮助:Write-Verbose cmdlet。

Write-Verbose

第三章中,我们简要讨论了标准流:存在于大多数编程语言中的输出流,用于分类和引导输出。详细流就是其中之一,用于传达帮助用户理解 cmdlet 正在做什么的消息。它对于排查看似没有任何操作的长时间运行的 cmdlet 特别有用。每个 PowerShell cmdlet 都有一个-Verbose开关参数,允许我们看到 cmdlet 正在做什么,以及它是否卡住了,尽管并不是所有 cmdlet 都会产生详细流输出。我们已经在脚本中看到它的作用,因为Invoke-RestMethod cmdlet 具有特别有用的详细输出。试试这个:

.\weatherdata.ps1 -Verbose

你应该看到类似于下图的内容:

图 8.11 – 脚本内部的详细输出

图 8.11 – 脚本内部的详细输出

我们能够访问这些详细输出是因为我们在脚本开始时使用了CmdletBinding()特性,它为我们提供了访问高级功能的权限。如果没有它,我们可以调用-Verbose参数而不会出错,但不会产生任何输出。

然而,我们可以做得比这更多;我们可以使用Write-Verbose cmdlet 编写我们自己的详细输出信息。为什么这有用呢?因为当我们在脚本中循环处理多个项目时,它可以告诉我们循环中的哪个项目出错。假设我们让脚本生成以下三个城市的数据:

.\weatherdata.ps1 -City "London", "Paris", "Llareggub"

然后,我们会收到一个错误,说明在Invoke-RestMethod运行时,未找到匹配的位置,但抛出错误的代码行如下:

Invoke-RestMethod -Uri $uri -Method GET -Headers $headers

所以它可能是任何一个不存在的城市。

我们可以通过更改foreach循环的开始部分来修复此问题:

foreach ($item in $City) {
    Write-Verbose "Processing $item"
    $Output = "$($OutputPath)\$($item)_$($OutputFile)"

我们在循环开始时添加了行Write-Verbose "Processing $Item",这意味着如果现在使用-Verbose参数,脚本会告诉我们正在处理哪个城市,并且我们可以看到哪些城市是虚构的,如下图所示:

图 8.12 – 使用 Write-Verbose 来照亮黑暗

图 8.12 – 使用 Write-Verbose 来照亮黑暗

当我们运行脚本的第一行时,我们只看到错误,但当我们使用-Verbose参数运行时,它会告诉我们在遇到错误时正在处理哪个城市。让我们来看一下本章将介绍的最后一种帮助类型:参数帮助信息。

参数帮助信息

除了控制脚本中参数的工作方式外,我们还可以写出有用的注释,以指导人们如何在我们的脚本中使用参数。这些注释仅适用于必填参数,并且在没有提供必填参数时可用。让我们试试看。编辑$City参数如下:

Param(
[Parameter(Mandatory, HelpMessage="Enter one or more city names separated by commas")]
[string[]]$City = "London",

现在,当我们在没有为$City参数提供值的情况下运行时,会得到一条提示信息,建议我们输入一个反问符号(!?)来获取帮助:

图 8.13 – 访问 HelpMessage 参数

图 8.13 – 访问 HelpMessage 参数

一旦我们输入反问符号,脚本中编写的信息就会显示出来。就这么简单;只要记住,只有在必填参数时才能使用参数帮助消息。

本章内容差不多到此为止。让我们回顾一下我们所学的内容。

总结

在本章中,我们已经从学习 PowerShell 语法的基本构件开始,接下来将把它们组合起来。我们学到的技术可能对我们来说不太熟悉,但随着时间和实践的积累,它们将变得熟悉且易于掌握。在接下来的书籍中,我们将有很多机会使用它们。

我们首先讨论了脚本是什么,以及我们为什么要编写脚本。我们简要了解了如何找到其他人的脚本,以及如何在自己的机器上运行它们。

本章我们做了很多实践工作。首先,我们探讨了如何通过拆分长管道并用变量替代 cmdlet 中的硬编码值,特别是可能会变化的值,来使他人更容易理解我们正在做的事情。

我们接着讨论了如何通过使用参数从外部将这些值传递到脚本中,并查看了我们可以通过将参数设置为必填项或从管道中获取值来定义和操作这些参数的不同方法。

然后,我们研究了如何通过提供注释使我们的脚本更易于理解,接着为它们创建了完整的基于注释的帮助,以便我们能够使用Get-Help cmdlet。

之后,我们查看了如何使用Write-Verbose cmdlet 来提供详细输出。这帮助我们理解当脚本似乎没有按预期工作时,脚本到底在做什么。

最后,我们研究了如何为必填参数提供帮助消息,以便让人们更容易地运行我们的脚本。

在下一章中,我们将探讨如何使用脚本块、lambda 表达式和函数来使我们的代码更简洁、更容易编写。

进一步阅读

练习

  1. 当我们尝试运行我们写的脚本时,机器上的另一个用户收到一条错误信息,提示running scripts is disabled on this system。我们需要做什么才能允许他们运行这个脚本?

  2. 我们有一个喜欢龙与地下城的年轻亲戚,但他们丢失了 20 面骰。我们为他们写了一个简短的 PowerShell 脚本Get-Die.ps1,如下所示:

    Get-Random -Minimum 1 -Maximum 20
    

    这段代码每次运行时都会生成一个介于 1 和 20 之间的随机数。如果他们失去了一颗不同面数的骰子,那么这行代码中最有可能变化的值是什么?

  3. 在前一个问题中的Get-Die.ps1脚本中,我们该如何为变化的值设置参数?

  4. 这个参数应该是什么类型的,我们该如何指定它?

  5. 他们喜欢这个脚本,现在他们失去了四面骰。他们发现可以在参数中放入任何自己喜欢的数字。虽然一开始这很酷,但现在他们希望脚本只允许他们掷游戏中使用的骰子。我们该怎么做呢?我们需要知道龙与地下城使用的是 4 面、6 面、8 面、10 面、12 面和 20 面骰子,并且我们还需要阅读文本中提供的关于参数的链接。

  6. 我们的亲戚解释说,有时他们需要一次掷多个相同类型的骰子,并问是否有办法做到这一点。当然有……我们来为他们写一个吧。

  7. 他们注意到有时会忘记输入骰子的数量,然后结果总是 0。我们该如何阻止这种情况发生?

  8. 我们不可能一直在场,有时他们不确定该在参数中输入什么。我们该如何让他们更容易使用?

  9. 他们真是让我们耐心到了极限。时不时他们需要掷一个叫做 d100 的骰子。这是一个 10 面骰,数字以十为单位递增:0, 10, 20, 30,依此类推,再加上一颗普通的 10 面骰子,两者加在一起给出一个介于 1 和 100 之间的数字。我们答应自己再也不为他们看管了,应该怎么做呢?

第九章:不要重复自己 – 函数和脚本块

在这一章,我们将学习软件开发的基本原则之一,并学会如何应用它来节省我们的精力,使代码更易于维护:不要重复自己,也称为DRY原则。在安迪·亨特和戴夫·托马斯的《务实程序员》中,这一原则被表述为:“每一条知识都必须在系统中有一个单一、明确、权威的表现形式。” 一些程序员会将这一原则推向其逻辑极限,确保代码中没有任何重复。我们写的是脚本,所以我们会在方便的情况下应用这一原则。我们将讨论代码中的函数概念。到本章结束时,我们将看到如何在脚本中编写函数来替换重复的代码,并了解这样做如何使我们的脚本更容易适应和修复。我们还将了解与函数相关的另一种表达式:脚本块。在此过程中,我们还必然会讨论范围的概念。

在这一章,我们将涵盖以下主题:

  • 为什么我们关心重复的代码?

  • 如何将重复的代码转换为函数

  • 范围的概念

  • 探索脚本块

  • 让我们做点有用的事情

为什么我们关心重复的代码?

显而易见的原因是,在 PowerShell 中搞清楚如何做某事之后,却还需要不断重复相同的行,实在是很无聊。例如,在第四章《PowerShell 变量与数据结构》中,我们通过引用TypeName Imaginary.Bike来讨论对象。Imaginary.Bike对象有三个属性:handlebarwheelcolor。假设我们想编写一个简短的脚本来验证假想自行车是否具备所有的属性。它可能看起来像这样:

$mybike = @{handlebar = "ApeHanger"; color = "Red"; wheel = 15}
if (!($mybike.handlebar)) {
    write-output "this bike has no handlebars"
}
if (!($mybike.wheel)) {
    write-output "this bike has no wheels"
}
if (!($mybike.color)) {
    write-output "this bike has no handlebars"
}
if (!($mybike.gears)) {
    write-output "this bike has no gears"
}

在第一行,我只是定义了我的假想自行车。验证脚本从那之后开始。我加入了一个不存在的属性gears,只是为了确保代码能正常工作。我们可以看到脚本中有很多if语句,它们几乎做着完全相同的事情:检查一个属性是否存在,如果缺失则输出一条消息。我们对这些重复不太在意,因为我们只检查了四个属性。但如果我们需要检查 40 个或者 400 个属性呢?这就是 DRY 原则发挥作用的地方。我们可以像这样编写一个脚本来完成相同的事情:

$properties = @('handlebar', 'wheel', 'color', 'gears')
foreach ($property in $properties) {
    if (!($mybike.($property))) {
        write-output "this bike has no $property"
    }
}

从现在开始,我们添加的每个属性只需要在$properties数组中添加一个单词,而不需要三行新的代码。很棒,对吧?

然而,我们这里不仅仅是在省去重复输入的麻烦。我们还在省去找出打字错误的努力,这些错误可能会出现在数百行代码中。如果foreach循环对一个属性有效,那它对所有属性都有效,除非我们在将属性添加到$properties数组时打错了属性名称。如果有什么不工作,我们知道该去哪里查找,任何需要在我们之后排查问题的人也能知道该查找哪里。

DRY 原则不仅仅适用于过度的代码重复,它在软件中可以广泛应用。再次引用《程序员修炼之道》中的话,“许多人以为 DRY 只指代码:他们认为 DRY 意味着‘不要复制粘贴源代码’…… DRY 是关于知识、意图的重复。它是指在两个不同的地方表达相同的内容,可能用的是完全不同的方式。

想象一下,如果我们不仅写了一个脚本,而是写了一堆与虚拟自行车相关的脚本。也许我们经营一家虚拟自行车店,并希望拥有一个库存管理应用程序。我们可能会发现,在这些脚本中,我们需要频繁引用虚拟自行车的属性。最终,可能会出现很多地方都有属性列表。我们不难想象,这些列表可能会有所不同。某些列表可能不完整,某些可能已经过时。如果我们应用 DRY 原则,那么就会有一个属性列表供所有脚本使用,当提到虚拟自行车的属性时,它们就会去这个列表中查找。

总结一下,我们关心重复代码的三个原因:

  • 我们不想编写超过必要的代码。

  • 我们希望更容易发现错误。

  • 我们希望我们的代码有一个单一的真实来源。

我们是否希望在所有情况下、每时每刻都减少代码重复?可能不是。回到第八章中的自动化部分,编写我们的第一个脚本 - 将简单的 Cmdlet 转换为可重用代码,有时候将重复最小化所需的努力并不值得。也许我们只会用到某段代码两次;在这种情况下,复制粘贴完全没问题,而不是想办法只写一次。还有两个值得了解的软件开发原则:if语句,比起编写第二个示例中的脚本,使用一个不会被用到的抽象(你不会需要它)更好。我们应该做出可能最简单的有效方案。这个原则的一个主要启示是,软件开发人员喜欢使用缩写。

现在我们知道了为什么要减少代码重复,让我们来看看在 PowerShell 中实现这一点的主要方式之一:使用函数。

如何将重复的代码转化为函数

函数是一个命名的代码块,脚本或程序中的一段代码,它被赋予一个标签,可以通过引用该标签来使用。这是一个非常常见的编程范式,几乎所有的命令式编程语言都有出现。有些语言,如 PowerShell 和 Python,将这些代码块称为函数。有些则称它们为子程序、子程序块、方法或过程。这个范式被称为过程式编程,位于大多数人使用 PowerShell 编写脚本与像 C++ 和 Java 这样的全面面向对象编程语言之间。函数式编程是另一种范式,具有声明式风格。

命令式、声明式、过程式和函数式

我在这里抛出了一些新词,因此值得快速解释一下它们的含义。

命令式语言是像 Python 和 PowerShell 这样的配方语言。脚本或程序中的每一步都会告诉计算机做某事,并且通常会指定如何去做。这通常是人们学习的第一种编程风格;并且它通常是他们学到的唯一一种风格。

<Heading>,但它的样式由解释 HTML 的浏览器决定。

过程式编程是我们在本章中将要探讨的编程风格:编写过程,这些过程在 PowerShell 中被称为函数,并且从脚本中调用它们。总体编程风格依然是命令式的。

函数式编程是一种声明式的编程风格,在这种风格中,我们在对象上创建函数,然后调用这些函数。纯粹的函数式编程通常被描述为声明式编程。PowerShell 中的函数式编程并不是不可能,但它相当高级,因此我们在本书中不打算讲解。如果你真的有兴趣了解一下,可以访问 Chris Kuech 的 GitHub 页面:github.com/chriskuech/functional,并试用他为 PowerShell 提供的函数式模块。

让我们来看一下一个函数。在 PowerShell 控制台中,试试这个:

function get-square($a) {$a * $a}

现在,输入 get-square,然后输入一个数字,像这样:

get-square 23

就像魔法一样,我们的数字的平方值被返回了。只要控制台保持打开,我们可以随时使用get-square

我们做了什么呢?嗯,我们取了一些代码,{$a * $a},并通过使用 function 关键字给它一个标签(或名称)get-square。我们还告诉它,期望一个变量$a,通过将变量放在名称后面紧跟的括号中。每当我们使用函数名时,代码就在我们传递的对象上运行——在这个例子中是整数 23

我们在控制台中定义并使用函数的情况相对较少。相反,我通常将函数整合到我的脚本中。现在让我们来做这个。打开一个新的 PowerShell 文件,在 VS Code 中继续操作。让我们做一些更有挑战性的事情。我们来创建一个通过近似计算平方根的函数。输入以下内容:

function Get-RoughRoot {
    param (
        $number
    )
    $start = 1
    while (($start * $start) -le $number)  {
        $result = $start
        $start += 1
            }
       return $result
}
Get-RoughRoot 785692

这段代码的作用只是从1开始,不断将数字与自身相乘,直到得到一个大于我们最初传给函数的数字——在这种情况下是785692。它非常简单且直接。如果我们运行这段代码,我们会发现785692的近似平方根是 886,离实际的平方根 886.392 非常接近,但并不完全准确。这展示了机器的一个优势:快速重复。它已经执行了 887 次while循环。

注意我们在第二行开启的param()块。它与我们之前使用的param()块是一样的。事实上,我们可以将这个函数转变为一个cmdletbinding()属性,这样就可以在Get-RoughRoot中使用所有常用参数。我们可以像在脚本中使用param()块一样在函数中使用param()块。

活动 1

如果我们在脚本末尾输入65378 | Get-RoughRoot并运行,结果会怎样?为什么会这样,我们该如何使这行代码有效?

一旦我们在脚本中创建了函数,就可以通过引用函数名称并提供参数值来多次调用它。是的,PowerShell 中确实有一种非常简单的方式来得到精确的平方根,而且代码要短得多,但这样做有什么乐趣呢?

什么是函数?

创建函数需要以下内容:

  • 关键字function,位于开始位置。

  • 一个描述性且有意义的名称。最好遵循 PowerShell 的命名约定,并使用已批准的动词-名词搭配,尤其是当这个函数需要与他人共享时。

  • 一组 PowerShell cmdlet,位于大括号{}中。

我们可以在param()块中包含一组参数,放在大括号后面,或者将它们包含在方括号中并附加到名称后面。更推荐使用param()块,特别是当我们有多个参数时,或者如果我们希望向参数中添加语句——例如,控制它们如何接受管道输入,或者它们接受什么类型的输入。

当我们在脚本中包含函数时,必须记住,计算机是从上到下读取脚本,而不是先读取整个脚本再执行。因此,我们必须在调用函数之前先定义它们。在脚本中通常会把所有函数的定义放在最上面,参数之后。

每个函数由四个语句块组成:beginprocessendclean。虽然我们不需要包含这些语句块来使函数正常工作,但如果不包含它们,我们需要了解 PowerShell 如何解析我们的代码。

begin 语句块用于设置函数的语句,并且只在函数被调用时执行一次,且在函数对输入进行任何操作之前执行。类似地,end 语句块也只会在函数调用时执行一次。process 语句块会对传递给函数的管道中的每个对象执行,因此当从管道接受多个对象时,我们需要包含它,因为如果没有指定语句块,PowerShell 会将所有语句分配给 end 块。

clean 语句块是在 PowerShell 7.3 中引入的。它类似于 end 语句块,但无论函数是因错误终止还是用户按下 Ctrl + C 强制停止,clean 块都会始终执行,而 end 块则不会。另一个区别是,end 块可以将对象输出到管道,而 clean 块不能。

让我们看看实际代码中是如何实现的。在以下图中,我们为 Get-RoughRoot 函数添加了 beginendprocessclean 语句块:

图 9.1 – Begin、process、end 和 clean 语句块

图 9.1 – Begin、process、end 和 clean 语句块

12 行的 begin 语句块包含一个 Write-Output cmdlet,用于显示当前在 -number 参数中保存的值。从底部的输出可以看到,当 begin 块运行时,-number 参数中并没有保存任何值。接着第 15 行的 process 语句块运行,并将当前的 -number 值传递到管道中,且会对管道中每个 -number 的值进行重复处理。然后,第 24 行的 end 语句块执行一次,只能看到 -number 参数传递的最终值。最后,第 28 行的 clean 块执行。我们可以看到它没有任何输出,但成功执行。如果你不相信我,可以试试——写一行无法运行的代码,比如 write-rubbish "here is some rubbish"。这时 PowerShell 会报错,输出一堆红色的文本。

还有一个需要注意的重要关键字:returnreturn 关键字用于结束当前正在执行的语句块的迭代。让我们看看使用它时会发生什么。以下是 Get-RoughRoot 函数的代码,在 process 块中添加了一个 if 语句,如果原始数字小于 10,则返回 pop 字符串:

function Get-RoughRoot {
    param (
        [Parameter(ValueFromPipeline)]
        $number
    )
    begin {
    write-output "for the begin statement, the number is $number"
    }
    process {
        $start = 1
        while (($start * $start) -le $number)  {
            $result = $start
            $start += 1
                }
        if ($number -lt 10) {
            return "pop"
        }
        write-output "The rough root of $number is $result"
        }
    end {
        write-output "for the end statement, the number is $number"
    }
}

现在让我们在管道中传递一个小于 10 的数字,如下所示:

785692, 4, 3492858  | Get-RoughRoot

我们将在以下图中看到输出:

图 9.2 – 使用 return 关键字

图 9.2 – 使用 return 关键字

我们可以看到两个大数字的粗略根被返回,但当输入一个小于 10 的数字时,进程块提前终止。

return 关键字返回了它所分配的值(pop),但这完全是可选的——我们不需要给 return 分配一个值。值得注意的是,对于那些熟悉其他语言的人来说,这一点可能会有些困惑。例如,在 Python 中,return 语句用于提供函数的结果。在 Python 和 PowerShell 中,return 语句的工作方式非常相似,但它们背后的意图不同;PowerShell 会在没有 return 关键字的情况下返回函数的输出,而 Python 则不会。对于 Python 和 PowerShell,两者的 return 都会在使用的地方停止函数的执行。

基本函数和高级函数

我在脚本中编写的大多数函数都是基本函数,但我们可以通过使用 CmdletBinding 特性使函数表现得像一个 Cmdlet。这让我们能够访问 Cmdlet 中看到的一系列行为,比如 -whatif-confirm 参数。它还让我们能够访问像 WriteCommandDetailWriteError 这样的高级方法。

函数参数

如我们所提到的,函数的参数和脚本中的参数工作原理相同,但值得在这里重新回顾一下。

参数类型

我们可以在函数(以及脚本中!)使用四种类型的参数:

  • 命名参数

  • Switch 参数

  • 位置参数

  • 动态参数

让我们仔细看看。

命名参数

命名参数是我们一直在使用的参数,我们提供一个变量,作为参数的名称,像这样:

Param(
$number
)

该参数的名称,无论是在函数还是脚本中,都是 -number。我们可以在函数的任何地方使用 $number 变量,它将具有在参数中提供的值。正如我们在第八章中所看到的,编写我们的第一个脚本——将简单的 Cmdlet 转换为可重用的代码,我们可以像这样为参数设置默认值:

$number = 100

我们还可以通过在变量前用方括号提供类型值来指定参数的类型:[int]$number

Switch 参数

Switch 参数不需要值,它们只是开或关。如果我们为函数提供了参数,则表示开;如果没有提供,则表示关。我们将参数的类型指定为 [switch],像这样:[switch]$heads。我们可以在函数中使用 if 语句来测试开关是否存在,像这样:

Function get-flip {
    param([switch]$heads)
    if ($heads.ispresent) {"Heads!"}
    else {"Tails!"}
}

我们不需要 .ispresent 属性,可以直接输入 if ($heads),但我认为那样不够清晰。我们也可以使用 if ($heads -eq $true) 结构,这在旧的网络文章中可能会看到,但那不是微软推荐的方法。

位置参数

这些参数利用了$Args自动变量,而不是使用param()块。这个参数存在于每个基本函数中,并允许我们创建未命名的参数,像这样:

Function get-product {
$product = $Args[0] * $Args[1]
Write-output "The product of $($Args[0]) and $($Args[1]) is $product"
}

这意味着我们可以像这样提供参数:

Get-product 2 4

我们可以在下图中看到结果:

图 9.3 – 使用位置参数

图 9.3 – 使用位置参数

我们可以看到,函数已经正确地赋值了我们提供的参数。$Args参数是一个数组。

请注意,如果我们使用param()块,则它将覆盖此功能;它将无法正常工作,因为我们需要指定位置参数。默认情况下,参数是位置性的,按照参数的书写顺序排列,因此这将是有效的:

Function get-product {
Param($a,$b)
$product = $a * $b
$product
}
Get-product 2 4

2将被赋值给$a,值4将被赋值给$b。然而,如果某个参数必须位于特定位置,我们应该使用position属性,具体内容将在下一节中讨论。

$Args自动变量还可以用于将参数传递到函数中——有关 splatting 的更多信息,请查看第四章PowerShell 变量与数据结构。让我们通过下图来看一下它是如何工作的:

图 9.4 – 将  变量传递到函数中

图 9.4 – 将 $Args 变量传递到函数中

在第一行中,我们创建了一个名为Get-Fifteen20的简单函数。它运行Get-Random。我们告诉它接受来自命令行的参数,并将这些参数存入一个名为@Args的数组中(请注意数组符号@)。之后,我们告诉它输出$Args自动变量的值。从输出结果来看,首先我们得到一个介于 15 和 20 之间的随机数(19),然后我们看到$Args变量的内容。它是一个包含四个对象的数组:-minimum15-maximum20。这些参数顺序传递给函数中的Get-Random cmdlet,形成有效的 PowerShell 语句Get-Random -minimum 15 -maximum 20,该语句运行正常。

活动 2

为什么Get-Fifteen20 15 20不起作用?

动态参数

动态参数仅在指定的条件为真时可用。它们不是在param()块中定义的,而是在单独的DynamicParam{}块中定义——请注意,这里使用的是大括号,而不是方括号。动态参数是一个相对高级的话题,只有在绝对必要时才应使用,因此我们在这里不会花太多时间讨论它们。关于它们如何工作的一个很好的例子可以在Get-ChildItem命令中找到。Get-ChildItem可以与任何 PSProvider 一起使用——我们在第六章中讨论过 PSProvider,PowerShell 与文件操作——读取、写入和处理数据。根据我们使用的 PSProvider 不同,Get-ChildItem的可用参数也会不同。例如,如果我们使用的是FileSystem提供者,我们可以使用诸如-File-Hidden等参数。如果我们使用的是Certificate提供者,那么这些参数将不可用,取而代之的是-DnsName-SslServerAuthentication等参数。

现在我们已经涵盖了参数的类型,让我们来看看我们可以应用于参数的一些属性。

参数属性

属性是可选的参数,我们可以用它们来控制参数的行为——例如,是否是必选的,或者是否接受管道输入。要使用属性,我们必须以Parameter()属性开始每个参数,它的形式如下:

Param(
[Parameter(Argument = Value)]
$ParameterName
)

Parameter()属性接受多个参数,但我们必须用逗号(,)将它们分开。让我们来看一下更常见的几个参数。

CmdletBinding

CmdletBinding属性使函数表现得像一个 cmdlet。如果我们在函数中使用此属性,我们可以访问常见的参数,如-whatif-confirm。这将移除该函数对$Args自动变量的访问。

必选

正如我们在第八章中看到的,编写我们的第一个脚本——将简单的 cmdlet 转化为可重用的代码,我们可以使用Mandatory参数来确保为某个参数提供值,使用HelpMessage参数来为该必选参数提供帮助信息:

Param(
[Parameter(Mandatory, HelpMessage="Type one or more integers, separated by commas." )]
[int[]]
$number
)

这意味着必须至少为-number参数提供一个整数,并且可以访问帮助信息。

Position

Position参数允许在不明确指定名称的情况下传递参数,并且可以指定参数必须提供的位置:

Param(
[Parameter(Mandatory, Position=0 )]
[int[]]
$number
)

上面的代码告诉我们,未指定名称的第一个传递值将应用于-number参数。有两点需要注意:第一,命名参数不计入其内,第二,编号是从0开始的,因此第二个位置为Position=1。请注意,如果我们使用的是param()块,我们之前讨论的$Args功能将无法使用,我们将无法在函数内将参数传递给 cmdlet。

如果我们不使用Position参数,那么所有参数将按照在param()块中声明的顺序分配位置,但我们不应依赖这种顺序完全按预期工作。如果我们希望参数是位置参数,那么我们应该明确声明它。

ParameterSetName

我们可以使用ParameterSetName参数来定义仅在特定参数集存在的参数。如果未提供ParameterSetName,则该参数属于所有参数集。我坚持认为一个函数应该做一件事,因此我从未真正需要参数集。然而,有些人喜欢编写多功能的“瑞士军刀”函数,因此参数集对他们来说是有用的。

ValueFromPipeline

我们在本章中已经使用过这个属性——它是允许函数参数接受管道中的整个对象的必要条件。

ValueFromPipelineByPropertyName

如果我们只希望参数接受一个对象的属性(例如,名称),那么我们可以使用这个参数。如果管道中的对象有一个名称属性(例如,$object.name),那么该属性将被用作该参数。对象的属性名必须与参数名匹配;否则,参数不会传递。如果我们将一个字符串通过管道传递给一个接受管道值的-length参数,那么该参数将被填充为字符串的长度,而不是实际的字符串。如果参数名为-stringlength,那么它将为空,因为字符串没有stringlength属性。

ValueFromRemainingArguments

我们可以使用这个参数来保存一个不确定数量的参数,作为数组传递给函数。然后,我们可以通过数组中位置的索引访问这些参数。这在高级函数中非常有用,在这些函数中我们无法访问$Args自动变量,且我们希望捕获不确定数量的参数。

HelpMessage

如我们已经看到的,这个属性可以用来为必填参数提供一个有帮助的提示信息。

别名

我将其包括在这里是为了完整性,但我认为我已经明确表达了关于别名的看法。别名有时可能有用,但它们也会导致混淆,并使代码更难以阅读。不过,确实可以使用Alias属性为参数提供别名。

SupportsWildcards

如果我们希望参数接受通配符,则可以使用这个参数。请注意,这并不意味着函数支持通配符——我们仍然需要在函数中编写能够处理通配符输入的代码。

参数完成属性

有两个参数完成属性允许用户通过Tab键完成参数值。这些属性类似于ValidateSet属性。我们在本书中不会详细讨论它们,但知道如果需要,我们可以使用它们。

验证属性

目前有 14 个属性可以用于验证参数及其提供的值。我们可以验证一个值是否为 null,验证一个值是否符合特定模式(如信用卡号或 IP 地址),以及验证一个值是否具有特定的长度或是否在某个范围内。我们在第八章中发现了ValidateSet属性,编写我们的第一个脚本—将简单的 Cmdlet 转化为可重用代码。它的工作方式如下:

Param(
[Parameter (Mandatory)]
[ValidateSet("Pontypool", "Newport", "Swansea","Llanelli", "Cardiff")]
[string]$team
)

只有当字符串出现在ValidateSet列出的数组中时,才会接受该字符串。请注意,验证属性位于Parameter()属性之外。

虽然我们可以覆盖更多关于参数的方面,但现在就先到这里吧。让我们来看一个专门类型的函数:过滤器。

过滤器

过滤器是一个专门的函数,自动在管道中的所有对象上运行。过滤器类似于一个只包含process{}语句块的函数。我们可以这样使用它们:

图 9.5 – 使用过滤器

图 9.5 – 使用过滤器

我们没有使用function关键字,而是使用了filter。我们把过滤器命名为square,并定义它为将管道对象与自身相乘。现在我们可以通过管道将一个值传递给过滤器,并获得输出。请注意,我们不能像使用函数那样使用它;square 36是无效的,因为管道中没有任何东西可以供过滤器处理。

然而,在我们真正开始使用函数之前,我们需要讨论一个重要的概念:作用域。

范围的概念

PowerShell 使用作用域的概念来保护变量、函数、PSDrives 和别名,避免它们被无意间更改,通过限制它们的访问和修改方式。让我们来演示一下:

  1. 创建一个变量并设置其值:

    $ScopeTest = 10
    
  2. 创建一个函数:

    $ScopeTest by calling the variable:
    
    

    $ScopeTest

    
    
  3. 运行我们的函数:

    $ScopeTest inside the function is 15. Let’s check whether the value has changed permanently:
    
    

    $ScopeTest

    
    No, it hasn’t. That’s because the function is operating on its local scope; it can read the value of the variable, and it can change it while it’s running, but it can’t change it permanently because the variable exists outside the function. This is known as the scope of the function.
    

PowerShell 有以下几种作用域类型:

  • 我们刚才使用的$scopeTest变量。任何在全局作用域中定义的内容在当前会话内的任何地方都可以使用。

  • Set-ScopeTest函数中,有一个与全局作用域不同的作用域。当我们在函数内部更改$SetScope变量时,它只会在函数的本地作用域中更改,而不会影响全局作用域。本地作用域相对于其他作用域,不是预定义的作用域,因此它可能指向全局作用域、脚本作用域或全局或脚本作用域的子作用域。我们可以创建多个嵌套的本地作用域。当我们运行脚本或函数时,我们就创建了一个新的本地作用域。

  • 脚本:这是我们运行脚本时创建的作用域。只有脚本中的语句在脚本作用域内运行,这些语句将脚本作用域视为本地作用域。每个正在运行的脚本都有自己的脚本作用域;它们不会共享对象。

父作用域和子作用域

作用域有层级关系。包含在另一个作用域中的作用域被称为子作用域,包含作用域的作用域则被称为父作用域。全局作用域始终是根作用域,是所有作用域的父作用域,其他作用域都是全局作用域的子作用域。因此,当我们之前运行 Set-ScopeTest 时,我们为该函数创建了一个本地作用域,它是全局作用域的子作用域。该本地作用域可以读取父作用域中的变量,但默认情况下不会更改父作用域中的变量;它只能在本地作用域中更改变量。接下来,让我们在 Set-ScopeTest 函数中添加几行代码来说明这一点:

图 9.6 – 示意父作用域和子作用域

图 9.6 – 示意父作用域和子作用域

我们添加的第一行代码获取了函数从其父作用域继承的 $ScopeTest 变量的值(在此情况下为 10),并将其输出到屏幕。然后我们像之前一样在函数内更改 $ScopeTest 的值,并输出新的值。接着,我们使用 Get-Variable 获取全局和本地作用域中 $ScopeTest 变量的值。我们可以看到,即使在函数内,$ScopeTest 的全局值仍然是 10,只有函数内部的本地实例的值被更改。那么,如果我们想要通过函数修改父作用域中变量的值,该怎么办呢?让我们看看如何实现这一点。

作用域修饰符

作用域的使用是有原因的——它们可以保护我们避免不小心更改内容——我们应当谨慎地跳出自动作用域,但有时我们确实需要这么做。PowerShell 包含多个作用域修饰符,允许我们更改默认作用域。它们包括以下几种:

  • Global:指定全局作用域中的对象。

  • Local:指定当前作用域中的对象。

  • Script:指定父脚本作用域中的对象,或者如果没有父脚本,则指定全局作用域;我们在脚本中的函数中使用这个修饰符。

  • Private:在创建变量时使用该修饰符,可以防止子作用域使用该对象。

  • <Variable-namespace> 这些指的是 PSDrive 名称空间,例如 env:variable:

还有一些其他的修饰符我们这里不讨论:workflow:(已弃用)和 using:(用于像 Invoke-CommandStart-Job 这样的 cmdlet)。脚本默认在 script: 作用域中运行。即使在脚本中定义,函数也默认在 local: 作用域中运行。

我们在 $ 符号和变量名之间使用作用域修饰符,像这样:

$global:ScopeTest = 10

这将创建一个名为 ScopeTest 的全局作用域变量,并将其值设置为 10(记住,变量名没有 $ 符号——我们使用 $ 符号来引用变量的内容)。

同样,我们也可以像这样定义函数的作用域:

Function Global:Set-ScopeTest {$ScopeTest = 15}

注意,将函数的作用域设置为全局作用域并不会改变函数的作用方式,而是改变它的可用范围。因此,一个全局作用域的函数并不会自动作用于全局变量,如我们在这里看到的:

图 9.7 – 全局函数和全局变量

图 9.7 – 全局函数和全局变量

我们可以看到 $ScopeTest 的原始值是 10。然后我们定义了一个名为 Set-ScopeTest 的全局函数,将值设置为 15。调用该函数并不会改变 $ScopeTest 的全局值。为了做到这一点,我们需要告诉函数作用于全局变量,而不是在函数内运行的本地版本。我们在第二个命令中通过以下代码来做到这一点:$Global:ScopeTest = 15

在接下来的书中,我们会看到作用域的概念,并且无疑会偶尔为它感到沮丧。记住,它的存在是为了保护我们,提供一套有用的护栏,防止我们遇到滑稽的后果。我们应该在一些思考和考虑之后才跳出默认的作用域。我们通常可以通过将变量作为参数传递,或者明确地从函数内部写出输出并将其存储为变量来避免这样做。

现在我们理解了函数,让我们来看一下它们的亲戚:脚本块(scriptblocks)。

探索脚本块

脚本块是大括号({})内的一组语句,可以作为一个单元使用。我们已经多次使用过它们:在 Where-Object cmdlet、ifelse 语句、foreach 循环中,以及在本章前面,当我们写函数时。在本节中,我们将看看脚本块的一些属性,以及如何在代码中使用它们。

考虑我们刚才做过的所有关于函数的内容。一个函数由 function 关键字、名称和一个脚本块组成:一组位于大括号内的语句。我们不需要使用关键字来使用脚本块——关键字提供了一个标签,我们用它来调用脚本块,当我们需要时。

脚本块返回它们包含的所有语句的输出;这可能是一个单一对象或一个对象数组。我们可以使用 return 关键字,它的作用和在函数中的作用一样:它会在那一点退出脚本块。

我们可以使用 param() 块为脚本块创建参数,它将接受我们在函数中使用的所有参数类型和属性。我们不能做的是像函数一样在大括号外传递参数,因为没有名称值将其附加到。

如何运行脚本块

调用脚本块有很多方法——许多 cmdlet 和语句(如 foreach)都接受它们。然而,有时我们只想运行脚本块。Invoke-Command 是一种实现方式,像这样:

$a = 10
Invoke-Command -ScriptBlock {$a * $a}

这将返回输出 100。让我们看看其他几种可能的实现方式。

与函数不同,脚本块可以存储在变量中,像这样:

$square = {$a * $a}

现在,我们可以以以下方式使用$square变量:

  • 我们可以将其作为参数传递给Invoke-Command,无论是否带有-ScriptBlock参数名:

    Invoke-Command $square
    
  • 使用Get-Member,我们可以看到$squareTypeNameSystem.Management.Automation.ScriptBlock。这个类型有一个叫做invoke()的方法,我们可以像这样使用它:

    $square.invoke()
    
  • 假设我们在脚本块中放了一个参数,像这样:

    $square = {param($a) $a * $a}
    $square.invoke(20)
    

    我们不能做的事是直接调用变量来运行脚本块:

图 9.8 – 如何不运行脚本块

图 9.8 – 如何不运行脚本块

如你所见,如果我们调用这个变量,我们得到的是脚本块的内容,而不是输出。相反,我们使用调用操作符(&)来像这样调用:

&$square 20

我们可以在&$square之间留一个空格,写作& $square,我们经常会看到这样写。

  • 还有一种方法:点源。我们可以使用一个点(.)代替调用操作符,像这样:

    . $square
    

当我们这样调用脚本块时,需要非常小心。使用Invoke-Commandinvoke()和调用操作符都会在子作用域中运行脚本块,这正是我们所期望的。点源会在父作用域中运行脚本块,如下图所示:

图 9.9 – 调用操作符和点源

图 9.9 – 调用操作符和点源

在第一行,我们定义了一个变量$SbScope,它的值是Global scope!字符串。接着我们定义了一个变量$ChangeSbScope,它是一个脚本块,设置$SbScopeLocal scope!并返回它。当我们用调用操作符调用$ChangeSbScope时,它会在本地作用域内运行,并保持$SbScope的全局值Global scope!不变。然而,当我们使用点源(dot sourcing)调用$ChangeSbScope时,我们会看到它改变了$SbScope的全局作用域值。要小心,除非你非常确定必须这样做,否则不要使用点源。

Lambda

大多数编程语言都有lambda的概念——匿名函数,即没有名字的函数。在 PowerShell 中,我们有脚本块,它是一个更广泛的概念,包含了 lambda。所以在 Python 中,我们可能会有如下的 lambda 语句,它将一个值加到它自身:

Add = lambda a: a + a
Add(20)

然后我们得到输出40。在 PowerShell 中,相应的 lambda,使用脚本块,应该如下所示:

$add = {param($a) $a + $a}
&$add(20)

再次地,我们得到40。Python 中的 lambda 仅限于表达式,不能包含语句,而在 PowerShell 中,我们没有这样的限制。

我们什么时候需要使用脚本块而不是函数呢?通常情况下,我们使用脚本块来处理简短且简单的代码块,这些代码块在脚本中可能只会出现几次。我也常常在控制台中使用它们。如果我要写一个其他人也会用的脚本,那么任何超过一两行的代码都会放入函数中,以提高可读性,并确保其他人能清楚地知道我在做什么。

对于脚本块的最终应用场景,是针对那些已经掌握其他编程语言(如 Python)的人。将脚本块用作 lambda 表达式的替代方案,允许他们继续以自己习惯的风格编写代码。

这就是关于函数和脚本块的全部内容。接下来,我们来看看一个实际应用。

让我们做点有用的事情

当我为客户编写 PowerShell 脚本时,我喜欢确保脚本能够记录它正在做的事情以及它所做的任何更改到日志文件中,这样我可以快速找到发生了什么情况。然而,每次编写脚本时,我并不会编写这些功能;我只是将一些已保存的 PowerShell 代码片段包含在脚本中,给我一个日志文件功能。现在,让我们考虑一下如何做到这一点。

首先,我们需要考虑将日志文件创建在哪里。我使用C:\temp,因为这个目录通常是我们放置不需要永久保存的东西的地方,而且它通常具有较为宽松的权限。对于 Linux 或 macOS,我们可能需要考虑选择不同的目录,例如/var/log/

我们还希望创建一个尽可能易于阅读的文件;我写入的是文本文件,但我使用.log后缀,这样我就知道它是什么类型的文本文件。我们创建的文件需要带有日期戳,以便知道哪个文件与脚本运行的哪个实例相关。这意味着我们需要在函数外部创建它,在脚本级别创建;否则,每次调用函数时,我们都会创建一个新文件。

每个文件条目需要有时间戳,这样我们就能知道日志中事件发生的顺序,同时它还需要记录一个可以从函数外部传递的字符串。

看起来我们需要两个参数:$LogFile$LogString。让我们开始编写:

  1. 在 VS Code 中创建一个新的 PowerShell 文件;我将我的文件命名为Write-Log.ps1

  2. 首先,让我们创建$LogFilename

    -UFormat parameter of Get-Date; it’s to produce an easily readable text string that will work with Windows file-naming rules.
    
  3. 现在,我们需要一个描述性的名称,例如Write-Log

    Function Write-Log {
    param() block for our $LogString parameter:
    
    

    Function Write-Log {

    Param(

    $logString

    )

    }

    
    
  4. 现在,让我们来看一下功能部分。我们需要一个变量,用来存储当前日期和时间的字符串。这里有另一种实现方式:

    $stamp variable and the $LogString variable that we get from the parameter:
    
    

    \(LogMessage = "\)stamp $Logstring"

    
    
  5. 最后,我们需要将其写入日志文件:

    Add-Content $Logfile –Value $LogMessage
    
  6. 就这样。让我们在函数后面加一行代码来检查它是否有效:

    Write-Log "is this thing on?"
    

    然后让我们运行它看看效果。

图 9.10 – 是的,就是这样

图 9.10 – 是的,就是这样

这是我的完整代码:

$LogFile = "c:\temp\MyLogFile" + $(Get-Date -UFormat "%Y-%m-%d_%H-%M-%S") + ".log"
Function Write-Log {
    Param (
        [string]$LogString
    )
    $stamp = (Get-Date).ToString("yyyy/m/dd HH:mm:ss")
    $LogMessage = "$stamp $Logstring"
    Add-Content $Logfile –Value $LogMessage
}
Write-Log "Is this thing on?"

我用于工作的函数还做了许多其他事情,我们将在下一章中讨论其中一些内容——第十章错误处理——哦不!出错了!

活动 3

这个函数将文件写入客户端的某个位置。如果我们经常运行使用它的脚本,最终会有很多文件堆积起来。那么,我们如何编写一个函数,在运行脚本时清理旧的日志文件呢?

总结

在本章中,我们做了一些有趣的事情。我们考虑了一些软件工程的基本原则,特别是 DRY(不要重复自己),并探讨了如何在 PowerShell 脚本中应用这些原则。我们详细了解了函数是如何构建的以及它们的工作原理。我们简要讨论了基本函数与高级函数之间的区别。

接着我们讨论了函数中可以使用的四种参数类型:命名参数、开关参数、位置参数和动态参数。我们还学习了 $Args 自动变量,并了解了如何使用它将参数传递给我们的基本函数中的 cmdlet。

接下来,我们讨论了可以应用于参数的多种属性类型,以控制脚本的行为。我们集中讨论了更常见的属性,并提到了一些补全属性和验证属性的存在。

最后,我们看了一种特殊类型的函数——筛选器(filter),并了解了如何使用它来处理管道对象。

然后我们花了一些时间研究作用域的概念,了解了如何利用它来保护我们的环境,并保持脚本和函数在受限的内存区域内工作。我们学习了父作用域和子作用域以及作用域层次结构的概念。最后,我们看到了如何强制函数和脚本在默认作用域之外工作。

接着我们学习了脚本块,并了解了它们与函数的关系。我们讨论了调用它们的不同方式,包括点源(dot sourcing)——一种需要谨慎使用的方法。然后我们了解了脚本块如何与一个常见的编程概念:匿名函数或 lambda 相关。

我们讨论了不少理论,所以我们在本章结束时,探讨了如何构建一个有用的函数,这个函数可以在多个脚本中使用——写入日志文件。我希望你觉得这很有用;我知道我经常用它。

在下一章,我们将讨论如何处理错误,包括在脚本中的错误以及脚本与外部世界交互时遇到的错误。

进一步阅读

)

+   关于高级函数:[`learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced?view=powershell-7.3`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced?view=powershell-7.3)

+   关于高级函数方法: [`learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_methods?view=powershell-7.3`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_methods?view=powershell-7.3)

+   关于高级函数参数: [`learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-7.3`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-7.3)

+   关于高级函数参数补全: [`learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_argument_completion?view=powershell-7.3`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_argument_completion?view=powershell-7.3)

+   关于 CmdletBinding: [`learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute?view=powershell-7.3`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute?view=powershell-7.3)

+   关于函数输出: [`learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_outputtypeattribute?view=powershell-7.3`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_outputtypeattribute?view=powershell-7.3)

+   关于脚本块: [`learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_script_blocks?view=powershell-7.3`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_script_blocks?view=powershell-7.3)

练习

  1. AHA 代表什么?

  2. 为什么在使用点来源时需要特别小心?

  3. 我们创建了一个名为$ScriptBlock的变量,并用一个脚本块填充它。假设我们这样调用它:

    $ScriptBlock
    

    我们没有得到预期的输出。为什么呢?

  4. 我们可以使用什么参数来检查输入是否为有效的 IP 地址?

  5. 我们创建了一个过滤器get-square,并尝试这样使用它:

    Get-square 365
    

    为什么没有输出?

  6. 我们在这里做了什么?

    $private:number
    
  7. 函数和脚本块之间的主要区别是什么?

  8. 我们在这里做错了什么?

    Function get-square($a) {$a*$a}
    15 | get-square
    
  9. 我们在这一章花了很多时间得到了一个不准确的平方根。我们如何编写一个函数来获得精确的平方根?

    Function get-root($a) {
    <what goes here?>
    }
    

第十章:错误处理 – 哎呀!出错了!

在使用 PowerShell 时,我们通常会遇到两种类型的问题:一种是我们的代码遇到的问题,另一种是代码本身存在的问题。第一种类型的问题可能就像在执行 Get-ChildItem 时,出现 FileNotFound 消息那样简单。第二种类型的问题则可能更难理解,因为它可能涉及到作用域等问题,正如我们在第九章《不要重复自己 – 函数和脚本块》中所看到的,或者是一个意外的除零错误。我们将在本章的前半部分处理代码遇到的问题,然后在后半部分讨论可能出现在代码中的问题。

我们将从定义什么是错误开始,接着讨论我们的代码可能会遇到的两种错误:终止错误和非终止错误。我们将探讨 PowerShell 如何处理这两种错误,如何改变这种行为,以及为什么我们可能会这样做。然后我们将讨论如何捕获错误,以便我们能够理解它们并围绕它们编写代码。

在本章的后半部分,我们将探讨如何通过调试过程识别代码中的错误原因,并看看 VS Code 中的一些选项,帮助我们简化这个过程。

本章我们将覆盖的主要内容:

  • 什么是错误?

  • 理解错误操作

  • 捕获错误

  • 探索调试

  • 使用 VS Code 进行调试

什么是错误?

正如我们反复看到的,PowerShell 与大多数语言一样,有其独特的术语和对常见词汇的定义。错误也不例外(不过... 有时它是例外,我们稍后会看到。这是一个非常有趣的笑话,我保证你稍后会笑出来。)。在 PowerShell 中,错误广义上是指任何可能在控制台中产生红色文本的情况。让我们看一个例子。在控制台中,输入以下命令:

Get-ChildItem -Name nosuchfile

我们将看到一条红色消息,显示 Get-ChildItem: cannot find path because it does not exist

PowerShell 是一种非常友好和有帮助的语言。它会尽力从错误中恢复,并继续执行它被要求做的事情。在之前的实例中,它被要求做一件事,但未能做到,于是它提供了一条帮助性的错误消息,用通俗易懂的语言描述了为什么它无法完成我们要求的操作。

在这里,后台有很多工作正在进行。我们看到的上一个错误不仅仅是一个文本字符串;它是一个复杂错误对象的一部分,当 PowerShell 找不到文件时,它会生成这个错误对象。还有很多信息可以帮助我们理解发生了什么,并帮助我们解决问题。

我们看到的信息量由一个自动变量 $ErrorView 决定。默认情况下,在 PowerShell 7 中,这个变量设置为 ConciseView,PowerShell 会显示一个简短的友好消息。当我们在互联网上进行研究时,会发现通过将 $ErrorView 变量设置为 NormalView 值,PowerShell 可以显示更多的信息,正如我们在以下截图中看到的那样:

图 10.1 – 更改 $ErrorView 变量

图 10.1 – 更改 $ErrorView 变量

在第一个示例中,框 1$ErrorView 被设置为默认的 ConciseView,我们只看到了一条友好的消息。在第二个示例中,框 2,我们已将 $ErrorView 的值更改为 NormalView,并且获得了更多的额外信息,告诉我们错误发生的位置(line:1char:1)以及它属于哪种类别的错误(ObjectNotFound)。ConciseView 是 PowerShell 7 中引入的;Windows PowerShell 仍然使用 NormalView 作为默认视图。

在本章的剩余部分,让我们通过输入以下命令,将 $ErrorView 变量设置为 NormalView

 $ErrorView = "NormalView"

然而,NormalView 仍然不是完整的故事。要查看生成的完整错误对象,请输入以下内容:

Get-Error

我们应该能看到类似下面的内容:

图 10.2 – 错误对象

图 10.2 – 错误对象

Get-Error 是 PowerShell 7 的一个 cmdlet,允许我们访问存储在 $Error 变量中的错误;我们在 Windows PowerShell 中找不到这个 cmdlet。每个在会话中遇到的错误都会写入这个变量。它由一组错误对象组成,最多可以达到由 $MaximumErrorCount 自动变量设置的最大值。默认情况下,在 PowerShell 7 中,这个值是 256——希望这个数量足够用。我们可以使用 Get-Error 并加上 -Newest 参数,后跟一个整数,来获取指定数量的错误,从最近的错误开始。我们也可以使用标准语法访问数组的单个成员,如下所示:

$Error[0]

这将获取最新的错误——在 Windows PowerShell 中,我们必须这样做。

不是每个我们遇到的错误都有一个友好的消息。有时我们只会看到错误代码,如之前截图中的框中所示。这个代码通常是 10 位数字,如截图中所示,从 -2 开始,或者可能是十六进制的,在这种情况下它会以 0x 开头,后面跟着 8 个字符。这种情况常见于网络错误。

代码相当难懂,但幸运的是,我们有一个工具可以用来解码它,叫做err.exe。它可以在微软官网上免费下载;只需搜索err.exe即可。下载后,我们无需安装它;只需在 PowerShell 控制台或命令提示符中直接运行即可,如下所示:

图 10.3 – 运行 err.exe

图 10.3 – 运行 err.exe

如我们所见,输出信息仅稍微少一些晦涩,并且高度依赖于上下文;在这种情况下,如果我收到代码0x80010002,如图 10.3所示,在我尝试通过网络打开文件时,我会将其解释为ERROR_FILE_NOT_FOUND

现在我们对错误有了更好的理解,接下来让我们看一下在 PowerShell 中会遇到的两种错误类型:终止性错误和非终止性错误。

终止性和非终止性异常与错误

编程语言有一个异常的概念:需要特殊处理的异常情况。通常,当一段代码遇到异常时,它会停止正在执行的任务,并切换到执行异常处理器:一个记录错误的函数或子程序。对于大多数语言来说,错误就是异常,异常也意味着错误。

PowerShell 的一个不同之处在于,错误并不总是异常(尽管异常总是错误)。PowerShell 会尝试从错误中恢复;它会尽量继续执行它正在做的事情,而不是直接停止。它识别两种类型的错误:终止性错误和非终止性错误。终止性错误与异常相同。PowerShell 会输出错误信息并停止正在做的事情。非终止性错误意味着 PowerShell 会记录错误并在可能的情况下继续执行(除非我们告诉它停止)。

PowerShell 异常与 .NET 异常

所以,这可能会让人感到困惑,而且有关这方面的信息也常常更令人困惑(至少对我来说是这样)。例如,Don Jones 在《Learn Windows PowerShell in a Month of Lunches》中像我在这里解释的一样,认为非终止性错误不是异常。然而,Bruce Payette 在《Windows PowerShell in Action》中表示,所有 PowerShell 错误都是异常,包括终止性和非终止性错误。这两位作者(及其书籍)都很出色。我不想说一个是对的,另一个是错的;幸运的是,我不必这么做。仔细阅读《Windows PowerShell in Action》的第 532、543 和 546 页,可以得出以下结论。

每次 PowerShell 记录错误时,都有一个底层的 .NET 异常。请记住,PowerShell 是建立在 .NET 之上的,因此要使一个 .NET 异常成为 PowerShell 的异常,它必须停止 PowerShell 的执行并启动异常处理器函数。非终止性错误不会停止 PowerShell,异常处理函数也不会执行,因此它们对于 PowerShell 来说不是异常,尽管对于 .NET 来说它们是异常。明白了吗?

让我们来说明一下。在上一节中,我们查看了尝试获取一个不存在的文件信息时收到的错误,当时我们运行了Get-ChildItem -Name nosuchfile。现在我们再试一次,将其作为管道的一部分。在我的ch10目录下,我有两个文件:foo.txtbar.txt。让我们看看当我运行以下命令时会发生什么:

"foo.txt", "nosuchfile", "bar.txt" | Get-ChildItem

我们可以在以下屏幕截图中看到结果:

图 10.4 – 一个非终止性错误

图 10.4 – 非终止性错误

如我们所见,PowerShell 获取了第一个项 foo.txt 的详细信息,报告了第二个项 nosuchfile 的错误,然后继续获取第三个项 bar.txt 的信息。这是一个非终止性错误的例子。

那么,什么是终止性错误呢?这是一种会完全停止 PowerShell 的错误——会阻止脚本或管道继续执行的错误。一个很好的例子是拼写错误的 cmdlet,例如:

图 10.5 – 终止性错误

图 10.5 – 终止性错误

在这里,我使用了一个不存在的 cmdlet,Get-ChildItems(记住——PowerShell 的 cmdlet 总是单数形式,从不使用复数),PowerShell 在第一次尝试运行该 cmdlet 时就停止了。如果这是一个非终止性错误,我会期望看到错误出现三次,每次显示管道中的一个文件名。但实际情况是,我们只看到了一次错误,因为管道已经完全终止。

PowerShell 能够区分终止性错误和非终止性错误,这非常棒;在控制台环境中,它非常有用,但当我们运行脚本时,我们不太可能一直在场看到错误闪现然后消失。编写脚本时,我们希望将非终止性错误转化为终止性错误,并将所有错误转化为异常。这样我们可以设置异常处理程序,进行例如将错误日志记录到文件等操作。在我们学习如何处理终止性错误之前,先来看看如何将所有错误转化为异常。

理解错误处理方式

有两种简单的方法可以改变 PowerShell 处理错误的方式:$ErrorAction 首选项变量和 -ErrorAction 参数。我们先来看一下变量。

$ErrorActionPreference 变量

$ErrorActionPreference 自动变量可用于更改 PowerShell 处理错误的方式。默认情况下,它设置为 Continue,意味着显示任何错误并继续执行。以下是该变量的几个更重要的有效设置:

  • Break:此选项会在发生错误时使 PowerShell 进入调试模式。关于调试模式的内容将在本章下半部分讲解。

  • Continue(默认):显示错误信息并继续处理。

  • Inquire:此选项会显示错误信息并请求是否继续或停止。

  • SilentlyContinue:错误信息不会显示,但会被添加到 $Error 变量中。PowerShell 继续执行。

  • Stop:显示错误信息并停止处理。这会生成一个 Action PreferenceStopException 异常。

请注意,关于首选项变量(about_Preference_Variables)的帮助文档说明该变量仅对非终止错误有效;但事实并非如此。更改该变量会影响终止和非终止错误,尽管在终止错误之后,ContinueSilentlyContinue 不会强制 cmdlet 或脚本继续执行;它仍然是一个终止错误。

$ErrorActionPreference 变量的作用域正如我们预期的那样。所以,尽管会话的值可能是 Continue,我们可以在脚本中设置不同的值。然而,我们绝对不能在脚本顶部写 $ErrorActionPreference = 'SilentlyContinue',无论它多么诱人,或者我们在互联网上看到多少人这么做。这会抑制脚本中的所有错误,并且当出现问题时非常难以排查,还会让 Don Jones(PowerShell 的大佬)感到难过。相反,如果脚本中有一个我们知道会产生大量非终止错误的 cmdlet —— 例如,使用 Test-NetConnection 测试活动机器的函数 —— 我们可以使用 -ErrorAction 常见参数。

-ErrorAction 参数

-ErrorAction 参数是所有 cmdlet 和高级函数可用的常见参数之一。请注意,这只会改变非终止错误的处理方式。在下图中,我将 $ErrorActionPreference 设置为 Inquire,通过输入 $ErrorActionPreference = Inquire,然后使用 -ErrorAction 参数来尝试改变行为:

图 10.6 –  参数

图 10.6 – -ErrorAction 参数

在第一个 cmdlet 中,我们正在生成一个非终止错误,错误动作是 Silently Continue。如我们所见,没有任何输出;Continue 的默认值会以红色显示错误。在第二个 cmdlet 中,我们通过错误地输入 cmdlet 名称生成了一个终止错误,-ErrorAction 参数被忽略;我们会看到一个操作提示。

-ErrorAction 参数与 $ErrorPreference 变量有相同的值。让我们看看 Ignore 值。这会像 SilentlyContinue 一样抑制错误消息。与 SilentlyContinue 不同的是,错误不会写入 $Error 变量,而是完全被忽略。

活动 1

如果我们运行以下 cmdlet,会发生什么呢?

$ErrorActionPreference = “SilentlyContinue”

“foo.txt”,“nosuchfile”,“bar.txt” | Get-ChildItem -ErrorAction “Stop”

现在我们知道如何让所有错误成为终止错误,接下来我们来看一下为什么我们可能会这么做。

捕获错误

正如我们所发现的,错误包含了大量有用的信息,我们可以利用这些信息让代码运行得更加顺畅。虽然 Get-Error$Error 变量对于实时故障排除非常有用,但在编写脚本时,我们需要有另一种方式来处理错误。

Try/Catch/Finally

处理 PowerShell 中的终止错误的最佳方式是使用Try/Catch/Finally语句。此语句允许我们根据是否发生错误来设置备用的操作流程。语句包括一个强制性的Try块,其中包含可能会生成错误的代码,然后是Catch块、Finally块或两者。Catch块会在Try块中的代码生成终止错误时执行;这是我们的异常处理器。Finally块中的代码无论是否生成错误都会执行;该块用于在Try块中的代码执行后可能需要的任何清理工作。我们很少看到Finally块的使用——我自己从不使用它。我们可以编写多个Catch块来处理不同的错误。

让我们试一个例子。在 VS Code 中创建一个新文件,输入以下内容:

function Get-Files {
    PROCESS {
        try {
                $filename = $_
                Get-ChildItem -Path "$_" -ErrorAction "Stop"
        }
        catch {
            write-output "error! file not found: $filename"
        }
    }
}
'foo.txt', 'nosuchfile', 'bar.txt' | Get-Files

所以,在这里,我们创建了一个名为Get-Files的函数。我们从PROCESS块开始,这样就可以从管道中获取输入。我们在第三行打开一个Try块,并在其中包含函数的操作代码。接下来,我们创建一个名为$filename的变量,并将管道中的当前内容存储到该变量中;我们可能会在Catch块中用到它。

活动 2

为什么我们不能直接使用管道变量作为Catch块的内容?为什么我们需要将当前的管道变量存储到另一个变量中?

然后,我们编写实际执行处理的 cmdlet:Get-ChildItem。我们添加-ErrorAction "Stop"以确保所有错误都是终止错误;请记住,Catch块仅会在捕获到异常时执行。

接下来,我们编写Catch块。我们使用此块来包含在捕获到终止错误时要执行的代码;在这种情况下,我们只想要一个屏幕上的错误消息以及当前字符串(我们将其写入$filename)。

最后,在最后一行,我们将三个字符串放入管道并传递给我们的函数。

运行时,所有内容的效果如下:

图 10.7 – 使用 Try/Catch

图 10.7 – 使用 Try/Catch

正如我们在前面的终端中看到的,foo.txtbar.txt被处理了,但由于找不到nosuchfileCatch块的动作被触发——在这种情况下,将错误字符串和文件名写入屏幕。

编写多个Catch块相对容易,但我们需要知道可以预期哪些类型的错误;具体来说,我们需要知道完整的.NET 名称,包括命名空间。从图 10.4中可以看到,当我们找不到文件时,.NET 异常名称是ItemNotFoundException,但我们还需要知道命名空间;在这种情况下,它是System.Management.Automation。如果我们不知道错误的命名空间,那么在互联网上搜索之前,尝试这个命名空间总是值得的。

我们将错误类型放在Catch和大括号({)之间的方括号中,如下图所示:

图 10.8 – 多个 catch 块

图 10.8 – 多个 catch 块

我们的第一个Catch块从第 7 行开始,只有当抛出类型为System.Management.Automation.ItemNotFoundException的错误时,它才会触发。所有其他错误将触发第二个Catch块。试试看;将 cmdlet 更改为Get-ChildItems来触发CommandNotFound错误。我们应该看到an unspecified error. boo.被写入三次。

那么,我们可以用Try/Catch做什么呢?其实,我们可以将它与上一章中编写的日志功能结合使用,利用Catch块将错误消息写入日志文件,而不是直接写到屏幕上。

我们可能会在互联网上看到对Trap语句的引用。这个语句可以追溯到 PowerShell v1。它真的很难使用,并且在作用域上有问题,因此建议我们应该使用更新的Try/Catch语句。和Finally块一样,我从不使用Trap语句。

这几乎涵盖了我们在运行脚本时可能遇到的错误;只剩下最后一种情况需要考虑。那就是一些不是错误的事情,但会阻止我们的脚本运行。

考虑这段代码:

Get-ChildItem -path *.csv

如果目录中没有.csv文件,会发生什么?PowerShell 不会显示错误;它根本不会显示任何内容。然而,我们可能会将其视为脚本中的错误。让我们看看如何将其转换为终止错误。

创建错误

Throw语句用于在脚本中创建一个终止错误,使脚本在该点停止并记录异常——例如,写入日志。我们来看一个例子。请在 VS Code 中尝试:

try {
    $files = (Get-ChildItem -Path *.csv)
    if (!($files)) {
        Throw "There are no CSV files here!"
    }
    else {
        #do something with the files
    }
}
catch {
    Write-Output $_.tostring()
}

这是一个Try/Catch块的配对。在Try块中,我们正在创建一个变量$files,它包含Get-ChildItem -Path *.csv的结果:当前目录下所有的.csv文件。

接下来,我们运行一个if语句,条件是!($files)——即,如果$files$null,则执行Throw语句,抛出There are no CSV files here!消息。这个消息被包装成一个错误对象,并传递到Catch块中。然后我们可以像处理真实错误一样,使用Write-Output $_.tostring()来仅显示消息。让我们看看它的效果:

图 10.9 – 抛出我们自己的错误

图 10.9 – 抛出我们自己的错误

正如我们在红框中看到的,Throw语句产生了一个错误,我们可以像处理其他终止错误一样处理它。如何写一个非终止错误是本章下一节——调试的内容。

关于如何处理错误的内容已经足够了。在下一节中,我们将着眼于调试我们的代码。

探索调试

我的代码有漏洞,你的代码也有漏洞。我们并不是糟糕的程序员,所有代码都有漏洞——只是我们还没有找到所有漏洞。根据 Coverity(一家代码质量扫描公司)的数据,经过质量控制的专业编写软件每 1,000 行代码大约有 1 个缺陷(或漏洞)。其中一些漏洞永远无法被发现,因为代码没有在那些特定的、罕见的情况下表现不如预期(在行话中叫做边界情况)。

错误主要分为两种类型:

  • 语法错误,我们拼写错了 cmdlet 或参数名称,或者漏掉了闭括号或引号。语法错误基本上是打字错误——只是我们有时打错了,以为自己打的是对的。我们已经看到,使用 VS Code 可以在这方面大大帮助我们,通过颜色标注、语法检查、代码提示和标签自动完成。

  • 逻辑错误,我们对 PowerShell 工作原理的理解不足。例如,我们可能遇到的作用域问题就属于这一类。可以说,许多我们已经遇到的错误也可以归到这一类;例如,在尝试使用文件之前检查文件是否存在,可以防止抛出意外的错误,就像我们在讨论Throw语句时看到的那样。

在本节中,我们将快速浏览 PowerShell 7 的各个功能,看看它们如何帮助我们发现并理解代码中的漏洞。不过,在进入有趣的部分之前,我们应该提醒自己故障排除的基本规则:

  • FileNotFound,在我们亲自确认文件确实位于我们认为的地方之前,不要开始排查网络连接问题。

  • 阅读帮助文件:我们有没有正确获取参数名称?这个 cmdlet 是否做我们认为它应该做的事情?

  • 理解期望与现实的差异:我们需要阅读正在故障排除的脚本,准确理解我们期望它做什么以及它实际做了什么。举个例子——我最近与一个客户合作,他在将服务器置于静默状态以便进行维护时遇到了问题。他认为自己使用的脚本可以做到这一点,但我们检查后发现它做的事情稍有不同。脚本本身没有问题;需要调整的是他的期望。

话虽如此,让我们来看一下如何为脚本提供一些仪表化支持。

脚本仪表化

脚本仪表化是指嵌入到脚本中的代码片段,旨在向我们提供关于脚本正在做什么以及它做得如何的信息。它可以是我们在加载模块时看到的进度条,表示循环完成了多少次的信息,找到的对象数量,或者仅仅是脚本的某个部分运行了多长时间。在本节中,我们将讨论如何使用一些 write-* cmdlet 来为我们提供故障排除信息。在 第八章 中,编写我们的第一个脚本 – 将简单的 Cmdlet 转换为可重用的代码,我们详细介绍了 Write-Verbose cmdlet,而在 第三章 中,PowerShell 管道 – 如何将 Cmdlet 串联起来,我们讨论了标准流以及与每个流相关的 Write-* cmdlet。让我们在这里重新回顾一下它们:

流编号 描述 Cmdlet 常用参数
1 成功 Write-Output 无 – 这是默认输出
2 错误 Write-Error -ErrorAction-ErrorVariable
3 警告 Write-Warning -WarningAction-WarningPreference
4 冗长 Write-Verbose -Verbose
5 调试 Write-Debug -Debug
6 信息 Write-Information -InformationAction-InformationVariable

表 10.1 – PowerShell 流

这些流针对不同的群体;Write-OutputWrite-WarningWrite-ErrorWrite-Verbose 是为脚本的最终用户设计的。Write-Information 是为脚本操作员设计的,Write-Debug 是为开发人员设计的;也就是我们。

我们之前使用过 Write-OutputWrite-Verbose;在这里我们不再重新介绍它们。它们为运行脚本的人提供不同层次的信息。然而,Write-Error 是新的。我们可以使用 Write-Error cmdlet 来生成一个非终止错误,就像我们使用 Throw 来生成终止错误一样。在接下来的 Write-Error.ps1 脚本中,我将 $ErrorActionPreference 设置为默认值,并用 Write-Error cmdlet 替换了我们之前使用的 Throw 信息:

图 10.10 – Write-Error cmdlet

图 10.10 – Write-Error cmdlet

第 4 行,我用 Write-Error "没有找到 CSV 文件" 替换了 Throw 语句,并在 第 9 行 添加了一个 Write-Output cmdlet,它位于 第 1 行 打开的 try 语句内部。

当我们运行它时,可以看到 Write-Error cmdlet 会将一条红色信息输出到控制台。我们可以看到它是一个非终止错误,因为 第 9 行Write-Output cmdlet 也会执行并在控制台中输出 脚本继续 字符串。从截图中的控制台窗口可以看到,错误也被写入了 $Error 变量。我们通过之前在本章看到的同一个 $ErrorActionPreference 变量来控制它的显示。

那么,什么时候使用 Write-Error,什么时候使用 ThrowWrite-Error 是当我们想告诉用户某些东西出错了,但让脚本继续执行时使用的;Throw 则是在我们希望脚本停止当前操作并处理错误时使用的。

Write-Warning 是我几乎从不使用的一个 cmdlet。它将文本写入警告流,输出为黄色文本。就是这样,它只会产生黄色文本。我们可以使用 $WarningActionPreference 变量和 -WarningAction 参数来强制脚本在写入警告流时停止或悄悄继续,但我觉得这是不必要的冗余;最好使用 Write-Error 并将输出写入 $Error 变量。其他意见也是存在的。

Write-Information 通常用于提供我们脚本已执行的详细信息;当我们与针对信息流的日志聚合器工作时,它非常有用,但我通常不太使用它。

Write-Debug 是我们在这里感兴趣的 cmdlet。它写入调试流,并且可以通过确保我们的脚本是高级脚本来访问,使用我们在 第八章 中看到的 CmdletBinding 属性,编写我们的第一个脚本——将简单的 cmdlet 转化为可重用的代码。如果我们使用了这个属性,那么我们就可以访问 -Debug 参数。

我们什么时候使用 Write-Verbose,什么时候使用 Write-DebugWrite-Verbose 的输出是为用户提供的,我们使用它来告诉用户脚本正在做什么——例如,当我们有一个循环从目录中处理并加载文件时,可能会显示类似“加载文件”的消息。我们在循环内部使用 Write-Debug 来列出我们正在加载的文件。考虑以下简短脚本:

图 10.11 – Write-Verbose 与 Write-Debug

图 10.11 – Write-Verbose 与 Write-Debug

这包含了在 第 7 行 外部的 Write-Verbose 消息和在 第 9 行 循环内的 Write-Debug 消息。Write-Verbose 消息面向脚本用户,告诉他们脚本正在做某件事,即使他们觉得它没有做什么。Write-Debug 消息则面向我们,精确地告诉我们脚本正在做什么,以及它正在处理哪个文件。Write-DebugWrite-Host 操作分别受 $DebugPreference$VerbosePreference 变量的控制。这些变量默认都设置为 SilentlyContinue,并通过各自的 -Debug-Verbose 开关参数被覆盖,正如我们在前面的控制台输出中看到的那样。

这些都很好,但这只告诉我们如何处理我们预期的错误。那当我们不知道出了什么问题时呢?我们会转向调试 cmdlet。

调试 cmdlet

调试非常复杂,随着我们深入研究,我们很快意识到,要彻底理解它,我们需要对计算机科学有相当好的掌握。这并不意味着对常见 cmdlet 的工作知识不非常有用。在这一节中,我们将讨论如何在控制台中使用断点调试脚本。当断点被触发时,脚本将暂停运行,并启动内置的 PowerShell 调试器。设置和操作断点的 cmdlet 都使用 PSBreakpoint 作为名词。我们应该关注以下这些 PSBreakpoint cmdlet:

  • Set-PSBreakpoint 在脚本、变量或命令的特定行启用一个断点

  • Get-PSBreakpoint 列出会话中设置的所有断点

  • Remove-PSBreakpoint 移除指定的断点

  • Disable-PSBreakpoint 停止断点触发,但不移除它

  • Enable-PSBreakpoint 启用一个被禁用的断点

让我们使用之前的 debugVsVerbose.ps1 脚本。如果你还没有创建,创建一个新的文件,将其保存为 debugVsVerbose.ps1,并添加以下内容。你需要将 $path 变量更改为你存放文件的路径:

[CmdletBinding()]
param (
    $Path = "C:\temp\poshbook\ch10"
)
$files = (Get-ChildItem -Path $path)
Write-Verbose "Getting Files..."
foreach ($file in $files) {
    Write-Debug "Current `$file is $file"
}

现在,在控制台中,而不是在 VS Code 中,导航到你保存脚本的目录,并输入以下命令:

Set-PSBreakpoint -Script .\debugVsVerbose.ps1 -Variable file -Mode read

这告诉 PowerShell,当运行 debugVsVerbose.ps1 脚本时,在读取 $file 变量的值时启动调试器。记住——变量名不包括前面的美元符号($)。

现在,运行脚本以便在读取变量时进入调试模式:

.\debugVsVerbose.ps1

一旦进入调试模式,我们可以检查变量的值、运行 cmdlet、查看调用堆栈、显示脚本,并做很多其他操作。试试这个;输入以下命令获取 $file 的当前值:

$file

你应该能看到你所在位置的第一个文件名。它应该看起来像这样:

图 10.12 – 使用控制台调试器

图 10.12 – 使用控制台调试器

我们可以看到,在第一条命令中,我设置了一个断点,当读取 $file 时触发。下一条命令是运行脚本。控制台会告诉我们已经在黄色中触发了变量断点,并且指出了脚本中断点的位置。然后,提示符切换到调试模式提示符 [DBG]。我输入 $file,然后变量的内容被显示出来。

我输入的最后一个命令是 h,这是一个调试器命令,不是调试过程的一部分。试试看。

输入 h 以查看帮助内容。它将显示我们可以在调试器中使用的命令列表:

命令 效果
s, stepInto 单步执行(进入函数、脚本等)。
v, stepOver 执行到下一条语句(跳过函数、脚本等)。这将作为单步执行运行一个函数或脚本块。
o, stepOut 跳出当前函数、脚本等。
c, continue 继续操作。
q, quit 停止操作并退出调试器。
d, detach 继续操作并分离调试器。
k, Get-PSCallStack 显示调用堆栈。
l, list 列出当前脚本的源代码。
使用 list 从当前行开始,list <m> 从第 <m> 行开始,list <m> <n> 从第 <m> 行开始列出 <n> 行。
Enter 如果上一个命令是 stepIntostepOverlist,则重复该命令。
?, h 显示帮助信息。

表 10.2 – PowerShell 调试器命令

有一些命令可以帮助我们在脚本中导航,需要进一步解释,如下所示:

  • StepOver 会带我们到下一条语句;如果该语句是一个函数,调试器只会运行该函数,而不会逐行执行。

  • StepInto 会让我们跳到断点之后的下一行代码并执行它。它会进入函数并逐行执行,而不是仅仅运行函数。

  • StepOut 会在我们处于某个函数时完成该函数,并将我们带到该函数之后的下一条语句。

  • Continue 会运行脚本,直到结束或者下一个断点。

  • Get-PSCallStack 会显示我们当前在脚本和函数中的位置。例如,如果我们在前面的调试器中按下 K 键,它会显示我们当前在 debugVsVerbose.ps1 脚本中的 第 9 行。如果我们在脚本中的某个函数内,它会显示函数名及我们在其中的位置,后面跟着脚本名和位置。

  • List 会显示我们选择的脚本内容,从任意一行开始。

  • 最后,quit 将退出调试器。

根据我的经验,我们并不常常需要在命令行中进行调试。通常,在像 VS Code 这样的编辑器中调试要容易得多。让我们看看如何操作。

使用 VS Code 调试

VS Code 可能是调试 PowerShell 最好的工具。它具有大多数人需要的调试功能,例如远程调试能力,可以连接到其他机器;功能繁多,无法在此一一列举。本节将涵盖基础内容,并展示如何使用 VS Code 执行我们刚刚在命令行中介绍的基本调试流程。

在新的 VS Code 会话中,按 Ctrl + Shift + P 打开命令面板并输入 exam;你应该能看到一个指向 PowerShell 扩展示例文件夹的链接,如下所示:

图 10.13 – PowerShell 扩展示例文件夹快捷方式

图 10.13 – PowerShell 扩展示例文件夹快捷方式

在左侧面板的文件资源管理器中,双击 DebugTest.ps1 文件以打开它。

这是一个相当简短的教程脚本,随 PowerShell VS Code 扩展提供。它由两个函数组成,Do-WorkWrite-Item。这两个函数都没有做太多的事情;Do-Work 使用两个不同的 cmdlet,Write-OutputWrite-Host,写入两行文本,并且调用了 Write-Item 函数。Write-Item 是一个计数函数,它将计数到 $Count 变量的值,每次输出一个字符串,并在每次迭代之间有短暂的延迟。Do-Work 在脚本的最后一行,第 28 行 被调用。

现在,让我们设置一个断点。可以通过点击 第 15 行 并按 F9,或者将鼠标悬停在行号左侧并点击。以下截图中看到的暗红色圆点将变成亮红色:

图 10.14 – 设置断点

图 10.14 – 设置断点

现在我们已经设置了断点,可以开始调试器。最简单的方式是按 Ctrl + Shift + D,或者从 视图 菜单中选择 运行

图 10.15 – VS Code 调试界面

图 10.15 – VS Code 调试界面

调试 视图相当复杂,我们需要运行文件到断点处以查看它的作用。在左上角,按下绿色箭头,位于 运行和调试 旁边(图 10.15 中的 1)。脚本会运行到 第 15 行 的断点并停止。让我们来探索这个界面。首先,我们有运行控制按钮 (2)。从左到右分别是 继续单步跳过单步进入单步跳出重启停止,这些与上一节中的控制台调试器中的控制按钮类似。如果将鼠标悬停在它们上面,可以看到相应的快捷键;例如,我们也可以按 F5 来继续执行。这些命令也可以通过顶部工具栏中的 运行 菜单找到。我们可以看到在脚本中被高亮显示的断点 (3)。

左侧的面板包含一些非常有趣的工具,三个窗口,Write-Item 函数,变量有 $i$str$itemCount。如果我们将鼠标悬停在它们上面,可以看到变量的类型。讽刺的是,Write-Item。如果我们点击 $args$MyInvocation 左侧的箭头,展开 $PSBoundParameters,我们可以看到与 Write-Item 相关的参数:[itemCount]

$i 变量存在于局部作用域中,但不存在于脚本或全局作用域中。

现在我们继续讨论 <ScriptBlock>。它运行到 第 28 行,并调用 Do-Work 函数,该函数运行到 第 24 行 并调用 Write-Item 函数,该函数运行到 第 15 行 的断点。这使我们能够跟踪脚本的执行流程。如果我们点击调用堆栈中的项,可以看到顶部面板中的变量值发生变化,显示它们在相关作用域中的值。例如,如果我们选择 Do-Work 而不是 Write-Item,我们可以看到 $PSBoundParameter 现在包含 [workCount]

最后,我们输入 $i*2 并按 Enter。我们应该能看到该表达式的结果是 4。如果我们现在按下 6

我们才刚刚触及使用 VS Code 调试的表面,但已经足够让我们了解如何利用它来理解为什么我们的代码没有按照预期运行。调试是一个非常复杂的话题,学习它的最佳方式就是亲自实践。不过,VS Code 可以让这一任务变得更加容易。让我们总结一下本章的内容。

总结

本章开始时,我们探讨了错误到底是什么,并介绍了一些适用于大多数编程语言的计算机科学知识。我们看到了 PowerShell 与许多语言的不同之处,它有终止错误和非终止错误的概念,并且这也是它作为一种解释型语言、建立在 .NET 上的一个特性。

一旦我们理解了什么是错误,我们就探讨了 PowerShell 使用 $ErrorActionPreference 变量和 -ErrorAction 参数处理错误的不同方式。

我们看到,如何将错误转化为终止错误,以便我们使用最常见的捕捉错误的方式——Try/Catch/Finally 语句族。我们还学习了如何使用这些语句提供自定义的错误处理流程。

在了解了如何处理错误之后,我们学会了如何使用 Throw 语句在遇到不希望发生但不会自然导致错误的情况时生成我们自己的终止错误。

在探讨了如何处理错误后,我们在本章后半部分研究了调试的艺术。我们从使用 Write-Debug 进行脚本仪器化的概念开始,了解了如何通过使用 -Debug 参数访问调试输出流,来生成关于代码执行情况的洞察。

我们继续研究了如何使用调试 cmdlet 进行交互式调试,使用 Set-PSBreakPoint 等命令访问内置的 PowerShell 调试器。在了解了这一点的困难后,我们通过使用 VS Code 这种更强大、更简单的方式来进行交互式调试,结束了本章。

在下一章中,我们将探讨如何分发我们的代码,以便其他人能以灵活的方式使用它,通过将脚本转化为模块。迫不及待了!

练习

  1. 在运行脚本时,终止性错误和非终止性错误的主要区别是什么?

  2. 我们如何访问关于错误的详细信息?

  3. -ErrorActionPreference 变量在 PowerShell 中的作用是什么?

  4. Write-Error cmdlet 在 PowerShell 中的作用是什么?

  5. 为什么我们可能想要使用 Throw 语句?

  6. 我们如何在脚本中显示调试消息?

  7. 我们如何在脚本中编写调试消息,我们是为谁写这些消息的?

  8. PowerShell 中的断点是什么?

  9. stepOver 调试命令的作用是什么?

进一步阅读

)

第十一章:创建我们的第一个模块

大多数编程语言都包含库的概念——它是一个包含代码、文档、编程对象(如类)、消息模板以及许多其他内容的对象。这些库通过帮助我们使用他人的代码和重用我们自己的代码,扩展了我们可以用该语言做的事情。在本章中,我们将探讨 PowerShell 模块——一种分发 PowerShell 代码的便捷方式。

我们将从简要回顾如何使用模块以及用于此的 cmdlet 开始。接下来,我们将查看模块的组成部分。我们将学习如何手动编写模块,然后简单介绍一下使用一个名为 Plaster 的模块脚手架应用程序。

本章我们将涵盖的主要主题如下:

  • 使用模块

  • 编写一个简单的模块

  • 模块清单

  • 使用如 Plaster 这样的脚手架工具

使用模块

回到第二章探索 PowerShell Cmdlet 和语法,我们花了一些时间探讨了如何使用模块来查找新的 cmdlet。在本章中,我们将编写模块,但首先,让我们回顾一下之前所学的内容,并将其放置在某种上下文中。

模块允许我们重用和分发代码,以便通过将操作模块的 cmdlet 包含在我们的脚本中轻松实现自动化。所以,如果我们需要在脚本中使用 PowerShell 数学模块的 cmdlet,我们可以通过编程方式导入该模块(或仅导入所需的 cmdlet)并使用它。我们可以以可预测且可控的方式进行,而无需用户干预。

模块履行三个基本功能:

  • 配置环境:它们提供一个可重复的自定义工作环境——例如,Exchange 的 PowerShell 模块,以及特定于 Exchange 的 cmdlet——将 PowerShell 环境配置为以特定方式与 Exchange 协同工作。

  • 代码重用:它们提供函数库,供我们或他人使用,例如数学模块。

  • 解决方案工程:因为模块可以嵌套在其他模块中,所以可以将一整组模块分发出去,创建一个应用程序进行再分发——这在 Windows 管理环境中很常见。

我们可以在互联网上找到模块,例如 GitHub,这里有软件发布的模块;也可以在公共仓库(如 PowerShell Gallery)或我们工作场所或学校的内部仓库中找到,或者通过朋友和同事获得。

通过将模块安装到标准位置,我们可以控制在特定机器上的访问权限,或者我们可以将 PowerShell 搜索模块的默认位置列表进行扩展。让我们从查看客户端设备上模块的常见位置开始。

模块位置

模块有三个默认位置。这些位置如下表所示:

Windows Linux
系统 PowerShell 7 没有此位置,但 Windows PowerShell 使用C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\ /opt/Microsoft/PowerShell/7/Modules
所有用户
  • C:\Program Files\PowerShell\Modules

  • C:\Program Files\PowerShell\7\Modules

/usr/local/share/PowerShell/Modules
用户

表 11.1 – Windows 和 Linux 中的默认模块位置

系统位置保留给微软模块。用户位置在 Windows 中默认不存在,需要先创建才能使用。AllUsers位置在 PowerShell v4 中引入,提供了一个用于需要在系统账户下运行的非微软模块的位置。这也为我们提供了一个为客户端上任何用户安装模块的地方。从 PowerShell Gallery 下载的模块默认放在这里。

正如我们在第二章中看到的,探索 PowerShell Cmdlet 和语法,我们可以通过操作$ENV:PSModulePath变量来添加可能安装模块的位置。位置应在 Windows 中用分号(;)分隔,在 Linux 中用冒号(:)分隔,如下所示:

图 11.1 – Linux 中的$env:PSModulePath 变量

图 11.1 – Linux 中的$env:PSModulePath 变量

我们可以通过调用$env:PSModulePath -Split ":"变量使此列表更整洁,这将把每个位置输出为单独的一行。显然,在 Windows 客户端上,分隔符应为分号(;)。路径在安装应用程序时通常会被添加到此变量中。

模块自动加载

在某些情况下,我们可以自动加载模块。位于$env:PSModulePath定义的模块路径中,并且文件夹命名正确的模块会被 PowerShell 自动发现。我们可以通过以下方法做到这一点:

  • 从模块中运行 cmdlet

  • 使用Get-Command获取模块中的 cmdlet

  • 使用Get-Help获取模块中的 cmdlet

模块自动加载在我们交互式地使用 Shell 时非常有用,但在脚本中不应依赖它。推荐的加载模块的方式是在脚本中使用using关键字,如下所示:

using module <module name>

这里有一个示例:

Using module ActiveDirectory

请注意,关于此内容的一些注意事项已在嵌套 模块部分中讨论。

导入模块

自动加载模块非常方便,但它并不是特别可控。例如,它会加载模块中的所有内容,其中一些我们可能不需要,而且我们无法控制加载的模块版本。它还可能占用大量内存。因此,我们可能会选择使用Import-Module cmdlet 手动将模块导入会话,这样可以给我们提供几个选项来控制导入的方式和内容:

  • -Name:用来指定要导入的模块名称。如果模块不在$ENV:PSModulePath指定的模块路径中,我们还可以在此处包含路径。

  • -Cmdlet:这个参数允许我们从一个字符串数组中导入一组 cmdlet。类似的参数,如 -Alias-Function-Variable,也有预期的效果。

  • -Force:这个参数强制模块完全重新加载。默认情况下,如果模块已经加载,则 Import-Module 不会重新导入它。如果我们在开发模块并需要反复测试它时,这个参数非常有用。

  • -RequiredVersion:使用此参数指定要导入的模块版本。

  • -Prefix:这个参数会为从模块导入的 cmdlet 中的名词添加前缀,以避免与当前会话中已存在的 cmdlet 混淆。

  • -NoClobber:如果 cmdlet 与当前会话中已存在的 cmdlet 同名,这个参数会阻止它们被导入。

还有其他一些参数,但我们在这里不考虑它们。

我们可以使用 Remove-Module cmdlet 卸载一个模块。为什么我们可能需要这么做呢?因为当我们第一次导入一个模块时,所有它所依赖的嵌套模块也会被一并导入。然而,如果我们随后使用 Import-Module -Force 来重新加载该模块,它只会重新加载指定的模块,而不会重新加载嵌套模块。Remove-Module 也会移除嵌套模块。当然,在实际使用中,通常直接启动一个新的会话会更快捷、更清洁。

我们经常会在互联网上看到使用 Import-Module cmdlet 使模块内容在脚本中可用的脚本。我自己也这么做,尽管这不是推荐的做法;对我来说,这样看起来更易读。微软推荐我们使用 using 关键字来实现这个功能,尽管我也常常忽略这个建议,就像我的医生推荐我少吃盐一样,我也倾向于忽视他的建议。

PowerShellGet

Microsoft 有一个名为 PowerShellGet 的模块,其中包含了大量用于操作仓库和模块的资源。这个模块包含在 PowerShell 7 中。它使我们能够轻松地使用 PowerShell Gallery,这样我们就可以查找、注册和注销其他仓库,从而找到、安装和卸载仓库中的模块和脚本,并且操作这些模块和脚本。我们在第二章《探索 PowerShell Cmdlets 和语法》中详细介绍了 PowerShellGet 的基本用法。

从 PowerShell 7.4 开始,PowerShellGet v2.2.5 模块将与版本 3 一同发布。版本 3 的模块名为 Microsoft.Powershell.PSResourceGet,它将用一个单一的 Install-PSResource cmdlet 替代 Install-ModuleInstall-Script cmdlet,并做出许多其他更改。在 PowerShell 7.4 中,这两个模块将并行发布,允许任何当前使用 PowerShellGet 的资源继续工作。然而,不会包含兼容层,因此,除非我们使用将在 PowerShell 7.5 中提供的独立 CompatPowerShellGet 模块,否则为 v2.2.5 及更早版本编写的脚本将无法与版本 3 一起使用。

接下来,我们将学习如何创建一个模块,但首先有一个警告。PowerShell 中有一个 cmdlet 叫做New-Module。这个命令创建的是一种非常特定类型的模块——New-Module

编写一个简单的模块

模块最基本的形式是一个包含函数的脚本文件,文件扩展名为.psm1。就这么简单。这是最简单的模块。试试看——将以下内容保存为.psm1文件,放在一个与文件同名的文件夹内,位置为你的\\users\\<username>\\documents\\powershell\\modules文件夹(或 Linux 中的home/<user>/.local/share/powershell/Modules文件夹):

function Get-Square($a) {
  $result = $a * $a
  return $result
    }

文件名无关紧要,只要文件和文件夹的名称相同,并且文件夹在模块路径中,以便 PowerShell 能够找到它,就像这样:

图 11.2 – 正确地将模块保存到模块路径中

图 11.2 – 正确地将模块保存到模块路径中

现在,如果我们启动一个 PowerShell 会话,可以输入以下内容:

Import-Module <ModuleName>

PowerShell 将加载它。一旦加载完成,我们就可以使用模块内的函数,如下所示:

图 11.3 – 使用我们的第一个模块

图 11.3 – 使用我们的第一个模块

如你所见,Import-Module cmdlet 没有输出,但模块内的函数是可用的。如果我们运行Remove-Module,函数也会消失。或者它真的消失了吗?

活动 1

尝试运行Remove-Module来移除我们刚才安装的模块,然后再运行Get-Square。会发生什么?为什么会这样?

在我们研究如何创建模块之前,让我们先谈谈 PowerShell 中最早构建库的方法——点来源。

提醒一下 – 点来源

在 PowerShell 的第一个版本中,只有一种方法可以在一个脚本中包含另一个脚本中的函数:将Dot-Source.ps1放在适当的文件夹中——我使用的是c:\\temp\\poshbook\\ch11

Write-Message "dot source test"

很明显,如果我们运行这个,肯定无法工作。没有名为Write-Message的 cmdlet,而且脚本中也没有定义一个。让我们在另一个脚本中创建一个Write-Message函数,并将其保存为Write-Message.ps1

$text = "default message"
function Write-Message($text) {
    Write-Output "$text"
}

现在,让我们回到Dot-Source.ps1,并在开头添加这一行:

. C:\temp\poshbook\ch11\Write-Message.ps1

更改路径以反映你保存Write-Message.ps1的位置。现在,当我们运行Dot-Source.ps1时,我们的消息应该会显示出来,如下所示:

图 11.4 – 在脚本中使用点来源

图 11.4 – 在脚本中使用点来源

我们也可以通过交互式方式在 PowerShell 会话中做到这一点,只需点来源脚本,如下所示:

图 11.5 – 交互式点来源

图 11.5 – 交互式点来源

那么,如果点源(dot-sourcing)这么简单,为什么我们还需要使用模块呢?原因是点源带来的管理问题。当我们进行点源时,我们将脚本的成员以及变量和函数引入到父作用域中。我们可以在前面的截图中看到这一点;如果我们调用$text变量,就会得到default message字符串。记住,作用域的概念是为了保护我们免受不明确代码的影响;通过点源代码,我们去除了这种保护。当我们交互式地点源Write-Message.ps1时,我们将一个函数带入了全局作用域,而现在我们没有简单的方法将其删除。该脚本中函数外的任何变量也会被引入。如果这些变量命名不当,可能会与现有的重要变量发生冲突,导致“可笑的后果”。

如果我们不确定一个函数的来源,可以使用对象的File属性进行检查,像这样:

${function:Write-Message}.File

这将给我们该函数所在文件的路径。我们还可以使用Remove-Item cmdlet 从会话中删除它,如下所示:

图 11.6 – 查找函数来源并删除它们

图 11.6 – 查找函数来源并删除它们

模块允许我们控制从代码中导出的函数和变量,并将它们作为一个整体进行控制。让我们看看如何将现有的脚本转换为模块。

将脚本转换为模块

正如我们在本节开始时看到的,将脚本转换为模块的基本过程是将文件扩展名从.ps1更改为.psm1。现在,我们来处理之前写的Write-Message.ps1脚本,并将其保存为Write-Message.psm1

现在,我们可以打开一个新的会话,并使用以下命令将我们的模块导入到会话中:

Import-Module C:\temp\poshbook\ch11\Write-Message.psm1

让我们详细查看这个模块:

图 11.7 – 模块的详细信息

图 11.7 – Write-Message模块的详细信息

在第一行中,我们导入了模块。我们使用了完整路径,因为我们没有将其保存到包含在模块路径中的位置。这是为了防止 PowerShell 自动加载它。现在,让我们运行以下命令:

Get-Module Write-Message

我们将看到一个脚本模块、它的位置以及导出的命令——也就是Write-Module。现在,让我们运行以下命令:

Get-Module Write-Message | Format-List

我们将看到只有函数被导出,而不是变量。

我们可以使用Get-Command检查Write-Message函数的详细信息,查看其来源是Write-Message模块。

让我们尝试使用这个模块做一些其他事情。打开模块文件并添加以下代码:

function setMessage {
    Write-Output "$text"
}

再次导入模块,使用-Force参数重新加载它。现在,我们的模块中有两个函数。如果我们再次运行Get-Module,我们会看到它们都已经显示出来:

图 11.8 – 多重导出

图 11.8 – 多重导出

正如你所看到的,setMessage cmdlet 现在对我们可用了。如果我们不希望这样会发生什么呢?我们使用的名称不符合 cmdlet 命名约定,通常我们会使用这种命名来表示一个私有函数;它是我们需要在模块内部的其他函数中使用的,但我们不希望它对外部函数可用。我们可以使用Export-ModuleMember cmdlet 来控制访问权限。在Write-Message.psm1的底部添加以下行:

Export-ModuleMember -Function Write-Message

然后,使用-Force参数再次导入模块。结果如下图所示:

图 11.9 – 控制导出的函数

图 11.9 – 控制导出的函数

现在,只有Write-Message函数被导出,当我们尝试运行setMessage时,这次我们会收到一个错误。我们还可以使用Export-ModuleMember来导出模块中的变量和别名,这些默认情况下是不会被导出的,像这样:

Export-ModuleMember -variable $MyVariable

如果我们没有显式使用Export-ModuleMember,模块中的所有函数都会被导出,但除函数外,其他任何内容都不会被导出。

在本章的介绍中,我们提到过模块的一个使用场景:构建应用程序和解决方案。为了实现这一点,我们通常会从一个模块内部调用另一个模块——这就叫做模块嵌套,接下来我们将详细探讨这一点。

嵌套模块

第九章《不要重复自己——函数与脚本块》中,我们查看了一些用于将输出写入日志文件的函数。将这些函数写入模块是非常有用的,这样我们在需要时就可以调用它们,而不是每次创建脚本时都要重新编写。这些是我们希望保持私有的函数,而不是为一般用途导出的好例子。我们来做一下这个操作。将之前编写的Write-Log.ps1脚本复制到一个名为Write-Log.psm1的模块文件中,并放置在合适的位置。我们需要编辑.psm1文件,删除以下行:

Write-Log "Is this thing on?"

这是我的模块文件的样子:

图 11.10 – Write-Log.psm1 模块

图 11.10 – Write-Log.psm1 模块

如我们所见,我注释了第 11 行,而不是删除它。剩下的两个函数和一个变量是Write-LogRemove-Log$LogFile

现在我们可以在Write-Message.psm1模块中添加几行代码,以便调用Write-Log模块并运行其中的某个函数。像这样编辑你的Write-Message函数:

Import-Module "C:\temp\poshbook\ch11\write-log.psm1"
$text = "default message"
function Write-Message($text) {
    Write-Output "$text"
    Write-Log "$text"
}
function setMessage {
    Write-Output "$text"
}

我已添加了第一行来导入Write-Log模块,第五行用于从中调用Write-Log函数。我还删除了Export-ModuleMember行。现在,让我们看看当我们导入Write-Message模块时会发生什么:

图 11.11 – 嵌套模块

图 11.11 – 嵌套模块

如我们所见,模块如之前一样被导入并按预期工作。我们可以从 Get-Module 的输出中看到有四个导出的函数——两个来自 Write-Message,两个来自 Write-Log——并且我们可以看到 Write-Log 模块是嵌套的。然而,当我们尝试通过 Get-Command 访问它时,我们看不到任何已加载的函数,且 Get-Module Write-Log 不返回任何内容。不过,如果检查创建的日志文件,我们会看到一条消息,表明 nested module test,所以它是有效的。

这是因为嵌套模块仅对调用模块可见,因此我们不能直接访问 Write-Log。然而,由于我们移除了 Export-ModuleMember 这一行,所有函数都会被导出,包括嵌套模块中的函数;它们将作为调用模块的函数出现。如果我们使用 Get-Module -All,我们将能够看到 Write-Log 模块,并且可以像以前一样找到 Write-Log 函数的实际位置——即通过调用 File 属性:

图 11.12 – 查找嵌套模块

图 11.12 – 查找嵌套模块

如我们所见,Write-Log 函数定义在 write-log.psm1 文件中。

有时,我们可能不希望嵌套模块像这样被隐藏;如果是这样,我们可以使用 Import-Module-Global 参数,像这样:

Import-Module -Global "C:\temp\poshbook\ch11\write-log.psm1"

Write-Log 模块将与 Write-Message 模块处于同一顶级导入状态。

现在,让我们来看一些其他类型的模块,包括二进制模块和清单模块。

更多模块

你可能注意到,当我们在模块中创建一个函数时,我们像使用 cmdlet 一样使用它,但它仍然被称为函数。它会像 cmdlet 一样行为,具有参数、帮助、成员等等,但它并不是 cmdlet。考虑以下内容:

图 11.13 – 函数和 cmdlet

图 11.13 – 函数和 cmdlet

如我们所见,Write-MessageFunction 类型,而微软的 Get-Service 命令是 Cmdlet 类型。

要编写自定义 cmdlet,我们需要编写一个二进制模块。请记住,PowerShell 基于 .NET,是一种解释型语言,通常使用编译语言(如 C#)编写,就像 Python 是一种解释型语言但用 C 编写一样。

我们不会在这里讨论如何编写 C# 模块,但我们会讨论它们是如何工作的以及如何使用它们。

二进制模块没有 .psm1 扩展名 —— 它是一个从 C# 等代码编译而成的 .NET 程序集,具有 .dll 扩展名。我们可以通过在 @" "@ 结构中的 Here-String 声明中编写 C# 代码,然后使用 Add-Type -OutputAssembly cmdlet 来编译它:

$code = @"
using System.Management.Automation;
namespace SendMessage
{
    [Cmdlet(VerbsCommunications.Send, "Message")]
    public class SendMessageCommand : Cmdlet
    {
        [Parameter(Mandatory = true)]
        public string Name { get; set; }
        protected override void ProcessRecord()
        {
            WriteObject(Name + " loves PowerShell!");
        }
    }
}
"@
Add-Type -TypeDefinition $Code -OutputAssembly  c:\temp\MyBinaryModule.dll

除此之外,我们可以像使用脚本模块一样,使用 Import-Module 导入它:

Import-Module c:\temp\MyBinaryModule.dll

如果我们运行 Get-Module 来查看虚构的 MyBinaryModule,我们会看到 ModuleTypeBinary,并且它导出了 cmdlet,而不是导出函数。

二进制模块和脚本模块之间的主要区别在于,一旦它们被加载到会话中,就无法卸载。如果我们需要更改正在编写的二进制模块,则需要先关闭 PowerShell。

我们可能还会看到用 CDXML 编写的 PowerShell 模块,CDXML 是一种用于公共信息模型命令的 XML 包装器。这种方式在 Windows 管理模块中曾经很常见,但现在已经大多被弃用,因为用这种方式编写的模块比脚本模块加载和运行速度慢,因为需要额外的工作将 XML 解析到 PowerShell 中,然后 PowerShell 还需要进一步解析。正如官方文档所言,"避免使用 CDXML。"类似地,我们可能会看到有关 PowerShell SnapIns 的引用。这些是 Windows PowerShell 的已弃用形式,在 PowerShell 7 中不再支持,因此我们不需要担心它们。

还有一种模块类型需要我们考虑——清单模块。清单模块是一个包含模块清单的脚本或二进制模块。接下来我们将讨论这个内容。

模块清单

到目前为止我们操作的脚本模块是单一文件,要么是独立的,要么是嵌套的。这对于个人使用来说没有问题,但在生产环境中,这样做就不太合适了,因为我们可能需要将函数拆分成多个文件,并且包括版本信息和其他大量的元数据与资源,如 XML 格式文件或二进制文件。为了组织一个更复杂的模块,我们需要一个文档来说明如何加载和实现它;这个文档被称为模块清单,它是一个哈希表,保存为 .psd1 扩展名的文件。让我们来看一个例子。如果我们浏览到 C:\Program Files\PowerShell\7\Modules(或者在 Linux 上是 /opt/microsoft/PowerShell/7/Modules)中的 PowerShellGet 模块,我们将看到以下文件和文件夹:

图 11.14 – PowerShellGet 模块

图 11.14 – PowerShellGet 模块

如我们所见,有四个文件和三个文件夹。这些文件夹包含几个 .psm1.psd1 文件,用于定义函数和嵌套模块的清单。模块的主要代码在 PSModule.psm1 中。PSGet.Format.ps1xml 包含用于显示函数输出的格式化信息。PowerShellGet.psd1 是模块清单。PSGet.Resource.psd1 是模块使用的一组输出字符串;我们在此不做过多考虑。如果我们打开 PSModule.psm1 文件,会看到它是用 PowerShell 编写的,而不是 C#,因此它是一个脚本模块。这是一个较大的文件,定义了很多函数。我们来看看 PowerShellGet.psd1 文件。

首先要注意的是,它被赋予了与其所在文件夹相同的名称。这是故意的;如果在模块文件夹中存在清单文件,它必须与文件夹的名称相同;否则模块将无法加载。如果没有清单,.psm1 文件也采用相同的名称,这允许 PowerShell 容易地找到它并自动加载,但除此之外这并不重要。清单文件则不同。

让我们打开它,看看里面是什么:

图 11.15 – PowerShellGet 清单

图 11.15 – PowerShellGet 清单

在第 1 行,我们可以看到这是一个从 @{ 开始的哈希表,之后的内容都采用键值对格式。如我们所见,这是一个长文件,包含了大量信息;超过 200 行,尽管其中许多部分是包含 ReleaseNotes 键的大段文本。

一个清单由多达 37 个不同的键值对组成,这些键值对可能包含字符串或数组。我们可以向清单中添加额外的代码,包括比较运算符、算术运算符、基本数据类型和 if 语句,但我们在这里不涉及这些内容;我从未需要这么做。让我们为自己创建一个新的清单,看看里面有什么。

在合适的目录中创建一个名为 ManifestModule 的文件夹。现在,打开 PowerShell 会话并输入以下 cmdlet:

New-ModuleManifest

在提示符下,提供新清单文件的路径和名称,如下所示:

C:\temp\poshbook\ch11\ManifestModule\ManifestModule.psd1

就是这样——这将为名为 ManifestModule 的模块创建一个新的清单文件。让我们在 VS Code 中打开它:

图 11.16 – 基本模块清单

图 11.16 – 基本模块清单

首先需要注意的是,一些值已经为你生成。ModuleVersion0.0.1,还有一个自动生成的 GUID 用以确保这个模块与同名的其他模块可以区分开,且 AuthorCompanyNameCopyright 键已被填充。除此之外,其他部分为空,且通常已被注释掉。

清单中的键分为三组,涵盖以下几个方面:

  • 生产数据:谁编写了它,何时编写,给谁用,及其运行的系统类型。

  • RootModule 定义了调用其他所有内容的主模块文件——主 .psm1 文件。

  • 模块内容:这些是模块中包含的所有模块、文件和其他资产的列表。这些键是可选的,但通常应该准确地填充。

我们可以通过调用 New-ModuleManifest cmdlet 并将键名作为参数来填充这些键:

New-ModuleManifest -Path 'C:\temp\newmodule\newmodule.psd1' -ModuleVersion '1.0.0'

另一种方法是直接在文本编辑器或 VS Code 中编辑清单文件。如果我们直接编辑,始终有可能打错某些内容并破坏文件,因此最好像这样测试我们编辑过的清单:

Test-ModuleManifest -Path 'C:\temp\newmodule\newmodule.psd1'

我们可以在下图中看到结果:

图 11.17 – 测试模块清单文件

图 11.17 – 测试模块清单文件

如果返回了模块的信息,那么文件已经正确格式化。

正如我们所看到的,模块构建可能会迅速变得复杂。让我们通过查看一个可以让它变得简单得多的工具——Plaster,来结束这一章。

使用诸如 Plaster 之类的脚手架工具

如果我们长期在一个模块上工作,或者与他人协作,那么使用一个将所有内容拆分成单独文件和资产的框架是一个非常好的主意。这就是脚手架工具发挥作用的地方。我的选择是 Plaster,这是一个最初由 Microsoft 制作的模块,现在由 PowerShell.Org 维护,PowerShell.Org 是一个最具影响力的 PowerShell 社区之一。

Plaster 使用一个模板文件,该文件由一个清单(类似于模块清单)和一组内容文件及目录组成。模板使用 XML 编写,且高度可定制。清单文件分为三个部分:

  • 元数据,包含有关模板的信息,如名称、版本和作者

  • 参数,定义了用户可以在模块结构中做出的选择——创建和包含哪些文件和文件夹

  • 内容,指定 Plaster 将执行的操作——复制文件、修改文件、检查是否已安装必要的模块。

让我们从 PowerShell Gallery 安装模块。输入以下内容以下载模块:

Install-Module Plaster

我们可能需要确认 PowerShell gallery 是一个不受信任的仓库。现在,让我们导入模块:

Import-Module Plaster

就这样——我们已经准备好了。让我们看看我们得到了什么:

图 11.18 – 检查 Plaster 模块

图 11.18 – 检查 Plaster 模块

正如我们所看到的,我们已安装了版本 1.1.4 的模块,并且有了四个新的命令可以使用:

  • Get-PlasterTemplate:列出我们可以使用的现有模板。我们可以编写或下载 XML 格式的模板文件。这里只包含了两个模板,而我们需要的是 NewPowerShellScriptModule

  • Invoke-Plaster:运行 Plaster 脚手架工具。

  • New-PlasterManifest:此命令创建一个新的清单文件。

  • Test-PlasterManifest:此命令测试清单文件是否正确格式化。

让我们运行它,看看会得到什么:

Get-PlasterTemplate

然后,复制 NewPowerShellScriptModule 的路径。现在,输入以下内容:

Invoke-Plaster

系统会询问你默认模板的路径以及目标路径;这个路径需要是文件夹,而不是文件。

你还需要提供模块的名称和版本,以及是否希望将 VS Code 设置为默认编辑器。对我而言,它是这样的:

图 11.19 – 调用 Plaster

图 11.19 – 调用 Plaster

正如我们所看到的,我的沙箱机器缺少一个必要的模块——Pester。Pester 是一个让单元测试和测试驱动开发变得非常简单的模块,但这超出了本书的范围。

让我们看看目标路径中创建了什么:

图 11.20 – 使用默认 Plaster 模板创建的模块

图 11.20 – 使用默认 Plaster 模板创建的模块

如我们所见,Plaster 已经创建了两个文件——一个脚本模块文件和一个模块清单文件——一个用于测试脚本的文件夹,以及一个用于 VS Code 设置的文件夹。

NewPlasterModule.psm1文件如下所示:

图 11.21 – 一个由 Plaster 生成的基础模块文件

图 11.21 – 一个由 Plaster 生成的基础模块文件

如我们所见,这非常简单,但它包含了一个很棒的技巧——如果文件中的函数按照标准的 cmdlet 命名约定命名,比如Verb-Noun,那么它们将被导出。如果不是,比如我们在本章前面写的setMessage函数,那么它们将不会被导出。很整洁。

Plaster 的美妙之处在于它的可扩展性;编写模板来创建公共和私有函数及类的文件夹结构非常简单,并且这一切都可以以可重复的方式完成。为了了解 Plaster 的多功能性,可以查看 Kevin Marquette 的博客:powershellexplained.com/2017-05-12-Powershell-Plaster-adventures-in/以及他的 GitHub 页面:github.com/KevinMarquette/PlasterTemplates。不妨尝试一下他的示例模板。

这就是我们将要涉及的关于 Plaster 的内容——还有其他几种脚手架模块可用,如果你不喜欢 Plaster,可以看看其他的模块。这也标志着本章的结束;让我们回顾一下我们所做的。

总结

我们从回顾前几章关于模块的内容开始,并将其放入一个更正式的背景中。我们查看了标准的模块位置,并学习了如何通过编辑$ENV:PSModulePath变量来添加位置。我们了解了 PowerShell 如何利用这些位置来促进自动加载,以及有时我们可能不希望发生这种情况。接着,我们研究了如何手动导入模块,并通过查看PowerShellGet模块结束了我们的复习。

之后,我们开始编写自己的模块。我们从查看最早的代码导入方法——点源(dot-sourcing)开始,并了解了为什么这可能是一个糟糕的主意。接着,我们通过编写脚本并将其转换,创建了我们的第一个模块。然后,我们研究了如何通过嵌套模块来构建应用程序,接着讨论了其他类型的模块,如二进制模块。

然后,我们研究了最常见的复杂模块类型——清单模块。我们了解了清单文件如何控制加载和导出的内容,以及如何编写和测试清单文件。最后,我们看了一个可以使编写模块变得更加简单的工具——Plaster。

在下一章中,我们将讨论 PowerShell 的安全性方面,以及如何最好地保护自己、同事和用户,使用如此强大的工具。

练习题

以下是本章的练习题:

  1. 如何列出当前 PowerShell 会话中所有已导入的模块?

  2. 导入模块时,-Global 参数的作用是什么?

  3. 如何导入一个不在 $ENV:PSModulePath 变量指定路径中的模块?

  4. 我们想导入一个包含与我们会话中已存在的 cmdlet 同名的函数的模块。我们可以通过哪两种方式解决这个问题?

  5. 默认情况下,模块的所有函数都会被导出。我们可以通过哪两种方式控制导出的函数?

  6. 模块清单中 HelpInfoURI 键的作用是什么?

  7. 扩展名为 .ps1xml 的文件可能包含什么内容?

  8. 如果我们加载一个扩展名为 .dll 的模块,我们会得到什么样的命令?

  9. 为什么我们不编写 CDXML 模块?

进一步阅读

要了解更多本章涵盖的主题,请查看以下资源:

第十二章:加固 PowerShell

正如我们所见,PowerShell 是一个极其强大的工具,引用本·叔叔的话:“伟大的力量伴随着伟大的责任。”如果你不知道本·叔叔是谁,可以问问你身边的蜘蛛侠。PowerShell 能够在系统或组织内造成巨大的破坏。这种破坏可以是故意的,由某人故意制造损害,但同样也可能是无意的。

本章将从 PowerShell 的一个强大特性——PowerShell 远程功能开始讲解。接下来我们将介绍 PowerShell 如何防止无意的错误,然后继续讨论 PowerShell 中可以保护我们免受故意攻击的功能。随后,我们将探索 PowerShell 提供的功能,这些功能能够通过日志分析机器上发生的事件,最后总结我们可以采取的措施来提升编写代码的安全性。由于 PowerShell 与 Windows 的历史关系,其中许多功能仅限于 Windows,或者在 Windows 中得到了更充分的开发。当出现这种情况时,会特别提及。

本章将涵盖的主要主题如下:

  • 为什么安全性如此重要?

  • 防止无意错误对 PowerShell 的影响

  • 安全地运行 PowerShell

  • PowerShell 日志记录

  • 编写安全代码

为什么安全性如此重要?

我们已经看到 PowerShell 是多么强大,但还没有看到它为何如此危险。到目前为止,我们所做的一切都需要在我们使用的客户端进行交互式登录,这意味着在攻击之前,某人需要物理访问设备。然而,PowerShell 有一个概念叫做 PowerShell 远程功能,它允许我们登录到远程设备并像在现场一样执行 PowerShell 代码。这就是它成为如此强大管理工具的原因。虽然本书不会详细讨论远程功能,因为它主要是管理员工具,但了解基础概念仍然很重要。让我们仔细看一看。

PowerShell 远程功能速览

许多较老的 Windows PowerShell cmdlet 都有一个 -ComputerName 参数,它允许 PowerShell 查询远程机器的信息。这个参数的问题在于,它依赖于我们运行 PowerShell 会话时所使用的凭据必须具备查询远程机器的权限。如果我们幸运的话,cmdlet 会有一个 -Credential 参数,允许我们提供其他凭据。然而,很多时候我们并不幸运;请参见以下截图。

图 12.1 – Get-Process 不走运

图 12.1 – Get-Process 不走运

如我们所见,Get-Process cmdlet 没有-Credential参数,因此我们只能希望我们的会话凭据在远程计算机上有效。如果我们在 PowerShell 会话中没有正确的凭据,就无法在远程计算机上运行Get-Process。即便我们有正确的凭据,远程防火墙也很可能会阻止该请求。在 PowerShell 7.2 及更高版本中,它也没有–ComputerName参数。

为了绕过这个问题,PowerShell 远程连接是在 Windows PowerShell 3.0 中开发的。它使用5985(HTTP)和5986(HTTPS)端口,这意味着它可以使用 SSL 加密。在使用 Active Directory 的企业配置中,流量也会被加密,使用的是 Kerberos。显然,Linux 机器不包括 WinRM,因此 PowerShell 7.x 版本贴心地包括了通过 SSH 运行远程会话的支持。我们将在本章的安全运行 PowerShell部分中更详细地讨论这一点。现在我们来看看 PowerShell 远程连接在 Windows 环境中的工作方式;我们将从启用 PowerShell 远程连接开始。请注意,我在一台 Windows 10 客户端上运行此操作,并创建了一个名为Admin的新本地用户,并为其设置了密码和管理员权限。

启用 PowerShell 远程连接

默认情况下,PowerShell 远程连接在所有 Windows 服务器上启用。然而,它在 Windows 客户端上是禁用的。让它运行的最简单方法是使用Enable-PSRemoting cmdlet。这个 cmdlet 做了我们需要做的一切,从启用端点到创建正确的 Windows 防火墙规则。此命令需要从提升的管理员提示符中运行。看看下面的截图:

图 12.2 – 启用 PowerShell 远程连接

图 12.2 – 启用 PowerShell 远程连接

如我们所见,我已使用几个常见参数运行了Enable-PSRemoting-Force参数表示我们不会被反复询问是否确定要执行此操作,我们之前也见过这种用法。不过,-SkipNetworkProfileCheck是一个比较有趣的参数。许多人没有在独立客户端上配置网络配置文件,默认的网络类型是公共网络。然而,默认情况下,PowerShell 远程连接无法在公共网络上运行。使用-SkipNetworkProfileCheck可以跳过网络配置文件检查,但会创建一条防火墙规则,仅允许来自本地子网的远程会话。这个规则可以很容易地进行编辑,但最好还是正确设置网络配置文件,或者避免在公共网络上尝试运行远程会话。

创建会话

现在我们已经在客户端上设置了远程连接,让我们创建一个会话。我们可以使用New-PSSession cmdlet 来完成。我将把我的Admin用户凭据存储在名为$cred的变量中,然后像这样运行它:

New-PSSession -ComputerName localhost -Authentication Negotiate -Credential $cred

我们可以看到这里得到的输出:

图 12.3 – 创建新的远程连接会话

图 12.3 – 创建新的远程连接会话

这将创建一个持久会话,可以根据需要连接和断开连接。

加入和退出会话

我们可以使用Enter-PSSession cmdlet 连接到一个会话,方法如下:

Enter-PSSession -Name <session name>

我们创建的会话是一个 PowerShell 对象(和 PowerShell 中的其他一切一样),因此我们也可以通过将会话传递给管道来进入:

Get-PSSession -Name <session name> | Enter-PSSession

这是我机器上的运行效果:

图 12.4 – 进入和退出远程会话

图 12.4 – 进入和退出远程会话

在第一个命令中,我通过名称调用了会话。在第二个命令中,我将会话对象传递给了Enter-PSSession。我使用exit关键字退出了会话。请注意,一旦进入会话,PowerShell 提示符会更改为反映我连接的客户端的ComputerName以及远程工作目录。

这称为一对一远程会话。一旦进入这种会话,就等同于直接连接到远程机器。只有远程机器上的 PowerShell 模块可用。

我们也可以通过不指定现有会话来进入临时会话;相反,我们指定计算机名称和凭证,像这样:

Enter-PSSession -ComputerName <name> -Credential <credential>

正如我们所看到的,通过这种方式创建的会话在退出时会消失,而不是保持存在:

图 12.5 – 临时远程会话

图 12.5 – 临时远程会话

在前面的截图中的第一个命令里,我创建了一个临时会话,然后通过exit退出。正如我们所看到的,当我输入exit后,会话不再可用,在最后的命令中我运行了Get-PSSession cmdlet。

要移除持久化会话,我们可以将会话名称传递给Remove-PSSession,像这样:

Remove-PSSession –Name <session name>

或者,我们将会话对象通过管道传递:

Get-PSSession -Name <session name> | Remove-PSSession

如果我们运行前面的Get-PSSession cmdlet,但不指定会话名称,那么所有会话都会被移除。

一对多会话

我们还可以通过使用Invoke-Command cmdlet,在多台机器上同时运行命令和脚本,方法如下:

Invoke-Command -ComputerName <name 1>, <name 2> -ScriptBlock {Get-Service}

这将把脚本块中的命令或脚本应用于-ComputerName参数中指定的计算机名称列表。这个命令使用 PowerShell 远程功能,和之前的PSSession cmdlet 一样。

能够在远程机器上运行命令,使得 PowerShell 成为一个极其有用的管理工具,但遗憾的是,这也意味着它存在一定的安全风险。现在,让我们来看看如何防止 PowerShell 造成问题。

保护 PowerShell 免受无意错误的影响

我们首先要介绍的一组工具是那些能保护我们避免因为操作失误而造成问题的工具。其中最有用的工具之一是一个内置的执行策略,它可以用来控制脚本的运行方式。

执行策略

我们在第八章中遇到了执行策略功能,编写我们的第一个脚本——将简单的 Cmdlet 转换为可重用代码,并提到我们将在本章中更详细地介绍它。执行策略是一个安全功能,控制我们如何运行脚本,但仅适用于 Windows 环境。Don Jones(那位 Don!)曾描述执行策略的目的是“…减缓一个没有经验的用户,防止他无意中运行一个不受信任的脚本”。这句话里有很多无意的成分。执行策略会给有知识的用户带来一些障碍,他们是故意运行脚本的。防止用户运行潜在破坏性脚本的最佳方法是确保他们没有执行脚本中命令的权限。我曾在客户环境中看到,域管理员帐户数量达到数百个,其中许多多年未使用,但仍然启用。这些客户的问题比 PowerShell 的执行策略要大得多。

执行策略有不同的安全级别,并且应用于不同的作用域。级别由脚本的来源决定,以及它是否由受信任的证书颁发机构的代码签名证书签名;我们将在本章的签名脚本部分讨论代码签名证书。首先,让我们看看这些级别:

  • Restricted:这是 Windows 客户端计算机上的默认策略,禁止运行脚本,包括格式化文件和模块——任何扩展名为 .ps1xml.psm1.ps1 的文件。可以在终端运行单个命令,但仅此而已。

  • AllSigned:此级别允许运行由受信任发布者签名的脚本;这包括在计算机上编写的脚本。如果脚本没有签名,则不能运行。如果它是由不受信任的发布者签名,PowerShell 将提示用户。显然,如果脚本是恶意的并且已签名,它仍然可以运行。

  • RemoteSigned:来自本地计算机之外的脚本需要数字签名,但在本地计算机上编写的脚本不需要。显然,有许多隐蔽的方式可以绕过这一点,可能涉及打开记事本并使用复制粘贴。这是 Windows 服务器计算机的默认策略。

  • Default:这是设置默认策略的选项。在 Windows 客户端上,默认策略是 Restricted。在 Windows 服务器上,默认策略是 RemoteSigned

  • Undefined:这将移除当前作用域上设置的任何执行策略。如果我们在所有作用域上使用此策略,那么结果将是默认策略,如前所述。

  • Unrestricted:这是非 Windows 计算机上的默认策略。来自任何地方的未签名文件都可以运行;用户在运行未来自本地网络的脚本时,可能会收到警告。

  • Bypass:不阻止任何操作,也没有警告。执行策略被忽略。可以通过调用 pwsh.exe 程序的 -ExecutionPolicy 参数来设置。我们可以在下图中看到这种情况:

图 12.6 – 绕过执行策略

图 12.6 – 绕过执行策略

如我们所见,执行策略仅在 pwsh.exe 进程级别被绕过,但在其他作用域中保持默认设置。当该 pwsh 进程结束时,执行策略绕过将失效。

接下来,让我们考虑可以应用执行策略的作用域。

如我们在图 12.6中所见,我们可以在五个作用域中应用执行策略。每个作用域具有不同的优先级。MachinePolicyUserPolicy 作用域只能在组策略中设置——这是 Windows Active Directory 的一项功能。组策略是一个用于集中控制用户和机器配置的企业应用程序,我们在这里不需要过多关注它。这里有一篇关于组策略的优秀入门文章:techcommunity.microsoft.com/t5/ask-the-performance-team/the-basics-of-group-policies/ba-p/372404

作用域有优先级顺序,较低级别的作用域会被较高级别的作用域覆盖。作用域的优先级从高到低依次为:

  • MachinePolicy:此作用域通过组策略设置,适用于机器上的所有用户。

  • UserPolicy:此作用域通过组策略设置,适用于机器的当前用户。

  • Process:此作用域指当前的 PowerShell 会话。没有与之关联的注册表位置;它由 $env:PSExecutionPolicyPreference 变量的内容控制。当会话关闭时,变量的内容会被移除。

  • CurrentUser:此作用域仅适用于当前用户,并存储在 Windows 注册表中,这是一个包含机器设置的数据库。

  • LocalMachine:此作用域适用于机器上的所有用户,也存储在注册表中。

图 12.6中,我们可以看到作用域按优先级顺序列出。由于在 MachinePolicyUserPolicy 级别的执行策略是 Undefined,因此有效的执行策略是 Bypass,它定义在 Process 级别。

我们可以与执行策略一起使用的两个 cmdlet 是 Get-ExecutionPolicySet-ExecutionPolicy,我们在第八章编写我们的第一个脚本 – 将简单的 Cmdlet 转化为可重用的代码中见过。接下来,让我们简要地了解 PowerShell 的另外两个旨在帮助防止错误的功能。

其他功能

这些功能仅防止有人不小心运行脚本。首先,PowerShell 扩展名(如.ps1)默认与记事本关联,而不是作为可执行文件。这意味着如果有人不小心双击一个脚本,它会在记事本中打开,而不是直接运行。这与早期的 Windows 脚本文件(如批处理文件和 Visual Basic 脚本文件)不同,后者如果双击会直接执行。

另一种防止意外执行的保护措施是,PowerShell 不会在当前文件夹中搜索脚本文件,因此必须提供相对路径或绝对路径才能在终端提示符下运行脚本,例如这样:

图 12.7 – 绝对路径和相对路径

图 12.7 – 绝对路径和相对路径

在第一行,我使用了绝对路径来调用一个名为HelloWorld.ps1的脚本,脚本输出hello world。在第二行,我切换到包含该脚本的文件夹,第三行,我尝试通过使用HelloWorld来调用它,这对 Windows 识别为可执行的程序(如批处理文件)有效,但对 PowerShell 脚本无效。注意 PowerShell 是如何友好地告诉我哪里出错了的。第四行,我通过在脚本名称前加上.\来使用相对路径调用它,成功运行了。

过去,PowerShell 的安全性就是这样了。幸运的是,在最近的版本中,添加了很多新的功能,这些功能更加主动。让我们来探讨一下如何安全地运行 PowerShell。

安全地运行 PowerShell

PowerShell 有几个功能可以主动提高我们环境的安全性。我们先来看一下应用程序控制。

应用程序控制

应用程序控制解决方案可以防止未经授权的应用程序运行。市面上有第三方应用程序,如 Trellix,但 Windows 10 及更高版本自带了两种内置应用程序——Windows Defender 应用程序控制WDAC)和 AppLocker。这些可以用来创建策略,强制执行允许执行的应用程序白名单,并阻止其他任何应用程序运行。由于 AppLocker 不再进行开发,因此推荐使用 WDAC。这些解决方案主要用于企业环境,并允许集中控制。我并不清楚有任何能够与 Linux 或 macOS 上的 PowerShell 一起使用的解决方案。当 PowerShell 在默认的 WDAC 策略下运行时,受信任的模块和脚本将比不受信任的模块和脚本获得更多的访问权限,这得益于一种叫做语言模式的功能。

语言模式

语言模式用于控制如何在受应用程序控制策略影响的 PowerShell 环境中运行脚本。值得注意的是,网上有很多文章展示了如何通过变量设置语言模式;这只是用于测试代码在特定模式下的表现,并不安全,正如我们接下来所说的那样。根据微软的说法,唯一能够通过语言模式来强制执行安全策略的方法是通过像 WDAC 这样的应用程序。共有三种语言模式:

  • FullLanguage:当未在应用程序控制策略下运行时,这是默认模式。

  • ConstrainedLanguage:这会阻止创建和使用某些 .NET 类型,并限制从 PowerShell 访问 C# 代码。它还限制了对诸如 ScheduledJob 等功能的访问。许多脚本在受限语言模式下无法运行;它们需要签名,并且发布授权机构需要被添加到白名单中。

  • NoLanguage:此模式完全禁用 PowerShell 脚本。只能运行本地命令和 cmdlet。New-Object 也被禁用。

我们可以通过调用 $ExecutionContext.SessionState.LanguageMode 变量来检查会话的语言模式,如下所示:

图 12.8 – 获取和设置语言模式

图 12.8 – 获取和设置语言模式

在第一行,我调用了变量,可以看到语言模式已设置为 FullLanguage。然后,我将变量设置为 ConstrainedLanguage;我们可以看到,当我再次调用该变量时,它已被设置为 ConstrainedLanguage。不幸的是,当我尝试在第三行将其恢复为 FullLanguage 时,失败了,因为我们处于 ConstrainedLanguage 模式,访问变量的权限受限。解决此问题的最简单方法是关闭会话并重新打开一个新的会话;语言模式会恢复为 FullLanguage。如我们之前所讨论的,设置这个变量并不能使 PowerShell 更加安全。

安全服务标准

PowerShell 受微软的 Windows 安全服务标准影响,因此当检测到漏洞时,某些功能会收到安全更新。不幸的是,这些仅在 Windows 上操作的功能,如执行策略和应用程序控制,才会受到影响。

软件材料清单

软件材料清单SBOM)通过识别软件创建过程中使用的每个资源,帮助为软件提供透明性、完整性和身份验证,并提供可以用于将软件与已知漏洞关联的代码签名和软件身份。许多政府要求使用 SBOM,以响应 2020 年的 SolarWinds 供应链攻击。这适用于 Windows 和 Linux/Mac。

Windows 防恶意软件扫描接口支持

PowerShell 将脚本块和.NET 调用传递给 Windows 反恶意软件扫描接口AMSI)API,以便反恶意软件应用程序(如 Windows Defender)可以检查是否存在恶意代码。这是一个仅适用于 Windows 的功能。

安全外壳(SSH)远程访问

SSH 协议是一种加密协议,它支持在不安全的网络上提供安全的网络服务。它是跨平台的,能够在 Windows 和 Linux/Mac 系统上运行。它依赖于公钥加密;我们需要生成一对密钥,并将公钥传递给我们想要远程连接的系统。随后当我们打开会话时,我们指定私钥在本机上的路径,SSH 会验证远程机器上的公钥是否与本地私钥配对。私钥在任何时候都不会通过网络传输。虽然设置过程相当复杂,但一旦设置完成,使用 SSH 非常简单。

足够的管理权限

Get-Process,采用最小权限原则,排除其他一切进程运行,这意味着它们将无法运行Start-Process。我们还可以配置端点使用虚拟的特权账户,这样用户就不需要管理员账户来运行命令了。这使我们能够大幅度减少拥有管理员权限账户的用户数量。

正如我们所见,几乎所有这些功能仅适用于 Windows 环境。幸运的是,所有使用 Linux 的人都完全值得信任。让我们来看看可以使用的功能,以便了解 PowerShell 的用途——日志记录。

PowerShell 日志记录

PowerShell 有多种方式来记录我们使用它的操作。在本节中,我们将介绍其中的三种:肩膀旁的日志记录、深度脚本块日志记录和模块日志记录。

肩膀旁的日志记录

PowerShell 可以记录会话的转录,通过使用Start-TranscriptStop-Transcript命令。转录功能会记录屏幕上显示的所有内容。这对于记录我们所做的操作和获得的输出,并与他人分享非常有用。

我们可以使用-Path参数设置转录文件的保存路径,并使用-Append参数向现有转录文件添加内容,后接现有转录文件的名称和路径。-InvocationHeader参数记录每个命令运行的时间。当我们想停止记录时,可以使用Stop-Transcript命令。

在较早版本的 PowerShell 中,我们需要进行一些操作才能确保转录功能开启,即使如此,它也仅会记录控制台中的交互式会话,但现在情况已经不一样了。我们可以通过使用组策略或编辑位于 $pshome 位置的 powershell.config.json 文件来在 Windows 系统中启用自动转录功能——在 Windows 中,该位置是 C:\Program Files\PowerShell\7。编辑配置文件适用于 Windows 和 Linux/Mac,尽管你可能需要先创建该文件。要启用转录功能,我们需要将以下 JSON 添加到文件中:

{
    "Transcription": {
        "EnableTranscripting": true,
        "EnableInvocationHeader": true,
        "OutputDirectory": "c:\\Temp\\MyTranscriptPath"
      }
}

要停止转录,只需删除 JSON 条目。

转录文件包含一个有用的头部信息,其中包括会话运行时的账户、会话是否使用了 RunAs 凭据(例如管理员)、使用了哪个版本的 PowerShell 以及进程号。让我们来看一下:

图 12.9 – 转录头部

图 12.9 – 转录头部

我使用了带有 -Head 参数的 Get-Content 命令来调用转录文件的前 19 行,并且我们可以看到它包含的信息。当我运行 Stop-Transcript 时,转录文件的名称和路径会显示出来。

旁路日志记录是一个非常强大的工具,能够记录 PowerShell 的使用情况,特别是当转录文件被发送到中央共享位置并由 安全信息与事件管理 (SIEM) 应用程序扫描时。然而,它并不能捕获所有内容。让我们看一下下一个技术——深度脚本块日志记录。

深度脚本块日志记录

如果我们调用了一个函数或脚本,转录将记录该调用,但它不会告诉我们关于函数或脚本的内容。这时,我们需要启用深度脚本块日志记录,它会记录脚本的内容以及其他相关信息到日志系统中。在 Windows 中,这就是 PowerShellCore 日志,位于 /var/log/journal

我们可以通过 Windows 上的组策略启用深度脚本块日志记录,或者如果我们使用的机器不在 Active Directory 域中,则可以通过配置文件启用。

在我的机器上,我已经编辑了 PowerShell.config.json 文件,将第 4–16 行内容包含在以下截图中:

图 12.10 – 在 PowerShell 中启用深度脚本块日志记录

图 12.10 – 在 PowerShell 中启用深度脚本块日志记录

记住,这是 JSON 格式,因此正确的语法非常重要。VS Code 会将任何语法错误标出。我们还需要管理员权限才能保存文件。

现在,如果我打开一个新的会话并调用本章开始时使用的 HelloWorld.ps1 脚本,我可以期待在事件日志中看到类似的事件:

图 12.11 – 事件日志中的脚本块日志记录事件

图 12.11 – 事件日志中的脚本块日志记录事件

记住,HelloWorld.ps1脚本只有一行代码——Write-Output "hello world"。我们可以在前面的事件中清楚地看到这一行代码。我们还可以看到运行该代码的账户、运行时间以及运行所在的机器。

模块日志记录

我们将要查看的最后一种日志记录类型是模块日志记录。这类似于脚本块日志记录,跟踪 PowerShell 中加载和调用的模块。这可能会迅速生成大量信息,因此可以选择仅记录指定的模块。如果我们想记录所有模块,可以在添加到配置文件的 JSON 中使用通配符(*)而不是模块名称的数组。同样,如果有组策略可用,我们可以使用它来启用模块日志记录。

还有其他类型的日志记录可用,但它们是相当专业化的,例如用于可能包含敏感信息(如个人详细信息)的事件的保护事件日志记录。我自己没有需要使用这些。

这就是我们要介绍的安全功能。接下来让我们进入本章的最后部分,看看如何编写安全代码。

编写安全代码

大多数代码都有安全漏洞;我们的工作是确保我们编写的代码尽可能安全。在本节中,我们将探讨一些编写更安全 PowerShell 代码的方法。我们将看看如何存储脚本可能需要的密码、对脚本进行代码签名和参数验证。

安全存储密码

很多时候,我们会编写一个包含需要使用特定凭证执行的命令的脚本。我们在第六章PowerShell 和文件——读取、写入和操作数据中看到过如何将凭证存储在 XML 文件中。简单回顾一下,我们可以这样做:

$cred = Get-Credential
$cred | Export-Clixml Credential.xml

凭证随后存储在一个 XML 对象中。这个对象包含一个加密的标准字符串,它是通过基于账户和加密机器的可逆加密加密的密码。让我们看看它是如何工作的。

我有一个想要作为安全字符串保存的密码:

$pwd = "ILovePowershell"

要将其转换为安全字符串,我使用ConvertTo-SecureString cmdlet:

$securepwd = $pwd | ConvertTo-SecureString -AsPlainText -Force

现在,密码已经被加密了,如果我们尝试这样做的话,可以看到这一点:

$encryptedpwd = $securepwd | ConvertFrom-SecureString
write-host $encryptedpwd

在这种情况下,加密是基于登录账户和机器的,只有原始用户(或以原始用户身份登录的人)在原始机器上才能解密该字符串。让我们看看它的表现如何:

图 12.12 – 将密码放入安全字符串

图 12.12 – 将密码放入安全字符串

在第 1 行,我创建了一个名为$pwd的变量,保存了一个字符串ILovePowerShell。在第 2 行,我通过将$pwd传递给ConvertTo-SecureString cmdlet,并使用-AsPlainText参数,再配合-Force参数以抑制任何提示,创建了一个新的变量$securepwd。在第 3 行,我将$securepwd的内容从安全字符串转换回文本,并将其存储在$encryptedpwd变量中。最后,在第 4 行,我输出$encryptedpwd变量的内容,正如我们所看到的,它现在是一个加密字符串,而不是原始密码。

$encryptedpwd可以存储在文件或注册表中,并且可以用来创建一个PSCredential对象,传递给 cmdlet,使用Add-Content cmdlet:

图 12.13 – 将密码作为安全字符串添加到 PSCredential

图 12.13 – 将密码作为安全字符串添加到 PSCredential

在第 1 行,我将$encryptedpwd的内容添加到一个文件encryptedpwd.txt中。在第 2 行,我将该文件的内容转换为安全字符串并存入一个变量$securepwd2中。在第 3 行,我创建了一个新的PSCredential对象,包含两个值——用户名字符串和$securepwd2变量。最后,在第 4 行,我检查凭据的密码属性,正如我们所看到的,它是ILovePowerShell,即我最初使用的字符串,在第 5 行。通过这种方式,我们可以将密码安全地存储在计算机上的文件中,且无法将其复制到另一台计算机并进行解密。当然,有人可以远程连接到 PowerShell 并进行解密,所以这并非万无一失。

那么,安全字符串和加密字符串之间有什么区别呢?加密字符串是一个明文字符串,它已经被加密,正如你在encryptedpwd.txt文件中看到的那样。安全字符串是System.Security.SecureString类型的对象。它们是两种不同的类型。许多 PowerShell cmdlet 只接受System.Security.SecureString类型的密码,因此理解需要哪种类型是非常重要的。

签名脚本

在本章前面,我们讨论了执行策略以及它们如何依赖于脚本签名来确定脚本的可信度。数字签名有两个作用——它提供了脚本由可信来源签名的保证,并验证了脚本在签名后是否未被编辑。

要签署一个脚本,我们需要一个来自可信机构的代码签名证书,例如 VeriSign,它会被大多数计算机信任,或者来自 Active Directory 证书授权中心的证书,这会被该目录中的计算机信任,或者我们可以使用自签名证书,这种证书仅在用于签署脚本的计算机上有效。代码签名证书并不便宜,虽然有一些公司可能在探索这种方法,但截至写作时,这些证书还没有普遍可用。

一旦我们拥有了代码签名证书,就可以使用Set-AuthenticodeSignature cmdlet 通过证书对脚本进行签名。签名包括脚本的哈希值,因此签名后所做的任何更改都会破坏签名,导致脚本无法被信任。此签名仅在 Windows 环境中有效,类似于执行策略功能的使用方式。

参数安全性

第八章PowerShell 和文件——读取、写入和操作数据中,我们看到可以使用加速器强制参数只接受特定类型的输入。我们还可以使用正则表达式来确保输入格式正确,例如日期字符串或 IP 地址。以下代码将测试一个参数,并且只接受 DD/MM/YYYY 格式的日期字符串:

function Test-Date {
    param(
        [string]$date
    )
    if ($date -match "^(3[01]|[12][0-9]|0?[1-9])(\/|-)(1[0-2]|0?[1-9])\2([0-9]{2})?[0-9]{2}$")
{
        return $true
    }
    else {
        return $false
    }
}

我们可以在以下截图中看到它的效果:

图 12.14 – 验证参数输入

图 12.14 – 验证参数输入

我创建了一个Test-Date函数,使用正则表达式来测试输入是否为有效日期。当我传入一个格式正确的有效日期时,函数返回True。当我传入一个无效日期21/13/2023时,它返回False。我们可以利用返回值来阻止脚本继续执行,或者提示用户输入有效值。

上面的例子是微不足道的,但未经验证的参数可能会用于恶意代码注入,即代码被插入到参数中并执行。参数验证有助于防止这种情况。

这就是本章内容的总结。让我们回顾一下我们学到的知识。

总结

本章开始时,我们研究了 PowerShell 远程执行功能,发现该功能可能使 PowerShell 成为安全风险。我们了解了如何创建、加入和离开会话,以及如何同时在多个远程计算机上执行表达式。

接着我们探讨了如何保护 PowerShell 免受无意错误的影响,了解到执行策略可以“…减缓一个未受过培训的用户,他们无意间试图运行一个不受信任的脚本。” 我们还看到了一些其他早期的安全功能,比如要求脚本执行时使用绝对路径或相对路径。

然后我们讨论了更现代的安全功能,其中许多仅适用于 Windows,例如应用程序控制和语言模式。不幸的是,Linux 和 macOS 的安全功能仍然缺乏。

我们了解了记录 PowerShell 操作的不同方式,包括肩膀日志、脚本块日志和模块日志,并了解了如何开启或关闭这些功能,以及在哪里查看日志。

在最后一部分,我们探讨了三种编写更安全脚本的技术——将密码存储为加密字符串、签署我们的脚本和验证参数。

本章的内容就到这里,这部分我们讨论了脚本编写和工具使用。在书的最后一部分,我们将探讨 PowerShell 在三个不同环境中的应用——Windows、Linux/macOS 和 ARM,并以一章关于如何在 .NET 平台上使用 PowerShell 作为结束。很期待这部分内容。

练习

  1. 哪个 cmdlet 用于创建一个新的 PowerShell 会话以进行远程管理?

  2. 如何在 Linux 上安全地使用 PowerShell 远程操作?

  3. PowerShell 中允许仅运行已签名脚本的执行策略是什么?

  4. -ExecutionPolicy Bypass 开关的目的是什么?

  5. 在 Windows 上,如何分析并阻止已知的恶意脚本和配置?

  6. PowerShell 中限制语言模式的目的是什么?

  7. 如何在 Windows 上限制特定用户使用 PowerShell 中的特定 cmdlet?

  8. PowerShell 中的脚本块日志记录是什么?它为何对安全性重要?

  9. 安全字符串和加密字符串有什么区别?

进一步阅读

第三部分:使用 PowerShell

现在我们已经了解了如何使用 PowerShell 7,以及如何编写简单的脚本和模块,是时候学习如何在不同平台上使用它了。PowerShell 7 的一个重要特点是它是跨平台的。本节将详细介绍如何在各种平台上使用 PowerShell。本部分包括以下章节:

  • 第十三章在 PowerShell 7 和 Windows 中工作

  • 第十四章PowerShell 7 在 Linux 和 macOS 上的使用

  • 第十五章PowerShell 7 和 Raspberry Pi

  • 第十六章在 PowerShell 和 .NET 中工作

第十三章:使用 PowerShell 7 和 Windows

欢迎来到本书的最后一部分,在接下来的四个章节中,我们将讨论如何在不同的环境中使用 PowerShell 7。在本章中,我们将重点介绍基于 Windows 系统的特殊性,并探讨现有的各种解决方法,以便我们能够有效地使用 PowerShell 7。我们还将讨论何时必须使用原生的 Windows PowerShell。需要记住的是,PowerShell 7 是一个开源产品,并且会快速变化。本章及随后的示例在写作时是准确的,但在阅读时可能不再适用。另外,PowerShell 7 也存在一些 bug。PowerShell GitHub 页面是一个强大的信息来源,可以提供有关最新变化的信息,同时也有一些功能不按预期工作的情况:github.com/PowerShell/PowerShell

正如我们在 第一章中介绍的那样,PowerShell 7 简介——它是什么以及如何获取,PowerShell 7.2 及之后的版本是基于 .NET 6 平台,这是 .NET Core 的最新版本。然而,Windows PowerShell 和许多 Windows 应用程序是使用 .NET 4.5 框架构建的,后者并非开源,且包含许多专有代码。这导致了一些不兼容的情况,某些在 Windows PowerShell 中有效的内容在 PowerShell 7 中无法工作。我们将在本章中讨论这些问题,然后探讨如何使用通用信息模型CIM)和Windows 管理工具(WMI)来管理我们的 Windows 机器。

本章将涵盖的主要主题如下:

  • 理解 PowerShell 7 和 Windows PowerShell

  • 探索兼容性

  • PowerShell 7 不兼容的内容

  • 管理带有 CIM 和 WMI 的机器

理解 PowerShell 7 和 Windows PowerShell

理解如何在 Windows 上使用 PowerShell 7 的关键,是要知道 PowerShell 7 是建立在一个完全不同的平台上的;PowerShell 7 基于开源的、简化版的 .NET,而 Windows PowerShell 是建立在完整的专有 .NET 框架上的。这意味着 Windows PowerShell 与 Windows 操作系统及其上运行的许多应用程序具有更高程度的原生兼容性,并且可以使用 .NET 框架中 PowerShell 7 无法访问的元素。微软用术语桌面版来指代运行在 .NET 框架上的 PowerShell,而用术语核心版来指代运行在开源 .NET 上的 PowerShell。

第十一章创建我们的第一个模块中,我们学到了大多数 PowerShell 功能来自使用可扩展的库,称为模块,并了解了模块是如何组合的。要在模块中运行命令,PowerShell 必须首先加载该模块。微软已经做了相当不错的工作,重写了核心 PowerShell 模块以及一些使用较多的模块,如 Active Directory 模块,但并非所有模块都与 PowerShell 7 兼容;有时作者还没有做这方面的工作,但有时不兼容的原因是模块中的关键功能依赖于 .NET Framework 中的一些特性,而这些特性在开源的 .NET Core 中并没有。如果我们尝试加载这些不兼容的模块,它们通常会导致错误,或者根本无法工作。在下一节中,我们将看到如何解决这个问题。然而,有时我们会发现某些事情根本无法工作,这时我们需要使用 Windows PowerShell。我们将在本章的第三节中探讨一些这样的情况。

探索兼容性

说一些模块与 PowerShell 7 不兼容倒是很容易,但我们怎么知道哪些模块可以使用,哪些不能使用呢?好消息是,大多数这个过程是自动化的,并且对普通用户来说是透明的;然而我们需要了解这些,以便理解为什么有时会出现问题。为了理解兼容性,我们需要记住我们在第十一章创建我们的第一个模块中学到的关于模块和模块清单的知识,以及在上一章第十二章保护 PowerShell中涉及的 PowerShell 远程操作部分。

哪些模块与 PowerShell 7 兼容?

让我们看看一些模块,了解如何判断它们是否与 PowerShell 7 兼容。在 Windows PowerShell 中运行此命令:

Get-Module | Format-Table -Auto -Wrap Name, CompatiblePSEditions, Version

我们应该看到类似以下的输出:

图 13.1 – 揭示兼容的版本

图 13.1 – 揭示兼容的版本

我们可以看到,已经加载了七个模块。其中四个模块有兼容性信息;Microsoft.PowerShell.Management表示它与桌面版和核心版兼容。Microsoft.Powershell.UtilityMicrosoft.PowerShell.SecurityMicrosoft.WSMan.Management仅与桌面版兼容。我们本可以在 PowerShell 7 中运行该命令,但当然那时它通常只会显示与核心版兼容的模块。

这三个模块没有提供兼容性信息;这可能意味着它们是在 PowerShell Core 发布之前编写的,因此仅与桌面版兼容,或者它们可能是错误类型的模块,无法提供兼容性信息。回想一下,模块有四种类型;脚本模块、清单模块、二进制模块和动态模块。清单模块包含一个扩展名为.Psd1的清单文件。我们可以通过运行Get-Module <modulename>来检查模块的类型,像这样:

图 13.2 – 检查模块类型

图 13.2 – 检查模块类型

在这种情况下,我们可以看到PSReadline是一个脚本模块,因此没有与其关联的清单文件,无法包含兼容性信息。

那么,当我们需要某些功能,但该模块未列出与 PowerShell 7 兼容时,我们该怎么办?我们有三个选择:

  • 查找兼容版本

  • 无论如何加载它

  • 使用兼容性模式

让我们来看一下这些方法。

查找兼容版本

如前所示的Microsoft.PowerShell.Utility模块在图 13.1中非常有用;首先,它提供了ConvertTo-* cmdlet。我们知道这些在 PowerShell 7 中可以使用,因为我们在整本书中都在使用它们。请看下面的图示:

图 13.3 – PowerShell 7 中的 Microsoft.PowerShell.Utility

图 13.3 – PowerShell 7 中的 Microsoft.PowerShell.Utility

如我们所见,Microsoft.PowerShell.Utility模块已经加载到我们的 PowerShell 7 会话中。怎么做到的?检查不同截图中显示的版本号。PowerShell 7 加载的是Microsoft.PowerShell.Utility 7.0.0.0,而 Windows PowerShell 加载的是Microsoft.PowerShell.Utility 3.1.0.0。PowerShell 7 提供了不同版本的核心 PowerShell 模块,并安装在C:\program files\powershell\7\Modules中。一些第三方模块会有与 PowerShell 7 兼容的特定版本,即使我们当前的版本不兼容,值得在库中检查是否存在这种情况。例如,在 PowerShell Gallery 中,我们可以使用搜索词Tags:"core"来查找与 PowerShell 7 兼容的模块,如下所示:

图 13.4 – 在 PowerShell gallery 中搜索兼容模块

图 13.4 – 在 PowerShell gallery 中搜索兼容模块

如你所见,我包含了两个搜索词;第一个是字符串databases,第二个是Tags:"core"。这找到了一个可以在 PowerShell 7 中运行的数据库操作模块。让我们看看如果没有兼容版本该怎么办。

无论如何加载它

如果我们找不到与 PowerShell 7 兼容的模块版本,我们仍然可以尝试加载它。虽然这可能有效,但往往模块会加载成功,但在使用时并不会按预期工作。我将这种方法作为最后的手段,并作为一种警告;网上有些文章推荐这样做。就个人而言,我不会这么做。

话虽如此,如何加载没有兼容版本的模块呢?我们可以像这样使用 Import-Module-SkipEditionCheck 参数:

图 13.5 – 强制加载模块

图 13.5 – 强制加载模块

在这个示例中,我使用了-SkipEditionCheck参数,试图强制 PowerShell 7 在管理员权限下加载 RemoteDesktop 模块。正如我们所见,这并没有很好地工作。问题在于,某些模块可以正常工作,而有些模块则不能,但它们不会像RemoteDesktop那样产生错误。那么,正确的方法是什么呢?我们使用兼容模式。

使用兼容模式

在最新版本的 PowerShell 7 中使用兼容模式很简单。只需导入您想加载的模块,如下所示:

Import-Module RemoteDesktop

正如我们在下面的图中看到的,发生了一些奇妙的事情:

图 13.6 – 使用兼容模式

图 13.6 – 使用兼容模式

在第一个命令中,我所做的就是使用Import-Module命令没有任何参数。PowerShell 会检查是否有兼容的模块,如果没有找到,它就会创建一个远程会话,在后台运行 Windows PowerShell。该模块被加载到这个在本地机器上运行的远程会话中。它会警告我们这是它所做的操作。在第二个命令中,我检查了正在运行的远程会话,我们可以看到创建了一个名为WinPSCompatSession的会话。我们在第十二章《保护 PowerShell》中看到过远程会话。虽然在那一章中,我们明确地创建了会话。WinPSCompatSession是隐式远程处理的一个示例。

关于兼容模式,有两件重要的事情需要记住。第一件事是WinPSCompatSession运行的是 Windows PowerShell,而不是 PowerShell 7。看看这个:

图 13.7 – 演示在 WinPSCompatSession 中运行的 PowerShell 版本

图 13.7 – 演示在 WinPSCompatSession 中运行的 PowerShell 版本

在第一个命令中,我将WinPSCompatSession放入名为$session的变量中,以便可以方便地使用它。在第二行,我将$session变量传递给Invoke-Command,并从该会话中调用$PSVersionTable自动变量的PSVersion属性。我们从结果中可以看到该会话正在运行 PowerShell 版本 5.1——Windows PowerShell。在第三个命令中,我获取了本地终端会话中的相同信息,在该会话中我正在运行 PowerShell 7.3.8。

第二个重要的事情是这是一个远程会话,因此其行为与直接运行命令不同。最重要的区别在于输出已反序列化,我们可以在 图 13**.6 的警告中看到。请记住,命令的输出是一个 PowerShell 对象。要将 PowerShell 对象从远程计算机传递到本地计算机,对象在会话的远程端被转换为 CliXML(PowerShell 用于序列化对象的专门形式的 XML),然后在本地端被转换回 PowerShell 对象。这影响了我们可以对对象执行的操作,因为它现在具有不同的方法和属性。让我们看看这是什么样子的:

图 13.8 – 反序列化对象

图 13.8 – 反序列化对象

在顶部窗格中,我正在使用 Windows PowerShell 运行 Get-WmiObject win32_bios | gm 命令来获取 TypeNameGet-WmiObject 命令生成的对象 System.Management.ManagementObject 的成员。请注意它没有方法,而 PSComputerName 是一个 AliasProperty,因此如果我们调用 PSComputerName,实际上会获取 __SERVER 属性的值。

在底部窗格中,我正在使用 PowerShell 7 在 WinPSCompatSession 会话内部运行 Get-WmiObject 命令,并获取返回对象的成员。正如我们所看到的,现在这是一个 Deserialized.System.Management.ManagementObject,方法和属性已经微妙地改变了。例如,PSComputerName 现在是一个 NoteProperty,包含一个字符串:PSComputerName=localhost

现在,当我们明确使用 Invoke-Command 时,这相当容易记住。我们知道返回的内容在远程会话中运行,并且会是一个反序列化对象。然而,由于这可能在后台透明地发生,所以很容易意外加载一个兼容性模式的模块,然后惊讶地发现我们正在处理的对象的行为并不完全符合我们的预期。一些管理员喜欢通过编辑 powershell.config.json 文件来防止 PowerShell 兼容性中的隐式导入,包括 JSON 行:

"DisableImplicitWinCompat" : "true"

然而,这并不完全禁用兼容性模式,只是让我们更加注意何时使用它。我们仍然可以通过在 Import-Module cmdlet 中添加 -UseWindowsPowerShell 参数来显式使用兼容性模式,如下所示:

图 13.9 – 明确使用 PowerShell 兼容性

图 13.9 – 明确使用 PowerShell 兼容性

在第一行的命令中,我正常导入了 ScheduledTasks 模块,没有使用任何参数。在第二行,我测试是否存在兼容性会话——正如我们所见,并未打开远程会话。在第三行,我移除了该模块,在第四行重新加载它,这次添加了 -UseWindowsPowershell 参数。在第五行,我再次测试兼容性会话,结果它存在。

我们还可以通过编辑 powershell.config.json 文件中的拒绝列表,完全阻止特定模块在兼容性模式下加载:

"WindowsPowerShellCompatibilityModuleDenyList":  [
   "PSScheduledJob","BestPractices","UpdateServices"
]

如果我们尝试加载该列表上的模块,那么我们将看到类似以下的错误:

图 13.10 – 不,您不能加载 PSScheduledJob 模块

图 13.10 – 不,您不能加载 PSScheduledJob 模块

理解兼容性模式的限制非常重要。主要有四个限制:

  • 它仅在本地计算机上有效——无法在已经处于远程会话中的另一台机器上调用兼容性模式

  • 它要求在本地机器上安装 Windows PowerShell 5.1——较旧的机器可能仍然只安装了 Windows PowerShell 3.0 或 4.0

  • 它返回基于值的反序列化对象——而非可以操作的实时对象

  • 在任何一台机器上,同一时间只能运行一个兼容性会话,因此所有使用兼容性的模块将共享一个运行空间

同样重要的是要记住,PowerShell 7 的这一领域有许多人在积极工作,每个新版本的 PowerShell 都比上一个版本更好,每个新版本的 Windows 也与 PowerShell 7 更兼容。互联网上的许多指令和教程会迅速过时,这一章也是如此。

什么在 PowerShell 7 中无法使用

然而,Windows 上有些东西与 PowerShell 7 不兼容,而且也不太可能兼容。在本节中,我们将介绍其中的一些,这将为本章最后一节的内容,使用 CIM 和 WMI 管理机器,做好铺垫。

-ComputerName 参数已经从 Windows PowerShell 移植到 PowerShell 7;不幸的是,该参数在 Windows 中使用的一些协议和模型与 PowerShell 7 不兼容,因此该参数无法使用。在 PowerShell 7 的最新版本中,如 7.3,该参数终于从无法工作的 cmdlet 中移除。然而,对于从 Windows PowerShell 转向 PowerShell 7 的用户来说,这仍然可能会导致混淆。解决方法是改为使用 Invoke-Command

一些重要的管理模块,如用于管理 Windows Server 更新服务WSUS)的 UpdateServices,在兼容性模式下无法使用,因为它们依赖于操作通过其方法返回的对象,而这些对象无法在反序列化过程中保留下来。

Windows 管理工具WMI)命令不包括在 PowerShell 7 中,因为微软自 PowerShell 3.0 起就开始尝试弃用它们,更倾向于让用户使用更轻量级的 CIM 命令。然而,WMI 依然很受欢迎,许多人仍然使用 WMI 而非 CIM。我们将在下一节讨论这些 CIM 命令及其使用方法,这是一个重要话题。

使用 CIM 和 WMI 管理机器

CIM 和 WMI 是用于管理本地和远程机器的相关技术。在本节中,我们将介绍它们的基础知识,并讨论它们的相似性和差异。

CIM 和 WMI 简介

CIM 和 WMI 基于 Web-Based Enterprise ManagementWBEM)标准,该标准由 分布式管理工作组DMTF)于 1996 年提出。WMI 是微软基于 WBEM 的工具集实现,发布于 1998 年,而 CIM 是 DMTF 提出的开放标准,定义了环境中实体的表示和相互关系。该标准于 1999 年发布。在 Windows 环境中,CIM 使用 WMI 的元素,但使用不同的协议集来访问它们;WMI 使用 分布式公共对象模型DCOM)协议,这是专有的,而 CIM 使用 Web 服务管理WS-MAN)协议通过 HTTP 进行访问,我们在 第十二章 中讨论了这个问题,保护 PowerShell

这两种技术都链接到一个公共存储库——WMI 存储库,我们稍后将查看它。这个存储库包含有关我们可能想要管理的对象类型的信息,例如打印机、客户端、网络适配器等,以及这些对象的实例。

PowerShell CIM 命令允许我们通过 WS-MAN 连接到本地和远程机器,连接到我们用于远程会话的 Windows 远程管理WinRM)端点。我们可以通过临时连接或通过 CIM 会话进行连接,类似于远程连接。

WMI 仅支持通过 DCOM 进行临时连接。这意味着远程机器上不需要启用 WinRM。

为什么 CIM 比 WMI 更好

CIM 稍微更新一些,但关键在于它使用了开放连接协议——WS-MAN。这要求机器和网络中的任何设备都必须提供一对静态网络端口。DCOM 使用 远程过程调用RPC)协议,依赖于临时 TCP/IP 端口;由于这个差异,WS-MAN 更容易在网络上运行,而 DCOM 则较为困难。还有许多其他原因说明 WS-MAN 比 DCOM 更好,但根据我的经验,最大的障碍是 DCOM 对临时端口的依赖。

微软自 2012 年 PowerShell 3.0 以来,一直在努力弃用 WMI cmdlet,并说服人们改用 CIM cmdlet,因为 CIM 使用更轻的协议,网络占用更少;RPC 有许多缺点,而我也花了大量时间在职业生涯中识别并修复由于 RPC 导致的问题。然而,仍然有许多人使用 WMI cmdlet,互联网上也有许多流行的脚本使用它们。

WMI cmdlet 在 PowerShell 7 中不可用,但我们会在互联网上看到许多涉及Invoke-Command的变通方法,使我们能够通过兼容模式使用它们。

使用 CIM 和 WMI 的命令

WMI 仓库庞大且令人困惑,正如我们将看到的那样。因此,尽可能使用专用 cmdlet 而不是 CIM 或 WMI cmdlet 会更好。这看起来像什么?例如,我们可能使用Get-CimInstance Win32_Printer查询机器上的打印机,但使用Get-Printer cmdlet 会更快速且直观,如下所示:

图 13.11 – 查找打印机

图 13.11 – 查找打印机

这两个命令都在查找我本地机器上的 HP 打印机。它们都使用 CIM 查询 WMI 仓库。可以说,第二个命令更容易运行和记住,通常也更快。Get-Printer支持-ComputerName参数用于远程机器,甚至支持使用 CIM 会话。在深入研究几个小时,尝试弄清楚如何使用 CIM cmdlet 做某事之前,先检查一下是否已经有现成的方便的 cmdlet 完成了我们想要的操作。当然,Get-Printer是一个 Windows PowerShell cmdlet,因此我们需要在 Windows 客户端上运行,并使用兼容模式。

仓库

WMI 仓库是一个层次化的数据库——一种树形结构。在最顶层是命名空间——本质上是容器。命名空间包含类。类表示一种对象类型,例如打印机,并包含该类型对象的实例。类的每个实例具有与其他实例相同的属性和方法——尽管它们可能并不全都被填充或启用。还有一个名为__NAMESPACE的类(带有两个下划线_),它包含命名空间,因此一个命名空间也可能包含其他命名空间。

根命名空间被称为毫无创意的root。我们可以通过以下命令查看其内容:

Get-CimClass -Namespace 'Root' | Select-Object -First 10

我们可以通过以下方式查看__NAMESPACE类的实例:

Get-CimInstance -Namespace 'Root' -ClassName __NAMESPACE

每台机器上的命名空间和类的内容可能有所不同,因为硬件和软件在安装时会创建它们自己的命名空间和类。然而,大多数人的重要命名空间是root/CIMV2,如下图所示。

图 13.12 – 查找 CIMV2 命名空间

图 13.12 – 查找 CIMV2 命名空间

在第一个命令中,我列出了root命名空间中返回的前 10 个类。我的客户端中root命名空间下有 75 个类,因此我只列出了前 10 个——这个数字在你的系统中可能不同。在第二个命令中,我列出了我计算机上__NAMESPACE类的实例——同样,这里数量较多,你的数量可能不同。CIMV2命名空间已被突出显示。

CIMV2命名空间包含 Microsoft Windows 类。我们可以使用Get-CimClass列出它们,但我的计算机上该命名空间中有超过 1200 个类;大多数类名以MSFT_*Win32_*开头。由于类的数量庞大,我们可以看到在这个存储库中查找内容可能会是一个挑战。微软在线上对root/CIMV2命名空间的内容进行了合理的文档记录,但其他命名空间的内容通常文档化得不太好,了解这些内容的作用及如何使用它们可能需要一些额外的努力。

图 13.11中,我们使用了Get-CimInstance Win32_Printer命令,而没有指定命名空间——这是因为 PowerShell 将CIMV2命名空间设置为默认命名空间,因此我们无需在每个命令中添加-Namespace 'root/CIMV2'参数。

查询数据

我们使用 CIM 和 WMI 来完成两个主要任务——获取信息和更改内容,通常是同时在大量机器上执行这些任务。让我们从了解如何查找信息开始。我们已经看过两个可以使用的 CIM 命令;Get-CimClassGet-CimInstance

Get-CimClass

这个命令用于获取指定命名空间中的 CIM 类列表,如果未指定命名空间,则获取默认命名空间中的类。这个命令有几个参数:

  • -ClassName:此参数允许我们指定一个类,或者在查找特定类时提供一个部分类名;例如,Get-CimClass -ClassName *disk*将获取CIMV2中所有名称中包含disk的类。

  • -Namespace:此参数允许我们指定一个不同于root/CIMV2的命名空间。

  • -ComputerName:此参数允许我们指定除本地计算机外的其他计算机。我们可以使用 FQDN、NetBIOS 名称或 IP 地址。

  • -MethodName:此参数允许我们搜索具有特定方法的类。例如,Get-CimClass -MethodName 'term*'将返回Win32_Process类,该类具有一个Terminate方法。猜猜这个方法的作用是什么,答案显而易见。

  • -PropertyName:与-MethodName参数类似,允许我们搜索具有特定属性的类。

  • -QualifierName:限定符类似于应用于类的标签。没有标准的列表(有点像标签),但它们有时非常有用。例如,我们可以使用Get-CimClass -QualifierName 'deprecated'来检索正在淘汰的类列表,这些类可能不应在脚本中使用。

  • -Amended:此参数获取修订后的信息——通常是根据机器所在的区域设置变化的信息。可本地化的信息通常以数字形式呈现,并需要查找才能翻译为本地语言——此参数会执行该操作。

  • -OperationTimeoutSec:如果我们对远程计算机执行命令,可能会出现机器无响应或未开机的情况。此参数允许我们指定一个超时时间,超出默认的本地机器网络超时(默认是 3 分钟)。如果我们查询 1000 台机器,其中 10%未开机或未连接网络,使用此参数可以在不到 5 小时内完成命令。

  • -CimSession:此参数允许我们在已存在的 CIM 会话中运行命令。稍后我们会详细介绍 CIM 会话。

接下来,让我们来看一下Get-CimInstance

Get-CimInstance

该命令用于获取给定 CIM 类的实例——同样,默认命名空间是 root/CIMV2\。它有许多与Get-CimClass相同的参数,但也有一些不同之处。让我们来看一下:

  • -CimSession:与Get-CimClass相同。

  • -ClassName:与Get-CimClass相同。

  • -ComputerName:与Get-CimClass相同。

  • -Filter:允许我们指定过滤器字符串,以仅获取某些实例。例如,Get-CimInstance -Classname Win32_Printer -Filter "Name like 'HP%'"会查找我机器上的 HP 打印机。请注意,我们必须使用Windows 查询语言WQL)或Cassandra 查询语言CQL)。

  • -KeyOnly:仅返回实例的关键属性,而不是所有属性。

  • -Namespace:与Get-CimClass相同。

  • -OperationTimeoutSec:与Get-CimClass相同。

  • -Property:与Get-CimClass不同,这个参数不会搜索具有指定属性的实例;记住,类的所有实例都有相同的属性和方法。注意,它是-Property,而不是-PropertyName。我们可以使用此参数来检索指定属性的列表,而不是实例的所有属性。

  • -Query:允许我们指定一个用 WQL 或 CQL 编写的查询字符串。例如,Get-CimInstance -Query "SELECT * from Win32_Printer WHERE name LIKE 'HP%'"将再次查找我的 HP 打印机。

  • -QueryDialect:指定查询所使用的方言。默认是 WQL,因此我们通常只在提供 CQL 编写的查询时使用此参数。

  • -Shallow:默认情况下,Get-CimInstance返回类的所有实例以及任何子类的实例。此参数可防止返回子类的任何结果。

我们可以看到,这两个命令都具有-ComputerName参数,用于远程计算机的操作。每次使用此参数时,我们都会创建并删除一个临时的 CIM 会话。如果我们有一堆命令要在远程机器上运行,那么我们可以像创建持久的 PowerShell 远程会话一样创建一个持久的 CIM 会话。让我们看看如何做到这一点。

CIM 会话

有四个 cmdlet 用于操作 CIM 会话——New-CimSessionGet-CimSessionRemove-CimSessionNew-CimSessionOption。它们的行为与 PowerShell 远程 cmdlet 非常相似,Get-CimSessionRemove-CimSession 完全按预期执行。请注意,CIM 会话在使用 Kerberos(默认身份验证方案)的 Active Directory 域环境中效果最佳。如果没有这个环境,远程计算机将需要添加到 WinRM 的 TrustedHosts 异常列表中。让我们快速看一下 New-CimSession 如何工作。

该 cmdlet 有以下参数:

  • -Authentication:我们希望使用的身份验证方案。这将取决于我们所在的环境。在域环境中,Kerberos 是最佳选择。

  • -CertificateThumbprint:如果我们在使用基于证书的身份验证方案,则需要提供证书的详细信息。

  • -ComputerName:我们希望操作的远程计算机名称。如果未指定,则会创建一个通过 DCOM 连接到本地计算机的 CIM 会话。对于远程计算机,使用的是 WSMan。

  • -Credential:同样,根据我们使用的身份验证方案,可能需要提供一个凭据对象。

  • -Name:我们可以为会话分配一个友好的名称,以便更容易操作。

  • -OperationTimeoutSec:与 Get-CimClass 参数相同。

  • -Port:如果我们的网络受到限制,可以指定一个特定的 TCP 端口,但不推荐这样做。

  • -SessionOption:这允许我们通过使用 New-CimSessionOption 创建一个 SessionOption 对象来设置会话的高级选项。

  • -SkipTestConnection:此参数会停止 cmdlet 在创建会话前测试连接性。

有一些高级选项可以控制代理服务器的使用以及如何处理不同的身份验证方案。所有这些选项都可以通过 New-CimSessionOption cmdlet 设置,但我们在此不作深入讨论。

让我们看看以下图示中这些如何运作:

图 13.13 – 使用 CIM 会话

图 13.13 – 使用 CIM 会话

在第一个命令中,我在本地计算机上创建了一个新的 CIM 会话,并为其指定了一个友好的名称 localsession。在第二个命令中,我使用友好的名称获取我的会话详情;请注意,尽管它是本地计算机,但这里使用的是 WSMan 而不是 DCOM。这是因为我通过 -ComputerName 参数指定了本地计算机,因此 cmdlet 自动使用 WSMan 会话。很酷吧?在最后一个命令中,我删除了该会话。

所以,这就是我们可以用来获取类和实例信息的 cmdlet。在下一节中,我们将看看如何操作实例的属性并使用它们的方法。

修改设置

我们也可以使用 CIM cmdlets 操作 WMI 仓库中的对象;我们可以更改它们的属性(有时)或使用它们的方法。让我们从更改属性开始。

更改属性

让我们运行以下命令:

Get-CimInstance Win32_Printer | Where-Object {$_.Name -like 'HP*'} | gm

接下来,我们将看到安装在本地客户端上的 HP 打印机的所有方法和属性。如果我们没有安装 HP 打印机,那么会得到一个错误,但我们可以更改搜索字符串以匹配可能拥有的打印机类型;例如,'can*'将找到佳能打印机。注意,属性后面跟着一个看起来像{get;set;}的字符串。如果它只是{get;},那么它是只读的。让我们看看修改可写属性的效果:

图 13.14 – 使用 Set-CimInstance 更改属性

图 13.14 – 使用 Set-CimInstance 更改属性

在第一个命令中,我创建了一个名为$printer的变量,并将我的 HP 打印机放入其中。在第二个命令中,我调用了该变量,只是为了检查里面的内容是否正确。它是正确的。

在第三个命令中,我正在检查$printer变量的Comment属性的当前值——我们可以看到它是空的。在第四个命令中,我将变量的属性更改为I Love PowerShell

第五个命令是关键部分——我正在获取变量的内容,现在使用Set-CimInstance cmdlet 将它们写回到 WMI 仓库中的实例。该变量包含一个 WMI 对象,因此我使用了 cmdlet 的-InputObject参数。它是否成功了?我们来看一下。

图 13.15 – 我的打印机有意见

图 13.15 – 我的打印机有意见

当然它成功了。呼~ 如果我们查看打印机对象的属性列表,我们会发现大多数属性是不可写的。例如,有一个Default属性,但它是只读的,我们无法使用Set-CimInstance设置它。在接下来的章节中,我们将查看一个可能实现这一功能的 cmdlet,Invoke-CimMethod

发现方法

我们知道对象有属性和方法,这包括 WMI 仓库中的对象。我们已经查看了属性,了解了如何获取它们以及如何设置它们。现在,我们将看看这些对象可能拥有的方法。在这里,我们将看到为什么有时候与 WMI 仓库打交道会如此麻烦。

我们面临的第一个问题是知道我们需要的类的名称,我们在前面的查询数据部分已经看到了如何做到这一点。一旦知道了类的名称,我们需要了解可用的方法。与Get-WmiObject cmdlet 不同,Get-CimClass cmdlet 默认不会发现所有方法,因此我们必须使用类似下面的命令:

Get-CimClass -Class Win32_Printer | Select-Object -ExpandProperty CimClassMethods

这将暴露完整的方法集。一旦我们找到了方法,当然,我们需要知道如何使用它。由于 WMI 不是 PowerShell 的一部分,PowerShell 文档不会帮助我们。大多数情况下,我们可以从微软官网找到所需的信息,链接如下:

learn.microsoft.com/en-us/windows/win32/cimwin32prov/cimwin32-wmi-providers

然而,这需要一点搜索,且经常会看到示例是用 Visual Basic 而不是 PowerShell 编写的。我们来看看如何实现它。

在接下来的部分,我们将看到如何启动和停止进程,所以让我们查看 Win32_Process 文档,链接如下:

learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-process

如果我们往下滚动,可以看到一个 Methods(方法)部分。最可能的方法是 Create。如果点击链接,我们可以看到 Create 方法有一个 CommandLine 参数。我们来看看它是如何工作的。

调用方法

现在我们有了方法和可能的参数,试试看吧。首先,我们需要使用 Invoke-CimMethod cmdlet。这个 cmdlet 有以下参数,其中许多参数的用法我们应该已经熟悉:

  • -ClassName:与 Get-CimClass 相同。

  • -CimClass:我们可以使用此参数来指定 WMI 类,而不是使用 -ClassName 参数。我们需要通过变量传递一个 WMI 类对象,而不是字符串。

  • -CimSession:与 Get-CimClass 相同。

  • -Namespace:与 Get-CimClass 相同。

  • -ComputerName:与 Get-CimClass 相同。

  • -MethodName:这是一个必填参数,接受一个字符串;方法的名称。

  • -Arguments:在这里,我们指定传递给方法的参数,作为一个 iDictionary 哈希表,像这样:-Arguments @{ ParameterName = '``value'}

  • -OperationTimeoutSeconds:与 Get-CimClass 相同。

  • -Query-QueryDialect:与 Get-CimInstance 相同。

让我们启动记事本程序;如果我们在 Windows 11 上工作,可能需要关闭任何正在运行的记事本实例。首先,我们将使用 Invoke-CimMethod cmdlet。我们需要类名 Win32_Process、方法名 Create() 和参数。我们将使用 CommandLine = 'notepad.exe',因为 notepad.exePATH 环境变量中,所以不需要指定位置:

Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine = 'notepad.exe'}'}

它看起来像这样:

Figure 13.16 – Invoking a method

图 13.16 – 调用方法

我们可以看到返回值(0)表示成功,并且我们得到了新创建的记事本进程的 ProcessId。请注意,这个命令可能会在后台创建进程;我们不会突然看到记事本窗口出现,但我们会在任务栏看到它。

活动

  1. 如何使用 PowerShell 关闭我们刚刚启动的 notepad.exe 进程?

  2. 如何使用 PowerShell 设置打印机为默认打印机?提示:我们不能使用 Windows PowerShell 中的 Set-Printer cmdlet。

这就是我们在这里要讲解的全部内容。只需记住,属性和方法的文档不完整,而且经常会有一个专门为我们的目的编写的 PowerShell cmdlet,通常作为 PowerShell 7 或 Windows PowerShell 的一部分。让我们总结一下本章内容。

总结

本章我们开始了书的最后部分,讲解如何在不同环境中使用 PowerShell。我们从 Windows 环境开始,可以说这是 PowerShell 最有效的环境。然而,我们也看到 PowerShell 7 和 Windows PowerShell 之间有一些重要的区别。

我们看到 PowerShell 7 不一定能使用为 Windows PowerShell 编写的模块,且我们探讨了三种应对方法:为 PowerShell 7 寻找等效模块、无论如何加载模块,或者使用兼容模式。

尽管大多数时候兼容模式是透明使用的,但我们了解了它的工作原理,以及一些我们可能希望限制其使用的情况。不过,它非常有用,我们看到它一直在不断改进。

我们继续探讨了与 PowerShell 7 不兼容的某些功能,以及为什么会这样。通常,这是因为一些旧模块不再值得重写以适配 PowerShell 7。

接下来我们看了在 Windows 机器上使用 PowerShell 7 的最有用方法之一:使用 CIM 来操作 WMI 存储库。我们花了一些时间了解了 WMI 存储库,然后继续讨论如何获取那里的对象属性,以便了解我们本地机器和远程机器上的环境。

最后的部分讲解了如何操作 WMI 存储库中对象的属性,然后介绍了如何调用这些对象的方法来影响更广泛的计算环境。

本章就到这里。在下一章中,我们将讨论如何在 Linux 和 macOS 机器上使用 PowerShell 7。

练习

  1. 哪些类型的模块包含兼容性信息?

  2. 为什么一个模块可能没有兼容性信息?

  3. 我们运行了 Get-PSSession,可以看到本地主机上运行着一个名为 WinPSCompatSession 的远程会话。它正在运行哪个版本的 PowerShell?

  4. 我们从在 WinPSCompatSession 中运行的命令中获得的是什么类型的对象?

  5. 我们的本地管理员禁用了我们机器上的隐式 Windows 兼容性。那么我们如何在兼容模式下仍然导入模块呢?

  6. 哪个 CIM 类包含命名空间的实例?

  7. Get-CimInstance cmdlet 中的 -OperationTimeoutSecs 参数的作用是什么?

  8. 我们会使用哪个 cmdlet 来更改 WMI 对象的属性?

  9. 我们如何将方法参数传递给 Invoke-CimMethod,它们需要什么格式?

进一步阅读

第十四章:Linux 和 macOS 的 PowerShell 7

通常说,Linux 是最受欢迎的服务器操作系统,虽然这是真的,但这并没有说明几乎每台服务器都有几种不同类型的 Linux。与 Windows 和 macOS 的紧密工程化和维护不同,这些系统的公司致力于确保每个人都在运行同质、安全、并且经常强制更新的代码,而 Linux 世界则更加自由,拥有多种免费、开源且常常维护不足的变种,这些变种可能在服务器上存活多年甚至几十年。在本章中,我们将看看如何在一些常见的 Linux 版本上使用 PowerShell;在我们的例子中,使用 Ubuntu 和 CentOS,它们是免费的 Red Hat Enterprise Linux (RHEL) 版本。

我们将首先了解如何访问一台 Linux 机器进行练习;如果没有这个,本章内容会非常无聊。一旦我们有了机器,我们将探索三种不同的安装 PowerShell 的方法,使用包管理器和直接下载。

接下来,我们将安装 VS Code,并在 CentOS 7 上设置它,然后看看在 Linux 上运行 PowerShell 和在 Windows 上运行之间的主要区别。

我们将了解在 Linux 上使用 PowerShell 的最常见方法之一:通过 安全外壳协议 (SSH) 进行远程会话。这一点很重要,而且无疑是我看到人们登录 Linux 机器时最常用的方法。

最后,我们将快速了解如何使用免费开源包管理器 Homebrew 在 macOS 上轻松安装 PowerShell 和 VS Code。

本章将涵盖的主要主题如下:

  • 安装 PowerShell 7

  • 安装 VS Code

  • 在 Linux 上运行 PowerShell

  • 使用 OpenSSH 远程连接

  • macOS 上的 PowerShell

技术要求

除非我们已经有了运行某种 Linux 的客户端,否则我们需要一台 Linux 设备来工作。获取一台设备有两种方法,一种简单,另一种稍微困难一些。我在我的机器上做了这两种方法,获取了本章的截图,提供了一个没有图形界面的 Ubuntu 服务器和一个 CentOS 桌面客户端。

Ubuntu 服务器非常容易安装:

  1. 转到 控制面板 | 程序和功能 | 启用或关闭 Windows 功能,确保选中 Windows Subsystem for Linux(Windows 子系统 Linux)的复选框,像这样:

图 14.1 – 启用 Windows 子系统 Linux

图 14.1 – 启用 Windows 子系统 Linux

Windows Subsystem for Linux 在红色框中显示。

  1. 一旦启用了 Windows Subsystem for LinuxWSL),重启计算机,访问 Microsoft Store (apps.microsoft.com),并选择一个 Linux 应用。我使用的是最新版本的 Ubuntu – 22.04:

图 14.2 – 在 Microsoft Store 中的 Ubuntu 应用

图 14.2 – 在 Microsoft Store 中的 Ubuntu 应用

我在这里做的就是在搜索框中搜索 Ubuntu,并选择了最新的应用程序。

  1. 点击 安装,会弹出一个 Microsoft Store 窗口;点击 获取,下载应用程序后,按钮会变成 打开

图 14.3 – Microsoft Store 应用

图 14.3 – Microsoft Store 应用

  1. 点击正在安装,这可能需要几分钟...。几分钟后,我们会被要求创建用户名,然后输入两次密码,完成后就可以了。现在,我们已经在 Windows 上的 WSL 中运行了一个 Ubuntu 服务器:

图 14.4 – Ubuntu 在 WSL 上运行

图 14.4 – Ubuntu 在 WSL 上运行

在第一行,我创建了一个新的用户名 nickp(富有创意吧),在第二行和第三行,我输入了新的密码。Ubuntu 在输入时隐藏密码,但与 Windows 不同,它不会通过显示点或星号来隐藏,而是完全不显示……什么都不显示。

就这样——我们的 Ubuntu 环境已经准备好安装 PowerShell。

注意

这些说明适用于 Windows 10 Pro 版本 19045。其他平台,如 Windows 11 或 Windows Server,可能有不同的安装 WSL 的说明。另外,这是 WSL,后来的操作系统还有一个版本叫做 WSL2。

我还在 Oracle VirtualBox 中安装了 CentOS;从 Microsoft Store 安装也完全可以,但我需要一台单独的机器,并且希望它有图形用户界面(GUI),我们可以在本章中使用。

安装 VirtualBox 的说明可以在这里找到:

www.virtualbox.org/manual/UserManual.html#installation

这非常简单。

一旦 VirtualBox 安装完成,我们需要从这里下载 CentOS 镜像:

isoredirect.centos.org/centos/7/isos

我们还需要创建一个虚拟机来安装它。这里有很好的说明:www.linuxfordevices.com/tutorials/centos/centos-on-virtualbox

我安装了带有 GNOME 桌面图形界面的 CentOS 7,这样我们就可以看看如何在较旧的操作系统上安装 PowerShell;我发现我使用的许多 Linux 系统都已经有些年头了。有些地方我仍在使用 Ubuntu 12.04。精确穿山甲(Precise Pangolin)是一个很棒的操作系统名称,但实际上,我们都应该使用 Jammy Jellyfish——至少为了安全起见。

最后,为了跟进 macOS 部分,我们需要一台 Mac。我是个极度节省的人,所以我借了朋友 Paul 的一台 MacBook,运行的是 OS 13 Ventura,这个版本相当新。谢谢你,Paul。

现在我们已经介绍了我们需要的环境类型以及如何安装它,让我们来看看如何安装 PowerShell。

安装 PowerShell 7

有些许反常的是,我发现将 PowerShell 安装在 Linux 上可能比在 Windows 上安装更容易,尽管 PowerShell 是为 Windows 开发的。微软发布了适用于支持版本 Linux 的好脚本;不过请注意,支持的 Linux 版本非常有限:RHEL、Ubuntu、Alpine 和 Debian。

这并不意味着我们不能在其他版本上安装 PowerShell,只是微软不提供任何支持。微软只支持特定的较新版本。而且,由于操作系统更新的速度较快,确保我们的 PowerShell 和 Linux 版本处于支持矩阵中是值得的;否则,我们可能会遇到意想不到的结果,正如我们在尝试在 CentOS 上安装时会看到的。

让我们从 Ubuntu 开始,我在我的 Windows 10 客户端中运行着 Ubuntu 的 WSL。

在 Ubuntu 22.04 上安装 PowerShell

在本节中,我们将安装 PowerShell 7.4 版本在 Ubuntu 22.04 上。Ubuntu 在我的 Windows 10 客户端中通过 WSL 运行,但在虚拟机或物理机器上同样适用。这里有详细的替代安装说明:learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu

让我们试试吧。我们将运行接下来的命令:

  1. 在第一行,我们正在从默认的软件库更新本地软件包至最新版本。在 Linux 上安装任何软件之前,这是一个好习惯。我们使用sudo命令告诉 Linux 我们希望使用管理员权限或根权限来执行该命令。显然,要做到这一点,我们需要拥有这些权限,并且在运行命令之前,系统会要求我们输入账户密码以确认:

    sudo apt-get update
    
  2. 在第二行,我们正在安装一些前置的软件包:

    sudo apt-get install -y wget apt-transport-https software-properties-common
    
  3. 在第三行,我们正在获取操作系统的确切版本,然后在第四行使用它:

    wget to download the correct repository package for our operating system. This is the Linux package repository for Microsoft products, abbreviated PMC (short for packages.microsoft.com):
    
    

    dpkg,Ubuntu 的软件包管理系统:

    sudo dpkg -i packages-microsoft-prod.deb
    
    
    
  4. 在第六行,我们正在删除密钥文件以确保安全:

    sudo apt-get update again to make sure we’ve got the latest list of packages for the new repository:
    
    

    sudo apt-get update

    
    
  5. 最后,在第八行,我们正在安装 PowerShell:

    sudo apt-get install -y powershell
    

Linux 输出非常详细,我们在输入上述命令时会看到 60 到 70 行输出,但整个过程非常直接,且运行良好。

要启动 PowerShell,我们只需输入pwsh,即可进入 PowerShell 提示符,如下图所示:

图 14.5 – 在 Ubuntu 上启动 PowerShell

图 14.5 – 在 Ubuntu 上启动 PowerShell

在前面的截图中,我用 pwsh 启动了 PowerShell。一旦 PowerShell 启动,我调用了 $PSVersionTable 自动变量来获取有关环境的一些信息,包括 PowerShell 的版本、版本号以及运行的操作系统。我通过输入 exit 关闭了 PowerShell。如果我想让 PowerShell 在后台运行,我可以输入 bash,然后切换到 Bash 提示符。正如我们所看到的,在 Linux 上,我们获得了与 Windows 相同的颜色编码,这有助于跨平台保持一致的体验。

了解 Linux 中也支持制表符补全是很好的。Linux 中最常让我困惑的事情之一就是大小写问题;Windows 上的 PowerShell 已经让我变得懒惰。因此,能够使用制表符来完成路径非常有用。请注意,PowerShell 在 Linux 上对大小写不敏感;get-processGet-Process 作用一样。

现在让我们来看一下如何在 CentOS 上安装 PowerShell。

在 CentOS 8 和 9 上安装 PowerShell

在 CentOS 的最新版本(CentOS 8 或 CentOS 9)上安装 PowerShell 的过程与在 Ubuntu 上安装非常相似,只是使用了 RHEL 8 的 yum 包管理器,或使用 RHEL 9 的 dnf 包管理器;如果说有区别的话,它甚至更简单。不过需要注意的是,微软官方只在 RHEL 上支持 PowerShell,而不是 CentOS 或 Fedora。我们接下来要运行的是以下命令:

  1. 在第一行,我们使用 curl 应用程序而不是 wget 来获取 PCM:

    rpm, the package manager:
    
    

    sudo rpm -i packages-microsoft-prod.rpm

    
    
  2. 在第三行,我们再次移除密钥文件:

    rm packages-microsoft-prod.rpm
    
  3. 在第四行,我们正在更新软件包列表,现在我们已经注册了新的仓库:

    -y switch at the end specifies that we are answering yes to all questions. We can start PowerShell using pwsh, again, just like on Ubuntu:
    
    

    sudo dnf install powershell -y

    
    

故障排除提示

如果这不起作用,很有可能是代理设置的问题。在 CentOS 中,至少有三个不同的地方可能包含代理设置。对于 yum,它在 /etc/yum.conf。对于 dnf,它在 /etc/dnf/dnf.conf

那么,如果我们想在旧版本上使用 PowerShell 怎么办?我们必须安装一个特定的旧版本 PowerShell。

对于 RHEL 7(因此,也包括 CentOS 7),PowerShell 的最后一个可用版本是 7.2. 7.3 的早期版本可以使用,但后续版本不行,因为它们依赖于与 CentOS 7 不兼容的 Linux 库。如果我们尝试安装 PowerShell 的新版本,我们会看到类似这样的错误信息:

GLIBCXX_3.4.21 not found (required by pwsh)

这里的答案是安装 PowerShell 7.2. 让我们看看如何做到这一点。

在 CentOS 7 上安装 PowerShell

在 CentOS 上通过直接下载安装非常简单;只需要一行命令:

sudo yum install https://github.com/PowerShell/PowerShell/releases/download/v7.2.17/powershell-7.2.17-1.rh.x86_64.rpm

我们在这里所做的就是使用yum,CentOS 7 上的包管理器,从 URL 获取并安装一个包。这里的技巧是知道你需要下载的包的 URL。所有这些包都由 Microsoft 在 GitHub 上维护,网址是:github.com/PowerShell/PowerShell/releases/

要下载版本,点击软件包名称中的链接(在我们的例子中是powershell-7.2.17-1.rh.x86_64.rpm),并复制超链接:github.com/PowerShell/PowerShell/releases/download/v7.2.17/powershell-7.2.17-1.rh.x86_64.rpm

一旦我们有了它,就可以像这样将其传递给sudo yum install

图 14.6 - 在 CentOS 7 上使用直接下载安装 PowerShell

图 14.6 - 在 CentOS 7 上使用直接下载安装 PowerShell

在第一行中,我运行了之前展示过的直接下载命令。其余的屏幕输出是操作系统的聊天信息,告诉我们它正在做什么。在我们最终运行 PowerShell 之前,还有很多行这样的输出。这是安装 PowerShell 的一个非常简单的方法;缺点是我们没有注册 Microsoft 的仓库。

这基本上就是我们在 Linux 上安装 PowerShell 时要涵盖的内容;我们已经涵盖了两个主要 Linux 发行版的代表,Ubuntu 和 CentOS,并且我们已经了解了如何安装不同版本的 PowerShell。现在让我们来看看如何在 Linux 上安装 VS Code。我们将使用我的 CentOS 系统,因为它有图形界面。

安装 VS Code

在 Linux 上安装 VS Code 是简单的。在最近的 Ubuntu 机器上,我们可以像这样使用snapd,一个 Ubuntu 的包管理系统:

sudo snap install --classic code

就这样。在 RHEL 和 CentOS 机器上,我们可能需要先启用snapd,然后才能使用它来安装 VS Code。

我们将运行接下来的命令:

  1. 在第一行中,我们正在安装snapd包:

    snap uses:
    
    

    /snap 到 /var/lib/snapd/snap:

    sudo ln -s /var/lib/snapd/snap /snap
    
    
    
  2. 最后,我们需要注销并重新登录,或者重启计算机,以确保所有内容都已更新。

现在,我们准备像之前一样使用snap来安装 VS Code。使用snap的最大优势是它会在后台保持 VS Code 的更新。

如果我们无法使用snap,也可以手动安装 VS Code。在 CentOS 7 上,我们可以这样做:

sudo rpm --import  https://packages.microsoft.com/keys/microsoft.asc

这将使用rpm,CentOS 7 的包管理器,注册 Microsoft 的 GPG 加密密钥。接下来,键入以下内容:

sudo nano /etc/yum.repos.d/vscode.repo

这将创建一个名为vscode.repo的空文本文件。我们需要向该文件中添加一些行并保存,因此请键入以下内容:

[code]
name=Visual Studio Code
baseurl=https://packages.microsoft.com/yumrepos/vscode
enabled=1
gpgcheck=1
gpgkey=https://packages.microsoft.com/keys/microsoft.asc

CtrlX退出,并在提示保存文件时按y

vscode.repo文件应该是这样的:

图 14.7 - 使用 nano 查看 vscode.repo 文件

图 14.7 - 使用 nano 查看 vscode.repo 文件

当我们创建文件时,它是空的,我们必须输入代码并保存退出。

最后,安装 VS Code,请输入以下命令:

sudo yum install code

就这样。我们可以通过在终端提示符下输入code来启动 VS Code。VS Code 在 Linux 和 Windows 上的工作方式完全相同。请参阅 第五章配置 VS Code 用于 PowerShell 部分,PowerShell 控制流 – 条件语句 和循环

现在我们已经安装好了一切,让我们看看如何在 Linux 上使用 PowerShell。

在 Linux 上运行 PowerShell

通常,PowerShell 在 Linux 上的工作方式与 Windows 上完全相同,但显然,两种操作系统之间存在一些差异,我们需要注意并理解 PowerShell 如何处理这些差异。

大小写敏感是显而易见的区别;虽然在 Linux 上 get-contentGet-Content 是等效的,但如果文件名是 MyFile.txt,则 get-content ./myfile.txt 不会生效;请看下面的截图:

图 14.8 – 大小写的重要性

图 14.8 – 大小写的重要性

如你所见,如果路径或文件名的大小写不正确,PowerShell 将无法找到该文件。我找到的最佳解决方法是尽可能使用 Tab 完成,因为 Tab 完成忽略大小写,所以输入 myfi 并按下 Tab 键会找到名为 MyFile.txt 的文件。

文件系统也不同。Linux 不使用字母区分驱动器,而是使用正斜杠 (/),而不是反斜杠 (\)。PowerShell 识别两者作为文件路径分隔符,因此 Get-Content ./MyFile.txtGet-Content .\MyFile.txt 没有功能上的区别:

图 14.9 – 多功能路径分隔符

图 14.9 – 多功能路径分隔符

如你所见,无论我们选择哪种文件路径分隔符,都能获取到文件的内容。这使得编写跨平台脚本变得更容易。

我已经表明了我对别名的看法,看来我的愤怒信件写作有了回报,因为 PowerShell 7 在 Linux 上不再包含像 ls 这样的常用别名,尽管它们在 Windows 上运行 PowerShell 7 时仍然存在。相反,PowerShell 现在调用 Bash 命令,像这样:

图 14.10 – PowerShell 7 在 Linux 上的别名减少

图 14.10 – PowerShell 7 在 Linux 上的别名减少

在前面的截图中,我们可以看到在 Bash 和 PowerShell 中运行 ls 的区别。输出是相同的,但我们在 Bash 中运行时没有颜色编码。相比之下,当我们运行 Get-ChildItem 时,输出完全不同。与 Windows 上的行为对比,ls 在 Windows 上是 Get-ChildItem 的别名。其他不再有别名的 Linux 命令包括 cpmvrmcatmanmountps

在 Linux 上以管理员身份运行也有所不同。熟悉 Linux 的人通常习惯使用 sudo 前缀来以 root 用户身份运行命令。这在 PowerShell 中不起作用。相反,我们必须以 sudo 启动 PowerShell,像这样:

sudo pwsh

这将为我们提供一个具有 root 权限的新 PowerShell 会话。

鉴于在 Linux(以及 macOS 和 ARM)上运行 PowerShell 的一个主要吸引力是我们可以开始编写跨平台脚本,那么如何知道我们的脚本在哪个平台上运行呢?很简单——我们测试自动变量。有三个自动变量,分别是 $IsWindows$IsLinux$IsMacOS,根据操作系统的不同,这些变量会返回 truefalse。我们可以使用这些变量在脚本中编写 if 语句,以根据环境改变行为。

活动 —— 编写跨平台脚本

根据我们之前学到的内容,编写一个跨平台脚本,能够在 Windows 和 Linux 上运行,并返回当前 CPU 使用率最高的五个进程。将进程按降序输出到一个文本文件中,文件名应包含运行该脚本的计算机名称。

我们可以在 Windows 中使用 $env:computername 获取计算机名称,而在 Linux 中可以通过输入 hostname 来获取。

当然,大多数时候,我们不会直接在 Linux 机器上运行命令和脚本;大多数时候,我们会想要远程连接到它。在接下来的部分中,我们将看看远程连接到 Linux 机器的推荐方法。

使用 OpenSSH 进行远程连接

第十二章《确保 PowerShell 安全》一节中,我们看到远程连接是一种强大的方式,可以与机器建立连接并进行控制。当我们在该章节中探讨远程连接时,我们研究了在其他 Windows 机器上通过Windows 远程管理WinRM)协议进行远程连接。我们还提到过可以使用 SSH 来建立远程会话。由于 Linux 不支持 WinRM 协议,因此我们必须使用 SSH 来远程管理它。

OpenSSH 是一个开源的 SSH 工具集,在 Linux 和其他 Unix 系统上几乎是无处不在的。从 2018 年起,它也可以在 Windows 上使用,使得管理异构环境变得更加容易。设置它可能有些复杂,但一旦配置完成,它会让远程连接变得非常简单。让我们来看看。

检查 PowerShell 是否支持 OpenSSH

首先要检查的是,我们的 PowerShell 7 版本是否支持 OpenSSH;如果我们从 GitHub 下载并安装了它,那么应该没问题,但首先让我们使用以下命令进行检查:

(Get-Command New-PSSession).ParameterSets.Name

如果我们看到名为 SSHHostSSHHostHashParam 的参数集,那么我们就可以继续了。如果没有,则应从 GitHub 下载最新版本的 PowerShell 7。

在 Windows 上安装 OpenSSH

我们只需要在 Windows 上安装 OpenSSH,前提是我们想要远程连接到这台 Windows 机器。如果我们要从这台机器远程连接到其他机器,则可以跳过安装它;PowerShell 已经内置了一个可以使用 PowerShell 远程连接 Linux 机器的 SSH 客户端。

如果我们决定要安装 OpenSSH 服务器,那么首先需要检查我们是否正在运行一个可行版本的 Windows,并且我们是否拥有正确的权限。启动一个提升权限的管理员 PowerShell 会话,然后输入以下内容:

(New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Winver.exe

这是我在机器上得到的结果:

图 14.11 – 检查 Windows 上 OpenSSH 的前提条件

图 14.11 – 检查 Windows 上 OpenSSH 的前提条件

正如我们所见,我的权限测试返回了True,说明我确实拥有正确的权限,当我运行winver.exe时,弹出窗口显示我正在运行 Windows 10 22H2 版本,比最低要求的 Windows 10 1809 版本还要新。我们已经准备好安装了。在刚才使用的提升权限提示符中,输入以下内容:

Get-WindowsCapability -Online | Where-Object {$_.Name -like 'OpenSSH*'} | Add-WindowsCapability -Online

这将安装 OpenSSH。我们还需要启动sshd服务,并将其设置为自动启动:

Start-Service sshd
Set-Service sshd -StartupType Automatic

最后,我们需要配置sshd以允许 PowerShell 使用它。以管理员身份打开notepad.exe(右键点击并选择C:\ProgramData\ssh\sshd_config文件,在文件的最后一行之前添加以下行,并保存文件:

PasswordAuthentication yes
PubkeyAuthentication yes
Subsystem powershell c:/progra~1/powershell/7/pwsh.exe -sshs -nologo

如果我们使用的是 PowerShell 7.4 或更高版本,则-``nologo参数不再需要。小心不要将文件保存为sshd_config.txt

警告!

等等!那条c:/progra~1/powershell/7/pwsh.exe路径是怎么回事?在 Windows 上使用非微软的开源软件有时会令人沮丧。OpenSSH 就是一个例子。它无法理解带有空格的路径,即使路径被单引号或双引号括起来,因此我们不得不使用一种叫做 8.3 格式的东西,这是一种旧版微软操作系统中使用的短文件名格式。

不幸的是,情况更复杂。一些版本的 OpenSSH 也不喜欢这种格式;当我们将 PowerShell 子系统添加到sshd_config文件中时,sshd服务拒绝启动。这里的解决办法是将另一个 PowerShell 副本侧载到一个没有空格、且文件夹名不超过八个字符的目录中。为了让它正常工作,我从 PowerShell GitHub 页面下载了 PowerShell 7.4 的 ZIP 文件,通过右键点击下载的文件并选择c:\scratch\pwsh来解锁它。然后,我将以下行添加到sshd_config中:

Subsystem powershell c:/scratch/pwsh/pwsh.exe -sshs

现在,sshd服务将正常启动。

最后,使用以下命令重新启动服务:

Restart-Service sshd

就这样。我们的 Windows 机器已经准备好通过 OpenSSH 接收 PowerShell 远程连接。

在 Linux 上安装 OpenSSH

现在,我们需要在 Linux 上配置 OpenSSH。我们将使用我的 CentOS 7 机器,它已经安装了 OpenSSH,但如果我们想在 Ubuntu 机器上安装它,首先需要使用以下命令安装:

sudo apt install openssh-client
sudo apt install openssh-server

一旦安装了 OpenSSH,我们需要编辑/etc/ssh下的sshd_config文件。为此,我们需要使用sudo启动文本编辑器:

sudo nano /etc/ssh/sshd_config

我们需要添加以下几行:

PasswordAuthentication yes
PubkeyAuthentication yes
Subsystem powershell /usr/bin/pwsh -sshs -nologo

然后,我们保存文件。接下来,我们需要重新启动sshd服务:

sudo systemctl restart sshd

然后,我们设置它自动启动:

sudo systemctl enable sshd

这就是我们设置远程连接的方式。请注意,如果最后的命令已经启用,可能会抛出错误。

运行远程会话

通过 SSH 使用远程会话和使用 WinRM 一样简单,我们在第十二章中看到过,Securing PowerShell。我们首先创建一个会话对象:

$session = New-PSSession -HostName <name of remote computer> -UserName <username>

请注意,我们使用的是-HostName参数,而不是-ComputerName参数。这告诉 PowerShell 创建一个 SSH 会话,而不是 WinRM 会话。系统会要求我们输入用户的密码,然后创建会话对象。然后我们可以使用包含会话对象的变量来启动远程会话:

Enter-PSSession -Session $session

我们会看到提示符变化,反映我们正在远程连接的机器,并且直接进入该机器上的 PowerShell 会话。要离开会话,我们只需输入exit并返回本地机器。这是实际操作的样子:

图 14.12 – 通过 SSH 远程连接到 Linux 服务器

图 14.12 – 通过 SSH 远程连接到 Linux 服务器

上述截图中的编号解释如下:

  1. 第 1 行,我创建了一个新的会话对象并将其存储在变量中。我传递了远程机器的 IP 地址,因为我没有设置名称解析。我还传递了一个具有登录权限的远程机器用户的用户名。

  2. 第 2 行,系统会要求输入远程用户的密码;一旦密码输入,会创建会话对象。

  3. 第 3 行,我调用了$session变量,显示了新会话的属性。

  4. 第 4 行,我将$session变量传递给Enter-PSSession,并进入了远程会话。

  5. 第 5 行,我们可以看到提示符已经变更为[nick@192.168.56.101]: PS /home/nick>,这告诉我们我正在192.168.56.101的机器上进行 PowerShell 会话,并且登录用户名是nick。工作目录是/home/nick。我正在运行Get-Process PowerShell cmdlet,查找包含shell字符串的进程,返回了两个gnome进程。这显然是 Linux——更准确地说,是我的 CentOS 7 图形界面系统。如果我们在 Ubuntu 上运行,可能根本看不到任何进程,前提是没有安装图形界面。

  6. 第 6 行,我运行了hostname Bash 命令,返回了远程系统的名称:localhost.localdomain

  7. 第 7 行,为了避免疑惑,我们可以看到$IsLinux自动变量的值是True

  8. 最后,在第 8 行,我输入exit并返回到我本地运行在 Windows 上的 PowerShell 会话。

问题是,我在这里使用了用户名和密码组合,而许多 Linux 机器将被设置为使用基于密钥的身份验证。让我们来看看如何设置它。

身份验证

基于密钥的身份验证是一种更安全的 SSH 远程访问方式。它还使得自动化脚本更加简单,因为一旦设置完成,就不需要手动输入密码。让我们看看如何使它正常工作。

PowerShell 7 包括一个名为Ssh-keygen的工具,我们可以使用它来创建公钥/私钥对,进而用来认证我们自己到远程机器。我们可以这样使用它:

Ssh-keygen -t Ed25519

我们正在要求 PowerShell 使用 Ed21559 算法生成一个密钥对,这是相当现代的算法。较旧的系统可能需要我们改用 RSA 算法。系统会要求我们提供保存文件的路径,最好接受默认路径,只需按Enter即可。系统还会要求输入密码短语;同样,这个步骤是可选的,我们可以按Enter两次以保存没有密码短语的文件。

现在,我们可以将公钥保存到 Linux 机器的.ssh目录中,保存路径是我们想要登录的用户目录。PowerShell 还有一个叫做scp的工具,我们可以用它来复制文件(注意,可能需要先创建一个.ssh目录):

scp c:\Users\<username>\.ssh\id_ed25519.pub <user>@<remote_host>:~/.ssh/authorized_keys

我在这里使用scp传递两个参数——第一个是我们创建的文件路径,第二个是我们希望将文件复制到的路径。我们会再次被要求提供远程用户的密码,但这是我们最后一次需要这样做。现在,当我们以该用户身份登录时,我们将传递本地机器上私钥的哈希值,这个哈希与远程机器上公钥的哈希值配对,我们就会被识别为远程用户。这就是它在我的机器上的表现:

图 14.13 – 设置基于密钥的身份验证

图 14.13 – 设置基于密钥的身份验证

在前面的截图中,我已经运行了设置基于密钥身份验证所需的命令:

  1. 第 1 行,我运行Ssh-keygen来创建我的密钥对。

  2. 第 2 行,我通过按Enter接受默认路径。

  3. 第 3 行,我通过按Enter两次设置了一个空的密码短语。

  4. 第 4 行,我使用scp将公钥复制到远程机器上用户的.ssh目录中。

  5. 第 5 行,我提供了密码——希望这是最后一次提供密码。

  6. 第 6 行,我将一个新的会话对象存储在名为$sessionSSH的变量中。请注意,我没有提供密码,而是提供了-``KeyFilePath参数中的私钥路径。

  7. 第 7 行,我正在调用变量以检查其属性。

  8. 第 8 行,我正在使用$``sessionSSH变量进入会话。

  9. 在最后一行,我们可以看到提示符已经改变,表明我正在远程会话中工作。

使用 SSH 还有很多要学的内容,但这些已经足够让我们入门。接下来,我们进入本章的最后部分:macOS 上的 PowerShell

macOS 上的 PowerShell

macOS 与 Linux 非常相似;这两个操作系统都基于 Unix 元素,并且许多 Linux 程序可以在 macOS 上运行,无需修改源代码。我们将关注的差异在于如何安装 PowerShell 和 VS Code。我使用了一个朋友的 MacBook,它运行的是 Ventura(macOS 13)。如果有什么不同的话,在 macOS 上安装比在 Linux 上更简单。

在 macOS 上安装 Homebrew

Homebrew 是一个免费的开源包管理器,适用于 Linux 和 macOS,但我们大多在 macOS 上看到它。它非常容易安装和使用,正是我们用来在 macOS 上安装 PowerShell 和 VS Code 的工具。它是通过一行命令安装的。打开终端并输入以下命令:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

我们将 curl 命令传递给 bash shell,从一个 URL 下载并运行一个 Bash 脚本。可能会要求我们提供密码。macOS 和 Linux 一样健谈,但几分钟后,我们应该会看到一条消息,显示 安装成功!。现在,我们可以安装 PowerShell 了。

在 macOS 上安装 PowerShell

一旦安装了 Homebrew,其他一切都很简单。要安装 PowerShell,我们输入以下命令:

brew install powershell/tap/powershell

就这样。我们可以通过在终端中输入 pwsh 启动 PowerShell。这是 Paul 机器上的样子:

图 14.14 – 在 macOS 上安装 PowerShell

图 14.14 – 在 macOS 上安装 PowerShell

第 1 行 中,我使用 Homebrew 安装 PowerShell。在 第 2 行 中,我使用 pwsh 启动 PowerShell,在 第 3 行 中,我调用 $PSVersionTable 来检查我们安装了什么版本。

我们需要保持 PowerShell 更新。我们可以通过这两行命令来实现:

brew update
brew upgrade powershell

第一行更新 Homebrew 数据库,第二行根据最新信息升级 PowerShell。

最后,要卸载 PowerShell,我们只需输入以下命令:

brew uninstall --cask powershell

然而,我不知道为什么我们会想这样做。接下来我们来看如何安装 VS Code。

在 macOS 上安装 VS Code

我们可以使用 Homebrew 下载并安装 VS Code,使用以下命令,这会更新 Homebrew 的最新文件:

brew update

如果 cask 仓库尚未存在,这将安装该仓库:

brew tap caskroom/cask

这会在仓库中搜索 VS Code:

brew cask search visual-studio-code

这行命令安装它:

brew cask install visual-studio-code

我们可以使用启动器应用程序启动 VS Code,或者也可以通过以下 cat 命令将其添加到 PATH 变量中:

cat << EOF >> ~/.bash_profile
# Add Visual Studio Code (code)
export PATH="\$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin"
EOF

我们也可以通过直接从 code.visualstudio.com/ 下载并双击下载的文件来安装它。

我们可以通过运行以下命令卸载 VS Code:

brew cask uninstall visual-studio-code

但我敢打赌我们不想这么做。

本章到此为止。让我们回顾一下所学的内容。

总结

在本章中,我们看到安装 PowerShell 和 VS Code 没有标准的方式。我们展示了如何使用包管理器在 Ubuntu 和 RHEL 上安装 PowerShell 7,然后使用直接下载的方法在较旧版本的 Linux(CentOS 7)上进行安装。

之后,我们查看了如何在 Linux 上安装 VS Code,并且在 CentOS 7 上进行了实际操作演示。

我们查看了 PowerShell 在 Linux 上与 Windows 上运行的不同之处,包括文件系统、大小写和别名的使用。

我们花了一些时间讨论了一个重要话题:如何通过 SSH 远程连接到 Linux 机器。Linux 作为桌面操作系统的使用较为罕见,大多数在 Linux 机器上的工作都是通过 SSH 远程会话进行的,无论是使用 PowerShell 还是直接进入 Bash 终端。

最后,我们快速浏览了在 macOS 上安装 PowerShell 和 VS Code 的几种方法。我们看到,通过免费的开源包管理器 Homebrew 安装和卸载这些应用程序非常容易。

在下一章,我们将介绍如何在不同的处理器 ARM 和相关的操作系统 Raspbian 上运行 PowerShell。

练习

  1. 如果我们需要在 Kali Linux 上安装 PowerShell,应该去哪里寻求支持?

  2. 当我们在 Linux 上的 PowerShell 会话中输入 ls 时,调用的是哪个命令?

  3. PowerShell 在 Linux 上使用哪个文件路径分隔符?

  4. 我们如何轻松判断是否在 macOS 机器上工作?

  5. 我们如何在 Linux 中以 root 权限运行 PowerShell 脚本?

  6. 我们应该使用哪个 cmdlet 和参数来创建到 Linux 机器的远程会话?

  7. 我们应该使用哪个 cmdlet 和参数来避免通过网络发送密码?

  8. scp 做什么?

  9. 什么是 Ed25519?

进一步阅读

第十五章:PowerShell 7 和 Raspberry Pi

这是我最期待写的章节之一。我家里有一堆 Raspberry Pi(还有 Arduino、micro:bit 和 ESP32 等)。我用它们来教孩子们(和成年人)编程,还用它们运行广告拦截器、媒体中心、野生动物监控摄像头和园艺系统。我有一个 Raspberry Pi,配有按钮,能够随机选择我最喜欢的广播节目并播放(叫做 Shendomizer)。大多数时候,我用 Python 来编程,但 PowerShell 也是一个选择。我们甚至可以在它们上安装简化版的 Windows 10,不过这本书不会讲到这个。相反,我们将讨论如何安装 PowerShell 7 和 Visual Studio (VS) Code,如何使用 PowerShell 和 VS Code 通过 SSH 远程连接到没有显示器的 Pi,以及 Raspberry Pi 上与 PowerShell 配合使用的默认模块,最后创建一个脚本,完成物理计算的第一步:让 LED 灯闪烁。

本章我们将涵盖的主要内容如下:

  • Raspberry Pi 简介

  • 安装 PowerShell 和 VS Code

  • 远程连接到 Pi

  • 在 Raspberry Pi OS 上运行 PowerShell

  • 简单的物理计算

技术要求

对于本章内容,我们需要一台 Raspberry Pi、一个电源、屏幕、键盘和鼠标,以及所需的电缆。本章的部分内容是在 Pi 400 上编写的,Pi 400 是一款将 Raspberry Pi 安装在键盘上的方便版本,除了显示器外,其他所需配件都已经包含。其他部分则是使用 Raspberry Pi 3 单板计算机编写的。

请注意,Pi Zero 或 Pi Pico 无法使用。它们使用不同版本的 ARM 芯片,因此架构无法与 .NET 配合使用。

对于简单的自动化,我们需要以下设备:

  • 一个面包板

  • 一个 LED

  • 一个 300-400 欧姆的电阻(但 250-500 欧姆的也可以)

  • 两根公对母跳线

Raspberry Pi 简介

Raspberry Pi 是一款小型、价格实惠且多功能的单板计算机,由英国的 Raspberry Pi 基金会开发。它的主要目标是促进在学校和发展中国家教授基础计算机科学。然而,由于其可获得性、低成本和易用性,使其在爱好者、教育工作者和专业人士中非常流行,用途包括从学习编程到构建复杂的项目。

它被设计成一个空白的起点,让用户理解硬件和软件之间的基本交互。这个计算机本质上是一个小型、自包含的 PC,可以用来完成许多与台式机或笔记本电脑类似的任务,比如浏览网页、文字处理和玩游戏。此外,它的 通用输入输出 (GPIO) 引脚使其能够与外部硬件互动,非常适合用于电子项目和 物联网 (IoT) 应用。

多年来,树莓派已经发布了多个型号,每个型号都对前一代进行了改进。最新的型号是树莓派 4 Model B。该型号配备了 64 位四核 ARM Cortex-A72 CPU,处理速度可达 1.5 GHz。它有 2 GB、4 GB 或 8 GB 的 LPDDR4-3200 SDRAM 可选。为了连接,它支持千兆以太网、蓝牙 5.0 和双频 Wi-Fi(2.4 GHz 和 5.0 GHz)。此外,它还配备了两个 USB 3.0 端口、两个 USB 2.0 端口、两个支持最高 4K 分辨率的 micro HDMI 端口,以及一个用于电源的 USB-C 端口。

树莓派可以运行各种操作系统OS),其中树莓派操作系统(以前称为 Raspbian)是最受欢迎的。这个基于 Debian 的操作系统针对树莓派硬件进行了优化,并预装了必要的工具、编程语言和应用程序。用户还可以安装不同版本的 Linux,甚至是 Windows 10 IoT Core 版本。操作系统可以通过将镜像烧录到 MicroSD 卡来安装。

在编程方面,Python 是最常用的语言,因为它简洁且功能强大。然而,树莓派支持众多其他语言,如 JavaScript、PHP、C++、Java,最重要的是,PowerShell 7。它的多功能性使其成为软件开发中的一项有价值的工具,尤其是在物联网和嵌入式系统中。

树莓派的应用范围非常广泛。在教育领域,它被用来教授编程、计算机科学基础,甚至硬件设计。爱好者用它来制作复古游戏主机、媒体中心和智能家居系统。在专业领域,它作为一种经济实惠的工具,用于原型制作、数据收集和自动化。我们也可以用它进行并行计算;例如,英国 GCHQ 运行的 OctaPi 项目就是一个例子。一些机构甚至将它用于超级计算机;洛斯阿拉莫斯国家实验室就用树莓派搭建了一个 750 节点的高性能计算机。

自 2012 年发布以来,树莓派生态系统不断扩展。树莓派有三种独立的系列:Pi 系列、Pi Zero 系列,后者作为轻量级且更便宜的替代品,适用于专门的项目,并可以持续运行(我有几台使用可充电电池运行的 Pi Zero 野生动物相机),以及 Pi Pico 系列,作为流行的 Arduino 单板计算机的替代品。PowerShell 7 仅能在完整版本的 Pi 上运行,不能在 Pi Zero 或 Pico 上运行。这些更小的替代品运行在 ARMv6 架构的芯片上;而 .NET,因此 PowerShell 7,需要 ARMv7 或 ARMv8 架构的芯片,正如树莓派 2、3 和 4 所使用的那样。这部分是因为 ARMv6 功耗较低,但也因为没有人愿意为老旧硬件设计,不是吗?

好的,我们开始在 Pi 上安装 PowerShell 吧?

安装 PowerShell 7 和 VS Code

Raspberry Pi OS 是基于 Debian 的 Linux 发行版,和 Ubuntu 一样,所以我们在 第十四章 中跟随的 Ubuntu 指令,PowerShell 7 for Linux 和 macOS,也适用,但有一种更简单的方法。

安装 PowerShell

如果我们访问 PowerShell 在 Linux 上的社区支持 页面 learn.microsoft.com/en-us/powershell/scripting/install/community-support,我们会找到一个非常方便的脚本,它将为我们安装 PowerShell,如下截图所示:

图 15.1 – Raspberry Pi OS 安装脚本

图 15.1 – Raspberry Pi OS 安装脚本

如果我们点击脚本框右上角的 复制 按钮,我们可以简单地在 Raspberry Pi 上打开终端窗口,右键粘贴到终端中,如下所示:

图 15.2 – 通过将脚本粘贴到终端安装 PowerShell

图 15.2 – 通过将脚本粘贴到终端安装 PowerShell

粘贴完脚本后,我们只需按下前面截图中高亮显示的那一行上的 Enter 键,脚本就会运行,安装并启动 PowerShell。

当然,这可能容易出错,所以我们可能希望实际创建一个脚本,检查它是否正确,然后执行它。为此,从终端提示符输入以下命令:

nano

这将打开 nano 文本编辑器。将脚本粘贴到 nano 中,如下所示:

图 15.3 – 在 nano 中创建 PowerShell 安装脚本

图 15.3 – 在 nano 中创建 PowerShell 安装脚本

检查你是否已正确粘贴脚本,然后按 Ctrl + X 保存,系统提示时输入 Y 以保存文件,并为文件命名——我将我的命名为 installPosh.sh。返回到终端提示符后,我们可以通过输入以下命令来运行脚本:

sudo bash ./installPosh.sh

再次,我们将直接进入 PowerShell。现在,让我们来看看安装 VS Code。

安装 VS Code

将 VS Code 安装到我们的 Pi 上更容易。VS Code 已包含在 Raspberry Pi OS 的官方软件库中,所以我们不需要手动下载文件或设置替代软件库——我们只需打开终端并输入以下命令:

sudo apt-get update
sudo apt install code

完成后,几分钟内,VS Code 将出现在我们的机器上,并伴随许多信息:

图 15.4 – 在 Raspberry Pi 上安装并启动 VS Code

图 15.4 – 在 Raspberry Pi 上安装并启动 VS Code

我们可以在终端中输入 code,或者通过点击左上角的树莓图标,进入 编程 子菜单,从应用程序菜单中找到它。

现在,让我们来看一下我最常用的 Raspberry Pi 使用方式:远程连接。

远程连接到 Pi

虽然在教育环境中,Raspberry Pi 被用作个人电脑是很常见的,但更有可能的是我们想把它作为某种服务器使用,因此我们会希望远程连接,而不是为其配置独立的显示器、鼠标和键盘。这被称为无头模式,接下来我们将讨论这个模式。

使用无头模式的 Pi

要使用无头模式的 Pi,我们需要设置它,使其能够连接到网络并远程访问。我们将配置一个新的 Pi(或者重新构建旧的 Pi),使其能够访问无线网络,并使用 SSH 进行访问,这在第十四章中已经提到过,Linux 和 macOS 上的 PowerShell 7。我们可以通过 Raspberry Pi 网站上的 Raspberry Pi Imager 工具来设置这两项功能,网址是 www.raspberrypi.com/software/。需要注意的是,许多网上文档建议我们可能需要创建并编辑一个名为 wpa_supplicant.conf 的文件。这对老版本的 Raspberry Pi OS 和 Raspbian 是适用的,但最新版本不会使用它。

下载适合我们将要运行操作系统的安装程序版本——在我的情况下是 Windows——并进行安装。

当我们运行它时,如果没有保持图形驱动程序更新,可能会遇到 OpenGL 错误,因此请确保安装了最新的驱动程序。

我们还需要一张准备好进行镜像的 microSD 卡。

当我们打开镜像工具时,会询问我们想安装在哪个设备上,想要什么操作系统,以及使用什么存储设备,如下所示:

图 15.5 – Raspberry Pi Imager 工具

图 15.5 – Raspberry Pi Imager 工具

在我的情况下,我正在安装在 Raspberry Pi 4 上,我想要最新的 64 位操作系统,并且我希望将镜像写入我笔记本电脑上的 SDHC 卡。当我们点击下一步时,会询问是否要应用操作系统定制设置。是的,我们要。点击编辑设置,我们将看到操作系统定制对话框打开在常规标签页上,如下所示:

图 15.6 – 定制我们的 Raspberry Pi 操作系统

图 15.6 – 定制我们的 Raspberry Pi 操作系统

在前面的截图中,我已设置了我的主机名、希望在 Pi 上使用的用户名和密码,最重要的是,我配置了无线局域网设置,使其能够自动连接到我想要使用的 Wi-Fi 网络 ShedWifi,这是我小屋里的 Wi-Fi 网络。现在,我们需要切换到SERVICES选项卡以启用 SSH。默认情况下,当我们点击启用 SSH时,它会选择使用密码认证。我将继续使用此设置,但我们也可以配置为仅允许公钥认证。如果我们点击保存,会收到警告,提醒我们目标 SDHC 卡上的所有数据将被覆盖,并要求我们确认此操作。一旦确认,几分钟后,我们会收到一个写入成功的弹窗,告诉我们可以移除 SDHC 卡。让我们照做——将其插入 Pi 并开机。SERVICES选项卡在以下截图中显示:

图 15.7 – Raspberry Pi Imager 的 SERVICES 选项卡

图 15.7 – Raspberry Pi Imager 的 SERVICES 选项卡

一旦 Pi 启动,我们应该能够在网络上看到它,前提是我们的客户端与 Pi 在同一个子网内。为了测试这一点,在客户端的 PowerShell 会话中,输入以下内容:

Test-NetConnection <pi hostname> -InformationLevel Detailed

我们应该能看到 IPv6 和 IPv4 地址,以及许多其他信息:

图 15.8 – 确认我的 Pi 是否存在

图 15.8 – 确认我的 Pi 是否存在

如我们所见,我的 Pi 已经获得了它的 IP 地址,正如预期的那样,现在我可以连接到它。

让我们试试吧。

使用 PowerShell 连接到 Pi

在客户端的 PowerShell 会话中,输入以下内容:

ssh <pi hostname>

如果客户端和主机上的用户名不同,你可以输入以下内容:

Ssh <username>@<hostname>

以下截图显示了该过程的样子:

图 15.9 – 使用 SSH 连接到 Pi

图 15.9 – 使用 SSH 连接到 Pi

在第 1 行,我们使用 ssh poshpi 命令开始了一个 SSH 会话连接到 Pi。

在第 2 行,我们被提示同意连接,因为无法验证 Pi 的真实性。我们大概可以在这里输入 yes;请注意,单独输入 Y 是无效的。

在第 3 行,我们被要求输入用户的密码——在我的情况下,这是 nickp

在第 4 行,我们看到来自 Pi 的 bash 提示符——即 nickp@poshpi.local:~ $

现在,我们需要安装 PowerShell。我们可以像之前那样安装——也就是通过将微软脚本的内容复制到命令行,或者在 nano 中创建一个 bash 脚本。安装完成后,我们可以使用以下命令启动 PowerShell:

~/powershell/pwsh

另外,我们可以创建一个符号链接symlink),像这样:

sudo ln -s ~/powershell/pwsh /usr/bin/pwsh

然后,未来我们只需输入 pwsh,就像这样:

图 15.10 – 设置符号链接以运行 PowerShell

图 15.10 – 设置符号链接以运行 PowerShell

在第 1 行,我们创建了一个符号链接,在第 2 行,我们启动了 PowerShell。

在第 3 行,我们正在通过PS /home/nickp>提示符在 Linux 上运行 PowerShell;我们可以调用$PSVersionTable变量查看我们运行的 PowerShell 版本。

最后,在第 4 行,我们使用Ctrl + Break退出 SSH 会话并返回到在 Windows 客户端上运行的 PowerShell 会话——PS C:\users\nickp>——在第 5 行。我们还可以使用Ctrl + D注销会话。

很棒!让我们来看另一种连接到无头 Pi 的方法:使用 VS Code。

使用 VS Code 连接到 Pi

这种方法适用于我们希望通过 SSH 连接的任何计算机,包括 Linux。我们将使用一个名为Remote-SSH的 VS Code 扩展,在搜索栏中输入,如下所示:

图 15.11 – 安装 Remote – SSH 扩展

图 15.11 – 安装 Remote – SSH 扩展

找到扩展后,点击它,然后在中间窗格点击安装,如前面的截图所示。

完成此操作后,我们将在左侧边栏中看到一个桌面图标。点击该图标可以打开远程资源管理器窗口并设置与 Pi 的 SSH 连接。我们将被要求选择远程类型——远程计算机或 WSL 目标。我们选择远程计算机,所以选择它,然后点击SSH

图 15.12 – 选择 SSH

图 15.12 – 选择 SSH

点击ssh <username>@<hostname>。在我的情况下,我输入了以下内容并按下Enter

ssh nickp@poshpi

接下来,我们将被要求选择一个 SSH 配置文件来更新。我正在更新我的个人文件,C:\Users\nickp\.ssh\config。然后,我们将看到一个消息框提示我们主机已添加!远程资源管理器区域,点击远程(隧道/SSH)旁边的刷新图标,如图 15.13中绿色高亮显示的部分;我们应该会看到新主机出现在SSH子部分,旁边有两个图标——一个用箭头表示的是打开当前窗口中的主机,另一个用红色高亮显示的是在新窗口中打开。点击新窗口中连接图标:

图 15.13 – 打开与 Pi 的连接

图 15.13 – 打开与 Pi 的连接

接下来,我们将被要求选择远程主机的平台;选择 Linux。我们还需要输入密码。完成后,我们需要等待一两分钟来完成设置,并关闭一两个消息窗口。最后,我们将会看到一个新窗口连接到我们的 Pi;我们知道这一点是因为在左下角会显示一个框,里面写着SSH: 。在这个窗口中做的所有操作都发生在 Pi 上。很酷吧?这意味着,我们不再需要在客户端上编写脚本并将其传输到 Pi,而是可以直接从 VS Code 编写到 Pi。

好了,准备好了。我们可以像本书中展示的那样,在 Pi 上使用 PowerShell,但那并不是我使用 Raspberry Pi 的主要目的。接下来,我们将看看如何在 Pi 上使用 PowerShell。

在 Raspberry Pi OS 上运行 PowerShell

Raspberry Pi 的魅力在于它能以多种方式与外部世界连接,从摇杆控制器到相机,再到传感器、马达,甚至……哦,所有东西。在这一章中,我们将学习如何使用 GPIO 引脚让 LED 闪烁,但首先,我们需要了解如何与 GPIO 交互。这里有两个选项,都不太完善。更好的选择是安装一个新的操作系统:Windows 10 IoT Core。这本身就需要一到两章的篇幅,而且并未真正解决在 Raspberry Pi OS 上运行 PowerShell 的问题。另一种方法是使用 PowerShell IoT 模块。自 2020 年以来,这个模块没有更新,似乎也不支持较新的 Pi 4B 版本,但它在旧版本上运行相当不错,我们只能希望它未来能得到更新。我打算利用我抽屉里的 Pi 3B。

安装 IoT 模块

我们在这里使用硬件,因此需要以 root 权限启动 PowerShell,输入以下命令:

sudo pwsh

一旦进入 PowerShell,我们可以像往常一样安装模块:

Install-Module Microsoft.powershell.iot

最后,我们可能需要从 GitHub 克隆仓库,这样我们就可以访问示例了。我们可以通过以下命令实现:

git clone https://github.com/PowerShell/PowerShell-IoT.git

这将把 GitHub 仓库中的所有代码安装到我们选择文件夹下的新文件夹中:

图 15.14 – 安装 PowerShell IoT 模块并克隆 GitHub 仓库

图 15.14 – 安装 PowerShell IoT 模块并克隆 GitHub 仓库

这让我们可以访问 Examples 文件夹中的所有示例模块。里面包括一些有趣的工具,我们可以用来配合各种传感器,比如 BME280 环境传感器。

下一步是导入模块并检查它是否正常工作:

Import-Module Microsoft.PowerShell.IoT
Get-GpioPin 15

如果运气好的话,我们会看到类似以下的内容:

图 15.15 – 导入模块并检查给定 GPIO 引脚的电压

图 15.15 – 导入模块并检查给定 GPIO 引脚的电压

我们是否要探索一下这个模块中的 cmdlet?

探索 IoT 模块

在 IoT 模块中,有六个 cmdlet 用于操作 Pi 上的三种 I/O 接口:简单 GPIO、I2C 和 同步外设接口 (SPI)。令人困惑的是,这三个接口都使用 GPIO 引脚。这有时会使我们很难选择哪些引脚用于特定的目的:

  • 简单的 GPIO 读取或设置特定 GPIO 引脚的电压,使用一对名为 Get-GpioPinSet-GpioPin 的 cmdlet。我们很快就会使用到这个。

  • I2C 使用 Get-I2CRegisterSet-I2CRegister,还有一个 Get-I2CDevice cmdlet。

  • 最后是 SPI。这相当复杂,我们在本书中不打算深入探讨。这里只有一个 cmdlet:Send-SPIData

让我们更仔细地看看我们最常使用的五个 cmdlet。这些 cmdlet 的帮助文件可以在 /home/<username>/PowerShell-IoT/docs/help/ 文件夹中找到,但我们在这里简单介绍它们的基本用法:

  • Get-GpioPin:此 cmdlet 获取指定 GPIO 引脚的电压。它有三个参数:

    • -Id,它接受一个 Int32 值,指定我们要查看的 GPIO 引脚。

    • -PullMode,可以设置为 OffPullDownPullUp,某些芯片组可能需要此设置,但树莓派不需要。默认值为 Null

    • 一个 -Raw 开关,它返回 HighLow 的值。

  • Set-GpioPin:此 cmdlet 将指定引脚的电压设置为 HighLow。它有三个参数:

    • -Id,它接受一个 Int32 值,指定引脚。

    • -Value,它接受 HighLow 值。

    • -PassThru,默认情况下,该 cmdlet 不返回任何内容。如果我们希望它返回一个 PowerShell 对象,确认已设置该值,那么可以使用此参数。

  • Get-I2CDevice:此 cmdlet 创建一个 I2C 设备对象,并为其分配一个友好的名称,之后可以与 *-I2Cregister cmdlet 一起使用。它有两个参数:

    • -Id,它接受一个 Int32 值,并指定设备的地址

    • -FriendlyName,用于为设备分配一个字符串

  • Get-I2Cregister:此 cmdlet 获取特定设备上寄存器中的值。它有四个参数:

    • -Device,它接受一个 I2C 设备对象

    • -Register,它接受一个 Uint16 值,指定我们要读取的设备上的寄存器

    • -Raw,它返回寄存器中存储的值,而不是 I2CdeviceRegisterData 对象

    • -Bytecount,它接受一个字节值,并指定数据中预期的字节数

  • Set-I2Cregister:此 cmdlet 设置设备上的寄存器值。它有四个参数:

    • -Device,它接受一个 I2C 设备对象。

    • -Register,它接受一个 Uint16 值,指定我们要设置的设备上的寄存器。

    • -Data,一个字节值,用于写入寄存器。

    • -PassThru,默认情况下,该 cmdlet 不返回任何内容。如果我们希望它返回一个 PowerShell 对象,确认已设置该值,那么可以使用此参数。

就是这样。最好的方法是亲自上手并尝试,看看它们如何工作。

简单的物理计算

Raspberry Pi 的物理计算使用的是板子右侧的 GPIO 引脚,如下所示:

图 15.16 – Raspberry Pi 的 GPIO 引脚

图 15.16 – Raspberry Pi 的 GPIO 引脚

需要记住的重要事项是,接地引脚为负,电压引脚 3V3 和 5V 为正且始终开启。虽然 GPIO 引脚可能有其他特殊用途,但它们是我们可以开关的引脚——它们会输出正电流。

当我们学习一种新的编程语言时,通常会从最简单的程序开始——Hello World

Write-Output "Hello World"

在 Python 中,我们可以编写如下代码:

print("Hello World")

物理计算稍微不同——我们编写一个程序让 LED 闪烁。平台似乎无关紧要,这就是我们开始的地方。例如,让 Arduino 闪烁 LED 的程序是用 C 语言写的,类似这样:

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(1000);
}

v``oid setup()初始化板载 LED 为输出,并在每次重置 Arduino 时运行一次。void loop()命令设置了一个在 Arduino 打开时持续运行的循环(void只是告诉 Arduino 不产生输出)。digitalwrite()命令将电压设置为HIGH(开)或LOW(关)。在树莓派上,这在 Python 中看起来也很相似:

From RPi.gpio import LED
Red_led = LED(17)
Red_led.blink(on_time=1, off_time=1)

让我们在 PowerShell 中试试。首先,我们需要根据以下示意图设置硬件:

图 15.17 – 如何设置组件

图 15.17 – 如何设置组件

LED 上会有两条腿,其中一条比另一条长。长腿需要连接到电路的正极,短腿需要连接到负极或接地端。树莓派输出的电流对 LED 来说太高,所以我们需要使用大约 300-400 欧姆的电阻来稍微降低电流。如果电阻大于 1K 欧姆,可能会导致 LED 不亮;如果小于 200 欧姆,电流过大可能会烧坏 LED(如果 LED 比较便宜的话)。面包板插孔是按五列连接的,因此电阻会跨越两列。

现在,我们需要编写一些 PowerShell 代码来控制 LED 的开关。让我们使用 VS Code 的 SSH 远程连接到树莓派,打开一个新文档,命名为blink.ps1

让我们首先导入 IoT 模块:

Import-Module Microsoft.PowerShell.IoT

现在,我们需要一个始终运行的循环:

While ($true) {
}

在这个循环中,我们需要使用Set-GpioPin来控制 LED 的开关。如果你遵循了前面的示意图,你应该使用 17 号引脚。

我们还需要使用Start-Sleep来在每个命令之间等待;否则我们可能会把 LED 烧坏:

Import-Module Microsoft.PowerShell.IoT
while ($true) {
    Set-GpioPin -Id 17 -Value "High"
    Write-Host "LED on"
    Start-Sleep 1
    Set-GpioPin -Id 17 -Value "Low"
    Write-Host "LED off"
    Start-Sleep 1
}

我在这里添加了几行输出代码,以便我能够在不使用 YouTube 链接的情况下合理展示。运行结果在 VS Code 中是这样的:

图 15.18 – 闪烁的 LED

图 15.18 – 闪烁的 LED

正如我们所见,LED 的状态每秒变化一次;直到手动停止脚本,它将继续变化。我们可以看到它与我们为 Arduino 编写的 C 程序和 Python 程序进行比较的方式。

Examples文件夹中还有一个 LED 模块示例——非常值得看看,看看他们是如何做的。

这一章差不多就到这里了。让我们总结一下我们学到的内容。

总结

我们从快速了解树莓派开始,包括它的功能、设计用途以及人们如何使用它。我们看了不同的系列;主要的 B 系列单板计算机、Zero 迷你版以及 Pico 单芯片板。我们了解到,由于 Zero 和 Pico 系列的芯片架构不匹配,我们只能在 B 系列上安装 PowerShell。

接下来,我们查看了使用 Microsoft 提供的脚本在 Pi 上安装 PowerShell 的不同方法。然后,我们发现安装 VS Code 在 Pi 上是如此简单,因为它已包含在官方的 Raspberry Pi 仓库中。

虽然有一些适合作为桌面 PC 替代品的 Raspberry Pi 型号,但大多数人将使用没有屏幕或鼠标的 Raspberry Pi —— 即在无头模式下。我们了解了如何在无头模式下设置 Raspberry Pi,然后如何通过 PowerShell 使用 SSH 连接到它,接着再看了如何通过 VS Code 直接在 Pi 上方便地工作。

然后,我们讨论了 Pi 最受欢迎的应用案例之一:物理计算。这是我们与物理世界中的传感器和物体进行交互的地方。我们查看了 Microsoft 模块,用于与 Raspberry Pi 上的 GPIO 进行交互,并通过一个脚本来实现 LED 闪烁的功能。

这就是我们将要查看的所有环境内容。在下一章也是最后一章中,我们将探讨如何访问 PowerShell 所依赖的 .NET 系统,并讨论接下来的步骤。

问题

  1. 为什么我们不能在 Raspberry Pi Zero 或 Pico 上安装 PowerShell?

  2. VS Code 在 Windows 机器上的 SSH 配置存储在哪里?

  3. 用于测试 Raspberry Pi 与另一台设备的网络连接的 PowerShell cmdlet 是什么?

  4. 如何从 PowerShell 创建到无头 Pi 的 SSH 连接?

  5. 我们已经将安装脚本保存为 Install.sh。我们该如何运行它?

  6. 为什么我们可能要创建指向 pwsh 可执行文件的符号链接?

  7. 我们如何创建指向 pwsh 的符号链接?

  8. 如何从 GPIO 引脚获取 HighLow 的值?

  9. Raspberry Pi OS 是什么样的操作系统?

进一步阅读

第十六章:使用 PowerShell 和 .NET

现在,我们来到了最后一章。我们将探讨 PowerShell 7 所依赖的产品——.NET,以及如何利用它扩展我们在 PowerShell 中能做的事情。需要注意的是,这一章只能提供一个简短的概览;.NET 是一个庞大的主题,关于它的书籍远远多于关于 PowerShell 的书籍。PowerShell 只是一个基于 .NET 编写的应用程序,它仅触及了我们在 .NET 中可以做的一部分事情。话虽如此,还是让我们来看看它的工作原理,以及我们可以用它做一些什么令人兴奋的事情。

本章将涵盖的主要内容如下:

  • 探索 .NET

  • .NET 的组件

  • 在 PowerShell 中使用 .NET

  • 使用 .NET

探索 .NET

.NETdot-net)是一个软件框架。它是免费的、开源的,可以用来编写 Web 应用程序、命令行应用程序和运行在 图形用户界面 (GUI) 下的应用程序。它基于专有的 .NET 框架,这个框架是 Windows 操作系统所使用的。它可以与多种编程语言一起使用,包括 C#、F# 和 Visual Basic .NET。让我们来详细了解一下吧?

软件框架解释

软件框架是构建应用程序的工具。一些框架是为特定目的和特定语言编写的;AngularJS 是一个用于开发 JavaScript Web 应用程序前端的框架。PhaserJS 是一个游戏开发框架,同样适用于 JavaScript。虽然框架包含了许多类库,但与类库不同,框架需要在其结构内工作。类库为我们的代码提供工具,可以随意使用;而框架则规定了应用程序的基本结构,我们需要在此基础上提供具体的实现。

.NET 实现了 公共语言基础设施 (CLI),允许不同的高级语言(如 C#)在多个操作系统平台上使用,而无需为每个架构重写代码;这就是 PowerShell 7 如何能够在 Intel 和 ARM 处理器上运行的原因。通过安装正确版本的 .NET,我们的 PowerShell 代码就能在任何地方运行。

一个名为 CoreFX 的 .NET 组件包含了 .NET 类库、接口和值类型,组成了 框架类库。不过,.NET 提供的不仅仅是类库。 .NET 应用程序在一个名为 CoreCLR 的虚拟机中运行,就像 Java 应用程序在 Java 虚拟机中运行一样。

公共语言基础设施

CLI 是由微软开发的开放技术标准,主要只存在于 .NET 的不同变种中,尽管也有一个叫做 Mono 的开放开发平台也使用它。CLI 定义了五个要素:

  • 公共类型系统(CTS) – 可由编程语言通过框架访问的类型集合。

  • 元数据,用于描述程序结构。

  • 公共语言规范(CLS) – 使用框架的规则。

  • 虚拟执行系统(VES) – 它加载并执行应用程序。它使用元数据在运行时执行用兼容语言生成的代码,并将其编译成平台无关的 公共中间语言(CIL),然后将其即时编译为平台特定的机器语言。在 .NET 中,VES 是通过 CoreCLR 组件实现的。

  • 标准库,提供常用功能,如访问网络和文件。

让我们来看一下 CoreCLR 组件。

公共语言运行时 – CoreCLR

CoreCLR 提供了一个公共语言运行时,这是一个位于应用程序和操作系统之间的层。它的原理类似于 PowerShell 程序;.NET 应用程序需要在机器上运行 .NET 才能将应用程序代码解释为机器代码。这意味着 PowerShell 需要 .NET 运行,因为 PowerShell 本身是一个 .NET 应用程序。

然而,CoreCLR 不仅仅提供运行时。它还包括额外的服务,如内存管理(为应用程序分配虚拟内存的一部分)、垃圾回收(回收不再需要的未使用内存)和线程管理。这意味着,当我们使用 .NET 编写应用程序时,我们无需担心内存泄漏或内存寻址错误,因为这些都由 CoreCLR 为我们处理。

幸运的是,我们不需要知道 CoreCLR 是如何工作的,就能在 PowerShell 中使用 .NET。我们感兴趣的大部分内容都在库中 – CoreFX。

框架类库 – CoreFX

CoreFX 包含了 .NET 使用的类库,其中包括类型、函数和类。例如,所有 PowerShell 数据类型都是可用 .NET 类型的一个子集。我们已经看到这一点;在 第四章《PowerShell 变量与数据结构》中,我们看到改变 PowerShell 数组内容是资源密集型的,因为每次改变数组时,我们会创建一个新数组并删除旧数组。我们看到,解决这个问题的方法之一是使用一个在 PowerShell 中原生不可用的 .NET 类型 ArrayList,就像这样:

$ArrayList = [System.Collections.ArrayList]@()
1..10000 | ForEach-Object { $Null = $ArrayList.Add($_) }

我们使用 [System.Collections.ArrayList] 完整的 .NET 类型来将数组设置为 ArrayList。有时候,使用 .NET 确实是这么简单。

.NET 历史

.NET Framework 首次发布于 2002 年,旨在创建 Windows 应用程序。它引入了微软的托管代码概念,即只与 CLI 交互的代码。托管代码可以更严格地控制资源使用和安全性,并且由于只接触 CLR 而不是底层操作系统,它也不太容易导致系统崩溃(蓝屏),这就是其特点之一。随着时间推移,微软的许多最受欢迎的应用程序,如 Microsoft Exchange Server,都是用托管代码编写的,需要.NET Framework 来运行。.NET Framework 仅能在 Windows 上运行,并包含许多 Windows 特定的功能。

在 2014 年,微软发布了.NET Core,这是一个开源的、跨平台实现 CLI 的项目。它与.NET Framework 共享许多特性,但并非全部,并且它还包含了许多.NET Framework 中没有实现的功能,特别是在不同操作系统上运行的能力。2022 年,微软发布了一个新版本的.NET Core,简称.NET 5;其意图是最终取代.NET Framework。就像 PowerShell 和 Windows PowerShell 一样,实际上,两个版本现在并存。截至撰写本文时,最新的.NET Framework 版本是 2022 年 8 月发布的 4.8.1 版本,而.NET 每年发布一次,大约在 11 月左右;最新版本是 2023 年 11 月发布的.NET 8.0 版本。

那么,我们可以用.NET 做什么呢?让我们来看看。

.NET 的用途

.NET Framework 是为创建 Windows 应用程序而开发的——尽管我们可以将其用于命令行程序,它包括Windows Presentation FoundationWPF)框架、用于 Internet 应用程序的 ASP.NET 以及用于图形应用程序的 Windows Forms。

当前的.NET 版本包括云原生应用程序和 Azure 上的无服务器函数的库、跨平台桌面应用程序和游戏、使用.NET Multi Application User InterfaceMAUI)的移动应用程序、带有 ML.NET 的机器学习应用程序以及带有.NET IoT 的物联网应用程序。

然而,最成熟的库是为 Windows 开发的,它们包括用于 WPF、Windows Forms 和Universal Windows PlatformUWP)的 Windows 桌面应用程序库以及允许我们将应用程序作为服务运行的 Windows 服务库。

谈到.NET,可以轻易写一整套的书籍,而且许多人已经这么做了。然而,我们更感兴趣的是如何在 PowerShell 中使用.NET。我们将重点关注.NET 库以及如何从 PowerShell 访问其内容,进而访问我们希望使用的 API。让我们首先看一下.NET 库的结构。

.NET 的组件

在我们开始使用 .NET 库之前,我们需要理解它们的结构。成员(属性、方法等)包含在类型中,而类型又包含在命名空间中。这就是逻辑类型封装。还有物理类型封装。这些逻辑结构在物理上由程序集保存。我们在 PowerShell 中已经看到过这些组件。我们先从程序集开始。

程序集

程序集是类型和支持它们所需资源的集合。它们可以是静态的,从文件加载,或者动态的,仅存在于内存中。PowerShell 在启动时会加载一组默认程序集,然后随着我们导入模块,这个列表会扩展。我们可以通过以下方式查看加载的程序集列表:

[System.AppDomain]::CurrentDomain.GetAssemblies()

这将输出如下表格:

图 16.1 – 枚举程序集

图 16.1 – 枚举程序集

我们可以查看这些静态程序集的版本和文件存储位置。还有一列,GAC,表示 False。我们可以通过使用 .NET 包管理器 NuGet 安装通常存储在 GAC 中的程序集来使用它们;只不过我们不能从 GAC 访问它们。我们还可以看到这些程序集是 .dll 文件,这在我们讨论二进制模块时提到过,第十一章创建我们的第一个模块,我们还看到二进制模块是 .NET 程序集的一种类型。我们可以将加载 .NET 程序集看作类似于加载二进制模块。PowerShell 在启动时加载一组默认程序集,这些程序集定义了我们可以使用的类型。要访问其他类型,我们需要加载更多程序集。

类型

我们第一次遇到类型是在 第四章PowerShell 变量和数据结构 中。PowerShell 类型是 .NET 中类型的一个子集。在本章之前,我们在创建 ArrayList 对象时使用了一个 .NET 类型。类型用方括号括起来。

类型与类

在类型理论中,类型是一个抽象概念,而类是一组创建给定类型对象的指令。一个对象属于某种类型——例如,一个字符串或一辆虚构的自行车。类是类型的实现。对象是类的实例。例如,我们可以有多个不同的虚拟自行车类,它们都属于类型 Imaginary.Bike,但它们可能具有不同的特征——例如,猿把手或下拉把手。然后,我们可以为每个类创建多个实例(即对象)——在这种情况下,虚拟自行车。

在实践中,PowerShell 中的类是用户定义的,而类型是由 .NET 提供的——当然,除非情况并非如此。

枚举类型

枚举是常量值的列表。虽然我们之前没有讨论过它们,但我们肯定用过它们。在第十章错误处理 – 哎呀!出错了!中,我们看到自动变量$ErrorActionPreference以及通过更改该变量的值,如何临时控制 PowerShell 处理错误的方式。我们可以通过调用变量的GetType()方法来检查$ErrorActionPreference变量的类型,并且可以在以下截图中看到它是System.Enum类型:

图 16.2 – 如何设置你?让我数一数有哪些方式

图 16.2 – 如何设置你?让我数一数有哪些方式

我们可以使用GetEnumValues()方法列出可能的值,并看到熟悉的可能值列表。$ErrorActionPreference只能拥有这些值,且无法更改。

类是对象的逻辑定义,它定义了对象的属性和方法——就像一份食谱。它是某种类型的实现。回到第四章PowerShell 变量与数据结构,我们通过创建类型为Imaginary.Bike的三个对象,定义了这些对象及其属性,并将其标记为Imaginary.Bike。我们也可以定义一个名为Imaginary.Bike的类,并赋予它相同的属性和方法,使用构造函数来创建该类的实际实例。当我们希望能够轻松且可重复地创建对象时,我们使用类。

命名空间

命名空间类似于文件系统中的文件夹;我们在第十三章使用 PowerShell 7 和 Windows中讨论 CIM 类时看到了命名空间的使用。大多数 PowerShell 类型和函数都位于System.Management.Automation命名空间中。当我们与文件系统交互时,我们使用System.IO命名空间。我们在 PowerShell 中引用命名空间时,不需要写System,因为会自动搜索System命名空间;Management.Automation在功能上与System.Management.Automation相同。不幸的是,我们需要为任何不在System命名空间中的类型指定命名空间,如下所示:

图 16.3 – 访问命名空间

图 16.3 – 访问命名空间

Path类型定义了对象的文件路径,并位于System.IO命名空间中。如果我们在第 1 行没有指定命名空间来引用它,就会出现错误。在第 2 行,我们指定了完整的命名空间,但在第 3 行,我们可以看到无需指定System部分的命名空间,因为它是隐式的。

我们还可以使用using关键字来加载命名空间,如下所示:

using namespace System.IO

运行此操作后,我们可以像这样调用Path类型,而无需指定命名空间:

图 16.4 – 关键字的作用

图 16.4 – using关键字的作用

我们在第十一章《创建我们的第一个模块》中看到过using关键字,我们了解它是脚本中加载模块的推荐方式。正如我们所看到的,我们也可以使用它来加载命名空间和程序集。不幸的是,在控制台中,只有最新的using语句会生效,因此如果我们在当前会话中使用它访问另一个命名空间,就会失去对System.IO命名空间的访问权限。脚本允许多个using语句,通常写在脚本的开头;它们只能被注释符号所前置。我们可以通过使用分号(;)分隔using语句,在控制台中加载多个命名空间。

成员

类型和类有成员;在本书中,我们使用了Get-Member命令来检查对象的属性和方法,并且我们已经看到对象是某种特定类型的实例。 .NET 类型有一个我们之前没有见过的成员——构造函数。构造函数提供了实例化给定类对象的方式,并与类同名。当我们对类运行Get-Member时,构造函数不会出现,也无法直接调用。构造函数可能有参数,我们可以使用这些参数来填充新对象的数据成员,也可能没有;如果没有参数,则对象会被创建时具有一组空值属性。构造函数可能有重载,可以在构造对象时传递不同的参数集。大多数 .NET 类都有一个自动构造函数new(),这是 PowerShell 添加的一个静态方法。

让我们看一个例子。[string]类型是不可变的;当我们修改字符串时,我们会销毁旧字符串并创建一个新字符串。如果我们需要字符串频繁变化,这可能会导致性能问题,这与我们在数组中看到的问题类似。在.NET 中有一个定义可变字符串的类,它解决了这个问题;它被称为StringBuilder类,该类的对象是System.Text.StringBuilder类型。如果我们使用默认的自动构造函数创建一个新的StringBuilder对象,我们会得到一个具有三个属性的对象:CapacityMaxCapacityLength,这些属性的单位是字符。如果我们不带括号调用new()方法,我们就能看到所有可能的重载列表:

图 16.5 – 类的重载

图 16.5 – StringBuilder类的重载

正如我们所看到的,我们可以向new()方法传递不同的参数组合,从而构造具有不同属性的StringBuilder对象。让我们试试看。输入以下内容:

$string1 = ([System.Text.StringBuilder]::new(32))
$string2 = ([System.Text.StringBuilder]::new('32'))

现在,如果我们调用$string1$string2,我们会看到$string1是空的,且其容量为 32;$string2的长度为 2,容量为 16。这是因为我们对$string1调用了重载new(int capacity),对$string2调用了重载new(string value)

我们可以使用ToString()方法查看内容,如下所示:

$string1.tostring()
$string2.tostring()

$string1 是空的,而 $string2 包含 '32' 字符串。如果我们使用 GetType() 方法,我们可以看到两者都是 StringBuilder 对象。如果我们将它们传递给 Get-Member cmdlet,我们还可以看到 new() 方法没有列出,因为它是一个构造函数。

版本管理

程序集是一个包含称为程序集清单的元数据的 .dll 文件,清单列出了文件的内容以及文件的名称和版本。强名称的概念是在 .NET 中引入的;一个强名称由模块的名称、版本以及用于验证文件作者的加密哈希组成。当 .NET 程序链接到一个强名称程序集时,文件的名称、版本和哈希必须与链接的强名称匹配。如果我们只是简单地用一个较新的版本替换 .dll 文件,那么程序将无法加载它。这导致了具有相同版本号的 .dll 文件的不同版本,以防止引入破坏性更改。太棒了。

还有许多其他组件,但这些是我们在使用 .NET 和 PowerShell 时最需要注意的内容。现在,让我们来看看 PowerShell 如何利用 .NET。

在 PowerShell 中使用 .NET

在本节中,我们将查看 PowerShell 如何访问 .NET 库的详细信息。我们将了解默认的程序集、PowerShell 如何查找类型,以及另一种创建对象的方法。

为什么要在乎?

PowerShell 和 C# 都是 .NET 家族的一部分,因此它们可以很好地协同工作,因为它们基于相同的 .NET 基础。它们共享许多功能,例如类和库。我们可以通过使用 Add-Type 在 PowerShell 中调用 C#,这让我们在运行 PowerShell 脚本时编译并运行 C# 代码。这样我们就能利用 PowerShell 的简洁和易用性,但当需要时,C# 也能随时调用,而不必编写整个程序。

PowerShell 程序集

我们在章节开始时看到,可以通过以下语句列出已加载的程序集:

[System.AppDomain]::CurrentDomain.GetAssemblies()

AppDomain 是一个封装和隔离执行环境的类;它有点像 PSSession,但更加安全;每个 PSSession 实例共享一组程序集,而每个 AppDomain 实例加载自己的程序集。CurrentDomain 获取当前执行环境。双冒号(::)是 C# 命名空间别名运算符;我们需要使用它来访问别名命名空间的成员,它位于两个标识符之间。让我们像这样再次运行语句:

[System.AppDomain]::CurrentDomain.GetAssemblies() | select FullName

然后,我们可以看到已加载程序集的强名称列表:

图 16.6 – 默认程序集及其强名称

图 16.6 – 默认程序集及其强名称

请注意,每个强名称包含一个短名称、一个版本、一个文化标识符和一个加密密钥,该密钥标识作者。

动态程序集加载

自动加载适用于像pwsh.exe这样的已编译程序,但它依赖于可执行文件中的所需程序集列表。我们也可以将所需的程序集添加到模块清单中的RequiredAssemblies元素。如果在编写脚本时需要加载非默认程序集,我们可以使用前面的using关键字、Add-Type cmdlet,甚至使用Import-Module cmdlet,如果程序集在一个.dll文件中。

Add-Type cmdlet 有五个参数集;其中三个用于定义新类型,但我们也可以使用它从指定路径导入程序集,或仅从程序集导入我们需要的类型,例如以下内容:

Add-Type -AssemblyName PresentationCore,PresentationFramework

这将添加所需的程序集,以便从 PowerShell 中调用简单的 Windows GUI 元素。

一旦我们导入(或创建)了新的类型,就可以使用New-Object cmdlet 来创建该类型的实例。

创建类型的实例

New-Object cmdlet 创建一个类型的实例。该类型必须存在于 PowerShell 默认的程序集内,或者我们必须先使用Add-Type导入它。New-Object很容易使用。我们只需要提供TypeName和一个与可用重载匹配的参数列表。例如,StringBuilder类型的一个重载允许使用一个字符串来定义新对象的值,另一个整数来定义初始容量。请注意,它接受一个字符串值(System.Text.String.Builder),而不是命名空间和命名空间别名限定符([System.Text.StringBuilder]::),所以我们可以这样做:

$loveit = (New-Object -TypeName System.Text.StringBuilder -ArgumentList "i love powershell", 128)

这将创建一个名为$loveit的变量,它包含"i love PowerShell"字符串,并且初始容量为 128 个字符:

图 16.7 – 喜欢它

图 16.7 – 喜欢它

我们可以在前面的截图中看到,初始容量是 128,而如果我们只传递一个字符串值的话,预计它应该是 17,即字符串的长度。请注意,我们需要理解我们希望使用的构造函数的重载所需的参数——在本例中是"i love powershell"128。例如,如果我们提供两个字符串,则会出错。

另一种选择是使用-Property参数,它接受一个包含属性名称和所需值的哈希表。我们将在下一节中看到如何使用它,但请注意,如果你拼写错误,PowerShell 会将拼写错误的属性添加到对象中,而不是告诉你有错误。

让我们尝试几个例子,以便更好地了解如何在 PowerShell 中使用 .NET。

使用 .NET

在本节中,我们将尝试两个示例——一种触发操作的替代方法,比如脚本,以及如何从 PowerShell 中调用 Windows GUI 元素。

任务调度程序的替代方案

在这个示例中,我们将创建一个定时器对象,然后使用Register-Event cmdlet 在定时的间隔内触发一个操作。

首先,让我们创建一个定时器:

$timer = (New-Object -TypeName System.Timers.Timer -Property @{
AutoReset = $true
Interval = 5000
Enabled = $true
}
)

现在,我们需要注册事件并定义一个操作:

Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier Test -Action {Write-Host "hello"}

现在,让我们开始计时,操作如下:

$timer.start()

然后,我们应该看到hello字符串每隔五秒钟出现在屏幕上,直到我们输入以下内容:

$timer.stop()

这就是它在我机器上的样子:

图 16.8 – hello

图 16.8 – hello

我们可以看到每当计时器达到 5000 毫秒时,命令就会被触发。酷吧?

让我们尝试创建一个 GUI 消息框。

创建 GUI 对象

这个例子将在 Windows 中创建一个弹出消息框,包含一对“是/否”按钮。按下的按钮值将记录在 PowerShell 会话中:

Add-Type -AssemblyName PresentationCore,PresentationFramework
$Button = [System.Windows.MessageBoxButton]::YesNo
$Title = "PowerShell for Everyone"
$Body = "Do you love PowerShell?"
$Icon = [System.Windows.MessageBoxImage]::Warning
[System.Windows.MessageBox]::Show($Body,$Title,$Button,$Icon)

我们可以将这个与前面的计时器对象结合使用,显示一个消息框,允许用户取消长时间运行的脚本。这是它在我机器上的样子:

图 16.9 – 当然你会

图 16.9 – 当然你会

请注意,默认答案是。我们还可以使用响应来设置参数、添加事件或触发条件语句。

让我们总结一下本章的内容。

总结

我们首先了解了什么是.NET——一个软件框架。我们了解到它是基于公共语言基础结构(Common Language Infrastructure)并且类似于 Windows 中的.NET Framework,但并不完全相同。我们看到它包含了自己的运行时 CoreCLR 和一套库 CoreFX。我们了解了.NET 和.NET Framework 之间的关系以及它们如何共存。我们看到.NET 可以用于许多领域,包括机器学习和物联网应用,但它主要用于 Azure 和 Windows。

我们了解了.NET 的各个组件,并理解了它们如何相互关联,并在 PowerShell 中表示。我们看到了构造函数成员,这是用于实例化对象的一种特殊类型的方法,并理解了为什么版本控制经常让人困惑。

然后,我们查看了如何从 PowerShell 访问.NET 库的具体方法,学习了动态加载,并看到了如何使用New-Object cmdlet 来创建类的实例。

最后,我们通过两个例子演示了可以使用.NET 做的事情——创建事件计时器和 Windows 消息框。

接下来呢?这取决于你的需求。如果你想了解更多 PowerShell 的内容,有很多很棒的书籍,比如 Packt 出版的 Chris Dent 的《Mastering PowerShell Scripting》,或者 Bruce Payette 等人编写的绝对经典之作《Windows PowerShell in Action》。无论你选择哪本书,你需要做的就是多加练习。任何语言,最好的学习方式就是用它。若你对.NET 感兴趣,PowerShell 是一个不错的起点,但迟早你会想使用编译型语言,比如 C#。虽然在 PowerShell 中编写机器学习应用程序从技术上来说是可行的,但我认为用 C#做会容易得多。

就这样。这本书到此为止。感谢你与我一起坚持读完,希望你读得和我写得一样愉快。我可以向你保证,我在这个过程中学到了很多,希望你也有所收获。希望你像我享受陪伴一样也享受我的陪伴。

练习

  1. 如何在 PowerShell 中创建 .NET 类的新实例?

  2. PowerShell 中用于将 .NET 程序集添加到会话的命令是什么?

  3. 如何在 PowerShell 中调用 .NET 类的静态方法?

  4. 在 PowerShell 中,如何访问 .NET 类的静态属性?

  5. 如何在 PowerShell 中调用带参数的 .NET 构造函数?

  6. 用于从文件加载 .NET 程序集的 PowerShell cmdlet 是什么?

  7. 如何在 PowerShell 中确定对象的 .NET 类型?

  8. 在 PowerShell 中,如何列出 .NET 对象的所有方法?

  9. 在 PowerShell 中,调用 .NET 对象实例方法的语法是什么?

  10. 如何在 PowerShell 中访问 .NET 对象的实例属性?

进一步阅读

活动和练习的答案

第一章

活动

  1. 你可以像这样使用 ADD_FILE_CONTEXT_MENU_RUNPOWERSHELL

    kill(). Charming, right? CloseMainWindow() might work for a graphical process, and Close() will ask politely, but kill() should do it. Note that sometimes it won’t, for instance, if the process you are trying to kill is running with higher privileges than the account you are running PowerShell with.You can use it like this. Here’s my list of `pwsh` processes:
    

图 A.1 – 一些 PowerShell 进程

图 A.1 – 一些 PowerShell 进程

让我们去掉 4052

图 A.2 – 更少的 PowerShell 进程

图 A.2 – 更少的 PowerShell 进程

练习

  1. 输入 Get-Random

  2. 输入 Get-Random -Minimum 1 -Maximum 11

  3. 输入 Get-ChildItem -Path <文件夹路径>

  4. 输入 Get-ChildItem -Path <文件夹路径> -Recurse

  5. 输入 New-Item -Path <文件夹路径> -Name <项目名称> -ItemType

  6. 目录。

  7. 输入 Get-Uptime

  8. 输入 Out-File

  9. 存储一个用户名和密码,以便在 shell 或脚本中稍后使用。

  10. 这个 cmdlet 会将之前 cmdlet 或管道的输出转换为 HTML,然后可以在 web 浏览器中显示。请注意,你可能还需要使用 out-file 保存为文件,否则它只会在 shell 中显示 HTML 代码。

第二章

活动

  1. New 用于创建一个新对象。例如,New-Item C:\foo\bar.txt 将在 C:\foo 目录下创建一个名为 bar.txt 的空文本文件。Add 会将内容添加到现有对象中,因此 Add-Content C:\foo\bar.txt "PowerShell rocks my world" 会将该字符串添加到之前空的文本文件中。

  2. 只需像这样指定 -InputObject 参数:Get-Random -InputObject 20

  3. 使用 -Prefix 参数。这个参数在使用远程会话时特别有用。

练习

  1. Get-Content 是正确的。这个问题有点狡猾,因为 PowerShell 命令的批准动词 网页上指出,Get 应该保留用来获取有关对象的信息,而不是其内容。然而,Get-Content 是正确的,因为我们是获取文件的内容,并将其作为一个对象保留,以供将来使用,而不是从远程资源(如网页)读取数据。Read-Host 是一个读取数据的 cmdlet 示例——在这种情况下,它从 shell 中读取信息。

  2. 你应该看到屏幕上打印出 alive alive 这两个词。这是因为 ohOut-Host 的别名,它接收一个对象——在这种情况下是字符串 alive alive——并将其打印到默认的主机,通常是屏幕上。

  3. Get-ChildItem 有两个参数集。决定使用哪一组的参数是 -LiteralPath

  4. *.exe 被传递给 -filter 参数。如果查看帮助文件,你会看到 -filter 是一个位置参数,位置为 1,因此没有指定参数的第二个值将被理解为过滤器参数。

  5. 不行。-Filter 参数只能接受一个参数。如果你希望这个 cmdlet 执行,Get-ChildItem c:\foo\* -include *.exe, *.txt 会有效。

  6. Find-module *aws* 将查找许多由 Amazon 提供的用于与 AWS 工作的官方模块。

  7. 截至写作时,PowerShell Gallery 上没有,但 GitHub 上有一些。我不确定它们的官方性,使用时要小心。

  8. 按住 Ctrl 键并滚动鼠标滚轮是最简单的临时方式。Ctrl+(加号)或 Ctrl-(减号)也可以实现。如果要永久更改,请打开设置,点击你想要更改的配置文件,进入外观子部分。

第三章

活动

  1. 我们可以结合使用 -first-last-skip 来实现这一点,如下所示:

    -skip parameter will skip from the start, unless it is combined with -last. However, it is not positional, so if we specify both the -first and -last parameters, -skip will always skip from the start of the array; it doesn’t matter where we put it in the cmdlet:
    

图 A.3 – 使用 first、last 和 skip 时的输出

图 A.3 – 使用 first、last 和 skip 时的输出

  1. 这是因为 -contains 不支持通配符。值必须完全匹配,除了它不区分大小写。

  2. Get-Command -ParameterName filter 会为我们完成此操作。如果你运行它,你会看到有很多这样的命令。它们大多数都使用我们在本章中看到的相同的 filter 块语法。

练习

  1. Get-Command 让我们可以找到 Get-Date cmdlet。接下来,我们需要使用 Get-Member 查看 Get-Date 返回的对象的属性。最后,我们需要使用 Select-Object 只显示 DayOfWeek 属性 – Day 返回月份中的天数。

  2. path 不是我们运行 Get-Process 时显示的属性,所以我们需要使用 Get-Member 来查找它。

  3. Get-Process | select-object name, cpu, path | sort-object path -descending

    简单。

  4. Where-Object 需要尽早使用。记住,过滤要从左侧开始

  5. 最好利用 cmdlet 的过滤属性,而不是将所有内容通过管道传递给 Where-Object,因此以下代码是最有效的方式:

    ForEach-Object here, like this:
    
    

    交互式的 ForEach-Object。

    
    
  6. Get-Content 是你需要的 cmdlet,我们将在第六章中详细讲解,PowerShell 与文件 – 读取、写入和操作数据。你会记得在本章之前的工作中,Get-Process-Name参数只接受ByProcessName,而不是对象ByValue,因此我们不能使用Get-Content来获取名称列表。相反,我们必须使用括号并将其直接传递给-Name参数。

  7. 这样无法运行;虽然 -computername 参数接受一个 system.string 对象,但它是通过 ByPropertyName 而不是 ByValue 方式进行的。正确的运行方式如下:

    shutdown command and can’t work against remote machines. Hopefully, you followed the instructions not to try it, as it will ignore the bobscomputer string and shut down your local machine if you don’t include the -WhatIf parameter.
    

第四章

活动

  1. 这完全是关于内存的。如前所述,栈的空间有限,值类型对象存储在栈中。因此,在尽量节省内存的同时,对编写代码的人透明是有意义的。[Int64] 类型的对象在栈上占用的空间是 [Int32] 类型对象的两倍。

  2. MyString 被告知获取 MyVariable 的内容,即整数 42,并将其作为字符串处理。随后,我们可以将一个整数放入 MyString,因为在创建时我们没有对其进行任何限制。

    反过来,用 [string]$MyOtherString,会将 MyOtherString 限制为只能包含字符串。

练习

  1. 里面有一个空格——空格是不允许的。如果我们真的必须在变量名中使用空格,那么必须将其括在大括号中——{My Variable},这比使用不包含空格的变量名可能更麻烦。

  2. System.Management.Automation.PSVariablePSVariable,甚至是 Variable

  3. 使用首选项变量 $ErrorView。默认情况下,它设置为 ConciseView,这是一个较短、便于阅读的消息,只包含错误信息。这个功能是在 PowerShell 7.0 中引入的,取代了稍显模糊的格式。我们仍然可以通过将 $ErrorView 设置为 NormalView 来查看旧格式。有趣的是,PowerShell 文档将 $ErrorView 列为首选项变量和自动变量——我怀疑这是错误的;我认为它是一个首选项变量。

  4. $Null 会这样做。$Null 与 0 不同,它仍然是一个值,只是一个空值。

  5. 我们可以使用 CompareTo() 方法。这将给我们三个可能的输出:如果整数相同,则为 0;如果第一个整数小于第二个,则为 -1;如果第一个整数大于第二个,则为 1。尝试以下操作:

    $x = 42
    $y = 23
    $y.CompareTo($x)
    

    这种用法将在下一章中派上用场,第五章PowerShell 控制流 – 条件语句与循环,我们将在其中探讨条件语句。

  6. 它是一个 System.Array 对象或数组。

  7. MyString.ToCharArray() 将把每个 char 输出为数组中的一个元素,并且每个元素占一行。

  8. 因为我们只使用了单引号,所以输出将是 My Name is $MyName。如果我们希望变量被展开,就必须使用双引号。

  9. 这是一个有序哈希表的 TypeName,我们可以使用 [ordered] 加速器来创建它。记住,这个放在语句的右侧,而不是左侧,像这样:

    $OrderedHash = [ordered]@{a=10;b=20;c=30}
    

第五章

活动

  1. 因为这个 switch7 { Write-Output 'contains 7' },它在查找一个整数,而它正在搜索的这一行是一个字符串,因此没有包含任何整数。如果我们把 switch 语句中的 7 替换为 '*7',使其成为一个字符串,那么它就能正常工作。

  2. 这是因为我们在输出之前执行了递增语句。如果交换这两个语句,便能证明当条件不满足时,脚本块并不会执行。

练习

  1. 没有。该语句只有在$x大于 4 时才会产生输出。这里没有else语句来提供替代的输出。这是一个真正的条件语句,而不是提供替代流程的条件语句。

  2. $x = 4 ; IF ($x -gt 4) {Write-Host '$x is larger than 4'}Else {Write-Host '$x is not larger than 4'}

  3. $x = 4 ; IF ($x -gt 4) {Write-Host '$x is larger than 4'}elseif ($x -lt 4) {Write-Host '$x is smaller than 4'} Else {Write-Host '$x is 4'}

  4. $x = 4 ; ($x -gt 4) ? (Write-Host '$x is larger than 4') : (Write-Host '$x is not larger than 4')

  5. 由于foreach位于管道字符之后,这里只有一个语句,并且foreach被解释为ForEach-Object的别名,因此语法是错误的。我们可以通过将管道字符替换为分号来纠正它。这将其分为两个语句,foreach被正确地解释:

    $processes = Get-process ; foreach ($process in $processes) {$process.name}
    
  6. number = 0 ; Do {$number ++ ; Write-Host "Number is $number"} While (!($number -eq 5))将能正常工作。我们会看到使用not运算符别名(!)来反转语句的结构会很常见。

  7. 它缺少迭代器。这样写就能正常工作:for ($i = 0 ; $i -lt 5 ; $i ++) {``Write-Host $i}.

  8. switch语句。将它们用于循环外部或switch语句中可能会导致不可预测的结果。

  9. 有几种方法可以实现这个;任何有效的方式都是对的,但我的解决方案是用for循环替换现有的while循环来实现计数器。我还添加了一个额外的elseif语句来处理胜利条件,像这样:

图 A.4 – 猜限量布鲁西

图 A.4 – 猜限量布鲁西

网络上有很多写法——我选择了这种方式,主要是使用了我们在本章中介绍的概念。

第六章

活动

这只是实现的方法之一。如果你有其他有效的做法,恭喜你。那也是正确的方式——至少是其中之一。根据编程风格练习,至少有 41 种其他方法:

$TheTrial = Get-Content -Path .\thetrial.txt -Raw
$StopWords = Get-Content -Path .\stopwords.txt -Raw
$TrialWords = $TheTrial.Split(" ", "`t", "`n", ",","`"",".", [System. StringSplitOptions]::RemoveEmptyEntries)
$Words = [System.Collections.ArrayList]@()
Foreach ($Word in $TrialWords) {
$LWord = $Word.ToLower()
if (!($StopWords.Contains($LWord))) {
$Words.Add($Word)
}
}
$Grouped = ($Words | Group-Object | Sort-Object Count)
$Grouped[-1 .. -10]

这里是运行的结果:

图 A.5 – 《审判》英文版中最常见的十个单词

图 A.5 – 《审判》英文版中最常见的十个单词

让我们快速地回顾一下:

  • 第 1 行和第 2 行使用Get-Content将我们的两个文件导入 PowerShell,以Raw格式作为单一字符串。

  • 第 3 行添加了一些额外的分隔符并移除了空字符串。我不指望你了解字符串拆分选项,所以我在提示中给了你这个信息。

  • 第 5 行创建了一个空的数组列表来存储有趣的单词;如果我们使用 PowerShell 数组,这将非常缓慢。

  • 第 7 行开始了一个Foreach循环,遍历$TrialWords中的每个单词。

  • 行 8 创建了一个变量,并且每次循环重复时,都将每个单词转换为小写。

  • 行 9 开始一个 if 语句,匹配条件“$Lword 不在 $StopWords 中”。请注意,我们使用的是 -Contains 方法,它匹配单个字符串中的子字符串,因此它在 $StopWords 字符串中搜索与 $LWord 匹配的子字符串。

  • 如果条件为真,行 10 会将 $Word 添加到 $Words 数组列表中。

  • 行 13 将 $Words 中的单词分组并排序。

  • 行 14 返回前 10 个最常见的单词,按降序排列。

练习

  1. Get-Childitem -Path C:\Temp -File | Format-Wide -****Column 3

  2. Get-Process | Format-Wide -column 5 | Where-Object id -****gt 100

    它将不会产生任何输出。记住,格式正确。正确的代码应该如下:

    Get-Process | Where-Object id -gt 100 | Format-Wide -column 5
    
  3. “我爱 PowerShell” | Out-File -****Path Q3.txt

  4. “Sooo much” | Out-File -Path Q3.txt -Append

  5. Get-ChildItem | Export-Csv -Path items.csv -****Delimiter “;”

  6. (****Get-ChildItem 函数:).count

  7. Get-Content Q3.txt -Delimiter “ “(Get-Content Q3.txt).Split(“ “)

  8. PSCustomObjects

  9. Import-Clixml: 找不到命名空间名称为 ‘http:// schemas.microsoft.com/powershell/2004/04’ 的元素 ‘Objs’。

    我们尝试导入的 XML 文件格式不正确,无法被 cmdlet 识别,或者它不是 PowerShell 对象。

第七章

活动

这是我的解决方案:

图 A.6 – 一个解决方案

图 A.6 – 一个解决方案

行 2 从 API 获取 JSON 格式的数据,并将其放入一个变量中。如果我们查看 $astronauts 变量,我们可以看到它有两个键值对,messagepeoplepeople 包含一个 JSON 对象数组,这些对象本身有两个键值对;namecraft。我们可以在下图中看到这一点:

图 A.7 – 获取 JSON 数据

图 A.7 – 获取 JSON 数据

因此,我们知道所需的数据在 $astronauts.people.name 键值对中。现在我们只需要以一种愉悦的方式显示它。我们在 第六章 中讨论了如何使用 ConvertTo-HtmlPowerShell 和文件 - 读取、写入与处理数据,这是我选择的方式。

在第 5 行,我们设置了一个 $params 哈希表,这样就可以展开我们需要的所有参数。我选择包括一个 CSS 样式表来显示一张美丽的图片,但这是不必要的。这是我的 CSS:

Table {
color: white;
text-align: left;
background-color: black;
}
Body {
background-image: url("iss.jpg");
background-size: cover;
background-repeat: no-repeat;
background-color: black;
font-family: 'Trebuchet MS';
color: yellow;
}

最后,魔法发生在第 12 行。我们通过管道将感兴趣的值传递给 ConvertTo- Html,然后使用 Out-File 将 HTML 写入文件。之后,我们可以在我们选择的浏览器中打开此文件。

希望这已经向你展示了使用 API 获取、处理和显示数据是多么简单。

练习

  1. Invoke-WebRequest -Uri ‘https://httpbin.org/delete’ -****Method Delete

  2. 我们在第一次请求(通常是登录请求)中使用-SessionVariable参数传递一个字符串,然后在后续的请求中,我们使用-WebSession参数作为变量传递会话变量。

  3. 我们可以使用-SkipCertificateCheck参数,但只有在我们确定该站点有效且不具恶意时才应该这么做。

  4. 我们犯的错误是在通过请求头提供令牌之前对其进行编码。应该将令牌编码为参数并以明文形式提供给请求头。这意味着通过参数传递稍微更安全,但并不是所有服务都会接受这种方式。

  5. WebSocket API 通常是有状态的。这意味着关于请求者的信息会在多个请求之间持续存在;这使得使用起来更加复杂,因为我们需要持续保存会话信息,而且也容易受到网络条件的影响。

  6. 有很多方法可以做到这一点,但最直接的方法可能就是这个:

    (Invoke-RestMethod 'http://universities. hipolabs.com/search?country=United+kingdom').name | Where-Object {$_ -like '*x*'} | Measure | select -Property 'count'.I get `8`, but this is subject to change.
    
  7. 我们可以通过-Schema参数传递一个描述自定义模式的长字符串,或者我们可以通过-SchemaFile传递一个文件位置。你说得对!这一点没有在章节中提到。希望你通过阅读帮助文件找到了答案。

第八章

活动

  1. 做这件事的简单方法是在Param()块中创建一个$Output变量,然后将其作为参数传递给脚本中的Out-File cmdlet,像这样:

    [CmdletBinding()]
    Param(
    $City = "London",
    $Output = "c:\temp\poshbook\ch8\WeatherData.html"
    )
    $headers = @{"key" = "<Key>"}
    $uri = "https://api.weatherapi.com/v1/current.json?q=$($City)&aqi=no"
    $response = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers
    $response | Convertto-Html | Out-File $Output
    

    这样做的问题是,如果我们想更改文件名,每次都需要输入文件名和路径。我们更有可能更频繁地更改文件名,而不是更改路径。让我们像这样将路径和文件名分开:

图 A.8 – 分离文件名和文件路径

图 A.8 – 分离文件名和文件路径

现在,我们只需在需要时传递一个不同的文件名,或者在需要时传递一个不同的路径,而不必每次都输入整个内容。

  1. 从一个具有可以接收多个字符串的参数的 cmdlet 帮助文件中,我们可以看到该属性包含一对空的方括号,像这样:[string[]]。这一部分相对简单。我们还需要处理传递给-City参数的数组中的每个元素。为了做到这一点,我们还需要一个foreach循环,将其包裹在脚本的工作部分周围。

    最后,我们需要某种方式将每个城市的数据发送到单独的输出文件。我选择通过传递foreach循环中的$item变量来实现。以下是我的解决方案。你的方案可能会有所不同,但只要它能工作,那就太好了。

图 A.9 – 处理多个城市

图 A.9 – 处理多个城市

在第3行,我已将[string[]]属性添加到-City参数中,以允许它接收多个字符串。

我在第9行打开了一个foreach循环,并在第22行关闭。中间的行现在将为$City参数中包含的每个字符串$item重复。我还缩进了中间的行,以便更容易阅读。

我已将第17行更改为使用$item变量(当前城市),而不是$City中的数组,因为那样会导致错误——API 一次只接受一个字符串。

最后,我更改了第11行,使其将每个输出发送到一个包含城市名称的文件中。

  1. 这是我的示例。你的可能不同,但希望你提前包含了关于需要 API 密钥的警告:

    <#
    .SYNOPSIS
    Gathers weather data for a number of cities and stores the API output.
    .DESCRIPTION
    This cmdlet will gather the current weather data from a number of cities from
    the API endpoint at https://api.weatherapi.com and outputs the responses to a
    set of named HTML files stored in the specified directory.
    The -City parameter takes an array of strings, either explicitly or via the
    pipeline (ByValue).
    The -OutputFile parameter takes a single string specifying the filename and
    suffix. This filename will be prefixed by the string provided in the -City
    parameter, eg. London_WeatherData.html
    The -OutptPath parameter specifies a location for the output file.
    The -Key parameter specifies a txt file that contains the key from
    weatherapi.com
    .NOTES
    This script requires a personal API key from https://weatherapi. com
    The output path will need to exist before running the script
    .LINK
    No link, sorry.
    .EXAMPLE
    .\weatherdata.ps1 -City london,paris
    This will generate two html files; one for London and one for Paris
    #>
    

练习

  1. 这可能是由于一些设置问题,但让我们假设它是最简单的情况:执行策略对你来说是正确的,但对他们来说不正确。这意味着CurrentUser策略在限制他们。

    使用以下方法应该可以,如果脚本是在本地机器上编写的,或者如果我们在另一台机器上使用代码签名证书签名过:

    Unrestricted.
    
  2. -Maximum 参数。我没有见过任何龙与地下城的骰子是从 1 以外的数字开始的(除了 d100,但稍后我们会在一个问题中讨论它)。

  3. 嗯,我们可以有几种方法来做到这一点,但希望你能想出类似这样的解决方案:

    [CmdletBinding()]
    param(
    $Sides = 20
    )
    get-random -minimum 1 -Maximum $Sides
    

    你不需要在其中包含CmdletBinding()属性,但我总是会加上。

  4. 它应该是一个整数,我们会用[``int]属性来指定:

    [int]$Sides = 20
    
  5. 所以,如果我们阅读这个链接,我们可以看到我们可以将ValidateSet属性分配给一个参数,并传递一个合法值的数组,它看起来是这样的:

    [CmdletBinding()]
    param(
    [ValidateSet(4,6,8,10,12,20)]
    [int]$Sides = 20
    )
    get-random -minimum 1 -Maximum $Sides
    
  6. 为了做到这一点,我们需要一个循环,并通过参数指定运行循环的次数,然后将每次循环的输出添加到累积总和中。它可能像这样:

    [CmdletBinding()]
    param(
    [ValidateSet(4,6,8,10,12,20)]
    [int]$Sides = 20,
    [int]$Dice
    )
    $total = 0
    while ($Dice -gt 0) {
    $result = (Get-Random -Minimum 1 -Maximum $Sides)
    $Dice -= 1
    $total += $result
    write-output "die says $result"
    }
    Write-Output "The total is $total"
    
  7. 这是因为没有为$Dice分配默认值。我们可以为它分配一个默认值,但更好的方法可能是通过添加[Parameter(Mandatory)]使其成为一个必需的参数,如下所示:

    param(
    [ValidateSet(4,6,8,10,12,20)]
    [int]$Sides = 20,
    [Parameter(Mandatory)]
    [int]$Dice
    )
    
  8. 我们可以使两个参数都成为必需参数,并包括一个HelpMessage属性,解释每个参数需要输入什么。

  9. 所以,第一件事是将100的值添加到$``sides参数的ValidateSet属性中。

    完成后,我们需要以不同的方式处理100的值,所以不能只是将它添加到循环中。我使用了ifelse语句。以下是我的最终脚本:

图 A.10 – 我的 15 级圣武士会击败你的混乱邪恶牧师

图 A.10 – 我的 15 级圣武士会击败你的混乱邪恶牧师

记住,有很多方法可以做到这一点;如果你的代码与我的完全不同,但它能正常工作,那也没问题。

第九章

活动

  1. 没有任何效果,因为我们的参数没有写成接受管道输入。正如我们在 第八章 中发现的,编写我们的第一个脚本 – 将简单的 Cmdlet 转化为可重用的代码,要使参数接受管道输入,我们必须添加一个参数,如下所示:

图 A.11 – 从管道接收值

图 A.11 – 从管道接收值

在第 9 行,我们为参数添加了 ValueFromPipeline 参数,这样它就可以接受来自管道的值。我们还将函数包含在一个 process 块中,在第 12 行打开,第 20 行关闭;如果没有 process 块,函数将仅作用于管道中的最后一个值。

  1. 因为 Get-Random 只接受一个位置参数,-Maximum。如果我们像之前那样运行,那么最大值将被设置为 15,而 cmdlet 不知道如何处理 20 这个值。同样,Get-Fifteen20 15 -maximum 20 也无法正常工作,因为 -Maximum 参数已经由命名值 20 填充,所以它不知道如何处理 15 这个值。然而,Get-Fifteen20 -minimum 15 20 是可以工作的。

  2. 这总是有很多方法可以做。我的方法如下:

    function Remove-Log {
    $limit = (Get-Date).AddDays(-7)
    Get-ChildItem -Path "C:\temp" -Include "MyLogFile*" -Recurse -Force |
    Where-Object {$_.CreationTime -lt $limit} |
    Remove-Item -Force
    }
    

    我创建了一个名为 Remove-Log 的函数, 我可以在脚本中调用它。我创建了一个名为 $limit 的变量,它获取运行时日期前七天的日期。然后我使用通配符从 C:\temp 目录中获取所有以 MyLogFile 开头的项。接着我使用 Where-Object 对列表进行过滤,只选择早于 $limit 日期的文件。最后,我将其管道传输到 Remove-Item,并使用 -Force 参数来抑制任何确认。

练习

  1. 避免草率的抽象化 – 这是一个软件工程原则,鼓励我们仅在知道需要它并且确切知道它需要做什么时才创建抽象,例如函数。

  2. 因为点源会导致被调用的内容在本地或父作用域中运行,而不是在适当的子作用域中运行。

  3. 因为仅仅调用变量会产生脚本块中的代码;但它不会执行。我们需要使用 invoke() 方法、调用操作符或 Invoke-Command cmdlet。我们不应该在没有仔细考虑的情况下点源它。

  4. ValidatePattern 验证属性应该能解决这个问题,但我们需要使用正则表达式。唉!希望你已经查阅了 进一步阅读 部分中提到的帮助文件。

  5. 因为过滤器期望管道输入,而我们没有提供任何输入。然而,365 | get-square 将是有效的。

  6. 我们正在防止 $number 变量从另一个作用域中访问。

  7. 函数是有名字的,而脚本块是匿名的。

  8. 我们正尝试通过管道传递一个值,但没有参数接受管道输入。我们需要将其设置为高级函数并创建一个接受管道输入的参数,或者我们需要使用 $Args

  9. 我们将按如下方式编写函数:

    Function get-root($a) {
    <what goes here?>
    }
    [math]::Sqrt($a)
    

    我们可以这样使用它:

图 A.12 – 简单获取根的方式

图 A.12 – 简单获取根的方式

第十章

活动

  1. -ErrorAction 参数将覆盖 $ErrorActionPreference 变量,而 nosuchfile 字符串将导致一个终止错误。由于这是一个终止错误,cmdlet 将不会处理 bar.txt

  2. 因为如果出现错误,错误对象会被放入管道中,替代导致错误的字符串。

练习

  1. 终止错误会完全停止脚本执行。非终止错误可能会停止脚本执行当前步骤,但 PowerShell 会继续执行脚本的下一步。

  2. 可以使用 Get-Error 命令,它会显示最近的错误对象,或者使用 $Error 变量。这个变量包含了会话期间创建的所有错误对象,默认最大数量为 256。

  3. -ErrorActionPreference 变量允许我们为特定 PowerShell 会话中运行的所有 cmdlet 和脚本设置默认的错误处理偏好。它决定了错误是否应该显示、忽略,或者以特定方式处理。

  4. Write-Error cmdlet 允许我们手动生成并在脚本中显示自定义的非终止错误消息。当我们希望明确向用户或调用代码信号错误状态时,它非常有用。

  5. 生成一个终止错误,这个错误可以通过 try/catch 语句对进行处理。

  6. 可以通过在 cmdlet 或高级脚本中使用 -Debug 参数,或者将 $DebugPreference 变量设置为 Continue 来启用调试;默认值是 SilentlyContinue

  7. 通过在脚本中使用 Write-Debug cmdlet。调试消息是为编写代码的人提供的;错误消息是为使用代码的人提供的。因此,调试消息应该包含有关脚本当前状态的详细信息,可能包括变量值和步骤计数。

  8. 断点是设置在脚本中的一个标记,用于在特定行或条件下暂停脚本执行。它允许我们检查该点时脚本和变量的状态。网球中的断点与此完全不同。

  9. 它执行脚本的当前行并继续到下一行,但会把整个函数当作一行来执行。所以,如果下一行代码是一个函数,或者我们当前在一个函数中,那么整个函数调用会完成,而不是继续执行函数中的下一行。

第十一章

活动

尝试运行 Remove-Module 来移除我们刚刚安装的模块,然后运行 Get-Square。会发生什么?为什么会发生这种情况?

Get-Square cmdlet 仍然可用。这是因为我们正确地将模块保存在模块路径中;这意味着当我们调用模块中的函数时,PowerShell 会自动加载该模块。我们可以在以下截图中看到它的工作原理:

图 A.13 – 使用 PowerShell 自动加载

图 A.13 – 使用 PowerShell 自动加载

在第一行,我列出了加载的模块。然后,我运行 Get-Square 7,这将自动加载 MyFirstModule。之后,我通过再次运行 Get-Module 确认了这一点。我们可以看到,通过在最后一行使用 -Verbose 开关运行 Remove-Module,它移除了 Get-Square cmdlet。

练习

  1. Get-Module

  2. 它将模块导入到全局作用域。请注意,当我们从命令提示符导入模块时,它已经被导入到全局作用域——我们在从另一个模块内部导入模块时会使用这一点;这就是嵌套模块的情况。

  3. 我们需要在 Import-Module-Name 参数中提供模块的完整路径。

  4. 如果它们不是我们要使用的函数,可以使用 -NoClobber 参数,或者如果是我们要使用的函数,可以使用 -Prefix 参数。

  5. 可以通过模块文件中的 Export-ModuleMembers cmdlet 或使用模块清单来实现。

  6. 它提供了一个指向模块在线文档的链接,允许帮助文档得到更新。

  7. 在模块的上下文中,它可能是函数或 cmdlet 输出的格式化信息,或者是自定义类型信息。

  8. 我们将获取 cmdlet;.dll 扩展名表示二进制模块,因此其中的命令将属于 Cmdlet 类型。脚本模块包含 Function 类型的命令。

  9. 因为它们很慢。CDXML 会被解析成 PowerShell 脚本,然后该脚本需要被解释。

第十二章

练习

  1. New-PSSession 创建一个持久会话,但可以使用 Enter-PSSession 创建一个临时会话。

  2. 通过使用 SSH。

  3. AllSigned 执行策略只允许运行由受信发布者签名的脚本。

  4. -ExecutionPolicy Bypass 开关用于在运行脚本时暂时绕过执行策略。

  5. PowerShell AMSI。

  6. 受限语言模式用于限制访问 PowerShell 中的危险 cmdlet 和脚本功能。

  7. 通过使用 JEA。

  8. 脚本块日志记录会记录在 PowerShell 中执行的脚本块的内容,从而提供对潜在恶意操作的可视性。它将内容记录在 Windows 的 PowerShellCore 操作事件日志中,在 Linux 中则记录在 systemd 日志中。

  9. 安全字符串是类型为 System.Security.SecureString 的 PowerShell 对象,已加密字符串是已使用密钥加密的字符串对象,因此无法在文件中读取。

第十三章

活动

  1. 我们可以用Invoke-CimMethod编写一个相当复杂的 cmdlet,但这不是最好的方法。相反,我们应该使用专门为此目的编写的 cmdlet,Stop-Process,并指定notepad进程的ProcessId

    Invoke-CimMethod like this:
    

图 A.14 – 使用 CIM 命令设置默认打印机

图 A.14 – 使用 CIM 命令设置默认打印机

在第一个命令中,我将打印机对象放入一个变量中,然后使用该变量作为Invoke-CimMethod的输入对象,并调用SetDefaultPrinter方法。返回值0表示成功。

请注意,SetDefaultPrinter方法没有出现在$printer变量中。不幸的是,我们需要阅读文档来发现这个方法:

练习

  1. 显示模块。

  2. 它可能是在 PowerShell Core 发布之前编写的,作者可能没有在清单中包含兼容性信息,或者它可能不是一个清单模块。

  3. Windows PowerShell 5.1。

  4. 反序列化的对象。

  5. 使用- UseWindowsPowershell参数。

  6. __NAMESPACE

  7. 它允许我们在查询远程机器时指定不同的超时时间,因为默认情况下每台机器的超时时间是 3 分钟。

  8. 我们可能可以使用Set-CimInstance,但由于许多属性是不可写的,我们更可能会使用Invoke-CimMethod

  9. 我们将它们作为Dictionary哈希表传递给- Arguments参数。

第十四章

活动

由于 PowerShell 对于文件路径分隔符非常宽容,唯一需要根据平台不同而不同的代码部分是如何获取机器名称。其他部分非常简单。这是我的解决方案;你的解决方案可能会有很大不同,但仍能完成任务:

if ($IsWindows) {
$computername = $env:COMPUTERNAME
}
elseif ($IsLinux) {
$computername = (hostname)
}
Get-Process |
Sort-Object -Property CPU -Descending |
Select-Object -First 5 |
Out-File "$($computername)_processes.txt"

因为我们需要使用不同的方法来获取机器名称,所以这两行代码位于if语句中。其他部分在 Linux 和 Windows 上工作相同,所以非常简单。这是在我的 CentOS 机器上运行时的样子:

图 A.15 – 在 CentOS 上运行跨平台脚本

图 A.15 – 在 CentOS 上运行跨平台脚本

如我们所见,它运行得很好。这个脚本可以加入一些错误检查;例如,如果两个自动变量都是false,该怎么办?

练习

  1. 互联网。与大多数 Linux 发行版一样,Kali Linux 不受 Microsoft 支持。

  2. 诱导性问题。实际上,我们在 Linux 上调用的是ls Bash 命令。当我们在 Windows 上输入它时,实际上是通过ls别名调用Get-ChildItem

  3. 无论是\还是/,都可以,幸运的是。这意味着编写跨平台脚本变得更加容易。

  4. 通过调用$IsMacOS变量。如果返回true,那么我们就在 macOS 上运行。

  5. 通过使用sudo pwsh命令启动 PowerShell。PowerShell 会话中没有提升权限的方式。

  6. 使用带有-``HostName参数的New-PSSession

  7. 使用带有-``KeyFilePath参数的New-PSSession

  8. 这是一个文件传输程序,包含在 PowerShell 7 的安装包中。

  9. Ed25519 是一种基于公钥/私钥的数字签名加密算法,较新且更安全。

第十五章

答案

  1. 因为 Zero 和 Pico 使用 ARMv6 芯片架构,这与.NET 不兼容,因为.NET 要求 ARMv7 或 ARMv8 架构。

  2. 无论是C:\Users\<用户名>\.sshconfig还是C:\ProgramData\ssh\ssh_config

  3. Test-NetConnection

  4. ssh <用户名>@<主机名>

  5. sudo bash ./install.sh

  6. 因为每次都输入~/powershell/pwsh很麻烦。

  7. sudo ln -s ~/****powershell/pwsh /usr/bin/pwsh

  8. 使用带有-Raw开关参数的Get-GpioPin cmdlet。

  9. Debian Linux,类似于 Ubuntu。

第十六章

练习

  1. $object = New-Object -****TypeName Namespace.ClassName

  2. Add-Type -****AssemblyName “AssemblyName”

  3. [****Namespace.ClassName]::MethodName()

  4. [****Namespace.ClassName]::PropertyName

  5. $object = New-Object -TypeName Namespace.ClassName -ArgumentList (arg1, arg2, ...)

  6. Add-Type -****Path “Path\To\Assembly.dll”

  7. 使用 $object.GetType()

  8. $object | Get-Member -****MemberType Method

  9. $****object.MethodName()

  10. $****object.PropertyName

posted @ 2025-07-04 15:40  绝不原创的飞龙  阅读(56)  评论(0)    收藏  举报