#

Bash

Subscribe
Bash October 29, 2026·7 min read

Bash Tests and File Checks: Choose the Right Operator

In a Bash script, use [[ ... ]] for most string and file conditions, (( ... )) for integer arithmetic, and a command directly in if when its exit status is the condition. If the script must run as POSIX sh, use test or [ ... ] instead.

For example, [[ -f "$path" ]] checks for a regular file, while [[ -d "$path" ]] checks for a directory.

Choose the right Bash test: [ ], [[ ]], (( )), or a command

Bash test selection
QuestionFormExample
Did a command succeed?if commandif grep -q "enabled" config.txt; then
Is a file or string property true in portable shell code?[ ... ] or test[ -f "$path" ]
Does a file or string meet a Bash condition?[[ ... ]][[ -f "$path" ]]
Is an integer expression true?(( ... ))(( count > 3 ))

if grep -q "enabled" config.txt; then
    echo "Match found"
fi

The traditional test and [ ... ] forms receive their arguments after shell expansion. Inside [[ ... ]], an empty variable still counts as an operand, and the right side of == is a pattern. (( ... )) evaluates arithmetic.

Shell condition choices

The GNU Bash conditional-expression reference documents Bash's test operators.

Understand what “true” means to Bash

Bash exit status

A shell command succeeds with exit status 0 and fails with any nonzero status, not always 1. [ 3 -eq 3 ] succeeds and [ 3 -eq 4 ] fails. if picks its branch from that status. Arithmetic follows its own rule: (( 1 )) succeeds because the expression evaluates to a nonzero integer, and (( 0 )) fails because its value is zero.

Check a file, directory, or symbolic link

Bash file types

Someone testing a symbolic link to a configuration file asked on r/bash, “What is the point of -e if -f works exactly the same?”

A regular file satisfies both -e and -f. A directory separates them: -e succeeds and -f fails. For a dangling symlink, both fail, while -L succeeds.

Path-e-f-d-L
Regular fileTrueTrueFalseFalse
DirectoryTrueFalseTrueFalse
Symlink to a regular fileTrueTrueFalseTrue
Dangling symlinkFalseFalseFalseTrue

File tests such as -f follow a symbolic link to the target. -L (also written -h) tests the link itself.

if [[ -f "$config" ]]; then
    echo "Regular configuration file found"
fi
Dangling link behavior

Test size, access, and other file properties

File property checks
TestPropertyUseful scenario
-sExists with size greater than zeroSkip a generated report that is empty
-rFile is readableConfirm you can read a configuration file
-wFile is writableConfirm you can write to a planned output file
-xExecute access (search access for a directory)Confirm you can run a file or enter a directory

A report with zero bytes fails -s. A directory also passes -x, so use -f alongside it when you need a runnable script:

if [[ -f "$script" && -x "$script" ]]; then
    echo "Regular file with execute access"
fi

Passing a file check doesn't guarantee the later read, write, or execution will succeed. The file or its permissions can change in between.

File access limits

Bash also provides -nt and -ot to compare file modification times and -ef to test whether two paths refer to the same file.

Compare strings and handle empty or unset values

Bash string tests

In Bash, string tests cover equality, inequality, and emptiness:

[[ "$mode" == "production" ]]
[[ "$mode" != "production" ]]
[[ -z "$mode" ]]
[[ -n "$mode" ]]

With traditional brackets:

[ "$mode" = "production" ]
[ -z "$mode" ]

A beginner on r/bash testing whether a directory existed wrote, “It always defaults to the else statement.”

unset ABC
[ -d $ABC ]       # status 0
[[ -d $ABC ]]     # status 1

In [ -d $ABC ], the unquoted unset variable contributes no argument. Bash is left with [ -d ], a one-argument test of the nonempty string -d, so it succeeds. In [[ -d $ABC ]], Bash keeps the unary file-test expression intact and tests an empty pathname. The test fails, and a failed test sends the script to else.

Unset test operands

For the traditional form, keeping the operand quoted preserves the intended directory test:

[ -d "$ABC" ]

An empty variable and an unset variable are different states, though -z succeeds for both.

Know when quotes and wildcards change the test

Wildcard test syntax

Inside Bash [[ ... ]], ordinary pathname expansion does not run on operands.

