The fish Shell

by Mark Volkmann
mark@objectcomputing.com
Object Computing, Inc.
November 2017

Come for the colors and autosuggestions, stay for the sane scripting!

Table of Contents

Overview

Nearly all developers spend time using terminal windows, entering commands, such as git, and automating tasks by writing and running scripts. How would you like to use a shell that is more friendly and provides a simpler, more consistent syntax for scripts?
Fish is a *nix shell that offers an alternative to shells like bash and zsh. At a high level, fish provides the following benefits:
This article explains the fish shell in enough detail for you to determine whether you might prefer it over other shells. Really learning a shell is similar to learning a new programming language. Similar topics are covered here such as variables, functions, string operations, and so on. If writing scripts in other shells has felt tedious in the past, this is your chance to learn a shell that makes scripting easier!
The latest version of fish at the time of this article is 2.6.0. Version 2.7.0 will be released very soon.

License

The fish shell uses the GNU General Public License, version 2. Here's a summary of that license:
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build and install instructions.

Goals

The goals for fish are listed at https://fishshell.com/docs/current/design.html . Here are the most significant goals:

History

The first release of fish was on February 13, 2005. Axel Liljencrantz was the main developer and maintainer of versions 1.0 to 1.23.1 which were in SourceForge. The last 1.x release was in March 2009. Various people maintained forks at Gitorious and elsewhere but it seems no releases were ever made. A person that goes by "ridiculousfish" got involved in late 2011 and released a beta of a fork called "fishfish" in 2012. This incorporated much of the work of previous fish contributors and eventually became fish 2.0 which was released on September 1, 2013.
fish was originally implemented in C but is now primarily C++.
fish 3.0 is targeted to be released in Q1 2018. For a list of fixes/features targeted for this release, see https://github.com/fish-shell/fish-shell/milestone/18. For a list of additional fixes/features that *may* be in this release, see https://github.com/fish-shell/fish-shell/milestone/7.

Sections

The remainder of this article is split into four sections. The first section describes using fish. Some readers may wish to stop there and just reap the benefits of fish in its default configuration. The second section describes customizing fish. This targets readers who want to go beyond the defaults. The third section describes scripting in fish. Readers who wish to automate repetitive tasks will benefit from learning this material. The fourth section provides additional information that doesn't fit into the previous sections.

Using Fish

Installing

Instructions for installing fish are at https://fishshell.com/. Search that page for "Go fish".
For macOS, there is an installer. If Homebrew has been installed, fish can also be installed by entering brew install fish.
For Windows, fish can be run in Cygwin. In Windows 10, fish can be run in the "Windows Subsystem for Linux" (WSL).
I highly recommend installing fish now so you can try each feature as it is explained here. It's easy enough to uninstall later if desired.

Default Shell

To make fish the default shell so new terminal windows automatically use it, enter chsh -s /usr/local/bin/fish. This assumes that the path to fish is included in the file /etc/shells which is the case when standard installers are used. Perhaps it's best to hold off on doing this until you become convinced that fish is for you. I'm confident you will!
Once fish is the default shell, if the need arises to execute a bash command use bash -c 'someBashCommand'. An example of when this might be useful is when a complicated bash command is being copied from a website. This avoids the need to translate the command to fish syntax.

Uninstalling

To uninstall the fish shell, change the default shell to something else (like bash), and then run the following commands, adjusting the paths if fish was installed somewhere other than /usr/local:

Starting

Once fish has been installed, open a terminal. If fish is not the default shell, enter fish to start a fish shell. To exit a fish shell, enter exit.

Help

There are many sources for help on fish. The main web site at https://fishshell.com/ provides excellent documentation. Entering just help in a terminal opens the local copy of that documentation in the default web browser for the version of fish that is installed.
Entering man command-name displays help in the terminal. For many commands, entering command-name -h does the same.
Entering help command-name displays the same help in the default web browser, but only for fish commands and with better formatting.
Some commands, such as expr, are considered to be "system commands" and not fish commands. Use man rather than help to get help on those.
Other great sources of help include:

Autosuggestions

Autosuggestions suggest remaining text for a command while the command is being typed, without requiring another key like tab to be pressed. Typically this appears as gray text, sometimes referred to as ghosting, but the color can be customized. It derives suggestions from command history, possible commands, possible switches, variables, and file paths.
If the suggestion is not desired, continue typing. To accept a suggestion, press the right arrow. To execute the command, press enter.
Possible commands and switches are derived from man pages that fish has parsed. If new man pages are installed, enter fish_update_completions to parse them.
Autosuggestions from command history provide more productivity gains than you might guess. This is a killer feature!

Tab Completions

Autosuggestions only provide a single suggestion. To see more options, press tab. This presents a list of options that can include command names, switches, variable names, file paths, git branch names (including remote branches), process names/ids, job names/ids, man page names, ssh hosts, users, and more. To see the available switches for a command including their descriptions, enter the command followed by a space and a dash, then press tab.
The options presented depend on the command that has been entered. For example, if the su command has been entered, pressing tab displays all known usernames.
If there is only one option, it will be expanded in place. If there is more than one option, up to four rows will be listed. If there are more than four rows of options, they will be followed by "...and n more rows". To see the remaining options, press tab again. To select an option, press tab repeatedly until the desired option is highlighted. Highlighting cycles back to first option if tab is pressed while the last option is highlighted. Alternatively, use the arrow keys to move to an option. When the desired option is highlighted, press enter to accept it. Press enter again to execute the command.
To filter the list of options, press tab until some option is highlighted, then type a substring that must be contained in the options. A search entry field starting with "search:" will be displayed.
To skip selecting an option, press the escape key.

Examples

To list files in the current directory whose names contain "d", enter ls d and press tab. This also works when only ls followed by a space is entered before pressing tab. In this case the options are all files in the current directory. Enter "ls -" and press tab to see all the available switches.
To output the value of a variable whose name contains "re", enter echo $re and press tab repeatedly until the desired variable is highlighted. Then press enter once to select it and again to execute the echo command.
To kill a process whose name contains "lo", enter kill lo and press tab repeatedly until the desired process is highlighted. Then press enter once to select it and again to execute the kill command.
To get help on a command that contains "lo", enter man lo and press tab repeatedly until the desired command is highlighted. Then press enter once to select the command and again to execute the man command.
To run an npm command, enter npm followed by a space and press tab twice. All npm commands will be displayed. Use the arrow keys to navigate to the desired command and press enter to select it. Optionally continue typing to specify arguments. Press enter to execute the command.

Changing Working Directory

Like in other shells, the current working directory can be changed by using the cd command. However, since the initial character of directory paths (., /, and ~) is not a valid character in any command, fish interprets just entering a directory path as a request to change the working directory. For example, entering .. cds to the parent directory and entering ~ cds to your home directory.
Tab completion can be used to assist in entering each part of a directory path. This even works without entering anything, so it's possible to navigate to any child directory by just pressing tab repeatedly until the desired directory name is highlighted and then pressing enter once to select it and again to cd to it.
Like in other shells, to print the current working directory enter pwd.
The dirh command prints a list of the last 25 directories visited. The current directory will be highlighted. To go back one, enter prevd or press alt-left (in macOS, cmd-option-left). To go forward one, enter nextd or press alt-right (in macOS, cmd-option-right). This is a very convenient way to change to a previous directory! Both prevd and nextd can be followed by a number to switch to a particular numbered entry output by dirh.
In addition, a stack of specific directories is maintained. To push a new directory onto the stack and cd to it, enter pushd directory. To pop the current directory off the stack and cd to the directory now at the top, enter popd. It is not possible to pop the last directory off the stack. To view the current directory stack, enter dirs.

Commands

