Kindness City Blog
12 Mar 2022

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.

Tags: emacs bash awk unix linux

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.

Other posts