PowerShell 技能连载 - 小心使用数组

使用 PowerShell,您永远不知道 cmdlet 是返回数组还是单个对象。这是因为一旦命令返回多个项目,PowerShell 就会自动包装成数组:

1
2
3
4
5
6
7
# no array:
$test = Get-Service -Name Spooler
$test -is [Array]

# array:
$test = Get-Service -Name S*
$test -is [Array]

理解这一点很重要,因为这意味着运行时条件可以确定变量的类型。这可能会导致意外情况。以下是说明问题的一个示例:

下面的代码返回以 “C” 开头的所有服务的名称,然后取第一个服务名称。这是有可能的,因为不仅有一个以 “C” 开头的服务,因此 PowerShell 返回 $ServiceNames 中的数组,然后您可以在此数组中使用数字索引来选择特定的元素:

1
2
3
4
5
6
7
$Name = 'c*'

# get service names
$servicenames = Get-Service -Name $Name | Select-Object -ExpandProperty Name

# get first service name
$servicenames[0]

但是,您不能假设 $servicenames 始终是一个数组。如果在运行时只有一项与您的请求匹配的服务,则结果不再是一个数组,而是直接是服务名称。

为什么(以及何时)这有关系?当您的代码采用数组特定功能的那一刻,它就十分重要。因为在某些情况下可能不存在该功能或行为不同。

为了说明这一点,下面的代码现在列出了以 “cry” 开头的所有服务。只有一项服务与请求匹配。因此,$servicenames 不再是一个数组。现在是一个字符串。当您在字符串上使用索引时,您会得到该字符串中的一个字母。

现在,相同的代码返回的是一个字符,而不是服务名称:

1
2
3
4
5
6
7
$Name = 'cry*'

# get service names
$servicenames = Get-Service -Name $Name | Select-Object -ExpandProperty Name

# get first service name
$servicenames[0]

这些示例似乎有些人为构造,但是您可以在许多难以找到的脚本错误的内部中找到潜在的问题。这就是为什么重要的是要始终确保您在代码使用数组功能时获得的真正是一个数组。

确保您获得数组的一种简单方法是构造器 @():括号中的任何内容都以数组的形式返回。这就是为什么下面代码有效的原因,无论命令是否返回一个或多个结果:

1
2
3
4
5
6
7
8

$Name = 'cry*'

# get service names
$servicenames = @(Get-Service -Name $Name | Select-Object -ExpandProperty Name)

# get first service name
$servicenames[0]
1
2
3
4
5
6
7
$Name = 'cry*'

# get service names
[array]$servicenames = Get-Service -Name $Name | Select-Object -ExpandProperty Name

# get first service name
$servicenames[0]
1
2
3
4
5
6
7
$Name = 'cry*'

# get service names
[string[]]$servicenames = Get-Service -Name $Name | Select-Object -ExpandProperty Name

# get first service name
$servicenames[0]

但是,[array] 更容易使用,因为无论数据类型如何,它总是可以使用,并且 [array] 对于不熟悉类型的用户也更容易理解。

PowerShell 技能连载 - Determining Language Packs (Part 3)

在本系列的第 2 部分中,您已经看到了使用 WMI 与使用命令行工具(如 dism.exe)相比,使用 WMI 查询安装的操作系统语言列表的速度要容易得多,且更快。但是,使用 WMI 仍然需要您知道适当的 WMI 类名。

这就是为什么 PowerShell 提供一个全能的 cmdlet Get-ComputerInfo 的原因。它为您查询各种与计算机相关的信息,然后将其与您联系。 我们也可以通过这种方法解决这个问题:

1
2
$a = Get-ComputerInfo
$a.OsMuiLanguages

但不幸的是,Get-ComputerInfo 总是查询完整的信息集,这使得它很缓慢。但总比没有好,甚至比 dism.exe 更好,而第 2 部分的直接 WMI 查询仍然是效率最高的方法。

