奇怪的 Powershell

Posted aswr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了奇怪的 Powershell相关的知识,希望对你有一定的参考价值。

本文用来收集一些使用 Powershell 的时候的一些奇奇怪怪的体验。

0x00 :: 生存还是毁灭?

在操作 集合 (比如 数组链表哈希映射表 等)的时候,有一种叫 算子 的概念。
在 Scala 里它本质上是一些对象的方法,一些语言里它被叫做「高级函数」。

这里主要需要先知道这俩:

  • map : 对 集合 里的每一个 元素 ,都指定一个计算的逻辑,该 算子 可以返回一个新的 集合 ,里面每个 元素 都是上一个集合每个 元素 经过该逻辑后的值。

    • 比如: {2,3,3} 这个集合,经过 map(你们每个都乘以3) 处理后,就能返回 {6,9,9} 这样的集合。
  • flatten : 把一个 集合 里,若有的 元素 也是 集合 ,那么该 算子 会返回一个打破一层里面集合的新集合。

    • 比如: {{1,2},7,{2,3,3},{2,{6,6,6},12},4} 这个集合,经过 flatten 处理后,返回的集合就是 {1,2,7,2,3,3,2,{6,6,6},12,4} 这样的。
  • flatmap : 其实, flatmap(xxx) 就等同于先 map(xxx) 然后 flatten

    • 比如:
      假如,集合乘法效果是这样:{1,2,3} * 2 => {1,2,3,1,2,3}
      那么, {{1,2},7,{{2,3,3}}} 经过 flatmap(每个元素给我乘2) 后,就等于先被变成 {{1,2,1,2},14,{{2,3,3},{2,3,3}}} 这样,再变成 {1,2,1,2,14,{2,3,3},{2,3,3}} 这样。

      这其实就好像,你有一个小破船,船上运者一些货物,有的货是普通货、有的货也是小破船,就叫小小破船好了。现在,小破船被鱼雷击中了,这除了导致里面的货物同样被颠簸了一下以外,那些小小破船都被颠簸侧漏了。小小破船上的货物就都漏了出来,从而它们不再在小小破船上了、而是漏到了小破船上,乱七八糟地、平铺地,摊在那里。
  • foreach : 在 Scala 里的话这个其实就是无返回(即返回 Unit 表示返回空)的 map 。一般是要用每个元素做同样一件有副作用的事。(比如打印)(这里只要信息不只通过返回传出 or 信息不只通过参数进入函数就都算有副作用)
上面的 flatmap 是前两者的组合而 foreach 可理解为 特殊的 map 那么上面的确只是说了俩 算子 ,没错。

而本部分要说的,就是 Powershell 的 foreach 似乎有着类似于 flatmap 的作用这件事了。

就是说,「里面的东西」,存在还是毁灭?要不要击破小小破船、以使其侧漏?

示例

先简单测试,再演示一个稍微复杂的例子进一步验证。

0.0

1,2,(1,2,3) | foreach { $_ * 3 }

应有输出:

3
6
1
2
3
1
2
3
1
2
3

可以看到其中的 (1,2,3) 被重复了三次而没有数值三倍

0.1

1,2,(1,2,3) | foreach { $_ * 3 } | foreach { $_ * 7 }

应有输出:

21
42
7
14
21
7
14
21
7
14
21

可以看到上一步的结果的 集合 里其实就已经是平坦的了,那个 (1,2,3) 在乘以三的时候还是一个 集合 ,但乘以三完了以后也一并被侧漏了开来,在乘以七的时候,就是漏出来的每个元素像前面两个元素一样都被作为外层 集合直属元素来对待了。

1.0

1,2,,(1,2,3) | foreach { $_ * 3 } | foreach { $_ * 7 }

这里给开头代码断下句:两个逗号 ,, 并不是一个整体,像 1,2 的逗号作用就像 1+2+ 一样是一个运算符,二元的,相当于一个两参数的函数,只不过,算式 1,2 的返回结果是个元素为 12 的数组;而 ,2 其实也可以类比着 +2 来理解,也是运算符,一元运算符,只不过 ,2 的返回还是个数组,一个单元素的数组。

