Created
October 22, 2025 14:02
-
-
Save AdamGagorik/7bc5a2852c84d923a4313a3ff07a89e7 to your computer and use it in GitHub Desktop.
Rsync wrapper with 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="${SEARCH_ROOT:-$HOME}" | |
| SEARCH_DEPTH="${SEARCH_DEPTH:-3}" | |
| NO_PROMPT="${NO_PROMPT:-false}" | |
| SRC_PATH="" | |
| DST_PATH="" | |
| declare -a FZF_ARGS=() | |
| 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 | |
| -d, --depth <int> The --max-depth to use when searching | |
| -u, --unrestricted Display all the files when searching | |
| -I, --no-ignore Display ignored files when searching | |
| -H, --no-hidden Display hidden files when searching | |
| -h, --help Show this help message and exit | |
| EOF | |
| } | |
| parse_args() { | |
| local parsed_options | |
| parsed_options=$(getopt -o s:fi:o:e:d:uIHh --long search:,no-prompt,src:,dst:,exclude:,depth:,unrestricted,no-ignore,no-hidden,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 | |
| ;; | |
| -d|--depth) | |
| SEARCH_DEPTH="$2" | |
| shift 2 | |
| ;; | |
| -u|--unrestricted) | |
| FZF_ARGS+=("-u") | |
| shift | |
| ;; | |
| -I|--unrestricted) | |
| FZF_ARGS+=("-I") | |
| shift | |
| ;; | |
| -H|--no-hidden) | |
| FZF_ARGS+=("-H") | |
| shift | |
| ;; | |
| --) | |
| 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 | |
| HEADER="Choose Source Directory : run with -u if files are not shown!" | |
| SRC_PATH="$(fd . "${FZF_ARGS[@]}" --type d --max-depth 1 | fzf --header "${HEADER}" --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 | |
| HEADER="Choose Output Directory : run with -u if files are not shown!" | |
| DST_PATH="$(fd . "${SEARCH_ROOT}" "${FZF_ARGS[@]}" --type d --max-depth "${SEARCH_DEPTH}" | fzf --header "${HEADER}" --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