From zsh to fish: a shellfish out of water

I’ve been using zsh for a while and I’ve been pretty content with it. I love the deep levels of customizability you can achieve with it, but it comes at a cost. At the end of the day, said configurations aren’t portable enough and need to be frequently updated when moving my .zshrc from one Linux distribution to the next, and even sometimes to Mac OS X or Windows…

The fish shell is one of those things that has been in my list of things to check out for a long time, and now that I finally gave it a try, I can safely say that I regret not migrating to it sooner. I’m pretty sure I will not going back to bash or zsh anytime soon.

What makes fish stand out

Functional

In fish, everything is function. From what I could see (and read in the fantastic manual, said function are lazily evaluated (meaning that you won’t be warned of syntax errors in a function you wrote until it’s actually used) and can be composed of primitive instructions, just like any other shell, and from functions provided by fish on startup. These functions include stuff like git status and staging support

Replaces other tools

An other point of importance is that fish replaces not only your shell, but also other third party userland tools. A lot of interesting completion and navigation behavior are supported out of the box, without the need of additionnal configuration.

Documentation

Fish has a comprehensive manpage, but it also has a built-in web-based documentation (reachable by entering help in a running fish shell) as well as a web-based configuration tool (fish_config).

Completion from the man pages

An interesting aspect of fish is that it completes commands by using a self-made database of the possible arguments built from the man pages you have on your machine. To update that database, simply run fish_update_completion from the shell itself.

Works out of the box

An appreciable aspect of fish, especially when compared to zsh (which has a lot of cool features too) is that most of the great smart completion will work out of the box without the need to run a wizard or to write a comprehensive configuration file. It’s important to note that it is also very portable.

Migrating my zsh configuration

The main configuration file of fish is located under ~/.config/fish/configg.fish for the current user.

My configuration is roughly based on my old .zshrc and I’ll use this as an excuse to showcase some aspects of my fish configuration file.

Variables

Variables are set with the following syntax:

set PATH ~/Programs/bin $PATH

Like in any shell, they’re mostly used to defined variables expected by other programs (just like the Go and Haskell programming enthronement) or to expand the shell’s path.

I set my GOPATH, add a bunch of tools and output directories to the path, set my default text editor and browser:

# Some default applciations
set -U EDITOR vim
set -U BROWSER firefox

# Stuff for Haskell development
set PATH ~/.cabal/bin $PATH

# Stuff for Android development
set PATH ~/Programming/Android/sdk/platform-tools $PATH
set PATH ~/Programming/Android/sdk/tools $PATH

# Stuff for Go development
set GOPATH ~/Programming/Go/
set PATH $GOPATH/bin $PATH

I also disable the greetings by seting the fish_gretting function to nothing:

# Disable the default greetings
set fish_greeting

Aliases

Aliasing is done through a simple syntax:

alias cd.. "cd.."

It should be noted that aliases won’t show up in the completion list unless they are named after executables in your path. If you want them to show up, you have to define your alias as a function and define a completion syntax for it (see Custom functions).

Coloring

I use aliases a lot to color various command line tools that haven’t this set as their default behavior (ls, grep, tree…). Fortunately, most of these are already covered by fish itself since it defines alias functions for most of these tools and enable the coloration there. That’s one less thing to port to my new config file.

The default colored commands don’t include tree so I do that myself:

alias tree "tree -C"

open

open is a functionality I fell in love with when using Mac OS X. With zsh on Linux I had to install xdg-open and aliased it to open. Once again, fish handles that by default in a cross-platform manner. Again, nothing to port and one less platform dependant executable to install.

Custom functions

In zsh like in any shell, you can implement your own functions. I made some use of this feature in my default configuration with extract function (which I probably stole frome somebody else’s .zshrc). This function simply handles multiple decompression procedures for multiple common archive formats.

# Unarchive stuff
extract () {
    if [ -f $1 ] ; then
        case $1 in
            *.tar.bz2)   tar xvjf $1        ;;
            *.tar.gz)    tar xvzf $1     ;;
            *.tar.xz)    tar xvfJ $1 ;;
            *.bz2)       bunzip2 $1       ;;
            *.rar)       unrar x $1     ;;
            *.gz)        gunzip $1     ;;
            *.tar)       tar xvf $1        ;;
            *.tbz2)      tar xvjf $1      ;;
            *.tgz)       tar xzf $1       ;;
            *.zip)       unzip $1     ;;
            *.Z)         uncompress $1  ;;
            *.7z)        7z x $1    ;;
            *)           echo "'$1' cannot be extracted via extract()" ;;
         esac
     else
         echo "'$1' is not a valid file"
     fi
}

