ワンライナーの長いコマンドがスクリプトファイル上にあると読み辛いのでどうにかしたい話。
改行のエスケープ
正統派。下記の例のように、行末にエスケープ文字(アクサングラーブ)を置けば、コマンドが次の行に続くと解釈される。(参考:about_Escape_Characters)
これを利用すれば、見やすい位置でコマンドを改行できる。
PS C:\> Write-Host `
>> -Object (1..9) `
>> -Separator ", "
1, 2, 3, 4, 5, 6, 7, 8, 9
式が完了していない状態での改行
一番やりやすい気がする。パイプ |
の後ろにコマンドがない場合や、閉じ括弧 )
}
によって括弧が閉じられていない場合など、コマンドが次の行に続くと解釈される。
個人的にはパイプの後ろで改行するのが好み。
PS C:\> 0,1,2,3 |
>> ? {$_ % 2 -eq 0} |
>> % {$_ * $_}
0
4
スプラッティング
PowerShell では引数の名前と値のペアを HashTable (もしくは値を配列)で作成しておくと、後でそれをコマンドの引数として分配(スプラッティング)できる。(参考:about_Splatting )
長い引数を指定する場合や、引数を使い回す場合に使える。
PS C:\> $param = @{
>> Object = 1..9
>> Separator = ", "
>> }
PS C:\> Write-Host @param
1, 2, 3, 4, 5, 6, 7, 8, 9
<!-- ここから先は独自研究 -->
即時スプラッティング
スプラッティングを使って書く場合、引数を先に書いて、コマンドを後に書く。だが、普通にコマンドを書く場合は、コマンド名を先に書いて、引数を後に書く。この逆転現象のせいで、スプラッティングを使って書かれたコードが読みづらいと感じることがある。
そこで、スプラッティングをラップした関数 Invoke-Splatting
を書いた。Invoke-Splatting
を使うと、コマンド名を先に書いて、引数を後に書ける。
filter Invoke-Splatting
{
Param
(
[Parameter(Mandatory=$True)]
[string] $Name,
[Parameter(Mandatory=$True)]
$Parameters
)
& $Name @Parameters
}
Set-Alias splatting Invoke-Splatting
PS C:\> splatting Write-Host @{
>> Object = 1..9
>> Separator = ", "
>> }
1, 2, 3, 4, 5, 6, 7, 8, 9
パイプラインEval
Invoke-Splatting
はパイプラインを想定していないので、そこらへんをどうにかするために、コードを文字列で生成して実行してみる。
Invoke-PipelineDictionary(欠陥品)
下記 Invoke-PipelineDictionary
は、順序を保持したハッシュテーブルを元に、パイプラインを組み上げて実行する。ハッシュテーブルのキーはコマンド名で、値はスプラッティングする引数である。
filter Invoke-PipelineDictionary
{
Param
(
[Parameter(Mandatory=$True)]
[System.Collections.Specialized.OrderedDictionary] $Commands
)
$kyes = $Commands.Keys
if ($kyes.Count -eq 0) {return}
# 下記例のような、スクリプトブロックを生成
# 例:{fn0 @0 | fn1 @1 | ... | fnk @k}
$str = ($kyes | % {$i=0} {"${_} @${i}"; $i++}) -join ' | '
$sb = [ScriptBlock]::Create($str)
# スプラッティング用の変数を連番で定義
$kyes | % {$i=0} {Set-Variable $i $Commands[$_]; $i++}
# 実行
& $sb
}
Set-Alias pipelineD Invoke-PipelineDictionary
使い方の例。
PS C:\> pipelineD ([ordered] @{
>> # 読み取り専用のテキストファイルについて
>> 'Get-ChildItem' = @{
>> Path = '*.txt'
>> ReadOnly = $true
>> }
>>
>> # ベースネームに接尾辞「-ro」を追加し
>> 'Rename-Item' = @{
>> NewName = {$_.BaseName + "-ro.txt"}
>> PassThru = $true
>> }
>>
>> # 最初の5ファイルのみを表示する
>> # 6ファイル以降は接尾辞は追加されるが表示はされない
>> 'Select-Object' = @{
>> First = 5
>> Wait = $true
>> }
>> })
pipelineD
はコマンドやら関数を順次実行するものなので、0..9 | % {...
のようなコマンドで始まらないパイプラインを再現できない。そのため、下記のような関数を定義しておくと良い。
filter Start-Pipeline {$args}
Set-Alias inlet Start-Pipeline
PS C:\> pipelined ([ordered] @{
>> inlet = 0..100
>> where = {$_ % 2 -eq 0}
>> foreach = {$_ * $_}
>> select = @{First = 3}
>> })
0
4
16
Invoke-Pipeline
pipelineD
はハッシュテーブルを使う関係上、パイプライン上で同じコマンドが使えない(同一のコマンド名を二度書けない)という致命的欠陥がある。
そんな欠陥を回避するため、コマンド名と引数のペアを作成するNew-Pipe
と、ペアの配列からパイプラインを構築するNew-Pipeline
関数を作った。なお、v2で使えるようにしてあるので書き方が古い。
filter Invoke-Pipeline
{
Param
(
[Parameter(Mandatory=$True)]
[ScriptBlock] $Commands
)
$pipes = & $Commands
if ($pipes.length -eq 0) {return}
# 下記例のような、スクリプトブロックを生成
# 例:{_alias_0 @_variable_0 | _alias_1 @_variable_1 | ... }
$str = ($pipes | % {$i=0} {"_alias_${i} @_variable_${i}"; $i++}) -join ' | '
$sb = [ScriptBlock]::Create($str)
# 実行コマンドのエイリアスを作成
$pipes | % {$i=0} {Set-Alias "_alias_${i}" $_.Command; $i++}
# スプラッティング用の変数を連番で定義
$pipes | % {$i=0} {Set-Variable "_variable_${i}" $_.Parameters; $i++}
# 実行
& $sb
}
filter New-Pipe
{
param
(
[Parameter(Mandatory=$True)]
[string] $Command,
$Parameters = @{}
)
# v2 の書き方
New-Object PSObject -Property @{
Command = $Command
Parameters = $Parameters
}
}
filter Start-Pipeline {$args}
Set-Alias pipeline Invoke-Pipeline
Set-Alias pipe New-Pipe
Set-Alias inlet Start-Pipeline
使い方の例。pipeline
の引数は単なるスクリプトブロックなのであらゆるコードが書ける。下記のとおりパイプの途中で変数を定義するというキモイ書き方もできてしまう。
PS C:\> pipeline {
>> pipe inlet (0..100)
>> pipe where {$_ % 2 -eq 0}
>> pipe foreach {$_ * $_}
>>
>> $x = 3
>> pipe select @{First = $x}
>> }
0
4
16