Quantcast
Channel: Vimタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 5608

コマンドライン編集機能 Zsh Line Editor を使いこなす

$
0
0

Zsh Line Editorってご存じですか。


皆さん使っているであろう、「Ctrl-A で行頭に移動、Ctrl-E で行末に移動」とかのアレである。zsh の持つコマンドライン編集機能を ZLEZsh Line Editor)と呼ぶ。ZLE でコマンドライン操作体系として Emacs ライクなものと vi ライクなものが選択できるようになっている。また、ZLE ではデフォルトで 4 つのキーマップ(キー割り当ての集合)が開放されている。

  • emacs(Emacs ライクなキーマップ)
  • viins(vi のインサートモードのキーマップ)
  • vicmd(vi のコマンドモードのキーマップ)
  • .safe(カスタマイズが禁止されているキーマップ)

これらとは別に main というキーマップがあり、ZLE では main に紐付いたキーマップをデフォルトキーマップとして使用する。
zsh では最初はコマンドライン編集機能を持たない .safe キーマップを選択した状態で起動される。コマンドライン編集機能を利用するには emacs か viins かを選択しなければならない。

$ bindkey -e    # emacs キーマップを選択

または

$ bindkey -v    # viins キーマップを選択

そしてこれによって選択したキーマップが main として設定される。

多くの人は emacs キーマップを使っているだろう。もしくは vi モードの存在すら知らずに過ごしているだろう。viins ならびに vicmd はライフチェインジングなほど便利な機能なので紹介する。

vi ライクなインターフェース

上で紹介したように vi ライクなコマンドライン編集機能のインターフェースを利用するには、

$ bindkey -v    # viins キーマップを選択

として viins キーマップを選択する必要がある。vi/Vim でいう「挿入(インサート)モード」と「コマンドモード」を行き来して操作する。よって、vi ライクな編集モードを選択した場合、viins と vicmd の両キーマップを使い分けることになる。

コマンドラインを開始したときの状態は viins モードになる。このモードは文字挿入が主目的で、文字削除程度の編集しかできない。そこで vicmd モードである。

viins

viins は vi/Vim の挿入モードにもあるように、打ち込んだ文字がそのまま表示される。つまり、なにか意味のある操作を行うには修飾キーが必要となる。

