Why I prefer fish over bash for scripting

Scripting with fish is simple. It is readable. You don’t need to have deep experience with it in order to understand the syntax.

Compared to bash, it feels intuitive.

Everything is a builtin

builtins are commands that you call from the shell, but unlike regular commands that execute external binaries and spawn new processes, they are built right into the shell (e.g. echo, math, set, count).

The core syntax of fish is very simple, and most all of the operations are done via calling builtins.

Want to set a variable? Use the set builtin. Want to declare a function? Use the function builtin. Want perform some sort of calculation? Use the math builtin. Want to find out what some other builtin does? Use the help builtin.

In fact, prior to fish 3.0, even conditional command execution was done using and and or builtins (unconditional command chaining is still done using the seimicolon ;):

1$ git commit; and git push

(it still is, though support for && and || syntax was added).

String replacement

In bash, string replacement is done using the ${$text//pattern/replacement} syntax. For example, if you wanted to replace every instance of 'foo' with 'bar' in a certain text, you could do it like so:


In fish, however, this is done using the string builtin:

1set replaced_text (string replace -a foo bar $text)

If it wasn’t for the variable name, newcomers would have absolutely no idea what the fuck was going on in the bash version, whereas the fish versions almost completely “spells out” the entire operation.

Array item count

In fish, getting the number of items in an array is done using the count builtin:

1set N (count $array)

In bash, the syntax is, to no surprise, more obscure:



In fish, mathematical operations are peformed using the math builtin, and unlike in bash, they support decimal numbers as well as functions:

1$ echo '3 + sqrt(4) = ' (math 3 + 'sqrt(4)')
33 + sqrt(4) = 5

Since bash doesn’t support decimal operations nor mathematical functions, you’d have to call external processes to do the work for you.

For someone who isn’t familiar with shell scripting, the fish versions of these example operations are going to make much more sense at first (and probably second) glance.

This philosophy makes scripts insanely readable.

Argument substitution is more intuitive

This was one of my first serious gripes back when I was first learning bash.

Let’s assume that barg is a readily available program that simply prints its commandline arguments (as well as the number of them), to standard out, one per line:

1$ barg 1 2 3
3User arguments passed: 3

Parameter substition in fish makes sense.

Expanding a single variable results in a single argument on the commandline. For example:

1$ set arg "Hello there, you!"
2$ barg $arg
4User arguments passed: 1
6Hello there, you!

It is only if you pass in an array of multiple values, that the resulting commandline ends up with multiple values, for example:

1$ set args "Hello there, you!" "--some-long-option"
2$ barg $args
4User arguments passed: 2
6Hello there, you!

Again, it is intuitive and it makes sense.

Now let’s take a look at the same examples, but in bash:

1$ arg="Hello there, you!"
2$ barg $arg
4User arguments passed: 3

The source operand was split on spaces, and a single variable substitution resulted in 3 different commandline arguments.

To avoid this, you have to encapsulate substituion in double quotes (" "):

1$ arg="Hello there, you!"
2$ barg "$args"
4User arguments passed: 1
6Hello there, you!

To me this makes no sense, however from a historical point of view I can see why it is: in fish, declaring arrays of multiple values each of which expands to a signle value is easy, unlike in bash, where, if you’d written something like arguments="-a -D --long-opt", you would probably in fact want it to expand to 3 different arguments.


These are just some of the examples. The true strength of fish lies in its power as an interactive shell - robust automatic completion, autosuggestions, configurability, colors, etc…