Skip to content

Instantly share code, notes, and snippets.

@AdamGagorik
Created October 1, 2025 17:41
Show Gist options
  • Save AdamGagorik/0b44edaf4e7d16531f2d0982eb71639a to your computer and use it in GitHub Desktop.
Save AdamGagorik/0b44edaf4e7d16531f2d0982eb71639a to your computer and use it in GitHub Desktop.
Rsync + fd + fzf
#!/usr/bin/env bash
set -euo pipefail
EXCLUDE_PATTERNS=()
SEARCH_ROOT="/"
NO_PROMPT=false
SRC_PATH=""
DST_PATH=""
usage() {
cat <<EOF
Usage: $0 [options]
Options:
-i, --src <path> Source directory path
-o, --dst <path> Destination directory path
-s, --search <path> Search this directory for destination
-e, --exclude <pattern> Exclude pattern for rsync (can be used multiple times)
-f, --no-prompt Skip confirmation prompt before copying
-h, --help Show this help message and exit
EOF
}
parse_args() {
local parsed_options
parsed_options=$(getopt -o s:fi:o:e:h --long search:,no-prompt,src:,dst:,exclude:,help -- "$@")
if [[ $? -ne 0 ]]; then
echo "Error: Failed to parse options." >&2
exit 1
fi
eval set -- "$parsed_options"
while true; do
case "$1" in
-h|--help)
usage
exit 0
;;
-s|--search)
SEARCH_ROOT="$2"
shift 2
;;
-f|--no-prompt)
NO_PROMPT=true
shift
;;
-e|--exclude)
EXCLUDE_PATTERNS+=("$2")
shift 2
;;
-i|--src)
SRC_PATH="$2"
shift 2
;;
-o|--dst)
DST_PATH="$2"
shift 2
;;
--)
shift
break
;;
*)
echo "Error: Parsing arguments: $1" >&2
exit 1
;;
esac
done
if [[ "$#" -gt 0 ]]; then
echo "Error: Unrecognized arguments: $*" >&2
exit 1
fi
}
enforce_tooling() {
local cmd
for cmd in fd fzf rsync realpath getopt; do
if ! command -v "$cmd" &> /dev/null; then
echo "Error: Required command '$cmd' is not installed." >&2
exit 1
fi
done
}
handle_src_path() {
if [[ -z "${SRC_PATH}" ]]; then
SRC_PATH="$(fd . -u --type d --max-depth 1 | fzf --header 'Choose Source Directory' --height 50%)"
fi
if [[ ! -z "${SRC_PATH}" ]]; then
SRC_PATH="$(realpath "${SRC_PATH}")"
fi
echo "SRC_PATH: ${SRC_PATH}"
if [ ! -d "${SRC_PATH}" ]; then
echo "missing SRC_PATH!"
exit 1
fi
}
handle_dst_path() {
if [[ -z "${DST_PATH}" ]]; then
DST_PATH="$(fd . "${SEARCH_ROOT}" -u --type d --max-depth 3 | fzf --header 'Choose Output Directory' --height 50%)"
fi
if [[ ! -z "${DST_PATH}" ]]; then
DST_PATH="$(realpath "${DST_PATH}")"
fi
echo "DST_PATH: ${DST_PATH}"
if [ ! -d "${DST_PATH}" ]; then
echo "missing DST_PATH!"
exit 1
fi
}
do_rsync_action() {
local pattern
declare -a exclude
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
exclude+=("--exclude='${pattern}'")
done
local confirm="n"
if [ "${NO_PROMPT}" != true ]; then
read -r -p "Are you sure you want to copy SRC into DST? (y/n): " confirm
fi
if [[ "${confirm}" =~ ^[Yy]$ ]] || [ "${NO_PROMPT}" = true ]; then
mkdir -p "${DST_PATH}"
rsync -avz --delete --delete-before --info=progress2 "${exclude[@]}" "${SRC_PATH}" "${DST_PATH}"
chmod -Rv a+rwx "${DST_PATH}/$(basename "${SRC_PATH}")"
exit 0
else
echo "Copy cancelled."
exit 1
fi
}
parse_args "$@"
enforce_tooling
handle_src_path
handle_dst_path
do_rsync_action
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment