Skip to content

Instantly share code, notes, and snippets.

@kevin1sMe
Created April 22, 2026 22:24
Show Gist options
  • Select an option

  • Save kevin1sMe/c2b368b7803f6cdb768bc5507219a2f7 to your computer and use it in GitHub Desktop.

Select an option

Save kevin1sMe/c2b368b7803f6cdb768bc5507219a2f7 to your computer and use it in GitHub Desktop.
iterm_smart_paste.sh
#!/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