1. Introduction

Linux offers many choices for a shell. Of course, there’s always the ubiquitous Bash. Still, we also have the ancient rudimentary sh, feature-rich zsh, fairly rare ksh, and the basic dash and [t]csh shells. Yet, few of them are as friendly and helpful as a relatively new addition.

In this tutorial, we talk about the fish shell. First, we briefly overview its history, purpose, and features. After that, we go over the syntax and differences with other major shells. Next, we explore functions, colors, and subshell handling. Then, we turn to a fundamental feature of Fish. Finally, we see how this feature applies to an important mechanic in Linux.

For clarity, we use $ as the Bash prompt and > as the Fish prompt in code snippets.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. It should work in most POSIX-compliant environments unless otherwise specified.

2. Fish Shell

The first version of the Fish shell came out in 2005. This makes it one of the newer entries in the Linux shell world.

There are a few main advantages that fish

  • automatic suggestions based on history and directory
  • open-source project built mainly in Rust
  • command-specific completions
  • optimized syntax
  • searchable command history

Of course, Fish also has drawbacks, one of which is its limited adoption. Further, it lacks full POSIX compliance.

3. Syntax

In many ways, fish is similar to a POSIX shell like sh. For example, it has a standard echo built-in.

However, there are important differences.

3.1. Variable Assignment and Expansion

In shells like sh or bash, we use equals for assignments:

$ var=value
$ echo $var
value

Also, we prefix a variable name with a $ dollar sign to perform variable expansion. The latter works the same in Fish.

Unlike many other shells, Fish doesn’t use the equals sign for assignments:

> set var value
> echo $var
value

Critically, the idea behind this is to avoid the significance of whitespace after the equals sign.

3.2. Command Substitution

Importantly, although command substitution is part of its feature set, Fish only uses parentheses for that:

> set var (echo value)
> echo $var
value

Since version 3.4, fish also supports the Bash $() syntax, but not backticks.

3.3. String Manipulation

String manipulation in Fish works through the special string command:

  • length: return string length
  • escape, unescape: escape or remove escaping from string
  • joinjoin0: compile a single string with the passed (array) components separated by a given delimiter or NULL
  • splitsplit0: break string into an array based on a delimiter
  • lower, upper: change case of string

There are many other subcommands as well.

Let’s see an example:

> string match --regex '(?<=t).*?x' 'texxxt'
ex

In this case, we match the supplied PCRE regular expression (–regex) to the string represented by the last argument.

3.4. Arrays and Indexing

Indices of Fish arrays start at 1 instead of 0.

Array declarations also work differently:

> set arr 1 2 3 4 5
> echo $arr[3]
3

Indexing is very flexible and it provides slicing and erasing:

> echo $arr[1..3]
1 2 3
> echo $arr[$arr[1]]
1

Notably, we also have a way to –erase:

> echo $arr
1 2 3 4 5
> set --erase $arr[1]
> echo $arr
2 3 4 5

Of course, a scripting language isn’t very useful without decisions.

3.5. Conditionals

Fish still uses the test builtin, albeit with its own syntax, for conditional expressions:

> if test 1 = 1
    echo equality
  end

However, it doesn’t support the POSIX [] and Bash-specific [[]] syntax.

3.6. Loops

When it comes to loops, Fish has both for and while:

> for i in (seq 1 3)
    echo $i
  end
1
2
3
> while test $i != 1
    echo $i
    set i 1
  end
3

Instead of dodone, fish starts each loop with its control expression and only uses end to conclude its body.

4. Functions and Colors

Naturally, function definitions exist in Fish:

> function f1
    echo [fun]ction (math "$argv[1] + 1")
  end
> f1 666
[fun]ction 667

Function definitions start with function and also conclude with end. Yet, we can also add a –description to aid with the forte of Fish: autocompletion.

Here, we also use the special math built-in to perform basic calculations.

Fish also makes color handling fairly straightforward:

> function _fish_logo
    echo '                 '(set_color F00)'___
  ___======____='(set_color FF7F00)'-'(set_color FF0)'-'(set_color FF7F00)'-='(set_color F00)')
