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)

更新(2021/07/11)

pwsh的array带了ForEach(type convertToType)官方文档)这个方法。试试看:

$result = [int[]] $arr.ForEach([int])

结果同上。

也有需要注意的事。ForEach(type convertToType)本身的返回值类型是Collection`1,为了以后方便,需要cast成int[]

关于反射

虽然非常没必要,要是特别闲的话可以用反射的办法(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很有意思。

更新(2020/10/14)

回过头来想,其实没必要那么执着:这恰好是for的用武之地。

至于说scope和变量冲突的问题,可以用scriptblockfor循环包起来:

$a = [string[]]@('a', 'b', 'c')
 { for ($idx = 0; $idx -lt $a.Count; $idx++) {
     "{0}: {1}" -f $idx, $a[$idx]
   }
 }.Invoke()
# -> 0: a
# -> 1: b
# -> 2: c

这个时候,内部变量idx不会影响到外面去,毕竟在此之后这个scope已经没了w

有点IIFE的意思。可以这么想。

至于array的长度,除了$a.Count,其实$a.Length也可以用,如果熟悉JS之类的会很亲切吧。

更新(2021/01/20)

不喜欢用.Invoke()也没关系,可以用&执行:

$a = [string[]]@('a', 'b', 'c')
& {
  for ($idx = 0; $idx -lt $a.Count; $idx++) {
    "{0}: {1}" -f $idx, $a[$idx]
  }
}
<# ->
0: a
1: b
2: c
#>

不同于.(dot source),&在这种情况下不会把scriptblock内部的变量带出去,就是说在外面取$idx的话,如果外面没有事先定义,是取不到的。这样或许更符合我们平常的思路,也容易驾驭,不过还请具体问题具体分析。

另外,我们有ref可以用:

$a = [string[]]@('a', 'b', 'c')
$idx = [ref] 0
& {
  for ($idx.Value = 0; $idx.Value -lt $a.Count; $idx.Value++) {
    "{0}: {1}" -f $idx.Value, $a[$idx.Value]
  }
}
<# ->
0: a
1: b
2: c
#>
$idx.Value
# -> 3

并无必要,但请万千留意。

更新(2021/07/11)

不如试一下scriptblockbeginprocess看看:

$a = [string[]] @('a', 'b', 'c')
$a |
& {
  begin {
    $idx = 0
  }
  process {
    "${idx}: $_"
    $idx++
  }
}
<# ->
0: a
1: b
2: c
#>

要是习惯了的话,或许这才是最方便的做法w

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

实际上是偷懒的桁数(けたすう)判断。这边通常用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用吧。

试用LINQ的SelectMany

这挺多的。已经移到在PowerShell里使用LINQ那边去了。

取多行文本的某列文字再连成一行

怎么说,就,鸡猪牛鱼,肉肉肉肉。

<#
剪贴板内:
鸡肉
猪肉
牛肉
鱼肉
#>
(Get-Clipboard).ForEach{
  [System.Globalization.StringInfo]::GetNextTextElement($_)
} -join ''
# -> 鸡猪牛鱼

(Get-Clipboard).ForEach{
  [System.Globalization.StringInfo]::new($_).SubstringByTextElements(1, 1)
} -join ''
# -> 肉肉肉肉

以上是现在找到的相对方便的办法。

果然要写的太多了,看这边吧:

PascalCase转UNDERSCORE_CASE

经常遇到class名和数据库表的物理名基本一致,只是表示形式不同。有时直接用数据库工具查看定义更方便,转换成表的物理名自然更方便查询之类:

$hoge = 'HogeHogePiyoPiyo'
$hoge | % { $_ -creplace '(?<=.)[A-Z]', '_$0' } | % { $_.ToUpper() }
# -> HOGE_HOGE_PIYO_PIYO

如果爱用管道的话,以上方法就可以了。或者% {}%也就是ForEach-Object的alias之一)替换成.{ process {} },效率有一定提高,不过自己用用的话没关系,一目了然就行。

另外,如果喜欢链式的话:

$hoge.ForEach{ $_ -creplace '(?<=.)[A-Z]', '_$0' }.ForEach{ $_.ToUpper() }

以上方法更好一些,毕竟这整个就是一个表达式,不会太松散,况且效率也不错。

实际应用的话,更多用到*-Clipboard这些cmdlet,不再赘述。

至于说为什么要坚持使用管道加ForEach-Object.ForEach(),主要是想批量处理,比如说剪贴板里存了不止一行的情况。

UNDERSCORE_CASE转PascalCase

这是上面的反向操作。要是还用正则的话,算不上简单,虽然确实能用吧w

$hogepiyo = 'HOGE_PIYO', 'HOGE_HOGE_PIYO_PIYO'
$hogepiyo.ForEach{
  [regex]::Replace(
    $_,
    '(?:^|_)[^_]+',
    {
      param ([string] $tmp)
      $tmp = $tmp.TrimStart('_')
      $len = $tmp.Length
      (
        ($tmp.Substring(0, 1).ToUpper() + $tmp.Substring(1, $len - 1).ToLower()),
        $tmp.ToUpper()
      )[$len -le 1] # (1)
    })
}
<# ->
HogePiyo
HogeHogePiyoPiyo
#>

现在的pwsh 7可以把(1)那一句用三目运算符表示(注意逻辑要反过来):

$len -gt 1 ?
  $tmp.Substring(0, 1).ToUpper() + $tmp.Substring(1, $len - 1).ToLower() :
  $tmp.ToUpper()

这长度有点抱歉,完全是随便写一下w

或许有更方便的办法,以后看看。

更新(2021/07/11)

说到String.Substring,其实Substring(Int32)这个overload也很好用。以上例子可以写成:

$tmp.Substring(0, 1).ToUpper() + $tmp.Substring(1).ToLower()

依照bean仕样生成Java Bean诸项目

更新(2021/07/24)

移走力,,,

参见→PowerShell 5.1正则表达式进阶使用例

简体中文Windows下处理S-JIS来源的乱码

嘛,这个我不好复现,因为这边现在就已经是日文系统环境了。不过并不耽误理解。

$sjis = [Text.Encoding]::GetEncoding(932)
$gb2312 = [Text.Encoding]::GetEncoding(936)
$dld = '東方地霊殿'
$mjbk = $gb2312.GetString($bytes)
$mjbk # -> 搶曽抧楈揳
$bytes = $gb2312.GetBytes($mjbk)
$fkgn = $sjis.GetString($bytes)
$fkgn # -> 東方地霊殿

嗯,我们敬爱的“搶曽”系列w

实际处理只需后半部分就是了。

小neta:以scalar为单位的字符串长度计算

承上(pwsh 6及以上):

& { $count = 0; $dld.EnumerateRunes().ForEach{ $count++ }; $count }
# -> 5

从字符串得到StringRuneEnumerator,再消费掉,同时计数。虽然看着不高级但确实有效就是了。

小neta:以grapheme为单位的字符串长度计算

承上:

& { $count = 0; [Globalization.StringInfo]::GetTextElementEnumerator($dld).ForEach{ $count++ }; $count }
# -> 5

呃,至于说什么是grapheme……当它是“字儿”就行。

再就是当心版本和环境之间的区别,毕竟时代在发展,现在认为某一个字是“一个字”,以前可能不那么认为。这话不好说,抱歉了。

有兴趣可以去我的文章dotNET: Rune、StringInfo还有Unicode Normalization那边瞅瞅,或许有帮助。

字符串依序替换

更新(2021/07/24)

移走力,,,

参见→PowerShell 5.1正则表达式进阶使用例

字符串依键值替换

更新(2021/07/24)

移走力,,,

参见→PowerShell 5.1正则表达式进阶使用例

字符串依索引替换

更新(2021/07/24)

移走力,,,

参见→PowerShell 5.1正则表达式进阶使用例

表格行列互换

更新(2021/07/24)

移走力,,,

参见→PowerShell 5.1正则表达式进阶使用例

简易列出Unicode区块

不好说,举个例子,比如说我要打出类似这样的结构出来,相对简单但粗糙的办法如下:

-join
(0x30A0..0x30FF).ForEach{
  [char]::ConvertFromUtf32($_)
  if ($_ % 0x10 -eq 0xF) { [Environment]::NewLine }
}

结果如下:

゠ァアィイゥウェエォオカガキギク

グケゲコゴサザシジスズセゼソゾタ

ダチヂッツヅテデトドナニヌネノハ

バパヒビピフブプヘベペホボポマミ

ムメモャヤュユョヨラリルレロヮワ

ヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ

实际上会多一个换行哦,这边用markdown的quote不知道怎么弄所以先不表示出来。

到此为止也没关系。非要处理掉多余换行的话用StringBuilder可能好些:

$sb = [Text.StringBuilder]::new()
(0x30A0..0x30FF).ForEach{
  if ($_ % 0x10 -eq 0xF) {
    [void] $sb.AppendLine([char]::ConvertFromUtf32($_))
  } else {
    [void] $sb.Append([char]::ConvertFromUtf32($_))
  }
}
if ($sb.Length -ge [Environment]::NewLine.Length) { $sb.Length -= [Environment]::NewLine.Length }
$sb.ToString()

结果同上,只是没有多余换行。

逻辑有点重复,幸好还算清晰,至少能用。

更新(2020/07/26)

要不,稍微整美观点儿?

要是弄出来之后能直接粘贴进Excel表格里就好了。试试看:

& {
    $sb = [Text.StringBuilder]::new()
    # header
    [void] $sb.AppendLine(
      " `t" + @((0x0..0xF).ForEach{ "{0:X}" -f $_ } -join "`t")
    )
    # contents
    foreach ($i in 0x30A..0x30F) {
      [void] $sb.Append("U+{0:X}x`t" -f $i)
      [void] $sb.AppendLine(
        @((0x0..0xF).ForEach{ [char]::ConvertFromUtf32(($i -shl 4) + $_) }) -join "`t"
      )
    }
    # remove last newline
    $sb.Length -= [Environment]::NewLine.Length
    # output
    $sb.ToString()
} | Set-Clipboard

