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 ls output is unreliable; use globs or find -print0.
  • Predictable temp names invite symlink attacks — use mktemp.
  • cd "$dir" without checking lets a later command run in the wrong directory.