PowerShell 技能连载 - 用 Pester Tests 做测试

Pester 是一个随 Windows 10 和 Windows Server 2016 发布的开源模块,可以通过 PowerShell Gallery 免费下载(需要事先安装最新版本的 PowerShellGet):

1
PS C:\> Install-Module -Name Pester -Force -SkipPublisherCheck

Pester 是一个主要用来测试 PowerShell 代码的测试框架。您不仅可以用它来测试代码,而且可以用它来测试任何东西。以下是一个测试 PowerShell 版本号和一些设置的小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Describe 'PowerShell Basic Check' {

Context 'PS Versioning' {
It 'is current version' {
$host.Version.Major -ge 5 -and $host.Version.Minor -ge 1 | Should Be $true
}
}
Context 'PS Settings' {
It 'can execute scripts' {
(Get-ExecutionPolicy) | Should Not Be 'Restricted'
}
It 'does not use AllSigned' {
(Get-ExecutionPolicy) | Should Not Be 'AllSigned'
}
It 'does not have GPO restrictions' {
(Get-ExecutionPolicy -Scope MachinePolicy) | Should Be 'Undefined'
(Get-ExecutionPolicy -Scope UserPolicy) | Should Be 'Undefined'
}
}
}

当您运行它时(当然,前提是已经安装了 Pester 模块),这是得到的输出结果:

1
2
3
4
5
6
7
8
9
10
11
Describing PowerShell Basic Check

Context PS Versioning
[+] is current version 76ms

Context PS Settings
[+] can execute scripts 47ms
[+] does not use AllSigned 18ms
[+] does not have GPO restrictions 21ms

PS>

当然,这只是一个例子。您可以把它做详细并且将测试扩展到更多的其它设置或依赖条件。

PowerShell 技能连载 - 读取最新的环境变量

当您在 PowerShell 中读取环境变量时,您可能会使用 “env:“ 驱动器。例如这行代码使用 %USERNAME% 环境变量,告知您执行这段代码的用户名:

1
2
3
4
PS C:\> $env:USERNAME
tobwe

PS C:\>

env: 驱动器总是存取环境变量的操作集合。所以大多数情况所有环境变量(例如 “UserName”)都定义在这个集合之中。基本上,环境变量的操作集合是当一个应用程序启动时所有环境变量的“快照”,加上一些额外的信息(例如 “UserName”)。

要从系统或用户集合中读取最新的环境变量,请使用类似如下的代码:

1
2
3
4
5
$name = 'temp'
$scope = [EnvironmentVariableTarget]::Machine

$content = [Environment]::GetEnvironmentVariable($name, $scope)
"Content: $content"

例如您可以使用这个技术在两个进程间通信。实践方法是,打开两个 PowerShell 控制台。现在在第一个控制台中键入以下信息:

1
[Environment]::SetEnvironmentVariable("PS_Info", "Hello", "user")

在第二个 PowerShell 控制台中,键入这行代码来接收信息:

1
[Environment]::GetEnvironmentVariable("PS_Info", "user")

要清除环境变量,请在任意一个控制台中输入这行代码:

1
[Environment]::SetEnvironmentVariable("PS_Info", "", "user")

PowerShell 技能连载 - 设置环境变量

当通过 PowerShell 的 “env:“ 驱动器来设置环境变量,您只需要操作其中的数据即可。它会应用到当前的 PowerShell 实例,和所有由它启动的应用程序。不过改变并不会保存。

要设置环境变量并使之持久化,请用这段代码代替:

1
2
3
4
5
$name = 'Test'
$value = 'hello'
$scope = [EnvironmentVariableTarget]::User

[Environment]::SetEnvironmentVariable($name, $value, $scope)

这个例子设置了一个名为 “test”,值为 “hello” 的用户级别新环境变量。请注意这个改变只会影响设置这个变量之后启动的应用程序。

要彻底删除一个环境变量,请将 $value 设置成一个空字符串。

PowerShell 技能连载 - 检测宿主

在过去,Microsoft 发布了两个 PowerShell 宿主 (host):一个时基本的 PowerShell 控制台,以及更复杂的 PowerShell ISE。一些用户使用类似以下的代码来分辨脚本时运行在控制台中还是运行在 PowerShell ISE 中:

1
2
3
$inISE = $psISE -ne $null

"Running in ISE: $inISE"

然而,现在有越来越多的宿主。Visual Studio 可以作为 PowerShell 的宿主,Visual Studio Code 也可以。而且还有许多商业编辑器。所以您需要确定一个脚本是否在一个特定的环境中运行,请使用宿主标识符来代替:

1
2
3
4
$name = $host.Name
$inISE = $name -eq 'Windows PowerShell ISE Host'

"Running in ISE: $inISE"