Every fish command is executed by entering its name followed by switches and arguments, if any, separated by spaces. Everything in fish is done with commands. This includes control statements like if, switch, for, and while. It also includes defining functions. It's hard to think of another programming language with such a consistent syntax. The fish shell is somewhat like Lisp in this regard.
By default, when entering commands, text is red until a valid command is entered. The text color then changes to "normal" which is the configurable, default, foreground color. When typing file paths, the text is underlined unless no directory or file matches what has been typed so far.
Many commands accept switches that affect their functionality. Switches can have short and long forms. Long switch names are preceded by two dashes. Short switch names are preceded by one dash and their names are a single letter. For example, the grep command can perform case insensitive matches. This is specified using the --ignore-case (long) or -i (short) switch. Long switch names can be abbreviated provided the abbreviation uniquely identifies one switch of the command. For example, --ignore-case can be abbreviated to --ig, but not -i because grep also supports the switch --include-dir . Command examples in this article usually use the long forms of switches and commands are followed by the short form in parentheses.
Nearly all commands set the status variable (referred to with $? in other shells) to 0 for success and another number for failure. true and false are not boolean literals. true is a command that sets status to 0 and false is a command that sets status to 1. Commands that do not set status include and, begin, break, builtin, case, command, continue, else, end, for, if, or, switch, and while. Despite being commands, most of these are used more like language keywords.
Commands are terminated by a newline or semicolon. In an interactive shell, pressing the enter key produces a newline. A line can contain more than one statement if they are separated by semicolons. For example, cd foo; pwd; ls.
There is a short list of commands that create a "block" which is a set of commands. These include block, if, switch, for, while, and function. These commands can span any number of lines and are terminated when the corresponding end command is reached.
Commands can be split over multiple lines in two ways. One is by pressing enter when there is an unterminated block. Another is by typing the backslash (\) continuation character before a command has been completely entered and pressing the enter key. Backslashes and newlines used for line continuation are removed before commands are executed.
Multi-line commands can be recalled and edited just like single-line commands. After a multi-line command has been recalled, press the left arrow and then press the up and down arrows to navigate between lines.
To include a space in an argument, precede the space with a backslash or enclose the argument in single or double quotes.
Arguments are expanded before commands are executed. This includes evaluating wildcards in file paths.

and & or Commands

In fish, and and or are commands, not keywords or operators. Both take a single argument which is a command to be conditionally executed based on the status of the previously executed command. This behavior is based on "short-circuiting" that is commonly used in many programming languages for boolean logic. If they do run the specified command, the status variable is set to the status of that command. Otherwise it is not changed.
To conditionally execute a command only if the previous command was successful, use command1; and command2. Note the semicolon at the end of the first command and the use of and instead of && like in bash. This is the same as
command1
and command2
This is not the same as command1; command2 because that will run command2 even if command1 was not successful.
To conditionally execute a command only if the previous command failed, use command1; or command2. Note the semicolon at the end of the first command and the use of or instead of || like in bash.
While these conventions for and and or may seem unusual at first, they follow the consistent syntax of fish where everything is done with commands and all commands are terminated with a newline or semicolon.
If an attempt is made to use the bash && and || operators, fish outputs a message explaining the correct alternatives. For example,
šŸ  date && ls
fish: Unsupported use of '&&'. In fish, please use 'COMMAND; and COMMAND'.
date && ls
      ^
The and and or commands are often used in if and while commands.

Command History

All commands entered are saved in command history, except those that begin with a space. Those are treated like incognito commands. This exception may be removed in fish 3. Duplicate commands are automatically removed, retaining only their most recent occurrence.
Command history for all sessions is stored in the file ~/.local/share/fish/fish_history. This file stores each command and the timestamp of when it was entered.
Each session begins with the history from this file, but stores commands entered within the session in memory. This is why a session does not recall commands entered in other sessions after it was started. However, a session can add command history from all other sessions to its own by entering history merge. This is a one-time merge that can be manually repeated later. The histories of other sessions will not be continually merged into the current session.
To navigate through all previously entered commands, press the up and down arrow keys. Press enter to execute the displayed command or press ctrl-c to exit without executing one. To restrict to commands that contain a given substring, type the substring before pressing the arrow keys.
To list all commands in the history, enter history.
To list only commands that contain a given substring, enter history substring. This is the same as entering history search --contains substring (-c).
To list only commands that begin with a given prefix, enter history search --prefix prefix (-p).
To include the date and time that commands were issued, add the --show-time-prepends (-t) switch.
To limit the number of commands output, add the --max (-n) switch followed by a number. For example, history -n 5. Note that the space after -n is required.
To clear all command history, enter history clear.
To delete commands from history that contain a given substring, enter history delete --contains substring (-c).
To delete commands from history that begin with a given prefix, enter history delete --prefix prefix (-p).

Command Substitution

To use the output of one command as an argument to another, surround the command with parentheses, not backticks like other shells. This allows nesting of command substitution which is achieved in other shells by using $(command) instead of `command`. If the command outputs more than one line, each will be treated as a separate argument to the outer command. For example, to list all files whose name contains the current year, ls *(date "+%Y")*.
Variables will be discussed in great detail in the "Scripting in Fish" section. For now all you need to know is that the set command sets a variable. Its arguments are a name and a value.
To set a variable to the output of a command, use set name (command). For example, set today (date).
Command output can be concatenated with literal strings and other command output. For example,
set dateFormat '+%A %B %d, %G' # ex. Tuesday, November 1, 2017
set announcement 'Today is '(date $dateFormat)'.'
The # above indicates the beginning of a comment that extends to the end of the line, like other shells.
Note the use of string concatention by placing literal strings next to the parentheses.
Command substitution does not work inside strings. For example, the command in the following string is not evaluated: "Today is (date $dateFormat)."

Wildcards

? matches a single character.
* matches any number of characters not including /.
** is like *, but also matches /. This enables recursing into subdirectories.

Pipes and Redirection

To "pipe" the stdout of one command into the stdin of another, use | like other shells. For example, echo 'some input' | someCommand.
For commands that read from stdin, to read from a file instead use < like other shells. For example, someCommand < someFilePath.
For commands that write to stdout, to write to a file instead use > like other shells. For example, someCommand > someFilePath. To avoid overwriting an existing file, use >? instead of >. To append to an existing file, use >> instead of >.
For commands that write to stderr, to write to a file instead use ^ unlike other shells that use 2>. For example, someCommand ^ someFilePath. For compatibility with other shells, 2> can also be used. To avoid overwriting an existing file, use ^? instead of ^. To append to an existing file, use ^^ instead of ^.
To redirect stderr to stdout, use someCommand 2>&1 like other shells.

Brace Expansion

Brace expansion is used to produce multiple arguments for a command. For example, ls *.{css,html,js} expands to ls *.css *.html *.js. This is also useful in the cp and mv commands.
Brace expansion can also be used to generate "Cartesian products". For example, to set a variable to this kind of result:
set cells {a,b,c}{1,2} # a1 b1 c1 a2 b2 c2
In the line above, the value assigned to the variable is shown in a comment at the end of the line.
Note that spaces cannot appear around the commas in brace expansion.

Customizing Fish

Aliases

An alias is an alternate name, usually shorter, for some command.
To define an alias, enter alias name command. For example, alias cdp 'cd $PROJECT_DIR' creates an alias that can be used to cd to one of your commonly used directories. It assumes that the PROJECT_DIR variable has been set to a directory path.
To list all aliases, enter alias.
To remove an alias, enter functions --erase name (-e).
Defining an alias creates a function with the specified name that runs when the alias is used as a command. Functions are described later in the "Scripting in Fish" section. The function is only defined in the current session and goes away when the session exits. To create aliases that are available in all future sessions, define them in config.fish. For more information, see "Defining aliases" at https://fishshell.com/docs/current/index.html#introduction.

Abbreviations

In addition to aliases, fish supports abbreviations. These are similar but expand when typed.
To define an abbreviation, enter abbr --add name value (-a).
For example, the following defines an abbreviation for checking out a git branch: abbr --add co git checkout. Once this abbreviation is defined, enter co followed by a space and that will expand to git checkout. Continue the command by typing a branch name and press enter to execute the command. The branch name is often auto-suggested and can be tab completed.
Unlike aliases, when abbreviations are defined in a terminal (as opposed to inside config.fish), they become available in all current and future sessions.
In general, abbreviations are preferred over aliases.
To output a list of all abbreviation names, enter abbr --list (-l).
To output a list of all abbreviation names and their values, enter abbr --show (-s) or just abbr.
To delete (erase) an alias, enter abbr --erase name (-e).

