PowerShell 技能连载 - 列出已安装的更新(第 1 部分)

Get-Hotfix 只会列出操作系统相关的 hotfix:

1
Get-HotFix

实际上,它只是一个 WMI 查询的简单包装,结果是一样的:

1
Get-CimInstance -ClassName Win32_QuickFixEngineering

一个更简单更完整的方法是查询系统事件日志获取所有安装的更新:

1
2
3
4
5
6
7
8
9
10
Get-WinEvent @{
Logname='System'
ID=19
ProviderName='Microsoft-Windows-WindowsUpdateClient'
} | ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated
Update = $_.Properties.Value[0]
}
}

显然,当系统事件日志清除之后,查询结果就不完整了。此外,该日志只是记录任何更新安装,因此随着时间的推移,新的更新可能取代旧的更新。

要保证获取到完整的已安装更新列表,您需要请求 Windows Update 客户端,从实际安装的更新中重建列表,这要消耗更多的时间:

1
2
3
4
$result = (New-Object -ComObject Microsoft.Update.Session).CreateupdateSearcher().Search("IsInstalled=1").Updates |
Select-Object LastDeploymentChangeTime, Title, Description, MsrcSeverity

$result | Out-GridView -Title 'Installed Updates'

PowerShell 技能连载 - 退出 PowerShell 管道(第 2 部分:手动退出)

在前一个技能中我们学到了如何当达到一定次数的时候退出 PowerShell 管道,这样可以节约很多时间:

1
2
3
$fileToSearch = 'ngen.log'
Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch |
Select-Object -First 1

显然,当一定数量的结果传递给 Select-Object 之后,Select-Object 会发送秘密的信息到上一级管道的 cmdlet,告知它们停止。实际上,Select-Object 会抛出一个特殊的异常,PowerShell 会处理这个异常,才产生这个魔术的效果。

但是如果事先不知道确切的结果数量呢?如果您希望在其它情况下中断管道呢?如果您希望实现某种超时呢?要手动退出一个管道,只需要让 Select-Object 发出这个特殊的异常。以下是一个专门做这件事的 Stop-Pipeline 函数:

1
2
3
4
5
6
7
function Stop-Pipeline
{
$pipeline = { Select-Object -First 1 }.GetSteppablePipeline()
$pipeline.Begin($true)
$pipeline.Process(1)
$pipeline.End()
}

它调用了一个 Select-Object 并且模仿在管道中执行。它是通过 GetSteppablePipeline() 实现的。您现在可以通过 Process() 人工传递数据给 Select-Object。由于通过 -First 1 参数执行 Select-Object 命令,所以当传递任何数据给 Select-Object,都会产生该特殊的异常。

您现在获得了控制权,并且可以通过任何条件来调用 Stop-Pipeline。以下示例程序将搜索文件并且在最长 2 秒之内退出管道:

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
function Stop-Pipeline
{
$pipeline = { Select-Object -First 1 }.GetSteppablePipeline()
$pipeline.Begin($true)
$pipeline.Process(1)
$pipeline.End()
}

# abort pipeline after 2000 milliseconds
$timeout = 2000
# create a stopwatch
$stopwatch = [system.diagnostics.stopwatch]::StartNew()

Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
if ($stopwatch.ElapsedMilliseconds -gt $timeout)
{
$stopwatch.Stop()
Write-Warning "Timeout, Pipeline Aborted."
# abort pipeline
Stop-Pipeline
}

# return the original object
$_
}

PowerShell 技能连载 - 退出 PowerShell 管道(第 1 部分:Select-Object)

有些时候人工退出 PowerShell 管道可以节约很多时间。例如,在开始递归搜索之前不能确切地知道一个文件所在的位置,当搜到文件的时候立刻停止。当找到文件以后没理由继续搜索其它目录。

下面是一个演示该问题的场景:假设您在 Windows 文件夹中的某个地方搜索一个名为 “ngen.log” 的文件:

1
2
3
$fileToSearch = 'ngen.log'

Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch

PowerShell 将会查找这个文件但是找到之后还会继续搜索目录树的其它部分,这会消耗很多时间。

如果您知道需要查找结果的个数,那么可以当搜到指定数量的结果后使用 Select-Object 来立刻退出管道。

1
2
3
$fileToSearch = 'ngen.log'
Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch |
Select-Object -First 1

在这个例子中,当找到文件时 PowerShell 立即退出。作为最佳实践,如果您事先知道需要从命令中查找多少个结果,那么在管道尾部添加 Select-Object,并且用 -First 参数来告诉 PowerShell 需要的结果个数。

PowerShell 技能连载 - 使用一个计时器来测量执行时间

有一些情况下您希望知道一段代码需要执行多长时间,例如返回统计或者对比代码,有许多方法可以测量命令,包括 Measure-Command cmdlet:

1
2
3
4
5
6
7
$duration = Measure-Command -Expression {
$result = Get-Hotfix
}

