hamacs/ha-eshell.org
Howard Abrams ecc9c1ee8f Now $$ is an array of command output history
Extended the `eshell-variable-aliases-list` to call a new function that
stores the output of the commands in an ring. This is a pretty cool feature.
2022-09-26 17:21:42 -07:00

38 KiB
Raw Blame History

Eshell

A literate programming file for configuring the Emacs Shell.

Introduction

While I like vterm, especially for logging into remote systems, I find Emacs shell, eshell, an interesting alternative. If you find the documentation lacking, I documented most features, and you might find the following helpful.

Navigation and Keys

Along with the regular Emacs keybindings, Eshell comes with some interesting features:

  • M-RET can be used to accumulate further commands while a command is currently running. Since all input is passed to the subprocess being executed, there is no automatic input queueing as there is with other shells.
  • C-c C-t can be used to truncate the buffer if it grows too large.
  • C-c C-r will move point to the beginning of the output of the last command. With a prefix argument, it will narrow to view only that output.
  • C-c C-o will delete the output from the last command.
  • C-c C-f will move forward a complete shell argument.
  • C-c C-b will move backward a complete shell argument.

Control-D Double Duty

Used to C-d exiting from a shell? Want it to keep working, but still allow deleting a character? We can have it both (thanks to wasamasa):

  (defun ha-eshell-quit-or-delete-char (arg)
    "The `C-d' sequence closes window or deletes a character."
    (interactive "p")
    (if (and (eolp) (looking-back eshell-prompt-regexp))
        (progn
          (eshell-life-is-too-much) ; Why not? (eshell/exit)
          (ignore-errors
            (delete-window)))
      (delete-forward-char arg)))

Pager Setup

If any program wants to pause the output through the $PAGER variable, well, we don't really need that:

  (setenv "PAGER" "cat")

Aliases

Gotta have some shell aliases, right? We have three ways of doing that. First, enter them into an eshell session:

  alias less 'view-file $1'

Note that you need single quotes, and multiple arguments dont really work with aliases.

Second, you can create/populate the alias file, ~/.emacs.d/eshell/alias … as long as you dont use those single quotes:

  alias ll ls -l $*
  alias clear recenter 0
  alias d dired $1
  alias e find-file $1
  alias less view-file $1
  alias more view-file $1
  alias find echo 'Please use fd instead.'

Which happens when you type those commands into an eshell.

Third, you want control, write a function to define the aliases:

  (defun ha-eshell-add-aliases ()
    "Call `eshell/alias' to define my aliases."
    (eshell/alias "e" "find-file $1")
    (eshell/alias "d" "dired $1")
    (eshell/alias "gd" "magit-diff-unstaged")
    (eshell/alias "gds" "magit-diff-staged")

    ;; The 'ls' executable requires the Gnu version on the Mac
    (let ((ls (if (file-exists-p "/usr/local/bin/gls")
                  "/usr/local/bin/gls"
                "/bin/ls")))
      (eshell/alias "ll" (concat ls " -AlohG --color=always"))))

Predicate Filters and Modifiers

The T predicate filter allows me to limit file results that have have internal org-mode tags. For instance, files that have a #+TAGS: header with a mac label will be given to the grep function:

  $ grep brew *.org(T'mac')

As described in this essay, to extend Eshell, we need a two-part function:

  1. Parse the Eshell buffer to look for the parameter (and move the point past the parameter).
  2. A predicate function that takes a file as a parameter.

