PowerShell 技能连载 - 高级排序(第 2 部分)

Sort-Object 支持高级排序,并在您传入哈希表时提供更多控制。例如,哈希表可以单独控制多个属性的排序方向。

例如,此行代码按状态对服务进行排序,然后按名称排序。排序方向可以通过 -Descending 开关参数控制,但始终适用于所有选定的属性:

1
2
3
Get-Service | Sort-Object -Property Status, DisplayName | Select-Object -Property DisplayName, Status

Get-Service | Sort-Object -Property Status, DisplayName -Descending | Select-Object -Property DisplayName, Status

要单独控制排序方向,请传入哈希表并使用 ExpressionDescending 键。这首先按状态(降序)对服务进行排序,然后按显示名称(升序):

1
2
3
Get-Service |
Sort-Object -Property @{Expression='Status'; Descending=$true}, @{Expression='DisplayName'; Descending=$false } |
Select-Object -Property DisplayName, Status

注意:当您查看结果时,您可能会对“状态”属性的内容排序方式感到恼火。尽管代码要求降序排序,但您首先会看到正在运行的服务,然后是停止的服务。

这个问题的简单答案是:您现在知道如何单独控制排序,因此如果排序顺序错误,只需尝试颠倒排序顺序,看看是否适合您。

深入的答案是:”Status“ 属性实际上是一个数字常量,而 Sort-Object 始终对底层数据进行排序。因此,当您使数字常量可见时,会发现排序顺序是正确的:

1
2
3
Get-Service |
Sort-Object -Property @{Expression='Status'; Descending=$true}, @{Expression='DisplayName'; Descending=$false } |
Select-Object -Property DisplayName, { [int]$_.Status }

正如您现在所看到的,”Running“ 实际上是常量 “4”,而 “Stopped” 由常量 1 表示。

不要错过我们的下一个技能,以获得更多控制(以及对由底层数字常量引起的问题的优雅修复)。

PowerShell 技能连载 - 高级排序(第 1 部分)

Sort-Object easily sorts results for you. For primitive data such as numbers or strings, simply add Sort-Object to your pipeline. This gets you a sorted list of lottery numbers:
Sort-Object 能轻松对结果排序。对于数字或字符串等原始数据,只需将 Sort-Object 添加到您的管道中。这将为您提供一个排序的彩票号码列表:

1
2
3
4
$lottery = 1..49 | Get-Random -Count 7 | Sort-Object
# set the string you want to use to separate numbers in your output
$ofs = ','
"Your numbers are $lottery"

具有多个属性的对象数据要求您定义要排序的属性。此行代码按状态对服务进行排序:

1
Get-Service | Sort-Object -Property Status

Sort-Object 甚至支持多种排序。此行代码按状态对服务进行排序,然后按服务名称排序:

1
Get-Service | Sort-Object -Property Status, DisplayName | Select-Object -Property DisplayName, Status

要反转排序顺序,请添加 -Descending 开关参数。

有关排序的更多控制,例如,对一个属性升序排序和另一个属性降序排序,请参阅我们的下一个提示。

PowerShell 技能连载 - 速度很重要

PowerShell 是一种通用自动化语言,因此它的目标是多功能且易于使用。速度不是首要任务。

如果您确实关心最大速度,那么有一些 cmdlet 几乎可以完全满足 .NET 调用的功能。在这些实例中使用直接 .NET 调用会更快,尤其是在经常调用这些 cmdlet 时(例如在循环中)。但另一方面是它使您的代码更难阅读。

这里有一些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# cmdlet
PS> Join-Path -Path $env:temp -ChildPath test.txt
C:\Users\tobia\AppData\Local\Temp\test.txt

# direct .NET
PS> [System.IO.Path]::Combine($env:temp, 'test.txt')
C:\Users\tobia\AppData\Local\Temp\test.txt


# cmdlet
PS> Get-Date

Monday, October 4, 2021 12:34:46

# direct .NET
PS> [DateTime]::Now

Monday, October 4, 2021 12:34:52

PowerShell 技能连载 - 读取文本文件(快速)

