PowerShell 技能连载 - 使用自定义作用域来屏蔽任何输出

昨天我们看了自定义作用域能够自动还原变量并在您的代码之后清除现场。

自定义作用域也可以用来忽略域里任何一段代码输出的任何结果。要实现它,请使用这样的解构:$null = .{[code]}。无论您在方括号里执行什么代码,您创建的所有的变量和函数在域外都能使用,但是不会产生任何输出。

让我们看看这个函数:

1
2
3
4
5
6
7
function Out-Voice ($Text)
{
$sapi = New-Object -ComObject Sapi.SpVoice
$sapi.Speak($Text)
}

Out-Voice -Text 'Hello, Dude.'

当你运行它时,它将能播放语音,但也输出了数字“1”。所以 Speak() 方法会造成这样的现象——当您的代码变得庞大而复杂时,有许多地方在输出不必要的数字。

以下是一个极简单的“补丁”函数能产生相同的小郭,但是保证不会返回任何值:

1
2
3
4
5
6
7
8
9
function Out-Voice ($Text)
{
$null = . {
$sapi = New-Object -ComObject Sapi.SpVoice
$sapi.Speak($Text)
}
}

Out-Voice -Text 'Hello, Dude.'

PowerShell 技能连载 - 使用自定义域

当您改变变量时,您可能需要在稍后清除它们并且确保它们回退到缺省值——用自定义作用域就可以做到。昨天,我们学习了如何处理控制台程序的错误。并且回顾那段代码,您会发现重置 $ErrorActionPreference 系统变量要费很多事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try
{
# set the preference to STOP
$old = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
# RUN THE CONSOLE EXE THAT MIGHT EMIT AN ERROR,
# and redirect the error channel #2 to the
# output channel #1
net user doesnotexist 2>&1
}

catch [System.Management.Automation.RemoteException]
{
# catch the error emitted by the EXE,
# and do what you want
$errmsg = $_.Exception.Message
Write-Warning $errmsg
}

finally
{
# reset the erroractionpreference to what it was before
$ErrorActionPreference = $old
}

一个简单得多的办法是使用自定义作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
& {
try
{
# set the preference to STOP
$ErrorActionPreference = 'Stop'
# RUN THE CONSOLE EXE THAT MIGHT EMIT AN ERROR,
# and redirect the error channel #2 to the
# output channel #1
net user doesnotexist 2>&1
}

catch [System.Management.Automation.RemoteException]
{
# catch the error emitted by the EXE,
# and do what you want:
$errmsg = $_.Exception.Message
Write-Warning $errmsg
}
}

${[code]} 这段代码创建了一个新的作用域,并且任何在其中定义的变量都会在退出该作用域时删除。这是为何在上述例子中,$ErrorActionPreference 能够自动还原为它之前的值。

PowerShell 技能连载 - 捕获 Native EXE 的错误

是否想知道如何捕获 native 控制台 EXE 程序的错误?PowerShell 的错误处理器只能处理 .NET 代码的错误。

这段代码是捕获控制台应用程序错误的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try
{
# set the preference to STOP
$old = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
# RUN THE CONSOLE EXE THAT MIGHT EMIT AN ERROR,
# and redirect the error channel #2 to the
# output channel #1
net user doesnotexist 2>&1
}

catch [System.Management.Automation.RemoteException]
{
# catch the error emitted by the EXE,
# and do what you want
$errmsg = $_.Exception.Message
Write-Warning $errmsg
}

finally
{
# reset the erroractionpreference to what it was before
$ErrorActionPreference = $old
}

一旦控制台程序发出一个错误,它就会输出到控制台的 #2 通道。由于示例代码中该通道直接重定向到普通的 output,所以 PowerShell 能接收到它。当 ErrorActionPreference 设成 “Stop“ 时,PowerShell 会将任何该通道的输入数据转发到一个 .NET RemoteException,这样您就可以捕获它。

1
WARNING: The user name  could not be found.

