Apple - How do I copy to the OSX clipboard from a remote shell using iTerm2?

Solution

Piecing together lots of information from several different sources, here's what I came up with.

Local Daemon

From the local computer (OSX), setup a daemon to listen on a specific port, via launchd (see links below). The daemon process will simply call pbcopy, which will take anything passed in via STDIN and put it on the clipboard. To do this you need to setup a launchd plist file. Mine looked like this:

<?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>joe</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>

By convention, the plist file name should be the label name with .plist appended, so for the above example, it would be local.pbcopy.9999.plist. If you wish to use a port other than 9999, just change it everywhere (keeping in mind that it should be something above 1024 and should not be a well-known port that you might already be using). Once you have things working, you can remove the StandardOutPath and StandardErrorPath keys and strings, as they're only needed for debugging.

In order to load the daemon, run the following command:

$ launchctl load local.pbcopy.9999.plist

You can see that it's loaded or remove it with the following commands:

$ launchctl list local.pbcopy.9999
$ launchctl remove local.pbcopy.9999

If you would like this to load every time you login, place the plist file into the ~/Library/LaunchAgents directory.

Note: you will need to set this up on each local host you want this to work on.

SSH Port-Forwarding

Because I could be accessing the remote machine from several different local computers, I can't hard-code the sending of the data on the remote machine to a specific host. To make this as painless and dynamic as possible, I've used SSH port-forwarding to create a dynamic link from the remote machine back to the local computer (the how's and why's of this are beyond this answer; see below for more information). Specifically, I create a link from the remote machines's port 9997 to the local computer's port 9999, which now has a daemon listening on it, thanks to the launchd stuff above. I could use port 9999 on both the remote machine and the local computer, but I don't need to.

To setup this tunnel, execute the following command:

$ ssh -R 9997:localhost:9999 [email protected]

You can remote into multiple different remote machines with the same command and all will function as expected. You can remote into the same remote machine multiple times with the same command, and it will sort of work as expected; see note below.

If you don't feel like typing the -R 9997:localhost:9999 on every SSH invocation you make, you can put the remote forwarding definition in the SSH config file to do it automatically. Here's an example from my ~/.ssh/config file:

Host ufo*
  RemoteForward 9997 localhost:9999

With that, any time that I SSH to a host whose name starts with 'ufo', the remote forwarding from 9997 to localhost:9999 will automatically be set up. See the config file man page link below for more options.

Sending Data

On the remote end I use netcat to send the desired content back to the listening daemon.

$ date | nc localhost 9997

You can get as complicated as you'd like:

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

You can even dynamically decide whether or not to send any data, based on whether or not anyone is listening (there's probably a more efficient way to do this, but it works):

#!/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

Observations & Caveats

Things I noted:

  • pbcopy plays nicely with Copy'em Paste
  • if you initiate more than one SSH session with the same remote port forwarding to the same remote machine, only the first one will have any effect; no "duplicate port" type errors will be reported on either end
  • similarly, if you remote in from several different local computers to the same remote machine using the same remote port (e.g. 9997), only the first one will have any effect

Reference Links

  • launchd Tutorial
  • launchd Examples
  • SSH Port Forwarding
  • SSH Config files

remote pbcopy for iTerm2 seems to be a piece of software made especially for this problem.