PowerShell代码备忘录

不定时更新。主要是些常用的one-liner。

时常有其他语言的代码作对照,经常是JavaScript(ES6+)。这样的话,文章似乎更有用些w

缘起

常用的PowerShell代码段写在本子上自己也不看,干脆写博客里还方便点儿。

环境

未特地指明的话:

操作系统Win10 1909,PowerShell版本($PSVersionTable.PSVersion.Major)是5,$PSCulture$PSUICulture均为ja-JP

若指明PowerShell版本,只代表在默认环境下不可用而改用指定版本,不代表指定版本以下就不好使。

正文

URL Encoding解码

Add-Type -AssemblyName System.Web
$s = 'https://ja.wikipedia.org/wiki/%E5%8D%8A%E8%A7%92%E3%82%AB%E3%83%8A'
[System.Web.HttpUtility]::UrlDecode($s)
# → https://ja.wikipedia.org/wiki/半角カナ

值得一提的是,[System.Uri]::UnescapeDataString()虽然也能做到类似的事,但它只是将percent-encoding处理好而已。现在常用的是application/x-www-form-urlencoded,半角空格会被处理成+号。就这个来说,UrlDecode能处理好而UnescapeDataString不行。

更新(2020/06/17)

JavaScript那边有decodeURIdecodeURIComponent这两个可以用,建议看一下encodeURI的文档(反正编码解码成对出现w)。

但是JavaScript这边加号还是不能直接解成空格。如果需要的话,解完之后替换一下。

字符实体引用解码

字符实体引用,我看了一下日文维基百科,上面是“文字参照(character reference)”。行吧,反正已经乱了,不管那么多。

Add-Type -AssemblyName System.Web
$s = '<div id="siteSub" class="noprint">出典: フリー百科事典『ウィキペディア(Wikipedia)』</div>'
[System.Web.HttpUtility]::HtmlDecode($s)
# → <div id="siteSub" class="noprint">出典: フリー百科事典『ウィキペディア(Wikipedia)』</div>

顺带一提,这种东西实际上分为两种,一种是所谓“数値文字参照”,如&#30334;);一种是所谓“文字実体参照”,如我们耳熟能详的&gt;>)。

更新(2020/06/17)

用JavaScript的话,可以用Web API的DOMParser试试看:

let s = '&lt;div id=&quot;siteSub&quot; class=&quot;noprint&quot;&gt;出典: フリー百科事典『ウィキペディア(Wikipedia)』&lt;/div&gt;';
let parser = new DOMParser();
parser.parseFromString(s, 'text/html').body.textContent;
// → "<div id=\"siteSub\" class=\"noprint\">出典: フリー百科事典『ウィキペディア(Wikipedia)』</div>"

Unicode码位获取

码位……说code point应该大家都知道吧。

[char]::ConvertToUtf32('百',0)
# → 30334,十进制表示
'0x{0:X}' -f [char]::ConvertToUtf32('百', 0)
# → 0x767E,十六进制表示

哪怕它超出了Unicode的BMP(基本多言語面,U+0000~U+FFFF)的范围也可以搞定。

'0x{0:X}' -f [char]::ConvertToUtf32('🥳', 0)
# → 0x1F973

PowerShell 7那边,由于采用的是dotNET Core 3,System.Text.Rune就可以用了:$PSVersionTable.PSVersion.ToString(): 7.0.0-preview.6

'0x{0:X}' -f [System.Text.Rune]::GetRuneAt('🥳' , 0).Value
# → 0x1F973
[System.Text.Rune]::new(0x1F973).ToString() -eq '🥳'
# → True

Base64编码与解码

$enc = [System.Text.Encoding]::UTF8

$s = 'お前だけ消費税100な'
[System.Convert]::ToBase64String($enc.GetBytes($s))
# → 44GK5YmN44Gg44GR5raI6LK756iOMTAw44Gq

$s = '44GK5YmN44Gg44GR5raI6LK756iOMTAw44Gq'
$enc.GetString([System.Convert]::FromBase64String($s))
# → お前だけ消費税100な