For the first step, we have our function called as it helps parse the text at this time. Based on what it sees, it returns the predicate function used to filter the files:

  (defun eshell-org-file-tags ()
    "Helps the eshell parse the text the point is currently on,
  looking for parameters surrounded in single quotes. Returns a
  function that takes a FILE and returns nil if the file given to
  it doesn't contain the org-mode #+TAGS: entry specified."

    ;; Step 1. Parse the eshell buffer for our tag between quotes
    ;;         Make sure to move point to the end of the match:
    (if (looking-at (rx "'" (group (one-or-more (not (or ")" "'"))))"'"))
        (let* ((tag (match-string 1))
               (reg (rx line-start
                        "#+" (optional "file") "tags:"
                        (one-or-more space)
                        (zero-or-more any)
                        (literal tag) word-end)))
          (goto-char (match-end 0))

          ;; Step 2. Return the predicate function:
          ;;         Careful when accessing the `reg' variable.
          `(lambda (file)
             (with-temp-buffer
               (insert-file-contents file)
               (re-search-forward ,reg nil t 1))))
      (error "The `T' predicate takes an org-mode tag value in single quotes.")))

Then we need add that function to the eshell-predicate-alist as the T tag:

  (defun ha-eshell-add-predicates ()
    "A hook to add a `eshell-org-file-tags' predicate filter to eshell."
    (add-to-list 'eshell-predicate-alist '(?T . (eshell-org-file-tags))))

Note: We cant add it to the list until after we start our first eshell session, so we just add it to the eshell-pred-load-hook which is sufficient.

Eshell Functions

Any function that begins with eshell/ can be called with the remaining letters. I used to have a function eshell/f as a replacement for find, but the fd project is better. I used to have a number g-prefixed aliases to call git-related commands, but now, I just need to call Magit instead.

However, since eshell is an Emacs shell, I want to think of how to use Emacs buffers in a shell-focused workflow. For instance, use view-file instead of less, as it will show a file with syntax coloring, and typing q returns to your shell session.

Map

While I like eshells for loop well enough (if I can remember the syntax), as in:

  for file in *.org {
    chmod a+x $file
  }

I like the idea of using a map structure, as in:

  map chmod a+x *.org

How would this work without special syntax? Well, eshell sends the *.org as a list of files, which we could use as the delimiter. The downside is that we want to list the files, we need to actually list the files, as in:

  map chmod a+x (list "a.org" "c.org")

Pretty ugly, but that isnt my use-case. I could introduce syntax like:

  map chmod a+x :: *.org b.txt

But what if the file isnt the last element? Well, I could replace a keyword, _, with the filename when encountered.

Here is my initial function. After separating the arguments into two groups (split on the :: string), we iterate over the file elements, creating a form that includes the filename.

  (defun eshell/map (&rest args)
    "Execute a command sequence over a collection of file elements.
  The sequence and the elements are separated with a `::' string.
  For instance:

      map chmod a+x _ :: *.org b.txt

  The `_' sequence is substituted with a single filename element,
  and if not specified, the filename is appended to the command.
  "
    (seq-let (forms elements) (--split-when (equal it "::") args)
      (dolist (element (-flatten (-concat elements)))
        (let* ((form (if (-contains? forms "_")
                         (-replace "_" element forms)
                       (-snoc forms element)))
               (cmd  (car form))
               (args (cdr form)))
          (eshell-named-command cmd args)))))

The eshell-named-command takes the command separately from the arguments, so we use car and cdr on the form.

Git

My gst command is just an alias to magit-status, but using the alias doesn't pull in the current working directory, so I make it a function, instead:

  (defun eshell/gst (&rest args)
      (magit-status (pop args) nil)
      (eshell/echo))   ;; The echo command suppresses output

Editing Files

