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.
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."
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:
Note that you need single quotes, and more than one argument doesn’t work with aliases. To resolve that, we need to write [[Eshell Functions][a function]].
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:
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.
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. This will be called when we directly specify 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:
Perhaps we should add this feature to eshell’s version of [[help:eshell/cat][cat]].
** Piper
My [[https://gitlab.com/howardabrams/emacs-piper][piper]] project seems to like a good match with eshell. For instance, typing =piper= in eshell with a file or a command, and the output from that goes into a Piper buffer, where standard Emacs commands can filter, sort or otherwise alter that output. Then, closing it and calling =piper= in eshell without arguments outputs that buffer … to use as part of a pipe or something.
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, 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 isn’t 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 isn’t 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.
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:
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.
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:
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.
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
(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."
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.