hamacs/ha-eshell.org
Howard Abrams 334c3f9cff Access past version's of eshell command output
Simple addition of some rings to store a bit of eshell command
history. I really can't believe how easy this stuff is to write.
2022-09-23 21:56:49 -07:00

772 lines
36 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#+TITLE: Eshell
#+AUTHOR: Howard X. Abrams
#+DATE: 2022-09-13
#+FILETAGS: :emacs:
A literate programming file for configuring the Emacs Shell.
#+begin_src emacs-lisp :exports none
;;; ha-eshell --- Emacs Shell configuration. -*- lexical-binding: t; -*-
;;
;; © 2022 Howard X. Abrams
;; This work is licensed under a Creative Commons Attribution 4.0 International License.
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams
;; Created: September 13, 2022
;;
;; While obvious, GNU Emacs does not include this file or project.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; /Users/howard.abrams/other/hamacs/ha-eshell.org
;; And tangle the file to recreate this one.
;;
;;; Code:
#+end_src
* Introduction
While I like [[https://github.com/akermu/emacs-libvterm][vterm]], especially for logging into [[file:ha-remoting.org][remote systems]], I find Emacs shell, =eshell=, an interesting alternative.
If you find the documentation lacking, I [[http://www.howardism.org/Technical/Emacs/eshell-fun.html][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 [[https://github.com/wasamasa/dotemacs/blob/master/init.org#eshell][wasamasa]]):
#+begin_src emacs-lisp
(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)))
#+END_SRC
** Pager Setup
If any program wants to pause the output through the =$PAGER= variable, well, we don't really need that:
#+begin_src emacs-lisp
(setenv "PAGER" "cat")
#+end_src
* Aliases
Gotta have some [[http://www.emacswiki.org/emacs/EshellAlias][shell aliases]], right? We have three ways of doing that. First, enter them into an =eshell= session:
#+begin_src sh
alias less 'view-file $1'
#+end_src
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:
#+begin_src shell :tangle ~/.emacs.d/eshell/alias
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.'
#+end_src
Which happens when you type those commands into an =eshell=.
Third, you want /control/, write a function to define the aliases:
#+begin_src emacs-lisp :tangle no
(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"))))
#+end_src
* 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:
#+begin_src sh
$ grep brew *.org(T'mac')
#+end_src
As described in [[http://www.howardism.org/Technical/Emacs/eshell-fun.html][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:
#+begin_src emacs-lisp
(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.")))
#+END_src
Then we need add that function to the =eshell-predicate-alist= as the =T= tag:
#+begin_src emacs-lisp
(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))))
#+end_src
*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 [[https://github.com/sharkdp/fd][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 [[file:ha-config.org::*Magit][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:
#+begin_src sh :tangle no
for file in *.org {
chmod a+x $file
}
#+end_src
I like the idea of using a /map/ structure, as in:
#+begin_src sh :tangle no
map chmod a+x *.org
#+end_src
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:
#+begin_src sh :tangle no
map chmod a+x (list "a.org" "c.org")
#+end_src
Pretty ugly, but that isnt my use-case. I could introduce syntax like:
#+begin_src sh :tangle no
map chmod a+x :: *.org b.txt
#+end_src
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.
#+begin_src emacs-lisp
(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)))))
#+end_src
The [[help:eshell-named-command][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:
#+begin_src emacs-lisp
(defun eshell/gst (&rest args)
(magit-status (pop args) nil)
(eshell/echo)) ;; The echo command suppresses output
#+end_src
** Editing Files
Which an alias to [[help:find-file][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.
#+begin_src emacs-lisp
(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))))))
#+end_src
This allows us to replace some of our aliases with functions:
#+begin_src emacs-lisp
(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))
#+end_src
Well leave the =e= alias to replace the =eshell= buffer window.
** Last Results
The [[https://github.com/mathiasdahl/shell-underscore][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 variable, =$$= can be used /sometimes/ as the output of the last command. For instance:
#+begin_example
$ echo "hello world"
hello world
$ echo $$
hello world
#+end_example
However, what I would like is something like this to work:
#+begin_example
$ ls *.org(U)
a.org b.org f.org
$ rg "foobar" $$
#+end_example
The problem /may/ be between calling Emacs functions versus external commands, as the =echo= works, but the call to =ls= doesnt:
#+begin_example
$ ls *.org(U) b.txt
a.org b.org f.org b.txt
$ echo Nam $$
("Nam" nil)
#+end_example
However, we could add a hook that runs /after/ every command to copy the output to a variables of our choosing:
#+begin_src emacs-lisp
(defvar OUTPUT ""
"Contains the output from the last eshell command executed.")
(defvar LAST nil
"Contains a list of elements from the last eshell command executed.")
(defvar OUTAF ""
"Contains a filename that contains the output from the last eshell command.")
#+end_src
Why three variables? Well 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:
#+begin_example
$ echo a b *.txt
("a" "b"
("b.txt" "date today.txt"))
#+end_example
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 we want =$OUTPUT= to contain the commands output /as a string/, and we have, =$LAST= contains the same stuff, but separated by spaces, into a list. So, if we are passing the output from =ls= to =grep=, we would use =$LAST= to represent files. And, like the =shell-underscore= project mentioned earlier, I may want to have the output stored in a file, so =$OUTAF= will hold this temporary filename… you know, /OUTput As a File/, right?
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, [[elisp:(describe-variable 'eshell-last-input-end)][eshell-last-input-end]] (the start of the output), and [[elisp:(describe-variable 'eshell-last-output-start)][eshell-last-output-start]] (the end of the output):
#+begin_src emacs-lisp
(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'."
(setq OUTPUT
(s-trim
(buffer-substring-no-properties eshell-last-input-end eshell-last-output-start)))
(setq OUTAF (make-temp-file "ha-eshell-"))
(setq LAST (split-string OUTPUT))
;; Put the three values in the historical rings (see below):
(ha-eshell-store-output-history OUTPUT LAST OUTAF)
(ring-insert (gethash :text ha-eshell-output) OUTPUT)
(ring-insert (gethash :list ha-eshell-output) LAST)
(ring-insert (gethash :file ha-eshell-output) OUTAF)
(with-temp-file OUTAF
(insert OUTPUT)))
#+end_src
Now we save this output after every command by adding it to the [[elisp:(describe-variable 'eshell-post-command-hook)][eshell-post-command-hook]]:
#+begin_src emacs-lisp
(add-hook 'eshell-post-command-hook 'ha-eshell-store-last-output)
#+end_src
Success:
#+begin_example
$ ls *.org(U) b.txt
a.org b.txt
$ rg Nam $LAST
a.org
8:Nam vestibulum accumsan nisl.
b.txt
1:Nam euismod tellus id erat.
#+end_example
*** Accessing Output from the Past
It would also be great if you could grab /historical/ versions of those output. Instead of storing two or three objects to hold them, what about a hash table as a single interface?
#+begin_src emacs-lisp
(defvar ha-eshell-output (make-hash-table :size 3)
"A collection of rings representing the various historical output")
#+end_src
How would we store the historical lists? This is what [[info:elisp#Rings][rings]] are for:
#+begin_src emacs-lisp
(puthash :text (make-ring 10) ha-eshell-output)
(puthash :file (make-ring 10) ha-eshell-output)
(puthash :list (make-ring 10) ha-eshell-output)
#+end_src
The [[help:ha-eshell-store-last-output][ha-eshell-store-last-output]] function calls this function in order to store the results in the three rings:
#+begin_src emacs-lisp
(defun ha-eshell-store-output-history (last-output last-list last-output-file)
"Store the LAST-OUTPUT as a string in a ring in the `ha-eshell-output'.
The LAST-LIST and LAST-OUTPUT-FILE are also store in separate rings."
(ring-insert (gethash :text ha-eshell-output) last-output)
(ring-insert (gethash :list ha-eshell-output) last-list)
(ring-insert (gethash :file ha-eshell-output) last-output-file))
#+end_src
How best to access this historical data. If there we some other shell, I might have variables like =$OUTPUT_3= or something. I think a function may be sufficient in practice. Ill just call it [[help:eshell/output][output]] until something better comes along.
#+begin_src emacs-lisp
(defun eshell/output (frmt &optional element)
"Return an eshell command output from its history.
The FORMAT represents how the output should be returned, and must
be `:text', `:list' or `:file'. The ELEMENT is the index into the
historical past, where `0' is the most recent, `1' is the next oldest, etc."
(if-let ((ring (gethash frmt ha-eshell-output)))
(ring-ref ring (or element 0))
""))
#+end_src
How would this function work in practice?
#+begin_example
$ echo (output :text 0) # The same as echo $OUTPUT
#+end_example
A bit verbose. I think some syntactic sugar functions would be in order:
#+begin_src emacs-lisp
(defun eshell/output-t (&optional element)
(eshell/output :text element))
(defun eshell/output-l (&optional element)
(eshell/output :list element))
(defun eshell/output-f (&optional element)
(eshell/output :file element))
#+end_src
How would this look? Something like:
#+begin_example
$ cat (output-f 4)
#+end_example
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:
#+begin_example
$ setq OUTPUT_A $OUTPUT
#+end_example
* Special Prompt
Following [[http://blog.liangzan.net/blog/2012/12/12/customizing-your-emacs-eshell-prompt/][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"
#+begin_src emacs-lisp :tangle no
(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))))
#+end_src
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...
#+begin_src emacs-lisp :tangle no
(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)))
#+end_src
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.
#+begin_src emacs-lisp :tangle no
(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
#+end_src
Break up the directory into a "parent" and a "base":
#+begin_src emacs-lisp :tangle no
(defun split-directory-prompt (directory)
(if (string-match-p ".*/.*" directory)
(list (file-name-directory directory) (file-name-base directory))
(list "" directory)))
#+END_SRC
Using virtual environments for certain languages is helpful to know, especially since I change them based on the directory.
#+begin_src emacs-lisp :tangle no
(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))))))
#+end_src
Now tie it all together with a prompt function can color each of the prompts components.
#+begin_src emacs-lisp :tangle no
(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)
#+end_src
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 [[http://projects.ryuslash.org/eshell-fringe-status/][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...
#+begin_src emacs-lisp
(use-package eshell-fringe-status
:hook (eshell-mode . eshell-fringe-status-mode))
#+end_src
** Opening Banner
Whenever I open a shell, I instinctively type =ls= … so why not do that automatically? Perhaps we hook into the [[elisp:(describe-variable 'eshell-banner-load-hook)][eshell-banner-load-hook]]:
#+begin_src emacs-lisp :tangle no
(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 "")
#+end_src
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).
#+begin_src emacs-lisp
(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)))
#+end_src
** Shell Here
This version of the =eshell= is based on the current buffers parent directory:
#+begin_src emacs-lisp
(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)))
#+end_src
And lets bind it:
#+begin_src emacs-lisp
(bind-key "C-!" 'eshell-here)
#+end_src
** Shell for a Project
I usually want the =eshell= to start in the projects root, using [[help:projectile-project-root][projectile-project-root]]:
#+begin_src emacs-lisp
(defun eshell-project ()
"Open a new shell in the project root directory, in a smaller window."
(interactive)
(eshell-there (projectile-project-root)))
#+end_src
And we can attach this function to the =projectile= menu:
#+begin_src emacs-lisp
(ha-leader "p s" '("shell" . eshell-project))
#+end_src
** 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:
#+begin_src emacs-lisp
(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)))
#+END_SRC
** 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:
#+begin_src emacs-lisp
(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))))
#+END_SRC
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:
#+begin_src emacs-lisp
(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)))))))
#+end_src
Tramp reference can be long when attempting to connect as another user account using the pipe symbol.
#+begin_src emacs-lisp
(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))))
#+end_src
Finally, a function to pull it all together.
#+begin_src emacs-lisp
(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))))
#+end_src
* Better Command Line History
On [[http://www.reddit.com/r/emacs/comments/1zkj2d/advanced_usage_of_eshell/][this discussion]] a little gem for using IDO to search back through the history, instead of =M-R= to prompt for the history.
#+begin_src emacs-lisp
(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)))))
#+END_SRC
* 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.
#+begin_src emacs-lisp
(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)))
#+end_src
* 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:
#+begin_src emacs-lisp
(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)))
#+end_src
Note that the default list to [[emacs-lisp:(describe-variable 'eshell-visual-commands)][eshell-visual-commands]] is mostly good enough.
Finally, add some leader commands to call my previously defined functions:
#+begin_src emacs-lisp
(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))
#+end_src
No, im not sure why =use-package= has an issue with both =:hook=, =:bind= and =:config= directives in sequence.
* Technical Artifacts :noexport:
Let's =provide= a name so we can =require= this file:
#+begin_src emacs-lisp :exports none
(provide 'ha-eshell)
;;; ha-eshell.el ends here
#+end_src
#+DESCRIPTION: Emacs configuration for the Emacs Shell.
#+PROPERTY: header-args:sh :tangle no
#+PROPERTY: header-args:emacs-lisp :tangle yes
#+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes
#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil
#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil
#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js