About Archive Tags RSS Feed


Entries posted in March 2024

A simple package for running many linters

3 March 2024 13:00

I used to configure Emacs to run a linter when saving some specific type of files. For example I'd have a perl-utilities package to reformat perl code, and run the perl-linter on saving, then I'd have a hook to do the same thing for Dockerfiles, etc, etc.

It occurred to me recently that I should have a linter for both JSON and YAML files, since I have to edit those filetypes so damn often, and that there wasn't a great solution for those - Until it occurred to me I wrote sysbox which is a simple collection of tools in one binary, and that supports some validation commands:

sysbox validate-json /path/to/file
sysbox validate-yaml /path/to/file
sysbox validate-xml  /path/to/file

With that in mind it became obvious that what I want to do is pretty much always the same:

  • Run an external command, when the file is saved.
    • If the exit-code of that command is "success" (i.e. zero):
      • Do nothing.
    • If the exit-code is "failure" (i.e. non-zero):
      • Show the output.

And this process is the same for ANY of the linters I run. The only thing that changes is the command to run, based on the mode/type of file in question.

That lead to the following configuration:

(defvar save-check-config

        (:mode cperl-mode
         :exec "perl -wc -I. %f"
         :cond (executable-find "perl"))

        (:mode dockerfile-mode
         :exec "hadolint --no-color %f"
         :cond (executable-find "hadolint"))

        (:mode json-mode
         :exec "sysbox validate-json %f"
         :cond (executable-find "sysbox"))

        (:mode nxml-mode
         :exec "sysbox validate-xml %f"
         :cond (executable-find "sysbox"))

        (:mode perl-mode
         :exec "perl -wc -I. %s"
         :cond (executable-find "perl"))

        ;; This avoids creating .pyc files, which would happen if we had
        ;; used the more natural/obvious "python3 -m py_compile %s" approach
        (:mode python-mode
         :exec "python3 -c 'import ast; ast.parse(open(\"%f\").read())'"
         :cond (executable-find "python3"))

        (:mode sh-mode
         :exec "shellcheck %f"
         :cond (executable-find "shellcheck"))

        (:mode terraform-mode
         :exec "tflint --no-color --chdir %d"
         :cond (executable-find "tflint"))

        (:mode yaml-mode
         :exec "sysbox validate-yaml %f"
         :cond (executable-find "sysbox"))

Basically a list of things:

  • We have the mode of files to which the linter/validator applies.
  • We have the command to run
    • %f is changed to the filename which has just been saved.
    • %d is changed to the directory-name containing that file.
  • We add a :cond key to decide if we should run.
    • Which basically is used for "if the binary is found .. run it, otherwise silently do nothing".

I'm quite pleased with how simple the package was to write, and now I have all my linting configuration in one-place.

I'd be tempted to do the same for "format on save", but to be honest with LSP most of the code I care about has that in-place already.

Should I rename to "multi-lint[er].el"? Probably, but I guess we'll see in the future.