ステップ記録ツール(psr.exe)をコマンドラインから使いやすくしてみる(PowerShell・Register-ArgumentCompleter)

# 概要

PowerShell で ステップ記録ツール(psr.exe) のコマンドラインオプションの補完ができるようにしてみた。


## ステップ記録ツール(psr.exe) とは？

ステップ記録ツール(psr.exe)は、最近の Windows であれば標準で含まれているプログラム。
マウスなどで操作をする度に、スクリーンショットの取得＆操作した UI 要素の情報を記録し、保存できる。
(UI 要素の情報にはウィンドウのクラス名なども含まれており、使えるツールが限定される場面でも地味に役立ったりする)

プログラム自体は GUI ではあるが、コマンドラインオプションを指定することで、いくつか設定をカスタマイズできる。
しかし、一般的な CLI プログラムのように`psr.exe /?`などでヘルプを表示できないため、その場でオプションを確認、というのが難しい。

その解決のために、PowerShell の ArgumentCompleter の機能を使って入力補完を表示できるようにした。

## ArgumentCompleter とは？

PowerShell のタブ補完や IntelliSense の候補を列挙する機能。

通常は PowerShell 関数の作成時に作成者が設定するものだが、
[`Register-ArgumentCompleter`](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/register-argumentcompleter?view=powershell-5.1 "Register-ArgumentCompleter")コマンドレットを使用することで、ネイティブコマンド(.exe など)に後付けで入力補完を設定できる。

この記事では、[`Register-ArgumentCompleter`](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/register-argumentcompleter?view=powershell-5.1 "Register-ArgumentCompleter")コマンドレットを使用して、`psr.exe`に入力補完を設定した。

# 参考:psr.exe のコマンドラインオプション

以下のページの内容を元に作成。