PowerShell 技能连载 - 当格式化失败时

您在使用 PowerShell 时可能会遇到一个奇怪的格式问题:当您一行一行执行代码时,得到的输出结果和以整体的方式执行一段代码有所不同。

这是要执行的示例代码:

Get-Process -Id $pid
Get-Date
Get-Service -Name Spooler

当您将三行代码作为脚本整体运行时,只有第一个命令返回表格,后两个显示列表。但是,当您逐行执行代码时,它们的格式不同,并显示为表格,甚至是单行纯文本。

这是 PowerShell 实时输出格式的副作用:当输出格式化器遇到第一个返回数据时,它必须决定格式化和写入,即表头。所有剩余的数据将插入到该输出格式中。

每当您的脚本返回多个对象并且这些对象具有不同类型时,PowerShell 就会意识到这些对象不适合现有表设计。在这种情况下,所有后续对象将格式化成列表视图。

如果您想更好地控制此行为,则可以随时将输出发送到 Out-Default,这将关闭当前的输出。任何后续对象都将启动新的输出格式。以下代码将始终显示相同的显示格式,无论您是作为脚本运行还是单独运行命令:

1
2
3
Get-Process -Id $pid | Out-Default
Get-Date | Out-Default
Get-Service -Name Spooler

PowerShell 技能连载 - 获取系统正常运行时间

The Get-ComputerInfo cmdlet 可以提供有关 Windows 客户端或服务器的大量信息,例如正常运行时间和其他相关信息。

试试以下代码:

1
2
3
4
5
PS> Get-ComputerInfo | Select-Object -Property *Upt*

CsWakeUpType OsLastBootUpTime OsUptime
------------ ---------------- --------
PowerSwitch 25.10.2022 17:32:23 6.23:07:47.4872044

该命令获取所有名字中包含 “upt” 的属性,这些属性恰好都包含了和系统运行时间有关的信息。

当然,您还可以将信息存储到变量中,并单独查询属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PS> $info = Get-ComputerInfo | Select-Object -Property *Upt*

PS> $info.OsUptime


Days : 6
Hours : 23
Minutes : 9
Seconds : 28
Milliseconds : 617
Ticks : 6017686177952
TotalDays : 6,96491455781481
TotalHours : 167,157949387556
TotalMinutes : 10029,4769632533
TotalSeconds : 601768,6177952
TotalMilliseconds : 601768617,7952


PS> $info.OsUptime.TotalHours
167,157949387556

由于属性值显示在一列中,它们显示为一行字符串。如果您单独查询它们,例如 OsUptime,它们将暴露它们的所有自身属性。

PowerShell 技能连载 - 无人值守读取 PFX 证书

PowerShell 配备了一个名为 Get-PfxCertificate 的 cmdlet,您可以用来将证书和私钥加载到内存中。但是如果证书受密码保护,则有一个强制性提示来输入密码。您不能通过参数提交密码,因此该 cmdlet 不能无人值守使用。

这是一个替代的函数,允许通过参数输入密码,从而允许以无人值守的方式即时加载 pfx 证书:

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

[SecureString]
[Parameter(Mandatory)]
$Password
)

# get clear text password
$plaintextPassword = [PSCredential]::new("X", $Password).GetNetworkCredential().Password


[void][System.Reflection.Assembly]::LoadWithPartialName("System.Security")
$container = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$container.Import($FilePath, $plaintextPassword, 'PersistKeySet')
$container[0]
}

请注意,该功能始终返回 pfx 文件中发现的第一个证书 如果您的PFX文件包含多个证书,则可能需要在最后一行代码中调整索引。

PowerShell 技能连载 - 创建新的代码签名测试证书

PowerShell 配备了一个名为 New-SelfSignedCertificate 的 cmdlet,可以创建各种自签名的测试证书。但是,使用它为 PowerShell 代码签名创建证书并不直观,更不用说在测试机上确保测试证书值得信任。

