Emacs Lisp でプリントデバッグをする (6)

こちらの記事より新しい記事があります：

https://qiita.com/javacommons/items/8fb62f9052df31582168

----

最新版の xprint.el は以下のGitHubリポジトリからダウンロードできます。

> [https://github.com/emacs-pkg/xprint/releases/latest](https://github.com/emacs-pkg/xprint/releases/latest)

# インストール方法 (xprint.el)

とりあえず使ってみたい(または最新の変更に追随したい)方は、.emacs または init.el に以下を追加しておくと自動的にロードされます。.emacs 内で xprint 等を呼びたい場合は、呼び出す前の時点で以下のコードを記述しておいてください。

```elisp:~/.emacs or ~/.emacs.d/init.el
(unless (featurep 'get-feature)
  (defun get-feature (feature-name &optional url file-name)
    (if (featurep feature-name) t
      (unless url (setq url (format "https://github.com/emacs-pkg/%s/raw/main/%s.el"
                                    feature-name feature-name)))
      (unless file-name (setq file-name (format "%s.el" feature-name)))
      (let ((make-backup-files nil)
            (file-path (expand-file-name file-name user-emacs-directory)))
        (ignore-errors
          (url-copy-file url file-path 'ok-if-already-exists))
        (ignore-errors
          (load file-path nil 'nomessage))
        (featurep feature-name))))
  (get-feature 'get-feature))

(get-feature 'xprint)
```

straight.el でロードしたい場合は以下を .emacs または init.el に入れてください(但し git のインストールが必要です)：

```elisp:~/.emacs or ~/.emacs.d/init.el
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 6))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(straight-use-package '(xprint :type git :host github :repo "emacs-pkg/xprint"))
(require 'xprint)
```

----

#### ※ Emacs のアプリ(マクロ)は「コンソールアプリかウィンドウアプリか」といえばウィンドウアプリに近いです。プリント文が使えないので、簡単な(内部変数の表示などを含む)動作確認が行えないからです。
#### ※ xprint関数およびxdumpマクロは、Emacsのバッファに対する操作等を行うEmacs Lispコード(マクロ)の任意の位置から呼び出せて、マクロの処理を一切邪魔することがありませんので、インタラクティブな操作を行うプログラムの開発・デバッグに向いています。また、xmessageマクロは、message関数を少し便利に拡張しています。
#### ※ xprint/xdump/xmessage は、Emacs のバッチモード(`--batch`)もしくは、スクリプトモード(`--script`)でも使えます。インタラクティブモード(通常の起動モード)で利用する際と記述方法は同じでかまいません。

この前以下の記事を書きましたが

> [Emacs Lisp でプリントデバッグをする (5) - Qiita](https://qiita.com/javacommons/items/50b2ac5e84143308c2cc)

ソースを微修正しましたので上げなおしました。今回からソース(xprint.el)にバージョン番号と最終更新日時を表記することにしました。最新版かどうかを容易に判別できるようにしました。

----

# 関数/マクロの説明

## xprint/xdump

xprint, xdump が呼び出されると自動的に `*xprint*` バッファが表示されてそこにデバッグしたい値や変数を表示させることができます。
xprint は評価後の値のみ表示されるのに対して、xdump は「変数の名前 := 値」という風に表示されます。

```
(xprint 1 (+ 2 3)) ==> 1 5

(progn
  (setq a 123 b "abc xyz")
  (xprint "label-1" a b (list a b))
  (xdump "label-2" a b (list a b))
  )
⇓
"label-1" 123 "abc xyz" (123 "abc xyz")
"label-2" a := 123 b := "abc xyz" (list a b) := (123 "abc xyz")
```

以下に xprint と xdump の出力の違いをデモした際のスクリーンショットを示します：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140610/0869ca77-c8b9-effe-204a-520880eba7bb.png)

## xclear

```
(xclear) ==> *xprint* バッファをクリアします。
```

xclear 関数はプログラムの任意の位置から呼び出せます。interactive指定付きなので、M-x xclear でも呼び出せます。global-set-key してもよいでしょう。
(Windowsの場合は、Alt-x x c ENTER で呼び出せます)

## xsleep

指定したミリ秒間実行を停止(スリープ)します。(また強制的にEmacsの画面再描画を促します。)
また引数にnilを渡した場合には何も行いません。(xsleep文自体が無効になります)

`1.` Emacs は処理するGUIイベントが空になるまで画面の再描画をしませんので、xprint/xdump を呼んですぐに(リアルタイムで)表示させたい場合には、

```
(xprint 123)
(xsleep 0)
```

のように xsleep を呼ぶ必要があります。

`2.` 表示がながれてしまうので、ディレイを入れたい場合にもxsleepは使えます。(指定がミリ秒ということに留意ください)

```elisp:xsleepサンプル
(progn
  (xclear)
  (dotimes (i 10)
    (xprint i)
    (xsleep 2000)
    )
  )
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140610/b9075f71-8af0-4eb7-c0d8-9f1b0749e722.png)

## xmessage

```
;; ミニバッファにいきなり「i=49」とだけ表示される
(dotimes (i 50)
  (message "i=%d" i))


;; xmessageマクロも第一引数に整数(スリープするミリ秒)を指定しないと、やはりミニバッファにいきなり「i=49」とだけ表示される
(dotimes (i 50)
  (xmessage "i=%d" i))

;; 「i=0」から「i=49」まで、200ミリ秒(0.2秒)ずつ再表示およびスリープしながらカウントアップが見れる(第一引数に整数定数を指定)
(dotimes (i 50)
  (xmessage 200 "i=%d" i))
```

### xsleep を開発・デバッグに使うときのシナリオ

```elisp:my-sleep-test.el
(defcustom *my-sleep* nil "スリープする時間")

(defun my-test ()
  (interactive)
  (dotimes (i 50)
    (xmessage *my-sleep* "i=%d" i)))
```

`*my-sleep*` という変数を定義することでデバッグしたい時だけディレイをかけることができます：

* defcustom でスリープを制御する変数 `*my-sleep*` を nil として定義(第三引数のコメントも必須)
* xmessage の第一引数に `*my-sleep*` を指定しておく
* M-x my-test でマクロを実行 ⇒ ミニバッファにいきなり「i=49」とだけ表示される
* M-x set-variable を実行し、「`*my-sleep*`」 と入力後「200」を入力。
* 再度 M-x my-test でマクロを実行 ⇒ ミニバッファのメッセージがディレイ付きでカウントアップするのが確認できる
* デバッグが終わったら M-x set-variable を実行し、「`*my-sleep*`」 と入力後「nil」を入力。⇒ディレイがなくなる

# ソース添付

```elisp:xprint.el
;;;; xprint.el v1.2.1                ;;;;
;;;; Last Modified: 2023/01/02 00:00 ;;;;

(require 'cl-lib)
(require 'cl-extra)

(defun xprint (&rest args)
  (let ((raw nil))
    (when (eq (car args) :raw)
      (setq raw t)
      (setq args (cdr args))
      )
    (prog1 args
      (let ((msg ""))
        (dotimes (i (length args))
          (if (zerop i) nil
            (setq msg (concat msg " "))
            )
          (setq msg (concat msg (format (if raw "%s" "%S") (nth i args))))
          )
        (if noninteractive (message "%s" msg)
          (let ((cb (current-buffer))
                (cw (selected-window)))
            (if (equal (buffer-name) "*xprint*") nil
              (switch-to-buffer-other-window "*xprint*")
              )
            ;;(emacs-lisp-mode)
            (lisp-interaction-mode)
            (goto-char (point-max))
            (insert msg)
            (insert "\n")
            (let ((wins (window-list)))
              (dolist (win wins)
                (select-window win)
                (when (equal (buffer-name) "*xprint*")
                  (goto-char (point-max))
                  (cond
                   ((pos-visible-in-window-p (point)) nil)
                   ((< (point) (window-start)) (recenter 0))
                   (t (recenter -1)))
                  )
                )
              )
            (select-window cw)
            (switch-to-buffer cb)
            )
          )
        )
      )
    )
  )

(defmacro xdump (&rest list)
  (let ((exp '(xprint)))
    (dolist (x list)
      (if (and (not (consp x)) (not (and (symbolp x) (not (keywordp x))))) (push x exp)
        (push (list 'quote x) exp)
        (push := exp)
        (push x exp)
        )
      )
    (reverse exp)
    )
  )

(defun xclear ()
  (interactive)
  (let ((cb (current-buffer))
        (cw (selected-window)))
    (let ((wins (window-list)))
      (dolist (win wins)
        (select-window win)
        (when (equal (buffer-name) "*xprint*")
          (ignore-errors (delete-window win))
          )
        )
      )
    (ignore-errors (kill-buffer "*xprint*"))
    (ignore-errors (select-window cw))
    nil)
  )

(defun xsleep (millisec)
  (when millisec
    (sit-for 0)
    (sleep-for 0 millisec)
    )
  )

(defmacro xmessage (&rest list)
  (let ((sleep nil))
    (if (and (not (integerp (nth 0 list))) (not (symbolp (nth 0 list)))) nil
      (setq sleep (pop list))
      )
    (if (not sleep)
        `(message ,@list)
      `(progn (message ,@list) (xsleep ,sleep))
      )
    )
  )

(defmacro xformat (&rest list)
  `(xprint :raw (format ,@list))
  )

(defun xpp (x)
  (xprint :raw (xpp-to-string x))
  )

(defun xpp-to-string (form)
  (with-temp-buffer
    (cl-prettyprint form)
    (let ((str (buffer-string)))
      (replace-regexp-in-string "\\`[ \t\n\r]*\\|[ \t\n\r]*\\'" "" str)
      )
    )
  )

(defun xpand-macro-scan (form callback data)
  (cond
   ((symbolp form) (funcall callback form data))
   ((consp form)
    (cons
     (xpand-macro-scan (car form) callback data)
     (xpand-macro-scan (cdr form) callback data)))
   (t form)
   )
  )

(defun xpand-macro (form)
  (let ((result (macroexpand-all form))
        (hash (make-hash-table :test #'equal)))
    (xprint :raw "")
    (xprint :raw ";;; Expanding Macro:")
    (xprint
     :raw
     (xpp-to-string form)
     )
    (xprint :raw "    |")
    (xprint :raw "    |")
    (xprint :raw "    v")
    (xpand-macro-scan
     result
     #'(lambda (sym data)
         (let ((lst (gethash (symbol-name sym) data)))
           (when (not (member sym lst))
             (push sym lst)
             (puthash (symbol-name sym) lst data)
             )
           )
         )
     hash
     )
    (setq result
          (xpand-macro-scan
           result
           #'(lambda (sym data)
               (let ((lst (gethash (symbol-name sym) data)))
                 (if (= 1 (length lst)) sym
                   (intern (format "%s_%d" sym (length (member sym lst))))
                   )
                 )
               )
           hash
           )
          )
    (xprint
     :raw
     (xpp-to-string result)
     )
    result
    )
  )

(defmacro xpand (form)
  `(xpand-macro (quote ,form))
  )

(provide 'xprint)
```
----

何かお気づきの点がございましたら、コメント等で教えていただけると助かります。