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

# まとめ
- zshからfishに乗り換えました
- 移行時にハマった点を紹介します
- 高機能なzshの全体像を把握できていない人にはfishは良い選択肢です


# これまでのあらすじ

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

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

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

- http://fishshell.com/docs/current/tutorial.html
  - 本家本元のチュートリアルページです
  - fishを入れて `help fish` と打っても同じ内容が見れます
- http://fish.rubikitch.com/tutorial/
  - [@rubikitch](https://twitter.com/rubikitch)さんによるチュートリアルの日本語訳ページです
  - チュートリアルの他にも設計理念のページの翻訳やTipsについても載っておりとても有益です

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


# 移行時の障壁

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

- 各種構文の違い
- zleを使った各種カスタマイズ
- グローバルエイリアス

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

## 構文の違い

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

### 構文の違いの例

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

```zsh
if CONDITION; then
    COMMAND_TRUE
else
    COMMAND_FALSE
fi
```

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

```fish
if CONDITION
    COMMAND_TRUE
else
    COMMAND_FALSE
end
```

大体同じですが `then` や `fi` を書かなくて良くなっています。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で動きません。

```zsh
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で変数への格納する際の方法が異なっており、注意が必要です。

```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らしい設定になります。

```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
```

```fish:$XDG_CONFIG_HOME/fish/functions/fish_user_key_bindings.fish
function fish_user_key_bindings
    bind \cgc __select_ghq_cd
end
```

[antigen](http://antigen.sharats.me/)や[zplug](https://github.com/zplug/zplug)を使ってプラグインを導入したり自前のスクリプトで作業の効率化を行っている人ほどfishへ移行するのが大変ですが、そこは気合で書き直すか、[fisherman](https://github.com/fisherman/fisherman)などのプラグインマネージャーを使うなどして機能を揃える必要があります。

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

## グローバルエイリアス

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

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

```zsh
alias -g P='| peco'
```

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

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

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

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

これでは困ります。

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

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

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

```fish
abbr ll 'ls -lh'
```

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

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

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

### 最終的に自作することに

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

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

```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
```

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

```fish:$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を含む各種設定ファイルは[こちら](https://github.com/masa0x80/dotfiles)にあります。のっぴきならない事情が発生しzshに戻らないといけなくなった場合に備えてzshの設定は残してありますので、`.zsh/` 配下にあるzshの設定と `.config/fish/` 配下にあるfishの設定を比較してみるのも面白いかもしれません。