Fish Shell Functions & Custom Commands Guide
How to create, edit, save, and autoload functions in Fish Shell. Covers event handlers, argument parsing, scope, and practical examples.
Functions in Fish are how you build reusable commands. If you’ve used Bash functions before, the idea is the same, but Fish’s implementation is cleaner — no curly braces, explicit argument handling through $argv, and a lazy-loading system that keeps startup fast.
I use functions for everything from quick shortcuts to project-specific tooling. This guide covers creating them, saving them permanently, and the Fish-specific features that make them more useful than Bash functions.
Creating a basic function
function greet
echo "Hello, $argv"
end
Run greet World and it prints Hello, World. The variable $argv contains all arguments passed to the function. You can access individual arguments with $argv[1], $argv[2], etc.
That function only lasts for the current shell session. Close the terminal and it’s gone. I’ll cover how to make functions permanent in a moment.
Functions with named arguments
Fish doesn’t have named parameters the way Python does, but --argument-names gives you something close:
function mkcd --argument-names dir
mkdir -p $dir
cd $dir
end
Now mkcd projects/new-app creates the directory and moves into it. The first argument gets assigned to $dir. Extra arguments are still available through $argv.
You can list multiple argument names:
function connect --argument-names host port
ssh -p $port $host
end
Adding a description
function ll --description "List files in long format"
ls -la $argv
end
The description shows up when you run functions or type ll. It’s documentation for future-you.
Saving functions permanently
Fish has three ways to keep functions across sessions.
Method 1: funcsave (the Fish way)
Create a function interactively, then save it:
function weather --argument-names city
curl "wttr.in/$city?format=3"
end
funcsave weather
This saves the function to ~/.config/fish/functions/weather.fish. Fish automatically loads it when you use the command in any future session — not at startup, but on demand. This lazy loading is why Fish stays fast regardless of how many saved functions you have.
Method 2: Create the file directly
Write the function file yourself:
# ~/.config/fish/functions/weather.fish
function weather --argument-names city
curl "wttr.in/$city?format=3"
end
Same result as funcsave. I prefer this method for functions I want to version-control with my dotfiles.
Method 3: Put it in config.fish
# ~/.config/fish/config.fish
function weather --argument-names city
curl "wttr.in/$city?format=3"
end
This works but has two downsides: the function loads on every shell startup (not lazily), and your config.fish gets longer. Use autoloading files for anything beyond trivial functions.
Autoloading rules
For autoloading to work, the file name must match the function name. A function called weather must be in weather.fish. If the file contains multiple functions, only the one matching the filename will autoload.
Editing functions
Fish has a built-in function editor:
funced weather
This opens the function in $EDITOR (or $VISUAL). When you save and close the editor, Fish loads the updated function into your current session. You can then persist it with funcsave weather.
To see a function’s current definition without editing:
functions weather
# or
type weather
Practical function examples
Git shortcut with defaults
function gc --description "Git commit with message"
if test (count $argv) -eq 0
echo "Usage: gc <message>"
return 1
end
git add --all
git commit -m "$argv"
end
Usage: gc "fix login redirect" stages everything and commits with that message.
Quick project switcher
function proj --argument-names name
set -l base ~/projects
if test -z "$name"
ls $base
return
end
if test -d $base/$name
cd $base/$name
else
echo "Project '$name' not found in $base"
return 1
end
end
Run proj to list projects, or proj myapp to jump to ~/projects/myapp. Add tab completions for it too:
# ~/.config/fish/completions/proj.fish
complete -c proj -f -a "(ls ~/projects)"
Now proj followed by Tab lists your project directories. See my autocomplete guide for more on writing completions.
Backup with timestamp
function bak --argument-names file
if test -z "$file"
echo "Usage: bak <file>"
return 1
end
cp $file $file.bak.(date +%Y%m%d-%H%M%S)
end
bak config.yaml creates config.yaml.bak.20260224-141500.
Docker cleanup
function docker-clean --description "Remove stopped containers, dangling images, unused volumes"
echo "Removing stopped containers..."
docker container prune -f
echo "Removing dangling images..."
docker image prune -f
echo "Removing unused volumes..."
docker volume prune -f
end
Event handlers
Functions can respond to events. This is useful for running code when variables change, when a command finishes, or when Fish exits.
Run code when a variable changes
function __on_pwd_change --on-variable PWD
if test -f .node-version
echo "Node version: "(cat .node-version)
end
end
Every time you change directories, this checks for a .node-version file. The --on-variable PWD flag triggers the function whenever $PWD changes.
Run code on Fish exit
function __on_exit --on-event fish_exit
echo "Goodbye!"
end
Run code after a command finishes
function __notify_long_command --on-event fish_postexec
if test $CMD_DURATION -gt 10000
echo "Command took "(math $CMD_DURATION / 1000)" seconds"
end
end
This prints a notice when any command takes more than 10 seconds. $CMD_DURATION is a special Fish variable that holds the last command’s execution time in milliseconds.
Event handler naming
Event handler functions should start with double underscores or a unique prefix to avoid name collisions. Also, event handlers in autoloaded files won’t trigger until the function has been loaded once. For handlers that need to work from the start, put them in config.fish or conf.d/.
Scope and variable visibility
Functions have their own local scope by default. Variables set with set -l inside a function aren’t visible outside it:
function test_scope
set -l secret "hidden"
echo $secret
end
test_scope # prints "hidden"
echo $secret # prints nothing
Use set -g for global variables (visible everywhere in the session) or set -U for universal variables (persist across all Fish sessions):
set -g session_var "I last until you close this terminal"
set -U persistent_var "I survive restarts"
Functions vs abbreviations vs aliases
Fish has three ways to create shortcuts. Here’s when to use each:
| Functions | Abbreviations | Aliases | |
|---|---|---|---|
| Best for | Complex logic, multi-line commands | Simple command shortcuts | Simple command wrapping |
| Expansion | No expansion, runs as-is | Expands on command line before running | Wraps as a function internally |
| History shows | Function name | Expanded command | Alias name |
| Arguments | Full $argv handling | Limited (position/regex-based) | Pass-through $argv |
I use abbreviations for simple shortcuts (gs → git status), and functions for anything that needs logic. I covered abbreviations vs aliases in detail in Fish Shell abbreviations vs aliases.
Managing functions
functions # list all defined functions
functions -n # list function names only
functions weather # show a function's definition
functions -e weather # erase a function
funcsave weather # save to autoload file
funced weather # edit in $EDITOR
To delete a saved function permanently:
functions -e weather
funcsave weather
The second command writes the “erased” state, removing the file from ~/.config/fish/functions/.
Related guides
- Fish Shell autocomplete and suggestions — writing custom completions for your functions
- Fish Shell abbreviations vs aliases — when to use each
- Best Fish Shell plugins — extend Fish with Fisher and community plugins
- Fish Shell on macOS — if you’re setting up Fish on a Mac
- Install Fish Shell on Ubuntu — getting started on Linux