PowerShell 技能连载 - 小心“Throw”语句(第 1 部分)

throw 是一个 PowerShell 语句,会抛出一个异常到调用者,并退出代码。至少理论上是这样。实际中,throw 可能不会退出代码,而且结果可能是毁灭性的。

要理解这个问题,请查看这个演示函数:

1
2
3
4
5
6
7
8
9
function Copy-Log
{
"Doing prerequisites"
"Testing whether target path exists"
"If target path does not exist, bail out"
throw "Target path does not exist"
"Copy log files to target path"
"Delete log files in original location"
}

当您运行 Copy-Log 时,它模拟了一个失败情况,假设一个目标路径不存在。当目标路径不存在时,不能复制日志文件。如果日志文件没有复制,那么不能删除它们。这是为什么调用 throw 时代码需要退出得原因。而且它确实有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out
Target path does not exist
In Zeile:8 Zeichen:3
+ throw "Target path does not exist"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Target path does not exist:String) [], RuntimeExceptio
n
+ FullyQualifiedErrorId : Target path does not exist

PS>

然而,这是基于 $ErrorActionPreference 为缺省值 “Continue” 时的行为。当一个用户恰好将它改为 “SilentlyContinue” 来禁止错误信息时,throw 会被彻底忽略,而且所有代码将会执行:

1
2
3
4
5
6
7
8
PS> $ErrorActionPreference = 'SilentlyContinue'

PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out
Copy log files to target path
Delete log files in original location

在这个场景中,您可能会丢失所有日志文件,因为复制操作没有生效,而代码继续执行并删除了原始文件。

重要的知识点:

  • 如果退出函数对您来说很重要,throw 可能并不会真正地退出函数。您可能需要用其他方法来退出代码,例如 return 语句。

PowerShell 技能连载 - 读取新闻订阅

以下是一个针对有德语技能的用户的特殊服务——对于其他人修改代码会有所挑战:以下代码使用了德国主要新闻杂志的 RSS 订阅,打开一个选择窗口。在窗口中您可以选择一篇或多篇文章,然后在缺省的浏览器中打开选择的文章:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# URL to RSS Feed
$url = 'http://www.spiegel.de/schlagzeilen/index.rss'


$xml = New-Object -TypeName XML
$xml.Load($url)

# the subproperties (rss.channel.item) depend on the RSS feed you use
# and may be named differently
$xml.rss.channel.item |
Select-Object -Property title, link |
Out-GridView -Title 'What would you like to read today?' -OutputMode Multiple |
ForEach-Object {
Start-Process $_.link
}

基本的设计过程是一致的:要将代码改为另一个 RSS 订阅,只需要导航到相应的属性(背后的 XML 的嵌套结构)。

PowerShell 技能连载 - 在资源管理器中启用预览 PowerShell 文件

当您在 Windows 的资源管理器中打开预览窗格查看 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
function Enable-PowerShellFilePreview
{
[CmdletBinding()]
param
(
[string]
$Font = 'Courier New',

[int]
$FontSize = 60
)

# set the font and size (also applies to Notepad)
$path = "HKCU:\Software\Microsoft\Notepad"
Set-ItemProperty -Path $path -Name lfFaceName -Value $Font
Set-ItemProperty -Path $path -Name iPointSize -Value $FontSize

# enable the preview of PowerShell files
$path = 'HKCU:\Software\Classes\.ps1'
$exists = Test-Path -Path $path
if (!$exists){
$null = New-Item -Path $Path
}
$path = 'HKCU:\Software\Classes\.psd1'
$exists = Test-Path -Path $path
if (!$exists){
$null = New-Item -Path $Path
}

$path = 'HKCU:\Software\Classes\.psm1'
$exists = Test-Path -Path $path
if (!$exists){
$null = New-Item -Path $Path
}


Get-Item HKCU:\Software\Classes\* -Include .ps1,.psm1,.psd1 | Set-ItemProperty -Name PerceivedType -Value text
}

运行这个函数后,使用这个命令:

1
PS> Enable-PowerShellFilePreview

如果您喜欢的话,还可以改变预览的字体系列和字号。请注意该设置和记事本共享:

1
PS> Enable-PowerShellFilePreview -Font Consolas -FontSize 100

不需要重启系统就可以生效。只需要确保 Windows 资源管理器的预览窗格可见,并选取一个 PowerShell 文件。

PowerShell 技能连载 - 移除空的数组元素(第 2 部分)

如果您想彻底移除空的数组元素(而不需要关心任何空属性),以下是一些性能根本不同的几种实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
# create huge array with empty elements
$array = 1,2,3,$null,5,0,3,1,$null,'',3,0,1
$array = $array * 1000

