Continuing my love of semi-nonsensical blog entry titles only tangentially related to the topic in any way…

I have some boxes at work that I need to log into frequently. Now that I'm trying to use Eshell instead of xterm, I need to set up Tramp properly. Tramp is an Emacs mode that abstracts the connection to another machine in such a way that editing files, running remote commands, and navigating remote directories with Dired all work.

The first thing I needed to do to make Tramp work was to add the following line to the top of my ~/.zshrc. This line checks for terminals that are 'dumb', meaning they lack advanced features or are likely not normal terminal emulators, and if so: disables Zsh's line-editing facilities, sets the prompt to a traditional style, and stops processing the remainder of the configuration file.

#######################################################
# Emacs Tramp
#######################################################
[[ $TERM == "dumb" ]] && unsetopt zle && PS1='$ ' && return

Once the above line is in place, Eshell immediately works:

👻 cd /ssh:stormy:
👻 pwd
/ssh:stormy:/home/fasciism
👻 cat ../../etc/motd
 _____ _
/  ___| |
\ `--.| |_ ___  _ __ _ __ ___  _   _
 `--. | __/ _ \| '__| '_ ` _ \| | | |
/\__/ | || (_) | |  | | | | | | |_| |
\____/ \__\___/|_|  |_| |_| |_|\__, |
                                __/ |
Stormageddon: Dark Lord of All |___/
https://www.youtube.com/watch?v=bWK61bkQ-ME

Eshell and Tramp ignore all the login banners and message-of-the-day (MOTD) information from the remote system. From this point on, you can execute commands on the remote system since you are effectively SSH'd into it:

👻 hostname
stormy
👻 cat /etc/hostname
aboyne

It's important to know that absolute paths still refer to the system on which Emacs resides, while relative paths refer to the current working directory of Eshell (i.e., the remote host). If I attempt to run find-file on the remote machine to edit the MOTD, the buffer will be marked as read-only

👻 find-file ../../etc/motd
#<buffer motd>
👻 (with-current-buffer "motd" buffer-read-only)
t

Thankfully, Tramp also supports elevating our privilege via sudo on local and remote hosts. The downside is that this operation requires a horrific syntax. Using sudo ffap isn't an option either, since sudo opens a new shell on the remote machine through which Emacs has no special relationship.

👻 sudo find-file ../../etc/motd
[sudo] password for fasciism: [redacted]
sudo: find-file: command not found

However, using Tramp's sudo syntax and multi-hop capabilities, we can use C-x C-f /ssh:stormy|sudo:stormy:/etc/motd, type in the password, and start editing the MOTD in Emacs, resulting in:

👻 cat ../../etc/motd
 _____ _
/  ___| |
\ `--.| |_ ___  _ __ _ __ ___  _   _
 `--. | __/ _ \| '__| '_ ` _ \| | | |
/\__/ | || (_) | |  | | | | | | |_| |
\____/ \__\___/|_|  |_| |_| |_|\__, |
                                __/ |
SUDO VIA TRAMP APPEARS TO WORK |___/
https://www.youtube.com/watch?v=bWK61bkQ-ME

Since this syntax is so ungainly, I've decided to make two emacs functions f (an alias for find-file) and f! which tries to edit the file using sudo (locally or remote).

(defun mak::get-buffer-path (&optional name)
  "Finds the current path, including for Eshell buffers where it is the working directory."
  (interactive "b")
  (with-current-buffer name
    (if (eq major-mode 'eshell-mode)
        (substring-no-properties default-directory)
      (buffer-file-name))))

(defun mak::get-buffer-tramp-context (&optional name)
  "Finds a buffer's Tramp context based on its file name."
  (interactive "b")
  (let ((path (mak::get-buffer-path name)))
    ;; Match single and chained contexts.
    (if (string-match "^\\(/\\(ssh\\|sudo\\):[^:|]+\\(|\\(ssh\\|sudo\\):[^:|]+\\)*:\\)" path)
        (match-string 1 path)
      (user-error "Failed to find Tramp context in path %s." path))))

(defun mak::get-last-hop-from-tramp-context (ctx)
  "Finds the last host or user@host hop in a Tramp context."
  (if (string-match "[/:]\\(?:ssh\\|sudo\\):\\([^:]+\\):$" ctx)
      (match-string 1 ctx)
      (user-error "Failed to find last hop in context %s." ctx)))

(defun mak::tramp-remote-find-file-with-sudo (file)
  "Attempts to open a file using Tramp and Sudo."
  ;; We need to currently be within a Tramp 'context'.
  (let* ((ctx (mak::get-buffer-tramp-context (current-buffer)))
         (hop (mak::get-last-hop-from-tramp-context ctx)))
    (find-file (format "%s|sudo:%s:%s"
                       (substring ctx 0 -1)
                       hop
                       file))))

(defun eshell/f (file)
  "An alias for find-file."
  (find-file file))

(defun eshell/f! (file)
  "An alias for find-file-with-sudo."
  (if (equal "/" (substring file 0 1))
      (find-file (concat "/sudo::" file))
    (mak::tramp-remote-find-file-with-sudo file)))

And with this we now have an easy way to edit either local (absolute) or remote (relative) files with much less typing.

👻 f! /etc/motd
#<buffer motd::>
👻 f! ../../etc/motd
#<buffer motd::/ssh:stormy:>

While the determination of whether to access the local or remote file isn't ideal, it's not bad for a couple hours of work. I can always touch up the logic in a future entry.