Org-mode 如何自动 tangle 一个代码块?

看到这里有个例子,执行第二个 block 的时候,先行自动把第一个 block 导出为文件:

#+NAME: my_hello
#+BEGIN_SRC emacs-lisp :tangle /tmp/hello.el
  (message "Hello")
#+END_SRC

#+BEGIN_SRC sh :var DUMMY=(progn (org-babel-goto-named-src-block "my_hello") (org-babel-tangle '(4))) :results output
  cat /tmp/hello.el
  rm -f /tmp/hello.el
#+END_SRC

#+RESULTS:
: (message "Hello")

有没有更简单(或正常一点)的方法?

如果不限定必须 tangle 指定代码块可以

C-c C-c 之前 tangle 所有 :tangle 参数值不是 no 的代码块

不清楚是不是因为这个需求本身比较怪(少见),好像没有比较正常的解决方法。

Org 代码块有个 :post 参数,可以用来在执行完当前代码块后,再执行另一个代码块:

#+NAME: upcase
#+BEGIN_SRC sh
tr a-z A-Z < /tmp/hello.txt
#+END_SRC

#+BEGIN_SRC sh :post upcase
echo hello world > /tmp/hello.txt
#+END_SRC

#+RESULTS:
: HELLO WORLD

对于这个问题,如果非要用它的话(这个比你的方法还麻烦):

#+NAME: my_hello
#+BEGIN_SRC emacs-lisp :tangle /tmp/hello.el
(message "Hello")
#+END_SRC

#+NAME: cat_hello
#+BEGIN_SRC sh :results output
cat /tmp/hello.el
rm -f /tmp/hello.el
#+END_SRC

#+BEGIN_SRC emacs-lisp :post cat_hello
(save-excursion
  (org-babel-goto-named-src-block "my_hello")
  (org-babel-tangle '(4))) 
#+END_SRC

#+RESULTS:
: (message "Hello")

或许把你的 progn 封装成一个函数

(defun your-org-babel-tangle (src-name)
  (save-excursion
    (org-babel-goto-named-src-block src-name)
    (org-babel-tangle '(4))))

然后再调用,或许看起会更好一点点(?)

#+NAME: my_hello
#+BEGIN_SRC emacs-lisp :tangle /tmp/hello.el
(message "Hello")
#+END_SRC

#+BEGIN_SRC sh :results output :var _=(your-org-babel-tangle "my_hello")
cat /tmp/hello.el
rm -f /tmp/hello.el
#+END_SRC

#+RESULTS:
: (message "Hello")

有时候需要使用到大段的 raw text,但不适合直接放在当前 block 中。

还有一种情况,例如 haskell 这样的语言,代码写在文件中,跟写在 repl 中,是有显著差别的,所以需要:

  • block1 写常规代码,tangle 到文件;
  • block2 写 repl 执行代码:加载 block1 的文件,然后调其中的方法。

所以就想有没有一种比较方便的操作,只需在 block2 上按 C-c C-c,自动 tangle 所需的文件。

看到你这个 :post 我马上去查了一下有没有想对应的参数,可以在执行代码块之前运行做些预处理,可惜并没有找到 :sweat:

:var 里触发 tangle 是很取巧的操作,希望有更常规的做法,哪怕一个更适合放 (progn ...) 这段代码的地方。


顺便吐槽一下 :var n= 这种混搭写法:

#+BEGIN_SRC python :results output :var n=(+ 1 1)
  print(n)
#+END_SRC

#+RESULTS:
: 2

难道不是应该 :var (n (+ 1 1)) 吗?就像 snippet 这样。

要执行的语句不必放在 :var 中的变量后, 在任意以冒号开头名称(如 :name )之后都行, C-c C-c 时 parse 阶段会先执行第一个带括号的语句,后面的语句会忽略。

代码块头部加上以下就行 :pref (your-org-babel-tangle "my_hello"):var (n (+ 1 1))n 也会被当作函数执行,而不是变量。

若想省去写函数名, 可以自定义属性名, 后接要 tangle 的代码块名。 如 :pref my_hello my_hello_2, 然后用 (cdr (assq :pref (nth 2 (org-babel-get-src-block-info)))) 可以获取这个参数值, 但多个参数时会以整个字符串返回,中间的空格未被去掉, 需要自行处理。而当你调用 org-babel-get-src-block-info 时,会多 parse 一次,有副作用时可能会产生奇怪的结果。

这里写了一个简单 parse 代码块头部的函数,输入符号,返回字符串列表:

     (defun org-babel-parse-current-header (arg)
       "Return the values of ARG from current code block."
       (if-let ((src-params (org-element-property :parameters (org-element-context))))
           (if-let ((args (member (symbol-name arg) (split-string src-params))))
               (seq-take-while (lambda (str)
                                 (/= ?: (string-to-char str)))
                               (cdr args)))))

     (org-babel-parse-current-header :pref) ;; => ("my_hello" "my_hello_2")
     (mapc 'your-org-babel-tangle (org-babel-parse-current-header :pref))

然后再写个函数 advice-add :aroundC-c C-corg-babel-execute-src-blockorg-babel-confirm-evaluate , 后者有确认的开关 org-confirm-babel-evaluate 可以设置。

我吐槽的就是 n=(+ 1 1) 这种混搭写法,n= 右边来一串 elisp,给人一种错乱的感觉。如果只是简单赋一个数字 n=1 就更让人分不清到底右边是 elisp,还是底下 block 所使用的语言。

也可能 :var 这个关键字本身带有一定的误导性,如果写成 :let ((n (+ 1 1))),应该不会产生混淆。

既然都自己 perse 头部了,何不干脆 #+TANGLE: my_hello 呢:

#+NAME: my_hello
#+BEGIN_SRC emacs-lisp :tangle /tmp/hello.el
  (message "Hello")
#+END_SRC

#+TANGLE: my_hello
#+BEGIN_SRC sh :results output
  cat /tmp/hello.el
  rm -f /tmp/hello.el
#+END_SRC

这样

#+HEADER: :pref (your-org-babel-tangle "my_hello")
#+BEGIN_SRC sh :results output
  cat /tmp/hello.el
  rm -f /tmp/hello.el
#+END_SRC

或者这样

#+BEGIN_SRC sh :results output :pref (your-org-babel-tangle "my_hello")
  cat /tmp/hello.el
  rm -f /tmp/hello.el
#+END_SRC

注:只会执行第一条语句,后面的忽略。下面这样 func2 和 func4 都不会执行。

#+BEGIN_SRC sh :results output :pref (func1 arg1) (func2 arg2) :blah (func3 arg3) (func4 arg4)
  cat /tmp/hello.el
  rm -f /tmp/hello.el
#+END_SRC