# "traditional" approach (6 sec)
Measure-Command {
$newArray2 = $array | Where-Object { ![string]::IsNullOrWhiteSpace($_) }
}

# smart approach (0.03 sec)
Measure-Command {
$newArray3 = foreach ($_ in $array) { if (![String]::IsNullOrWhiteSpace($_)){ $_} }
}

PowerShell 技能连载 - 移除空的数组元素(第 1 部分)

有些时候您会遇到包含空元素的列表(数组)。那么移除空元素的最佳方法是?

让我们首先关注一个普遍的场景:以下代码从注册表读取已安装的软件并创建一个软件清单。该软件清单将显示在一个网格视图窗口中,而很可能能看到包含空属性的元素:

1
2
3
4
5
6
7
8
9
$Paths = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'

$software = Get-ItemProperty -Path $paths -ErrorAction Ignore |
Select-Object -Property DisplayName, DisplayVersion, UninstallString

$software | Out-GridView

让我们忽略所有显示名称为空的元素:

1
2
# remove elements with empty DisplayName property
$software = $software | Where-Object { [string]::IsNullOrWhiteSpace($_.DisplayName)}

由于空属性既包含“真正”为空 ($null) 也包含空字符串 (''),您需要检查它们两者。更简单的方法是将它们隐式转换为 Boolean。然而,这样做仍然会移除数值 0:

1
2
# remove elements with empty DisplayName property
$software = $software | Where-Object { $_.DisplayName }

使用 PowerShell 3 引入的简化语法,您甚至可以这样写:

1
2
# remove elements with empty DisplayName property
$software = $software | Where-Object DisplayName

如果你想节省几毫秒,请使用 where 方法:

1
2
# remove elements with empty DisplayName property
$software = $software.Where{ $_.DisplayName }

如果您想处理一个大数组,用 foreach 循环更有效(效率提升 15 倍):

1
2
# remove elements with empty DisplayName property
$software = foreach ($_ in $software){ if($_.DisplayName) { $_ }}

PowerShell 技能连载 - “危险的”比较

假设您希望排除某个数组中所有为空字符串或者 null 元素。以下是许多人可能的做法:

1
2
3
4
5
6
7
8
PS> 1,2,$null,"test","",9 | Where-Object { $_ -ne '' -and $_ -ne $null }

1
2
test
9

PS>

然而,这个对比是危险的,因为它也排除了数值 0:

1
2
3
4
5
6
7
8
PS> 1,2,0,$null,"test","",0,9 | Where-Object { $_ -ne '' -and $_ -ne $null }

1
2
test
9

PS>

PowerShell 过滤掉了数值 0,因为它等同于一个空字符串:

1
2
3
4
5
PS> 0 -eq ''
True

PS> 1 -eq ''
False

这是因为在比较时,以等号左侧的数据类型为准,而由于左侧是一个 integer 值,所以 PowerShell 将空字符串也转换成一个 integer,而转换的结果值是 0。

为了安全地进行比较,请记住一定将相关的数据类型放在等号左侧,而不是右侧:

1
2
3
4
5
6
7
8
9
10
PS> 1,2,0,$null,"test","",0,9 | Where-Object { '' -ne $_ -and $null -ne $_ }

1
2
0
test
0
9

PS>

或者更好一点,使用 API 函数来确认空值:

1
2
3
4
5
6
7
8
9
10
PS> 1,2,0,$null,"test","",0,9 | Where-Object { ![string]::IsNullOrWhiteSpace($_) }

1
2
0
test
0
9

PS>

PowerShell 技能连载 - 计算一个月的第一天和最后一天

对于报表以及类似的场景,脚本可能需要获得指定月份的第一天和最后一天。第一天很简单,但最后一天依赖于月份和年份。以下是一个简单的计算器。只需要指定您需要的月份和年份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[ValidateRange(1,12)][int]$month = 3
$year = 2019
$last = [DateTime]::DaysInMonth($year, $month)
$first = Get-Date -Day 1 -Month $month -Year $year -Hour 0 -Minute 0 -Second 0
$last = Get-Date -Day $last -Month $month -Year $year -Hour 23 -Minute 59 -Second 59



PS> $first
3/1/2019 12:00:00 AM

PS> $last
3/31/2019 11:59:59 PM

PS>

PowerShell 技能连载 - 格式化 DateTime

当您拥有一个真正的 DateTime 对象(比如不是字符串)时,您就拥有了许多强大的格式化功能。您可以直接获取一个 DateTime 对象:

1
2
3
4
PS> $installDate = (Get-CimInstance -Class Win32_OperatingSystem).InstallDate

PS> $installDate.GetType().FullName
System.DateTime

