3편에서 텍스트를 읽고 바꾸고 골라내는 도구를 다뤘다면, 이 글은 그 도구들로 견고한 스크립트를 짜는 실전 패턴을 모은다. 임시 파일을 안전하게 다루는 법부터 Job Control, 에러 핸들링, 자주 쓰는 명령, 흔한 함정, 이식성, 그리고 실전 템플릿까지 본다.
1. 임시 파일과 파일 안전하게 수정하기
스크립트가 파일을 수정할 때는 원본을 곧장 덮어쓰기 전에 임시 파일을 거치는 게 안전하다. 임시 파일을 만드는 mktemp부터, 그것으로 파일에서 특정 줄을 지우는 실전 패턴까지 묶어 본다. (sed·awk 자체는 3편에서 다뤘다.)
mktemp — 안전한 임시 파일
임시 파일 이름을 tmp.txt처럼 직접 짓는 건 위험하다. 이미 있으면 덮어쓰고, 여러 프로세스가 같은 이름을 동시에 쓰면 충돌하며, 예측 가능한 이름은 심볼릭 링크 공격 같은 보안 취약점이 된다. mktemp는 겹치지 않는 임시 파일을 원자적으로 만들고 그 경로를 출력한다.
tmp=$(mktemp) # /tmp/tmp.Ab3kZ9 같은 파일 생성, 경로를 반환echo "작업..." > "$tmp"rm -f "$tmp"
생성과 동시에 trap으로 정리를 걸어두는 게 관용구다. (trap은 섹션 4에서 자세히 다룬다.)
tmp=$(mktemp)trap 'rm -f "$tmp"' EXIT # 스크립트가 어떻게 끝나든 삭제
XXXXXX 템플릿 — 이름 끝의 연속된 X(최소 6개)를 mktemp가 무작위 문자로 바꿔 유일한 이름을 만든다. 템플릿을 직접 주면 용도를 알아보기 쉬운 접두사를 붙일 수 있다.
원본을 건드리지 않고 새 파일에 결과를 만든 뒤, 성공했을 때만(&&) 교체한다. sed/awk가 없어도 어떤 필터(grep, cut…)와도 조합된다. 바로 위에서 본 mktemp로 임시 파일을 만든다.
방법 2 — sed -i (in-place 편집)
sed -i '3d' file.txt # 3번째 줄 삭제 (GNU)sed -i '/^#/d' file.txt # 주석 줄 삭제sed -i '/^$/d' file.txt # 빈 줄 삭제sed -i '2,5d' file.txt # 2~5번째 줄 삭제# macOS(BSD sed)는 -i 뒤에 백업 접미사가 필수. 백업을 안 만들려면 빈 문자열:sed -i '' '3d' file.txt
sed가 내부적으로 임시 파일을 만들어 교체해 준다. 한 줄로 끝나 가장 간편하지만 GNU와 BSD의 -i 문법이 달라 이식성에 주의해야 한다.
방법 3 — awk로 거르기
# 3번째 줄만 빼고 출력 → 임시 파일로 교체awk 'NR != 3' file.txt > "$tmp" && mv "$tmp" file.txt# 조건으로 거르기: 첫 필드가 8080인 줄 삭제awk '$1 != 8080' lease.txt > "$tmp" && mv "$tmp" lease.txt
awk는 in-place 옵션이 없어 임시 파일 패턴(방법 1)과 결합한다. 대신 NR(줄 번호)·필드 조건 같은 복잡한 삭제 기준을 표현하기 좋다.
방법
한 줄
이식성
복잡한 조건
임시 파일 + 필터
△
◎
필터에 의존
sed -i
◎
△ (GNU/BSD 차이)
정규식·줄 범위
awk + 임시 파일
△
◎
◎ (필드·NR)
2. Job Control
백그라운드 실행
# & 로 백그라운드 실행long_task &echo "PID: $!" # 백그라운드 프로세스의 PID# 여러 작업 병렬 실행 후 대기task1 &task2 &task3 &wait # 모든 백그라운드 작업 완료 대기echo "All done"# 특정 PID만 대기pid1=$!wait "$pid1"
jobs, fg, bg
인터랙티브 쉘에서 사용하는 명령들이다.
jobs # 백그라운드 작업 목록fg %1 # 1번 작업을 포그라운드로bg %1 # 정지된 1번 작업을 백그라운드로 재개kill %1 # 1번 작업 종료
스크립트에서의 병렬 실행
스크립트에서는 jobs/fg/bg보다 &와 wait를 조합하는 것이 일반적이다.
pids=()for url in "${urls[@]}"; do curl -sO "$url" & pids+=($!)donefor pid in "${pids[@]}"; do wait "$pid" || echo "Failed: $pid"done
3. exec, 동적 FD, flock
지금까지는 명령 하나하나에 리다이렉션을 붙였다(cmd > file). exec를 쓰면 스크립트 전체의 입출력을 한 번에 돌리거나, 파일 디스크립터(FD)를 직접 열고 닫을 수 있다. 그 위에 flock을 얹으면 “이 스크립트가 동시에 두 번 돌지 못하게” 막는다. (FD 기초 — 0/1/2, 2>&1 — 는 2편에서 다뤘다.)
exec — 두 가지 얼굴
exec는 뒤에 무엇이 오느냐에 따라 완전히 다르게 동작한다.
(1) 명령을 주면: 현재 프로세스를 그 명령으로 교체
exec python app.py # 쉘이 python으로 "변신". 이 줄 다음은 실행되지 않는다echo "여기는 안 온다"
새 프로세스를 자식으로 띄우는 게 아니라 현재 쉘 자체를 덮어쓴다. 래퍼 스크립트가 환경만 세팅하고 본체로 넘길 때(exec "$@") 쓴다. 프로세스가 하나 줄고 PID·시그널이 그대로 이어져, 컨테이너 진입점(entrypoint)에서 특히 흔하다.
(2) 명령 없이 리다이렉션만 주면: 스크립트 전체에 영구 적용
exec > run.log 2>&1 # 이 줄 이후 모든 stdout/stderr가 run.log로echo "이건 파일로 간다"date # 이것도 파일로
매 명령에 >> run.log를 붙일 필요 없이, 한 줄로 이후 출력을 전부 돌린다. 로그를 파일에 모으는 스크립트에서 자주 쓴다. FD를 직접 열고 닫을 수도 있다.
FD 번호(테이블 칸)는 그 프로세스에 종속이라, exec {fd}>&-로 닫거나 프로세스가 끝날 때까지 산다. 단 프로세스를 교체하는 exec command를 거쳐도 FD는 닫히지 않고 새 프로그램으로 넘어간다 — FD_CLOEXEC(close-on-exec) 플래그가 붙은 FD만 그때 닫힌다. fork/dup로 복제한 FD는 같은 열린 파일을 공유한다. 이 수명이 flock 잠금의 해제 시점과 직결된다(→ Shell Script Concurrency and flock).
동적 FD 할당 ({fd})
위에서는 FD 번호(3, 4)를 직접 골랐다. 문제는 그 번호가 이미 다른 데서 쓰이고 있을 수 있다는 점이다. {변수}> 문법을 쓰면 비어 있는 FD를 쉘이 골라 변수에 담아준다(10 이상에서 할당).
여러 개를 열면 10, 11처럼 차례로 붙는다. 어떤 번호가 비어 있는지 모르는 범용 스크립트에서 안전하다.
동적 FD는 Bash 4.1+
{fd}> 문법은 Bash 4.1 이상에서만 동작한다. macOS 기본 /bin/bash는 3.2라 여기서는 못 쓴다(brew install bash로 받은 최신 bash나 Linux에서 동작). 3.2를 피할 수 없으면 번호를 직접(exec 9>) 쓰되, 9처럼 높은 번호를 골라 충돌을 줄인다.
flock — 동시 실행 막기
cron이 5분마다 도는 스크립트가 한 번에 5분을 넘기면, 이전 게 안 끝났는데 새 게 또 뜬다. flock은 파일에 잠금을 걸어 이미 도는 인스턴스가 있으면 새 실행을 막는다.
# 스크립트 안에서 한 구간만 잠그기exec {lock_fd}>/var/lock/myjob.lockflock -n "$lock_fd" || { echo "이미 실행 중" >&2; exit 1; }# --- 여기부터 한 번에 하나만 ---long_running_job# 명령 하나를 통째로 잠그려면 (cron에 좋다)# */5 * * * * /usr/bin/flock -n /var/lock/myjob.lock /path/to/job.sh
-n은 잠겨 있으면 기다리지 않고 즉시 실패한다(논블로킹). 프로세스가 끝나면 FD가 닫히며 잠금이 풀린다.
flock 더 깊이 알기
flock이 advisory lock이라는 점(모두가 호출해야 효력), 잠금이 FD 번호가 아니라 OFD에 걸리는 커널 동작, mkdir·set -o noclobber 같은 macOS·이식성 대안, 옵션(-w·-x·-s)과 실전 패턴은 Shell Script Concurrency and flock에 따로 정리했다.
4. 에러 핸들링
set 옵션
#!/bin/bashset -euo pipefail
옵션
동작
-e
명령이 실패하면 즉시 종료
-u
미선언 변수 사용 시 에러
-o pipefail
파이프라인 중 하나라도 실패하면 전체 실패
디버깅할 때는 set -x
실행되는 명령을 한 줄씩 출력해준다. 특정 구간만 감싸서 쓸 수도 있다.
set -x# 디버그 구간set +x
trap
프로세스가 시그널을 받거나 종료될 때 실행할 명령을 등록한다.
# 임시 파일 정리tmpfile=$(mktemp)trap "rm -f $tmpfile" EXIT# 여러 시그널 처리cleanup() { echo "Cleaning up..." rm -f "$tmpfile"}trap cleanup EXIT INT TERM# trap 해제trap - EXIT
시그널
설명
EXIT
스크립트 종료 시 (정상/비정상 모두)
INT
Ctrl+C
TERM
kill 명령
ERR
명령 실패 시 (set -e와 조합)
에러 처리 패턴
# || 로 실패 시 대체 동작cd /some/dir || { echo "디렉토리 없음"; exit 1; }# 커스텀 에러 함수die() { echo "ERROR: $*" >&2 exit 1}[[ -f config.yml ]] || die "config.yml not found"