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 整个子网只花费了几秒钟。

PowerShell 技能连载 - 备份事件日志

有一系列有用的 cmdlet 可以管理事件日志,然而缺失了这个功能:

1
2
3
4
5
6
7
8
9
10
11
PS> Get-Command -Noun EventLog

CommandType Name Version Source
----------- ---- ------- ------
Cmdlet Clear-EventLog 3.1.0.0 Microsoft.PowerShell.Management
Cmdlet Get-EventLog 3.1.0.0 Microsoft.PowerShell.Management
Cmdlet Limit-EventLog 3.1.0.0 Microsoft.PowerShell.Management
Cmdlet New-EventLog 3.1.0.0 Microsoft.PowerShell.Management
Cmdlet Remove-EventLog 3.1.0.0 Microsoft.PowerShell.Management
Cmdlet Show-EventLog 3.1.0.0 Microsoft.PowerShell.Management
Cmdlet Write-EventLog 3.1.0.0 Microsoft.PowerShell.Management

没有一个将事件日志被分到 *.evtx 文件的 cmdlet。让我们动手编一个吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Backup-Eventlog
{
param
(
[Parameter(Mandatory)]
[string]
$LogName,

[Parameter(Mandatory)]
[string]
$DestinationPath
)

$eventLog = Get-WmiObject -Class Win32_NTEventLOgFile -filter "FileName='$LogName'"
if ($eventLog -eq $null)
{
throw "Eventlog '$eventLog' not found."
}

[int]$status = $eventLog.BackupEventlog($DestinationPath).ReturnValue
New-Object -TypeName ComponentModel.Win32Exception($status)
}

现在备份事件日志变得十分方便,以下是一个使用示例:

1
2
3
4
5
6
7
PS> Backup-Eventlog -LogName Application -DestinationPath c:\test\backup.evtx
The operation completed successfully

PS> Backup-Eventlog -LogName Application -DestinationPath c:\test\backup.evtx
The file exists

PS>

PowerShell 技能连载 - 将 Windows 错误 ID 转换为友好的文字

当您在 PowerShell 中调用一个底层的函数时,您可能经常会得到一个数值型的返回值。如果这个返回值是来自一个 Windows API 函数,那么有一个非常简单的方法将它转换成有意义的文本:

例如,当一个 API 函数因为权限不足导致失败,它的返回值是 5。让我们将它翻译为一个有意义的文本:

1
2
PS> New-Object -TypeName ComponentModel.Win32Exception(5)
Access is denied

在 PowerShell 5 中,您还可以使用这种语法:

1
2
PS> [ComponentModel.Win32Exception]::new(5)
Access is denied

请试试这个例子:

1
1..200 | ForEach-Object { '{0} = {1}' -f $_, (New-Object -TypeName ComponentModel.Win32Exception($_)) }

PowerShell 技能连载 - 查找注册过的事件日志数据源名

当您用 Write-EventLog 将日志写入日志记录时,您需要指定一个合法的事件源名称。然而,并没有一个很方便的办法能查出哪个事件源文件对应注册到某个事件日志。这在您用 New-EventLog 创建新的事件日志时可能会带来麻烦:您不能指定一个已经存在的事件源名称。

以下是一个查找所有事件源名称,并且显示它们注册的事件日志的简单方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS> Get-WmiObject -Class Win32_NTEventLOgFile | Select-Object FileName, Sources


FileName Sources
-------- -------
Application {Application, .NET Runtime, .NET Runtime Optimization Service, Application Error...}
Dell {Dell, DigitalDelivery, Update}
HardwareEvents {HardwareEvents}
Internet Explorer {Internet Explorer}
isaAgentLog {isaAgentLog, isaAgent}
Key Management Service {Key Management Service, KmsRequests}
OAlerts {OAlerts, Microsoft Office 16 Alerts}
PowerShellPrivateLog {PowerShellPrivateLog, Debug, Logon, Misc...}
PreEmptive {PreEmptive, PreEmptiveAnalytics}
Security {Security, DS, LSA, Microsoft-Windows-Eventlog...}
System {System, 3ware, ACPI, ADP80XX...}
TechSmith {TechSmith, TechSmith Uploader Service}
Windows PowerShell {Windows PowerShell, PowerShell}

您还可以将这个列表转换为一个有用的哈希表:

