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:

1
2
3
4
5
6
7
8
$ align-equals
  foo = 1
  a = 2
  asdf = 3
  <Ctrl-D>
  foo  = 1
  a    = 2
  asdf = 3

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.

1
2
3
(defun align-equals (b e) 
  (interactive "r")
  (shell-command-on-region b e "align-equals" t t))

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: ~ 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.

1
2
3
(defun align-buffer ()
  (interactive)
  (shell-command-on-region (point-min) (point-max) "align-equals" t t))

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.

1
2
3
4
(defun run-eslint ()
  (interactive)
  (let ((current-file (buffer-name (current-buffer))))
    (shell-command (concat "eslint " current-file))))

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.

1
    (shell-command (concat "eslint --fix" current-file))))

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.

1
2
3
  (add-hook 'js-mode-hook
            (lambda ()
              (add-hook 'before-save-hook 'run-eslint nil t)))

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.