マイナビニュースマイナビ

PowerShell 7をインタラクティブシェルとして使う - PSReadLineでbash的機能を獲得

【連載】

PowerShell Core入門 - 基本コマンドの使い方

【第137回】PowerShell 7をインタラクティブシェルとして使う - PSReadLineでbash的機能を獲得

[2021/02/26 08:00]後藤大地 ブックマーク ブックマーク

本連載の第134回でPowerShellのショートカットキーについて取り上げた。しかし、PowerShell Core以降のバージョンに詳しいユーザーは少し違和感を抱いたかもしれない。「PSReadLine」があるじゃないか、と。実は比較的新しいバージョンのPowerShellにはPSReadLineというモジュールが最初から取り込まれている。

最初からバンドルされているPSReadLineモジュール

PSReadLineモジュールはbashのreadlineのアイデアに基づいて開発されたもので、要するにPowerShellでbashのようなショートカットキーを使えるようにしよう、ということだ。PowerShell 7を使っているならPSReadLine 2.0.0が同梱されており、上記スクリーンショットのようにGet-Moduleコマンドレットで最初から読み込まれていることを確認できる。

PSReadLineモジュールが提供している機能はGet-PSReadLineKeyHandlerコマンドレットを実行することで手っ取り早く知ることができる。

Get-PSReadLineKeyHandlerコマンドレットの実行結果

PSReadLineモジュールの提供する機能は、bashのreadlineが提供している機能と似たようなものが用意されている。まったく同じようにとはいかないが、これを使うと使わないのとでは操作効率がかなり変わってくる。PSReadLineモジュールはカスタマイズを行うことができる点も魅力だ。

PSReadLineモジュール

PSReadLineモジュールはPowerShell 3以降のバージョンのPowerShellのライン編集機能を置き換えるためのモジュール。UNIX系OSで使われることが多いインタラクティブシェルであるbashやzshに発想を得たモジュールで、デフォルトのショートカットキー設定はWindowsでよく使われるものに合わせてあり扱いやすい。

PSReadLineモジュールが提供する主な機能は次の通りだ。

  • シンタックスカラー
  • シンタックスエラー通知
  • 複数行編集機能
  • カスタマイズ可能なキーバインディング
  • モード(Windows、Vi、Emacs)
  • コマンド履歴検索機能
  • コマンド履歴検索機能(インタラクティブ)
  • Emacs yank/killリング
  • PowerShellトークンベースの単語ベース移動機能
  • アンドゥ/リドゥ機能
  • コマンド履歴の自動保存機能
  • ライブセッションを跨いだコマンド履歴データの共有
  • 補完候補メニュー表示機能
  • 各種設定オプション


本稿執筆時点の最新リリース版はPSReadLine v2.1.0 GAだ。開発は継続しており、同じく執筆時点ではPSReadLine v2.2.0 Beta1が公開されている。PowerShellに新しい機能が追加されるのに合わせて、PSReadLineにも機能が追加されており、細かい修正や機能追加なども随時行われている。

PowerShell 7.1にはPSReadLine 2.0.0が取り込まれているが、次のバージョンではPSReadLineのバージョンもアップデートされ、新しい機能が使えるものと見られる。

PSReadLineの機能を使ってみよう

PSReadLineの機能を使いこなせるようになることは、インタラクティブシェルとしてPowerShellを効率的に使っていく上で欠かせない。bashやzshとまったく同じキー操作ではないので、UNIX系OSの操作に慣れている場合は少し練習するか、またはキーバインディングを変更する必要があるが、その投資は悪くない。

PSReadLineについては機能を詳しく取り上げていくとして、今回は特徴的な機能を1つ紹介しておく。デフォルトのPowerShellではTabキーが補完機能に結び付いている。例えば、次のようにパラメータの最初の指定となる「-」を入力した段階で「Tab」キーを押すと入力補完機能が動き出す。

「Tab」キーによって入力補完機能が動き出す

