使用 Org Babel 進行文藝程式設計與程式碼纏出 (Tangling)
Org Babel 是 Donald Knuth 文藝程式設計 (Literate Programming) 概念的實作。與其撰寫帶有散落註解的程式碼,您應該撰寫以人為導向的散文、架構筆記和設計原理,並將功能性的程式碼區塊嵌入其中。
一個單一的 Org 檔案作為兩個不同產出的最終真實來源:
- 織出 (Weaving/人類文件): 將筆記本匯出為格式化的 HTML、LaTeX/PDF 或 Markdown 供讀者閱讀。
- 纏出 (Tangling/機器執行): 萃取出純淨、可執行的原始碼檔案 (
.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 文件同步。