PowerShell 技能连载 - PowerShell 技能连载 - 动态参数完成(第 5 部分)

在前面的技能中,我们研究了完成应用程序路径的复杂的完成代码。收集完成值可能需要一些时间,并且有可能使 IntelliSense 超时。对于不太可能更改的完成值,最好先计算一次,然后再使用缓存的值。

通过这种方式,安装自动完成器可能会占用一两秒时间,但是在那之后就可以享受快速的 IntelliSense:

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
# define a function without argument completer
function Start-Software {
param(
[Parameter(Mandatory)]
[string]
$Path
)

Start-Process -FilePath $Path
}

# define the code used for completing application paths
$code = {


}

# calculate the completion values once, and reuse the values later
# store results in a script-global variable
$script:applicationCompleter = & {
# get registered applications from registry
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*"

[System.Collections.Generic.List[string]]$list =
Get-ItemProperty -Path $key |
Select-Object -ExpandProperty '(Default)' -ErrorAction Ignore

# add applications found by Get-Command
[System.Collections.Generic.List[string]]$commands =
Get-Command -CommandType Application |
Select-Object -ExpandProperty Source
$list.AddRange($commands)

# add descriptions and compose completionresult entries
$list |
# remove empty paths
Where-Object { $_ } |
# remove quotes and turn to lower case
ForEach-Object { $_.Replace('"','').Trim().ToLower() } |
# remove duplicate paths
Sort-Object -Unique |
ForEach-Object {
# skip files that do not exist
if ( (Test-Path -Path $_))
{
# get file details
$file = Get-Item -Path $_
# quote path if it has spaces
$path = $_
if ($path -like '* *') { $path = "'$path'" }
# make sure tooltip is not null
$tooltip = [string]$file.VersionInfo.FileDescription
if ([string]::IsNullOrEmpty($tooltip)) { $tooltip = $file.Name }
# compose completion result
[Management.Automation.CompletionResult]::new(
# complete path
$path,
# show friendly text in IntelliSense menu
('{0} ({1})' -f $tooltip, $file.Name),
# use file icon
'ProviderItem',
# show file description
$tooltip
)
}
}
}

# instead of complex code, simply return the cached results when needed
$code = { $script:applicationCompleter }

# tie the completer code to all applicable parameters of own or foreign commands
Register-ArgumentCompleter -CommandName Start-Software -ParameterName Path -ScriptBlock $code
Register-ArgumentCompleter -CommandName Start-Process -ParameterName FilePath -ScriptBlock $code

当您运行上面的代码然后使用 Start-SoftwareStart-Process 命令时,您将获得高度响应的 IntelliSense。与内置的完成功能相反,您需要手动按 CTRL + SPACE。

PowerShell 技能连载 - 动态参数完成(第 4 部分)

在上一个技能中,我们解释了如何使用 [ArgumentCompleter] 为参数添加功能强大的参数完成器。但是有一些限制:

  • 当完成代码变得复杂时,您的代码将变得难以阅读
  • 您不能将参数完成添加到现有命令。[ArgumentCompleter] 属性仅适用于您自己的函数。

但是,实际上,该属性只是将参数完成程序代码添加到 PowerShell 的两种方法之一。您也可以使用 Register-ArgumentCompleter 并将代码添加到现有命令中。

让我们首先看一下先前技巧中的示例:

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
function Start-Software {
param(
[Parameter(Mandatory)]
[ArgumentCompleter({


# get registered applications from registry
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*"

[System.Collections.Generic.List[string]]$list =
Get-ItemProperty -Path $key |
Select-Object -ExpandProperty '(Default)' -ErrorAction Ignore

# add applications found by Get-Command
[System.Collections.Generic.List[string]]$commands =
Get-Command -CommandType Application |
Select-Object -ExpandProperty Source
$list.AddRange($commands)

# add descriptions and compose completion result entries
$list |
# remove empty paths
Where-Object { $_ } |
# remove quotes and turn to lower case
ForEach-Object { $_.Replace('"','').Trim().ToLower() } |
# remove duplicate paths
Sort-Object -Unique |
ForEach-Object {
# skip files that do not exist
if ( (Test-Path -Path $_))
{
# get file details
$file = Get-Item -Path $_
# quote path if it has spaces
$path = $_
if ($path -like '* *') { $path = "'$path'" }
# make sure tooltip is not null
$tooltip = [string]$file.VersionInfo.FileDescription
if ([string]::IsNullOrEmpty($tooltip)) { $tooltip = $file.Name }
# compose completion result
[Management.Automation.CompletionResult]::new(
# complete path
$path,
# show friendly text in IntelliSense menu
('{0} ({1})' -f $tooltip, $file.Name),
# use file icon
'ProviderItem',
# show file description
$tooltip
)
}
}

})]
[string]
$Path
)

Start-Process -FilePath $Path
}

