【PowerShell】PsReadLine 設定のススメ

（2025-03-11追記）最新状況はコチラ：

https://github.com/AWtnb/PowerShell/blob/main/cmdlets/_psreadline.ps1

---

PowerShell ユーザーの人権と言っても過言ではない存在、それが [PsReadLine](https://github.com/PowerShell/PSReadLine) 。PowerShell 5.0 から標準で同梱されるようになり、コマンドの着色やキーバインド、補完の表示形式など諸々を設定できるツールです。

「 PowerShell 使いにくい！！」という怨嗟の声もチラホラ耳にしますが、これを活用することで解決できる問題もあるのではないでしょうか。

以下、大半は [公式のサンプル](https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1) をもとに個人的にカスタムしているものです。

## ポイント

1. コマンドラインの状態を取得する：`[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState()`
    + 下記のように参照渡しすると、 `$line` でその時点で表示されているコマンド文字列、 `$cursor` でカーソル位置を取得できるようになります。

    ```PowerShell
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
    ```

1. 選択状態を取得する： `[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState()`
    + 下記の参照渡しで選択範囲の開始位置と終了位置を取得できます。何も選択していない状態では `$selectionStart` は -1 になります。

    ```PowerShell
    $selectionStart = $null
    $selectionLength = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
    ```

なお、 `-key` の指定で Shift キーによる修飾は、対象文字を大文字にすることで表現します。 `ctrl+v` は Ctrl と v ですが `ctrl+V` は Ctrl と Shift と v に相当する形です。

## 用途別コード

### 履歴の管理

重複した履歴を保存しないようにするには下記のようにします。

```PowerShell
Set-PSReadlineOption -HistoryNoDuplicates
```

その他、履歴に残さない条件も指定することができます。下記では `SKIPHISTORY` の文字が含まれるコマンド（用途は後述）、アルファベット1文字だけのコマンド、終了コマンドを保存しないようにしています。

```PowerShell
Set-PSReadlineOption -AddToHistoryHandler {
    param ($command)
    switch -regex ($command) {
        "SKIPHISTORY" {return $false}
        "^[a-z]$" {return $false}
        "exit" {return $false}
    }
    return $true
}
```

### 単語区切り

Ctrl + 矢印でカーソル移動するときの単語区切りです。デフォルトでは `;:,.[]{}()/\|^&*-=+'" + "\u2013\u2014\u2015` とのこと。日本語に対応させて業務上よく使う約物を追加しています。

```PowerShell
Set-PSReadLineOption -WordDelimiters ";:,.[]{}()/\|^&*-=+'`" !?@#$%&_<>「」（）『』『』［］、，。：；／"
```

### 括弧／引用符の入力補完

#### 括弧
+ 文字列選択時：  
    選択範囲を括弧で囲みカーソルを閉じ括弧の後ろに移動させる
+ 非選択時：
    + 開き括弧に対応した閉じ括弧を入力して間にカーソルを移動させる
    + 一方の括弧が先に入力されていた場合は対応する括弧のみ入力する


```PowerShell
Set-PSReadLineKeyHandler -Key "(","{","[" -BriefDescription "InsertPairedBraces" -LongDescription "Insert matching braces or wrap selection by matching braces" -ScriptBlock {
    param($key, $arg)
    $openChar = $key.KeyChar
    $closeChar = switch ($openChar) {
        <#case#> "(" { [char]")"; break }
        <#case#> "{" { [char]"}"; break }
        <#case#> "[" { [char]"]"; break }
    }

    $selectionStart = $null
    $selectionLength = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

    if ($selectionStart -ne -1) {
        [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $openChar + $line.SubString($selectionStart, $selectionLength) + $closeChar)
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
        return
    }
    $nOpen = [regex]::Matches($line, [regex]::Escape($openChar)).Count
    $nClose = [regex]::Matches($line, [regex]::Escape($closeChar)).Count
    if ($nOpen -ne $nClose) {
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($openChar)
    }
    else {
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($openChar + $closeChar)
        [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor - 1)
    }
}
```

#### 余計な閉じ括弧を入力しない
下記のようにすることで、既に閉じ括弧が入力されている状態では代わりにカーソルが動くのみになります。
上記のように自動で閉じ括弧を補完すると、手癖で入力したときに余計な閉じ括弧を入力してしまうことがあるので、安全のための設定です。

```PowerShell
Set-PSReadLineKeyHandler -Key ")","]","}" -BriefDescription "SmartCloseBraces" -LongDescription "Insert closing brace or skip" -ScriptBlock {
    param($key, $arg)

    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

    if ($line[$cursor] -eq $key.KeyChar) {
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
    }
    else {
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($key.KeyChar)
    }
}
```

#### 引用符
上記の括弧まわりの入力補完を引用符にも適用しています。こちらは開き／閉じの区別がないので少し単純に書けます。

```PowerShell
Set-PSReadLineKeyHandler -Key "`"","'" -BriefDescription "smartQuotation" -LongDescription "Put quotation marks and move the cursor between them or put marks around the selection" -ScriptBlock {
    param($key, $arg)
    $mark = $key.KeyChar

    $selectionStart = $null
    $selectionLength = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

    if ($selectionStart -ne -1) {
        [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $mark + $line.SubString($selectionStart, $selectionLength) + $mark)
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
        return
    }

    if ($line[$cursor] -eq $mark) {
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
        return
    }

    $nMark = [regex]::Matches($line, $mark).Count
    if ($nMark % 2 -eq 1) {
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($mark)
    }
    else {
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($mark + $mark)
        [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor - 1)
    }
}
```

### コマンドを括弧で囲む
文字列選択時は選択文字を丸括弧で囲み、非選択時はその時点のコマンドライン全体をカッコで囲みます。
`Get-Childitem -file` と入力してから `(Get-Childitem -file).LastWriteTime` と特定のプロパティだけ取り出したいときなどにカーソルの移動量が減ります。

```PowerShell
Set-PSReadLineKeyHandler -Key "alt+w" -BriefDescription "WrapLineByParenthesis" -LongDescription "Wrap the entire line and move the cursor after the closing parenthesis or wrap selected string" -ScriptBlock {
    $selectionStart = $null
    $selectionLength = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
    if ($selectionStart -ne -1) {
        [Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, "(" + $line.SubString($selectionStart, $selectionLength) + ")")
        [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
    }
    else {
        [Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, $line.Length, '(' + $line + ')')
        [Microsoft.PowerShell.PSConsoleReadLine]::EndOfLine()
    }
}
```

### メソッドの補完時に閉じ括弧を忘れない
（Shift + ）Tab でメソッドを補完するとき、デフォルトでは `.ToString(` などと開き括弧しか表示されないのが気になったので閉じ括弧も補完するようにしています。

```PowerShell
Remove-PSReadlineKeyHandler "tab"
Set-PSReadLineKeyHandler -Key "tab" -BriefDescription "smartNextCompletion" -LongDescription "insert closing parenthesis in forward completion of method" -ScriptBlock {
    [Microsoft.PowerShell.PSConsoleReadLine]::TabCompleteNext()
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

    if ($line[($cursor - 1)] -eq "(") {
        if ($line[$cursor] -ne ")") {
            [Microsoft.PowerShell.PSConsoleReadLine]::Insert(")")
            [Microsoft.PowerShell.PSConsoleReadLine]::BackwardChar()
        }
    }
}

Remove-PSReadlineKeyHandler "shift+tab"
Set-PSReadLineKeyHandler -Key "shift+tab" -BriefDescription "smartPreviousCompletion" -LongDescription "insert closing parenthesis in backward completion of method" -ScriptBlock {
    [Microsoft.PowerShell.PSConsoleReadLine]::TabCompletePrevious()
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

    if ($line[($cursor - 1)] -eq "(") {
        if ($line[$cursor] -ne ")") {
            [Microsoft.PowerShell.PSConsoleReadLine]::Insert(")")
            [Microsoft.PowerShell.PSConsoleReadLine]::BackwardChar()
        }
    }
}
```

### プロファイルの再読み込み
プロファイルを更新するショートカットキーを用意しています。プロファイルから読み込んでいる自作コマンドレットを手直ししたときなど、すぐに修正をターミナルに反映させられるので便利です。
上記設定で `SKIPHISTORY` の文字が含まれるコマンドを履歴に残さないようにしているので、このショートカットキーで入力された内容は履歴を汚しません。

```PowerShell
Set-PSReadLineKeyHandler -Key "alt+r" -BriefDescription "reloadPROFILE" -LongDescription "reloadPROFILE" -ScriptBlock {
    [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
    [Microsoft.PowerShell.PSConsoleReadLine]::Insert('<#SKIPHISTORY#> . $PROFILE')
    [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}
```

### 直前に使用した変数を利用する
デフォルトでは Alt + . （ linux と同じらしいですね）で直前のコマンドの最終要素を入力することができますが、 PowerShell では変数に `$` をつける必要があるので一工夫しています。

コマンドの出力を確かめてから ` | sv hoge` をつけて変数に格納することが多いので重宝しています。

```PowerShell
Set-PSReadLineKeyHandler -Key "alt+a" -BriefDescription "yankLastArgAsVariable" -LongDescription "yankLastArgAsVariable" -ScriptBlock {
    [Microsoft.PowerShell.PSConsoleReadLine]::Insert("$")
    [Microsoft.PowerShell.PSConsoleReadLine]::YankLastArg()
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
    if ($line -match '\$\$') {
        $newLine = $line -replace '\$\$', "$"
        [Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, $line.Length, $newLine)
    }
}
```

### クリップボード内容を変数に格納する
簡単な文字列操作はコンソール上で済むことが多いので、 Ctrl + Shift + v でその時点のクリップボード内容を `$CLIPPING` という変数に格納できるようにしています。 `AddToHistory()` で直近の履歴として `$CLIPPING ` を挿入しているため、 Ctrl + Shift + v で変数に格納後に上キーを押すだけでクリップボード内容の編集モードに入れます。

```powershell
Set-PSReadLineKeyHandler -Key "ctrl+V" -BriefDescription "setClipString" -LongDescription "setClipString" -ScriptBlock {
    $command = "<#SKIPHISTORY#> get-clipboard | sv CLIPPING"
    [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
    [Microsoft.PowerShell.PSConsoleReadLine]::Insert($command)
    [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory('$CLIPPING ')
    [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}
```


このほかにも [豊富なサンプル](https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/SamplePSReadLineProfile.ps1) が公開されています。自分好みの PowerShell ターミナルを作ってみましょう！