远程主机及本地主机之间共享 Clipboard,以及 Neovim 访问 Clipboard 全解析

Chunhui Shi
23 min readNov 2, 2022

--

前言

从本地主机登录到一台远程服务器或虚拟主机,并且把远程主机上的某个文件中的内容复制到本地是一个比较常见的场景,而我们常见的做法通常是用鼠标在命令行中选择然后复制(Ctrl + v),比如:

显然,这种方式在遇到长文本的时候非常低效,在 Terminal 中使用鼠标滚屏选择文本可不是一个很好的体验。这时候,我们需要一种能直接在 Terminal 中与 System Clipboard 交互的工具。

一、在命令行界面通过标准输入输出与 System Clipboard 交互

我们知道 Ctrl + C 和 Ctrl + V 是我们最常用的在图形界面下与 System Clipboard 交互的方式,幸运的是,Windows, MacOS 及 Linux 也都提供了在命令行界面中使用 stdio/stdout 与 System Clipboard 交互的工具。比如:

(1) Windows

Windows 自带 clip.exe,可以在 WSL 中使用:

clip.exe < test.txt

或者可以使用 Powershell 提供的 Set-ClipboardGet-Clipboard 工具来访问

ls | Set-Clipboard
Get-Clipboard > file.txt

还可以使用第三方工具 win32yank,它是使用 Rust 语言开发的:

# The --lf option pastes data unix style.
win32yank.exe -o --lf

(2) MacOS

macOS 提供了 pbcopypbpaste 用于在命令行访问 clipboard 中的内容,比如

cat file.txt | pbcopy
pbpaste

另外,iTerm2 还提供了一个叫 it2copy 的工具,可以把来自 stdin 的内容导入 clipboard,然后我们 CMD + V 可以粘贴出来。

cat file.txt | it2copy
it2copy file.txt

(3) Linux

使用 xclip/xsel, wl-copy/wl-paste 等。比如:

cat file.txt | xclip -i -sel c

-i 选项就是 — input, 它的作用就是接受 stdin 的 input。

-o 就是 — output,同理,我们可以使用:

xclip -o -sel clipboard

另外,pbcopy 原本是 macOS 中的一个工具,但有人在 Linux 中也实现了一个同样的(见:https://github.com/skaji/remote-pbcopy-iterm2),因此,我们在 remote machine 上也可以用 pbcopy 代替 xclip 来使用。

不过,由于安全的原因, iTerm2 不允许远程读取 pbpaste,因此也就没有相应的 Linux 下的 pbpaste 了,当然,这个并不影响我们粘贴内容,我们依然可以使用 CMD + V 来进行 paste。

二、把一台主机的 Clipboard 中的内容发送给另一台主机

假设我们需要把本地主机 Clipboard 中的内容发送给另一台远程主机,那太容易了,直接登录远程主机,然后 Ctrl + V 就行了。

但是,如果我们想把远程主机 Clipboard 中的内容发送给本地主机,那可就得费点周章了。因为:

  • 1)远程主机可能根本就没有 Clipboard,比如 Linux 系统本身就没有 Clipboard 这个概念,clipboard 只有图形界面才有,而图形界面并不是 Linux 系统的一部分,只是一个 GUI 程序而已。也就是说,如果我们访问的是一个只有命令行界面而没有桌面环境的 Linux Server,那么我们是没办法访问其 Clipboard 的。
  • 2)即便远程主机具有图形界面并且我们通过 VNC, MWare, VirtualBox 或 ParalellDesktop 等方式访问到远程主机的 GUI 界面,我们也无法把其 Clipboard 中的内容共享到本地。

那怎么办?

方法一:TCP 通信

假设我们现在访问的是只有命令行界面的 Linux Server,我们可以在本地主机(MacOS)上启动一个 TCP Server 侦听,然后在远端的 Linux Server 上连接到这个 TCP Server(比如通过 nc 命令),连接成功后,直接发送数据即可 (cat file.txt | nc localhost 9997)。本地主机的 TCP Server 收到数据后,可以通过调用 pbcopy 等工具把收到的数据导入到本地主机中的 System clipboard 中,这样我们就可以直接访问来自远端 Linux 主机上的数据了。

下面是两个实施这种方法的例子:

案例 1:

  • 远程主机:Ubuntu Server (headless)
  • 本地主机:MacOS
  • 目标需求:把 Linux server 上的 clipboard 内容同步到本地主机 macOS 上

