PowerShell 技能连载 - 转义独立的字符

在前一个技能中,我们解释了如何转义整个字符串序列。如果您只需要转义单个字符,请使用 HexEscape() 如:

1
2
PS> [Uri]::HexEscape('a')
%61

此方法实际上是检索 ASCII 代码并将其转换为十六进制。

实际上,还可以进行相反的操作,您可以将转义的字符转换回正常字符。例如,”a” 的 ASCII 代码为 65,它的十六进制表达是 41。因此,”A” 的转义表示为 “%41”,这行代码将得到 “A”:

1
2
PS C:\> [Uri]::HexUnescape('%41',[ref]0)
A

(第二个参数表示要转换转义的字符在字符串中的位置)。

有了这个,您现在可以生成一个范围内的字母:首先,生成所需字母的 ASCII 代码,并以十六进制形式手动转换它们。-f 运算符可以执行此转换:

1
2
3
4
PS> $decimal = 65
PS> $hex = '{0:X}' -f $decimal
PS> $hex
41

以下是来自 A 到 Z 的转义字母:

1
65..90 | ForEach-Object { '%{0:X}' -f $_ }

反转义它们的方法:

1
65..90 | ForEach-Object { [Uri]::HexUnescape( ('%{0:X}' -f $_), [ref]0) }

不过,不要爱上这个过度的技巧。类型转换可以让您更轻松实现:

1
[char[]](65..90)

PowerShell 技能连载 - 安全地转义数据字符串

通常,您使用像 EscapeUriString() 这样的方法来安全地转义要追加到 URL 的字符串数据(如我们之前的技能中所指出的)。

但是,这可能会导致头疼的情况,因为 EscapeUriString() 专门设计用于转义包括域名部分的完整URL,而不仅仅是您的参数。这就是为什么它可能损坏您要发送给其他人的数据,即莫尔斯 Web Service。尝试运行以下代码:

1
2
3
4
5
6
$url = "https://api.funtranslations.com/translate/morse.json?text=One&Two"

$Escaped = [Uri]::EscapeUriString($url)

$result = Invoke-RestMethod -Uri $url
$result.contents

返回的结果可以看出问题:

translated text translation
---------- ---- -----------
--- -. .   One  morse

即使您将文本 “One&Two” 发送给 WebService,它也仅会返回 “One” 的摩尔斯代码。当您查看 $Escaped 的内容时,就能发现原因:

1
2
PS C:\> $escaped
https://api.funtranslations.com/translate/morse.json?text=One&Two

EscapeUriString 没有将 & 字符转义——因为 & 是URL的有效部分,它用于分割参数。实质上,WebService 接收了两个参数,因为它只支持一个,所以它丢弃了第二个参数。

虽然 EscapeUriString() 能很方便快速转义完整的网址,但它具有严重的缺点。要解决此问题,请务必确保将 base URL 和数据参数分开。您可以使用 EscapeDataString(),来代替 EscapeUriString() 来确保所有特殊字符被正确转义:

1
2
3
4
5
6
7
$Text = 'One&Two'
$Escaped = [Uri]::EscapeDataString($Text)
$baseurl = "https://api.funtranslations.com/translate/morse.json?text="
$url = $baseurl + $Escaped

$resultok = Invoke-RestMethod -Uri $url
$resultok.contents

现在结果是正确的(用于演示的 WebService 确实具有速率限制,因此如果您过于频繁调用它,将需要等待一小时来验证):

这是因为 $escaped 现在转义了所有特殊字符,包括 & 符号:

1
2
PS> $escaped
One%26Two

此外,您还有一张“返程票:UnescapeDataString(),它能将转义后的数据恢复正常:

1
2
3
4
5
6
7
8
9
# escape special characters
$text = 'This is a text with [special] characters!'

$escaped = [Uri]::EscapeDataString($text)
$escaped

# unescape escaped strings
$unescaped = [Uri]::UnescapeDataString($escaped)
$unescaped

结果如下所示:

This%20is%20a%20text%20with%20%5Bspecial%5D%20characters%21
This is a text with [special] characters!

PowerShell 技能连载 - 转义 URL 字符串

将字符串信息添加到 URL 时,即要构造用于调用 REST Web 服务的请求时,重要的一环是要转义特殊字符。[Uri] 类型为 URL 提供了转义与反转义方法:

1
2
3
$Text = 'SOS Save me please!'
$Escaped = [Uri]::EscapeUriString($Text)
$Escaped

结果如下所示:

SOS%20Save%20me%20please!

现在,您可以安全地将转义的字符串数据发送到 RESTful Web 服务。以下代码将文本转换为摩尔斯码:

1
2
3
4
5
6
7
8
$Text = 'SOS Save me please!'
$url = "https://api.funtranslations.com/translate/morse.json?text=$Text"

$Escaped = [Uri]::EscapeUriString($url)


$result = Invoke-RestMethod -Uri $url
$result.contents.translated

结果现在看起来像这样:

... --- ...     ... .- ...- .     -- .     .--. .-.. . .- ... . ---.

有些时候,使用此方法转义字符串可能会破坏某些查询字符串。要解决这个问题,请查看明天的技能。

PowerShell 技能连载 - 动态生成 IntelliSense.

在设计 PowerShell 函数时,您可以通过添加智能参数完成 IntelliSense 来提高可用性。

要编写某个参数的 IntelliSense 自动完成,您可以使用动态生成 IntelliSense 列表的 PowerShell 代码来填充函数的每个参数。当然,您使用的代码应该能快速计算出结果,IntelliSense 才不会超时。

如果需要将方法(命令)添加到对象,请通过 Add-Member 添加它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Get-MyLocalUser
{
#Content
param
(
[String]
[Parameter(Mandatory)]
[ArgumentCompleter({
# receive information about current state:
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

Get-LocalUser |
ForEach-Object {
$name = $_.Name
$desc = $_.Sid # showing SID as QuickTip
[System.Management.Automation.CompletionResult]::new($name, $name, "ParameterValue", $desc)
}
})]
$UserName
)

"You chose $UserName"

}

运行代码后,在交互式控制台中键入:

1
PS> Get-MyLocalUser -UserName

-UnerName 后按下空格,IntelliSense 介入并显示所有本地用户名。当您选择一个 IntelliSense 项目时,QuickTip 会显示用户的 SID。

此智能参数完成在 [ArgumentCompleter()] 属性中定义。 它内部的代码生成了 CompletionResult 对象,每个对象对应一个 IntelliSense 列表项。

PowerShell 技能连载 - Windows 重启后自动登录

如果您的自动化脚本需要重新启动机器,并且您希望在重新启动后自动登录,那么以下是一个快速的脚本,它将登录凭据保存到 Windows 注册表:

1
2
3
4
5
6
7
8
9
10
11
12
13
# ask for logon credentials:
$cred = Get-Credential -Message 'Logon automatically'
$password = $cred.GetNetworkCredential().Password
$username = $cred.UserName

# save logon credentials to registry (WARNING: clear text password used):
$path = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
Set-ItemProperty -Path $path -Name AutoAdminLogon -Value 1
Set-ItemProperty -Path $path -Name DefaultPassword -Value $password
Set-ItemProperty -Path $path -Name DefaultUserName -Value $username

# restart machine and automatically log on: (remove -WhatIf to test-drive)
Restart-Computer -WhatIf

如果您希望每次计算机启动时自动登录,那么可以使用相同的方法。

显然,此技术可能会增加安全风险:密码以明文的方式写入注册表。请只在合适的地方谨慎使用它。

PowerShell 技能连载 - 自动化下载联想驱动程序(第 2 部分)

在前面的示例中,我们说明了如何从 Web 抓取联想驱动程序信息。在此示例中,返回的一些信息是原始数字信息:例如,表示 “3” 表示需要重启。

