fish shell scripting manual
- 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 fzf for interactive selection
- 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
- How to debug fish scripts
- Fish scripting best practices
- Always quote variables
- Use meaningful variable names
- Validate inputs and handle errors
- Use local variables in functions
- Prefer fish builtins over external commands
- Use command substitution appropriately
- Use functions for reusable code
- Handle signals gracefully
- Use consistent exit codes
- Document your functions
- Common fish scripting pitfalls
- Forgetting that all variables are lists
- Using bash syntax in fish
- Mixing up
set -q
(exists) vstest -z
(empty) - Not quoting variables with spaces
- Incorrect variable expansion in loops
- Forgetting command substitution captures stdout only
- Using
!
for negation instead ofnot
- Assuming
$0
contains the script name - Not checking command exit status
- Global variable pollution
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:
#!/usr/bin/env fish
. - 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
name) use
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 asset -l fname (realpath .)
- Export variable using
set -x
(this is only available inside the current fish shell). An example of this is setting theDISPLAY
environment variable for X11 session in a fish function that is running in acrontab
headless environment. - 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 theJAVA_HOME
environment variable for all programs running on the machine.
Lists #
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[1] # one
echo $LIST[2] # two
echo $LIST[3] # three
echo $LIST[-1] # This is the same element as above
Ranges #
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
condition.
Commonly used conditions #
Checking the size of an array. $argv
contains the list of arguments passed to a 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 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 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
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
orreturn 0
- This means that the function exited normally.return 1
or 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
above.
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 -z
make 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 -q
to 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 or
and and
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
will describe.
- 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[1]"; or test -z "$argv[2]"
echo "arg1 or arg2 can not be empty"
return 1
end
echo "Thank you, got 1) $argv[1] and 2) $argv[2]"
end
Here are some notes on the code.
- What does the
set -q variable
function do? It returns true ifvariable
exists (is defined), regardless of whether it contains a value. - Instead of
set -q
, if you wanted to usetest
function in order to determine if a variable is empty, you can use:if test -z "$variable"
.if test ! -n "$variable"
orif not test -n "$variable"
.
- If you wanted to replace the
or
check above w/test
, this is what it would look likeif test -z "$argv[1]"; or test -z "$argv[2]"
. - When you use
or
,and
operators 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
not.
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 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 " " $CHECKSUM_ARRAY_STRING)
set CHECKSUM $CHECKSUM_ARRAY[1]
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 -q
or --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 ruby-dev
and ruby-bundler
are
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
using \
.
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
, eg: 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_
instead. - Do not use the
(
and)
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[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
Advanced function features #
Functions with default values #
You can provide default behavior when arguments are missing:
#!/usr/bin/env fish
function greet -a name greeting
# Provide default values
if test -z "$name"
set name "World"
end
if test -z "$greeting"
set greeting "Hello"
end
echo "$greeting, $name!"
end
greet # "Hello, World!"
greet Alice # "Hello, Alice!"
greet Bob "Good morning" # "Good morning, Bob!"
Functions with variable arguments (variadic) #
Handle functions that accept any number of arguments:
#!/usr/bin/env fish
function sum_numbers
set -l total 0
if test (count $argv) -eq 0
echo "Usage: sum_numbers <number1> [number2] [number3] ..." >&2
return 1
end
for num in $argv
if not string match -q -r '^\d+$' "$num"
echo "Error: '$num' is not a valid number" >&2
return 1
end
set total (math $total + $num)
end
echo $total
end
sum_numbers 1 2 3 4 5 # Output: 15
sum_numbers 10 20 # Output: 30
Functions with options/flags #
Parse command-line style options in functions:
#!/usr/bin/env fish
function my_copy
set -l verbose false
set -l force false
set -l source ""
set -l dest ""
# Parse options
while test (count $argv) -gt 0
switch $argv[1]
case -v --verbose
set verbose true
set argv $argv[2..-1]
case -f --force
set force true
set argv $argv[2..-1]
case -*
echo "Unknown option: $argv[1]" >&2
return 1
case '*'
if test -z "$source"
set source $argv[1]
else if test -z "$dest"
set dest $argv[1]
else
echo "Too many arguments" >&2
return 1
end
set argv $argv[2..-1]
end
end
# Validate required arguments
if test -z "$source"; or test -z "$dest"
echo "Usage: my_copy [-v|--verbose] [-f|--force] <source> <dest>" >&2
return 1
end
# Build command
set -l cp_args
if test "$force" = "true"
set cp_args $cp_args -f
end
if test "$verbose" = "true"
set cp_args $cp_args -v
echo "Copying '$source' to '$dest'..."
end
cp $cp_args "$source" "$dest"
end
# Usage examples:
# my_copy file.txt backup/
# my_copy -v -f important.doc /backup/
Functions that modify global state #
Sometimes you need functions that modify variables in the calling scope:
#!/usr/bin/env fish
function append_to_path -a new_path
if not contains "$new_path" $PATH
set -gx PATH $PATH "$new_path"
echo "Added '$new_path' to PATH"
else
echo "'$new_path' already in PATH"
end
end
function remove_from_path -a path_to_remove
if contains "$path_to_remove" $PATH
set -l new_path
for path_entry in $PATH
if test "$path_entry" != "$path_to_remove"
set new_path $new_path "$path_entry"
end
end
set -gx PATH $new_path
echo "Removed '$path_to_remove' from PATH"
else
echo "'$path_to_remove' not found in PATH"
end
end
Functions with comprehensive error handling #
Build robust functions with proper error checking:
#!/usr/bin/env fish
function safe_file_operation -a operation source dest
# Validate operation type
if not contains "$operation" copy move
echo "Error: operation must be 'copy' or 'move'" >&2
return 1
end
# Validate source file
if test -z "$source"
echo "Error: source file not specified" >&2
return 1
end
if not test -e "$source"
echo "Error: source file '$source' does not exist" >&2
return 1
end
if not test -r "$source"
echo "Error: cannot read source file '$source'" >&2
return 1
end
# Validate destination
if test -z "$dest"
echo "Error: destination not specified" >&2
return 1
end
set -l dest_dir (dirname "$dest")
if not test -d "$dest_dir"
echo "Creating destination directory: $dest_dir"
if not mkdir -p "$dest_dir"
echo "Error: failed to create destination directory" >&2
return 1
end
end
# Check if destination exists and prompt for confirmation
if test -e "$dest"
echo "Destination '$dest' already exists. Overwrite? [y/N]"
read -l confirm
if test "$confirm" != "y"
echo "Operation cancelled"
return 0
end
end
# Perform operation
switch "$operation"
case copy
if cp "$source" "$dest"
echo "Successfully copied '$source' to '$dest'"
else
echo "Error: failed to copy file" >&2
return 1
end
case move
if mv "$source" "$dest"
echo "Successfully moved '$source' to '$dest'"
else
echo "Error: failed to move file" >&2
return 1
end
end
end
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 source my-script.fish
.
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
it in 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
Using 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 #
The set_color
function allows fish to
colorize and format text content that is printed to stdout
using 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
Notes:
set_color normal
has to be called to reset whatever formatting options were set in previous statements.set_color -u
does underline, andset_color -o
does 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
reading stdin
using the read
function.
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 _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 fzf for interactive selection #
The fzf
command-line fuzzy finder enables interactive selection from lists with powerful search capabilities. It’s particularly useful for creating interactive menus and file selection interfaces in fish scripts.
Basic selection example #
Here’s a simple example of using fzf to select from a list of options:
#!/usr/bin/env fish
# Create a list of options to choose from
set options 'Option 1' 'Option 2' 'Option 3' 'Exit'
# Use fzf for fuzzy searching and selection
set selection (
printf '%s\n' $options | fzf --prompt 'Select an option: '
)
# Handle the selection
if test -n "$selection"
echo "You selected: $selection"
else
echo "No selection made"
end
Interactive file menu example #
Here’s a more practical example that creates an interactive file operations menu:
#!/usr/bin/env fish
function interactive_file_menu
# Define menu options
set menu_options \
"📁 Browse files" \
"🔍 Search in files" \
"📝 Edit a file" \
"🗑️ Delete a file" \
"📊 Show file stats" \
"❌ Exit"
while true
# Show menu with fzf (with colors and preview)
set selection (
printf '%s\n' $menu_options | \
fzf --prompt '➤ Choose action: ' \
--height 40% \
--layout reverse \
--border \
--ansi
)
# Process selection
switch "$selection"
case "📁 Browse files"
set chosen_file (ls -la | fzf --prompt 'Select file: ')
echo "You browsed: $chosen_file"
case "🔍 Search in files"
echo "Enter search term: "
read search_term
grep -r "$search_term" . | fzf
case "📝 Edit a file"
set file_to_edit (find . -type f | fzf --preview 'head -20 {}')
if test -n "$file_to_edit"
$EDITOR "$file_to_edit"
end
case "🗑️ Delete a file"
set file_to_delete (find . -type f | fzf --preview 'ls -la {}')
if test -n "$file_to_delete"
echo "Delete $file_to_delete? [y/N]"
read confirm
if test "$confirm" = "y"
rm "$file_to_delete"
echo "Deleted: $file_to_delete"
end
end
case "📊 Show file stats"
find . -type f | fzf --preview 'stat {}'
case "❌ Exit" ""
echo "Goodbye!"
break
end
end
end
Multiple selection example #
You can also select multiple items using the --multi
flag:
# Select multiple files for batch operations
set selected_files (
find . -type f -name "*.txt" | \
fzf --multi \
--prompt 'Select files (TAB to mark): ' \
--preview 'cat {}' \
--preview-window right:50%
)
if test (count $selected_files) -gt 0
echo "Selected files:"
for file in $selected_files
echo " - $file"
end
end
Common fzf options #
--prompt
: Custom prompt text--height
: Display height (percentage or lines)--layout reverse
: Show prompt at top--border
: Add border around fzf--preview
: Show preview window with command--preview-window
: Configure preview window position and size--multi
: Allow multiple selections (use TAB to mark)--ansi
: Enable ANSI color codes
Installation and usage notes #
- fzf must be installed first (
brew install fzf
on macOS,apt install fzf
on Ubuntu) - Use TAB for multiple selections when
--multi
is enabled - Use ESC or Ctrl-C to cancel without making a selection
- Type to fuzzy search through the available options
- Arrow keys or Ctrl-J/Ctrl-K to navigate up and down
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 find
.
Here’s an example that removes ./
from the start of each file that’s found.
echo "./.Android" | sed 's/^\.\///'
Here’s a more complex example of 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 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:
delimiter-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 subl
.
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
How to debug fish scripts #
Fish provides several debugging capabilities to help troubleshoot your scripts and understand what’s happening during execution.
Enable command tracing #
Fish can show you every command that’s being executed, which is useful for debugging complex scripts:
#!/usr/bin/env fish
# Enable tracing for debugging
set -g fish_trace 1
# Your script code here
echo "Starting script..."
set MY_VAR "test value"
echo "MY_VAR is: $MY_VAR"
# Disable tracing when done
set -e fish_trace
You can also enable tracing for just a portion of your script by setting and unsetting fish_trace
around specific sections.
Conditional debug output #
Create debug output that only shows when debugging is enabled:
#!/usr/bin/env fish
function debug_echo -a message
if set -q DEBUG
echo "DEBUG: $message" >&2
end
end
function my_function -a param1 param2
debug_echo "my_function called with: $param1, $param2"
set result (math $param1 + $param2)
debug_echo "calculation result: $result"
echo $result
end
# Usage: DEBUG=1 ./my_script.fish
# Or: set -x DEBUG 1; ./my_script.fish
Check command success and status codes #
Always check if commands succeeded, especially external commands:
#!/usr/bin/env fish
function safe_git_pull
if git pull
echo "✅ Git pull successful"
return 0
else
echo "❌ Git pull failed with status: $status" >&2
return $status
end
end
# Check if a command exists before using it
if command -v fzf >/dev/null
echo "fzf is available"
else
echo "fzf is not installed" >&2
exit 1
end
Validate function arguments #
Add argument validation to catch errors early:
#!/usr/bin/env fish
function process_file -a filename
# Validate arguments
if test (count $argv) -eq 0
echo "Error: filename required" >&2
echo "Usage: process_file <filename>" >&2
return 1
end
if not test -f "$filename"
echo "Error: file '$filename' does not exist" >&2
return 1
end
if not test -r "$filename"
echo "Error: file '$filename' is not readable" >&2
return 1
end
# Process the file
echo "Processing $filename..."
end
Use verbose output for complex operations #
Show what your script is doing step by step:
#!/usr/bin/env fish
function verbose_copy -a source dest
set -l verbose_flag ""
if set -q VERBOSE
set verbose_flag "-v"
echo "Copying $source to $dest..."
end
if cp $verbose_flag "$source" "$dest"
if set -q VERBOSE
echo "✅ Copy successful"
end
else
echo "❌ Copy failed" >&2
return 1
end
end
# Usage: VERBOSE=1 ./my_script.fish
Debug script timing and performance #
Profile sections of your script to identify bottlenecks:
#!/usr/bin/env fish
function time_section -a section_name
if set -q DEBUG_TIMING
set start_time (date +%s.%3N)
# Execute the commands passed as arguments
$argv[2..-1]
set end_time (date +%s.%3N)
set duration (math $end_time - $start_time)
echo "TIMING: $section_name took $duration seconds" >&2
else
# Just execute the commands without timing
$argv[2..-1]
end
end
# Usage example
time_section "file_processing" find . -name "*.txt" -exec wc -l {} \;
time_section "git_operations" git add . && git commit -m "Update files"
Environment variable debugging #
Show important environment variables for troubleshooting:
#!/usr/bin/env fish
function show_debug_info
if set -q DEBUG
echo "=== DEBUG INFO ===" >&2
echo "Script: "(status --current-filename) >&2
echo "PWD: $PWD" >&2
echo "USER: $USER" >&2
echo "PATH: $PATH" >&2
echo "fish version: "(fish --version) >&2
echo "=================" >&2
end
end
# Call at start of script
show_debug_info
Fish scripting best practices #
Following these best practices will make your fish scripts more robust, readable, and maintainable.
Always quote variables #
Quote variables to handle spaces and special characters correctly:
#!/usr/bin/env fish
# Good: quoted variables
set filename "my file with spaces.txt"
if test -f "$filename"
echo "File exists: $filename"
end
# Bad: unquoted variables (will break with spaces)
if test -f $filename
echo "This will fail with spaces in filename"
end
Use meaningful variable names #
Choose descriptive names that make your code self-documenting:
#!/usr/bin/env fish
# Good: descriptive names
set config_file_path "$HOME/.config/myapp/config.json"
set backup_directory "/backup/myapp"
set max_retry_attempts 3
# Bad: cryptic names
set cfp "$HOME/.config/myapp/config.json"
set bd "/backup/myapp"
set mra 3
Validate inputs and handle errors #
Always validate function parameters and handle potential errors:
#!/usr/bin/env fish
function backup_file -a source_file backup_dir
# Validate required parameters
if test (count $argv) -lt 2
echo "Usage: backup_file <source_file> <backup_dir>" >&2
return 1
end
# Validate source file exists and is readable
if not test -f "$source_file"
echo "Error: Source file '$source_file' does not exist" >&2
return 1
end
if not test -r "$source_file"
echo "Error: Cannot read source file '$source_file'" >&2
return 1
end
# Validate backup directory
if not test -d "$backup_dir"
echo "Creating backup directory: $backup_dir"
mkdir -p "$backup_dir"
end
# Perform backup with error checking
if cp "$source_file" "$backup_dir/"
echo "Successfully backed up '$source_file' to '$backup_dir'"
return 0
else
echo "Failed to backup '$source_file'" >&2
return 1
end
end
Use local variables in functions #
Use set -l
to keep variables local to function scope:
#!/usr/bin/env fish
function calculate_average
set -l numbers $argv
set -l sum 0
set -l count (count $numbers)
# Local variables don't pollute global scope
for num in $numbers
set sum (math $sum + $num)
end
set -l average (math $sum / $count)
echo $average
end
Prefer fish builtins over external commands #
Use fish’s built-in commands when possible for better performance and portability:
#!/usr/bin/env fish
# Good: use fish string builtin
set filename "document.pdf"
if string match -q "*.pdf" "$filename"
echo "PDF file detected"
end
# Less optimal: external grep
if echo "$filename" | grep -q "\.pdf$"
echo "PDF file detected"
end
# Good: use fish test builtin
if test -f "$filename"
echo "File exists"
end
# Less optimal: external test command
if /bin/test -f "$filename"
echo "File exists"
end
Use command substitution appropriately #
Store command output in variables for reuse and error checking:
#!/usr/bin/env fish
function check_git_status
# Store command output for reuse
set git_status (git status --porcelain 2>/dev/null)
# Check if git command succeeded
if test $status -ne 0
echo "Not a git repository" >&2
return 1
end
# Now we can use the result multiple times
if test -z "$git_status"
echo "Repository is clean"
else
echo "Repository has changes:"
echo "$git_status"
end
end
Use functions for reusable code #
Break complex scripts into smaller, reusable functions:
#!/usr/bin/env fish
function log_message -a level message
set timestamp (date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message"
end
function create_backup -a source dest
log_message "INFO" "Starting backup from '$source' to '$dest'"
if cp -r "$source" "$dest"
log_message "SUCCESS" "Backup completed successfully"
return 0
else
log_message "ERROR" "Backup failed"
return 1
end
end
# Main script logic
create_backup "/important/data" "/backup/location"
Handle signals gracefully #
Set up signal handlers for clean script termination:
#!/usr/bin/env fish
# Clean up temporary files on exit
function cleanup
if set -q temp_file
rm -f "$temp_file"
end
if set -q temp_dir
rm -rf "$temp_dir"
end
end
# Set up signal handlers
trap cleanup EXIT
trap 'echo "Script interrupted"; cleanup; exit 130' INT
# Your script logic here
set temp_file (mktemp)
set temp_dir (mktemp -d)
Use consistent exit codes #
Follow standard exit code conventions:
#!/usr/bin/env fish
function my_script
# 0 = success
# 1 = general error
# 2 = usage error
# 130 = script terminated by Control-C
if test (count $argv) -eq 0
echo "Usage: my_script <filename>" >&2
return 2 # Usage error
end
if not test -f "$argv[1]"
echo "File not found: $argv[1]" >&2
return 1 # General error
end
# Process file...
echo "Processing $argv[1]"
return 0 # Success
end
Document your functions #
Use the --description
flag to document function purpose:
#!/usr/bin/env fish
function process_log_files --description "Process and rotate log files older than specified days"
# Function implementation here
echo "Processing log files..."
end
function backup_database --description "Create timestamped backup of database to specified directory"
# Function implementation here
echo "Backing up database..."
end
# Users can see function descriptions with: functions --details function_name
Common fish scripting pitfalls #
These are frequent mistakes that can cause confusing behavior in fish scripts. Learning to avoid them will save you debugging time.
Forgetting that all variables are lists #
In fish, every variable is a list, even if it contains only one element. This can lead to unexpected behavior:
#!/usr/bin/env fish
# Pitfall: assuming single values
set filename "my file.txt"
set result (echo $filename) # This works fine
# But when you have multiple values:
set filenames "file1.txt" "file2.txt" "file3.txt"
set result (echo $filenames) # This passes 3 arguments to echo
# Correct: quote the variable to treat as single argument
set result (echo "$filenames") # This treats the whole list as one string
Using bash syntax in fish #
Fish syntax is different from bash/sh. These bash patterns don’t work in fish:
#!/usr/bin/env fish
# Bash syntax that doesn't work in fish:
# if [ "$var" = "value" ] # Use 'test' instead
# export VAR=value # Use 'set -x VAR value'
# VAR=value command # Use 'env VAR=value command'
# $((1 + 2)) # Use 'math 1 + 2'
# ${var:-default} # Use separate if/else logic
# Fish equivalents:
if test "$var" = "value"
echo "correct fish syntax"
end
set -x VAR value # export variable
env VAR=value command # set variable for single command
set result (math 1 + 2) # arithmetic
Mixing up set -q
(exists) vs test -z
(empty) #
These test different things and can cause logic errors:
#!/usr/bin/env fish
# set -q tests if variable EXISTS
# test -z tests if variable is EMPTY
set empty_var ""
set undefined_var # This variable doesn't exist
# This will be TRUE (variable exists but is empty)
if set -q empty_var
echo "empty_var exists"
end
# This will be FALSE (variable doesn't exist)
if set -q undefined_var
echo "This won't print"
end
# This will be TRUE (variable is empty)
if test -z "$empty_var"
echo "empty_var is empty"
end
# This will be TRUE (undefined variables are treated as empty strings)
if test -z "$undefined_var"
echo "undefined_var is also considered empty"
end
Not quoting variables with spaces #
Unquoted variables with spaces will be split into multiple arguments:
#!/usr/bin/env fish
set file_with_spaces "my document.pdf"
# Wrong: this will fail because it becomes 'test -f my document.pdf'
# which is 3 arguments instead of the filename
if test -f $file_with_spaces
echo "This test will fail incorrectly"
end
# Correct: quote the variable
if test -f "$file_with_spaces"
echo "This works correctly"
end
# Wrong: will try to copy 'my', 'document.pdf' separately
cp $file_with_spaces /backup/
# Correct: treats as single filename
cp "$file_with_spaces" /backup/
Incorrect variable expansion in loops #
Variable expansion behaves differently in different contexts:
#!/usr/bin/env fish
set items "item1" "item2" "item3"
# Pitfall: trying to modify the loop variable
for item in $items
set item "modified_$item" # This creates a new local variable!
echo $item # Prints modified version
end
echo $items # Original list is unchanged!
# Correct approach: use a different variable or array indexing
for i in (seq (count $items))
set items[$i] "modified_$items[$i]"
end
Forgetting command substitution captures stdout only #
Command substitution with ()
only captures stdout, not stderr:
#!/usr/bin/env fish
# This will capture the error count, but error messages go to terminal
set error_output (find /root -name "*.txt" 2>&1) # Capture both stdout and stderr
# Better: separate handling of stdout and stderr
find /root -name "*.txt" 2>/tmp/find_errors.log
set found_files (find /root -name "*.txt" 2>/dev/null)
if test -s /tmp/find_errors.log
echo "Errors occurred during find operation"
end
Using !
for negation instead of not
#
Fish uses not
for logical negation, not !
:
#!/usr/bin/env fish
set filename "document.txt"
# Wrong: bash/sh syntax
# if ! test -f "$filename"
# Correct: fish syntax
if not test -f "$filename"
echo "File doesn't exist"
end
# Also works with commands
if not git pull
echo "Git pull failed"
end
Assuming $0
contains the script name #
In fish, $argv[0]
or (status current-filename)
should be used instead of $0
:
#!/usr/bin/env fish
# Wrong: $0 doesn't exist in fish
# echo "Script name: $0"
# Correct ways to get script name:
echo "Script name: "(status current-filename)
echo "Script basename: "(basename (status current-filename))
# For command line arguments:
echo "First argument: $argv[1]"
echo "All arguments: $argv"
echo "Number of arguments: "(count $argv)
Not checking command exit status #
Always verify that commands succeeded, especially in automated scripts:
#!/usr/bin/env fish
# Pitfall: assuming commands always succeed
git clone https://github.com/user/repo.git
cd repo
make install
# Better: check each step
if not git clone https://github.com/user/repo.git
echo "Failed to clone repository" >&2
exit 1
end
if not cd repo
echo "Failed to enter repository directory" >&2
exit 1
end
if not make install
echo "Failed to install" >&2
exit 1
end
Global variable pollution #
Functions can accidentally modify global variables if you don’t use local scope:
#!/usr/bin/env fish
set counter 10
function increment_counter
# Pitfall: modifying global variable unintentionally
set counter (math $counter + 1)
echo "Counter in function: $counter"
end
increment_counter
echo "Global counter: $counter" # This is now 11, which might be unexpected
# Better: use local variables when appropriate
function safe_increment -a input
set -l local_counter (math $input + 1)
echo $local_counter # Return the result
end
set counter (safe_increment $counter)
👀 Watch Rust 🦀 live coding videos on our YouTube Channel.
📦 Install our useful Rust command line apps usingcargo install r3bl-cmdr
(they are from the r3bl-open-core project):
- 🐱
giti
: run interactive git commands with confidence in your terminal- 🦜
edi
: edit Markdown with style in your terminalgiti in action
edi in action