Configuration

The fish shell can be configured in three ways:
  1. executing commands in a shell
  2. using the web configuration UI
  3. adding commands in a configuration file

Configuration via Web UI

To start the web configuration UI, enter fish_config starting-tab-name. Specifying the starting tab name is optional and defaults to "colors". This command starts a local web server and opens a new tab in the default web browser. The UI contains seven tabs that are described below.

Colors Tab

The "colors" tab displays the current color settings and allows them to be customized. To use a provided color theme, select one, press the "Set Theme!" button, and wait a few seconds for it to change to "Theme Set!".
color config screenshot
Colors can be customized for the following syntax elements: commands, parameters, statement terminators, quoted strings, redirections, errors, comments, and autosuggestions. To customize one of these, click the corresponding syntax example near the top of the UI and select a color from the color grid that will be displayed below. When finished customizing the colors, press the "Set Theme" button. Note that these colors are not used when files containing fish function definitions are simply cat'ed in a terminal.
While the "color" tab allows selecting a background color, this is only for seeing how the other colors look against that background. It does not actually change the terminal background color.

Prompt Tab

The "prompt" tab displays the contents of the current fish shell prompt. It allows selection from 17 predefined prompts. To change the prompt, select one and press "Prompt Set!". DANGER: This overwrites ~/.config/functions/fish_prompt.fish, so if you have defined a custom prompt in that file and don't want to wipe it out, don't do this! Defining custom prompts is discussed later in the "Scripting in Fish" section.
prompt config screenshot

Functions Tab

The "functions" tab provides a read-only view of all fish functions that have been defined. Select a function name to see its definition which includes all the commands it executes.
functions config screenshot

Variables Tab

The "variables" tab provides a read-only view that lists all universal variables and their values. This does not list global or local variables. The distinctions between these variable types is discussed later.
variables config screenshot

History Tab

The "history" tab provides a mostly read-only view of your command history in the order in which commands were issued from any terminal, starting with the most recent commands. To delete a command from history, click the circled "X" to its right.
color config screenshot

Bindings Tab

The "bindings" tab provides a read-only list of the current key bindings.
bindings config screenshot

Abbreviations Tab

The "abbreviations" tab provides an editable list of all the abbreviations that have been defined. To modify an existing abbreviation, click the current command, change it, and press the "Save" button. To delete an existing abbreviation, click the circled "X" to its right. To add a new abbreviation, enter a name and a command in the two inputs at the bottom, one for the name and one for the associated command, and press the "Save" button. Changing the name of an existing abbreviation creates a new abbreviation with that name, but doesn't delete the existing one. Often abbreviations are defined in config.fish (discussed later), but they can also be defined by entering the abbr command in a terminal.
abbreviations config screenshot
...
abbreviations inputs config screenshot

Configuration Server

Pressing the enter key in the terminal where the fish_config command was entered shuts down the server that serves this web UI. To restart it, enter fish_config again, which will open the web configuration UI in a new browser tab.

Configuration via config.fish

Fish is billed as not needing configuration. Strictly speaking this is true. However, if you currently use another shell like bash, you likely have configuration for it (in ~/.bash_profile and ~/.bashrc) and will want similar configuration for fish. Examples include creating aliases, creating abbreviations, and setting variables such as PATH.
The fish configuration file is named config.fish and should be placed in your ~/.config/fish directory. Placing configuration in this file enables copying the file to other machines where the same configuration is desired.
Inside config.fish, to run commands only when starting a login shell, place the commands inside an if command as follows: if status --is-login; ...; end. To run commands only when starting an interactive shell, use if status --is-interactive; ...; end.

Custom Greeting

By default, new interactive fish shells display the greeting "Welcome to fish, the friendly interactive shell". To change this, enter something like set -U fish_greeting 'You are a fish!'. To suppress the greeting, erase the current setting with set -e fish_greeting.

PATH environment variable

PATH is a variable that holds a list of paths that the shell searches when a command is entered. PATH is a global variable which means that changes to it are available anywhere in the current session but not in other sessions. Its value is a space-separated string, not a colon-separated string like other shells. .
Typically PATH is defined in config.fish.
To see the current value, enter echo $PATH.
The universal variable fish_user_paths is another list of paths that is automatically prepended to PATH. Universal variables are described later. Setting this modifies PATH in a way that affects all current and future sessions without having to modify config.fish and source it again. However, a benefit of changing PATH in config.fish is that the file can easily be copied to other machines to propagate the change.
fish_user_paths should only be set in a terminal, not in config.fish.
To prepend a path to PATH,
set -U fish_user_paths $fish_user_paths new-path
This outputs a warning if no directory matching new-path is found, but adds it anyway.
As we will see later, all variables hold a list. It's just that many of them only contain a single value. Lists are discussed in the "Scripting in Fish" section. Removing a path from PATH or fish_user_paths is done the same way as removing an item from any list.
To remove a path from fish_user_paths:
if set -l index (contains -i some-path $fish_user_paths)
  set -e fish_user_paths[$index]
end

Tools for Fish

Here's a list of popular tools for fish in alphabetical order.

Scripting in Fish

If you appreciate programming languages with simple, consistent syntax, you will love scripting in fish!
A script can be implemented by writing a function. Functions can become custom commands that are available to be executed from a terminal.
In other shells, many steps are required to write and enable a new script.
While the same can be done with fish scripts, most of these steps are not necessary. The easiest way to implement and enable a new fish command is:

echo Command

The echo command writes its expanded arguments to stdout. It is often used to output the values of variables. For example, echo $PATH. Normally all arguments are output with a space between them. To suppress this, add the -s switch (no long form). To suppress the newline that is normally output at the end, add the -n switch (no long form). To make a sound (or beep), output the \a character.

Variables

Variable names consist of letters, digits, and underscores. Unlike variable names in other shells (and programming languages), these can begin with a digit. Variable names are case-sensitive, so myvar and myVar are different variables.
The value of every variable is a list that holds zero, one, or more string values, although most have only one.
There are three variable scopes.
Local variables are available only in the current block. Blocks begin with one of these commands, each of which are discussed later: begin, if, for, function, switch, or while. Blocks are terminated by a corresponding end command.
Global variables are available anywhere in the current session.
Universal variables are available in every session, even in future sessions because they are saved across session terminations (even reboots). Settings of universal variables are stored in the file ~/.config/fish/fishd.machine-id.
Regardless of scope, every variable is set using the command set name value. This command accepts many switches.
To make a variable local, add --local (-l).
To make a variable global, add --global (-g).
To make a variable universal, add --universal (-U).
To export a variable so it is visible in child processes, add --export (-x). This is not a new kind of scope. It is convention for exported variable names to be all uppercase.
The same variable name can be assigned a different value in each scope. The value used depends on context, using the lowest visible scope, local then global then universal.
What happens when a variable is set without specifying a scope depends on whether it has been previously set. If the variable has already been set in any scope, this changes the value of the lowest visible scope already set. If the variable has not already been set in any scope and it is not being set inside a function, the variable will be global. If the variable has not already been set in any scope, and it is being set inside a function, the variable will be local to that function, not the block where it is set. To force it to be local to a block, use the --local (-l) switch. When a variable is set in config.fish with no scope switch, it becomes global. While these rules may seem challenging to memorize, setting variables with no scope switches inside functions typically does what is desired.
To get the value of a variable use $name.
To delete (or erase) a variable, enter set --erase name (-e). This will delete the variable from the closest scope in which it is currently defined, considering local then global then universal. A scope can also be specified using a scope switch.
To list the names and values of all variables in the lowest in which they can be found, enter set. A scope switch can also be specified to list only the variables in that scope. To list all variables that have been exported, add the --export (-x) switch.
To list only the names of all defined variables, enter set --names (-n). A scope switch can also be specified to list only the variable names in that scope.
To determine if a variable has been set, enter set --query name (-q). This sets the status variable to 0 if set and 1 if not. For example, set -q fish_greeting; echo $status. A scope switch can also be specified to query only in that scope.
To interactively edit the value of a variable, enter vared name. This is useful for values that are long strings. It cannot be used to edit values that are lists containing more than one item. To edit a specific item in a list, specify the item index. For example, if the variable colors holds a list of colors, the third color can be edited with vared colors[3]. List indexes start at 1.
Currently there is no way to request the value of a variable in a specific scope. Version 2.7 of fish (coming very soon) will add a --show switch to the set command. To display information about a given variable name in all scopes in which it is defined, you will be able to enter set --show name.

