Zsh
Linux
zshrc

俺的爆速zshrc/zprofile

どうも。
zsh使ってますか?zsh。

zshユーザーにはおなじみの.zshrcですが、自分が最高の反応速度を叩き出すように、起動やコマンド実行に対して高速化するように改良しました。
また、新しく追加するときや、あとから見返したときの可読性などもあげる工夫をしているので、ぜひ参考にしてください。

ファイル分割

zshrcを、意味のあるファイルに分割します。

まず、以下のようなシェルスクリプト(Makefileでも可)を作成します。

make.sh
touch ../dists/zshrc
echo > ../dists/zshrc
rm -f ~/.zcompdump  # Clear zcomp cache


command ls ./zshrc.d/*.zsh       | sort | xargs cat >> ../dists/zshrc

if which zsh >/dev/null;then
    # zshが存在すれば、./zshrc.d/hooks/以下のスクリプトをすべて実行し、結果をzshrcに書き出す。
    find ./zshrc.d/hooks -maxdepth 1 -name *.zsh | sort | xargs zsh >> ../dists/zshrc
else
    echo "zsh not found"
fi

zsh -n ../dists/zshrc # 文法チェック
zsh -c "zcompile ../dists/zshrc" # コンパイル
zsh -c "autoload -Uz compinit && compinit" # compinitの実行

見てわかるように、./zshrc.d/*.zshを連結して、../dists/zshrcに書き出します。
また、後述するhookも起動し、結果を../dists/zshrcに書き出します。
(本当はrun-partsを使ってもう少しシンプルにしたかったのですが、そうするとシェルスクリプトでないファイルまでrun-partsが実行しようとしてエラーになりました。原因が分かった人がいたらおしえろください。)

compinitについては、~/.zcompdumpさえあれば動作するため、zshrcの中では呼び出さず、make.shの中でのみ削除・更新するようにしています。

この上で、自分はzshrc.dを以下のように分割しています。

$ ls ./zshrc.d/
00-exec.zsh  10-autoload.zsh  20-zstyle.zsh  30-setopt.zsh  40-alias.zsh  50-prompt.zsh  60-bindkey.zsh  99-end.zsh  hooks/

00-exec.zshはtmuxやfbtermの実行、99-end.zshはzprofの起動をしています。
それ以外のファイルはファイル名どおりの記述をしています。

同じように、zprofileもmake.shを生成します。

zplug / hook

プラグイン管理についてです。
自分は、zplugというプラグインマネージャーを利用していましたが、起動が重いことに悩まされました。
結局、zplugは通常のシェルでは使わないことにしました。
その代わりに、zplug installやzplug updateはmake.shの中でのみ呼び出すようにしています。
また、zplug loadはやめて、zplugが管理するプラグインディレクトリの中のzshファイルを検知し、source文をzshrcの中に書き出すようにしています。

このために作られたのがhookです。mkinitcpioだったりgrub-mkconfigに触発されています。内容はただのシェルスクリプトで、「シェルスクリプトの標準出力がzshrcに吐き出される」ようになっています。
詳しい動作はさきほどのmake.shを確認してください。

これにより、柔軟にzshrcを動的生成できるようにしました。

たとえば、実際のzplugのhookは以下のように作りました。

zplug.zsh
cd `dirname $0`

if [[ ! -d ./zplug ]] ;then
    echo zplug not found >&2
    exit
fi

export ZPLUG_HOME="$PWD/zplug"
export ZPLUG_REPOS=$MYLOCAL_DIST/.zsh-plugins
export ZPLUG_BIN=$ZPLUG_REPOS/bin

source $ZPLUG_HOME/init.zsh

zplug "zsh-users/zsh-autosuggestions"
zplug "zsh-users/zsh-completions", lazy:true


zplug check --verbose >&2 || zplug install >&2
zplug update >&2

zplug load --verbose >&2
zplug clear >&2

list=(`zplug list`)

echo "##### zplug #####"

for i in $list ; do
    eval $(zplug info $i | sed -e 's/-//' | sed -e 's/: /=/'|grep "dir" | sed 's/\x1b\[[0-9;]*m//g')
    echo source $(ls $dir/*.zsh | head -n1)
done

echo "##### /zplug #####"

zplug.zshがあるディレクトリと同じ場所にzplugをgit submoduleとして読み込ませています。
$MYLOCAL_DISTについては../distが入っています。

最後のevalについては、zplug loadしたあとにzplugが吐き出すyamlをシェルスクリプトだと誤認させて無理やり実行し、値を環境変数として読み出しています。なんちゃってパーサーです。

それ以外に特に難しいところは無いと思います。

ちなみに、zshrcのなかでこのzplug.zshによって生成された部分は以下のようになります。

zshrc
# 中略

##### zplug #####
source /home/katio/.conf-to-git/zsh/../dists/.zsh-plugins/zsh-users/zsh-completions/zsh-completions.plugin.zsh
source /home/katio/.conf-to-git/zsh/../dists/.zsh-plugins/zsh-users/zsh-autosuggestions/zsh-autosuggestions.plugin.zsh
##### /zplug #####

# 中略

ちなみに、今は使っていませんが、anyenvのhook版のあります。anyenv initがあまりにもおそすぎて気に入らなかったので作りましたが、今はanyenv自体を使っていないです。

anyenv.zsh.old
    #eval "$(anyenv init - --no-rehash)"
    #eval "$(anyenv init -)"
    #anyenv() {
    #   typeset command
    #   command="$1"
    #   if [ "$#" -gt 0 ]; then
    #       shift
    #   fi
    #   command anyenv "$command" "$@"
    #}

if [[ -d $ANYENV_ROOT ]] ; then
    echo "export PATH=$ANYENV_ROOT/bin:\$PATH"
    xenvlist=(`command ls $ANYENV_ROOT/envs`)
    for xenv in ${xenvlist[@]};do 
        cat <<- EOS
            export ${xenv}_ROOT='$ANYENV_ROOT/envs/${xenv}'
            export PATH="$ANYENV_ROOT/envs/${xenv}/bin:\$PATH"
            export PATH="$ANYENV_ROOT/envs/${xenv}/shims:\$PATH"
            hash -r 2>/dev/null || true
            ${xenv}() {
                local command
                command="\$1"
                if [ "\$#" -gt 0 ]; then
                    shift
                fi

                case "\$command" in
                rehash|shell)
                    hash -r 2>/dev/null || true;;
                *)
                    command ${xenv} "\$command" "\$@";;
                    esac
            }
        EOS
    done
else
    echo 'ANYENV_ROOT is not set.'
fi

zprofile

zprofileも同じ要領で作っていきます。
先程のmake.shに以下を追記します。

make.zsh
# 中略

touch $MYLOCAL_DIST/zprofile
echo -n > ../dists/zprofile
find ./zprofile.d -maxdepth 1 -name '*.zsh' | sort | xargs cat >> ../dists/zprofile
if which zsh >/dev/null;then
    find ./zprofile.d/hooks -maxdepth 1 -name *.zsh | sort | xargs zsh >> ../dists/zprofile
else
    echo "zsh not found"
fi
zsh -n ../dists/zprofile
zsh -c "zcompile ../dists/zprofile"

./zprofile.dは以下のようになっています。

$ ls ./zprofile.d/
10-env.zsh  30-common.zsh  40-path.zsh  hooks/

PATH

PATHやLD_LIBRARY_PATHなど、prefixがパソコンの中に複数ある場合の処理です。
複数のPCでzprofileを使いまわしているため、以下のように、ディレクトリが存在する場合のみ読み込むようにしました。

40-path.zsh
add-bin(){
    if [[ -e "$1" ]];then
        export PATH=$1:$PATH
    fi
}

add-local-path(){
    if [[ -e "$1" ]];then
        export PATH="$1/sbin:$PATH"
        export PATH="$1/bin:$PATH"
        export LD_LIBRARY_PATH="$1/lib:$LD_LIBRARY_PATH"
        export LD_LIBRARY_PATH="$1/lib64:$LD_LIBRARY_PATH"
        export LD_RUN_PATH="$1/lib:$LD_RUN_PATH"
        export LD_RUN_PATH="$1/lib64:$LD_RUN_PATH"
        export PKG_CONFIG_PATH="$1/lib/pkgconfig:$PKG_CONFIG_PATH"
        export PKG_CONFIG_PATH="$1/lib64/pkgconfig:$PKG_CONFIG_PATH"
        export MANPATH="$1/share/man:$MANPATH"
        export INFOPATH="$1/share/info:$INFOPATH"
        export XDG_DATA_DIRS="$1/share:$XDG_DATA_DIRS"
        PY2VERSION="2.7.14"
        PY3VERSION="3.6.3"
        PY2MODULE="$ANYENV_ROOT/envs/pyenv/versions/$PY2VERSION/lib/python2.7/site-packages/custom.pth"
        PY3MODULE="$ANYENV_ROOT/envs/pyenv/versions/$PY3VERSION/lib/python3.6/site-packages/custom.pth"
        if [[ -e "$ANYENV_ROOT/envs/pyenv/versions/$PY2VERSION" ]];then
            if [[ ! -e "$PY2MODULE" ]] || ! grep "$1/lib/python2.7/site-packages" "$PY2MODULE" >/dev/null 2>&1;then
                echo "$1/lib/python2.7/" >> $PY2MODULE
                echo "$1/lib/python2.7/site-packages" >> $PY2MODULE
                echo "$1/lib/python2.7/site-packages/gtk-2.0" >> $PY2MODULE
            fi
        fi
        if [[ -e "$ANYENV_ROOT/envs/pyenv/versions/$PY3VERSION" ]];then
            if [[ ! -e "$PY3MODULE" ]] || ! grep "$1/lib/python3.6/site-packages" "$PY3MODULE" >/dev/null 2>&1;then
                echo "$1/lib/python3.6/" >> $PY3MODULE
                echo "$1/lib/python3.6/site-packages" >> $PY3MODULE
                echo "$1/lib/python3.6/site-packages/gtk-2.0" >> $PY3MODULE
            fi
        fi
    fi
}

add-bin /bin
add-bin /sbin
add-local-path /usr
add-local-path /usr/local
add-local-path /home/local
add-bin /home/local/python/anaconda3/bin
add-local-path /home/local/python/anaconda3
add-bin /usr/lib/ccache/bin
add-bin $HOME/.yarn/bin
add-bin ./node_modules/.bin

pyenvや、ネイティブのパッケージマネージャーなど、複数のツールでpythonのライブラリを管理する場合、うまく解決ができないため、動的に.pthファイルを作り出してpyenvのpythonに読み込ませています。
これについては、いつかまた別の記事で詳しく書こうと思います。

今考えると、この環境変数処理は環境依存のため、hookにしてmake.shを実行するときのみif文で判断し、動的にexport文を書き出したほうが数ミリ秒早くなりそうな気がしてきました。今度やります。

まとめ

hookを使ったzplugを使わなくていいzshrcで、ずいぶん読み込みが早くなりました。
今回紹介したdotfile群は、githubで公開しています。ぜひ参考にしてもえらると嬉しいです。

https://github.com/onokatio/.conf-to-git/tree/master/zsh