zshで究極のオペレーションを

第3回zsh使いこなしポイント即効編

zshを使い始めて最初に気になる点のうち、すぐに設定してすぐに効果を実感できる即効薬的なものを今回は紹介しよう。

プロンプト

これまでとは違うシェルを起動してまず目にするのがプロンプトで、これが変わるとちょっとした違和感がある。この違和感は結構大切でスーパーユーザに切り替えたり、あまり設定していないマシンを触っていたりということが実感できるので慎重に作業を進めるきっかけに利用できる。

zshに乗り換えたときもそうした違和感を感じ、それまでのシェルと似たものにしたくなるかもしれないが、ちょっとした違和感を保ってこれまでとは違う機能をたくさん利用するのだ、という心構えにしたらどうだろうか。心構えはともかくとして、プロンプト出力にもzshの細やかな配慮があるので、それを活かさないのは損である。

その機能をフルに活かしている、とまでは行かないが、筆者の ~/.zshrc のプロンプト設定部分は概ね以下のようになっている。

nprom () {
    setopt prompt_subst
    local rbase=$'%{\e[33m%}[%%{\e[m%}' lf=$'\n'
    local pct=$'%0(?||%18(?||%{\e[31m%}))%#%{\e[m%}'
    RPROMPT="%9(~||$rbase)"
    case "$USER" in
      yatex)	PROMPT=$'%{\e[33m%}%U%m{%n}%%%{\e[m%}%u ' ;;
      java)	PROMPT=$'%{\e[36m%}%U%m{%n}%%%{\e[m%}%u ' ;;
      *)
    local pbase=$'%{\e[$[32+RANDOM%5]m%}%U%B%m{%n}%b'"$pct%u "
    PROMPT="%9(~|$rbase$lf|)$pbase"
    ;;
    esac
    [[ "$TERM" = "screen" ]] && RPROMPT="[%U%~%u]"
}
nprom

複雑に見えるが、見た目は第2回で示したサンプルzshrcにあるPROMPT='%m{%n}%% 'およびRPROMPT='[%'と基本的に同じである。これにちょっとした味付けを行なっている。この設定にしたときの端末で操作した様子を模したものをまず示そう。

firestorm{yuuji}%                                                       ]
firestorm{yuuji}%                                                       ]
firestorm{yuuji}% ls /usr                                               ]
X11R6/         include/       local/         share/         tools.i386/
X11R7/         lib/           lost+found/    spool/         x11r6.tar.gz
bin/           libdata/       mdec/          src/           xsrc/
emul/          libexec/       pkg/           tests/
games/         lkm/           sbin/          tmp/
firestorm{yuuji}% ls /usr/hoge                                          ]
ls: /usr/hoge: No such file or directory
firestorm{yuuji}%                                                       ]

firestorm{yuuji}% cd /usr/src/external/bsd/openldap/dist/servers/slapd  ]
firestorm{yuuji}%       [/usr/src/external/bsd/openldap/dist/servers/slapd]
firestorm{yuuji}% cd back-bdb
[/usr/src/external/bsd/openldap/dist/servers/slapd/back-bdb]
firestorm{yuuji}%

要点を示すと以下のようになる。

プロンプトに色付け

コマンドの出力が多くてさかのぼって見るときなど、その出力がどこから始まっているのか判別しやすいよう太字にして色を付けている。

プロンプト文字列に文字属性を変えるエスケープシーケンスを%{%}で括って入れておけばよい。エスケープシーケンス先頭のESC文字は、zshでは$'…'でクォートした\eで表現できる。

数式展開

以前遊びで入れた設定で「zshっぽく見える」効果しかないが、zshでは数値演算や乱数生成ができるので色をランダムで変えている。数式展開ほか、さまざまな展開をプロンプト文字列に対して施したい場合にはsetoptでシェルオプションprompt_substを有効化しておく。

右プロンプト

右側のプロンプト(RPROMPT)に現在ディレクトリを出しておく。あまりに階層が深くなった場合は右に出すのをやめて2行にする。これを切り替えているのが %n(~|string1|string2) という記法で、スラッシュ区切りがn個以上の場合に string1 を、そうでないときに string2 を設定する。上記設定例では、PROMPTRPROMPTともにn=9での場合分けを行ない、スラッシュが9個未満のときは右プロンプト、9個以上のときは左プロンプトに改行つきでパス名を出すようにしている。

エラーの明示

