Fish Shell Completion System: A Complete Guide
Fish shell's completion system is one of its most celebrated features. Unlike traditional shells that rely on complex, often inscrutable completion scripts, Fish provides a clean, declarative syntax for defining how commands, arguments, and options should be completed. This guide covers everything you need to know to write robust, user-friendly completions for your own scripts and commands.
What Is the Fish Completion System?
The completion system is the mechanism that suggests possible arguments, options, file paths, and other values when a user presses Tab after typing part of a command. Fish goes far beyond simple filename completion β it can complete command-specific options, subcommands, environment variables, process IDs, git branches, and virtually anything your script or program might accept. Completions in Fish are defined using the complete builtin, which registers completion rules in a way that is both human-readable and highly maintainable.
Why the Completion System Matters
A well-designed completion system transforms the command-line experience. Here is why investing time in writing completions pays off:
- Discoverability β Users can explore your command's options and subcommands by pressing Tab, rather than constantly consulting man pages or --help output.
- Speed β Typing partial commands and completing them reduces keystrokes and prevents typos.
- Consistency β Fish completions follow a uniform style, making all commands feel cohesive.
- Reduced cognitive load β Users do not need to memorize exact flag names; the shell presents them on demand.
- Validation β Completions can prevent invalid combinations of arguments from being offered.
How the Completion System Works
When you press Tab, Fish looks up all registered completions for the command currently being typed. It evaluates conditions, checks the token being completed against the cursor position, and filters the candidate list based on what the user has already typed. Completions are defined globally using the complete command and stored persistently. Fish also ships with a rich set of built-in completions for common Unix commands.
The core building blocks of a completion definition are:
- The command name β which command this completion applies to.
- The option or argument β what flag, subcommand, or positional argument is being completed.
- The completion candidates β the list of possible values to suggest.
- Conditions β when this completion rule should be active.
Your First Custom Completion
Let's start with a simple example. Suppose you have a script called greet that accepts --name and --language options:
#!/usr/bin/env fish
# greet.fish - A friendly greeter script
function greet
argparse 'n/name=' 'l/language=' -- $argv
set -l name $_flag_name
set -l lang $_flag_language
test -z "$name" && set name "World"
test -z "$lang" && set lang "English"
switch $lang
case English
echo "Hello, $name!"
case Spanish
echo "Β‘Hola, $name!"
case French
echo "Bonjour, $name!"
case '*'
echo "Unknown language: $lang"
end
end
Now we write completions for greet. Place the following in a file called greet.fish inside your completions directory (typically ~/.config/fish/completions/):
# Completions for greet
# Complete the --name option: suggest no specific value, just indicate it takes a string
complete -c greet -l name -d "Name of the person to greet" -x
# Complete the --language option with a fixed list of languages
complete -c greet -l language -d "Language to use for greeting" -x -a "English Spanish French"
# Short flags: -n for --name, -l for --language
complete -c greet -s n -l name -d "Name of the person to greet" -x
complete -c greet -s l -l language -d "Language to use for greeting" -x -a "English Spanish French"
# Also complete --help
complete -c greet -l help -d "Show help message"
Here is what each flag to complete means:
-cor--commandβ The command this completion applies to.-lor--long-optionβ A long option name (e.g.,--name).-sor--short-optionβ A short option character (e.g.,-n).-dor--descriptionβ A description shown next to the completion candidate.-aor--argumentsβ The list of possible argument values.-xor--no-filesβ Prevents file completion; the option requires a specific argument.-ror--require-parameterβ The option must be followed by an argument.
Completing Subcommands
Many modern CLI tools (like git, docker, or kubectl) use a subcommand pattern. Fish handles this elegantly with the --condition flag and by using the __fish_seen_subcommand_from helper. Let's build a completion for a hypothetical todo command with subcommands:
#!/usr/bin/env fish
# todo.fish - A task manager
function todo
set -l subcommand (string tolower "$argv[1]")
test -z "$subcommand" && set subcommand "list"
switch $subcommand
case list
echo "Listing all tasks..."
case add
test -z "$argv[2]" && echo "Usage: todo add <task description>" && return 1
echo "Added task: $argv[2]"
case done
test -z "$argv[2]" && echo "Usage: todo done <task-id>" && return 1
echo "Marked task $argv[2] as done"
case '*'
echo "Unknown subcommand: $subcommand"
return 1
end
end
The completion file for todo would look like this:
# Completions for todo
# First, complete subcommands when no subcommand has been entered yet
complete -c todo -n "not __fish_seen_subcommand_from list add done" \
-a "list" -d "List all tasks"
complete -c todo -n "not __fish_seen_subcommand_from list add done" \
-a "add" -d "Add a new task"
complete -c todo -n "not __fish_seen_subcommand_from list add done" \
-a "done" -d "Mark a task as completed"
# Complete the --help flag for all subcommands
complete -c todo -l help -d "Show help"
# For the 'add' subcommand, suggest nothing specific (free-form text)
# but prevent file completion since we want a task description string
complete -c todo -n "__fish_seen_subcommand_from add" -x -r \
-d "Task description (free text)"
# For the 'done' subcommand, we might complete task IDs
# This would typically come from a dynamic source β see next section
complete -c todo -n "__fish_seen_subcommand_from done" -x -r \
-d "Task ID to mark as done"
The -n or --condition flag accepts a fish command that returns true (zero exit status) or false. The helper __fish_seen_subcommand_from checks whether the user has already typed one of the listed subcommands. Negating it with not means "only activate this completion when no subcommand has been entered yet."
Dynamic Completions
Static lists are fine for fixed options, but many commands need completions that change based on the system state. Fish supports dynamic argument completion through several mechanisms:
Using Functions as Argument Sources
You can use the -a flag with parentheses to call a function that generates candidates dynamically:
# A function that returns a list of available task IDs
function __todo_list_ids
# Simulate fetching task IDs from a data store
echo "1\tFirst task"
echo "2\tSecond task"
echo "3\tThird task"
end
# Use the function for dynamic completion
complete -c todo -n "__fish_seen_subcommand_from done" \
-a "(__todo_list_ids)" -d "Task ID to mark as done"
Fish calls __todo_list_ids each time completion is triggered and uses its output as candidates. Each line can contain a value and a description separated by a tab character.
Using External Commands
You can also pipe the output of external commands directly into completions:
# Complete Docker container IDs dynamically
complete -c mydockerctl -n "__fish_seen_subcommand_from stop restart" \
-a "(docker ps -q --format '{{.ID}}\t{{.Names}}')" -d "Container ID"
Completing File Paths with Filters
Fish excels at file path completion. You can restrict file completions to specific types or patterns:
# Complete only .tar.gz files for an archive extractor command
complete -c untar -a "*.tar.gz" -d "Tarball to extract"
# Complete only directories (useful for --output-dir style options)
complete -c build -l output-dir -a "(find . -type d -not -path '*/.*' | string replace './' '')" \
-d "Output directory"
For simple directory-only completion, Fish provides a built-in helper:
complete -c mycmd -l config-dir -x -a "(__fish_complete_directories)" \
-d "Configuration directory"
Chaining Multiple Completion Rules
Complex commands often need multiple completion rules that fire at different stages. Fish evaluates all matching rules and combines their candidates. Here is a complete example for a deploy command:
# deploy.fish completions
# Stage 1: Complete subcommands
complete -c deploy -n "not __fish_seen_subcommand_from staging production rollback status" \
-a "staging" -d "Deploy to staging environment"
complete -c deploy -n "not __fish_seen_subcommand_from staging production rollback status" \
-a "production" -d "Deploy to production environment"
complete -c deploy -n "not __fish_seen_subcommand_from staging production rollback status" \
-a "rollback" -d "Rollback the last deployment"
complete -c deploy -n "not __fish_seen_subcommand_from staging production rollback status" \
-a "status" -d "Show deployment status"
# Stage 2: Common options available for deployment subcommands
complete -c deploy -n "__fish_seen_subcommand_from staging production" \
-l branch -x -d "Git branch to deploy" -a "(__fish_git_branches)"
complete -c deploy -n "__fish_seen_subcommand_from staging production" \
-l force -d "Force deployment even if checks fail"
complete -c deploy -n "__fish_seen_subcommand_from staging production" \
-l dry-run -d "Simulate deployment without executing"
# Stage 3: Rollback-specific options
complete -c deploy -n "__fish_seen_subcommand_from rollback" \
-l to -x -d "Revision to roll back to" -a "(__fish_print_deploy_revisions)"
# Stage 4: Global flags available everywhere
complete -c deploy -l verbose -d "Enable verbose output"
complete -c deploy -l help -d "Show help message"
# Helper function for deploy revisions
function __fish_print_deploy_revisions
# This would query the deployment system for recent revision IDs
deploy --list-revisions 2>/dev/null | string split ' ' | head -n 20
end
Using __fish_use_subcommand for Complex Hierarchies
For commands with deeply nested subcommands, you can use __fish_use_subcommand which returns true if none of the given subcommands have been seen yet β a cleaner pattern for deep trees:
# A cloud CLI with nested commands
# cloud compute instances list|start|stop
# cloud storage buckets list|create|delete
# Top-level
complete -c cloud -n "__fish_use_subcommand" -a "compute" -d "Compute resources"
complete -c cloud -n "__fish_use_subcommand" -a "storage" -d "Storage resources"
# compute subcommands
complete -c cloud -n "__fish_seen_subcommand_from compute; and __fish_use_subcommand" \
-a "instances" -d "Virtual machine instances"
complete -c cloud -n "__fish_seen_subcommand_from compute; and __fish_use_subcommand" \
-a "images" -d "Machine images"
# instances subcommands
complete -c cloud -n "__fish_seen_subcommand_from compute instances; and __fish_use_subcommand" \
-a "list" -d "List instances"
complete -c cloud -n "__fish_seen_subcommand_from compute instances; and __fish_use_subcommand" \
-a "start" -d "Start an instance"
complete -c cloud -n "__fish_seen_subcommand_from compute instances; and __fish_use_subcommand" \
-a "stop" -d "Stop an instance"
# start takes an instance ID
complete -c cloud -n "__fish_seen_subcommand_from compute instances start" \
-x -a "(cloud compute instances list --ids 2>/dev/null)" \
-d "Instance ID to start"
Advanced Conditional Logic
The -n condition flag accepts arbitrary fish commands. This gives you enormous flexibility:
# Only offer --force if the user hasn't already specified --dry-run
complete -c deploy -n "__fish_seen_subcommand_from staging production; and not __fish_contains_opt -s dry-run" \
-l force -d "Force deployment"
# Offer environment-specific options based on config files
complete -c deploy -n "test -f deploy/staging.yml" \
-l config -d "Custom config file" -a "deploy/staging.yml deploy/production.yml"
# Complete based on the day of the week
complete -c backup -n "test (date +%u) -eq 1" \
-l full -d "Perform full backup (Monday only)"
Fish provides several built-in helper functions for common conditions:
__fish_seen_subcommand_fromβ Checks if any of the given subcommands appear earlier in the command line.__fish_use_subcommandβ Returns true if no subcommand from the given list has been seen (useful for nesting).__fish_contains_optβ Checks if a specific option has already been supplied.__fish_not_contain_optβ The inverse; true if an option has not been supplied.__fish_is_first_argβ True if the cursor is at the first positional argument.__fish_is_nth_argβ True if the cursor is at the Nth positional argument.
Completing Environment Variables
Fish can complete environment variables inside commands. To enable variable completion for your command's arguments:
# Allow environment variable expansion in arguments
complete -c mycmd -l env-var -x -a "(__fish_print_environment_variables)" \
-d "Environment variable name"
Completing Process IDs
For commands like kill or custom process managers, Fish provides PID completion:
complete -c mydaemon -n "__fish_seen_subcommand_from stop restart" \
-x -a "(pidof mydaemon 2>/dev/null)" -d "Process ID"
Descriptions That Enrich the Experience
Every completion candidate can carry a description. Descriptions appear in the pager when multiple candidates match. Use them generously:
# Rich descriptions help users understand options instantly
complete -c git -n "__fish_use_subcommand" \
-a "commit" -d "Record changes to the repository"
complete -c git -n "__fish_use_subcommand" \
-a "push" -d "Update remote refs along with associated objects"
complete -c git -n "__fish_use_subcommand" \
-a "fetch" -d "Download objects and refs from another repository"
# For options, describe what they do and their valid values
complete -c build -l optimization-level -x \
-a "0\tNo optimization" \
-a "1\tBasic optimization" \
-a "2\tFull optimization" \
-a "3\tAggressive optimization" \
-d "Compiler optimization level"
Note the use of \t (tab) to separate the value from its description within individual -a arguments. This is an alternative to the two-line tab-separated format used in function output.
Best Practices for Fish Completions
- Place completions in the right directory β User-specific completions go in
~/.config/fish/completions/. System-wide completions go in/usr/share/fish/completions/or equivalent. Each command gets its own file named<command>.fish. - Use
-x(--no-files) when appropriate β If an option expects a specific value (not a file path), always use-xto prevent file completion from polluting the suggestion list. - Always pair
-land-sfor options with both forms β When an option has both a long and short form, define them together so Fish knows they are equivalent. - Write helper functions for dynamic data β Keep completion files clean by moving complex logic into helper functions prefixed with
__fish_or__<command>_. - Test completions interactively β Source your completion file with
source ~/.config/fish/completions/mycmd.fishand test with Tab immediately. - Use conditions to prevent conflicting completions β Always scope your completions with
-nconditions so users don't see irrelevant options. - Provide meaningful descriptions β A short description on every completion candidate dramatically improves usability.
- Cache expensive dynamic completions β If your completion function queries a slow API, consider caching results for the session.
Debugging Completions
Fish provides built-in debugging aids for completion development:
# Enable debug output for completions
fish --debug-level=3 --debug-categories=complete 2>&1 | grep -i completion
# List all completions registered for a command
complete -c mycmd
# Erase a completion rule (useful while iterating)
complete -c mycmd -e
# Erase all completions for a command
complete -c mycmd -e
# Then re-source your completion file to start fresh
source ~/.config/fish/completions/mycmd.fish
Real-World Example: A Complete Completion File
Below is a full, production-quality completion file for a hypothetical dbctl database management command. It demonstrates multiple techniques together:
# ~/.config/fish/completions/dbctl.fish
# Completions for dbctl β a database cluster manager
# --- Helper Functions ---
function __dbctl_list_clusters
# Fetch cluster names from the config file or API
cat ~/.dbctl/clusters.json 2>/dev/null | jq -r '.clusters[].name' 2>/dev/null
end
function __dbctl_list_databases
set -l cluster (__fish_seen_subcommand_from --print -- cluster)
if test -n "$cluster"
dbctl --cluster="$cluster" list-databases --short 2>/dev/null
else
dbctl list-databases --short 2>/dev/null
end
end
function __dbctl_list_backups
dbctl list-backups --short 2>/dev/null | string split ' ' | head -n 50
end
# --- Subcommand Completion ---
# Top-level subcommands
set -l subcommands cluster database backup migrate status help
for sub in $subcommands
complete -c dbctl -n "__fish_use_subcommand" -a "$sub" \
-d (string match -r "^$sub" "cluster\tManage clusters" \
"database\tManage databases" \
"backup\tManage backups" \
"migrate\tRun migrations" \
"status\tShow cluster status" \
"help\tShow help" | string replace -r '.*\t' '')
end
# --- Global Options (available before subcommand) ---
complete -c dbctl -l config -x -a "(__fish_complete_path)" \
-d "Path to configuration file"
complete -c dbctl -l verbose -d "Enable verbose logging"
complete -c dbctl -l help -d "Show help message"
# --- Cluster Subcommand ---
complete -c dbctl -n "__fish_seen_subcommand_from cluster; and __fish_use_subcommand" \
-a "create" -d "Create a new cluster"
complete -c dbctl -n "__fish_seen_subcommand_from cluster; and __fish_use_subcommand" \
-a "delete" -d "Delete an existing cluster"
complete -c dbctl -n "__fish_seen_subcommand_from cluster; and __fish_use_subcommand" \
-a "list" -d "List all clusters"
complete -c dbctl -n "__fish_seen_subcommand_from cluster; and __fish_use_subcommand" \
-a "info" -d "Show cluster details"
# cluster create options
complete -c dbctl -n "__fish_seen_subcommand_from cluster create" \
-l name -x -d "Name for the new cluster"
complete -c dbctl -n "__fish_seen_subcommand_from cluster create" \
-l size -x -a "small medium large" -d "Cluster size tier"
complete -c dbctl -n "__fish_seen_subcommand_from cluster create" \
-l region -x -a "(dbctl list-regions --short 2>/dev/null)" -d "Deployment region"
# cluster delete / info: complete cluster names dynamically
complete -c dbctl -n "__fish_seen_subcommand_from cluster delete info" \
-x -a "(__dbctl_list_clusters)" -d "Cluster name"
# --- Database Subcommand ---
complete -c dbctl -n "__fish_seen_subcommand_from database; and __fish_use_subcommand" \
-a "create" -d "Create a database"
complete -c dbctl -n "__fish_seen_subcommand_from database; and __fish_use_subcommand" \
-a "drop" -d "Drop a database"
complete -c dbctl -n "__fish_seen_subcommand_from database; and __fish_use_subcommand" \
-a "list" -d "List databases"
# database create/drop: require cluster context and database name
complete -c dbctl -n "__fish_seen_subcommand_from database create drop" \
-l cluster -x -a "(__dbctl_list_clusters)" -d "Target cluster"
complete -c dbctl -n "__fish_seen_subcommand_from database create" \
-x -r -d "New database name"
complete -c dbctl -n "__fish_seen_subcommand_from database drop" \
-x -a "(__dbctl_list_databases)" -d "Database to drop"
# --- Backup Subcommand ---
complete -c dbctl -n "__fish_seen_subcommand_from backup; and __fish_use_subcommand" \
-a "create" -d "Create a new backup"
complete -c dbctl -n "__fish_seen_subcommand_from backup; and __fish_use_subcommand" \
-a "restore" -d "Restore from a backup"
complete -c dbctl -n "__fish_seen_subcommand_from backup; and __fish_use_subcommand" \
-a "list" -d "List backups"
complete -c dbctl -n "__fish_seen_subcommand_from backup create" \
-l cluster -x -a "(__dbctl_list_clusters)" -d "Cluster to back up"
complete -c dbctl -n "__fish_seen_subcommand_from backup restore" \
-x -a "(__dbctl_list_backups)" -d "Backup ID to restore"
# --- Status Subcommand ---
complete -c dbctl -n "__fish_seen_subcommand_from status" \
-l watch -d "Watch status continuously"
complete -c dbctl -n "__fish_seen_subcommand_from status" \
-l json -d "Output in JSON format"
# --- Help Subcommand ---
complete -c dbctl -n "__fish_seen_subcommand_from help" \
-a "cluster database backup migrate status" -d "Topic to get help on"
Loading and Distributing Completions
Fish loads completion files automatically when a command is first typed. You don't need to manually source them β just place the file in the completions directory and Fish will find it. If you want to make completions available immediately after writing them:
# Source the completion file directly
source ~/.config/fish/completions/mycommand.fish
# Or restart the fish session
exec fish
For project-specific completions, you can add a custom completions path:
# In your config.fish
set -g fish_complete_path ~/.config/fish/completions ~/myproject/completions $fish_complete_path
Common Pitfalls and How to Avoid Them
- Forgetting
-xon non-file arguments β Without-x, Fish will offer file paths alongside your custom completions. If your option takes a specific value like a cluster name, always add-x. - Overly broad conditions β A completion without a
-ncondition fires everywhere in the command line. This can cause irrelevant options to appear. Always scope with conditions. - Not handling the empty case β Dynamic completion functions should handle the case where no data exists gracefully (return nothing, not an error).
- Tab-separated format confusion β When using
-a "value\tdescription", the\tmust be a literal tab character or the escape sequence. In fish scripts, use a real tab orprintfto generate the string. - Performance β Dynamic completions run every time the user presses Tab. If your completion function is slow, users will feel the lag. Cache results or keep queries fast.
Conclusion
Fish's completion system is a thoughtfully designed framework that turns the tedious task of writing shell completions into an enjoyable, declarative process. By mastering the complete builtin, understanding condition-based scoping, and leveraging dynamic argument sources, you can create command-line interfaces that feel polished, discoverable, and genuinely delightful to use. The investment in writing good completions pays dividends every time a user β or your future self β presses Tab and instantly finds the right option, subcommand, or value without breaking flow. Start with simple static completions, gradually add dynamic data sources and conditions, and before long you will have a completion suite that rivals the best built-in Fish completions.