← Back to DevBytes

Fish Scripting: Conditionals Complete Guide

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:

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:

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:

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

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

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles