Check If File or Directory Exists in Bash

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
Short on time?Beta Get the gist of this 9-minute read in seconds, then ask follow-up questions

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:

bash
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.

bash
path='/etc/os-release'

if [[ -e "$path" ]]; then
  echo "path exists: $path"
fi
output
path 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:

bash
echo 'config=1' > app.conf

cfg='./app.conf'
if [[ -f "$cfg" ]]; then
  echo "config exists: $cfg"
fi
output
config exists: ./app.conf

Stop early when a required config file is missing:

bash
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:

bash
backup_dir='./backup'

mkdir -p -- "$backup_dir"
echo "backup directory ready: $backup_dir"
output
backup directory ready: ./backup

mkdir -p creates missing parent directories and does nothing harmful when the path already exists. To branch on an existing directory without creating it:

bash
log_dir='/var/log'

if [[ -d "$log_dir" ]]; then
  echo "log directory present: $log_dir"
fi

If you specifically need to inspect whether Bash considers an existing directory writable, combine -d and -w:

bash
out_dir='./backup'

mkdir -p -- "$out_dir"

if [[ -d "$out_dir" && -w "$out_dir" ]]; then
  echo "writable directory: $out_dir"
fi
output
writable directory: ./backup

This 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 [[ ]]:

bash
if [[ ! -e "ghost" ]]; then
  echo 'ghost missing'
fi
output
ghost missing

Be 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.


-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:

bash
echo hello > real.txt
ln -sf real.txt goodlink
ln -sf missing badlink

Compare the operators on each link:

bash
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
done
output
goodlink: -L -e -f
badlink: -L !-e !-f

A 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
bash
link='./goodlink'

if [[ -L "$link" && -e "$link" ]]; then
  echo "working symlink: $link"
else
  echo "missing or broken symlink: $link" >&2
fi
output
working symlink: ./goodlink

Swap 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:

bash
data='data.txt'
echo data > "$data"

if [[ -s "$data" && -r "$data" ]]; then
  echo "$data ready to read"
fi
output
data.txt ready to read

Operator 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:

bash
file='data.txt'
echo data > "$file"

if [[ -f "$file" && -s "$file" ]]; then
  echo 'non-empty regular file'
fi
output
non-empty regular file

An empty file fails -s:

bash
touch empty.txt

if [[ ! -s "empty.txt" ]]; then
  echo 'empty.txt is empty'
fi
output
empty.txt is empty

Check Multiple Files or Paths

Test two known files in one condition:

bash
a='/etc/passwd'
b='/etc/os-release'

if [[ -f "$a" && -f "$b" ]]; then
  echo 'Both files exist'
fi

Loop over a list when a script depends on several paths:

bash
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'
fi
output
missing: ghost

[ ], [[ ]], and test

Common file-test operators such as -f, -d, and -e can be used with test, [ ], and Bash's [[ ]] construct:

bash
file='/etc/os-release'

test -f "$file" && echo 'test: file exists'
[ -f "$file" ] && echo '[ ]: file exists'
[[ -f "$file" ]] && echo '[[ ]]: file exists'
output
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:

bash
touch 'a b'
name='a b'

if [ -f "$name" ]; then
  echo 'quoted path works'
fi
output
quoted path works

Without 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:

bash
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'
output
validation passed

Return 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


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.

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.”

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)