Linux 서버 관리 완전 정복 6편: 쉘 스크립트 기초
Linux Server Administration Complete Guide Part 6
Linux 서버 관리 완전 정복 시리즈
5편: SSH와 원격 관리 | 6편: 쉘 스크립트 기초 (현재) | 7편: 쉘 스크립트 고급
들어가며: 쉘 스크립트로 자동화의 세계로
반복적인 작업을 수동으로 처리하는 것은 시간 낭비이자 실수의 원인입니다. 쉘 스크립트를 활용하면 복잡한 작업을 자동화하고, 일관성 있는 서버 관리가 가능합니다. 이번 편에서는 Bash 쉘 스크립트의 기초를 다룹니다.
1. 쉘 스크립트 시작하기
1.1 첫 번째 스크립트
#!/bin/bash
# 첫 번째 쉘 스크립트
# 파일명: hello.sh
echo "Hello, World!"
echo "현재 시간: $(date)"
echo "현재 사용자: $USER"
echo "현재 디렉토리: $PWD"
1.2 스크립트 실행
# 스크립트 파일 생성
vim hello.sh
# 실행 권한 부여
chmod +x hello.sh
# 실행 방법들
./hello.sh # 현재 디렉토리에서 실행
bash hello.sh # bash로 직접 실행
/path/to/hello.sh # 절대 경로로 실행
# 실행 권한 없이 실행
bash hello.sh
sh hello.sh
1.3 Shebang 이해하기
#!/bin/bash # Bash 쉘 사용
#!/bin/sh # POSIX 호환 쉘 사용
#!/usr/bin/env bash # 환경에서 bash 찾기 (이식성 좋음)
#!/usr/bin/python3 # Python 스크립트
#!/usr/bin/perl # Perl 스크립트
2. 변수
2.1 변수 선언과 사용
#!/bin/bash
# 변수 선언 (= 양쪽에 공백 없음)
name="홍길동"
age=25
readonly PI=3.14159 # 읽기 전용 변수
# 변수 사용
echo "이름: $name"
echo "나이: ${age}세" # 중괄호 사용 권장
# 변수 삭제
unset name
# 기본값 설정
echo "${undefined_var:-기본값}" # 값이 없으면 기본값 출력
echo "${undefined_var:=새값}" # 값이 없으면 새값 할당
echo "${name:?변수가 없습니다}" # 값이 없으면 에러 출력
2.2 특수 변수
#!/bin/bash
# special_vars.sh arg1 arg2 arg3
echo "스크립트 이름: $0"
echo "첫 번째 인자: $1"
echo "두 번째 인자: $2"
echo "모든 인자: $@"
echo "모든 인자 (문자열): $*"
echo "인자 개수: $#"
echo "현재 프로세스 ID: $$"
echo "마지막 백그라운드 프로세스 ID: $!"
echo "마지막 명령 종료 코드: $?"
2.3 환경 변수
#!/bin/bash
# 주요 환경 변수
echo "홈 디렉토리: $HOME"
echo "현재 경로: $PATH"
echo "현재 쉘: $SHELL"
echo "사용자 이름: $USER"
echo "호스트 이름: $HOSTNAME"
echo "터미널 종류: $TERM"
# 환경 변수 설정
export MY_VAR="my value"
# 모든 환경 변수 확인
env
printenv
2.4 배열
#!/bin/bash
# 배열 선언
fruits=("사과" "바나나" "오렌지" "포도")
# 배열 접근
echo "첫 번째: ${fruits[0]}"
echo "세 번째: ${fruits[2]}"
echo "모든 요소: ${fruits[@]}"
echo "배열 길이: ${#fruits[@]}"
# 배열 수정
fruits[1]="망고"
fruits+=("키위") # 요소 추가
# 배열 순회
for fruit in "${fruits[@]}"; do
echo "과일: $fruit"
done
# 연관 배열 (Bash 4+)
declare -A person
person[name]="홍길동"
person[age]=25
person[city]="서울"
echo "이름: ${person[name]}"
echo "모든 키: ${!person[@]}"
3. 연산자
3.1 산술 연산
#!/bin/bash
a=10
b=3
# 산술 연산 방법들
echo "더하기: $((a + b))"
echo "빼기: $((a - b))"
echo "곱하기: $((a * b))"
echo "나누기: $((a / b))"
echo "나머지: $((a % b))"
echo "거듭제곱: $((a ** 2))"
# let 명령어
let "c = a + b"
echo "let 결과: $c"
# expr 명령어 (공백 필수)
result=$(expr $a + $b)
echo "expr 결과: $result"
# 증감 연산자
((a++))
echo "증가 후: $a"
# 소수점 계산 (bc 사용)
result=$(echo "scale=2; 10 / 3" | bc)
echo "소수점 계산: $result"
3.2 문자열 연산
#!/bin/bash
str="Hello, World!"
# 문자열 길이
echo "길이: ${#str}"
# 부분 문자열
echo "부분 문자열: ${str:0:5}" # Hello
echo "뒤에서 5글자: ${str: -5}" # orld!
# 문자열 치환
echo "치환: ${str/World/Korea}" # 첫 번째만
echo "모두 치환: ${str//o/O}" # 모든 o를 O로
# 문자열 삭제
filename="document.txt.bak"
echo "확장자 제거: ${filename%.*}" # document.txt
echo "첫 확장자까지 제거: ${filename%%.*}" # document
echo "경로 제거: ${filename#*/}"
echo "접두사 제거: ${filename##*.}" # bak
# 대소문자 변환 (Bash 4+)
text="Hello World"
echo "대문자: ${text^^}"
echo "소문자: ${text,,}"
4. 조건문
4.1 if 문
#!/bin/bash
age=20
# 기본 if 문
if [ $age -ge 18 ]; then
echo "성인입니다"
fi
# if-else
if [ $age -ge 18 ]; then
echo "성인입니다"
else
echo "미성년자입니다"
fi
# if-elif-else
if [ $age -lt 13 ]; then
echo "어린이입니다"
elif [ $age -lt 20 ]; then
echo "청소년입니다"
else
echo "성인입니다"
fi
# [[ ]] 사용 (더 많은 기능, Bash 전용)
name="홍길동"
if [[ $name == "홍길동" && $age -ge 18 ]]; then
echo "성인 홍길동님입니다"
fi
4.2 비교 연산자
#!/bin/bash
# 숫자 비교
a=10
b=20
[ $a -eq $b ] # 같음 (equal)
[ $a -ne $b ] # 다름 (not equal)
[ $a -lt $b ] # 작음 (less than)
[ $a -le $b ] # 작거나 같음 (less or equal)
[ $a -gt $b ] # 큼 (greater than)
[ $a -ge $b ] # 크거나 같음 (greater or equal)
# 문자열 비교
str1="hello"
str2="world"
[ "$str1" = "$str2" ] # 같음
[ "$str1" != "$str2" ] # 다름
[ -z "$str1" ] # 빈 문자열
[ -n "$str1" ] # 비어있지 않음
# [[ ]] 에서 문자열 비교
[[ $str1 == $str2 ]] # 같음
[[ $str1 < $str2 ]] # 사전순 비교
[[ $str1 =~ ^h ]] # 정규식 매칭
4.3 파일 테스트
#!/bin/bash
file="/etc/passwd"
dir="/home"
# 파일 존재 및 유형
[ -e $file ] # 존재함
[ -f $file ] # 일반 파일
[ -d $dir ] # 디렉토리
[ -L $file ] # 심볼릭 링크
[ -b $file ] # 블록 장치
[ -c $file ] # 문자 장치
[ -S $file ] # 소켓
# 파일 권한
[ -r $file ] # 읽기 가능
[ -w $file ] # 쓰기 가능
[ -x $file ] # 실행 가능
# 파일 속성
[ -s $file ] # 크기가 0이 아님
[ -O $file ] # 소유자가 현재 사용자
[ -G $file ] # 그룹이 현재 그룹
# 파일 비교
[ $file1 -nt $file2 ] # 더 새로움 (newer than)
[ $file1 -ot $file2 ] # 더 오래됨 (older than)
[ $file1 -ef $file2 ] # 같은 파일 (같은 inode)
# 예제
if [ -f "/etc/passwd" ] && [ -r "/etc/passwd" ]; then
echo "passwd 파일이 존재하고 읽을 수 있습니다"
fi
4.4 case 문
#!/bin/bash
echo "과일을 선택하세요 (1-4):"
echo "1) 사과"
echo "2) 바나나"
echo "3) 오렌지"
echo "4) 종료"
read choice
case $choice in
1)
echo "사과를 선택했습니다"
;;
2)
echo "바나나를 선택했습니다"
;;
3)
echo "오렌지를 선택했습니다"
;;
4)
echo "종료합니다"
exit 0
;;
*)
echo "잘못된 선택입니다"
;;
esac
# 패턴 매칭 예제
read -p "파일명 입력: " filename
case $filename in
*.txt)
echo "텍스트 파일입니다"
;;
*.jpg|*.png|*.gif)
echo "이미지 파일입니다"
;;
*.sh)
echo "쉘 스크립트입니다"
;;
*)
echo "알 수 없는 파일 형식입니다"
;;
esac
5. 반복문
5.1 for 문
#!/bin/bash
# 리스트 순회
for fruit in 사과 바나나 오렌지 포도; do
echo "과일: $fruit"
done
# 범위 순회
for i in {1..5}; do
echo "숫자: $i"
done
# 증가값 지정
for i in {0..10..2}; do
echo "짝수: $i"
done
# C 스타일 for 문
for ((i=0; i<5; i++)); do
echo "인덱스: $i"
done
# 파일 순회
for file in /var/log/*.log; do
echo "로그 파일: $file"
done
# 명령어 결과 순회
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "사용자: $user"
done
5.2 while 문
#!/bin/bash
# 기본 while
count=1
while [ $count -le 5 ]; do
echo "카운트: $count"
((count++))
done
# 무한 루프
while true; do
read -p "종료하려면 'q' 입력: " input
if [ "$input" = "q" ]; then
break
fi
done
# 파일 읽기
while IFS= read -r line; do
echo "라인: $line"
done < /etc/passwd
# 파이프와 함께
cat /etc/passwd | while read line; do
echo "$line"
done
# 여러 필드 읽기
while IFS=: read user pass uid gid desc home shell; do
echo "사용자: $user, UID: $uid, 홈: $home"
done < /etc/passwd
5.3 until 문
#!/bin/bash
# until: 조건이 참이 될 때까지 반복
count=1
until [ $count -gt 5 ]; do
echo "카운트: $count"
((count++))
done
# 서비스 대기 예제
until nc -z localhost 80 2>/dev/null; do
echo "웹 서버 시작 대기 중..."
sleep 1
done
echo "웹 서버가 시작되었습니다!"
5.4 break와 continue
#!/bin/bash
# break: 루프 종료
for i in {1..10}; do
if [ $i -eq 5 ]; then
echo "5에서 종료"
break
fi
echo "숫자: $i"
done
# continue: 다음 반복으로
for i in {1..10}; do
if [ $((i % 2)) -eq 0 ]; then
continue # 짝수는 건너뛰기
fi
echo "홀수: $i"
done
# 중첩 루프에서 break
for i in {1..3}; do
for j in {1..3}; do
if [ $j -eq 2 ]; then
break 2 # 바깥 루프까지 종료
fi
echo "i=$i, j=$j"
done
done
6. 함수
6.1 함수 정의와 호출
#!/bin/bash
# 함수 정의 방법 1
function greet {
echo "안녕하세요!"
}
# 함수 정의 방법 2 (권장)
greet2() {
echo "반갑습니다!"
}
# 함수 호출
greet
greet2
6.2 함수 인자
#!/bin/bash
# 인자 받는 함수
print_info() {
echo "이름: $1"
echo "나이: $2"
echo "모든 인자: $@"
echo "인자 개수: $#"
}
print_info "홍길동" 25
# 기본값 설정
greet() {
local name=${1:-"손님"}
echo "안녕하세요, ${name}님!"
}
greet "홍길동"
greet
6.3 반환값
#!/bin/bash
# return으로 종료 코드 반환 (0-255)
is_even() {
if [ $(($1 % 2)) -eq 0 ]; then
return 0 # 성공/참
else
return 1 # 실패/거짓
fi
}
if is_even 4; then
echo "4는 짝수입니다"
fi
# 값 반환 (echo 사용)
get_double() {
echo $(($1 * 2))
}
result=$(get_double 5)
echo "5의 두 배: $result"
# 여러 값 반환
get_stats() {
local sum=$(($1 + $2))
local diff=$(($1 - $2))
echo "$sum $diff"
}
read sum diff <<< $(get_stats 10 3)
echo "합: $sum, 차: $diff"
6.4 지역 변수
#!/bin/bash
global_var="전역"
test_scope() {
local local_var="지역"
global_var="함수에서 수정"
echo "함수 내 지역 변수: $local_var"
echo "함수 내 전역 변수: $global_var"
}
test_scope
echo "함수 밖 전역 변수: $global_var"
echo "함수 밖 지역 변수: $local_var" # 비어있음
7. 입력과 출력
7.1 사용자 입력
#!/bin/bash
# 기본 입력
echo "이름을 입력하세요:"
read name
echo "안녕하세요, $name님!"
# 프롬프트와 함께
read -p "나이를 입력하세요: " age
# 비밀번호 입력 (표시 안 함)
read -sp "비밀번호: " password
echo ""
# 타임아웃 설정
read -t 5 -p "5초 안에 입력하세요: " input
# 글자 수 제한
read -n 1 -p "계속하시겠습니까? (y/n): " answer
echo ""
# 배열로 읽기
read -a colors -p "좋아하는 색상들 (공백으로 구분): "
echo "첫 번째 색상: ${colors[0]}"
7.2 출력
#!/bin/bash
# echo
echo "일반 출력"
echo -n "줄바꿈 없이"
echo -e "탭\t줄바꿈\n특수문자"
# printf (형식 지정)
printf "이름: %s, 나이: %d\n" "홍길동" 25
printf "소수점: %.2f\n" 3.14159
printf "%-10s | %5d\n" "사과" 100
printf "%-10s | %5d\n" "바나나" 50
# 색상 출력
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
echo -e "${RED}에러 메시지${NC}"
echo -e "${GREEN}성공 메시지${NC}"
7.3 리다이렉션
#!/bin/bash
# 표준 출력 리다이렉션
echo "로그 내용" > logfile.txt # 덮어쓰기
echo "추가 내용" >> logfile.txt # 추가
# 표준 에러 리다이렉션
command 2> error.log # 에러만
command > output.log 2>&1 # 출력과 에러 모두
command &> all.log # 출력과 에러 모두 (Bash)
# 표준 입력 리다이렉션
while read line; do
echo "$line"
done < input.txt
# Here Document
cat << EOF
여러 줄의
텍스트를
한 번에 출력합니다.
변수 치환: $USER
EOF
# Here String
grep "pattern" <<< "검색할 문자열"
# 파이프
cat /etc/passwd | grep "root" | cut -d: -f1
8. 명령어 실행과 치환
8.1 명령어 치환
#!/bin/bash
# $() 사용 (권장)
current_date=$(date)
file_count=$(ls -1 | wc -l)
# 백틱 사용 (구형)
current_date=`date`
# 중첩 가능
inner=$(echo "Hello $(whoami)")
echo "현재 날짜: $current_date"
echo "파일 수: $file_count"
8.2 명령어 실행 결과 확인
#!/bin/bash
# 종료 코드 확인
ls /nonexistent 2>/dev/null
if [ $? -eq 0 ]; then
echo "성공"
else
echo "실패"
fi
# 직접 조건문에서 사용
if grep -q "root" /etc/passwd; then
echo "root 사용자가 있습니다"
fi
# && 와 ||
mkdir test_dir && echo "디렉토리 생성 성공"
rm nonexistent 2>/dev/null || echo "파일이 없습니다"
# 명령어 그룹
{ echo "첫 줄"; echo "둘째 줄"; } > output.txt
9. 실용적인 스크립트 예제
9.1 시스템 정보 스크립트
#!/bin/bash
# system_info.sh - 시스템 정보 출력
echo "========================================"
echo " 시스템 정보 리포트"
echo "========================================"
echo ""
echo "호스트명: $(hostname)"
echo "운영체제: $(uname -o)"
echo "커널 버전: $(uname -r)"
echo "아키텍처: $(uname -m)"
echo ""
echo "--- CPU 정보 ---"
echo "CPU: $(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2)"
echo "코어 수: $(nproc)"
echo ""
echo "--- 메모리 정보 ---"
free -h | grep -E "Mem|Swap"
echo ""
echo "--- 디스크 사용량 ---"
df -h | grep -E "^/dev"
echo ""
echo "--- 네트워크 정보 ---"
ip -4 addr show | grep inet | awk '{print $2}'
echo ""
echo "--- 시스템 가동시간 ---"
uptime
echo ""
echo "========================================"
9.2 백업 스크립트
#!/bin/bash
# backup.sh - 간단한 백업 스크립트
# 설정
SOURCE_DIR="/home/$USER/documents"
BACKUP_DIR="/home/$USER/backups"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${DATE}.tar.gz"
# 백업 디렉토리 생성
mkdir -p "$BACKUP_DIR"
# 백업 실행
echo "백업 시작: $SOURCE_DIR"
if tar -czf "${BACKUP_DIR}/${BACKUP_FILE}" -C "$(dirname $SOURCE_DIR)" "$(basename $SOURCE_DIR)" 2>/dev/null; then
echo "백업 완료: ${BACKUP_DIR}/${BACKUP_FILE}"
echo "파일 크기: $(du -h ${BACKUP_DIR}/${BACKUP_FILE} | cut -f1)"
else
echo "백업 실패!"
exit 1
fi
# 오래된 백업 삭제 (7일 이상)
echo ""
echo "오래된 백업 정리 중..."
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete
echo "완료!"
9.3 로그 모니터링 스크립트
#!/bin/bash
# log_monitor.sh - 로그 파일 모니터링
LOG_FILE="${1:-/var/log/syslog}"
KEYWORD="${2:-error}"
if [ ! -f "$LOG_FILE" ]; then
echo "파일이 존재하지 않습니다: $LOG_FILE"
exit 1
fi
echo "로그 모니터링 시작: $LOG_FILE"
echo "키워드: $KEYWORD"
echo "종료: Ctrl+C"
echo "----------------------------"
tail -f "$LOG_FILE" | while read line; do
if echo "$line" | grep -qi "$KEYWORD"; then
echo "[$(date '+%H:%M:%S')] $line"
fi
done
10. 디버깅
10.1 디버그 모드
#!/bin/bash
# 스크립트 전체 디버그
bash -x script.sh
# 스크립트 내에서 디버그 활성화
set -x # 디버그 시작
# 명령어들...
set +x # 디버그 종료
# 특정 부분만 디버그
debug() {
if [ "$DEBUG" = "1" ]; then
echo "[DEBUG] $*" >&2
fi
}
DEBUG=1
debug "이것은 디버그 메시지입니다"
10.2 에러 처리
#!/bin/bash
# 에러 발생 시 즉시 종료
set -e
# 미정의 변수 사용 시 에러
set -u
# 파이프 에러 감지
set -o pipefail
# 모두 설정
set -euo pipefail
# 에러 핸들러
trap 'echo "에러 발생: 라인 $LINENO"; exit 1' ERR
# 종료 시 정리
cleanup() {
echo "정리 작업 수행..."
rm -f /tmp/tempfile
}
trap cleanup EXIT
마무리
쉘 스크립트의 기초를 배웠습니다. 변수, 조건문, 반복문, 함수를 조합하여 다양한 자동화 스크립트를 작성할 수 있습니다. 다음 편에서는 정규표현식, 텍스트 처리, 고급 패턴 등 쉘 스크립트 고급 기법을 다룹니다.