Org Babel でのシェル&Bashコードブロック
Org Babelは、UnixおよびPOSIXシェル(Bash、Zsh、Sh、Fish、Dash、Csh、Ksh、Ash)を幅広くサポートしています。
シェルブロックを使うと、OrgモードがインタラクティブなDevOpsノートブック、ランブック、システム管理コンソールに変わり、ローカル実行、バックグラウンド非同期処理、TRAMP経由のSSHリモートサーバー管理が可能になります。
1. 要件と設定
1.1 Emacsの設定
他のBabel言語のように別個のモジュールを必要とせず、すべてのシェルは単一の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 シェルの指定
ブロック言語タグ(#+BEGIN_SRC <shell>)で使用するシェルのバイナリ名を正確に指定します。
# Bash:
#+BEGIN_SRC bash
echo "Running in $BASH_VERSION"
#+END_SRC
# Zsh:
#+BEGIN_SRC zsh
echo "Running in Zsh $ZSH_VERSION"
#+END_SRC
# POSIX / Dash (高速、軽量実行):
#+BEGIN_SRC dash
echo "Running in dash"
#+END_SRC
# Fish shell:
#+BEGIN_SRC fish
set greeting "Hello from fish"
echo $greeting
#+END_SRC
指定したシェルのバイナリがシステムの $PATH 上でアクセス可能である必要があります。
2. 実行モード: 独立サブシェル、セッション、非同期
2.1 独立サブシェル(デフォルト、:session none)
各ブロックは新しい一時的なサブシェルプロセスで実行されます。環境変数や作業ディレクトリは引き継がれません。
#+BEGIN_SRC bash :results output
echo "PID: $$"
export WORKSPACE="/tmp/build"
#+END_SRC
#+BEGIN_SRC bash :results output
# 新しいサブシェルで実行されるため、$WORKSPACE は空です:
echo "PID: $$, WORKSPACE: $WORKSPACE"
#+END_SRC
2.2 インタラクティブセッション(:session <名前>)
永続的な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 "Deploying to $CLUSTER_ID from $(pwd)"
#+END_SRC
#+RESULTS:
: Deploying to prod-east-01 from /var/log
2.3 ノンブロッキング非同期実行(:async yes)
長時間実行されるスクリプト(バックアップ、コンテナビルド、モデルトレーニング)は同期実行するとEmacsをフリーズさせます。:async yes を追加すると、バックグラウンドで実行され、エディタをロックしません!
ルール:
:asyncには:session <名前>が必須です。- 実行中、Babelは
#+RESULTS:ブロックに一時的なUUIDトークンを挿入します。 - バックグラウンドで実行が完了すると、UUIDは自動的にコマンドの標準出力に置き換えられます。
#+BEGIN_SRC bash :session *bg-job* :async yes :results output
echo "Starting heavy database backup at $(date)..."
sleep 5
echo "Backup finished successfully at $(date)!"
#+END_SRC
#+RESULTS:
: Starting heavy database backup at Sun May 18 14:00:00 UTC 2025...
: Backup finished successfully at Sun May 18 14:00:05 UTC 2025!
セッション内で同期的に実行するには、:async no を指定します。
3. シェル固有のヘッダー引数
Org Babelは、シェル操作に特化した3つのユニークなヘッダー引数を提供します。
3.1 :stdin <要素名>(標準入力)
名前付きOrg要素(テーブル、生テキスト、他のコードブロックの結果)をシェルコマンドの標準入力にパイプします。
# 生テキスト入力:
#+NAME: raw_server_payload
{"node": "alpha", "cores": 8, "status": "active"}
# ペイロードを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 を使用するブロックは、:session を無視して一時的なサブシェルで評価されます。)
3.2 :cmdline <引数>(コマンドライン引数)
位置引数($1、$2、$@)をスクリプト本体に渡します。
#+BEGIN_SRC bash :cmdline "staging" 8080 :results output
ENV="$1"
PORT="$2"
echo "Starting server on port $PORT in $ENV mode..."
#+END_SRC
#+RESULTS:
: Starting server on port 8080 in staging mode...
⚠️ セキュリティ警告: :cmdline の引数はホストシェルによって直接展開されます。コマンドインジェクションを防ぐため、信頼できない文字列やエスケープされていない文字列を渡さないでください。
3.3 :shebang <ディレクティブ>(スタンドアロンスクリプトとクリーンな出力)
ブロックを一時的な実行可能ファイルに書き込み、そのshebangディレクティブを介して実行するように強制します。
#+BEGIN_SRC bash :shebang "#!/usr/bin/env bash" :results output
for i in {1..3}; do
echo "Step $i"
done
#+END_SRC
:shebang が重要な理由:
複数行の :session ブロックでshebangがない場合、行はインタラクティブシェルに1行ずつ送信され、結果に不要なプロンプト文字列(例:bash-5.1$ )が含まれる可能性があります。:shebang を指定するか、セッションなしで実行すると、完全にクリーンな出力が得られます。
4. TRAMP経由のリモートサーバー管理(:dir)
:dir にTRAMPパスを指定することで、リモートサーバー、仮想マシン、Dockerコンテナ上でコマンドを実行できます。
:dir /ssh:<ユーザー>@<ホスト>:<リモートパス>
:dir /docker:<コンテナID>:<パス>
:dir /sudo::/etc
例: リモートシステム管理ワークフロー
# リモートサーバーのディスク使用量とメモリを確認:
#+BEGIN_SRC bash :dir /ssh:[email protected]:/tmp :results output
hostname
uptime
df -h / | awk 'NR==2 {print "Root Disk Usage: " $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
: Root Disk Usage: 48%
リモートセッション
: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)
シェルコマンドがタブ区切りまたは構造化された表形式の出力を生成すると、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 <パス>)
#+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 "Connecting to $host on port $port..."
#+END_SRC
6.2 Bash 1次元配列
Bashは、リスト構文で定義された1次元配列パラメータをサポートしています。
#+BEGIN_SRC bash :results output :var servers='("web-01" "web-02" "db-01")'
echo "Total servers: ${#servers[@]}"
for s in "${servers[@]}"; do
echo "Pinging $s..."
done
#+END_SRC
#+RESULTS:
: Total servers: 3
: Pinging web-01...
: Pinging web-02...
: Pinging db-01...
7. よくある落とし穴と安全のベストプラクティス
| 落とし穴 | 原因 | 解決策 |
|---|---|---|
| EmacsがSSHやffmpegコマンドでハングアップ | コマンドがサブシェルの標準入力を読み取ろうとする | 標準入力を明示的にリダイレクトする(</dev/null)か、ssh -n を使用する。 |
結果に bash-5.1$ などのプロンプト文字が含まれる | :session 内でのインタラクティブな1行ごとのエコー | :shebang #!/usr/bin/env bash を追加するか、セッション外で評価する。 |
| ヒアドキュメント内での意図しない変数展開 | <<EOF を使用するとシェルが $VARS を早期に展開する | 引用符で囲まれたデリミタ <<"EOF" を使用してテキストをリテラルに保ち、早期展開を防ぐ。 |
sh ブロックでPOSIX非互換の構文が失敗する | #+BEGIN_SRC sh は /bin/sh(Debian/Ubuntuではdash)にマッピングされる | スクリプトでbashism([[、((、配列)を使用する場合は #+BEGIN_SRC bash を使用する。 |
:cmdline によるコマンドインジェクション | シェル引数がそのまま評価される | 引数を注意深く引用符で囲むか、代わりに :stdin や :var を使用してデータを渡す。 |
| 非同期ブロックの出力が表示されない | :async yes にはアクティブな :session が必要 | :async yes を使用する場合は常に :session <名前> を指定する。 |
8. 完全なDevOpsランブックの例
#+TITLE: 本番サービスヘルスランブック
#+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 "Initiating rolling cache purge across nodes..."
sleep 4
echo "Cache purge completed successfully."
#+END_SRC