Files & Directories
Testing Files
Before you touch a file, test it. The test operators were listed in the conditionals lesson; here is how they combine in practice.
Existence and Type
f="/etc/hosts"
[[ -e "$f" ]] && echo "exists"
[[ -f "$f" ]] && echo "regular file"
[[ -d "$f" ]] || echo "not a directory"
[[ -s "$f" ]] && echo "non-empty"
Permissions
[[ -r "$f" ]] && echo "readable"
[[ -w "$f" ]] && echo "writable"
[[ -x "$f" ]] && echo "executable"
if [[ ! -w "$log" ]]; then
echo "cannot write $log" >&2; exit 1
fi
Listing & Navigating
find
find walks a directory tree with precise tests and safe file handling.
find . -type f -name '*.log' # by name
find . -type f -size +10M # larger than 10 MB
find . -type f -mtime -7 # modified in the last 7 days
find . -type f -name '*.tmp' -delete # delete matches
find . -type f -print0 | xargs -0 rm -f # NUL-safe: handles spaces/newlines
-print0 + xargs -0 is the only correct way to feed filenames to another program. Filenames may contain spaces, newlines and quotes.
Inspecting Size and Space
du -sh */ # size of each subdirectory
df -h . # free space on the filesystem
ls -lh # human-readable sizes
Reading Files
Line by Line with read
while IFS= read -r line; do
[[ -z "$line" ]] && continue # skip blank lines
[[ "$line" == \#* ]] && continue # skip comments
printf 'value: %s\n' "$line"
done < "$config"
Whole File with mapfile
When you want the file in memory rather than streamed, mapfile reads it into an array in one call.
mapfile -t lines < "$config" # -t strips the trailing newline
echo "${#lines[@]} lines"
printf '%s\n' "${lines[0]}"
# Read a small file wholly (careful: strips trailing newlines)
content="$(cat "$config")"
$(cat file) is fine for small files but loads everything into memory and removes trailing newlines. For large or binary files stream them with while read or hand them to a tool that reads files directly.
Writing & Temporary Files
Safe Temporary Files
Never hard-code /tmp/myfile — a predictable name is a symlink attack and a race between concurrent runs. Use mktemp.
tmp="$(mktemp)" # a unique file
tmpdir="$(mktemp -d)" # a unique directory
mkdir -p "$(dirname "$out")" # create parents before writing
Cleanup with trap
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT # always cleaned up, even on error
trap 'echo "interrupted" >&2; exit 130' INT
printf 'report data\n' > "$tmp"
> "$f" truncates before the command writes. To update a file in place, write a sibling temp file and mv it over the original — mv within one filesystem is atomic.
new="$f.tmp.$$"
generate > "$new" && mv "$new" "$f" || rm -f "$new"
Working with Paths
path="/var/log/app/error.log"
echo "${path%/*}" # /var/log/app (directory)
echo "${path##*/}" # error.log (basename)
echo "${path##*.}" # log (extension)
echo "${path%.*}" # /var/log/app/error
readlink -f "$path" # absolute, resolved path
realpath "$f"
The Script's Own Directory
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
echo "resources live in $SCRIPT_DIR/assets"
Pitfalls
- Unquoted paths with spaces split into arguments — always quote.
- Parsing
lsoutput is unreliable; use globs orfind -print0. - Predictable temp names invite symlink attacks — use
mktemp. cd "$dir"without checking lets a later command run in the wrong directory.