キー機能意味
^Dlist-choicesマッチする補完候補を一覧表示する
^Glist-expandマッチするものを展開する
^H \^?vi-backward-delete-char
^Iexpand-or-completeカーソル位置の単語について展開または補完を試みる
^J \^Maccept-line
^Lclear-screen端末画面をクリアする
^Q \^Vvi-quoted-insert
^Rredisplay編集バッファを再表示する
^Uvi-kill-lineviins モードに移ってから入力された文字をすべて消去する
^Wvi-backward-kill-wordカーソルの前の単語を削除する(viins モード中にタイプされた単語に限る)
^[vi-cmd-modevicmd モードに移行する

機能はすべて vi/Vim に似せたものとなっている。

viins は emacs モードのように文字入力が可能であるが、^A(カーソルを行頭へ移動)や ^E(カーソルを行末に移動)などは定義されていないので使用できない。

vicmd

viins キーマップから ESC^[)をタイプすると vi/Vim のコマンドモードを模した vicmd モードに移行する。

キー機能意味
^D \=list-choices
^Glist-expandマッチするものを展開する
SPC \lvi-forward-char
^H \h \^?
^J \^Maccept-line
^Lclear-screen端末画面をクリアする
^Ndown-historyヒストリリストの新しいほうに進む
^Pup-historyヒストリリストの古いほうに進む
^Rredisplay編集バッファを再表示する
#pound-insert行に #があれば剥ぎ取り、なければ行頭に追加して accept-lineする
$vi-end-of-line行末に移動する
%vi-match-bracketカーソル位置が括弧上であれば対応する括弧に移動し、でなければ行末方向に見つかる括弧の対応括弧に移動する
+vi-down-line-or-historyバッファ内の次の行の非空白文字に移動する。既に最下行にいる場合は新しい方向のヒストリ要素に移動する
-vi-up-line-or-historyバッファ内の前の行の非空白文字に移動する。既に最上行にいる場合は古い方向のヒストリ要素に移動する
.vi-repeat-change前回のバッファ変更操作を繰り返す
;vi-repeat-findf/Fによる行内文字検索を繰り返し一致する文字に移動する
,vi-rev-repeat-findf/Fによる行内文字検索を繰り返し逆方向に一致する文字に移動する
/vi-history-search-backwardヒストリを遡るように(逆方向)文字列を検索する
?vi-history-search-forwardヒストリを辿るように(正方向)文字列を検索する
19digit-argument数引数を入力する
0vi-digit-or-beginning-of-line数引数を入力中(例えば 1の次にタイプされたとき)ならゼロを意味し、それ以外なら行頭へ移動する
Avi-add-eol行末の文字の後ろに文字を追加する形で viins モードに移行する
Bvi-backward-blank-word空白を単語区切りと見なして1単語前に移動する
Cvi-change-eolカーソル位置から行末までを削除し、viins モードに移行する
Dvi-kill-eolカーソル位置から行末までを削除する
Evi-forward-blank-word空白を単語区切りと見なして次の単語末尾に移動する
Fvi-find-prev-char次にタイプする文字と同じ文字を行頭方向に向かって行内検索し、その文字の位置に移動する
Gvi-fetch-history数引数で指定したヒストリ番号のコマンドラインを取り出す
Ivi-insert-bol現在行の最初に現れる非空白文字の位置に移動して viins モードに移行する
Jvi-join現在行と次の行を結合する
Nvi-rev-repeat-search/または ?による検索を、逆方向で進める
Ovi-open-line-above現在行の上に新規に行を挿入して viins モードに移行する
Pvi-put-beforeキルバッファにある文字列をカーソルよりも前の位置にペーストする
Rvi-replaceカーソル位置から上書き入力モードに移行する
Svi-change-whole-line現在行の内容をすべて削除したあと viins モードに移行する
Tvi-find-prev-char-skip次にタイプする文字を行頭方向に向かって検索して、見つかった場合はその文字の一歩手前の文字の位置に移動する
Wvi-forward-blank-word空白を単語区切りと見なして次の単語に進む
Xvi-backward-delete-charカーソルの前の文字を消す
Yvi-yank-whole-line現在行の内容をキルバッファにコピーする
^vi-first-non-blank現在行の最初の非空白文字に移動する
avi-add-next現在行の文字の次に挿入する形で viins モードに移行する
bvi-backward-wordvi 風に1単語戻る
cvi-replacevi 風に修正する。移動ワードと組み合わせてその分、変更するが cが続けてタイプされたら vi-change-whole-lineを実行する
dvi-deletevi 風に削除する。続けて dがタイプされたら1行すべてを削除する
evi-forward-word-end次の単語末尾に移動する
fvi-find-next-char次にタイプする文字と同じ文字を行末方向に向かって行内検索し、その文字の位置に移動する
ivi-insert現在のカーソル位置で viins モードに移行する
jdown-line-or-history下行に移動するかヒストリリストの新しい方に進む
kup-line-or-history上行に移動するかヒストリリストの古い方に進む
nup-line-or-history/または ?による検索を繰り返す
ovi-open-line-below現在行の下に新規の行を追加して viins モードに移行する
pvi-put-afterキルバッファの内容をカーソルより後ろにペーストする
svi-substituteカーソル位置の文字を置換する形で viins モードに移行する
tvi-find-next-char-skip次にタイプする文字を行末方向に向かって検索して、見つかった場合はその文字の一歩手前の文字の位置に移動する
uvi-undo-changeコマンドラインの編集操作を undo する。続けて uがタイプされたら redo する
wvi-forward-wordvi 風に次の単語に移動する
xvi-delete-charカーソル位置の文字を削除する
yvi-yank更にカーソル移動コマンドを受け付け、現在の位置からカーソル移動コマンドで移動するポイントまでをキルバッファにコピーする。続けて yがタイプされたら行全体をヤンクする

マークやバッファ系のコマンドなどは省略したが、上にあるコマンド(ウィジェット)が vicmd モードで利用できるものになる。

さて、これらは非常に便利である。以下の gif アニメをみれば簡単にコマンドラインで操作ができる。

カスタマイズ

便利には便利なのだが、vi/Vim においても .vimrcファイルでカスタマイズするようにより良いキーバインドにすることが望ましい。例えば、viins において emacs ライクなキーバインドを利用しようと思っても ^Aなどは未定義である。

また、使用できるキー機能(ウィジェット)は決め打ちではない。ユーザによって拡充することも可能である。

viins と emacs

viins モードと emacs モードの共通点は、打鍵したものがすぐ表示されるモードであること。文字入力に特化している。加えて、何か特別の操作をするにはコントロールキーなどの修飾キーが必要であること。そして viins にはそれらのキーカスタマイズがされていない。
viins モードに emacs モードのいいところを取り込めば、emacs モードとしての機能すらも持つ viins モードと、強力な編集操作体系を持つ vicmd モードが組み合わされば、vi ライクなインターフェースはコマンドライン編集機能としてとても有力なキーマップになる。

viins を emacs モードの如くカスタマイズして、より良い CLI ライフを提供するコードがこちら。

bindkey -M viins '\er'history-incremental-pattern-search-forward
bindkey -M viins '^?'  backward-delete-char
bindkey -M viins '^A'  beginning-of-line
bindkey -M viins '^B'  backward-char
bindkey -M viins '^D'  delete-char-or-list
bindkey -M viins '^E'  end-of-line
bindkey -M viins '^F'  forward-char
bindkey -M viins '^G'  send-break
bindkey -M viins '^H'  backward-delete-char
bindkey -M viins '^K'kill-line
bindkey -M viins '^N'  down-line-or-history
bindkey -M viins '^P'  up-line-or-history
bindkey -M viins '^R'history-incremental-pattern-search-backward
bindkey -M viins '^U'  backward-kill-line
bindkey -M viins '^W'  backward-kill-word
bindkey -M viins '^Y'  yank

ウィジェット

ZLE で機能している accept-lineなどの単位のことをウィジェットという。ZLE では独自の関数を作りウィジェットとして登録することにより、好きなように機能拡張することができる。

ウィジェットの作り方は以下だ。

  1. ウィジェットに登録するためのシェル関数を定義する
  2. 作ったシェル関数をウィジェットに登録する
  3. キーへ割り当てる(任意)

実際に作るとこんな感じになる。例えば pecoを使ったヒストリ補完を作ってみるとしよう。

# 1. ウィジェットに登録するためのシェル関数を定義する
peco-select-history(){# peco があるかないかで分岐する# なければ違うアプローチをするiftype"peco">/dev/null 2>&1;thenBUFFER=$(history 1| sort -k1,1nr | perl -ne 'BEGIN { my @lines = (); } s/^\s*\d+\s*//; $in=$_; if (!(grep {$in eq $_} @lines)) { push(@lines, $in); print $in; }'| peco --query "$LBUFFER")CURSOR=${#BUFFER}# peco で選んでる最中に Enter を押した瞬間実行する
        zle accept-line
        # 画面をクリアする
        zle clear-screen
    else# バージョンによって条件分岐するために使用するモジュールを開放する
        autoload -Uz is-at-least

        # 4.3.9 以降ではインクリメンタルパターンサーチが出来るので、それを利用する# なければデフォルトでマッピングされているものを利用するif is-at-least 4.3.9;then# zsh -la <widget> とすることで、widget に完全一致するウィジェットが# 存在する場合、返却値 0 で終了する
            zle -la history-incremental-pattern-search-backward && bindkey "^r"history-incremental-pattern-search-backward
        elsehistory-incremental-search-backward
        fifi}# 2. 作ったシェル関数をウィジェットに登録する
zle -N peco-select-history

# 3. キーへ割り当てる(任意)
bindkey '^r' peco-select-history

シェル関数さえ書いてしまえば簡単に好きな機能を実現できる。
それと少し説明で、ウィジェット内で有用な決め打ちの変数が存在する。

変数説明
BUFFERZLE の編集バッファの内容を全て保持している文字列が入っている。この変数は書き込み可能で、値をセットすると編集バッファがその内容に置き換えられる
CURSORバッファ中にカーソルが位置する桁位置を示す整数値が入っている。バッファの先頭はゼロとなる。この変数に先頭と末尾を越えないサイズの整数値を代入することでカーソルをその桁位置に移動することができる。また CURSORの値を変更するとそれに連動して LBUFFERRBUFFERの値も変動し、それぞれカーソル位置より左の内容、右の内容を保持する
LBUFFERバッファの中でカーソルよりも左側に位置する文字列の内容がセットされている。BUFFER同様、新しい値をセットすることで、該当する部分のバッファの内容が置き換えられる
RBUFFERバッファの中でカーソルよりも右側に位置する文字列の内容がセットされている。BUFFER同様、新しい値をセットすることで、該当する部分のバッファの内容が置き換えられる
......

他にもたくさんあるのだが、よく利用されるものだけを紹介した。これによって、簡単にやりたいことがウィジェットとして実現できる。

より高度なカスタマイズ

Visual mode の実装

vi/Vim にはヴィジュアルモードという操作体系がある。簡単に文字列を反転させまとめて処理することができる。しかし、Zsh Line Editorに実装されているものは Insert mode(viins)と Command mode(vicmd)のみである(※)。

そこで、簡単な Visual mode を実装した ZLE キーマップを追加した。キーマップ名は vivis である。

これによって、従来通りの viins と vicmd の行き来に加え、vivis という新モードが追加され使用することができる。

実は、デフォルトで 4 つのキーマップが開放されていたが、このキー割り当ての領域をユーザによって拡張することが簡単にできる。

$ bindkey -N new_keymap

vivis

キー機能意味
^[vi-visual-exit作業を全て取りやめ vicmd モードに移行する
^Mvi-visual-yank編集バッファ上の選択された文字列をクリップボードにコピーする
$vi-visual-eol行末に移動する
%vi-visual-match-bracketカーソル位置が括弧上であれば対応する括弧に移動し、でなければ行末方向に見つかる括弧の対応括弧に移動する
;vi-visual-repeat-findf/Fによる行内文字検索を繰り返し一致する文字に移動する
,vi-visual-rev-repeat-findf/Fによる行内文字検索を繰り返し逆方向に一致する文字に移動する
19digit-argument数引数を入力する
0vi-visual-bol数引数を入力中(例えば 1の次にタイプされたとき)ならゼロを意味し、それ以外なら行頭へ移動する
Bvi-visual-backward-blank-word空白を単語区切りと見なして1単語前に移動する
Cvi-visual-substitute-linesカーソル位置から行末までを削除し、vicmd モードに移行する
Dvi-visual-kill-and-vicmdカーソル位置から行末までを削除する
Evi-visual-forward-blank-word空白を単語区切りと見なして次の単語末尾に移動する
Fvi-visual-find-prev-char次にタイプする文字と同じ文字を行頭方向に向かって行内検索し、その文字の位置に移動する
Gvi-visual-goto-line末尾行(行末)に移動する
Ivi-visual-insert-bol現在行の最初に現れる非空白文字の位置に移動して viins モードに移行する
Jvi-visual-join現在行と次の行を結合する
Ovi-visual-exchange-points現編集バッファ上の選択部分の末端を行き来する
Rvi-visual-substitute-lines選択された文字列上で上書き入力モードに移行する
Svi-visual-surround-space選択文字列をスペースで囲うように置き換える
S'vi-visual-surround-squote選択文字列をシングルクォートで囲うように置き換える
S"vi-visual-surround-dquote選択文字列をダブルクォートで囲うように置き換える
S(vi-visual-surround-parenthesis選択文字列を()で囲うように置き換える
S)vi-visual-surround-parenthesis選択文字列を()で囲うように置き換える
Tvi-visual-find-prev-char-skip次にタイプする文字を行頭方向に向かって検索して、見つかった場合はその文字の一歩手前の文字の位置に移動する
Uvi-visual-uppercase-region編集バッファ上の選択部分を大文字に変換する
Vvi-visual-exit-to-vlines行全体を選択し vivli モードに移行する
Xvi-visual-backward-delete-charカーソルの前の文字を消す
Yvi-visual-yank編集バッファ上の選択された文字列をクリップボードにコピーする
^vi-visual-first-non-blank現在行の最初の非空白文字に移動する
bvi-visual-backward-wordvi 風に1単語戻る
cvi-visual-changevi 風に修正する。移動ワードと組み合わせてその分、変更するが cが続けてタイプされたら vi-change-whole-lineを実行する
dvi-visual-kill-and-vicmdvi 風に削除する。続けて dがタイプされたら1行すべてを削除する
evi-visual-forward-word-end次の単語末尾に移動する
fvi-visual-find-next-char次にタイプする文字と同じ文字を行末方向に向かって行内検索し、その文字の位置に移動する
ggvi-visual-goto-first-line先頭行(行頭)に移動する
hvi-visual-backward-char行頭に向かって 1 文字移動する
jvi-visual-down-line下の行に移動する
kvi-visual-up-line上の行に移動する
lvi-visual-forward-char行末に向かって 1 文字移動する
ovi-visual-exchange-points編集バッファ上の選択部分の末端を行き来する
pvi-visual-putキルバッファの内容をカーソルより後ろにペーストする
rvi-visual-replace-regionカーソル位置の文字を置換する形で viins モードに移行する
tvi-visual-find-next-char-skip次にタイプする文字を行末方向に向かって検索して、見つかった場合はその文字の一歩手前の文字の位置に移動する
uvi-visual-lowercase-region編集バッファ上の選択部分を大文字に変換する
vvi-visual-exit作業を全て取りやめ vicmd モードに移行する
wvi-visual-forward-wordvi 風に次の単語に移動する
yvi-visual-yank編集バッファ上の選択された文字列をクリップボードにコピーする

vicmd モードで vまたは Vキー押下で vivis モードに移行する。特に Vでは vivli という行全体を意味するモードに分けているが、ここらへんの実装は変わっていくかもしれない。また、ウィジェット名やキーも変わっていくかもしれない。これについては、公式の README を参照して欲しい。

テキストオブジェクト

Vim にはテキストオブジェクトという文字加工に役立つ便利な概念がある。'"によって囲われた文字列・単語をひとつのハンク(かたまり)と見なしてくれるのだ。これによって Vim の編集モードでは "内の文字列などに対し、簡単に変更したり削除したりすることができる。

詳しくは Vim を開いて、

:help text-objects

とすればいい。

これを zsh に実現するプラグインがある。

既に zsh には cwといった簡単なものは実装されている(ので bindkey -vとした時点ですぐに使用できる)。しかし、このプラグインを利用することでそれらに加えて、ciwci"を使うことができる。この便利さはあなたが Vimmer なら説明する必要はないでしょう。

※ zsh 5.0.8から autoloadによって高度なテキストオブジェクトが開放された

zsh プラグイン

zsh 5.0.8 の機能拡充によってプラグインではなく、zsh の機能として高度なテキストオブジェクトや visual モードを使用できるようになったが少し欠点があった。それは他のプラグインに干渉することだ。

zsh でのテキストオブジェクトでは "などの字句解析にノイズが交じるとうまく判定できないようだ。例えば有名な zsh プラグインに Fish ライクなシンタックスハイライトを提供する zsh-syntax-highlightingがある。この色付けにはエスケープシーケンス的なものを使っているのだが、visual モードにて字句解析をするときにそれがノイズとなり狂いをもたらしている(と思う)。

しかし、上で紹介した 2 つのプラグイン実装である、

これらを使えば、テキストオブジェクトとビジュアルモードの両方を実現できる(ただし、visual モード時のテキストオブジェクト viw/vi"などは使用できない。しかし zsh visual では使用できる。ここらへんうまく出来ないものか)。

プロンプト

ここまで説明してきて、ZLE の emacs モードと比べて vi モードは有用なものであるとしてきた。しかし唯一、不便なことがある。それは現在のモードが分からないことだ。

emacs モードと比べ、vi モードはモードが複数ある。いま viins なのか vicmd なのか、それともプラグインによって拡張された vivis なのか分からなくなるのだ(残念ながら bindkey -vとしただけじゃモード表示はしてくれない)。

その解決には、プロンプトが適役である。

autoload -Uz colors; colors
autoload -Uz add-zsh-hook
autoload -Uz terminfo

terminfo_down_sc=$terminfo[cud1]$terminfo[cuu1]$terminfo[sc]$terminfo[cud1]
left_down_prompt_preexec(){
    print -rn -- $terminfo[el]}
add-zsh-hook preexec left_down_prompt_preexec

function zle-keymap-select zle-line-init zle-line-finish
{case$KEYMAP in
        main|viins)PROMPT_2="$fg[cyan]-- INSERT --$reset_color";;
        vicmd)PROMPT_2="$fg[white]-- NORMAL --$reset_color";;
        vivis|vivli)PROMPT_2="$fg[yellow]-- VISUAL --$reset_color";;esacPROMPT="%{$terminfo_down_sc$PROMPT_2$terminfo[rc]%}[%(?.%{${fg[green]}%}.%{${fg[red]}%})%n%{${reset_color}%}]%# "
    zle reset-prompt
}

zle -N zle-line-init
zle -N zle-line-finish
zle -N zle-keymap-select
zle -N edit-command-line

プロンプトはコマンドラインを実行する(accept-line)たびに、表示内容を一新する。それにより、モードも毎回チェックして更新することが出来る。

これによって実現できるプロンプトがこれだ。

-- NORMAL --は vicmd モードを指し、-- INSERT --は viins モードを指す。プラグインによるモードも解釈するようになっていて -- VISUAL --とでる。Vim そのものだ。

また、このプロンプトの実現には RPROMPT(右プロンプト)は使っていないので、ユーザの設定領域は確保してある。

まとめ

長々と zsh(ZLE)の vi モードについて取り扱った。より拡張してくれるプラグイン実装を使ったり、キー設定をすることでより便利なエディットが可能になる。Vimmer は特に導入して試してみる価値がある!!!!!!


あ、それと、長い設定例が続いたため、記事内のコードをまとめた ZLE vi mode のリポジトリを作成しました。

ダウンロードして sourceすればプロンプトから何からいい感じに動きます。Antigenユーザなら、

$ antigen bundle b4b4r07/zle-vimode

でいけます。お試しあれ。


Viewing all articles
Browse latest Browse all 5608

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>