PowerShell 技能连载 - 将安全字符串转换为明文

安全字符串的内容并不能很轻易地查看:

1
2
3
4
5
6
$password = Read-Host -Prompt 'Your password' -AsSecureString



PS C:\> $password
System.Security.SecureString

然而,如果您是第一个要求安全字符串的人,您可以用这个聪明的技巧轻松地将它转换为纯文本:

1
2
$txt = [PSCredential]::new("X", $Password).GetNetworkCredential().Password
$txt

本质上,SecureString 是用来创建一个 PSCredential 对象,并且一个 PSCredential 对象包含了 GetNetworkCredential() 方法,它能够自动地将加密的密码转换为明文。

通过这种方式,您可以使用 Red-Hsot -AsSecureString 提供的遮罩输入框来输入敏感信息,即便您需要该信息的明文字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Read-HostSecret([Parameter(Mandatory)]$Prompt)
{
$password = Read-Host -Prompt $Prompt -AsSecureString
[PSCredential]::new("X", $Password).GetNetworkCredential().Password
}



PS C:\> Read-HostSecret -Prompt 'Your secret second first name'
Your secret second first name: ********
Valentin

PS C:\>

PowerShell 技能连载 - 使用目录文件

目录文件支持 (.cat) 是 PowerShell 5.1 新引入的特性。目录文件基本上是包含哈希值的文件列表。您可以用它们来确保一个指定的文件结构没有改变。

以下是一个简单的示例。它输入一个文件夹(请确保文件夹存在,否则将它重命名)并对整个文件夹的内容创建一个目录文件。请注意这将对文件夹中的每个文件创建一个哈希值,所以请留意文件夹的体积不要太大:

1
2
3
$path = c:\folderToCheck"
$catPath = "$home\Desktop\summary.cat"
New-FileCatalog -Path $path -CatalogVersion 2.0 -CatalogFilePath $catPath

New-FileCatalog 将在桌面上创建一个 summary.cat 文件。这个文件可以用于测试文件夹结构并确保文件没有改变过:

1
Test-FileCatalog -Detailed -Path $path -CatalogFilePath $catPath

结果看起来类似这样:

Status        : Valid
HashAlgorithm : SHA1
CatalogItems  : {[remoting systeminventar.ps1,
                F43C8D6F9CB93FB9AA5DBA6733D9996645832256], [klonen und
                prozessprio.ps1, F3DE20424CD90CDB5B85933B777A2F9A3F3D3187],
                [scriptblock rueckgabewerte.ps1,
                EB239D7906EF42E2639CACBE68C6FDD8F4AD899F], [Untitled4.ps1,
                E5E4DC20934287ED869230706A1DEEDEB550B8DE]...}
PathItems     : {[beispielsyntax.ps1,
                5183C82B7F0F3D0623242DD5F97A658724BE3B81], [closure.ps1,
                D2A036B068548B3E773E3BEBCF40997231576ED1], [debug1.ps1,
                3547D2659792A9ABA9E6E12F287D7A8116540FCF], [debug2.ps1,
                76C63FA578C09F30DF2BE055C37C039AFB1EFEDE]...}
Signature     : System.Management.Automation.Signature

请注意 New-FileCatalog 当前并不支持路径中的特殊字符,例如德语的 “Umlaute”。

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
function ConvertFrom-ErrorRecord
{
param
(
# we receive either a legit error record...
[Management.Automation.ErrorRecord[]]
[Parameter(
Mandatory,ValueFromPipeline,
ParameterSetName='ErrorRecord')]
$ErrorRecord,

# ...or a special stop exception which is raised by
# cmdlets with -ErrorAction Stop
[Management.Automation.ActionPreferenceStopException[]]
[Parameter(
Mandatory,ValueFromPipeline,
ParameterSetName='StopException')]
$Exception
)



process
{
# if we received a stop exception in $Exception,
# the error record is to be found inside of it
# in all other cases, $ErrorRecord was received
# directly
if ($PSCmdlet.ParameterSetName -eq 'StopException')
{
$ErrorRecord = $Exception.ErrorRecord
}

# compose a new object out of the interesting properties
# found in the error record object
$ErrorRecord | ForEach-Object { [PSCustomObject]@{
Exception = $_.Exception.Message
Reason = $_.CategoryInfo.Reason
Target = $_.CategoryInfo.TargetName
Script = $_.InvocationInfo.ScriptName
Line = $_.InvocationInfo.ScriptLineNumber
Column = $_.InvocationInfo.OffsetInLine
}
}
}
}

您可以在 $error 中辨认出收集到的错误信息:

1
PS C:\> $Error | ConvertFrom-ErrorRecord | Out-GridView