$time = $duration.TotalMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

不过 Measure-Command 有一些不受人喜欢的副作用:

  • 所有输出都被丢弃,这样输出数据不会影响测量时间,而且您无法控制这个行为
  • 出于好几个原因,它会减慢您的代码执行,其中一个是 Measure-Command 在 dot-sourced 表达式中以一个独立的脚本快执行

所以,常常使用另一个技术,用的是 Get-Date,例如这样:

1
2
3
4
5
6
$start = Get-Date
$result = Get-Hotfix
$end = Get-Date
$time = ($end - $start).TotalMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

它可以有效工作,不过也有一些不受欢迎的副作用:

  • 它产生更多的代码
  • 当计算机处于睡眠或休眠状态,将会影响结果,因为计算机关闭的时间不应该计入统计时间

一个更简洁的解决方案是使用 .NET 的 stopwatch 对象,它产生的代码更少,并且不会减缓代码的执行,而且不受睡眠或休眠的影响:

1
2
3
4
5
$stopwatch =  [system.diagnostics.stopwatch]::StartNew()
$result = Get-Hotfix
$time = $stopwatch.ElapsedMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

此外,您可以对 stopwatch 对象调用 Stop()Restart()Reset() 方法。通过这些方法,您可以暂停测量代码中的某些部分(例如数据输出)并且继续测量。

PowerShell 技能连载 - Foreach -parallel (第 3 部分:批量 Ping)

在 PowerShell 7 中,带来了一个新的并行的 Foreach-Object,它可以并行执行代码并显著加快操作的速度。同样的技术可通过以下模块在 Windows PowerShell 中使用:

1
Install-Module -Name PSParallel -Scope CurrentUser -Force

我们来看看有趣的案例。例如,如果您需要获得一个能够响应 ping (ICMP) 的计算机列表,这将需要很长时间。一个加速操作的方法是使用带超时设置的 ping,这样无需对不响应的机器等待太久。

