PowerShell 技能连载 - Where-Object: 只是一个带管道的 IF 语句

Where-Object 是一个最常用的 PowerShell 命令,不过新手可能对它不太熟悉。对于熟悉 “SQL” 数据库查询语言的人可以像 SQL 中的 Where 从句一样使用它它是一个客户端的过滤器,能去除不需要的项目。以下这行代码将处理所有服务并只显示当前正在运行的服务:

1
Get-Service | Where-Object { $_.Status -eq "Running" }

要更好地理解 Where-Object 如何工作,实际上它只是一个对管道发生作用的 IF 语句。以上代码等同于这个:

1
2
3
4
Get-Service | ForEach-Object {
if ($_.Status -eq 'Running')
{ $_ }
}

或者,完全不用代码的传统实现方式:

1
2
3
4
5
6
7
8
$services = Get-Service
Foreach ($_ in $services)
{
if ($_.Status -eq 'Running')
{
$_
}
}

PowerShell 技能连载 - 对必选参数使用自定义提示

当您在 PowerShell 定义了必选参数,那么当用户没有传入这个参数时将会收到提示。如您所见,当您运行这段代码时,该提示只使用了参数的名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
param
(
[Parameter(Mandatory)]
[string]
$UserName

)

"You entered $Username"



UserName: tobi
You entered tobi

To get more descriptive prompts, you can use more explicit variable names:
要获得描述的更具体的提示,您需要使用更明确的变量名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
param
(
[Parameter(Mandatory)]
[string]
${Please provide a user name}

)

$username = ${Please provide a user name}
"You entered $Username"



Please provide a user name: tobi
You entered tobi

只需要在一个函数中使用 param() 块就可以将函数转为命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function New-CorporateUser
{
param
(
[Parameter(Mandatory)]
[string]
${Please provide a user name}
)

$username = ${Please provide a user name}
"You entered $Username"
}



PS C:\> New-CorporateUser
Cmdlet New-CorporateUser at command pipeline position 1
Supply values for the following parameters:
Please provide a user name: Tobi
You entered Tobi

它的副作用是参数名中含有空格和特殊字符,将使它无法通过命令行指定值,因为参数无法用双引号包起来:

1
PS C:\> New-CorporateUser -Please  provide a user name

PowerShell 技能连载 - 检查 Cmdlet 可用性和脚本兼容性(第 3 部分)

并不是所有的 PowerShell cmdlet 都随着 PowerShell 发行。许多 cmdlet 是随着第三方模块发布。当安装某些软件时会同时安装这些模块,或者需要使用特定的 Windows 版本。

在之前的部分中我们创建了一个函数,它能够获取某个脚本中的所有外部命令。只需要再做一些额外努力,这就可以变成一个有用的兼容性报告:来自某个模块的所有 cmdlet,或者随着 PowerShell 发行的所有以 “Microsoft.PowerShell” 开头的模块。任何其它模块都属于具体的 Windows 版本或第三方扩展。

检查这个函数:

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
function Get-ExternalCommand
{
param
(
[Parameter(Mandatory)][string]
$Path
)
function Get-ContainedCommand
{
param
(
[Parameter(Mandatory)][string]
$Path,

[string][ValidateSet('FunctionDefinition','Command')]
$ItemType
)

$Token = $Err = $null
$ast = [Management.Automation.Language.Parser]::ParseFile($Path, [ref] $Token, [ref] $Err)

$ast.FindAll({ $args[0].GetType().Name -eq "${ItemType}Ast" }, $true)

}


$functionNames = Get-ContainedCommand $Path -ItemType FunctionDefinition |
Select-Object -ExpandProperty Name

$commands = Get-ContainedCommand $Path -ItemType Command
$commands | Where-Object {
$commandName = $_.CommandElements[0].Extent.Text
$commandName -notin $functionNames
} |
ForEach-Object { $_.GetCommandName() } |
Sort-Object -Unique |
ForEach-Object {
$module = (Get-Command -name $_).Source
$builtIn = $module -like 'Microsoft.PowerShell.*';

[PSCustomObject]@{
Command = $_
BuiltIn = $builtIn
Module = $module
}
}
}

