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!')

所以使用规则提炼如下:

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

PowerShell 技能连载 - 使用类(构造函数 - 第五部分)

Class 也可以称为构造函数。构造函数是创建一个新对象的方法。构造函数只是和类名相同的方法。通过构造函数,可以更简单地创建事先为属性赋过值的对象。以下是一个例子:“Person”类定义了一个 person。

以下是一个构造函数,输入姓和名,以及生日。当一个对象实例化的时候,构造函数会被调用,并且事先填充好对象的属性:

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 Person
{
[string]$FirstName
[string]$LastName
[int][ValidateRange(0,100)]$Age
[DateTime]$Birthday

# constructor
Person([string]$FirstName, [string]$LastName, [DateTime]$Birthday)
{
# set object properties
$this.FirstName = $FirstName
$this.LastName = $LastName
$this.Birthday = $Birthday
# calculate person age
$ticks = ((Get-Date) - $Birthday).Ticks
$this.Age = (New-Object DateTime -ArgumentList $ticks).Year-1
}
}

有了这个类之后,您可以很方便地创建 person 对象的列表:

1
2
3
[Person]::new('Tobias','Weltner','2000-02-03')
[Person]::new('Frank','Peterson','1976-04-12')
[Person]::new('Helen','Stewards','1987-11-19')

结果类似如下:

1
2
3
4
5
FirstName LastName Age Birthday
--------- -------- --- --------
Tobias Weltner 16 2/3/2000 12:00:00 AM
Frank Peterson 40 4/12/1976 12:00:00 AM
Helen Stewards 29 11/19/1987 12:00:00 AM

PowerShell 技能连载 - 使用类(重载 - 第四部分)

类中的方法可以重载:您可以定义多个同名的方法,但是参数类型不同。它用起来和 cmdlet 中的参数集类似。请看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#requires -Version 5.0
class StopWatch
{
# property is marked "hidden" because it is used internally only
# it is not shown by IntelliSense
hidden [DateTime]$LastDate = (Get-Date)

# when no parameter is specified, do not emit verbose info

[int] TimeElapsed()
{
return $this.TimeElapsedInternal($false)
}

# user can decide whether to emit verbose info or not
[int] TimeElapsed([bool]$Verbose)
{
return $this.TimeElapsedInternal($Verbose)
}

# this method is called by all public methods

hidden [int] TimeElapsedInternal([bool]$Verbose)
{
# get current date
$now = Get-Date
# and subtract last date, report back milliseconds
$milliseconds = ($now - $this.LastDate).TotalMilliseconds
# use $this to access internal properties and methods
# update the last date so that it now is the current date
$this.LastDate = $now
# output verbose information if requested
if ($Verbose) {
$VerbosePreference = 'Continue'
Write-Verbose "Last step took $milliseconds ms." }
# use "return" to define the return value
return $milliseconds
}

Reset()
{
$this.LastDate = Get-Date
}
}

# create instance
$stopWatch = [StopWatch]::new()

# do not output verbose info
$stopWatch.TimeElapsed()


Start-Sleep -Seconds 2
# output verbose info
$stopWatch.TimeElapsed($true)

$a = Get-Service
# output verbose info
$stopWatch.TimeElapsed($true)

结果类似如下:

1
2
3
4
5
0
VERBOSE: Last step took 2018.1879 ms.
2018
VERBOSE: Last step took 68.8883 ms.
69

PowerShell 技能连载 - 使用类(增加方法 - 第三部分)

相对于 [PSCustomObject],使用 class 的最大好处之一是它也可以定义方法(命令)。以下例子实现了秒表功能。秒表可以用来计算代码执行了多少时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#requires -Version 5.0
class StopWatch
{
# property is marked "hidden" because it is used internally only
# it is not shown by IntelliSense
hidden [DateTime]$LastDate = (Get-Date)

[int] TimeElapsed()
{
# get current date
$now = Get-Date
# and subtract last date, report back milliseconds
$milliseconds = ($now - $this.LastDate).TotalMilliseconds
# use $this to access internal properties and methods
# update the last date so that it now is the current date
$this.LastDate = $now
# use "return" to define the return value
return $milliseconds
}

Reset()
{
$this.LastDate = Get-Date
}
}

以下是秒表的使用方法:

1
2
3
4
5
6
7
8
9
10
# create instance
$stopWatch = [StopWatch]::new()

$stopWatch.TimeElapsed()

Start-Sleep -Seconds 2
$stopWatch.TimeElapsed()

$a = Get-Service
$stopWatch.TimeElapsed()

结果类似如下:

1
2
3
0
2018
69

当您在一个函数中定义方法时,要遵守一系列规则:

  • 如果一个方法有返回值,那么必须指定返回值的数据类型
  • 方法的返回值必须用关键字“return”来指定
  • 方法中不能使用未赋值的变量,也不能从父作用域中读取变量
  • 要引用这个类中的属性或方法,请在前面加上“$this.