PowerShell 技能连载 - 高级错误处理:重新抛出异常

在处理错误时,您有时会希望将原始的异常替换成您自己的。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Do-Something
{
# function uses internal error handling
try
{
Get-Process -Name NotThereOhWell -ErrorAction Stop
}
# catch this error type
catch [Microsoft.PowerShell.Commands.ProcessCommandException]
{
$oldE = $_.Exception

# handle the error, OR SHOWN HERE: issue a new exception to the caller
$newE = New-Object -TypeName System.InvalidOperationException('Do-Something: A fatal error occured', $oldE)
Throw $newE
}
}

# function will encounter an internal error
# error message shows error message generated by function instead
Do-Something

调用者看到的内容如下:

1
2
3
4
5
6
7
PS C:\>  Do-Something
Do-Something: A fatal error occured
At line:18 char:5
+ Throw $newException
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], InvalidOperationException
+ FullyQualifiedErrorId : Do-Something: A fatal error occured

如果调用者也用一个错误处理函数来接收它,则会出现这种情况:

1
2
3
4
5
6
7
8
9
10
11
try
{
Do-Something
}
catch [System.InvalidOperationException]
{
[PSCustomObject]@{
Message = $_.Exception.Message
Originalmessage = $_.Exception.InnerException.Message
}
}

结果看起来如下:

1
2
3
4
5
Message                             Originalmessage
------- ---------------
Do-Something: A fatal error occured Cannot find a process with the name
"NotThereOhWell". Verify the process name
and call the cmdlet again.

这样调用者可以看到返回的错误信息,并且经过内部处理之后,还可以传递原始的错误信息。

PowerShell 技能连载 - 用其他身份启动程序

假设您想以不同的身份打开多个 PowerShell 控制台,或以其他人的身份打开任何程序。

要实现这个目标,您需要以其他人的身份登录,这很明显是个负担。以下是将凭据以安全的方式保存到文件的方法:密码采用您的身份和您的机器加密成密文。只有保存它们的那个人可以取回它,而且只能在保存该文件的机器上操作:

1
2
# saving credential securely to file
Get-Credential | Export-Clixml -Path "$home\login.xml"

这个凭据将保存到用户配置中。如果您希望保存到其它地方,请改变路径。喜欢保存多少份,就调用多少次该方法。

下一步,假设您加载了一个保存的凭据,并且使用该身份启动了一个程序:

1
2
3
4
# getting back saved credential
$cred = Import-Clixml -Path "$home\login.xml"
# launch application
Start-Process -FilePath powershell -Credential $cred -Work c:\ -LoadUserProfile

这将以您之前指定的用户身份创建一个新的 PowerShell 实例——无需手动登录。

PowerShell 技能连载 - 轻轻跳进 PowerShell 版本的丛林

PowerShell 同时有五个主要版本在发行。除掉最新的小版本,例如 Windows 10 和 Server 2016 上的 PowerShell 5.1。加上 beta 版和预发行版本,以及 Linux 和 Nano 服务器上的 PowerShell。哇哦!

要跟踪这些版本,并知道正在用哪个版本、它从哪里来、可能有哪些兼容性问题很不容易。MVP 大学里的 Egil Ring 维护着一个很酷的模块。如果您装了 PowerShell 5 或从 powershellgallery.com 安装了 PowerShellGet,您可以用一行代码下载这个模块:

1
PS C:\> Install-Module -Name PSVersion -Scope CurrentUser

回答几个问题后,该模块安装完成。它只包含两个命令:

1
2
3
4
5
6
PS C:\> Get-Command -Module PSVersion

CommandType Name Version Source
----------- ---- ------- ------
Function Get-PSVersion 1.6 PSVersion
Function Update-PSVersionData 1.6 PSVersion

PSVersion 是一个社区项目,跟踪 PowerShell 的发行编号、它们的含义、它们的来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS C:\> Get-PSVersion -ListVersion

