In a bash script, what do the test operators -e, -f, -d, -s and -r check about a path, and why can `[ -e "$p" ]` be true while `[ -f "$p" ]` is false for the same path?
answer
- exists versus type of thing
- directory passes one, fails the other
- -s adds "and not zero bytes"
- permission tests answer for you, now
- symlink tests report the target
basics
~20 sIn bash, -e means the path exists at all, -f means it is a regular file, -d means it is a directory, -s means it exists with a size above zero, and -r means the current user can read it. A directory exists, so -e is true and -f is false.
solid answer
~50 s`[ ... ]` is just the `test` builtin, and these operators stat the path. `-e` is true for anything that exists — file, directory, device, socket. `-f` is true only for a regular file and `-d` only for a directory, so a directory passes `-e` and fails `-f`. `-s` means it exists *and* has a size above zero, which is how you tell "the job wrote nothing" from "the job wrote no file". `-r` (like `-w` and `-x`) asks whether the current effective user could read it right now. They all follow symlinks, so a symlink whose target was deleted is false for `-e` but true for `-L`. Always quote the operand — `[ -f "$p" ]` — and treat a permission test as a snapshot, not a promise that the later open will succeed.
code
bash · 10 linestmp=$(mktemp -d)
mkdir "$tmp/adir"; : > "$tmp/empty"; echo data > "$tmp/full"
for p in "$tmp/adir" "$tmp/empty" "$tmp/full" "$tmp/missing"; do
printf '%s: -e=%s -f=%s -d=%s -s=%s\n' "${p##*/}" \
"$([ -e "$p" ] && echo yes || echo no)" \
"$([ -f "$p" ] && echo yes || echo no)" \
"$([ -d "$p" ] && echo yes || echo no)" \
"$([ -s "$p" ] && echo yes || echo no)"
done
rm -rf "$tmp"go deeper
Know the five by heart and say plainly which one you would use before reading a file (-f), before entering a directory (-d), and to check for output (-s). Quote the path in every example you write.
Explain that these are arguments to the test builtin, that they stat the path and follow symlinks, and that -L is the exception. Be ready to show the exists-versus-type distinction with a directory or /dev/null.
Show the judgment: permission tests are advisory snapshots for the effective user, so a check that passes interactively can fail under a service account, and the check-then-act gap is a real race. Argue for attempting the operation and handling the error.
Own the convention: decide whether scripts guard defensively or fail loudly on the operation itself, and how that interacts with how the platform reports failures. Guards that merely produce nicer messages must not be mistaken for correctness controls.
## What you are actually calling `[` is not punctuation and it is not part of `if`. It is the `test` builtin wearing a different name, and it requires a closing `]` as its final argument. `[ -f "$p" ]` and `test -f "$p"` are the same call. Everything below is therefore an *argument* the shell hands to that command after it has finished expanding your variables, which is why quoting matters even in the simplest file check. A file test asks the operating system for the metadata of the path and answers yes (exit status 0) or no (exit status 1). A path that does not exist is not an error — it is simply a false answer. ## The operators you will use daily - `-e path` — true if the path exists, whatever kind of thing it is. - `-f path` — true only for a **regular file**: not a directory, not a device node, not a socket or FIFO. - `-d path` — true only for a directory. - `-s path` — true if the path exists **and its size is greater than zero**. - `-r path` — true if the path is readable by the effective user of the running process; `-w` and `-x` are the same idea for write and execute. Bash has more of them — `-L` (or `-h`) for a symlink, `-p` for a named pipe, `-S` for a socket, `-t` for "this file descriptor is a terminal" — but the five above cover almost every guard you will write. ## Exists versus is-a-file This is the crux of the question. `-e` answers a question about *existence*; `-f` answers a question about *type*. `/etc` exists, so `[ -e /etc ]` succeeds, but it is a directory, so `[ -f /etc ]` fails. The same split catches `/dev/null`, which exists and is readable but is a character device, not a regular file. The practical rule: use the operator that matches what you are about to do. About to read a config file? `-f`. About to `cd` into something or write inside it? `-d`. Use `-e` only when you genuinely mean "is there anything at all here", for example before refusing to clobber a path. ```bash [ -f "$config" ] || { echo "no config at $config" >&2; exit 1; } [ -d "$outdir" ] || mkdir -p "$outdir" ``` ## Symlinks are followed `-e`, `-f`, `-d`, `-s` and the permission tests all resolve symlinks and report on the **target**. A symlink whose target has been deleted is therefore false for `-e`, which surprises people who expect "the link file is right there". `-L` is the one that reports on the link itself, so the idiom for a broken link is: ```bash if [ -L "$p" ] && [ ! -e "$p" ]; then echo "dangling symlink: $p"; fi ``` ## Empty versus missing `-s` is the operator people forget, and it is the one that distinguishes the two failure modes of a job that produces output: the file is missing (nothing ran) versus the file is there but zero bytes (something ran and produced nothing). `[ -f "$out" ] && [ ! -s "$out" ]` is "the file exists and is empty". Note that `-s` on its own is not a file test: directories normally report a non-zero size, so pair it with `-f` when you mean an empty *file*. ## Permission tests are advisory `-r`, `-w` and `-x` answer for the effective user, at that instant. Two consequences. First, the answer is user-specific: a check that passes when you run the script by hand tells you nothing about the service account that runs it from cron. Second, it is a snapshot — between the test and the open, the file can be replaced, the mode can change, or the filesystem can be remounted read-only. For anything that matters, prefer attempting the operation and handling the failure over testing first: ```bash if ! exec 3>"$logfile"; then echo "cannot write $logfile" >&2; exit 1; fi ``` ## Quote the operand Every example above quotes `"$p"`, and that is not decoration. `[` receives its arguments after the shell has split them on whitespace, so `[ -f $p ]` with `p='my file.txt'` becomes a three-argument call that fails with a parse complaint rather than a useful answer. Inside a bash-only script `[[ -f $p ]]` is immune to that, but the habit of quoting costs nothing and works in both. ## Negation and combination `!` negates a test: `[ ! -d "$p" ]`. To combine, prefer two separate commands joined with `&&` — `[ -f "$p" ] && [ -r "$p" ]` — rather than the older `-a`/`-o` operators inside a single `[ ]`, which are ambiguous when operands can look like operators and are flagged by linters.
- How would you test the symbolic link itself rather than what it points to?Use `-L` (or its synonym `-h`), the only one of these operators that does not follow the link. `-e` and `-f` resolve the target, so a symlink to a deleted file is false for both. The idiom for detecting a broken link is `[ -L "$p" ] && [ ! -e "$p" ]`: the link exists, its target does not.
- Your script does `[ -w "$dir" ]` and then writes into it. Why is that not a guarantee?Because the test is a snapshot for the effective user at that moment. Permissions can change, the path can be replaced, the filesystem can be remounted read-only, and the disk can fill — none of which the earlier test sees. It is a time-of-check/time-of-use gap. Prefer attempting the write and handling the error, using `-w` only to fail early with a friendlier message.
- What does `[ -s "$f" ]` return when the file does not exist at all?False. `-s` requires both existence and a size above zero, so "missing" and "present but empty" both come back false. If you need to tell those apart — a job that never ran versus a job that produced nothing — combine them: `[ -f "$f" ]` for existence, then `[ -s "$f" ]` for content.
-e asks whether the mailbox exists; -f asks whether what is inside it is a letter rather than a parcel locker.
saying these in an interview costs you the question
- Uses -e where the script really needs -f
- Thinks -f is true for directories too
- Says a dangling symlink still passes -e
- Believes -s only means the file exists
- Treats -r as a guarantee the later read succeeds