UTF8 ⇔ UTF7

$s = 'お前だけ消費税100な'
[System.Text.Encoding]::UTF8.GetString(
  [System.Text.Encoding]::UTF7.GetBytes($s))
# → +MEpSTTBgMFFtiIy7eg4-100+MGo-

$s = '+MEpSTTBgMFFtiIy7eg4-100+MGo-'
[System.Text.Encoding]::UTF7.GetString(
  [System.Text.Encoding]::UTF8.GetBytes($s))
# → お前だけ消費税100な

另一种办法:

$utf8 = [System.Text.Encoding]::UTF8
$utf7 = [System.Text.Encoding]::UTF7

$s = 'お前だけ消費税100な'
$utf8.GetString(
  [System.Text.Encoding]::Convert($utf8, $utf7, $utf8.GetBytes($s)))
# → +MEpSTTBgMFFtiIy7eg4-100+MGo-
$s = '+MEpSTTBgMFFtiIy7eg4-100+MGo-'
$utf7.GetString(
  [System.Text.Encoding]::Convert($utf7, $utf8, $utf7.GetBytes($s)))
# → お前だけ消費税100な

稍微清晰了一些,但更繁琐。

全角片假名转换为半角

文档:Strings.StrConv(String, VbStrConv, Int32) Method

Add-Type -AssemblyName Microsoft.VisualBasic
[regex]::Replace('ウィキペディア', '\p{IsKatakana}', {
  [Microsoft.VisualBasic.Strings]::StrConv($args.Groups[0].Value, [Microsoft.VisualBasic.VbStrConv]::Narrow, 0)})
# → ウィキペディア

竟然要的是Microsoft.VisualBasic,非常有意思。

其实那个正则表达式替换成\p{IsKatakana}+更好,它支持一并处理。

半角片假名转换为全角

 [regex]::Replace("ウィキペディア", "[\uFF61-\uFF9F]+", { $args.Groups[0].Value.Normalize(5) })
# → ウィキペディア

这个,就不只是有意思那么简单了。

  • "[\uFF61-\uFF9F]+":半角片假名的范围是U+FF61\~U+FF9F。这个范围属于Halfwidth and Fullwidth Forms,U+FF00\~U+FFEF。由于并没有自己独有的区块,只好这么做。
  • String.Normalize:用日语来说就是“Unicode正規化”。参见String.Normalize Method
  • .Normalize(5):里面那个5其实表示FormKC,参见NormalizationForm Enum

稍微直观一点的内容参见:Unicode正規化とは。例子挺多,好看。

至于说,为什么不能像上面那样对单个假名用[Microsoft.VisualBasic.VbStrConv]::Wide这样的办法处理,因为只对单个字符操作的话,这个Wide只会将浊点和半浊点转化为它的单独形式(U+309B和U+309C),无可厚非,但并不是我们想要的。

不过我们可以一并处理:

Add-Type -AssemblyName Microsoft.VisualBasic
[regex]::Replace("ウィキペディア", "[\uFF61-\uFF9F]+", {
  [Microsoft.VisualBasic.Strings]::StrConv($args.Groups[0].Value, [Microsoft.VisualBasic.VbStrConv]::Wide, 0) })
# → ウィキペディア

这样也是可以的。

全角 ⇔ 半角(P/Invoke kernel32.dll)

开启修罗模式!

由于代码量相对庞大,已经拆到这边了:P/Invoke kernel32.dll实现全角半角转换

还是这句话:没什么必要用www

用ANSI escape code打印8bit色盘

$sb = [System.Text.StringBuilder]::new()
foreach ($_ in 0..255) {
  $sb.Append("$([char]0x1B)[48;5;${_}m ") > $null }
Write-Host $sb

老漂亮了。