次のように補完された結果が表示されていることがわかる。この場合、パラメータは複数存在するので、「Tab」キーを連続して押していくとさらに次の候補を表示させることができる。

「Tab」キーを押すことで次の補完候補が表示される

さらに次の補完候補が表示された状態

「Tab」キーによる入力補完はインタラクティブシェルのみならず、現在ではさまざまなソフトウエアで機能が提供されている。「Tab」キーを押すとなんとなく入力が補完される、そんな状況だ。

PSReadLineは補完候補が複数存在した場合にメニュー形式で候補を選択できるようにする機能が備わっている。多機能インタラクティブシェルには大体備わっている機能だ。PowerShellでも同じことができるようになる。

この機能はデフォルトでは「Ctrl」+「Space」または「Ctrl」+「@」に紐付けられている。例えば、次のようにパラメータを入力する状態になってから「Ctrl」+「Space」を押す。

パラメータ入力の段階で「Ctrl」+「Space」を押す

そうすると次のようにパラメータ一覧がメニュー形式で表示される。表示されたメニューはカーソルキーで行き来することができる。

カーソルキーで対象を選び「Enter」キーで選択

使いたい対象を選んだら「Enter」キーを押す。選択はスペースキーでも可能。

「Enter」キーやスペースキーで選択する

「Ctrl」+「Space」で補完候補を表示させることができると、いちいちパラメータを繰っていく必要がなく、どのようなパラメータが使えるのか一気に調べることができる。ヘルプを引く場合には一旦入力を中止する必要があるが、この方法であればコマンドレットを入力している途中で対象を調べることができて便利なのだ。

こんな感じでPSReadLineには便利な機能が実装されている。PSReadLineを使いこなすことができるようになると、インタラクティブシェルとしてのPowreShellの利便性はぐっと引き上がる。今後しばらくはPSReadLineの機能を紹介していく。

なお、PSReadLineがデフォルトで提供している機能は付録として以下にまとめておく。参考にしてもらえれば幸いだ。

付録: PSReadLineショートカットキー一覧

