PowerShell 技能连载 - 安全地嵌入变量

当您在 PowerShell 中使用双引号时,您可以向字符串中增加变量,PowerShell 能自动将它们替换成它们的值——这并不是什么新鲜事:

1
2
3
$ID = 234

"Server $ID Rack12"

然而,PowerShell 自动判断一个变量的结束位置,所以当您希望在一个文本中插入一个不含空格的数字时,这种写法可能会失败:

1
2
3
$ID = 234

"Server$IDRack12"

如同语法高亮的信息,PowerShell 会将变量识别成 $IDRack12 因为它无法意识到变量名提前结束。

在这些情况下,只需要用大括号将变量名括起来即可:

1
2
3
$ID = 234

"Server${ID}Rack12"

PowerShell 技能连载 - 语音合成 – 使用不同的语音(第 4 部分)

Windows 10 自带优秀的文本转语音功能,以及不同的高品质语音。要查看哪些语音可用,请试试以下代码:

1
2
3
4
5
6
Add-Type -AssemblyName System.speech
$synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer

$synthesizer.GetInstalledVoices().VoiceInfo |
Where-Object { $_.Name -notlike 'Microsoft Server*' } |
Select-Object -Property Name, Gender, Age, Culture

结果类似如下(根据您的 windows 版本、语言文化,和安装的组件可能有所不同):

1
2
3
4
5
Name                    Gender   Age Culture
---- ------ --- -------
Microsoft Zira Desktop Female Adult en-US
Microsoft David Desktop Male Adult en-US
Microsoft Hedda Desktop Female Adult de-DE

要使用这些语音,请用 SelectVoice()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$sampleText = @{
[System.Globalization.CultureInfo]::GetCultureInfo("en-us") = "Hello, I am speaking English! I am "
[System.Globalization.CultureInfo]::GetCultureInfo("de-de") = "Halli Hallo, man spricht deutsch hier! Ich bin "
[System.Globalization.CultureInfo]::GetCultureInfo("es-es") = "Una cerveza por favor! Soy "
[System.Globalization.CultureInfo]::GetCultureInfo("fr-fr") = "Vive la france! Je suis "
[System.Globalization.CultureInfo]::GetCultureInfo("it-it") = "Il mio hovercraft è pieno di anguille! Lo sono "


}


Add-Type -AssemblyName System.speech
$synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer

$synthesizer.GetInstalledVoices().VoiceInfo |
Where-Object { $_.Name -notlike 'Microsoft Server*' } |
Select-Object -Property Name, Gender, Age, Culture |
ForEach-Object {
$_
$synthesizer.SelectVoice($_.Name)
$synthesizer.Speak($sampleText[$_.Culture] + $_.Name)

}

哪些语音可用,依赖于系统已安装了哪些语言。以下链接解释了不同语言的 Windows 10 自带了哪些语音:

https://support.microsoft.com/en-us/help/22797/windows-10-narrator-tts-voices.
请注意 SeletVoice() 并不能使用所有已安装的语音。

PowerShell 技能连载 - 合成语音 – 使用语音合成标记语言 SSML(第 3 部分)

Windows 内置的文字转语音引擎可以输入纯文本,并且将它转换为语音,但它也可以通过“语音合成标记语言”来控制。通过这种方式,您可以对语音调优,控制音调,以及语言。

Windows 自带本地的语音引擎,所以最好控制一下采用的语言。否则,在德文系统上,您的英文文本发音听起来会很奇怪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Add-Type -AssemblyName System.speech
$synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer

$Text = '
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis"
xml:lang="en-US">
<voice xml:lang="en-US">
<prosody rate="1">
<p>Normal pitch. </p>
<p><prosody pitch="x-high"> High Pitch. </prosody></p>
</prosody>
</voice>
</speak>
'
$synthesizer.SpeakSsml($Text)

根据已经安装的语音引擎,您现在甚至可以切换语言:

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
Add-Type -AssemblyName System.speech
$synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer

$Text1 = '
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis"
xml:lang="en-US">
<voice xml:lang="en-US">
<prosody rate="1">
<p>Normal pitch. </p>
<p><prosody pitch="x-high"> High Pitch. </prosody></p>
</prosody>
</voice>
</speak>
'

