Bash Check if File or Directory Exists

Check if a file or directory exists in Bash with -f, -d, -e, and -L. Covers symlinks, empty files, permissions, globs, and common errors.

PublishedAuthorJoshua JamesRead time9 minGuide typeUbuntu

Good Bash scripts check paths before they assume a config file, log directory, upload target, or optional dependency is available. The safest way to check if a file or directory exists in Bash is to use the file-test operators built into test, [ ... ], or Bash’s [[ ... ]] conditional expression.

The key is choosing the right test for the job. Use -f for regular files, -d for directories, -e for any existing path, and -L when the path must be a symbolic link. From there, you can build checks that handle missing paths, empty files, permissions, globs, and common script errors without guessing.

Use [ ... ] when a script needs POSIX-style portability, and use [[ ... ]] when the script is explicitly Bash. Bash supports both forms, but [[ ... ]] is not available in plain /bin/sh.

Understand Bash File and Directory Tests

Bash conditionals return an exit status. A valid true test returns 0, and a valid false test returns 1. The if statement does not read printed text; it reads that exit status and decides which branch to run.

The Bash conditional expressions manual documents the file-test operators used by [[ ... ]], test, and [ ... ]. GNU Coreutils also documents the external test utility, including the important spacing rule: each part of a test expression must be a separate argument.

Choose the Right Bash File Test Operator

Most existence checks come down to one of these operators:

TaskOperatorUse When
Check a regular file-f pathThe resolved path must be a normal file, not a directory, device, socket, or pipe.
Check a directory-d pathThe resolved path must be a directory that the script can treat as a folder.
Check any existing path-e pathAny existing file type is acceptable, including directories, devices, sockets, and named pipes.
Check a symbolic link-L path or -h pathThe path itself must be a symlink, including a broken symlink.
Check a non-empty file-s pathThe resolved file must exist and have a size greater than zero.
Check read access-r pathThe current user must be able to read the path.
Check write access-w pathThe current user must be able to write to the path.
Check execute or search access-x pathThe current user must be able to execute a file or enter/search a directory.

Except for -L and -h, Bash file tests normally follow symbolic links and test the target. A symlink to a regular file can pass -f, while a broken symlink can pass -L but fail -e.

Use -e only when the path type truly does not matter. A script that expects a config file should prefer -f; a script that writes logs into a folder should prefer -d. That small distinction prevents a directory, socket, or device from passing a check meant for a normal file.

Compare test, Single Brackets, and Double Brackets

Bash gives you three common syntaxes for the same basic file-test idea:

test -f "$path"
[ -f "$path" ]
[[ -f "$path" ]]

The single-bracket form is the most familiar in portable shell scripts. The double-bracket form is a Bash keyword with cleaner compound expressions, pattern matching, and fewer quoting pitfalls for string tests, but quote path variables anyway so your intent stays obvious.

SyntaxBest UseImportant Detail
test -f "$path"Readable POSIX-style checks.No closing bracket is used.
[ -f "$path" ]Portable scripts and simple examples.The spaces after [ and before ] are required.
[[ -f "$path" ]]Bash-only scripts with compound conditions.Do not use it in scripts that run with /bin/sh.

Create a Small Bash Test Fixture

A disposable practice directory keeps the examples predictable. It creates one config file, one empty file, one directory, one valid symlink, and one broken symlink under ~/bash-exists-demo:

demo="$HOME/bash-exists-demo"
mkdir -p "$demo/logs"
printf 'port=8080\n' > "$demo/config.ini"
: > "$demo/empty.log"
rm -f "$demo/config-link.ini" "$demo/broken-link"
ln -s "config.ini" "$demo/config-link.ini"
ln -s "missing-target" "$demo/broken-link"

The mkdir command in Linux covers directory creation in more depth, including -p, parent paths, permissions, and common file-exists errors.

Check if a File Exists in Bash

Use -f when the resolved path must be a regular file. This is the right default for config files, scripts, text files, downloads, and other ordinary file paths.

Check a Regular File with -f

file="$HOME/bash-exists-demo/config.ini"

if [ -f "$file" ]; then
    printf 'regular file found\n'
else
    printf 'regular file missing\n'
fi

The configured fixture returns:

regular file found

The same check works as a compact one-liner when you only need one action on success:

