Created
April 22, 2026 22:24
-
-
Save kevin1sMe/c2b368b7803f6cdb768bc5507219a2f7 to your computer and use it in GitHub Desktop.
iterm_smart_paste.sh
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
| #!/bin/bash | |
| # iterm_smart_paste.sh — iTerm2 零配置剪贴板文件/图片粘贴 | |
| # | |
| # 功能: | |
| # 1. Finder 中复制的文件 → 保留原始文件名上传到远程 | |
| # 2. 截图/图片数据 → 以 clip_<时间戳>.png 上传 | |
| # 3. 自动检测当前 iTerm2 会话的 SSH / autossh / mosh 连接 | |
| # 4. 尽量复用当前连接里的 host / user / port / identity / jump 配置 | |
| # 5. 超过 20MB 的文件自动跳过并提示 | |
| # | |
| # 可选环境变量: | |
| # ITERM_SMART_PASTE_TARGET=user@host 手动指定远程目标 | |
| # ITERM_SMART_PASTE_PORT=2222 配合 TARGET 指定端口 | |
| # ITERM_SMART_PASTE_LOG_FILE=/path 指定日志文件路径 | |
| # ITERM_SMART_PASTE_DEBUG=1 输出更详细的探测日志 | |
| # | |
| # iTerm2 快捷键设置: | |
| # Action: Run Coprocess... | |
| # Parameters: /bin/bash ~/bin/iterm_smart_paste.sh | |
| REMOTE_DIR="/tmp/remote-paste-files" | |
| MAX_SIZE=$((20 * 1024 * 1024)) # 20MB | |
| LOG_FILE="${ITERM_SMART_PASTE_LOG_FILE:-/tmp/iterm_smart_paste.log}" | |
| SSH_DEST="" | |
| SSH_ARGS=() | |
| SCP_ARGS=() | |
| # ============================================================ | |
| # 工具函数 | |
| # ============================================================ | |
| notify() { | |
| osascript -e "display notification \"$1\" with title \"${2:-Paste}\"" >/dev/null 2>&1 | |
| } | |
| append_log() { | |
| printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" >>"$LOG_FILE" 2>/dev/null || true | |
| } | |
| log_info() { | |
| append_log "INFO" "$*" | |
| } | |
| log_warn() { | |
| append_log "WARN" "$*" | |
| } | |
| log_error() { | |
| append_log "ERROR" "$*" | |
| } | |
| debug_log() { | |
| [ -n "$ITERM_SMART_PASTE_DEBUG" ] || return 0 | |
| append_log "DEBUG" "$*" | |
| } | |
| log_command_stderr() { | |
| local tag="$1" | |
| local stderr_file="$2" | |
| [ -s "$stderr_file" ] || return 0 | |
| while IFS= read -r line; do | |
| log_error "[$tag] $line" | |
| done < "$stderr_file" | |
| } | |
| run_with_logged_stderr() { | |
| local tag="$1" | |
| shift | |
| local stderr_file rc | |
| stderr_file="$(mktemp /tmp/iterm-smart-paste-stderr.XXXXXX)" || return 1 | |
| "$@" 2>"$stderr_file" | |
| rc=$? | |
| log_command_stderr "$tag" "$stderr_file" | |
| rm -f "$stderr_file" | |
| return $rc | |
| } | |
| remote_escape() { | |
| printf '%q' "$1" | |
| } | |
| trim_quotes() { | |
| local value="$1" | |
| value="${value#\"}" | |
| value="${value%\"}" | |
| value="${value#\'}" | |
| value="${value%\'}" | |
| printf '%s' "$value" | |
| } | |
| configure_remote_target() { | |
| local dest="$1" | |
| shift | |
| SSH_DEST="$(trim_quotes "$dest")" | |
| SSH_ARGS=() | |
| SCP_ARGS=() | |
| while [ $# -gt 0 ]; do | |
| SSH_ARGS+=("$1") | |
| shift | |
| done | |
| local i=0 | |
| while [ $i -lt ${#SSH_ARGS[@]} ]; do | |
| case "${SSH_ARGS[$i]}" in | |
| -p) | |
| if [ $((i + 1)) -lt ${#SSH_ARGS[@]} ]; then | |
| SCP_ARGS+=("-P" "${SSH_ARGS[$((i + 1))]}") | |
| fi | |
| ((i += 2)) | |
| ;; | |
| -F|-i|-J|-l|-o|-S) | |
| if [ $((i + 1)) -lt ${#SSH_ARGS[@]} ]; then | |
| SCP_ARGS+=("${SSH_ARGS[$i]}" "${SSH_ARGS[$((i + 1))]}") | |
| fi | |
| ((i += 2)) | |
| ;; | |
| *) | |
| ((i++)) | |
| ;; | |
| esac | |
| done | |
| log_info "configured target dest=$SSH_DEST ssh_args=${SSH_ARGS[*]} scp_args=${SCP_ARGS[*]}" | |
| } | |
| parse_ssh_command() { | |
| local cmdline="$1" | |
| local -a args parsed | |
| local i=1 | |
| local arg value dest="" | |
| read -ra args <<< "$cmdline" | |
| [ ${#args[@]} -gt 0 ] || return 1 | |
| while [ $i -lt ${#args[@]} ]; do | |
| arg="${args[$i]}" | |
| case "$arg" in | |
| --) | |
| ((i++)) | |
| if [ $i -lt ${#args[@]} ]; then | |
| dest="${args[$i]}" | |
| fi | |
| break | |
| ;; | |
| -F|-i|-J|-l|-o|-p|-S) | |
| if [ $((i + 1)) -ge ${#args[@]} ]; then | |
| return 1 | |
| fi | |
| value="$(trim_quotes "${args[$((i + 1))]}")" | |
| parsed+=("$arg" "$value") | |
| ((i += 2)) | |
| ;; | |
| -F*|-i*|-J*|-l*|-p*|-S*) | |
| value="$(trim_quotes "${arg:2}")" | |
| [ -n "$value" ] || return 1 | |
| parsed+=("${arg:0:2}" "$value") | |
| ((i++)) | |
| ;; | |
| -o*) | |
| value="$(trim_quotes "${arg:2}")" | |
| [ -n "$value" ] || return 1 | |
| parsed+=("-o" "$value") | |
| ((i++)) | |
| ;; | |
| -*) | |
| ((i++)) | |
| ;; | |
| *) | |
| dest="$(trim_quotes "$arg")" | |
| break | |
| ;; | |
| esac | |
| done | |
| [ -n "$dest" ] || return 1 | |
| configure_remote_target "$dest" "${parsed[@]}" | |
| } | |
| parse_mosh_command() { | |
| local cmdline="$1" | |
| local -a args | |
| local i=1 | |
| local arg value dest="" port="" | |
| read -ra args <<< "$cmdline" | |
| [ ${#args[@]} -gt 0 ] || return 1 | |
| while [ $i -lt ${#args[@]} ]; do | |
| arg="${args[$i]}" | |
| case "$arg" in | |
| --ssh=*) | |
| value="$(trim_quotes "${arg#--ssh=}")" | |
| if [[ "$value" =~ -p[[:space:]]*([0-9]+) ]]; then | |
| port="${BASH_REMATCH[1]}" | |
| fi | |
| ((i++)) | |
| ;; | |
| --port=*) | |
| port="$(trim_quotes "${arg#--port=}")" | |
| ((i++)) | |
| ;; | |
| -p) | |
| if [ $((i + 1)) -lt ${#args[@]} ]; then | |
| port="$(trim_quotes "${args[$((i + 1))]}")" | |
| fi | |
| ((i += 2)) | |
| ;; | |
| --) | |
| ((i++)) | |
| if [ $i -lt ${#args[@]} ]; then | |
| dest="${args[$i]}" | |
| fi | |
| break | |
| ;; | |
| -*) | |
| ((i++)) | |
| ;; | |
| *) | |
| dest="$(trim_quotes "$arg")" | |
| break | |
| ;; | |
| esac | |
| done | |
| [ -n "$dest" ] || return 1 | |
| if [ -n "$port" ]; then | |
| configure_remote_target "$dest" -p "$port" | |
| else | |
| configure_remote_target "$dest" | |
| fi | |
| } | |
| parse_mosh_client_command() { | |
| local cmdline="$1" | |
| local -a args | |
| local dest="" | |
| read -ra args <<< "$cmdline" | |
| if [ ${#args[@]} -ge 2 ]; then | |
| dest="$(trim_quotes "${args[1]}")" | |
| fi | |
| [ -n "$dest" ] || return 1 | |
| configure_remote_target "$dest" | |
| } | |
| detect_remote_target() { | |
| if [ -n "$ITERM_SMART_PASTE_TARGET" ]; then | |
| if [ -n "$ITERM_SMART_PASTE_PORT" ]; then | |
| configure_remote_target "$ITERM_SMART_PASTE_TARGET" -p "$ITERM_SMART_PASTE_PORT" | |
| else | |
| configure_remote_target "$ITERM_SMART_PASTE_TARGET" | |
| fi | |
| log_info "using manual override target=$ITERM_SMART_PASTE_TARGET" | |
| return 0 | |
| fi | |
| local session_tty="$1" | |
| local tty_name comm pid args | |
| [ -n "$session_tty" ] || return 1 | |
| tty_name="${session_tty#/dev/}" | |
| debug_log "detecting remote target from tty=$tty_name" | |
| while read -r pid tty_name comm args; do | |
| [ -n "$pid" ] || continue | |
| debug_log "candidate pid=$pid comm=$comm args=$args" | |
| case "$comm" in | |
| ssh|autossh|slogin) | |
| if parse_ssh_command "$args"; then | |
| log_info "matched remote session via $comm pid=$pid dest=$SSH_DEST" | |
| return 0 | |
| fi | |
| ;; | |
| mosh) | |
| if parse_mosh_command "$args"; then | |
| log_info "matched remote session via mosh pid=$pid dest=$SSH_DEST" | |
| return 0 | |
| fi | |
| ;; | |
| mosh-client) | |
| if parse_mosh_client_command "$args"; then | |
| log_info "matched remote session via mosh-client pid=$pid dest=$SSH_DEST" | |
| return 0 | |
| fi | |
| ;; | |
| esac | |
| done < <(ps -axo pid=,tty=,comm=,args= 2>/dev/null | awk -v target_tty="$tty_name" '$2 == target_tty { print }' | sort -nr) | |
| return 1 | |
| } | |
| run_remote_ssh() { | |
| local remote_cmd="$1" | |
| debug_log "running ssh dest=$SSH_DEST cmd=$remote_cmd" | |
| run_with_logged_stderr "ssh" \ | |
| ssh -q "${SSH_ARGS[@]}" -o BatchMode=yes -o ConnectTimeout=5 \ | |
| "$SSH_DEST" "$remote_cmd" | |
| } | |
| run_remote_scp() { | |
| local local_path="$1" | |
| local remote_path="$2" | |
| debug_log "running scp local=$local_path dest=$SSH_DEST remote=$remote_path" | |
| run_with_logged_stderr "scp" \ | |
| scp -q "${SCP_ARGS[@]}" -o BatchMode=yes -o ConnectTimeout=5 \ | |
| "$local_path" "${SSH_DEST}:$(remote_escape "$remote_path")" | |
| } | |
| ensure_remote_dir() { | |
| [ -n "$SSH_DEST" ] || return 1 | |
| run_remote_ssh "mkdir -p $(remote_escape "$REMOTE_DIR")" >/dev/null 2>&1 | |
| } | |
| remote_file_exists() { | |
| local remote_path="$1" | |
| run_remote_ssh "test -e $(remote_escape "$remote_path")" >/dev/null 2>&1 | |
| } | |
| upload_to_remote() { | |
| local local_path="$1" | |
| local remote_name="$2" | |
| local remote_path="$REMOTE_DIR/$remote_name" | |
| log_info "upload start local=$local_path remote=$remote_path dest=$SSH_DEST" | |
| run_remote_scp "$local_path" "$remote_path" || return 1 | |
| remote_file_exists "$remote_path" || return 1 | |
| log_info "upload success local=$local_path remote=$remote_path dest=$SSH_DEST" | |
| printf '%s' "$remote_path" | |
| } | |
| # ============================================================ | |
| # 0. 查找 pngpaste | |
| # ============================================================ | |
| PNGPASTE_CMD="/opt/homebrew/bin/pngpaste" | |
| [ ! -x "$PNGPASTE_CMD" ] && PNGPASTE_CMD="/usr/local/bin/pngpaste" | |
| [ ! -x "$PNGPASTE_CMD" ] && PNGPASTE_CMD="$(command -v pngpaste 2>/dev/null)" | |
| touch "$LOG_FILE" 2>/dev/null || true | |
| trap 'rc=$?; append_log "INFO" "script exit rc=$rc pid=$$"' EXIT | |
| log_info "----- script start pid=$$ -----" | |
| log_info "log_file=$LOG_FILE" | |
| log_info "pngpaste_cmd=${PNGPASTE_CMD:-<missing>}" | |
| # ============================================================ | |
| # 1. 检测当前 iTerm2 会话的 SSH 连接 | |
| # ============================================================ | |
| SESSION_TTY=$(osascript -e ' | |
| tell application "iTerm2" | |
| tell current session of current window | |
| return tty | |
| end tell | |
| end tell' 2>/dev/null) | |
| log_info "session_tty=${SESSION_TTY:-<none>}" | |
| if ! detect_remote_target "$SESSION_TTY"; then | |
| log_warn "no remote target detected from current iTerm session" | |
| fi | |
| # ============================================================ | |
| # 2a. 尝试获取 Finder 复制的文件路径 | |
| # ============================================================ | |
| FILE_PATHS=$(osascript <<'APPLESCRIPT' 2>/dev/null | |
| use framework "AppKit" | |
| use scripting additions | |
| set pb to current application's NSPasteboard's generalPasteboard() | |
| set fileURLs to pb's readObjectsForClasses:{current application's NSURL} options:(missing value) | |
| if fileURLs is missing value then return "" | |
| if (count of fileURLs) is 0 then return "" | |
| set output to "" | |
| repeat with fileURL in fileURLs | |
| if (fileURL's isFileURL()) as boolean then | |
| set filePath to (fileURL's |path|()) as text | |
| if output is not "" then | |
| set output to output & linefeed | |
| end if | |
| set output to output & filePath | |
| end if | |
| end repeat | |
| return output | |
| APPLESCRIPT | |
| ) | |
| VALID_FILES=() | |
| if [ -n "$FILE_PATHS" ]; then | |
| while IFS= read -r fpath; do | |
| [ -z "$fpath" ] && continue | |
| [ -f "$fpath" ] && VALID_FILES+=("$fpath") | |
| done <<< "$FILE_PATHS" | |
| fi | |
| log_info "clipboard file count=${#VALID_FILES[@]}" | |
| if [ ${#VALID_FILES[@]} -gt 0 ]; then | |
| if [ -n "$SSH_DEST" ]; then | |
| if ! ensure_remote_dir; then | |
| log_error "failed to create remote dir dest=$SSH_DEST dir=$REMOTE_DIR" | |
| notify "远程目录创建失败:$SSH_DEST" "Paste" | |
| exit 1 | |
| fi | |
| OUTPUT="" | |
| SKIPPED=0 | |
| FAILED=0 | |
| for fpath in "${VALID_FILES[@]}"; do | |
| fname=$(basename "$fpath") | |
| fsize=$(stat -f%z "$fpath" 2>/dev/null || echo 0) | |
| if [ "$fsize" -gt "$MAX_SIZE" ]; then | |
| SKIPPED=$((SKIPPED + 1)) | |
| log_warn "skip oversized file local=$fpath size=$fsize limit=$MAX_SIZE" | |
| notify "文件 $fname 超过 20MB,已跳过" "Paste" | |
| continue | |
| fi | |
| REMOTE_PATH=$(upload_to_remote "$fpath" "$fname") | |
| if [ $? -eq 0 ] && [ -n "$REMOTE_PATH" ]; then | |
| OUTPUT="${OUTPUT:+$OUTPUT }$(remote_escape "$REMOTE_PATH")" | |
| else | |
| FAILED=$((FAILED + 1)) | |
| log_error "upload failed local=$fpath remote=$REMOTE_DIR/$fname dest=$SSH_DEST" | |
| fi | |
| done | |
| if [ -n "$OUTPUT" ]; then | |
| log_info "file upload output=$OUTPUT" | |
| echo -n "$OUTPUT" | |
| [ "$FAILED" -gt 0 ] && notify "部分文件上传失败,请检查 SSH 参数或远程权限" "Paste" | |
| elif [ "$FAILED" -gt 0 ]; then | |
| log_error "all file uploads failed dest=$SSH_DEST" | |
| notify "上传失败:$SSH_DEST" "Paste" | |
| exit 1 | |
| fi | |
| else | |
| OUTPUT="" | |
| for fpath in "${VALID_FILES[@]}"; do | |
| OUTPUT="${OUTPUT:+$OUTPUT }$(remote_escape "$fpath")" | |
| done | |
| log_warn "no remote session, returning local file paths output=$OUTPUT" | |
| echo -n "$OUTPUT" | |
| notify "未检测到 SSH 会话,输出本地路径" "Paste" | |
| fi | |
| exit 0 | |
| fi | |
| # ============================================================ | |
| # 2b. 尝试获取剪贴板中的图片数据(截图等) | |
| # ============================================================ | |
| if [ -x "$PNGPASTE_CMD" ]; then | |
| FILENAME="clip_$(date +%Y%m%d_%H%M%S)_$$.png" | |
| LOCAL_TMP_PATH="/tmp/$FILENAME" | |
| log_info "capturing clipboard image to $LOCAL_TMP_PATH" | |
| run_with_logged_stderr "pngpaste" "$PNGPASTE_CMD" "$LOCAL_TMP_PATH" >/dev/null | |
| if [ $? -eq 0 ]; then | |
| fsize=$(stat -f%z "$LOCAL_TMP_PATH" 2>/dev/null || echo 0) | |
| if [ "$fsize" -gt "$MAX_SIZE" ]; then | |
| log_warn "skip oversized image local=$LOCAL_TMP_PATH size=$fsize limit=$MAX_SIZE" | |
| notify "图片超过 20MB,已取消" "Paste" | |
| rm -f "$LOCAL_TMP_PATH" | |
| exit 1 | |
| fi | |
| if [ -n "$SSH_DEST" ]; then | |
| if ! ensure_remote_dir; then | |
| log_error "failed to create remote dir dest=$SSH_DEST dir=$REMOTE_DIR" | |
| notify "远程目录创建失败:$SSH_DEST" "Paste" | |
| rm -f "$LOCAL_TMP_PATH" | |
| exit 1 | |
| fi | |
| REMOTE_PATH=$(upload_to_remote "$LOCAL_TMP_PATH" "$FILENAME") | |
| if [ $? -eq 0 ] && [ -n "$REMOTE_PATH" ]; then | |
| log_info "image upload output=$REMOTE_PATH" | |
| echo -n "$(remote_escape "$REMOTE_PATH")" | |
| rm -f "$LOCAL_TMP_PATH" | |
| else | |
| log_error "image upload failed local=$LOCAL_TMP_PATH remote=$REMOTE_DIR/$FILENAME dest=$SSH_DEST" | |
| notify "图片上传失败:$SSH_DEST" "Paste" | |
| rm -f "$LOCAL_TMP_PATH" | |
| exit 1 | |
| fi | |
| else | |
| log_warn "no remote session, returning local image path path=$LOCAL_TMP_PATH" | |
| echo -n "$(remote_escape "$LOCAL_TMP_PATH")" | |
| notify "未检测到 SSH 会话,输出本地路径" "Paste" | |
| fi | |
| exit 0 | |
| fi | |
| log_warn "pngpaste did not produce an image from clipboard" | |
| else | |
| log_warn "pngpaste not found" | |
| fi | |
| # 剪贴板中没有可用的文件或图片 | |
| log_warn "clipboard contains no usable file or image" | |
| exit 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment