Emacs supports editing remote files using the Tramp package. Emacs transparently handles the remote files based on the special path prefixes specified, so the experience is similar to operating on a local file. Tramp uses a remote shell underneath to access remote files. In this article, I'll talk about how to extend Tramp to support custom Methods to access remote files.

Background

I work on a Cloud platform that manage and operate Robots remotely. To test the platform, we have an internal service that implements an API to create virtual Robots on-demand. These virtual robots are accessible over SSH Port on internal VPN.

It is often required to edit configuration files on the virtual Robots or check debug logs. This is a multi-step process and requires more than one tool:

  1. Query IP of the Robot from the API.
    Requires: curl or Postman.
  2. SSH on the Robot.
    Requires: Terminal and ssh.
  3. Operate on the Files.
    Requires: text-editor on the Robot, usually vim.

Like most Emacs users, I want to achieve this without leaving Emacs. I started by writting a simple shell script that wraps the REST API and exposes a simple command-line interface to work with. This allows me to use it from Terminal directly as well. I'll be using this command-line interface later on to integrate it with the Tramp package.

hwil list
hwil create <DEVICE_NAME>
hwil delete <DEVICE_NAME>
hwil get    <DEVICE_NAME>
hwil ssh    <DEVICE_NAME>

Tramp over SSH

Let's look at a common use-case of Tramp, edit files on a SSH Host. To open the remote file, it must be prefixed with the special value /ssh:user@host:/path. Tramp determines appropriate method to use from the filepath. In this case, it is the built-in SSH Method.

Tramp uses the SSH Method to login to the remote host, prompting for password if required. It then uses the remote shell to operate on the files on the host. Tramp can work with popular shells (sh, bash, zsh, etc) out of the box. In case of a non-UNIX shell (eg fish), Tramp invokes a UNIX-compatible sub-shell and uses that instead.

Tramp defines the tramp-methods list that is used to look-up Method definitions. The SSH Method is defined and added to the list in the tramp-sh.el file. The definition is a list of cons defining parameters that Tramp uses to start the remote connection. The full list of parameters are defined in the documentation of the tramp-methods variable.

(add-to-list 'tramp-methods
	`("ssh"
	  (tramp-login-program      "ssh")
	  (tramp-login-args         (("-l" "%u") ("-p" "%p") ("%c") ("-e" "none") ("%h")))
	  (tramp-async-args         (("-q")))
	  (tramp-direct-async       t)
	  (tramp-remote-shell       ,tramp-default-remote-shell)
	  (tramp-remote-shell-login ("-l"))
	  (tramp-remote-shell-args  ("-c"))))

Custom Tramp Method

We know now that SSH Method is just an entry in the tramp-methods alist. To implement the custom method, it must be added to this list as well. We will use the hwil command-line interface in the method.

(add-to-list 'tramp-methods
	`("hwil"
	  (tramp-login-program     "hwil")
	  (tramp-login-args        (("ssh") ("%h")))
	  (tramp-remote-shell      "/bin/bash")
	  (tramp-remote-shell-args ("-i" "-c"))))

After adding the hwil method to the list, Tramp can operate on /hwil:host-name:/path files.

Bonus: Host Completion

The Tramp package also supports completions for the special paths. See the SSH Method's completion list for instance.

Tramp Completion

To handle the completions, Tramp defines the tramp-set-completion-function function. It binds the Method's name with a list of completion functions. The completion function takes a single parameter and returns a list of completions. I will use the hwil command-line interface again to implement the completion function.

;; The list command returns multiple columns. This fetches the Name
;; column from the output lines.
(defun hwil--parse-devices (line)
  (list nil
		(caddr (split-string line "[[:space:]]+" t))))

(defun hwil--list-devices ()
  (mapcar #'hwil--parse-devices (process-lines "hwil" "list")))

(defun hwil--tramp-completion (&optional ignored)
  (hwil--list-devices))

The hwil--tramp-completion is the final completion function. It invokes the hwil list command and parses the Robot's name from the output. We ignore the parameter because we don't need it. Next, this function must be set for the custom Tramp Method defined earlier.

(tramp-set-completion-function "hwil" '((hwil--tramp-completion "")))

Finally, I have the custom Tramp method to interact with the internal API and operate on the files of virtual Robots.