/T            \_'(set_color FF0)'--='(set_color FF7F00)'=='(set_color F00)')
[ \ '(set_color FF7F00)'('(set_color FF0)'0'(set_color FF7F00)')   '(set_color F00)'\~    \_'(set_color FF0)'-='(set_color FF7F00)'='(set_color F00)')
 \      / )J'(set_color FF7F00)'~~    \\'(set_color FF0)'-='(set_color F00)')
  \\\\___/  )JJ'(set_color FF7F00)'~'(set_color FF0)'~~   '(set_color F00)'\)
   \_____/JJJ'(set_color FF7F00)'~~'(set_color FF0)'~~    '(set_color F00)'\\
   '(set_color FF7F00)'/ '(set_color FF0)'\  '(set_color FF0)', \\'(set_color F00)'J'(set_color FF7F00)'~~~'(set_color FF0)'~~     '(set_color FF7F00)'\\
  (-'(set_color FF0)'\)'(set_color F00)'\='(set_color FF7F00)'|'(set_color FF0)'\\\\\\'(set_color FF7F00)'~~'(set_color FF0)'~~       '(set_color FF7F00)'L_'(set_color FF0)'_
  '(set_color FF7F00)'('(set_color F00)'\\'(set_color FF7F00)'\\)  ('(set_color FF0)'\\'(set_color FF7F00)'\\\)'(set_color F00)'_           '(set_color FF0)'\=='(set_color FF7F00)'__
   '(set_color F00)'\V    '(set_color FF7F00)'\\\\'(set_color F00)'\) =='(set_color FF7F00)'=_____   '(set_color FF0)'\\\\\\\\'(set_color FF7F00)'\\\\
          '(set_color F00)'\V)     \_) '(set_color FF7F00)'\\\\'(set_color FF0)'\\\\JJ\\'(set_color FF7F00)'J\)
                      '(set_color F00)'/'(set_color FF7F00)'J'(set_color FF0)'\\'(set_color FF7F00)'J'(set_color F00)'T\\'(set_color FF7F00)'JJJ'(set_color F00)'J)
                      (J'(set_color FF7F00)'JJ'(set_color F00)'| \UUU)
                       (UU)'(set_color normal)
end

Here, we define the _fish_logo function:
                 ___
  *___======____=-=)*
**/T            \_*–===)*
*[ \* (0)   *\~    \_*-==)*
 \      / )J
~~    \\*-=
)**
  \\\\___/  )JJ
~~~   \)
   \_____/JJJ
******    *\\*
   / *\*  **, \\J~******     *\\*
  **(-*\)\=|\\\\\\*~~*~~*       L_****_
  **(****\\*\\)  (\\*\\\)_           \==__
   \V    **\\\\****\) ==**=_____   *\\\\\\\\**\\\\*
          \V)     \_) *\\\\**\\\\JJ\\*J\)
                      **/J\\JT\\JJJJ)

                      (J*
JJ
**
| \UUU)
*
                       **(UU)**It just shows the Fish logo in color.

5. Subshells

In sh, Bash, and many other shells, there are certain situations in which a subshell is automatically created:

Yet, this is a double-edged blade as far as usability is concerned.

On the one hand, we can isolate the subshell environment from the main one, thereby safely changing special variables like $IFS without affecting our main script or context.

On the other hand, subshells can lead to resource overhead and the hidden isolation can be confusing when certain variables don’t have the correct (or any) values.

So, Fish developers have opted to omit implicit subshells. In short, the only way to create a subshell with fish is by explicitly calling the process again.

To compensate for this, Fish provides another unique feature that resolves the isolation dilemma.

6. Universal Variables

In Bash and other shells, variables are usually separate for each session. Even exported environment variables have to be initialized per session and can deviate relative to their initial value.

Uniquely, Fish crosses that boundary and enables the creation of so-called universal variables, which are shared among all fish sessions of the same user. In fact, such variables preserve their last value after logouts and even a reboot. Further, any value changes immediately take effect in all sessions.

Let’s see a basic example:

> set --universal fish_color_user white

This command sets the –universal variable fish_color_user to white. This change propagates to all open fish processes immediately. The result is that the next prompt of any Fish session of the same user shows their name in white.

Thus, we can modify any setting that depends on an environment variable, thereby eliminating the need to modify RC files.

7. $PATH Variable Handling

Considering the implications of universal variables, using the $PATH variable can be much more convenient in Fish.

However, fish considers $PATH a global and not a universal variable. This means that it’s set via inheritance, in the configuration files, or via a hard-coded algorithm. On the other hand, $fish_user_paths contains the Fish-specific variables.

Manually, we can use the builtin fish_add_path command:

> fish_add_path /custom/bin/
> echo $PATH
/custom/bin [...]

By default, $PATH is in sync with $fish_user_paths, the Fish-specific paths that we add or remove. In particular, $fish_user_paths is part of $PATH. This way, we have compatibility with the value of the parent process that calls fish.

Conveniently, fish_add_path provides ways to -append (-a), –prepend (-p), and –move (-m) components:

> fish_add_path --prepend /first/bin
> echo $PATH
/first/bin /custom/bin [...]
> fish_add_path --append /last/bin
> fish_add_path --append /two/bin
> echo $PATH
/first/bin /custom/bin [...] /last/bin /two/bin
> fish_add_path --append --move /last/bin
> echo $PATH
/first/bin /custom/bin [...] /two/bin /last/bin

Further, Fish can treat $fish_user_paths as both global or universal, as long as it’s not already created. Finally, we can modify $PATH directly via the –path (-P) switch

8. Summary

In this article, we discussed the Fish shell, its characteristics, and how it differs from many major shells.

In conclusion, apart from the syntax changes, one of the most fundamental features in fish is universal variables, which also affects how we handle the critical $PATH variable.