PowerShellがデフォルトで読み込むようになったPSReadLineモジュールには、コマンド履歴機能が搭載されている。同機能は過去に入力したコマンドを取り出して入力するというもので、UNIX系OSの多機能インタラクティブシェルでは必須の機能だ。PowerShellでもぜひこの機能を使いこなせれば、過去の入力を利用してタイプミスを減らし、タイプの手間も減らすことができる。覚えるキーは少ないので、ぜひとも活用していただきたい。
コマンド履歴 - 1つ前へ
PSReadLineモジュールでは「↑」キーが過去に入力したコマンドを1つずつ表示する機能に割り当てられている。まず、次のスクリーンショットを見てほしい。
上記スクリーンショットでは3回異なるコマンドを実行している。この状態で1回「↑」キーを押すと、次のよう直近で実行したコマンドがプロンプトに表示される。これが最も基本的なコマンド履歴機能だ。
さらに「↑」キーを押すと、もう1つ前のコマンドが表示される。
このように「↑」キーを押すことで順に過去のコマンドを表示させることができる。「Enter」キーを押せば、そのまま表示されたコマンドが実行される。
コマンド履歴 - 1つ後へ
「↑」キーと組み合わせて覚えておきたいのが「↓」キーだ。「↑」キーで過去入力したコマンドへ戻っていったわけだが、逆に直近に入力したコマンドへ戻ってくるのが「↓」キーだ。まとめて逆方向への動きを見てみよう。
基本となるのは「↑」キーだが、「↑」キーで戻っていると行き過ぎることがある。この場合に「↓」キーで戻ってくるわけだ。これらのキーは直感的にわかりやすいので、すぐに覚えられるはずだ。最も基本的なコマンド履歴機能として覚えてしまおう。
コマンド履歴検索
「↑」キーは1つずつ過去へコマンド履歴をさかのぼっていく処理だったが、そのうち慣れると1つずつ移動するのが面倒になってくる。そうなったら、次のステップとしてコマンド履歴「検索」を行ってみよう。この処理は、「F8」キーに割り当てられている。コマンド履歴検索を使うには過去に入力したコマンドの最初の文字列を入力した状態で「F8」キーを押す。次のスクリーンショットは「e」と入力した状態で「F8」キーを押している。
すると、次のようにコマンド履歴の検索が行われ、「e」から始まっているコマンドがヒットして表示される。この段階で「Enter」キーを押せば、そのコマンドが実行される。
ここでさらに「F8」キーを押すと、さらに過去にさかのぼって一致する対象が表示される。「F8」キーを押すごとに過去のコマンドへと検索が進んでいく。一致するコマンドがない場合には何も起こらない。「↑」キーと「↓」キーのときと同じように、「F8」キーにも逆方向に検索を進める方法がある。「Shift」+「F8」だ。戻りたくなったら「Shift」+「F8」で戻ってこよう。
コマンド履歴インタラクティブ検索
場合によっては「F8」キーよりも、インタラクティブにコマンド履歴検索が可能な「Ctrl」+「r」のほうが使いやすいかもしれない。入力した文字に合わせて自動的に候補を表示してくれるのが、コマンド履歴のインタラクティブ検索だ。
「Ctrl」+「r」と入力すると、次のようにインタラクティブ検索用のプロンプトが表示される。
ここで過去に入力したコマンドの文字を入力すると、候補が表示される。次のスクリーンショットでは「d」と入力している。
さらに文字を入力すると、入力した文字にヒットする別の候補が表示される。
こんな感じで、入力する文字に応じてインタラクティブに候補が変わるのがインタラクティブコマンド履歴検索だ。
「Ctrl」+「r」をさらに押すと、「F8」キーのときと同じようにさらに過去に一致するコマンドが表示される。「F8」の逆の動きが「Shift」+「F8」でできたように、「Ctrl」+「r」にも逆の動きをするキーが用意されている。「Ctrl」+「s」だ。「Ctrl」+「r」でインタラクティブコマンド履歴検索モードに入り、一致しそうな文字を入力し、後は「Ctrl」+「r」および「Ctrl」+「s」で行ったり来たりして候補を選ぶことができる。
F8および「Shift」+「F8」がコマンドの始まりの文字列にヒットするのに対し、「Ctrl」+「r」および「Ctrl」+「s」はコマンド中の文字列などにも柔軟に一致する。コマンドの最初の文字を数個入力してからバシッと過去のコマンドを引き出すなら「F8」キー、コマンドライン中でも特徴的だった文字列などを入力して対象を絞り込むなら「Ctrl」+「r」といったところだろうか。どちらもPowerShellをインタラクティブシェルとして使う場合には、欠かせない機能だ。
コマンド履歴機能はインタラクティブシェルとして重要な機能
bashやzshといった多機能インタラクティブシェルではコマンド履歴検索は当然実装されており、多くのエンジニアにとってなくてはならない機能になっている。fishのようにより後発の機能では、コマンド履歴検索をさらに進めて、文脈を加味して過去に入力したコマンドが推測表示されたりもする。こうしたサポート機能は、インタラクティブシェルには必須なのだ。
「↑」キーおよび「↓」キーによるコマンド履歴機能は、いわば基礎である。そして、「F8」キー、「Shift」+「F8」、「Ctrl」+「r」、「Ctrl」+「s」などのコマンド履歴検索機能はPowerShellをインタラクティブシェルとして利用する上で欠かせない。ぜひとも身に付けていただきたい。
付録: PSReadLineショートカットキー一覧
キー | 機能 |
---|---|
Enter | 入力行を実行しカーソルを次の行へ移動。入力が閉じていない場合には入力行を実行せずにカーソルを次の行へ移動 |
Shift-Enter | 入力行を実行せずにカーソルを次の行へ移動 |
Ctrl-Enter | 入力行を実行せずに前の行に空行を挿入 |
Ctrl-Shift-Enter | 入力行を実行せずに次の行に空行を挿入 |
Backspace | カーソルより1つ前の文字を削除 |
Ctrl-h | カーソルより1つ前の文字を削除 |
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-] | カーソルを対応するカッコへ移動 |
キー | 機能 |
---|---|
↑ | コマンド履歴の1つ前のコマンドへ入れ替え |
↓ | コマンド履歴の1つ後のコマンドへ入れ替え |
F8 | コマンド履歴検索(古い履歴へ向かって) |
Shift-F8 | コマンド履歴検索(新しい履歴へ向かって) |
Ctrl-r | コマンド履歴検索(古い履歴へ向かってインタラクティブに検索) |
Ctrl-s | コマンド履歴検索(新しい履歴へ向かってインタラクティブに検索) |
Alt-F7 | コマンド履歴をクリア |
キー | 機能 |
---|---|
Ctrl-@ | 補完。補完対象が複数ある場合にはメニュー形式で候補を表示 |
Ctrl-Space | 補完。補完対象が複数ある場合にはメニュー形式で候補を表示 |
Tab | 補完。次の補完候補を表示 |
Shift-Tab | 補完。前の補完候補を表示 |
キー | 機能 |
---|---|
Ctrl-a | 行全部を選択し、カーソルを行末へ移動 |
Shift-← | カーソルの選択位置を1つ左へ移動 |
Shift-→ | カーソルの選択位置を1つ右へ移動 |
Shift-Home | カーソルから行頭までを選択 |
Shift-End | カーソルから行末までを選択 |
Shift-Ctrl-← | カーソルから1つ前の単語の先頭までを選択 |
Shift-Ctrl-→ | カーソルから1つ後の単語の先頭までを選択 |
キー | 機能 |
---|---|
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
# Copyright (c) 2020,2021 Daichi GOTO <daichi@ongs.co.jp>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.
# author: Daichi GOTO (daichi@ongs.co.jp)
# first edition: Mon Jun 22 18:20:36 JST 2020
#========================================================================
# 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()
#========================================================================
# Linux commands integration mode
#========================================================================
function linuxcmds {
#----------------------------------------------------------------
# Definition of Linux commands used via wsl
#----------------------------------------------------------------
# Linux pagers
$_linux_pagers = @("less", "lv")
# Linux PATH and commands
Write-Host "checking linux 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 global:$_n {
if (`$Input.Length) {
`$Input.Reset()
`$Input | wsl $n ([String]`$(_path_to_linux
`$Args)).Split(' ')
}
else {
wsl $n ([String]`$(_path_to_linux `$Args)).Split(' ')
}
}
Write-Host -NoNewline .
"
Write-Host -NoNewline '_'
}
# Generate Linux pagers functions
ForEach($_n in $_linux_pagers) {
$_linux_functions += "
function global:$_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(' ')
}
}
Write-Host -NoNewline .
"
Write-Host -NoNewline '_'
}
# Function that converts Windows paths to Linux paths
$_linux_functions += @'
function global:_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
}
Write-Host .
'@
Write-Host -NoNewline '_'
# 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
Write-Host
Write-Host "functionizing linux commands:"
. $_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_pagers
Remove-Variable _linux_path
Remove-Variable _linux_command_paths
Remove-Variable _linux_functions
#----------------------------------------------------------------
# Individual Linux command function definitions
#----------------------------------------------------------------
# grep
function global: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 global:ls { wsl ls --color=auto $(_path_to_linux
$Args).Split(' ') }
function global:ll { ls -l $(_path_to_linux $Args).Split(' ') }
function global: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 }