PowerShell 技能连载 - 测试网络连接(第 1 部分)

PowerShell 随附了 Test-NetConnection 命令,它可以像复杂的 ping 工具一样工作。默认情况下,您可以这样 ping 计算机:

1
2
3
4
5
6
7
8
9
PS> Test-NetConnection -ComputerName powershell.one


ComputerName : powershell.one
RemoteAddress : 104.18.46.88
InterfaceAlias : Ethernet 4
SourceAddress : 192.168.2.108
PingSucceeded : True
PingReplyDetails (RTT) : 26 ms

使用 -TraceRoute,它包括路由跟踪,列出了用于传播到目标的所有网络节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS> Test-NetConnection -ComputerName powershell.one -TraceRoute


ComputerName : powershell.one
RemoteAddress : 104.18.46.88
InterfaceAlias : Ethernet 4
SourceAddress : 192.168.2.108
PingSucceeded : True
PingReplyDetails (RTT) : 25 ms
TraceRoute : 192.168.2.1
62.155.243.83
62.154.2.185
62.157.250.38
195.22.215.192
195.22.215.59
104.18.46.88

PowerShell 技能连载 - 探索即插即用设备(第 4 部分)

在前面的技能中,我们研究了 UPnP.UPnPDeviceFinder 以及如何识别网络中的设备。让我们看一些用例。显然,这些用例是您的灵感来源,因为您可以做的事情完全取决于连接到网络的实际设备。

我的第一个用例是管理 NAS 设备,名为 Synology Disk Station。可以通过 WEB 界面进行管理,但我总是会忘记 URL,当然 URL 和端口会根据配置而有所不同。

这是一种搜索任何磁盘站并自动打开其 Web 界面的方法。由于所有连接数据都是从设备中检索的,因此即使 IP 地址更改或您配置了别的端口,此操作也仍然有效。

1
2
3
4
5
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("ssdp:all", 0) |
Where-Object ManufacturerName -eq 'Synology' |
Select-Object -ExpandProperty PresentationUrl |
ForEach-Object { Start-Process -FilePath $_ }

注意:如果您没有 Synology 磁盘站,请查看您拥有的其他设备。只需查看 FindByType() 返回的数据,然后根据需要定制 Where-Object

由于该代码枚举了所有 UPnP 设备,因此需要 10 到 20 秒。如果要加快处理速度,可以找出所用设备的 UDN(唯一名称),以后再使用这些 UDN:

1
2
3
4
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("ssdp:all", 0) |
Where-Object ManufacturerName -eq 'Synology' |
Select-Object -ExpandProperty UniqueDeviceName

UDN 是唯一的,并且因设备而异,因此,如果您需要多次访问同一设备,则此方法才有意义。在我的情况下,UDN 是 uuid:7379AA6F-6473-6D00-0000-001132283f5e,现在打开 Web 界面的速度要快得多:

1
2
3
4
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("uuid:7379AA6F-6473-6D00-0000-001132283f5e", 0) |
Select-Object -ExpandProperty PresentationUrl |
Foreach-Object { Start-Process -FilePath $_ }

这引出了我的第二个使用场景:我正在使用 Philips Hue 系统管理家里的灯光。飞利浦提供了丰富的 REST API 来实现自动化。您所需要的是网桥的 IP 地址。

这段代码将列出本地网络中所有 Philips Hue 网桥的 IP 地址:

``powershell
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType(“upnp:rootdevice”, 0) |
Where-Object Description -like ‘Philips hue*’ |
Select-Object -ExpandProperty FriendlyName |
ForEach-Object {
if ($_ -match ‘(?\w*).?((?.))‘)
{
$null = $matches.Remove(0)
[PSCustomObject]$matches
}
}


该代码查找根设备,其描述以 "Philips hue" 开头的根设备,然后使用正则表达式拆分 "FriendlyName" 属性的内容并返回网桥名称及其 IP 地址。

