Previously I introduced the reader to ShellCheck. In this post I detail how I use Flycheck in Emacs and offer an Emacs function to automatically suppress Shellcheck errors at the current line.

I’m an avid Emacs user and it follows that I’ve set up editor customization to exude the most from ShellCheck. If you, dear reader, are not an Emacs user, I cannot help you! Please, for the love of shell scripts, ensure ShellCheck works within your preferred text editor, lest you wish to ship edgecased buggy scripts!

§How to use Shellcheck in Emacs

First, ensure you have shellcheck installed. Check Repology for a list of distros and OSes that package Shellcheck. On Debian/Ubuntu try apt install shellcheck. Verify that shellcheck works by invoking shellcheck --version.

$ shellcheck --version || echo 'No shellcheck found :('
ShellCheck - shell script analysis tool
version: 0.10.0
license: GNU General Public License, version 3
website: https://www.shellcheck.net

Next — this is the only mandatory Emacs step — install flycheck. The simplest way is to Type M-x package-install RET flycheck RET followed by M-x global-flycheck-mode RET, though I personally employ use-package to declare my package usage up front. Here is what I have checked into my Emacs configuration git repository:

(use-package flycheck
  :ensure t
  :init
  (global-flycheck-mode 1))

After global-flycheck-mode is enabled, you’ll see errors underlined in prog-mode derived buffers. Try C-c ! l to list all errors in another buffer. Or C-c ! n or C-c ! p to go to next or previous error. See M-x describe-command RET flycheck-mode RET and its complimentary Flycheck documentation website for further learnings.

If you want to learn more about other keys in the C-c ! prefix from within Emacs, check out which-key for Emacs or read the flycheck.el sources (or try M-x describe-variable RET flycheck-mode-map RET from within Emacs).

§But, ShellCheck likes to complain

Like all powerful tools, ShellCheck has its tradeoffs. It can save loads of time avoiding bugs in scripts later when already placed in production. On the other hand, ShellCheck can slow down development because one has to fix every single trifle to quell ShellCheck. A savvy shell scripter will find themself encountering Shellcheck errors that are intentional. Consider this Bash function that decrypts a pass managed login credential (source):

decrypt() {
    local file
    file="${PASSWORD_STORE_DIR}/${1}.gpg"
    if [[ ! -f $file ]]; then
        echo "No such entry \"${1}\"" >&2
        return 1
    fi
    # shellcheck disable=SC2086
    gpg $PASSWORD_STORE_GPG_OPTS --quiet --decrypt "$file"
}

Without the # shellcheck disable=... ignore directive, which itself is a comment (a line that begins with # [POSIX]​), ShellCheck unyieldingly complains about the unquoted use of $PASSWORD_STORE_GPG_OPTS.

Time and time again, I’ve forgotten the exact syntax of this ShellCheck directive. Moreover, resultant of my forgetfulness, I’ve devised a solution to automatically generate these comment lines in Emacs! Simply move the point to the line with a ShellCheck error (try C-c ! n or C-c ! p to find the next or previous error) then type C-c ! k to generate the necessary comment to shut up ShellCheck.

I’ve embedded the Elisp here for easy copy-paste and to discuss the anatomy of the code:

(require 'cl)                           ; for cl-loop
(require 'sh-script)                    ; For sh-mode-map

(defun winny--extract-shellcheck-error (err)
  (and-let* (((eq (flycheck-error-checker err) 'sh-shellcheck)))
    (flycheck-error-id err)))
(defun winny/shellcheck-disable-at-line ()
  "Insert \"# shellcheck disable=SC...\" line to silence shellcheck errors."
  (interactive)
  (save-match-data
    (save-excursion
      (and-let* ((errs
                  (cl-loop for err in (flycheck-overlay-errors-in (pos-bol) (pos-eol))
                           if (winny--extract-shellcheck-error err)
                           collect (winny--extract-shellcheck-error err))))
        (beginning-of-line)
        (insert (format "# shellcheck disable=%s"
                        (mapconcat 'identity errs ",")))
        (indent-according-to-mode)
        (newline-and-indent)))))
(add-hook 'sh-mode-hook
          (defun winny--bind-shellcheck-disable ()
            (define-key sh-mode-map (kbd "C-c ! k") 'winny/shellcheck-disable-at-line)))

As a sort of preamble, this code ensures cl-loop and sh-mode-map variables are in scope using the two require forms.

Then, this code defines a helper function winny--extract-shellcheck-error to determine if a flycheck-error object is a shellcheck error and return the SC prefixed identifier string. Next comes the interactive command winny/shellcheck-disable-at-line which searches for unaddressed ShellCheck errors on the current line then inserts a comment directive above the current line in order to silence those ShellCheck errors.

Finally, the add-hook function call adds a function named winny--bind-shellcheck-disable to execute whenever sh-mode is used. It has one job: bind winny/shellcheck-disable-at-line to a key.

Notice the usage of a defun instead of a lambda for add-hook’s second argument. I believe it prudent to name your anonymous functions such that the M-x describe-variable (C-h v) output is less busy and the code itself is accessible by name for further monkeying about with M-x eval-expression (M-:) and friends.

§Conclusion

Equipped with this post, I hope the reader has ShellCheck set up in Emacs. If Emacs isn’t your prefererred editor, worry not, there is surely a blogpost or official documentation describing how to set up ShellCheck for your favorite editor!

Stay tuned for a post on how to set up ShellCheck with pre-commit to ensure code quality with team-based projects.