如果您需要加载大文本文件并使用 Get-Content,那么您可以节省大量时间。

如果您没有立即通过管道处理通过管道发出的结果,则可能需要添加参数 -ReadCount 0。这可以使读取文本文件的速度提升 100 倍。

如果没有此参数,Get-Content 会单独对每个文本行产生一次输出。如果这些行要传递给管道处理,那么没有问题。但是如果要将文本存储在变量中并使用其他处理方式,则这是浪费时间,例如经典的 foreach 循环。

PowerShell 技能连载 - Working with Get-WinEvent

如果您想从 Windows 事件日志中读取系统事件,Get-Eventlog 是个易于使用且简单的选择。这段代码能够获取最新的 10 个错误和警告事件:

1
PS> Get-EventLog -LogName System -EntryType Error,Warning -Newest 10

不幸的是,Get-Eventlog 已被弃用,并且它不再可用于 PowerShell 7。弃用有很明显的原因:该 cmdlet 只能从“经典”日志文件中读取,它很缓慢并且具有其他限制。

这就是为什么 PowerShell 在3.0版中推出更好的替代命令:Get-WinEvent。PowerShell 7 也有此 cmdlet。

Unfortunately, Get-WinEvent is much harder to use because there are no intuitive parameters, and instead your filter criteria needs to be specified as a hash table.
不幸的是,Get-Winevent 更难使用,因为没有直观的参数,而是需要通过哈希表指定过滤条件。