Eval

The eval command evaluates a command in a string that can be built using concatenation and variable substitution. For example,
set extension 'js'
set command 'ls *.'$extension
eval $command # lists all JavaScript files in the current directory

Advanced Use of Variables

Multiple $'s can be used to interpret the value of a variable as a variable name. Consider the following examples:
set foo 1
set bar foo
echo $$bar # 1

set pocket wallet
set pant pocket
echo $$pant # wallet

set foreground blue
set background green
set side fore
set name $side'ground'
echo $$name # blue
From https://fishshell.com/docs/current/index.html#expand-variable, "When using this feature together with array brackets, the brackets will always match the innermost $ dereference." To understand this, consider the following example.
set colors red green blue
set listName colors
How can we get the value "green" from the list of colors?
echo $$listName[2] doesn't work because it evaluates like tmp = $listName[2] and then $tmp on that. $listName[2] gives array index out of bounds because the value of $listName is just 'colors' and that has no value at index 2.
echo $$listName[1][2] works! $$listName[1][2] evaluates like $listName[1] which gives 'colors' and then $colors[2] which gives 'green'.

Special Variables

There are many global variables that are automatically set to various values. Examples include:
Variables whose names begin with __fish are for internal use and should not be used or changed.

Strings

Literal string values are specified by enclosing text in single or double quotes. Variable substitution occurs in double quotes, but not in single quotes. For example, the reference to $USER in the following is replaced by its value: set msg "Good morning, $USER. Have a great day!"
In a single quoted string, single quotes can be escaped with \'. In a double quoted string, double quotes can be escaped with \" and $ (which is used for variable substitution) can be escaped with \$. In both kinds of strings, backslashes can be escaped with \\.
The string command has many subcommands that are specified by following string with a space and the name of the subcommand. To suppress output from these and just use the value of the status variable, add the --quiet (-q) switch. If any arguments begin with a dash, add " -- " after any switches and before any arguments to indicate where the switches end.
To output the length of a string, use string length $name.
Literal strings can be concatenated with the value of a variable. For example,
set middle 'some middle'
set result 'some prefix'$middle'some suffix'
These are similar, but result in a list with a "count" (list length) of 3 because the first token "some" and the last token "suffix" are treated as separate list items from the string concatenation that occurs in the middle.
set result some prefix"$middle"some suffix
set result some prefix{$middle}some suffix
Here are some tests than can be performed on values that are a single string.
To test the relationship between two strings in lexical order, use the expr command. This evaluates an expression where each operand and operator is passed as a separate argument. It outputs 1 if the expression evaluates to true and 0 if false, but it sets status to the opposite value which is convenient when used in conjunction with commands like if, while, and, and or.
For example, to determine if a string in the variable v1 is less than or equal to a string in the variable v2, use:
if expr $v1 '<=' $v2
  # code goes here
end
Note that expr coerces strings to numbers if it can, so expr '3' '<' '12' outputs 1 for true despite the fact that the string '3' sorts after the string '12'.
To get a substring, use string sub --start startIndex --length length $name (-s and -l). String indexes start at 1, not 0. If -s is omitted, it defaults to 1. If -l is omitted, it gets characters to the end. If -s is negative, it counts from the end of the string. For example, to get last three characters, use string sub -s -3.
To split a string on a delimiter resulting in a list, use string split delimiter $var
For example,
set csv 'red,green,blue'
set colors (string split , $csv) # red green blue
Note that in the previous line the string split command was surrounded by parentheses. This allows its stdout to be captured.
To trim leading and/or trailing characters, use string trim $var
By default, this trims whitespace from both ends of the string. Switches can be used to change this.
For example, to remove leading zeros:
set myText '00019'
set result (string trim --left --chars 0 $myText) # 19
To create a new string by joining existing ones with a delimiter between them, use string join. For example,
set dateStr (string join '/' $month $day $year) # 11/1/2017
Another example of joining strings is getting initials from a full name. Note how string sub can be used to get substrings from multiple strings in one call. Also note how the delimiter passed to string join can be an empty string.
set first Richard
set middle Mark
set last Volkmann
set initialList (string sub -l 1 $first $middle $last) # R M V
set initials (string join '' $initialList) # RMV
To create a string that repeats a given string, use string repeat with the --count (-n) switch For example,
set newText (string repeat -n3 'foo') # foofoofoo

Regular Expressions

To determine if a string matches a pattern, use string match. By default, this just compares one string to another for an exact match which isn't very useful.
A more common use is to match against a regular expression by adding the --regex (-r) switch. This uses Perl regular expression syntax. By default, only the first match is output, or nothing if no match is found. It sets status to 0 if at least one match was found and 1 if not. To suppress output describing the matches, add the --quiet (-q) switch.
For example,
set name 'Mark Volkmann'
string match --quiet --regex 'Vo' $name
echo $status # 0 for found
To output all matches, add the --all (-a) switch.
To make matching case-insensitive, add the --ignore-case (-i) switch.
To get indexes and lengths of matches instead of matching text, add the --index (-n) switch. This outputs an alternating list of indexes and lengths.
Regular expressions can use capture groups. In this case, string match outputs a list containing each full match followed by the corresponding capture matches.
Here are several examples of using string match with regular expressions:
set text 'This is foolish work for a boolean.'
set matches (string match -ar '.oo' $text) # foo boo
set matches (string match -anr '.oo' $text) # 9 3 28 3
set text 'A foal isn\'t boolean.'
# The next regular expression uses a capture group.
set matches (string match -ar '.(o.).' $text) # foal oa bool oo
To determine if a string doesn't match, add the --invert (-v) switch.
To create a new string where specific text in another is replaced, use string replace. This is similar to string match in that it can match against literal text, or a regex using the --regex (-r) switch. When using a regex, it can utilize capture groups. To replace all occurrences instead of just the first, include the --all (-a) switch.
For example, to replace all occurrences of "foo" with "bar",
set text 'This is foolish work for a fool.'
set newText (string replace --all 'foo' 'bar' $text) # not using a regex
# this is barlish work for a barl
set newText (string replace -ar '.oo' 'bar' $text) # regex matching more than foo
# this is barlish work for a barl

Numbers

Only integers are supported, not floating point. When a variable is set to a number, it is stored as a string.
For example, set score 12345 sets the variable score to a string containing the five characters "12345".
To test the relationship between two numbers, use the test command. This supports many switches for various comparisons. For numeric comparisons, both operands must be numbers, or strings that contain only digits which will be converted to numbers for the comparison.

Math

