This article is translated from FISH SHELL SCRIPTING MANUAL. Due to my limited knowledge, there may be errors or awkward phrasing, and I hope readers will kindly provide feedback in the comments.
Learn how to write fish shell scripts through examples.
Shebang Line at the Top of the Script#
To run fish scripts in the terminal, you need to do two things:
- Add the following shebang line at the top of the script:
#!/usr/bin/env fish
. - Mark the file as executable using the following command:
chmod +x <your fish script filename>
.
How to Set Variables#
Note that all types of values assigned to variables in fish are strings; there are no concepts of boolean values, integers, or floating-point numbers. Here is a simple example of assigning a value to a variable. Here is more information about this.
set MY_VAR "some value"
One of the most useful things you can do is store the output of a command executed in the shell into a variable. This is useful when you are testing whether a program or command returns a value that requires you to execute other commands (using string comparisons, if statements, and switch statements). Here are some examples of doing this.
set CONFIG_FILE_DIFF_OUTPUT (diff ~/Downloads/config.fish ~/.config/fish/config.fish)
set GIT_STATUS_OUTPUT (git status --porcelain)
Variable Scope: local, global, global-export#
Sometimes you need to export a variable to a subprocess, sometimes you need to export a variable to the global scope. There are also times when you want to restrict a variable to the local scope of the function you are writing. The set
function section in the fish documentation has more information about this.
- To restrict a variable to the local scope of a function (even if there is a global variable with the same name), use
set -l
. This type of variable is not available throughout the fish shell. An example is a local variable used only to store a value within the function's scope, such asset -l fname (realpath)
. - Use
set -x
to export a variable (available only in the current fish shell). For example, to set theDISPLAY
environment variable for an X11 session in a fish function running in acrontab
headless environment. - Use
set -gx
to globally export a variable (available to any program in the operating system, not just the currently running fish shell process). For example, to set theJAVA_HOME
environment variable for all programs running on the machine.
Lists#
Here is an example of appending values to a variable. By default, fish variables are lists.
set MY_VAR $MY_VAR "another value"
This is how to create a list.
set MY_LIST "value1" "value2" "value3"
Storing the Return Value of a Command#
Here is an example of storing the value returned by executing a command into a variable.
set OUR_VAR (math 1+2)
set OUR_VAR (date +%s)
set OUR_VAR (math $OUR_VAR / 60)
Since all fish variables are lists, you can access individual elements using the [n]
operator, where n=1
represents the first element (not 0). Here is an example. Negative numbers indicate accessing elements from the end.
set LIST one two three
echo $LIST[1] # one
echo $LIST[2] # two
echo $LIST[3] # three
echo $LIST[-1] # equivalent to the previous line
Ranges#
You can use ranges with variables/lists, continuing from the previous example.
set LIST one two three
echo $LIST[1..2] # one two
echo $LIST[2..3] # two three
echo $LIST[-1..2] # three two
How to Write a For Loop#
Since variables by default contain lists, it is easy to iterate over them. Here is an example:
set FOLDERS bin
set FOLDERS $FOLDERS .atom
set FOLDERS $FOLDERS "my foldername"
for FOLDER in $FOLDERS
echo "item: $FOLDER"
end
You can also place the set
command on the same line to simplify the above code, like this:
set FOLDERS bin .atom "my foldername"
for FOLDER in $FOLDERS
echo "item: $FOLDER"
end
You can also place the entire for statement on a single line, like this:
set FOLDERS bin .atom "my foldername"
for FOLDER in $FOLDERS ; echo "item: $FOLDER" ; end
How to Write an If Statement#
The key to writing an if statement is to use the test
command to evaluate a boolean value for an expression. This can be string comparisons or even testing for the existence of files or folders. Here are some examples. You can also use the not
operator as a prefix to test
to check for the inverse condition.
Common Conditions#
Check the size of an array. $argv
contains the list of arguments passed to the script from the command line.
if test (count $argv) -lt 2
echo "Usage: my-script <arg1> <arg2>"
echo "Eg: <arg1> can be 'foo', <arg2> can be 'bar'"
else
echo "👋 Do something with $arg1 $arg2"
end
String comparison of variables.
if test $hostname = "mymachine"
echo "hostname is mymachine"
end
Check if a file exists:
if test -e "somefile"
echo "somefile exists"
end
Check if a folder exists:
if test -d "somefolder"
echo "somefolder exists"
end
Checking for the existence of file wildcards is slightly different from checking for files and folders. This is due to how fish handles wildcards—fish expands them before executing any commands.
set -l files ~/Downloads/*.mp4 # This wildcard expression is expanded to include the actual files
if test (count $files) -gt 0
mv ~/Downloads/*.mp4 ~/Videos/
echo "📹 Moved '$files' to ~/Videos/"
else
echo "⛔ No mp4 files found in Downloads"
end
An example using the not
operator in the above example:
if not test -d "somefolder"
echo "somefolder does not exist"
end
Exit Codes of Programs, Scripts, or Functions#
The idea of using exit codes is that your function or the entire fish script can be used by other programs that understand exit codes. In other words, there may be if statements that use exit codes to determine a condition. This is a very common pattern when used with other command-line programs. Exit codes are different from the return values of functions.
Here is an example using the exit code of the git
command:
if (git pull -f --rebase)
echo "git pull with rebase worked without any issues"
else
echo "Something went wrong that requires manual intervention, like a merge conflict"
end
An example testing whether a command executed without errors:
if sudo umount /media/user/mountpoint
echo "Successfully unmounted /media/user/mountpoint"
end
You can also check the value of the $status
variable. Fish stores the return value in this variable after executing a command. Here is more information about this.
When writing functions, you can use the following keywords to exit a function or loop: return
. A return
may be followed by a number, which means:
return
orreturn 0
- indicates normal exit from the function.return 1
or any other number greater than 0 - indicates that something went wrong in the function.
You can use exit
to exit the fish shell itself. The meaning of integer exit codes is the same as described above.
Differences Between set -q and test -z#
There are some subtle differences when using set -q
and test -z
in if statements to check if a variable is empty.
- When using
test -z
, make sure to wrap the variable in quotes, as the command may fail in some edge cases if the variable is not quoted. - However, you can use
set -q
to test if a variable is set without needing to wrap it in quotes.
Here is an example:
set GIT_STATUS (git status --porcelain)
if set -q $GIT_STATUS ; echo "No changes in repo" ; end
if test -z "$GIT_STATUS" ; echo "No changes in repo" ; end
Multi-Condition Checks with and, or Operators#
If you want to combine multiple conditions into a single statement, you can use the or
and and
operators. If you want to check the inverse of a condition, you can use !
. Here is an example of a function that checks for two parameters passed from the command line. This is the logic we described:
- If both parameters are missing, it should print help information to the command line and return early.
- If one of the parameters is missing, it should display a prompt indicating that one of the parameters is missing and return early.
function requires-two-arguments
# No parameters passed
if set -q "$argv"
echo "Usage: requires-two-arguments arg1 arg2"
return 1
end
# Only one parameter passed
if test -z "$argv[1]"; or test -z "$argv[2]"
echo "arg1 or arg2 cannot be empty"
return 1
end
echo "Thank you, got 1) $argv[1] and 2) $argv[2]"
end
Here are some comments on the code:
-
What does the
set -q $variable
function do? It returns true when$variable
is empty. -
If you want to replace
set -q
with thetest
function to check if a variable exists, you can use:if test -z "$variable"
if test ! -n "$variable"
orif not test -n "$variable"
-
If you want to replace the
or
check above withtest
, it would look like:if test -z "$argv[1]"; or test -z "$argv[2]"
.Note: The above code is already
test -z
, so this statement is somewhat redundant.It is speculated that the original code may have been
set -q "$argv[1]"; or set -q "$argv[2]"
, which is why this expression exists. -
When using
or
andand
operators, you must use;
to end the conditional expression. -
Make sure to wrap variables in empty quotes. If a variable contains an empty string, then without these quotes, the statement will result in an error.
Here is another example for testing if $variable
is empty:
if test -z "$variable" ; echo "empty" ; else ; echo "non-empty" ; end
Here is another example for testing if $variable
contains a string:
if test -n "$variable" ; echo "non-empty" ; else ; echo "empty" ; end
Another Common Operator: not#
Here is an example using the not
operator to test if a string contains a substring:
if not string match -q "*md" $argv[1]
echo "The argument passed does not end in md"
else
echo "The argument passed ends in md"
end
References#
- test command
- set command
- if command
- stackoverflow answer: how to check if fish variable is empty
- stackoverflow answer: how to put multiple conditions in fish if statement
How to Split Strings Using Delimiters#
In some cases, you may want to get the output of a command (a string) and then split it using a delimiter to only use part of the output string. For example, to get the SHA checksum of a given file. The command shasum <filename>
produces output like df..d8 <filename>
. Assuming we only want the first part of this string (SHA), knowing that the delimiter is two space characters, we can do the following to get the checksum part and store it in $checksum
. Here is more information about the string split
command.
set CHECKSUM_ARRAY_STRING (shasum $FILENAME)
set CHECKSUM_ARRAY (string split " " $SOURCE_CHECKSUM_ARRAY)
set CHECKSUM $CHECKSUM_ARRAY[1]
How to Perform String Comparisons#
To test for substring matches in a string, you can use the string match
command. Here is more information about this command:
Here is a practical example. Note that when using -q
or --quiet
, if the match condition is satisfied (successful), it will not print the output of the string.
if string match -q "*myname*" $hostname
echo "$hostname contains myname"
else
echo "$hostname does not contain myname"
end
Here is an example of exact string matching:
if test $hostname = "machine-name"
echo "Exact match"
else
echo "Not exact match"
end
An example of testing if a string is empty:
if set -q $my_variable
echo "my_variable is empty"
end
Here is a more complex example that tests whether the ruby-dev
and ruby-bundler
packages are installed. If they are, it runs jekyll
; if not, it installs these packages.
# Return "true" if $packageName is installed, and "false" otherwise.
# Use it in an if statement like this:
#
# if string match -q "false" (isPackageInstalled my-package-name)
# echo "my-package-name is not installed"
# else
# echo "my-package-name is installed"
# end
function isPackageInstalled -a packageName
set packageIsInstalled (dpkg -l "$packageName")
if test -z "$packageIsInstalled"
set packageIsInstalled false
else
set packageIsInstalled true
end
echo $packageIsInstalled
end
# More info to find if a package is installed: https://askubuntu.com/a/823630/872482
if test (uname) = "Linux"
echo "🐒isPackageInstalled does-not-exist:" (isPackageInstalled does-not-exist)
if string match -q "false" (isPackageInstalled ruby-dev) ;
or string match -q "false" (isPackageInstalled ruby-bundler)
# Install ruby
echo "ruby-bundler or ruby-dev are not installed; installing now..."
echo sudo apt install -y ruby-bundler ruby-dev
else
bundle install
bundle update
bundle exec jekyll serve
end
end
How to Write a Switch Statement for Strings#
To create a switch statement for strings, the test
command is also used (just like for if statements). The case
statements need to match substrings, which can be represented using wildcards and combinations of the substrings you want to match. Here is an example.
switch $hostname
case "*substring1*"
echo "Matches $hostname containing substring1"
case "*substring2*"
echo "Matches $hostname containing substring2"
end
You can also mix these with if statements, and it would look like this:
if test (uname) = "Darwin"
echo "Machine is running macOS"
switch $hostname
case "*MacBook-Pro*"
echo "hostname has MacBook-Pro in it"
case "*MacBook-Air*"
echo "hostname has MacBook-Air in it"
end
else
echo "Machine is not running macOS"
end
How to Safely Execute Strings#
The safest way to execute strings generated in a script is to use the following pattern.
echo "ls \
-la" | sh
This not only makes debugging easier but also avoids strange errors when using \
for multi-line breaks.
How to Write Functions#
A fish function is simply a list of commands that optionally accept parameters. These parameters are passed as a list (since all fish variables are lists).
Here is an example:
function say_hi
echo "Hi $argv"
end
say_hi
say_hi everybody!
say_hi you and you and you
After writing a function, you can check what it is by using type
. For example: type say_hi
will show you the function you just created.
Passing Parameters to Functions#
In addition to using $argv
to find out the parameters passed to a function, you can provide a named parameter list that the function expects. Here is more information in the official documentation.
Some key points to remember:
- Parameter names cannot contain
-
characters; use_
instead. - Do not use
(
and)
to pass parameters to functions; just pass them in a single line with spaces.
Here is an example:
function testFunction -a param1 param2
echo "arg1 = $param1"
echo "arg2 = $param2"
end
testFunction A B
Here is another example testing whether the parameters passed to the function exist:
# Note parameter names can't have dashes in them, only underscores.
function my-function -a extension search_term
if test (count $argv) -lt 2
echo "Usage: my-function <extension> <search_term>"
echo "Eg: <extension> can be 'fish', <search_term> can be 'test'"
else
echo "✋ Do something with $extension $search_term"
end
end
Returning Values from Functions#
You may need to return values from a function (usually just a string), and you can return many strings separated by new lines. In any case, the mechanism to achieve this is the same. Just use echo
to dump the return value to stdout.
Here is an example:
function getSHAForFilePath -a filepath
set NULL_VALUE ""
# No $filepath provided, or $filepath does not exist -> early return w/ $NULL_VALUE.
if set -q $filepath; or not test -e $filepath
echo $NULL_VALUE
return 0
else
set SHASUM_ARRAY_STRING (shasum $filepath)
set SHASUM_ARRAY (string split " " $SHASUM_ARRAY_STRING)
echo $SHASUM_ARRAY[1]
end
end
function testTheFunction
echo (getSHAForFilePath ~/local-backup-restore/does-not-exist.fish)
echo (getSHAForFilePath)
set mySha (getSHAForFilePath ~/local-backup-restore/test.fish)
echo $mySha
end
testTheFunction
How to Handle Dependency File and Folder Paths#
As scripts become more complex, you may need to deal with loading multiple scripts. In this case, you can use source my-script.sh
to import other scripts from the current script. However, fish looks for the my-script.sh
file in the current directory, which is the directory where you started executing the script, and that directory may not match the location where you need to load this dependency. This happens if your main script is in $PATH
and the dependency is not. In this case, you can do the following in the main script:
set MY_FOLDER_PATH (dirname (status --current-filename))
source $MY_FOLDER_PATH/my-script.fish
What this code does is get the folder where the main script is running and store it in MY_FOLDER_PATH
, then you can use the source
command to load any dependencies. This method has a limitation in that MY_FOLDER_PATH
stores a path relative to the execution location of the main script. This is a subtle detail you may not care about unless you need an absolute pathname. In that case, you can do the following:
set MY_FOLDER_PATH (realpath (dirname (status --current-filename)))
source $MY_FOLDER_PATH/my-script.fish
Using realpath
provides the absolute path of the folder for use when needed.
How to Write Single-Line Strings to Files#
In many cases, you need to write strings and multi-line strings to new or existing files in a script.
Here is an example of writing a single-line string to a file:
# echo "echo 'ClientAliveInterval 60' >> recurring-tasks.log" | xargs -I% sudo sh -c %
set linesToAdd "TCPKeepAlive yes" "ClientAliveInterval 60" "ClientAliveCountMax 120"
for line in $linesToAdd
set command "echo '$line' >> /etc/ssh/sshd_config"
executeString "$command | xargs -I% sudo sh -c %"
end
Here is an example of writing multi-line strings to a file:
# More info on writing multiline strings: https://stackoverflow.com/a/35628657/2085356
function _workflowWriteEmptyMarkdownContentToFile --argument datestr filename
echo > $filename "\
---
Title: About $filename
Date: $datestr
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Your heading
"
end
How to Create Colored Echo Output#
The set_color
function allows fish to color and format the text content printed to stdout
using echo
. This is useful when creating text output that requires different foreground and background colors, as well as bold, italic, or underlined output. There are many ways to use this command; here are two examples (inline with echo
statements and used separately):
function myFunction
if test (count $argv) -lt 2
set -l currentFunctionName (status function)
echo "Usage: "(set_color -o -u)"$currentFunctionName"(set_color normal)\
(set_color blue)" <arg1> "\
(set_color yellow)"<arg2>"(set_color normal)
set_color blue
echo "- <arg1>: Something about arg1."
set_color yellow
echo "- <arg2>: Something about arg2"
set_color normal
return 1
end
end
Note:
- You must call
set_color normal
to reset the formatting options set in previous statements. set_color -u
indicates underline, andset_color -o
indicates bold.
How to Get User Input#
In some cases, you may need to ask the user for confirmation before performing some potentially destructive operations or may need user input for certain parameters of a function (that are not passed via the command line). In these cases, you can read stdin
to get user input using the read
function.
The following function simply returns 0 for 'Y'/'y' and 1 for 'N'/'n'.
# More info on prompting a user for confirmation using fish read function: https://stackoverflow.com/a/16673745/2085356
# More info about fish `read` function: https://fishshell.com/docs/current/cmds/read.html
function _promptUserForConfirmation -a message
if not test -z "$message"
echo (set_color brmagenta)"🤔 $message?"
end
while true
# read -l -P '🔴 Do you want to continue? [y/N] ' confirm
read -l -p "set_color brcyan; echo '🔴 Do you want to continue? [y/N] ' ; set_color normal; echo '> '" confirm
switch $confirm
case Y y
return 0
case '' N n
return 1
end
end
end
Here is an example of using the _promptUserForConfirmation
function:
if _promptUserForConfirmation "Delete branch $featureBranchName"
git branch -D $featureBranchName
echo "👍 Successfully deleted $featureBranchName"
else
echo "⛔ Did not delete $featureBranchName"
end
How to Use sed#
This is useful for removing unwanted file snippets, especially when piping find
results with xargs
.
Here is an example of removing './' from the beginning of each file:
echo "./.Android" | sed 's/.\///g'
A more complex example using sed
, find
, and xargs
together:
set folder .Android*
find ~ -maxdepth 1 -name $folder | sed 's/.\///g' | \
xargs -I % echo "cleaned up name: %"
How to Use xargs#
This is useful for passing the output of some commands as arguments to more commands.
Here is a simple example: ls | xargs echo "folders: "
.
- The output produced is:
folders: idea-http-proxy-settings images tmp
- Note how the parameters are concatenated in the output.
Here is a slightly different example, using -I %
to place the parameters anywhere (not just at the end).
ls | xargs -I % echo "folder: %"
Produces the following output:
folder: idea-http-proxy-settings
folder: images
folder: tmp
Note that each parameter is on a separate line.
How to Use cut to Split Strings#
Suppose you have a string "token1:token2"
and you want to split this string and keep only its first part; this can be done using the following cut
command.
echo "token1:token2" | cut -d ':' -f 1
-d ':'
- splits the string by the:
delimiter-f 1
- keeps the first field of the tokenized string
Here is an example of finding all HTML files containing fonts.googleapis
in ~/github/developerlife.com
and opening them with subl
:
cd ~/github/developerlife.com
echo \
"find . -name '*html' | \
xargs grep fonts.googleapis | \
cut -d ':' -f 1 | \
xargs subl" \
| sh
How to Measure the Time Taken to Run a Script#
function timed -d Pass the program or function that you want to execute as an argument
set START_TS (date +%s)
# This is where your code would go.
$argv
sleep 5
set END_TS (date +%s)
set RUNTIME (math $END_TS - $START_TS)
set RUNTIME (math $RUNTIME / 60)
echo "⏲ Total runtime: $RUNTIME min ⏲"
end