【PowerShell】ParameterSet を活用する

調べてみたら PowerShell の パラメータ指定が奥深かったのでメモ。
[公式ドキュメント](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-7) を読む限り、 `ArgumentCompleter` などほかにも相当奥が深そうですが、まずは `ParameterSet` についてまとめておきます。

環境：

```
Name                           Value
----                           -----
PSVersion                      7.0.3
PSEdition                      Core
GitCommitId                    7.0.3
OS                             Microsoft Windows 10.0.18362
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
```

## ParameterSet とは

> The ParameterSetName argument specifies the parameter set to which a parameter belongs.

渡されたパラメータが属するセットを指定できるよ、とのことです。

### 具体例1

```powershell
function Test-ParamSet {
    [CmdletBinding(DefaultParameterSetName="A")]
    param (
        [Parameter(ParameterSetName="A")]
            [Switch]$a,
        [Parameter(ParameterSetName="B")]
            [Switch]$b,
        [Parameter(ParameterSetName="C")]
            [Switch]$c,
        [Parameter(ParameterSetName="D")]
            [Switch]$d,
        [Parameter(ParameterSetName="E")]
            [Switch]$e,
        [switch]$nonSet
    )
    Write-Host $PSCmdlet.ParameterSetName
}
```

`Test-ParamSet -` とハイフンまで入力して Tab を押すと、 `-a` ～ `-e` および `-nonSet` のほかに共通パラメータの `-Verbose` などが補完されます。

そして重要な点として、たとえば `Test-ParamSet -a` とパラメータ `-a` を指定すると、 `-b` ～ `-e` は Tab を押しても **補完されなくなります** 。

無理やり指定するとエラーになります。

```
> Test-ParamSet -a -b
Test-ParamSet: Parameter set cannot be resolved using the specified named parameters. One or more parameters issued cannot be used together or an insufficient number of parameters were provided.
```

これは、パラメータ `-a` を指定した時点で「パラメータセットの `A` を使用している」と解釈され、残りの `B` などのセットが補完の候補から外されたということです（対して、 `-nonSet` はセットを指定していないので常に補完の対象）。

### 具体例2

各パラメータセットのメンバーを増やしてみます。

```powershell
function Test-ParamSet2 {
    [CmdletBinding(DefaultParameterSetName="A")]
    param (
        [Parameter(ParameterSetName="A")]
            [Switch]$a1,
        [Parameter(ParameterSetName="A")]
            [Switch]$a2,
        [Parameter(ParameterSetName="B")]
            [Switch]$b1,
        [Parameter(ParameterSetName="B")]
            [Switch]$b2,
        [Parameter(ParameterSetName="B")]
            [Switch]$b3,
        [Parameter(ParameterSetName="C")]
            [Switch]$c1,
        [Parameter(ParameterSetName="C")]
            [Switch]$c2,
        [Parameter(ParameterSetName="E")]
            [Switch]$e
    )
    Write-Host $PSCmdlet.ParameterSetName
}
```

`Test-ParamSet2 -a1` とするとセット `A` が適用され、 `-a2` には Tab 補完が効きますが、 `-b1` などは候補に挙がらなくなります。

つまり、「●●の処理をしたいときはセット `A`」といったように、用途別にパラメータのセットを指定できるわけです。

### 具体例3

[公式のサンプル](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parameter_sets?view=powershell-7) からパラメータ指定部分を抜き出してみます。

```PowerShell
function Measure-Lines {
    [CmdletBinding(DefaultParameterSetName = 'Path')]
    param (
        [Parameter(Mandatory = $true,
            ParameterSetName = 'Path',
            HelpMessage = 'Enter one or more filenames',
            Position = 0)]
        [Parameter(Mandatory = $true,
            ParameterSetName = 'PathAll',
            Position = 0)]
        [string[]]$Path,

        [Parameter(Mandatory = $true, ParameterSetName = 'LiteralPathAll')]
        [Parameter(Mandatory = $true,
            ParameterSetName = 'LiteralPath',
            HelpMessage = 'Enter a single filename',
            ValueFromPipeline = $true)]
        [string]$LiteralPath,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [switch]$Lines,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [switch]$Words,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'LiteralPath')]
        [switch]$Characters,

        [Parameter(Mandatory = $true, ParameterSetName = 'PathAll')]
        [Parameter(Mandatory = $true, ParameterSetName = 'LiteralPathAll')]
        [switch]$All,

        [Parameter(ParameterSetName = 'Path')]
        [Parameter(ParameterSetName = 'PathAll')]
        [switch]$Recurse
    )
}
```

このようにパラメータ1つに複数のパラメータセットを割り当てることもできるそうです。詳細に書かれているので複雑になりましたが、ここでは `Path` `PathAll` `LiteralPath` `LiteralPathAll` の4種類のパラメータセットが指定されています。

少し複雑なので表にしました（セット名を行ごとに整理）。


| |-Path|-LiteralPath|-Lines|-Words|-Characters|-All|-Recurse|
|:--- |:---: |:---: |:---: |:---: |:---: |:---: |:---: |
|Path |○||○|○|○||○|
|PathAll|○|||||○|○|
|LiteralPath||○|○|○|○|||
|LiteralPathAll||○||||○||


`Measure-Lines -Path \.hoge.txt` のようにして `-Path` を指定すると、セットは `Path` もしくは `PathAll` に絞り込まれ、 `-LiteralPath` と `-LiteralPathAll` は補完されなくなります。
同様に、 `-Lines` を指定すると、その時点で `PathAll` と `LiteralPathAll` のセットは使われないことが確定するので `-All` は補完されなくなります。
極端なところでは `-All` を指定すると、 `-Path` と `-LiteralPath` と `-Recurse` しか補完されなくなります。

実際に補完動作を見ながらだとわかりやすいと思います。


## 実践

google 検索するコマンドレットを作ってみました。
`-global` でグローバル検索、 `-image` で画像検索といったように、スイッチパラメータで検索モードを切り替えられます。 `ParameterSetName` のおかげで複数のモードを同時に指定してしまうというミスが減っています。

```powershell
function Invoke-GoogleSearch {
    [CmdletBinding(DefaultParameterSetName="global")]
    param (
        [Parameter(ParameterSetName="global")]
        [Switch]$global,
        [Parameter(ParameterSetName="image")]
        [Switch]$image,
        [Parameter(ParameterSetName="map")]
        [Switch]$map,
        [Parameter(ParameterSetName="scholar")]
        [Switch]$scholar,
        [switch]$strict,
        [Parameter(ValueFromRemainingArguments)]
        [string[]]$s
    )

    $keyword = ($strict)?
        ($s | ForEach-Object{'"{0}"' -f $_}) -join " " :
        $s -join " "

    $url = switch ($PsCmdlet.ParameterSetName) {
        "global" {"http://www.google.co.jp/search?q={0}"; break}
        "image" {"https://www.google.com/search?tbm=isch&q={0}"; break}
        "map" {"https://www.google.co.jp/maps/search/{0}"; break}
        "scholar" {"https://scholar.google.co.jp/scholar?q={0}"; break}
    }
    Start-Process ([string]$url -f [System.Web.HttpUtility]::UrlEncode($keyword))
}
```

備考：　コマンドライン引数を受け取るには自動変数 `$args` を使うことが多いですが、パラメータを色々設定しているとこの方法は使えないようです。同様の動きを実現させるためには `ValueFromRemainingArguments` を指定する必要があるようです。