这段代码 ping powershell.one 并且等待最长 1000 毫秒(代码来自 https://powershell.one/tricks/network/ping):

1
2
3
4
5
6
$ComputerName = 'powershell.one'
$timeout = 1000
$ping = New-Object System.Net.NetworkInformation.Ping

$ping.Send($ComputerName, $timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address

现在让我们来加速整件事,整批 ping 整个 IP 段!

我们先来看看在 PowerShell 7 中如何实现:

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
#requires -Version 7.0

# IP range to ping
$IPAddresses = 1..255 | ForEach-Object {"192.168.0.$_"}

# timeout in milliseconds
$timeout = 1000

# number of simultaneous pings
$throttleLimit = 80

# measure execution time
$start = Get-Date

$result = $IPAddresses | ForEach-Object -ThrottleLimit $throttleLimit -parallel {
$ComputerName = $_
$ping = [System.Net.NetworkInformation.Ping]::new()

$ping.Send($ComputerName, $using:timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address
} | Where-Object Status -eq Success

$end = Get-Date
$time = ($end - $start).TotalMilliseconds

Write-Warning "Execution Time $time ms"

$result

在 5 秒钟左右,整个 IP 段都 ping 完毕,并且返回会 ICMP 请求的 IP 地址。

在 Windows PowerShell 中,这段代码基本差不多(假设您已从 PowerShell Gallery 中安装了 PSParallel 模块):

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
#requires -Modules PSParallel
#requires -Version 3.0

# IP range to ping
$IPAddresses = 1..255 | ForEach-Object {"192.168.0.$_"}

# number of simultaneous pings
$throttleLimit = 80

# measure execution time
$start = Get-Date

$result = $IPAddresses | Invoke-Parallel -ThrottleLimit $throttleLimit -ScriptBlock {
$ComputerName = $_
# timeout in milliseconds
$timeout = 1000

$ping = New-Object -TypeName System.Net.NetworkInformation.Ping

$ping.Send($ComputerName, $timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address
} | Where-Object Status -eq Success

$end = Get-Date
$time = ($end - $start).TotalMilliseconds

Write-Warning "Execution Time $time ms"

$result

ForEach-Object 不同,代码使用的是 Invoke-Parallel,而且由于 Invoke-Parallel 不支持 “use:“ 前缀,所以所有局部变量都必须包含在脚本块中(在我们的示例中,是变量$timeout)。

Invoke-Parallel 支持一个友好的进度条,这样您可以知道有多少个任务正在并行执行。

PowerShell 技能连载 - Foreach -parallel (第 2 部分:Windows PowerShell)

PowerShell 7 发布了一个内置参数,可以并行运行不同的任务:

1
1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; $_ }

如果您使用的是 Windows PowerShell,那么您也可以使用类似的并行技术。例如,下载并安装这个免费的模块:

1
Install-Module -Name PSParallel -Scope CurrentUser -Force

它带来一个新的命令:Invoke-Parallel:您可以这样使用它:

1
1..100 | Invoke-Parallel -ThrottleLimit 20 -Scrip-tBlock { Start-Sleep -Seconds 1; $_ }

由于 Invoke-Parallel 使用的是和 PowerShell 7 相同的技术,所以有相同的限制:每个线程都在它自己的线程中执行,并且不能存取本地变量。在下一个技能中,我们将学习一些有趣的示例。

PowerShell 技能连载 - Foreach -parallel(第 1 部分:PowerShell 7)

PowerShell 7 发布了一个内置参数,可以并行运行不同的任务。以下是一个简单示例:

1
1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; $_ }

在普通的 ForEach-Object 循环中,这将花费 100 秒的时间执行。如果使用了 parallel,代码可以并行地执行。-ThrottleLimit 定义了“块”,因此在本例中,有20个线程并行运行,使总执行时间减少到5秒。

在过于激动之前,请记住每个线程都在其自己的 PowerShell 环境中运行。幸运的是,您可以访问前缀为“using:”的局部变量:

1
2
3
$text = "Output: "

1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; "$using:text $_" }

不过,当您开始使用多线程时,您需要了解线程安全知识。复杂对象,例如 ADUser 对象可能无法在多个线程之间共享,所以需要每个案例独立判断是否适合使用并行。

由于并行的 ForEach-Object 循环内置在 PowerShell 7 中,这并不意味着可以在 Windows PowerShell 中使用并行。在 Windows PowerShell 中有许多模块实现了该功能。我们将会在接下来的技能中介绍它们。

PowerShell 技能连载 - -RepeatHeader 参数

有一个不太为人所知的参数:RepeatHeader,它是做什么用的?

假设您希望分页显示结果(在命令行中有效,而在 PowerShell ISE 中无效):

1
PS> Get-Process | Out-Host -Paging

结果输出时每页会暂停,直到按下空格键。然而,列标题只显示在第一页中。

以下是更好的输出形式:

1
PS> Get-Process | Format-Table -RepeatHeader | Out-Host -Paging

现在,每一页都会重新显示列标题。-RepeatHeader 在所有的 Format-* cmdlet 中都有效。再次提醒,这个技巧只在基于控制台的 PowerShell 宿主中有效,并且在 PowerShell ISE 中无效。原因是:PowerShell ISE 没有固定的页大小,所以它无法知道一页什么时候结束。

PowerShell 技能连载 - PowerShell 7 中的三元操作符

在 PowerShell 7 中,语言增加了一个新的操作符,引发了大量的辩论。基本上,您不必要使用它,但是有编程背景的用户会喜欢它。

直到现在,要创建一个条件判断,总是需要写许多代码。例如,要查询脚本运行环境是 32 位还是 64 位,您可以像这样查询指针的长度:

1
2
3
4
5
6
7
8
if ([IntPtr]::Size -eq 8)
{
'64-bit'
}
else
{
'32-bit'
}

三元操作符可以大大缩短代码:

1
[IntPtr]::Size -eq 8 ? '64-bit' : '32-bit'

本质上,三元操作符 (“?“) 是标准 “if“ 条件判断的缩写。它对所有取值为 $true$false 的表达式有效。如果该表达式的执行结果是 $true,那么执行 “?“ 之后的表达式。如果该表达式的执行结果是 $false,那么执行 “:“ 之后的表达式。

如果您安装了 PowerShell 7 preview 版,请确保您更新到了最新版本,才能确保使用三元操作符。它并不是 PowerShell 7 preview 版本的一部分。

PowerShell 技能连载 - Get-ComputerInfo 和 systeminfo.exe 的对比(第 2 部分)

在 PowerShell 5 中,引入了一个名为 Get-ComputerInfo 的新的 cmdlet,它完成曾经 systeminfo.exe 的功能,而 Get-ComputerInfo 是直接面向对象的。没有本地化的问题:

1
$infos = Get-ComputerInfo

您现在可以查询您电脑独立的详情:

1
2
3
$infos.OsInstallDate
$infos.OsFreePhysicalMemory
$infos.BiosBIOSVersion

或者使用 Select-Object 来选择所有兴趣的属性:

1
$infos | Select-Object -Property OSInstallDate, OSFreePhysicalMemory, BiosBIOSVersion

在缺点方面,请考虑这一点:Get-ComputerInfo 是在 PowerShell 5 中引入的,您可以很容易地更新到该版本,或者将 PowerShell Core 与旧版本的 Windows PowerShell 并行使用。然而,Get-ComputerInfo 检索到的许多信息仅来自于最近的 Windows 操作系统中添加的 WMI 类。

如果您在 Windows 7 中更新到了 Windows PowerShell 5.1,Get-ComputerInfo 有可能不能正常工作。在旧的系统中,systeminfo.exe 是您的最佳依赖,而在新的操作系统中,Get-ComputerInfo 用起来方便得多。