Edited at

zshからfishに乗り換えてみた

More than 1 year has passed since last update.


まとめ


  • zshからfishに乗り換えました

  • 移行時にハマった点を紹介します

  • 高機能なzshの全体像を把握できていない人にはfishは良い選択肢です


これまでのあらすじ

zshは名前に Z がついていてカッコいいですし、自分好みの設定をコツコツ作り上げていく楽しみがある反面、多機能で何がどうなっているのかわかりにくい部分があります。一方fishは、名前が魚を意味する単語でカッコ良さの欠片もありませんが、ゲーム開始直後にコナミコマンドで最強装備にするように、インストール直後の状態でもシンタックスハイライトや補完が有効な状態になっていますし、更に設定を追加することで、より自分好みに改善するという余地まであります。

fishについては以前から気になっていましたが、普段の作業はzshでも問題なく行えていましたし、何と言ってもシェルを変更するというのは一大事ということで踏みとどまっていました。しかし、先日まとまった時間が取れたのでfishへの移行にチャレンジし、普段使いしても問題ない状態になったので記事を書くことにしました。

ちなみに、「fishは初めてでもっと詳しく知りたい」という方には下記ページがオススメです。

設定をいじりながら理解を深めたいという方は、インストール後に fish_config と入力するのがオススメです。こちらにも説明がありますが、ブラウザー上でfishの設定を確認・変更できます。シェルの設定をブラウザーからポチポチするのはなかなか画期的ではないでしょうか。


移行時の障壁

fishに移行にあたり、下記の問題に対処する必要がありました。


  • 各種構文の違い

  • zleを使った各種カスタマイズ

  • グローバルエイリアス

以降、それぞれの対処方法について紹介します。


構文の違い

zshとfishでは各種構文が異なります。これへの対処方法は徐々に慣れる以外特にありません。しかし、fishの構文の方が直感的なので慣れることができた暁には幸せになれます。


構文の違いの例

例えば、zshで下記のように記述していた内容が、

if CONDITION; then

COMMAND_TRUE
else
COMMAND_FALSE
fi

fishでは下記のようになります。

if CONDITION

COMMAND_TRUE
else
COMMAND_FALSE
end

大体同じですが thenfi を書かなくて良くなっています。ifを fi で閉じるのは慣れれば問題はないかもしれませんが、caseを esac で閉じるというのはわかりにくさ(考えればタイプできても、瞬時にタイプできない)を感じていました。一方、fishではとりあえず end と書いておけば良いので条件反射で書ける点が良いですね。

また別の例としては、zshで $? として取得していたコマンドの終了ステータスがfishでは $status で取得できますし(9/20追記:zshでも $status で取得できました)、zshで COMMAND1 && COMMAND2 などと書いていたのもfishでは COMMAND1; and COMMAND2 と書くことになります。(個人的には &&|| が使えないのは違和感の方が多かったです...)

zshに慣れていると最初は戸惑いますが、fishの構文の方が直感的に書けるので気に入っています。


zleを使った各種カスタマイズ

pecoやfzfを使って作業の効率化を行っている方は多いと思います。

例えば、zshで ghq 管理しているリポジトリーにpecoやfzfを使って移動を行いたい場合は下記のような設定を記述していると思いますが、このままではfishで動きません。

peco-ghq-cd() {

local repository_path=$(ghq list --full-path | sed -e "s|$HOME/||g" | peco --query "$LBUFFER")
if [ -n "$repository_path" ]; then
BUFFER="cd ~/$repository_path"
zle accept-line
fi
zle clear-screen
}
zle -N peco-ghq-cd
bindkey '^gc' peco-ghq-cd

fishでは下記のようにする必要があります。特にzshとfishで変数への格納する際の方法が異なっており、注意が必要です。

function __select_ghq_cd

commandline | read -l buffer
ghq list --full-path | \
sed -e "s|$HOME/||g" | \
fzf --query "$buffer" | \
read -l repository_path
if test -n "$repository_path"
commandline "cd ~/$repository_path"
commandline -f execute
end
commandline -f repaint
end

function fish_user_key_bindings
bind \cgc __select_ghq_cd
end

補足ですが、この設定を $XDG_CONFIG_HOME/fish/config.fish に書いても動きますが $XDG_CONFIG_HOME/fish/functions/ 配下に2つのファイルを配置する方がfishらしい設定になります。


$XDG_CONFIG_HOME/fish/functions/__select_ghq_cd.fish

function __select_ghq_cd

