silly business

Compounding Returns With Fish Shell

A few weeks ago I was fiddling around with my neglected Zsh setup, wherein some years ago I had installed a theme through the Zim framework – one of many open source frameworks that allows you to quickly "supercharge" a shell like Zsh which has a lot of latent power if you know how to use it, for mere mortals like me who just want better ergonomics and don't want to learn all the intricate details of Zsh in order to have a meaningful PS1 – and I decided for one reason or another to update the framework to the latest version.

Oops! The theme I've been using for years has been modified! In an unacceptable (to me) way!

Now, I'm faced with some shitty options. I can:

  1. Just use the theme how it is now. I angrily did this for a few days but it moved the little → from the end of the prompt to the beginning, and now the current directory was the last thing before my cursor, and I kept confusing it with current context. I couldn't take it.
  2. Modify the theme I started to look into how the theme was implemented and into getting a better understanding of Zsh. I'd used it for years but really only use it as a slightly more fancy Bash prompt.. I don't know what makes the Zsh language different or anything.. and this seems like a big lift. And then, I have to maintain it. I didn't really understand Zim's structure, and I felt like investing in it further was risky.
  3. Go back to Bash Thought about this. A lot. But man, Bash sucks to actually use, and I say this as someone who actually kind of finds joy in writing shell scripts. But it's finicky, and so is Zsh, and I've read about alternative shells over the years and I'm feeling awful tempted
  4. Try an alternative shell Give up on the classics and see if one of these Hacker News darlings is any good.

Trying out Fish Shell and Realizing What I've Been Missing Out On

Spoiler: this is where I landed. I did a bit of research. Talked to some friends. Read about oils and vanilla zsh and kept encountering people talking about fish shell. Coworkers who use it, and so on.

I tried fish out ten years ago, and I set it as my login shell. Apparently you aren't supposed to do that, because fish isn't POSIX compliant. This created weird problems in other programs, and while I found fish to be fun to use, the added problems weren't worth the trouble.

This time, with an extra ten years of experience under my belt, I did what a wizened senior engineer does: I read the fucking manual. And hey, the docs say not to set fish as your login shell, and after reading the tutorial and the guide for bash users, I was off to the races.

I installed fish and set it as the default in my terminal emulator, since I wasn't supposed to have it as my login shell. I picked out a theme interactively using the fucking web UI because it was easiest, and had an acceptable prompt that was nearly identical to my long-lost Zsh prompt in a few minutes with little hassle. ctrl-r behaved the same way as it did in Zsh with fzf installed, so I didn't bother to find a replacement for that, and I purloined acceptable replacements1 for !! and !? from StackOverflow, as I use them frequently.

Then, while setting up my prompt – which is just defined by a normal Fish function in a script stored alongside the rest of them in ~/.config/fish/functions/ – I learned about funced and funcsave and a lightbulb went off.

An aside about your PATH

Bash, Zsh, and Fish all use a variable called PATH to find executables that become the commands that make up the corpus of available interactive commands while using the shell. A good idea for every programmer is to put a directory of executables that are owned by your user on your PATH, wherever the shell considers it to have high precedence.

I have used just ~/bin for a long time – many people put theirs in a hidden directory like ~/.bin or, for instance, the XDG standard puts it in ~/.config/bin, and those are all fine options – but I like to have mine visible and easy to remember because I use it actively like any other normal directory.

And of course, aliases are one of the first things that you learn about when learning the Unix shell but they have limitations, and so I had already gotten into the habit of writing functions and adding them to a file that was sourced by my rc file, or actually writing an executable (either a Bash script or Go program) to put in ~/bin depending on what I was doing.

This works pretty well, and I still advise putting a personal binary directory somewhere on your PATH to store your specialized tools, but with this workflow there's always the friction of navigating to ~/bin and opening a file, and then defining the function, saving the file as something meaningful, and either marking it executable or running source if it's just a function, or rehash in Zsh to re-detect binaries added to PATH.

Whatever the pattern, it's never as simple as just "save the file and now I can use it." I had thought of automating this workflow in Zsh for myself before; it certainly is possible. But Fish just gives you a convention to do this. And it does it completely by default, and its implementation is brilliant, and saves me the trouble!

Fish lets you easily save the things you organically discover to be reusable

Fish introduces two functions, funced and funcsave, and preserves the EDITOR pattern from Bash/Zsh, where certain tools look to the EDITOR variable when they want to allow the user to edit text in their preferred text editor. For instance, I keep EDITOR set2 so that I get a terminal emacsclient connected to my current Emacs session or vim if Emacs isn't running.

funced opens a custom function, automatically stored in ~/.config/fish/functions as function_name.fish, for editing in EDITOR. funcsave saves a named function from the current session to function_name.fish in the aforementioned directory.