The math command is a wrapper around the bc command which is an abbreviation for "basic calculator". It allows an expression to be specified as an argument instead of reading from a file or piping in through stdin as is required with bc. The result is written to stdout. It sets status to a non-zero number if expression is invalid.
Unlike the fish shell itself, the math command supports floating point numbers.
For information on available operators and functions, enter man bc.
The math command accepts any number of arguments that are concatenated to form the expression that will be parsed by the bc command. It is only necessary to surround the expression in quotes if it contains characters that the shell would interpret specially. However, since these characters include * (for multiplication) and parentheses, quotes are often required.
Here is an example that utilizes variables.
set width 2
set height 3
set area (math "$width * $height") # 6
By default, the division and modulo operators output an integer result obtained by truncating the decimal portion (not rounding) even if all operands are floating point numbers. To include decimal places in the result, add the -s switch followed by a number of decimal points with no space after -s. For example, math '2 / 3' outputs 0, but math -s3 '2 / 3' outputs .666. Note how this does not perform rounding at the last decimal place. This is why math -s0 '2 / 3' outputs 0, not 1.
The easiest way to get rounding is to use printf. For example, to round at three decimal places, use printf '%.3f' (math -s4 '2 / 3') which outputs 0.667. Note how this requests one more decimal place from math that is requested from printf.
The bc command supports functions from the Unix standard math library if the --mathlib (-l) switch is added. These functions include sine (s), cosine (c), arctangent (a), natural logarithm (l), exponentiation (e), and bessel (j). Unfortunately the math command doesn't support the --mathlib switch, so it cannot use these functions. However, the bc command can be used directly in fish. Here's an example that determines the sine of 45 degrees.
set pi (echo "a(1) * 4" | bc -l) # 3.14159265
set degrees 45
set radians (math -s8 "$degrees * $pi / 180") # .78539816
set result (echo "s($radians)" | bc --mathlib) # .70710678
While this kind of math is possible in fish, it may be better to use another language or tool such as Node.js, Ruby, Python, or Perl.

Lists (aka Arrays)

Recall that the value of all variables is a list which may contain any number of items, including none or just one. Lists contain an immutable collection of string values. They cannot contain other lists.
To create an empty list, use set listName
To create a non-empty list, use set listName item1 item2 ...
For example, set colors red green blue is equivalent to set colors 'red' 'green' 'blue'. Neither of these is equivalent to set colors 'red green blue' in which the value of colors is set to a single string.
To get the length of a list, use count $listName
To append an item to a list, use set listName $listName newItem
To prepend an item to a list, use set listName newItem $listName
To get an item from a list by index, use $listName[index]. Indexes start at 1, not 0. Use negative indexes to retrieve from the end of a list. For example, $listName[-1] gets the last item.
To set a list item by index, use set listName[index] value
To get a slice of an existing list which is a new list, use $listName[start..end]
start and end can be positive or negative.
To get a new list that contains specific elements from another, use a space-separated list of indexes in the form $listName[index1 index2 index3]. For example, to get the first and last colors, use $colors[1 -1].
To create a reversed version of an existing list, use set newListName existingListName[-1..1]
To remove the first item in a list, use set --erase listName[1] (-e) or set listName listName[2..-1]
To remove the last item in a list, use set --erase listName[(count $listName)]
To test whether an item is in a list, use:
if contains item $listName
  # code goes here
end
The contains command sets status to 0 if the item is found and 1 if not.
To get the index of an item in a list, use
contains --index item $listName (-i)
To remove an item from a list, use
if set --local index (contains --index item $listName)
  set --erase listName[$index]
end
Note that --local is used above to restrict the scope of the index variable.
To iterate over all the items in a list, use
for item in $listName
  # use $item here
end
Adjacent lists result in cartesian products. For example,
set columns a b c
set rows 1 2
set cells $columns$rows # a1 b1 c1 a2 b2 c2
count $cells # 6

set dashedCells $columns'-'$rows # a-1 b-1 c-1 a-2 b-2 c-2

set labels 'Column '$columns # Column a Column b Column c
count $labels # 3
When a fish shell is started, if the variables PATH, CDPATH, or MANPATH are set in a parent shell (such as bash), they are converted to lists by splitting on colons.

Hashmaps

Hashmaps are not currently supported, but have been discussed at https://github.com/fish-shell/fish-shell/issues/390. They will be added in fish 3.
Hashmaps can be simulated with plain variables, but there isn't a way to iterate over the keys unless they are held in a list. For example,
set colors red green blue
set items_red fire
set items_green grass
set items_blue water
for color in $colors
  eval echo '$'items_$color # fire, grass, water
end
Without the use of eval above, this would output $items_red, $items_green, and $items_blue.

Colors

The color of output can be customized using the set_color command. This command outputs a sequence of characters that changes the foreground color for subsequently output text. For example:
echo Roses are (set_color red)red(set_color normal), \
  violets are (set_color blue)blue(set_color normal).
The characters output by the set_color command can be captured in a variable and output later. For example, set makeBlue (set_color blue); echo "I am$makeBlue blue"
Colors can be specified by using a color name or value. The supported color names include black, red, green, yellow, blue, magenta, cyan, and white. There are also bright versions of these where "br" is prepended to the name, for example, brblue. Color values are specified with three or six hex characters. For example, f0f is full red, no green, and full blue which is between magenta and brmagenta.
The set_color command supports many switches.
Using set_color without these switches turns all modes off.
To reset all colors and switches, use set_color normal. This sets the foreground color to the default for the terminal. When commands are entered in a shell, the color of their output is automatically reset to normal after each line is entered. The color is not automatically reset inside a function.
There are many universal variables whose names begin with fish_color_ or fish_pager_color_ that control colors of various syntax elements. These variables can be modified to customize the colors.
For another example of using the set_color command, see the custom prompt defined in the Custom Prompt section.

printf Command

The printf command writes to stdout using a format string. It takes a format string as its first argument, followed by the values to be output.
The format string can contain a format specifier (taken from the printf library) for each of the values to be output. Examples include:
The printf command supports several escape characters. Examples include:
Here are some examples of using the printf command.
set format 'My name is %s and I am '(set_color red)%i(set_color normal)' years old.'
printf $format Paige 6

# This is another approach that produces the same result.
set format 'My name is %s and I am %s%i%s years old.'
printf $format Paige (set_color red) 6 (set_color normal)

status Command

The status command has many subcommands. Examples that are useful inside functions include:

Files

To iterate over the lines in a text file, redirect a file into a loop as follows:
cat filePath | while read line
  # do something with $line
end
or
while read line
  # do something with $line
end < filePath 
To test various characteristics of a file, assuming file is a variable that holds a file path,
The test command does not support long forms of the switches used above.

Tests

The test command is commonly used in if and while commands. Tests for strings, numbers, and files were described previously in those sections. For example, test "$color" = 'yellow'. This can also be written as [ "$color" = 'yellow' ] for compatibility with other shells (although most use double square brackets now), but explicitly using the test command is preferred.
To negate any test, add ! after test and before the expression being tested. For example, test ! $color = 'yellow'. This can also be written as test $color != 'yellow'.
There are two ways to combine conditions. The first option is to use the and and or commands. For example,
if test $color = 'yellow'; and test $size = 'large'
  # code here
end
This can also be written on two lines by replacing the semicolon with a newline as follows.
if test $color = 'yellow'
  and test $size = 'large'
  # code here
end
The second option is to place -a or -o between the conditions. For example,
if test \( $color = 'yellow' \) -a \( $size = 'large' \)
  # code here
end
This option requires parentheses and those must be escaped which results in ugly code, so the first option is preferred.

Functions

Functions give a name to a set of commands that are executed when the function is invoked. They can become available to execute as commands when their name is used as a command. In that case they can override commands found in PATH and builtin commands.
To call a function, specify its name optionally followed by arguments. No parentheses are used around arguments and no commas are used between them.
Functions cannot return a value, but they can write to a stream like stdout and stderr and they can return a status that is used to set the status variable. To capture stdout from a function in a variable, use set variable (someFn args).
Functions in fish do not have access to local variables set outside them. They are not closures like functions in programming languages such as JavaScript.
Function names cannot begin with a dash (-), but they can begin with any other character, even digits. Function names cannot contain a slash (/), but they can contain any other character, even spaces. To include a space, prefix it with \. For example, function foo\ bar; ...; end To call this function, use foo\ bar. While this is supported, utilizing this will likely cause confusion.
Functions can be defined interactively using the function command. This command continues reading from stdin until a corresponding end command is entered. The syntax for defining a function is:
function name
  # code goes here
end
The arguments specified in a function call are held in a list in the argv variable. The first argument can be accessed with $argv[1]. Iterating over the arguments is done in the same way as iterating over any list.
Many switches can follow the function name. The --argument-names (-a) switch of the function command specifies named parameters. Here is an example of a function that uses named parameters:
function orderShirt -a size color
  # Use $size and $color here.
  # $argv is also set to a list of the arguments.
  echo I see you want to order a $size shirt that is $color.