就我而言,结果如下所示:

    IP            BridgeName
    --            ----------
    192.168.22.10 BridgeOne
    192.168.23.16 BridgeWork

<!--本文国际来源:[Exploring Plug&amp;Play Devices (Part 4)](https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/exploring-plug-play-devices-part-4)-->

PowerShell 技能连载 - 探索即插即用设备(第 3 部分)

在前一个技能中我们演示了如何使用 UPnP.UPnPDeviceFinder 来查找网络中的设备。您已了解到如何枚举所有的根设备 (“upnp:rootdevice“),以及如何通过设备的唯一标识符来访问设备。

1
2
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("upnp:rootdevice", 0) | Out-GridView

在这一部分中,让我们完成搜索类型,看看如何枚举所有设备(而不仅仅是根设备),以及如何枚举设备类型组。

要列出所有设备,请使用 “ssdb:all" 而不是 "upnp:rootdevice`”:

1
2
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("ssdp:all", 0) | Out-GridView

结果包括根设备(“IsRootDevice” 为 $true,“ParentDevice”为空)以及所有子设备(“IsRootDevice” 为 $false,“ParentDevice” 指向该设备链接到的上级设备)。

在 “UniqueDeviceName” 中,可以找到可用于直接访问设备的唯一设备名称:

1
2
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByUDN("uuid:...", 0)

每个设备都属于一个类别,该类别在“Type”中显示。要查看类型列表,请尝试以下操作:

1
2
3
4
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("ssdp:all", 0) |
Select-Object -ExpandProperty Type |
Sort-Object -Unique

结果取决于网络中找到的设备。这是我得到的清单:

urn:dial-multiscreen-org:device:dial:1
urn:schemas-upnp-org:device:Basic:1
urn:schemas-upnp-org:device:InternetGatewayDevice:1
urn:schemas-upnp-org:device:MediaRenderer:1
urn:schemas-upnp-org:device:WANConnectionDevice:1
urn:schemas-upnp-org:device:WANDevice:1

要查找特定类型的所有设备,请将该类型与 FindByType() 一起使用:

1
2
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("urn:schemas-upnp-org:device:InternetGatewayDevice:1", 0)

最后一点:设备是否响应组搜索,甚至是“upnp:rootdevice”,都取决于设备及其实现。在我的场景中,即使存在那种类型的设备,我也无法获得“Basic”和“WANDevice”组的结果。

如果找不到特定设备,请尝试适用于所有设备的唯一搜索,然后通过“ssdp:all”列出所有设备。如果设备现在显示出来,则可以通过 Where-Object 使用“ssdp:all”和客户端过滤,或者通过查找唯一的设备标识符并通过其 UDN 和 FindByUDN() 直接访问特定设备来加快搜索速度。

PowerShell 技能连载 - 探索即插即用设备(第 2 部分)

在前一个技能中,我们使用 UPnP.UPnPDeviceFinder 来发现连入您网络的智能设备。今天,让我们仔细看看返回的对象。

1
2
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$result = $UPnPFinder.FindByType("upnp:rootdevice", 0)

当您运行一个搜索(可能需要执行一定的时间),将会获取到许多信息,但是某些属性只是以 System.__ComObject 的形式返回。如何查看它们背后隐藏的信息?

让我们取其中的一个返回对象。在我的例子中,通过查看 $result,我注意到一个来自 “NetGear” 对象。接下来,我使用 Where-Object 来存取它。

在您的场景中,可用的对象明显不同,这取决于您网络中使用的设备,所以您需要调整过滤器表达式来匹配返回的对象之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS> $switch = $result | Where-Object ManufacturerName -like *NetGear*

PS> $switch


IsRootDevice : True
RootDevice : System.__ComObject
ParentDevice :
HasChildren : True
Children : System.__ComObject
UniqueDeviceName : uuid:4d696e69-444c-164e-9d42-3894ed0e1db5
FriendlyName : RBR50 (Gateway)
Type : urn:schemas-upnp-org:device:InternetGatewayDevice:1
PresentationURL : http://www.orbilogin.net/
ManufacturerName : NETGEAR, Inc.
ManufacturerURL : http://www.netgear.com/
ModelName : NETGEAR Orbi Desktop AC3000 Router
ModelNumber : RBR50
Description : http://www.netgear.com/home/products/wirelessrouters
ModelURL : http://www.netgear.com/orbi
UPC : RBR50
SerialNumber : 5R21945T06DA4
Services : System.__ComObject

大多数属性使用普通的数据类型,例如 string 或者 integer。例如,”UniqueDeviceName” 返回一个设备的唯一名称(稍后会变得很重要)。然而某些属性,只是返回 “System.__ComObject”。让我们看看这些:

每个 PnP 设备可以是一个链条的一部分,并且由一个根设备开始。由于我们一开始搜索的是根设备,所以 “RootDevice” 属性总是对应返回的对象:

1
2
PS> $switch -eq $switch.RootDevice
True

要找出那些设备链接到根设备,请查看 “HasChildren” 和 “Children”:”HasChildren” 是一个简单的 Boolean 值。然而 “Children” 是一个 “System.__ComObject” 对象并且包含了我们想了解的信息:

1
2
PS> if ($switch.HasChildren) { $switch.Children.Count } else { 'no children' }
1

显然地,”Children” 看起来是某种数组。总之,一个根设备可以有许多子设备。通过存取该属性,PowerShell 自动将 “System.__ComObject” 转换为可读取的 .NET 对象,以下是我的 NetGear 设备的子设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PS> $switch.Children


IsRootDevice : False
RootDevice : System.__ComObject
ParentDevice : System.__ComObject
HasChildren : True
Children : System.__ComObject
UniqueDeviceName : uuid:4d696e69-444c-164e-9d43-3894ed0e1db5
FriendlyName : WAN Device
Type : urn:schemas-upnp-org:device:WANDevice:1
PresentationURL :
ManufacturerName : NETGEAR
ManufacturerURL : http://www.netgear.com/
ModelName : NETGEAR Orbi Desktop AC3000 Router
ModelNumber : RBR50
Description : WAN Device on NETGEAR RBR50 Orbi Router
ModelURL : http://www.netgear.com/
UPC : RBR50
SerialNumber : 5R21945T06DA4
Services : System.__ComObject

这个子设备之下还有子设备。但是,继续深挖并列出子设备之下的子设备看起来失败了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS> # works:
PS> $switch.Children | Select-Object HasChildren, Children

HasChildren Children
----------- --------
True System.__ComObject



PS> # fails:
PS> $switch.Children.HasChildren
PS> $switch.Children.Children
PS> $switch.Children[0].HasChildren
Value does not fall within the expected range.
PS> $switch.Children[0].Children
Value does not fall within the expected range.

失败的原因是 COM 数组的一个特异性。它们使用一个非常规的枚举器,和普通的对象数组不同,所以 PowerShell 无法直接存取数组元素。一个简单的解决方案是使用 ForEach-Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS> $child = $switch.Children

PS> $child | ForEach-Object { $_.Children }


IsRootDevice : False
RootDevice : System.__ComObject
ParentDevice : System.__ComObject
HasChildren : False
Children : System.__ComObject
UniqueDeviceName : uuid:4d696e69-444c-164e-9d44-3894ed0e1db5
FriendlyName : WAN Connection Device
Type : urn:schemas-upnp-org:device:WANConnectionDevice:1
PresentationURL :
ManufacturerName : NETGEAR
ManufacturerURL : http://www.netgear.com/
ModelName : NETGEAR Orbi Desktop AC3000 Router
ModelNumber : RBR50
Description : WANConnectionDevice on NETGEAR RBR50 Orbi Router
ModelURL : http://www.netgear.com/
UPC : 606449084528
SerialNumber : 5R21945T06DA4
Services : System.__ComObject

同样地,也适用于 “ParentDevice” 和 `Services”。我们来查看我的 NetGear 设备的更多服务。这次,我希望直接存取我的 NetGear 设备(这样比枚举所有设备快得多)。不过您需要知道它的唯一设备名。在我的例子中,”UniqueDeviceName” 的值是 “uuid:4d696e69-444c-164e-9d42-3894ed0e1db5”:

