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

生活和工作中经常用到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;
#>

  1. 这里使用的是Regex.Match(String),作用是返回第一组匹配,返回类型为MatchMatch这个对象直接用就可以了,没什么特别。由于这里只需要第一个匹配,所以如此没有问题(记得在正则表达式把^$写好)。
    其实单纯对于本例而言,可以使用Regex.Matches(String),返回所有匹配,返回类型则为MatchCollectionMatchCollection稍微有点特殊,由于本例只能得到一组匹配,在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
# 结果同上

表格行列互换

用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真好玩。

后记

或许以后还会在这边补充吧,有需要的话。

InSb

InSb

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