Each host emits its own host name, so this approach can be adjusted to any host. When you run a script inside Visual Studio Code, for example, the host name is “Visual Studio Code Host”.
每个宿主会提供它的宿主名称,所以这种方法可以适用于任何宿主。例如当您在 Visual Studio Code 中运行一个脚本,宿主名会变为 “Visual Studio Code Host”。

PowerShell 技能连载 - 接触 PowerShell 6.0

PowerShell 现在是开源的,而且 PowerShell 下一个主要的 release 是在开放的环境中开发。如果您希望看一眼预览版,只需要打开源项目的发布发布页面,并且下载合适的 release:

https://github.com/PowerShell/PowerShell/releases

而且现在 PowerShell 6.0 是跨平台的。您可以同时找到适合 Linux 或 OS X 和 Windows 操作系统的版本。

当您下载某个了 Windows 平台的 ZIP 格式的 release,请先对文件解锁(右键单击文件,选择“属性”,然后解锁)。下一步,解压压缩包。在压缩包里,找到 powershell.exe,可以双击它启动一个新的 PowerShell 6.0 控制台。它是一个完全独立发行的 PowerShell,可以和现有的 PowerShell 版本同时运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PowerShell
Copyright (C) 2016 Microsoft Corporation. All rights reserved.

PS C:\Users\tobwe\Downloads\PowerShell_6.0.0-alpha.15-win10-win2k16-x64> $PSVersionTable

Name Value
---- -----
PSVersion 6.0.0-alpha
CLRVersion
WSManStackVersion 3.0
SerializationVersion 1.1.0.1
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion 2.3
GitCommitId v6.0.0-alpha.15
BuildVersion 3.0.0.0
PSEdition Core


PS C:\Users\tobwe\Downloads\PowerShell_6.0.0-alpha.15-win10-win2k16-x64>

如果您是一个开发者,请查看 GitHub 工程:您可以查看所有源代码,甚至可以加入这个版本的开发者社区。

PowerShell 技能连载 -用 JSON 缓存凭据

当您需要将登录凭据缓存到一个文件,通常的做法是用管道将凭据传给 Export-Clixml 命令,这将会产生一个很长的 XML 文件。使用 Import-Clixml 命令,缓存的凭据可以随时导回脚本中。PowerShell 自动使用用户和机器身份来加密密码(它只能被同一个人在同一台机器上读取)。

可以用 JSON 格式来做同样的事情,并且不会产生更多凌乱的文件。只有对密码加密的部分需要人工完成。

这个例子提示输入登录凭据,然后将它们保存到桌面的 “mycred.json” 文件中,然后在记事本中打开它们,这样您可以查看它的内容并确认密码是加密的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$path = "$home\Desktop\mycred.json"

$cred = Get-Credential
$cred |
Select Username,@{n="Password"; e={$_.password | ConvertFrom-SecureString}} |
ConvertTo-Json |
Set-Content -Path $path -Encoding UTF8


notepad.exe $path

To later reuse the file and import the credential, use this:
$path = "$home\Desktop\mycred.json"

$o = Get-Content -Path $path -Encoding UTF8 -Raw | ConvertFrom-Json
$cred = New-Object -TypeName PSCredential $o.UserName,
($o.Password | ConvertTo-SecureString)

# if you entered a valid user credentials, this line
# will start Notepad using the credentials retrieved from
# the JSON file to prove that the credentials are
# working.
Start-Process notepad -Credential $cred

回头要使用该文件并导入凭据,请使用这段代码:

1
2
3
4
5
6
7
8
9
10
11
$path = "$home\Desktop\mycred.json"

$o = Get-Content -Path $path -Encoding UTF8 -Raw | ConvertFrom-Json
$cred = New-Object -TypeName PSCredential $o.UserName,
($o.Password | ConvertTo-SecureString)

# if you entered a valid user credentials, this line
# will start Notepad using the credentials retrieved from
# the JSON file to prove that the credentials are
# working.
Start-Process notepad -Credential $cred

请注意这个例子将使用存储在 JSON 文件中的凭据来启动记事本的实例。如果您在第一个示例脚本中键入了非法的登录信息,以上操作显然会失败。

也请注意密码事是以加密的方式存储的。加密是以您的账户和机器作为密钥。所以保存的密码是经过安全加密的,但是这里展示的技术只适合同一个人(在同一台机器上)希望下次再使用保存的凭据。一个使用场景是保存再您自己机器上常用的脚本凭据。

PowerShell 技能连载 - 检测有问题的 Execution Policy 设置

PowerShell 用执行策略 (execution policy) 来决定是否执行某个脚本。实际上定义执行策略可以定义 5 种作用域。要查看这所有五种情况,请使用这个命令:

1
2
3
4
5
6
7
8
9
PS C:\> Get-ExecutionPolicy -List

Scope ExecutionPolicy
----- ---------------
MachinePolicy Undefined
UserPolicy Undefined
Process Undefined
CurrentUser RemoteSigned
LocalMachine Undefined