以下是根据一个 PowerShell 脚本生成的一个示例报告,它列出了所有外部的 cmdlet,以及它们是否是 PowerShell 的一部分或来自外部模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PS> Get-ExternalCommand -Path $Path


Command BuiltIn Module
------- ------- ------
ConvertFrom-StringData True Microsoft.PowerShell.Utility
Get-Acl True Microsoft.PowerShell.Security
Get-ItemProperty True Microsoft.PowerShell.Management
Get-Service True Microsoft.PowerShell.Management
Get-WmiObject True Microsoft.PowerShell.Management
New-Object True Microsoft.PowerShell.Utility
out-default True Microsoft.PowerShell.Core
Test-Path True Microsoft.PowerShell.Management
Where-Object True Microsoft.PowerShell.Core
write-host True Microsoft.PowerShell.Utility

PS>

PowerShell 技能连载 - 检查 Cmdlet 可用性和脚本兼容性(第 2 部分)

并不是所有的 PowerShell cmdlet 都随着 PowerShell 发行。许多 cmdlet 是随着第三方模块发布。当安装某些软件时会同时安装这些模块,或者需要使用特定的 Windows 版本。

在前一部分中我们处理了一个脚本并且读出这个脚本所使用的所有外部命令。我们用这个函数合并了找到的所有外部命令:

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
function Get-ExternalCommand
{
param
(
[Parameter(Mandatory)][string]
$Path
)
function Get-ContainedCommand
{
param
(
[Parameter(Mandatory)][string]
$Path,

[string][ValidateSet('FunctionDefinition','Command')]
$ItemType
)

$Token = $Err = $null
$ast = [Management.Automation.Language.Parser]::ParseFile($Path, [ref] $Token, [ref] $Err)

$ast.FindAll({ $args[0].GetType().Name -eq "${ItemType}Ast" }, $true)

}


$functionNames = Get-ContainedCommand $Path -ItemType FunctionDefinition |
Select-Object -ExpandProperty Name

$commands = Get-ContainedCommand $Path -ItemType Command
$commands | Where-Object {
$commandName = $_.CommandElements[0].Extent.Text
$commandName -notin $functionNames
} |
ForEach-Object { $_.GetCommandName() } |
Sort-Object -Unique
}

您可以向这个函数传入任何 PowerShell 脚本路径并且得到这个脚本使用的所有外部命令(只需要确保在 $path 中传入了一个脚本的合法路径):

1
2
3
4
5
6
7
8
9
10
11
PS C:\> Get-ExternalCommand -Path $Path
ConvertFrom-StringData
Get-Acl
Get-ItemProperty
Get-Service
Get-WmiObject
New-Object
out-default
Test-Path
Where-Object
write-host

PowerShell 技能连载 - 检查 Cmdlet 可用性和脚本兼容性(第 1 部分)

并不是所有的 PowerShell cmdlet 都随着 PowerShell 发行。许多 cmdlet 是随着第三方模块发布。当安装某些软件时会同时安装这些模块,或者需要使用特定的 Windows 版本。

