Build the mental model
A script that touches the filesystem should never assume a file or directory is in the state it expects, because the environment around a script is never fully under its control — a disk can fill up, a path can be missing or renamed, permissions can be wrong, or a previous run of the same script could have left things in an unexpected state. Bash gives you a family of test operators for exactly this reason: -e checks whether something exists at all, -f narrows that to a regular file, -d to a directory, and -r, -w, -x check read, write, and execute permission respectively — each returning true or false as an exit status you can use directly inside [[ ]] before acting. Just as important as checking a file's state is deciding how to enumerate a set of files in the first place. A plain glob like *.txt is simple, predictable, and usually all you need for a flat directory, but it only expands relative to the current shell context and does not recurse into subdirectories. The find command is considerably more powerful: it can search recursively, filter by type with -type, filter by name pattern, and — critically — handle filenames containing spaces or unusual characters correctly when combined with a null-safe read loop, situations where a glob alone would silently misbehave.
Connect it to a real scenario
Think of a backup script that needs to check whether a target directory exists before writing into it, create a safe scratch space instead of hardcoding a path like /tmp/mydata that another process could collide with, and confirm each operation actually succeeded before deleting anything. This lesson's code walks through that exact sequence: work_dir=$(mktemp -d) creates a directory with a guaranteed-unique name, sidestepping any race condition or collision that a hardcoded temp path invites. Before treating work_dir as usable, the script verifies it with [[ -d "$work_dir" ]], confirms report.txt specifically is a regular file (not a directory or something stranger) with -f, and checks both -r and -w together to make sure it is actually readable and writable before relying on it — combined inside a single [[ ]] with &&, both conditions must hold. It also demonstrates the negative case, [[ ! -e "$work_dir/missing.txt" ]], confirming a file genuinely does not exist rather than just assuming so. For enumeration, the glob for file in "$work_dir"/*.txt is simple and sufficient for a flat directory, while the find version — find "$work_dir" -maxdepth 1 -type f -name "*.txt" | sort | while read -r file — shows the more robust pattern you'd reach for once depth, filtering, or predictable ordering matters. Finally, checking [[ $? -eq 0 ]] after rm -rf is the same discipline applied to cleanup: never assume a destructive operation succeeded just because it ran.
Try the working example
#!/bin/bash
work_dir=$(mktemp -d)
# Create some files inside the temp directory
touch "$work_dir/report.txt"
touch "$work_dir/notes.txt"
mkdir "$work_dir/archive"
if [[ -d "$work_dir" ]]; then
echo "work_dir exists and is a directory"
fi
if [[ -f "$work_dir/report.txt" ]]; then
echo "report.txt exists and is a regular file"
fi
if [[ -r "$work_dir/report.txt" && -w "$work_dir/report.txt" ]]; then
echo "report.txt is readable and writable"
fi
if [[ ! -e "$work_dir/missing.txt" ]]; then
echo "missing.txt does not exist"
fi
echo "Looping over .txt files with a glob:"
for file in "$work_dir"/*.txt; do
echo "Found: $(basename "$file")"
done
echo "Looping over files with find:"
find "$work_dir" -maxdepth 1 -type f -name "*.txt" | sort | while read -r file; do
echo "Found via find: $(basename "$file")"
done
rm -rf "$work_dir"
if [[ $? -eq 0 ]]; then
echo "Cleanup succeeded"
fiRunning this script prints:
work_dir exists and is a directory
report.txt exists and is a regular file
report.txt is readable and writable
missing.txt does not exist
Looping over .txt files with a glob:
Found: notes.txt
Found: report.txt
Looping over files with find:
Found via find: notes.txt
Found via find: report.txt
Cleanup succeeded5-minute try-it
Write a script that creates a temporary directory with mktemp -d, checks whether it is writable, creates two files with different extensions inside it, then loops over only the files matching one extension and prints their names before cleaning everything up.
One important caution
Looping over a glob like *.txt without checking whether it actually matched anything — if no file matches, bash passes the literal unexpanded string "*.txt" into the loop
Assuming a command like cp or mv succeeded just because it ran, instead of checking its exit status ($?) before proceeding to delete the original file
GNU Bash Manual: Bash Conditional Expressions — Bash / Shell Scripting