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.
After reading [[https://xenodium.com/my-emacs-eye-candy/][this essay]], I decided to try hiding the mode line in eshell windows … at least, until I get the mode line to display more important information. Note that hiding the mode line is fairly straight-forward, but others might want to use the [[https://github.com/hlissner/emacs-hide-mode-line][hide-mode-line]] package that turns that /mode-line definition/ into a minor mode that can be toggled.
- ~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.
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."
Shell completion uses the flexible =pcomplete= mechanism internally, which allows you to program the completions per shell command. To know more, check out this [[https://www.masteringemacs.org/article/pcomplete-context-sensitive-completion-emacs][blog post]], about how to configure =pcomplete= for git commands. The [[https://github.com/JonWaltman/pcmpl-args.el][pcmpl-args]] package extends =pcomplete= with completion support for more commands, like the Fish and other modern shells. I love how a package can gives benefits without requiring learning anything.
Note that this will work with =shell-command= as well.
** 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."
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:
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:
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, [[file:~/.emacs.d/eshell/alias][~/.emacs.d/eshell/alias]] … as long as you don’t use those single quotes:
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
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.
Calling Emacs functions that take a single argument from =eshell= that could accept zero or more, can result in an error. This helper function can open each argument in a different window. It takes two functions, and calls the first function on the first argument, and calls the second function on each of the rest:
#+begin_src emacs-lisp
(defun eshell-fn-on-files (fun1 fun2 args)
"Call FUN1 on the first element in list, ARGS.
Call FUN2 on all the rest of the elements in 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.
The =eshell-command= is supposed to be an interactive command for prompting for a shell command in the mini-buffer. However, I have some functions that run a command and gather the output. For that, we call =eshell-command= but a =t= for the second argument:
I need a function to analyze command line options. I’ve tried to use [[help:eshell-eval-using-options][eshell-eval-using-options]], but it lacks the ability to have both dashed parameter arguments /and/ non-parameter arguments. For instance, I want to type:
To set a variable in Eshell, you use good ol’=setq=, but that would create global variables. We can make a version for Eshell, that makes buffer-local variables.
While I can type =find-file=, I often use =e= as an alias for =emacsclient= in Terminals, so let’s do something similar for =eshell=:
Also note that we can take advantage of the =eshell-fn-on-files= function to expand the [[help:find-file][find-file]] (which takes one argument), to open more than one file at one time.
#+begin_src emacs-lisp
(defun eshell/e (&rest files)
"Essentially an alias to the `find-file' function."
No way would I accidentally type any of the following commands:
#+begin_src emacs-lisp
(defalias 'eshell/emacs 'eshell/e)
(defalias 'eshell/vi 'eshell/e)
(defalias 'eshell/vim 'eshell/e)
#+end_src
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."
Typing a command, but the output isn’t right. So you punch the up arrow, and re-run the command, but this time pass the output through executables like =tr=, =grep=, and even =awk=. Still not right? Rinse and repeat. Tedious. Since using Emacs to edit text is what we do best, what if we took the output of a command from Eshell, edit that output in a buffer, and then use that edited output in further commands?
I call this workflow of sending command output back and forth into an Emacs buffer, an /ebb/ and /flow/ approach, where the =ebb= function (for Edit a Bumped Buffer … or something like that), takes some command output, and opens it in a buffer (with an =ebbflow= minor mode), allowing us to edit or alter the data. Pull that data back to the Eshell session with the [[help:emacs/flow][flow]] function (for Fetch buffer data by Lines or Words … naming is hard).
*** The ebbflow Buffer
If I don’t specify a specific buffer name, we use this default value:
Eshell can send the output of a command sequence to a buffer:
#+begin_src sh
rg -i red > #<scratch>
#+end_src
But I can’t find a way to use the contents of buffers to use as part of the standard input to another as the start of a pipeline. Let’s create a function to fetch buffer contents.
I’m calling the ability to get a buffer contents, /flow/ (Fetch contents as Lines Or Words). While this function will /fetch/ the contents of any buffer, if one is not given, it will fetch the default, =ha-eshell-ebbflow-buffername=. Once the content is fetched, given the correct argument, it may convert the data:
- /as lines/ :: separating the data on newlines. Useful for passing to =for= loops
- /as words/ :: separating on spaces. Useful if the data is filenames
Specify the buffers with either the Eshell approach, e.g. =#<buffer buffer-name>=, or a string, =’*scratch*’=, and if I don’t specify any buffer, we’ll use the default buffer:
#+begin_src emacs-lisp
(defun eshell-flow-buffers (buffers)
"Convert the list, BUFFERS, to actual buffers if given buffer names."
(if buffers
(--map (cond
((bufferp it) it)
((stringp it) (get-buffer it))
(t (error (format "Illegal argument of type %s: %s\n%s"
One way to call =ebb= is with a command wrapped in braces, e.g. =ebb { ls -1 }=, which calls this function, as the output from the ={ … }=/sub-shell/ is passed as arguments to the =ebb= command, and appears as =command-results=:
"Insert the FILES at the INSERT-LOCATION tin `ha-eshell-ebbflow-buffername'."
(ha-eshell-ebb-switch-to-buffer insert-location)
(dolist (file files)
(insert-file file)
(insert "\n")))
#+end_src
If we were not given a command to execute or a list of files to insert, we want to grab the output from the last executed command in the eshell buffer. To do this, we need to move to the start of the output, and then search for the prompt. Luckily Eshell assumes we have set up the [[elisp:(describe-variable 'eshell-prompt-regexp)][eshell-prompt-regexp]] variable:
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
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."
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):
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]].
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:
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.
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:
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'."
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:
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).
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:
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.
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:
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:
I want both the command and the output (as well as comments) to be able to go into an org-mode file, I call my /engineering notebook/. Where in that file? If I use =en= that goes in a “General Notes” section, and =ec= goes into the currently clocked in task in that file.
I use =ex= to refer to both =en= / =ec=. Use cases:
- =ex <command>= :: run the command given and send the output to the notebook
- =ex [-n #]= :: grab the output from a previously executed command (defaults to last one)
- =ex -c "<comment>" <command>= :: run command and write the comment to the current date in the notebook
- =ex <command> :: <comment>= :: run command and write comment to the notebook
- =<command> > ex= :: write output from /command/ to the notebook. This won’t add the command that generated the output.
The =-c= option can be combined with the /command/, but I don’t want it to grab the last output, as I think I would just like to send text to the notebook as after thoughts. If the option to =-c= is blank, perhaps it just calls the capture template to allow me to enter voluminous content.
This requires capture templates that don’t do any formatting. I will reused =c c= from [[file:ha-capturing-notes.org::*General Notes][capturing-notes]] code, and create other templates under =e= prefix:
The [[https://codeberg.org/akib/emacs-eat][Emulate a Terminal]] project provides flicker-free, perfect display, of visual commands in Eshell, eliminating one of my primary issue with using Eshell all the time.
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)."
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...
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.
Display detailed information, like the current working directory, in the mode line using [[https://www.emacswiki.org/emacs/WhichFuncMode][which-function-mode]].
The [[help:eshell/pwd][eshell/pwd]] function returns the current working directory, but we need to have a function that returns that only in =eshell-mode=, otherwise, we will have the current working directory in /every buffer/:
#+begin_src emacs-lisp
(defun ha-eshell-mode-line ()
"Return the current working directory if in eshell-mode."
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...
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:
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.
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]]):
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"
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
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.
Note that the default list to [[elisp:(describe-variable 'eshell-visual-commands)][eshell-visual-commands]] is good enough, but some of my /newer/ Rust-based apps need to be added:
#+begin_src emacs-lisp :tangle no
(use-package eshell
:config
(add-to-list 'eshell-visual-commands "ssh"))
#+end_src
Calling =use-package= with =:config= seems to be just as effective as calling =with-eval-after-load=.