生活和工作中经常用到pwsh和正则表达式搞定一些(自己的)小需求,这些内容大多记录在PowerShell代码备忘录那边,不过那也太膨胀了点儿,所以我寻思着干脆剥离出来一部分另开一篇文章得了。
环境
未特别指明的话:
操作系统Win10 21H1,PowerShell版本如果说5则为5.1,如果说7则为7.1,$PSCulture
和$PSUICulture
均为ja-JP
(工作生活需要)。
正文
依照bean仕样生成Java Bean诸项目
我做PG的工作嘛,需要根据详细设计仕样书去实装。详细设计是Excel表格,里面经常写上一些Java Bean的定义。
太详细的情况暂且不提,我这边如果把一个bean定义洗出方便自己用的形式,可能是这样的:
<#
bean spec
假的,看个意思就行
#>
@'
カスタマーID Long cstId 連番
氏名(カナ) String smiKn 半角カナ
氏名(漢字) String smiKj 全角文字だけ。全角スペースで姓名区切り
削除フラグ Integer delFlg 論理削除。0:有効;1:削除
'@
简化了很多,大体是这个感觉就是。
第一步自然是把bean的文件建好,之后就根据上述形式生成:
# 总之先把上述那个形式的定义复制在剪贴板中
<#
bean generate
#>
Get-Clipboard |
& {
begin {
$regex = [regex]::new('^\s*(\S+)\s+(\S+)\s+(\S+)\s*.*$')
$format = "/** {0} */`nprivate {1} {2};"
}
process {
$m = $regex.Match($_) # 1
$format -f $m.Groups[1].Value, $m.Groups[2].Value, $m.Groups[3].Value
}
} |
Set-Clipboard
# 应该好了。检证一下看看:
Get-Clipboard
<#
->
/** カスタマーID */
private Long cstId;
/** 氏名(カナ) */
private String smiKn;
/** 氏名(漢字) */
private String smiKj;
/** 削除フラグ */
private Integer delFlg;
#>
注
- 这里使用的是Regex.Match(String),作用是返回第一组匹配,返回类型为Match。
Match
这个对象直接用就可以了,没什么特别。由于这里只需要第一个匹配,所以如此没有问题(记得在正则表达式把^
和$
写好)。
其实单纯对于本例而言,可以使用Regex.Matches(String),返回所有匹配,返回类型则为MatchCollection。MatchCollection
稍微有点特殊,由于本例只能得到一组匹配,在pwsh里实际上连MatchCollection.Item[Int32]
都不必显式调用,就可以直接调用它其中的Match
对象。虽然可以这么用,不过为了别人能懂,也为了不给自己挖坑,还是慎重点好。这也算是pwsh里非常weird的地方。
第4列是备考之类的东西,在本例中跳过,以免过于复杂。
至于说之后怎么办,用IDE生成getter和setter就好(如果用Eclipse的话,建议使用选择之后直接生成的功能,具体是什么我忘了,总之得让注释好看),让用lombok的话那就挂个相关注解就行。
再就是当心Boolean
类型,自动生成的getter可能是is
开头的,根据需要处理吧。
字符串依序替换
就比如说吧,MyBatis打log的时候会生成SQL文,其中参数用问号表示,另外还有参数表。参数表好处理,洗出来一个数组找个地方放着就行,但SQL文的问号该如何替换则是个问题。
总之先做一下例子。如下:
# 假的。看看就得
$dml = 'select * from NNS_CST where TORK_D = ? and CST_KBN = ? and YUKO_FLG = ? and DEL_FLG = ?'
# 用一下postgres风格的时间表示。虽然其实我不会,但且凑合用着
# 剪贴板内容如下:
@'
TO_DATE('2020/03/11', 'YYYY/MM/DD')
'103'
1
0
'@
# 塞进去。类型是Object[],实际其中每一个元素都是string
$params = Get-Clipboard
思路的话,一个个来。
首先是,dotNET的正则提供了“只替换一次”的功能,数量少的话用链式也没问题,就像JS那边常做的那样。但东西一多,还是不可避免地要拆开:
$pattern = '(?<=\s)\?(?=\s|$)'
$regex = [regex]::new($pattern)
$result = $dml
$params.ForEach{ $result = $regex.Replace($result, $_, 1) }
$result
# -> select * from NNS_CST where TORK_D = TO_DATE('2020/03/11', 'YYYY/MM/DD') and CST_KBN = '103' and YUKO_FLG = 1 and DEL_FLG = 0
不是不行,但$result
真是被折腾了好多次w
不想直接用string
的话,那用StringBuilder
会不会好些?确实,方法足够使用,只是思路需要变一下。具体见下文:
$f =
{
param (
[string] $Dml,
[string[]] $Params
)
begin {
$sb = [Text.StringBuilder]::new($Dml)
$ms = [regex]::Matches($Dml, '(?<=\s)\?(?=\s|$)')
}
end {
for ($i = $ms.Count - 1; $i -ge 0; $i--) {
[void] $sb.Remove($ms[$i].Index, $ms[$i].Length) # 本例中第二个参数其实完全可以确定,写成1也没有关系
[void] $sb.Insert($ms[$i].Index, $Params[$i])
}
$sb.ToString()
}
}
& $f $dml $params
# -> select * from NNS_CST where TORK_D = TO_DATE('2020/03/11', 'YYYY/MM/DD') and CST_KBN = '103' and YUKO_FLG = 1 and DEL_FLG = 0
代码风格稍微原谅一下。
明显复杂很多。要注意的是,由于需要移除后再添加,正方向操作会非常复杂,现阶段暂且倒过来处理,如此便不必太过考虑插入索引问题。for
循环在此非常值得用,不必吝惜。
更新(2021/07/24)
当然还是不要用比较好。继续往下看吧。
字符串依键值替换
承上。若拿到的数据明显可以转换为键值对,或许就不用太麻烦了吧。试试看:
# 假的。改了一下上面的
$dml = 'select * from NNS_CST where TORK_D = :TORK_D and CST_KBN = :CST_KBN and YUKO_FLG = :YUKO_FLG and DEL_FLG = :DEL_FLG'
# 总之提前用文本编辑器之类的先收拾一下数据
# 剪贴板内容如下:
@'
TORK_D = TO_DATE('2020/03/11', 'YYYY/MM/DD')
CST_KBN = '103'
YUKO_FLG = 1
DEL_FLG = 0
'@
# 存放进变量时请注意,由于我们不需要一行一行零散的结构,所以应该:
$in = Get-Clipboard -Raw
# 如此,$in这个变量的类型是string,而非string[]
# 直接做成hashtable:
$ht = ConvertFrom-StringData $in
# 抱歉我忍不住了:
[regex]::new(':(\w+)\b').
Replace(
$dml,
{ param ($match); $ht[$match.Groups[1].Value] }
) |
Set-Clipboard
# 剪贴板内容如下:
@'
select * from NNS_CST where TORK_D = TO_DATE('2020/03/11', 'YYYY/MM/DD') and CST_KBN = '103' and YUKO_FLG = 1 and DEL_FLG = 0
'@
这次写得尽量易懂了w
千万注意变量命名,不要使用$input
,因为它是所谓自动变量之一。
稍微注意一下hashtable的用法。直接用类似数组索引的语法就可以,或许不太痛快,但确实有效。
字符串依索引替换
承上。其实不必用hashtable,直接用array也行——至少pwsh里可以这样。
# 假的。看看就得。咱又说一遍了呢w
$dml = 'select * from NNS_CST where TORK_D = ? and CST_KBN = ? and YUKO_FLG = ? and DEL_FLG = ?'
# 剪贴板内容如下:
@'
TO_DATE('2020/03/11', 'YYYY/MM/DD')
'103'
1
0
'@
# 这次确实只需要一个array
[string[]] $params = Get-Clipboard
# closure式计数器
$f = {
param ([int] $i = -1)
{
$script:i++
$i
}.GetNewClosure()
}
$ff = & $f
# 中间产物
$result = [regex]::new('(?<=\s|^)\?(?=\s|$)').Replace($dml, { "{$(& $ff)}" })
# -> select * from NNS_CST where TORK_D = {0} and CST_KBN = {1} and YUKO_FLG = {2} and DEL_FLG = {3}
# 索引的值可以直接代入。总之:
[regex]::new('(?<=\s|^){(\d+)}(?=\s|$)').
Replace(
$result,
{ param ($match); $params[$match.Groups[1].Value] }
) |
Set-Clipboard
# 剪贴板内容如下:
@'
select * from NNS_CST where TORK_D = TO_DATE('2020/03/11', 'YYYY/MM/DD') and CST_KBN = '103' and YUKO_FLG = 1 and DEL_FLG = 0
'@
如果不喜欢closure式计数器,“中间产物”那边可以直接:
$i = -1
$result = [regex]::new('(?<=\s|^)\?(?=\s|$)').Replace($dml, { $script:i++; "{$i}" })
$script:i
这样的写法还是需要的。又要牵扯到scope,啊啊啊。暂且先放放。
虽然需要两次替换,不过由于步骤循序渐进,相对容易理解和再现,利用价值还是不错的。(大嘘)
但是吧,为什么需要两次替换呢?一次搞定就好:
# 承接上例“中间产物”以上(不含)内容
[regex]::new('(?<=\s|^)\?(?=\s|$)').
Replace(
$dml,
{ $params[(& $ff)] }
) |
Set-Clipboard
# 结果同上
更加简便而且直接。不过由于过于直接,千万注意正则表达式是否确实能匹配到,不然计数器可不一定会好好走。
更新(2021/07/24)
有关计数器,友情赠送两个更简便的写法:
$f = { param ($i = 0); { ($script:i++) }.GetNewClosure() }
或者这样也行,因为int
类型的默认值就是0
:
$f = { param ([int] $i); { ($script:i++) }.GetNewClosure() }
至于说为什么($script:i++)
可以用,不知道你有没有发现直接运行比如$i++
这样的语句它是不会返回的,但用括号包裹,括号里面的内容是会求值的,括号及其内容整个就成了一个ParenExpression,如此就有了返回值。刚才的说法是我凭记忆整理出来的,不一定是这样,但至少解释得通。返回值的情况如同C语言那样有前后之分,不会的自己试一下也行。
更新(2021/09/23)
之前说的($script:i++)
……虽然我也不好解释,不过可以看一下这个:
# $i是否存在并无所谓,只是观察一下Ast而已
{ $i++ }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.GetType().FullName
# -> System.Management.Automation.Language.UnaryExpressionAst
{ ($i++) }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.GetType().FullName
# -> System.Management.Automation.Language.ParenExpressionAst
{ $($i++) }.Ast.EndBlock.Statements[0].PipelineElements[0].Expression.GetType().FullName
# -> System.Management.Automation.Language.SubExpressionAst
我只能说这三个是不同的东西,别的真不清楚。
更新(2021/07/24)
那么,导演剪辑版如下:
# 假的。看看就得。这是第三遍了么w
$dml = 'select * from NNS_CST where TORK_D = ? and CST_KBN = ? and YUKO_FLG = ? and DEL_FLG = ?'
# 剪贴板内容如下:
@'
TO_DATE('2020/03/11', 'YYYY/MM/DD')
'103'
1
0
'@
# 这次确实只需要一个array,就不要-Raw了
[string[]] $params = Get-Clipboard
# 大家最喜欢的closure计数器
$f = { param ([int] $i); { ($script:i++) }.GetNewClosure() }
$ff = & $f
# 作成
[regex]::new('(?<=\s|^)\?(?=\s|$)').
Replace(
$dml,
{ $params[(& $ff)] }
) |
Set-Clipboard
# 结果同上
更新(2021/11/12)
有个事儿啊,我得说一下:
System.Array
这个类实现了IEnumerable
,也就是说array可以使用GetEnumerator
这个方法。
……恍然大悟了是吧!
# $dml 和 $params 的定义同上
# 虽然无关,但是我想到了就说:$args 这个东西是automatic variable,建立变量时尽量不要起这个名字
$etor = $params.GetEnumerator()
[regex]::Replace($dml, '(?<=\s|^)\?(?=\s|$)', { [void] $etor.MoveNext(); $etor.Current }) | Set-Clipboard
# 结果同上
要想再运行一遍,别忘了先$etor.Reset()
。
需要注意的是MoveNext()
返回一个bool
,记得用[void]
避免它输出。
表格行列互换
用Excel的话,粘贴表格数据时有“行/列の入れ替え”这么一项(咱也不知道简中版叫什么就是),用这个模式可以让行列互换,用途意外地广泛。
那么,试着用pwsh实现一下类似功能如何呢:
# 剪贴板内容如下:
# 啊,具体这是什么,以后再说w
@'
Lv 12
総数 1995
Tap 949
Hold 154
Slide 398
Air 399
Flick 95
定数 12.2
'@
[string[]] $src = Get-Clipboard
$rowNum = $src.Length # 8
$colNum = ([char[]] $src[0]).Where{ $_ -eq "`t" }.Length + 1 # 2
$2dArr = [string[,]]::new($rowNum, $colNum)
# 二次元配列に格納
for ($i = $2dArr.GetLowerBound(0); $i -le $2dArr.GetUpperBound(0); $i++) {
[string[]] $rowArr = $src[$i] -split "`t"
for ($j = $2dArr.GetLowerBound(1); $j -le $2dArr.GetUpperBound(1); $j++) {
$2dArr[$i,$j] = $rowArr[$j]
}
}
# 行・列を入れ替えしてStringBuilderに格納
$sb = [Text.StringBuilder]::new()
for ($j = $2dArr.GetLowerBound(1); $j -le $2dArr.GetUpperBound(1); $j++) {
[void] $sb.AppendLine(
@(
for ($i = $2dArr.GetLowerBound(0); $i -le $2dArr.GetUpperBound(0); $i++) {
$2dArr[$i,$j]
}
) -join "`t"
)
}
# 余計な改行を削除
if ($sb.Length -ge [Environment]::NewLine.Length) {
$sb.Length -= [Environment]::NewLine.Length
}
# クリップボードに出力
$sb.ToString() | Set-Clipboard
# 剪贴板内容如下:
@'
Lv 総数 Tap Hold Slide Air Flick 定数
12 1995 949 154 398 399 95 12.2
'@
以后详谈。
实际运用的话,直接开个Excel表格,全选sheet内cell,セルの書式設定,表示形式,文字列。之后就用吧。
更新(2021/07/24)
这里用了“正统的”多维数组,详情见此。使用原因无非是思考起来比较方便而已,效率什么的还不在考虑范围内。
至于说这数据是什么,这个是おいでよ!高須らいむランド紫谱的部分数据。别看定数只有12.2,难点还是有的。CHUNITHM真好玩。
后记
或许以后还会在这边补充吧,有需要的话。