函数 Start-Software 使用 [ArgumentCompleter] 属性定义了参数完成器,并且当使用 Start-Software 时,能获得 -Path 参数丰富的完成信息。

以下是一种替代方法,可以将完成程序代码单独发送到 PowerShell,而不使用属性。而是使用 Register-ArgumentCompleter 将完成程序代码绑定到任何命令的任何参数:

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
# define a function without argument completer
function Start-Software {
param(
[Parameter(Mandatory)]
[string]
$Path
)

Start-Process -FilePath $Path
}

# define the code used for completing application paths
$code = {

# get registered applications from registry
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*"

[System.Collections.Generic.List[string]]$list =
Get-ItemProperty -Path $key |
Select-Object -ExpandProperty '(Default)' -ErrorAction Ignore

# add applications found by Get-Command
[System.Collections.Generic.List[string]]$commands =
Get-Command -CommandType Application |
Select-Object -ExpandProperty Source
$list.AddRange($commands)

# add descriptions and compose completionresult entries
$list |
# remove empty paths
Where-Object { $_ } |
# remove quotes and turn to lower case
ForEach-Object { $_.Replace('"','').Trim().ToLower() } |
# remove duplicate paths
Sort-Object -Unique |
ForEach-Object {
# skip files that do not exist
if ( (Test-Path -Path $_))
{
# get file details
$file = Get-Item -Path $_
# quote path if it has spaces
$path = $_
if ($path -like '* *') { $path = "'$path'" }
# make sure tooltip is not null
$tooltip = [string]$file.VersionInfo.FileDescription
if ([string]::IsNullOrEmpty($tooltip)) { $tooltip = $file.Name }
# compose completion result
[Management.Automation.CompletionResult]::new(
# complete path
$path,
# show friendly text in IntelliSense menu
('{0} ({1})' -f $tooltip, $file.Name),
# use file icon
'ProviderItem',
# show file description
$tooltip
)
}
}
}

# tie the completer code to all applicable parameters of own or foreign commands
Register-ArgumentCompleter -CommandName Start-Software -ParameterName Path -ScriptBlock $code
Register-ArgumentCompleter -CommandName Start-Process -ParameterName FilePath -ScriptBlock $code

现在,您自己的 Start-Software 函数的 -Path 参数和内置 cmdlet Start-Process 功能参数完成的 -FilePath 参数。完成代码可以重复利用。

注意:根据计算机上安装的软件和驱动器的速度,此示例中的完成代码可能需要一些时间才能执行。如果 IntelliSense 菜单超时,请按 CTRL + SPACE 再试一次。

PowerShell 技能连载 - 动态参数完成(第 3 部分)

根据我们过去讨论的技巧,让我们编写一个有用的最终代码,以列出所有可以启动的程序:

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
function Start-Software {
param(
[Parameter(Mandatory)]
[ArgumentCompleter({


# get registered applications from registry
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*"

[System.Collections.Generic.List[string]]$list =
Get-ItemProperty -Path $key |
Select-Object -ExpandProperty '(Default)' -ErrorAction Ignore

# add applications found by Get-Command
[System.Collections.Generic.List[string]]$commands =
Get-Command -CommandType Application |
Select-Object -ExpandProperty Source
$list.AddRange($commands)

# add descriptions and compose completionresult entries
$list |
# remove empty paths
Where-Object { $_ } |
# remove quotes and turn to lower case
ForEach-Object { $_.Replace('"','').Trim().ToLower() } |
# remove duplicate paths
Sort-Object -Unique |
ForEach-Object {
# skip files that do not exist
if ( (Test-Path -Path $_))
{
# get file details
$file = Get-Item -Path $_
# quote path if it has spaces
$path = $_
if ($path -like '* *') { $path = "'$path'" }
# make sure tooltip is not null
$tooltip = [string]$file.VersionInfo.FileDescription
if ([string]::IsNullOrEmpty($tooltip)) { $tooltip = $file.Name }
# compose completion result
[Management.Automation.CompletionResult]::new(
# complete path
$path,
# show friendly text in IntelliSense menu
('{0} ({1})' -f $tooltip, $file.Name),
# use file icon
'ProviderItem',
# show file description
$tooltip
)
}
}

})]
[string]
$Path
)

Start-Process -FilePath $Path
}

当您运行上述代码然后调用 Start-Software 时,请按 CTRL + SPACE,以使用简称来查看可用应用程序的完整列表。选择一个后,将自动完成绝对路径。路径包含空格时将自动加上单引号。

请注意,您可以先输入一些字符,例如 exc,然后按 CTRL + SPACE。这将预过滤 IntelliSense 列表。

另请注意:根据计算机上安装的软件和驱动器的速度,此示例中的完成代码可能需要一些时间才能执行。如果 IntelliSense 菜单超时,请按 CTRL + SPACE 再试一次。

PowerShell 技能连载 - 动态参数完成(第 2 部分)

在前面的技巧中,我们研究了 [ArgumentCompleter] 以及此属性如何将聪明的代码添加到为参数提供自动完成值的参数。自动完成功能甚至可以做更多的事情:您可以根据实际情况生成 IntelliSense 菜单。

请看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Get-OU {
param(
[Parameter(Mandatory)]
[ArgumentCompleter({

[Management.Automation.CompletionResult]::new("'OU=managers,DC=company,DC=local'", "Management", "ProviderItem", "OU where the Management lives")
[Management.Automation.CompletionResult]::new("'OU=subtest,OU=test,DC=company,DC=local'", "Experimental", "DynamicKeyword", "Reserved")
[Management.Automation.CompletionResult]::new("'OU=External,OU=IT,DC=company,DC=local'", "Help Desk", "ProviderItem", "OU where the Helpdesk people reside")

})]
[string]
$OU
)

"Chosen path: $OU"
}

完整代码基本上只创建三个新的CompletionResult对象。每个参数都有四个参数:

  • 自动完成的文字
  • 显示在 IntelliSense 菜单中的文字
  • IntelliSense 菜单的图标
  • IntelliSense 菜单的工具提示

您甚至可以控制 IntelliSense 菜单中显示的图标。这些是预定义的图标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS> [Enum]::GetNames([System.Management.Automation.CompletionResultType])
Text
History
Command
ProviderItem
ProviderContainer
Property
Method
ParameterName
ParameterValue
Variable
Namespace
Type
Keyword
DynamicKeyword

当您运行此代码然后调用 Get-OU 时,可以按 TAB 键完成 OU X500 路径,也可以按 CTRL + SPACE 打开 IntelliSense 菜单。在菜单内,您会看到所选的图标和友好的文本。选择项目后,将使用 X500 自动完成的文字。

PowerShell 技能连载 - 动态参数完成(第 1 部分)

在前面的技巧中,我们介绍了将参数完成符添加到参数的各种方法。一种方法是使用 [ArgumentCompleter] 属性,如下所示:如您所见,这仅是完善补全代码的问题:当文件名包含空格时,表达式放在引号内,否则不用。如果希望完成者仅返回文件并忽略文件夹,则将 -File 参数添加到 Get-ChildItem

1
2
3
4
5
6
7
8
9
10
function Get-File {
param(
[Parameter(Mandatory)]
[ArgumentCompleter({Get-ChildItem -Path $env:windir -Name})]
[string]
$FileName
)

"Chosen file name: $FileName"
}

本质上,运行此代码然后调用 Get-File 时,一旦使用 -FileName 参数,就可以按 TAB 或 CTRL + SPACE 自动列出 Windows 文件夹中所有文件的文件名。 PowerShell执行 [ArgumentCompleter] 中定义的脚本块以动态计算出自动完成的列表。

有一个反馈称,完成后,这些值需要检查特殊字符(例如空格),并在必要时用引号将它包裹起来。让我们听取这些反馈意见,看看如何改进自动完成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Get-File {
param(
[Parameter(Mandatory)]
[ArgumentCompleter({
Get-ChildItem -Path $env:windir -Name |
ForEach-Object {
if ($_ -like '* *')
{
"'$_'"
}
else
{
$_
}
}

})]
[string]
$FileName
)

"Chosen file name: $FileName"
}

