At some point last year I fell in love with the fish
shell. It features almost zsh-like power, very nice editing and searching, for example autocompletion like in a browser. I’ve been using it as my primary command line ever since.
I’ve also switched to using fish
for scripting, rather than bash. This has a great advantage, and also a disadvantage: The bad thing is that its syntax is not sh
-compatible. The good thing is that its syntax is not sh
-compatible.
sh
syntax is of course time-tested and much beloved by many users and admins. But if we’re really honest, it’s a bit of a mess. fish
improves on this a lot. Have a look at its tutorial to see things like in-terminal syntax highlighting, sane arrays, sane functions and plenty more.
However, with sh
being a standard and bash
being nearly everywhere, there is a lot of stuff out there that blindly assumes it can just dump sh
-style commands into e.g. an ssh
connection and have things work on the other end. The culprit that annoys me the most is ssh-copy-id
— when I changed my login shell to fish
on a server, it broke down, because fish
didn’t understand what that script wanted.
There’s an easy fix for that bug in the script: It could simply make ssh
launch an sh
-compatible shell before pouring in the commands. This fix is in the works in upstream, but at least in Ubuntu 15.04 it’s not available yet.
That said, I wanted a general fix anyway, because there will certainly be other scripts that try to do this. fish
will never be fully sh
-compatible, and I don’t mind that. I like fish
-style scripting. So I want to make the server smart enough to figure out what is going on. It turns out that that is pretty simple.
This is what I want to achieve:
ssh
I want to end up in a nice interactive fish
.fish
should act as a login shell so things like the automatic byobu
launcher in Ubuntu still
work.fish
, the connection should terminate instead of dropping me into an outer shell.ssh-copy-id
connects, fish
should not be started. Instead, something sh
-capable should run, for example bash
.bash
interactively, by running bash
from fish
.Here’s how I did it:
I changed my login shell back to bash
:
$ chsh -s /bin/bash
I added the following at the end of ~/.bash_login
:
case "$-" in
*i*) fish -il; exit ;;
*) ;;
esac
What does this little monstrosity do?
Since this is in ~/.bash_login
, we know we are a login shell. “Manually” launched bash
instances will not run this script and so will behave normally.
bash
stores some of its runtime flags in $-
. If it’s an interactive shell, that string will contain i
. Given that here we are in a login shell and combining that with the i
flag
being present, we know there is a human who just logged in and who has a keyboard to do interactive
things. Let’s give him a fish
.
The -i
argument makes sure fish
will be interactive too, and -l
tells it to behave like a
login shell.
Finally, once fish
exits, we call exit
so bash
will exit too, so the user will not even
notice that there is a bash
involved in all this.
In the other case, when bash
is not interactive, we now know that there is a script connecting to
our server. We do nothing and let bash
start up normally, so even bad scripts like ssh-copy-id
will find their familiar environment and just work.