新しいPowerShellには最初からPSReadLineモジュールが同梱されており、bashのようなショートカット操作ができるようになっている。このモジュールの提供する基本機能の使い方をマスターしておくと、PowerShellをインタラクティブシェルとして使うのがはかどるのだ。今回はPSReadLineモジュールが提供する機能の中から特に基本的な編集機能のうち、知っておくとよいものをピックアップして紹介する。

行内削除

入力した文字の削除にはカーソルキーで該当箇所まで移動して「Backspace」キーまたは「Del」キーで1文字ごとに削除するのが基本だろう。PSReadLineは、もっと広い範囲で削除を行う機能を提供している。例えば、次のような状態だとしよう。

カーソルの位置に注目

カーソルが行の中間辺りに位置している。この状態で「Ctrl」+「End」を入力すると、カーソルから行末までが削除される。カーソル以降をまとめて削除する場合に使うショートカットキーだ。

「Ctrl」+「End」でカーソルから行末までを削除

最初の状態で、今度は「Ctrl」+「Home」と入力すると次のようになる。

「Ctrl」+「Home」でカーソルから行頭までを削除

カーソルから行頭までを削除している。「Home」キーと「End」キーがそれぞれ、カーソルを行頭または行末へ移動させるキーになっている。これに「Ctrl」を付けると行頭または行末までを削除という意味になる。知っておくと便利だ。

単語内削除

先ほどは行単位で削除を行ったが、同じような削除は単語単位でも行うことができる。次の動作例を見てみよう。

カーソルの位置に注目

上記の状態で、カーソルは単語の中にある。この状態で「Ctrl」+「Del」を入力すると、次のようにカーソルから単語の末尾までが削除される。

「Ctrl」+「Del」でカーソルから単語の末尾までを削除

最初の状態で今度は「Ctrl」+「Backspace」と入力する。これでカーソルから単語の先頭まで削除される。

「Ctrl」+「Backspace」でカーソルから単語の先頭までを削除

「Backspace」キーが左方向へ1文字削除、「Del」キーが右方向へ1文字削除だ。「Ctrl」+「Backspace」や「Ctrl」+「Del」のように「Ctrl」キーを付けると、これが単語単位での削除へと拡張される。

アンドゥとリドゥ

知っているかどうかで大きな差が生まれるのは、操作のアンドゥとリドゥだ。例えば、次のように「Ctrl」+「End」と「Ctrl」+「Home」を実行して行を削除していったとしよう。

基本の状態

「Ctrl」+「End」でカーソルから行末までを削除

「Ctrl」+「Home」でカーソルから行頭までを削除

この状態で「Ctrl」+「z」を押すとアンドゥが実行される。次のように1つ前の状態に戻る。

「Ctrl」+「z」でアンドゥ(1つ前の状態に戻る)

続けて「Ctrl」+「z」を押すとさらにアンドゥが実行され、さらに前の状態へ戻る。

「Ctrl」+「z」でアンドゥ(1つ前の状態に戻る)

アンドゥで戻した操作を逆に差し戻す処理がリドゥだ。リドゥは「Ctrl」+「y」に割り当てられており、実行すると次のようにアンドゥされた処理がキャンセルされることを確認できる。

「Ctrl」+「y」でリドゥ(戻した処理をキャンセルする)

アンドゥとリドゥは、操作を誤ってデータを削除してしまったときなどに力を発揮する。ぜひ覚えておきたい。

すべてアンドゥ

アンドゥに関しては、「一度に全部アンドゥする」という機能も用意されている。

いろいろ作業した後の状態

「ESC」キーで全部アンドゥして最初に戻る

「ESC」キーは、すべてなかったことにしてスッキリさせる目的などに使用できる。入力をクリアする機能だと覚えてしまってもいいかもしれない。

削除処理に関しては好みが分かれるので、カーソルを移動させて1文字ずつ確認しながら削除したいというユーザーもいるだろう。だとしても、アンドゥとリドゥに関しては覚えておいて損はない。もし、PowerShellでアンドゥとリドゥを使ったことがなければ、ぜひこれを機に使い方をマスターしていただきたい。

付録: 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-→ カーソルからひとつ後の単語の先頭までを選択
キー 機能
F3 指定した文字へカーソルを後ろへ移動
Shift-F3 指定した文字へカーソルを前へ移動
キー 機能
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 }