这样就可以了。

如此一来粘贴进去就大概是这样:

0123456789ABCDEF
U+30Ax
U+30Bx
U+30Cx
U+30Dx
U+30Ex
U+30Fx

不是很麻烦的事,但需要留意的细节也很多。

数字抽选装置

哎,怎么说好,比如说bingo吧,从1到9,随机抽出不重复的9个数字。

并没有什么性能需求,况且数字也很少,索性用Collections.ArrayList把数字装进去,抽选出来的数字Remove掉就好。

在此还有一个问题:随机数字先完全生成出来确定好,还是每次生成一个数字呢?我寻思这东西具体问题具体分析吧w

以下代码并没有很严谨,看看就得。真要用之前拜托多测测嚯,咱不负责w(公司抽奖之类的,嗯。)

试试第一种,先生成出来放进数组:

<#
pwsh 5
#>
$f = {
  param (
    [Collections.ICollection] $Clct,
    [Random] $Random
  )
  $arrList = [Collections.ArrayList]::new($Clct)
  $result = [Collections.ArrayList]::new($Clct.Count)
  while ($arrList.Count) {
    $atari = $arrList[$Random.Next($arrList.Count)]
    $arrList.Remove($atari)
    [void] $result.Add($atari)
  }
  $result
}
# 看看效果:
(& $f -Clct (1..9) -Random ([Random]::new())) -join ', '
# -> 4, 1, 8, 6, 3, 5, 2, 7, 9
<#
pwsh 7
#>
(Get-Random -InputObject (1..9) -Shuffle) -join ', '
# -> 3, 6, 2, 7, 9, 5, 4, 8, 1

