zsh

Ippei Kishida

Last-modified:2016/10/12 21:48:20.

『UNIX今日の技』の zsh の項目について纏めたものです。

zsh は(おそらく)最強のシェルです。 しかし、どんなシェルでもそうですが、自分で設定してやらないと使えたものではありません。 デフォルトの zsh は鍛えられた tcsh に圧倒的な敗北を喫するでしょう。

本記事では zsh の上手な設定のレシピを提案します。 適当に取捨選択して取り込んで、シェルを「鍛えて」下さい。

なお、本項では zsh に特徴的なことのみをまとめていく方針です。 tcsh など、シェル一般に共通的な事柄については、『UNIX今日の技/シェル』に書いてありますので、こちらも併せてご覧頂くと良いと思います。

1 zshrc

zsh の初期化ファイルは幾つかありますが、最初は全部 zshrc に書いておけば良いです。 後々、他の設定ファイルの役割とかタイミングとか必要であれば書くつもりです。

1.1 岸田の初期設定ファイル

岸田が丹精込めて育てた初期設定ファイルを公開しておきます。 ご参考まで。

.zshrc zsh のメインの設定ファイル
.zshenv 環境変数などの設定
.dircolors ** ls や補完候補の色の設定をするファイル

2 emacs キーバインド

とりあえず手に慣れ親しんだ emacs キーバインドにしなければ大変です。

bindkey -e

これで以下の操作が可能になります。

3 補完機能

3.1 補完定義のロード

zsh の最強たる所以の一つがキメ細かな補完機能です。 tcsh でも同様のことはできますが、zsh ではユーザが定義しなくても用意されている定義をロードするだけでかなりの事が出来るようになるという点が異なります。

autoload -U compinit
compinit -u

この2行で以下のことができるようになります。

他にも沢山あります。 「こーゆー補完をして欲しい」という場面があったら、とりあえず [TAB] を打って試してみると面白いと思います。

3.2 補完候補を順番にサーチする[デフォルト]

一回目の TAB では補完候補を表示するだけですが、2回目以降の TAB ではその候補の中の一つを順番に聞いてきます。 日本語ファイル名を指定する時にはこれがもう有難くって。

3.3 補完リストが流れない[デフォルト]

気付いたでしょうか。 tcsh では補完リストを表示するたびにシェルの画面が流れていったのが、zsh では流れずにその場で補完候補が切り替わって行きます。 出力をコピペして利用しようとする時に、この差はデカいです。

4 グロビング

グロビングとはパターンマッチによるファイル指定のことです。 今迄しばしば「ワイルドカード展開」というてましたが、これも「ワイルドカードによるグロビング」と表現するのが正しいみたいです。

4.1 ワイルドカードがどのように展開されるかを確認[設定:bindkey -e]

ワイルドカードを使ったコマンド入力は便利ですが、 同時にケアレスミスを誘発し易く注意が必要です。 実際にワイルドカードがどのように展開されるかを、 echo * のように打って確認する人もいるでしょう。 zsh では次のキーバインドが便利です。

C-x g #emacs キーバインドの場合

例えば、 「ls /usr/*」で * の後で C-x g を打つと、 ワイルドカード展開でマッチするファイルやディレクトリをコマンドラインの下に表示してくれます。

4.2 再帰的にディレクトリを掘るワイルドカード[デフォルト]

「あるディレクトリより下にある全てのファイルを調べたい」と思ったことはありませんか? tcsh ではディレクトリの階層毎にコマンドを実行しなければなりませんでした。 例えば、

grep ENCUT */INCAR
grep ENCUT */*/INCAR
grep ENCUT */*/*/INCAR
grep ENCUT */*/*/*/INCAR
 :
 :

zsh では以下のコマンドで十分です。

grep ENCUT **/INCAR

私は find よりも以下のようなコマンドをよく使います。