$text2 = '<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis"
xml:lang="en-US">
<voice xml:lang="de-de">
<prosody rate="1">
<p>Normale Tonhöhe. </p>
<p><prosody pitch="x-high"> Höhere Tonlage. </prosody></p>
</prosody>
</voice>
</speak>'

$synthesizer.SpeakSsml($Text1)
$synthesizer.SpeakSsml($Text2)

如果您想在文字中混合多种语言,您也可以使用传统的 COM 对象 Sapi.SpVoice。以下代码来自前一个技能:

1
2
3
4
5
6
7
8
9
$text = "<LANG LANGID=""409"">Your system will restart now!</LANG>
<LANG LANGID=""407""><PITCH MIDDLE = '2'>Oh nein, das geht nicht!</PITCH></LANG>
<LANG LANGID=""409"">I don't care baby</LANG>
<LANG LANGID=""407"">Ich rufe meinen Prinz! Herbert! Tu was!</LANG>
"

$speaker = New-Object -ComObject Sapi.SpVoice
$speaker.Rate = 0
$speaker.Speak($text)

PowerShell 技能连载 - 合成语音(第 2 部分)

在前一个技能中我们介绍了文字转语音引擎。这个引擎可以将文本转为一个 WAV 声音文件。这样我们可以利用它来生成语音信息:

1
2
3
4
5
6
7
8
9
10
11
$Path = "$home\Desktop\clickme.wav"

Add-Type -AssemblyName System.Speech
$synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer
$synthesizer.SetOutputToWaveFile($Path)
$synthesizer.Rate = -10
$synthesizer.Speak('Uh, I am not feeling that well!')
$synthesizer.SetOutputToDefaultAudioDevice()

# run the WAV file (you can also double-click the file on your desktop!)
Invoke-Item -Path $Path

PowerShell 技能连载 - 合成语音(第 1 部分)

在之前的技能中,我们演示了 PowerShell 如何通过播放系统声音或 WAV 声音文件来生成声音信号。PowerShell 也可以调用内置的语音合成器:

1
2
3
Add-Type -AssemblyName System.speech
$synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer
$synthesizer.Speak('Hello! I am your computer!')

请注意 Windows 10 自带了本地化的文字转语音引擎,所以如果您的 Windows 不是使用英语语言,您可能需要将以上文字转为您的语言。

可以用一系列属性来调整输出的效果。请试试这段代码:

1
2
3
4
Add-Type -AssemblyName System.speech
$synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer
$synthesizer.Rate = -10
$synthesizer.Speak('Uh, I am not feeling that well!')

PowerShell 技能连载 - 播放声音文件

在前一个技能里我们演示了如何用 PowerShell 播放系统声音。对于更灵活一些的场景,PowerShell 也可以播放任意的 *.wav 声音文件:

1
2
3
$soundPlayer = New-Object System.Media.SoundPlayer
$soundPlayer.SoundLocation="$env:windir\Media\notify.wav"
$soundPlayer.Play()

默认情况下,PowerShell 并不会等待声音播放完毕。如果您需要同步播放声音,请试试这段代码:

1
2
3
4
$soundPlayer = New-Object System.Media.SoundPlayer
$soundPlayer.SoundLocation="$env:windir\Media\notify.wav"
$soundPlayer.PlaySync()
"Done."

要播放不同的声音文件,只需要将路径替换为声音文件即可:

sound player 也可以用后台线程循环播放一个文件:

1
2
3
$soundPlayer = New-Object System.Media.SoundPlayer
$soundPlayer.SoundLocation="$env:windir\Media\notify.wav"
$soundPlayer.PlayLooping()

当后台正在播放声音时,请确保用这段代码停止播放声音:

1
$soundPlayer.Stop()

PowerShell 技能连载 - 播放声音

如果您只是需要蜂鸣,那么 PowerShell 可以很轻松地帮助您:

1
2
3
$frequency = 800
$durationMS = 2000
[console]::Beep($frequency, $durationMS)

