들어가며: 전문가 수준의 스크립팅

기초를 넘어 정규표현식, 텍스트 처리 도구(sed, awk), 프로세스 관리, 시그널 처리 등 고급 기법을 익히면 복잡한 서버 관리 작업도 자동화할 수 있습니다.

1. 정규표현식 (Regular Expression)

1.1 기본 메타문자

# 메타문자
.      # 임의의 한 문자
^      # 줄의 시작
$      # 줄의 끝
*      # 0회 이상 반복
+      # 1회 이상 반복 (확장)
?      # 0 또는 1회 (확장)
[]     # 문자 클래스
[^]    # 부정 문자 클래스
|      # OR (확장)
()     # 그룹화 (확장)
\      # 이스케이프

# 문자 클래스
[abc]      # a, b, c 중 하나
[a-z]      # 소문자
[A-Z]      # 대문자
[0-9]      # 숫자
[a-zA-Z]   # 모든 알파벳
[^0-9]     # 숫자가 아닌 것

1.2 POSIX 문자 클래스

[[:alpha:]]   # 알파벳
[[:digit:]]   # 숫자
[[:alnum:]]   # 알파벳 + 숫자
[[:space:]]   # 공백 문자
[[:upper:]]   # 대문자
[[:lower:]]   # 소문자
[[:punct:]]   # 구두점

1.3 grep과 정규표현식

# 기본 정규표현식 (BRE)
grep "pattern" file

# 확장 정규표현식 (ERE)
grep -E "pattern" file
egrep "pattern" file

# 예제
grep "^root" /etc/passwd          # root로 시작
grep "bash$" /etc/passwd          # bash로 끝남
grep -E "[0-9]{3}" file           # 숫자 3개
grep -E "error|warning" log       # error 또는 warning
grep -v "^#" config               # 주석 제외

# IP 주소 매칭
grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" file

# 이메일 매칭
grep -E "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}" file

1.4 Bash에서 정규표현식

#!/bin/bash

string="Hello World 123"

# =~ 연산자로 매칭
if [[ $string =~ [0-9]+ ]]; then
    echo "숫자 포함"
    echo "매칭된 부분: ${BASH_REMATCH[0]}"
fi

# 그룹 캡처
if [[ $string =~ ([A-Za-z]+)\ ([A-Za-z]+) ]]; then
    echo "전체 매칭: ${BASH_REMATCH[0]}"
    echo "첫 번째 그룹: ${BASH_REMATCH[1]}"
    echo "두 번째 그룹: ${BASH_REMATCH[2]}"
fi

2. sed (Stream Editor)

2.1 기본 사용법

# 기본 구문
sed 's/old/new/' file          # 각 줄의 첫 번째만 치환
sed 's/old/new/g' file         # 모든 occurrence 치환
sed -i 's/old/new/g' file      # 파일 직접 수정

# 여러 명령
sed -e 's/a/A/' -e 's/b/B/' file
sed 's/a/A/; s/b/B/' file

# 파일에서 명령 읽기
sed -f commands.sed file

2.2 주소 지정

# 줄 번호
sed '3s/old/new/' file         # 3번째 줄만
sed '1,5s/old/new/' file       # 1~5줄
sed '5,$s/old/new/' file       # 5줄부터 끝까지

# 패턴 매칭
sed '/pattern/s/old/new/' file # 패턴 포함 줄만
sed '/start/,/end/s/old/new/' file  # 범위

# 부정
sed '/pattern/!s/old/new/' file # 패턴 미포함 줄

2.3 고급 기능

# 줄 삭제
sed '/^#/d' file               # 주석 삭제
sed '/^$/d' file               # 빈 줄 삭제
sed '1,10d' file               # 1~10줄 삭제

# 줄 추가
sed '/pattern/a\새로운 줄' file  # 뒤에 추가
sed '/pattern/i\새로운 줄' file  # 앞에 추가
sed '/pattern/c\대체 줄' file    # 줄 대체

# 여러 줄 처리
sed 'N;s/\n/ /' file           # 두 줄을 한 줄로

# 역참조
sed 's/\(.*\):\(.*\)/\2:\1/' file  # 순서 바꾸기
sed -E 's/(.*):(.*)$/\2:\1/' file  # 확장 정규식

# 대소문자 변환
sed 's/[a-z]/\U&/g' file       # 대문자로
sed 's/[A-Z]/\L&/g' file       # 소문자로