One participant in r/bash put it this way: “Apparently [[ doesn't expand wildcards.”

[[ "file.txt" == *.txt ]]      # succeeds: pattern
[[ "file.txt" == "*.txt" ]]    # fails: literal text

In the first comparison, the unquoted right-hand side matches a string ending in .txt. In the second, the quotes make the asterisk literal.

Quoted wildcard examples
[[ -e foo* ]]

That expression tests the literal pathname operand instead of enumerating every file beginning with foo. An ordinary test -e foo* receives several arguments when the shell finds several matches. The extra arguments can change the test's meaning or make it invalid.

Compare integers

Integer comparison syntax

Bash reads the same characters as strings or integers, so use integer operators or arithmetic evaluation for numeric thresholds.

Traditional numeric tests use -eq, -ne, -gt, -ge, -lt, and -le. In (( ... )), the corresponding arithmetic operators include ==, !=, >, >=, <, and <=.

ExpressionWhat it comparesResult
[[ 10 < 2 ]]Lexical string orderTrue (C locale)
(( 10 < 2 ))Integer magnitudeFalse
[ 10 -lt 2 ]Integer magnitudeFalse

[[ 10 < 2 ]] compares characters, and 1 sorts before 2. Locale affects string order but not numeric comparison.

if (( count > 3 )); then
    echo "Threshold exceeded"
fi
Integer threshold example

The traditional form is [ "$count" -gt 3 ]. Both forms handle integers only.

Combine and negate tests

Combined conditional tests

Use && when two conditions must succeed, || when either one is enough, and ! to invert a result.

if [[ -f "$path" && -r "$path" ]]; then
    echo "Regular, readable file"
fi

if [ -f "$path" ] && [ -r "$path" ]; then
    echo "Regular, readable file"
fi

The first combines tests inside Bash's [[ ... ]] grammar. The second uses the shell's && operator to connect two separate [ ... ] commands.

For a regular file, [[ -f "$path" && -d "$path" ]] fails and [[ -f "$path" || -d "$path" ]] succeeds. Both operators stop evaluating as soon as the result is known.

Logical condition examples
if [[ ! -e "$path" ]]; then
    echo "Path does not resolve"
fi

For portable code, prefer [ condition ] && [ condition ] or the || form. ShellCheck warns against the older -a and -o operators inside a single traditional test, where argument parsing can be ambiguous.

Keep a test portable when the script runs under sh

Portable shell conditions

The interpreter decides which conditional syntax a script can use. A .sh extension does not select Bash. The launch command and, when honored, the shebang determine what runs the script. For portable scripts, keep POSIX test syntax separate from Bash-only double brackets and arithmetic evaluation.

#!/bin/sh

if [ -f "$path" ]; then
    echo "Regular file found"
fi

POSIX shell syntax does not require [[ ... ]], so in sh scripts use [ ... ] instead.

Standalone (( ... )) is also Bash-specific. In sh, use integer operators such as -gt inside [ ... ]. The portable $(( ... )) syntax performs arithmetic expansion and produces a value.

In dash, [[ 1 = 1 ]] returns status 127.

Interpreter portability check

ShellCheck reports [[ in sh scripts as SC3010.

Verify and debug a test before trusting it

Bash test fixture

Save this as a Bash script and run it without sourcing it into your interactive shell. GNU mktemp -d gives each run its own disposable directory.

#!/usr/bin/env bash
workdir=$(mktemp -d) || exit 1
cd "$workdir" || exit 1

: > "real file.txt"
mkdir real-dir
ln -s "real file.txt" live-link
ln -s "missing-file.txt" broken-link
unset ABC
export LC_ALL=C

[[ -f "real file.txt" ]]; printf 'regular file: %s\n' "$?"
[[ -d "real file.txt" ]]; printf 'directory test: %s\n' "$?"
[[ -e broken-link ]];    printf 'broken target: %s\n' "$?"
[[ -L broken-link ]];    printf 'symlink itself: %s\n' "$?"
[ -d $ABC ];            printf 'legacy empty operand: %s\n' "$?"
[[ -d $ABC ]];          printf 'Bash empty operand: %s\n' "$?"
[[ "file.txt" == *.txt ]]; printf 'pattern match: %s\n' "$?"
[[ "file.txt" == "*.txt" ]]; printf 'literal comparison: %s\n' "$?"
[[ 10 < 2 ]];         printf 'lexical comparison: %s\n' "$?"
(( 10 < 2 ));         printf 'integer comparison: %s\n' "$?"

The temporary directory holds a regular file with a space in its name, a directory, a live link to that file, and a link whose target is missing. Each printed value is the status of the test before it.

Conditional test results

The expected statuses, in print order, are 0, 1, 1, 0, 0, 1, 0, 1, 0, 1.

These results come from GNU Bash 5.2.37 with LC_ALL=C.

When a result surprises you, check the pathname, the variable's expansion, and which shell runs the command. Then compare one passing case with one failing case.

NoScript Team