概述:整理近来 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