# 실용 예제
# 설정 파일에서 값 변경
sed -i 's/^max_connections=.*/max_connections=200/' config

# 로그에서 IP 마스킹
sed -E 's/([0-9]{1,3}\.){3}[0-9]{1,3}/xxx.xxx.xxx.xxx/g' log

3. awk (패턴 스캐닝과 처리)

3.1 기본 구문

# 기본 구조: awk 'pattern { action }' file

# 필드 출력
awk '{print $1}' file          # 첫 번째 필드
awk '{print $1, $3}' file      # 1, 3번째 필드
awk '{print $NF}' file         # 마지막 필드
awk '{print NF}' file          # 필드 개수
awk '{print NR, $0}' file      # 줄 번호와 전체 줄

# 구분자 지정
awk -F: '{print $1}' /etc/passwd
awk -F'[,:]' '{print $1}' file  # 여러 구분자

3.2 패턴 매칭

# 조건 필터링
awk '/pattern/' file           # 패턴 포함 줄
awk '!/pattern/' file          # 패턴 미포함 줄
awk '$3 > 100' file            # 3번째 필드 > 100
awk '$1 == "root"' file        # 1번째 필드가 root
awk 'NR > 5' file              # 5줄 이후
awk 'NR==1 || NR==10' file     # 1번째 또는 10번째 줄

# 범위
awk '/start/,/end/' file       # start~end 사이

3.3 변수와 연산

# 내장 변수
FS     # 필드 구분자 (입력)
OFS    # 필드 구분자 (출력)
RS     # 레코드 구분자 (입력)
ORS    # 레코드 구분자 (출력)
NF     # 현재 줄의 필드 수
NR     # 현재 줄 번호
FNR    # 파일별 줄 번호
FILENAME  # 현재 파일명

# 변수 사용
awk '{sum += $1} END {print sum}' file
awk '{count++} END {print count}' file

# BEGIN과 END
awk 'BEGIN {print "시작"} {print} END {print "끝"}' file

awk 'BEGIN {FS=":"; OFS="\t"} {print $1, $3}' /etc/passwd

3.4 제어 구조

#!/usr/bin/awk -f

# if-else
{
    if ($3 > 1000) {
        print $1, "일반 사용자"
    } else {
        print $1, "시스템 계정"
    }
}

# for 루프
{
    for (i = 1; i <= NF; i++) {
        print i, $i
    }
}

# while 루프
{
    i = 1
    while (i <= NF) {
        print $i
        i++
    }
}

# 배열
{
    count[$1]++
}
END {
    for (key in count) {
        print key, count[key]
    }
}

3.5 실용 예제

# 열 합계
awk '{sum += $2} END {print "합계:", sum}' file

# 평균 계산
awk '{sum += $1; count++} END {print "평균:", sum/count}' file

# 중복 제거
awk '!seen[$0]++' file

# 특정 열 기준 정렬 (sort와 조합)
awk -F: '{print $3, $1}' /etc/passwd | sort -n

# 로그 분석: IP별 접속 횟수
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head

# CSV 처리
awk -F, '{print $1 ":" $2}' data.csv

# 필드 재정렬
awk -F: 'BEGIN {OFS=":"} {print $1, $3, $7}' /etc/passwd

# 조건부 합계
awk '$2 == "ERROR" {count++} END {print "에러:", count}' log

4. 프로세스 관리

4.1 백그라운드 실행

#!/bin/bash

# 백그라운드 실행
command &
pid=$!
echo "PID: $pid"

# 백그라운드 작업 대기
wait $pid
echo "종료 코드: $?"

# 모든 백그라운드 작업 대기
wait

# nohup: 터미널 종료 후에도 실행
nohup long_running_script.sh &

# disown: 작업 목록에서 제거
command &
disown

4.2 프로세스 제어

#!/bin/bash

# PID 파일 사용
start_daemon() {
    if [ -f /var/run/myapp.pid ]; then
        echo "이미 실행 중"
        return 1
    fi
    ./myapp &
    echo $! > /var/run/myapp.pid
}

stop_daemon() {
    if [ -f /var/run/myapp.pid ]; then
        kill $(cat /var/run/myapp.pid)
        rm /var/run/myapp.pid
    fi
}

# 프로세스 존재 확인
is_running() {
    local pid=$1
    [ -d "/proc/$pid" ]
}

