EMACS-DOCUMENT

=============>随便,谢谢

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 NewsReddit的讨论也值得一看。

我们希望能够在不执行命令的情况下整合粘贴的内容。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启用剪贴板访问。

  1. 通过tmux识别我们什么时候粘贴到Emacs。
  2. 使用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-pasteC-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 仓库。 这里的问题是 pbpastepbcopy 在tmux下不起作用。这个问题可以通过某个没有写入文档的函数来解决。

  1. 安装 reattach-to-user-namespace.

    brew install reattach-to-user-namespace
    
  2. 将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整合.