L'uso di questo sito
autorizza anche l'uso dei cookie
necessari al suo funzionamento.
(Altre informazioni)

Friday, December 6, 2013

Local editing of remote file, from the CLI, with emacs.

What I do all day long involves hopping, mostly with ssh, on various machines and editing files over there.

Over the years I have settled on emacs as my sole editor. I have even tried to plug it in various IDEs (such as Eclipse: without much luck, so I backed off). So the initial approach requires having it installed everywhere I log in. Cool, but it deprives me of the graphical environment because...

  1. X over the network is pitiably slow
  2. FreeNX is better, but not enough, and sometimes screws up on fonts, keyboard type, etc. Also you end up with a mess of emacsen frames, and copypasting is still not ideal.
As an old time emacs hand I would not be (much) bothered by having to use the tty version, if it were not for the clunkyness of cutting and pasting between windows on different servers (which I often do). For a while I relied on mounting the remote machine's file systems locally with sshfs, under a hierarchy such as
as
/remote/server1/... 
/remote/server2/... 
I could then use my local emacs to edit remote files as if they were local. Not bad - and I still use this technique now and then, for different purposes - except:

  • It's overkill
  • You now have to remember to unmount
  • You now have this wonderful opportunity of wiping out an entire server (or part thereof) by mistyping a command on your local machine (I know. It happened - shudder)
After that, I began using an emacs extension called TRAMP. It gives emacs the ability to edit remote files whose path is of the form:

//server:/my/nifty/file.txt

TRAMP does its thing by using a variety of transports, mostly ssh based. It also gives you access to countless emacs goodies, like diff'ing (via ediff) two buffers sitting on different machines, editing remote directories, remote VC (careful of C-c C-Q, though) and more. But I digress. So this TRAMPish solution is much better except for the (not huge, really, but still) snag of having to remember to:
  1. Look up the path of the file (in the terminal)
  2. Restrain the habit of editing in the terminal window itself
  3.  Move to my emacs window (maybe on a different desktop, hidden under a pile of junk; also, makes me pick up the mouse even if not strictly needed)
  4. Type again (or maybe paste) the file path, in tramp digestible format.
Because of this, and to my chagrin, I sometime ended up in - unwillingly - editing within the terminal. Would it not be supercool if I could type on the server:

server1:/# hed /my/nifty/file.txt

and have the file pop up in my local emacs window? Which brings us to my latest, greatest solution to this monumental problem of computer science.

Update: Andrew McGlashan alerted me to vim's capability of opening remote files from the CLI much like emacs does. Which means that suitably combining that and gvim's client-server capabilities, you can pull the same trick if you are a vi head. Which is good, because, as soon as I posted this, a number of dudes felt obliged to point out that vi is the editor. Which may well be the case, for them and for all I (want to) know. I will leave the development of the idea to vi users more gifted than I (not a hard feat to achieve). 

(Caveat: I tried what follows with emacs 24.2.1, tramp 2.2.24-1 (which ships with emacs) on a Fedora Core 18 Linux. I suppose it may work with other combinations of versions, operating systems... even with windows, putty and some sort of ssh server on top of that. But I did not even venture to try.)


  1. Be sure to have the following in your local .emacs:

      (server-start)

    This make emacs listen to a local socket so you later on, can type 


    localhost:/# emacsclient file.txt

    and have the (local) file pop up inside emacs. Man emacsclient explains this.
  2. Have shared ssh keys between your desktop and your servers, and an operating ssh-agent so you don't have to type your password every time (if you don't know what I am talking about, man ssh, man ssh-agent, man ssh-add are your friends)
  3. Have a .ssh/config file containing stanzas like the following:
    
    Host server1
     HostName server1.foo.bar
     RemoteForward 8222  127.0.0.1:22 
     User luser
    
    
    So you can type
    
    
    localhost:/ #ssh server1 
    
    
    and be connected to server1 as luser, passwordless, and tunneling the port 8222 (arbitrary number) on the local interface of server1 back to your sshd port on your local machine (this is quite a mouthful).
  4. Now install the following thingy on all your servers, say in /usr/local/bin, and call it hed (don't forget to chmod 755 /usr/local/bin/hed):
    
    #!/bin/bash
    
    PORT=8222
    HOST=localhost
    LHOST=${HOSTNAME:-$(hostname -f) }
    RU=${HUSERNAME:-$USER}
    
    ver=0.1
    author="Alessandro Forghieri "
    usage () {
     name=`basename $0`
     echo "$name $ver $author"
     echo
     echo "Usage: $name [-p port] [-h host ] [-l lname] [-u user]  [-v] file1 file2 ..."
     echo
     echo "        -p   port number (defaults to $PORT)"
     echo "        -h   host remote host (defaults to $HOST)"
     echo "        -l   lname name of this host "
     echo "             (defaults to content of HOSTNAME or output or hostname -f command, currrently: $LHOST)"
     echo "        -u   user remote user "
     echo "             (defaults to the contents of the HUSERNAME or USER environment variable, "
     echo "             currently: $RU)"
     echo "        file1 file2 localfiles to edit with remote editor"
     echo
     echo "See also: man boilerplate"
     echo
    }
    
    #http://stackoverflow.com/questions/3915040/bash-fish-command-to-print-absolute-path-to-a-file
    abspath() {
        curdir=$(pwd)
        if [[ -d "$1" ]]; then
     retval=$( cd "$1" ; pwd )
        else 
     retval=$( cd $( dirname "$1" ); pwd )/$(basename "$1")
        fi
        cd $curdir
        echo $retval
    }
    
    while getopts p:h:u:l:v opt ; do
        case "$opt" in
     p) PORT=$OPTARG    ;;
     h) HOST="$OPTARG"  ;;
     i) usage ;  exit   ;;
     v) set -x          ;;
     ?) usage ;  exit   ;;
        esac
    done
    
    shift `expr $OPTIND - 1`
    
    while [[ x$1 != x ]]; do
        target=$(abspath $1)
        ssh -f -p $PORT ${RU}@${HOST} "emacsclient -n \"/${LHOST}:/${target}\""
        shift
        # or some funky race condition screws everything up 
        sleep 2
    done
  5. Explanation: the script above formats a file path in a form digestible to tramp - that is/hostname:/blah/blah (I need two leading slashes within emacs, only one from a shell, I wonder why). It then sends the command emacsclient -n , via ssh, to my local desktop, using the tunnel I just created - my machine, like 99.9% of modern enduser machines is behind a firewall with NAT o top - that explains the need of the firewall. Now emacs opens the file(s) via the regular forward ssh channel
And there you have it: local editing of remote files, from the CLI, with emacs.

No comments: