PowerShell で選択肢メニューと、それに伴う分岐処理を簡潔に書きたい

# 選択肢メニューとは

下記画像のように、`Remove-Item -Confirm`とか実行すると出て来る選択肢のこと。(を個人的に選択肢メニューと呼んでいる)

![Remove-Item -Confirm](https://qiita-image-store.s3.amazonaws.com/0/36744/d0dc9c8c-dc50-4409-4a3a-c11f8a36c64c.png)

# 簡潔に書けないの？

上記のような選択肢は下記サイトを参考にすると、

* [Windows PowerShell Tip: Adding a Simple Menu to a Windows PowerShell Script](https://technet.microsoft.com/en-us/library/ff730939.aspx)

次のようなコードで書ける。`Write-Host`や`Read-Host`で自前実装するより楽に書けるので良い。

```.ps1
$title = "確認"
$message = @"
この操作を実行しますか?
対象 "C:\demo\PromptForChoice\hoge.txt" に対して操作 "ファイルの削除" を実行しています。
"@

$tChoiceDescription = "System.Management.Automation.Host.ChoiceDescription"
$options = @(
    New-Object $tChoiceDescription ("はい(&Y)",       "操作の次のステップのみを続行します。")
    New-Object $tChoiceDescription ("すべて続行(&A)", "操作のすべてのステップを続行します。")
    New-Object $tChoiceDescription ("いいえ(&N)",     "この操作をスキップし、次の操作に進みます。")
    New-Object $tChoiceDescription ("すべて無視(&L)", "この操作および後続のすべての操作をスキップします。")
    New-Object $tChoiceDescription ("中断(&S)",        "現在のコマンドを中断し、コマンドプロンプトに戻ります。")
)

$result = $host.ui.PromptForChoice($title, $message, $options, 0)
switch ($result)
{
    0 {"「はい」が選ばれました。"; break}
    1 {"「すべて続行」が選ばれました。"; break}
    2 {"「いいえ」が選ばれました。"; break}
    3 {"「すべて無視」が選ばれました。"; break}
    4 {"「中断」が選ばれました。"; break}
}
```

ただ、選択肢の定義と分岐処理を記述する箇所が分離されていて、双方をインデックスで紐付けるというのはいささか簡潔さに欠ける気がする。


## どう書きたいの？

次のように書きたい。というか書けるようにした（`cSwitch`、`cCase`の実装は次節に書く）。

```.ps1
cSwitch "確認" @"
この操作を実行しますか?
対象 "C:\demo\PromptForChoice\hoge.txt" に対して操作 "ファイルの削除" を実行しています。
"@ @(
    cCase "はい(&Y)" "操作の次のステップのみを続行します。" {
        "「はい」が選ばれました。"
    }

    cCase "すべて続行(&A)" "操作のすべてのステップを続行します。" {
        "「すべて続行」が選ばれました。"
    }

    cCase "いいえ(&N)" "この操作をスキップし、次の操作に進みます。" {
        "「いいえ」が選ばれました。"
    }

    cCase "すべて無視(&L)" "この操作および後続のすべての操作をスキップします。" {
        "「すべて無視」が選ばれました。"
    }

    cCase "中断(&S)" "現在のコマンドを中断し、コマンドプロンプトに戻ります。" {
        "「中断」が選ばれました。"
    }
)
```


# 実装

```.ps1
filter Invoke-Choice
{
    Param
    (
        [Parameter(Mandatory=$true)]
        [string] $Caption,

        [Parameter(Mandatory=$true)]
        [string] $Message,

        [Parameter(Mandatory=$true)]
        [array] $Choices,

        [switch] $MultipleSelection
    )

    $tCollection = "System.Collections.ObjectModel.Collection"
    $tChoiceDescription = "System.Management.Automation.Host.ChoiceDescription"

    $collection = New-Object "${tCollection}[${tChoiceDescription}]"
    $defaultChoices = New-Object "System.Collections.Generic.List[Int]"
    for ($i = 0; $i -lt $Choices.Count; $i++)
    {
        $collection.Add($Choices[$i].ChoiceDescription)
        if ($Choices[$i].Default) {$defaultChoices.Add($i)}
    }
    
    if ($defaultChoices.Count -eq 0) {$defaultChoices.Add(0)}
    $default = if ($MultipleSelection) {,$defaultChoices}
               else {$defaultChoices[0]}

    foreach ($_ in $Host.UI.PromptForChoice($Caption, $Message, $collection, $default))
    {
        & $Choices[$_].ScriptBlock
    }
}

filter New-ChoiceCase
{
    Param
    (
        [Parameter(Mandatory=$true)]
        [string] $Label,

        [Parameter(Mandatory=$true)]
        [string] $HelpMessage,

        [Parameter(Mandatory=$true)]
        [scriptblock] $ScriptBlock,

        [switch] $Default
    )

    $tChoiceDescription = "System.Management.Automation.Host.ChoiceDescription"
    
    @{
        ChoiceDescription = New-Object $tChoiceDescription ($Label, $HelpMessage)
        ScriptBlock = $ScriptBlock
        Default = $Default
    }
}

Set-Alias cSwitch Invoke-Choice
Set-Alias cCase New-ChoiceCase
```

# 細かい使い方

* `cSwitch`は選択肢の表示と、ユーザが選んだ選択肢に応じたロジックの実行を行う関数。
    * 第一引数がキャプション（タイトル）
    * 第二引数がメッセージ
    * 第三引数が各選択肢(`cCase`)の配列
    * `MultipleSelection`スイッチを指定すると、複数選択の選択肢になる。

* `cCase`は選択肢と、その選択肢が選ばれた際に実行されるロジックをまとめるための関数。
    * 第一引数が選択肢のラベル
    * 第二引数が選択肢の説明
    * 第三引数がロジック
    * `Default`スイッチを指定すると、規定の選択肢になる。
        * どの選択肢も指定しない場合は自動的に先頭の選択肢が規定になる。


## 単一選択、既定選択肢自動の例

下記は、Yse か No **しか** 選べず、規定の選択肢が **Yse** となる選択肢メニューを表示する。

```
PS C:\demo\PromptForChoice> cSwitch "【質問】" "もっと簡潔に選択肢の表示を書きたいですか？" `
>> @(
>>     cCase "&Yes" "はい"   { "書きたいよね！" }
>>     cCase "&No"  "いいえ" { "そうでもないですか。。。" }
>> )

【質問】
もっと簡潔に選択肢の表示を書きたいですか？
[Y] Yes  [N] No  [?] ヘルプ (既定値は "Y"):
```

## 単一選択、既定選択肢指定の例

下記は、Yse か No **しか** 選べず、規定の選択肢が **No** となる選択肢メニューを表示する。

```
PS C:\demo\PromptForChoice> cSwitch "【質問】" "もっと簡潔に選択肢の表示を書きたいですか？" `
>> @(
>>     cCase "&Yes" "はい" { "書きたいよね！" }
>>     cCase -Default "&No" "いいえ" { "そうでもないですか。。。" }
>> )

【質問】
もっと簡潔に選択肢の表示を書きたいですか？
[Y] Yes  [N] No  [?] ヘルプ (既定値は "N"):
```

## 複数選択、既定選択肢自動の例

下記は、Yse と No **どちらも** 選べ、規定の選択肢が **Yse** となる選択肢メニューを表示する。

```
PS C:\demo\PromptForChoice> cSwitch -MultipleSelection "【質問】" "もっと簡潔に選択肢の表示を書きたいですか？" `
>> @(
>>     cCase "&Yes" "はい"   { "書きたいよね！" }
>>     cCase "&No"  "いいえ" { "そうでもないですか。。。" }
>> )

【質問】
もっと簡潔に選択肢の表示を書きたいですか？
[Y] Yes
[N] No
[?] ヘルプ
(既定は "Y" です)
選択肢 [0]:
```


## 複数選択、既定選択肢指定の例

下記は、Yse と No **どちらも** 選べ、規定の選択肢が **Yse と No** となる選択肢メニューを表示する。

```
PS C:\demo\PromptForChoice> cSwitch -MultipleSelection "【質問】" "もっと簡潔に選択肢の表示を書きたいですか？" `
>> @(
>>     cCase -Default "&Yes" "はい"   { "書きたいよね！" }
>>     cCase -Default "&No"  "いいえ" { "そうでもないですか。。。" }
>> )
>>

【質問】
もっと簡潔に選択肢の表示を書きたいですか？
[Y] Yes
[N] No
[?] ヘルプ
(既定の選択肢は Y,N です)
選択肢 [0]:
```