armacs/customsrc/eshell.org

452 lines
18 KiB
Org Mode
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

#+TITLE: Eshell
#+AUTHOR: Abraham Raji
#+EMAIL: abrahamraji99@gmail.com
#+STARTUP: overview
#+CREATOR: avronr
#+LANGUAGE: en
#+OPTIONS: num:nil
* Eshell
** Basics
*** Set up the Correct Path
Need the correct PATH even if we start Emacs from the GUI:
#+BEGIN_SRC emacs-lisp
(setenv "PATH"
(concat
"/usr/local/bin:/usr/local/sbin:"
(getenv "PATH")))
#+END_SRC
*** Pager Setup
If any program wants to pause the output through the =$PAGER= variable, well
, we don't really need that:
#+BEGIN_SRC emacs-lisp
(setenv "PAGER" "cat")
#+END_SRC
*** Navigation and Keys
Eshell comes with some interesting features:
- ~M-RET~ can be used to accumulate further commands while a command is currently
running. Since all input is passed to the subprocess being executed, there is no
automatic input queueing as there is with other shells.
- ~C-c C-t~ can be used to truncate the buffer if it grows too large.
- ~C-c C-r~ will move point to the beginning of the output of the last command.
With a prefix argument, it will narrow to view only that output.
- ~C-c C-o~ will delete the output from the last command.
- ~C-c C-f~ will move forward a complete shell argument.
- ~C-c C-b~ will move backward a complete shell argument.
** Configuration
Scrolling through the output and searching for results that can be copied to the
kill ring is a great feature of Eshell. However, instead of running =end-of-buffer
= key-binding, the following setting means any other key will jump back to the prompt:
#+BEGIN_SRC emacs-lisp
(use-package eshell
:init
(setq ;; eshell-buffer-shorthand t ... Can't see Bug#19391
eshell-scroll-to-bottom-on-input 'all
eshell-error-if-no-glob t
eshell-hist-ignoredups t
eshell-save-history-on-exit t
eshell-prefer-lisp-functions nil
eshell-destroy-buffer-when-process-dies t))
#+END_SRC
I can never seem to remember that =find= and =chmod= behave differently from
Emacs than their Unix counterparts, so the last setting will prefer the native
implementations.
** Visual Executables
Eshell would get somewhat confused if I ran the following commands directly through
the normal Emacs-Lisp library, as these need the better handling of ansiterm:
#+BEGIN_SRC emacs-lisp
(use-package eshell
:init
(add-hook 'eshell-mode-hook
(lambda ()
(add-to-list 'eshell-visual-commands "ssh")
(add-to-list 'eshell-visual-commands "tail")
(add-to-list 'eshell-visual-commands "top"))))
#+END_SRC
** Aliases
Gotta have some [[http://www.emacswiki.org/emacs/EshellAlias][shell aliases]], right?
#+BEGIN_SRC emacs-lisp
(add-hook 'eshell-mode-hook (lambda ()
(eshell/alias "e" "find-file $1")
(eshell/alias "ff" "find-file $1")
(eshell/alias "emacs" "find-file $1")
(eshell/alias "ee" "find-file-other-window $1")
(eshell/alias "gd" "magit-diff-unstaged")
(eshell/alias "gds" "magit-diff-staged")
(eshell/alias "d" "dired $1")
;; 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
** Git
My =gst= command is just an alias to =magit-status=, but using the =alias= doesn't
pull in the current working directory, so I make it a function, instead:
#+BEGIN_SRC emacs-lisp
(defun eshell/gst (&rest args)
(magit-status (pop args) nil)
(eshell/echo)) ;; The echo command suppresses output
#+END_SRC
** Find File
We should have an "f" alias for searching the current directory for a file, and
a "ef" for editing that file.
#+BEGIN_SRC emacs-lisp
(defun eshell/f (filename &optional dir try-count)
"Searches for files matching FILENAME in either DIR or the
current directory. Just a typical wrapper around the standard
`find' executable.
Since any wildcards in FILENAME need to be escaped, this wraps the shell command.
If not results were found, it calls the `find' executable up to
two more times, wrapping the FILENAME pattern in wildcat
matches. This seems to be more helpful to me."
(let* ((cmd (concat
(executable-find "find")
" " (or dir ".")
" -not -path '*/.git*'"
" -and -not -path '*node_modules*'"
" -and -not -path '*classes*'"
" -and "
" -type f -and "
"-iname '" filename "'"))
(results (shell-command-to-string cmd)))
(if (not (s-blank-str? results))
results
(cond
((or (null try-count) (= 0 try-count))
(eshell/f (concat filename "*") dir 1))
((or (null try-count) (= 1 try-count))
(eshell/f (concat "*" filename) dir 2))
(t "")))))
(defun eshell/ef (filename &optional dir)
"Searches for the first matching filename and loads it into a
file to edit."
(let* ((files (eshell/f filename dir))
(file (car (s-split "\n" files))))
(find-file file)))
#+END_SRC
Typing =find= in Eshell runs the =find= function, which doesnt do what I expect
, and creating an alias is ineffective in overriding it, so a function will do:
#+BEGIN_SRC emacs-lisp
(defun eshell/find (&rest args)
"Wrapper around the find executable."
(let ((cmd (concat "find " (string-join args))))
(shell-command-to-string cmd)))
#+END_SRC
** Clear
While deleting and recreating =eshell= may be just as fast, I always
forget and type =clear=, so let's implement it:
#+BEGIN_SRC emacs-lisp
(defun eshell/clear ()
"Clear the eshell buffer."
(let ((inhibit-read-only t))
(erase-buffer)
(eshell-send-input)))
#+END_SRC
** Predicate Filters and Modifiers
The =T= predicate filter allows me to limit file results that have have internal =org-mode= tags. For instance, files that have a =#+TAGS:= header with a =mac=
label will be given to the =grep= function:
#+BEGIN_SRC sh
$ grep brew *.org(T'mac')
#+END_SRC
To extend Eshell, we need a two-part function.
1. Parse the Eshell buffer to look for the parameter (and move the point past the parameter).
2. A predicate function that takes a file as a parameter.
For the first step, we have our function /called/ as it helps parse the text at
this time. Based on what it sees, it returns the predicate function used to filter
the files:
#+BEGIN_SRC emacs-lisp
(defun eshell-org-file-tags ()
"Helps the eshell parse the text the point is currently on,
looking for parameters surrounded in single quotes. Returns a
function that takes a FILE and returns nil if the file given to
it doesn't contain the org-mode #+TAGS: entry specified."
(if (looking-at "'\\([^)']+\\)'")
(let* ((tag (match-string 1))
(reg (concat "^#\\+TAGS:.* " tag "\\b")))
(goto-char (match-end 0))
`(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
Add it to the =eshell-predicate-alist= as the =T= tag:
#+BEGIN_SRC emacs-lisp
(add-hook 'eshell-pred-load-hook (lambda ()
(add-to-list 'eshell-predicate-alist '(?T . (eshell-org-file-tags)))))
#+END_SRC *Note:* We cant add it to the list until after we start our first
eshell session, so we just add it to the =eshell-pred-load-hook=
which is sufficient.
** 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
(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
(defun pwd-replace-home (pwd)
"Replace home in PWD with tilde (~) character."
(interactive)
(let* ((home (expand-file-name (getenv "HOME")))
(home-len (length home)))
(if (and
(>= (length pwd) home-len)
(equal home (substring pwd 0 home-len)))
(concat "~" (substring pwd home-len))
pwd)))
#+END_SRC
Make the directory name be shorter...by replacing all directory
names with just its first names. However, we leave the last two to
be the full names. Why yes, I did steal this.
#+BEGIN_SRC emacs-lisp
(defun pwd-shorten-dirs (pwd)
"Shorten all directory names in PWD except the last two."
(let ((p-lst (split-string pwd "/")))
(if (> (length p-lst) 2)
(concat
(mapconcat (lambda (elm) (if (zerop (length elm)) ""
(substring elm 0 1)))
(butlast p-lst 2)
"/")
"/"
(mapconcat (lambda (elm) elm)
(last p-lst 2)
"/"))
pwd))) ;; Otherwise, we just return the PWD
#+END_SRC
Break up the directory into a "parent" and a "base":
#+BEGIN_SRC emacs-lisp
(defun split-directory-prompt (directory)
(if (string-match-p ".*/.*" directory)
(list (file-name-directory directory) (file-name-base directory))
(list "" directory)))
#+END_SRC
Using virtual environments for certain languages is helpful to know,
especially since I change them based on the directory.
#+BEGIN_SRC emacs-lisp
(defun ruby-prompt ()
"Returns a string (may be empty) based on the current Ruby Virtual Environment."
(let* ((executable "~/.rvm/bin/rvm-prompt")
(command (concat executable "v g")))
(when (file-exists-p executable)
(let* ((results (shell-command-to-string executable))
(cleaned (string-trim results))
(gem (propertize "\xe92b" 'face `(:family "alltheicons"))))
(when (and cleaned (not (equal cleaned "")))
(s-replace "ruby-" gem cleaned))))))
(defun python-prompt ()
"Returns a string (may be empty) based on the current Python
Virtual Environment. Assuming the M-x command: `pyenv-mode-set'
has been called."
(when (fboundp #'pyenv-mode-version)
(let ((venv (pyenv-mode-version)))
(when venv
(concat
(propertize "\xe928" 'face `(:family "alltheicons"))
(pyenv-mode-version))))))
#+END_SRC
Now tie it all together with a prompt function can color each of the
prompts components.
#+BEGIN_SRC emacs-lisp
(defun eshell/eshell-local-prompt-function ()
"A prompt for eshell that works locally (in that is assumes
that it could run certain commands) in order to make a prettier,
more-helpful local prompt."
(interactive)
(let* ((pwd (eshell/pwd))
(directory (split-directory-prompt
(pwd-shorten-dirs
(pwd-replace-home pwd))))
(parent (car directory))
(name (cadr directory))
(branch (curr-dir-git-branch-string pwd))
(ruby (when (not (file-remote-p pwd)) (ruby-prompt)))
(python (when (not (file-remote-p pwd)) (python-prompt)))
(dark-env (eq 'dark (frame-parameter nil 'background-mode)))
(for-bars `(:weight bold))
(for-parent (if dark-env `(:foreground "dark orange") `(:foreground "blue")))
(for-dir (if dark-env `(:foreground "orange" :weight bold)
`(:foreground "blue" :weight bold)))
(for-git `(:foreground "green"))
(for-ruby `(:foreground "red"))
(for-python `(:foreground "#5555FF")))
(concat
(propertize "⟣─ " 'face for-bars)
(propertize parent 'face for-parent)
(propertize name 'face for-dir)
(when branch
(concat (propertize " ── " 'face for-bars)
(propertize branch 'face for-git)))
(when ruby
(concat (propertize " ── " 'face for-bars)
(propertize ruby 'face for-ruby)))
(when python
(concat (propertize " ── " 'face for-bars)
(propertize python 'face for-python)))
(propertize "\n" 'face for-bars)
(propertize (if (= (user-uid) 0) " #" " $") 'face `(:weight ultra-bold))
;; (propertize " └→" 'face (if (= (user-uid) 0) `(:weight ultra-bold :foreground "red") `(:weight ultra-bold)))
(propertize " " 'face `(:weight bold)))))
(setq-default eshell-prompt-function #'eshell/eshell-local-prompt-function)
#+END_SRC
Turn off the default prompt, otherwise, it won't use ours:
#+BEGIN_SRC emacs-lisp
(setq eshell-highlight-prompt nil)
#+END_SRC
Here is the result:
[[http://imgur.com/nkpwII0.png]]
** Tramp
The ability to edit files on remote systems is a wonderful win,
since it means I don't need to have my Emacs environment running on
remote machines (still a possibility, just not a requirement).
According to [[http://www.gnu.org/software/emacs/manual/html_node/tramp/Filename-Syntax.html][the manual]], I can access a file over SSH, via:
#+BEGIN_EXAMPLE
/ssh:10.52.224.67:blah
#+END_EXAMPLE
However, if I set the default method to SSH, I can do this:
#+BEGIN_EXAMPLE
/10.52.224.67:blah
#+END_EXAMPLE
So, let's do it...
#+BEGIN_SRC emacs-lisp
(setq tramp-default-method "ssh")
#+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 display the history in a selectable
buffer.
Also, while =M-p= cycles through the history, =M-P= actually moves
up the history in the buffer (easier than =C-c p= and =C-c n=?):
Since eshell's history often gets confused with blank lines in the
output, we can fix that with a better replacement functions pegged
to the =eshell-prompt-regexp= string:
#+BEGIN_SRC emacs-lisp
(defun eshell-next-prompt (n)
"Move to end of Nth next prompt in the buffer. See `eshell-prompt-regexp'."
(interactive "p")
(re-search-forward eshell-prompt-regexp nil t n)
(when eshell-highlight-prompt
(while (not (get-text-property (line-beginning-position) 'read-only) )
(re-search-forward eshell-prompt-regexp nil t n)))
(eshell-skip-prompt))
(defun eshell-previous-prompt (n)
"Move to end of Nth previous prompt in the buffer. See `eshell-prompt-regexp'."
(interactive "p")
(backward-char)
(eshell-next-prompt (- n)))
(defun eshell-insert-history ()
"Displays the eshell history to select and insert back into your eshell."
(interactive)
(insert (ido-completing-read "Eshell history: "
(delete-dups
(ring-elements eshell-history-ring)))))
(add-hook 'eshell-mode-hook (lambda ()
(define-key eshell-mode-map (kbd "M-S-P") 'eshell-previous-prompt)
(define-key eshell-mode-map (kbd "M-S-N") 'eshell-next-prompt)
(define-key eshell-mode-map (kbd "M-r") 'eshell-insert-history)))
#+END_SRC
** Helpers
Sometimes you just need to change something about the current file
you are editing...like the permissions or even execute it. Hitting =Command-1= will prompt for a shell command string and then append
the current file to it and execute it.
#+BEGIN_SRC emacs-lisp
(defun execute-command-on-file-buffer (cmd)
(interactive "sCommand to execute: ")
(let* ((file-name (buffer-file-name))
(full-cmd (concat cmd " " file-name)))
(shell-command full-cmd)))
(bind-key "A-1" #'execute-command-on-file-buffer)
(defun execute-command-on-file-directory (cmd)
(interactive "sCommand to execute: ")
(let* ((dir-name (file-name-directory (buffer-file-name)))
(full-cmd (concat "cd " dir-name "; " cmd)))
(shell-command full-cmd)))
(bind-key "A-!" #'execute-command-on-file-directory)
(bind-key "s-!" #'execute-command-on-file-directory)
#+END_SRC
Some prompts, shells and terminal programs that display the exit
code as an icon in the fringe. So can the [[http://projects.ryuslash.org/eshell-fringe-status/][eshell-fringe-status]]
project. Seems to me, that if would be useful to rejuggle those
fringe markers so that the marker matched the command entered
(instead of seeing a red mark, and needing to scroll back in order
to wonder what command it was that made it). Still...
#+BEGIN_SRC emacs-lisp
(use-package eshell-fringe-status
:config
(add-hook 'eshell-mode-hook 'eshell-fringe-status-mode))
#+END_SRC