如果您需要做一些更复杂的事,那么您可以让 PowerShell 播放一段系统声音:

1
[System.Media.SystemSounds]::Asterisk.Play()

以下是支持的系统声音列表:

1
2
3
4
5
6
7
8
9
10
11
12
PS> [System.Media.SystemSounds] | Get-Member -Static -MemberType Property


TypeName: System.Media.SystemSounds

Name MemberType Definition
---- ---------- ----------
Asterisk Property static System.Media.SystemSound Asterisk {get;}
Beep Property static System.Media.SystemSound Beep {get;}
Exclamation Property static System.Media.SystemSound Exclamation {get;}
Hand Property static System.Media.SystemSound Hand {get;}
Question Property static System.Media.SystemSound Question {get;}

PowerShell 技能连载 - 正确地对 IPv4 和 IPv6 地址排序

当您尝试使用 Sort-Object 对 IPv4 地址排序的时候,结果是错误的:

1
2
3
4
PS> '10.1.2.3', '2.3.4.5', '1.2.3.4' | Sort-Object
1.2.3.4
10.1.2.3
2.3.4.5

这并不令人意外,因为数据类型是字符串型,所以 Sort-Object 使用的是字母排序。在前一个技能中我们介绍了如何将数据转换未 [Version] 类型,并且假装在排序软件版本号:

1
2
3
4
PS> '10.1.2.3', '2.3.4.5', '1.2.3.4' | Sort-Object -Property { $_ -as [Version] }
1.2.3.4
2.3.4.5
10.1.2.3

然而,这种方法对 IPv6 地址行不通,因为它无法转换为版本号:

1
2
3
4
5
6
7
PS> '10.1.2.3', 'fe80::532:c4e:c409:b987%13', '2.3.4.5', '2DAB:FFFF:0000:3EAE:01AA:00FF:DD72:2C4A', '1.2.3.4' | Sort-Object -Property { $_ -as [Version] }

fe80::532:c4e:c409:b987%13
2DAB:FFFF:0000:3EAE:01AA:00FF:DD72:2C4A
1.2.3.4
2.3.4.5
10.1.2.3

-as 操作符将 IPv6 地址转为 [Version] 返回的是 NULL,所以当用 Sort-Object 排序时,IPv6 地址会出现在列表的最顶部。

要对 IPv6 地址排序,缺省的字母排序是够用的。所以我们做一些改变,当遇到一个 IPv6 地址时,用它的字符串值来排序:

1
2
3
4
5
6
7
$code = {
$version = $_ -as [Version]
if ($version -eq $null) { "z$_" }
else { $version }
}

'10.1.2.3', 'fe80::532:c4e:c409:b987%13', '2.3.4.5', '2DAB:FFFF:0000:3EAE:01AA:00FF:DD72:2C4A', '1.2.3.4' | Sort-Object -Property $code

现在结果看起来清爽多了:

10.1.2.3
2.3.4.5
1.2.3.4
2DAB:FFFF:0000:3EAE:01AA:00FF:DD72:2C4A
fe80::532:c4e:c409:b987%13

请注意代码中向 IPv6 的字符串开头加入字母 “z”。这是为了保证 IPv6 地址位于列表的底部。如果您希望它们位于列表的顶部,请试试这段代码:

1
2
3
4
5
$code = {
$version = $_ -as [Version]
if ($version -eq $null) { "/$_" }
else { $version }
}

由于 “/“ 的 ASCII 值比 “0” 更小,所以 IPv6 地址位于列表的顶部:

2DAB:FFFF:0000:3EAE:01AA:00FF:DD72:2C4A
fe80::532:c4e:c409:b987%13
1.2.3.4
2.3.4.5
10.1.2.3

PowerShell 技能连载 - 正确地排序 IPv4 地址

在前一个示例中我们发布了一个超快的名为 Test-OnlieFast 的函数,并且这个函数可以在短时间内 ping 整个 IP 段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS> $iprange = 1..200 | ForEach-Object { "192.168.189.$_" }

PS> Test-OnlineFast -ComputerName $iprange