用pwsh 5要多费一番周折,不过pwsh 7的Get-Random已经附带了”洗牌“的功能,直接用就好。

再就是,Random实例的Next方法,这里使用的是int Next(int maxValue)这个,是从0maxValue之间选取整数,但不包含maxValue。所以这个情况下直接把$arrList.Count传进去也没问题就是了。

还有,-Random这个参数要传Random实例,一般来说直接用new无惨无参方法就好了。如果想测试的话可以用System.Random new(int Seed)

别看pwsh 5的解决方案复杂,这是之后代码的基础。

那么,再来试试每次生成一个数字的办法(抱歉,这里还是想用一下闭包):

$f = {
  param (
    [Collections.ICollection] $Clct,
    [Random] $Random
  )
  $arrList = [Collections.ArrayList]::new($Clct)
  return {
    if ($arrList.Count) {
      $atari = $arrList[$Random.Next($arrList.Count)]
      $arrList.Remove($atari)
      $atari
    } else {
      $null
    }
  }.GetNewClosure()
}
# 先“拿出来”
$ff = & $f -Clct (1..9) -Random ([Random]::new())
# 以下代码执行9次:
& $ff
# 这边得出的结果分别是:4 1 6 7 3 5 8 2 9
# 以上代码再执行一遍的话会返回$null,在终端上自然是不表示的

