PowerShell 技能连载 - 拆分而不丢失字符

拆分文本时,通常会丢失拆分字符。这就是为什么这个例子中的反斜杠丢失的原因:

1
2
3
4
PS> 'c:\test\file.txt' -split '\\'
c:
test
file.txt

重要提示:请注意 -split 运算符需要一个正则表达式。如果你想在反斜杠处拆分,因为反斜杠是正则表达式中的一个特殊字符,你需要对它进行转义。以下调用告诉您需要转义的内容:提交要使用的文字文本。结果是转义的正则表达式文本:

1
2
PS> [regex]::Escape('\')
\\

如果你想拆分而不丢失任何字符,你可以使用所谓的先行断言和后行断言。以下代码在一个反斜杠分割(不删除它):

1
2
3
4
PS> 'c:\test\file.txt' -split '(?<=\\)'
c:\
test\
file.txt

以下代码在每个反斜杠之前拆分:

1
2
3
4
PS> 'c:\test\file.txt' -split '(?=\\)'
c:
\test
\file.txt

PowerShell 技能连载 - 排序技巧(第 4 部分)

在上一个技能中,我们展示了 Sort-Object 通过属性名称、哈希表或普通脚本块来排序,并且我们使用脚本块来控制排序算法,并按日期和时间而不是字母数字对字符串信息进行排序。

在这最后一个示例中,让我们使用它来对 IPv4 地址进行排序。默认情况下,Sort-Object 将它们视为纯文本并使用字母排序:

1
2
3
4
PS> '10.12.11.1', '298.12.11.112', '8.8.8.8' | Sort-Object
10.12.11.1
298.12.11.112
8.8.8.8

要正确排序 IPv4 地址,您可以将它们转换为 [version] 类型,该类型也包含四个数字:

1
2
3
4
5
PS> '10.12.11.1', '298.12.11.112', '8.8.8.8' | Sort-Object -Property { $_ -as [version] }

8.8.8.8
10.12.11.1
298.12.11.112

PowerShell 技能连载 - 排序技巧(第 3 部分)

在上一个技能中,我们展示了 Sort-Object 接受属性名称、哈希表或普通脚本块来对事物进行排序。让我们看看为什么将脚本块提交给 Sort-Object 是个好主意。

假设您有一堆表示日期的字符串数据,并且您想对它们进行排序:

1
2
3
4
5
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' | Sort-Object

Feb 1, 1999
June 12, 2021
May 12, 2020

结果已排序,但不按日期排序。由于输入是字符串,Sort-Object 使用字母数字排序算法。您现在可以将原始数据转换为另一种格式并对其进行排序。

但是,您也可以要求 Sort-Object 进行转换。不同之处在于原始数据格式保持不变:一系列字符串将按日期排序,但仍然是字符串:

1
2
3
4
5
6
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' |
Sort-Object -Property { [DateTime]$_ }

Feb 1, 1999
May 12, 2020
June 12, 2021

当然,您的工作是想出一个脚本块,将 $_ 中传入的原始数据正确转换为您想要用于排序的所需类型。对于日期,您可能还想查看使用本地日期和时间格式的 -as 运算符,而直接转换始终使用美国格式:

1
2
3
4
5
PS> 'May 12, 2020', 'Feb 1, 1999', 'June 12, 2021' | Sort-Object -Property { $_ -as [DateTime] }

Feb 1, 1999
May 12, 2020
June 12, 2021

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

在上一个技能中,我们展示了 Sort-Object 如何对多个属性进行排序,以及如何使用哈希表分别控制每个属性的排序方向。不过,哈希表还可以做更多的事情。

例如,哈希表键 “Expression“ 可以是一个脚本块,然后针对您要排序的每个项目执行该脚本块。脚本块的结果决定了排序顺序。

这就是为什么这行代码每次都会以不同的方式重新排列数字列表:

1
1..10 | Sort-Object -Property @{Expression={ Get-Random }}

本质上,本示例使用 Get-Random 的随机结果对数字列表进行随机排序。这可能很有用,即当您使用密码生成器并希望随机分配脚本计算的字符时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# compose password out of these
$Capitals = 2
$Numbers = 1
$lowerCase = 3
$Special = 1

# collect random chars from different lists in $chars
$chars = & {
'ABCDEFGHKLMNPRSTUVWXYZ'.ToCharArray() | Get-Random -Count $Capitals
'23456789'.ToCharArray() | Get-Random -Count $Numbers
'abcdefghkmnprstuvwxyz'.ToCharArray() | Get-Random -Count $lowerCase
'!§$%&?=#*+-'.ToCharArray() | Get-Random -Count $Special
} | # <- don't forget pipeline symbol!
# sort them randomly
Sort-Object -Property { Get-Random }

# convert chars to one string
$password = -join $chars
$password

正如您在实际示例中看到的,Sort-Object 还接受一个简单的脚本块,它表示哈希表的 “Expression“ 键。两行的工作方式相同(但当然会产生不同的随机结果):

1
1..10 | Sort-Object -Property @{Expression={ Get-Random }}1..10 | Sort-Object -Property { Get-Random }

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

Sort-Object 是用于排序的 cmdlet:只需指定要排序的属性,Sort-Object 涵盖其余部分,包括根据属性数据类型选择正确的排序算法:

1
Get-Service | Sort-Object -Property DisplayName -Descending

一个鲜为人知的事实是 Sort-Object 也接受哈希表,这给了你更多的控制权。例如,您可以像这样轻松地对多个属性进行排序:

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

但是,如果您想控制每个属性的排序方向,则需要一个哈希表。此示例按降序对状态进行排序,但按升序对显示名称进行排序:

1
2
3
4
5
6
$displayName = @{
Expression = "DisplayName"
Descending = $false
}

Get-Service | Sort-Object -Property Status, $displayName -Descending

PowerShell 技能连载 - 对文本做哈希

PowerShell 附带了 Get-FileHash 命令,它读取文件并计算唯一的哈希值。这非常适合测试文件是否具有相同的内容。但是,无法对纯文本进行哈希处理。

当然,您可以将要哈希的文本写入文件,然后使用 Get-FileHash

更好的方法是 Get-FileHash 的一个很少人知道的功能。除了传入文件路径之外,您还可以提交所谓的 “MemoryStream“,当您将文本加载内存流,再调用命令,就能得到哈希值:

1
2
3
4
5
6
7
8
9
10
11
$text = 'this is a test'

$memoryStream = [System.IO.MemoryStream]::new()
$streamWriter = [System.IO.StreamWriter]::new($MemoryStream)
$streamWriter.Write($text)
$streamWriter.Flush()
$memoryStream.Position = 0
$hash = Get-FileHash -InputStream $MemoryStream -Algorithm 'SHA1'
$memoryStream.Dispose()
$streamWriter.Dispose()
$hash.Hash

只是不要忘记在使用后处理 MemoryStreamStreamWriter 对象以释放内存。

PowerShell 技能连载 - 检测 Windows 版本(第 3 部分)

在上一个技巧中,我们说明访问 WinRT 类 AnalyticsInfo 似乎是读取当前 Windows 10 版本的唯一受支持方式。与使用上一个示例中的异步方法不同,为了仅获取当前的 Windows 10 版本,这里有一个更简单的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# get raw Windows version
[int64]$rawVersion =
[Windows.System.Profile.AnalyticsInfo,Windows.System.Profile,ContentType=WindowsRuntime].
GetMember('get_VersionInfo').Invoke( $Null, $Null ).DeviceFamilyVersion

# decode bits to version bytes
$major = ( $rawVersion -band 0xFFFF000000000000l ) -shr 48
$minor = ( $rawVersion -band 0x0000FFFF00000000l ) -shr 32
$build = ( $rawVersion -band 0x00000000FFFF0000l ) -shr 16
$revision = $rawVersion -band 0x000000000000FFFFl

# compose version
$winver = [System.Version]::new($major, $minor, $build, $revision)
$winver

请注意,PowerShell 7 (pwsh.exe) 无法访问此 API。该代码需要 Windows PowerShell (powershell.exe)。

PowerShell 技能连载 - 检测 Windows 版本(第 2 部分)

在之前的技能中,我们报告了 ReleaseId 已弃用,无法再用于正确识别当前的 Windows 10 版本。相反,应该使用 DisplayVersion

1
2
PS> (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').DisplayVersion
20H2

但是,DisplayVersion 也不是确定当前 Windows 10 版本的可靠方法,因为其最初的目的是使外壳能够向用户报告当前版本。它也可能在未来发生变化。

识别当前 Windows 10 版本的唯一受支持的安全方法是使用名为 AnalyticsInfo 的操作系统类。不过用起来比较复杂,因为该类在 WinRT 中异步运行。PowerShell 7 (pwsh.exe) 无法访问此类。但是,内置的 Windows PowerShell (powershell.exe) 可以创建包装器并返回信息:

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
# load WinRT and runtime types
[System.Void][Windows.System.Profile.AnalyticsInfo,Windows.System.Profile,ContentType=WindowsRuntime]
Add-Type -AssemblyName 'System.Runtime.WindowsRuntime'

# define call and information to query
[Collections.Generic.List[System.String]]$names = 'DeviceFamily',
'OSVersionFull',
'FlightRing',
'App',
'AppVer'

$task = [Windows.System.Profile.AnalyticsInfo]::GetSystemPropertiesAsync($names)

# use reflection to find method definition
$definition = [System.WindowsRuntimeSystemExtensions].GetMethods().Where{
$_.Name -eq 'AsTask' -and
$_.GetParameters().Count -eq 1 -and
$_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'
}

# create generic method
$Method = $definition.MakeGenericMethod( [System.Collections.Generic.IReadOnlyDictionary[System.String,System.String]] )

# call async method and wait for completion
$task = $Method.Invoke.Invoke($null, $task)
$null = $task.Wait(-1)

# emit output
$task.Result

结果类似于:

Key           Value
---           -----
OSVersionFull 10.0.19042.985.amd64fre.vb_release.191206-1406
FlightRing    Retail
App           powershell_ise.exe
AppVer        10.0.19041.1
DeviceFamily  Windows.Desktop

OSVersionFull 返回有关当前 Windows 版本的完整详细信息。

请注意,上面示例中调用的方法可以检索更多详细信息。$names 列出要查询的属性名称。不幸的是,没有办法发现可用的属性名称,因为第三方方可能会添加无穷无尽的附加信息。本示例中使用的五个属性是唯一保证的属性。

PowerShell 技能连载 - 检测 Windows 版本(第 1 部分)

可以从 Windows 注册表轻松读取当前的 Windows 版本:

1
2
3
4
5
PS> Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' | Select-Object -Property ReleaseId, DisplayVersion

ReleaseId DisplayVersion
--------- --------------
2009 20H2

请注意,不推荐使用 ReleaseId 中显示的信息。新的 Windows 10 版本改为使用 DisplayVersion 属性。要正确识别 Windows 10 版本,请确保使用 DisplayVersion 而不是 ReleaseId

在上面的示例中,您可以看到 Windows 10 报告了许多不同版本的相同 ReleaseId (2009)。例如,DisplayVersion 20H1 也使用 ReleaseId 2009。

微软宣布 ReleaseId 已弃用,不能再用于正确识别 Windows 10 版本。这对于脚本以及 WSUS、WAC 等工具可能很重要。例如,像 Get-WindowsImage 这样的 cmdlet 现在很难识别正确的 Windows 10 版本。

PowerShell 技能连载 - 原版 Windows 10 产品密钥

有大量 PowerShell 脚本到处流传,它们生成可以解码原始的 Windows 10 产品密钥。大多数这些脚本使用过时的算法,这些算法不再适用于 Windows 10。

这是检索原始 Windows 10 产品密钥的更简单的方法:

1
2
Get-CimInstance -ClassName SoftwareLicensingService |
Select-Object -ExpandProperty OA3xOriginalProductKey

如果该命令不产生任何返回信息,则操作系统安装中没有存储单独的产品密钥。