PowerShell 陷阱
StackOverflow 上反复出现了一些陷阱。如果您在asking 一个新问题之前不熟悉这些 PowerShell 陷阱,建议您进行一些研究。在answering PowerShell 问题之前调查这些 PowerShell 陷阱甚至可能是一个好主意,以确保您教提问者正确的事情。
TLDR: 在 PowerShell 中:
- 比较相等运算符为:
-eq
(Stackoverflow 示例:Powershell simple syntax if condition not working)
- 括号和逗号不与参数一起使用
(Stackoverflow 示例:How do I pass multiple parameters into a function in PowerShell?)
- 输出属性基于管道中的第一个对象
(Stackoverflow 示例:Not all properties displayed)
- 管道展开
(Stackoverflow 示例:Pipe complete array-objects instead of array items one at a time?)
一种。单个项目集合
(Stackoverflow 示例:Powershell ArrayList turns a single array item back into a string)
湾。嵌入式数组
(Stackoverflow 示例:Return Multidimensional Array From Function)
C。输出集合
(Stackoverflow 示例:Why does PowerShell flatten arrays automatically?)
-
$Null 应该在相等比较运算符的左侧
(Stackoverflow 示例:Should $null be on the left side of the equality comparison)
- 括号和赋值阻塞了管道
(Stackoverflow 示例:Importing 16MB CSV Into Variable Creates >600MB's Memory Usage)
- 增加赋值运算符 (
+=) 可能会变得昂贵
Stackoverflow 示例:Improve the efficiency of my PowerShell scrip
-
Get-Content cmdlet 返回单独的行
Stackoverflow 示例:Multiline regex to match config block
示例和说明
有些陷阱可能真的让人觉得反直觉,但通常可以用一些非常好的 PowerShell 功能以及pipeline、expression/argument mode 和type casting 来解释。
1。比较相等运算符为:-eq
与 Microsoft 脚本语言 VBScript 和其他一些编程语言不同,comparison equality operator 不同于 assignment operator (=) 并且是:-eq强>。
注意:如果需要,为变量赋值可能会通过该值:
$a = $b = 3 # The value 3 is assigned to both variables $a and $b.
这意味着以下语句可能意外地是truthy or falsy:
If ($a = $b) {
# (assigns $b to $a and) returns a truthy if $b is e.g. 3
} else {
# (assigns $b to $a and) returns a falsy if $b is e.g. 0
}
2。括号和逗号不与参数一起使用
与许多其他编程语言以及原始 PowerShell 函数的定义方式不同,调用函数不需要括号或逗号作为其相关参数。使用 空格 分隔参数参数:
MyFunction($Param1, $Param2 $Param3) {
# ...
}
MyFunction 'one' 'two' 'three' # assigns 'one' to $Param1, 'two' to $Param2, 'three' to $Param3
- 括号和逗号用于调用 (.Net) 方法。
- 逗号用于定义数组。
MyFunction 'one', 'two', 'three'(或MyFunction('one', 'two', 'three'))会将数组@('one', 'two', 'three')加载到第一个参数($Param1)中。
- 圆括号会将包含的内容解释为单个集合到内存中(并阻塞 PowerShell 管道),并且只能这样使用,例如调用嵌入函数,例如:
MyFunction (MyOtherFunction) # passes the results MyOtherFunction to the first positional parameter of MyFunction ($Param1)
MyFunction One $Two (getThree) # assigns 'One' to $Param1, $Two to $Param2, the results of getThree to $Param3
注意:仅当文本参数包含空格或特殊字符时才需要引用文本参数(如后面示例中的单词 one)。
3。输出属性基于管道中的第一个对象
在 PowerShell 管道中,每个对象都由cmdlet(即implemented for the middle of a pipeline)处理和传递,类似于装配线上的工作站处理和传递对象的方式。这意味着每个 cmdlet 一次处理一项,而前一个 cmdlet(工作站)同时处理即将到来的一项。这样,对象不会立即加载到内存中(更少的内存使用),并且可以在提供下一个对象(甚至存在)之前已经处理。此功能的缺点是没有监督 (or how many) 对象应该遵循的内容。
因此,大多数 PowerShell cmdlet 假定管道中的所有对象都对应于第一个对象并具有通常情况下相同的属性,但并非总是...
$List =
[pscustomobject]@{ one = 'a1'; two = 'a2' },
[pscustomobject]@{ one = 'b1'; two = 'b2'; three = 'b3' }
$List |Select-Object *
one two
--- ---
a1 a2
b1 b2
如您所见,结果中缺少第三列 three,因为它在第一个对象中不存在,并且 PowerShell 在知道第二个对象存在之前已经在输出结果。
解决此问题的方法是在正手显式定义(以下所有对象的)属性:
$List |Select-Object one, two, three
one two three
--- --- -----
a1 a2
b1 b2 b3
另见提案:#13906 Add -UnifyProperties parameter to Select-Object
4。管道展开
如果符合直接的期望,此功能可能会派上用场:
$Array = 'one', 'two', 'three'
$Array.Length
3
一个。单品合集
但它可能会让人困惑:
$Selection = $Array |Select-Object -First 2
$Selection.Length
2
$Selection[0]
one
当集合减少到单个项目时:
$Selection = $Array |Select-Object -First 1
$Selection.Length
3
$Selection[0]
o
说明
当管道输出分配给变量的单个项目时,它不分配为集合(有 1 个项目,例如:@('one'))而是作为标量项目(项目本身,例如:'one')。
这意味着属性.Length(实际上是数组属性.Count 的别名)不再应用于数组,而是应用于字符串:'one'.length,等于3。如果是索引$Selection[0],则返回字符串'one'[0](等于字符o)的第一个字符。
解决方法
要解决此问题,您可以使用 Array subexpression operator @( ) 将标量项强制为数组:
$Selection = $Array |Select-Object -First 1
@($Selection).Length
1
@($Selection)[0]
one
知道$Selection已经是一个数组的情况下,就不会再进一步增加深度了(@(@('one', 'two')),见下一节 4b. 嵌入式集合被展平)。
b.嵌入式数组
当数组(或集合)包含嵌入式数组时,例如:
$Array = @(@('a', 'b'), @('c', 'd'))
$Array.Count
2
所有嵌入的项目都将在管道中进行处理,并因此在显示或分配给新变量时返回一个平面数组:
$Processed = $Array |ForEach-Object { $_ }
$Processed.Count
4
$Processed
a
b
c
d
要迭代嵌入式数组,您可以使用foreach statement:
foreach ($Item in $Array) { $Item.Count }
2
2
或者简单的for loop:
for ($i = 0; $i -lt $Array.Count; $i++) { $Array[$i].Count }
2
2
c。输出集合
集合通常在放置在管道上时展开:
function GetList {
[Collections.Generic.List[String]]@('a', 'b')
}
(GetList).GetType().Name
Object[]
要将集合作为单个项目输出,请使用comma operator ,:
function GetList {
,[Collections.Generic.List[String]]@('a', 'b')
}
(GetList).GetType().Name
List`1
5。 $Null 应该在等式比较运算符的左侧
此问题与 comparison operators 功能相关:
When the input of an operator is a scalar value, the operator returns a Boolean value. When the input is a collection, the operator returns the elements of the collection that match the right-hand value of the expression. If there are no matches in the collection, comparison operators return an empty array.
这意味着对于标量:
'a' -eq 'a' # returns $True
'a' -eq 'b' # returns $False
'a' -eq $Null # returns $False
$Null -eq $Null # returns $True
对于集合,返回匹配的元素,其计算结果为 truthy or falsy 条件:
'a', 'b', 'c' -eq 'a' # returns 'a' (truthy)
'a', 'b', 'c' -eq 'd' # returns an empty array (falsy)
'a', 'b', 'c' -eq $Null # returns an empty array (falsy)
'a', $Null, 'c' -eq $Null # returns $Null (falsy)
'a', $Null, $Null -eq $Null # returns @($Null, $Null) (truthy!!!)
$Null, $Null, $Null -eq $Null # returns @($Null, $Null, $Null) (truthy!!!)
换句话说,要检查变量是否为$Null(并排除包含多个$Nulls的集合),将$Null放在相等比较运算符的LHS(左侧):
if ($Null -eq $MyVariable) { ...
6。括号和赋值阻塞了管道
PowerShell Pipeline 不只是由管道操作员 (|) (ASCII 124) 连接的一系列命令。这是一个通过cmdlets 序列同时流单个对象的概念。如果根据Strongly Encouraged Development Guidelines 和implemented for the middle of a pipeline 编写 cmdlet(或函数),它会从管道中获取每个对象,对其进行处理并将结果传递给下一个 cmdlet,然后再获取并处理管道中的下一个对象管道。这意味着对于一个简单的管道:
Import-Csv .\Input.csv |Select-Object -Property Column1, Column2 |Export-Csv .\Output.csv
当最后一个 cmdlet 将对象写入 .\Output.csv 文件时,Select-Object cmdlet 选择下一个对象的属性,Import-Csv 从 .\input.csv 文件中读取下一个对象(另请参见:@987654358 @)。这将使内存使用率保持在较低水平(尤其是在需要处理大量对象/记录的情况下),因此可能会导致更快的吞吐量。为了方便管道,PowerShell 对象非常胖,因为每个单独的对象都包含所有属性信息(以及例如属性名称)。
因此,无缘无故地阻塞管道不是一个好习惯。有两种情况会阻塞管道:
-
括号,例如:
(Import-Csv .\Input.csv) |Select-Object -Property Column1, Column2 |Export-Csv .\Output.csv
所有.\Input.csv 记录在将其传递给Select-Object cmdlet 之前作为PowerShell 对象数组加载到内存中。
-
作业,例如:
$Objects = Import-Csv .\Input.csv
$Objects |Select-Object -Property Column1, Column2 |Export-Csv .\Output.csv
所有.\Input.csv 记录在将其传递给Select-Object cmdlet 之前作为PowerShell 对象数组加载到$Objects(也包括内存)中。
7。增加赋值运算符 (+=) 可能会变得昂贵
increase assignment operator (+=) 是 syntactic sugar 增加和分配 primitives 为 .e.g. $a += $b 其中$a 被分配$b + 1。增加赋值运算符也可用于向集合(或String 类型和hash tables)添加新项目,但随着每次迭代(集合的大小)成本增加,可能会变得很漂亮expensive。这样做的原因是,作为数组集合的对象是不可变的,并且右侧变量不仅附加,而且 *附加并重新分配到左侧变量。详情另见:avoid using the increase assignment operator (+=) to create a collection
8。 Get-Content cmdlet 返回单独的行
可能还有更多 cmdlet 陷阱,因为知道存在很多(内部和外部)cmdlet。与引擎相关的陷阱相比,这些陷阱通常更容易突出显示(例如警告),就像发生在 ConvertTo-Json (参见:Unexpected ConvertTo-Json results? Answer: it has a default -Depth of 2)或“修复”中一样。但是Get-Content 中有一个非常经典的问题,它与 PowerShell 流对象的一般概念(在本例中为行)紧密结合,而不是一次性传递所有内容(文件的全部内容):
Get-Content .\Input.txt -Match '\r?\n.*Test.*\r?\n'
永远不会工作,因为默认情况下,Get-Contents 返回一个对象流,其中每个对象都包含一个字符串(没有任何换行符的行)。
(Get-Content .\Input.txt).GetType().Name
Object[]
(Get-Content .\Input.txt)[0].GetType().Name
String
事实上:
Get-Content .\Input.txt -Match 'Test'
返回包含单词Test 的所有行,因为Get-Contents 将每一行都放在管道上,when the input is a collection, the operator returns the elements of the collection that match the right-hand value of the expression。
注意: 由于 PowerShell 版本 3,Get-Contents 有一个 -Raw 参数,可以一次读取相关文件的所有内容,这意味着:Get-Content -Raw .\Input.txt -Match '\r?\n.*Test.*\r?\n' 将在加载时工作整个文件到内存中。