> [Problems Step Recorder (PSR.exe) Command Line Options](https://social.technet.microsoft.com/Forums/office/en-US/b78253b1-6e38-4563-9efa-4973414e9a75/problems-step-recorder-psrexe-command-line-options?forum=w7itprogeneral "Problems Step Recorder (PSR.exe) Command Line Options")

| スイッチ | 指定する値 | 備考(参考ページの内容＆独自調査の結果) |
|:-------------|:-------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|`/start`|  | 「ステップ記録ツール」を起動し、記録を開始します。`/output`スイッチも指定する必要があります。 |
|`/stop`|  | 「ステップ記録ツール」を停止します。実行中の「ステップ記録ツール」のインスタンスがない場合は何も起きません。`/stop`スイッチを指定したとき、他のスイッチは指定できません。 |
|`/sc`|(0&#124;1) | 1(On) のとき、各ステップ毎にスクリーンショットを撮影します。0(Off) または 1(On) を指定します。既定値は 1(On) です。 |
|`/maxsc`| &lt;value&gt; | 記録されるスクリーンショットの最大数を指定します。 |
|`/maxlogsize`| &lt;value&gt; | 圧縮前のログファイルの容量を MB 単位で指定します。 |
|`/gui`| (0&#124;1) | 1(On) のとき、「ステップ記録ツール」のウィンドウを表示します。0(Off) または 1(On) を指定します。既定値は 1(On) です。0(Off) を指定する場合、`/start`スイッチも指定する必要があります。 |
|`/arcetl`| (0&#124;1) | 1(On) のとき、出力ファイルに etw ファイルを含めます。0(Off) または 1(On) を指定します。既定値は 0(Off) です。 |
|`/arcxml`| (0&#124;1) | 1(On) のとき、出力ファイルに xml ファイルを含めます。0(Off) または 1(On) を指定します。既定値は 0(Off) です。 |
|`/recordpid`| &lt;pid&gt; | 指定されたプロセス ID に関連した操作のみが記録されます。 |
|`/sketch`| (0&#124;1) | 1(On) のとき、スクリーンショットの代わりにスケッチを保存します。0(Off) または 1(On) を指定します。既定値は 0(Off) です。`/sc`スイッチに 0 が指定されている必要があります。 |
|`/slides`| (0&#124;1) | 1(On) のとき、出力される mht ファイルにスライドショーを設定します。0(Off) または 1(On) を指定します。既定値は 1(On) です。 |
|`/output`| &lt;filepath&gt; | 出力先のファイルパスを指定します。拡張子は .zip である必要があります。 |
|`/stopevent`| &lt;eventname&gt; | ログファイルが出力された後イベントを発生させます。 |

# psr.exe に入力補完を設定するコード

以下のコードを実行すると、その PowerShell インスタンス内では psr.exe について入力補完が表示されるようになります。

> 最新版(GitHub) : [NativeCommandCompleter/psr.ps1 at master · imihito/NativeCommandCompleter](https://github.com/imihito/NativeCommandCompleter/blob/master/ps1/psr.ps1 "NativeCommandCompleter/psr.ps1 at master · imihito/NativeCommandCompleter")
リポジトリ全体をモジュールとして使用する事が前提。

```powershell:psr.ps1
using namespace System
using namespace System.Collections.Specialized
using namespace System.Management.Automation
using namespace System.Management.Automation.Language

# [ステップ記録ツール(psr.exe)をコマンドラインから使いやすくしてみる(PowerShell・Register-ArgumentCompleter) - Qiita](https://qiita.com/nukie_53/items/58a5d0e4f33fb58a8bab "ステップ記録ツール(psr.exe)をコマンドラインから使いやすくしてみる(PowerShell・Register-ArgumentCompleter) - Qiita")
# Register-ArgumentCompleter に指定するスクリプトブロックを定義。
# 入力中のコマンド構文木を元に、入力候補をパイプライン出力する。
[scriptblock]$psrCompleter = {
    [CmdletBinding(HelpUri = 'https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/register-argumentcompleter')]
    [OutputType([System.Management.Automation.CompletionResult])]
    param (
        # 入力中のスイッチ。
        [Parameter(Position = 0)]
        [string]$commandName
        ,
        # 入力中のコマンド構文木。
        [Parameter(Position = 1)]
        [CommandAst]$wordToComplete
        ,
        # 補完を開始したカーソルの位置。
        [Parameter(Position = 2)]
        [int]$cursorPosition
    )
    # カーソルまでの CommandElementAst(includes cursor position ast)。
    # ○○.exe があるため最低でも1要素はある。
    [CommandElementAst[]]$astsOfBeforeCursor = @($wordToComplete.CommandElements |
        Where-Object -FilterScript {
            $_.Extent.StartOffset -le $cursorPosition
        }
    )
    
    [string[]]$allSwitchs = @(
        '/start'
        '/stop'
        '/sc'
        '/maxsc'
        '/maxlogsize'
        '/gui'
        '/arcetl'
        '/arcxml'
        '/recordpid'
        '/sketch'
        '/slides'
        '/output'
        '/stopevent'
    )
    
    # ツールチップの情報を取得
    # see also:https://social.technet.microsoft.com/Forums/office/en-US/b78253b1-6e38-4563-9efa-4973414e9a75/problems-step-recorder-psrexe-command-line-options?forum=w7itprogeneral
    [hashtable]$tooltipInfo = @{
        # For psr.exe ja-JP ToolTips
        # 日本語用ツールチップ文字列。
        # see also : https://social.technet.microsoft.com/Forums/office/en-US/b78253b1-6e38-4563-9efa-4973414e9a75/problems-step-recorder-psrexe-command-line-options?forum=w7itprogeneral
        '/start'      = '/start:「ステップ記録ツール」を起動し、記録を開始します。/output スイッチも指定する必要があります。'
        '/stop'       = '/stop:「ステップ記録ツール」を停止します。実行中の「ステップ記録ツール」のインスタンスがない場合は何も起きません。/stop スイッチを指定したとき、他のスイッチは指定できません。'
        '/sc'         = '/sc (0|1):1(On) のとき、各ステップ毎にスクリーンショットを撮影します。0(Off) または 1(On) を指定します。既定値は 1(On) です。'
        '/maxsc'      = '/maxsc <value>:記録されるスクリーンショットの最大数を指定します。'
        '/maxlogsize' = '/maxlogsize <value>:圧縮前のログファイルの容量を MB 単位で指定します。'
        '/gui'        = '/gui (0|1):1(On) のとき、「ステップ記録ツール」のウィンドウを表示します。0(Off) または 1(On) を指定します。既定値は 1(On) です。0(Off) を指定する場合、/start スイッチも指定する必要があります。'
        '/arcetl'     = '/arcetl (0|1):1(On) のとき、出力ファイルに etw ファイルを含めます。0(Off) または 1(On) を指定します。既定値は 0(Off) です。'
        '/arcxml'     = '/arcxml (0|1):1(On) のとき、出力ファイルに xml ファイルを含めます。0(Off) または 1(On) を指定します。既定値は 0(Off) です。'
        '/recordpid'  = '/recordpid <pid>:指定されたプロセス ID に関連した操作のみが記録されます。'
        '/sketch'     = '/sketch (0|1):1(On) のとき、スクリーンショットの代わりにスケッチを保存します。0(Off) または 1(On) を指定します。既定値は 0(Off) です。/sc スイッチに 0 が指定されている必要があります。'
        '/slides'     = '/slides (0|1):1(On) のとき、出力される mht ファイルにスライドショーを設定します。0(Off) または 1(On) を指定します。既定値は 1(On) です。'
        '/output'     = '/output <filepath>:出力先のファイルパスを指定します。拡張子は .zip である必要があります。'
        '/stopevent'  = '/stopevent <eventname>:ログファイルが出力された後イベントを発生させます。'

        # for /sc|gui|arcetl|arcxml|sketch|slides Switch.
        '0'           = '指定したスイッチを無効にします。'
        '1'           = '指定したスイッチを有効にします。'
    }
    
    if ($astsOfBeforeCursor.Extent.Text -icontains '/stop') {
        # /stop スイッチが指定されているとき、他のスイッチは全て無効のため、補完を停止。
        return
    }
    
    # 最後から1個目、2個目の情報を取得。
    # 指定しているスイッチと、その値を想定。
    [string]$last1Token = $astsOfBeforeCursor[-1].Extent.Text
    [string]$last2Token = if ($astsOfBeforeCursor.Length -ge 2) {
        $astsOfBeforeCursor[-2].Extent.Text
    } else {
        [string]::Empty
    }
    # 指定したパラメータが入力中かどうか。
    [scriptblock]$swtichIsInputting = {
        param ([string[]]$Switches)
        return (
            # カーソル前の最後の要素がスイッチかつ、カーソルがスイッチの後ろ(これから入力しようとしている場合)。
            [string]::IsNullOrEmpty($commandName) -and 
            ($last1Token -iin $Switches)
        ) -or (
            # 入力中の値を変更しようとしている場合。
            -not [string]::IsNullOrEmpty($commandName) -and 
            ($last2Token -iin $Switches)
        )
    }
    
    [string[]]$disableCompletionParameters = @(
        '/maxsc', '/maxlogsize', '/output', '/stopevent'
    )
    
    if (& $swtichIsInputting $disableCompletionParameters) {
        # カーソルの直前が任意入力のスイッチだったら、補完を停止。
        return
    }

    [string[]]$onOffParameters = @(
        '/sc', '/gui', '/arcetl', '/arcxml', '/sketch', '/slides'
    )
    if (& $swtichIsInputting $onOffParameters) {
        # カーソルの直前が 0, 1 を指定するスイッチだったら、0, 1 を表示。
        [CompletionResult]::new('0', '0 : Off', [CompletionResultType]::ParameterValue, $tooltipInfo['0'])
        [CompletionResult]::new('1', '1 : On' , [CompletionResultType]::ParameterValue, $tooltipInfo['1'])
        return
    }
    if (& $swtichIsInputting '/recordpid') {
        # カーソルの直前が /recordpid だったら、ウィンドウを持っているプロセスの ID をリストアップ。
        Get-Process |
            Where-Object -Property MainWindowHandle -NE -Value ([IntPtr]::Zero) |
            ForEach-Object -Process {
                [string]$procInfo = '{0} : {1} - {2}' -f $_.Id, $_.Name, $_.MainWindowTitle
                [CompletionResult]::new($_.Id, $procInfo, [CompletionResultType]::ParameterValue, $procInfo)
            }
        return
    }

    
    # 今の位置のスイッチ or 入力中の文字列にマッチするスイッチを取得。
    [string[]]$assginedParams = $wordToComplete.CommandElements.Extent.Text
    [string[]]$showSwitchs = @(
        # 補完開始位置にすでにスイッチがあればそれを優先。
        if ($tooltipInfo.Contains($commandName)) {
            $commandName
        }
        [string[]]$tmpFilterdSwitch = $allSwitchs
        [string[]]$exclusiveSwitch = @('/start', '/stop')
        # any contains.
        if ($exclusiveSwitch.Where({$assginedParams -icontains $_}).Count -ne 0) {
            $tmpFilterdSwitch = $allSwitchs.Where({$_ -inotin $exclusiveSwitch})
        }
        $tmpFilterdSwitch.Where({
            # まだ指定されていないスイッチかつ、入力中の文字列に一致するものを探す。
            ($assginedParams -inotcontains $_) -and
            $_ -imatch [regex]::Unescape($commandName)    
        })
    )
    if ($showSwitchs.Length -eq 0) {
        # 入力中の文字列が無かったり、マッチするスイッチが無かった場合は、指定していないスイッチ全部。
        $showSwitchs = $showSwitchs = $tmpFilterdSwitch.Where({$assginedParams -inotcontains $_}).ForEach({$_.ToString()})
    }
    $showSwitchs | 
        Select-Object -Unique | 
        ForEach-Object -Process {
            [CompletionResult]::new($_, $_, [CompletionResultType]::ParameterName, $tooltipInfo[$_])
        }
    return
}

# 定義したスクリプトブロックを psr.exe に紐付けて登録。
Register-ArgumentCompleter -CommandName psr.exe -Native -ScriptBlock $psrCompleter
```

## 動作イメージ

Windows PowerShell ISE 上での動作イメージ。
コンソール上では`PSReadline`がインストールされていれば、アイコン以外は表示される。
Visual Studio Code ではツールチップ以外は表示される。

![psr.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142842/e51ec0f8-3e16-d8e3-9b31-61719c41af52.gif)

## ハマったところ

~~入力中の文字列の最後に空白があっても`$wordToComplete`には、空白が含まれないっぽい挙動のため、スイッチの後の値指定の箇所で手間取った。~~

~~特に`/output`スイッチの対応がうまくいかなかった。
`/output`スイッチの後ろはファイルパスを指定したいため、自前の補完ではなくPowerShell側の補完機能を使いたい。
そのためには、自前の補完を切る(候補を出さない)必要があるが、補完を復活させるタイミングの制御が難しい。~~

~~191231時点の実装では、パスの後ろにスペースを空けて`/`などを入力すれば、補完が表示されるようになっている。~~

上記の問題は、`$wordToComplete`の型を`string`だと誤認していたことが原因。
実際の型は[`System.Management.Automation.LanguageCommandAst`](https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.language.commandast?view=pscore-6.2.0 "CommandAst Class (System.Management.Automation.Language) | Microsoft Docs")であり、この型を使うことでより詳細な情報を取得できる。

# 参考ページ

> [Windows ステップ記録ツール(psr)のススメ - Qiita](https://qiita.com/gzock/items/1c934d6577eec3b7f7ff "Windows ステップ記録ツール(psr)のススメ - Qiita")

psr.exe のラッパーバッチや、psr.exe そのものの罠についても記載されているため、こちらの記事も一度読んでおくといいと思います。


> [問題ステップ記録ツール (PSR.exe) について - yaimairiの備忘録](http://yaimairi.hateblo.jp/entry/2016/08/22/004029 "問題ステップ記録ツール (PSR.exe) について - yaimairiの備忘録")
> [Problems Step Recorder (PSR.exe) Command Line Options](https://social.technet.microsoft.com/Forums/office/en-US/b78253b1-6e38-4563-9efa-4973414e9a75/problems-step-recorder-psrexe-command-line-options?forum=w7itprogeneral "Problems Step Recorder (PSR.exe) Command Line Options")

コマンドラインオプションの情報はこちらから確認しました。

> [問題を再現する手順の記録 - Windows ヘルプ](https://support.microsoft.com/ja-jp/help/22878/windows-10-record-steps "問題を再現する手順の記録 - Windows ヘルプ")

MS公式の説明(GUI 部分のみ)。