commandline | read -l buffer
ghq list --full-path | \
sed -e "s|$HOME/||g" | \
fzf --query "$buffer" | \
read -l repository_path
if test -n "$repository_path"
commandline "cd ~/$repository_path"
commandline -f execute
end
commandline -f repaint
end


$XDG_CONFIG_HOME/fish/functions/fish_user_key_bindings.fish

function fish_user_key_bindings

bind \cgc __select_ghq_cd
end

antigenzplugを使ってプラグインを導入したり自前のスクリプトで作業の効率化を行っている人ほどfishへ移行するのが大変ですが、そこは気合で書き直すか、fishermanなどのプラグインマネージャーを使うなどして機能を揃える必要があります。

ここがfish移行時の最大の山場になると思います。がんばりましょう。


グローバルエイリアス

zshにはグローバルエイリアスという便利な機能があります。

例えば下記のような設定を行っている状態で、ls P でreturnを押すと P が展開され ls | peco を入力してreturnを押したのと同じ状態となります。

alias -g P='| peco'

つまり、文の途中に書いてもよしなに展開してくれるので、タイプ数の削減につながるのですが、これをfishで実現しようとすると少し困ります。


まずは、aliasの使用を検討してみた

fishにもzshにもaliasという機能がありますが、fishのaliasはzshのものとは違います。ドキュメントを読むと、fishのaliasはfunctionビルトインの簡易ラッパーであることが判明します。

fishのaliasで定義されたものは、すなわちfunctionで定義されたコマンドであるため、行頭またはセミコロンやパイプの後に書いた場合にしか展開されません。したがって、上述のようなパイプと組み合わせたエイリアスは上手く動作しません。

これでは困ります。


次に、abbrの使用を検討してみた

fishにはabbrという機能(abbrという名前の通り、略語を定義することができる)があります。

例えば、下記のような設定を行い、

abbr ll 'ls -lh'

ll と入力してreturnを押すと ls -lh と展開されてからコマンドが実行されます。ヒストリーには展開後のコマンドが保存されますので、タイプ数の削減とヒストリーの正規化が両立できます。

これを使って abbr ll 'ls -lh' としても良いように思いますが、こちらも行頭でないと上手く展開してくれません。

こちらも今回の用途には不適です。


最終的に自作することに

fishは入れるだけで良さげな状態になっていますが、更にカスタムすることもできます。やりたいことが標準の方法でできないのであれば自作すればよいのです。(良い方法があるのかもしれませんが、見つけることができなかったので作りました。)

まず、下記のような関数(入力値の中から事前にエイリアスとして設定したキーワードを置換して実行する関数)準備します。


$XDG_CONFIG_HOME/fish/functions/__execute_wrapper.fish

function __execute_wrapper

commandline | read -l buffer

string replace -a -r '(;|\|)' ' $1' $buffer | read buffer

for word in (string split ' ' $buffer)
for keyword in P
if string match -q $keyword -- $word
switch $word
case P
set replacement '| peco'
end
string replace $keyword $replacement $buffer | read buffer
end
end
end

string replace -a -r '\s+' ' ' $buffer | read buffer
string replace -a ' ;' ';' $buffer | read buffer

commandline $buffer
commandline -f execute
end


次に、下記の要領でキーバインドの設定を行います。


$XDG_CONFIG_HOME/fish/functions/fish_user_key_bindings.fish

function fish_user_key_bindings

bind \cj __execute_wrapper
bind \r __execute_wrapper
end

少々アグレッシブな設定にはなりますが \r にbindするとreturnを押した瞬間に置換と実行が行われるのでzshと同じような使用感となります。


おわりに

zshっからfishへ移行した際にハマった点について紹介しました。fishへの移行作業には2週間程度かかりましたが、設定ファイルがスッキリしましたし、日常利用しても問題ない状態となり満足感が高いです。特に、abbrの設定を行っておくとヒストリーが正規化されることと、returnを押すと瞬時に略語が展開されるという挙動がとてもクールで気に入っています。

ただ、現状でzshを十分使いこなしている人がfishに移行してメリットがあるかというと、特にないように思います。その一方で、あまりシェルをカスタムしていない人がfishを導入すると、補完とシンタックスハイライトとオートサジェストの便利さにより生産性が圧倒的に高まります。また、zshよりもfishの方がコンパクトにまとまっており学習コストが低いので、多機能なzshの全貌を把握しきれないという人にとっては良い選択肢だと思います。

最後に、fishを含む各種設定ファイルはこちらにあります。のっぴきならない事情が発生しzshに戻らないといけなくなった場合に備えてzshの設定は残してありますので、.zsh/ 配下にあるzshの設定と .config/fish/ 配下にあるfishの設定を比較してみるのも面白いかもしれません。