在这个技能中,我们想展示如何幻数 (cryptic numeric values) 转换为友好的文本。首先,让我们来看看改进的 Lenovo 函数:

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
function Get-LenovoDriver
{
param
(
$Model = '20JN',

$Os = 'Win7',

$Category = '*'
)


$restartText = @{
'0' = "No restart"
'1' = "Forced restart"
'3' = "Restart required"
'4' = "Shutdown after install"
}

$url = "https://download.lenovo.com/catalog/${model}_$os.xml"
$info = $model, $os
$url = "https://download.lenovo.com/catalog/{0}_{1}.xml" -f $info

$xml = Invoke-RestMethod -Uri $url -UseBasicParsing
$data = [xml]$xml.Substring(3)
[xml]$data = $xml.Substring(3)



$data.packages.package |
Where-Object { $_.Category -like $Category } |
ForEach-Object {
$location = $_.location
$name = $_.category
$rohdaten = Invoke-RestMethod -Uri $location -UseBasicParsing
[xml]$info = $rohdaten.Substring(3)
$filename = $info.Package.Files.Installer.File.Name
$readme = [System.IO.Path]::ChangeExtension($filename, 'txt')
$readme = $info.Package.Files.Readme.file.Name

[PSCustomObject]@{
Category = $name
Command = $info.package.ExtractCommand
Space = $info.package.DiskspaceNeeded
SpaceMB = [Math]::Round( ($info.package.DiskspaceNeeded / 1MB), 1)
Reboot = $info.package.Reboot.type
RebootFriendly = $restartText[$info.package.Reboot.type]
Version = $info.package.version
Download = "https://download.lenovo.com/pccbbs/mobiles/$filename"
ReadMe = "https://download.lenovo.com/pccbbs/mobiles/$readme"
Datum = Get-Date
}
}
}

您现在可以通过以下方式获取有关任何 Lenovo 机器的任何驱动程序更新的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
PS C:\> Get-LenovoDriver -Model 20JN -Os Win10 | Out-GridView -PassThru


Category : Camera and Card Reader
Command : n1qib04w.exe /VERYSILENT /DIR=%PACKAGEPATH% /EXTRACT="YES"
Space : 6442356
SpaceMB : 6,1
Reboot : 1
RebootFriendly : Forced restart
Version : 3760
Download : https://download.lenovo.com/pccbbs/mobiles/n1qib04w.exe
ReadMe : https://download.lenovo.com/pccbbs/mobiles/n1qib04w.txt
Datum : 09.12.2021 13:21:21

属性 “Reboot“ 显示原始的幻数。 新属性 ““RebootFriendly”“ 则用友好的文本表示,在这个例子中是 “Forced restart”。

让我们解读一下源代码来了解转换的过程。

对于任何转换过程,您需要准备一个哈希表,该表将幻数映射到友好的文本:

1
2
3
4
5
6
$restartText = @{
'0' = "No restart"
'1' = "Forced restart"
'3' = "Restart required"
'4' = "Shutdown after install"
}

接下来,将数值转换为文本,与查找哈希表值一样简单:

1
2
3
4
...
Reboot = $info.package.Reboot.type
RebootFriendly = $restartText[$info.package.Reboot.type]
...

PowerShell 技能连载 - 自动化下载联想驱动程序(第 1 部分)

许多硬件供应商提供基于 Web 的自助服务门户。以下是 Lenovo 返回有关驱动程序和其他更新下载的详细信息的示例:

https://download.lenovo.com/cdrt/tools/drivermatrix/dm.html

如果您需要管理成百上千台机器或需要定期查找信息,您当然希望自动化此资源。典型的第一种方法是检查 HTML 源代码并搜索 Web 服务,或者如果一切都失败了,则使用 Invoke-RestMethod 和 session cookie 来发送表单数据并模仿用户输入。

这不仅复杂,甚至可能完全失败。例如,Lenovo 网站使用 Javascript 编写 Web 前端,因此 PowerShell 和 Invoke-RestMethod 此时起不了作用。您必须使用基于 Selenium 的测试浏览器或其他高级 Web 浏览器自动化。

