Org中exec的概念

概述:整理近来 Org 中各种 exec 相关的代码。

0 一次旅途

起初,你什么都没有,你手里只有这十来行代码:

#+begin_src emacs-lisp :eval no
;; -*- lexical-binding: t; -*-
(defalias 'org-exec
  (lambda (link)
    (letrec ((url (string-trim link "\\[\\[" "\\]\\]"))
             (hook (lambda ()
                     (org-mode)
                     (org-babel-execute-buffer)
                     (remove-hook 'eww-after-render-hook hook))))
      (add-hook 'eww-after-render-hook hook)
      (eww-browse-url url t)
      (with-timeout (60 (error "Timeout"))
        (while eww-after-render-hook (sit-for 1))))))
#+end_src

这十来行代码能做什么呢?你会好奇。

——这十来行代码能让你执行任意页面中的 Org 代码块。

比如用这十来行代码执行本页面:

#+begin_src emacs-lisp :eval no
(org-exec "[[https://emacs-china.org/t/org-exec/29893]]")
#+end_src

你将得到一个升级版的 org-exec, 一个可定位代码块版的 org-exec:

#+name: 2025-08-02-15-12
#+begin_src emacs-lisp :eval yes :lexical t :results silent
;;; -*- lexical-binding: t; -*-
(defalias 'org-exec
  (let ((timeout 30) (hook (make-symbol "hook"))
        (fetch (make-symbol "fetch"))
        (hash (make-hash-table :test #'equal))
        (rcmd (when (executable-find "curl")
                `("curl" "-s" "--ssl-no-revoke"))))
    (defalias hook
      (lambda ()
        (remove-hook 'eww-after-render-hook hook)))
    (defalias fetch
      (lambda (url)
        (add-hook 'eww-after-render-hook hook)
        (let* ((eww-retrieve-command
                (if rcmd rcmd eww-retrieve-command))
               (st (float-time)))
          (save-window-excursion
            (with-timeout (timeout (error "Timeout"))
              (eww-browse-url url t)
              (while (memq hook eww-after-render-hook)
                (sit-for 1)
                (message "%d" (- (float-time) st))))
            (org-mode)
            (message "done")
            (puthash url (current-buffer) hash)))))
    (lambda (link)
      (let* ((link (string-trim
                    link "\\[\\[" "\\]\\]"))
             (link (with-temp-buffer
                     (org-mode)
	             (insert "file:///" link)
	             (goto-char (point-min))
	             (org-element-link-parser)))
             (url (org-element-property :path link))
             (url (substring url 1))
             (opt (org-element-property
                   :search-option link)))
        (with-current-buffer
            (or (if (buffer-live-p (gethash url hash))
                    (gethash url hash))
                (funcall fetch url))
          (if (null opt) (org-babel-execute-buffer)
            (eval
             `(org-sbe
               ,opt ,(concat
                      ":eval yes :results silent "
                      ":noweb yes :lexical t")))))))))
#+end_src

然后呢?你问。

然后你可以进一步用这个功能更强的版本做这样的事情:

#+begin_src emacs-lisp :eval no
(org-exec "[[https://emacs-china.org/t/topic/29891::2025-08-02-11-33]]")
(org-exec "[[https://emacs-china.org/t/topic/29891::2025-08-02-11-32]]")
#+end_src

你看见区别了吗?这个功能更强的版本可以更精确地选择页面中某个代码块执行,并且它还会以如下方式更改代码块的参数:

#+begin_src emacs-lisp :eval no
(org-sbe
 ,opt ,(concat
        ":eval yes :results silent "
        ":noweb yes :lexical t"))
#+end_src

这意味着:即使页面中的某代码块的执行配置为 :eval no, 这个版本的 org-exec 依旧可以执行它。

sate

你刚才执行了上面两句 exec 了吗?如果没有,一定要执行,它们会帮助你加载 !let, !let*, !def, 这些东西是后续的基础。

现在,你再执行这个:

#+begin_src emacs-lisp :eval no
(org-exec "[[https://emacs-china.org/t/topic/29891::2025-08-02-11-29]]")
#+end_src

执行之后,你的环境里应该有个名为 link-open-conf 的东西,关于它的详情,可以看这里(关于将给定函数裹上一层配置的方法?), 现在我们暂时不管它。

接着(必须按如上的顺序!),你再执行:

#+begin_src emacs-lisp :eval no
(org-exec "[[https://emacs-china.org/t/org-exec/29893::2025-08-02-14-05]]")
#+end_src

此时,你会得到一个算是最终版本的 org-exec, 关于它的一切都在本帖的末尾,这里我们不细究它的细节。

虽说它是最终版本,但此时的 org-exec 失去了执行远程资源中的代码块的能力,也就是说前面所有的 (org-exec http-link) 都变得无法执行,不信你可以试试:

#+begin_src emacs-lisp :eval no
(org-exec "[[https://orgmode.org/manual/Using-Header-Arguments.html::named-block]]")
#+end_src

好在通过刚才加载的 link-open-conf, 你可以用下面的方式重新赋予 org-exec 执行远程的代码块的能力:

#+begin_src emacs-lisp :eval no
(!def (org-exec 'locate) (link-open-conf (symbol-function (org-exec 'locate))))
#+end_src

现在,不妨再试试:

#+begin_src emacs-lisp :eval no
(org-exec "[[https://orgmode.org/manual/Using-Header-Arguments.html::named-block]]")
#+end_src

这会儿,你应该能看到 "data:2" 的输出。

再改改参数试试:

#+begin_src emacs-lisp :eval no
(org-exec
  "[[https://orgmode.org/manual/Using-Header-Arguments.html::named-block]]" nil
  'data 123)
#+end_src

有没有看到 "data:123" 的输出?

好了,到这,我们的旅途就结束了,后续只剩 org-exec 的实现细节。


1 概述

:2025-07-23-14-38:

不同的 Org元素 具有不同的执行器,org-exec 将根
据所指元素的类型,将控制流和 args 参数转交给其他
执行器。

link 可以是一条 Org链接,还可以是用于触发命令的
symbol.

link 是 Org链接 时,支持的类型包括:

string, org-element-link, vector. 即:

(org-exec \"[[id:target]]\" ...)

(org-exec (org-element-link-parser) ...)

(org-exec [[id:target]] ...)

当 link 为 nil 时,执行当前位置的元素。

link 是 symbol 时,执行 org-exec 内部命令。
支持的命令包括:

'set-executor

(org-exec 'set-executor
 ORG-ELEMENT-TYPE FUNCTION)

FUNCTION 将会被 (apply FUNCTION args).

当 executor 为 nil 时, org-exec 会使用内置
的执行器执行 link 所指元素,非 nil 时用
executor 执行。调用方式为:

(apply executor args)

:end:


2 整体结构

#+name: 2025-08-02-13-49
#+begin_src emacs-lisp :eval no :noweb yes
;;; org-exec  -*- lexical-binding: t; -*-
(!def 'org-exec
 (!let* (org-exec
;;;; private
         <<2025-08-02-13-57>>)

;;;; entry
  (!def org-exec
   <<2025-08-02-13-56>>)

;;;; locate
  ;; 根据给定 Org 链接, 返回其指向的位置,以
  ;; marker 形式。
  ;;
  ;; 输入: `link', Org 链接。
  ;; 输出: marker or nil.
  (!def locate
   <<2025-07-26-10-27>>)

;;;; exec-default
  ;; 默认执行器,无合适的执行器时使用。
  (!let ((exec-default
          (make-symbol "exec-default")))
   (!def exec-default
    <<2025-08-02-14-01>>)
   (org-exec 'set-executor 'default exec-default))

;;;; exec-src-block
  ;; 默认 Org Src Block 执行器。
  (!let ((exec-src-block
          (make-symbol "exec-src-block")))
    (!def exec-src-block
     <<2025-08-02-13-59>>)
    (org-exec 'set-executor 'src-block
      exec-src-block))

;;;; end
  (put 'org-exec 'lisp-indent-function 'defun)
  org-exec))
#+end_src

3 入口

#+name: 2025-08-02-13-56
#+begin_src emacs-lisp :eval no
(lambda (&optional link executor &rest args)
  "Org 执行器,执行 `link' 所指元素。"
  (interactive)
  (cond
   ;; 我们也支持 (org-exec [[link]] ...)
   ;; 不过,link 的有效性受限于 elisp reader,
   ;; 使用时需注意。
   ((and (vectorp link)
         (setq link (format "%S" link))
         nil))
   ((or (stringp link)
        (org-element-type-p link '(link))
        (null link))
    (let* ((target (when link (locate link)))
           (link (when (org-element-type-p
                        link '(link))
                   (org-element-property
                    :raw-link link))))

      (when (and link (null target))
	(error "Link %s invalid." link))

      (org-with-point-at target
        (let* ((otype (org-element-type
                       (org-element-at-point)))
               (exec
                (or executor
                    (alist-get otype exec-types)
                    (alist-get
                     'default exec-types)))
               ret)
          (unless exec
            (error
             "No executor found for link %s"
             link))
          (when link
            (message
             "Executing link: %s..." link))
          (run-hooks pre-exec-hook)
          (unwind-protect
              (save-excursion
                (save-window-excursion
                  (setq ret (apply exec args))))
            (run-hooks post-exec-hook))
          ret))))
   ((eq link 'break) (throw 'org-exec-break t))
   ((eq link 'set-executor)
    (setf (alist-get executor exec-types)
          (car args)))
   ((eq link 'pre-exec-hook) pre-exec-hook)
   ((eq link 'post-exec-hook) post-exec-hook)
   ((eq link 'locate) locate)))
#+end_src

4 元素定位

#+name: 2025-07-26-10-27
#+begin_src emacs-lisp :eval no
;; 借 `org-link-open' 定位 `link' 所指。
;;
;; 很遗憾, Org 本身并没有提供编程级别的 API 实现类似的
;; 接口。为了尽可能复用已有代码,我们只能借
;; `org-link-open' 之类的带副作用 (改变 window 或
;; buffer 或 point) 的接口实现。
(lambda (link)
  (let ((inhibit-message t)
        (message-log-max nil)
        (org-link-search-must-match-exact-headline t)
        (org-link-frame-setup
         `((file . find-file-other-window)
    	   ,@org-link-frame-setup))
        (marker (make-marker)))
    (ignore-errors
      (save-window-excursion
        ;; 这个 guard 实际只对当前 buffer 有效,无法处
        ;; 理 open-link 跑到别的 buffer 的情况。但考虑
        ;; 到有时我们会 open 当前 buffer 中的 link, 为
        ;; 了防止因为可见性引发的链接查找失败,我们还
        ;; 是在这里加上这个 gaurd, 当然,也许还有更好
        ;; 的实现方法,但到时再说。
        (org-with-wide-buffer
         (if (not (stringp link))
             (org-link-open link)
           (org-link-open-from-string link))
         (set-marker marker (point)))))))
#+end_src

5 内部变量

#+name: 2025-08-02-13-57
#+begin_src emacs-lisp :eval no
(locate (make-symbol "locate"))
exec-types exec-default
(pre-exec-hook
 (make-symbol "pre-exec-hook"))
(post-exec-hook
 (make-symbol "post-exec-hook"))
#+end_src

6 内置执行器

6.1 默认执行器

#+name: 2025-08-02-14-01
#+begin_src emacs-lisp :eval no
(lambda (&rest args)
  (let ((buf (current-buffer)))
    (message "Executing buffer %S..." buf)
    (org-babel-execute-buffer)
    (message
     "Buffer %S evaluation complete." buf)))
#+end_src

6.2 代码块执行器

#+name: 2025-08-02-13-59
#+begin_src emacs-lisp :eval no
(lambda (&rest args)
  ;; args 应为一对一对的参数, 如果入参长度为奇数
  ;; 我们直接 drop 掉最后一个参数。
  (when (= (% (length args) 2) 1)
    (setq args (seq-subseq args 0 -1)))
  (let* ((params "") header-args variables)
    (mapcar
     (lambda (kv)
       (cond
        ((keywordp (car kv))
         (push (format
                "%S %S" (car kv) (cadr kv))
               header-args))
        ((push (format
                "%S=%S" (car kv)
                (cond
                 ((symbolp (cadr kv))
                  `(identity ',(cadr kv)))
                 (t (cadr kv))))
               variables))))
     (seq-partition args 2))
    (when header-args
      (setq params
            (concat params
                    (string-join
                     (nreverse header-args)
                     " "))))
    (when variables
      (setq params
            (concat params
                    " :var "
                    (string-join
                     (nreverse variables)
                     " "))))
    (setq params (string-trim params))
    (message "[org-exec]params: %S" params)
    (org-babel-execute-src-block
     nil nil
     ;; 因为 `org-babel-execute-src-block' 开头
     ;; 调用 `org-babel-get-src-block-info' 时
     ;; 就已经 eval 了 header args, 所以我们在这里
     ;; 也直接对输入 header args 求值。
     (org-babel-parse-header-arguments params))))
#+end_src

7 构建目标

org-exec 完整代码,用于 eval 或 tangle.

#+name: 2025-08-02-14-05
#+begin_src emacs-lisp :eval no :noweb yes :lexical t :tangle ~/org/org-exec.el
<<2025-08-02-13-49>>
#+end_src
1 个赞