PowerShell 技能连载 - 将文本转为图像

WPF (Windows Presentation Foundation) 不仅仅是创建 UI 的技术。您可以用它来创建任意类型的矢量图并将它保存为图形文件。

以下是一个简单的例子,输入任意的文字和字体,然后将它渲染为一个 PNG 文件:

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
function Convert-TextToImage
{
param
(
[String]
[Parameter(Mandatory)]
$Text,

[String]
$Font = 'Consolas',

[ValidateRange(5,400)]
[Int]
$FontSize = 24,

[System.Windows.Media.Brush]
$Foreground = [System.Windows.Media.Brushes]::Black,

[System.Windows.Media.Brush]
$Background = [System.Windows.Media.Brushes]::White
)

$filename = "$env:temp\$(Get-Random).png"

# take a simple XAML template with some text
$xaml = @"
<TextBlock
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">$Text</TextBlock>
"@

Add-Type -AssemblyName PresentationFramework

# turn it into a UIElement
$reader = [XML.XMLReader]::Create([IO.StringReader]$XAML)
$result = [Windows.Markup.XAMLReader]::Load($reader)

# refine its properties
$result.FontFamily = $Font
$result.FontSize = $FontSize
$result.Foreground = $Foreground
$result.Background = $Background

# render it in memory to the desired size
$result.Measure([System.Windows.Size]::new([Double]::PositiveInfinity, [Double]::PositiveInfinity))
$result.Arrange([System.Windows.Rect]::new($result.DesiredSize))
$result.UpdateLayout()

# write it to a bitmap and save it as PNG
$render = [System.Windows.Media.Imaging.RenderTargetBitmap]::new($result.ActualWidth, $result.ActualHeight, 96, 96, [System.Windows.Media.PixelFormats]::Default)
$render.Render($result)
Start-Sleep -Seconds 1
$encoder = [System.Windows.Media.Imaging.PngBitmapEncoder]::new()
$encoder.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($render))
$filestream = [System.IO.FileStream]::new($filename, [System.IO.FileMode]::Create)
$encoder.Save($filestream)

# clean up
$reader.Close()
$reader.Dispose()

$filestream.Close()
$filestream.Dispose()

# return the file name for the generated image
$filename
}

以下是使用方法:

1
2
3
PS> $file = Convert-TextToImage -Text 'Red Alert!' -Font Stencil -FontSize 60 -Foreground Red -Background Gray

PS> Invoke-Item -Path $file

今日的知识点:

  • 通过 XAML,一个基于 XML 的 UI 描述语言,您可以定义图像。
  • PowerShell 可以使用 [Windows.Markup.XAMLReader] 类来快速地将任意合法的 XAML 转换为一个 UIElement 对象。
  • UIElement 对象可以保存成图形文件,例如 PNG 图像,可以在窗口中显示,或者打印出来。在这个例子中,我们主要是将它保存为文件,然后我们用了一个非常简单的 XAML 定义。您现在可能会感到很好奇。通过 Google 搜索一下这个例子中的方法,您将会找到很多知识。

PowerShell 技能连载 - 转换 IEEE754 (Float)(第 2 部分)

昨天我们研究了用 PowerShell 如何将传感器返回的 IEEE754 浮点值转换为实际值。这需要颠倒字节顺序并使用 BitConverter 类。

如果您收到了一个十六进制的 IEEE754 值,例如 0x3FA8FE3B,第一个任务就是将十六进制值分割为四个字节。实现这个目标简单得让人惊讶:将该十六进制值视为一个 IPv4 地址。这些地址内部也是使用四个字节。

以下是一个快速且简单的方法,能够将传感器的十六进制数值转换为一个有用的数值:

1
2
3
4
5
6
$hexInput = 0x3FA8FE3B

$bytes = ([Net.IPAddress]$hexInput).GetAddressBytes()
$numericValue = [BitConverter]::ToSingle($bytes, 0)

"Sensor: $numericValue"

今日的知识点:

  • 将数值转换为 IPAddress 对象来将数值分割为字节。这种方法也可以用来得到一个数字的最低有效位 (LSB) 或最高有效位 (MSB) 形式。

PowerShell 技能连载 - 转换 IEEE754 (Float)(第 1 部分)