[ -f "$HOME/bash-exists-demo/config.ini" ] && printf 'config exists\n'
config exists

Use a full if block when the script needs both success and failure branches. It reads better once more than one action depends on the result.

Check Any Existing Path with -e

Use -e when a file, directory, socket, pipe, or device should all count as “present.” It answers “does this path exist?” rather than “is this path a normal file?”

path="$HOME/bash-exists-demo/logs"

if [ -e "$path" ]; then
    printf 'path exists\n'
fi
path exists

That output is correct because logs exists, but it is a directory. If your next action expects a regular file, use -f instead.

Check That a File Does Not Exist

Put ! before the file test to invert it. This pattern is useful before creating a missing config, writing a default file, or skipping a task that would overwrite existing data.

file="$HOME/bash-exists-demo/missing.conf"

if [ ! -f "$file" ]; then
    printf 'regular file is missing\n'
fi
regular file is missing

Keep a space after !. In [ ... ], every operator and operand is a separate argument, so [!-f "$file"] is not valid shell syntax.

Check if a Directory Exists in Bash

Use -d when the path must be a directory. This matters before writing logs, copying files into a destination, reading a plugin folder, or entering a project path.

Check a Directory with -d

dir="$HOME/bash-exists-demo/logs"

if [ -d "$dir" ]; then
    printf 'directory found\n'
else
    printf 'directory missing\n'
fi
directory found

Directories need execute permission for traversal. If -d succeeds but the script still cannot list or write inside the directory, check permissions with ls -ld "$dir" and review the chmod command in Linux before changing modes.

Create a Directory Only When Missing

For most scripts, mkdir -p is simpler than testing first because it succeeds when the directory already exists and creates missing parent directories when needed:

dir="$HOME/bash-exists-demo/cache"
mkdir -p "$dir"

if [ -d "$dir" ]; then
    printf 'cache directory is ready\n'
fi
cache directory is ready

If a non-directory file already uses that name, mkdir -p fails instead of pretending the path is usable. That failure is helpful because it catches a real collision.

Existence is often only the first question. A script may need to know whether a path is a symlink, whether a file has content, or whether the current user can read, write, or execute it.

Detect a Symbolic Link with -L

Use -L or -h when the symlink itself matters. Bash normally follows symlinks for file tests, except for -L and -h, which test the link path directly.

link="$HOME/bash-exists-demo/config-link.ini"

if [ -L "$link" ]; then
    printf 'symbolic link found\n'
fi
symbolic link found

This distinction is important for broken links. A broken symlink fails -e because its target does not exist, but it still passes -L because the link object exists.

broken="$HOME/bash-exists-demo/broken-link"

if [ -L "$broken" ] && [ ! -e "$broken" ]; then
    printf 'broken symlink found\n'
fi
broken symlink found

GNU Coreutils calls out the same behavior for file type tests: -L and -h do not dereference the symlink the way other file-related tests usually do.

Check Whether a File Is Empty with -s

Use -s when the file must exist and contain at least one byte. This catches empty log files, incomplete downloads, and placeholder files.

file="$HOME/bash-exists-demo/config.ini"

if [ -s "$file" ]; then
    printf 'file has content\n'
fi
file has content

The empty fixture file fails the same test:

file="$HOME/bash-exists-demo/empty.log"

if [ ! -s "$file" ]; then
    printf 'file is missing or empty\n'
fi
file is missing or empty

If you need to distinguish “missing” from “empty,” split the tests so each branch says exactly what happened.

file="$HOME/bash-exists-demo/empty.log"

if [ ! -e "$file" ]; then
    printf 'file is missing\n'
elif [ ! -s "$file" ]; then
    printf 'file exists but is empty\n'
else
    printf 'file exists and has content\n'
fi
file exists but is empty

Check Read, Write, and Execute Access

Use -r, -w, and -x when permissions are part of the decision. These tests answer whether the current process has the relevant access, not whether a permission bit is visually present in ls -l.

file="$HOME/bash-exists-demo/config.ini"

if [ -r "$file" ] && [ -w "$file" ]; then
    printf 'file is readable and writable\n'
fi
file is readable and writable

For directories, -x means search permission. Without it, a user may see the directory name but still fail to enter it or access names inside it.

Combine Bash File and Directory Tests

