PowerShell 技能连载 - 记录变量类型

作为调试和质量控制的一部分,您可能需要记录赋值给一个变量的数据。例如,您可能需要找出赋值给指定变量的实际数据类型是什么,以便以后可以以强类型的方式访问变量以增加安全性。

这是一个自定义验证器类,可以用于这类分析。只需先运行下面的代码。除了声明新属性外,它不做任何事情:

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
# create a new custom validation attribute named "LogVariableAttribute":
class IdentifyTypeAttribute : System.Management.Automation.ValidateArgumentsAttribute
{


# this gets called whenever a new value is assigned to the variable:

[void]Validate([object]$value, [System.Management.Automation.EngineIntrinsics]$engineIntrinsics)
{
# get the global variable that logs all changes:
[System.Management.Automation.PSVariable]$variable = Get-Variable "loggedTypes" -Scope global -ErrorAction Ignore
# if the variable exists and does not contain an ArrayList, delete it:
if ($variable -ne $null -and $variable.Value -isnot [System.Collections.ArrayList]) { $variable = $null }
# if the variable does not exist, set up an empty new ArrayList:
if ($variable -eq $null) { $variable = Set-Variable -Name "loggedTypes" -Value ([System.Collections.ArrayList]@()) -Scope global -PassThru }

[string]$line = (Get-PSCallStack)[-1].Position.Text
$pattern = '\$(\w{1,})'
$match = [regex]::Match($line, $pattern)
if ($match.success)
{

# log the type contained in the variable
$null = $variable.Value.Add([PSCustomObject]@{
# use the optional source name that can be defined by the attribute:
Value = $value.GetType()
Timestamp = Get-Date
# use the callstack to find out where the assignment took place:
Name = [regex]::Match($line, $pattern).Groups[1]
Position = [regex]::Match($line, $pattern).Groups[1].Index + (Get-PSCallStack)[-1].Position.StartOffset
Line = (Get-PSCallStack).ScriptLineNumber | Select-Object -Last 1
Path = (Get-PSCallStack).ScriptName | Select-Object -Last 1
})

}
}
}

现在,在您的脚本中,在开始时,通过添加新属性来初始化要跟踪的所有变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[IdentifyType()]$test = 1
[IdentifyType()]$x = 0

# start using the variables:
for ($x = 1000; $x -lt 3000; $x += 300)
{
"Frequency $x Hz"
[Console]::Beep($x, 500)
}

& {
$test = Get-Date
}


$test = "Hello"
Start-Sleep -Seconds 1
$test = 1,2,3

然后正常运行脚本。结果将记录到全局变量 $loggedTypes 中,通过它可以查看所有结果:

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
# looking at the log results:
$loggedTypes | Out-GridView



PS C:\> $loggedTypes


Value : System.Int32
Timestamp : 04.07.2022 09:42:46
Name : test
Position : 17
Line : 1
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:46
Name : x
Position : 44
Line : 2
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:46
Name : x
Position : 89
Line : 5
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:46
Name : x
Position : 113
Line : 5
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:47
Name : x
Position : 113
Line : 5
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:47
Name : x
Position : 113
Line : 5
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:48
Name : x
Position : 113
Line : 5
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:48
Name : x
Position : 113
Line : 5
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:49
Name : x
Position : 113
Line : 5
Path :

Value : System.Int32
Timestamp : 04.07.2022 09:42:49
Name : x
Position : 113
Line : 5
Path :

Value : System.String
Timestamp : 04.07.2022 09:42:49
Name : test
Position : 215
Line : 16
Path :

Value : System.Object[]
Timestamp : 04.07.2022 09:42:50
Name : test
Position : 258
Line : 18
Path :

PowerShell 技能连载 - 将语言 ID 转为语言名称

在我们以前的迷你系列中,我们展示了使用不同的 PowerShell 方法来获取安装 OS 语言名称的几种方法。结果都是语言 ID 的列表,类似这样:

de-DE
en-US
fr-FR

如果我需要将它们转换为完整的国家名称怎么办?幸运的是,这只是数据类型的问题。我们演示的所有方法都可以以字符串的形式返回安装的语言包。让我们以 WMI 示例为例:

