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.shbash script.sh서브쉘에서 실행되므로 스크립트 내에서 변경한 환경변수가 현재 쉘에 영향을 주지 않는다. source현재 쉘에서 실행되므로 환경변수가 유지된다.

# 예시: script.sh 에 "export MY_VAR=hello" 가 있을 때
bash script.sh    # 실행 후 echo $MY_VAR → (빈 값)
source script.sh  # 실행 후 echo $MY_VAR → hello

2. 변수

변수 선언과 할당

# 선언과 할당 (= 양쪽에 공백 없어야 함!)
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 bar

4. 따옴표와 명령 치환

따옴표 규칙

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
    명령
fi

test / [ ] / [[ ]]

[ ]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 f2f1이 f2보다 새로운가 (newer than)
f1 -ot f2f1이 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
fi

6. 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" ;;
esac

7. 반복문

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"
done

while문

# 기본
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++))
done

break, 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 7

select문

대화형 메뉴 생성.

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;
EOF

Here String

# 한 줄 문자열을 stdin으로
grep "pattern" <<< "검색할 문자열"
bc <<< "3.14 * 2"
read -r first rest <<< "Hello World Bash"
echo $first    # Hello
echo $rest     # World Bash

11. 파이프와 명령 체이닝

파이프 (|)

# 앞 명령의 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 "패턴 없음"    # 정상 도달
fi

set -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 ")"}'