您也可以在 try..catch 代码快中使用它:

1
2
3
4
5
6
7
8
9
try
{
Get-Service -Name foo -ErrorAction Stop

}
catch
{
$_ | ConvertFrom-ErrorRecord
}

结果类似这样:

Exception : Cannot find any service with service name 'foo'.
Reason    : ServiceCommandException
Target    : foo
Script    :
Line      : 5
Column    : 3

您甚至可以用 -ErrorVariable 通用参数来收集一个 cmdlet 运行时发生的所有错误记录:

1
2
$r = Get-ChildItem -Path $env:windir -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue -ErrorVariable test
$test | ConvertFrom-ErrorRecord

相同地,结果类似这样:

Exception : Access to the path 'C:\Windows\AppCompat\Appraiser\Telemetry' is
            denied.
Reason    : UnauthorizedAccessException
Target    : C:\Windows\AppCompat\Appraiser\Telemetry
Script    :
Line      : 3
Column    : 6

Exception : Access to the path 'C:\Windows\AppCompat\Programs' is denied.
Reason    : UnauthorizedAccessException
Target    : C:\Windows\AppCompat\Programs
Script    :
Line      : 3
Column    : 6

Exception : Access to the path 'C:\Windows\CSC\v2.0.6' is denied.
Reason    : UnauthorizedAccessException
Target    : C:\Windows\CSC\v2.0.6
Script    :
Line      : 3
Column    : 6

...

PowerShell 技能连载 - 创建写保护的函数

PowerShell 的函数缺省情况下可以在任何时候被覆盖,而且可以用 Remove-Item 来移除它:

1
2
3
4
5
6
7
8
9
10
function Test-Lifespan
{
"Hello!"
}

Test-Lifespan

Remove-Item -Path function:Test-Lifespan

Test-Lifespan

对于安全相关的函数,您可能希望以某种不会被删除的方式创建它们。以下是实现方法:

1
2
3
4
5
6
7
$FuncName = 'Test-ConstantFunc'
$Expression = {
param($Text)
"Hello $Text, I cannot be removed!"
}

Set-Item -Path function:$FuncName -Value $Expression -Options Constant,AllScope

这个新函数是用 Set-Item 直接在 function: 驱动器内创建。通过这种方式,您可以对该函数增加新的选项,例如 ConstantAllScope。这个函数能够以期待的方式工作:

1
2
PS C:\> Test-ConstantFunc -Text $env:username
Hello DemoUser, I cannot be removed!

Constant“ 确保该函数无法被覆盖或是被删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS C:\> function Test-ConstantFunc { "Got you!!" }
Cannot write to function Test-ConstantFunc because it is read-only or constant.
At line:1 char:1
+ function Test-ConstantFunc { "got you!!" }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (Test-ConstantFunc:String) [], Sessio
nStateUnauthorizedAccessException
+ FullyQualifiedErrorId : FunctionNotWritable


PS C:\> Remove-Item -Path function:Test-ConstantFunc
Remove-Item : Cannot remove function Test-ConstantFunc because it is constant.
At line:1 char:1
+ Remove-Item -Path function:Test-ConstantFunc
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (Test-ConstantFunc:String) [Remove-It
em], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : FunctionNotRemovable,Microsoft.PowerShell.Command
s.RemoveItemCommand

更重要的是,”AllScope“ 确保该函数无法在子作用域中被掩盖。有了写保护之后,在一个常见的用独立的子作用于来定义一个同名的新函数的场景中:

1
2
3
4
5
& {
function Test-ConstantFunc { "I am the second function in a child scope!" }
Test-ConstantFunc

}

结果是,因为 “AllScope“ 的作用,将原来的保护函数覆盖的操作不再起作用:

Cannot write to function Test-ConstantFunc because it is read-only or constant.
At line:4 char:3
+   function Test-ConstantFunc { "I am a second function in a child sco ...
+   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : WriteError: (Test-ConstantFunc:String) [], Sessio
    nStateUnauthorizedAccessException
    + FullyQualifiedErrorId : FunctionNotWritable

Hello , I cannot be removed!

PowerShell 技能连载 - $FormatEnumerationLimit 作用域问题

如前一个技能所述,$FormatEnumerationLimit 隐藏变量决定了输出时会在多少个元素后截断。以下是再次演示该区别的例子:

1
2
3
4
5
6
7
8
9
$default = $FormatEnumerationLimit

Get-Process | Select-Object -Property Name, Threads -First 5 | Out-Default
$FormatEnumerationLimit = 1
Get-Process | Select-Object -Property Name, Threads -First 5 | Out-Default
$FormatEnumerationLimit = -1
Get-Process | Select-Object -Property Name, Threads -First 5 | Out-Default

$FormatEnumerationLimit = $default

输出结果类似这样:

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
Name       Threads
---- -------
acrotray {3160}
AERTSr64 {1952, 1968, 1972, 8188}
AGSService {1980, 1988, 1992, 2000...}
armsvc {1920, 1940, 1944, 7896}
ccSvcHst {2584, 2644, 2656, 2400...}



Name Threads
---- -------
acrotray {3160}
AERTSr64 {1952...}
AGSService {1980...}
armsvc {1920...}
ccSvcHst {2584...}



Name Threads
---- -------
acrotray {3160}
AERTSr64 {1952, 1968, 1972, 8188}
AGSService {1980, 1988, 1992, 2000, 2024, 7932}
armsvc {1920, 1940, 1944, 7896}
ccSvcHst {2584, 2644, 2656, 2400, 3080, 3120, 3124, 3128, 3132, 3136, 3140,...

然而这在函数(或是脚本块等情况)中使用时可能会失败:

1
2
3
4
5
6
7
function Test-Formatting
{
$FormatEnumerationLimit = 1
Get-Process | Select-Object -Property Name, Threads -First 5
}

Test-Formatting

虽然 $FormatEnumerationLimit 设置为 1,但数组仍然按缺省的显示 4 个元素。这是因为 $FormatEnumerationLimit 只对全局作用域有效。您需要在全局作用域中改变该变量才有效。所以需要用这种方法来写一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Test-Formatting
{
# remember the current setting
$default = $global:FormatEnumerationLimit

# change on global scope
$global:FormatEnumerationLimit = 1
Get-Process | Select-Object -Property Name, Threads -First 5

# at the end, clean up and revert to old value
$global:FormatEnumerationLimit = $default
}

Test-Formatting

PowerShell 技能连载 - 在输出中显示数组成员

当您输出包含数组属性的对象时,只会显示 4 个数组元素,然后用省略号截断剩余的结果:

1
2
3
4
5
6
7
8
9
10
PS C:\> Get-Process | Select-Object -Property Name, Threads -First 6

Name Threads
---- -------
acrotray {3160}
AERTSr64 {1952, 1968, 1972, 8188}
AGSService {1980, 1988, 1992, 2000...}
armsvc {1920, 1940, 1944, 7896}
audiodg {7436, 1460, 2192, 6784}
ccSvcHst {2584, 2644, 2656, 2400...}

要显示更多(或是全部)数组元素,请使用内置的 $FormatEnumerationLimit 变量。它的缺省值是 4,但是您可以将它改为希望显示的元素个数,或将它设置为 -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
PS C:\> $FormatEnumerationLimit = 1

PS C:\> Get-Process | Select-Object -Property Name, Threads -First 6

Name Threads
---- -------
acrotray {3160}
AERTSr64 {1952...}
AGSService {1980...}
armsvc {1920...}
audiodg {7436...}
ccSvcHst {2584...}



PS C:\> $FormatEnumerationLimit = 2

PS C:\> Get-Process | Select-Object -Property Name, Threads -First 6

Name Threads
---- -------
acrotray {3160}
AERTSr64 {1952, 1968...}
AGSService {1980, 1988...}
armsvc {1920, 1940...}
audiodg {7436, 2192...}
ccSvcHst {2584, 2644...}



PS C:\> $FormatEnumerationLimit = -1

PS C:\> Get-Process | Select-Object -Property Name, Threads -First 6

Name Threads
---- -------
acrotray {3160}
AERTSr64 {1952, 1968, 1972, 8188}
AGSService {1980, 1988, 1992, 2000, 2024, 7932}
armsvc {1920, 1940, 1944, 7896}
audiodg {7436, 2192, 6784, 4540, 8040}
ccSvcHst {2584, 2644, 2656, 2400, 3080, 3120, 3124, 3128, 3132, 3136, 3140,...

当您把值设为 -1,当一行的剩余空间用完的时候截断。如果仍要显示所有值,请显式地使用 Format-Table-Wrap 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PS C:\> Get-Process | Select-Object -Property Name, Threads -First 6 | Format-Table -Wrap

Name Threads
---- -------
acrotray {3160}
AERTSr64 {1952, 1968, 1972, 8188}
AGSService {1980, 1988, 1992, 2000, 2024, 7932}
armsvc {1920, 1940, 1944, 7896}
audiodg {7436, 2192, 6784, 4540, 8040}
ccSvcHst {2584, 2644, 2656, 2400, 3080, 3120, 3124, 3128, 3132, 3136, 3140,
3144, 3232, 3240, 3248, 3260, 3268, 3288, 3304, 3344, 3492, 3552,
3556, 3568, 3572, 3576, 3580, 3596, 3600, 3604, 3612, 3616, 3708,
3712, 3716, 3724, 3732, 3736, 3760, 3764, 3768, 3776, 3780, 3796,
3800, 3804, 3816, 3820, 3824, 3828, 3832, 3844, 3888, 3892, 4232,
5084, 5088, 3112, 7100, 7016, 480, 3020, 3044, 4744, 7148, 1828,
6476, 6516, 6524, 7160, 6652, 7000, 964, 6028, 4644, 4828, 6664,
7892, 5820, 8180, 4940, 5956, 7684, 7156}



PS C:\>

PowerShell 技能连载 - 查找所有二级深度的文件

这是另一个文件系统的任务:列出一个文件夹结构中所有 *.log 文件,但最多只包含 2 级文件夹深度:

1
2
Get-ChildItem -Path c:\windows -Filter *.log -Recurse -Depth 2 -File -Force -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty FullName

幸运的是,PowerShell 5 对 Get-ChildItem 命令增加了好用的 -Depth 选项。

PowerShell 技能连载 - 删除所有指定层级下的子文件夹

以下是另一个听起来复杂,但实际上并没有那么复杂的文件系统任务。假设您需要移除某个文件系统指定层级之下所有文件夹。以下是实现方法:

1
2
3
4
5
6
7
8
# Task:

# remove all folders one level below the given path
Get-ChildItem -Path "c:\sample\*\" -Directory -Recurse |
# remove -WhatIf to actually delete
# ATTENTION: test thoroughly before doing this!
# you may want to add -Force to Remove-Item to forcefully delete
Remove-Item -Recurse -WhatIf

只需要在路径中使用 “*“。要删除某个指定路径下的两层之下的目录,相应地做以下调整:

1
2
3
4
5
6
7
8
# Task:

# remove all folders TWO levels below the given path
Get-ChildItem -Path "c:\sample\*\*\" -Directory -Recurse |
# remove -WhatIf to actually delete
# ATTENTION: test thoroughly before doing this!
# you may want to add -Force to Remove-Item to forcefully delete
Remove-Item -Recurse -WhatIf

PowerShell 技能连载 - 从目录结构中删除所有文件

有些任务听起来复杂,实际上并没有那么复杂。假设我们需要清除一个目录结构,移除所有文件,而留下空白的文件夹。我们进一步假设有一些文件位于白名单上,不应移除。如果用 PowerShell,那么实现起来很容易:

1
2
3
4
5
6
7
8
9
10
11
12
# Task:
# remove all files from a folder structure, but keep all folders,
# and keep all files on a whitelist

$Path = "c:\sample"
$WhiteList = "important.txt", "something.csv"

Get-ChildItem -Path $Path -File -Exclude $WhiteList -Recurse -Force |
# remove -WhatIf if you want to actually delete files
# ATTENTION: test thoroughly before doing this!
# you may want to add -Force to Remove-Item to forcefully remove files
Remove-Item -WhatIf

PowerShell 技能连载 - 在 PowerShell 标题栏显示 RSS 标题

一个新的 PowerShell 后台线程可以在后台工作,例如每 5 秒钟在 PowerShell 窗口标题栏上显示新的 RSS 供稿。

首先,我们先看看如何获取新闻供稿内容:

1
2
3
4
5
$RSSFeedUrl = 'https://www.technologyreview.com/stories.rss'
$xml = Invoke-RestMethod -Uri $RSSFeedUrl
$xml | ForEach-Object {
"{0} {1}" -f $_.title, $_.description
}

可以自由调整 RSS 公告的地址。如果修改了地址,请也注意调整 “title“ 和 “description“ 属性名。每个 RSS 供稿可以任意命名这些属性。

以下是向标题栏添加新内容的完整解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$Code = {
# receive the visible PowerShell window reference
param($UI)

# get RSS feed messages
$RSSFeedUrl = 'https://www.technologyreview.com/stories.rss'
$xml = Invoke-RestMethod -Uri $RSSFeedUrl

# show a random message every 5 seconds
do
{
$message = $xml | Get-Random
$UI.WindowTitle = "{0} {1}" -f $message.title, $message.description
Start-Sleep -Seconds 5
} while ($true)
}

# create a new PowerShell thread
$ps = [PowerShell]::Create()
# add the code, and a reference to the visible PowerShell window
$null = $ps.AddScript($Code).AddArgument($host.UI.RawUI)

# launch background thread
$null = $ps.BeginInvoke()