Fish Scripting: Conditionals Complete Guide
Conditionals are the decision-making backbone of any scripting language. In Fish shell, they take a refreshingly clean, readable form that eliminates the cryptic syntax found in traditional shells like Bash. This guide covers everything you need to know — from basic if statements to complex switch blocks, operators, file tests, and advanced patterns.
What Are Conditionals in Fish?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Conditionals allow your script to make choices and branch execution based on whether a condition evaluates to true or false. In Fish, a conditional always lives inside an if / end block or a switch / end block. The condition itself is a command that returns an exit status: 0 means true (success), and anything else means false (failure). This is consistent with Unix convention but inverted from most programming languages — a point worth remembering.
The Core Principle: Exit Status as Truth
Every command in Fish returns an exit status. The if statement checks this status. If the command succeeds (exit 0), the condition is true. If it fails (exit 1 or higher), the condition is false. The test command and bracket syntax [ ... ] are specifically designed to evaluate expressions and return the appropriate exit status.
# The test command returns 0 when the expression is true
if test -f "/etc/passwd"
echo "File exists"
end
Why Fish Conditionals Matter
Fish conditionals stand out for several compelling reasons:
- Readability: No more cryptic
fi,esac, or]]terminators. Fish uses plainendto close blocks. - Consistency: The same
endkeyword closesif,for,while, andswitchblocks — one rule to remember. - No confusing quoting rules: Fish variables expand safely without the quoting nightmares of Bash.
- Built-in operators: Fish provides
andandoras first-class command combinators, not just syntax inside brackets. - Powerful
switch: Pattern matching with globs and regex makes multi-way branching elegant.
How to Use Fish Conditionals
The Basic if Statement
The simplest form runs a command or test and executes the body if the condition is true. The block always terminates with end.
if test -d "$HOME/.config"
echo "Config directory found"
end
You can also use the bracket form, which is syntactic sugar for the test command. Note: in Fish, brackets require a space after [ and before ], just like in Bash.
if [ -d "$HOME/.config" ]
echo "Config directory found"
end
Adding else and else if
Fish uses else and else if exactly as you would expect. There is no elif abbreviation — use the full else if.
set os_name (uname)
if test "$os_name" = "Linux"
echo "Running on Linux"
else if test "$os_name" = "Darwin"
echo "Running on macOS"
else
echo "Running on something else: $os_name"
end
Numeric Comparisons
Fish uses the standard POSIX numeric operators. These work exclusively with integers:
-eq— equal-ne— not equal-gt— greater than-lt— less than-ge— greater than or equal-le— less than or equal
set count 42
if test "$count" -gt 100
echo "Large number"
else if test "$count" -lt 10
echo "Small number"
else
echo "Medium number between 10 and 100"
end
You can also use math to compute comparisons inline. The math command returns exit status 0 when the expression is non-zero (true), and 1 when zero (false):
set a 5
set b 10
if math "$a > $b" >/dev/null
echo "$a is greater than $b"
else
echo "$a is not greater than $b"
end
String Comparisons
String equality uses a single = sign. Inequality uses !=. These work with test or brackets.
set name "fish"
if [ "$name" = "fish" ]
echo "The name is fish"
end
if test "$name" != "bash"
echo "The name is not bash"
end
For empty string checks, use -z (zero-length) and -n (non-zero-length):
set maybe_empty ""
if test -z "$maybe_empty"
echo "Variable is empty"
end
if test -n "$HOME"
echo "HOME is set and non-empty"
end
File and Directory Tests
Fish inherits the full suite of file test operators. These are incredibly useful for scripts that manipulate the filesystem:
-f— true if path is a regular file-d— true if path is a directory-x— true if path is executable-r— true if path is readable-w— true if path is writable-e— true if path exists (any type)-L— true if path is a symbolic link-s— true if path exists and has size greater than zero
set config_path "$HOME/.config/fish/config.fish"
if test -f "$config_path"
echo "Config file exists"
if test -r "$config_path"
echo "Config file is readable"
end
if test -s "$config_path"
echo "Config file is not empty"
end
else
echo "Config file does not exist, creating it..."
mkdir -p (dirname "$config_path")
touch "$config_path"
end
Combining Conditions with and and or
Fish provides and and or as command combinators, not just operators inside brackets. They chain commands left-to-right with short-circuit evaluation. This is one of Fish's most elegant features.
# Both conditions must succeed
if test -f "/tmp/myfile" && test -r "/tmp/myfile"
echo "File exists and is readable"
end
# Either condition succeeding is enough
if test -f "/etc/debian_version" || test -f "/etc/redhat-release"
echo "This appears to be a Linux system"
end
You can mix and / or with any commands, not just test:
# Run a command and check its output
if command -v git >/dev/null && git rev-parse --git-dir >/dev/null 2>&1
echo "Git is installed and we are in a git repository"
end
For bracket-style compound conditions, you can use -a (AND) and -o (OR), but the && / || combinators are preferred for readability:
# Bracket style (less preferred)
if [ -f "$file" -a -r "$file" ]
echo "File exists and is readable"
end
# Combinator style (preferred)
if test -f "$file" && test -r "$file"
echo "File exists and is readable"
end
Negation with not
The not keyword inverts the exit status of any command. It turns success (0) into failure (1) and vice versa:
if not test -f "/tmp/lockfile"
echo "Lock file is absent, proceeding..."
touch "/tmp/lockfile"
end
# Equivalent to
if ! test -f "/tmp/lockfile"
echo "Lock file is absent, proceeding..."
end
The switch Statement
When you need to match a value against multiple patterns, switch is far cleaner than cascading else if chains. Fish supports both glob patterns and regular expressions in case clauses.
Basic switch with String Literals
set fruit "apple"
switch "$fruit"
case "apple"
echo "Selected apple"
case "banana"
echo "Selected banana"
case "orange"
echo "Selected orange"
case "*"
echo "Unknown fruit: $fruit"
end
Glob Pattern Matching
The case clause accepts glob patterns by default. The wildcards * and ? work exactly as they do in file globbing:
set filename "document.txt"
switch "$filename"
case "*.txt"
echo "Text file detected"
case "*.jpg" "*.png" "*.gif"
echo "Image file detected"
case "*.sh" "*.fish"
echo "Shell script detected"
case "*"
echo "Unknown file type"
end
Multiple Patterns in One Case
List multiple patterns separated by spaces in a single case line. The first matching case wins:
set command "help"
switch "$command"
case "help" "h" "--help" "-h"
echo "Showing help..."
case "version" "v" "--version" "-v"
echo "Showing version..."
case "*"
echo "Unknown command: $command"
end
Regular Expression Matching
Prefix a case pattern with --regex to use full regular expressions. The match is tested against the entire string:
set email "user@example.com"
switch "$email"
case --regex "[a-z]+@[a-z]+\.com"
echo "Simple .com email address"
case --regex "[a-z.]+@[a-z.]+"
echo "General email pattern matched"
case "*"
echo "Not a valid-looking email"
end
Capturing Match Groups
When using --regex, you can capture groups and access them via the fish_match variables:
set url "https://example.com/path/to/page"
switch "$url"
case --regex '^(https?)://([^/]+)/(.*)$'
echo "Protocol: $fish_match[2]"
echo "Domain: $fish_match[3]"
echo "Path: $fish_match[4]"
end
# Output:
# Protocol: https
# Domain: example.com
# Path: path/to/page
Advanced Patterns and Techniques
Using begin / end Inside Conditionals
When you need multiple commands in a condition body, simply list them. For complex logic or when you want to capture output, wrap them in begin / end:
if test -f "/tmp/data.txt"
begin
set content (cat "/tmp/data.txt")
set lines (count $content)
echo "Processing $lines lines of data"
end
else
echo "No data file found"
end
Conditional Execution Without if
Fish allows and and or as standalone command combinators outside of if blocks. This is perfect for simple conditional execution:
# Run command only if previous succeeded
test -d "$HOME/.local/bin" && set -p PATH "$HOME/.local/bin"
# Run command only if previous failed
ping -c 1 google.com >/dev/null || echo "No internet connection"
# Ternary-like pattern
test -f "$file" && echo "exists" || echo "missing"
Checking Command Success
Any command can serve as a condition. This is powerful for checking whether external programs succeed:
if which git >/dev/null
echo "Git is available at: "(which git)
end
if systemctl is-active --quiet nginx
echo "Nginx is running"
else
echo "Nginx is stopped, starting it..."
systemctl start nginx
end
Variable-Based Conditions
Fish variables are lists by default. Use test with -z / -n or check the count directly:
set my_list apple banana cherry
if test (count $my_list) -gt 0
echo "List has elements: $my_list"
end
# Check if a variable is defined at all
if set -q my_list
echo "Variable my_list is set"
end
Nested Conditionals
Fish handles nesting cleanly — each if must have its own end:
if test -d "$HOME/projects"
echo "Projects directory exists"
if test -f "$HOME/projects/README.md"
echo "Found README in projects"
if grep -q "TODO" "$HOME/projects/README.md"
echo "README contains TODO items"
end
end
end
Best Practices
- Prefer
testover brackets for complex expressions. Brackets are fine for simple checks, buttestwith&&/||combinators scales better and reads more naturally. - Use
switchfor three or more branches. Aswitchblock is almost always clearer than a chain ofelse ifstatements. - Quote variables in
testand bracket expressions. Even though Fish handles empty variables better than Bash, quoting ensures the test command receives exactly one argument. - Use
set -qto check if a variable exists rather than comparing to an empty string, which can be ambiguous. - Leverage
and/orfor short-circuit chains. They make scripts concise and idiomatic:test -f file && cat file || echo "missing". - Add a wildcard
case '*'as a fallback inswitchblocks. This catches unexpected input gracefully instead of silently doing nothing. - Keep conditions simple. If a condition becomes hard to read, extract it into a variable or a separate
beginblock that computes a boolean-like result. - Remember exit status semantics: 0 = true/success, non-zero = false/failure. This is the opposite of most languages. When in doubt, test your condition in an interactive Fish session first.
Complete Real-World Example
Here is a practical script that demonstrates multiple conditional techniques working together — a system status checker:
#!/usr/bin/env fish
function check_system
set os (uname)
echo "=== System Check for $os ==="
# Check OS type with switch
switch "$os"
case "Linux"
echo "✓ Linux detected"
set package_manager "unknown"
if test -f "/etc/debian_version"
set package_manager "apt"
else if test -f "/etc/redhat-release"
set package_manager "yum"
else if test -f "/etc/arch-release"
set package_manager "pacman"
end
echo " Package manager: $package_manager"
case "Darwin"
echo "✓ macOS detected"
if command -v brew >/dev/null
echo " Homebrew is available"
set brew_count (brew list --formula 2>/dev/null | wc -l | string trim)
echo " Installed formulae: $brew_count"
else
echo " ⚠ Homebrew not found — consider installing it"
end
case "*"
echo "⚠ Unrecognized OS: $os"
end
# Check disk space
echo ""
echo "--- Disk Space ---"
if test "$os" = "Linux" || test "$os" = "Darwin"
set df_output (df -h / | tail -1)
set available (echo $df_output | awk '{print $4}')
set used_percent (echo $df_output | awk '{print $5}' | tr -d '%')
echo " Root partition available: $available"
if test "$used_percent" -gt 90
echo " ⚠ CRITICAL: Disk usage above 90%!"
else if test "$used_percent" -gt 70
echo " ⚠ Warning: Disk usage above 70%"
else
echo " ✓ Disk usage is healthy"
end
end
# Check essential directories
echo ""
echo "--- Essential Paths ---"
set dirs_to_check $HOME/.config $HOME/.local/bin /tmp
for dir in $dirs_to_check
if test -d "$dir"
if test -r "$dir" && test -w "$dir"
echo " ✓ $dir — exists, readable, writable"
else
echo " ⚠ $dir — exists but permissions may be restricted"
end
else
echo " ✗ $dir — does not exist"
end
end
# Check for critical tools
echo ""
echo "--- Critical Tools ---"
set tools git curl python3 node
set missing_tools 0
for tool in $tools
if command -v $tool >/dev/null
set version (command -v $tool >/dev/null && $tool --version 2>/dev/null | head -1 | string trim)
echo " ✓ $tool found"
else
echo " ✗ $tool NOT found"
set missing_tools (math "$missing_tools + 1")
end
end
if test "$missing_tools" -gt 0
echo ""
echo "⚠ $missing_tools critical tool(s) missing"
else
echo ""
echo "✓ All critical tools are installed"
end
echo ""
echo "=== Check Complete ==="
end
check_system
Common Pitfalls and How to Avoid Them
- Forgetting spaces around brackets:
[ -f file ]works;[-f file]does not. Always put spaces after[and before]. - Using Bash-style
[[ ]]: Fish does not support[[ ]]. Usetestor[ ]instead. - Confusing
=and==: Fish uses a single=for string equality intest. The double==is not recognized. - Mixing
else ifwithelif: Fish only acceptselse if(two words). Usingelifis a syntax error. - Forgetting
end: Everyifandswitchmust have a correspondingend. Fish will report a clear error at parse time if you forget one. - Using
-a/-oinside brackets with complex expressions: These are fragile. Prefer&&/||between separatetestcommands.
Conclusion
Fish conditionals combine the full power of Unix exit-status logic with a syntax that prioritizes human readability. The if / else if / else / end structure is immediately familiar, while switch with glob and regex matching provides expressive multi-way branching. The and / or combinators let you chain commands with short-circuit evaluation cleanly. By mastering these constructs, you can write shell scripts that are both powerful and pleasant to maintain — a rare combination in the world of shell scripting. Start with simple test checks, graduate to switch blocks for complex dispatch, and always let Fish's clear error messages guide you when something goes wrong.