步骤一:

在本地主机 macOS 上启动一个 server,把从连接中所接收到的所有内容导入到本地的 system clipboard 中去。

建立一个 plist 服务文件:local.pbcopy.9999.plist

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.pbcopy.9999</string>
<key>UserName</key>
<string>user1</string>
<key>Program</key>
<string>/usr/bin/pbcopy</string>
<key>StandardOutPath</key>
<string>/tmp/pb9999.out</string>
<key>StandardErrorPath</key>
<string>/tmp/pb9999.err</string>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockNodeName</key>
<string>localhost</string>
<key>SockServiceName</key>
<string>9999</string>
</dict>
</dict>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
</dict>
</plist>

启动该服务:

launchctl load local.pbcopy.9999.plist

步骤二:

在远端主机 Ubuntu 上,发起一个连接到 server。

由于本地主机 macOS 通常是在一个 LAN 中,不具有公网 IP,因此远端的主机 Ubuntu 通常没办法直接向其发起连接。

我们可以利用 SSH 从 MacOS 主机中主动向 远端的 Ubuntu 主机发起一个反向隧道连接(Reverse Tunnel),如下:

ssh -R 9997:localhost:9999 user@remote.com

解释:

其中 9997 是 Ubuntu 上的 port,localhost:9999 则是 macOS 中的 IP:Port。

如果不想每次都打这么一长串,我们可以在 ~/.ssh/config 中配置:

Host ubun*
RemoteForward 9997 localhost:9999

这样,每次我们连接 host name 以 ubun 开头的 server 都会自动建立反向隧道。

步骤三:

远端主机 Ubuntu 向 tunnel 中发送数据

现在有了 tunnel,我们只需要在 Ubuntu 中向 tunnel 所使用的端口(即 9997)发送数据即可,比如:

cat test.txt | nc localhost 9997

或者,复杂一点的:

nc localhost 9997 <<EOF
> `ls -ld *`
> `date`
> EOF

另外,我们在发送数据之前还可以先检查一下端口是否已在侦听:

#!/bin/bash
cnt=`(netstat -lnptu 2>/dev/null) | grep 127.0.0.1:9999 | grep -v grep | wc -l`
if [[ $cnt -eq 1 ]]; then
date | nc localhost 9999
fi

步骤四:

本地主机 macOS 访问 clipboard

由于 MacOS 中建立的 server 会执行 pbcopy 命令把从连接中收到的内容导入 system clipboard,我们可以直接使用 CMD + V 或者在命令行执行 pbpaste 把内容粘贴出来了。

案例 2:

上述的纯手动创建 Client/Server 的方式太过于繁琐,因此有人开发了一个专门的工具来完成这个事情,这个工具叫 lemonade。它使用 Go 语言实现,可以跨平台使用。有了 lemonade,我们不仅可以方便地启动 Client/Server,而且不再需要使用其他工具(比如 pbcopy/pbpaste, xclip 等)来访问 clipboard 了。

下面是使用 lemonade 来完成共享粘贴板的一个具体方法(非常简单)

步骤一:

在本地主机(MacOS) 上启动一个 server

lemonade server

server 的配置文件 ~/.config/lemonade.toml 可以指定 port:

port = 1234
allow = '192.168.0.0/24'
line-ending = 'crlf'

步骤二:

在远程主机 Ubuntu Server 上向 MacOS 发起一个连接

同样,由于本地的 MacOS 通常是在局域网中的,没有公网 IP,如果远程主机不在同一个内网就不可直接访问,我们同样需要使用上面介绍的 SSH Revserve Tunnel 来创建连接。

ssh -R 9997:localhost:9999 user@remote.com

建立 Tunnel 后,我们需要在 Ubuntu 这端配置好 lemonade client 所应连接的端口(即 9997)。

编辑如下配置 ~/.config/lemonade.toml

port = 9997
host = localhost
trans-loopback = true
trans-localfile = true
line-ending = 'crlf'

注意:

如果远程主机与本地主机在同一个网段中可以直接访问,那我们则不需要创建 SSH tunnel,但是由于 lemonade 缺乏 encryption 和 authorization 功能,所以这样建立的连接会有不少安全风险,因此依然建议建立 SSH tunnel (正向 tunnel) 来提供更多安全性保障。

步骤三:

远端主机(Ubuntu)作为 lemonade client 向本地主机发送数据

