« Back to home

Go Fish

Posted on

I’ve been trying Fish, the Friendly Interactive Shell. I was driven to do so by two Zsh bugs.

The first is that when I SSHed from a Linux machine to a Mac, something was getting messed up or not initiated correctly in my terminal session. There were control codes which messed up the display, and no matter what I set the prompt environment variables or TERM to, they remained. The problem didn’t occur in the opposite direction.

The more recent issue was that Zsh’s automatic “here’s what I think you might have meant” code broke, and commands which were unrecognized were completely ignored with no error message. This was enough of a potential issue to make me look again at other shells.

Over the years I’ve gone from csh to tcsh to bash to zsh, so the prospect of another change of shell wasn’t that daunting. (For the curious, I started with csh because it was the best shell available on Xenix in the mid 1980s.)

I don’t write code in shell script unless I absolutely have to, so scripting strength is relatively unimportant to me. All I really care about are ease of use and ease of configuration. Based on those criteria, my early impressions of fish are positive.

  1. It sets up my terminal correctly. SSH from Linux to Mac works fine now that it’s my login shell.

  2. Terminal emulator tabs in Linux actually show what’s running in them without my having to do any special configuration.

  3. The vi command line editing works well, again without any messing with configuration.

  4. The prompt is sensible by default – it shows vi mode, user name, hostname, and abbreviated pwd by default. It’s the first shell where I haven’t felt the need to mess with the prompt.

  5. Config is handled cleanly. Unlike the mess of startup scripts used by bash, which installers often try to patch for you, there’s a single config.fish for user preferences and a conf.d directory for package-provided scripts.

  6. Autocomplete works all over the place, thanks to fish parsing man pages to work out the available options. It also autosuggests completions in gray without my having to hit tab, so I end up using the feature more.

  7. Variables are all lists, making it easier to script operations on multiple files.

  8. There’s a really handy ‘universal’ global variable type which applies to all fish sessions, including ones already running.

Lacking a library of zsh or bash scripts, my first task was to convert my .zshenv file to fish syntax. The biggest part of that was rewriting the chunk of script which works out my PATH. For the sake of cross-platform compatibility and consistency of behavior, I make my shell run through a list of possible path directories found on all the Unix machines I use, in the order I would want them, and construct a path from the ones which exist. It didn’t take long to rewrite, though I admit I found myself wondering if the syntax changes between bash/zsh and fish were really worth the pain caused.

Then I tackled .zshrc, and the problem which inspired this article: ssh-agent. If SSH password prompting has always worked for you, well, consider yourself lucky, because I now know what a ghastly pile of hacks it is behind the scenes.

For starters, GNOME’s graphical SSH agent is about as defective and lacking in basic functionality as I’ve come to expect from GNOME, in that it doesn’t understand elliptic curve keys or bcrypt-protected keys. For that reason I’d been using ssh-agent, but it had never really remembered that I’d unlocked my SSH keys already, and I’d never bothered to dig in and find out why. I’d just been typing my key passphrase every time, or accepting that ssh-add would only apply to the current shell.

Here’s the problem: When you run ssh-agent it sets up a daemon with a socket for communication. It then outputs the location of the socket and the pid of the daemon. The shell is supposed to pick up those values, and then when you spawn a new shell that new shell can continue to use the existing daemon.

The problem is how the new shell is supposed to do so. The agent spits out the information in the form of a piece of shell script which the shell is supposed to execute, which you can see by using the -D option to prevent the agent from daemonizing:

% ssh-agent -D
SSH_AUTH_SOCK=/tmp/ssh-Xxp2vbEK9x4k/agent.16030; export SSH_AUTH_SOCK;
echo Agent pid 16030;
^C

There’s also an option to kill the current agent daemon and clean up the socket.

Clearly there’s an issue around the fact that ssh-agent outputs shell script. It knows how to output sh and csh, and that’s it – no fish, no rc, no emacs… But that’s the most superficial issue.

The deeper problem is that the operation each shell actually wants to perform is: “If the information in the environment points at a live daemon and a socket file, use them. Otherwise, kill any daemon with the pid that’s in the environment, start a new daemon with a new pid and socket, and use those.”

If I were writing ssh-agent I’d make that the default behavior, or at the very least a behavior available using command line options. No such luck with ssh-agent though. You can either kill an existing daemon, or you can start a new one. You can’t do both, presumably because they’re logically different operations. You also can’t ask the agent to check which is necessary.

No problem, you think, and you write a script which checks the socket and PID and calls ssh-agent -k to clean up if either of them are missing. And then you discover that if the environment points at a PID which isn’t a running daemon – say, if it crashed – the agent throws an error.

My summary would be that ssh-agent is pedantically correct rather than useful, with functionality implemented without regard to how anyone would want to use it in real life.

As evidence for this, my .zshrc (customized from a default provided by some Linux distro) simply called ssh-agent and hoped for the best, and I’m not the only one to have done so. There are long-winded attempts to work around the SSH agent’s behavior, but I’d never installed any of them. I’d just lived with typing my passphrase a lot. I imagine there are lots of Linux users who don’t even know that it can be any other way.

There are various solutions out there to make the SSH agent work with fish, of course. They mostly seem to be distributed as plugins and are fairly long and involve using grep, sed, and even /bin/sh. I felt like I could do better.

After quite a lot of experimentation, I came up with a short self-contained fish script for ssh-agent. It uses ps to test for a running agent, and kill to kill one, but everything else is done inside fish. It will either display “Found ssh-agent” or “Started ssh-agent” depending on which operation it had to perform, so you can check its behavior when you open new tabs or start subshells. (Oh, and on macOS you don’t need it at all.)

Once I’d gotten ssh-agent working, it turned out that none of the rest of my .zshrc was necessary in fish. It was 200+ lines of setup to make things work which simply worked by default in the new shell. So, if you don’t use your interactive shell as your main scripting language, give fish a try.