Org Babel 中的 Shell 與 Bash 程式碼區塊
Org Babel 提供對 Unix 與 POSIX shell 的全面支援,包括:Bash、Zsh、Sh、Fish、Dash、Csh、Ksh 與 Ash。
Shell 區塊將 Org Mode 轉變為一個互動式的 DevOps 筆記本、操作手冊與系統管理主控台,能夠執行本機指令、背景非同步處理,並透過 TRAMP 經由 SSH 進行遠端伺服器管理。
1. 需求與設定
1.1 Emacs 設定
與需要獨立模組的其他 Babel 語言不同,所有 shell 都由同一個 Babel 函式庫(ob-shell)管理。您只需在 org-babel-load-languages 中加入 (shell . t):
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(shell . t)))
(注意:在 Org 8.2 之前,此項目的名稱為 sh。在所有現代 Org 版本中,請使用 shell。)
1.2 指定 Shell
在區塊語言標籤中指定確切的 shell 二進位檔名稱(#+BEGIN_SRC <shell>):
# Bash:
#+BEGIN_SRC bash
echo "目前執行於 $BASH_VERSION"
#+END_SRC
# Zsh:
#+BEGIN_SRC zsh
echo "目前執行於 Zsh $ZSH_VERSION"
#+END_SRC
# POSIX / Dash(更快速、輕量的執行):
#+BEGIN_SRC dash
echo "目前執行於 dash"
#+END_SRC
# Fish shell:
#+BEGIN_SRC fish
set greeting "來自 fish 的問候"
echo $greeting
#+END_SRC
指定的 shell 二進位檔必須存在於系統的 $PATH 環境變數中。
2. 執行模式:隔離、互動式與非同步
2.1 隔離子 Shell(預設,:session none)
每個區塊都在一個全新的暫時子 shell 行程中執行。環境變數和工作目錄不會延續:
#+BEGIN_SRC bash :results output
echo "PID: $$"
export WORKSPACE="/tmp/build"
#+END_SRC
#+BEGIN_SRC bash :results output
# 此處 $WORKSPACE 為空,因為這在一個新的子 shell 行程中執行:
echo "PID: $$, WORKSPACE: $WORKSPACE"
#+END_SRC
2.2 互動式 Session(:session <name>)
在 Emacs 的 comint 緩衝區(例如 *dev-session*)內執行。變數、函數與工作目錄會跨多個區塊持續存在:
#+BEGIN_SRC bash :session *dev-session* :results output
export CLUSTER_ID="prod-east-01"
cd /var/log
#+END_SRC
#+BEGIN_SRC bash :session *dev-session* :results output
# 使用前一個區塊設定的狀態:
echo "正在從 $(pwd) 部署至 $CLUSTER_ID"
#+END_SRC
#+RESULTS:
: 正在從 /var/log 部署至 prod-east-01
2.3 非阻塞非同步執行(:async yes)
長時間執行的腳本(備份、容器建置、模型訓練)在同步執行時會凍結 Emacs。加入 :async yes 即可在背景執行,而不鎖定編輯器!
規則:
:async必須搭配:session <name>。- 執行期間,Babel 會在
#+RESULTS:區塊中插入一個暫時的 UUID 記號。 - 背景執行完成後,UUID 會自動被命令的標準輸出取代。
#+BEGIN_SRC bash :session *bg-job* :async yes :results output
echo "開始資料庫重度備份,時間:$(date)..."
sleep 5
echo "備份成功完成,時間:$(date)!"
#+END_SRC
#+RESULTS:
: 開始資料庫重度備份,時間:Sun May 18 14:00:00 UTC 2025...
: 備份成功完成,時間:Sun May 18 14:00:05 UTC 2025!
若要在 session 中同步執行,請傳入 :async no。
3. Shell 專用的 Header 引數
Org Babel 提供三個特別針對 shell 操作設計的專屬 header 引數:
3.1 :stdin <element-name>(標準輸入)
將指定的 Org 元素(表格、純文字或其他程式碼區塊的結果)透過管線傳遞給 shell 命令的標準輸入:
# 純文字輸入:
#+NAME: raw_server_payload
{"node": "alpha", "cores": 8, "status": "active"}
# 將 payload 直接導入 jq:
#+BEGIN_SRC bash :stdin raw_server_payload :results output
jq -r '.node + " has " + (.cores|tostring) + " cores"'
#+END_SRC
#+RESULTS:
: alpha has 8 cores
(注意:使用 :stdin 的區塊會在暫時子 shell 中求值,並忽略 :session。)
3.2 :cmdline <args>(命令列引數)
將位置引數($1、$2、$@)傳遞給腳本主體:
#+BEGIN_SRC bash :cmdline "staging" 8080 :results output
ENV="$1"
PORT="$2"
echo "正在以 $ENV 模式於連接埠 $PORT 啟動伺服器..."
#+END_SRC
#+RESULTS:
: 正在以 staging 模式於連接埠 8080 啟動伺服器...
⚠️ 安全性警告: :cmdline 引數會由宿主 shell 直接展開。請勿傳遞未受信任或未跳脫的字串,以避免命令注入。
3.3 :shebang <directive>(獨立腳本與乾淨輸出)
強制將區塊寫入一個暫時的可執行檔,並透過其 shebang 指令執行:
#+BEGIN_SRC bash :shebang "#!/usr/bin/env bash" :results output
for i in {1..3}; do
echo "步驟 $i"
done
#+END_SRC
為什麼 :shebang 很重要:
在沒有 shebang 的多行 :session 區塊中,各行會逐行發送到互動式 shell,這可能會在結果中產生多餘的提示字串(例如 bash-5.1$ )。指定 :shebang 或在不使用 session 的情況下執行會產生完全乾淨的輸出。
4. 透過 TRAMP 進行遠端伺服器管理(:dir)
您可以透過在 :dir 中指定 TRAMP 路徑,在遠端伺服器、虛擬機器或 Docker 容器上執行命令:
:dir /ssh:<user>@<host>:<remote-path>
:dir /docker:<container-id>:<path>
:dir /sudo::/etc
範例:遠端系統管理工作流程
# 檢查遠端伺服器的磁碟使用量與記憶體:
#+BEGIN_SRC bash :dir /ssh:[email protected]:/tmp :results output
hostname
uptime
df -h / | awk 'NR==2 {print "根磁碟使用率:" $5}'
#+END_SRC
#+RESULTS:
: bastion.example.com
: 14:22:05 up 42 days, 3:18, 1 user, load average: 0.12, 0.08, 0.05
: 根磁碟使用率:48%
遠端 Session
將 :dir 與 :session 結合使用,可以在多個區塊之間維持活躍的 SSH 連線,而不必在每個步驟重新連線:
#+BEGIN_SRC bash :dir /ssh:[email protected]:/home/admin :session *remote-bastion* :results output
WORKDIR=$(mktemp -d)
cd "$WORKDIR"
pwd
#+END_SRC
5. 結果格式與表格資料擷取
5.1 定位點分隔輸出轉換為 Org 表格(:results output table)
當 shell 命令產生定位點分隔或結構化的表格輸出時,Babel 會將其解析為原生的 Org 表格:
#+BEGIN_SRC bash :results output table
printf "PID\tUser\tCommand\n"
printf "101\troot\tsystemd\n"
printf "405\tpostgres\tpostgres\n"
printf "892\tnginx\tnginx: worker\n"
#+END_SRC
#+RESULTS:
| PID | User | Command |
|-----+----------+---------------|
| 101 | root | systemd |
| 405 | postgres | postgres |
| 892 | nginx | nginx: worker |
5.2 將輸出導向檔案連結(:results file :file <path>)
#+BEGIN_SRC bash :results file :file reports/sysinfo.txt
uname -a > reports/sysinfo.txt
cat /etc/os-release >> reports/sysinfo.txt
#+END_SRC
#+RESULTS:
[[file:reports/sysinfo.txt]]
6. 傳遞變數與 Bash 陣列(:var)
6.1 純量與字串
#+BEGIN_SRC bash :var port=8000 host="localhost" :results output
echo "正在連線至 $host 的連接埠 $port ..."
#+END_SRC
6.2 Bash 一維陣列
Bash 支援使用列表語法定義一維陣列參數:
#+BEGIN_SRC bash :results output :var servers='("web-01" "web-02" "db-01")'
echo "伺服器總數:${#servers[@]}"
for s in "${servers[@]}"; do
echo "正在 ping $s ..."
done
#+END_SRC
#+RESULTS:
: 伺服器總數:3
: 正在 ping web-01...
: 正在 ping web-02...
: 正在 ping db-01...
7. 常見陷阱與安全最佳實務
| 陷阱 | 原因 | 解決方案 |
|---|---|---|
| Emacs 在執行 SSH 或 ffmpeg 命令時凍結 | 命令嘗試從子 shell 讀取標準輸入 | 明確重新導向標準輸入(</dev/null)或使用 ssh -n。 |
結果中出現多餘的 bash-5.1$ 提示字串 | :session 中的互動式逐行回顯 | 加入 :shebang #!/usr/bin/env bash 或在 session 外部求值。 |
| Here-document 中的變數被意外展開 | 使用 <<EOF 允許 shell 過早展開 $VARS | 使用加引號的定界符 <<"EOF" 以保持文字字面值並防止過早展開。 |
sh 區塊中的非 POSIX 語法失敗 | #+BEGIN_SRC sh 對應至 /bin/sh(在 Debian/Ubuntu 上通常是 dash) | 如果腳本使用 bash 特有語法([[、((、陣列),請改用 #+BEGIN_SRC bash。 |
透過 :cmdline 發生命令注入 | Shell 引數被逐字求值 | 謹慎地為引數加上引號,或改用 :stdin 或 :var 傳遞資料。 |
| 非同步區塊的輸出沒有出現 | :async yes 需要一個活躍的 :session | 使用 :async yes 時,務必提供一個 :session <name>。 |
8. 完整 DevOps Runbook 範例
#+TITLE: 生產服務健康狀態 Runbook
#+PROPERTY: header-args:bash :session *runbook* :results output
* 步驟 1:健康檢查設定
#+NAME: cluster_nodes
| Hostname | Role |
|-----------------+---------|
| prod-app-01.lan | backend |
| prod-app-02.lan | backend |
| prod-db-01.lan | storage |
* 步驟 2:探測叢集節點
#+BEGIN_SRC bash :var nodes=cluster_nodes :colnames yes :results output table
printf "Host\tRole\tStatus\n"
for row in "${nodes[@]}"; do
# 解析以定位點/空格分隔的行資料:
host=$(echo "$row" | awk '{print $1}')
role=$(echo "$row" | awk '{print $2}')
# 快速 ping 探測:
if ping -c 1 -W 1 "$host" > /dev/null 2>&1; then
status="ONLINE"
else
status="OFFLINE"
fi
printf "%s\t%s\t%s\n" "$host" "$role" "$status"
done
#+END_SRC
#+RESULTS:
| Host | Role | Status |
|-----------------+---------+--------|
| prod-app-01.lan | backend | ONLINE |
| prod-app-02.lan | backend | ONLINE |
| prod-db-01.lan | storage | ONLINE |
* 步驟 3:非同步執行服務維護
#+BEGIN_SRC bash :session *runbook* :async yes
echo "正在跨節點啟動滾動式快取清除..."
sleep 4
echo "快取清除成功完成。"
#+END_SRC