但是,当您仔细查看 HTML 源代码时,您可能会遇到这样的代码:

1
$.get("../../../catalog/" + x + "_" + document.getElementById("os").value + ".xml", function(data, status)

显然,网站上显示的数据来自静态 XML 文件,因此实际上不需要对 Web 界面进行自动化操作。在这种情况下,您只需要知道这些 XML 文件的命名方式。

这是包装这些 XML 文档的 PowerShell 函数。它返回所有型号的 Lenovo 驱动程序信息,并且是完全自动化的:

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
function Get-LenovoDriver
{
param
(
$Model = '20JN',

$Os = 'Win7',

$Category = '*'
)




$url = "https://download.lenovo.com/catalog/${model}_$os.xml"
$info = $model, $os
$url = "https://download.lenovo.com/catalog/{0}_{1}.xml" -f $info

$xml = Invoke-RestMethod -Uri $url -UseBasicParsing
$data = [xml]$xml.Substring(3)
[xml]$data = $xml.Substring(3)



$data.packages.package |
Where-Object { $_.Category -like $Category } |
ForEach-Object {
$location = $_.location
$name = $_.category
$rohdaten = Invoke-RestMethod -Uri $location -UseBasicParsing
[xml]$info = $rohdaten.Substring(3)
$filename = $info.Package.Files.Installer.File.Name
[PSCustomObject]@{
Category = $name
Command = $info.package.ExtractCommand
Space = $info.package.DiskspaceNeeded
Reboot = $info.package.Reboot.type
Version = $info.package.version
Download = "https://download.lenovo.com/pccbbs/mobiles/$filename"
Datum = Get-Date
}
}
}

您现在可以直接从 PowerShell 命令行检索信息,而不是手动操作 HTML 页面,例如:

1
2
3
4
5
6
7
8
9
10
PS> Get-LenovoDriver -Model 20JN -Os Win10 | Out-GridView -PassThru


Category : ThinkVantage Technology
Command : n1msk20w.exe /VERYSILENT /DIR=%PACKAGEPATH% /EXTRACT="YES"
Space : 33206066
Reboot : 3
Version : 1.82.0.20
Download : https://download.lenovo.com/pccbbs/mobiles/n1msk20w.exe
Datum : 09.12.2021 13:16:00

即使您不管理 Lenovo 硬件,您也可以复用此示例中介绍的一些技术。

PowerShell 技能连载 - Out-GridView 自定义列

当您使用 -OutputMode-PassThru 参数时,Out-GridView 可以是一个通用对话框。执行此操作时,网格视图窗口会在其右下角显示其他按钮,以便您可以选择项目并将它们传递给其他 cmdlet。

此行代码可以帮助选择要停止的服务,例如:

1
Get-Service | Where-Object CanStop | Out-GridView -Title 'Service to stop?' -OutputMode Single | Stop-Service -WhatIf

但是,Out-GridView 无法控制它显示的属性。在上面的示例中,用户实际上只需要查看服务名称以及可能的依赖服务。

当然,您可以使用 Select-Object 来选择要显示的属性。现在网格视图窗口将准确显示您要求的列,但由于您永久删除了所有其他属性并更改了对象类型,后续 cmdlet 可能会如你所想的不能正常工作:

1
Get-Service | Where-Object CanStop | Select-Object -Property DisplayName, DependentServices | Out-GridView -Title 'Service to stop?' -OutputMode Single | Stop-Service -WhatIf

运行上面这行代码时,网格视图窗口现在看起来很棒,但 Stop-Service 将不再停止选择您选择的服务,因为 Select-Object 将对象类型从 Service 更改为自定义对象:

Stop-Service : The specified wildcard character pattern is not valid: @{DisplayName=Windows Audio Endpoint Builder;
DependentServices=System.ServiceProcess.ServiceController[]}

在上一个技能中,我们已经使用了一种隐藏的技术,您可以使用它来告诉 Out-GridView 它应该显示哪些列——无需删除任何属性或更改对象类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# create object that tells PowerShell which column(s) should be visible:
# show "DisplayName", and "DependentServices"
[string[]]$visible = 'DisplayName', 'DependentServices'
$type = 'DefaultDisplayPropertySet'
[System.Management.Automation.PSMemberInfo[]]$info =
[System.Management.Automation.PSPropertySet]::new($type,$visible)


Get-Service |
Where-Object CanStop |
# add the secret object to each object that you pipe into Out-GridView:
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $info -PassThru |
Out-GridView -Title 'Service to stop?' -OutputMode Single |
Stop-Service -WhatIf

不幸的是,当您这样做时,您可能会遇到红色错误消息。某些 PowerShell 对象(例如 Service)已经使用了我们尝试添加的巧妙技巧,因此您无法覆盖 PSStandardMembers 属性。要解决此问题,只需通过 Select-Object * 运行它们来克隆对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# create object that tells PowerShell which column(s) should be visible:
# show "DisplayName", and "DependentServices"
[string[]]$visible = 'DisplayName', 'DependentServices'
$type = 'DefaultDisplayPropertySet'
[System.Management.Automation.PSMemberInfo[]]$info =
[System.Management.Automation.PSPropertySet]::new($type,$visible)


Get-Service |
Where-Object CanStop |
# clone the objects so they now belong to you:
Select-Object -Property * |
# add the secret object to each object that you pipe into Out-GridView:
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $info -PassThru |
Out-GridView -Title 'Service to stop?' -OutputMode Single |
Stop-Service -WhatIf

现在一切都很神奇,Out-GridView 仅显示您选择的属性。尽管如此,Stop-Process 继续获得输出信息并停止您选择的服务(删除 -WhatIf 来真实地停止服务,请确保您有管理员权限进行此操作)。

虽然通过 Select-Object 运行对象确实会更改其对象类型,但大多数 cmdlet 仍会继续处理这些对象,因为它们仍包含所有属性。这是最后一个示例:即使 Out-GridView 仅显示您选择的属性,对象仍包含所有属性,包括隐藏在网格视图窗口中的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# create object that tells PowerShell which column(s) should be visible:
# show "Name", "Description" and "MainWindowTitle"
[string[]]$visible = 'Name', 'Description', 'MainWindowTitle'
$type = 'DefaultDisplayPropertySet'
[System.Management.Automation.PSMemberInfo[]]$info =
[System.Management.Automation.PSPropertySet]::new($type,$visible)


Get-Process |
Where-Object MainWindowTitle |
Sort-Object -Property Name |
# clone the objects so they now belong to you:
Select-Object -Property * |
# add the secret object to each object that you pipe into Out-GridView:
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $info -PassThru |
Out-GridView -Title 'Select a process' -OutputMode Single |
# still all properties available:
Select-Object -Property *

PowerShell 技能连载 - 修复 PowerShellGet 和 Publish-Module

Publish-Module 是一个 cmdlet,用于将模块发布(上传)到 NuGet 仓库。有时,此 cmdlet 会引发奇怪的异常。这种情况下的原因是 nuget.exe 的过时版本。该应用程序负责打包一个模块并保存为.nupkg 文件,并且在您第一次使用 Publish-Module 时会自动下载该应用程序。

要更正此问题并刷新您的 nuget.exe 版本,请运行以下命令:

1
Invoke-WebRequest -Uri https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile "$env:LOCALAPPDATA\Microsoft\Windows\PowerShell\PowerShellGet\NuGet.exe"

确保在此之后关闭并重新启动所有 PowerShell 会话。如果 Publish-Module 仍然拒绝工作,您可能需要运行以下命令(需要管理员权限):

1
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force

PowerShell 技能连载 - 检测挂起的重启

下面的代码检测是否有挂起的重启:

1
2
$rebootRequired = Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending"
"Pending reboot: $rebootRequired"