キー 機能
Enter 入力行を実行しカーソルを次の行へ移動。入力が閉じていない場合には入力行を実行せずにカーソルを次の行へ移動
Shift-Enter 入力行を実行せずにカーソルを次の行へ移動
Ctrl-Enter 入力行を実行せずに前の行に空行を挿入
Ctrl-Shift-Enter 入力行を実行せずに次の行に空行を挿入
Backspace カーソルよりひとつ前の文字を削除
Ctrl-h カーソルよりひとつ前の文字を削除
Delete カーソル下の文字を削除
Ctrl-Home カーソルから行頭までを削除
Ctrl-End カーソルから行末までを削除
Ctrl-Backspace カーソルから単語の先頭までを削除
Ctrl-w カーソルから単語の先頭までを削除
Ctrl-Delete カーソルから単語の末尾までを削除
Alt-d カーソルから単語の末尾までを削除
Ctrl-v システムクリップボードのテキストを貼り付け
Shift-Insert システムクリップボードのテキストを貼り付け
Ctrl-Shift-c 選択したテキストをシステムクリップボードへコピー
Ctrl-c 選択したテキストをシステムクリップボードへコピー。テキストが選択されていなかった場合には行の編集をキャンセル
Ctrl-x 選択したテキストを削除
Ctrl-z アンドゥ
Ctrl-y リドゥ
ESC すべてアンドゥ
Alt-. 直前実行の最後の引数を貼り付け
キー 機能
カーソルを左へ移動
カーソルを右へ移動
Ctrl-← カーソルを左の単語の先頭へ移動
Ctrl-→ カーソルを右の単語の先頭へ移動
Home カーソルを行頭へ移動
End カーソルを行末へ移動
Ctrl-] カーソルを対応するカッコへ移動
キー 機能
コマンド履歴のひとつ前のコマンドへ入れ替え
コマンド履歴のひとつ後のコマンドへ入れ替え
F8 コマンド履歴検索(古い履歴へ向かって)
Shift-F8 コマンド履歴検索(新しい履歴へ向かって)
Ctrl-r コマンド履歴検索(古い履歴へ向かってインタラクティブに検索)
Ctrl-s コマンド履歴検索(新しい履歴へ向かってインタラクティブに検索)
Alt-F7 コマンド履歴をクリア
キー 機能
Ctrl-@ 補完。補完対象が複数ある場合にはメニュー形式で候補を表示
Ctrl-Space 補完。補完対象が複数ある場合にはメニュー形式で候補を表示
Tab 補完。次の補完候補を表示
Shift-Tab 補完。前の補完候補を表示
キー 機能
Ctrl-a 行全部を選択し、カーソルを行末へ移動
Shift-← カーソルの選択位置をひとつ左へ移動
Shift-→ カーソルの選択位置をひとつ右へ移動
Shift-Home カーソルから行頭までを選択
Shift-End カーソルから行末までを選択
Shift-Ctrl-← カーソルからひとつ前の単語の先頭までを選択
Shift-Ctrl-→ カーソルからひとつ後の単語の先頭までを選択
キー 機能
Ctrl-a 行全部を選択し、カーソルを行末へ移動
Shift-← カーソルの選択位置をひとつ左へ移動
Shift-→ カーソルの選択位置をひとつ右へ移動
Shift-Home カーソルから行頭までを選択
Shift-End カーソルから行末までを選択
Shift-Ctrl-← カーソルからひとつ前の単語の先頭までを選択
Shift-Ctrl-→ カーソルからひとつ後の単語の先頭までを選択
キー 機能
Ctrl-l スクリーンのクリアと再描画およびカレント行をスクリーン上部へ移動
Alt— 数値を0回繰り返し入力
Alt-0 数値を0回繰り返し入力
Alt-1 数値を1回繰り返し入力。または追加指定した分繰り返し入力
Alt-2 数値を2回繰り返し入力。または追加指定した分繰り返し入力
Alt-3 数値を3回繰り返し入力。または追加指定した分繰り返し入力
Alt-4 数値を4回繰り返し入力。または追加指定した分繰り返し入力
Alt-5 数値を5回繰り返し入力。または追加指定した分繰り返し入力
Alt-6 数値を6回繰り返し入力。または追加指定した分繰り返し入力
Alt-7 数値を7回繰り返し入力。または追加指定した分繰り返し入力
Alt-8 数値を8回繰り返し入力。または追加指定した分繰り返し入力
Alt-9 数値を9回繰り返し入力。または追加指定した分繰り返し入力
PageDown スクリーンを1画面分下へスクロール
PageUp スクリーンを1画面分上へスクロール
Ctrl-PageDown スクリーンを1行分下へスクロール
Ctrl-PageUp スクリーンを1行分上へスクロール
Alt-? キーに割り当てられている機能を表示(指定したキー)
Ctrl-Alt-? キーに割り当てられている機能を表示(すべて)

付録: $PROFILE

本連載時点での$PROFILEを次に掲載しておく。

$PROFILE

#========================================================================
# Set encoding to UTF-8
#========================================================================
# [System.Console]::OutputEncoding is set to local encoding, so character 
# corruption occurs when piped from WSL to WSL. Therefore, set 
# [System.Console]::OutputEncoding and $OutputEncoding to UTF-8 to avoid 
# the problem.
$OutputEncoding = [System.Console]::OutputEncoding =
    [System.Text.UTF8Encoding]::new()

#========================================================================
# Definition of Linux commands used via wsl
#========================================================================
# Linux pagers
$_linux_pagers = @("less", "lv")

# Linux PATH and commands
$_linux_path = (wsl echo '$PATH').Split(":") -NotMatch "/mnt"
$_linux_command_paths = (
    wsl ls -d ($_linux_path[($_linux_path.Length - 1)..0] -replace "$","/*")
) 2> $null

