PowerShell で長いコマンドを綺麗に書きたい


ワンライナーの長いコマンドがスクリプトファイル上にあると読み辛いのでどうにかしたい話。


# 改行のエスケープ

正統派。下記の例のように、行末にエスケープ文字（アクサングラーブ）を置けば、コマンドが次の行に続くと解釈される。（参考：[about_Escape_Characters](https://technet.microsoft.com/ja-jp/library/hh847755.aspx)）

これを利用すれば、見やすい位置でコマンドを改行できる。

```ps1
PS C:\> Write-Host `
>> -Object (1..9) `
>> -Separator ", "
1, 2, 3, 4, 5, 6, 7, 8, 9
```



# 式が完了していない状態での改行

一番やりやすい気がする。パイプ `|` の後ろにコマンドがない場合や、閉じ括弧 `)` `}` によって括弧が閉じられていない場合など、コマンドが次の行に続くと解釈される。

個人的にはパイプの後ろで改行するのが好み。

```ps1
PS C:\> 0,1,2,3     |
>> ? {$_ % 2 -eq 0} |
>> % {$_ * $_}
0
4
```


# スプラッティング

PowerShell では引数の名前と値のペアを HashTable　（もしくは値を配列）で作成しておくと、後でそれをコマンドの引数として分配（スプラッティング）できる。（参考：[about_Splatting](https://technet.microsoft.com/ja-jp/library/jj672955.aspx) ）

長い引数を指定する場合や、引数を使い回す場合に使える。

```ps1
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` を使うと、コマンド名を先に書いて、引数を後に書ける。

```ps1
filter Invoke-Splatting
{
    Param
    (
        [Parameter(Mandatory=$True)]
        [string] $Name,

        [Parameter(Mandatory=$True)]
        $Parameters
    )

    & $Name @Parameters
}

Set-Alias splatting Invoke-Splatting
```

```ps1
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` は、順序を保持したハッシュテーブルを元に、パイプラインを組み上げて実行する。ハッシュテーブルのキーはコマンド名で、値はスプラッティングする引数である。

```.ps1
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
```

使い方の例。

```ps1
PS C:\> pipelineD ([ordered] @{
>>     # 読み取り専用のテキストファイルについて
>>     'Get-ChildItem' = @{
>>         Path     = '*.txt'
>>         ReadOnly = $true
>>     }
>> 
>>     # ベースネームに接尾辞「-ro」を追加し
>>     'Rename-Item' = @{
>>         NewName  = {$_.BaseName + "-ro.txt"}
>>         PassThru = $true
>>     }
>> 
>>     # 最初の５ファイルのみを表示する
>>     # ６ファイル以降は接尾辞は追加されるが表示はされない
>>     'Select-Object' = @{
>>         First = 5
>>         Wait  = $true
>>     }
>> })
```

`pipelineD` はコマンドやら関数を順次実行するものなので、`0..9 | % {... ` のようなコマンドで始まらないパイプラインを再現できない。そのため、下記のような関数を定義しておくと良い。

```.ps1
filter Start-Pipeline {$args}
Set-Alias inlet Start-Pipeline
```

```ps1
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で使えるようにしてあるので書き方が古い。

```ps1
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` の引数は単なるスクリプトブロックなのであらゆるコードが書ける。下記のとおりパイプの途中で変数を定義するというキモイ書き方もできてしまう。

```.ps1
PS C:\> pipeline {
>>     pipe inlet (0..100)
>>     pipe where {$_ % 2 -eq 0}
>>     pipe foreach {$_ * $_}
>>
>>     $x = 3
>>     pipe select @{First = $x}
>> }
0
4
16
```