直前に実行したコマンドが正常終了せず、終了値$?が0以外になったときはプロンプト末尾の%を赤にしている。ただし、あるプロセス起動中C-zを押しsuspendしてコマンドラインに戻った直後は$?=18となるが、suspend直後は赤でないほうがいいので$?=18 の場合も赤くしないよう除外している。

この切り替えは %n(?|string1|string2) の記法で行なう。終了値がnのときはstring1を、そうでないときはstring2 を出力する。

なお、起動したコマンドがエラー終了したかを知るにはシェルオプションprint_exit_valueをセットしておくのも効果的で、この場合はエラー終了したときのみ、その終了値を出力してくれる。

筆者個人の設定ではカラフルにしているが、スーパーユーザのプロンプトはシンプルにしている。

RPROMPT='(%~)'
PROMPT=$'%B%m%b:%?:%# '

スーパーユーザでは色を付けず一般ユーザ時と違うことを意識させつつ、太字にして過去のプロンプト位置を探しやすくしている。%?は直前のコマンドの終了コードの値をそのまま出す表記である。

ここまでの例にあるように、プロンプト文字列では%で始まる記法を様々な文字列に展開する。代表的なものを下記に示しておく。

記法意味
%%%文字自身
%#一般ユーザなら %スーパユーザなら #
%ltty名
%Mホスト名(全部)
%mホスト名(最初のドットまで)
%nユーザ名
%?直前のコマンドの終了値($?)
%/カレントディレクトリ
%~同上。ただし~記号などで可能な限り短縮する
%!ヒストリ中のイベント番号
%D{FMT}strftime(3)関数にFMTの書式を渡したときの現在時刻の文字列
%B太字開始
%b太字解除
%U下線開始
%u下線解除
%S強調開始
%s強調解除
%{%}生のエスケープシーケンスを挟む
%n>string> 以降のプロンプト文字列の最大長をn文字以下に後略表記する。省略を示す文字列としてstringを使う。
%n<string< 以降のプロンプト文字列の最大長をn文字以下に前略表記する。省略を示す文字列としてstringを使う。

最後の%n<string<は、プロンプトに%~などを設定してパス名を出しているときに、階層が深くてプロンプトが長くなるのを回避するもうひとつの方法として利用できる。

たとえば、以下のようにしてみる。

RPROMPT='[%39<...<%'