ls -d **/*LiF*

4.3 ファイルの属性によるグロビング[デフォルト]

*」などのワイルドカード指定に続けて「()」で指定することで、 マッチさせるファイルの属性を制限することができます。 例えば、

chmod 755 *(/)   # 全てのディレクトリのパーミッションを 755 に
chmod 644 *(^/)  # ディレクトリ以外の全ファイルのパーミッションを 644 に

使える指定には以下のようなのがあります

/ ディレクトリ
@ シムリンク
. 通常ファイル
^ 否定

追補:以下の指定もできるらしいですが、私は使ったことがありません。

* ディレクトリでない実行権のあるファイル
r 読み込み権のあるファイル
w 書き込み権のあるファイル
x 実行権のあるファイル
R 他人に読み込み権のあるファイル
W 他人に書き込み権のあるファイル
X 他人に実行権のあるファイル
f値 値で指定したアクセス権のファイル
U 自分が所有するファイル
G 自分が所属するグループのファイル
u値 値のユーザが所有するファイル
g値 値のグループが所有するファイル
s setuidされたファイル
- 次の指定にシンボリックリンクを含む
= ソケット
p 名前付きパイプ(FIFO)
^ それ以降の指定の否定

4.4 拡張グロブ その1[設定:setopt extended_glob]

さらに拡張グロブを設定しておくと、「○○というファイル以外にマッチする」ということもできます。 例えば、

less *.txt~memo.txt    # memo.txt 以外の *.txt にマッチ
rm *~POSCAR*~INCAR~KPOINTS~POTCAR # vaspの入力ファイル以外の出力ファイルを全て消す

後者は POSCAR の後ろに「*」がついてるのに注目のこと。 POSCAR, POSCAR.00, POSCAR.01 なども全て除外されます。 そして、「C-x g」でどう展開されるか確認してから実行します。

4.5 拡張グロブその2[設定:setopt extended_glob]

「その1」は一遍全てマッチさせてから引き算で除外していく感じでした。 「その2」は分かってる条件でマッチさせて、「マッチしている/していない」を反転させます。

ls ^*.txt      # .txtで終わるファイル以外全て
rm ^(POSCAR*|INCAR|KPOINTS|POTCAR)     # 「rm *~POSCAR*~INCAR~KPOINTS~POTCAR」と等価

4.6 グルーピング[デフォルト]

ls *.(sh|txt)   # 拡張子部分が「sh または txt」のもの
ls <10-15>*     # 1015の範囲の数字から始まるもの
ls *<->*        # 数字が省略されたら<最小の数-最大の数>でマッチする。この場合は「任意の数字を含むもの」

名前生成 {sh,txt} や 連番生成 {10..15} とは違って、ファイルとして存在しないものにはマッチしません。 以下に例を示します。

ippei@odin % ls
calc1  calc3  calc5

ippei@odin % ls calc<1-3>
calc1  calc3

ippei@odin % ls calc{1..3}
gnuls: calc2: No such file or directory
calc1  calc3

5 連番生成

以前「名前生成」の項目でちょろっと書いたのですが、zsh では名前生成の中でも特に「連番生成」という機能があります。

echo {1..10}  # 「1 2 3 4 5 6 7 8 9 10」に展開
echo {01..10} # 「01 02 03 04 05 06 07 08 09 10」に展開

ちなみに、{0..99} を tcsh 的にやるなら、{,1,2,3,4,5,6,7,8,9}{0,1,2,3,4,5,6,7,8,9} でしょうか。 {0..255}はとても大変。

私はスクリプトのテスト環境を拵えるのに、以下のようなコマンドをよく使います。

mkdir dir{00..10}
touch file{0..10}

あと for 文と組み合わせると結構強力です。 for 文については後述するつもりですが、一例を挙げときます。

for i in Cd{00..15} ; do ; ssh $i 'ps auxw | grep vasp' ; done

6 ヒストリ

ヒストリは自分の使ったコマンドを記録するものですが、 ノートみたいに便利に引き出せます。 オプションだとか 引数の順序だとか for文の構文だとか、間違いなく記述しなければならないことでも、基本的に自分でキチンと覚えておく必要はありません。 一度打ってしまえば、一度ログアウトした後でもシェルが覚えていてくれますから。

#### history
HISTFILE="$HOME/.zhistory"      # 履歴ファイル
HISTSIZE=10000                  # メモリ上に保存される $HISTFILE の最大サイズ?
SAVEHIST=10000                  # 保存される最大履歴数

#### option, limit, bindkey
setopt  hist_ignore_all_dups    # 既にヒストリにあるコマンド行は古い方を削除
setopt  hist_reduce_blanks      # コマンドラインの余計なスペースを排除
setopt  share_history           # ヒストリの共有

6.1 [C-p], [C-n]

bindkey '^P' history-beginning-search-backward # 先頭マッチのヒストリサーチ
bindkey '^N' history-beginning-search-forward # 先頭マッチのヒストリサーチ

例えば「for 文の構文ってどうだったっけ?」というとき、以前に for文を打ったことがあれば、 「for 」 の後 [C-p] を打てば以前打った for文を遡れます。 あとはチョチョイと修正してゴーです。

6.2 ヒストリサーチ C-r[設定:bindkey -e]

「データベースを更新するコマンドって何だっけ? 『update』という文字列が入ってたのは覚えてるんだけど……」 という時は、「[C-r] update」と打てば、

root@odin /home/ippei # /usr/libexec/locate.updatedb
bck-i-search: update_

みたいな感じで遡れます。

7 グローバルエイリアス

通常、エイリアスというのはコマンドラインの第1要素だけを対象にします。 グローバルエイリアスはコマンドライン上のどの要素であってもエイリアス(別名)として展開されるエイリアスです。

alias -g L="| $PAGER"
alias -g M="| $PAGER"
alias -g G='| grep'
alias -g C='| cat -n'
alias -g W='| wc'
alias -g H='| head'
alias -g T='| tail'
alias -g ....='../..'

ここで $PAGER になっているのは less です。 こうしておけば、「ls | less」を「ls L」で実行できます。

ただし、これらの文字がコマンドにとって意味を持つならば、例えば「echo 」のようにバックスラッシュでエスケープしたり、「echo ‘L’」のようにクォートしてやる必要があります。

岸田は、 W を計算機名にしてしまったときに名前が干渉したことがあります。 グローバルエイリアスは強力すぎるため、利便性とデメリットをよく考えるべきでしょう。

8 for 文(繰り返し処理)

別に zsh 特有の機能というわけではないのですが、一応紹介しときます。例えば、

for i in {1..26} ; do ; ssh moon$i 'ps auxw | grep vasp' ; done
       # ssh moon1 'ps auxw | grep vasp'
       # ssh moon2 'ps auxw | grep vasp'
       # :
       # ssh moon26 'ps auxw | grep vasp'

for i in * ; do ; head -n 5 $i/CONTCAR | tail -n 1 ; done
       # head -n 5 calc-1st/CONTCAR | tail -n 1 ; done
       # head -n 5 calc-2nd/CONTCAR | tail -n 1 ; done
       # :
       # head -n 5 calc-final/CONTCAR | tail -n 1 ; done

for i in */OUTCAR ; do ; grep TOTEN $i | tail -n 1 ; done
       # grep TOTEN calc-1st/OUTCAR | tail -n 1 ; done
       # grep TOTEN calc-2nd/OUTCAR | tail -n 1 ; done
       # :
       # grep TOTEN calc-final/OUTCAR | tail -n 1 ; done