1
2
$os = Get-CIMInstance -ClassName Win32_OperatingSystem
$os.MUILanguages

当您将结果转换到更合适的数据类型时,将获得合适的数据。相对于字符串,让我们使用表示国家名称的数据类型:CultureInfo

1
2
$os = Get-CIMInstance -ClassName Win32_OperatingSystem
[CultureInfo[]]$os.MUILanguages

瞬间,相同的数据现在可以以更丰富的格式表示:

LCID             Name             DisplayName
----             ----             -----------
1031             de-DE            German (Germany)
1033             en-US            English (United States)
1036             fr-FR            French (France)

PowerShell 技能连载 - 确定语言包(第 3 部分)

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

这就是为什么 PowerShell 全能的 Get-ComputerInfo 的原因。它为您查询各种与计算机相关的信息,然后由您决定需要哪个信息。我们也可以通过这种方法解决这个问题:

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

不好的方面是,Get-ComputerInfo 总是查询完整的信息集,这使得执行起来很慢。不过总比没有好,甚至比 dism.exe 更好,但是第二部分的直接 WMI 查询仍然是最高效的方法。

PowerShell 技能连载 - 确定语言包(第 2 部分)

在本系列的第二部分中,我们希望通过使用内置的 PowerShell 功能来解决我们的难题 - 获得安装的语言包。在第一部分中,我们使用了可行的控制台应用程序 (dism.exe),但很复杂,需要管理员特权。

Windows 机器上的面向对象的方法通常是 WMI,您可以在其中查询描述所需信息的类,并在不做字符串转换的情况下获取信息。WMI 的困难部分是找到合适的类名。

这是我们的解决方案:

1
2
$os = Get-CIMInstance -ClassName Win32_OperatingSystem
$os.MUILanguages

当您使用 dism.exe 将其与我们第一部分的解决方案进行比较时,您会立即发现它的速度更快,更方便。但是,两种方法最后都返回相同的信息。

PowerShell 技能连载 - 确定语言包(第 1 部分)

假设您需要查找 Windows 计算机已安装的语言包。在这个三部分的系列中,我们使用 PowerShell 的功能来解决此问题。

在第一部分中,我们只是尝试通过寻找可以利用的原生非 PowerShell 命令来解决问题。

事实证明,命令 dism.exe 可以为您找到该信息。但是,与许多二进制控制台命令一样,结果是 (a)本地化的,(b)字符串数据,(c)通常需要管理员特权。

如果我们别无选择,作为管理员,您可以运行以下代码:

1
DISM.exe /Online /Get-Intl /English

接下来,您可以使用 PowerShell 功能过滤和解析信息,直到找到所需的内容为止。

例如,使用 Select-String 仅选择那些对您的任务有趣的行:

1
2
DISM.exe /Online /Get-Intl /English |
Select-String -SimpleMatch 'Installed language(s)'

结果看起来像这样:

Installed language(s): de-DE
Installed language(s): en-US
Installed language(s): fr-FR

接下来,使用 PowerShell 管道机制处理每个结果并提取您真正需要的信息:

1
2
3
DISM.exe /Online /Get-Intl /English |
Select-String -SimpleMatch 'Installed language(s)' |
ForEach-Object { $_.Line.Split(':')[-1].Trim() }

在这里,每次取出一行,然后由冒号分隔,然后取出最后一部分(右)部分,删除所有空格。这很麻烦,但最后能够获取原生字符串的数据的结果。

实际上,Select-String 支持正则表达方式。因此,如果您了解正则表达式,则可以立即查询所需的信息:

1
2
3
DISM.exe /Online /Get-Intl /English |
Select-String -Pattern 'Installed language\(s\):\s(.*?)$' |
ForEach-Object { $_.Matches[0].Groups[1].Value }

请注意,在代码示例中如何使用 -Pattern 参数并忽略 -SimplePattern 参数,从而告诉 Select-String 我们正在使用完整的正则表达式。现在,搜索模式使用 “\“ 来转义所有特殊字符,并定义一组括号来定义我们要寻找的值,位于搜索文本之后和行末的 “$“ 之后)。