すると、最初の [ の次の文字、つまり % によって出される文字列が39文字を超える場合は先頭部分を切り詰めて `...' に変わる。

duke{yuuji}% pwd                                                  ]
duke{yuuji}%]
duke{yuuji}% cd /usr/src/sys/arch/amd64/compile/DUKE              ]
duke{yuuji}%                   [/usr/src/sys/arch/amd64/compile/DUKE]
duke{yuuji}% cd lib          [...src/sys/arch/amd64/compile/DUKE/lib]

プロンプト中の%記法には、%の直後に整数を付加して、個数や回数・長さを指定できるものもある。

キーカスタマイズ

シェルを手に馴染ませるために比較的簡単にできる設定の1つがキーカスタマイズである。そのための要点を示しておこう。

bindkeyコマンドとキーマップ

zshのキー割り当てはbindkeyコマンドで行なう。zshには複数のキーマップがあり、それぞれ全く違う操作体系のキー割り当てセットが格納されている。主なものにemacsキーマップ、viins(viの挿入モード)キーマップ、vicmd(viのコマンドモード)キーマップがある。emacs風の操作感を好む場合は、以下のようにemacsキーマップを選択する。

% bindkey -e

vi風の操作感を好む場合は、以下のようにviinsキーマップを選択する。

% bindkey -v

実はzshではさらに違うユーザ独自の新たなキーマップを作ることもできるし、キーに割り当てる機能(ウィジェット)をシェル関数で独自に作ることもできる。たとえば、以下のようにするとC-fでのカーソル移動が「3歩進んで2歩下がる」になる。

# 新しいウィジェットとなる関数 365march-forward() を定義
function 365march-forward () {
  : ${_365march:-0}
  if ((_365march++ % 4)) then
    zle forward-char
  else
    zle backward-char -n 2
  fi
}
# この関数をウィジェット化
zle -N 365march-forward
# emacsキーマップを見本に新しいキーマップ365marchを作る
bindkey -N 365march emacs
# 365marchキーマップの C-f に 365march-forward を割り当てる
bindkey -M 365march '^f' 365march-forward
# 使用するキーマップを 365march に変える
bindkey -A 365march main

この状態でコマンドラインに何文字か打って行頭に戻りC-a⁠、その後C-fを連打してみよう。もちろんまったく使い道がないのでキーマップをemacsに戻しておこう。

% bindkey -e

かくも過激な機能拡張ができるzshだが、ウィジェットの説明は別の機会に譲り、ここではもっとも利用者が多いと思われるemacs風キー割り当てでの簡単なカスタマイズ例をいくつか示そう。

ヒストリ呼出しのカスタマイズ

シェルを使い始めの頃は、(またはC-pキーを連打して前に打ったコマンドがどんどん戻ってくるというだけでずいぶんありがたく感じるものだが、より効率的にコマンド入力するなら、呼び戻そうとしている過去のコマンドラインに一発で辿り着けた方がよい。

そのためには検索機能つきで古いコマンドラインに戻る機能を用いればよい。その検索方法には大別して2つがある。

  • 行頭マッチの検索
  • インクリメンタル検索(EmacsでいうC-sC-r

zshのemacsキーマップのデフォルトではC-pC-nに検索なしの機能が割り当てられているが、これを検索つきに変えてしまっても問題なく、そのほうが狙ったコマンドラインの呼出しが楽になる。

bindkey '^p' history-beginning-search-backward
bindkey '^n' history-beginning-search-forward

これは、コマンドラインに途中入力した文字があれば、それと同じ文字列で始まるコマンドラインに移動する。tcshにある同種の先頭文字検索機能と違い、これらはカーソルの桁位置が保存されたままマッチするものに移動する。例を示す。

grep foo /usr/local/apache/logs/access_log
tail -f /usr/local/apache/logs/error_log
gimp &

コマンド入力履歴が上記のような場合に、次のコマンドラインでgrep まで入力してhistory-beginning-search-backwardを呼び出すとカーソル位置は以下のようになる。

% grepC-p        (C-pには history-beginning-search-backward が割り当てられている)
       ↓
% grep foo /usr/local/apache/logs/access_log
      ↑カーソル位置(pの次)

カーソルが行末に来る挙動に慣れていると最初はぎょっとするが、たとえば上記の例だとgrepの検索パターンを変えて何度も試したりするにはカーソル位置がその場の方が好都合だ。状況によるので一概にどちらが有利不利とは言えない。ただ、行末に移動するのはC-eですぐ行けるものの、中途位置にはすぐ行けないので、あらかじめカーソルが位置するのは中途位置の方がよいことの方が多いと経験上感ずる。

どうしても行末にカーソルが来た方がよい場合は別の機能を割り当てるとよい。

autoload -U up-line-or-beginning-search down-line-or-beginning-search
zle -N up-line-or-beginning-search
zle -N down-line-or-beginning-search
bindkey '^p' up-line-or-beginning-search
bindkey '^n' down-line-or-beginning-search

エディタ(vi)とコマンドラインの往復

tcshの行エディタにある便利な機能にrun-fg-editorというものがある。これは、入力途中のコマンドラインを保存したままsuspendしているエディタに戻る(フォアグラウンドにする)機能である。この機能はtcshのデフォルトでM-C-zに割り当てられているが、これをC-zに割り当てると、以下のようにC-z ーだけでviとコマンドラインを往復でき非常に便利である。

viC-z


C-z
コマンドライン

これと同じことをzshで行なうには、コマンドラインスタックESC-qを利用するキーの流れをbindkey -sで登録する。C-zに割り当てる例を示す。

setopt hist_ignore_space
bindkey -s '^z' '^[q %vi^m'

これは、C-zを押したときに、ESC-qを押してから" %vi"を挿入し、C-mをタイプした」ように振る舞う。%vi はシェルの管理下にあるジョブのうち、viという文字列で始まるジョブをフォアグラウンドにすることを意味し、その前にあるスペース1つはこれをヒストリに登録しないことを意味する(シェルオプションhist_ignore_space有効時⁠⁠。

ちなみに筆者自身は「vi決め打ち」ではなく、⁠環境変数EDITORに登録されているエディタ」で処理したいので以下のようにしている。

export EDITOR=vim
alias vi='$EDITOR'
bindkey -s '^z' '^[q %\\$EDITOR^m'

もっと bindkey -s を

tcshでも全く同様に使える機能だが、bindkey -sは、タイプした文字の流れを登録できるのでキーボードマクロ的な「複雑ではないが面倒」な処理をどんどん定義できる。しょっちゅうタイプする文字列や、キーの流れはどんどん登録したい。

シェルを主な作業場所としている人であれば、たとえば以下のような文字列やそれに類するものはよく打つはずである。

  • ファイルを展開するために"gtar zxpf "
  • 出力結果をページャで見るために「行末に移動して"| less"をタイプしてリターン」
  • 出力結果の行数を調べるために「行末に移動して "| wc" をタイプしてリターン」
  • 出力を捨てるために">/dev/null"あるいは">&/dev/null"
  • 何かの出力の第Nフィールドをawkで取り出すために" | awk '{print $N}' | "Nはその都度変わる)
  • カレントディレクトリ以下すべての通常ファイルを指定するために"**/*(.)"

このように、熟語的に用いるコマンド文字列は人によって様々な頻出用法があるはずである。これはbindkey -sで目の前にすぐ取り出せるようにしておくと効果的で、たとえば単純文字列なら以下のように設定するとESC-G"gtar zxpf "が出てくる。

bindkey -s '^[G' 'gtar zxpf '

また、以下のように設定すると、ESC-A のタイプでコマンドラインに"| awk '{print $}'|"が挿入され、カーソル位置が$の直後になる。

bindkey -s '^[A' "| awk '{print $}'|^B^B^B"

よく使うlessは以下のようにしておくとよいだろう。

bindkey -s '^[L' "^E|less^M"

ちなみにbashの場合は、~/.inputrcに記述する。

$if Bash
 "\ep": history-search-backward
 "\en": history-search-forward
 "\eG": "gtar zxpf "
 "\eL": downcase-word
 "\el": "\C-e|less\C-m"
$endif

割り当てるキーの選択

bindkeyで自分仕様のものをキーに割り当てるとき、どのキーに割り当てるかがちょっとした問題になる。emacsキーマップはほとんど空いているキーがない。標準では「ESC-英字」の英小文字と英大文字の両方に同じ機能が割り当てられているため、そのどちらかを上書きしても機能損失はない。たとえばESC-LESC-lはともにdown-case-wordで、カーソル位置の単語を小文字化する。小文字化と"^E|less^M"のよく使う方をESC-lそうでないほうをESC-Lに割り当てておくとよい。筆者は断然less起動の方をよく使うため、そちらをESC-lに割り当てている。

よりたくさんのキーを割り当てたいなら、新しいプレフィクスキーストロークを作ってしまうこともできる。zshのbindkeyで2ストローク以上のキーを指定すると、最後の1字以外の部分をプレフィクスキーストロークとして自動的に処理してくれる。

たとえば筆者は独自のキー割り当てを多数持ちたいので、あまり使わないC-qを新規プレフィクスキーストロークとしている。

# Ctrl-Q prefix
bindkey '^q '           set-mark-command
bindkey '^q^b'          vi-history-search-backward
bindkey '^q^d'          end-of-line
bindkey '^q^e'          up-line-or-beginning-search
bindkey '^q^n'          down-line-or-beginning-search
bindkey '^q^f'          history-incremental-search-backward
bindkey '^q^h'          backward-kill-word
bindkey '^q^i'          expand-or-complete-prefix
bindkey '^q^j'          exchange-point-and-mark
bindkey '^q^s'          beginning-of-line

特別な宣言なしに2ストローク以上のキーを指定しても、たとえばC-q C-dを押せばend-of-lineを呼び出してくれる。さらに、C-qだけをタイプして一定時間(デフォルトで100分の40秒=$KEYTIMEOUT経過するとC-q のみに割り当てた機能push-lineが呼び出される。

このように、連打する必要のないキーならばどのキーでも手軽にプレフィクスキーストロークにできるので欲張った拡張がやりやすい。また、キーに割り当てていない機能はM-xESC-xに機能名を指定して呼び出すことができる。

% ls /
     ↑ここで M-x beginning-of-line
% ls /

機能名はEmacsと同じ慣習で付けられている。機能名の入力時には、Tab補完やESC-C-dによる候補一覧表示が使えるため、zshのラインエディタ全体にどんな機能があるのかを調べることも容易にできる。

まとめ

プロンプトもキー割り当ても何度も触れるものだけに、とことん磨きあげる価値がある。キー割り当てに関して言えば、bindkeyコマンドを引数なしで起動して得られるものだけでなく、キー割り当てのない便利なものもたくさんあり、全部じっくり見るのはなかなかたいへんである。しょっちゅう面倒に感ずる操作があった場合は、それを解決する機能を見付けるチャンスかもしれない。

次回は「zshなら一発」を実感できる機能を紹介したい。

おすすめ記事

記事・ニュース一覧