Now $$ is an array of command output history

Extended the `eshell-variable-aliases-list` to call a new function that
stores the output of the commands in an ring. This is a pretty cool feature.
This commit is contained in:
Howard Abrams 2022-09-26 13:08:34 -07:00
parent 324ccce619
commit ecc9c1ee8f

View file

@ -213,7 +213,7 @@ Well leave the =e= alias to replace the =eshell= buffer window.
** Last Results ** 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. 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: While diving into the =eshell= source code, I noticed that the special variables, =$$= and =$_= can be used /sometimes/ as the output of the last command. For instance:
#+begin_example #+begin_example
$ echo "hello world" $ echo "hello world"
hello world hello world
@ -236,45 +236,25 @@ $ echo Nam $$
("Nam" nil) ("Nam" nil)
#+end_example #+end_example
However, we could add a hook that runs /after/ every command to copy the output to a variables of our choosing: However, I could easily over-write that special variables to behave as I would expect:
- A hook runs after every command
- It copies the previous commands output to a /ring/ (so that I can get the last as well as the fifth one)
- Create a replacement function for =$$= to read from my history ring
Lets first make a ring that stores the output:
#+begin_src emacs-lisp #+begin_src emacs-lisp
(defvar OUTPUT "" (defvar ha-eshell-output (make-ring 10)
"Contains the output from the last eshell command executed.") "A ring (looped list) storing history of eshell command output.")
(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 #+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): 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 #+begin_src emacs-lisp
(defun ha-eshell-store-last-output () (defun ha-eshell-store-last-output ()
"Store the output from the last eshell command. "Store the output from the last eshell command.
Called after every command by connecting to the `eshell-post-command-hook'." Called after every command by connecting to the `eshell-post-command-hook'."
(setq OUTPUT (let ((output
(s-trim
(buffer-substring-no-properties eshell-last-input-end eshell-last-output-start))) (buffer-substring-no-properties eshell-last-input-end eshell-last-output-start)))
(setq OUTAF (make-temp-file "ha-eshell-")) (ring-insert ha-eshell-output output)))
(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 #+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]]: Now we save this output after every command by adding it to the [[elisp:(describe-variable 'eshell-post-command-hook)][eshell-post-command-hook]]:
@ -282,78 +262,150 @@ Now we save this output after every command by adding it to the [[elisp:(describ
(add-hook 'eshell-post-command-hook 'ha-eshell-store-last-output) (add-hook 'eshell-post-command-hook 'ha-eshell-store-last-output)
#+end_src #+end_src
Success: Next, this function returns values from the history ring. I feel the need to have different ways of returning the output data.
Unlike the behavior of the original shell (and most of its descendents, like =bash=), =eshell= doesnt automatically split on whitespace. For instance, =echo= called this way:
#+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 if I specify and argument of =text=, it should return the commands output /as a string/, but if I give it, =list=, it should contain the same information, but separated by spaces, into a list. For instance, if we are passing the output from =ls= to =grep=, we would use this format.
Like the =shell-underscore= project mentioned earlier, I can access the output stored from a file when given a =file= argument (the output will hold this temporary filename).
#+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 how the output should be returned:
,* `text' :: as a string
,* `list' :: as a list of elements separated by whitespace
,* `file' :: as a filename that contains the output
If the first argument is not a number, then the format is assumed
to be `:text'.
"
(let (frmt element)
(cond
((> (length args) 1) (setq frmt (cadr args)
element (car args)))
((= (length args) 0) (setq frmt "text"
element 0))
((numberp (car args)) (setq frmt "text"
element (car args)))
((= (length args) 1) (setq frmt (car args)
element 0)))
(if-let ((results (ring-ref ha-eshell-output (or element 0))))
(cl-case (string-to-char frmt)
(?l (split-string results))
(?f (ha-eshell-store-file-output results))
(otherwise (s-trim results)))
"")))
(defun ha-eshell-store-file-output (results)
"Writes the string, RESULTS, to a temporary file and returns that file name."
(let ((filename (make-temp-file "ha-eshell-")))
(with-temp-file filename
(insert results))
filename))
#+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=. Lets try the history feature:
#+begin_example
$ echo "oldest"
oldest
$ echo "quite old"
quite old
$ echo "fairly recent"
fairly recent
$ echo "newest"
newest
$ echo { output 2 }
quite 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/. So =$$= can be text-formatted output, and =$_= can be the list formatted output, and =$OUTPUT= can be the output stored in a file.
#+begin_src emacs-lisp
(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 just need the define this 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 #+begin_example
$ ls *.org(U) b.txt $ ls *.org(U) b.txt
a.org b.txt a.org b.txt
$ rg Nam $LAST $ chmod o+w $_
$ rg Nam $_[1]
a.org a.org
8:Nam vestibulum accumsan nisl. 8:Nam vestibulum accumsan nisl.
b.txt b.txt
1:Nam euismod tellus id erat. 1:Nam euismod tellus id erat.
7:Name three things that start with C
#+end_example #+end_example
*** Accessing Output from the Past Wanna see something really cool about Eshell? Lets really swirl Lisp and Shell commands:
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 #+begin_example
$ echo (output :text 0) # The same as echo $OUTPUT $ rg (rx line-start "Nam ") $_[2]
#+end_example b.txt
1:Nam euismod tellus id erat.
A bit verbose. I think some syntactic sugar functions would be in order: a.org
#+begin_src emacs-lisp 8:Nam vestibulum accumsan nisl.
(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 #+end_example
* Special Prompt * 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" 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"