PowerShell 技能连载 - 探索对象

在 PowerShell 中,一切都是用对象描述。以下是一个检查任意对象并将它的成员以文本的方式复制到剪贴板的单行代码:

1
2
3
4
5
"Hello" |
Get-Member |
Format-Table -AutoSize -Wrap |
Out-String -Width 150 |
clip.exe

只需要将 “Hello” 替换成任何变量或命令,然后看看复制了什么到剪贴板中。您可以将信息粘贴到文本编辑器或文字处理器中,并将它打印出来或转成 PDF 备用。

PowerShell 技能连载 - 管理比特标志位(第四部分)

在 PowerShell 5 中,对枚举的新支持特性使得处理比特位比您在前面的 PowerShell 技能中看到的简单得多。现在设置或清除比特位不再需要冗长的逻辑操作符。

我们先定义一个枚举类型,这样更好管理十进制数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#requires -Version 5

[Flags()]
enum GardenPartyItems
{
Chair = 0
Table = 1
Barbecue = 2
Fridge = 4
Candle = 8
Knife = 16
}

$decimal = 11
[GardenPartyItems]$flags = $decimal
$flags

现在,十进制数的比特位可以很容易地转化为 GardenPartyItem 的列表:

1
2
3
4
PS C:\>  [GardenPartyItems]11
Table, Barbecue, Candle

PS C:\>

注意:将十进制数转换为枚举型时,请确保枚举型中定义了所有的比特。如果十进制数太大,包含枚举型之外的比特时,转换会失败。

要增加一个新的标志位,请试试以下的代码:

1
2
3
4
5
6
7
8
9
PS C:\> $flags
Table, Barbecue, Candle

PS C:\> $flags += [GardenPartyItems]::Knife

PS C:\> $flags
Table, Barbecue, Candle, Knife

PS C:\>

要移除一个标志位,请试试以下代码:

1
2
3
4
5
6
7
8
9
PS C:\> $flags
Table, Barbecue, Candle, Knife

PS C:\> $flags -= [GardenPartyItems]::Candle

PS C:\> $flags
Table, Barbecue, Knife

PS C:\>

然而,实际上并没有看起来这么简单。当移除一个已有的标志位,没有问题。但移除一个没有置位的标志位,会把比特值搞乱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS C:\> $flags
Table, Barbecue, Candle

PS C:\> $flags -= [GardenPartyItems]::Candle

PS C:\> $flags
Table, Barbecue

PS C:\> $flags -= [GardenPartyItems]::Candle

PS C:\> $flags
-5

PS C:\>

所以 PowerShell 在自动处理二进制算法方面明显还不够智能。要安全地使用该功能,您还是要用二进制操作符。要移除标志位,请使用 -band-bnot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS C:\> $flags
Table, Barbecue, Candle

PS C:\> $flags = $flags -band -bnot [GardenPartyItems]::Candle

PS C:\> $flags
Table, Barbecue

PS C:\> $flags = $flags -band -bnot [GardenPartyItems]::Candle

PS C:\> $flags
Table, Barbecue

PS C:\>

要设置标志位,请使用 -bor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS C:\> $flags
Table, Barbecue, Candle

PS C:\> $flags = $flags -bor [GardenPartyItems]::Knife

PS C:\> $flags
Table, Barbecue, Candle, Knife

PS C:\> $flags = $flags -bor [GardenPartyItems]::Knife

PS C:\> $flags
Table, Barbecue, Candle, Knife

PS C:\>

在所有这些操作中,实际上是在操作一个十进制数:

1
2
PS C:\> [Int]$flags
19

相当棒,对吧?

PowerShell 技能连载 - 管理比特标志位(第三部分)

对十进制数设置比特标志位不是很难,但是不够直观。以下是一个快速的新方法,演示如何设置或取消一个数字中特定的比特:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$decimal = 6254
[Convert]::ToString($decimal, 2)

# set bit 4
$bit = 4
$decimal = $decimal -bor [Math]::Pow(2, $bit)
[Convert]::ToString($decimal, 2)