PowerShell 6及之后可以把$([char]0x1B)替换为`e,稍微简化了一些。

简单的首字母大写

(Get-Culture).TextInfo.ToTitleCase("burning force")
# → Burning Force

(Get-Culture)的返回值类型是[cultureinfo]。暂且按照PowerShell这边的方式写了。

但是如果说mcDonald这种情况,我们不想让中间的字母大小写发生变化:

[regex]::Replace("mcDonald", "\b(.)", { $args.Groups[1].Value.ToUpper() })
# → McDonald

使用了祖传的[regex]::Replace。这样才符合预期。

数组内元素转换并返回新数组

举个例子,比如说我们有这样一个数组:

$arr = [string[]]@('1', '2', '3')

元素全都是能转换成int的string。就是说我们可以用[int]::Parse搞定。但是要是一个一个来,未免太麻烦。

我们可以使用[array]::ConvertAll官方文档):

$result = [array]::ConvertAll(
  $arr,
  [System.Converter[string, int]]{
    Param($i)
    [int]::Parse($i) })

这样$result.GetType().Name就是Int32[],结果也没有问题。

这里涉及到了Converter这个delegate(官方文档)。至于说在PowerShell如何使用,请见这里

这里的$i类型就是System.String

之所以有这样的需求,是因为做了这样的题:Basic Math (Add or Subtract)

关于反射

虽然非常没必要,要是特别闲的话可以用反射的办法(System.Reflection.MethodBase.Invoke)。需要注意的是,System.Array.ConvertAll用到了范型:

$result = [array].GetMethod("ConvertAll").
  MakeGenericMethod(@([string], [int])).
  Invoke(
    $null,
    @([string[]]@('1', '2', '3'), [System.Converter[string, int]]{ [int]::Parse($args) }))

由于本例中ConvertAll是个静态方法,这个Invoke的第一个参数并没作用。详情看文档。

稍稍注意一下,$args$args[0]的类型不同,分别是System.Object[]System.String。看来可以自动拆开,先不管也罢,估计是笔烂账。要是用上次提到的Param($i)$i类型还是System.String

没用到范型的,比如System.Math.Abs,用System.Type.InvokeMember

$r = [System.Math].InvokeMember(
  "Abs",
   [System.Reflection.BindingFlags]::InvokeMethod,
   $null,
   $null,
   -3)

就是求绝对值嘛。$r结果是3

简体繁体转换

虽然.NET自带这样的功能,不过还是建议用其他的库。总之先试试:

Add-Type -AssemblyName Microsoft.VisualBasic
using namespace Microsoft.VisualBasic
[Strings]::StrConv("神龜雖壽", [VbStrConv]::SimplifiedChinese, 2052)
# → 神龟虽寿
[Strings]::StrConv("神龟虽寿", [VbStrConv]::TraditionalChinese, 2052)
# → 神龜雖壽

由于是5.1的默认终端,输入时显示会有问题,不过不影响结果。

另外,using namespace Microsoft.VisualBasic就如同C#那边的using那样,现在写[Strings]::StrConv就相当于写[Microsoft.VisualBasicStrings]::StrConv,这不多提。

但是吧,如果使用简写,补全功能就不好用了,心里没底,建议在写脚本时再用using namespace吧。不过这个问题在7.0.0-rc.1时修正了,可以正常补全,打到[Strings]::StrC再Tab倒是没问题。不过比如说,打到[Strings再按Tab,还是会补全成[Microsoft.VisualBasic.Strings,看着很长啊。

StrConv的第三个参数是int LocaleID = 0,Locale ID参见文档:2.1.1906 Part 4 Section 7.6.2.39, LCID (Locale ID)

至于说为什么简转繁或者反过来都写2052,也就是zh-CN那边,咱也不太清楚原因,写1028也就是zh-TW那边结果又不太对劲。

字符串反转

我寻思.NET那边不怎么擅长这个www

基于.NET的char,使用string

Add-Type -AssemblyName System.Linq.Enumerable
@([System.Linq.Enumerable]::Reverse("神龜雖壽")) -join ""
# → 壽雖龜神

当然这样不太好,毕竟一个char并不一定是我们所说的“一个字”。

基于string(以grapheme为单位),使用TextElementEnumerator

Add-Type -AssemblyName System.Linq.Enumerable
Add-Type -AssemblyName System.Globalization
[System.Linq.Enumerable]::Reverse([string[]]@([System.Globalization.StringInfo]::GetTextElementEnumerator("🎢右臂有个鸟🎢"))) -join ""
# → 🎢鸟个有臂右🎢

显得冗长,不过起码能用。

再就是,打字带空格那边也值得看,不过也就开头那点儿有用。

想要在反转之后的基础上插空格什么的,很自然地只需要改一下-join的内容就好。

附带index的数组遍历

最省心的办法还是用System.LinqEnumerable.Select,至少不会很“脏”:

$a = [string[]]@('a', 'b', 'c')
$f = [System.Func[string, int, [System.Tuple[string, int]]]] {
    Param([string]$item, [int]$index);
    return [System.Tuple]::Create($item, $index) }
# 分岐開始
# pwsh 7.0.1
foreach ($tmp in [System.Linq.Enumerable]::Select($a, $f)) {
    Write-Host "$($tmp[1]): $($tmp[0])" 
}
# pwsh 5.1
foreach ($tmp in [System.Linq.Enumerable]::Select($a, $f)) {
    Write-Host "$($tmp.Item2): $($tmp.Item1)"
}
# 分岐完了
# -> 0: a
# -> 1: b
# -> 2: c

稍微注意一下System.Tuple实例的用法。

采用了C#那边的常用做法。毕竟有这样的东西:Select<TSource,TResult>(IEnumerable<TSource>, Func<TSource,Int32,TResult>)

只是这个复杂程度着实会令人思考人生。在pwsh里强行直接使用Linq的话就是这样的w

更新(2020/06/17)

JavaScript那边可以:

let a = ['a', 'b', 'c'];
a.forEach((element, index) => console.log(`${index}: ${element}`));
# -> 0: a
# -> 1: b
# -> 2: c

毕竟有Array.prototype.forEach()。它的callback很有意思。

基于字节的字符串长度判断

实际上是偷懒的桁数(けたすう)判断。这边通常用Shift-JIS编码,该编码的字节定义见

function ketasuu([string]$str) {
  return [System.Text.Encoding]::GetEncoding('Shift-JIS').
    GetBytes($str).Length
}

用用看:

ketasuu('パワーシェル')
# -> 7
ketasuu('パワーシェル')
# -> 12
ketasuu('桁数')
# -> 4

要是接受管道传进来的参数会更方便:

function Get-Ketasuu {
  Param(
    [Parameter(ValueFromPipeline)]
    [string]$str
  )
  return [System.Text.Encoding]::GetEncoding('Shift-JIS').
    GetBytes($str).Length
}

试试:

'桁数' | Get-Ketasuu
# -> 4

这样是不是更好些。

啊,另外不得不提的是,这里所谓function其实可以定义beginprocessend三个语句块的,如果没有显式地用这三种包裹,那默认就是end哦。非常奇特是吧。

闭包计数器

这www

我也不知道怎么说,直接来吧:

$f = [System.Func[int, System.Func[int]]]{
  Param([int]$c)
  return [System.Func[int]]{
    $script:c += 1
    return $c }.GetNewClosure() }
$ff = $f.Invoke(0)
$ff.Invoke()
# -> 1
$ff.Invoke()
# -> 2
$ff.Invoke()
# -> 3
$ff = $f.Invoke(8)
$ff.Invoke()
# -> 9
$ff.Invoke()
# -> 10

以上是用delegate强行实现的结果。能用,但恐怕这办法不常见。

常见的是使用function的办法:

function New-Counter {
  Param([int]$c = 0)
  { $script:c += 1; return $c }.GetNewClosure() }
$ff = New-Counter
& $ff
# -> 1
& $ff
# -> 2
& $ff
# -> 3
$ff = New-Counter 8
& $ff
# -> 9
& $ff
# -> 10

相关博客:Closures in PowerShell

这样更灵活,只是调用方法要稍稍注意。

至于为什么闭包内改变值要用$script:c这样的形式而不是单纯的$c(相当于$local:c)呢……它要是用local scope的话,闭包建成之后,它就不会变化了;用script scope,它的变动会保存住,而且不会泄漏出去;用global scope……别ww

相关博客:PowerShell のスコープ完全に理解した

Scope方面的问题真是很麻烦,当心为好。

更新(2020/06/17)

JavaScript那边可以:

const f = (c) => {
  return () => {
    c += 1;
    return c;
  };
};
let ff = f(0);
ff();
# -> 1
ff();
# -> 2
ff();
# -> 3
ff = f(8);
ff();
# -> 9
ff();
# -> 10

至少看上去简洁明了。

更新(2020/06/20)

前面那个用delegate实现的东西并没有可选参数一说,毕竟是delegate,参数都是定死的。

想“overload”怎么办呢?拜托再写一个[System.Func[System.Func[int]]]类型的,再提前把$c设好。

要是说传个$null也可以接受的话,个人建议可以写个[System.Func[System.Nullable[int], System.Func[int]]]类型的,pwsh 7的话就$c = $a ?? 0(参见Null-coalescing operator ??),低版本的话就写一下判断。啊,当然,参数必须得传。(悲)

写一下:

$f = [System.Func[System.Nullable[int], System.Func[int]]]{
  Param([System.Nullable[int]]$a)
  $c = $a ?? 0
  return [System.Func[int]]{
    $script:c += 1
    return $c }.GetNewClosure() }
$ff = $f.Invoke($null)
$ff.Invoke()
# -> 1
$ff = $f.Invoke(8)
$ff.Invoke()
# -> 9

类计数器

PowerShell 5开始支持自己直接建类了,之后再用New-Object就可以实例化,眼前的路唰一下开阔了起来。

class Counter {
  [int] $Count = 0

  [int] Increment() {
    $this.Count += 1
    return $this.Count
  }
}
# 实例化
$TestCounter = New-Object Counter
$TestCounter.Increment()
# -> 1
$TestCounter.Increment()
# -> 2
$TestCounter.Increment()
# -> 3
$TestCounter.Count = 8
$TestCounter.Increment()
# -> 9
$TestCounter.Increment()
# -> 10

是不是比之前闭包的方法容易接受很多呢。

相关博客:超簡単な PowerShell Class の使い方(その1)

只是这边也有用PowerShell 4的时候,这就用不到这办法了……

创建指定大小文件

做文件上传时会用到,基本用来造例子。

$f = New-Object System.IO.FileStream D:\tmp\test.dat, Create, ReadWrite
$f.SetLength(2MB)
$f.Close()

参考:How to Generate File of a determinate Size in Windows?

pwsh自带2MB的简易记法,相当于2097152。这并不是附带单位的数量,单纯只是一种记法而已。

从NuGet拖包拿来用(单个dll)

并不是什么东西都在PowerShell Gallery里面包得好好的,有时候也要直接用NuGet的包。

以Nett为例。它可以用来处理TOML文件:

基本上还是要直接调dll。幸亏没那么多麻烦事,直接用Add-Type就好。

# 总之先安装包
# 本例中是当前用户安装:
Install-Package -Scope CurrentUser -Name Nett -Source nuget.org

# 找到对应dll的位置
$path = (Split-Path (Get-Package | Where-Object Name -eq Nett).Source -Parent) + '\lib\netstandard2.0\Nett.dll'
# 把它加进来
Add-Type -Path $path

# 试用一下:
# 将$PSVersionTable(它是一个Hashtable)转成TOML字符串看看
[Nett.Toml]::WriteString($PSVersionTable)
# -> Platform = "Win32NT"
# -> GitCommitId = "7.0.2"
# -> OS = "Microsoft Windows 10.0.19041"
# -> PSEdition = "Core"
# (后略)

其实还好。虽然并没那么合情合理,但由于这是单独的dll,直接调用也没关系。

如果情况复杂,建议用C#包好点儿给pwsh用吧。

后记

真好用,用就完事儿了。以后有需要再补充。

InSb

InSb

只是跟工作和生活相关的记录