cat file.txt | lemonade copy

步骤四:

在 本地主机(MacOS)中访问 clipboard 内容

lemonade paste

总结:

上述方式有几个缺陷:

  • 如果有多个 local 机器连接到 remote machine 的同一个 port,只有第一个会收到 content。因此,我们需要 tunnel 到不同的 remote port。
  • 如果一个 local 机器被多个 remote machine 连接,那么也是只有第一个连接有效。
  • netcat 和 lemonade 方式都没有认证,因此很容易造成安全风险。虽然可以用 SSH 创建反向隧道连提升安全性,但总归不方便。

方法二:使用 X11 Forwarding (or Wayland)

上述的方案实施起来比较麻烦,易用性一般。有没有更好的办法?

答案是有的,使用 X11 Forwarding。

使用 X11 Forward 之前,我们先了解一下什么是 X 协议,它是 Linux 上 GUI 的通信协议,所有 GUI 的显示都是通过这个协议来完成。它分为 X server 和 X client 两部分,X Server 用来显示,而 X client 则负责发送显示指令。

如果我们安装的是 Linux Desktop,那么其已经安装了 X Window,如果我们安装的是 Headless Linux server(也就是没有 GUI 只有命令行的版本),那么 X Window 就没有安装。通常我们使用的 Linux 服务器就是 Headless linux server,没有 GUI 部分,但是有时我们又需要显示 GUI 怎么办?那我们可以在本地主机(比如 Windows 或 MacOS,它们自带桌面 GUI)上安装一个 X Server,然后在 Linux Server 中安装 X Client,这样,我们可以通过把两者建立连接从而把 Linux Server 中的 GUI 内容通过本地主机上的 X Server 显示出来。比如,我们在安装了 X Client 的只有命令行界面的 Ubuntu Server 中执行 firefox,可以在本地主机中打开 firebox 浏览器界面。

Linux 自身既没有图形界面也没有 system clipboard 这个概念的,因此你没办法在一个 Headless server 中使用 clipboard。只有 X Server (也就是负责 GUI 显示的部分)才实现了 clipboard,比如你在一个有 GUI 的桌面系统中使用鼠标选中文本然后 Ctrl + C 复制,这时候文本内容就保存在了你 GUI 系统的 clipboard 中了,再执行 Ctrl + V 就可以粘贴出来。

因此,现在我们要做的就是,在 Local machine 中安装一个 X Server,在 remote machine 中安装一个 X client (作用就是发送粘贴文本给 X Server 的 clipboard)。

我们有如下几种选择:

Local machine (X Server)

  • Windows: Xming, Vcxsrv, 如果使用 mobaxterm ,其自带了一个 X Server
  • MacOS: XQuartz
  • Linux Desktop: 自带 X Server

Remote machine (X Client)

  • Linux: xclip, xsel, wl-copy/wl-paste

一个 Linux Desktop 中,X Server 和 X Client 都是在同一个机器中的,但是我们这里 X Server 和 X Client 在两台不同的机器上。

X Server 和 X Client 需要建立通信,通常,我们是使用 SSH 从 X Server 所在的 local machine 登录到 X Client 所在的 remote machine。

这时候我们就需要用到 SSH 提供的 X11Forwarding 选项,在 Ubuntu 中的 /etc/ssh/sshd_config 设置为 yes 把它开启之后,就允许在 SSH 连接中转发基于 X 协议的通信,简单来说就是允许转发 X Client 与 X Server 之间通信的指令,使得它们可以在基于 SSH Tunnel 的连接中通信。比如 xclip 作为 X Client,需要把来自 stdin 的文本内容发送给 X Server 的 clipboard,反过来,X Server 也具有访问 X Client 的权限。

这里,我们以 MacOS 为 Local machine,Ubuntu/CentOS (headless) 为 Remote machine 作为例子

第一步:

在 Ubuntu上安装 xclip

# Ubuntu
apt-get install -y xsel xclip
# CentOS
yum -y install xclip

第二步:

SSH server 端(即 Ubuntu)上开启 X11Forwarding

# on Ubuntu
/etc/ssh/sshd_config
X11Forwarding yes

开启后,重启 sshd 服务

sudo systemctl restart sshd

第三步:

SSH Client 端(即 MacOS)上,也需要开启 ForwardX11,用于主动建立一条 X11 Forward Session。

在 ssh client 的配置文件 ~/.ssh/config 中如下配置:

# on macOS
~/.ssh/config
Host *
ForwardAgent yes
ForwardX11 yes
ForwardX11Trusted yes
XAuthLocation /opt/X11/bin/xauth

解释:

  • MacOS 端作为 SSH 连接发起方,需要启用 ForwardAgent,并且启用 ForwardX11 或 ForwardX11Trusted 来使用一个可信的 X11 forward session。
  • xauth 程序是用于 X11 通信的认证程序,安装 XQuartz 时会自动安装(默认安装路径是 /opt/X11/bin/xauth
  • 可以直接在命令行使用 -X 或 -Y 选项,其中 -X 表示 ForwardX11,-Y 表示 ForwardX11Trusted,如下:
ssh user@ubuntu -X
ssh user@ubuntu -Y

-X-Y 的区别在于:

-X 方式是 Untrusted 模式,Xauth cookie 有时间限制。如果 ssh 时间过长,会出现 Error: Can't open display: localhost:10.0) 的情况。而 -Y 则是 trusted 模式,Xauth cookie 没有时间限制。我们可以根据自身需要来进行选择,通常,由于我们是本地环境,使用 -Y 更加方便,不会因为 cookie 过期而导致频繁掉线。

第四步:

在 macOS 中安装 XQuartz,前面已经介绍了,它是一个 X11 Server。安装完成时它会要求我们重新登录 macOS 才能生效。

在 Windows 中,我们可以安装 xming 或 vcxsrv,或者使用 mobaxterm,它自带一个 X Server。

安装完成后,我们启用 XQuartz,然后到 Terminal 中使用 SSH 命令登录到 Ubuntu Server。

ssh user@ubuntu -Y

登录后,我们检查一下 X Server 和 X Client 是否已经成功建立连接。

在 macOS 和 linux 中分别查看环境变量 $DISPLAY 的值:

echo $DISPLAY

如果连接成功,会得到如下结果:

# on macOS
echo $DISPLAY
/private/tmp/com.apple.launchd.aOF9gd84Om/org.xquartz:0
# on Ubuntu
echo $DISPLAY
localhost:10.0

解释:

DISPLAY 是 X Server 提供给 X Client 访问的地址,X Client 通过往这个地址发送指令来控制 X Server 的 GUI app 的显示。如果 $DISPLAY 没有值,说明连接没成功,这时应该检查 /etc/ssh/sshd_config 中是否已开启 X11Forwarding,并重启 sshd 服务。

第五步:

在 Ubuntu 命令行中使用 xclip。

现在一切就绪,我们可以用 xclip 把内容复制到 X Server 的 clipboard 中了

# copy
ls | xclip -i
ls | xclip -sel c

执行上述命令之后,内容就已经通过 X11 Forward 到了 X Server 的 clipboard 中;也就是说,它已经在 MacOS 的 system clipboard 中了,这时,我们就可以在 MacOS 中像正常访问一样访问 system clipboard 了,比如 CMD + V 把内容粘贴出来,或者在命令行中使用 pbpaste 把内容粘贴出来。

同样,你在 MacOS 中选中文本 CMD + C 或 pbcopy,回到 Ubuntu 中,你可以直接使用 xclip -o 把内容 paste 出来。

# paste content from selections (by default it is primary)
xclip -o
xclip -o -sel second

至此,我们就已经完成了两个主机的 clipboard 共享了。

三、在 Neovim 中访问 System Clipboard

Neovim 重新实现了 Vim 的 clipboard 功能,具体来说就是,它通过调用第三方工具(也叫 provider)来实现访问 system clipboard。

打开 Neovim,执行 :checkhealth

  • 当没有检测到可用的第三方 clipboard 工具时,显示如下:

显然,此时 clipboard 功能不可用。

  • 当检测到可用的第三方 clipboard 工具,显示如下:

此时,clipboard 功能可用。

所以,我们想要使用 Neovim 的 clipboard 功能,一定要先确保类似 xclip/xsel, wl-copy/wl-paste 或 pbcopy/pbpaste 这样的工具已经存在。

另外,如果你系统中已经安装了上述工具,但 :checkhealth 依然提示 "No clipboard tool found" 错误的话,那是因为你没有成功连接 X Server,也就是你的 $DISPLAY 没有显示正确或没有值。要解决这个问题请确保已经在 SSH 中开启 X11Forwarding 并连接成功。

那 Neovim 中的 clipboard 怎么使用,原理又是如何?

首先,我们需要设置:

:set clipboard=unnamed
# or
:set clipboard=unnamedplus

这条语句的作用是把 clipboard 关联到一个 Neovim 中的寄存器,unamed 寄存器是 *,而 unnamedplus 寄存器是 +

也就是说当我们访问 *+ 这两个寄存器的时候,访问的就是 system clipboard。*+ 没什么区别,都是 system clipboard,唯一区别就是在 Linux 中 * 表示 selection clipboard,而在其他 OS 中,unamedplus 都表示 system clipboard。如果你设置了 unamedplus,那么你所有的操作都会自动被粘贴进 system clipboard 中。而设置的是 unamed 的话,那么想要访问 clipboard,必须手动执行 "+y"+p 等操作。

在 Vim 中访问寄存器的方式我们很熟悉。比如,往寄存器 + 中存入数据,我们可以这样

# in normal mode
"+diw
"+yy
"+yG
# in visual mode
"+y

把寄存器 + 中的内容粘贴出来:

# in normal mode
"+p
# in insert mode
@=<c-r>+

推荐:

不推荐设置 clipboard=unnamedplus,因为很容易与 system clipboard 中的内容误覆盖,还是建议使用 unnamed,然后通过手动的方式 yank/paste system clipboard。这样的话,你相当于有了两个 clipboards,一个是 vim only clipboard 的,一个是 system clipboard。

为了简化手动操作,只需要绑定一个快捷键即可:

vnoremap <c-c> "+y

上面介绍到 Neovim clipboard 就是关联到寄存器 +,寄存器 + 又通过第三方工具访问到 system clipboard。

至此 ,我们就打通了从 Neovim 到 OS clipboard 的互相访问。

Neovim clipboard 的实现原理

我们通过查看 :h clipboardruntime/autoload/clipboard.vim 源码可以得到如下信息:

The presence of a working clipboard tool implicitly enables the ‘+’ and ‘*’ registers. Nvim looks for these clipboard tools, in order of priority:

  • |g:clipboard|
  • pbcopy, pbpaste (macOS)
  • wl-copy, wl-paste (if $WAYLAND_DISPLAY is set)
  • In Arch I found that the package for Wayland is called wl-clipboard, which includes both wl-copy and wl-paste. Thanks for the tip!
  • xclip (if $DISPLAY is set)
  • xsel (if $DISPLAY is set)
  • lemonade (for SSH) https://github.com/pocke/lemonade
  • doitclient (for SSH) http://www.chiark.greenend.org.uk/~sgtatham/doit/
  • win32yank (Windows)
  • termux (via termux-clipboard-set, termux-clipboard-set)
  • tmux (if $TMUX is set)

解释:

  • 首先判断是否定义了 g:clipboard 变量,如果定义了,那么就调用该变量所定义的程序。
  • 然后判断 OS 是否是 mac,那么就执行 pbcopy 或 pbpaste
  • 继续判断是否已定义环境变量 $WAYLAND_DISPLAY,如果定义了,就使用 wl-copy/wl-past
  • 继续判断是否已定义环境变量 $DISPLAY,如果定义了,就是用 xclip/xsel
  • 。。。

总结:

简单来说,在 Ubuntu 中的 Neovim 执行 "+<action> 时,执行的就是 xclip,在 MacOS 中的 Neovim 中执行 "+<action> 时,执行的就是 pbcopy。

前面已经介绍过,我们通过 X11Forwarding 把 X Server 和 X Client 建立了连接,因此,在 Neovim 中就可以直接访问 X Server 的 clipboard 了,我们也就打通了从 Neovim 到 remote machine 再到 local machine 之间的 clipboard 访问了。

最后,附上一个示意图:

参考:

https://stackoverflow.com/questions/30691466/what-is-difference-between-vims-clipboard-unnamed-and-unnamedplus-settings

https://superuser.com/questions/407218/why-does-ssh-x-forwarding-timeout-after-a-while

https://github.com/lemonade-command/lemonade

https://developpaper.com/how-to-output-contents-to-the-clipboard-in-windows-mac-linux/

https://vim.fandom.com/wiki/Accessing\_the\_system\_clipboard

如果你觉得我的文章对你有帮助,欢迎留言或者关注我的专栏。

微信公众号:“知辉”

搜索“deliverit”或

扫描二维码

--

--

No responses yet