Which an alias to find-file (which takes one argument), we could define a special function that can take multiple arguments, and open them in different windows. We first define a helper function for dealing with multiple arguments. It takes two functions, the first function is called on the first argument, and the second function is called on each of the rest.

  (defun eshell-fn-on-files (fun1 fun2 args)
    (unless (null args)
      (let ((filenames (thread-last args
                                     (reverse)
                                     (-flatten)
                                     (-map 'file-expand-wildcards)
                                     (-flatten))))
        (apply fun1 (car filenames))
        (when (cdr filenames)
          (-map fun2 (cdr filenames))))))

This allows us to replace some of our aliases with functions:

  (defun eshell/e (&rest args)
    "Edit one or more files in current window."
    (eshell-fn-on-files 'find-file 'find-file-other-window args))

  (defun eshell/ee (&rest args)
    "Edit one or more files in another window."
    (eshell-fn-on-files 'find-file-other-window 'find-file-other-window args))

Well leave the e alias to replace the eshell buffer window.

Last Results

The shell-underscore project looks pretty cool, where the _ character represents a filename with the contents of the previous command (you know, like if you were planning on it, youd tee at the end of every command). An interesting idea that I could duplicate.

While diving into the eshell source code, I noticed that the special variables, $$ and $_ can be used sometimes as the output of the last command. For instance:

$ echo "hello world"
hello world
$ echo $$
hello world

However, what I would like is something like this to work:

$ ls *.org(U)
a.org b.org f.org
$ rg "foobar" $$

The problem may be between calling Emacs functions versus external commands, as the echo works, but the call to ls doesnt:

$ ls *.org(U) b.txt
a.org b.org f.org b.txt

$ echo Nam $$
("Nam" nil)

However, I could easily over-write that special variables to behave as I would expect:

  • A hook runs after every command
  • It copies the previous commands output to a ring (so that I can get the last as well as the fifth one)
  • Create a replacement function for $$ to read from my history ring

Lets first make a ring that stores the output:

  (defvar ha-eshell-output (make-ring 10)
    "A ring (looped list) storing history of eshell command output.")

The following function does the work of saving the output of the last command. We can get this because after every command, eshell updates two variables, eshell-last-input-end (the start of the output), and eshell-last-output-start (the end of the output):

  (defun ha-eshell-store-last-output ()
    "Store the output from the last eshell command.
  Called after every command by connecting to the `eshell-post-command-hook'."
    (let ((output
           (buffer-substring-no-properties eshell-last-input-end eshell-last-output-start)))
      (ring-insert ha-eshell-output output)))

Now we save this output after every command by adding it to the eshell-post-command-hook:

  (add-hook 'eshell-post-command-hook 'ha-eshell-store-last-output)

Next, this function returns values from the history ring. I feel the need to have different ways of returning the output data. Unlike the behavior of the original shell (and most of its descendents, like bash), eshell doesnt automatically split on whitespace. For instance, echo called this way:

$ echo a b *.txt
("a" "b"
 ("b.txt" "date today.txt"))

Is given a list of three elements: a, b, and a list of all files in the current directory with an .org extension. An interesting side-effect is that spaces in filenames are often okay. So if I specify and argument of text, it should return the commands output as a string, but if I give it, list, it should contain the same information, but separated by spaces, into a list. For instance, if we are passing the output from ls to grep, we would use this format.

Like the shell-underscore project mentioned earlier, I can access the output stored from a file when given a file argument (the output will hold this temporary filename).

  (defun eshell/output (&rest args)
    "Return an eshell command output from its history.

  The first argument is the index into the historical past, where
  `0' is the most recent, `1' is the next oldest, etc.

  The second argument represents how the output should be returned:
   ,* `text' :: as a string
   ,* `list' :: as a list of elements separated by whitespace
   ,* `file' :: as a filename that contains the output

  If the first argument is not a number, then the format is assumed
  to be `:text'.
  "
    (let (frmt element)
      (cond
       ((> (length args) 1)  (setq frmt (cadr args)
                                   element (car args)))
       ((= (length args) 0)  (setq frmt "text"
                                   element 0))
       ((numberp (car args)) (setq frmt "text"
                                   element (car args)))
       ((= (length args) 1)  (setq frmt (car args)
                                   element 0)))

      (if-let ((results (ring-ref ha-eshell-output (or element 0))))
        (cl-case (string-to-char frmt)
          (?l     (split-string results))
          (?f     (ha-eshell-store-file-output results))
          (otherwise (s-trim results)))
      "")))

  (defun ha-eshell-store-file-output (results)
    "Writes the string, RESULTS, to a temporary file and returns that file name."
    (let ((filename (make-temp-file "ha-eshell-")))
      (with-temp-file filename
        (insert results))
      filename))

How would this function work in practice?

$ ls
a.org  b.txt  c.org  date today.txt  ever

$ output
a.org  b.txt  c.org  date today.txt  ever

$ echo { output list }
("a.org" "b.txt" "c.org" "date" "today.txt" "ever")

Notice how commands between { … } are eshell commands, otherwise, if I replace the braces with parens, I would have to write eshell/output. Lets try the history feature:

$ echo "oldest"
oldest

$ echo "quite old"
quite old

$ echo "fairly recent"
fairly recent

$ echo "newest"
newest

$ echo { output 2 }
quite old

Eshell has a feature where special variables (stored in eshell-variable-aliases-list), can be a function. So $$ can be text-formatted output, and $_ can be the list formatted output, and $OUTPUT can be the output stored in a file.

  (add-to-list 'eshell-variable-aliases-list '("$"  ha-eshell-output-text))
  (add-to-list 'eshell-variable-aliases-list '("_"  ha-eshell-output-list))
  (add-to-list 'eshell-variable-aliases-list '("OUTPUT" ha-eshell-output-file))

Without this change, the $$ variable calls eshell-last-command-result, where I believe my version (with history) may work more reliably. I just need the define this helper functions:

  (defun ha-eshell-output (format-type indices)
    "Wrapper around `eshell/output' for the `eshell-variable-aliases-list'."
    (if indices
        (eshell/output (string-to-number (caar indices)) format-type)
      (eshell/output 0 format-type)))

  (defun ha-eshell-output-text (&optional indices &rest ignored)
    "A _text_ wrapper around `eshell/output' for the `eshell-variable-aliases-list'."
    (ha-eshell-output "text" indices))

  (defun ha-eshell-output-list (&optional indices &rest ignored)
    "A _list_ wrapper around `eshell/output' for the `eshell-variable-aliases-list'."
    (ha-eshell-output "list" indices))

  (defun ha-eshell-output-file (&optional indices &rest ignored)
    "A _file_ wrapper around `eshell/output' for the `eshell-variable-aliases-list'."
    (ha-eshell-output "file" indices))

How would this look? Something like:

$ echo a
a
$ echo b
b
$ echo c
c
$ echo $$
c
$ echo $$[2]
b

The final trick is being able to count backwards and remember they are always shifting. I guess if I wanted to remember the output for more than one command, I could do:

$ ls *.org(U) b.txt
a.org  b.txt

$ chmod o+w $_

$ rg Nam $_[1]
a.org
8:Nam vestibulum accumsan nisl.

b.txt
1:Nam euismod tellus id erat.
7:Name three things that start with C

Wanna see something really cool about Eshell? Lets really swirl Lisp and Shell commands:

$ rg (rx line-start "Nam ") $_[2]
b.txt
1:Nam euismod tellus id erat.

a.org
8:Nam vestibulum accumsan nisl.

Special Prompt

Following these instructions, we build a better prompt with the Git branch in it (Of course, it matches my Bash prompt). First, we need a function that returns a string with the Git branch in it, e.g. ":master"

  (defun curr-dir-git-branch-string (pwd)
    "Returns current git branch as a string, or the empty string if
  PWD is not in a git repo (or the git command is not found)."
    (interactive)
    (when (and (not (file-remote-p pwd))
               (eshell-search-path "git")
               (locate-dominating-file pwd ".git"))
      (let* ((git-url    (shell-command-to-string "git config --get remote.origin.url"))
             (git-repo   (file-name-base (s-trim git-url)))
             (git-output (shell-command-to-string (concat "git rev-parse --abbrev-ref HEAD")))
             (git-branch (s-trim git-output))
             (git-icon   "\xe0a0")
             (git-icon2  (propertize "\xf020" 'face `(:family "octicons"))))
        (concat git-repo " " git-icon2 " " git-branch))))

The function takes the current directory passed in via pwd and replaces the $HOME part with a tilde. I'm sure this function already exists in the eshell source, but I didn't find it…

  (defun pwd-replace-home (pwd)
    "Replace home in PWD with tilde (~) character."
    (interactive)
    (let* ((home (expand-file-name (getenv "HOME")))
           (home-len (length home)))
      (if (and
           (>= (length pwd) home-len)
           (equal home (substring pwd 0 home-len)))
          (concat "~" (substring pwd home-len))
        pwd)))

Make the directory name be shorter…by replacing all directory names with just its first names. However, we leave the last two to be the full names. Why yes, I did steal this.

  (defun pwd-shorten-dirs (pwd)
    "Shorten all directory names in PWD except the last two."
    (let ((p-lst (split-string pwd "/")))
      (if (> (length p-lst) 2)
          (concat
           (mapconcat (lambda (elm) (if (zerop (length elm)) ""
                                 (substring elm 0 1)))
                      (butlast p-lst 2)
                      "/")
           "/"
           (mapconcat (lambda (elm) elm)
                      (last p-lst 2)
                      "/"))
        pwd)))  ;; Otherwise, we just return the PWD

Break up the directory into a "parent" and a "base":

  (defun split-directory-prompt (directory)
    (if (string-match-p ".*/.*" directory)
        (list (file-name-directory directory) (file-name-base directory))
      (list "" directory)))

Using virtual environments for certain languages is helpful to know, especially since I change them based on the directory.

  (defun ruby-prompt ()
    "Returns a string (may be empty) based on the current Ruby Virtual Environment."
    (let* ((executable "~/.rvm/bin/rvm-prompt")
           (command    (concat executable "v g")))
      (when (file-exists-p executable)
        (let* ((results (shell-command-to-string executable))
               (cleaned (string-trim results))
               (gem     (propertize "\xe92b" 'face `(:family "alltheicons"))))
          (when (and cleaned (not (equal cleaned "")))
            (s-replace "ruby-" gem cleaned))))))

  (defun python-prompt ()
    "Returns a string (may be empty) based on the current Python
     Virtual Environment. Assuming the M-x command: `pyenv-mode-set'
     has been called."
    (when (fboundp #'pyenv-mode-version)
      (let ((venv (pyenv-mode-version)))
        (when venv
          (concat
           (propertize "\xe928" 'face `(:family "alltheicons"))
           (pyenv-mode-version))))))

Now tie it all together with a prompt function can color each of the prompts components.

  (defun eshell/eshell-local-prompt-function ()
    "A prompt for eshell that works locally (in that is assumes
  that it could run certain commands) in order to make a prettier,
  more-helpful local prompt."
    (interactive)
    (let* ((pwd        (eshell/pwd))
           (directory (split-directory-prompt
                       (pwd-shorten-dirs
                        (pwd-replace-home pwd))))
           (parent (car directory))
           (name   (cadr directory))
           (branch (curr-dir-git-branch-string pwd))
           (ruby   (when (not (file-remote-p pwd)) (ruby-prompt)))
           (python (when (not (file-remote-p pwd)) (python-prompt)))

           (dark-env (eq 'dark (frame-parameter nil 'background-mode)))
           (for-bars                 `(:weight bold))
           (for-parent  (if dark-env `(:foreground "dark orange") `(:foreground "blue")))
           (for-dir     (if dark-env `(:foreground "orange" :weight bold)
                          `(:foreground "blue" :weight bold)))
           (for-git                  `(:foreground "green"))
           (for-ruby                 `(:foreground "red"))
           (for-python               `(:foreground "#5555FF")))

      (concat
       (propertize "⟣─ "    'face for-bars)
       (propertize parent   'face for-parent)
       (propertize name     'face for-dir)
       (when branch
         (concat (propertize " ── "    'face for-bars)
                 (propertize branch   'face for-git)))
       ;; (when ruby
       ;;   (concat (propertize " ── " 'face for-bars)
       ;;           (propertize ruby   'face for-ruby)))
       ;; (when python
       ;;   (concat (propertize " ── " 'face for-bars)
       ;;           (propertize python 'face for-python)))
       (propertize "\n"     'face for-bars)
       (propertize (if (= (user-uid) 0) " #" " $") 'face `(:weight ultra-bold))
       ;; (propertize " └→" 'face (if (= (user-uid) 0) `(:weight ultra-bold :foreground "red") `(:weight ultra-bold)))
       (propertize " "    'face `(:weight bold)))))

  (setq-default eshell-prompt-function #'eshell/eshell-local-prompt-function)

Here is the result: http://imgur.com/nkpwII0.png

Fringe Status

Some prompts, shells and terminal programs that display the exit code as an icon in the fringe. So can the eshell-fringe-status project. Seems to me, that if would be useful to rejuggle those fringe markers so that the marker matched the command entered (instead of seeing a red mark, and needing to scroll back in order to wonder what command it was that made it). Still…

  (use-package eshell-fringe-status
    :hook (eshell-mode . eshell-fringe-status-mode))

Opening Banner

Whenever I open a shell, I instinctively type ls … so why not do that automatically? Perhaps we hook into the eshell-banner-load-hook:

  (defun ha-eshell-banner (&rest ignored)
    "My personal banner."
    (insert "ls")
    (eshell-send-input))

  (add-hook 'eshell-banner-load-hook 'ha-eshell-banner)

  (setq eshell-banner-message "")

The only thing I would like is to not have the ls shown at the top of the buffer, nor added to the history. Ill work on that some day.

Shell Windows

Now that I often need to quickly pop into remote systems to run a shell or commands, I create helper functions to create those buffer windows. Each begin with eshell-:

Shell There

The basis for opening an shell depends on the location. After that, we make the window smaller, give the buffer a good name, as well as immediately display the files with ls (since I instinctively just do that … every time).

  (defun eshell-there (parent)
    "Open an eshell session in a PARENT directory
  in a smaller window named after this directory."
    (let* ((name (thread-first parent
                               (split-string "/" t)
                               (last)
                               (car)))
           (eshell-buffer-name (format "*eshell: %s*" name))
           (height (/ (window-total-height) 3))
           (default-directory parent))
      (split-window-vertically (- height))
      (eshell)))

Shell Here

This version of the eshell is based on the current buffers parent directory:

  (defun eshell-here ()
    "Opens up a new shell in the directory of the current buffer.
  The eshell is renamed to match that directory to make multiple
  eshell windows easier."
    (interactive)
    (eshell-there (if (buffer-file-name)
                      (file-name-directory (buffer-file-name))
                    default-directory)))

And lets bind it:

  (bind-key "C-!" 'eshell-here)

Shell for a Project

I usually want the eshell to start in the projects root, using projectile-project-root:

  (defun eshell-project ()
    "Open a new shell in the project root directory, in a smaller window."
      (interactive)
      (eshell-there (projectile-project-root)))

And we can attach this function to the projectile menu:

  (ha-leader "p s" '("shell" . eshell-project))

Shell Over There

Would be nice to be able to run an eshell session and use Tramp to connect to the remote host in one fell swoop:

  (defun eshell-remote (host)
    "Creates an eshell session that uses Tramp to automatically
  connect to a remote system, HOST.  The hostname can be either the
  IP address, or FQDN, and can specify the user account, as in
  root@blah.com. HOST can also be a complete Tramp reference."
    (interactive "sHost: ")

    (let ((destination-path
             (cond
              ((string-match-p "^/" host) host)

              ((string-match-p (ha-eshell-host-regexp 'full) host)
               (string-match (ha-eshell-host-regexp 'full) host) ;; Why!?
               (let* ((user1 (match-string 2 host))
                      (host1 (match-string 3 host))
                      (user2 (match-string 6 host))
                      (host2 (match-string 7 host)))
                 (if host1
                     (ha-eshell-host->tramp user1 host1)
                   (ha-eshell-host->tramp user2 host2))))

              (t (format "/%s:" host)))))
      (eshell-there destination-path)))

Shell Here to There

Since I have Org files that contains tables of system to remotely connect to, I figured I should have a little function that can jump to a host found listed anywhere on the line.

The regular expression associated with IP addresses, hostnames, user accounts (of the form, jenkins@my.build.server, or even full Tramp references, is a bit…uhm, hairy. And since I want to reuse these, I will hide them in a function:

  (defun ha-eshell-host-regexp (regexp)
    "Returns a particular regular expression based on symbol, REGEXP"
    (let* ((user-regexp      "\\(\\([[:alnum:]._-]+\\)@\\)?")
           (tramp-regexp     "\\b/ssh:[:graph:]+")
           (ip-char          "[[:digit:]]")
           (ip-plus-period   (concat ip-char "+" "\\."))
           (ip-regexp        (concat "\\(\\(" ip-plus-period "\\)\\{3\\}" ip-char "+\\)"))
           (host-char        "[[:alpha:][:digit:]-]")
           (host-plus-period (concat host-char "+" "\\."))
           (host-regexp      (concat "\\(\\(" host-plus-period "\\)+" host-char "+\\)"))
           (horrific-regexp  (concat "\\b"
                                     user-regexp ip-regexp
                                     "\\|"
                                     user-regexp host-regexp
                                     "\\b")))
      (cond
       ((eq regexp 'tramp) tramp-regexp)
       ((eq regexp 'host)  host-regexp)
       ((eq regexp 'full)  horrific-regexp))))

The function to scan a line for hostname patterns uses different function calls that what I could use for eshell-there, so let's save-excursion and hunt around:

  (defun ha-eshell-scan-for-hostnames ()
    "Helper function to scan the current line for any hostnames, IP
  or Tramp references.  This returns a tuple of the username (if
  found) and the hostname.

  If a Tramp reference is found, the username part of the tuple
  will be `nil'."
    (save-excursion
      (goto-char (line-beginning-position))
      (if (search-forward-regexp (ha-eshell-host-regexp 'tramp) (line-end-position) t)
          (cons nil (buffer-substring-no-properties (match-beginning 0) (match-end 0)))

        ;; Returns the text associated with match expression, NUM or `nil' if no match was found.
        (cl-flet ((ha-eshell-get-expression (num) (if-let ((first (match-beginning num))
                                                           (end   (match-end num)))
                                                      (buffer-substring-no-properties first end))))

          (search-forward-regexp (ha-eshell-host-regexp 'full) (line-end-position))

          ;; Until this is completely robust, let's keep this debugging code here:
          ;; (message (mapconcat (lambda (tup) (if-let ((s (car tup))
          ;;                                       (e (cadr tup)))
          ;;                                  (buffer-substring-no-properties s e)
          ;;                                "null"))
          ;;             (-partition 2 (match-data t)) " -- "))

          (let ((user1 (ha-eshell-get-expression 2))
                (host1 (ha-eshell-get-expression 3))
                (user2 (ha-eshell-get-expression 6))
                (host2 (ha-eshell-get-expression 7)))
            (if host1
                (cons user1 host1)
              (cons user2 host2)))))))

Tramp reference can be long when attempting to connect as another user account using the pipe symbol.

  (defun ha-eshell-host->tramp (username hostname &optional prefer-root)
    "Returns a TRAMP reference based on a USERNAME and HOSTNAME
  that refers to any host or IP address."
    (cond ((string-match-p "^/" host)
             host)
          ((or (and prefer-root (not username)) (equal username "root"))
             (format "/ssh:%s|sudo:%s:" hostname hostname))
          ((or (null username) (equal username user-login-name))
             (format "/ssh:%s:" hostname))
          (t
             (format "/ssh:%s|sudo:%s|sudo@%s:%s:" hostname hostname username hostname))))

Finally, a function to pull it all together.

  (defun eshell-here-on-line (p)
    "Search the current line for an IP address or hostname, and call the `eshell-here' function.

  Call with PREFIX to connect with the `root' useraccount, via
  `sudo'."
    (interactive "p")
    (destructuring-bind (user host) (ha-eshell-scan-for-hostnames)
      (let ((destination (ha-eshell-host->tramp user host (> p 1))))
        (message "Connecting to: %s" destination)
        (eshell-there destination))))

Better Command Line History

On this discussion a little gem for using IDO to search back through the history, instead of M-R to prompt for the history.

  (defun eshell-insert-history ()
    "Displays the eshell history to select and insert back into your eshell."
    (interactive)
    (insert (completing-read "Eshell history: "
                                 (delete-dups
                                  (ring-elements eshell-history-ring)))))

Command on the File Buffer

Sometimes you just need to change something about the current file you are editing…like the permissions or even execute it. Hitting Command-1 will prompt for a shell command string and then append the current file to it and execute it.

  (defun execute-command-on-file-buffer (cmd)
    "Executes a shell command, CMD, on the current buffer's file.
  If the filename is not specified, then it is appended to the cmd, so

      chmod a+x

  Works as expected. The special variable `$$' is replaced with the
  filename of the buffer. Note that this is command is passed to
  `eshell-command', so eshell modifiers are available, for
  instance:

      mv $$ $$(:r).txt

  Will rename the current file to now have a .txt extension.
  See `eshell-display-modifier-help' for details on that."
    (interactive "sCommand to execute: ")
    (let* ((file-name (buffer-file-name))
           (full-cmd (cond ((string-match (rx "$$") cmd)
                            (replace-regexp-in-string (rx "$$") file-name cmd))
                           ((string-match (rx (literal file-name)) cmd)
                            cmd)
                           (t
                            (concat cmd " " file-name)))))
      (message "Executing: %s" full-cmd)
      (eshell-command full-cmd)))

Configuration

Here is where we associate all the functions and their hooks with eshell, through the magic of use-package.

Scrolling through the output and searching for results that can be copied to the kill ring is a great feature of Eshell. However, instead of running end-of-buffer key-binding, the following setting means any other key will jump back to the prompt:

  (use-package eshell
    :straight (:type built-in)
    :init
    (setq eshell-scroll-to-bottom-on-input 'all
          eshell-error-if-no-glob t
          eshell-hist-ignoredups t
          eshell-save-history-on-exit t

          ;; Since eshell starts fast, let's get rid of it quickly:
          eshell-kill-on-exit t
          eshell-destroy-buffer-when-process-dies t

          ;; Can you remember the parameter differences between the
          ;; executables `chmod' and `find' and their Emacs equivalent? Me
          ;; neither, so this makes it act a bit more shell-like:
          eshell-prefer-lisp-functions nil)

    :hook ((eshell-pred-load . ha-eshell-add-predicates)
           (eshell-exit      . delete-window))

    :bind (:map eshell-mode-map
                ("M-R"   . eshell-insert-history)
                ("C-d"   . ha-eshell-quit-or-delete-char)))

Note that the default list to eshell-visual-commands is mostly good enough.

Finally, add some leader commands to call my previously defined functions:

  (ha-leader
        "a e"   '(:ignore t :which-key "eshell")
        "a e e" '("new eshell"          . eshell-here)
        "a e r" '("remote"              . eshell-remote)
        "a e p" '("project"             . eshell-project)
        "a e g" '("at point"            . eshell-here-on-line)
        "a e !" '("exec on file-buffer" . execute-command-on-file-buffer))

No, im not sure why use-package has an issue with both :hook, :bind and :config directives in sequence.