end
All named parameters will be "set", but ones not specified will be set to an empty list versus undefined (has no value). To test whether a specific named parameter is set, use set -q argName[1]. To set a named parameter to a default value if it is not specified, use:
set -q argName[1]; or set argName defaultValue
Here is the previous example, modified to have default values for the arguments:
function orderShirt -a size color
  set -q size[1]; or set size 'large'
  set -q color[1]; or set color 'white'
  echo I see you want to order a $size shirt that is $color.
end
To add documentation, use --description 'some desc' (-d). To display the description of a function, enter functions --details --verbose fnName and note the fifth line of the output. The --details switch is described further below.
Here is a function that can be used to only output the description of another function:
function fndesc -a fnName -d 'displays description of a function'
  set lines (functions --details --verbose $fnName)
  echo $lines[5]
end
All functions are public. Prefixes can be used to denote "private" functions by convention, and to avoid name conflicts. For example, _somePrefix.
The most common way to make functions available as commands in future sessions is to define them in files in a directory in the fish_function_path list (not in subdirectories of these). By default, this list includes ~/.config/fish/functions. There is no need to mark these files as executable. There no need to modify the PATH variable because fish will autoload functions from the directories in the fish_function_path list without them being in PATH. There is no need to add a "shebang comment" in these files. All functions defined in these directories are assumed to be used only in the fish shell.
To list all currently defined functions, enter functions. To include private fish functions whose names begin with a double underscore, add the --all (-a) switch. To view all currently defined functions in the web UI, enter fish_config functions.
To output the definitions of one or more functions, enter functions name1 name2 ....
To output the path to where a function is defined, enter functions --details name (-D). If the function has not been saved to a file, this will output "stdin".
To see even more detail about a function, enter functions --details --verbose name (-Dv). This outputs the following five lines:
To copy a function definition to a new name, enter functions --copy oldName newName (-c). This only copies the function body, not any switches specified in the definition.
To delete (erase) a function, enter functions --erase name (-e). If the function is defined in a file, this doesn't delete the file. It just makes the function inactive in the current session.
To test if a function is defined, enter functions --query name (-q). This sets status to 0 if the function exists and 1 otherwise.
The return command can be used in a function to exit before the end. This also sets the exit status of the function which defaults to 0 for success, but can be specified. For example, return 2 exits the function in which it was executed and sets status to 2..
If a function needs to output an error message, a good approach is the following:
set_color $fish_color_error
echo someErrorMsg 1>&2 # writes to stdout
set_color normal
return someNonZeroStatus
To output a formatted version of a .fish file, enter fish_indent someName.fish. This just writes to stdout. To save the formatted version in place of the current version add the --write (-w) switch. To output using the colors specified using fish_config (or the color variables that sets which can also be set in the config.fish file), add the --ansi (no short form) switch. To output as HTML, add the --html (no short form) switch. With HTML it is possible to add colors to specific syntax items using CSS.
To read from a file and rewrite it with a formatted version, enter fish_indent --write someName.fish (-w). This uses four-space indentation and that cannot be customized. However, all occurrences of four spaces can be replaced with two using the following command which also saves a backup of the original file in a new file with .bak appended to the name.
sed -i .bak 's/    /  /g' fn-name.fish

Comparison to JavaScript

Let's look at an example of a simple function that takes two numbers and outputs their product. It's useful to see how fish functions differ from functions in more traditional programming languages like JavaScript.
Here is the function in JavaScript and a call to it.
function product(n1, n2) {
  return n1 * n2;
}
const result = product(2, 3); // 6
Here is the function in fish and a call to it.
function product -a n1 n2
  math "$n1 * $n2"
end
set result (product 2 3) # 6

Function Autoloading

Every time a command is evaluated, fish first checks whether there is a file in any path listed in fish_function_path with the same name and a .fish extension. If a matching file is found, and the file has not been loaded within the "staleness interval" (discussed later), it loads the file to get the current definition of the function and executes the function. This is referred to as "autoloading".
If no such function is found, fish then checks whether there is an executable file in any path listed in PATH with the same name. If so, that file is executed.
Multiple functions can be defined in the same file, but they will not be available until the function whose name matches the file is used once, so doing this is not recommended.
Function autoloading allows functions to be used in place of commands, builtins, and executables in PATH. For example, the date command can be overridden with a custom function by entering function date; echo pwned date; end. After this, entering date outputs "pwned date" instead of the output of the date command in the current fish session.
To save a function definition that has been entered interactively, enter funcsave name. This creates a file with the same name as the function in the ~/.config/fish/functions directory so it can be autoloaded. The funcsave command uses tabs for indentation.
To edit a function definition, enter funced name. If the function is not already defined, it will be created. This uses the editor specified in the VISUAL variable that can be set in config.fish. It defaults to emacs, but can be changed to Vim with set VISUAL vim. Using funced is more convenient than starting an editor first and then creating the file in the proper directory or locating it from within the editor. After every use of funced, if the changes should be available in current and future sessions, run funcsave name.
While functions can be defined in config.fish, it is more efficient to use autoloading functions.
The staleness interval avoids thrashing the file system every time a command is evaluated, and is around 15 seconds. This is an implementation detail that cannot be configured. To use a modified function definition before the staleness interval passes, source the file. For example,
funced foo; and funcsave foo; and source ~/.config/fish/functions/foo.fish

Auto-running Functions

Functions can run automatically based on events, variable changes, process exits, and signals.
These functions must be loaded before their triggers occur. They should not rely on autoloading. Consider defining them in config.fish or sourcing files that define them from config.fish. The command source filePath executes commands in the file without starting a new process.
Changes to non-local variables made in functions affect the current shell.

Auto-running on Events

To configure a function to run automatically when an event is fired, add the --on-event (-e) switch followed by an event name to the function definition. For example,
function takeShelter --on-event tornado
  echo Tornado warning! Take shelter!
end
Any number of functions can be registered to run on the same event. A function can be triggered by any number of events by specifying more than one --on-event switch.
Events can be emitted with emit eventName. The event name can be any string and it can be followed by any number of arguments. These become the value of the argv list inside the function. For example, emit tornado 'category 4' 12.
Events are only handled within the current process.
The fish shell generates these events:

Auto-running on Variable Changes

To configure a function to run automatically when variables changes, add the --on-variable (-v) switch followed by a variable name to the function definition. For example, the following function runs every time the PATH variable changes. It echoes the new value.
function announcePath --on-variable PATH
  echo PATH is now $PATH
end
set -U PATH /foo $PATH
Since changing the variable fish_user_paths also changes PATH, that also triggers this function.
A function can be triggered by any number of variable changes by specifying more than one --on-variable switch.

Auto-running on Process Exits

To configure a function to run automatically when a given process exits, add the --on-process-exit (-p) switch followed by a process id to the function definition. For example,
function announceProcessExit --on-process-exit 12345
  echo an important process exited
end

# %self expands to the current process id.
function announceMyExit --on-process-exit %self
  echo 'got process exit' > process.log
end

Auto-running on Signals

To configure a function to run automatically when a specific signal is received, add the --on-signal (-s) switch followed by a signal name or number to a function definition. For a list of possible signals, enter kill -l. To see the keystrokes that generate some of these signals, enter stty -a and look for "cchars:". For example, pressing ctrl-d sends an "eof" signal.
To send a signal to a process, enter kill -signalNameOrNumber processId. For example, to handle a "hup" signal,
function handleHupSignal --on-signal hup
  echo got hup
end

kill -hup %self # got hup
A function can be triggered by any number of signals by specifying more than one --on-signal switch.
For more information on Unix signals, see https://www.tutorialspoint.com/unix/unix-signals-traps.htm.

Types of Commands

A "command" is any program the shell can run. There are three types of commands: builtins, commands, and functions.
"Builtins" are commands that are provided by the shell. Examples include cd, echo, and if. To get a list of builtin names, enter builtin --names (-n)
"Commands" are executables found in PATH. Examples include chmod, ls, node, and vim. To get the path to a command, enter command --search name (-s) or which name.
"Functions" have two sources, those provided by fish and those that are user-defined. Examples of provided functions include abbr, cd, and eval. There are also many fish-specific functions whose names begin with fish_. Typically these function definitions are found in /usr/local/share/fish/functions.
User-defined functions are typically defined in files with a .fish extension in any directory listed in the fish_function_path directory.
It is possible to have a function, builtin, and command that all have the same name. Functions take precedence over builtins and builtins take precedence over commands. To run a function, just enter its name. To run a builtin instead of a function with the same name, enter builtin name. To run a command instead of a function or builtin with the same name, enter command name.
Some "commands" are available as both a builtin and a function or a command and a function. The function wraps access to the corresponding builtin or command and adds functionality. For example, cd is a builtin and a function. Enter type cd to see the definition of the function and the functionality that it adds.
Recall that the alias command generates a function that implements the alias. For example,
alias greet 'echo Hello, $USER'
functions greet
This outputs the following:
# Defined in - @ line 0
function greet --description 'alias greet echo Hello, $USER'
  echo Hello, $USER $argv;
end
Note how within this function $argv is added to the end of the command specified in the alias. This allows additional switches and arguments to be passed to the command when the alias is used.
To get the type of a name, enter type name. For a function, this outputs "name is a function with definition", followed by the definition. For a builtin, this outputs "name is a builtin". For a command, this outputs the path to the file that defines it. To get just the type, enter type -t name. This outputs "function", "builtin", or "file" (for commands).

if Command

The if command supports conditional logic. Its syntax is:
if command
  commands
else if command
  commands
else
  commands
end
Note that if is followed by a fish command, not a "condition". The status of the command is used to determine whether the commands in that branch will be executed. A status of 0 is treated as success or true and any other value is treated as an error or false. This is opposite from most programming languages, but is standard in shells.
A common command to use in an if is test. For example, to explicitly test the status of the last command executed before an if, use if test $status = 0 # success or if test $status != 0 # failure
A command status can be negated using the not command. This command changes status to 1 if it is currently 0, and 0 otherwise. For example, if not test $status or if not someCommand.
An if command can be specified on a single line using semicolons as follows:
if command; statements; end

switch Command

The switch command conditionally executes other commands based on the value of a expression. Often the expression is the value of a single variable. Its syntax is:
switch expression # ex. $color
  case value1 value2
    statements
  case value3
    statements
  case '*' # like default in other languages
    statements
end
A case can list more than one value. A "break" statement is not needed at the end of each case because execution does not fall through as it does in many programming languages.
To treat one case as the default to use when none of the others match, use case '*'.
For example, this function accepts a "help" switch specified with either -h or --help.
function product -d 'outputs the product of any # of arguments'
  set result 1
  for arg in $argv
    switch $arg
      case '-h' '--help'
        echo 'This function returns the product of its arguments.'
        return
      case '*'
        # If $arg starts with a dash ...
        # -- is needed in case $arg starts with a dash.
        if string match -qr '^-' -- $arg
          echo "Unsupported switch $arg"
          return
        end
        set result (math "$result * $arg")
    end
  end
  echo $result
end

Loop Commands

Two kinds of loops are supported, while and for.
The syntax for a while loop is:
while command
  # commands go here
end
This uses the status of a command to determine whether to iterate again. It continues as long as the status is 0 and stops when it is any other value.
For an endless loop, use the true command.
To test multiple conditions, use the and and or commands.
A for loop iterates over the elements of a list. Its syntax is:
for var in list
  # use $var here
end
While the seq command is not unique to fish, it is useful to see how it is used in conjunction with the for command. To iterate over a sequence of numbers:
for n in (seq 5)
  echo $n
end
This outputs 1, 2, 3, 4, and 5 on separate lines.
The seq command can be followed by 1, 2 or 3 numbers to specify the first, increment, and last values. If only one argument is supplied, it is the last, first defaults to 1, and increment defaults 1 If only two arguments are supplied, they are the first and last values, and increment defaults to 1.
A for command can be used to iterate over the relative file paths that match a pattern. For example:
for path in b*.fish
  echo $path
end
In both kinds of loops, use break to exit early and continue to skip the remainder of the current iteration.

Custom Prompt

The default fish prompt displays the output of the whoami and hostname commands, followed by the abbreviated working directory (returned by the prompt_pwd command) and ">".
The abbreviated working directory output by prompt_pwd is so abbreviated that it isn't very useful. It is somewhat better if the characters per path part, which defaults to 1, is increased. To increase it to 3, enter set fish_prompt_pwd_dir_length 3.
To customize the command prompt to something other than the predefined options available in fish_config, create the file ~/.config/fish/functions/fish_prompt.fish containing a function with the name fish_prompt that echoes the desired prompt. This can use the set_color command to control the colors of various parts. Any number of lines can be output, but typically there are only one or two.
If the prompt doesn't fit in the current terminal width, fish will instead just output ">". The fish_prompt function can compare the length of what it wants to output with the COLUMNS variable and output something that fits.
Here's an example of a custom prompt that takes the terminal width into account. Note that there is no way to determine if the window width is less than 20 characters because when that is the case, fish sets the COLUMNS variable to 80. This is done because fish may not behave sensibly if the terminal width is less than 20.
# This function is run every time fish displays a new prompt.
function fish_prompt
  set vimModeLen 2 # appears at beginning of prompt (described later)
  set remaining (math "$COLUMNS - $vimModeLen")

  # Display present working directory.
  set_color --bold brblue # pwd color
  set pwdLen (string length $PWD)
  if test $pwdLen -le $remaining
    echo -n $PWD # -n suppresses newline at end
    set remaining (math "$remaining - $pwdLen")
  else
    echo -n (prompt_pwd) # abbreviated working directory
    set remaining 0 # so nothing else is output on this line
  end

  # Get the current Git branch.
  # This will be an empty string if not in a Git repo.
  set branch (git rev-parse --abbrev-ref HEAD ^/dev/null)

  # If in a Git repo ...
  if test -n "$branch"
    set branchLen (string length $branch)
    # If branch name will fit on current line ...
    if test $branchLen -le $remaining
      echo -n ' ' # space between PWD and branch name
    else
      echo # newline
      # Indent past Vim mode on previous line.
      echo -n (string repeat -n$vimModeLen ' ')
      set remaining $COLUMNS # resets to full width
    end

    # If branch name will fit on current line ...
    if test $branchLen -le $remaining
      # Display current Git branch.
      set_color --bold yellow # git branch color
      echo -n $branch
    end
    # If branch name doesn't fit, it is not output.
  end

  # Always display "fish" prompt on new line.
  set_color normal
  # This uses printf instead of echo to output a leading newline.
  printf '\\nšŸ   ' # uses Unicode for tropical fish emoji &#x1F420;
end
It is also possible to define a "right prompt" by defining the function fish_right_prompt. This is right-aligned within terminals.
If the VISUAL global variable is set to "vim", fish can display the Vim mode in the prompt using the function fish_mode_prompt. For example, this could display "N" for normal, "I" for insert, "R" for replace, and "V" for visual mode. This function outputs a new prompt every time the mode changes which is a bit distracting. To disable this feature, make this a function that doesn't output anything. It is not enough to simply not define this function because fish provides a default implementation that must be overridden to change.
Here's an example of a custom mode prompt. Newlines normally output by the echo command are stripped when this function runs.
function fish_mode_prompt
  if test "$fish_key_bindings" = 'fish_vi_key_bindings'
    switch $fish_bind_mode
      case default
        set_color red
        echo N
      case insert
        set_color green
        echo I
      case replace_one # There is no replace_all.
        set_color green
        echo R
      case visual
        set_color magenta
        echo V
    end
    echo ' '
  end
end

Debugging Functions

