LoginSignup
38
38

More than 5 years have passed since last update.

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

Posted at

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

改行のエスケープ

正統派。下記の例のように、行末にエスケープ文字(アクサングラーブ)を置けば、コマンドが次の行に続くと解釈される。(参考: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
38
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
38