Linux 서버 관리 완전 정복 7편: 쉘 스크립트 고급
Linux Server Administration Complete Guide Part 7
Linux 서버 관리 완전 정복 시리즈
6편: 쉘 스크립트 기초 | 7편: 쉘 스크립트 고급 (현재) | 8편: 로그 관리
들어가며: 전문가 수준의 스크립팅
기초를 넘어 정규표현식, 텍스트 처리 도구(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
마무리
쉘 스크립트 고급 기법을 통해 복잡한 텍스트 처리, 프로세스 관리, 시그널 처리가 가능해졌습니다. 다음 편에서는 로그 관리와 모니터링을 통해 서버 상태를 효과적으로 파악하는 방법을 다룹니다.