要查看您的脚本的兼容性,在第一部分中我们先看看如何查找一个脚本实际使用哪些命令。以下是一个帮助函数,能够利用 PowerShell 内部的抽象语法树 (AST) 来检测命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Get-ContainedCommand
{
param
(
[Parameter(Mandatory)][string]
$Path,

[string][ValidateSet('FunctionDefinition','Command')]
$ItemType
)

$Token = $Err = $null
$ast = [Management.Automation.Language.Parser]::ParseFile($Path, [ref] $Token, [ref] $Err)

$ast.FindAll({ $args[0].GetType().Name -eq "${ItemType}Ast" }, $true)

Get-ContainedCommand 可以解析一个脚本中定义的函数,或是一个脚本中使用的命令。以下是获取某个脚本中所有定义的函数的代码:

1
2
3
4
5
6
$Path = "C:\scriptToPS1File\WithFunctionDefinitionsInIt.ps1"

$functionNames = Get-ContainedCommand $Path -ItemType FunctionDefinition |
Select-Object -ExpandProperty Name

$functionNames

以下是脚本内部使用的命令列表:

1
2
$commands = Get-ContainedCommand $Path -ItemType Command
$commands.Foreach{$_.CommandElements[0].Extent.Text}

要找出使用外部命令的地方,只需要从命令列表中减掉所有内部定义的函数,然后移除重复。以下将获取某个脚本用到的所有外部命令:

1
2
3
4
5
6
7
8
9
10
11
12
$Path = "C:\scriptToPS1File\WithFunctionDefinitionsInIt.ps1"

$functionNames = Get-ContainedCommand $Path -ItemType FunctionDefinition |
Select-Object -ExpandProperty Name

$commands = Get-ContainedCommand $Path -ItemType Command

$externalCommands = $commands | Where-Object {
$commandName = $_.CommandElements[0].Extent.Text
$commandName -notin $functionNames
} |
Sort-Object -Property { $_.GetCommandName() } -Unique

PowerShell 技能连载 - 从图片中创建彩色 ASCII 艺术

在之前的技能中我们介绍了如何读取任何图片或照片,并将它转换为一个黑白 ASCII 艺术。今天,我们将修改 Convert-ImageToAsciiArt 函数:它输入一个函数并将它转换为彩色的 ASCII 艺术!

像素的亮度值将被转换为合适的 ASCII 字符,并且像素的颜色值将应用到该字符。ASCII 艺术将写入 HTML 文件,因为 HTML 是表示彩色文字的最简单格式。

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

[Parameter(Mandatory)][String]
$OutputHtmlPath,

[ValidateRange(20,20000)]
[int]$MaxWidth=80
)

,

# character height:width ratio
[float]$ratio = 1.5

# load drawing functionality
Add-Type -AssemblyName System.Drawing

# characters from dark to light
$characters = '$#H&@*+;:-,. '.ToCharArray()
$c = $characters.count

# load image and get image size
$image = [Drawing.Image]::FromFile($ImagePath)
[int]$maxheight = $image.Height / ($image.Width / $maxwidth) / $ratio

# paint image on a bitmap with the desired size
$bitmap = new-object Drawing.Bitmap($image,$maxwidth,$maxheight)


# use a string builder to store the characters
[System.Text.StringBuilder]$sb = "<html><building style=&#39;font-family:""Consolas""&#39;>"


# take each pixel line...
for ([int]$y=0; $y -lt $bitmap.Height; $y++) {
# take each pixel column...
$null = $sb.Append("<nobr>")
for ([int]$x=0; $x -lt $bitmap.Width; $x++) {
# examine pixel
$color = $bitmap.GetPixel($x,$y)
$brightness = $color.GetBrightness()
# choose the character that best matches the
# pixel brightness
[int]$offset = [Math]::Floor($brightness*$c)
$ch = $characters[$offset]
if (-not $ch) { $ch = $characters[-1] }
$col = "#{0:x2}{1:x2}{2:x2}" -f $color.r, $color.g, $color.b
if ($ch -eq &#39; &#39;) { $ch = " "}
$null = $sb.Append( "<span style=""color:$col""; ""white-space: nowrap;"">$ch</span>")
}
# add a new line
$null = $sb.AppendLine("</nobr><br/>")
}

# close html document
$null = $sb.AppendLine("</building></html>")

# clean up and return string
$image.Dispose()

Set-Content -Path $OutputHtmlPath -Value $sb.ToString() -Encoding UTF8
}

还有以下是如何将一张图片转换为一个漂亮的 ASCII 艺术,并在浏览器VS显示,甚至在彩色打印机中打印出来:

1
2
3
4
5
$ImagePath = "C:\someInputPicture.jpg"
$OutPath = "$home\desktop\ASCIIArt.htm"

Convert-ImageToAsciiArt -ImagePath $ImagePath -OutputHtml $OutPath -MaxWidth 150
Invoke-Item -Path $OutPath

可以通过调整 -MaxWidth 来控制细节。如果增加了宽度,那么也必须调整字体大小并增加字符数。对于更小的字符,您可能需要调整这行:

1
[System.Text.StringBuilder]$sb = "<html><building style=&#39;font-family:""Consolas""&#39;>"

例如将它改为这行:

1
[System.Text.StringBuilder]$sb = "<html><building style=&#39;font-family:""Consolas"";font-size:4px&#39;>"