# set bit 0
$bit = 0
$decimal = $decimal -bor [Math]::Pow(2, $bit)
[Convert]::ToString($decimal, 2)

# clear bit 1
$bit = 1
$decimal = $decimal -band -bnot [Math]::Pow(2, $bit)
[Convert]::ToString($decimal, 2)

结果演示了代码做了什么。ToString() 从右到左显示比特,所以第 0 比特是在最右边。在第二行和第三行,设置了两个独立的比特位,而并不影响其它位。在最后一行中,清除了一个比特位。

1
2
3
4
1100001101110
1100001111110
1100001111111
1100001111101

PowerShell 技能连载 - 管理比特标志位(第二部分)

在前一个技能中我们演示了如何使用 PowerShell 5 新的枚举特性来解析bite标志位,甚至可以独立地检测每个标志位。

如果您无法使用 PowerShell 5,在早期的 PowerShell 版本中,仍然可以使用这个技术只需要通过 C# 代码来定义枚举即可:

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
# this is the decimal we want to decipher
$rawflags = 56823


# define an enum with the friendly names for the flags
# don't forget [Flags]
# IMPORTANT: you cannot change your type inside a PowerShell session!
# if you made changes to the enum, close PowerShell and open a new
# PowerShell!
$enum = '
using System;
[Flags]
public enum BitFlags
{
None = 0,
Option1 = 1,
Option2 = 2,
Option3 = 4,
Option4 = 8,
Option5 = 16,
Option6 = 32,
Option7 = 64,
Option8 = 128,
Option9 = 256,
Option10= 512,
Option11= 1024,
Option12= 2048,
Option13= 4096,
Option14= 8192,
Option15= 16384,
Option16= 32768,
Option17= 65536
}
'
Add-Type -TypeDefinition $enum

# convert the decimal to the new enum
[BitFlags]$flags = $rawflags
$flags

# test individual flags
$flags.HasFlag([BitFlags]::Option1)
$flags.HasFlag([BitFlags]::Option2)

如您所见,从十进制数转换到新的枚举类型使用正常而且非常简单:

1
2
3
4
PS C:\> [BitFlags]6625
Option1, Option6, Option7, Option8, Option9, Option12, Option13

PS C:\>

PowerShell 技能连载 - 管理比特标志位(第一部分)

有时候您会需要处理比特标志位值。一个数字中的每个比特代表一个特定的设置,并且您的代码可能需要决定一个标志位是否置位,而不能影响别的比特。

这常常需要一系列位操作。然而在 PowerShell 5 中,有一个简单得多的办法——标志位枚举。

假设有一个值 56823,并且希望知道哪个比特是置位的。您需要将该数字转换成可视化的比特:

1
2
PS C:\> [Convert]::ToString(56823, 2)
1101110111110111

如果您了解每个比特的意义,那么一个更强大的方法是定义一个枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#requires -Version 5

[flags()]
enum CustomBitFlags
{
None = 0
Option1 = 1
Option2 = 2
Option3 = 4
Option4 = 8
Option5 = 16
Option6 = 32
Option7 = 64
Option8 = 128
Option9 = 256
Option10= 512
Option11= 1024
Option12= 2048
Option13= 4096
Option14= 8192
Option15= 16384
Option16= 32768
Option17= 65536
}

对每个比特提供一个友好的名字,并且记得添加属性 [Flags](这将允许设置多个值)。

现在要解析这个十进制值非常简单——只需要将它转换成新定义的枚举类型:

1
2
$rawflags = 56823
[CustomBitFlags]$flags = $rawflags

这时得到的结果:

1
2
PS C:\> $flags
Option1, Option2, Option3, Option5, Option6, Option7, Option8, Option9, Option11, Option12, Option13, Option15, Option16

如果您只希望检测某个标志位是否置位,请使用 HasFlag() 方法:

1
2
3
4
5
6
7
PS C:\> $flags.HasFlag([CustomBitFlags]::Option1)

True