# 프로세스 이름으로 찾기
pgrep -f "process_name"
pkill -f "process_name"

4.3 서브셸과 프로세스 그룹

#!/bin/bash

# 서브셸
(
    cd /tmp
    # 여기서 디렉토리 변경은 서브셸에만 영향
    command
)
# 원래 디렉토리 유지

# 명령 그룹 (현재 셸에서 실행)
{
    command1
    command2
} > output.txt

# 파이프라인과 서브셸
# 파이프의 각 명령은 서브셸에서 실행
cat file | while read line; do
    count=$((count + 1))  # 서브셸의 변수
done
echo $count  # 원래 셸에서는 변경 안 됨

# 해결: 프로세스 치환
while read line; do
    count=$((count + 1))
done < <(cat file)
echo $count

5. 시그널 처리

5.1 trap 명령

#!/bin/bash

# 시그널 처리
trap 'echo "SIGINT 받음"; exit 1' INT
trap 'echo "SIGTERM 받음"; exit 1' TERM
trap 'cleanup' EXIT

cleanup() {
    echo "정리 작업 수행..."
    rm -f /tmp/tempfile
}

# 주요 시그널
# SIGHUP (1)   - 터미널 연결 끊김
# SIGINT (2)   - Ctrl+C
# SIGQUIT (3)  - Ctrl+\
# SIGTERM (15) - kill 기본값
# SIGKILL (9)  - 강제 종료 (처리 불가)

# 시그널 무시
trap '' INT  # SIGINT 무시

# 시그널 처리 초기화
trap - INT   # 기본 동작으로 복원

5.2 실용적인 시그널 처리

#!/bin/bash

# 정리 작업이 필요한 스크립트
TEMPFILE=$(mktemp)
LOCKFILE=/var/lock/myscript.lock

cleanup() {
    rm -f "$TEMPFILE"
    rm -f "$LOCKFILE"
    echo "정리 완료"
}

# 종료 시 항상 cleanup 실행
trap cleanup EXIT

# 락 파일로 중복 실행 방지
if [ -f "$LOCKFILE" ]; then
    echo "이미 실행 중"
    exit 1
fi
echo $$ > "$LOCKFILE"

# 메인 로직
echo "작업 수행 중..."
sleep 10

# 정상 종료 시 cleanup 자동 실행

6. 파일 디스크립터와 고급 리다이렉션

6.1 파일 디스크립터

#!/bin/bash

# 기본 파일 디스크립터
# 0: 표준 입력 (stdin)
# 1: 표준 출력 (stdout)
# 2: 표준 에러 (stderr)

# 파일 디스크립터 열기
exec 3> output.txt    # 쓰기용
exec 4< input.txt     # 읽기용
exec 5<> file.txt     # 읽기/쓰기

# 사용
echo "데이터" >&3
read line <&4

# 닫기
exec 3>&-
exec 4<&-

# 실용 예제: 로그 파일
exec 3>&1 4>&2                    # 백업
exec 1>log.txt 2>&1               # 리다이렉트
echo "이것은 로그"
exec 1>&3 2>&4                    # 복원
exec 3>&- 4>&-                    # 닫기

6.2 프로세스 치환

#!/bin/bash

# 프로세스 치환
# <(command) - 명령 출력을 파일처럼 사용
# >(command) - 파일처럼 쓰면 명령 입력으로

# 두 파일 비교
diff <(sort file1) <(sort file2)

# 여러 명령 출력 합치기
cat <(echo "Header") file <(echo "Footer")

# 실시간 로그 처리
tail -f log.txt > >(grep "ERROR") 2>&1

# tee와 프로세스 치환
command | tee >(grep "pattern" > matches.txt)

7. 고급 패턴과 기법

7.1 옵션 파싱

#!/bin/bash

# getopts 사용
usage() {
    echo "Usage: $0 [-v] [-f file] [-n num]"
    exit 1
}

verbose=0
file=""
num=10

while getopts "vf:n:h" opt; do
    case $opt in
        v) verbose=1 ;;
        f) file="$OPTARG" ;;
        n) num="$OPTARG" ;;
        h) usage ;;
        ?) usage ;;
    esac
done

shift $((OPTIND - 1))

echo "Verbose: $verbose"
echo "File: $file"
echo "Num: $num"
echo "나머지 인자: $@"

7.2 병렬 처리