PowerShell 技能连载 - 通过校验位测试原始数据——实际案例

随着 PowerShell 进入物联网世界,有些时候需要处理二进制的传感器数据和古老的校验模型来确保数据的完整性。

这是一个如何用 PowerShell 处理传感器数据并且校验其正确性的真实案例。这里介绍的案例仅适用于特定的用例,但是这里介绍的技术原理对类似的传感器依然有效。

在这个例子中,PowerShell 收到一系列十六进制数据 ($data)。校验码是最后一个字节 (3A)。它的定义是数据报文中所有字节的和。在这个总和中用的是最低有效字节来表示校验位,而且要求比特为反码。听起来很奇怪但是有道理。通过这种方式计算出来的校验和永远只有一个字节。

以下是 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
$data = '00030028401D2E8D4022C0EE4022C0E64022C0E6418B4ACD419FE7B641A05F0E41A060D041A061C23F0A7CDA3A'

# checksum is last byte
$checksum = [Convert]::ToByte($data.Substring($data.Length-2,2), 16)

# remove checksum from data
$data = $data.Substring(0, $data.Length -2)

# sum up all bytes
$sum = $data -split '(?<=\G.{2})(?=.)' |
Foreach-Object {$c = 0}{$c+=[Convert]::ToByte($_,16)}{ $c }

# get the least significant byte
$lsb = ([Net.IPAddress]$sum).GetAddressBytes()[0]

# invert bits
$checksumReal = $lsb -bxor 0xFF

# compare
if ($checksum -ne $checksumReal)
{
throw "Checksum does not match"
}
else
{
Write-Warning "Checksum ok"
}

今日知识点:

  • [Convert]::ToByte($number, 16) 将十六进制字符串转换为数字。
  • 通过正则表达式,可以很容易地将一个字符流(例如十六进制对)转换为一对一对字符的列表,这样可以计算总和。
  • 通过将一个数字转换为 IPAddress,可以方便地操作不同的字节顺序 (LSB, MSB)。
  • -bxor 求比特位反码。

PowerShell 技能连载 - 求比特位反码

有些时候需要求一个数字的比特位反码。最常见的情况是属于某种自定义的算法或计算校验。这引出了一个通用的问题:最简单的实现方法是什么?

“求比特位反码”操作可以通过 -bnot 操作符实现,类似这样:

1
2
3
4
5
$number = 76
[Convert]::ToString($number,2)

$newnumber = -bnot $number
[Convert]::ToString($newnumber,2)

不过,结果会显示一个警告:

1001100
11111111111111111111111110110011

这个操作符总是针对 64 位的有符号数。一个更好的方法是使用 -bxor 操作符,并且根据需要颠倒的数据类型提供对应的比特位掩码。对于一个字节,比特位掩码是 0xFF,对于 Int32,比特位掩码是 0xFFFFFFFF。以下是一个求某个字节的比特位反码的示例。我们将一个字符串结果填充至 8 个字符,来确保前导零可见:

1
2
3
4
5
$number = 76
[Convert]::ToString([byte]$number,2).PadLeft(8, '0')

$newnumber = $number -bxor 0xFF
[Convert]::ToString($newnumber,2).PadLeft(8, '0')

结果是正确的:

01001100
10110011

今日知识点:

  • PowerShell 包含了许多二进制操作符,它们都以 -b... 开头。
  • 要求比特位反码,您可以使用 -bnot。如果只要反转某几位,请使用 -bxor 和自定义比特掩码。

PowerShell 技能连载 - 计算最高和最低有效字节

数字在内部是以字节的形式存储的。例如一个 Int32 数值使用 4 个字节。有些时候需要将数字分割为字节,例如要以 TODO least significiant 字节顺序计算校验和。

我们创建了一个快速的全览,您也可以将它当作一个基本的数字处理教程。它演示了数字如何对应到字节,并且如何计算最低有效字节 (LSB) 和最高有效字节 (MSB),以及其它:

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
function Show-Header([Parameter(ValueFromRemainingArguments)][string]$Text)
{
$Width=80
$padLeft = [int]($width / 2) + ($text.Length / 2)
''
$text.PadLeft($padLeft, "=").PadRight($Width, "=")
''
}


