Using Emacs as my Shell
In this reddit post, u/awannaphasch2016 asks what tasks cause folks to
leave emacs. For OP a love of the command-line is one good reason. Of
course the command line is a wonderful place, and lots of emacsers
replied with ways to integrate the command line with their emacs –
often things like vterm
and so on.
I was surprised to find that no-one talked about using emacs itself
instead of a terminal. I've found that for most tasks, I don't need a
terminal window. I use dired for file navigation and manipulation, and
if I want to run some program (for example sudo apt update && sudo
apt upgrade
) I usually use M-&
(which is bound to
async-shell-command
).
One nice thing about M-&
is that I get a dedicated buffer with the
output of my command in it, and I can mess with that buffer in all the
usual emacs ways. This lets me use emacs' powerful editing features to
do sysadminning tasks. Like managing large numbers of docker
containers, for example.
Suppose I've been doing some docker-heavy dev work debugging some CI
scripts, and I realise they're not cleaning up properly after
themselves. I hit M-&
and use it to run docker ps
.
This gives me a buffer called *Async Shell Command*
with output that
looks something like the following:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 50e1414c52f0 ubuntu:latest "/bin/bash" 12 seconds ago Up 10 seconds sharp_kilby 0241e6b27122 ubuntu:latest "/bin/bash" 26 seconds ago Up 24 seconds reverent_nobel 81a3981578fb ubuntu:latest "/bin/bash" 38 seconds ago Up 36 seconds suspicious_montalcini c31212a3c541 ubuntu:latest "/bin/bash" 54 seconds ago Up 49 seconds beautiful_rubin ...etc
I can search and scroll through this output in the usual emacs way,
and I can edit it. I know I want to stop most of these containers, so
I'm going to use this buffer as a list of things to stop. Let's say I
want to keep the container 50e1414c52f0
running for some reason: so
I remove that line from the buffer in the usual way with C-k
:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0241e6b27122 ubuntu:latest "/bin/bash" 26 seconds ago Up 24 seconds reverent_nobel 81a3981578fb ubuntu:latest "/bin/bash" 38 seconds ago Up 36 seconds suspicious_montalcini c31212a3c541 ubuntu:latest "/bin/bash" 54 seconds ago Up 49 seconds beautiful_rubin
Now I position my cursor at the beginning of line 2, and hit the following keys:
C-x ( ;; kmacro-start-macro C-SPC ;; set-mark-command M-f ;; forward-word M-w ;; kill-ring-save C-a ;; move-beginning-of-line C-n ;; next-line M-! ;; shell-command d ;; self-insert-command o ;; self-insert-command c ;; self-insert-command k ;; self-insert-command e ;; self-insert-command r ;; self-insert-command SPC ;; self-insert-command s ;; self-insert-command t ;; self-insert-command o ;; self-insert-command p ;; self-insert-command SPC ;; self-insert-command C-y ;; yank <return> ;; exit-minibuffer C-x ) ;; kmacro-end-macro
(Incidentally, that description of what I just did was helpfully
generated by C-h l
)
With these keypresses, I've defined a keyboard macro which copies the
ID of the current container to my kill ring, moves the cursor to the
next container, and runs a shell command docker stop $THAT_ID_FROM_MY_KILL_RING
.
Now my macro is defined, I can run it whatever the appropriate number
of times is. I can run the macro, say, 4 times, by hitting C-x e e e e
, or by hitting C-4 C-x e
. If I have a lot of containers to
get through, I can hit C-0 C-x e
, which will run the macro over and
over again until we run out of containers to stop.
Of course there's a similar workflow possible at the terminal. You
could discover what containers are running with docker ps
, then stop
them all with something like:
for id in $(docker ps | awk -e '/[a-z0-9]+/ {print $1}') ; do docker stop $id ; done
For me, the advantage of keeping the workflow in emacs is that it feels easier to incrementally build. Both the emacs flow and the bash one can be saved for re-use later. In the case of emacs I can save the macro, while in bash I can save the one-liner in a script. The advantage of doing it in bash is that if I want to share my workflow, non-emacs users are likely to be happier with a bash script than an elisp one.
All of the functionality I've talked about here works out of the box
with no packages or configuration. It also works just as well on
remote machines as local ones, thanks to the power of TRAMP. You can
try for yourself with emacs -Q
.
This post was originally a reddit comment here.
There's no comments mechanism in this blog (yet?), but I welcome emails and tweets. If you choose to email me, you'll have to remove the .com from the end of my email address by hand.
You can also follow this blog with RSS.