然后,Select-String 在其属性 “Matches“ 中返回正则表达式匹配,因此我们可以从这里以面向对象的方式访问发现的匹配和组。我们正在寻找的信息是在第一个匹配项中(每行,index 为 0),在第二组中(索引号 0 代表整个匹配,索引号 1 是第一个括号内的匹配项)。

如果这个操作对你来说太低级了,请期待下一个技能!

PowerShell 技能连载 - 创建世界地图图像

如果您有与国家/地区有关的数据,则可能需要可视化并突出显示此地理数据。经典的数据图标在这里不起作用。

幸运的是,PowerShell 可以使用免费的在线地理图表。这是您可以试试这个函数:

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
function Show-MapGraph
{
param
(
[Parameter(Mandatory,ValueFromPipeline)]
$InputObject,

[Parameter(Mandatory)]
[string]
$Property,

[string]
$Label = 'Items'
)

begin
{

$bucket = [System.Collections.ArrayList]::new()
}
process
{
$null = $bucket.Add($_)
}
end
{
$groups = $bucket | Where-Object { $_.$Property } | Group-Object -Property $Property -NoElement
$data = foreach ($group in $groups)
{
"['{0}',{1}]" -f $group.Name, $group.Count
}

$datastring = $data -join "`r`n,"

$HTMLPage = @"
google.charts.load('current', {
'packages':['geochart'],
});
google.charts.setOnLoadCallback(drawRegionsMap);

function drawRegionsMap() {
var data = google.visualization.arrayToDataTable([
['Country', '$Label'],
$datastring
]);

var options = {

colorAxis: {colors: ['#00FF00', '#004400']},
backgroundColor: '#81d4fa',
datalessRegionColor: '#AAAABB',
defaultColor: '#f5f5f5',
};

var chart = new google.visualization.GeoChart(document.getElementById('regions_div'));

chart.draw(data, options);
}
"@

$timestamp = Get-Date -Format 'HHmmss'
$OutPath = "$env:temp\Graph$timestamp.html"
$HTMLPage | Out-File -FilePath $OutPath -Encoding utf8
Start-Process $outpath
}
}

Show-MapGraph 基本原理是创建一个 HTML 网页,并通过适当的脚本调用填充它,然后显示它。您需要做的就是通过管道传入您的国家数据,并使用 -Property 指示对象的哪个属性包含国家名称。

PowerShell 技能连载 - 在选中的代码中运行 $PSScriptRoot

PowerShell 代码中的最大陷阱之一是自动变量 $PSScriptRoot,它始终代表当前脚本所在的文件夹的路径。但是,这要求 (a)当前脚本实际上已经保存到文件中,并且 (b)您正在执行整个文件,即通过按 F5 执行。

当您仅使用 F8 执行选中的代码时,即使您选择了整个代码,$PSScriptRoot 也为空,因此也会导致您选择的代码执行错误。

但是,在 PowerShell ISE 中,添加一些代码就能很容易地实现在选中的代码中启用 $PSScriptRoot。这是您需要运行的代码:

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
function Invoke-Selection
{
try
{
# get the selected text:
$selectedText = $psise.CurrentFile.Editor.SelectedText
# if no text was selected...
if ($selectedText.Length -eq 0) {
# ...select the line the caret is in and get this line instead:
$psise.CurrentFile.Editor.SelectCaretLine()
$selectedText = $psise.CurrentFile.Editor.SelectedText
}

# try and parse the code
$sb = [ScriptBlock]::Create($selectedText)

# get the missing variable content from the underlying file:
$currentFile = $psise.CurrentFile.FullPath
$currentFolder = Split-Path -Path $currentFile

# append the selected code with these automatic variables, and set them:
$runcode = @"
`$PSCommandPath = '$currentFile'
`$PSScriptRoot = '$currentFolder'
$selectedText
"@
# turn text into script block...
$scriptblock = [ScriptBlock]::Create($runcode)
# ...and execute it without private scope:
. $scriptblock

}
catch
{
throw $_.Exception
}

}
$null = $psise.CurrentPowerShellTab.AddOnsMenu.Submenus.Add('ExecuteSelection', {. Invoke-Selection}, 'SHIFT+F8')