或者您可以将一个字符串转换为一个 DateTime 对象:

1
2
3
4
PS> $psconf = Get-Date -Date '2019-06-04 09:00'

PS> $psconf.GetType().FullName
System.DateTime

当您拥有一个 DateTime 对象时,请使用 ToString() 方法并提供一个或两个参数。

第一个参数决定您希望使用日期的哪些部分,并使用这些占位符(大小写敏感!):

y       Year
M       Month
d       Day
H       Hour
m       Minute
s       Second
f       Millisecond

指定了越多占位符,就可以得到越多的细节:

1
2
3
4
5
6
7
8
9
10
PS> (Get-Date).ToString('dd')
30

PS> (Get-Date).ToString('ddd')
So

PS> (Get-Date).ToString('dddd')
Sonntag

PS>

(如您所见,PowerShell 使用的是缺省的语言,这个例子中使用的是德语)

要以 ISO 格式输出一个 DateTime,请使用这段代码:

1
2
3
4
5
6
PS> $installDate = (Get-CimInstance -Class Win32_OperatingSystem).InstallDate

PS> $installDate.ToString('yyyy-MM-dd HH:mm:ss')
2018-06-08 18:24:46

PS>

如果您也希望指定区域设置(语言),请在第二个参数指定 CultureInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> (Get-Date).ToString('dddd', [System.Globalization.CultureInfo]'en-us')
Sunday

PS> (Get-Date).ToString('dddd', [System.Globalization.CultureInfo]'zh')
星期日

PS> (Get-Date).ToString('dddd', [System.Globalization.CultureInfo]'es')
domingo

PS> (Get-Date).ToString('dddd', [System.Globalization.CultureInfo]'fr')
dimanche

PS>

如果您想了解某个区域设置的区域代码,请试试这段代码:

1
PS> [System.Globalization.CultureInfo]::GetCultures('Installed') | Out-GridView -PassThru

PowerShell 技能连载 - 解析 Windows 安装日期

是否关心过您的 Windows 已经安装了多久?一个单行的代码可以告诉您结果:

1
2
3
PS> (Get-CimInstance -Class Win32_OperatingSystem).InstallDate

Freitag, 8. Juni 2018 18:24:46

有两件事值得注意:第一,我们显然在使用德文的系统。第二,安装的日期可能比您想象的更近:每个新的 Windows 10 主版本更新实际上导致了一个完整的重新安装过程。

如果您希望改变 DateTime 输出的语言,只需要使用 ToString() 和一个 CultureInfo 对象:

1
2
3
4
PS> (Get-CimInstance -Class Win32_OperatingSystem).InstallDate.ToString([System.Globalization.CultureInfo]'en-us')
6/8/2018 6:24:46 PM

PS>

如果您想了解 Windows 安装了多少填,请使用 New-TimeSpan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PS> New-TimeSpan -Start (Get-CimInstance -Class Win32_OperatingSystem).InstallDate


Days : 204
Hours : 18
Minutes : 53
Seconds : 52
Milliseconds : 313
Ticks : 176936323133869
TotalDays : 204,787411034571
TotalHours : 4914,89786482969
TotalMinutes : 294893,871889782
TotalSeconds : 17693632,3133869
TotalMilliseconds : 17693632313,3869


PS> (New-TimeSpan -Start (Get-CimInstance -Class Win32_OperatingSystem).InstallDate).TotalDays
204,78764150864

PS> (New-TimeSpan -Start (Get-CimInstance -Class Win32_OperatingSystem).InstallDate).Days
204

PowerShell 技能连载 - 在文件管理器中隐藏 OneDrive

是否厌倦了 OneDrive 图表污染了您的文件管理器树形视图?如果您不使用 OneDrive,那么有两个很好用的函数可以在文件管理器里隐藏或显示 OneDrive 图标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Disable-OneDrive
{
$regkey1 = 'Registry::HKEY_CLASSES_ROOT\CLSID\{018D5C66-4533-4307-9B53-224DE2ED1FE6}'
$regkey2 = 'Registry::HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{018D5C66-4533-4307-9B53-224DE2ED1FE6}'
Set-ItemProperty -Path $regkey1, $regkey2 -Name System.IsPinnedToNameSpaceTree -Value 0
}


function Enable-OneDrive
{
$regkey1 = 'Registry::HKEY_CLASSES_ROOT\CLSID\{018D5C66-4533-4307-9B53-224DE2ED1FE6}'
$regkey2 = 'Registry::HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{018D5C66-4533-4307-9B53-224DE2ED1FE6}'
Set-ItemProperty -Path $regkey1, $regkey2 -Name System.IsPinnedToNameSpaceTree -Value 1
}