Bash Scripting 23 ๐ง File Testing โ Checking file existence, permissions, and types
A Bash script that operates on files must check what it is operating on before it operates. The test command and its [[ ]] compound equivalent provide unary file operators that answer three questions: does the path exist, what type of file is it, and does the current user have permission to access it in the way the script intends. These checks are not a substitute for error handlingโa file can disappear between the check and the operationโbut they catch the common cases before a command fails with an opaque error message.
This chapter covers the file-testing operators in three categories: existence and type (-e, -f, -d, -L, -b, -c, -p, -S), permissions (-r, -w, -x), and attributes (-s, -O, -G, -N). It explains the difference between testing a symbolic link and testing its target, the root-user exception that makes -x and -r unreliable in some contexts, and the time-of-check-to-time-of-use race that means a passing test is not a guarantee.
Key point: The file-testing operators follow symbolic links by default. -f symlink tests the target of the link, not the link itself. To test the link, use -L or -h. A broken symbolic link passes -L but fails -e.
Why file testing has these distinctions
The existence versus type problem. A path can exist and be a regular file, a directory, a symbolic link, a block device, a character device, a named pipe, or a socket. A script that expects to read a regular file will behave differently if the path is a directory or a FIFO. The -e operator answers “does anything exist at this path,” while -f, -d, and the other type operators answer “what kind of thing is it” .
The permission versus access problem. The -r, -w, and -x operators test whether the current user has the corresponding permission on the file. But the GNU Autoconf manual documents two complications. First, POSIX allows implementations of -x to succeed for the root user even when no execute bits are set, because root can execute any file . Second, shells disagree on whether Access Control Lists affect the result: some base the test on the traditional owner/group/other mode bits, while others base it on whether the user actually has the right through an ACL . A passing -w test does not guarantee that a subsequent write will succeed.
The check-then-use race problem. Any sequence of [[ -f "$file" ]] && cat "$file" has a window between the test and the cat during which the file can be deleted, replaced, or have its permissions changed. The Autoconf manual is explicit about this: “there is a classic time of check to time of use race between any use of test followed by operating on the just-checked file” . The robust approach is to attempt the operation and handle the failure, rather than to rely solely on the test.
The symlink versus target problem. Unless otherwise specified, the file primaries follow symbolic links and operate on the target of the link . This means -f "$link" is true if the link points to a regular file, even though the link itself is not a regular file. The -L and -h operators test the link itself. A broken link passes -L but fails -e, because there is no target .
a. Existence and type operators
The -e operator tests whether anything exists at the path. The type operators narrow that to a specific kind of file. The operators are used inside [[ ]] or with the test command.
# Check existence
if [[ -e "$path" ]]; then
echo "exists"
fi
# Check for a regular file
if [[ -f "$path" ]]; then
echo "regular file"
fi
# Check for a directory
if [[ -d "$path" ]]; then
echo "directory"
fi
The type operators include -b for block special files, -c for character special files, -p for named pipes (FIFOs), -S for sockets, and -L or -h for symbolic links . A single path can be tested against multiple operators in a sequence to classify it.
if [[ -L "$path" ]]; then
echo "symbolic link"
elif [[ -f "$path" ]]; then
echo "regular file"
elif [[ -d "$path" ]]; then
echo "directory"
elif [[ -p "$path" ]]; then
echo "named pipe"
fi
The -L test should come first when classifying, because -f and -d follow the link and would report the target’s type for a symlink .
b. Permission operators and their limits
The -r, -w, and -x operators test readability, writability, and executability. They are the most commonly used file tests and the most commonly misunderstood.
if [[ -r "$file" ]]; then
echo "readable"
fi
if [[ -w "$file" ]]; then
echo "writable"
fi
if [[ -x "$file" ]]; then
echo "executable"
fi
The root-user exception is the first limit. For root, -x can return true even when no execute bit is set, because root is permitted to execute files regardless of the mode . A script that uses -x to decide whether to run a file may attempt to run a non-executable file when run as root. The second limit is the ACL ambiguity: some shells consult the mode bits only, while others consult the actual access rights including ACLs . The third limit is the race: a file can be replaced between the test and the operation.
The practical consequence is that permission tests are useful for producing clear error messages, but they are not a guarantee. A script that must write to a file should attempt the write and handle failure, rather than gating the write on -w.
c. Attribute operators
The -s operator tests whether a file exists and has a size greater than zero. It is commonly used to check that a file is not empty before processing it.
if [[ -s "$file" ]]; then
echo "file has content"
fi
The -O and -G operators test whether the file is owned by the effective user ID or effective group ID, respectively . The -N operator tests whether the file has been modified since it was last read, which is useful for detecting changes in a file the script has previously processed .
The -nt and -ot binary operators compare modification times: file1 -nt file2 is true if file1 is newer, and file1 -ot file2 is true if file1 is older . The -ef operator tests whether two paths refer to the same device and inode, which identifies hard links and duplicate paths .
Complete Example Session
# ============================================
# PART 1: BASIC EXISTENCE CHECK
# ============================================
path="/etc/passwd"
if [[ -e "$path" ]]; then
echo "Path exists"
fi
# -e is true for any type of file.
# ============================================
# PART 2: TYPE CLASSIFICATION
# ============================================
classify() {
local p=$1
if [[ -L "$p" ]]; then
echo "symlink"
elif [[ -f "$p" ]]; then
echo "regular file"
elif [[ -d "$p" ]]; then
echo "directory"
elif [[ -p "$p" ]]; then
echo "fifo"
elif [[ -S "$p" ]]; then
echo "socket"
elif [[ -b "$p" ]]; then
echo "block device"
elif [[ -c "$p" ]]; then
echo "character device"
else
echo "unknown"
fi
}
classify /etc/passwd # regular file
classify /tmp # directory
# ============================================
# PART 3: SYMLINK VERSUS TARGET
# ============================================
ln -sf /etc/passwd /tmp/passwd-link
[[ -L /tmp/passwd-link ]] && echo "is a symlink"
[[ -f /tmp/passwd-link ]] && echo "target is a regular file"
[[ -e /tmp/passwd-link ]] && echo "target exists"
# -L tests the link.
# -f and -e follow the link and test the target.
# ============================================
# PART 4: BROKEN SYMLINK
# ============================================
ln -sf /nonexistent /tmp/broken-link
[[ -L /tmp/broken-link ]] && echo "link exists"
[[ ! -e /tmp/broken-link ]] && echo "target does not exist"
# -L true, -e false: a broken link.
# ============================================
# PART 5: PERMISSION TESTS
# ============================================
file="/etc/shadow"
[[ -r "$file" ]] && echo "readable" || echo "not readable"
[[ -w "$file" ]] && echo "writable" || echo "not writable"
[[ -x "$file" ]] && echo "executable" || echo "not executable"
# As non-root, /etc/shadow is typically not readable.
# ============================================
# PART 6: THE ROOT EXCEPTION
# ============================================
touch /tmp/nonexec
chmod 644 /tmp/nonexec
# As non-root:
[[ -x /tmp/nonexec ]] || echo "not executable"
# As root, -x may still return false for a file
# with no execute bits, but root can execute it.
# The test reflects the mode, not the ability.
# ============================================
# PART 7: EMPTY FILE CHECK
# ============================================
touch /tmp/empty
echo "data" > /tmp/notempty
[[ -s /tmp/empty ]] || echo "empty file"
[[ -s /tmp/notempty ]] && echo "file has content"
# ============================================
# PART 8: OWNERSHIP TESTS
# ============================================
[[ -O /etc/passwd ]] && echo "owned by effective user"
[[ -G /etc/passwd ]] && echo "owned by effective group"
# Usually root owns /etc/passwd.
# ============================================
# PART 9: COMPARISON OPERATORS
# ============================================
touch /tmp/old /tmp/new
sleep 1
touch /tmp/new
[[ /tmp/new -nt /tmp/old ]] && echo "new is newer"
[[ /tmp/old -ot /tmp/new ]] && echo "old is older"
ln /tmp/new /tmp/hardlink
[[ /tmp/new -ef /tmp/hardlink ]] && echo "same inode"
# ============================================
# PART 10: THE CHECK-THEN-USE RACE
# ============================================
# The test is not a guarantee.
# Between the check and the operation, the file can change.
# Instead of:
# [[ -f "$file" ]] && cat "$file"
# Prefer:
if ! cat "$file" 2>/dev/null; then
echo "Could not read file" >&2
exit 1
fi
# The operation handles failure directly.
The ten parts covered existence, type classification, symlink versus target, broken links, permissions, the root exception, empty files, ownership, comparison operators, and the race condition.
Quick Reference
Existence and type operators
| Operator | True when |
|---|---|
-e file | Anything exists at path |
-f file | Regular file exists |
-d file | Directory exists |
-L file / -h file | Symbolic link exists |
-b file | Block special file |
-c file | Character special file |
-p file | Named pipe (FIFO) |
-S file | Socket |
Permission and attribute operators
| Operator | True when |
|---|---|
-r file | Readable by current user |
-w file | Writable by current user |
-x file | Executable by current user |
-s file | Exists and size > 0 |
-O file | Owned by effective user ID |
-G file | Owned by effective group ID |
-N file | Modified since last read |
Comparison operators
| Operator | True when |
|---|---|
file1 -nt file2 | file1 newer than file2 |
file1 -ot file2 | file1 older than file2 |
file1 -ef file2 | Same device and inode |
Symlink behavior
| Test | Symlink | Broken symlink |
|---|---|---|
-L | True | True |
-e | True (target) | False |
-f | True if target is file | False |
Best Practices
โ Do This:
# Test the link itself with -L before testing the target
[[ -L "$path" ]] && echo "link" # โ
# Use -e for existence, not -f, when any type is acceptable
[[ -e "$path" ]] && echo "exists" # โ
# Use -s to check for empty files
[[ -s "$file" ]] || echo "empty" # โ
# Handle operation failure directly instead of relying on -r/-w
cat "$file" || { echo "read failed"; exit 1; } # โ
โ Don’t Do This:
# Assume -f tests a symlink itself
[[ -f "$link" ]] && echo "link" # โ (tests target)
# Assume -x guarantees execution as root
[[ -x "$file" ]] && ./"$file" # โ (root exception)
# Rely solely on -w before writing
[[ -w "$file" ]] && echo data > "$file" # โ (race condition)
# Use -e when you specifically need a regular file
[[ -e "$path" ]] && cat "$path" # โ (may be a directory)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
-f true for a symlink | File tests follow links | Use -L to test the link itself |
| Broken link not detected | -e follows the link | Test -L and ! -e together |
-x true for non-executable as root | Root can execute any file | Accept the exception; do not rely on -x alone |
Write fails despite -w | ACL or race condition | Handle the write failure directly |
| Directory treated as file | -e true for directories | Use -f when a regular file is required |
Real-World Examples
1. Check if a file exists
[[ -e "$path" ]] && echo "exists"
2. Check for a regular file
[[ -f "$path" ]] && echo "regular file"
3. Check for a directory
[[ -d "$path" ]] && echo "directory"
4. Check for a symlink
[[ -L "$path" ]] && echo "symlink"
5. Check readability
[[ -r "$file" ]] && echo "readable"
6. Check for empty file
[[ -s "$file" ]] || echo "empty"
7. Check ownership
[[ -O "$file" ]] && echo "owned by me"
8. Compare modification times
[[ "$new" -nt "$old" ]] && echo "newer"
9. Check same inode
[[ "$a" -ef "$b" ]] && echo "same file"
10. Safe read with error handling
cat "$file" 2>/dev/null || { echo "read failed" >&2; exit 1; }
Visual
Symlink testing flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TESTING A SYMLINK โ
โ โ
โ Path: /tmp/link โโโบ /etc/passwd โ
โ โ
โ [[ -L /tmp/link ]] โ
โ Tests the link itself โโโบ true โ
โ โ
โ [[ -e /tmp/link ]] โ
โ Follows link to target โโโบ true (passwd exists) โ
โ โ
โ [[ -f /tmp/link ]] โ
โ Follows link to target โโโบ true (passwd is regular) โ
โ โ
โ Broken link: /tmp/broken โโโบ /nonexistent โ
โ -L: true (link exists) โ
โ -e: false (target missing) โ
โ -f: false (no target) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Operator categories
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FILE TEST OPERATORS โ
โ โ
โ EXISTENCE โ
โ -e anything exists โ
โ โ
โ TYPE โ
โ -f regular file โ
โ -d directory โ
โ -L symlink โ
โ -b block device โ
โ -c character device โ
โ -p FIFO โ
โ -S socket โ
โ โ
โ PERMISSION โ
โ -r readable โ
โ -w writable โ
โ -x executable โ
โ โ
โ ATTRIBUTE โ
โ -s size > 0 โ
โ -O owned by effective user โ
โ -G owned by effective group โ
โ -N modified since read โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The check-then-use race
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TIME OF CHECK TO TIME OF USE โ
โ โ
โ Time โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ โ
โ โ
โ [[ -f "$file" ]] โโโโโโโบ cat "$file" โ
โ โ โ โ
โ โ โ โ
โ โผ โผ โ
โ File exists File may be gone, โ
โ (test passes) replaced, or changed โ
โ โ
โ The gap between the test and the operation is the race. โ
โ The test does not guarantee the operation. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Root exception for -x
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ROOT AND -x โ
โ โ
โ File: /tmp/nonexec โ
โ Mode: 644 (no execute bits) โ
โ โ
โ As regular user: โ
โ [[ -x /tmp/nonexec ]] โโโบ false โ
โ โ
โ As root: โ
โ [[ -x /tmp/nonexec ]] โโโบ may be false (mode-based) โ
โ But root can execute the file anyway. โ
โ โ
โ POSIX allows -x to succeed for root even when the โ
โ execute bits are clear. The test reflects the mode, โ
โ not the kernel's permission to execute. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Existence | -e |
| Regular file | -f |
| Directory | -d |
| Symbolic link | -L or -h |
| Readable | -r |
| Writable | -w |
| Executable | -x |
| Non-empty | -s |
| Owned by user | -O |
| Newer than | -nt |
| Same inode | -ef |
| Symlink default behavior | Follows link to target |
Key takeaways:
- The file-testing operators follow symbolic links by default.
-f "$link"tests the target, not the link. Use-Lto test the link itself. A broken link passes-Lbut fails-e. - Permission tests have exceptions.
-xcan succeed for root even without execute bits, and-r/-wmay or may not account for ACLs depending on the shell . A passing test is a hint, not a guarantee. - There is a race between the test and the operation. The Autoconf manual recommends attempting the operation and handling failure, rather than relying solely on a preceding test .
-eis true for any type of file. A directory, a device, and a regular file all pass-e. Use-fwhen a regular file is required.-schecks for non-empty files. It is true when the file exists and has a size greater than zero. A zero-byte file fails-sbut passes-e.- The comparison operators compare metadata.
-ntand-otcompare modification times.-efcompares device and inode, which identifies hard links and duplicate paths .
Remember: File testing in Bash answers three questions: does it exist, what is it, and can I access it. The type operators (-f, -d, -L, and the special-file operators) are reliable classifications. The permission operators (-r, -w, -x) are useful for error messages but unreliable as gates, because of the root exception, ACL ambiguity, and the check-then-use race. Test the type, attempt the operation, and handle the failure. The one operator that catches the most scripts is -L: a symlink to a regular file passes -f, and a broken symlink passes -L but fails -e.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!