如您所见,这完全是完善补全代码的问题:当文件名包含空格时,表达式需要放在引号内,否则就不需要。如果希望完成者仅返回文件并忽略文件夹,则将 -File 参数添加到 Get-ChildItem

PowerShell 技能连载 - 列出安装的应用程序(第 2 部分)

在上一个技能中,我们读取了注册表,以查找可以启动的应用程序的路径。这种方法效果很好,但存在两个缺陷:首先,列表不包含应用程序的友好名称,其次,列表不完整。仅列出已注册的程序。

让我们获得完整的应用程序列表,并使用三个技巧来克服这些限制:

  • 使用 generic list 作为结果,以便能够将更多信息快速添加到列表中
  • Get-Command 的结果合并到 PowerShell 已知的应用程序中
  • 读取扩展文件信息得到应用程序友好名称

我们从这里开始:从注册表中注册的应用程序列表:

1
2
3
4
5
6
7
8
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*"

[System.Collections.Generic.List[string]]$list =
Get-ItemProperty -Path $key |
Select-Object -ExpandProperty '(Default)' -ErrorAction Ignore

$list

现在,我们将 Get-Command 已知的应用程序添加到列表中:

1
2
3
4
5
[System.Collections.Generic.List[string]]$commands =
Get-Command -CommandType Application |
Select-Object -ExpandProperty Source

$list.AddRange($commands)

最后,删除引号,空的和重复的项目来清理列表:

1
2
3
4
$finalList = $list |
Where-Object { $_ } |
ForEach-Object { $_.Replace('"','').Trim().ToLower() } |
Sort-Object -Unique

现在,通过读取每个文件的扩展信息,将列表变成具有应用程序名称、描述和绝对路径的对象。这也消除了所有不存在的路径。以下是完整的代码:

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
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*"

[System.Collections.Generic.List[string]]$list =
Get-ItemProperty -Path $key |
Select-Object -ExpandProperty '(Default)' -ErrorAction Ignore

[System.Collections.Generic.List[string]]$commands = Get-Command -CommandType Application |
Select-Object -ExpandProperty Source

$list.AddRange($commands)

$finalList = $list |
Where-Object { $_ } |
ForEach-Object { $_.Replace('"','').Trim().ToLower() } |
Sort-Object -Unique |
ForEach-Object {
try {
$file = Get-Item -Path $_ -ErrorAction Ignore
[PSCustomObject]@{
Name = $file.Name
Description = $file.VersionInfo.FileDescription
Path = $file.FullName
}
} catch {}
} |
Sort-Object -Property Name -Unique

$finalList | Out-GridView -PassThru

结果类似这样:

1
2
3
4
5
6
7
8
9
PS> $finalList

Name Description
---- -----------
7zfm.exe 7-Zip File Manager
accicons.exe Microsoft Office component
acrord32.exe Adobe Acrobat Reader DC
agentservice.exe AgentService EXE
aitstatic.exe Application Impact Telemetry...

PowerShell 技能连载 - 列出安装的应用程序(第 1 部分)

是否想知道启动一个应用程序的运行路径?Windows 注册表中有存储以下信息的键:

1
2
3
4
5
6
7
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*"

$lookup = Get-ItemProperty -Path $key |
Select-Object -ExpandProperty '(Default)' -ErrorAction Ignore |
Where-Object { $_ } |
Sort-Object

结果是已注册的应用程序路径的排序列表。您可以轻松地将其转换为查找哈希表,该表根据可执行文件名称查询完整路径:

1
2
3
4
5
6
7
8
9
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\*"

