Bash getopts with Positional Validation
Parse short options with getopts, distinguish missing arguments from unknown flags, then validate exactly two positional paths.
Code
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: publish.sh [-n] [-v] [-o DIR] <source-dir> <destination-dir>
-n dry run: print actions without copying
-v verbose: list every copied file
-o write a summary report into DIR (default: .)
-h show this help
EOF
}
dry_run=0
verbose=0
report_dir="."
while getopts ":nvo:h" opt; do
case "$opt" in
n) dry_run=1 ;;
v) verbose=1 ;;
o) report_dir="$OPTARG" ;;
h) usage; exit 0 ;;
:)
printf 'option -%s requires an argument\n' "$OPTARG" >&2
usage >&2
exit 2
;;
\?)
printf 'unknown option -%s\n' "$OPTARG" >&2
usage >&2
exit 2
;;
esac
done
shift "$((OPTIND - 1))"
if (( $# != 2 )); then
printf 'expected 2 positional arguments, got %d\n' "$#" >&2
usage >&2
exit 2
fi
source_dir=$1
destination_dir=$2
if [[ ! -d "$source_dir" ]]; then
printf 'source directory does not exist: %s\n' "$source_dir" >&2
exit 1
fi
if [[ -z "$report_dir" ]]; then
printf 'report directory must not be empty\n' >&2
exit 2
fi
if (( dry_run )); then
printf 'would copy %s -> %s\n' "$source_dir" "$destination_dir"
printf 'would write report to %s\n' "$report_dir"
exit 0
fi
mkdir -p -- "$destination_dir" "$report_dir"
copied=0
while IFS= read -r -d '' file; do
relative=${file#"$source_dir"/}
mkdir -p -- "$destination_dir/$(dirname -- "$relative")"
cp -- "$file" "$destination_dir/$relative"
copied=$((copied + 1))
if (( verbose )); then
printf 'copied %s\n' "$relative"
fi
done < <(find "$source_dir" -type f -print0)
printf 'copied %d files to %s\n' "$copied" "$destination_dir" |
tee -- "$report_dir/publish-report.txt"Report a problem with this exampleOpens a pre-filled issue on GitHub.