但是,通过“代理函数”,您可以指导 Get-WinEvent 使用您所熟悉的旧式参数。以下是代码:

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
<#Suggestion to improve Get-WinEvent in order to make it compatible to the commonly used Get-EventLog callsBelow is a prototype using a proxy function. Run it to enhance Get-WinEvent.To get rid of the enhancement, either restart PowerShell or run:Remove-Item -Path function:Get-WinEventNote that the prototype emits the composed hash table to the console (green)
\#>function Get-WinEvent
{ [CmdletBinding(DefaultParameterSetName='GetLogSet', HelpUri='https://go.microsoft.com/fwlink/?LinkID=138336')]
param(

[Parameter(ParameterSetName='ListLogSet', Mandatory=$true, Position=0)]
[AllowEmptyCollection()]
[string[]]
${ListLog}, [Parameter(ParameterSetName='LogNameGetEventlog', Mandatory=$true, Position=0)] <#NEW\#>
[Parameter(ParameterSetName='GetLogSet', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]
${LogName}, [Parameter(ParameterSetName='ListProviderSet', Mandatory=$true, Position=0)]
[AllowEmptyCollection()]
[string[]]
${ListProvider}, <# Get-EventLog supports wildcards, Get-WinEvent does not. Needs to be corrected. #> [Parameter(ParameterSetName='GetProviderSet', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
[string[]]
${ProviderName}, [Parameter(ParameterSetName='FileSet', Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
${Path}, [Parameter(ParameterSetName='FileSet')]
[Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[Parameter(ParameterSetName='XmlQuerySet')]
[ValidateRange(1, 9223372036854775807)]
[long]
${MaxEvents},
<# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateRange(0, 2147483647)]
[int]
${Newest},
[Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='ListProviderSet')]
[Parameter(ParameterSetName='ListLogSet')]
[Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[Parameter(ParameterSetName='XmlQuerySet')]
[Parameter(ParameterSetName='LogNameGetEventlog')] <#NEW\#>
[Alias('Cn')]
[ValidateNotNullOrEmpty()] <#CORRECTED\#> [string] <# used to be [String[]], Get-WinEvent accepts [string] only, should be changed to accept string arrays \#> ${ComputerName}, [Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='ListProviderSet')]
[Parameter(ParameterSetName='ListLogSet')]
[Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[Parameter(ParameterSetName='XmlQuerySet')]
[Parameter(ParameterSetName='FileSet')]
[pscredential]
[System.Management.Automation.CredentialAttribute()]
${Credential}, [Parameter(ParameterSetName='FileSet')]
[Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='GetLogSet')]
[ValidateNotNull()]
[string]
${FilterXPath}, [Parameter(ParameterSetName='XmlQuerySet', Mandatory=$true, Position=0)]
[xml]
${FilterXml}, [Parameter(ParameterSetName='HashQuerySet', Mandatory=$true, Position=0)]
[hashtable[]]
${FilterHashtable}, [Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='ListLogSet')]
[Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[switch]
${Force}, [Parameter(ParameterSetName='GetLogSet')]
[Parameter(ParameterSetName='GetProviderSet')]
[Parameter(ParameterSetName='FileSet')]
[Parameter(ParameterSetName='HashQuerySet')]
[Parameter(ParameterSetName='XmlQuerySet')]
[switch]
${Oldest},
<# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateNotNullOrEmpty()]
[datetime]
${After}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateNotNullOrEmpty()]
[datetime]
${Before},
<# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateNotNullOrEmpty()]
[string[]]
${UserName}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog', Position=1)]
[ValidateRange(0, 9223372036854775807)]
[ValidateNotNullOrEmpty()]
[long[]]
${InstanceId}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[ValidateNotNullOrEmpty()]
[ValidateRange(1, 2147483647)]
[int[]]
${Index}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[Alias('ET')]
[ValidateNotNullOrEmpty()]
[ValidateSet('Error','Information','FailureAudit','SuccessAudit','Warning')]
[string[]]
${EntryType}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[Alias('ABO')]
[ValidateNotNullOrEmpty()]
[string[]]
${Source}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[Alias('MSG')]
[ValidateNotNullOrEmpty()]
[string]
${Message}, <# NEW \#> [Parameter(ParameterSetName='LogNameGetEventlog')]
[switch]
${AsBaseObject},
[Parameter(ParameterSetName='ListGetEventlog')]
[switch]
${List}, [Parameter(ParameterSetName='ListGetEventlog')]
[switch]
${AsString}



)

begin {
try {
$outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1 }
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Diagnostics\Get-WinEvent', [System.Management.Automation.CommandTypes]::Cmdlet)

\# if the user chose the Get-EventLog compatible parameters, \# compose the appropriate filter hash table $scriptCmd = if ($PSCmdlet.ParameterSetName -eq 'LogNameGetEventlog')
{
\# mandatory parameter $filter = @{
LogName = $PSBoundParameters['Logname']
}
$null = $PSBoundParameters.Remove('LogName')

if ($PSBoundParameters.ContainsKey('Before'))
{
$filter['EndTime'] = $PSBoundParameters['Before']
$null = $PSBoundParameters.Remove('Before')
}
if ($PSBoundParameters.ContainsKey('After'))
{
$filter['StartTime'] = $PSBoundParameters['After']
$null = $PSBoundParameters.Remove('After')
}
if ($PSBoundParameters.ContainsKey('EntryType'))
{
\# severity is translated to an integer array
$levelFlags = [System.Collections.Generic.List[int]]@()

\# string input converted to integer array if ($PSBoundParameters['EntryType'] -contains 'Error')
{
$levelFlags.Add(1) \# critical $levelFlags.Add(2) \# error }
if ($PSBoundParameters['EntryType'] -contains 'Warning')
{
$levelFlags.Add(3) \# warning }
if ($PSBoundParameters['EntryType'] -contains 'Information')
{
$levelFlags.Add(4) \# informational $levelFlags.Add(5) \# verbose }


\# default to 0 if ($levelFlags.Count -gt 0)
{
$filter['Level'] = [int[]]$levelFlags }

\# audit settings stored in Keywords key if ($PSBoundParameters['EntryType'] -contains 'FailureAudit')
{
$filter['Keywords'] += 0x10000000000000 }
if ($PSBoundParameters['EntryType'] -contains 'SuccessAudit')
{
$filter['Keywords'] += 0x20000000000000 }
$null = $PSBoundParameters.Remove('EntryType')
}
if ($PSBoundParameters.ContainsKey('InstanceId'))
{
$filter['ID'] = $PSBoundParameters['InstanceId']
$null = $PSBoundParameters.Remove('InstanceId')
}
if ($PSBoundParameters.ContainsKey('Source'))
{
$filter['ProviderName'] = $PSBoundParameters['Source']
$null = $PSBoundParameters.Remove('Source')
}

$PSBoundParameters['FilterHashtable'] = $filter Write-Host ($filter | Out-String) -ForegroundColor Green
if ($PSBoundParameters.ContainsKey('Newest'))
{
$PSBoundParameters['MaxEvents'] = $PSBoundParameters['Newest']
$null = $PSBoundParameters.Remove('Newest')
}
}


$scriptCmd =
{
& $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw }
}

process {
try {
$steppablePipeline.Process($_)
} catch {
throw }
}

end {
try {
$steppablePipeline.End()
} catch {
throw }
}
<#

.ForwardHelpTargetName Microsoft.PowerShell.Diagnostics\Get-WinEvent .ForwardHelpCategory Cmdlet \#>}

当您执行完这个函数,就可以通过您在 Get-EventLog 中熟悉的相同参数使用 Get-WinEvent

1
2
3
4
5
6
PS> Get-WinEvent -LogName System -EntryType Error,Warning -Newest 10

Name Value
---- -----
LogName {System}
Level {1, 2, 3}

您还可以获得基于参数使用的哈希表过滤器的键和值。

PowerShell 技能连载 - 调整脚本性能

如果脚本运行速度速度慢,找出导致延迟的地方并优化并不是一件很简单的事。使用名为 “psprofiler” 的 PowerShell 模块,可以测试脚本中的每行所需的时间。它在 Windows PowerShell 和 PowerShell 7 中都能运行。

首先安装模块:

1
PS> Install-Module -Name PSProfiler -Scope CurrentUser

下一步,用 Measure-Script 执行脚本:

1
PS> Measure-Script -Path 'C:\Users\tobias\test123.ps1'

Once your script completes, you get a sophisticated report telling you exactly how often each line of your script was executed, and how long it took:
脚本完成后,就会获得一份复杂的报告,告诉您脚本每行执行的次数,以及消耗的时间:

  Count  Line       Time Taken Statement
  -----  ----       ---------- ---------
      1     1    00:00.0033734 $Path = "$env:temp\tv.json"
      0     2    00:00.0000000
      1     3    00:28.1602885 $data = Get-Content -Path $Path -Raw |
      0     4    00:00.0000000 ConvertFrom-Json |
      1     5    00:26.6558438 ForEach-Object { $_ } |
      0     6    00:00.0000000 ForEach-Object {
      0     7    00:00.0000000
 101000     8    00:01.4408993   $title = '{0,5} [{2}] "{1}" ({3})' -f ([Object[]]$_)
 101000     9    00:13.6815132   $title | Add-Member -MemberType NoteProperty -Name Data -Value $_ -PassThru
      0    10    00:00.0000000 }
      0    11    00:00.0000000
...

PowerShell 技能连载 - 将 Ticks 转换为 DateTime

偶尔,日期和时间信息以所谓的“缺陷”的格式存储为 “Ticks”。 Ticks 是自 01/01/1601 以来,100 纳秒的单位数。Active Directory 在内部使用此格式,但您也可以在其他地方找到它。 以下是以 “Ticks” 为单位的 Windows 安装时间的示例:

1
2
3
4
$values = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$installDateTicks = $values.InstallTime

$installDateTicks

结果是(非常)大的 64 比特数字:

132457820129777032

要将 Ticks 转换为 DateTime,请使用 [DateTimeOffset]

1
2
3
4
5
$values = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$installDateTicks = $values.InstallTime

$installDate = [DateTimeOffset]::FromFileTime($installDateTicks)
$installDate.DateTime

PowerShell 技能连载 - 将 UNIX 时间转为 DateTime

“UNIX时间”计算自 01/01/1970 以来经过的秒数。

例如,在 Windows 中,您可以从 Windows 注册表中读取安装日期,返回的值为 “Unix时间”:

1
2
3
4
$values = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$installDateUnix = $values.InstallDate

$installDateUnix

结果是类似这样的大数字:

1601308412

To convert “Unix time” to a real DateTime value, .NET Framework provides a type called [DateTimeOffset]:
要将“UNIX时间”转换为真实的 DateTime 值,请使用 .NET Framework 提供的 [DateTimeOffset] 类:

1
2
3
4
$values = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$installDateUnix = $values.InstallDate

[DateTimeOffset]::FromUnixTimeSeconds($installDateUnix)

现在您能得到不同的日期和时间表示:

DateTime      : 28.09.2020 15:53:32
UtcDateTime   : 28.09.2020 15:53:32
LocalDateTime : 28.09.2020 17:53:32
Date          : 28.09.2020 00:00:00
Day           : 28
DayOfWeek     : Monday
DayOfYear     : 272
Hour          : 15
Millisecond   : 0
Minute        : 53
Month         : 9
Offset        : 00:00:00
Second        : 32
Ticks         : 637369052120000000
UtcTicks      : 637369052120000000
TimeOfDay     : 15:53:32
Year          : 2020

要获取本地格式的安装时间,您可以在一行代码中写完它:

1
2
3
PS> [DateTimeOffset]::FromUnixTimeSeconds((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').InstallDate).DateTime

Moday, September 28, 2020 15:53:32

PowerShell 技能连载 - 创建动态参数

动态参数是一种特殊的参数,可以根据运行时条件显示或隐藏。 您的 PowerShell 函数可以例如具有一个参数,并基于用户选择的操作,将显示其他参数。或者,只有在用户具有管理员权限时才能显示参数。

不幸的是,组合动态参数并不是一件轻松的事。借助称为 “dynpar” 的模块,使用动态参数变得同样简单,就像使用“普通”静态参数一样简单,然后您可以简单地使用名为 [Dynamic()] 的新属性指定动态参数,该属性告诉 PowerShell 需要满足以哪些条件以显示参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
param
(
# regular static parameter
[string]
$Normal,

# show -Lunch only at 11 a.m. or later
[Dynamic({(Get-Date).Hour -ge 11})]
[switch]
$Lunch,

# show -Mount only when -Path refers to a local path (and not a UNC path)
[string]
$Path,

[Dynamic({$PSBoundParameters['Path'] -match '^[a-z]:'})]
[switch]
$Mount
)

您可以在此处找到一份详细的操作指南:https://github.com/tobiaspsp/modules.dynpar

PowerShell 技能连载 - 分割文本行(第 3 部分)

在上一个技能中,我们将一大块多行文本拆分为单独的行,并删除了所有空行。

然而,当一行不是真正的空,而是包含空格(空格或制表符)时,它仍然会被输出:

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
# $data is a single string and contains blank lines
$data = @'

Server1


Server2
Cluster4


'@

$data = $data.Trim()

# split in single lines and remove empty lines
$regex = '[\r\n]{1,}'
$array = $data -split $regex

$array.Count

$c = 0
Foreach ($_ in $array)
{
'{0:d2} {1}' -f $c, $_
$c++
}

在这里,我们在 “Server2” 正上方的行中添加了几个空格(当然,在列表中看不到)。以下是执行结果:

00 Server1
01
02 Server2
03 Cluster4

由于我们要在任意数量的 CR 和 LF 字符处拆分,因此空格会破坏该模式。

与其将正则表达式变成一个更复杂的野兽,不如为这些事情附加一个简单的 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
24
25
26
27
# $data is a single string and contains blank lines
$data = @'

Server1


Server2
Cluster4


'@

$data = $data.Trim()

# split in single lines and remove empty lines
$regex = '[\r\n]{1,}'
$array = $data -split $regex |
Where-Object { [string]::IsNullOrWhiteSpace($_) -eq $false }

$array.Count

$c = 0
Foreach ($_ in $array)
{
'{0:d2} {1}' -f $c, $_
$c++
}

[string]::IsNullOrEmpty() 代表我们所追求的情况,因此符合条件的行被 Where-Object 删除。结果就是所需要的理想结果:

00 Server1
01 Server2
02 Cluster4