Name FriendlyName ApplicableOS
---- ------------ ------------
5.1.14393.0 Windows PowerShell 5.1 Preview Windows 10 Anniversar...
5.1.14300.1000 Windows PowerShell 5.1 Preview Windows Server 2016 T...
5.0.10586.494 Windows PowerShell 5 RTM Windows 10 1511 + KB3...
5.0.10586.122 Windows PowerShell 5 RTM Windows 10 1511 + KB3...
5.0.10586.117 Windows PowerShell 5 RTM 1602 Windows Server 2012 R...
5.0.10586.63 Windows PowerShell 5 RTM Windows 10 1511 + KB3...
5.0.10586.51 Windows PowerShell 5 RTM 1512 Windows Server 2012 R...
5.0.10514.6 Windows PowerShell 5 Production Preview 1508 Windows Server 2012 R2
5.0.10018.0 Windows PowerShell 5 Preview 1502 Windows Server 2012 R2
5.0.9883.0 Windows PowerShell 5 Preview November 2014 Windows Server 2012 R...
4.0 Windows PowerShell 4 RTM Windows Server 2012 R...
3.0 Windows PowerShell 3 RTM Windows Server 2012, ...
2.0 Windows PowerShell 2 RTM Windows Server 2008 R...
1.0 Windows PowerShell 1 RTM Windows Server 2008, ...

这是在您的企业中以友好的 PowerShell 版本名称获得 PowerShell 版本号的方法:

1
2
3
4
5
PS C:\> Get-PSVersion

PSComputerName PSVersion PSVersionFriendlyName
-------------- --------- ---------------------
CLIENT12 5.1.14393.0 Windows PowerShell 5.1 Preview

PowerShell 技能连载 - 修复 PowerShell 5 帮助的 Bug

使用 Update-Help 下载 PowerShell 的帮助文件时,PowerShell 5 有一个 bug,目前可以修复:基于文本的帮助文件扩展名是“.txt”而不是“.help.txt”,所以 PowerShell 帮助系统会忽略它们。您可以自己试验一下——以下命令可能会返回一大堆关于主题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PS C:\> Get-Help about*

Name Category Module Synopsis
---- -------- ------ --------
about_Aliases HelpFile SHORT DESCRIPTION
about_Arithmetic_Operators HelpFile SHORT DESCRIPTION
about_Arrays HelpFile SHORT DESCRIPTION
about_Assignment_Operators HelpFile SHORT DESCRIPTION
about_Automatic_Variables HelpFile SHORT DESCRIPTION
about_Break HelpFile SHORT DESCRIPTION
about_Classes HelpFile SHORT DESCRIPTION
about_Command_Precedence HelpFile SHORT DESCRIPTION
about_Command_Syntax HelpFile SHORT DESCRIPTION
about_Comment_Based_Help HelpFile SHORT DESCRIPTION
about_CommonParameters HelpFile SHORT DESCRIPTION
about_Comparison_Operators HelpFile SHORT DESCRIPTION
about_Continue HelpFile SHORT DESCRIPTION
about_Core_Commands HelpFile SHORT DESCRIPTION
about_Data_Sections HelpFile SHORT DESCRIPTION
about_Debuggers HelpFile SHORT DESCRIPTION
about_DesiredStateConfiguration HelpFile SHORT DESCRIPTION

如果没有显示上述的内容,您也许还没有事先运行过 Update-Help 来下载帮助文件,或者您被 bug 吃了。

无论这个 bug 是否正在修复中,用 PowerShell 您可以轻松地修改这些东西。以下是一个用于修复这些受影响的帮助文件的扩展名的脚本。

这个脚本需要管理员权限,因为帮助文件是位于受保护的 Windows 文件夹中:

1
2
3
4
5
6
7
# find all text files inside the PowerShell folder that start
# with "about"
Get-ChildItem -Path $pshome -Filter about*.txt -Recurse |
# identify those that do not end with ".help.txt"
Where-Object { $_.Name -notlike '*.help.txt' } |
# rename the extension using a regex:
Rename-Item -NewName { $_.Name -replace '\.txt$', '.help.txt'}