要确定生效的设置,PowerShell 会从上到下遍历这些作用域,然后取第一个非 “Undefined“ 设置。如果所有作用域都设置成 “Undefined“,那么 PowerShell 将使用 “Restricted“ 设置,并且阻止脚本执行。这是缺省的行为。

请永远保持 “MachinePolicy“ 和 “UserPolicy“ 作用域设置成 “Undefined“。这些作用域智能由组策略集中设置。如果它们设置成任何非 “Undefined“ 的值,用户则无法改变生效的设置。

有些公司使用这种方式来限制脚本的执行,它们用 execution policy 作为安全的屏障——而它并不是。”LocalMachine“ 作用域种的 Execution policy 永远应该是缺省值,而不应该强迫一个用户的设置。

如果 “MachinePolicy“ 或 “UserPolicy“ 有设置值,在 PowerShell 5 以及以下版本也有个 bug,可能会导致启动一个 PowerShell 脚本时延迟近 30 秒。这个延迟可能是内部导致的:PowerShell 使用 WMI 确定当前运行的进程,来决定一个 PowerShell 脚本是否作为一个组策略执行,而这种实现方式可能会造成过多的延迟。

所以如果您看见 “MachinePolicy“ 或 “UserPolicy“ 作用域中的设置不是 “Undefined“,您应该和 Active Directory 团队商量并和他们解释执行策略的目的:这是一个偏好设置,而不是一个限制设置。应该使用其它技术,例如 ““Software Restriction Policy” 来安全地限制脚本的使用。

PowerShell 技能连载 - 检查 Execution Policy

execution policy 决定了 PowerShell 能执行哪类脚本。您需要将 execution policy 设置成 UndefinedRestricted,或 Default 之外的值,来允许脚本执行。

对于没有经验的用户,推荐使用 “RemoteSigned“。它可以运行本地脚本,也可以运行位于您信任的网络域的文件服务器上的脚本。它不会运行从 internet 上下载的脚本,或从其它非信任的源获取的脚本,除非这些脚本包含合法的数字签名。

以下是查看和设置当前 execution policy 的方法。

1
2
3
4
5
6
7
8
9
PS C:\> Get-ExecutionPolicy
Restricted

PS C:\> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force

PS C:\> Get-ExecutionPolicy
RemoteSigned

PS C:\>

当您使用 “CurrentUser“ 作用域,那么不需要管理员权限来更改这个设置。这是您个人的安全带,而不是公司级别的安全边界。这个设置将会保持住直到您改变它。

如果您需要确保确保可以无人值守地运行任何地方的脚本,您可能需要使用 “Bypass“ 设置,而不是 “RemoteSigned“。”Bypass“ 允许运行任意位置的脚本,而且不像 ““Unrestricted”“ 那样会弹出确认对话框。

PowerShell 技能连载 - 使用类(静态成员 - 第六部分)

Class 可以定义所谓的“静态”成员。静态成员(属性和方法)可以通过类本身调用,而不需要对象实例。

看看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#requires -Version 5.0
class TextToSpeech
{
# store the initialized synthesizer here
hidden static $synthesizer

# static constructor, gets called whenever the type is initialized
static TextToSpeech()
{
Add-Type -AssemblyName System.Speech
[TextToSpeech]::Synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer
}

# convert text to speech

static Speak([string]$text)
{
[TextToSpeech]::Synthesizer.Speak($text)
}
}

TextToSpeech“ 类包装了文本转语音的一切需要。它使用了静态的构造函数(当定义类型的时候执行)和一个静态方法,所以不需要实例化一个对象。立刻就可以使用 “Speak“ 方法:

1
2
3
# since this class uses static constructors and methods, there is no need
# to instantiate an object
[TextToSpeech]::Speak('Hello World!')

如果您不用“静态”成员来做相同的事情,这个类会长得十分相似。您只需要移除所有 “static“ 关键字,并且通过 $this 代替类型名来存取类的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#requires -Version 5.0
class TextToSpeech
{
# store the initialized synthesizer here
hidden $synthesizer

# static constructor, gets called whenever the type is initialized
TextToSpeech()
{
Add-Type -AssemblyName System.Speech
$this.Synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer
}

# convert text to speech

Speak([string]$text)
{
$this.Synthesizer.Speak($text)
}
}

最显著的区别可能是在用户端:用户现在需要先实例化一个对象:

1
2
$speaker = [TextToSpeech]::new()
$speaker.Speak('Hello World!')

所以使用规则提炼如下:

  • 使用静态成员来实现只需要存在一次的功能(所以文本到语音转换器是一个静态类的好例子)
  • 使用动态成员来实现需要在多于一个实例中同时存在(这样用户可以根据需要实例化任意多个独立的对象)的功能。