PowerShell 是非常多功能的,并且现在常常用于 IoT 和传感器。一些 IEEE754 浮点格式往往是一系列四字节的十六进制数。

我们假设一个传感器以 IEEE754 格式返回一个十六进制的数值 3FA8FE3B。那么要如何获取真实值呢?

技术上,您需要颠倒字节顺序,然后用 BitConverter 来创建一个 “Single” 值。

以 3FA8FE3B 为例,将它逐对分割,颠倒顺序,然后转换为一个数字:

1
2
$bytes = 0x3B, 0xFE, 0xA8, 0x3F
[BitConverter]::ToSingle($bytes, 0)

实际结果是,十六进制数值 0x3FA8FE3B 返回了传感器值 1.320258。今天,我们研究 BitConverter 类,这个类提供了多个将字节数组转换为数值的方法。明天,我们将查看另一部分:将十六进制文本数值成对分割并颠倒顺序。

今日的知识点:

  • 使用 [BitConverter] 来将原始字节数组转换为其它数字格式。这个类有大量方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS> [BitConverter] | Get-Member -Static | Select-Object -ExpandProperty Name

DoubleToInt64Bits
Equals
GetBytes
Int64BitsToDouble
IsLittleEndian
ReferenceEquals
ToBoolean
ToChar
ToDouble
ToInt16
ToInt32
ToInt64
ToSingle
ToString
ToUInt16
ToUInt32
ToUInt64

要查看其中某个方法的语法,请键入它们的名称,不包括圆括号:

1
2
3
4
5
PS> [BitConverter]::ToUInt32

OverloadDefinitions
-------------------
static uint32 ToUInt32(byte[] value, int startIndex)

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

在前一个技能中我们解释了将 $ErrorActionPreference 设为 “SilentlyContinue” 将如何影响 throw 语句,而且该 throw 将不会正常地退出函数代码。以下还是我们使用过的例子:

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
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"
}



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 = '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

虽然 ErrorAction 为默认值时,throw 语句能够退出函数,但是当 ErrorAction 设为 “SilentlyContinue” 时代码将继续执行。这很可能是一个 bug,因为 ErrorAction 的值设为 “Continue” 和 “SilentlyContinue” 的唯一区别只应该是错误信息可见与否。这些设置不应该影响实际执行的代码。

这个 throw 中的 bug 只发生在 throw 抛出的终止错误没有被处理的时候。当您使用 try..catch 语句或者甚至添加一个简单的(完全为空的)trap 语句时,一切都正确了,throw 能够像预期地工作了:

1
2
3
4
5
6
7
# add a trap to fix
trap {}

$ErrorActionPreference = "SilentlyContinue"
Copy-Log
$ErrorActionPreference = "Continue"
Copy-Log

一旦添加了捕获语句,这段代码将会在 throw 语句处跳出,无论 $ErrorActionPreference 的设置为什么。您可以在 PowerShell 用户设置脚本中添加一个空白的捕获语句来防止这个 bug,或者重新考虑是否使用 throw 语句。

重要的知识点:

  • throw 是错误处理系统的一部分,通过 throw 抛出的异常需要用 try..catch 或者 trap 语句。如果异常没有捕获,throw 的工作方式可能和预期的不一致。
  • 因为存在这个问题,所以对于暴露给最终用户的函数,不要用 throw 来退出函数代码。对于最终用户,与其抛出一个(丑陋的)异常,还不如用 Write-WarningWrite-Host 抛出一条对人类友好的错误信息,然后用 return 语句友好地退出代码。
  • 如果您必须抛出一个异常,以便调用者能够在他们的错误处理器中捕获它,但也需要确保无论 $ErrorActionPreference 为何值都能可靠地退出代码,请使用 Write-Errorreturn 语句的组合:
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"
Write-Error "Target path does not exist"; return
"Copy log files to target path"
"Delete log files in original location"
}

由于 return 语句不受 $ErrorActionPreference 的影响,您的代码总是能够退出。让我们做个测试:

1
2
3
4
5
6
7
8
9
10
PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out
Copy-Log : Target path does not exist
In Zeile:1 Zeichen:1
+ Copy-Log
+ ~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Copy-Log

如果将 $ErrorActionPreference 设为 SilentlyContinue,错误信息和预期一致地被屏蔽了,但是代码确实退出了:

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

PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out

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>