使用 Org Babel 进行文学编程与代码缠绕(Tangling)
Org Babel 是 Donald Knuth 的文学编程范式的一种实现。它不是编写带有零散注释的代码,而是编写面向人类叙述的散文、架构笔记和设计原理,并将功能性代码块嵌入其中。
一个单一的 Org 文件作为两个不同输出的最终真相来源:
- 编织(面向人类文档): 将笔记本导出为格式化的 HTML、LaTeX/PDF 或 Markdown,供读者阅读。
- 缠绕(面向机器执行): 提取纯净、可运行的源代码文件(
.py、.sh、.c、.conf、.el),用于编译器、解释器和部署系统。
1. 缠绕基础::tangle 头参数
默认情况下,代码块不会被缠绕。:tangle 系列头参数指定代码块提取到磁盘的位置和方式。
1.1 :tangle 参数的值
| 值 | 行为 |
|---|---|
:tangle no(默认) | 块不会写入任何文件。(但仍可通过 <<noweb>> 包含在其他块内部)。 |
:tangle "path/to/file.py" | 将块主体直接写入指定的文件路径(相对于缓冲区目录或绝对路径)。 |
:tangle yes | 缠绕到与 Org 文件基本名匹配且带有语言标准扩展名的文件(例如,model.org -> model.py)。 |
1.2 其他缠绕头参数
:shebang "#!/usr/bin/env python3": 将指定的 shebang 字符串作为缠绕文件的第一行前置。:tangle-mode (identity #o755): 设置生成文件上的文件系统权限(例如,#o755使脚本可执行)。:no-expand: 阻止变量赋值(:var)内联到缠绕代码中;仅写入原始块主体。:mkdirp yes: 自动创建缠绕路径中任何不存在的父目录。
2. 缠绕作用域:块级、子树级和缓冲区级
缠绕参数可以在整个项目中清晰地设置作用域:
2.1 子树级缠绕(推荐模式)
不同的标题可以缠绕到不同的文件或模块:
* 核心工具
:PROPERTIES:
:header-args:python: :tangle src/utils.py :mkdirp yes
:END:
#+BEGIN_SRC python
def slugify(text: str) -> str:
return "-".join(text.lower().split())
#+END_SRC
* 数据处理流水线
:PROPERTIES:
:header-args:python: :tangle src/pipeline.py :mkdirp yes
:END:
#+BEGIN_SRC python
from utils import slugify
def process_record(rec: dict) -> dict:
rec["slug"] = slugify(rec["name"])
return rec
#+END_SRC
2.2 缓冲区级缠绕默认值
在文件顶部设置:
#+PROPERTY: header-args:python :tangle src/main.py
3. Noweb 语法:模块化代码组装
Noweb 使您能够从更小、模块化、易于解释的片段中组装更大的源文件。您可以在另一个代码块中使用 <<block_name>> 引用一个代码块。
3.1 启用 Noweb 展开
Noweb 默认是禁用的。使用 :noweb 头参数控制它:
:noweb 值 | 在 Emacs 中求值? | 缠绕到磁盘? | 在文档中导出? |
|---|---|---|---|
no(默认) | 否 | 否 | 否 |
yes | 是 | 是 | 是 |
tangle | 否 | 是 | 否 |
strip-tangle | 是 | 否(引用被剥离) | 是 |
no-export | 是 | 是 | 否 |
eval | 是 | 否 | 否 |
3.2 Noweb 示例:文学化脚本构建
#+NAME: imports
#+BEGIN_SRC python :exports none
import sys
import argparse
from pathlib import Path
#+END_SRC
#+NAME: argument-parser
#+BEGIN_SRC python :exports code
parser = argparse.ArgumentParser(description="处理数据集。")
parser.add_argument("--input", required=True, type=Path, help="输入 CSV 路径")
parser.add_argument("--threshold", default=0.5, type=float, help="分数阈值")
args = parser.parse_args()
#+END_SRC
#+NAME: process-data
#+BEGIN_SRC python :exports code
print(f"从 {args.input} 加载数据...")
# 此处为处理逻辑
#+END_SRC
# 组装最终的可执行脚本:
#+NAME: main-cli
#+BEGIN_SRC python :tangle bin/run_pipeline.py :shebang "#!/usr/bin/env python3" :tangle-mode (identity #o755) :noweb yes :exports none
<<imports>>
def main():
<<argument-parser>>
<<process-data>>
if __name__ == "__main__":
main()
#+END_SRC
当缠绕时,bin/run_pipeline.py 被写成一个干净、可执行的脚本,所有片段都正确放置并缩进!
3.3 Noweb 前缀与缩进
- Noweb 尊重缩进:如果
<<argument-parser>>在def main():内部缩进了 4 个空格,则展开块的每一行都将缩进 4 个空格。 - 要禁用前缀行为:
:noweb-prefix no。 - 要插入块执行的结果而不是其源代码,请附加括号:
<<compute_constant()>>
3.4 调试展开
要在缠绕或运行之前验证 noweb 块将如何展开:
- 将光标放在块内,按
C-c C-v v(org-babel-expand-src-block)。 - Org 会打开一个临时缓冲区,显示所有 noweb 和
:var替换后的确切代码。
4. 将配置和数据内联到缠绕文件中
Babel 可以将 Org 表格或标题属性中的变量直接内联到缠绕代码中:
# 存储在 Org 表格中的配置:
#+NAME: server_config
| Key | Value |
|---------+------------------|
| host | 10.0.1.25 |
| port | 8080 |
| timeout | 30 |
# 缠绕后的配置模块:
#+BEGIN_SRC python :tangle config.py :var cfg=server_config :colnames yes
HOST = cfg[0][1]
PORT = int(cfg[1][1])
TIMEOUT = int(cfg[2][1])
#+END_SRC
要防止 Babel 在缠绕代码中生成变量包装器,请传递 :no-expand。
5. 缩进处理:对空格敏感的语言
在像 Python 或 YAML 这样对空格敏感的语言中,自动的 Org 重新缩进可能会破坏语法。
Python 缠绕的关键规则:
将 org-src-preserve-indentation 设置为 t。当为 t 时,Org 在导出或缠绕期间不会删除或添加缩进:
;; 在 Emacs 配置中:
(setq org-src-preserve-indentation t)
或者每个块使用开关 -i:
#+BEGIN_SRC python -i :tangle script.py
6. 如何执行缠绕
6.1 在 Emacs 内交互式操作
| 快捷键 | 命令 | 描述 |
|---|---|---|
C-c C-v t / C-c C-v C-t | org-babel-tangle | 缠绕当前缓冲区中的所有代码块。 |
C-c C-v f / C-c C-v C-f | org-babel-tangle-file | 提示选择一个磁盘上的 Org 文件并缠绕它。 |
6.2 从缠绕文件导航回 Org
当缠绕时,Babel 会在生成的文件中插入反向链接注释:
# [[file:~/project/notebook.org::*Core Utilities][core-slugify]]
def slugify(text: str) -> str:
...
# core-slugify ends here
在 Emacs 中,打开生成的文件并运行 M-x org-babel-tangle-jump-to-org 可以立即跳转回源 .org 文档中的确切标题和块!
6.3 无头命令行 / CI / Make 批量缠绕
无需打开 Emacs GUI 窗口即可缠绕文件(非常适合构建脚本、Makefile 和持续集成):
# 缠绕单个文件:
emacs -Q --batch \
--eval "(require 'org)" \
--eval "(require 'ob-tangle)" \
--eval '(org-babel-tangle-file "analysis.org")'
# 从 shell 循环或 Makefile 缠绕多个文件:
emacs -Q --batch --eval "
(progn
(require 'org)
(require 'ob-tangle)
(dolist (file command-line-args-left)
(with-current-buffer (find-file-noselect file)
(org-babel-tangle))))" *.org
7. 文学化配置管理:init.org 模式
Org Babel 文学编程的一个流行用例是管理 Emacs 配置或系统 dotfiles。
在 ~/.emacs.d/init.el 中:
;; 引导 Org Babel 文学化配置
(require 'org)
(require 'ob-tangle)
(org-babel-load-file (expand-file-name "config.org" user-emacs-directory))
org-babel-load-file 自动检查 config.org 自上次生成 config.el 以来是否已被修改,如果需要则缠绕新版本,并加载生成的 .el 文件。
8. 缠绕项目总结清单
- 显式缠绕路径: 使用显式文件名(例如
:tangle "src/core.py")。 - 使用 Noweb 提高可读性: 将复杂算法分解为逻辑步骤(
<<init-weights>>、<<forward-pass>>、<<backprop>>),以便叙述自然地呈现在读者面前。 - 保留缩进: 确保对于 Python 代码,
org-src-preserve-indentation始终为t。 - 使用 Make/CI 自动化: 在构建系统中包含无头
emacs --batch缠绕步骤,以使生成的源代码与 Org 文档保持同步。