Linux Tutorial / Handle Bash Errors and Work with Files Safely
A safer file script validates inputs and destinations, handles failures explicitly, and avoids presenting partial output as a completed result. Instead of relying solely on set -e, decide what failure means at each step and define cleanup and overwrite behavior.
Build on the first Bash script to handle failure branches and file-operation limits. Review &&, ||, and pipeline rules in shell execution flow.
Define the safety boundary of the example
The example copies one regular file to a new name under a trusted directory you control, then compares contents. The destination directory must already exist and must not be writable by untrusted users. It is not a security boundary against hostile concurrent path changes or a database backup tool. Run it without administrator privileges.
Create a complete checked-copy script
lab=$(mktemp -d "$HOME/linux-safe-copy.XXXXXX")
cd "$lab"
mkdir output
printf "important sample\n" > "source file.txt"
cat > checked-copy.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail
umask 077
if (( $# != 2 )); then
printf "Usage: %s SOURCE DESTINATION\n" "$0" >&2
exit 2
fi
source_file=$1
destination=$2
if [[ ! -f "$source_file" || -L "$source_file" ]]; then
printf "Source must be a regular non-symlink file\n" >&2
exit 1
fi
if [[ -e "$destination" || -L "$destination" ]]; then
printf "Destination already exists\n" >&2
exit 1
fi
parent=$(dirname -- "$destination")
if [[ ! -d "$parent" ]]; then
printf "Destination directory is missing\n" >&2
exit 1
fi
work=$(mktemp -d "$parent/.copy-work.XXXXXX") || exit 1
cleanup() { rm -rf -- "$work"; }
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if ! cp -- "$source_file" "$work/payload"; then
printf "Copy failed\n" >&2
exit 1
fi
if ! cmp -s -- "$source_file" "$work/payload"; then
printf "Verification failed\n" >&2
exit 1
fi
if ! ln -- "$work/payload" "$destination"; then
printf "Destination creation failed\n" >&2
exit 1
fi
printf "Created: %s\n" "$destination"
EOF
bash -n checked-copy.sh
bash checked-copy.sh "source file.txt" "output/copy file.txt"
cmp "source file.txt" "output/copy file.txt"
The script copies into a temporary directory, compares contents, then creates the destination name using a hard link. Temporary storage is on the destination file system; ln fails if that name already exists rather than overwriting it. EXIT cleanup removes the temporary name while the destination link remains. If the file system cannot create hard links, the final step fails and the design must be reconsidered.
The example checks content, not ownership, ACLs, extended attributes, or durable storage after a power loss. It is unsuitable for a source being modified continuously. Recursive cleanup targets only the successfully created mktemp work directory, never the user-supplied source or destination.
Test expected failures deliberately
bash checked-copy.sh "source file.txt" "output/copy file.txt"
printf "exit=%s\n" "$?"
bash checked-copy.sh missing.txt output/new.txt
printf "exit=%s\n" "$?"
find output -maxdepth 1 -name ".copy-work.*" -print
Repeating the successful copy should return status 1 and preserve the existing destination. A missing source must fail too. After normal exits or handled failures, find should show no temporary work directories. Traps cannot run after SIGKILL or power loss; abandoned temporary data requires a separate cleanup policy. Recheck the original and first copy after the failure tests.
Understand the limits of errexit and pipefail
bash -c 'false | cat; printf "status=%s\n" "$?"'
bash -o pipefail -c 'false | cat; printf "status=%s\n" "$?"'
The first command normally prints status=0 because cat succeeds; the second prints status=1 because pipefail exposes the failed component. Pipefail changes status propagation, not recovery. set -e has context-sensitive exceptions involving conditionals and pipelines, so test the exact context where you expect execution to stop.
if ! command; then is convenient for failure branches, but $? inside that branch reflects the negation rather than the original status. Preserve the original code immediately in an else branch when it matters. Define useful return codes and error messages for callers.
Check the operating assumptions before automation
- Quote expansions and use -- before filename arguments where supported.
- Separate preview from mutation and ensure preview cannot change data.
- Review ownership, permissions, symlinks, concurrency, and source changes.
- Test success, failures, and handled signals, and retain recoverable backups.