1
2
3
4
5
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
# the UDN is unique, so you need to find out the UDN for your device first
# you cannot use the UDN I used
$myNetgearSwitch = $UPnPFinder.FindByUDN('uuid:4d696e69-444c-164e-9d42-3894ed0e1db5')
$myNetgearSwitch

这次,几乎立即识别出我的设备。要列出它的服务,我只需要获取它的 “Services” 属性,并且 PowerShell 自动将该 COM 对象转为可见的属性:

1
2
3
4
5
PS> $myNetgearSwitch.Services

ServiceTypeIdentifier Id LastTransportStatus
--------------------- -- -------------------
urn:schemas-upnp-org:service:Layer3Forwarding:1 urn:upnp-org:serviceId:L3Forwarding1 0

PowerShell 技能连载 - 探索即插即用设备(第 1 部分)

您可能已经生活在一个互联的智能家居,许多设备连接到您的网络。PowerShell 只需几行代码就可以帮助您找到您的设备:

1
2
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("upnp:rootdevice", 0)

请注意 UPnP 查找器组件需要一些时间来检测您的设备。结果看起来类似这样:

IsRootDevice     : True
RootDevice       : System.__ComObject
ParentDevice     :
HasChildren      : False
Children         : System.__ComObject
UniqueDeviceName : uuid:73796E6F-6473-6D00-0000-001132283f5e
FriendlyName     : Storage2 (DS414)
Type             : urn:schemas-upnp-org:device:Basic:1
PresentationURL  : http://192.168.2.107:5000/
ManufacturerName : Synology
ManufacturerURL  : http://www.synology.com/
ModelName        : DS414
ModelNumber      : DS414 5.1-5055
Description      : Synology NAS
ModelURL         : http://www.synology.com/
UPC              :
SerialNumber     : 001132283f5e
Services         : System.__ComObject