Address Online DNSName Status
------- ------ ------- ------
192.168.189.200 True DESKTOP-7AAMJLF.fritz.box Success
192.168.189.1 True fritz.box Success
192.168.189.134 True PCSUP03.fritz.box Success
192.168.189.29 True fritz.repeater Success
192.168.189.64 True android-6868316cec604d25.fritz.box Success
192.168.189.142 True Galaxy-S8.fritz.box Success
192.168.189.65 True mbecker-netbook.fritz.box Success
192.168.189.30 True android-7f35f4eadd9e425e.fritz.box Success
192.168.189.10 False Request Timed Out
192.168.189.100 False Request Timed Out
192.168.189.101 False Request Timed Out
(...)

然而,IP 地址列并没有排序过。当然您可以使用 Sort-Object 来实现,但是由于地址中的数据是字符串格式,所以它会按字母排序。

以下是一个可以正确地对 IPv4 地址排序的简单技巧:

1
PS> Test-OnlineFast -ComputerName $iprange | Sort-Object { $_.Address -as [Version]}

基本上,您通过 Sort-Object 将数据转换为 Version 对象,它刚好像 IPv4 地址一样,也是由四位数字组成。由于如果数据无法转换时,操作符 -as 将返回 NULL 结果,任何 IPv6 地址将会出现在列表的顶部(未排序)。

如果您错过了之前的 Test-OnlineFast 代码,以下再次把它贴出来:

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
function Test-OnlineFast
{
param
(
# make parameter pipeline-aware
[Parameter(Mandatory,ValueFromPipeline)]
[string[]]
$ComputerName,

$TimeoutMillisec = 1000
)

begin
{
# use this to collect computer names that were sent via pipeline
[Collections.ArrayList]$bucket = @()

# hash table with error code to text translation
$StatusCode_ReturnValue =
@{
0='Success'
11001='Buffer Too Small'
11002='Destination Net Unreachable'
11003='Destination Host Unreachable'
11004='Destination Protocol Unreachable'
11005='Destination Port Unreachable'
11006='No Resources'
11007='Bad Option'
11008='Hardware Error'
11009='Packet Too Big'
11010='Request Timed Out'
11011='Bad Request'
11012='Bad Route'
11013='TimeToLive Expired Transit'
11014='TimeToLive Expired Reassembly'
11015='Parameter Problem'
11016='Source Quench'
11017='Option Too Big'
11018='Bad Destination'
11032='Negotiating IPSEC'
11050='General Failure'
}


# hash table with calculated property that translates
# numeric return value into friendly text

$statusFriendlyText = @{
# name of column
Name = 'Status'
# code to calculate content of column
Expression = {
# take status code and use it as index into
# the hash table with friendly names
# make sure the key is of same data type (int)
$StatusCode_ReturnValue[([int]$_.StatusCode)]
}
}

# calculated property that returns $true when status -eq 0
$IsOnline = @{
Name = 'Online'
Expression = { $_.StatusCode -eq 0 }
}

# do DNS resolution when system responds to ping
$DNSName = @{
Name = 'DNSName'
Expression = { if ($_.StatusCode -eq 0) {
if ($_.Address -like '*.*.*.*')
{ [Net.DNS]::GetHostByAddress($_.Address).HostName }
else
{ [Net.DNS]::GetHostByName($_.Address).HostName }
}
}
}
}

process
{
# add each computer name to the bucket
# we either receive a string array via parameter, or
# the process block runs multiple times when computer
# names are piped
$ComputerName | ForEach-Object {
$null = $bucket.Add($_)
}
}

end
{
# convert list of computers into a WMI query string
$query = $bucket -join "' or Address='"

Get-WmiObject -Class Win32_PingStatus -Filter "(Address='$query') and timeout=$TimeoutMillisec" |
Select-Object -Property Address, $IsOnline, $DNSName, $statusFriendlyText
}

}

PowerShell 技能连载 - 终极快速的 Ping 命令