#!/bin/bash

# 병렬 실행 (백그라운드)
for i in {1..10}; do
    process_item $i &
done
wait

# 병렬 처리 제한
MAX_JOBS=4
job_count=0

for item in "${items[@]}"; do
    process_item "$item" &
    ((job_count++))

    if [ $job_count -ge $MAX_JOBS ]; then
        wait -n  # 하나가 끝날 때까지 대기
        ((job_count--))
    fi
done
wait

# xargs로 병렬 처리
cat urls.txt | xargs -P 4 -I {} curl -O {}

# GNU parallel 사용
parallel -j 4 process_item {} ::: {1..100}

7.3 임시 파일과 디렉토리

#!/bin/bash

# 안전한 임시 파일 생성
TMPFILE=$(mktemp)
TMPDIR=$(mktemp -d)

# 종료 시 정리
trap "rm -rf $TMPFILE $TMPDIR" EXIT

# 임시 파일 사용
echo "데이터" > "$TMPFILE"

# Here Document로 임시 파일
cat > "$TMPFILE" << 'EOF'
임시 데이터
여러 줄
EOF

8. 실전 스크립트 예제

8.1 로그 분석 스크립트

#!/bin/bash
# log_analyzer.sh

LOG_FILE="${1:-/var/log/nginx/access.log}"

echo "=== 로그 분석 리포트 ==="
echo "파일: $LOG_FILE"
echo ""

echo "--- 상위 10개 IP ---"
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10

echo ""
echo "--- HTTP 상태 코드 분포 ---"
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn

echo ""
echo "--- 시간대별 요청 수 ---"
awk '{print substr($4,14,2)":00"}' "$LOG_FILE" | sort | uniq -c

echo ""
echo "--- 상위 10개 요청 URL ---"
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10

8.2 서비스 모니터링 스크립트

#!/bin/bash
# service_monitor.sh

SERVICES=("nginx" "mysql" "redis")
EMAIL="admin@example.com"
LOG_FILE="/var/log/service_monitor.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

check_service() {
    local service=$1
    if systemctl is-active --quiet "$service"; then
        return 0
    else
        return 1
    fi
}

restart_service() {
    local service=$1
    log "서비스 재시작 시도: $service"
    if systemctl restart "$service"; then
        log "서비스 재시작 성공: $service"
        return 0
    else
        log "서비스 재시작 실패: $service"
        return 1
    fi
}

for service in "${SERVICES[@]}"; do
    if ! check_service "$service"; then
        log "서비스 다운 감지: $service"

        if ! restart_service "$service"; then
            echo "서비스 $service 재시작 실패" | mail -s "서비스 알림" "$EMAIL"
        fi
    fi
done

8.3 설정 파일 관리 스크립트

#!/bin/bash
# config_manager.sh

CONFIG_FILE="${1:-config.ini}"
ACTION="${2:-get}"
KEY="${3:-}"
VALUE="${4:-}"

get_value() {
    local key=$1
    grep "^${key}=" "$CONFIG_FILE" | cut -d'=' -f2-
}

set_value() {
    local key=$1
    local value=$2

    if grep -q "^${key}=" "$CONFIG_FILE"; then
        sed -i "s|^${key}=.*|${key}=${value}|" "$CONFIG_FILE"
    else
        echo "${key}=${value}" >> "$CONFIG_FILE"
    fi
}

delete_key() {
    local key=$1
    sed -i "/^${key}=/d" "$CONFIG_FILE"
}

case "$ACTION" in
    get)
        [ -z "$KEY" ] && { echo "키를 지정하세요"; exit 1; }
        get_value "$KEY"
        ;;
    set)
        [ -z "$KEY" ] && { echo "키를 지정하세요"; exit 1; }
        set_value "$KEY" "$VALUE"
        ;;
    delete)
        [ -z "$KEY" ] && { echo "키를 지정하세요"; exit 1; }
        delete_key "$KEY"
        ;;
    list)
        cat "$CONFIG_FILE"
        ;;
    *)
        echo "Usage: $0   [key] [value]"
        exit 1
        ;;
esac

마무리

쉘 스크립트 고급 기법을 통해 복잡한 텍스트 처리, 프로세스 관리, 시그널 처리가 가능해졌습니다. 다음 편에서는 로그 관리와 모니터링을 통해 서버 상태를 효과적으로 파악하는 방법을 다룹니다.