#!/usr/bin/env bash set -euo pipefail usage() { cat <<'USAGE' Copy files via scp or rsync with consistent options. Usage: ssh_copy.sh [options] push HOST LOCAL_PATH REMOTE_PATH ssh_copy.sh [options] pull HOST REMOTE_PATH LOCAL_PATH Options: -u, --user USER Override SSH user (or set REMOTE_USER) -p, --port PORT SSH port (default: REMOTE_PORT or 22) -i, --key PATH Identity file (default: REMOTE_KEY) -r, --recursive Copy directories recursively (scp mode only; rsync always recurses) --rsync Use rsync instead of scp (incremental, shows progress) --delete With --rsync push: delete remote files not present in source --accept-new Set StrictHostKeyChecking=accept-new --dry-run Print the command that would run -h, --help Show help Environment defaults: REMOTE_USER, REMOTE_PORT, REMOTE_KEY Examples: ssh_copy.sh push my-server ./app.tar.gz /tmp/app.tar.gz ssh_copy.sh --rsync push my-server ./dist/ /var/www/html/ ssh_copy.sh --rsync --delete push my-server ./dist/ /var/www/html/ ssh_copy.sh --user ubuntu pull 10.0.0.1 /var/log/syslog ./syslog USAGE } port="${REMOTE_PORT:-22}" user="${REMOTE_USER:-}" key="${REMOTE_KEY:-}" recursive=false use_rsync=false delete_mode=false accept_new=false dry_run=false while [[ $# -gt 0 ]]; do case "$1" in -u|--user) user="${2:-}"; shift 2 ;; -p|--port) port="${2:-}"; shift 2 ;; -i|--key) key="${2:-}"; shift 2 ;; -r|--recursive) recursive=true; shift ;; --rsync) use_rsync=true; shift ;; --delete) delete_mode=true; shift ;; --accept-new) accept_new=true; shift ;; --dry-run) dry_run=true; shift ;; -h|--help) usage; exit 0 ;; -*) echo "Unknown option: $1" >&2 usage >&2 exit 2 ;; *) break ;; esac done if [[ $# -lt 4 ]]; then usage >&2 exit 2 fi direction="$1"; shift host="$1"; shift dest_host="$host" if [[ -n "$user" ]]; then host_no_user="${host#*@}" dest_host="${user}@${host_no_user}" fi if $use_rsync; then if ! command -v rsync >/dev/null 2>&1; then echo "rsync is required but not installed." >&2 exit 1 fi ssh_parts=("ssh" "-p" "$port") if [[ -n "$key" ]]; then ssh_parts+=("-i" "$key" "-o" "IdentitiesOnly=yes") fi if $accept_new; then ssh_parts+=("-o" "StrictHostKeyChecking=accept-new") fi rsync_opts=(-avz --progress -e "${ssh_parts[*]}") case "$direction" in push) local_path="$1" remote_path="$2" if $delete_mode; then rsync_opts+=(--delete) fi full_cmd=(rsync "${rsync_opts[@]}" "$local_path" "${dest_host}:${remote_path}") ;; pull) remote_path="$1" local_path="$2" full_cmd=(rsync "${rsync_opts[@]}" "${dest_host}:${remote_path}" "$local_path") ;; *) echo "Unknown direction: $direction (expected: push|pull)" >&2 usage >&2 exit 2 ;; esac else scp_opts=(-P "$port" -p) if [[ -n "$key" ]]; then scp_opts+=(-i "$key" -o "IdentitiesOnly=yes") fi if $recursive; then scp_opts+=(-r) fi if $accept_new; then scp_opts+=(-o "StrictHostKeyChecking=accept-new") fi case "$direction" in push) local_path="$1" remote_path="$2" full_cmd=(scp "${scp_opts[@]}" "$local_path" "${dest_host}:${remote_path}") ;; pull) remote_path="$1" local_path="$2" full_cmd=(scp "${scp_opts[@]}" "${dest_host}:${remote_path}" "$local_path") ;; *) echo "Unknown direction: $direction (expected: push|pull)" >&2 usage >&2 exit 2 ;; esac fi if $dry_run; then printf '%q ' "${full_cmd[@]}" printf '\n' exit 0 fi "${full_cmd[@]}"