- Shebang line at the top of your scripts
- How to set variables
- How to write for loops
- How to write if statements
- How to split strings by a delimiter
- How to perform string comparisons
- How to write switch statements for strings
- How to execute strings
- How to write functions
- How to handle file and folder paths for dependencies
- How to write multi line strings to files
- How to create colorized echo output
- How to get user input
- How to use sed
- How to use xargs
- How to use cut to split strings
- How to calculate how long the script took to run
Learn how to write fish shell scripts by example.
Shebang line at the top of your scripts #
To be able to run fish scripts from your terminal, you have to do two things.
- Add the following shebang line to the top of your script file:
- Mark the file as executable using the following command:
chmod +x <YOUR_FISH_SCRIPT_FILENAME>.
How to set variables #
Keep in mind that all types of values that can be assigned to variables in fish are strings. There is no such thing as boolean or integer or float, etc. Here’s a simple example of assigning a value to a variable. Here is more information on stackoverflow on this.
set MY_VAR "some value"
One of the most useful things that you can do is save the output of a command that you run in the shell in a variable. This is useful when you are testing to see if some program or command returned some values that mean that you should perform some other command (using string comparisons, if statements, and switch statements). Here’s are 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 scopes: local, global, global-export #
There are times when you have to export variables to child processes and also times when you have to
export variables to global scope. There are also times when you want this variable to be limited to
local scope of the function you are writing. The fish documentation on the
set function has more information on this.
- To limit variables to local scope of the function (even if there is a global variable of the same
set -l. This type of variable is not available to the entire fish shell. An example of this is a local variable that is used to hold some value just for the scope of a function, such as
set -l fname (realpath .)
- Export variable using
set -x(this is only available inside the current fish shell). An example of this is setting the
DISPLAYenvironment variable for X11 session in a fish function that is running in a
- Export variable globally using
set -gx(this is available to any programs in your OS, not just the currently running fish shell process). An example of this is setting the
JAVA_HOMEenvironment variable for all programs running on the machine.
Here’s an example of appending values to a variable. By default fish variables are lists.
set MY_VAR $MY_VAR "another value"
This is how you can create lists.
set MY_LIST "value1" "value2" "value3"
Storing return values from running a command #
Here’s an example of storing value returned from the execution of a command to 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
[n] operator, where
n=1 for the first element (not 0 index). Here’s an example. And negative numbers access elements
from the end.
set LIST one two three echo $LIST # one echo $LIST # two echo $LIST # three echo $LIST[-1] # This is the same element as above
You can also use ranges from the variable / list, continuing the example above.
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 for loops #
Since variables contain lists by default, it is very easy to iterate thru them. Here’s 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 simplify the code above by putting all the
set commands in a single line like this.
set FOLDERS bin .atom "my foldername" for FOLDER in $FOLDERS echo "item: $FOLDER" end
You can also put the entire for statement in a single line like this.
set FOLDERS bin .atom "my foldername" for FOLDER in $FOLDERS ; echo "item: $FOLDER" ; end
How to write if statements #
The key to writing if statements is using the
test command to evaluate some expression to a
boolean. This can be string comparisons or even testing the existence of files and folders. Here are
some examples. You can also use the
not operator to prefix the test to check for the inverse
Commonly used conditions #
Checking the size of an array.
$argv contains the list of arguments passed to a script from the
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 in variable.
if test $hostname = "mymachine" echo "hostname is mymachine" end
Checking for file existence.
if test -e "somefile" echo "somefile exists" end
Checking for folder existence.
if test -d "somefolder" echo "somefolder exists" end
Checking for file wildcard existence is a little different than both file and folder checks. The reason for this is how fish handles wildcards - they are expanded by fish before it performs whatever command on them.
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
Here’s an example of how to use the
not operator in the previous example.
if not test -d "somefolder" echo "somefolder does not exist" end
Program, script, or function exit code #
The idea with exit codes is that your function or entire fish script could be used by some other program that understands exit codes. In other words there could be an if statement that is going to use the exit code to determine some condition. This is a very common pattern that is used with other command line programs. Exit codes are different than return values from a function.
Here’s an example of using the exit code of some
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
Here’s an example of how to test 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, just after a command is executed. Here’s
more info on this.
When you are writing functions you can use the following keyword to exit functions or loops:
return. This may be followed by a number. So here’s what it means.
return 0- This means that the function exited normally.
return 1or some other number > 0 - This means that the function had some problem.
You can exit the fish shell itself using
exit. And the integer exit codes have the same meaning as
Difference between set -q and test -z #
There is a subtle difference between using
set -q and
test -z in if statements when checking to
see if a variable is empty.
- In the case of
test -zmake sure to wrap the variable in quotes, since it might just break in some edge cases if it isn’t wrapped in quotes.
- However, you can use
set -qto test if a variable has been set without wrapping it in quotes.
Here’s 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
Multiple conditions with operators: and, or #
If you want to combine multiple conditions into a single statement, then you can use
operators. Also if you want to check the inverse of a condition, you can use
!. Here’s an example
of a function that checks for 2 arguments to be passed via the command line. Here’s the logic we
- If both the arguments are missing, then usage information should be displayed to the CLI, and perform an early return.
- If either one of the arguments is missing, then display a prompt stating that one of the arguments is missing, and perform an early return.
function requires-two-arguments # No arguments are passed. if set -q "$argv" echo "Usage: requires-two-arguments arg1 arg2" return 1 end # Only 1 argument is passed. if test -z "$argv"; or test -z "$argv" echo "arg1 or arg2 can not be empty" return 1 end echo "Thank you, got 1) $argv and 2) $argv" end
Here are some notes on the code.
- What does the
set -q $variablefunction do? It returns true if
- Instead of
set -q, if you wanted to use
testfunction in order to determine if a variable is empty, you can use:
if test -z "$variable".
if test ! -n "$variable"or
if not test -n "$variable".
- If you wanted to replace the
orcheck above w/
test, this is what it would look like
if test -z "$argv"; or test -z "$argv".
- When you use
andoperators that you have to terminate the condition expression w/ a
- Make sure to wrap the variable in empty quotes. If an empty string is contained inside the variable, then without these quotes, the statements will cause errors.
Here’s another example of this to test if
$variable is empty or not.
if test -z "$variable" ; echo "empty" ; else ; echo "non-empty" ; end
Here’s another example of this to test if
$variable contains a string or not.
if test -n "$variable" ; echo "non-empty" ; else ; echo "empty" ; end
Another common operator: not #
Here’s an example of using the
not operator to test whether a string contains a string fragment or
if not string match -q "*md" $argv echo "The argument passed does not end in md" else echo "The argument passed ends in md" end
- 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 by a delimiter #
There are situations when you want to take the output of a command, which is a string, and then
split it by some delimiter, to use just a portion of the output string. An example of getting the
SHA checksum of a given file. The command
shasum <filename> produces something like
df..d8 <filename>. Let’s say that we just wanted the first portion of this string (the SHA),
knowing that the delimiter is two space characters, we can do the following to get just the checksum
portion and store it in
$CHECKSUM. Here’s more info on the
string split command.
set CHECKSUM_ARRAY_STRING (shasum $FILENAME) set CHECKSUM_ARRAY (string split " " $SOURCE_CHECKSUM_ARRAY) set CHECKSUM $CHECKSUM_ARRAY
How to perform string comparisons #
In order to test substring matches in strings you can use the
string match command. Here is more
information on the command:
Here’s an example of this in action. Note the use of
--quiet which does not echo the
output of the string if the match condition was met (succeeded).
if string match -q "*myname*" $hostname echo "$hostname contains myname" else echo "$hostname does not contain myname" end
Here’s an example of checking for an exact string match.
if test $hostname = "machine-name" echo "Exact match" else echo "Not exact match" end
Here’s an example of testing whether a string is empty or not.
if set -q $my_variable echo "my_variable is empty" end
Here’s a sophisticated example that tests to see if the packages
installed. If they are then
jekyll gets run, and if not, then these packages are installed.
# 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 switch statements for strings #
In order to create switch statements for strings, the
test command is used here as well (just like
it was for if statements). The
case statements need to match
substrings, which can be expressed using a combination of wildcard chars and the substring you want
to match. Here’s an example.
switch $hostname case "*substring1*" echo "Matches $hostname containing substring1" case "*substring2*" echo "Matches $hostname containing substring2" end
You can combine this w/ if statements as well, and end up w/ something 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 execute strings #
The safest way to execute strings that are generated in the script is to use the following pattern.
echo "ls \ -la" | sh
This not only makes it easier to debug, but also avoids strange errors when doing multi-line breaks
How to write functions #
A fish function is just a list of commands that may optionally take arguments. These arguments are just passed in as a list (since all variables in fish are lists).
Here’s an example.
function say_hi echo "Hi $argv" end say_hi say_hi everbody! say_hi you and you and you
Once you have written a function you can see what it is by using
type say_hi will show
you the function that you just created above.
Pass arguments to a function #
In addition to using
$argv to figure out what parameters were passed to a function, you can
provide a list of named parameters that a function expects. Here is more information on this
from the official docs.
Some key things to keep in mind:
- Parameter names can not have
-characters in them, so use
- Do not use the
)to pass arguments to a function, simply pass the arguments in a single line w/ spaces.
Here’s an example.
function testFunction -a param1 param2 echo "arg1 = $param1" echo "arg2 = $param2" end testFunction A B
Here’s another example that tests for the existence of a certain number of arguments that are passed to a function.
# 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
Return values from a function #
You might want to return a value from a function (typically just a string). You can also return many
strings delimited by new lines. Regardless, the mechanism for doing this is the same. You simply
have to use
echo to dump the return value(s) to stdout.
Here’s 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 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 file and folder paths for dependencies #
As your scripts become more complex, you might need to handle loading multiple scripts. In this case
you can just pull other scripts in from your current script by using
However fish looks for this
my-script.fish file in the current directory, from which you started
executing the script. And this current directory might not match where you need to load this
dependency from. This can happen if your main script is on the
$PATH but the dependencies are not.
In this case, you can do something like the following in your main script.
set MY_FOLDER_PATH (dirname (status --current-filename)) source $MY_FOLDER_PATH/my-script.fish
So what this snippet actually does is get the folder in which the main script is running, and stores
MY_FOLDER_PATH and then it become possible for any dependencies to be loaded using the
source command. There is one limitation to this approach, which is that the path stored in
MY_FOLDER_PATH is relative to the directory from which the main script is actually executed. This
is a subtle detail that you may not care about, unless you need to have absolute path names. In this
case you can do the following.
set MY_FOLDER_PATH (realpath (dirname (status --current-filename))) source $MY_FOLDER_PATH/my-script.fish
realpath gives you the fully
qualified path name for your folder for the uses cases where you need this capability.
How to write multi line strings to files #
There are many situations where you need to write strings and multi line strings to new or existing files in your scripts.
Here’s an example of writing single strings 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’s 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 colorized echo output #
set_color function allows fish to
colorize and format text content that is printed to
echo. This is great when
creating text output that needs to have different foreground, background colors, and bold, italic,
or underlined output. There are many ways to use this command, and here are two examples of how to
use it (inline of
echo statements, and just by itself).
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
set_color normalhas to be called to reset whatever formatting options were set in previous statements.
set_color -udoes underline, and
set_color -odoes bold.
How to get user input #
There are situations where you need to ask a user for confirmation before performing some
potentially destructive operation or you might need user input for some argument to a function (that
isn’t passed via the command line). In these cases it is possible to get user input from the user by
stdin using the
The following function simply returns a
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
And here is an example of using the
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 fragments of files that are not needed, especially when
xargs is used
to pipe the result of
Here’s an example that removes
./ from the start of each file that’s found.
echo "./.Android" | sed 's/.\///g'
Here’s a more complex example of using
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 piping the output of some commands as arguments for more commands.
Here’s a simple example:
ls | xargs echo "folders: ".
- Which produces this:
folders: idea-http-proxy-settings images tmp.
- Note how the arguments are concatenated in the output.
Here’s a slightly different example using
-I % which allows arguments to be placed anywhere (not
just at the end).
ls | xargs -I % echo "folder: %"
Which produces this output:
folder: idea-http-proxy-settings folder: images folder: tmp
Note how the arguments are each in a separate line.
How to use cut to split strings #
Let’s say you have a string
"token1:token2" and you want to split the string and only keep the
first part of it. This can be done using the following cut command.
echo "token1:token2" | cut -d ':' -f 1
-d ':'- this splits the string by the
-f 1- this keeps the first field in the tokenized string
Here’s a real example of finding all the HTML files in
~/github/developerlife.com with the string
"fonts.googleapis" in it and then opening them up in
cd ~/github/developerlife.com echo \ "find . -name '*html' | \ xargs grep fonts.googleapis | \ cut -d ':' -f 1 | \ xargs subl" \ | sh
How to calculate how long the script took to run #
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