PowerShell 技能连载 - 禁止按位置的参数

当您创建 PowerShell 函数时,参数可以是命名的也可以是按位置的。以下是一个例子:

如果您想检测文件系统中的非法字符,以下是一个简单的适配:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Test-Command
{
param
(
[string]$Name,
[int]$Id
)

"Name: $Name ID: $ID"
}

Test-Command -Name Weltner -Id 12
Test-Command Weltner 12

如您所见,使用按位置的参数(只需要指定参数,不需要显式地指定参数名)可能更适用于为特定目的编写的代码,但是可读性更差。这是有可能的,因为上述函数的语法看起来如下:

1
Test-Command [[-Name] <string>] [[-Id] <int>]

那么一个编写一个效果相反的 PowerShell 函数,实现这种语法呢:

1
Test-Command [-Name <string>] [-Id <int>] [<CommonParameters>]

目前这个方法比较生僻,不过是完全可行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Test-Command
{
param
(
[Parameter(ParameterSetName='xy')]
[string]$Name,

[Parameter(ParameterSetName='xy')]
[int]$Id
)

"Name: $Name ID: $ID"
}

一旦开始使用参数集合,缺省情况下所有参数都是命名的参数。

PowerShell 技能连载 - 在 PowerShell 函数中使用命名的函数

当您创建一个 PowerShell 函数时,所有参数都有默认的位置,除非人为地加上“Position”属性。一旦加上这个属性,所有不带“Position”的参数将立刻变为命名的必须参数。让我们看看例子:

这是一个经典的函数定义,创建了三个固定位置的参数:

1
2
3
4
5
6
7
8
9
10
11
function Test-Command
{
param
(
[string]$Name,
[int]$ID,
[string]$Email
)

# TODO: Code using the parameter values
}

语法如下:

1
Test-Command [[-Name] <string>] [[-ID] <int>] [[-Email] <string>]

一旦您在任何一个参数上添加“Position”属性,其它的就变为命名的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Test-Command
{
param
(
[Parameter(Position=0)]
[string]$Name,
[Parameter(Position=1)]
[int]$ID,
[string]$Email
)

# TODO: Code using the parameter values
}

以下是它的语法:

1
Test-Command [[-Name] <string>] [[-ID] <int>] [-Email <string>] [<CommonParameters>]

区别在哪?您不需要指定参数名 -Name-ID,但如果您希望为第三个参数指定一个值,必须指定 -Email。在第一个例子中,所有三个参数都可以按照位置来定位。

PowerShell 技能连载 - 隐藏启动 PowerShell

有时候 PowerShell 脚本只是用来生成某些东西,比如说生成报告,而不需要用 Excel 或记事本打开它。这时候您不会希望执行 PowerShell 的时候显示 PowerShell 控制台窗口。

并没有很简单的方法能隐藏 PowerShell 的控制台窗口,因为即使用了 -WindowsStyle Hidden 参数,也会先显示控制台,然后隐藏它(一闪而过)。

一种方法是使用 Windows 快捷方式来启动脚本。右键单击桌面的空白区域,然后选择新建/快捷方式。就可以新建一个快捷方式。当提示输入地址的时候,键入这行代码:

1
powershell -noprofile -executionpolicy bypass -file "c:\path\to\script.ps1"

点击“下一步”,然后添加脚本的名称,再点击“下一步”,就接近完成了。这个快捷方式显示蓝色的 PowerShell 图标。单击它的时候,脚本即可运行,只是还不是隐藏的。

您现在只需要右键单击新创建的快捷方式,选择“属性”,然后将“运行”的设置从“正常窗口”改为您想要的设置。您也可以设置一个快捷方式,这需要管理员权限。

一个缺点是,在 Windows 10 中,“运行”的设置不再包含隐藏程序的选项。您最多可以最小化执行。