*」の部分に適当な文字列を列挙すれば do と done で囲まれた部分で $i に代入されるという仕組みです。 BASIC や C を知ってる人のセンスでは変数 i は数値(のインクリメント/デクリメント)という印象が強いと思いますが、シェルでは文字列を順次代入します。 perl の foreach 構文の方が近いかもしれません。

ま、こんなもん正確な文法を覚える必要はありません。 一度打ってヒストリに覚えさせたら、次からは「for [C-p]」で呼び出してチョチョイと修正して使えばそれで十分です。

9 コマンドラインスタック[設定:bindkey -e]

/local/MS30/CASTEP/bin/RunCASTEP.sh -np 2 calc

というところまでコマンドラインまで打って、 「計算するのはこのディレクトリじゃなかった、先に cd しとかな」で、 コマンド全部消して cd してまた打つのは面倒ですね。 こういう時は「[ESC]-q」でコマンドラインスタックを使うことができます。 「[ESC]-q」を打つと現在入力中のコマンドラインが一旦消えます。 そこで先に入力すべきだったコマンドを入力すると、そのコマンドを実行した後のプロンプトにはさっきスタックに詰んだ状態のコマンドラインが復帰します。

スタックなので二重三重にすることもできますが、多重スタックを使う人はあまりいないんじゃないかなあ。

