모든 운영 스크립트는 같은 세 줄로 시작합니다.
#!/usr/bin/env bash
set -euo pipefail
trap 'rm -rf "$WORK"' EXIT| 옵션 | 뜻 |
|---|---|
-e |
명령 실패 시 즉시 종료 |
-u |
정의 안 된 변수 사용 시 종료 |
-o pipefail |
파이프 중간 실패도 실패 |
-e 가 없으면 cd /없는폴더 가 실패해도 다음 줄이 실행됩니다. -u 는 rm -rf "$DIR/" 에서 DIR 오타를 잡습니다. pipefail 은 grep | sort 에서 grep 이 실패해도 sort 가 성공하면 성공으로 보는 것을 막습니다.
$1, $2 가 위치 인자, $# 가 개수, $@ 가 전부입니다. 옵션은 while 과 case 로 처리하고 shift 로 하나씩 소비합니다.
DRY_RUN=0
while [ $# -gt 0 ]; do
case $1 in
--dry-run) DRY_RUN=1 ;;
-h|--help) usage ;;
-*) echo "모르는 옵션: $1"; usage ;;
*) break ;;
esac
shift
done
LOG_DIR=${1:-/opt/myapp/logs}옵션을 다 먹은 뒤 남은 $1 이 위치 인자입니다. ${1:-기본값} 으로 없을 때 기본값을 줍니다. ${1:?메시지} 는 없으면 메시지를 내고 종료합니다.
함수는 이름() { } 으로 정의하고 인자는 $1 로 받습니다. return 은 함수 종료 코드, exit 는 스크립트 종료입니다.
log() { printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2"; }
info() { log INFO "$1"; }
die() { log ERROR "$1" >&2; exit "${2:-1}"; }
run() { if [ "$DRY_RUN" = 1 ]; then echo " [dry] $*"; else "$@"; fi; }die 는 에러를 표준 에러로 내고 종료 코드를 정합니다. run 은 dry run 이면 명령을 출력만 합니다. 이 네 함수를 파일 앞에 두면 나머지가 읽기 쉬워집니다.
[ ] 는 test 명령이고 [[ ]] 는 bash 확장입니다. 파일·숫자 검사는 둘 다 되고 패턴·정규식은 [[ ]] 만 됩니다.
| 검사 | 문법 |
|---|---|
| 파일 존재 | [ -f "$f" ] |
| 폴더 존재 | [ -d "$d" ] |
| 비어 있지 않음 | [ -s "$f" ] |
| 문자열 비었나 | [ -z "$s" ] |
| 숫자 비교 | [ "$n" -gt 3 ] |
| 문자열 비교 | [ "$a" = "$b" ] |
| 패턴 | [[ $f == *.log ]] |
| 정규식 | [[ $s =~ ^[0-9]+$ ]] |
숫자는 -eq -ne -gt -lt, 문자열은 = != 입니다. [ ] 안에서는 변수를 꼭 따옴표로 감쌉니다. [[ ]] 는 감싸지 않아도 되지만 습관은 통일합니다.
for f in "$LOG_DIR"/*.log; do echo "$f"; done # 글로브
for i in $(seq 1 5); do echo "$i"; done # 숫자
while IFS= read -r line; do echo "$line"; done < file # 줄 단위
while true; do curl -sf URL && break; sleep 1; done # 조건 될 때까지파일을 줄 단위로 읽을 때는 while read 이지 for line in $(cat file) 이 아닙니다. 후자는 공백에서 쪼개집니다. read -r 은 백슬래시를 그대로 두고 IFS= 는 앞뒤 공백을 보존합니다.
문자열 자르기는 외부 명령 없이 확장 문법으로 합니다. basename·dirname 보다 빠르고 서브셸이 없습니다.
| 문법 | 뜻 | path=/opt/app/a-1.2.jar |
|---|---|---|
${path##*/} |
앞에서 긴 */ 제거 |
a-1.2.jar |
${path%/*} |
뒤에서 짧은 /* 제거 |
/opt/app |
${path%.jar} |
뒤 .jar 제거 |
/opt/app/a-1.2 |
${v:-기본} |
없으면 기본 | |
${v:?메시지} |
없으면 종료 | |
${#v} |
길이 |
# 은 앞, % 는 뒤, 두 번 쓰면 가장 긴 일치입니다. 04 레슨 app.sh 의 ${JAVA_HOME:+$JAVA_HOME/bin/} 는 있을 때만 값을 쓰는 :+ 입니다.
변수는 항상 큰따옴표로 감쌉니다. 파일명에 공백이 있으면 감싸지 않은 $f 가 두 인자로 쪼개집니다.
cp "$src" "$dst" # 항상 이렇게
for f in "$DIR"/*; do # 글로브는 따옴표 밖
echo '$HOME 은 그대로' # 작은따옴표는 확장 없음
java $JAVA_OPTS -jar app.jar # 단어 분리가 목적일 때만 예외예외는 JAVA_OPTS 처럼 일부러 여러 인자로 나눌 때뿐입니다. 그 경우에도 배열 "${OPTS[@]}" 가 더 안전합니다.
count=$(grep -c ERROR "$f") # 출력을 변수로
now=$(date +%F)
total=$((count + 10)) # 정수 산술
pct=$(( used * 100 / size )) # 정수 나눗셈
if grep -q ERROR "$f"; then ...; fi # 종료 코드를 조건으로백틱 대신 $( ) 를 씁니다. 중첩되고 읽기 쉽습니다. 소수 계산은 awk 나 bc 가 필요합니다.
set -e 는 if, while, &&, || 안의 실패에는 반응하지 않습니다. grep 이 못 찾아 1 을 돌려줘도 if grep -q 는 정상 흐름입니다.
grep -q ERROR "$f" || echo "에러 없음" # 실패해도 계속
set +e; risky_command; rc=$?; set -e # 종료 코드가 필요할 때종료 코드를 변수에 담아야 할 때만 잠시 set +e 로 풉니다. cmd || true 는 실패를 무시하는 관용구입니다.
trap 은 신호나 이벤트에 명령을 겁니다. EXIT 는 정상·비정상 종료 모두에 실행되어 정리에 씁니다.
trap 'rm -rf "$WORK"' EXIT # 임시 폴더 정리
trap 'echo "줄 $LINENO 실패" >&2' ERR # 어느 줄에서 죽었나
trap 'echo "중단 요청"; exit 130' INT # Ctrl+C03 레슨의 worker.sh 가 TERM 을 잡은 것과 같은 문법입니다. ERR trap 은 set -e 로 종료되기 직전에 실행되어 원인 줄을 알려 줍니다.
cron 이 겹치거나 두 사람이 동시에 배포하면 사고입니다. mkdir 는 원자적이라 잠금으로 씁니다.
LOCK=/var/run/myapp-deploy.lock
mkdir "$LOCK" 2>/dev/null || { echo "이미 실행 중"; exit 1; }
trap 'rm -rf "$LOCK"' EXITflock 명령이 있으면 flock -n /var/run/deploy.lock 명령 한 줄로도 됩니다. 스크립트가 죽어도 EXIT trap 이 잠금을 지웁니다.
| 항목 | 이유 |
|---|---|
| 절대 경로 | cron 의 PATH 가 짧음 |
cd 명시 |
실행 위치가 다름 |
| 출력을 파일로 | 메일 대신 로그 |
| 입력 없음 | 대화형 프롬프트 불가 |
| 잠금 | 겹침 방지 |
| 종료 코드 | 0 성공, 나머지 실패 |
shellcheck 파일.sh 로 정적 검사를 하면 따옴표 누락과 [ ] 실수를 잡아 줍니다. RHEL 은 dnf install ShellCheck, 없으면 PC 에서 돌립니다.