Show-Header Starting with this number:
$number = 26443007
$number

Show-Header Display the bits for this number:
$bits = [Convert]::ToString($number,2)
$bits

Show-Header Add missing leading zeroes:
# pad the string to its full bit range (32 bits)
$bitsAligned = $bits.PadLeft(32, '0')
$bitsAligned

Show-Header Display the four byte groups
$bitsAligned -split '(?<=\G.{8})(?=.)' -join '-'

Show-Header Get the bytes by conversion to IPAddress object:
$bytes = ([Net.IPAddress]$number).GetAddressBytes()
$bytes

Show-Header Display the bits for the IPAddress bytes:
$bitbytes = $bytes | ForEach-Object { [Convert]::ToString($_, 2).PadLeft(8,'0')}
$bitbytes -join '-'

Show-Header Show the Least Significant Byte LSB:
$bytes[0]

Show-Header Show LSB by turning the 8 bits to the right into a number to verify:
$bits = [Convert]::ToString($number, 2)
# take least significant bits
[Convert]::toByte($bits.Substring($bits.Length-8),2)

Show-Header Show the Most Significant Byte MSB:
$bytes[3]

如您所见,有许多方法可以实现。一个特别聪明的办法是将数字转换为一个 IPAddressIPAddress 对象有一个好用的 GetAddressBytes() 方法,可以将数字轻松地分割为字节。

结果看起来类似这样:

===========================Starting with this number:===========================

26443007

=======================Display the bits for this number:========================

1100100110111110011111111

===========================Add missing leading Zeroes:==========================

00000001100100110111110011111111

==========================Display the four byte groups==========================

00000001-10010011-01111100-11111111

================Get the bytes by conversion to IPAddress object:================

255
124
147
1

===================Display the bits for the IPAddress bytes:====================

11111111-01111100-10010011-00000001

======================Show the Least Significant Byte LSB:======================

255

======Show LSB by turning the 8 bits to the right into a number to verify:======

255

=======================Show the Most Significant Byte MSB:======================

1

PowerShell 技能连载 - 接受不带引号的参数

在前一个技能中,我们介绍了一个能够输入一个字符串并生成完美居中的标题的函数。以下是该函数和它的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Show-Header($Text)
{
$Width=80
$padLeft = [int]($width / 2) + ($text.Length / 2)
$text.PadLeft($padLeft, "=").PadRight($Width, "=")
}


PS> Show-Header Starting
====================================Starting====================================

PS> Show-Header "Processing Input Values"
=============================Processing Input Values============================

如您所见,这个函数工作起来很完美,但是如果该字符串包含空格或其它特殊字符,用户需要用双引号将它包起来。难道不能让这个函数接受没有双引号的字符串,并将所有的输入视为一个参数吗?

这是完全可能的,如下:

  • 这个参数定义了明确的数据类型,例如 [string],那么 PowerShell 就了解了应当如何处理您的参数。
  • 只能使用单个参数,这样 PowerShell 便知道了所有的输入项都需要转换为该参数。

以下是修改后的函数:

1
2
3
4
5
6
function Show-Header([Parameter(ValueFromRemainingArguments)][string]$Text)
{
$Width=80
$padLeft = [int]($width / 2) + ($text.Length / 2)
$text.PadLeft($padLeft, "=").PadRight($Width, "=")
}

魔力是通过 ValueFromRemainingArguments 属性产生的,用户可以简单地输入文字并切不需要使用双引号:

1
2
3
4
5
PS> Show-Header Starting
====================================Starting====================================

PS> Show-Header Processing Input Values
=============================Processing Input Values============================

然欧,有一点需要注意:任何特殊字符,例如圆括号和双引号仍会干扰解析。在这些情况下,您需要像之前那样将字符串用双引号包起来。

今日知识点:

  • ValueFromRemainingArguments 属性使 PowerShell 能够将所有未绑定(额外)的参数分配到该参数上。
  • 使用清晰的数据类型做为参数,这样 PowerShell 知道如何转换含糊的数据。例如,没有 [string] 数据类型的话,如果输入值包含空格,PowerShell 将会为创建一个字符串数组。