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()

PowerShell 技能连载 - Invoke-WebRequest vs. Invoke-RestMethod

Invoke-WebRequest 只是简单地从任意的网站下载内容。接下来读取内容并分析它,是您的工作。在前一个技能中我们解释了如何下载 PowerShell 代码,并且执行它:

1
2
3
4
5
$url = "http://bit.ly/e0Mw9w"
$page = Invoke-WebRequest -Uri $url
$code = $page.Content
$sb = [ScriptBlock]::Create($code)
& $sb

(请注意:请在 PowerShell 控制台中运行以上代码。如果在 PowerShell ISE 等编辑器中运行,杀毒引擎将会阻止运行)

类似地,如果数据是以 XML 格式返回的,您可以将它转换为 XML。并且做进一步处理,例如从银行获取当前的汇率:

1
2
3
4
$url = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
$page = Invoke-WebRequest -Uri $url
$data = $page.Content -as [xml]
$data.Envelope.Cube.Cube.Cube

作为对比,Invoke-RestMethod 不仅仅下载数据,而且遵循数据的类型。您会自动得到正确的数据。以下是上述操作的简化版本,用的是 Invoke-RestMethod 来代替 Invoke-WebRequest

调用 Rick-Ascii(从 PowerShell 控制台运行):

1
2
3
4
$url = "http://bit.ly/e0Mw9w"
$code = Invoke-RestMethod -Uri $url
$sb = [ScriptBlock]::Create($code)
& $sb

获取货币汇率:

1
2
3
$url = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
$data = Invoke-RestMethod -Uri $url
$data.Envelope.Cube.Cube.Cube

PowerShell 技能连载 - 下载 PowerShell 代码

在之前的技能中我们介绍了如何用 Invoke-WebRequest 下载任意网页的 HTML 内容。这也可以用来下载 PowerShell 代码。Invoke-WebRequest 可以下载任意 WEB 服务器提供的内容,所以以下示例代码可以下载一段 PowerShell 脚本:

1
2
3
4
$url = "http://bit.ly/e0Mw9w"
$page = Invoke-WebRequest -Uri $url
$code = $page.Content
$code | Out-GridView

如果您信任这段代码,您可以方便地下载和运行它:

1
Invoke-Expression -Command $code

这段代码适用于 PowerShell 控制台,您将见到一个“跳舞的 Rick Ascii 艺术”并听到有趣的音乐,如果在另一个编辑器中运行以上代码,您的 AV 可能会阻止该调用并且将它识别为一个严重的威胁。这是因为下载的代码会检测它运行的环境,由于它需要在控制台中运行,所以如果在其它地方运行它,将会启动一个 PowerShell 控制台。这个操作是由杀毒引擎控制的,随后会被系统阻止。

PowerShell 技能连载 - 隐藏进度条

有些时候,cmdlet 自动显示一个进度条,以下是一个显示进度条的例子:

1
2
$url = "http://www.powertheshell.com/reference/wmireference/root/cimv2/"
$page = Invoke-WebRequest -URI $url

Invoke-WebRequest 获取一个 WEB 页面的原始内容,如果获取内容消耗一定的时间,将会显示进度条。

Whenever you run a cmdlet that shows a progress bar, you can hide the progress bar by temporarily changing the $ProgressPreference variable. Just make sure you restore its original value or else, you permanently hide progress bars for the current PowerShell session:
当您运行一个会显示进度条的 cmdlet,您可以通过临时改变 $ProgressPreference 变量来隐藏进度条。只需要记得恢复它的初始值或是设置成其它值,这样就可以针对当前 PowerShell 会话隐藏进度条。

1
2
3
4
5
6
7
$old = $ProgressPreference
$ProgressPreference = "SilentlyContinue"

$url = "http://www.powertheshell.com/reference/wmireference/root/cimv2/"
$page = Invoke-WebRequest -URI $url

$ProgressPreference = $old

除了保存和恢复原始的变量内容,您还可以使用脚本块作用域:

1
2
3
4
5
6
7
& {
$ProgressPreference = "SilentlyContinue"
$url = "http://www.powertheshell.com/reference/wmireference/root/cimv2/"
$page = Invoke-WebRequest -URI $url
}

$ProgressPreference

如您所见,当脚本块执行完之后,原始变量自动恢复原始值。