emacsでjqをJSONファイルのformatterとして使う

emacsでjqをJSONファイルのformatterとして使う。方法は2つ。

  • shell-command-on-regionを使う
  • formatter専用の関数を作る

shell-command-on-regionを使う

M-|shell-command-on-regionという関数がbindされている。これに引数を与えてあげると、現在選択されているリージョンを上書きしてくれる。

shell-command-on-region の定義は以下の様になっている(引数の部分のみ)。

(defun shell-command-on-region (start end command
                      &optional output-buffer replace
                      error-buffer display-error-buffer
                      region-noncontiguous-p)

  (interactive (let (string)
         (unless (mark)
           (user-error "The mark is not set now, so there is no region"))
         ;; Do this before calling region-beginning
         ;; and region-end, in case subprocess output
         ;; relocates them while we are in the minibuffer.
         (setq string (read-shell-command "Shell command on region: "))
         ;; call-interactively recognizes region-beginning and
         ;; region-end specially, leaving them in the history.
         (list (region-beginning) (region-end)
               string
               current-prefix-arg
               current-prefix-arg
               shell-command-default-error-buffer
               t
               (region-noncontiguous-p))))
...
)

ここでreplaceの引数にnon-nilな値を与えてあげれば良い。例えば以下の様な形で使う。

C-u - 1 M-|
# minibufferで jq . -S

formatter専用の関数を作る

shell-command-on-region を使うのも便利なんだけれど。呼び出しが頻繁になってきた場合に以下の様な点で困る。

  • エラーのときにもエラーメッセージで置換されてしまう
  • そもそもjqとコマンドを打つのがめんどくさい

例えば、javascript-modeを拡張したjson-modeを自分で作ってあげて、拡張子が.jsonのファイルを開いた場合には、この自作のjson-modeで開くようにする。 そして、この自作したmodeのkey-mapにテキトウなformmatter専用の関数を割り当てる。

(require 'derived)
(define-derived-mode my:json-mode javascript-mode "json mode")

(defun my:json-mode-setup ()
  ;; 個人的にはバッファの保存は自動で行っているので`C-x C-s`に割り当ててしまっている
  (define-key my:json-mode-map (kbd "C-x C-s") 'my:jsonfmt)
  )

(add-to-list 'auto-mode-alist '("\\.json$" . my:json-mode))
(add-hook 'my:json-mode-hook 'my:json-mode-setup)

ここで、my:jsonfmtが自作したformatter関数。定義は以下の様な感じ。

(defun my:jsonfmt (beg end)
  (interactive "r")
  (unless (region-active-p)
    (setq beg (point-min))
    (setq end (point-max))
    )
  (my:execute-formatter-command "jq" "jq . -S -e" beg end))

(defun my:get-fresh-buffer-create (name)
  (let ((buf (get-buffer-create name)))
    (with-current-buffer buf
      (setf (buffer-string) ""))
    buf
    ))

(defun my:execute-formatter-command (cmd-name cmd beg end)
  (cond ((executable-find cmd-name)
         (save-excursion
           (save-restriction
             (narrow-to-region beg end)
             (let ((buf (my:get-fresh-buffer-create (format "*%s*" cmd-name)))
                   (err-buf (my:get-fresh-buffer-create (format "*%s error*" cmd-name))))
               (let ((status
                      ;; xxx
                      (flet ((display-message-or-buffer (&rest args) nil))
                        (shell-command-on-region (point-min) (point-max) cmd buf nil err-buf)
                        )))
                 (cond ((= 0 status)
                        (let ((replaced (with-current-buffer buf (buffer-string))))
                          (cond ((string= replaced "")
                                 (message "succeeded with no output"))
                                (t
                                 (delete-region (point-min) (point-max))
                                 (insert replaced)))))
                       (t (message (with-current-buffer err-buf (buffer-string))))))))))
        (t (message (format "%s is not found" cmd-name)))))

(メッセージを抑制するためにfletを使っているのだけれど。お行儀が良くない)