zshをtcshなキーバインドで

ログインシェルとしてはもう十何年も tcsh をずっと使って来ていたのですが、All about Ruby on Rails & Data recovery software を素直に動作させるのが難しかったので zsh へ移行することを決意しました*1

【連載】漢のzsh | マイナビニュースを参考にしてだいたいは違和感なく移行できたのですが、最後までなかなか馴染まなかったのがキーバインディングでした。

最近ようやく tcsh とほぼ同じ感じになってきたので、どう設定したのかを以下にメモしておきます。

基本は Emacs バインディング

tcsh は基本的に emacs っぽいキーバインドなので、まずは全体的に emacs っぽくしました。

bindkey -e

M-p と M-n を変更

tcsh の M-p と M-n にはカーソルより左側が一致する履歴をどんどん表示する機能が割り当てられています。そこで zsh では以下のようにしました。

autoload history-search-end
zle -N history-beginning-search-backward-end history-search-end
zle -N history-beginning-search-forward-end history-search-end
bindkey "^[p" history-beginning-search-backward-end
bindkey "^[n" history-beginning-search-forward-end

M-f や M-b などの単語を扱う機能の調節

この機能が一番のくせ者でした。結論から言えば、以下を ~/.zshrc に書き、

autoload -Uz select-word-style
select-word-style default
zstyle ':zle:*' word-chars '*?_-.[]~='

fpath=( ~/.home/zsh $fpath )
autoload -Uz tcsh-forward-word-match
zle -N forward-word tcsh-forward-word-match

以下を ~/.home/zsh/tcsh-forward-word-match に書きました。

# modified from /usr/share/zsh/4.3.12/functions/forward-word-match
emulate -L zsh
setopt extendedglob

autoload -Uz match-words-by-style

local curcontext=":zle:$WIDGET" word
local -a matched_words
integer count=${NUMERIC:-1}

if (( count < 0 )); then
    (( NUMERIC = -count ))
    zle ${WIDGET/forward/backward}
    return
fi

while (( count-- )); do

    match-words-by-style

    # For some reason forward-word doesn't work like the other word
    # commands; it skips whitespace only after any matched word
    # characters.

#    if [[ -n $matched_words[4] ]]; then
#        # just skip the whitespace
#	word=$matched_words[4]
#    else
#        # skip the word and trailing whitespace
#	word=$matched_words[5]$matched_words[6]
#    fi
    if [[ -n $matched_words[4] ]]; then
        # skip the whitespace and the word
	word=$matched_words[4]$matched_words[5]
    else
        # jist skip the word
	word=$matched_words[5]
    fi

    if [[ -n $word ]]; then
	(( CURSOR += ${#word} ))
    else
	return 1
    fi
done

return 0

M-f M-b の各シェルの動作

上記の設定の意味の解説をする前に、まずは tcsh, bash, zsh での動作を確認しておきます。(以下ではカーソル位置はスタイルシートの background-color で表現してありますので、RSS リーダーでご覧の場合には見えないかもしれません)

tcsh の場合 M-f を押すたびに次のようにカーソル(赤い四角)が移動します。

% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin 

逆に M-b を押すたびに次のようにカーソルが移動します。

% ls -l /usr/bin 
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin

M-f と M-b では停止位置が微妙に違うところがポイントです。

bash の場合 M-f の動作は tcsh と全く同じです。

$ ls -l /usr/bin
$ ls -l /usr/bin
$ ls -l /usr/bin
$ ls -l /usr/bin
$ ls -l /usr/bin 

しかし M-b の動作は -l の箇所の停止位置(青い四角)が tcsh とは異なります。

$ ls -l /usr/bin 
$ ls -l /usr/bin
$ ls -l /usr/bin
$ ls -l /usr/bin
$ ls -l /usr/bin

zsh を select-word-style bash で動作させた場合は、M-f の動作は次のようになります。

% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin 

bash の動作とは全く異なります。逆に M-b を押した場合は次のようになり bash の動作と一致します。

% ls -l /usr/bin 
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin

また、M-f での停止位置と M-b の停止位置が一致しています。

zsh を select-word-style default で動作させた場合は、M-f の動作は次のようになります。

% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin 

各引数の最初の文字に停止します。M-b を押した時も停止位置は同じです。

% ls -l /usr/bin 
% ls -l /usr/bin
% ls -l /usr/bin
% ls -l /usr/bin

M-f M-b 解説

コマンドラインを構成する文字は単語を構成する文字(w とする)と単語を構成しない文字(. とする)の2種類にわけることができます。

すると tcshbash の場合のカーソルが停止する位置は、

  • M-f の場合はカーソルより右側の w. の境目 (www...)
  • M-b の場合はカーソルより左側の .w の境目 (...www)

となります。

zsh の場合は、

  • M-f の場合はカーソルより右側の .w の境目 (...www)
  • M-b の場合は tcsh, bash と同じ

となります。

そして単語を構成する文字は以下のようになっています。

tcsh [:alnum:] と '*?_-.[]~='
bash [:alnum:]*2
zsh で select-word-style bash [:alnum:]
zsh で select-word-style default [:alnum:] と '*?_-.[]~=/&;!#$%^(){}<>'

そこで、まずは zsh の単語を構成する文字を tcsh と一致させるために次のような設定をしました。

autoload -Uz select-word-style
select-word-style default
zstyle ':zle:*' word-chars '*?_-.[]~='

select-word-style default 時は word-chars に設定した文字に加えて [:alnum:] が単語を構成する文字になりますので、これで tcsh と一致します。

そして M-f の時の動作を変更するために次のような設定をし、

fpath=( ~/.home/zsh $fpath )
autoload -Uz tcsh-forward-word-match
zle -N forward-word tcsh-forward-word-match

~/.home/zsh/tcsh-forward-word-match を作成しました。tcsh-forward-word-match はカーソルより右側の w. の境目 (www...) へカーソルを移動させる関数です。

これでようやく tcsh とほぼ同じ感じで動作するようになりました。

*1:C Shell 系ではなく Bourne Shell 系のシェルを使いたいとずっと思っていたというのもあります

*2:未確認ですがおそらくこうなっています