A common way to debug functions is to add echo commands and comment out parts of the code. Often a better approach is to add breakpoints to stop execution at specific places. While stopped, variables can be examined and modified. Then execution can be continued.
To add breakpoints in a function, add uses of the breakpoint command. After adding them, run the function. When a breakpoint is reached, the function will stop and control will be returned to the shell. Use the echo command to examine variables. and the set command to modify them. For example, echo $count and set count 7.
To resume execution, enter exit. To stop the running function before it reaches its end, enter kill %self.

Custom Tab Completions

Tab completions can be defined for custom commands using the complete command. Its syntax is:
complete -c commandName -s shortSwitchName -l longSwitchName -a 'argument words'
This command accepts many switches. The most useful are:
Here is an example of a custom command with custom completion of color names. It echoes text using colors specified with the --color (-c) switch which can be used any number of times. For example:
cecho -c red fire -c green grass -c normal cloud -c blue sky
Tab completion can be used to select colors by entering the --color (-c) switch, followed by a space, and pressing the tab key.
function cecho -d 'echoes given text in colors'
  for arg in $argv
    switch $arg
      case '-c' '--color'
        set expectColor 'yes'
      case '*'
        if test -n "$expectColor"
          set_color $arg
          set expectColor # no value
        else
          echo -n $arg' ' # includes space between words
        end
    end
  end
  echo # for newline
end

# Configure completions for the color switch.
set colors 'black blue cyan green magenta red white yellow'
complete --command cecho -f -r -s c -l color -a "$colors"
For more on this topic, see https://fishshell.com/docs/current/commands.html#complete and https://fishshell.com/docs/current/index.html#completion-own. This describes alternative locations for defining tab completions.

Extras

Absolute Path

To get the absolute path of a relative path, use the realpath command. For example, when in the directory /foo/bar/baz, entering realpath ../../qux outputs /foo/qux.

Dates

The date command outputs the current date. The default format looks like "Sat Aug 26 16:54:52 CDT 2017". To use a different format, specify it using a format string as defined by the strftime system library function. For details, enter man strftime.
For example, to format the date as "Saturday August 26, 2017" enter date '+%A %B %d, %G'. To format it as "08/26/2017" enter date '+%m/%d/%G'.

Opening Files

To open files using their default applications, enter open filePath1 filePath2 .... For example, open demo.html opens the file in the default web browser.

Reading From stdin

The command read var waits for the user to enter something, terminated by the enter key, and sets var to what was entered. Only a single line can be entered.
The read command accepts many switches. These include the same switches as the set command for choosing the scope of the variable that is set. It also accepts the following switches (and more not listed here):
The read command can set more than one variable. Each is set to the word at the corresponding position. The last variable is set to a string that is a space-separated list of all the remaining words. For example, if read first second is run and the user enters "foo bar baz", first is set to "foo" and second is set to "bar baz".
To read multiple lines from the user until they press enter on an empty line:
while read line
  if test -z $line; break; end
  echo you entered $line
end

Processes

The % character followed by text expands to a process id. %self expands to the current process id. %jobNumber expands to the process id of the specified job. %processName expands to the process id of the specified process name. For example, to see the process ids of all fish processes, enter echo %fish. If only a process name prefix is specified, the ids of all processes with names beginning with that prefix are output.

Jobs

To run a command in background, add & to end of the command. This works for commands, but not for functions. For a discussion on the reason why, see https://github.com/fish-shell/fish-shell/issues/238.
To suspend a currently running command, press ctrl-z. This does not work for functions, likely for the same reason that functions cannot be run in the background.
To continue running a suspended command in background enter bg.
To get a list of background jobs and their job numbers, enter jobs.
To bring the job most recently placed in the background to the foreground, enter fg. To bring a specific background job to the foreground, enter fg %jobNumber or fg processId.
If you attempt to exit from a shell that has background jobs, fish will warn about this and not exit. If you attempt to exit again, fish will kill all the background jobs and exit.

Random Numbers and Options

The random command outputs an integer in a range which defaults to [0, 32767]. It takes up to three arguments and typically at least two are specified. Their meaning depends on the number of arguments given. If only one is specified, it is a seed. If two are specified, they are the start and end of the range. For example, for a dice roll use random 1 6. If three are specified, they are the start, step, and end of the range where only numbers that are increments of step from start but not greater than end will be generated. For example, to generate a random number between 0 and 100 inclusive that is an increment of 5, use random 0 5 100.
To select a random item from a list of options, use random choice option1 option2 .... For example, to choose a random color from a list of options:
set colors red orange yellow green blue purple
set color (random choice $colors)

Example in Node, bash, and fish

For comparison purposes, this section presents the same utility, lscolor, implemented in Node.js, bash, and fish. It lists all files in the current directory on separate lines. However, files with certain extensions are output in specific colors. There are default colors for files with the extensions html, css, js, and scss. These can be overridden and colors for other file extensions can be specified with arguments of the form extension=color . By default, files with an unspecified extension are output in the default text color. The default color can be overridden with default=color. For example, lscolor fish=magenta js=cyan default=yellow produces the following output:
lscolors output
Here is a Node.js version:
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');

const colorMap = {
  'css': 'green',
  'html': 'blue',
  'js': 'red',
  'scss': 'green'
};

// Use colors specified in command-line arguments.
args = process.argv;
args.shift(); // path to node executable
args.shift(); // path to this file
for (const arg of args) {
  const [extension, color] = arg.split('=');
  if (!color) throw new Error(`invalid argument "${arg}"`);
  colorMap[extension] = color;
}

fs.readdir('.', (err, files) => {
  if (err) throw new Error(err);
  for (const file of files) {
    const extension = path.extname(file).substring(1);
    const color = colorMap[extension];
    if (color) {
      console.log(chalk.keyword(color)(file));
    } else {
      console.log(file);
    }
  }
});
Here is a bash version:
#!/bin/bash

# Color ANSI escape sequences
black='\033[0;30m'
red='\033[0;31m'
green='\033[0;32m'
orange='\033[0;33m'
blue='\033[0;34m'
purple='\033[0;35m' # magenta
cyan='\033[0;36m'
yellow='\033[1;33m'
white='\033[1;37m'
normal='\033[0m'

# Default colors
cssColor=$green
defaultColor=$white
htmlColor=$blue
jsColor=$red
scssColor=$green

usage() {
  echo invalid argument $0
  exit 1
}

# Use colors specified in switches.
for arg in "$@"
do
  extension=${arg%%=*}
  [[ -z $extension ]] && usage
  color=${arg##*=}
  [[ -z $color ]] && usage
  varName="${extension}Color"
  eval $varName=\${!color}
done

for file in *
do
  extension=${file##*.}
  if [[ $extension == $file ]]
  then # no extension found
    color=$defaultColor
  else
    colorVar=${extension}Color
    color=${!colorVar}
    if [[ -z $color ]]
    then
      color=$defaultColor
    fi
  fi
  echo -e "$color$file"
done
Here is a fish version:
function lscolor
  # Default colors
  set cssColor green
  set defaultColor normal
  set htmlColor blue
  set jsColor red
  set scssColor green

  # Use colors specified in switches.
  for arg in $argv
    set pieces (string split = $arg)
    if test (count $pieces) -lt 2
      echo 'invalid argument "'$arg'"'
      return 1
    end
    set extension $pieces[1]
    set color $pieces[2]
    set varName $extension'Color'
    set $varName $color
  end

  for file in (ls -d *)
    set pieces (string split . $file)
    if test (count $pieces) -ge 2
      set extension $pieces[-1] # last piece
      set varName $extension'Color'
      set color $$varName
      if test -z "$color"; set color $defaultColor; end
    else
      set color $defaultColor
    end
    set_color $color
    echo $file
  end
end

Summary

This article covered everything you need to know to make effective use of the fish shell. Using fish has been a game changer for me. I believe that by adopting fish you will make yourself more productive and happier at the command line!
Please send feedback on this article to mark@objectcomputing.com.

Acknowledgments

Many people helped me write this article by answering questions about fish or reviewing the article. In particularly I want to thank Kurtis Rader, Charles Sharp (Object Computing, Inc.), and Jason Schindler (Object Computing, Inc.).
For more articles like these, visit https://objectcomputing.com/resources/publications/sett/.