在之前的技能系列中,我们开发了一个名为 Test-OnlineFast 的新函数,可以在短时间内 ping 多台计算机。出于某些原因,最终版本并没有包含我们承诺的管道功能。以下是再次带给您的完整函数:

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
function Test-OnlineFast
{
param
(
# make parameter pipeline-aware
[Parameter(Mandatory,ValueFromPipeline)]
[string[]]
$ComputerName,

$TimeoutMillisec = 1000
)

begin
{
# use this to collect computer names that were sent via pipeline
[Collections.ArrayList]$bucket = @()

# hash table with error code to text translation
$StatusCode_ReturnValue =
@{
0='Success'
11001='Buffer Too Small'
11002='Destination Net Unreachable'
11003='Destination Host Unreachable'
11004='Destination Protocol Unreachable'
11005='Destination Port Unreachable'
11006='No Resources'
11007='Bad Option'
11008='Hardware Error'
11009='Packet Too Big'
11010='Request Timed Out'
11011='Bad Request'
11012='Bad Route'
11013='TimeToLive Expired Transit'
11014='TimeToLive Expired Reassembly'
11015='Parameter Problem'
11016='Source Quench'
11017='Option Too Big'
11018='Bad Destination'
11032='Negotiating IPSEC'
11050='General Failure'
}


# hash table with calculated property that translates
# numeric return value into friendly text

$statusFriendlyText = @{
# name of column
Name = 'Status'
# code to calculate content of column
Expression = {
# take status code and use it as index into
# the hash table with friendly names
# make sure the key is of same data type (int)
$StatusCode_ReturnValue[([int]$_.StatusCode)]
}
}

# calculated property that returns $true when status -eq 0
$IsOnline = @{
Name = 'Online'
Expression = { $_.StatusCode -eq 0 }
}

# do DNS resolution when system responds to ping
$DNSName = @{
Name = 'DNSName'
Expression = { if ($_.StatusCode -eq 0) {
if ($_.Address -like '*.*.*.*')
{ [Net.DNS]::GetHostByAddress($_.Address).HostName }
else
{ [Net.DNS]::GetHostByName($_.Address).HostName }
}
}
}
}

process
{
# add each computer name to the bucket
# we either receive a string array via parameter, or
# the process block runs multiple times when computer
# names are piped
$ComputerName | ForEach-Object {
$null = $bucket.Add($_)
}
}

end
{
# convert list of computers into a WMI query string
$query = $bucket -join "' or Address='"

Get-WmiObject -Class Win32_PingStatus -Filter "(Address='$query') and timeout=$TimeoutMillisec" |
Select-Object -Property Address, $IsOnline, $DNSName, $statusFriendlyText
}

}

让我们首先来确认 Test-OnlineFast 是如何工作的。以下是一些示例。我们首先 ping 一系列计算机。您可以同时使用计算机名和 IP 地址:

1
2
3
4
5
6
7
8
PS> Test-OnlineFast -ComputerName google.de, powershellmagazine.com, 10.10.10.200, 127.0.0.1

Address Online DNSName Status
------- ------ ------- ------
127.0.0.1 True DESKTOP-7AAMJLF Success
google.de True google.de Success
powershellmagazine.com True powershellmagazine.com Success
10.10.10.200 False Request Timed Out

我们现在来 ping 一整个 IP 段。以下例子从我们公共的酒店 WLAN 中(请确保将 IP 段调整为您所在的网段):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS> $iprange = 1..200 | ForEach-Object { "192.168.189.$_" }

PS> Test-OnlineFast -ComputerName $iprange

Address Online DNSName Status
------- ------ ------- ------
192.168.189.200 True DESKTOP-7AAMJLF.fritz.box Success
192.168.189.1 True fritz.box Success
192.168.189.134 True PCSUP03.fritz.box Success
192.168.189.29 True fritz.repeater Success
192.168.189.64 True android-6868316cec604d25.fritz.box Success
192.168.189.142 True Galaxy-S8.fritz.box Success
192.168.189.65 True mbecker-netbook.fritz.box Success
192.168.189.30 True android-7f35f4eadd9e425e.fritz.box Success
192.168.189.10 False Request Timed Out
192.168.189.100 False Request Timed Out
192.168.189.101 False Request Timed Out
(...)

神奇的是超快的速度。ping 整个子网只花费了几秒钟。