IsRootDevice     : True
RootDevice       : System.__ComObject
ParentDevice     :
HasChildren      : False
Children         : System.__ComObject
UniqueDeviceName : uuid:2f402f80-da50-11e1-9b23-001788ac0af1
FriendlyName     : BridgeOne (192.168.2.100)
Type             : urn:schemas-upnp-org:device:Basic:1
PresentationURL  : http://192.168.2.100/index.html
ManufacturerName : Signify
ManufacturerURL  : http://www.meethue.com/
ModelName        : Philips hue bridge 2015
ModelNumber      : BSB002
Description      : Philips hue Personal Wireless Lighting
ModelURL         : http://www.meethue.com/
UPC              :
SerialNumber     : 001788ac0af1
Services         : System.__ComObject
...

使用 Select-Object 来选择您感兴趣的属性:

1
2
3
4
$UPnPFinder = New-Object -ComObject UPnP.UPnPDeviceFinder
$UPnPFinder.FindByType("upnp:rootdevice", 0) |
Select-Object ModelName, FriendlyName, PresentationUrl |
Sort-Object ModelName

在我的家中,该列表看起来类似这样:

ModelName                          FriendlyName               PresentationURL
---------                          ------------               ---------------
AFTMM                              Tobias's 2nd Fire TV
AFTS                               Tobias's Fire TV
AFTT                               Tobias's 3rd Fire TV stick
DS414                              Storage2 (DS414)           http://192.168.2.107:5000/
NETGEAR Orbi Desktop AC3000 Router RBR50 (Gateway)            http://www.orbilogin.net/
Philips hue bridge 2015            BridgeOne (192.168.2.100)  http://192.168.2.100/index.html
Philips hue bridge 2015            BridgeWork (192.168.2.106) http://192.168.2.106/index.html
SoundTouch 20                      Bad
SoundTouch 30                      Portable
SoundTouch SA-4                    Garden
SoundTouch SA-4                    LivingRoom

