Created
October 1, 2025 17:41
-
-
Save AdamGagorik/0b44edaf4e7d16531f2d0982eb71639a to your computer and use it in GitHub Desktop.
Rsync + fd + fzf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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