スタック
先に入れたタスクが最後に出てくる処理のこと。机の上の本を重ねていって上の本から読んでいくイメージを描くと良いと思います。対立概念は「キュー」。
キュー
スタックとは逆に、先に入れたタスクが先に出てくる処理のこと。パイプにつっこんでいって逆の端から出て来るイメージで良いでしょう。

10 manページの呼び出し[設定:bindkey -e]

たまにしか使わないコマンドを使おうとした時、「あれ、このコマンドのオプションって何付けるべきだったかな?」ということありません? そこで別のターミナルを立ち上げて「man ほげほげ」とかやるのは面倒ですよね。

そーいう時には 「[ESC]-h」 でそのコマンドの man ページを呼び出してくれます。 man を見終わって quit したら、コマンドには先刻打ちかけたコマンドがコマンドラインに復帰します。

11 ディレクトリ名が第1要素のときはそのディレクトリに cd する

setopt  auto_cd # 第1引数がディレクトリだと自動的に cd を補完

好きな人は好きらしいです。 まあ邪魔になるもんでもないので設定してますが、私はほとんど使いません。 私がこれをあまり便利だと思わないのは以下の理由からです。

~/.zshrc に、

function cd() { builtin cd $@ && ls; }

12 関数

tcsh と比べて、zsh のエイリアス機能は制限がキツくなっています。 zsh のエイリアスは「単純な置換」しか許可されていないので、 例えば tcsh で「alias emacs 'emacs \!* &'」とするように 引数をエイリアスの中の任意の個所に挿入することができません。 こーゆー場合、zsh では「関数」を使います。 この関数という機能は大変強力で、 tcsh では簡単な処理でもスクリプトとして 単独のファイルに用意しなければならないところを、 zsh では ~/.zshrc 内部に埋め込んでしまえます。 (なお、関数という機能は bash にはありますが、tcsh には存在しないようです。)

岸田の zsh 初期化スクリプト( http://odin/zshrc ) に仕込んである関数の中から、 面白そうなのを幾つか紹介します。

12.1 psg

全プロセスから引数の文字列を含むものを grep する

function psg() {
    psa | head -n 1      # ラベルを表示
    psa | grep $* | grep -v "ps -auxww" | grep -v grep # grep プロセスを除外
}

12.2 euc, sjis

引数のファイルを euc(改行文字 LF) や sjis (改行文字CRLF)に変換

function euc() {                # 文字コードを euc-jp に変換
    for i in $@; do;
        nkf -e -Lu $i >! /tmp/euc.$$ # -Lu :改行を LF にする
        mv -f /tmp/euc.$$ $i
    done;
}

function sjis() {               # 文字コードを shift-jis に変換
    for i in $@; do;
        nkf -s -Lw $i >! /tmp/euc.$$ # -Lu :改行を CRLF にする
        mv -f /tmp/euc.$$ $i
    done;
}

12.3 google

引数の検索ワードで google 検索。 日本語可。 「w3m というブラウザの使い方が分からない!」という方は「w3m」→「mozilla」とかにすれば多分 OK。

function google() {
  local str opt
  if [ $# != 0 ]; then # 引数が存在すれば
    for i in $*; do
      str="$str+$i"
    done
    str=`echo $str | sed 's/^\+//'` #先頭の「+」を削除
    opt='search?num=50&hl=ja&ie=euc-jp&oe=euc-jp&lr=lang_ja'
    opt="${opt}&q=${str}"
  fi
  w3m http://www.google.co.jp/$opt #引数がなければ $opt は空になる
  # mozilla -remote openURL\(http::/www.google.co.jp/$opt\) # 未テスト
}
alias ggl=google

13 undo[設定:bindkey -e]

zsh はコマンドライン編集での undo が利きます。 デフォルトのキーバインドは[C-/]です。

redo は機能としてはあるらしいのですが、 デフォルトでキーにバインドされていないようです。 私はあまり使いたいと思うことがないので redo を設定していません。

14 右プロンプト

まあそれほど有難味はないかもしれませんが、 環境変数 RPROMPT に適当な文字列を仕込めば、その文字列や変数が右プロンプトになります。

PSCOLOR='00;04;32'      # 細字;下線;緑
RPROMPT=$'%{\e[${PSCOLOR}m%}[%~]%{\e[00m%}' # 右プロンプト

以下に私が右プロンプトを使う理由を挙げます。

14.1 2016年現在の個人的プロンプト

今は改行を使った 2行プロンプトを使っています。

[~]
ippei@Re %                                                            [16-10-12 21:39:14]

1行目にカレントディレクトリを表示する専用の行を作ることで、 長いパス名でも編集に干渉しないようにしています。 右端にシェルに制御が帰って来た時刻を表示しています (現在時刻として時々刻々と変化するわけではありません)。 時間のかかる処理をさせたあととか、地味に有り難いことがあります。 ~/.zshrc から抜粋すると、

PSCOLOR='00;04;32'
RPS1=$'%{\e[${PSCOLOR}m%}[%D %*]%{\e[00m%}'    # 右プロンプト
PS1=$'\n%{\e[${PSCOLOR}m%}[%~]%{\e[00m%}%{\e]2; %m:%~ \a'$'\e]1;%%: %~\a%}'$'\n%{\e[${PSCOLOR}m%}%n@%m ${WINDOW:+"[$WINDOW]"}%#%{\e[00m%} '

15 初期化スクリプトをどこでも使えるようにする

複数の計算機を使っているとそれぞれで異なった動作をさせたい場合があります。

勿論、それぞれ別の初期化スクリプトを作って管理しても良いですが、少しの修正を全ての計算機の全てのファイルに反映させて回るのはちょっとやりたくない仕事です。 また、moon のように NFS 共有されたホームディレクトリを使う場合は、別ファイルでの管理が基本的には出来ません。 こーゆー場合は、初期化スクリプトの中で条件分岐させて動作を制御し、それを複数の計算機にコピーするのが常道です。

#### jman がインストールされているシステムでは jman を man として呼び出す
if [ -x `where jman`  ]; then
    alias man="jman"
fi

#### ls, 補完リストのカラー設定があれば読み込む
if [ -x `where dircolors` ] && [ -e $HOME/.dircolors ]; then
    eval `dircolors $HOME/.dircolors` # 色の設定
fi

#### 個人用設定ファイルがあればそれを読み込む
if [ -e ~/.zshrc_private ]; then
    source ~/.zshrc_private
fi

#### スーパーユーザのプロンプトは赤にする
if [ $UID = 0 ] ; then
    PSCOLOR='01;04;31'      # 太字;下線;赤
else
    PSCOLOR='00;04;32'      # 細字;下線;緑
fi
RPROMPT=$'%{\e[${PSCOLOR}m%}[%~]%{\e[00m%}' # 右プロンプト
PS1=$'%{\e]2; %m:%~ \a'$'\e]1;%%: %~\a%}'$'\n%{\e[${PSCOLOR}m%}%n@%m ${WINDOW:+"[$WINDOW]"}%#%{\e[00m%} '

16 zsh: ホスト名の補完

ssh に続けて「192.168.0.1」とか打つのはかったるいなーとか思いませんか? rsync の引数に「q-eng.imat.eng.osaka-cu.ac.jp」とか、あー面倒。

そういう個人的によく使うホスト名をシェル変数 _cache_hosts に登録しておけば、ホスト名が入り得る箇所ではホスト名を補完候補に挙げてくれます。 ホスト名が入り得る箇所というのは例えば、ssh や scp、rsync の引数などですね。

参考までに、私の zsh の初期設定ファイル(~ippei/.zshrc) では以下のようになっています。

_cache_hosts=(localhost $HOST hashish loki3 mercury
  Li He Pt Au Ti{1,2} Ni{1,2} Co{1..8} Zn{1..8}
  192.168.0.1 192.168.1.1
)

ftp4.jp.FreeBSD.org や mirror.gentoo.gr.jp に頻繁にお世話になるのでしたらこれらを入れといても良いでしょう。 「192.168.……」は入れておくと嬉しい場面がきっとあるでしょう。

17 zsh:環境変数を一時的に変更

コマンドによっては特定の環境変数を要求するものがあります. しかし環境変数は複数のコマンドに共有される「環境」ですので, あるコマンドでの設定が他のコマンドの設定とバッティングする事があります.

例えば環境変数 LANG はユーザの使用する言語と文字コードを定義しますが, 「日本語 EUC を用いる」という値には歴史的経緯から「ja_JP.EUC」と「ja_JP.eucJP」 の2種類(もしくはそれ以上)があります. vim を日本語で適切に動かすには「LANG=ja_JP.EUC」が必要ですが, wmakerconf は「LANG=ja_JP.eucJP」が必要です. これらのコマンドを正常に動作させるには起動する前に環境変数を変更してやる必要があります.

(LANG が 「ja_JP.EUC」の時)
% LANG=ja_JP.eucJP
% wmakerconf
% LANG=ja_JP.EUC

この場合 wmakerconf が終了した場合には再度「LANG=ja_JP.EUC」に戻してやる必要があります. ここで zsh には一時的な環境変数でコマンドを実行する便利な構文が用意されています.

% LANG=ja_JP.eucJP wmakerconf

環境変数の定義に続けてコマンドを記述すると, コマンドには直前に定義した環境変数が渡されますが, シェル本体の環境変数は変更されません. すなわち「環境変数の戻し忘れ」を予防することができます. もし wmakerconf がよく使うコマンドならば, 以下のようにしておくと便利かもしれません.

alias wmakerconf='LANG=ja_JP.eucJP wmakerconf'

bash も同じ構文が使えますが, csh 系では無理っぽいです. (もちろん, 以下のように泥臭くやる方法はあります)

setenv temp $LANG;\
setenv LANG ja_JP.eucJP;\
wmakerconf;\
setenv LANG $temp
unsetenv temp

18 zsh:単語単位のバックスペース

コマンドライン編集中に C-w を打つと, 単語単位でバックスペースを実行してくれます.

% ls /usr/local/etc
  ↓[C-w]
% ls

しかし, パス指定の場合は全部消さずに「/」を単語の区切りとしてバックスペースしてもらった方が便利なことが多いです.

% ls /usr/local/etc
  ↓[C-w]
% ls /usr/local/

この目的のためにはシェル変数 WORDCHARS を設定します.

WORDCHARS='*?_-.[]~=&;!#$%^(){}<>'

WORDCHARS は「アルファベット, 数字以外の文字で単語の一部と見做される文字」を保持します. (参照:man zshparam) ちなみにデフォルトは「*?_-.[]~=/&;!#$%^(){}<>」で, 上記の設定はここから「/」を抜いたものになっています.