我正在使用群晖 NAS,假设我忘了访问它的 URL,那么现在可以快速地找回它。我的飞利浦灯光系统也是这样:UPnP 查找器返回所有 hub 的 IP 地址。

请注意 UPnP 查找器只能返回和您的计算机连入相同网络的设备。如果您的设备 IP 地址是在其它子网,那么不能通过这种方式检测。

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

在前一个技能中我们演示了如何通过 Windows Update 客户端获取当前已安装的更新。

可以对此列表进行润色,例如您可以使用哈希表创建计算的属性,提取默认情况下属于其他属性的信息,如知识库文章编号作为标题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$severity = @{
Name = 'Severity'
Expression = { if ([string]::IsNullOrEmpty($_.MsrcSeverity)) { 'normal' } else { $_.MsrcSeverity }}
}

$time = @{
Name = 'Time'
Expression = { $_.LastDeploymentChangeTime }
}

$kb = @{
Name = 'KB'
Expression = { if ($_.Title -match 'KB\d{6,9}') { $matches[0] } else { 'N/A' }}
}

$UpdateSession = New-Object -ComObject Microsoft.Update.Session
$UpdateSession.CreateupdateSearcher().Search("IsInstalled=1").Updates |
Select-Object $time, Title, $kb, Description, $Severity |
Out-GridView -Title 'Installed Updates'

结果显示在一个网格视图窗口中。如果您移除了 Out-GridView 那么信息看起来类似这样:

Time        : 9/10/2019 12:00:00 AM
Title       : 2019-09 Security Update for Adobe Flash Player for Windows 10 Version 1903 for
              x64-based Systems (KB4516115)
KB          : KB4516115
Description : A security issue has been identified in a Microsoft software product that could
              affect your system. You can help protect your system by installing this update
              from Microsoft. For a complete listing of the issues that are included in this
              update, see the associated Microsoft Knowledge Base article. After you install
              this update, you may have to restart your system.
Severity    : Critical

Time        : 10/8/2019 12:00:00 AM
Title       : Windows Malicious Software Removal Tool x64 - October 2019 (KB890830)
KB          : KB890830
Description : After the download, this tool runs one time to check your computer for infection by specific, prevalent malicious software (including Blaster, Sasser,
and Mydoom) and helps remove any infection that is found. If an infection is found, the tool will display a status report the next time that you start your computer. A new version of the tool will be offered every month. If you want to manually run the tool on your computer, you can download a copy from the Microsoft Download Center, or you can run an online version from microsoft.com. This tool is not a replacement for an antivirus product. To help protect your computer, you should use an antivirus product.
Severity    : normal

Time        : 10/8/2019 12:00:00 AM
Title       : 2019-10 Cumulative Update for .NET Framework 3.5 and 4.8 for Windows 10 Version
              1903 for x64 (KB4524100)
KB          : KB4524100
Description : Install this update to resolve issues in Windows. For a complete listing of the issues that are included in this update, see the associated Microsoft Knowledge Base article for more information. After you install this item, you may have to restart your computer.
Severity    : normal

Time        : 10/28/2019 12:00:00 AM
Title       : Update for Windows Defender Antivirus antimalware platform - KB4052623 (Version 4.18.1910.4)
KB          : KB4052623
Description : This package will update Windows Defender Antivirus antimalware platform’s components on the user machine.
Severity    : normal

...

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() 方法。通过这些方法,您可以暂停测量代码中的某些部分(例如数据输出)并且继续测量。