# Generate Linux commands functions
ForEach($n in $_linux_command_paths) {
    $_n = (Split-Path -Leaf $n)
    $_linux_functions += "
        function $_n {
            if (`$Input.Length) {
                `$Input.Reset()
                `$Input | wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
            }
            else {
                wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
            }
        }"
}

# Generate Linux pagers functions
ForEach($_n in $_linux_pagers) {
    $_linux_functions += "
        function $_n {
            if (`$Input.Length) { 
                `$Input.Reset(); 

                # Prepare temporary file path
                `$_temp = New-TemporaryFile

                # Write data from pipeline to the temporary file
                `$Input | Out-File `$_temp

                # Do $_n
                wsl $_n `$(_path_to_linux `$Args).Split(' ') ``
                    `$(_path_to_linux `$_temp.ToString()).Split(' ')

                # Delete unnecessary temporary file and variable
                Remove-Item `$_temp
                Remove-Variable _temp
            } 
            else { 
                wsl $_n `$(_path_to_linux `$Args).Split(' ')
            }
        }"
}

# Function that converts Windows paths to Linux paths
$_linux_functions += @'
    function _path_to_linux {
        $linuxpath = @()

        # Convert arguments to Linux path style
        ForEach($winpath in $Args) {
            if ($winpath -eq $null) {
                Break
            }

            # Change drive path to mount path
            if ($winpath -match '^[A-Z]:') {
                $drive = $winpath.Substring(0,1).ToLower()
                $linuxpath += "/mnt/" + $drive + $winpath.Substring(2).Replace('\','/')
            }
            # Option is not converted
            elseif ($winpath -match '^[-+]') {
                $linuxpath += $winpath
            }
            # Other argument is converted
            else {
                $linuxpath += ([String]$winpath).Replace('\','/')
            }
        }

        $linuxpath
    }
'@

# Prepare temporary file path with extension .ps1
$_temp = New-TemporaryFile
$_temp_ps1 = $_temp.FullName + ".ps1"
Remove-Item $_temp

# Write function definition to temporary .ps1 file and parse
$_linux_functions | Out-File $_temp_ps1
. $_temp_ps1
Remove-Item $_temp_ps1

# Delete unnecessary variables
Remove-Variable n
Remove-Variable _n
Remove-Variable _temp
Remove-Variable _temp_ps1
Remove-Variable _linux_path
Remove-Variable _linux_command_paths
Remove-Variable _linux_functions

#========================================================================
# Individual Linux command function definitions
#========================================================================
# grep
function grep {
    $pattern_exists = $False
    $path_exists = $False
    $skip = $False
    $i = 0

    ForEach($a in $Args) {
        if ($skip) {
            $skip = $False
            $i++
            continue
        }

        # Options without argumetn
        if ($a -cmatch '^-[abcdDEFGHhIiJLlmnOopqRSsUVvwxZ]') {
        }
        # Options with argument
        elseif ($a -cmatch '^-[ABC]') {
            $skip = $True
        }
        # Pattern file specification option
        elseif ($a -ceq '-f') {
            $skip = $True
            $pattern_exists = $True
            $Args[$i+1] = _path_to_linux $Args[$i+1]
        }
        # Pattern specification option
        elseif ($a -ceq '-e') {
            $skip = $True
            $pattern_exists = $True
        }
        # Pattern or file path
        elseif ($a -cnotmatch '^-') {
            if ($pattern_exists) {
                $path_exists = $True
            }
            else {
                $pattern_exists = $True
            }
        }

        $i++
    }

    # Change file path
    if ($path_exists) {
        $Args[-1] = _path_to_linux $Args[-1]
    }

    $Input | wsl grep $Args
}

# ls
Get-Alias ls *> $null && Remove-Item alias:ls
function ls { wsl ls --color=auto $(_path_to_linux $Args).Split(' ') }
function ll { ls -l $(_path_to_linux $Args).Split(' ') }
function la { ls -a $(_path_to_linux $Args).Split(' ') }

#========================================================================
# Alias definition
#========================================================================
Set-Alias -Name open -Value explorer
Set-Alias -Name edge -Value "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
Set-Alias -Name chrome -Value "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"

Get-Alias man *> $null && Remove-Item alias:man

#========================================================================
# cd shortcuts
#========================================================================
function cd1 { cd (dir -Directory)[-1] }
function cd2 { cd (dir -Directory)[-2] }
function cd3 { cd (dir -Directory)[-3] }
function cd4 { cd (dir -Directory)[-4] }
function cd5 { cd (dir -Directory)[-5] }
function cd6 { cd (dir -Directory)[-6] }
function cd7 { cd (dir -Directory)[-7] }
function cd8 { cd (dir -Directory)[-8] }
function cd9 { cd (dir -Directory)[-9] }
function cd10 { cd (dir -Directory)[-10] }
function cd11 { cd (dir -Directory)[-11] }
function cd12 { cd (dir -Directory)[-12] }
function cd13 { cd (dir -Directory)[-13] }
function cd14 { cd (dir -Directory)[-14] }
function cd15 { cd (dir -Directory)[-15] }
function cd16 { cd (dir -Directory)[-16] }
function cd17 { cd (dir -Directory)[-17] }
function cd18 { cd (dir -Directory)[-18] }
function cd19 { cd (dir -Directory)[-19] }
function cd20 { cd (dir -Directory)[-20] }
#function cd1 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-1] }
#function cd2 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-2] }
#function cd3 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-3] }
#function cd4 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-4] }
#function cd5 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-5] }
#function cd6 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-6] }
#function cd7 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-7] }
#function cd8 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-8] }
#function cd9 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-9] }
#function cd10 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-10] }
#function cd11 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-11] }
#function cd12 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-12] }
#function cd13 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-13] }
#function cd14 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-14] }
#function cd15 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-15] }
#function cd16 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-16] }
#function cd17 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-17] }
#function cd18 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-18] }
#function cd19 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-19] }
#function cd20 { cd (dir | Select-String -Pattern '[0-9]{8}$')[-20] }
function doc { cd ~/Documents/ }    
function dwm { cd ~/Downloads/ }    
function drv { cd ~/OneDrive/ }    
function pic { cd ~/Pictures/ }    
function vid { cd ~/Videos/ }

#========================================================================
# utilities shortcuts
#========================================================================
function g_pull { git pull }
function g_push { git commit -m 'updated'; git push origin master }
function g_add { git add }

function getpass { openssl rand -base64 20 }

※ 本記事は掲載時点の情報であり、最新のものとは異なる場合がございます。予めご了承ください。

一覧はこちら

連載目次

もっと知りたい!こちらもオススメ

【連載】RPA入門 - ツールで学ぶ活用シーン

【連載】RPA入門 - ツールで学ぶ活用シーン

AIには、ルールベース、機械学習、深層学習(ディープラーニング)の3つのレベルがあり、レベルが上がるに連れてより高度な人工知能を実現しますが、AIのスピンオフという位置付けで、Digital Labor(仮想知的労働者)によるホワイトカラー業務の自動化を実現するRPAが注目されています。

関連リンク

この記事に興味を持ったら"いいね!"を Click
Facebook で TECH+ の人気記事をお届けします
注目の特集/連載
[解説動画] Googleアナリティクス分析&活用講座 - Webサイト改善の正しい考え方
Google Workspaceをビジネスで活用する
ニューノーマル時代のオウンドメディア戦略
ミッションステートメント
教えてカナコさん! これならわかるAI入門
AWSではじめる機械学習 ~サービスを知り、実装を学ぶ~
Kubernetes入門
SAFeでつくる「DXに強い組織」~企業の課題を解決する13のアプローチ~
AWSで作るマイクロサービス
マイナビニュース スペシャルセミナー 講演レポート/当日講演資料 まとめ
セキュリティアワード特設ページ

一覧はこちら

今注目のIT用語の意味を事典でチェック!

一覧はこちら

会員登録(無料)

ページの先頭に戻る