What Are Fish Functions?
In Fish shell, functions are the fundamental building blocks for organizing and reusing commands. Unlike traditional shells like Bash that blur the line between functions and scripts, Fish treats functions as first-class citizens. A Fish function is a named block of commands that can accept arguments, define local variables, and return exit statuses — just like a built-in command.
Fish functions can live in several places: defined interactively in your session, stored in configuration files like ~/.config/fish/config.fish, or placed in dedicated autoload directories. This flexibility allows you to build everything from simple aliases to complex, modular command suites.
Key Characteristics
- First-class commands — Functions behave identically to built-in commands; they can be piped, redirected, and used in command substitution
- Autoloading system — Place a function in the right directory and Fish discovers it on demand, no manual sourcing required
- Rich parameter handling — Named arguments via
argparse, argument arrays via$argv, and no need for arcane quoting rules - Scoped variables — Local variables by default, preventing accidental pollution of the global namespace
- Discoverable documentation — Functions can include descriptions that appear in tab completions and the
functionscommand output
Why Fish Functions Matter
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Functions transform Fish from a simple interactive shell into a powerful scripting environment. They let you encapsulate logic, reduce repetition, and create domain-specific languages tailored to your workflow. For developers, this means cleaner shell scripts, easier debugging, and a more maintainable codebase over time.
Because Fish functions support autoloading, you can build entire toolkits — think Git helpers, Docker shortcuts, project scaffolding commands — that load lazily only when invoked. This keeps your shell startup snappy even with hundreds of custom commands defined.
Defining Your First Function
The simplest way to define a function interactively is using the function built-in, followed by the body and terminated with end. Here is a basic greeting function:
function greet
echo "Hello, $argv! Welcome to Fish."
end
Call it like any other command:
greet World
# Output: Hello, World! Welcome to Fish.
Notice that $argv is a special list variable containing all arguments passed to the function. You can access individual arguments with index notation like $argv[1] or iterate over all of them with a for loop.
Functions with Descriptions
Add a description using the --description flag so your function appears with context in tab completions and listings:
function greet --description "Print a friendly greeting to the given name"
echo "Hello, $argv! Welcome to Fish."
end
Now running functions --details greet will display your description, and users tab-completing will see helpful information.
Function Arguments and $argv
Every function receives its arguments in the $argv list. This is one of Fish's greatest usability improvements over legacy shells: no confusing $1, $2, $@, or quoting nightmares. $argv is always a list, and list operations work intuitively.
function show-args
echo "You passed" (count $argv) "arguments:"
for arg in $argv
echo " - $arg"
end
end
show-args foo bar baz
# Output:
# You passed 3 arguments:
# - foo
# - bar
# - baz
Handling Optional Arguments with Defaults
Use Fish's conditional expansion to provide default values when arguments are missing:
function greet --description "Greet someone, defaulting to World"
set name $argv[1]
if test -z "$name"
set name "World"
end
echo "Hello, $name!"
end
greet
# Output: Hello, World!
greet Alice
# Output: Hello, Alice!
Or more concisely using Fish's default value syntax:
function greet
set name (echo $argv[1] | or echo "World")
echo "Hello, $name!"
end
Named Arguments with argparse
For complex functions with flags and options, Fish provides argparse — a built-in argument parser that rivals dedicated libraries in other languages:
function deploy --description "Deploy application to environment"
argparse -n deploy 'h/help' 'e/env=' 't/timeout=' -- $argv
or return
if set -q _flag_help
echo "Usage: deploy [-h] [-e ENV] [-t TIMEOUT]"
return 0
end
set -q _flag_env; or set _flag_env "production"
set -q _flag_timeout; or set _flag_timeout "300"
echo "Deploying to $_flag_env with timeout $_flag_timeout seconds..."
end
deploy -e staging -t 120
# Output: Deploying to staging with timeout 120 seconds...
Key argparse patterns:
'flag/'— Boolean flag, accessible as$_flag_flag'option='— Option requiring a value, accessible as$_flag_option-n name— Sets the function name for error messagesor return— Exits the function if argument parsing failsset -q $_flag_X— Tests whether a flag or option was provided
Variable Scoping in Functions
Fish uses block-scoped variables. Inside a function, variables are local by default when declared with set. This prevents the common shell scripting pitfall of accidentally overwriting global variables.
set -g GLOBAL_COUNT 0
function increment-global
set -g GLOBAL_COUNT (math $GLOBAL_COUNT + 1)
echo "Global count is now $GLOBAL_COUNT"
end
function local-example
set LOCAL_VAR "I am local"
echo $LOCAL_VAR
end
increment-global
increment-global
local-example
echo $LOCAL_VAR # Nothing! LOCAL_VAR does not exist here.
Explicit scope flags give you precise control:
set -lor plainset— Local to the current block (function or begin/end block)set -g— Global, visible everywhereset -U— Universal, persisted across restarts and shared between all Fish instancesset --path— Marks the variable as a path, enabling path-specific completions
Return Values and Exit Statuses
Fish functions communicate results through two mechanisms: exit statuses (success/failure) and explicit output to stdout. The exit status of the last command run becomes the function's exit status. Use return with an integer to explicitly set it.
function is-even --description "Return success if number is even"
set num (math "$argv[1] % 2")
if test $num -eq 0
return 0
else
return 1
end
end
is-even 4; and echo "4 is even"
# Output: 4 is even
is-even 7; or echo "7 is odd"
# Output: 7 is odd
To return data, echo it to stdout and capture it with command substitution:
function multiply
math "$argv[1] * $argv[2]"
end
set result (multiply 6 7)
echo "6 × 7 = $result"
# Output: 6 × 7 = 42
The Autoloading System
Fish's autoloading mechanism is one of its most powerful features. You don't need to manually source function files — Fish discovers them on demand by searching specific directories. This works as follows:
- When you call an unknown command, Fish checks if a function file exists with that name
- It searches
$fish_function_pathdirectories in order - If found, it loads the function and executes it — all transparently
Create a function file by placing a .fish file in the right directory. The file must contain the function definition without the function/end wrapper — just the body content. Fish wraps it automatically on load.
Setting Up Autoload Directories
# Create a custom autoload directory
mkdir -p ~/.config/fish/functions/custom
# Add it to your function path (in config.fish)
set -p fish_function_path ~/.config/fish/functions/custom
# Create an autoloadable function file
echo 'echo "Building project $argv in (pwd)..."
# Build commands here' > ~/.config/fish/functions/custom/build.fish
# Now it works immediately in new sessions
build my-app
# Output: Building project my-app in /home/user/projects...
The standard Fish function directories include:
$fish_function_path— The search path (modifiable at runtime)~/.config/fish/functions/— User's persistent function directory/usr/share/fish/functions/— System-wide functions (vendor-supplied)/usr/share/fish/vendor_functions.d/— Package-installed functions
Lazy Loading Pattern
Autoloading enables a powerful pattern: create small wrapper functions that load heavy dependencies only when needed. The actual function body file is loaded on first invocation.
# In ~/.config/fish/functions/heavy-tool.fish
# This file is autoloaded only when 'heavy-tool' is first called
if not set -q _heavy_tool_initialized
pip install --quiet some-heavy-package
set -g _heavy_tool_initialized true
end
heavy-tool-command $argv
Event-Driven Functions
Fish supports special function names that automatically trigger on shell events. These are powerful hooks for customizing your environment without polluting your startup sequence.
# Runs whenever the current working directory changes
function fish_prompt --description "Custom prompt with git info"
set -l git_branch (git branch --show-current 2>/dev/null)
set -l prompt "["(set_color blue)"$PWD"(set_color normal)"]"
if test -n "$git_branch"
set prompt "$prompt ("(set_color yellow)"$git_branch"(set_color normal)")"
end
echo "$prompt \$ "
end
# Runs when Fish exits
function fish_remove_universal --on-event fish_exit
echo "Cleaning up temporary universal variables..."
set -e MY_TEMP_VAR
end
Common event function names:
fish_prompt— Called before each interactive promptfish_right_prompt— Called for the right-side promptfish_command_not_found— Triggered when a command isn't foundfish_greeting— Called at the start of each interactive sessionfish_title— Sets the terminal window title
Advanced Patterns
Functions That Accept Pipelines
Functions can consume stdin, making them perfect pipeline citizens. Use read to process incoming data:
function summarize --description "Count lines, words, and characters from stdin"
set -l lines 0
set -l words 0
set -l chars 0
while read -l line
set lines (math $lines + 1)
set words (math $words + (count (string split " " -- $line)))
set chars (math $chars + (string length "$line"))
end
echo "Lines: $lines, Words: $words, Characters: $chars"
end
ls -la | summarize
# Output: Lines: 15, Words: 45, Characters: 520
Recursive Functions
Fish functions can call themselves. Here is a classic factorial example demonstrating recursion:
function factorial --description "Compute factorial recursively"
if test $argv[1] -le 1
echo 1
return
end
set -l prev (factorial (math "$argv[1] - 1"))
math "$argv[1] * $prev"
end
factorial 5
# Output: 120
Functions That Modify Global State Safely
When a function needs to persist state across invocations, use universal variables with clear naming conventions to avoid collisions:
function cache-set --description "Store a value in a named cache"
set -U cache_$argv[1] $argv[2]
end
function cache-get --description "Retrieve a value from cache"
set -q cache_$argv[1]; and echo $cache_$argv[1]
end
cache-set user_name "Alice"
cache-get user_name
# Output: Alice
Function Decorators / Wrappers
You can create functions that wrap existing commands to add logging, timing, or pre/post hooks:
function timed --description "Run a command and report elapsed time"
set -l start (date +%s.%N)
$argv
set -l exit_code $status
set -l end (date +%s.%N)
set -l elapsed (math "$end - $start")
printf "Completed in %.3f seconds (exit: %d)\n" $elapsed $exit_code
return $exit_code
end
timed sleep 2
# Output: Completed in 2.001 seconds (exit: 0)
Debugging Fish Functions
Fish provides built-in tools for inspecting and debugging functions without external dependencies:
# List all functions with their descriptions
functions --all --details
# Show the source code of a specific function
functions --details greet
functions --body greet
# Trace function execution with fish_trace
set -g fish_trace 1
greet World
# Shows every command executed within the function
# Turn off tracing
set -g fish_trace 0
For more targeted debugging, use breakpoint inside a function to drop into an interactive prompt:
function complex-operation
set -l step1 (fetch-data)
breakpoint # Inspect variables, step through manually
set -l step2 (process-data $step1)
echo $step2
end
At the breakpoint, you can inspect all local variables, run arbitrary commands, and type exit to resume execution.
Best Practices for Fish Functions
1. Always Add Descriptions
Every function you intend to keep should have a --description. It takes seconds to add and pays dividends when you revisit code months later or share functions with a team.
function deploy --description "Deploy application to specified environment"
# ...
end
2. Use argparse for Complex Parameter Handling
Resist the temptation to manually parse $argv with string matching. argparse is built-in, fast, and produces consistent error messages that users expect.
3. Keep Functions Focused
A function should do one thing well. If your function exceeds 50 lines, consider splitting it into helper functions. Autoloading makes this cheap — each helper lives in its own file.
4. Prefer Local Variables
Default to local scope. Only use -g or -U when you explicitly need persistence or cross-function communication. This prevents hard-to-debug state leaks.
5. Handle Errors Explicitly
Use or chains to handle failures gracefully rather than ignoring exit statuses:
function safe-delete
rm -rf "$argv[1]"; or begin
echo "Failed to delete $argv[1]" >&2
return 1
end
end
6. Use the Autoload System Properly
Place standalone function files in ~/.config/fish/functions/ with the bare body (no function/end wrapper). Fish handles the wrapping. This keeps your config.fish lean and startup fast.
7. Test Functions Interactively
Define functions in-session first, test them thoroughly, then persist them to disk only when they're stable. Use funcsave to save a function defined interactively:
funcsave greet
# Saves the 'greet' function to ~/.config/fish/functions/greet.fish
8. Document with Comments Inside the Function Body
Since autoload files don't contain the function/end wrapper, comments at the top of the file serve as excellent documentation visible in functions --details output:
# deploy.fish — Deploy application to environments
# Usage: deploy [-e ENV] [-t TIMEOUT]
argparse 'e/env=' 't/timeout=' -- $argv
# ... rest of function body
Common Pitfalls and How to Avoid Them
- Forgetting
end— Everyfunction,if,for,while, andbeginblock must be closed withend. Fish will complain immediately if you forget. - Confusing
$argvin nested functions — Each function has its own$argv. If you call another function inside yours, pass arguments explicitly:inner-func $argv. - Not handling empty
$argv— Always testcount $argvor useset -q argv[1]before accessing arguments to avoid errors. - Overusing global variables — Global state makes functions non-reusable and hard to reason about. Pass data via arguments and command substitution instead.
- Assuming autoload paths exist — Test
echo $fish_function_pathto verify your custom directories are in the search path before placing files there.
Conclusion
Fish functions elevate shell scripting from a stringly-typed guessing game into a structured, discoverable, and genuinely pleasant programming experience. By embracing $argv lists, argparse for argument handling, local scoping, and the autoloading system, you can build a personal toolkit of commands that feels native to the shell while remaining maintainable over years of use. Start with small, focused functions, add descriptions early, and let the autoloading system handle discovery — your future self will thank you every time you type a command and it just works.