While there are packages that format on save, I'd like to be able to run a custom command that auto-formats a file on save.

Upon saving it runs an external process that:

  • Takes the buffer as stdin.
  • Outputs to a temporary file.

After that:

  • The output is used to replace the current buffer which is then saved.

  • Any errors from the stderr are reported as errors.

  • Any output from the stdout is printed as messages.

For the purpose of testing, this could be the auto-formatting command:

It converts the text to title-caps.

python -c "with open(__import__('sys').argv[-1], 'w') as fh: fh.write(__import__('sys').stdin.read().title())" -- /tmp/TEMP_FILE_FROM_EMACS.txt

... where /tmp/TEMP_FILE_FROM_EMACS.txt is a generated temp file name.

I realize this may be an involved answer. If this seems like too much hassle to answer, I'll investigate and post an answer myself, since I think it's useful to have a general function to handle this.


This can be done using:

  • call-process-region to run the command and catch the stdout/stderr.
  • replace-buffer-contents to update the region without causing the entire buffer to be replaced.

    This is important to avoid this to be seen as one very large undo-step which risks loosing your undo history for example.

This code used clang-format package as a reference.

(defun mycustom-fmt-buffer ()
  (let ((this-buffer (current-buffer))
        (my-command "python")
        (temp-buffer (generate-new-buffer " *mycustom-fmt*"))
        ;; Use for format output or stderr in the case of failure.
        (temp-file (make-temp-file "mycustom-fmt" nil ".el"))
        ;; Always use 'utf-8-unix' & ignore the buffer coding system.
        (default-process-coding-system '(utf-8-unix . utf-8-unix)))
    (condition-case err
            (let ((status
                      #'call-process-region nil nil my-command nil
                      ;; stdout is a temp buffer, stderr is file.
                      `(,temp-buffer ,temp-file) nil
                      ;; arguments.
                       "with open(__import__('sys').argv[-1], 'w') as fh: fh.write(__import__('sys').stdin.read().title())"
                     (unless (zerop (cadr (insert-file-contents
                       (insert ": "))
                      (point-min) (point-max)))))
               ((stringp status)
                (error "(mycustom-fmt killed by signal %s%s)"
                       status stderr))
               ((not (zerop status))
                (error "(mycustom-fmt failed with code %d%s)"
                       status stderr))
                ;; Include the stdout as a message,
                ;; useful to check on how the program runs.
                (let ((stdout
                       (with-current-buffer temp-buffer
                          (point-min) (point-max)))))

                  (unless (string-equal stdout "")
                    (message "%s" stdout)))))
              ;; Load the temp file into a temp buffer
              ;; & replace this-buffers contents.
                (insert-file-contents temp-file)
                (let ((temp-buffer (current-buffer)))
                  (with-current-buffer this-buffer
                    (replace-buffer-contents temp-buffer))))))
      ;; Show error as message, so we can clean-up below.
      (error (message "%s" (error-message-string err))))

    ;; Cleanup.
    (delete-file temp-file)
    (when (buffer-name temp-buffer)
      (kill-buffer temp-buffer))))

(defun mycustom-fmt-save-hook-for-this-buffer ()
  (add-hook 'before-save-hook
            (lambda ()
                ;; Continue to save.
            ;; Buffer local hook.

;; Example for elisp, could be any mode though.
(add-hook 'emacs-lisp-mode-hook
          (lambda () (mycustom-fmt-save-hook-for-this-buffer)))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.