$lookup = Get-ItemProperty -Path $key |
Select-Object -ExpandProperty '(Default)' -ErrorAction Ignore |
Where-Object { $_ } |
Group-Object -Property {
$_.Replace('"','').Split('\')[-1].ToLower()
} -AsHashTable -AsString

您现在可以列出所有已注册的应用程序的清单:

1
2
3
4
5
6
7
PS> $lookup.Keys
outlook.exe
winword.exe
snagit32.exe
7zfm.exe
msoasb.exe
...

或者您可以可以将可执行文件的路径转换为它的真实路径:

1
2
PS> $lookup['excel.exe']
C:\Program Files (x86)\Microsoft Office\Root\Office16\EXCEL.EXE

PowerShell 技能连载 - 神秘的动态参数完成器

在上一个技能中,我们介绍了鲜为人知的 “ArgumentCompletion“ 属性,该属性可以为参数提供类似 IntelliSense 的自动完成功能。但是,此属性可以做的更多。之前,我们介绍了以下代码:

1
2
3
4
5
6
7
8
9
10
function Get-Vendor {
param(
[Parameter(Mandatory)]
[ArgumentCompleter({'Microsoft','Amazon','Google'})]
[string]
$Vendor
)

"Chosen vendor: $Vendor"
}

当用户调用 Get-Vendor 时,通过按 TAB 或 CTRL-SHIFT 键,将显示 “ArgumentCompleter“ 属性中列出的建议。

您可能想知道为什么将字符串列表嵌入大括号(脚本块)中。答案是:因为这段代码实际上是在用户调用完成时执行的。您也可以动态生成该自动完成文本。

这个 Submit-Date 函数具有一个称为 -Date 的参数。每当您按 TAB 键时,自动完成程序都会以引用的 ISO 格式完成当前日期和时间:

1
2
3
4
5
6
7
8
9
10
function Submit-Date {
param(
[Parameter(Mandatory)]
[ArgumentCompleter({ '"{0}"' -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss") })]
[DateTime]
$Date
)

"Chosen date: $Date"
}

运行代码来试验效果。接下来,输入 Submit-Date,以及一个空格,然后按 TAB 键。

1
PS> Submit-Date -Date "2020-01-21 16:33:19"

同样,下一个函数实现 -FileName 参数,当您按 TAB 键时,它将自动完成 Windows 文件夹中的实际文件名:

1
2
3
4
5
6
7
8
9
10
11
12
function Get-File {
param(
[Parameter(Mandatory)]
[ArgumentCompleter({Get-ChildItem -Path $env:windir -Name})]
[string]
$FileName
)

"Chosen file name: $FileName"
}

Get-File -FileName

每当用户通过按 TAB 或 CTRL-SPACE 调用自动完成功能时,都会执行提交到 ArgumentCompleter 的脚本块中的代码,并将结果用于自动完成功能。

这可能就是为什么 AutoCompleter 属性不会自动弹出 IntelliSense 而是仅响应用户请求自动弹出的原因。请注意,自动完成功能可能无法在编辑器窗格中使用。它是为交互式PowerShell控制台设计的。

PowerShell 技能连载 - 带颜色的控制台硬拷贝

如果您想硬拷贝 PowerShell 控制台的内容,则可以复制和选择文本,但这会弄乱颜色和格式。

更好的方法是读取控制台屏幕缓冲区,并编写 HTML 文档。然后可以将这些 HTML 文档复制并粘贴到 Word 和其他目标中,并保持格式和颜色。

以下代码当然还不是完美的,但说明了采用的方法:

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-ConsoleBufferAsHtml{
$html = [Text.StringBuilder]'' $null = $html.Append("<pre style='MARGIN: 0in 10pt 0in; line-height:normal'; font-family:Consolas; font-size:10pt; >")
$bufferWidth = $host.UI.RawUI.BufferSize.Width $bufferHeight = $host.UI.RawUI.CursorPosition.Y $rec = [Management.Automation.Host.Rectangle]::new(
0,0,($bufferWidth - 1),$bufferHeight )
$buffer = $host.ui.rawui.GetBufferContents($rec)

for($i = 0; $i -lt $bufferHeight; $i++)
{
$span = [Text.StringBuilder]'' $foreColor = $buffer[$i, 0].Foregroundcolor $backColor = $buffer[$i, 0].Backgroundcolor for($j = 0; $j -lt $bufferWidth; $j++)
{
$cell = $buffer[$i,$j]
if (($cell.ForegroundColor -ne $foreColor) -or ($cell.BackgroundColor -ne $backColor))
{
$null = $html.Append("<span style='color:$foreColor;background:$backColor'>$($span)</span>" )
$span = [Text.StringBuilder]'' $foreColor = $cell.Foregroundcolor $backColor = $cell.Backgroundcolor }
$null = $span.Append([Web.HttpUtility]::HtmlEncode($cell.Character))

}
$null = $html.Append("<span style='color:$foreColor;background:$backColor'>$($span)</span><br/>" )
}

$null = $html.Append("</pre>")
$html.ToString()
}

请注意,此功能需要一个真实的控制台窗口,因此在 PowerShell ISE 中将无法使用。当您运行上述代码时,它将为您提供一个名为 Get-ConsoleBufferAsHtml 的新命令。

要将当前控制台内容硬拷贝到 HTML 文件,请运行以下命令:

1
PS>  Get-ConsoleBufferAsHtml | Set-Content $env:temp\test.html

要在关联的浏览器中打开生成的 HTML,请运行以下命令:

1
PS>  Invoke-Item $env:temp\test.html

PowerShell 技能连载 - 理解 $ErrorView

当 PowerShell 遇到问题时,它会显示一条相当长的错误消息:

1
2
3
4
5
6
7
PS> 1/0
Attempted to divide by zero.
At line:1 char:1
+ 1/0
+ ~~~
+ CategoryInfo : NotSpecified: (:) [], RuntimeException
+ FullyQualifiedErrorId : RuntimeException

在现实生活中,您通常只需要第一行,而早在 2006 年,PowerShell 团队就添加了一个名为 $ErrorView 的首选项变量,该变量可以控制错误消息的显示方式。当将它赋值为“CategoryView”时,将大大缩短错误消息:

1
2
3
4
5
6
PS> $ErrorView = 'CategoryView'

PS> 1/0
NotSpecified: (:) [], RuntimeException

PS>

不过,这并不太好用,因为一行无法体现真正重要的信息,并且您可以将它赋值为“NormalView”以返回到默认视图。同时,实用性较差导致多数人从未听说过 $ErrorView

幸运的是,在PowerShell 7(RC1 中引入)中,团队记住并最终解决了该问题。为了不破坏兼容性,他们选择添加第三个选项:ConciseView。现在,单行错误消息可以正常工作,并显示典型用户需要知道的所有信息:

1
2
3
4
5
6
PS> $ErrorView = ConciseView

PS> 1/0
RuntimeException: Attempted to divide by zero.

PS>

作为 PowerShell 开发人员,只需切换回“NormalView”即可查看其余的错误消息。或者(甚至更好)运行 Get-Error -Newest 1 以获取有关最新错误的详细信息:

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
PS C:\> 1/0
RuntimeException: Attempted to divide by zero.
PS C:\> Get-Error -Newest 1

Exception            :
  Type         : System.Management.Automation.RuntimeException
  ErrorRecord  :
      Exception            :
          Type  : System.Management.Automation.ParentContainsErrorRecordException
          Message : Attempted to divide by zero.
          HResult : -2146233087
      CategoryInfo         : NotSpecified: (:) [], ParentContainsErrorRecordException
      FullyQualifiedErrorId : RuntimeException
      InvocationInfo      :
          ScriptLineNumber : 1
          OffsetInLine    : 1
          HistoryId      : -1
          Line            : 1/0
          PositionMessage : At line:1 char:1
                             + 1/0
                             + ~~~
          CommandOrigin  : Internal
      ScriptStackTrace     : at , : line 1
  TargetSite    :
      Name         : Divide
      DeclaringType : System.Management.Automation.IntOps, System.Management.Automation, Version=7.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35
      MemberType  : Method
      Module      : System.Management.Automation.dll
  StackTrace    :
 at System.Management.Automation.IntOps.Divide(Int32 lhs, Int32 rhs)
 at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
 at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame)
 at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
  Message      : Attempted to divide by zero.
  Data         : System.Collections.ListDictionaryInternal
  InnerException :
      Type    : System.DivideByZeroException
      Message : Attempted to divide by zero.
      HResult : -2147352558 ;
  Source       : System.Management.Automation
  HResult      : -2146233087
CategoryInfo          : NotSpecified: (:) [], RuntimeException
FullyQualifiedErrorId : RuntimeException
InvocationInfo    :
  ScriptLineNumber: 1
  OffsetInLine    : 1
  HistoryId       : -1
  Line            : 1/0
  PositionMessage : At line:1 char:1
                     + 1/0
                     + ~~~
  CommandOrigin  : Internal
ScriptStackTrace     : at , : line 1