f9003e172f
This is a really nifty function, but should I call it all the time? Adding the name of the buffer automatically is sweet... until I don't want that feature.
1013 lines
47 KiB
Org Mode
1013 lines
47 KiB
Org Mode
#+TITLE: Expanding 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
|
||
;; 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]] 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~ gives you a prompt, even when you are running another command. Since =eshell= passes all input to subprocesses, there is no automatic input queueing as there is with other shells.
|
||
- ~C-c C-t~ truncates 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, =eshell= narrows to view its 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 need that:
|
||
#+begin_src emacs-lisp
|
||
(setenv "PAGER" "cat")
|
||
#+end_src
|
||
* Predicate Filters and Modifiers
|
||
The =T= predicate filter allows me to limit file results that have internal =org-mode= tags. For instance, =eshell= will send files that have a =#+TAGS:= header with a =mac= label 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. Based on what it sees, it returns the predicate function used to filter the files:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-org-file-tags ()
|
||
"Parse the eshell text at point.
|
||
Looks 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 can’t add it to the list until after we start our first eshell session, so we add it to the =eshell-pred-load-hook=.
|
||
* 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 ll 'ls -AlohG --color=always'
|
||
#+end_src
|
||
Note that you need single quotes (not double quotes). Also note that more than one parameter doesn’t work with aliases (to resolve that, we need to write [[Eshell Functions][a function]]).
|
||
|
||
Second, you can create/populate the alias file, =~/.emacs.d/eshell/alias= … as long as you don’t use those single quotes: ~/.emacs.d/eshell/alias
|
||
#+begin_src shell :tangle no
|
||
alias ll ls -AlohG --color=always
|
||
alias d dired $1
|
||
alias find echo 'Please use fd instead.'
|
||
#+end_src
|
||
Yeah, the variable =$*= doesn’t work as you’d expect, so use =$1= when calling Emacs functions that take one parameter).
|
||
For instance, I would like to have:
|
||
#+begin_src sh
|
||
alias less view-file
|
||
#+end_src
|
||
|
||
Third, you want more /control/, you can use the help:eshell/alias function, but it doesn’t honor =$1= and other parameters, so we could create conditionally create function that we add to the [[help:eshell-mode-hook][eshell-mode-hook]], for instance:
|
||
#+begin_src emacs-lisp :tangle no
|
||
(defun ha-eshell-add-aliases ()
|
||
"Call `eshell/alias' to define my aliases."
|
||
;; 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
|
||
|
||
I had a lot of trouble getting aliases to work, for instance =dired= works, but =less= does not:
|
||
#+begin_src sh :tangle no
|
||
alias less view-file $1
|
||
alias d dired $1
|
||
#+end_src
|
||
To work around this, I create functions instead.
|
||
* Eshell Functions
|
||
Any function that begins with =eshell/= is available as a command (with the remaining letters) Once I had a function =eshell/f= as a replacement for =find=, but the [[https://github.com/sharkdp/fd][fd]] project is better.
|
||
|
||
Since =eshell= is an /Emacs/ shell, I try to think 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.
|
||
|
||
This helper function can tell me if an executable program is available, and return its location:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-find-executable (program)
|
||
"Return full path to executable PROGRAM on the `exec-path'."
|
||
(first
|
||
(-filter 'file-executable-p
|
||
(--map (expand-file-name program it) (exec-path)))))
|
||
#+end_src
|
||
** Foobar
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/foobar (&rest args)
|
||
"The `foobar' in Lisp.
|
||
|
||
This does little more than print out information given to it as
|
||
an example of how to write eshell functions."
|
||
(setq args (eshell-flatten-and-stringify args))
|
||
(if eshell-in-pipeline-p
|
||
|
||
(eshell-parse-command (eshell-quote-argument ext-cat) args)
|
||
|
||
(eshell-eval-using-options
|
||
"foobar" args
|
||
'((?h "help" nil nil "show this usage screen")
|
||
(?l "line" nil single-line "display in a single line")
|
||
:show-usage
|
||
:usage "[OPTION] TEXT...
|
||
Display text, or standard input, to standard output.")
|
||
|
||
(if single-line
|
||
(format "Args: %s" args)
|
||
(mapconcat (lambda (word) (format "Arg: %s\n" word)) args "\n")))))
|
||
#+end_src
|
||
** Git
|
||
I used to have a number =g=-prefixed aliases to call git-related commands, but now, I call [[file:ha-config.org::*Magit][Magit]] instead. My =gst= command is 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
|
||
** Buffer Cat
|
||
Why not be able to read a buffer and use it as the start of a pipeline?
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/bcat (&rest args)
|
||
(mapconcat (lambda (buffer-name)
|
||
(when (bufferp buffer-name)
|
||
(save-window-excursion
|
||
(switch-to-buffer buffer-name)
|
||
(buffer-substring-no-properties (point-min) (point-max)))))
|
||
args "\n"))
|
||
#+end_src
|
||
Perhaps we should add this feature to eshell’s version of [[help:eshell/cat][cat]].
|
||
** Replace ls
|
||
I like the output of the [[https://github.com/Peltoche/lsd][lsd]] program, and want =ls= to call it, if available.
|
||
#+begin_src emacs-lisp
|
||
(defvar ha-lsd (ha-find-executable "lsd")
|
||
"Location of the `lsd' program, if installed.")
|
||
#+end_src
|
||
|
||
The problem I have with =lsd= is that it does not display in columns or /colorize/ its output in eshell (even when changing the =TERM= variable). Since I already wrote this code, I’m re-purposing it and expanding it. Step one is to have a function that gives a list of files for a =directory= (notice it doesn’t take options, for if I am going for special output, I’ll be calling =ls= directly).
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-ls-files (&optional directory)
|
||
"Return a list of directories in DIRECTORY or `default-directory' if null."
|
||
(let ((default-directory (or directory default-directory)))
|
||
(if ha-lsd
|
||
(shell-command-to-list (format "%s --icon always" ha-lsd))
|
||
|
||
(directory-files default-directory nil
|
||
(rx string-start
|
||
(not (any "." "#"))
|
||
(one-or-more any)
|
||
(not "~")
|
||
string-end)))))
|
||
#+end_src
|
||
|
||
Given a filename, let’s pad and colorize it based on file attributes:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-ls-filename (filename padded-fmt &optional directory)
|
||
"Return a prettized version of FILE based on its attributes.
|
||
Formats the string with PADDED-FMT."
|
||
(let ((file (expand-file-name (if (string-match (rx (group alpha (zero-or-more any))) filename)
|
||
(match-string 1 filename)
|
||
filename)
|
||
directory))
|
||
(import-rx (rx "README"))
|
||
(image-rx (rx "." (or "png" "jpg" "jpeg" "tif" "wav") string-end))
|
||
(code-rx (rx "." (or "el" "py" "rb") string-end))
|
||
(docs-rx (rx "." (or "org" "md") string-end)))
|
||
(format padded-fmt
|
||
(cond
|
||
((file-directory-p file)
|
||
(propertize filename 'face 'eshell-ls-directory))
|
||
((file-executable-p file)
|
||
(propertize filename 'face 'eshell-ls-executable))
|
||
((string-match import-rx file)
|
||
(propertize filename 'face '(:foreground "orange")))
|
||
((string-match image-rx file)
|
||
(propertize filename 'face 'eshell-ls-special))
|
||
((file-symlink-p file)
|
||
(propertize filename 'face 'eshell-ls-symlink))
|
||
((not (file-readable-p file))
|
||
(propertize filename 'face 'eshell-ls-unreadable))
|
||
(t
|
||
filename)))))
|
||
#+end_src
|
||
|
||
This function pulls all the calls to [[help:ha-eshell-ls-file][ha-eshell-ls-file]] to create columns to make a multi-line string:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-ls (&optional directory)
|
||
"Return a formatted string of files for a directory.
|
||
The string is a pretty version with columns and whatnot."
|
||
(let* ((files (ha-eshell-ls-files (or directory default-directory)))
|
||
(longest (--reduce-from (max acc (length it)) 1 files))
|
||
(width (window-total-width))
|
||
(columns (/ width (+ longest 3)))
|
||
(padded (if ha-lsd
|
||
(format "%%-%ds " longest)
|
||
(format "• %%-%ds " longest))))
|
||
(cl-flet* ((process-lines (files)
|
||
(s-join "" (--map (ha-eshell-ls-filename it padded directory) files)))
|
||
(process-files (table)
|
||
(s-join "\n" (--map (process-lines it) table))))
|
||
|
||
(concat (process-files (seq-partition files columns)) "\n\n"))))
|
||
#+end_src
|
||
|
||
While the =ha-eshell-ls= takes a directory, this version puts the canonical directory as a label before the listing, and this calls it directly specifying the directory name(s):
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-ls-directory (directory)
|
||
"Print the DIRECTORY name and its contents."
|
||
(let ((dir (file-truename directory)))
|
||
(concat
|
||
(propertize dir 'face '(:foreground "gold" :underline t))
|
||
":\n"
|
||
(ha-eshell-ls dir))))
|
||
#+end_src
|
||
I have the interface program to work with =eshell=.
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/lsd (&rest args)
|
||
(let ((lsd (ha-find-executable "lsd")))
|
||
(cond
|
||
;; I expect to call this function without any arguments most of the time:
|
||
((and lsd (null args))
|
||
(ha-eshell-ls))
|
||
;; Called with other directories? Print them all, one at a time:
|
||
((and lsd (--none? (string-match (rx string-start "-") it) args))
|
||
(mapconcat 'ha-eshell-ls-directory args ""))
|
||
;; Calling the function with -l or other arguments, don't bother. Call ls:
|
||
(t (eshell/ls args)))))
|
||
#+end_src
|
||
|
||
Which needs an =ls= alias:
|
||
#+begin_src emacs-lisp :tangle no
|
||
;; (eshell/alias "lss" "echo $@")
|
||
#+end_src
|
||
** Regular Expressions
|
||
I think using the [[help:rx][rx]] macro with applications like =grep= is great reason why =eshell= rocks. Assuming we can’t remember cryptic regular expression syntax, we could look for a GUID-like strings using =ripgrep= with:
|
||
#+begin_src sh
|
||
$ rg (rx (one-or-more hex) "-" (one-or-more hex))
|
||
#+end_src
|
||
The problem with this trick is that =rx= outputs an Emacs-compatible regular expression, which doesn’t always match regular expressions accepted by most applications.
|
||
|
||
The [[https://github.com/joddie/pcre2el][pcre2el]] project can convert from a Lisp regular expression to a [[http://www.pcre.org/][PCRE]] (Perl Compatible Regular Expression), acceptable by [[https://github.com/BurntSushi/ripgrep][ripgrep]].
|
||
#+begin_src emacs-lisp
|
||
(use-package pcre2el
|
||
:straight (:host github :repo "joddie/pcre2el")
|
||
:config
|
||
(defmacro prx (&rest expressions)
|
||
"Convert the rx-compatible regular EXPRESSIONS to PCRE.
|
||
Most shell applications accept Perl Compatible Regular Expressions."
|
||
`(rx-let ((integer (1+ digit))
|
||
(float (seq integer "." integer))
|
||
(time (seq digit (optional digit) ":" (= 2 digit) (optional ":" (= 2 digit))))
|
||
(date (seq (= 2 digit) (or "/" "-") (= 2 digit) (or "/" "-") (= 4 digit)))
|
||
(ymd (seq (= 4 digit) (or "/" "-") (= 2 digit) (or "/" "-") (= 2 digit)))
|
||
(guid (seq (= 8 hex) "-" (= 3 (seq (= 4 hex) "-")) (= 12 hex))))
|
||
(rxt-elisp-to-pcre (rx ,@expressions)))))
|
||
#+end_src
|
||
** Map
|
||
While I like eshell’s =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, for instance, wouldn’t it be cool to type something like:
|
||
#+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 what about using =::= as a separator of the /lambda/ from the /list/, like:
|
||
#+begin_src sh :tangle no
|
||
map chmod a+x :: *.org b.txt
|
||
#+end_src
|
||
|
||
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.
|
||
Separate the sequence and the elements with a `::' string.
|
||
For instance:
|
||
|
||
map chmod a+x _ :: *.org b.txt
|
||
|
||
The function substitutes the `_' sequence a single filename element,
|
||
and if not specified, it appends the file name 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.
|
||
** Editing Files
|
||
The =e= is an alias to [[help:find-file][find-file]] (which takes one argument), we define a special function to open each argument in a different window. We define a /helper function/ for dealing with more than one argument. It takes two functions, where we call the first function on the first argument, and call the second function on each of the rest.
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-fn-on-files (fun1 fun2 args)
|
||
(unless (null args)
|
||
(let ((filenames (flatten-list args)))
|
||
(funcall fun1 (car filenames))
|
||
(when (cdr filenames)
|
||
(mapcar fun2 (cdr filenames))))
|
||
;; Return an empty string, as the return value from `fun1'
|
||
;; probably isn't helpful to display in the `eshell' window.
|
||
""))
|
||
#+end_src
|
||
This allows us to replace some of our aliases with functions:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/e (&rest files)
|
||
"Edit one or more files in current window."
|
||
(eshell-fn-on-files 'find-file 'find-file-other-window files))
|
||
|
||
(defun eshell/ee (&rest files)
|
||
"Edit one or more files in another window."
|
||
(eshell-fn-on-files 'find-file-other-window 'find-file-other-window files))
|
||
#+end_src
|
||
We’ll leave the =e= alias to replace the =eshell= buffer window.
|
||
|
||
No way would I ever accidentally type any of the following commands:
|
||
#+begin_src emacs-lisp
|
||
(defalias 'eshell/vi 'eshell/e)
|
||
(defalias 'eshell/vim 'eshell/e)
|
||
(defalias 'eshell/emacs 'eshell/e)
|
||
#+end_src
|
||
** Less and More
|
||
Both =less= and =more= are the same to me. as I want to scroll through a file. Sure the [[https://github.com/sharkdp/bat][bat]] program is cool, but from eshell, we could call [[help:view-file][view-file]], and hit ~q~ to quit and return to the shell.
|
||
#+begin_src emacs-lisp
|
||
(defun eshell/less (&rest files)
|
||
"Essentially an alias to the `view-file' function."
|
||
(eshell-fn-on-files 'view-file 'view-file-other-window files))
|
||
#+end_src
|
||
Do I type =more= any more than =less=?
|
||
#+begin_src emacs-lisp
|
||
(defalias 'eshell/more 'eshell/less)
|
||
(defalias 'eshell/view 'eshell/less)
|
||
#+end_src
|
||
** 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, you’d =tee= at the end of every command). An interesting idea that I could duplicate.
|
||
|
||
While diving into the =eshell= source code, I noticed the special variables, =$$= and =$_= /sometimes/ contains the output of the last command. For instance:
|
||
#+begin_example
|
||
$ echo "hello world"
|
||
hello world
|
||
$ echo $$
|
||
hello world
|
||
#+end_example
|
||
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= doesn’t:
|
||
#+begin_example
|
||
$ ls *.org(U) b.txt
|
||
a.org b.org f.org b.txt
|
||
|
||
$ echo Nam $$
|
||
("Nam" nil)
|
||
#+end_example
|
||
|
||
I over-write that special variables to behave as expected:
|
||
- A hook runs after every command
|
||
- It copies the previous command’s 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
|
||
|
||
Let’s first make a ring that stores the output:
|
||
#+begin_src emacs-lisp
|
||
(defvar ha-eshell-output (make-ring 10)
|
||
"A ring (looped list) storing history of eshell command output.")
|
||
#+end_src
|
||
|
||
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'."
|
||
(let ((output
|
||
(buffer-substring-no-properties eshell-last-input-end eshell-last-output-start)))
|
||
(ring-insert ha-eshell-output 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
|
||
|
||
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= doesn’t 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
|
||
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/. If I specify and argument of =text=, it should return the command’s 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).
|
||
#+begin_src emacs-lisp
|
||
(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 the returned output:
|
||
,* `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, it assumes the format
|
||
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))
|
||
#+end_src
|
||
|
||
How would this function work in practice?
|
||
#+begin_example
|
||
$ 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")
|
||
#+end_example
|
||
Notice how commands between ={ … }= are =eshell= commands, otherwise, if I replace the braces with parens, I would have to write =eshell/output=. Let’s try the history feature:
|
||
#+begin_example
|
||
$ echo "oldest"
|
||
oldest
|
||
|
||
$ echo "old"
|
||
old
|
||
|
||
$ echo "recent"
|
||
recent
|
||
|
||
$ echo "newest"
|
||
newest
|
||
|
||
$ echo { output 2 }
|
||
old
|
||
#+end_example
|
||
|
||
Eshell has a feature where /special variables/ (stored in [[elisp:(describe-variable 'eshell-variable-aliases-list)][eshell-variable-aliases-list]]), can be a /function/. The =$$= holds text-formatted output, and =$_= contains list-formatted output, and =$OUTPUT= can be the output stored in a file.
|
||
#+begin_src emacs-lisp
|
||
(with-eval-after-load "eshell"
|
||
(defvar eshell-variable-aliases-list nil "Autoloading this eshell-defined variable")
|
||
(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)))
|
||
#+end_src
|
||
Without this change, the =$$= variable calls [[help:eshell-last-command-result][eshell-last-command-result]], where I believe my version (with history) may work more reliably. I define these helper functions:
|
||
#+begin_src emacs-lisp
|
||
(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))
|
||
#+end_src
|
||
|
||
How would this look? Something like:
|
||
#+begin_example
|
||
$ echo a
|
||
a
|
||
$ echo b
|
||
b
|
||
$ echo c
|
||
c
|
||
$ echo $$
|
||
c
|
||
$ echo $$[2]
|
||
b
|
||
#+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
|
||
$ 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 animals that start with C
|
||
#+end_example
|
||
Wanna see something cool about Eshell? Let’s swirl Lisp and Shell commands:
|
||
#+begin_example
|
||
$ rg (rx line-start "Nam ") $_[2]
|
||
b.txt
|
||
1:Nam euismod tellus id erat.
|
||
|
||
a.org
|
||
8:Nam vestibulum accumsan nisl.
|
||
#+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 its first names. 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 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, 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 I've called the M-x command:
|
||
`pyenv-mode-set'."
|
||
(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 it assumes it
|
||
could run certain commands) 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
|
||
The [[http://projects.ryuslash.org/eshell-fringe-status/][eshell-fringe-status]] project shows a color-coded icon of the previous command run (green for success, red for error). Doesn’t work reliably, but the fringe is inconspicuous. 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 to seethe command that made the error). 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? The [[elisp:(describe-variable 'eshell-banner-message)][eshell-banner-message]] variable, while defaults to a string, this variable can be a /form/ (an s-expression) that calls a function, so I made a customized =ls= that can be attractive:
|
||
#+begin_src emacs-lisp
|
||
(defun ha-eshell-banner ()
|
||
"Return a string containing the files in the current directory."
|
||
(eshell/lsd))
|
||
#+end_src
|
||
* Shell Windows
|
||
Now that I often need to pop into remote systems to run a shell or commands, I create helper functions to create those buffer windows. Each buffer begins with =eshell=: allowing me to have more than one eshells, typically, one per project.
|
||
** Shell There
|
||
The basis for distinguishing a shell is its /parent location/. Before starting =eshell=, we make a small window, set the buffer name (using the [[elisp:(describe-variable 'eshell-buffer-name)][eshell-buffer-name]]):
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-there (parent)
|
||
"Open an eshell session in a PARENT directory.
|
||
The window is smaller and named after this directory."
|
||
(let* ((name (thread-first parent
|
||
(split-string "/" t)
|
||
(last)
|
||
(car)))
|
||
(height (/ (window-total-height) 3))
|
||
(default-directory parent))
|
||
(split-window-vertically (- height))
|
||
(setq eshell-buffer-name (format "*eshell: %s*" name))
|
||
(eshell)))
|
||
#+end_src
|
||
** Shell Here
|
||
This version of the =eshell= bases the location on the current buffer’s parent directory:
|
||
#+begin_src emacs-lisp
|
||
(defun eshell-here ()
|
||
"Opens a new shell in the directory of the current buffer.
|
||
Renames the eshell buffer to match that directory to allow more
|
||
than one eshell window."
|
||
(interactive)
|
||
(eshell-there (if (buffer-file-name)
|
||
(file-name-directory (buffer-file-name))
|
||
default-directory)))
|
||
#+end_src
|
||
And let’s bind it:
|
||
#+begin_src emacs-lisp
|
||
(bind-key "C-!" 'eshell-here)
|
||
#+end_src
|
||
** Shell for a Project
|
||
This version starts =eshell= in the project’s 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 t" '("eshell" . 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
|
||
;; Is the HOST already an absolute tramp reference?
|
||
((string-match-p (rx line-start "/") host) host)
|
||
|
||
;; Does it match any acceptable reference? Get the parts:
|
||
((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))))
|
||
|
||
;; Otherwise, we assume we have a hostname from a string?
|
||
;; Convert to a simple 'default' tramp URL:
|
||
(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 found a Tramp reference, the username part of the tuple is `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 found no match
|
||
(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 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)
|
||
"Return 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
|
||
|
||
This function pulls 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 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.
|
||
Appends the filename to the command if not specified, so:
|
||
|
||
chmod a+x
|
||
|
||
Works as expected. We replace the special variable `$$' with the
|
||
filename of the buffer. Note that `eshell-command' executes this
|
||
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."
|
||
|
||
(let* ((file-name (buffer-file-name))
|
||
(full-cmd (cond ((string-match (rx "$$") cmd)
|
||
(replace-regexp-in-string (rx "$$") file-name cmd))
|
||
((and file-name (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=.
|
||
#+begin_src emacs-lisp
|
||
(use-package eshell
|
||
:straight (:type built-in)
|
||
:custom (eshell-banner-message '(ha-eshell-banner))
|
||
|
||
:init
|
||
(setq eshell-error-if-no-glob t
|
||
;; This jumps back to the prompt:
|
||
eshell-scroll-to-bottom-on-input 'all
|
||
eshell-hist-ignoredups t
|
||
eshell-save-history-on-exit t
|
||
|
||
;; Since eshell starts fast, let's dismiss it on exit:
|
||
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 counterpart?
|
||
;; 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))
|
||
|
||
:bind (("M-!" . eshell-command)
|
||
("s-1" . execute-command-on-file-buffer)
|
||
: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 good enough.
|
||
|
||
Add leader commands to call my defined functions:
|
||
#+begin_src emacs-lisp
|
||
(ha-leader
|
||
"!" '("eshell cmd" . execute-command-on-file-buffer)
|
||
"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 b" '("exec on file-buffer" . execute-command-on-file-buffer))
|
||
#+end_src
|
||
No, i’m 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
|