ZSH, tmux, Emacs 以及 SSH: 一个关于粘帖复制的故事
目录
如何在尽可能多的终端应用程序中启用复制-粘贴支持的说明和代码。包括完整的GUI $DISPLAY
, 通过SSH登录,同时支持macOS和Linux上,并在ZSH, tmux和Emacs之间相互复制。
tl;dr: 请参见我 dotfiles 中的配置,包括 剪贴板复制, 剪贴板粘帖, Emacs整合 和 tmux整合.
在多个环境中实现复制粘贴是 极度 困难的。下面的任何一个原因都会使问题复杂化,而且综合起来,难度会上升到前所未见过的复杂程度。
- 粘贴到终端的内容会被逐字解析,这意味着攻击者可以在复制外表无害的HTML命令时通过CSS诡计隐藏
;sudo do $really_evil_thing;
. 因此,我们不能盲目地将文本粘贴到终端中。 - 在终端中,Emacs不能访问剪贴板。
- Emacs有自己独立的剪贴板概念,叫做kill ring。
- tmux有自己独立的剪贴板概念,称为
paste buffers
. - zsh有自己独立的剪贴板概念,也叫kill ring。
- 在远程SSH会话中没有剪贴板,因为剪贴板是一个窗口系统的概念。
- 在X11中有3个独立的“剪贴板”,primary selection,clipboard selection,以及过时的cut buffers。
- macOS中的tmux在默认情况下不能访问剪贴板。
我们将逐个处理各个问题,制定统一的解决方案。
共享函数
我们需要几个函数来跨macOS和Linux统一复制-粘贴处理。 我这里只包含了最核心的几个函数。其余的辅助函数可以在我的dotfiles中找到,在zsh/functions下。
#!/bin/zsh # Copies data to clipboard from stdin. function clipboard-copy() { emulate -L zsh local clipper_port=8377 local fake_clipboard=/tmp/clipboard-data.txt if is-ssh && is-port-in-use $clipper_port; then # Pipe to the clipper instance and the fake clipboard. tee >(nc localhost $clipper_port) "$fake_clipboard" return fi if ! has-display; then # Copy to fake_clipboard. > fake_clipboard return fi if is-darwin; then pbcopy elif is-cygwin; then cat > /dev/clipboard else if command-exists xclip; then xclip -in -selection clipboard elif command-exists xsel; then xsel --clipboard --input else local message="clipboard-copy: Platform $(uname -s) not supported or " message+="xclip/xsel not installed" print message >&2 return 1 fi fi } clipboard-copy $@
#!/bin/zsh # Pastes data from the clipboard to stdout function clipboard-paste() { emulate -L zsh # If there's no X11 display, then fallback to our hacky reimplementation. The # data is populated by clipboard-copy. if ! has-display; then local fake_clipboard=/tmp/clipboard-data.txt [[ -e $fake_clipboard ]] && cat $fake_clipboard return fi if is-darwin; then pbpaste elif is-cygwin; then cat /dev/clipboard else if command-exists xclip; then xclip -out -selection clipboard elif command-exists xsel; then xsel --clipboard --output else message="clipboard-paste: Platform $GRML_OSTYPE not supported " message+="or xclip/xsel not installed" print $message >&2 return 1 fi fi } clipboard-paste $@
问题:粘贴的文本在终端中逐字解释
当终端中粘贴内容时,终端将与输入命令序列相同的方式解释您所粘贴的内容。 参见本网站了解定位剪贴板攻击的示例。由此产生的Hacker News和Reddit的讨论也值得一看。
我们希望能够在不执行命令的情况下整合粘贴的内容。ZSH可以使用ZSH line editor (ZLE)和 widgets 来编辑多行文本。 因此,我们可以将粘贴的文本转储到编辑缓冲区中,同时保证它不会被执行。
注意: 启用 Bracketed paste mode 后似乎没有必要使用本方法,但我不能100%肯定能防范所有剪贴板攻击。
widget-paste-from-clipboard
#!/bin/zsh # Pastes the current clipboard and adds it to the kill ring. function widget-paste-from-clipboard() { local paste_data=$(clipboard-paste \ | remove-trailing-empty-lines \ | remove-leading-empty-lines) zle copy-region-as-kill "$paste_data" LBUFFER=${LBUFFER}${paste_data} }
然后,我们在ZSH中绑定这个函数。
# Gotta catch them all. bindkey -M emacs 'C-y' widget-paste-from-clipboard bindkey -M viins 'C-y' widget-paste-from-clipboard bindkey -M vicmd 'C-y' widget-paste-from-clipboard
问题:终端Emacs无法访问剪贴板
在GUI Emacs中,一切都很好地集成在一起。在终端模式,即 emacs -nw
, emacs没有链接 到任何X11库。
因此,在终端模式下,Emacs不知道如何从剪切板读取数据或将数据放到剪贴板上。我们可以通过两个步骤为终端Emacs启用剪贴板访问。
- 通过tmux识别我们什么时候粘贴到Emacs。
- 使用emacsclient调用带有粘贴数据的函数。
注意:这里假定Emacs始终在tmux会话中运行。
第一步,我们需要 $PATH
包含以下shell函数。
~/bin/tmux-smart-paste
#!/bin/zsh # Paste specially into programs that know how to handle paste events. function tmux-smart-paste() { # display-message -p prints to stdout. local current_program=$(tmux display-message -p '#{window_name}') if [[ $current_program == 'zsh' ]]; then # ZSH must have C-y bound to a smart paste for this to work. tmux send-keys 'C-y' elif [[ ${current_program:l} == 'emacs' ]]; then emacsclient --no-wait --alternate-editor=false --quiet \ --eval "(my:paste-from-emacs-client)" \ 2>&1 > /dev/null else tmux set-buffer "$(clipboard-paste)" tmux paste-buffer fi } tmux-smart-paste
Next, we bind tmux-smart-paste
in tmux.conf to C-y
.
接下来,我们在tmux.conf中绑定 tmux-smart-paste
为 C-y
.
bind-key -T root C-y run-shell "tmux-smart-paste"
第二步,我们需要以下emacs-lisp函数。
(defun my:paste-from-emacs-client () "Paste into a terminal Emacs." (if window-system (error "Trying to paste into GUI emacs.") (let ((paste-data (s-trim (shell-command-to-string "clipboard-paste")))) (with-current-buffer (window-buffer) (insert paste-data)) (kill-new paste-data))))
注意:终端Emacs必须运行服务端才能工作。
问题:tmux使用paste buffer而不是剪贴板
较新的tmux版本中的 copy-pipe-and-cancel
正是我们所需要的。它只在visual selection 模式下使用 y
粘帖所选内容。
bind-key -T copy-mode-vi 'y' send-keys -X copy-pipe-and-cancel "clipboard-copy"
问题:macOS下的tmux无法访问剪贴板
关于tmux和macOS整合的权威参考文献是ChrisJohnsen的tmux-macosx-pasteboard 仓库。
这里的问题是 pbpaste
和 pbcopy
在tmux下不起作用。这个问题可以通过某个没有写入文档的函数来解决。
安装
reattach-to-user-namespace
.brew install reattach-to-user-namespace
将tmux配置为使用
reattachto-user-namespace
来调用shell。tmux.conf - load Darwin conf
# NOTE: tmux runs commands with 'sh', so the command must be POSIX compliant. # That means no ZSH functions. Use executables on PATH instead. if-shell '[ "$(uname -s)" = "Darwin" ]' 'source-file ~/.config/tmux/osx.conf'
~/.config/tmux/osx.conf
# Tmux options for OSX. # Hack to enable pbcopy and pbpaste to work in Tmux. See # https://github.com/ChrisJohnsen/tmux-MacOSX-pasteboard. set-option -g default-command 'reattach-to-user-namespace -l zsh'
问题:远程SSH sesssions无法通过剪贴板访问本地会话
当你通过SSH登录远程计算机时,若能在终端复制文本在本地计算机用那就太好了. 通常,人们的实现方法是通过鼠标选择文本然后在终端模拟器(例如 iterm2)上调用复制 。
我们希望能够使用普通的tmux命令从远程SSH会话复制文本,并将其放在本地剪贴板上。
Clipper就是为这个使用场景量身定制的,因为它提供了“供本地和远程tmux会话访问的剪贴板”。
clipper在远程服务器和本地运行后,我们就可以通过修改 clipboard-copy
函数向它发送数据了。
function clipboard-copy() { local clipper_port=8377 if is-ssh && is-port-in-use $clipper_port; then nc localhost $clipper_port return fi }
配置文件中的最新代码
最新代码在我的 dotfiles 仓库中。有意思的部分包括 clipboard-copy, clipboard-paste, Emacs整合 以及 tmux整合.