Real scripts often need more than one condition. You can combine separate bracket tests with shell operators, or use Bash’s [[ ... ]] form for a single compound expression.

Require Multiple Paths with AND Logic

Use && when every condition must pass:

config="$HOME/bash-exists-demo/config.ini"
log_dir="$HOME/bash-exists-demo/logs"

if [ -f "$config" ] && [ -d "$log_dir" ]; then
    printf 'config and log directory are ready\n'
fi
config and log directory are ready

Accept Either Path with OR Logic

Use || when one successful condition is enough:

primary="$HOME/bash-exists-demo/primary.conf"
fallback="$HOME/bash-exists-demo/config.ini"

if [ -f "$primary" ] || [ -f "$fallback" ]; then
    printf 'at least one config file exists\n'
fi
at least one config file exists

Use Bash Double Brackets for Compound Tests

In Bash-only scripts, [[ ... ]] keeps compound tests compact:

path="$HOME/bash-exists-demo/config.ini"

if [[ -e "$path" && ! -d "$path" ]]; then
    printf 'path exists and is not a directory\n'
fi
path exists and is not a directory

For scripts that also need user prompts, menus, or confirmations, the read command in Linux pairs naturally with these checks.

Check Files with Globs and Patterns in Bash

A file test checks one path. It does not search a directory tree, and it does not automatically mean “any file matching this pattern.” Globs such as *.log expand before the test runs, which can surprise scripts when no files match.

Check Whether Any Matching Files Exist

For Bash scripts, enable nullglob in a small scope, store the matches in an array, and test the array length:

(
    shopt -s nullglob
    matches=("$HOME"/bash-exists-demo/*.ini)

    if ((${#matches[@]})); then
        printf 'found %d ini pathname(s)\n' "${#matches[@]}"
    else
        printf 'no ini pathnames found\n'
    fi
)
found 2 ini pathname(s)

The fixture has one regular .ini file and one .ini symlink, so the glob reports two matching pathnames. Add a file-type test inside a loop if you need only regular files.

The parentheses run the check in a subshell, so nullglob does not stay enabled in the caller’s shell. That keeps the example from changing later glob behavior in the same terminal session.

Avoid Testing an Unmatched Glob Literally

Without nullglob, an unmatched pattern can stay literal. A test such as [ -f "$HOME/bash-exists-demo/*.conf" ] checks for a file whose name actually contains an asterisk, not for any .conf file.

Use arrays for Bash glob checks, or use find when you need recursive matching, depth limits, or actions on many paths. The find -exec command option in Linux is the better tool when matching files is only the first step before processing them.

Search File Content After the Path Exists

Existence checks only confirm that the file path is usable. If the next question is whether a file contains a setting, check the path first and then search the content:

file="$HOME/bash-exists-demo/config.ini"

if [ -f "$file" ] && grep -q '^port=' "$file"; then
    printf 'port setting found\n'
fi
port setting found

For more text-search patterns, use the grep command in Linux after the file existence branch has picked the right input file.

Use Bash File Checks Safely in Scripts

File tests are simple, but small shell details decide whether they stay reliable in real scripts.

Quote Path Variables

Always quote path variables inside [ ... ] and test. Quoting keeps spaces, wildcard characters, and empty values from turning one intended path into several shell arguments.

path="$HOME/bash-exists-demo/config file.ini"

if [ -f "$path" ]; then
    printf 'file exists\n'
fi

Unquoted variables are a common cause of silent false positives, [: binary operator expected, [: unary operator expected, and matches against filenames you did not mean to test.

Do Not Use a Test as a Lock

A successful file test describes the path at that moment. Another process can still delete, replace, or create the path before the next command runs. When the action itself can fail safely, let the action report success or failure instead of relying only on a pre-check.

source="$HOME/bash-exists-demo/config.ini"
dest="$HOME/bash-exists-demo/config.backup"

if cp -- "$source" "$dest"; then
    printf 'backup created\n'
else
    printf 'backup failed\n' >&2
fi
backup created

This matters in shared directories, temporary directories, deployment hooks, and any workflow where another process can change the same path.

Prefer Directories Over Parent Assumptions

When a script plans to write a file, check or create the parent directory, not just the final filename. A missing parent causes the write to fail even when the final file does not exist.

target="$HOME/bash-exists-demo/reports/latest.txt"
parent=${target%/*}

if [ -d "$parent" ]; then
    printf 'parent directory exists\n'
else
    printf 'parent directory is missing\n'
fi
parent directory is missing

Use mkdir -p "$parent" when the script owns that output location and should create missing parent directories.

Clean Up the Bash Test Fixture

Remove the practice directory after testing the examples:

The cleanup path removes only ~/bash-exists-demo, the practice directory created earlier. Check the path first if you changed the fixture location.

rm -rf -- "$HOME/bash-exists-demo"
test ! -e "$HOME/bash-exists-demo" && printf 'fixture removed\n'
fixture removed

Troubleshoot Bash File Test Errors

Most Bash file-test problems come from shell syntax, missing spaces, unquoted variables, or testing a pattern as if it were a single path.

Fix Syntax Error Near Unexpected Token else

A missing then before else usually produces an error like this:

bash: line 2: syntax error near unexpected token `else'
bash: line 2: `else'

An if statement always needs then before the commands in the true branch. You cannot jump straight from the test expression to else.

if [ -d "$HOME/bash-exists-demo/logs" ]; then
    printf 'directory exists\n'
else
    printf 'directory missing\n'
fi

If you only care about the missing case, invert the condition and skip else:

if [ ! -d "$HOME/bash-exists-demo/logs" ]; then
    mkdir -p "$HOME/bash-exists-demo/logs"
fi

Fix Missing Bracket or Missing Space Errors

The [ form is a command name, and ] is its final argument. Spaces are required around both brackets and around the operator.

[ -f "$file" ]

These forms are broken because the shell does not see the same arguments:

file="$HOME/bash-exists-demo/config.ini"

[-f "$file" ]
[ -f "$file"]

Bash reports errors such as:

bash: [-f: command not found
bash: [: missing `]'

Fix Unary Operator Expected or Binary Operator Expected

These errors often mean an unquoted variable expanded to nothing or split into unexpected words. With file tests, an empty unquoted value can be worse than noisy because [ -f $file ] becomes [ -f ], which Bash treats as a non-empty string test.

A path with spaces can produce an error like this:

bash: [: config: binary operator expected

Quote the variable and set a default when an empty value is possible.

file=${file:-}

if [ -f "$file" ]; then
    printf 'file exists\n'
else
    printf 'file missing or variable is empty\n'
fi

In Bash-only scripts, [[ -f $file ]] is more forgiving with empty variables, but quoting still makes path handling clearer.

Fix Glob Checks That Never Match

If a glob check treats *.log as literal text, the pattern probably did not match anything. Use the nullglob array pattern from the glob section, or switch to find for recursive searches.

Fix Broken Symlink Confusion

A broken symlink passes -L but fails -e. That is expected because -L tests the link path and -e usually follows the link to its target.

if [ -L "$path" ] && [ ! -e "$path" ]; then
    printf 'broken symlink\n'
fi

Conclusion

Your Bash scripts can handle path checks reliably when each test matches what the next command needs: -f for regular files, -d for directories, -e for any path, and -L for symlinks. Keep variables quoted, use arrays for globs, and let the real action report failure when another process could change the path between the check and the command.

Share this guide

Help another Linux user troubleshoot faster

Share this guide with someone troubleshooting Linux systems or saving it for later.

Follow LinuxCapable

Want more LinuxCapable guides in Google?

Add LinuxCapable as a preferred source so Google can show our tutorials more often in Top Stories and mark them as preferred in AI Mode and AI Overviews when relevant.

Add LinuxCapable as a preferred source on Google
Search LinuxCapable

Need another guide?

Search LinuxCapable for package installs, commands, troubleshooting, and follow-up guides related to what you just read.

Found this guide useful?

Support LinuxCapable to keep tutorials free and up to date.

Buy me a coffeeBuy me a coffee
Before commenting, please review our Comments Policy.
Formatting tips for your comment

You can use basic HTML to format your comment. Useful tags currently allowed in published comments:

You type Result
<code>command</code> command
<strong>bold</strong> bold
<em>italic</em> italic
<a href="https://example.com">link</a> link
<blockquote>quote</blockquote> quote block

Got a Question or Feedback?

We read and reply to every comment - let us know how we can help or improve this guide.

Verify before posting: