| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user; no root required for the examples |
| Scope | Bash file and directory tests with -e, -f, -d, -L/-h, -s, -r, -w, -x, negated tests, broken symlinks, multiple-path validation, and safe alternatives such as mkdir -p. Brief [ ] vs [[ ]] comparison only; does not cover full conditional syntax or find/stat workflows. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash provides file-test operators for checking paths in scripts. Use -f for a regular file, -d for a directory, and -e when any existing filesystem object is acceptable.
These tests work directly inside if, for example if [[ -f "$file" ]]; then ... fi. File tests return an exit status like any other command: status 0 means the condition is true. See Bash if else for the full if/elif/else grammar.
Check If a File or Directory Exists in Bash
The three most common patterns:
file='/etc/os-release'
dir='/etc'
path='/dev/null'
# Regular file
if [[ -f "$file" ]]; then
echo 'File exists'
fi
# Directory
if [[ -d "$dir" ]]; then
echo 'Directory exists'
fi
# Any existing path
if [[ -e "$path" ]]; then
echo 'Path exists'
fi| Need to check | Use |
|---|---|
| Regular file | -f |
| Directory | -d |
| Any path (file, directory, or resolvable symlink target) | -e |
Check If Any Path Exists with -e
Use -e when any existing filesystem object is acceptable, including regular files, directories, devices, sockets, and FIFOs. If your condition means “file or directory exists,” use -e rather than combining -f and -d.
path='/etc/os-release'
if [[ -e "$path" ]]; then
echo "path exists: $path"
fipath exists: /etc/os-release-e follows symlinks to the target. If the symlink is broken, -e is false even though the link file itself is still on disk. The symlink section below shows that split.
Check If a Regular File Exists with -f
Use -f when the path must be a regular file:
echo 'config=1' > app.conf
cfg='./app.conf'
if [[ -f "$cfg" ]]; then
echo "config exists: $cfg"
ficonfig exists: ./app.confStop early when a required config file is missing:
cfg='./app.conf'
if [[ ! -f "$cfg" ]]; then
echo "missing config: $cfg" >&2
exit 1
fi-f is false for directories, device nodes, and broken symlinks.
Check If a Directory Exists with -d
Use -d when you need to know whether an existing path is a directory. If your real goal is simply to ensure the directory exists before writing, mkdir -p often avoids a separate test:
backup_dir='./backup'
mkdir -p -- "$backup_dir"
echo "backup directory ready: $backup_dir"backup directory ready: ./backupmkdir -p creates missing parent directories and does nothing harmful when the path already exists. To branch on an existing directory without creating it:
log_dir='/var/log'
if [[ -d "$log_dir" ]]; then
echo "log directory present: $log_dir"
fiIf you specifically need to inspect whether Bash considers an existing directory writable, combine -d and -w:
out_dir='./backup'
mkdir -p -- "$out_dir"
if [[ -d "$out_dir" && -w "$out_dir" ]]; then
echo "writable directory: $out_dir"
fiwritable directory: ./backupThis is a permission/access check, not a guarantee that a later write will succeed; handle errors from the actual write operation as well.
Check If a Path Does Not Exist
Negate the test with ! inside [[ ]]:
if [[ ! -e "ghost" ]]; then
echo 'ghost missing'
fighost missingBe careful with negation: [[ ! -f "$path" ]] means “this is not an existing regular file.” It can also be true when $path is an existing directory. Use [[ ! -e "$path" ]] when you specifically mean “nothing exists here.”
For optional files, [[ ! -f "$path" ]] is appropriate when a directory or other type at the same path should not count as the file you expected. Combine with elif when you classify missing paths differently from wrong types.
Check Symlinks and Broken Symlinks
-L (same as -h) tests whether the path is a symbolic link. It does not check whether the target exists. That distinction matters for symbolic links that point at moved or deleted files.
Set up a good link and a broken one:
echo hello > real.txt
ln -sf real.txt goodlink
ln -sf missing badlinkCompare the operators on each link:
for f in goodlink badlink; do
printf '%s:' "$f"
[[ -L "$f" ]] && printf ' -L' || printf ' !-L'
[[ -e "$f" ]] && printf ' -e' || printf ' !-e'
[[ -f "$f" ]] && printf ' -f' || printf ' !-f'
echo
donegoodlink: -L -e -f
badlink: -L !-e !-fA broken symlink still passes -L because the link itself still exists; only its target is missing. -e and -f are false because the target cannot be resolved.
[[ -L "$link" && -e "$link" ]]— symlink with any resolvable target[[ -L "$link" && -f "$link" ]]— symlink whose target is a regular file
link='./goodlink'
if [[ -L "$link" && -e "$link" ]]; then
echo "working symlink: $link"
else
echo "missing or broken symlink: $link" >&2
fiworking symlink: ./goodlinkSwap goodlink for badlink and the same test reports a broken link.
Check File Size and Permissions
Before you read a data file, confirm it is non-empty and readable:
data='data.txt'
echo data > "$data"
if [[ -s "$data" && -r "$data" ]]; then
echo "$data ready to read"
fidata.txt ready to readOperator meanings:
-s— path exists and has size greater than zero (checks size, not file type)-r,-w,-x— path exists and Bash can read, write, or execute it in the current shell context
For a non-empty regular file, combine -f and -s in one condition:
file='data.txt'
echo data > "$file"
if [[ -f "$file" && -s "$file" ]]; then
echo 'non-empty regular file'
finon-empty regular fileAn empty file fails -s:
touch empty.txt
if [[ ! -s "empty.txt" ]]; then
echo 'empty.txt is empty'
fiempty.txt is emptyCheck Multiple Files or Paths
Test two known files in one condition:
a='/etc/passwd'
b='/etc/os-release'
if [[ -f "$a" && -f "$b" ]]; then
echo 'Both files exist'
fiLoop over a list when a script depends on several paths:
touch app.conf data.txt
missing=0
for p in app.conf data.txt ghost; do
if [[ ! -e "$p" ]]; then
echo "missing: $p"
missing=1
fi
done
if [[ $missing -eq 0 ]]; then
echo 'all deps present'
fimissing: ghost[ ], [[ ]], and test
Common file-test operators such as -f, -d, and -e can be used with test, [ ], and Bash's [[ ]] construct:
file='/etc/os-release'
test -f "$file" && echo 'test: file exists'
[ -f "$file" ] && echo '[ ]: file exists'
[[ -f "$file" ]] && echo '[[ ]]: file exists'test: file exists
[ ]: file exists
[[ ]]: file exists[ is another name for test. [[ is Bash-only syntax with safer parsing for unquoted variables. Quote paths in [ ] tests. Quoting is not required for simple variable expansions inside [[ ]], but using "$path" consistently makes examples portable to [ ] and commands outside the conditional. For a full comparison of [ ], [[ ]], and (( )), see Bash [ ] vs [[ ]].
Common Mistakes
Do not use ls as an existence test
ls lists paths; Bash already has boolean file tests designed for conditions. Parsing ls output is unnecessary and fragile when locales or odd filenames are involved. Use [[ -e "$path" ]] or [[ -f "$path" ]] directly.
Handle errors from the real operation
A file can change between the existence check and the command that uses it. When possible, handle errors from the actual operation rather than assuming an earlier [[ -f ... ]] guarantees the file will still be there.
Prefer commands that already handle missing paths
mkdir -p dir creates a directory tree without a prior -d check. rm -f file removes a file if present without testing first. Use these when the goal is idempotent setup or cleanup, not when you need to report a specific error to the user.
Quote path variables in [ ] and commands
Quoting is required to keep paths containing spaces as one argument in [ ] and ordinary commands:
touch 'a b'
name='a b'
if [ -f "$name" ]; then
echo 'quoted path works'
fiquoted path worksWithout quotes, $name is split into multiple arguments and the [ ] expression may fail with a syntax or operand error. Inside [[ ]], simple variable expansions do not undergo word splitting or pathname expansion, but consistent "$path" quoting stays readable and portable.
Practical Config-Validation Function
Wrap repeated checks in a Bash function so callers get a clear failure status:
echo data > app.conf
require_readable_file() {
local p="$1"
if [[ -f "$p" && -r "$p" && -s "$p" ]]; then
return 0
fi
printf 'error: need a non-empty readable file: %s\n' "$p" >&2
return 1
}
require_readable_file ./app.conf && echo 'validation passed'validation passedReturn 0 on success and 1 on failure so callers can use the function directly inside an if condition. Add -x when the path must be executable, or -d when you require a directory instead of a file.
Summary
Bash file tests let you branch on what is on disk before you read, copy, or delete paths. Use -e when any object counts, -f for regular files, and -d for directories. Remember that -L tests the symlink itself while -e and -f follow the target, so broken links produce -L true with -e false.
Use [[ ! -e "$path" ]] for a missing path and [[ ! -f "$path" ]] when you mean “not a regular file.” Combine -s, -r, -w, and -x when size or permissions matter. Prefer mkdir -p or handle errors on the real operation when that is enough. For portable scripts use [ or test; in Bash-only code [[ ]] is usually clearer.
References
- GNU Bash Manual — Bash Conditional Expressions — file test operators in
[[ ]] - GNU Bash Manual — test —
[andtestunary primaries - POSIX test utility — portable file tests
Frequently Asked Questions
What is the difference between -e and -f?
-e is true when any filesystem object exists at the path (file, directory, device, or resolvable symlink target). -f is true only for a regular file, including a symlink whose final target is a regular file. Use -f when a directory or socket at the same path should count as an error.
Does -e detect a broken symlink?
No. -e follows the symlink. If the target is missing, -e is false even though the link file remains. Use -L to test whether the path is a symlink, and combine -L with -e when the target must resolve.
How do I check a directory in Bash?
Use [[ -d "$path" ]]. It is true when the path is a directory or a symlink to a directory. When you only need to ensure the directory exists before writing files, mkdir -p "$path" is often simpler than a separate test.
How do I check if a file does not exist in Bash?
Use [[ ! -e "$path" ]] when nothing should exist at the path, or [[ ! -f "$path" ]] when you specifically mean “not an existing regular file.”

