1. 기본 - Shebang과 실행 방법
Shebang (#!)
스크립트 파일 첫 줄에 인터프리터를 지정하는 매직 넘버.
#!/bin/bash # Bash 사용
#!/usr/bin/env bash # PATH에서 bash를 찾아 실행 (이식성 우수)
#!/bin/sh # POSIX sh (Bash 기능 제한됨)
#!/usr/bin/env python3 # Python 스크립트도 동일 원리스크립트 실행 방법
# 1. 실행 권한 부여 후 직접 실행
chmod +x script.sh
./script.sh # shebang에 지정된 인터프리터로 실행
# 2. 인터프리터 명시 실행 (실행 권한 불필요)
bash script.sh
# 3. source 또는 . (현재 쉘에서 실행 - 서브쉘 아님!)
source script.sh
. script.sh # 동일./script.sh와 bash script.sh는 서브쉘에서 실행되므로 스크립트 내에서 변경한 환경변수가 현재 쉘에 영향을 주지 않는다. source는 현재 쉘에서 실행되므로 환경변수가 유지된다.
# 예시: script.sh 에 "export MY_VAR=hello" 가 있을 때
bash script.sh # 실행 후 echo $MY_VAR → (빈 값)
source script.sh # 실행 후 echo $MY_VAR → hello2. 변수
변수 선언과 할당
# 선언과 할당 (= 양쪽에 공백 없어야 함!)
name="Park"
count=10
path="/usr/local/bin"
# 올바르지 않은 예 (공백 때문에 에러)
# name = "Park" # bash: name: command not found
# readonly (상수)
readonly PI=3.14159
PI=3.0 # bash: PI: readonly variable
# unset (변수 삭제)
unset name변수 참조
echo $name
echo ${name} # 중괄호 - 변수명 경계를 명확히 할 때
echo "${name}_suffix" # 없으면 $name_suffix 로 해석됨
# 기본값 (Parameter Expansion)
echo ${var:-default} # var가 미설정/빈 문자열이면 "default" 출력 (var 변경 없음)
echo ${var:=default} # var가 미설정/빈 문자열이면 "default"을 var에 할당하고 출력
echo ${var:+alternate} # var가 설정되어 있으면 "alternate" 출력, 아니면 빈 문자열
echo ${var:?error msg} # var가 미설정이면 에러 메시지 출력 후 스크립트 종료환경변수 vs 쉘 변수
MY_VAR="local" # 쉘 변수 - 현재 쉘에서만 유효
export MY_VAR="global" # 환경변수 - 자식 프로세스에도 전달
export -n MY_VAR # 환경변수에서 제거 (쉘 변수로 전환)
env # 모든 환경변수 출력
set # 모든 변수(쉘 변수 + 환경변수) 출력3. 특수 변수
| 변수 | 설명 |
|---|---|
$0 | 스크립트 파일명 (또는 쉘 이름) |
$1 ~ $9 | 위치 매개변수 (1번째~9번째 인자). ${10} 이상은 중괄호 필수 |
$# | 인자 개수 |
$@ | 모든 인자 (각각 별도 단어로 확장). "$@" = "$1" "$2" ... |
$* | 모든 인자 (하나의 문자열로 확장). "$*" = "$1 $2 ..." |
$? | 직전 명령의 종료 코드 (0=성공, 1-255=실패) |
$$ | 현재 쉘의 PID |
$! | 마지막 백그라운드 프로세스의 PID |
$- | 현재 쉘 옵션 플래그 |
$_ | 직전 명령의 마지막 인자 |
#!/bin/bash
echo "스크립트: $0"
echo "인자 개수: $#"
echo "첫 번째 인자: $1"
echo "모든 인자: $@"
# $@ vs $* 차이 (따옴표 안에서 중요)
set -- "hello world" "foo bar"
for arg in "$@"; do echo "[@] $arg"; done
# [@] hello world
# [@] foo bar
for arg in "$*"; do echo "[*] $arg"; done
# [*] hello world foo bar4. 따옴표와 명령 치환
따옴표 규칙
name="World"
# 작은따옴표: 모든 것을 리터럴 문자열로 처리 (확장 없음)
echo 'Hello $name' # Hello $name
# 큰따옴표: 변수 확장, 명령 치환 수행. 글로빙/단어 분리 방지
echo "Hello $name" # Hello World
echo "Path: $(pwd)" # Path: /home/phh
# 따옴표 없음: 변수 확장 + 글로빙 + 단어 분리 (위험할 수 있음)
files="file1 file2"
ls $files # ls file1 file2 (두 개의 인자)
ls "$files" # ls "file1 file2" (한 개의 인자)명령 치환 (Command Substitution)
# $() 방식 (권장 - 중첩 가능)
today=$(date +%Y-%m-%d)
file_count=$(ls -1 | wc -l)
kernel=$(uname -r)
# 백틱 방식 (레거시 - 중첩 어려움)
today=`date +%Y-%m-%d`
# 중첩 예시
inner_dir=$(basename $(dirname /usr/local/bin/bash)) # local
# 백틱으로는 이스케이프 지옥: inner_dir=`basename \`dirname /usr/local/bin/bash\``5. 조건문
if/elif/else/fi
if [ 조건 ]; then
명령
elif [ 조건 ]; then
명령
else
명령
fitest / [ ] / [[ ]]
[ ]는 test 명령의 축약형이다. [[ ]]는 Bash 확장으로 더 안전하고 기능이 많다.
# 세 가지 모두 동일
test -f /etc/passwd
[ -f /etc/passwd ]
[[ -f /etc/passwd ]][[ ]]의 장점 (Bash 전용):
- 변수를 따옴표 없이 써도 단어 분리 안 됨
&&,||연산자 사용 가능 ([ ]에서는-a,-o)- 패턴 매칭:
[[ $str == *.txt ]] - 정규식:
[[ $str =~ ^[0-9]+$ ]]
파일 테스트 연산자
| 연산자 | 설명 |
|---|---|
-f file | 일반 파일인가 |
-d file | 디렉토리인가 |
-e file | 존재하는가 (파일/디렉토리/심볼릭링크 등 모두) |
-r file | 읽기 권한이 있는가 |
-w file | 쓰기 권한이 있는가 |
-x file | 실행 권한이 있는가 |
-s file | 크기가 0보다 큰가 (빈 파일이 아닌가) |
-L file | 심볼릭 링크인가 |
-h file | 심볼릭 링크인가 (-L과 동일) |
f1 -nt f2 | f1이 f2보다 새로운가 (newer than) |
f1 -ot f2 | f1이 f2보다 오래됐는가 (older than) |
문자열 비교
# [ ] 내에서
[ "$str" = "hello" ] # 같음 (POSIX)
[ "$str" != "hello" ] # 다름
[ -z "$str" ] # 빈 문자열인가 (zero length)
[ -n "$str" ] # 비어있지 않은가 (non-zero length)
# [[ ]] 내에서
[[ "$str" == "hello" ]] # 같음
[[ "$str" == *.txt ]] # 패턴 매칭 (glob)
[[ "$str" =~ ^[0-9]+$ ]] # 정규식 매칭
[[ "$str" < "bbb" ]] # 사전순 비교숫자 비교
[ "$a" -eq "$b" ] # equal (같음)
[ "$a" -ne "$b" ] # not equal (다름)
[ "$a" -lt "$b" ] # less than (미만)
[ "$a" -le "$b" ] # less than or equal (이하)
[ "$a" -gt "$b" ] # greater than (초과)
[ "$a" -ge "$b" ] # greater than or equal (이상)
# [[ ]] 에서는 (( ))를 사용할 수도 있음
if (( a > b )); then echo "a가 더 크다"; fi논리 연산
# [ ] 내부
[ 조건1 -a 조건2 ] # AND
[ 조건1 -o 조건2 ] # OR
[ ! 조건 ] # NOT
# [[ ]] 내부 (권장)
[[ 조건1 && 조건2 ]]
[[ 조건1 || 조건2 ]]
[[ ! 조건 ]]
# 조건문 바깥에서
[ 조건1 ] && [ 조건2 ]
[ 조건1 ] || [ 조건2 ]실전 예시
#!/bin/bash
FILE="/etc/nginx/nginx.conf"
if [[ -f "$FILE" ]]; then
if [[ -r "$FILE" ]]; then
echo "설정 파일 읽기 가능"
line_count=$(wc -l < "$FILE")
if (( line_count > 100 )); then
echo "대규모 설정 파일 ($line_count 줄)"
fi
else
echo "읽기 권한 없음"
exit 1
fi
else
echo "파일이 존재하지 않음"
exit 1
fi6. case문
패턴 매칭 기반 다중 분기. 복잡한 if/elif 체인 대신 사용하면 깔끔하다.
case "$변수" in
패턴1)
명령
;; # break 역할 (필수)
패턴2|패턴3) # OR 패턴
명령
;;
*) # default (일치하는 패턴 없을 때)
명령
;;
esac#!/bin/bash
case "$1" in
start)
echo "서비스 시작"
;;
stop)
echo "서비스 중지"
;;
restart|reload)
echo "서비스 재시작"
;;
status)
echo "서비스 상태 확인"
;;
*)
echo "Usage: $0 {start|stop|restart|reload|status}"
exit 1
;;
esac# 파일 확장자 판별
case "$filename" in
*.tar.gz|*.tgz) tar xzf "$filename" ;;
*.tar.bz2) tar xjf "$filename" ;;
*.tar.xz) tar xJf "$filename" ;;
*.zip) unzip "$filename" ;;
*.gz) gunzip "$filename" ;;
*) echo "지원하지 않는 형식: $filename" ;;
esac7. 반복문
for문
# 리스트 순회
for fruit in apple banana cherry; do
echo "$fruit"
done
# 범위 (Bash brace expansion)
for i in {1..10}; do echo "$i"; done
for i in {0..100..5}; do echo "$i"; done # 0, 5, 10, ..., 100
# C 스타일
for ((i=0; i<10; i++)); do
echo "$i"
done
# 파일 순회 (글로빙)
for file in /var/log/*.log; do
echo "$(wc -l < "$file") $file"
done
# 명령 결과 순회
for user in $(cut -d: -f1 /etc/passwd); do
echo "User: $user"
done
# 배열 순회
arr=("one" "two" "three")
for item in "${arr[@]}"; do
echo "$item"
donewhile문
# 기본
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
((count++))
done
# 파일 한 줄씩 읽기 (가장 자주 쓰이는 패턴)
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hosts
# 무한 루프
while true; do
echo "모니터링 중... (Ctrl+C로 종료)"
sleep 5
done
# 파이프에서 읽기 (서브쉘 주의!)
# 파이프 뒤의 while은 서브쉘에서 실행되므로 변수 변경이 유지 안 됨
count=0
cat /etc/passwd | while read -r line; do
((count++))
done
echo $count # 0 (서브쉘 문제!)
# 해결: 프로세스 치환 사용
count=0
while read -r line; do
((count++))
done < <(cat /etc/passwd)
echo $count # 정상 출력until문
조건이 거짓인 동안 반복 (while의 반대).
count=1
until [ $count -gt 5 ]; do
echo "Count: $count"
((count++))
donebreak, continue
for i in {1..10}; do
if [ $i -eq 3 ]; then continue; fi # 3 건너뜀
if [ $i -eq 8 ]; then break; fi # 8에서 중단
echo "$i"
done
# 출력: 1 2 4 5 6 7select문
대화형 메뉴 생성.
PS3="선택하세요: " # select의 프롬프트
select opt in "시작" "중지" "종료"; do
case $opt in
"시작") echo "시작합니다" ;;
"중지") echo "중지합니다" ;;
"종료") break ;;
*) echo "잘못된 선택" ;;
esac
done
# 출력:
# 1) 시작
# 2) 중지
# 3) 종료
# 선택하세요:8. 배열
인덱스 배열
# 선언
arr=("apple" "banana" "cherry")
arr[3]="date"
# 접근
echo ${arr[0]} # apple (0-based index)
echo ${arr[-1]} # date (마지막 요소, Bash 4.3+)
# 모든 요소
echo ${arr[@]} # apple banana cherry date
echo ${arr[*]} # apple banana cherry date (문자열)
# 요소 개수
echo ${#arr[@]} # 4
# 특정 요소 길이
echo ${#arr[0]} # 5 (apple의 길이)
# 슬라이싱
echo ${arr[@]:1:2} # banana cherry (인덱스 1부터 2개)
# 요소 삭제
unset arr[1] # banana 삭제 (인덱스 유지 - 1이 빈자리)
# 요소 추가
arr+=("elderberry")
# 모든 인덱스 출력
echo ${!arr[@]} # 0 2 3 4 (1은 unset했으므로 없음)연관 배열 (Associative Array, Bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
config[env]="production"
echo ${config[host]} # localhost
echo ${config[@]} # 모든 값
echo ${!config[@]} # 모든 키: host port env
echo ${#config[@]} # 3
# 순회
for key in "${!config[@]}"; do
echo "$key = ${config[$key]}"
done
# 초기화와 동시에 선언
declare -A colors=(
[red]="#FF0000"
[green]="#00FF00"
[blue]="#0000FF"
)9. 함수
# 선언 방법 1
function greet() {
echo "Hello, $1!"
}
# 선언 방법 2 (POSIX 호환)
greet() {
echo "Hello, $1!"
}
# 호출
greet "World" # Hello, World!
# 인자 전달
calculate() {
local result=$(( $1 + $2 )) # local: 함수 내 지역 변수
echo $result # "반환값" 출력
}
# 함수의 출력을 변수에 저장
sum=$(calculate 10 20)
echo "합계: $sum" # 합계: 30
# return: 종료 코드 반환 (0-255, 값 반환이 아님!)
is_even() {
if (( $1 % 2 == 0 )); then
return 0 # 성공(참)
else
return 1 # 실패(거짓)
fi
}
if is_even 4; then
echo "짝수"
fi
# local 변수 중요성
bad_func() {
result="modified" # 전역 변수 오염!
}
good_func() {
local result="safe" # 함수 내에서만 유효
}10. 입출력 리다이렉션
기본 리다이렉션
# stdout 리다이렉션
command > file # 덮어쓰기 (파일 생성/초기화)
command >> file # 추가 (append)
# stdin 리다이렉션
command < file # 파일 내용을 stdin으로
# stderr 리다이렉션
command 2> file # stderr만 파일로
command 2>> file # stderr 추가
# stdout + stderr
command > file 2>&1 # 둘 다 같은 파일로 (순서 중요!)
command &> file # Bash 축약 (동일 효과)
command &>> file # 둘 다 추가
# 각각 다른 파일로
command > stdout.log 2> stderr.log
# 출력 버리기
command > /dev/null # stdout 버림
command 2> /dev/null # stderr 버림
command &> /dev/null # 둘 다 버림Here Document (Heredoc)
# 여러 줄 입력
cat << EOF
Hello, ${USER}!
현재 디렉토리: $(pwd)
날짜: $(date)
EOF
# 변수 확장 방지 (따옴표로 감싸기)
cat << 'EOF'
이것은 $USER 그대로 출력됨
$(pwd)도 확장되지 않음
EOF
# 들여쓰기 제거 (<<- 사용, 탭만 제거)
if true; then
cat <<- EOF
들여쓰기된 heredoc
탭이 제거됨
EOF
fi
# 파일에 쓰기
cat << EOF > /tmp/config.txt
server=localhost
port=8080
EOF
# 명령에 입력
mysql -u root << EOF
CREATE DATABASE testdb;
USE testdb;
SHOW TABLES;
EOFHere String
# 한 줄 문자열을 stdin으로
grep "pattern" <<< "검색할 문자열"
bc <<< "3.14 * 2"
read -r first rest <<< "Hello World Bash"
echo $first # Hello
echo $rest # World Bash11. 파이프와 명령 체이닝
파이프 (|)
# 앞 명령의 stdout을 뒤 명령의 stdin으로
ls -la | grep ".log"
cat /etc/passwd | sort | head -5
ps aux | grep nginx | grep -v grep
dmesg | tail -20
# stderr도 파이프에 포함 (|& = 2>&1 |)
command |& grep "error"
# tee: stdout을 파일에도 저장하면서 파이프 계속
command | tee output.log | grep "error"
command | tee -a output.log # append 모드명령 체이닝
# ; - 순차 실행 (성공/실패 무관)
command1; command2; command3
# && - AND (앞 명령 성공 시에만 다음 실행)
mkdir /tmp/mydir && cd /tmp/mydir && echo "성공"
# || - OR (앞 명령 실패 시에만 다음 실행)
cd /nonexistent || echo "디렉토리 없음"
# 실전 패턴: command && success_action || failure_action
ping -c1 google.com &>/dev/null && echo "온라인" || echo "오프라인"
# 그룹핑
{ command1; command2; } > output.log # 현재 쉘에서 그룹 실행
(command1; command2) > output.log # 서브쉘에서 그룹 실행12. trap - 시그널 처리
스크립트 종료 시 정리(cleanup) 작업이나 시그널 핸들링에 사용.
# 기본 문법
trap '명령' SIGNAL_LIST
# EXIT: 스크립트 종료 시 (정상/비정상 모두)
trap 'echo "스크립트 종료"; rm -f /tmp/lockfile' EXIT
# INT: Ctrl+C (SIGINT)
trap 'echo "인터럽트 감지!"; exit 1' INT
# TERM: kill 명령 (SIGTERM)
trap 'cleanup_function' TERM
# ERR: 명령 실패 시 (set -e와 함께 유용)
trap 'echo "에러 발생: $BASH_COMMAND (line $LINENO)"' ERR
# DEBUG: 모든 명령 실행 전 (디버깅용)
trap 'echo "실행: $BASH_COMMAND"' DEBUG
# 트랩 해제
trap - INT
# 트랩 무시 (시그널 차단)
trap '' INT # Ctrl+C 무시실전: 임시 파일 정리
#!/bin/bash
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT # 어떻게 종료되든 임시 파일 삭제
# 작업 수행
curl -s "https://api.example.com/data" > "$TMPFILE"
process_data "$TMPFILE"
# EXIT 트랩에 의해 자동 정리됨13. 산술 연산
$(( )) - Arithmetic Expansion
a=10; b=3
echo $(( a + b )) # 13
echo $(( a - b )) # 7
echo $(( a * b )) # 30
echo $(( a / b )) # 3 (정수 나눗셈)
echo $(( a % b )) # 1 (나머지)
echo $(( a ** 2 )) # 100 (거듭제곱)
# 증감
(( a++ ))
(( a-- ))
(( a += 5 ))
# 비트 연산
echo $(( 5 & 3 )) # 1 (AND)
echo $(( 5 | 3 )) # 7 (OR)
echo $(( 5 ^ 3 )) # 6 (XOR)
echo $(( ~5 )) # -6 (NOT)
echo $(( 1 << 3 )) # 8 (Left shift)
# 삼항 연산자
echo $(( a > b ? a : b )) # max(a, b)let
let "result = a + b"
let "a++"
let "a += 5"expr (레거시)
result=$(expr 10 + 3) # 공백 필수, 연산자 이스케이프 필요
result=$(expr 10 \* 3) # * 는 이스케이프bc - 소수점 연산
echo "3.14 * 2" | bc # 6.28
echo "scale=2; 10 / 3" | bc # 3.33
echo "scale=4; sqrt(2)" | bc -l # 1.4142 (-l: math library)
result=$(echo "scale=2; 100 / 7" | bc)14. 문자열 조작
str="Hello World Bash Scripting"
# 길이
echo ${#str} # 26
# 부분 문자열 (offset:length)
echo ${str:0:5} # Hello
echo ${str:6:5} # World
echo ${str:(-9)} # Scripting (뒤에서부터)
# 패턴 제거
filepath="/home/user/docs/file.tar.gz"
echo ${filepath#*/} # home/user/docs/file.tar.gz (앞에서 최단 매칭 제거)
echo ${filepath##*/} # file.tar.gz (앞에서 최장 매칭 제거 = basename)
echo ${filepath%/*} # /home/user/docs (뒤에서 최단 매칭 제거 = dirname)
echo ${filepath.*} # document (모든 확장자 제거)
echo ${file##*.} # gz (확장자 추출)
# 치환
echo ${str/World/Bash} # Hello Bash Bash Scripting (첫 번째만)
echo ${str//a/A} # Hello World BAsh Scripting (모두 치환)
echo ${str/#Hello/Hi} # Hi World Bash Scripting (앞부분 매칭)
echo ${str/%Scripting/Coding} # Hello World Bash Coding (뒷부분 매칭)
# 대소문자 변환 (Bash 4+)
echo ${str^^} # HELLO WORLD BASH SCRIPTING (모두 대문자)
echo ${str,,} # hello world bash scripting (모두 소문자)
echo ${str^} # Hello World Bash Scripting (첫 글자만 대문자)15. 안전한 스크립팅 - set 옵션
프로덕션 스크립트에서는 거의 항상 사용해야 하는 옵션들.
#!/bin/bash
set -euo pipefail
# 또는 개별적으로:
set -e # errexit: 명령 실패 시 즉시 스크립트 종료
set -u # nounset: 미정의 변수 참조 시 에러 (오타 방지)
set -x # xtrace: 실행되는 명령을 출력 (디버깅용)
set -o pipefail # 파이프라인에서 마지막이 아닌 중간 명령 실패도 감지각 옵션 상세 설명
set -e (errexit):
set -e
false # 여기서 스크립트 종료됨
echo "도달 불가"
# 예외: if, while, ||, && 내부 명령은 실패해도 종료 안 됨
set -e
if ! grep "pattern" file.txt; then
echo "패턴 없음" # 정상 도달
fiset -u (nounset):
set -u
echo $undefined_var # bash: undefined_var: unbound variable → 스크립트 종료
# 기본값으로 우회
echo ${undefined_var:-"기본값"} # 정상 동작set -o pipefail:
# pipefail 없이
false | true
echo $? # 0 (마지막 명령 true의 종료 코드)
# pipefail 있으면
set -o pipefail
false | true
echo $? # 1 (파이프라인 중 실패한 명령의 종료 코드)set -x (xtrace):
set -x
name="world"
echo "hello $name"
# 출력:
# + name=world
# + echo 'hello world'
# hello world
# 부분적으로 적용
set -x # 디버깅 시작
문제가_있는_코드
set +x # 디버깅 종료16. 실전 예시 스크립트
예시 1: 로그 파일 모니터링 + 알림
#!/bin/bash
set -euo pipefail
# 설정
LOG_FILE="${1:?사용법: $0 <로그파일경로>}"
KEYWORDS=("ERROR" "FATAL" "CRITICAL")
ALERT_LOG="/tmp/alert_$(date +%Y%m%d).log"
# 임시 파일 정리
trap 'echo "모니터링 종료: $(date)" >> "$ALERT_LOG"' EXIT
echo "모니터링 시작: $LOG_FILE ($(date))"
# 키워드 패턴 생성
pattern=$(IFS='|'; echo "${KEYWORDS[*]}")
# 실시간 로그 감시
tail -F "$LOG_FILE" 2>/dev/null | while IFS= read -r line; do
if [[ "$line" =~ ($pattern) ]]; then
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
alert_msg="[$timestamp] ALERT: $line"
echo "$alert_msg" | tee -a "$ALERT_LOG"
fi
done예시 2: 시스템 정보 수집 스크립트
#!/bin/bash
set -euo pipefail
REPORT_FILE="/tmp/sysinfo_$(hostname)_$(date +%Y%m%d_%H%M%S).txt"
header() {
echo ""
echo "========================================"
echo " $1"
echo "========================================"
}
{
header "시스템 기본 정보"
echo "호스트명: $(hostname)"
echo "커널: $(uname -r)"
echo "아키텍처: $(uname -m)"
echo "업타임: $(uptime -p 2>/dev/null || uptime)"
echo "현재 시각: $(date)"
header "CPU 정보"
lscpu 2>/dev/null | grep -E "^(Model name|CPU\(s\)|Thread|Core)" || echo "lscpu 불가"
header "메모리 정보"
free -h
header "디스크 사용량"
df -h | grep -v tmpfs
header "네트워크 인터페이스"
ip -br addr 2>/dev/null || ifconfig 2>/dev/null || echo "네트워크 정보 불가"
header "로그인 사용자"
who
header "상위 프로세스 (CPU 기준)"
ps aux --sort=-%cpu 2>/dev/null | head -11
} > "$REPORT_FILE"
echo "리포트 생성 완료: $REPORT_FILE"
echo "크기: $(du -h "$REPORT_FILE" | cut -f1)"예시 3: 디렉토리 백업 스크립트
#!/bin/bash
set -euo pipefail
# 설정
SOURCE_DIR="${1:?사용법: $0 <소스디렉토리> [백업디렉토리]}"
BACKUP_DIR="${2:-/tmp/backups}"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$(basename "$SOURCE_DIR")_${DATE}"
MAX_BACKUPS=7
# 유효성 검사
if [[ ! -d "$SOURCE_DIR" ]]; then
echo "에러: 소스 디렉토리가 존재하지 않음 - $SOURCE_DIR" >&2
exit 1
fi
# 백업 디렉토리 생성
mkdir -p "$BACKUP_DIR"
# 임시 파일 정리 트랩
TMPFILE=""
trap '[[ -n "$TMPFILE" && -f "$TMPFILE" ]] && rm -f "$TMPFILE"' EXIT
echo "백업 시작: $SOURCE_DIR → $BACKUP_DIR/$BACKUP_NAME.tar.gz"
# tar 압축
TMPFILE=$(mktemp "${BACKUP_DIR}/${BACKUP_NAME}.XXXXXX.tmp")
tar czf "$TMPFILE" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")" 2>/dev/null
mv "$TMPFILE" "$BACKUP_DIR/${BACKUP_NAME}.tar.gz"
TMPFILE="" # 트랩에서 삭제 방지
# 백업 크기 출력
size=$(du -h "$BACKUP_DIR/${BACKUP_NAME}.tar.gz" | cut -f1)
echo "백업 완료: ${size}"
# 오래된 백업 정리 (최근 MAX_BACKUPS개만 유지)
backup_count=$(ls -1 "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l)
if (( backup_count > MAX_BACKUPS )); then
echo "오래된 백업 정리 중... (유지: ${MAX_BACKUPS}개)"
ls -1t "$BACKUP_DIR"/backup_*.tar.gz | tail -n +$(( MAX_BACKUPS + 1 )) | while IFS= read -r old; do
echo " 삭제: $(basename "$old")"
rm -f "$old"
done
fi
echo "현재 백업 목록:"
ls -lh "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | awk '{print " " $NF " (" $5 ")"}'