喜欢用class的话也可以试试用一下,不过咱寻思着这个复杂程度的话可以不必。

丸数字与普通数字的比较

哎,这个有版本差别,拜托注意:

$maru5 = '⑤'
# pwsh 7の場合
$maru5 -eq 5
# -> True
# pwsh 5の場合
$maru5.Normalize([System.Text.NormalizationForm]::FormKC) -eq 5
# -> True

至少可以看得出pwsh 7和pwsh 5对-eq的实现有差别(pwsh 5的话,$maru5 -eq 5返回False),原因暂不清楚,或许他们在决定使用ICU的时点前后变更了比较运算符的仕样。总之千万千万当心。

如果不希望这样的情况,请用case sensitive-ceq之类:

# 承上
# pwsh 7の場合
$maru5 -ceq 5
# -> False
# pwsh 5の場合
$maru5 -ceq 5
# -> False

再就是,在这里的NormalizationForm用FormKCFormKD没有结果上的区别。

还有,(至少)-eq是会自动统一左右类型再去比较的,当然在这里没有特别的影响,注意一下就是了。

简单查看pwsh正在用的dotNET版本

之前到处查,办法倒是不少,但效果则各有不同。总之终于找到了一个方便的办法,返回值看起来也清楚:

[System.Runtime.InteropServices.RuntimeInformation, mscorlib]::FrameworkDescription

至于为什么这个能用,详细的并不清楚,只知道mscorlib是dotNET里相当重要而且基础的……那么一个东西。

这边得出的结果如下:

PSVersionFrameworkDescription
5.1.19041.1320.NET Framework 4.8.4420.0
7.2.0-rc.1.NET 6.0.0-rc.2.21480.5
7.2.0.NET 6.0.0-rtm.21522.10
7.2.1.NET 6.0.0-rtm.21522.10
7.3.0-preview.1.NET 6.0.0-rtm.21522.10
7.2.3.NET 6.0.4
7.3.0-preview.3.NET 7.0.0-preview.2.22152.2

(2021/12/20记)之前pwsh版本更新到了7.2.1和7.3.0-prv1,dotNET版本并没变化的样子。(2022/05/06记)之前pwsh版本更新到了7.2.3和7.3.0-prv3,prv那边dotNET版本起飞了哦。

后记

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

InSb

InSb

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