Fish: further exploration

[Tweet]

Over the past few months I’ve been playing with Fish, the Friendly Interactive SHell, and I’ve posted a couple of times on the topic. I think I’m sold on it now, and you’ll probably be seeing a lot more Fish posts than Bash ones (though I’ll try to offer Bash equivalents when possible).

So I’ve officially chsh‘d to /usr/local/bin/fish and am 100% comfortable using it day to day. For the record, I did spend some time with Zsh as well, but it just didn’t tickle the same fancy as Fish has for me.

Here are some of my tips, tricks, and observations since my last post.

Autoloading Functions

I had originally been sticking with my Bash habits and defining all of my functions in files I sourced at login. But you don’t have to store a bunch of functions in memory with Fish. It can load functions temporarily when they’re needed. You just place a file in ~/.config/fish/functions/, titled with the same name as the main function, and with a .fish extension. Ideally one “main” function per file, with any necessary helper functions/subcommands.

For example, my Fish port of bid, my command for getting the bundle id of any installed app, exists at ~/.config/fish/functions/bid.fish, so when I run bid curio, it loads the function automatically and returns /Applications/Curio 13 Preview.app: com.zengobi.curio.

This is a great way to improve performance, and with tools like funced (edit or create any function) and funcsave (save any function from memory to your autoload folder), it’s easy to gather all your odds and ends. It’s also really obvious where a function is sourced from, you just grep the folder for the command name… although Fish’s type command (and functions -vD) do an excellent job of pointing you to exactly where just about any function is defined.

Creating autoload functions quickly

In addition to funcsave, using the alias function with the -s switch automatically saves an autoload function for you. There’s really no alias concept in Fish, it’s just shorthand for functions. Using alias gc=git commit -am creates a function:

function gc --description 'alias gc=git commit -am'
    git commit -am $argv;
end

Once created, you can always save it (and any function in memory) to your autoload folder with funcsave [function_name]. But if you run alias -s gc=git commit -am instead (with the -s), it will go ahead and finish the job in one step, creating and writing the aforementioned function to ~/config/fish/functions/gc.bid.

Prompt stuff

My current prompt:

iTerm marks for navigation, a highlighted path, ruby version, git branch and status, time stamp on the right side, special features when you’re in an SSH session. When the previous command returns an error, the > turns red and the error number is listed in red on the right side.

Here are the cool things that I’ve learned on the way:

  1. The string commands are awesome. split, join, match, replace, trim, escape, and more. It’s how I did the highlighting on the path without any headache.
  2. The set_color command is great. No more escape codes, it accepts hex values (3 or 6 digit), and can perform dim, bold, underline, etc. on any of 256 colors.
  3. You can inject iTerm marks with an escape code, but it’s easier to use the Fish shell integration.
  4. You can include a right_prompt function in your fish_prompt.fish file and it will generate content to display at the right side of the Terminal for each prompt.

A special SSH prompt

I work mainly on my MacBook Pro, and have three minis, two in the house, one co-located. The minis all run headless (no displays) and I do most of my work on them via SSH. I’ve loaded Fish on all of them now, and synced all of the config files, so my prompt looks exactly the same no matter which machine I’m logged in to. Which is dangerous.

I like my prompt to remind me if I’m in an SSH session on another machine. I don’t just want a user@host in the prompt as that’s easy to get used to seeing and miss it changing. I wanted a badge that only showed during an SSH session, so I added the following function. It’s a good demo of both the set_color function and some string functions:

# Adds a badge if we're in an SSH session (first letter of hostname, uppercased)
function __ssh_badge
    # See if any standard SSH environment variables contain anything
    if test -n "$SSH_CLIENT$SSH2_CLIENT$SSH_TTY"
        # dark purple on light purple
        set_color -b d6aeec -o 2a0a8b
        # first character of remote hostname, uppercase
        echo -n " "(string upper (string sub -s 1 -l 1 (hostname -s)))" "
        set_color normal
    end
end

The badge this function creates is the first letter of the hostname, capitalized, reversed out on a bright background.

You can see that I’m using set_color with hex colors, which is so much easier than escape codes that it makes me tear up a little every time I do it.

To get the letter, I’m using the string sub function to get a substring (1,1) of hostname -s, and then string upper to uppercase it. The result:

If you’re interested, here’s my entire fish_prompt.fish file, based on Bira. I just call it “Bira’s Weird Cousin.”

More Colors

I spend too much time messing with my colors in iTerm and Terminal, and now that I use a shell that fully embraces a 256-color palette, it hasn’t gotten any better. I love that I can visually modify the various colors that show up on the fish command line using the browser-based configuration tool.

Just run fish_config colors to get to open your browser to a screen where you can assign colors to every element (errors, redirection, autcomplete suggestions, etc.). Because it’s actually assigning hex codes, the colors from this screen show up as intended, undisturbed by whatever palette you’re using in your terminal app. (This does mean that when I switch terminal themes, in most cases, I’ll have to update my command line colors to fit. I’ll figure out how to automate that…)

The fish_config tool can also swap out your prompt for some pre-configured options, review all available functions (built-in and custom), see all environment variables and their values, command history, key bindings, and abbreviations.

Abbreviations

Speaking of abbreviations, this is a pretty cool feature. Much like TextExpander, you can assign snippets to expand in place. For example, ga can expand to git commit -am. These are different from aliases in that they substitute in while you’re typing, allowing you to fully edit the results as desired.

My two favorite abbreviations right now are for directory navigation. Which is its own cool thing: prevd and nextd are the fish equivalent to pushd and popd, but they can traverse your directory history no matter what commands you’ve used to navigate (cd, z, fasd, etc.). Anyway, here are two abbreviations I find useful:

abbr -a -U -- - prevd
abbr -a -U -- = nextd

Now I can just type - to go to the previous directory, = to go forward, fully repeatable (whereas cd - just shuffles you back and forth between the two most recent directories).

Bindings

Binding keys to commands in fish is as easy as can be (though in fairness it wasn’t that hard in Bash, either). One of my favorite bindings is part of the fzf package (installed via ohmyfish). It binds ⌃r to a fuzzy reverse isearch through your command history. This was a default keybinding from Bash that I was missing, but this replacement is significantly more powerful than the Bash version.

My other favorite keybindings include a couple of experiments I’ve been playing with. These bind ⌥, to repeat the last token in the command line, and ⌥z to a function for deleting the extension of the last token. This makes changing the extension of a long filename as easy as completing the file path/name once, hitting ⌥, to repeat it, and hitting ⌥z to remove the extension, ready for a new one. I know, it’s a lot of work for a not-very-big-deal, but it was a good learning exercise.

Here are the functions, defined in the autoloading directory:

commandline.fishraw
# Defined in /Users/ttscoff/.config/fish/functions/__prev_token.fish @ line 1
function __prev_token -d "repeats last token on the command line"
    set -l buffer (commandline -bo)
    commandline -a " "$buffer[-1]
    commandline -f end-of-line
end

# Defined in /Users/ttscoff/.config/fish/functions/__re_extension.fish @ line 1
function __re_extension --description 'remove extension from word under/before cursor'
    commandline -f forward-word
    commandline -f backward-word
    set -l token (commandline -t)
    set token (echo "$token" | sed -E 's/\.[^.]*\.?$/./')
    commandline -t ""
    commandline -i $token
end

Then I just bind them in my init files:

bind \ez __re_extension
bind \e, __prev_token

So that’s where my exploration has taken me up to this point. I’m enjoying spending a bit of time every weekend learning new tricks and exploring, so there’s probably more coming in this series.