I rewrote this zsh function for the fish like so:

# Unarchive stuff
function extract
    if [ $argv ]
        if [ -f $argv ]
            switch $argv
                case '*.tar.bz2';   tar xvjf $argv
                case '*.tar.gz';    tar xvzf $argv
                case 'tar.xz';      tar xvfJ $argv
                case '*.bz2';       bunzip2 $argv
                case '*.rar';       unrar x $argv
                case '*.gz';        gunzip $argv
                case '*.tar';       tar xvf $argv
                case '*.tbz2';      tar xvjf $argv
                case '*.tgz';       tar xzf $argv
                case '*.zip';       unzip $argv
                case '*.Z';         uncompress $argv
                case '*.7z';        7z x $argv
                case '*' 
                    printf "\"%s\" cannot be extracted with this command\n" $argv
            end
        else
            echo "\"$argv\" is not a valid file"
        end
    else
        echo "This command expects a paramter"
    end
end

Completion for custom functions

One of the strongest points is the ability to easily define completion options for your custom commands. I added the following section to let the shell filter the possible arguments according to the proper file types expected by the extract script thus reducing the chances of a mistake even more.

complete -c extract -x -a "(
    __fish_complete_suffix .tar.bz2
    __fish_complete_suffix .tar.gz
    __fish_complete_suffix .tar.xz
    __fish_complete_suffix .bz2
    __fish_complete_suffix .rar
    __fish_complete_suffix .gz
    __fish_complete_suffix .tar
    __fish_complete_suffix .tbz2
    __fish_complete_suffix .tgz
    __fish_complete_suffix .zip
    __fish_complete_suffix .Z
    __fish_complete_suffix .7z
)"

This is what the completion looks like:

Prompt

In fish, the prompt is defined by the fish_prompt function.

Here’s mine:

# Prompt
function fish_prompt
        set last_status $status
        printf '%s@%s:%s%s%s ' (whoami) (hostname) (set_color $fish_color_cwd) (prompt_pwd) (set_color normal)

        # Sets the color of the last part of the prompt
        # according to the status of the last command
        set_color red
        switch $last_status
        case 0
            set_color green
        end
        echo -n "% "
        set_color normal
end

Git integration

In zsh I used the __git_ps1 script provided by git itself to display the name of the branch of the current repository. Now fish handles git integration out of the box, you can even set custom characters depending of your current editing state (staging status, HEAD location… that kind of stuff).

I add these lines to define the needed characters (and color configuration):

# Fish git prompt configuration
set __fish_git_prompt_showdirtystate 'yes'
set __fish_git_prompt_showstashstate 'yes'
set __fish_git_prompt_showupstream 'yes'
set __fish_git_prompt_color_branch yellow

set __fish_git_prompt_char_dirtystate '⚡'
set __fish_git_prompt_char_stagedstate '→'
set __fish_git_prompt_char_stashstate '↩'
set __fish_git_prompt_char_upstream_ahead '↑'
set __fish_git_prompt_char_upstream_behind '↓'

Once this is set, calling the __fish_git_prompt will bring up all the git status info if needed.

Right side of the prompt line

zsh offered a RPROMPT variable to hold the stuff you want to display in the right side of your prompt. I put my the name of the current git branch if I’m in a repo here.

In fish, the right side of the prompt is defined in a function called fish_right_prompt.

I just use the __fish_git_prompt function in it to display the git status if I’m in a git repo:

function fish_riright_prompt 
        printf '%s ' (__fish_git_prompt)
end

And that’s it, I have everything that I need.

Conclusion

fish is cool and you should be using it. Give it a try if you haven’t already.

Additional links

Bonus: Drawing the fish logo

This issue on GitHub features a neat little function you can define in your configuration file to draw the fish shell logo in your shell and in color.

You can call it in the body of your fish_welcome function to display the logo each time time you start up a shell.



Enter your desired user name/password and after your comment has been reviewed by an admin it will be posted and your account will be enabled. If you are already registered please login before posting.