This simple but powerful convention, combined with the command to edit the current input in my editor, allows you to follow this brilliant workflow:

  1. Figure out how to do whatever it is you were doing, interactively, as normal
  2. Realize you've written a bit of shell that could be useful in the future.

    This is where in Bash or Zsh I would think "ah, but there'll be some sharp edges on the script inevitably, and I don't want to change context to my editor, make a new file in ~/bin, save it, mark it executable, and then finally be able to use it.. even if all I'm really doing is copy-pasting in whatever I just did. Instead:

  3. Press the up arrow to pull the command back up (or ctrl-p if you're cool)
  4. Press alt-e to edit the line in your editor, and add a function definition to the command.

So for instance, if I was looking for a certain type of Kubernetes pod, and I decide after crafting a command like

kubectl get pods -n my-service -l some_label=foo | tail -n1 | cut -d ' ' -f1

I press alt-e and Fish drops me in the editor with this command present, and I quickly add the function declaration so it becomes

  function get-my-pod-with-label-foo
    kubectl get pods -n my-service -l some_label=foo | tail -n1 | cut -d ' ' -f1
  end

and as soon as I save and exit, it's ready to use in every Fish session. No calling source or chmod +x or even calling whatever automation I could've written to do those things in Zsh.

Then if there's something that needs tweaking, I could just do funced get-my-pod-with-label-foo (with tab-completion of course), and Fish opens the file in my editor, save and use. Iterate and refine the solution, and then funcsave to keep the function permanently once it's perfect.

This interactivity means that I find myseslf funcsave-ing stuff that I wouldn't've bothered to save back before I made the jump to Fish, and of course this kind of casual abstraction creation is an investment with compounding returns. It's fucking awesome!

Mo' Custom Functionality, Mo' Problems

Now the only issue with the ease with which Fish lets me create functions, is that now I have a lot of .fish files with a myriad of purposes kicking around in ~/.config/fish/functions, and some of them are only relevant to one machine, or set of machines.

I have created a git repository there, and the most obvious approach for managing these files and distributing them to their destinations is git branches, but that will have to be something I will have to put some thought into. It seems to me to be a good problem to have.

In the end, I wound up editing my prompt after all

Even though I had recoiled from the task of editing my complex third-party PS1 in Zsh, and had promised myself that I would just choose a default Fish prompt, Fish makes it so easy to edit the prompt by just calling funced fish_prompt that I couldn't resist the temptation after all, and was delighted at how easy it was to add a hostname in a custom color when I was connected over SSH, to help me differentiate when I have many consoles open

  # hostname for remote connections
  if test -n "$SSH_TTY"
      set_color yellow
      printf '%s ' (hostname)
      set_color normal
  end

This snippet might look alien to a Bash user but it's super readable, and as it turns out, it's mostly a copy-paste-edit job from the surrounding prompt theme that I had chosen interactively on day 1. It was so easy to add that I got inspired and decided to add the air temperature to my prompt, too.

I happen to have a daemon writing local temperature data to a file on most of my systems, so all Fish has to do is check to see if the file is there (so it doesn't break on my systems that don't have this feature) and make it look nice.

  # outside temp for home computers running weatherboy
  if test -e /tmp/weatherboy
      printf '%d° ' (math round (jq .AirTemperature /tmp/weatherboy))
  end

Nice:

In_the_end,_I_wound_up_editing_my_prompt_after_all/2024-11-16_19-35-39_Screenshot_20241116_191655.png

All this only took a few minutes. I guess if it breaks down the line like my Zsh prompt did, I won't be too sad, since it didn't take long to set up.

I like that Fish lets me make a lot of small investments that will compound over time, just like Emacs does, paying off over and over in exponentially greater and greater sums. Each little change is small risk, but over time I can amass a growing corpus of capabilities tailored to my specific tasks, roles, and preferences.

It's gonna be awesome 😎

Footnotes


1

In config.fish, to replace !! and !$. If I can find the original thread again where I found this snippet, I'll link it here to give credit where it's due.

  function bind_bang
      switch (commandline -t)[-1]
          case "!"
              commandline -t -- $history[1]
              commandline -f repaint
          case "*"
              commandline -i !
      end
  end

  function bind_dollar
      switch (commandline -t)[-1]
          case "!"
              commandline -f backward-delete-char history-token-search-backward
          case "*"
              commandline -i '$'
      end
  end

  function fish_user_key_bindings
      bind ! bind_bang
      bind '$' bind_dollar
  end
2

I used this in config.fish to launch emacsclient with a workflow familiar to vim and nano cli users.

  if status is-interactive
      # Commands to run in interactive sessions can go here
      set -gx VISUAL 'emacsclient -t -a/usr/bin/vim'
  end