CLI Commands in Emacs
Contents
A few months ago, Gabriel Gonzalez wrote an excellent article on creating useful tools with Haskell. He used an example of a small CLI tool that aligns the equals signs of a multi-line text input. The article wrapped up by integrating this tool into vim.
I love the overarching concept in the article: create small tools that are useful in multiple contexts. It's directly inline with the Unix philosophy.
The only problem is that I am an Emacs user. So in this article, I wanted to show how I get the same effect in Emacs. And demonstrating how to take CLI interaction in the text editor even further.
Take Action on a Region
In his article, Gabriel uses the command he created to take action on a region. This involves selecting text and executing a CLI command on it's contents – replacing the region with the command's output.
For this example, we will use the command from Gabrial's article,
align-equals
. To see the full implementation and learn a little Haskell,
you can read his article. Or you can follow along while keeping the below
command's usage in mind:
|
|
Emacs has a built in command, shell-command-on-region
, which executes the
specified command on the selected region. This would work for our example, but
it requires two commands. First you execute shell-command-on-region
, then you
provide the command you want to run. If the cli command needs flags or
arguments, it becomes tedious to put in every time. Wouldn't it be nice if we
could select a region and only run one command?
To accomplish this, we need a wrapper. This implementation combines a couple
elisp components. We start our function by expecting the beginning and ending of
the region as arguments. The region(r
) flag passed to interactive
ensure
that the correct arguments get passed in.
interactive
lets us invoke our function from anywhere, and supports many more
options, which I encourage you to explore on your own.
With our region bounds ready, we call shell-command-on-region
to
executes a shell command on a region specified by the beginning(b
) and
ending(e
) arguments. We also supply the shell command to execute, and two
flags which make the command replace the selected region.
|
|
We can expand the CLI command in this wrapper to take any number or arguments
and flags. No matter how we change it, we can still invoke it with one command:
<M-x> align-equals
.
Take Action on a Buffer
A common scenario I run into, is wanting to run a tool on the whole file. This normally comes in two variants. I either want to modify the content of the file in some way, like using a beautifier. Or I want to produce an output based on the file content, like getting totals or showing lint errors.
Again, there are built in ways to do this, but a wrapper lets us do more with our shell commands.
Replacing Buffer Contents
This time, we don't need to pass any arguments to interactive
. Instead, we use
shell-command-on-region
with the results of calling point-min
and
point-max
– the start and end of the buffer, respectively.
|
|
align-buffer
aligns the entire buffer without the need to manually select a
region. This makes the it less error prone and easier to execute. We can't
select the region incorrectly, because we don't need to anymore. We only need to
execute <M-x> align-buffer
.
Displaying The Output of a Shell Command
For this examples, let's use eslint
, a popular tool for linting JavaScript
code.
eslint
is different from our previous command. It takes a file name, not the
buffer contents. This means we will need to use some new functions.
Once again, we don't accept any arguments and pass nothing to interactive
. We
get the buffer name by using the buffer-name
function. Once we have the name,
we merge it with the string eslint
to create the final command, and pass it to
shell-command
. This logs the output to a dedicate shell command buffer.
|
|
Now we can execute run-eslint
in any JavaScript file and see the results of
executing the command.
Seeing the results is a good start. But eslint
comes with a handy --fix
flag
that automatically fixes simple problems.
To add this flag you only need to change the string you pass to shell-command
.
|
|
With that change, the simple issues get fixed automatically and we get a log of the complex issues to fix manually.
Running Command Automatically
From the last example we have a useful run-eslint
function. But we need to
remember to run it every time we want to check a file. Let's reduce our mental
burden, and let Emacs automatically execute this function every time we save a
JavaScript file.
There are two facilities that makes automatic function execution precise and safe: modes and hooks.
Modes allow us to know what context we are in. When we open a new file, a number
of modes can activate. For our example, there is a built in js-mode
that
activates when we open a JavaScript file.
We will hook into the activation of this mode to limit the scope of our automatic function execution to only JavaScript files. We wouldn't want to execute a command that changes file contents in an unsupported file type.
Once we are inside js-mode
, we will add another hook – this time to an
action. Since eslint
can fix some issues for us, we will run it before the
file is saved by hooking onto the before-save
action.
In the implementation, we use add-hook
to listen to the two action described
above. js-mode-hook
only needs the function to execute when the mode is
activated. But before-save-hook
needs the LOCAL
option. This only runs the
action in the buffer it was activated in. Without this flag, eslint
would run
on every file after we opened any JavaScript file.
|
|
Going Further
I only scratched the surface of what's possible with Elisp. Projects like magit provide amazing examples of extending basic CLI tools.
Magit is a wrapper around git. It doesn't change what git does. Instead, it adds on text manipulation and file awareness that Emacs is good at. If you need inspiration for how to integrate other tools into Emacs, look no further.
I hope that this article provided some inspiration and a few new tricks. Happy hacking.