PS C:\> $flags.HasFlag([CustomBitFlags]::Option4)

False

PowerShell 技能连载 - 使用泛型

泛型可以作为实际类型的占位符,您可能会好奇为什么它会有意思。

有许多不同的数据类型没有 NULL 值。例如 Integer 和 Boolean 型,没有办法指出一个值是非法的还是未设置。您可以通过将一个 0(或者 -1)指定为某个 integer 变量的 “undefined” 值。但如果所有的数字都是合法的值呢?对于 Boolean,情况也是一样:虽然您可以定义 $false 值为 “undefined” 值,但许多情况下的确需要三种值:$true$flaseundefined

泛型是解决的办法,您可以使用 Nullable 类型根据任何合法的类型来创建自己的可空值类型。

1
2
3
4
5
[Nullable[int]]$number =  $null
[Nullable[bool]]$flag = $null

$number
$flag

用常规数据类型来做数据转换:

1
2
3
4
5
6
7
PS C:\> [int]$null
0

PS C:\> [bool]$null
False

PS C:\>

PowerShell 技能连载 - Power Shell 5 的类继承(第二部分)

以下是在 PowerShell 5 中使用新的类特性的另一个用例。在前一个例子中,我们演示了如何从 System.Diagnostics.Process 派生一个新类,从而获得代表进程的功能更强大的对象。

以下是一个从 WebClient 派生的类,WebClient 主要是用来连接网站。当您使用标准的 WebClient 对象是,它拒绝连接到证书错误的 HTTPS 网站。这是一件好事,但是有时候您仍需要连接这类网站。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#requires -Version 5

class MyWebClient : System.Net.WebClient
{
MyWebClient() : base()
{
# with SSL certificate errors, connect anyway
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$proxy = [System.Net.WebRequest]::GetSystemWebProxy()
$proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials
$this.Proxy = $proxy
$this.UseDefaultCredentials = $true
$this.Proxy.Credentials = $this.Credentials
}
}

$client = [mywebclient]::new()
$client.DownloadString('http://www.psconf.eu')

这样,”“MyWebClient”“ 类继承于 WebClient() 并改变了 ServerCertificateValidationCallBack 的行为。它只是返回 $true,所以所有的连接都是成功的,而且证书检验变得无关紧要。

PowerShell 技能连载 - Power Shell 5 的类继承(第一部分)

PowerShell 5 内置了类的支持。您可以使用这个新特性来增强已有的 .NET 类的功能。以下是一个例子:创建一个包含新功能的增强的进程类。

进程通常是由 System.Diagnostics.Process 对象代表。它们只有有限的功能,并且假设没有能直接使用的以友好方式关闭一个应用程序的方法。您可以杀除进程(会丢失未保存的数据),或关闭它(用户可以取消关闭)。

以下是一个新的 继承于 System.Diagnostics.Process 的名为 AppInstance 的类。所以它拥有 Process 类中所有已有的功能,您可以增加额外的属性和方法:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#requires -Version 5
class AppInstance : System.Diagnostics.Process
{
# Constructor, being called when you instantiate a new object of
# this class
AppInstance([string]$Name) : base()
{
# launch the process, get a regular process object, and then
# enhance it with additional functionality
$this.StartInfo.FileName = $Name
$this.Start()
$this.WaitForInputIdle()
}

# for example, rename an existing method
[void]Stop()
{
$this.Kill()
}

# or invent new functionality
# Close() closes the window gracefully. Unlike Kill(),
# the user gets the chance to save unsaved work for
# a specified number of seconds before the process
# is killed
[void]Close([Int]$Timeout = 0)
{
# send close message
$this.CloseMainWindow()
# wait for success
if ($Timeout -gt 0)
{
$null = $this.WaitForExit($Timeout * 1000)
}
# if process still runs (user aborted request), kill forcefully
if ($this.HasExited -eq $false)
{
$this.Stop()
}
}

# example of how to change a property like process priority
[void]SetPriority([System.Diagnostics.ProcessPriorityClass] $Priority)
{
$this.PriorityClass = $Priority
}

[System.Diagnostics.ProcessPriorityClass]GetPriority()
{
if ($this.HasExited -eq $false)
{
return $this.PriorityClass
}
else
{
Throw "Process PID $($this.Id) does not run anymore."
}
}

# add static methods, for example a way to list all processes
# variant A: no arguments
static [System.Diagnostics.Process[]] GetAllProcesses()
{
return [AppInstance]::GetAllProcesses($false)
}
# variant B: submit $false to see only processes that have a window
static [System.Diagnostics.Process[]] GetAllProcesses([bool]$All)
{
if ($All)
{
return Get-Process
}
else
{
return Get-Process | Where-Object { $_.MainWindowHandle -ne 0 }
}
}
}