该代码向 ISE 添加了一个新命令,可以通过按 CTRL+F8 来调用该命令。现在,假设您选择了一段代码,这段代码包含了 $PSScriptRoot,您若希望运行它,只需按 CTRL+F8 而不是 F8,它将正常执行。

该快捷键调用了 Invoke-Selection 函数。此函数将接受当前选择的文本,添加缺少的自动变量 $PSScriptRoot$PSCommandPath 到代码中,并根据当前脚本的当前文件路径来定义这些变量。然后执行脚本块。

这样,您现在可以调试并演示任何选中的代码,即使它包含自动变量。只需确保您将脚本保存在某个地方,以便 PowerShell 知道您的代码所在的位置。

PowerShell 技能连载 - 在 PowerShell 中粘贴多行

当您复制多行 PowerShell 代码并将其粘贴到 Shell 窗口中时,结果通常不是您所期望的。PowerShell 开始执行第一行,不会以整块的方式执行粘贴的代码。试着复制下面的代码,然后将其粘贴到 PowerShell 控制台窗口中来查看默认行为:

1
2
3
4
"Starting!"
$a = Read-Host -Prompt 'Enter something'
"Entered: $a"
"Completed"

粘贴块的每一行都是单独执行的,在每个输出行之前,可以看到命令提示符。

尽管此默认行为也可正常执行,但是如果您希望确保整个代码块作为一个整体执行,则将其嵌入大括号中,并用 “.“ 执行此脚本块。尝试复制这段代码:

1
2
3
4
5
6
. {
"Starting!"
$a = Read-Host -Prompt 'Enter something'
"Entered: $a"
"Completed"
}

当您粘贴此代码时,它会像从脚本文件中存储并加载它一样作为一个整体执行。

PowerShell 技能连载 - 恢复被浪费的硬盘空间

当软件收到更新时,它往往并不会清除之前不需要的更新。这些过期的“补丁文件”积累在 C:\Windows\installer 文件夹下,其中有许多 *.msp 文件。由于您不知道那些 *.msp 文件还会被用到,以及哪个文件可以安全地删除,所以不太容易恢复空间。除非您拥有 Administrator 特权(需要它才能处理存储在 Windows 文件夹中的数据)并使用 PowerShell。

只需要下载该模块(需要 Administrator 特权):

1
Install-Module -Name MSIPatches

下一步,以 Administrator 特权启动一个 PowerShell 控制台,并像这样查看可恢复的空间:

1
2
3
4
5
6
7
8
9
PS> Get-MsiPatch


TotalPatchCount : 19
TotalPatchSize : 0,96 GB
InstalledPatchCount : 5
InstalledPatchSize : 0,32 GB
OrphanedPatchCount : 14
OrphanedPatchSize : 3,64 GB

“Orphaned Patch Size” 可能是 0 到好几 GB 之间的任意值。在一个系统上,由于安装了 Office 2016,我恢复了 45GB 的孤儿补丁(显然没有清理已安装的更新)。

要真正清理不必要的补丁,请使用此行代码(需要管理员特权):

1
Get-OrphanedPatch | Move-OrphanedPatch -Destination C:\Backup

这样,您可以在安全的地方“隔离”补丁文件一段时间。不过,不要忘记在某个时间点清空目标文件夹。或者,您当然可以立即删除孤立的补丁。不过,无论您做什么,都要自担风险。

PowerShell 技能连载 - 下载文件

可以通过许多方法实现简单的文件下载。例如,使用 Invoke-RestMethodInvoke-WebRequest 或通过 BitsTransfer 模块。

如果您需要下载流式内容,那么需要更复杂的命令。在 Windows 机器上,您可以下载并安装 PSODownloader 模块:

1
Install-Module -Name PSODownloader -Scope CurrentUser

它为您提供了一个更简单的 cmdlet:Invoke-DownloadFile。您可以先将要下载的 URL 复制到剪贴板然后调用该命令,或者使用 -Url 参数。

在此可以查看更多信息:

https://github.com/TobiasPSP/Modules.PsoDownloader