不定时更新。主要是些常用的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那边有decodeURI
和decodeURIComponent
这两个可以用,建议看一下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>
顺带一提,这种东西实际上分为两种,一种是所谓“数値文字参照”,如百
(百
);一种是所谓“文字実体参照”,如我们耳熟能详的>
(>
)。
更新(2020/06/17)
用JavaScript的话,可以用Web API的DOMParser试试看:
let s = '<div id="siteSub" class="noprint">出典: フリー百科事典『ウィキペディア(Wikipedia)』</div>';
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.Linq
的Enumerable.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和变量冲突的问题,可以用scriptblock
把for
循环包起来:
$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)
不如试一下scriptblock
的begin
和process
看看:
$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
其实可以定义begin
、process
和end
三个语句块的,如果没有显式地用这三种包裹,那默认就是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
这样更灵活,只是调用方法要稍稍注意。
至于为什么闭包内改变值要用$script:c
这样的形式而不是单纯的$c
(相当于$local:c
)呢……它要是用local scope的话,闭包建成之后,它就不会变化了;用script scope,它的变动会保存住,而且不会泄漏出去;用global scope……别ww
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)
移走力,,,
简体中文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)
移走力,,,
字符串依键值替换
更新(2021/07/24)
移走力,,,
字符串依索引替换
更新(2021/07/24)
移走力,,,
表格行列互换
更新(2021/07/24)
移走力,,,
简易列出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
这样就可以了。
如此一来粘贴进去就大概是这样:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
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)
这个,是从0
到maxValue
之间选取整数,但不包含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用FormKC
或FormKD
没有结果上的区别。
还有,(至少)-eq
是会自动统一左右类型再去比较的,当然在这里没有特别的影响,注意一下就是了。
简单查看pwsh正在用的dotNET版本
之前到处查,办法倒是不少,但效果则各有不同。总之终于找到了一个方便的办法,返回值看起来也清楚:
[System.Runtime.InteropServices.RuntimeInformation, mscorlib]::FrameworkDescription
至于为什么这个能用,详细的并不清楚,只知道mscorlib
是dotNET里相当重要而且基础的……那么一个东西。
这边得出的结果如下:
PSVersion | FrameworkDescription |
---|---|
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版本起飞了哦。
后记
真好用,用就完事儿了。以后有需要再补充。