# you can always run static methods
[AppInstance]::GetAllProcesses($true) | Out-GridView -Title 'All Processes'
[AppInstance]::GetAllProcesses($false) | Out-GridView -Title 'Processes with Window'


# this is how you instantiate a new process and get back
# a new enhanced process object
# classic way:
# $notepad = New-Object -TypeName AppInstance('notepad')
# new (and faster) way in PowerShell 5 to instantiate new objects:
$notepad = [AppInstance]::new('notepad')

# set a different process priority
$notepad.SetPriority('BelowNormal')

# add some text to the editor to see the close message
Start-Sleep -Seconds 5

# close the application and offer to save changes for a maximum
# of 10 seconds
$notepad.Close(10)

如您在这个例子中所见,当您从这个类创建一个新实例时,它启动了一个新的进程,而且这些进程照常暴露出相同的属性和方法。而且,有一些新的例如 SetPriority()Close() 的新方法。

PowerShell 技能连载 - 显示或隐藏窗口

PowerShell 可以调用 Windows 内部的 API,在这个例子中,我们想向您展示如何改变一个应用程序窗口的显示状态。比如可以最大化、最小化、隐藏或显示窗口。

这个例子使用 PowerShell 5 最新的枚举特性对 showstate 数值赋予有意义的名字。在 PowerShell 的更早版本中,只需要移除枚举部分,并在代码中直接使用合适的 showstate 数字即可。

这里的学习要点是如何使用 Add-Type 来包装一个 C# 形式的 API 方法并在 PowerShell 代码中返回一个暴露这个方法的 type:

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
#requires -Version 5
# this enum works in PowerShell 5 only
# in earlier versions, simply remove the enum,
# and use the numbers for the desired window state
# directly

Enum ShowStates
{
Hide = 0
Normal = 1
Minimized = 2
Maximized = 3
ShowNoActivateRecentPosition = 4
Show = 5
MinimizeActivateNext = 6
MinimizeNoActivate = 7
ShowNoActivate = 8
Restore = 9
ShowDefault = 10
ForceMinimize = 11
}


# the C#-style signature of an API function (see also www.pinvoke.net)
$code = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'

# add signature as new type to PowerShell (for this session)
$type = Add-Type -MemberDefinition $code -Name myAPI -PassThru

# access a process
# (in this example, we are accessing the current PowerShell host
# with its process ID being present in $pid, but you can use
# any process ID instead)
$process = Get-Process -Id $PID

# get the process window handle
$hwnd = $process.MainWindowHandle

# apply a new window size to the handle, i.e. hide the window completely
$type::ShowWindowAsync($hwnd, [ShowStates]::Hide)

Start-Sleep -Seconds 2
# restore the window handle again
$type::ShowWindowAsync($hwnd, [ShowStates]::Show)

请注意这个例子将 PowerShell 窗口临时隐藏 2 秒钟。您可以对任何运行中的应用程序窗口做相同的事情。只需要用 Get-Process 来查找目标进程,并使用它的 “MainWindowHandle“ 属性来发送 showstate 改变请求。

一些应用程序有多个窗口。在这种情况下,您只能针对主窗口操作,否则需要先靠其它 API 来获取子窗口的句柄集合。

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>

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