PowerShell 技能连载 - 是否在 Windows PowerShell 中运行(第 2 部分)

在上一个技能中,我们介绍了一个向后兼容的单行代码,能够判断您的脚本是否运行在传统的 Windows PowerShell 环境中,还是运行在新的 PowerShell 7 便携版 shell 中。

如果您使用的是跨平台的 PowerShell 7,那么有一个名为 [Management.Automation.platform] 的新类型,能返回更多的平台信息。Windows PowerShell 尚未包含此类型,因此您可以使用此类型来确定您是否当前正在 Windows PowerShell 上运行。如果没有,则该类型提供了额外的平台信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# testing whether type exists
$type = 'Management.Automation.Platform' -as [Type]
$isWPS = $type -eq $null

if ($isWPS)
{
Write-Warning 'Windows PowerShell'
} else {
# query all public properties
$properties = $type.GetProperties().Name
$properties | ForEach-Object -Begin { $hash = @{} } -Process {
$hash[$_] = $type::$_
} -End { $hash }
}

在 Windows PowerShell 上,脚本只会产生警告。 在 PowerShell 7 上,它返回一个哈希表,其中包含所有相关平台信息:

Name                           Value
----                           -----
IsStaSupported                 True
IsLinux                        False
IsCoreCLR                      True
IsWindows                      True
IsNanoServer                   False
IsMacOS                        False
IsWindowsDesktop               True
IsIoT                          False

PowerShell 技能连载 - 是否在 Windows PowerShell 中运行(第 1 部分)

现在的 PowerShell 可以在各种平台上运行,并且在上一个技能中,我们解释了如何查看脚本运行的操作系统。

如果操作系统是 Windows,您仍然不能知道您的脚本是由内置 Windows PowerShell 还是新的便携式 PowerShell 7 运行。

以下是一种安全和向后兼容的方式,可以了解您的脚本是否在 Windows PowerShell 上运行:

1
2
3
4
$RunOnWPS = !($PSVersionTable.ContainsKey('PSEdition') -and
$PSVersionTable.PSEdition -eq 'Core')

"Runs on Windows PowerShell? $RunOnWPS"

PowerShell 技能连载 - 决定您的平台

现在的 PowerShell 已是跨平台的,因此即使能在 Windows 服务器上正常使用 Windows PowerShell,您的脚本也有可能在不同的操作系统上停止运行。

如果您的脚本想要知道它正在运行的平台,以向后兼容的方式运行,请尝试这些代码:

1
2
3
4
5
$RunOnWindows = (-not (Get-Variable -Name IsWindows -ErrorAction Ignore)) -or $IsWindows
$RunOnLinux = (Get-Variable -Name IsLinux -ErrorAction Ignore) -and $IsLinux
$RunOnMacOS = (Get-Variable -Name IsMacOS -ErrorAction Ignore) -and $IsMacOS

Get-Variable -Name RunOn*

在 Windows 系统上,结果如下所示:

Name                           Value
----                           -----
RunOnLinux                     False
RunOnMacOS                     False
RunOnWindows                   True

您现在可以安全地检查先决条件,并确保您的脚本代码仅在适当的情况下运行。

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 硬件,您也可以复用此示例中介绍的一些技术。