1
2
3
4
5
6
7
8
9
10
# find all registered sources
$Sources = Get-WmiObject -Class Win32_NTEventLOgFile |
Select-Object FileName, Sources |
ForEach-Object -Begin { $hash = @{}} -Process { $hash[$_.FileName] = $_.Sources } -end { $Hash }

# list sources for application log
$Sources["Application"]

# list sources for system log
$Sources["System"]

PowerShell 技能连载 - 使用事件日志方便地记录日志

脚本常常需要记录它们做了什么,并且 PowerShell 脚本开发者们需要做投入许多时间精力来将信息记录到文本文件中。

作为另一个选择,您可以方便地使用 Microsoft 已投入建设的工作:PowerShell 可以使用事件日志系统来记录信息。要做测试实验,请用以下代码创建一个新的事件日志。这部分需要管理员特权(写日志时不需要):

1
2
3
4
5
6
7
8
9
10
11
12
#requires -RunAsAdministrator

# name for your log
$LogName = 'PowerShellPrivateLog'
# size (must be dividable by 64KB)
$Size = 10MB

# specify a list of names that you'd use as source for your events
$SourceNames = 'Logon','Work','Misc','Test','Debug'

New-EventLog -LogName $LogName -Source $SourceNames
Limit-EventLog -LogName $LogName -MaximumSize $Size -OverflowAction OverwriteAsNeeded

当日志创建了以后,任何用户都可以记录日志文件:

1
2
3
4
5
6
7
8
9
10
11
12
PS> Write-EventLog -LogName PowerShellPrivateLog -Message 'Script Started' -Source Work -EntryType Information -EventId 1

PS> Write-EventLog -LogName PowerShellPrivateLog -Message 'Something went wrong!' -Source Work -EntryType Error -EventId 1



PS> Get-EventLog -LogName PowerShellPrivateLog | ft -AutoSize

Index Time EntryType Source InstanceID Message
----- ---- --------- ------ ---------- -------
2 Jan 30 21:57 Error Work 1 Something went wrong!
1 Jan 30 21:57 Information Work 1 Script Started

当您创建日志时,必须指定一个合法的 -Source 名称。使用这项技术的一个好处是您可以用 Get-EventLog 来方便地分析您的日志记录。

PowerShell 技能连载 - 还原短网址

类似 “http://bit.ly/e0Mw9w" 这样的网址十分短小,并且使用起来很方便。但它们往往屏蔽了原始的信息。

PowerShell 查找它们真实指向的地址,来还原短网址:

1
2
3
4
5
6
7
$url = "http://bit.ly/e0Mw9w"

$request = [System.Net.WebRequest]::Create($url)
$request.AllowAutoRedirect=$false
$response=$request.GetResponse()
$trueUrl = $response.GetResponseHeader("Location")
"$url -> $trueUrl"

以下是使用结果:

http://bit.ly/e0Mw9w -> http://www.leeholmes.com/projects/ps_html5/Invoke-PSHtml5.ps1

PowerShell 技能连载 - 自动壁纸下载器

您对桌面壁纸感到厌倦了吗?PowerShell 可以帮您下载最新的壁纸!以下是一个函数:

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
function Download-Wallpaper
{
param
(
[string]
[Parameter(Mandatory)]
$Folder,

[Parameter(ValueFromPipeline)]
[Int]
$Page=1
)

begin
{
$url = "http://wallpaperswide.com/page/$Page"
$targetExists = Test-Path -Path $Folder
if (!$targetExists) { $null = New-Item -Path $Folder -ItemType Directory }
}
process
{
$web = Invoke-WebRequest -Uri $url -UseBasicParsing

$web.Images.src |
ForEach-Object {

$filename = $_.Split('/')[-1].Replace('t1.jpg','wallpaper-5120x3200.jpg')
$source = "http://wallpaperswide.com/download/$filename"

$TargetPath = Join-Path -Path $folder -ChildPath $filename

Invoke-WebRequest -Uri $source -OutFile $TargetPath
}
}
end
{
explorer $Folder
}
}

以下是使用方法:

1
PS> Download-Wallpaper -Folder c:\wallpapers

它将从一个公开的壁纸网站下载所有壁纸到本地文件夹,然后打开该文件夹。您所需要做的只是右键单击壁纸并且选择“设为桌面背景”。

默认情况下,Download-Wallpaper 从第一个页面下载壁纸。通过指定 -Page 参数,您可以从其它页面挖掘壁纸。请试试以下代码:

1
PS> Download-Wallpaper -Folder c:\wallpapers