所以我们编写了一个函数将上述 cmdlet 包装起来,使得创建既持久且可导出的代码签名证书变得更加容易:

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
function New-CodeSigningCert
{
[CmdletBinding(DefaultParametersetName="__AllParameterSets")]
param
(
[Parameter(Mandatory)]
[String]
$FriendlyName,

[Parameter(Mandatory)]
[String]
$Name,

[Parameter(Mandatory,ParameterSetName="Export")]
[SecureString]
$Password,

[Parameter(Mandatory,ParameterSetName="Export")]
[String]
$FilePath,

[Switch]
$Trusted
)

# create new cert
$cert = New-SelfSignedCertificate -KeyUsage DigitalSignature -KeySpec Signature -FriendlyName $FriendlyName -Subject "CN=$Name" -KeyExportPolicy ExportableEncrypted -CertStoreLocation Cert:\CurrentUser\My -NotAfter (Get-Date).AddYears(5) -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3')


if ($Trusted)
{
$Store = New-Object system.security.cryptography.X509Certificates.x509Store("Root", "CurrentUser")
$Store.Open("ReadWrite")
$Store.Add($cert)
$Store.Close()
}


$parameterSet = $PSCmdlet.ParameterSetName.ToLower()

if ($parameterSet -eq "export")
{
# export to file
$cert | Export-PfxCertificate -Password $Password -FilePath $FilePath

$cert | Remove-Item
explorer.exe /select,$FilePath
}
else
{
$cert
}
}

PowerShell 技能连载 - 获取卷 ID(第 2 部分)

在 Windows 10 及以上版本,您可以使用 Get-Volume 获取有关驱动器的卷 ID 和其他信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS> Get-Volume

DriveLetter FriendlyName FileSystemType DriveType HealthStatus OperationalStatus SizeRemaining Size
----------- ------------ -------------- --------- ------------ ----------------- ------------- ----
WINRETOOLS NTFS Fixed Healthy OK 315.58 MB 990 MB
C OS NTFS Fixed Healthy OK 154.79 GB 938.04 GB
Image NTFS Fixed Healthy OK 107.91 MB 12.8 GB
DELLSUPPORT NTFS Fixed Healthy OK 354.46 MB 1.28 GB
``

请注意,尽管您最初只会看到一小部分可用信息。但将数据发送到管道可以查看到所有信息,包括卷 ID:

```powershell
Get-Volume | Select-Object -Property *

这是一个示例:

1
2
3
4
5
6
7
8
PS> Get-Volume | Select-Object -Property DriveLetter, FileSystemLabel, Size, Path

DriveLetter FileSystemLabel Size Path
----------- --------------- ---- ----
WINRETOOLS 1038086144 \\?\Volume{733298ae-3d76-4f5f-acc4-50fdca0c6401}\
C OS 1007210721280 \\?\Volume{861c48b0-d434-48d3-995a-0573c1336eb7}\
Image 13739487232 \\?\Volume{9dc0ed9d-86fd-4cd5-9ed8-3249f57720ad}\
DELLSUPPORT 1371533312 \\?\Volume{b0f36c9e-2372-47f9-8b84-cdf65447c9c6}\

PowerShell 技能连载 - 遮罩输入框(第 2 部分)

永远不要将纯文本输入框用于保密信息和密码——用户输入的文本可能被记录和利用。请始终使用遮罩输入框。这是使用参数的一种简单方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
param
(
[Parameter(Mandatory)]
[SecureString]
# asking secret using masked input box
$secret
)

# internally, get back plain text
$data = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret)
$plain =[Runtime.InteropServices.Marshal]::PtrToStringAuto($data)

Write-Host "You secret: $plain" -ForegroundColor Yellow

只需将数据类型 [SecureString] 用于您的参数,这样将将其强制添加一个带遮罩的输入框。