Introduction to Fish Shell Variables
Variables in Fish shell (the friendly interactive shell) are a fundamental building block for scripting. Unlike traditional shells such as Bash, Fish takes a modern, consistent approach to variable handling that eliminates many common pitfalls. A variable in Fish is a named storage location that holds a valueβwhether a simple string, a list of strings, or an environment variable accessible to child processes.
Fish variables are created and manipulated primarily through the set command, which serves as the single, unified interface for all variable operations. This design philosophy means you never need to remember different syntaxes for declaring, updating, or destroying variables.
Why Variables Matter in Fish Scripting
π Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Variables are the backbone of any scripting language. In Fish scripting, they allow you to:
- Store configuration β paths, filenames, thresholds, and user preferences
- Capture command output β process results dynamically
- Control program flow β loop counters, condition flags, state tracking
- Pass data between functions β arguments, return values, and shared state
- Customize the shell environment β prompt colors, aliases, and key bindings
What makes Fish variables particularly valuable is their intuitive syntax. Newcomers and seasoned scripters alike benefit from reduced cognitive overheadβno more debugging silent failures caused by unquoted variable expansions or unexpected word splitting.
Understanding Variable Scopes
Fish offers three distinct scopes for variables, each serving a specific purpose. Understanding scope is critical to writing predictable scripts.
Universal Variables
Universal variables persist across all Fish sessions and are stored in a database file (typically ~/.config/fish/fish_variables). They survive shell restarts, terminal closures, and even system reboots. Use them for settings that should remain consistent everywhere.
# Set a universal variable β available in ALL fish sessions
set -U FAVORITE_EDITOR nvim
# Set a universal color for the prompt
set -U fish_color_operator yellow
# Universal variables are immediately available in other terminal windows
# without needing to source any configuration file
Global Variables
Global variables are available throughout the current shell session but are not passed to child processes unless explicitly exported. They are ideal for session-wide configuration that doesn't need persistence.
# Set a global variable (default scope when no flag is given in most contexts)
set -g PROJECT_ROOT /home/user/projects/myapp
# Global variables are accessible from any function in this session
function get_root
echo $PROJECT_ROOT
end
Local Variables
Local variables exist only within the current blockβa function, a begin..end block, or an if..end statement. They are automatically cleaned up when the block exits, preventing pollution of the outer namespace.
function process_data
# -l flag creates a local variable scoped to this function
set -l temp_file (mktemp)
set -l result ""
echo "Processing..." > $temp_file
# ... work with temp_file ...
set result (cat $temp_file)
rm -f $temp_file
echo $result
# temp_file and result are destroyed when function ends
end
# Local variables in a begin..end block
begin
set -l counter 0
set -l message "Inside block"
echo $message
# counter and message vanish after 'end'
end
Exported Variables
The -x flag exports a variable to child processes. This is how you set environment variables that commands like grep, python, or git can see. Exporting can be combined with scope flags.
# Export a variable globally so all child processes inherit it
set -gx EDITOR nvim
# Export locally β only this function's children see it
function run_python_script
set -lx PYTHONPATH /custom/libs
python3 my_script.py # python3 inherits PYTHONPATH
end
# Universal + exported (persistent and inherited everywhere)
set -Ux LANG en_US.UTF-8
Setting and Using Variables: The set Command
The set command is the only tool you need for variable management. Its basic syntax is:
set [SCOPE_FLAGS] [OPTIONS] VARIABLE_NAME VALUE
Let's explore the most common patterns:
# Basic assignment β creates a global variable by default in the shell context
set name "Alice"
echo $name # Output: Alice
# Assign multiple values (creates a list)
set colors red green blue
echo $colors # Output: red green blue
# Empty value β the variable exists but holds nothing
set empty_var ""
echo "Value: $empty_var" # Output: Value:
# No value at all β creates an empty list (count = 0)
set no_values
echo (count $no_values) # Output: 0
# Variable with whitespace
set greeting "Hello, World!"
echo $greeting # Output: Hello, World!
# Using command substitution to set a variable
set files (ls *.txt)
echo $files # Output: list of .txt files
# Capture command output with line preservation
set -l output (some_command | string collect)
Variable Expansion and Quoting
Fish handles variable expansion differently from Bash. Variables are expanded with $, but Fish never performs word splitting on variable values. This eliminates an entire class of bugs.
# Safe expansion β no word splitting, no globbing
set filename "my document with spaces.pdf"
cat $filename # Works correctly! No need for quotes here.
# However, quoting is still good practice for clarity:
cat "$filename" # Explicitly safe, same result
# Brace expansion for disambiguation
set dir "docs"
echo $dir_files # Fish looks for variable named 'dir_files' β likely empty
echo {$dir}_files # Output: docs_files β correct disambiguation
# Nested variable expansion
set prefix "base"
set varname "{$prefix}_path"
echo $$varname # Output: value of base_path variable if it exists
Quoting Rules Summary
- Single quotes: Literal string, no expansion at all β
echo '$HOME'prints$HOME - Double quotes: Allows variable expansion but prevents command substitution β
echo "$HOME"prints/home/user - No quotes: Allows both variable expansion and command substitution β use carefully
# Single quotes = everything literal
echo 'Price: $100 (no expansion)' # Output: Price: $100 (no expansion)
# Double quotes = variables expanded, commands NOT executed
set name "Bob"
echo "Hello $name, today is (date)" # Output: Hello Bob, today is (date)
# Note: (date) is literal inside double quotes
# No quotes = everything fires
echo Hello $name, today is (date) # Output: Hello Bob, today is Fri Mar 15...
Lists and Arrays in Fish
Fish variables are inherently list-based. A variable can hold zero, one, or many values. This is one of Fish's most powerful features and differs significantly from other shells where arrays require special syntax.
# Creating lists
set fruits apple banana cherry
echo $fruits # Output: apple banana cherry
# Access by index (1-indexed, like most human-friendly things)
echo $fruits[1] # Output: apple
echo $fruits[2] # Output: banana
echo $fruits[-1] # Output: cherry (last element, negative indexing!)
# Slicing
echo $fruits[2..3] # Output: banana cherry
echo $fruits[1..-2] # Output: apple banana
# Count elements
echo (count $fruits) # Output: 3
# Check if list contains a value
if contains banana $fruits
echo "Found banana!"
end
# Iterating over lists
for fruit in $fruits
echo "Current fruit: $fruit"
end
# Appending to a list
set -a fruits dragonfruit
echo $fruits # Output: apple banana cherry dragonfruit
# Prepending to a list
set -p fruits avocado
echo $fruits # Output: avocado apple banana cherry dragonfruit
# Removing by value (removes ALL matching elements)
set -e fruits[2] # Remove element at index 2
echo $fruits # avocado banana cherry dragonfruit
# Remove specific value
set fruits (string match -v "cherry" $fruits)
echo $fruits # avocado banana dragonfruit
# Merging lists
set list1 a b c
set list2 d e f
set combined $list1 $list2
echo $combined # Output: a b c d e f
Advanced List Operations
# List filtering with string commands
set numbers 1 2 3 4 5 6 7 8 9 10
set even (string match -r "[02468]$" $numbers)
echo $even # Output: 2 4 6 8 10
# Transforming lists
set upper (string upper $fruits)
echo $upper # Output: AVOCADO BANANA DRAGONFRUIT
# List length manipulation
set long_list (seq 1 100)
echo (count $long_list) # Output: 100
# Empty list handling
set empty_list
echo (count $empty_list) # Output: 0
# Test for empty
if test (count $empty_list) -eq 0
echo "List is empty"
end
Special Fish Variables
Fish provides built-in variables that give you access to shell internals and control behavior.
# $status β exit status of the last command
false
echo $status # Output: 1
true
echo $status # Output: 0
# $argv β arguments passed to a function or script
function show_args
echo "Number of args: "(count $argv)
for arg in $argv
echo "Arg: $arg"
end
end
show_args hello world # Output: Number of args: 2, then each arg
# $fish_pid β the process ID of the current shell
echo "Fish PID: $fish_pid"
# $CMD_DURATION β execution time of last command in milliseconds
sleep 1
echo "Took $CMD_DURATION ms"
# $COLUMNS and $LINES β terminal dimensions
echo "Terminal: $COLUMNS columns x $LINES lines"
# $PWD β current working directory (auto-updates)
cd /tmp
echo $PWD # Output: /tmp
# $HOME β user's home directory
echo $HOME # Output: /home/username
Color and Prompt Variables
# Fish color variables (universal by convention)
set -U fish_color_normal white
set -U fish_color_command blue
set -U fish_color_error red
set -U fish_color_param cyan
set -U fish_color_quote yellow
# Custom prompt via fish_prompt function
function fish_prompt
set -l last_status $status
set -l prompt_symbol "β― "
# Use color variables
echo -n (set_color $fish_color_cwd)(prompt_pwd)(set_color normal)
echo -n $prompt_symbol
end
Environment Variables Deep Dive
Environment variables are passed from parent processes to child processes. Fish gives you fine-grained control over what gets inherited.
# View all environment variables
env
# Setting a persistent environment variable (universal + export)
set -Ux JAVA_HOME /usr/lib/jvm/java-11-openjdk
# Setting session-only environment variable
set -gx DEBUG_MODE 1
# Temporarily modify PATH
set -l original_path $PATH
set -gx PATH /custom/bin $PATH
# ... do work ...
set -gx PATH $original_path # Restore
# PATH manipulation helpers
fish_add_path /usr/local/bin # Add to PATH if not already present
fish_add_path -p /opt/bin # Prepend to PATH
fish_add_path -m /opt/bin # Move to front if already in PATH
# Remove from PATH
set -g PATH (string match -v "/bad/path" $PATH)
# Inheriting environment in scripts
# When you run a script with './script.fish', it sees exported variables
# from the parent shell automatically
Variable Testing and Conditionals
Fish integrates variables seamlessly with its conditional constructs.
# Test if variable is defined
if set -q myvar
echo "myvar exists"
else
echo "myvar does not exist"
end
# Test if variable is empty
set myvar ""
if test -z "$myvar"
echo "myvar is empty string"
end
# Test list length
set mylist a b c
if test (count $mylist) -gt 0
echo "mylist has elements"
end
# Using variables in switch statements
set os_type "linux"
switch $os_type
case linux
echo "Running on Linux"
case macos
echo "Running on macOS"
case '*'
echo "Unknown OS"
end
# String comparison with variables
set name1 "Alice"
set name2 "Bob"
if test "$name1" = "$name2"
echo "Names match"
else
echo "Names differ"
end
# Numeric comparison
set count 42
if test $count -gt 10
echo "Count is greater than 10"
end
Variable Persistence and Configuration
Universal variables are stored automatically. You can inspect and manage them directly.
# List all universal variables
set -U --names
# Show value of a specific universal variable
set -U --show FAVORITE_EDITOR
# Remove a universal variable
set -e -U FAVORITE_EDITOR
# Universal variables are loaded on shell startup
# Location of stored variables:
# ~/.config/fish/fish_variables (per-user)
# Can also be set in config.fish for session initialization
# Example config.fish snippet
if status is-interactive
set -U fish_greeting "" # Disable greeting
set -gx EDITOR nvim # Session editor
set -U hydro_color_prompt blue # Persistent prompt color
end
Best Practices for Fish Variables
1. Use the Right Scope
- Universal (-U) for preferences that span sessions (colors, editor choice, API keys)
- Global (-g) for session-wide configuration (project roots, debug flags)
- Local (-l) for function-internal temporary values (counters, temp files)
- Exported (-x) only when child processes genuinely need the value
# Good: Local scope for temporary work
function calculate_stats
set -l intermediate_result 0
set -l temp_file (mktemp)
# ... processing ...
rm -f $temp_file
end
# Good: Universal for persistent preferences
set -U PREFERRED_THEME dark
# Avoid: Using global for temporary function data
# This pollutes the namespace and can cause conflicts
2. Name Variables Descriptively
# Good names
set project_root "/home/user/projects/webapp"
set backup_destination "/mnt/backup/daily"
set max_retry_count 5
# Bad names (unclear, ambiguous)
set x "/home/user/projects/webapp"
set dest "/mnt/backup/daily"
set cnt 5
# Use UPPER_CASE for environment/universal variables
set -gx DATABASE_URL "postgresql://localhost/mydb"
set -U DEFAULT_USER "admin"
# Use lower_case or snake_case for local/global script variables
set -l current_index 0
set -g config_path "$HOME/.config/myapp"
3. Avoid Unnecessary Exporting
# Only export if child processes need it
set -gx SSH_AUTH_SOCK "$SSH_AUTH_SOCK" # SSH needs this
set -gx PKG_CONFIG_PATH "/opt/lib/pkgconfig" # Build tools need this
# Don't export internal script variables
set -g script_state "initializing" # No -x flag needed
set -l loop_counter 0 # Local, definitely no export
4. Handle Empty and Undefined Variables Gracefully
# Always check before using optional variables
if set -q OPTIONAL_CONFIG
echo "Using config: $OPTIONAL_CONFIG"
else
echo "No optional config provided, using defaults"
end
# Provide defaults with conditional expansion
set output_dir (set -q CUSTOM_OUTPUT_DIR && echo $CUSTOM_OUTPUT_DIR || echo "/default/output")
# Or use the simpler approach:
if not set -q OUTPUT_DIR
set OUTPUT_DIR "/default/output"
end
5. Quote When in Doubt
# Fish is forgiving, but explicit quoting aids readability
set filename "report draft v2.pdf"
cp $filename backups/ # Works in Fish, but...
cp "$filename" backups/ # ...this is clearer to readers
# Always quote in test conditions
if test "$user_input" = "delete"
echo "Dangerous operation requested"
end
# Use single quotes for regex patterns and literal strings
set pattern '^[A-Za-z_][A-Za-z0-9_]*$'
6. Leverage Fish's List Nature
# Fish variables are lists by default β embrace it
set targets host1 host2 host3
for target in $targets
ssh $target "run_update"
end
# Instead of building strings with spaces and splitting later,
# keep data in its natural list form
set packages nginx postgresql redis
sudo apt-get install $packages
7. Clean Up After Yourself
# Local variables auto-clean, but for globals, explicitly remove when done
function setup_environment
set -g TEMP_CONFIG "/tmp/setup_config_$$"
# ... use TEMP_CONFIG ...
set -e TEMP_CONFIG # Explicit cleanup
end
# For temporary files combined with variables
function process_files
set -l work_dir (mktemp -d)
# ... work ...
rm -rf $work_dir
# work_dir auto-cleaned as local variable
end
Common Pitfalls and How to Avoid Them
Pitfall 1: Confusing Variable Creation with Assignment
# Wrong: This looks like assignment but creates a new local
function update_count
set count 10 # Creates LOCAL 'count', doesn't touch global
end
set -g count 0
update_count
echo $count # Still 0! The function created its own local
# Correct: Specify scope explicitly
function update_count
set -g count 10 # Now modifies the global
end
set -g count 0
update_count
echo $count # Now 10
Pitfall 2: Forgetting that Fish Lists are 1-Indexed
set items first second third
echo $items[1] # 'first' (not 'second' like in 0-indexed languages)
echo $items[0] # Returns nothing! Fish ignores index 0
Pitfall 3: Variable Expansion in Different Quotes
set secret "password123"
echo 'Using $secret' # Literal: Using $secret
echo "Using $secret" # Expanded: Using password123
# Be careful with secrets in logs β prefer single quotes or explicit handling
Pitfall 4: Command Substitution Timing
# Command substitution happens at assignment time
set timestamp (date)
sleep 2
echo $timestamp # Shows OLD date, not current
# Use functions or re-evaluate if you need current values:
echo (date) # Always fresh
Advanced Variable Techniques
Dynamic Variable Names
# Indirect variable access using dereferencing
set var_name "dynamic_var"
set $var_name "Hello from dynamic!"
echo $$var_name # Output: Hello from dynamic!
# Building variable names programmatically
for i in (seq 1 5)
set "item_$i" "Value $i"
end
echo $item_3 # Output: Value 3
Variable Binding with read
# Reading user input into variables
read -p "Enter your name: " user_name
echo "Hello, $user_name!"
# Reading multiple values
read -p "Enter first and last name: " first last
echo "First: $first, Last: $last"
# Reading into a list
read -p "Enter colors (space-separated): " -a color_list
echo (count $color_list) " colors entered"
Using Variables with string Commands
# Fish's string manipulation with variables
set url "https://example.com/api/v2/users"
set protocol (string match -r '^[^:]+' $url)
echo $protocol # Output: https
set domain (string replace -r '^https?://([^/]+).*' '$1' $url)
echo $domain # Output: example.com
# Splitting strings into lists
set csv "apple,banana,cherry"
set items (string split "," $csv)
echo $items # Output: apple banana cherry
# Joining lists back
set joined (string join " :: " $items)
echo $joined # Output: apple :: banana :: cherry
Conclusion
Fish shell variables represent a thoughtful evolution in shell scripting design. By unifying variable operations under the set command, eliminating word splitting, and embracing lists as the fundamental data type, Fish removes entire categories of scripting errors that plague traditional shells. The clear separation of universal, global, and local scopes gives you precise control over variable lifetime and visibility, while the export flag keeps environment management explicit and intentional.
Whether you are writing a simple configuration snippet in config.fish, building a complex multi-function script, or crafting an interactive prompt, mastering Fish variables will make your shell experience smoother and your scripts more robust. Start with the right scope, name things clearly, leverage lists naturally, and let Fish's sane defaults work in your favor. The time invested in understanding these concepts will pay dividends in cleaner, more maintainable, and less frustrating shell scripting.