들어가며: 쉘 스크립트로 자동화의 세계로

반복적인 작업을 수동으로 처리하는 것은 시간 낭비이자 실수의 원인입니다. 쉘 스크립트를 활용하면 복잡한 작업을 자동화하고, 일관성 있는 서버 관리가 가능합니다. 이번 편에서는 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

마무리

쉘 스크립트의 기초를 배웠습니다. 변수, 조건문, 반복문, 함수를 조합하여 다양한 자동화 스크립트를 작성할 수 있습니다. 다음 편에서는 정규표현식, 텍스트 처리, 고급 패턴 등 쉘 스크립트 고급 기법을 다룹니다.