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:
| Task | Operator | Use When |
|---|---|---|
| Check a regular file | -f path | The resolved path must be a normal file, not a directory, device, socket, or pipe. |
| Check a directory | -d path | The resolved path must be a directory that the script can treat as a folder. |
| Check any existing path | -e path | Any existing file type is acceptable, including directories, devices, sockets, and named pipes. |
| Check a symbolic link | -L path or -h path | The path itself must be a symlink, including a broken symlink. |
| Check a non-empty file | -s path | The resolved file must exist and have a size greater than zero. |
| Check read access | -r path | The current user must be able to read the path. |
| Check write access | -w path | The current user must be able to write to the path. |
| Check execute or search access | -x path | The 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.
| Syntax | Best Use | Important 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.
Check Symlinks, Empty Files, and Permissions in Bash
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.


Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed in published comments:
<code>command</code>command<strong>bold</strong><em>italic</em><a href="https://example.com">link</a><blockquote>quote</blockquote>