Org Babel의 셸 및 Bash 코드 블록
Org Babel은 Unix 및 POSIX 셸(Bash, Zsh, Sh, Fish, Dash, Csh, Ksh, Ash) 전반에 걸쳐 포괄적인 셸 스크립팅을 지원합니다.
셸 블록은 Org Mode를 로컬 실행, 백그라운드 비동기 처리, TRAMP를 통한 SSH 기반 원격 서버 관리가 가능한 대화형 DevOps 노트북, runbook, 시스템 관리 콘솔로 변환합니다.
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 셸:
#+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 <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 "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 <name>이 필수입니다.- 실행 중에 Babel은 임시 UUID 토큰을
#+RESULTS:블록에 삽입합니다. - 백그라운드 실행이 완료되면 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.1 :stdin <요소-이름> (표준 입력)
Org 요소(테이블, 원시 텍스트 또는 다른 코드 블록 결과)를 셸 명령의 stdin으로 파이프합니다:
# 원시 텍스트 입력:
#+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이 중요한 이유:
shebang이 없는 여러 줄 :session 블록에서는 줄이 대화형 셸로 한 줄씩 전송되어 결과에 불필요한 프롬프트 문자열(예: bash-5.1$ )이 나타날 수 있습니다. :shebang을 지정하거나 세션 없이 실행하면 완전히 깔끔한 출력이 생성됩니다.
4. TRAMP를 통한 원격 서버 관리 (:dir)
:dir에 TRAMP 경로를 지정하여 원격 서버, 가상 머신 또는 Docker 컨테이너에서 명령을 실행할 수 있습니다:
:dir /ssh:<사용자>@<호스트>:<원격-경로>
:dir /docker:<컨테이너-id>:<경로>
:dir /sudo::/etc
예: 원격 Sysadmin 워크플로우
# 원격 서버의 디스크 사용량 및 메모리 확인:
#+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 1D 배열
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 명령에서 멈춤 | 명령이 서브셸에서 stdin을 읽으려고 시도 | stdin을 명시적으로 리디렉션(</dev/null)하거나 ssh -n 사용 |
결과에 bash-5.1$ 프롬프트 문자가 나타남 | :session에서 대화형 줄 단위 에코 | :shebang #!/usr/bin/env bash 추가 또는 세션 외부에서 평가 |
| heredoc에서 의도하지 않은 변수 확장 | <<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 <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 "Initiating rolling cache purge across nodes..."
sleep 4
echo "Cache purge completed successfully."
#+END_SRC