Red Hatter,
open sourcerer

Fish, bash, zsh

Fish, bash, zsh

Over the last year, I’ve been using fish as my shell on Linux. Before that, I have tried both zsh (with and without oh-my-zsh) and bash. For bash, I wrote my own configuration framework, which - let’s be real - everyone needs to do and probably has done at some point.

Last week, I decided to switch back from fish to bash. This blog is the story why I did that and what I’m using now. I might look into zsh again at some point, but not now.


Fish. I like fish. Fish shell is a great shell. It really is. I love the frameworks that exist for it, like omf, and I love some of the features is offers. I’ll talk about those first, because that’s what switching shells is generally all about, right? ;)

Fish features

The good

I’ll talk about abbreviations, named function arguments, variable scoping, and the help mechanism.

Let’s start with abbreviations. In bash, you have functions and aliases. Functions are functions, and aliases are a shortened version of a (series of) command(s). The fish concept of abbreviations is like an alias, but instead of you entering an alias and the shell invisibly executing the aliased command(s), an abbreviation shows you the command it expands to when you hit space or enter.

This means you can abbreviate long bits of code you need to use regularly, and always see the command you are executing on screen, which is great for understanding and from a security perspective.

Another great feature of fish is named function arguments. When you define a function in bash or sh, you use something like (I know the function keyword is optional / non-canonical, but I like using it for clarity):

function myfunction() {
  echo "the result: $1"

The above function might take or might not take any arguments. From the signature of the function, we cannot tell. Enter fish. In fish you would use:

function myfunction -a result
  echo "the result: $result"

As you can see, the way fish handles defining that function is much clearer to a reader of the code, as the argument to the function is now named $result, not $1.

You can also have fish add a description to a function with the -d flag, or trigger it when a variable changes with -v. Awesome features.

Anyway, on to variable scoping. Bash has variables that are local (which are local to their block), as indicated by the local keyword, and the rest. Any variable that is not local in Bash is therefore scoped to the current shell context (either your current interactive shell or a script). This is all ok, and we’ve been doing this for year, except that the local keyword is a bashism, I think, but whatever.

Fish has more scopes. In fish, a variable can be local, like in bash, it can be global, or they can be universal. A universal variable is shared between fish instances of the same user, which can be quite convenient.

While setting variables in fish, you can also tell the variable to be exported, treated as a path variable and much more. The downside of this all is the required usage of the set keyword for any variable operations. I’ll come back to why that is a nuisance later.

Finally, let’s talk about the fish help mechasim. In bash, when you want to learn about a built-in command, you call help for and bash will give you

$ help for
for: for NAME [in WORDS ... ] ; do COMMANDS; done
    Execute commands for each member in a list.

    The `for' loop executes a sequence of commands for each member in a
    list of items.  If `in WORDS ...;' is not present, then `in "$@"' is
    assumed.  For each element in WORDS, NAME is set to that element, and
    the COMMANDS are executed.

    Exit Status:
    Returns the status of the last command executed.

In fish, running help for will open a local copy of the fish docs on your browser, allowing for a much better reading experience, links to other commands and much more. Really nice.

But now for the bad.

The bad

Ok, so what’s bad. Bash / sh incompatibility is bad. When running demos or tutorials, what is generally provided is bash commands. Some will work, but especially the ones where variables are set, will not. Truly frustrating sometimes. Dropping into a bash shell works, but then the commands will be part of my bash history, not my fish history, so I’ll need to remember what history a certain command will be in. Annoying.

There are some plugins for omf (a fish config framework) that aim to mimic the bash experience around setting the $PATH variable, mainly, but you’ll end up with a weird config and having to work around fish when running a tutorial.

This is the (big) downside of the variable scoping bit I talked about above.

The $PATH variable on fish is both an awesome and an aweful thing. It’s awesome because fish allows you to prepend another variable to it, $fish_users_paths, in order to prevent having to override $PATH itself. You can add directories to $fish_users_paths with the fish_add_path command. Per directory, you’ll only ever run that once, since it manipulates a universal variable, and universal variables are saved between session.

I don’t think there is a fish_remove_path function though, so removing a directory from your path requires you to dig into ~/.config/fish_variables manually.

Another thing I do not like in the above examples, is that fish does not allow you to do ${result}. That is considered a syntax error. Over the years, I made a habit of wrapping all my variables in curly braces to be consistent through my code and prevent ambiguity with variable names. Fish would require me to do {$result} instead of ${result}, which looks weird. This is trivial though :)

As a final example of things I do not like: completions. Not really fish’s fauly. Fish has awesome ones, like bash and zsh do. Problem is, neither of the three can read the completion format of the others. Or at least fish cannot read bash or zsh completions, and there simply are less completions for fish. This means that completions for pretty important binaries, like, for example, oh, I don’t know, kubectl might not be supported for fish. This situation is improving, but it’s far from perfect.

My biggest problem is with following tutorials and howtos though. This was the ultimate reason I switched back

Config framework

Any decent Linux person has written some kind of bash / shell configuration framework at some point, amirite?

So did I, years ago. I just revived it from my personal gitea server and improved it a bit. I might open that repo up soon, just for fun.

I know that there are a ton of bash config frameworks, like oh-my-bash, out there, but I think my philosophy here is a bit different.


I called my framework bashrc.d a long time ago, and it’ll probably still be called that when I open it up.

My idea is as follows. It’s simple. I do not need a million plugins that provide ‘handy’ aliases that I do not know exist, that I do not understand, that are not applicable to me and might, in fact, be harmful.

No plugins with aliases.

If I add a plugin to bashrc.d, it is because the plugin provides me with some functionality that is otherwise missing. As an example, I have a plugin with a bunch of functions to set, view and unset a number of (free) 1GiB huge pages on Linux. (I use them for virtualization performance.) That’s functionality that’s hard to do in an alias and quite useful, potentially also for others.

Bashrc.d has a help system for plugins as well, so you can not only enable and disable them through a cli, but also view some help on what they do and what they provide.

No plugins without built-in help.

As another example, I have a plugin that helps with the generation of SSL certificates and keys and viewing their properties, complete with docs. I think that is useful.

I will however not add any huge lists of aliases for all kinds of things. Aliases are mainly a personal preference if you ask me, unless we are talking about really commonly used ones, like kc for kubectl. Maybe. Huge lists of aliases in different plugins are also bound to start conflicting at some point.

This is why I stopped using all the other frameworks to begin with. I don’t need someone else’s aliases. They’ll never become real muscle memory anyway.

Aliases go into local_settings.sh in bashrc.d. Aliases are yours. Not mine.

I like shell configuration frameworks that offer functions to do useful things. Cosmetic plugins, easy to install completions for tools.

And I suffer from NIH really, really bad :D