所以,不难理解,这里的 ,(1,2,3) 才是一个整体,手动明确一下运算符的优先级的话,开头那块儿的 1,2,,(1,2,3) 就应该是这样写了: 1,2,(,(1,2,3))

它应有这样的输出:

21
42
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3
1
2
3

可以看见这回里面的 (1,2,3) 没有轻易侧漏。但真的是这样吗?

1,2,,(1,2,3) | foreach { $_ * 3 } | foreach { $_ * 7 } | foreach { $_ * 2 }

看看输出:

42
84
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6
2
4
6

1.1

可以看到,其实经过两层 flatten 后,里面的 数字元素们 都已经平铺到了外面,再到第三层的逻辑为 让所有元素乘以二 的转化操作的结果,就是都被作为数字来操作了。

就如同是,小破船上的小小破船,在颠簸后终于上完了厕所,然后永远地离开了这个它们原本所在的小破船。

多嘴一句,其实用 -join 也可以查看它前面的集合对象的情况:

(1,2,(4,5)|select)  -join \' x \' # ret: 1 x 2 x System.Object[]
(1,2,(4,5)|%{$_})  -join \' x \' # ret: 1 x 2 x 4 x 5

这个的返回已经是字符串了。不信试试这个:

((1,2,(4,5)|select)  -join \' x \').split(\'S\')

应有输出:

1 x 2 x
ystem.Object[]

可见那个 System.Object[] 也只是返回值的内容的一部分而已。

上面的 -join 我理解为糖:

  • 写法: $a -join \',\'
    等同于: [system.String]::Join(\',\', $a)

    参考自:
    https://qastack.cn/programming/7723584/how-do-i-convert-an-array-object-to-a-string-in-powershell

总结

可见,在 Powershell 里, foreach 其实和 flatmap 有类似的逻辑,或者说二者就是一样的。

其实它确切的名字是 ForEach-Object ,而 foreach 则是别名,而且它还有个更短的别名: %
(这部分的知识来源: https://www.jb51.net/article/115518.htm

而它为何会成为 flatmap ,或许就可以讲另一个故事了吧。我个人的猜测是这是 Powershell 上的一个特性(都这么久了应该不是BUG了吧),即在被返回时会自动拆一次。你可以试试 1,2,(1,2,3)1,2,,(1,2,3) 对比一下并看看后者的提示。

之所以同 flatmap 联系起来是因为看了这些:

  • https://stackoverflow.com/questions/10442333/what-is-the-powershell-equivalent-to-linqs-select
  • https://stackoverflow.com/questions/5241441/powershell-equivalent-of-linq-selectmany-method
  • https://insbex.jixun.moe/linq-in-pwsh
  • https://or1ko.hatenablog.com/entry/20100125/1264425759

里面有很不错的例子。第四个链接提到了 Map ,并指出它类似于 Linux 上的 Awk 。
不过并没提及 FlatMap 。不过,如果元素确保不会有 集合 ,那么 % 的表现的确等同于 map 算子了。

而之所以能把 flatmapforeach 联系起来,还是因为前两个链接里有的回答举的例子,特别是第一个链接里,直接问了这方面的事情。

0x01 :: 是..但也没有完全是....

跟别的语言的函数不太一样, Powershell 的函数似乎是可以有多个返回的。而 return 关键字只是标志函数在哪被打断而已。(这简直就是妥妥的命令式编程的理念嘛!)

现在还不一定全部返回完事

定义这么个函数:

function aax { 2;3;5;7 }

然后用于管道:

aax | % { $_ * 2 }

你会看到输出:

4
6
10
14

如果用 -join 的话:

(aax) -join \',,\' # ret: 2,,3,,5,,7

或许这就可以作为判断,多个返回会编程一个序列的依据。不过其实也不一定:

aax -join \',,\'

它的输出效果就和没有 -join \',,\' 是完全一样的,而我还没找到办法确定这到底是因为什么。。。。

返回的不完全是那个返回

这样定义一个函数:

function aaxx { 2;3;5;7;11,13 }

注意后面两个数之间是逗号。但其实它会返回啥呢?

aaxx -join \',,\'

这样的输出和只是执行 aaxx 效果完全一样。

(aaxx) -join \',,\' # ret: 2,,3,,5,,7,,11,,13

这则说明那个 11,13 在返回的时候被自动拆开了,在这里的效果变得像 11;13 一样了。

不过接下来的事或许能说明什么,也或许不能:

(aaxx) -is [array] # ret: True

而不加括号的话:

aaxx -is [array]

输出的效果和只是执行 aaxx 依然是完全一样的。。。

到底是什么呢?

定义好的函数在 Powershell 里似乎有着非常复杂的玩法。

下面用 aaxx 举例——这仨写法的输出效果是一样的:

  • aaxx
  • {aaxx}.Invoke()
  • {return aaxx}.Invoke()

甚至后面跟上 | %{$_*2} 也完全一样。

那么它们真的一样吗?一样,但也不完全一样。

function aaxx { 2;3;5;7;@(11,13) }
##########################
(aaxx) -is [array] # ret: True
{aaxx}.Invoke() -is [array] # ret: False
({aaxx}.Invoke()) -is [array] # ret: False
{aaxx}.Invoke().Count # ret: 6
(aaxx).Count # ret: 6
{aaxx}.Count # ret: 1
{aaxx} # out: aaxx
{return aaxx}.Invoke() -join \'``\' # ret: 2``3``5``7``11``13
{aaxx}.Invoke() -join \'``\' # ret: 2``3``5``7``11``13
(aaxx) -join \'``\' # ret: 2``3``5``7``11``13
这么 TM 多玩法?
先不管了。毁灭吧,赶紧的。

生存还是毁灭?

这样看来,上面说 %flatmap 其实也确实是不严谨的:

  • 对于 (1..10),(11..20) ,首先有:

    (1..10),(11..20) -join \';::;\' # ret: System.Object[];::;System.Object[]
  • 那么有:

    • 先给出如下定义:

      function pxn ($Num) { process { return $_ * $Num } }
  • 则有:

    ( (1..10),(11..20) | % { $_ * 2 } ) -join \',,\' # ret: 1,,2,,3,,4,,5,,6,,7,,8,,9,,10,,1,,2,,3,,4,,5,,6,,7,,8,,9,,10,,11,,12,,13,,14,,15,,16,,17,,18,,19,,20,,11,,12,,13,,14,,15,,16,,17,,18,,19,,20
    ( (1..10),(11..20) | pxn(2) ) -join \',,\' # ret: 1,,2,,3,,4,,5,,6,,7,,8,,9,,10,,1,,2,,3,,4,,5,,6,,7,,8,,9,,10,,11,,12,,13,,14,,15,,16,,17,,18,,19,,20,,11,,12,,13,,14,,15,,16,,17,,18,,19,,20

这说明, % 被称为 Map 其实也是没错的。
flatten 的效果,则应也的确是来自 Powershell 的返回所带来的效果了。就像,前面用 aaxx 试探的那样。

这里用 -join 只是对显示格式化。如果对 -join 感到疑惑,你也可以自己执行以下下面几个,看看输出。

  • (1..10),(11..20) | % { $_ * 2 }
  • (1..10),(11..20) | pxn(2)
  • (1..10),(11..20) | % { $_ * 2 } | % { $_ * 3 }
  • (1..10),(11..20) | pxn(2) | pxn(3)

这几个应该能够证明,可读管道的命名函数和 % { ... } 结构的效果是完全一样的。

函数怎么读管道可以参考这个链接:
https://www.pstips.net/powershell-func-filters-pipeline.html
—— 而且,你还会发现,这管道函数的设计,其实跟 Awk 非常像的。

以上是关于奇怪的 Powershell的主要内容,如果未能解决你的问题,请参考以下文章

将 Excel 工作表导出到 PDF 文件时出现奇怪的 Powershell 行为

Powershell编码原因导致部分代码不执行

Powershell编码原因导致部分代码不执行

Powershell编码原因导致部分代码不执行

Powershell编码原因导致部分代码不执行

奇怪的 Powershell