My shell configuration in Emacs
My default shell is bash
run with the M-x shell
in Emacs. I’ve found it extremely useful and elegant, and after some trial-and-error quite usable as well. In this post I’ll present my configuration in detail.
Background
I used to be one of those people who wondered why on earth anyone in their right mind would want to run shell under Emacs. Then one post in Reddit changed my perspective. The post is specific to eshell
, but many concepts apply to any shell under Emacs, for example the bash
I’m using.
I realized that what I’m actually doing with shell quite often is this: I run programs that produce textual output, and then I use all sorts of pagers and filtering, and try to search for results I’m after, and then often copy the output to Emacs. All that could be done by Emacs itself, in much more efficient and consistent manner. So why not just run the shell in Emacs buffer, and let the output flow to the buffer, which I can then search and manipulate freely with the Emacs?
I started with ansi-term
which is quite impressive in its own right, but soon found myself switching back and forth in line
-and char
-mode. I did some experiments with shell
, but I was still depending on quite many curses
programs that simply broke without proper terminal emulation. Since then my setup has been settled down to using shell
as my main shell, and then I have a copy of real non-emacs terminal around for few things that are not working in the shell
, namely couple of long-running tasks inside screen
sessions. I also still use ansi-term
for ssh
, maybe more on that in some later post.
What is also really great with the shell integration is the fact that bash
is under total control of Emacs.
As a practical example: one day I was editing a set of Beamer
slides, but for a certain reason Emacs was not able to export to PDF directly. So I exported to latex, and then jumped to another window where I had a shell, and typed in my latex->pdf command, and then jumped back. On another frame I had a PDF-viewer that reflected the change. I found myself doing this for a few times, and thought it’s time to define a keyboard macro:
<F3> Start macro
C-c C-e Export
l b Latex/Beamer
C-x o Jump to other window
M-p <enter> Execute previous command
C-x o Jump back to my document
<F4> Stop recording
After this, I could just generate the slides with a single button <F4>
.
I know this is silly example because I should have fixed the PDF export, but still I think it represents the benefits of shell integration. It’s hard to think how to automate this sort of thing if the shell is run by window manager as external program, like gnome-terminal
.
Now let’s dive in to the actual configurations.
bash-completion
One of the first problems in running the shell
was its lack of Unix program’s argument completion. Simple example: if I don’t remember what was the name of some argument in some specific program, let’s say openssl
, in the normal shell I could just hit the tab
to find out some candidates. Shell under Emacs didn’t have that sort of knowledge by default, but this was solved with the bash-completion
package in ELPA. I have this in my .emacs
:
(require 'bash-completion)
(bash-completion-setup)
Now I can just hit the tab
as I would in any other terminal, and I get the candidates I can then select with helm
.
bash configuration
I have this in my .bashrc
to deal with some curses
-based application that my come out of my muscle memory:
vi ()
{
if [ $TERM = "dumb" ]; then
emacsclient "$@";
else
/usr/bin/vi "$@"
fi
}
git ()
{
if [ $TERM = "dumb" ]; then
emacsclient -e "(magit-status)";
else
/usr/bin/git "$@"
fi
}
man ()
{
if [ $TERM = "dumb" ]; then
emacsclient -e "(man \"$1\")";
else
/bin/man "$@"
fi
}
top ()
{
if [ $TERM = "dumb" ]; then
emacsclient -e "(helm-top)";
else
/usr/bin/top "$@"
fi
}
less ()
{
if [ $TERM = "dumb" ]; then
emacs-pager "$@"
else
/bin/less "$@"
fi
}
ssh ()
{
if [ $TERM = "dumb" ]; then
echo "Makes no sense to run ssh in emacs shell!"
else
/usr/bin/ssh "$@"
fi
}
emacs-pager
is a simple shell script:
#!/bin/bash
INPUT="-"
if [ $# -ge 1 ]; then
INPUT="$1"
fi
t=$(mktemp) || exit 1
cat $INPUT >> $t
emacsclient "$t"
rm -f -- $t
I use it sometimes when I’m expecting some really long output, or when I want the output verbatim, not affected by the “long lines” problem, and its solution.
Some Emacs utilities are useful on any terminal:
alias magit='emacsclient -e "(magit-status)"'
alias dired='emacsclient -e "(dired \"`pwd`\")"'
Problematic long lines
Emacs comint
is not really good in dealing with longs lines in the shell output. Try cat
for example some huge ass minified JavaScript… To my knowledge this problem has no perfect solution, but as a work-around I use this snippet in my .emacs
:
;; Shorten lines in comint mode
(defun my-comint-shorten-long-lines (text)
(let* ((regexp "\\(.\\{300\\}[;,:\({ ]\\)")
(shortened-text (replace-regexp-in-string regexp "\\1\n" text)))
(if (string= shortened-text text)
text
shortened-text)))
(add-hook 'comint-preoutput-filter-functions 'my-comint-shorten-long-lines)
Original source here. I just modified the regexp
to cover some extra characters and allow longer lines.
Finally, running the shell
I always have several shells open, in multiple Emacs frames. I’ve found very handy to define this global shortcut in .emacs
:
;; Go to shell, or:
;; - If this buffer is already shell, spawn new
;; - If there's no shell, create one
(defun goto-shell ()
(interactive)
(if (string= (buffer-name) "*shell*")
(shell (generate-new-buffer-name "*shell*"))
(if (get-buffer "*shell*")
(switch-to-buffer "*shell*")
(shell))))
;; Bind to key
(global-set-key (kbd "<f2>") 'goto-shell)
With this, I can always hit <F2>
and get my shell, or get a new shell, if necessary.
If I want to go to a specific shell (instead of the first), I use helm
and filter out shell instances *shell
.