매일 백업하고 로그를 정리하는 일을 매번 손으로 하면 사람은 지치고 실수한다. 셸 스크립트는 별도 컴파일 없이 셸이 한 줄씩 해석·실행하는 인터프리터 프로그램이다. 이 영역은 1급 실기 시험의 중심이다.
스크립트 파일의 첫 줄에 오는 #!로 시작하는 줄을 shebang(셔뱅, 해시뱅)이라 부른다.
이 파일을 어떤 인터프리터로 실행할지 커널에게 알려준다.
shebang이 없으면 현재 셸로 실행되어 다른 셸에서는 문법이 깨질 수 있다. 항상 명시하는 것이 좋다.
#!/bin/bash# 위 줄이 shebang — 이 스크립트는 /bin/bash로 실행하라는 뜻echo "Hello, Shell Script"
#!/bin/bash # bash로 실행#!/bin/sh # POSIX sh로 실행#!/usr/bin/env python3 # PATH에서 python3을 찾아 실행 (이식성↑)
스크립트를 실행하는 방법은 세 가지다.
chmod +x script.sh # 실행 권한 부여./script.sh # ① 권한 부여 후 직접 실행 (shebang 사용)bash script.sh # ② bash에 인자로 넘겨 실행 (권한 불필요)source script.sh # ③ 현재 셸에서 실행 (변수가 현재 셸에 남음)
./script.sh(직접 실행)와 source script.sh(소싱)는 결정적으로 다르다. 직접 실행은 새 자식 셸을 띄워 그 안에서 돌리므로 스크립트 안에서 바꾼 변수가 끝나면 사라진다. 반면 source는 현재 셸 안에서 실행하므로 스크립트가 정의한 변수·함수가 그대로 남는다. 환경 설정 스크립트를 적용할 때 source를 쓰는 이유다(Day 1의 .bashrc source와 같은 원리).
⚠️ 함정: shebang의 #!는 반드시 파일의 맨 첫 줄, 맨 앞(1행 1열)에 와야 한다. 앞에 빈 줄이나 공백이 있으면 일반 주석으로 취급되어 무효가 된다. 또한 #! 뒤 인터프리터 경로는 절대 경로여야 한다.
스크립트 안의 변수는 Day 1에서 배운 셸 변수와 동일하다. 단, 대입할 때 = 양옆에 공백이 없어야 한다.
#!/bin/bashNAME="linux" # 올바름COUNT=10# NAME = "linux" # 틀림! 공백이 있으면 NAME이라는 명령으로 해석됨echo "$NAME 버전 $COUNT"
스크립트에 인자(argument)를 넘기면 위치 매개변수(positional parameter)로 받는다. 이것이 스크립트를 유연하게 만드는 핵심이다.
변수
의미
$0
스크립트 이름
$1 ~ $9
첫 번째 ~ 아홉 번째 인자
${10}
열 번째 이상 인자(중괄호 필수)
$#
전달된 인자의 개수
$*
모든 인자를 하나의 문자열로
$@
모든 인자를 각각 분리해서
$?
직전 명령의 종료 상태(0=성공)
$$
현재 스크립트의 PID
$!
마지막 백그라운드 작업의 PID
#!/bin/bashecho "스크립트 이름: $0"echo "첫 인자: $1, 둘째 인자: $2"echo "인자 개수: $#"echo "모든 인자: $@"# 실행: ./test.sh apple banana# 첫 인자: apple, 둘째 인자: banana / 인자 개수: 2
$*와 $@는 따옴표 없이 쓰면 동작이 같아 보이지만, 큰따옴표로 감쌌을 때 결정적으로 달라진다. "$*"는 모든 인자를 공백으로 이은 하나의 문자열("apple banana")이 되고, "$@"는 각 인자를 독립된 단어("apple" "banana")로 유지한다. 인자를 그대로 다른 명령에 넘길 때는 거의 항상 "$@"를 써야 공백 포함 인자가 깨지지 않는다.
$?(종료 상태)는 스크립트의 흐름 제어에 핵심이다. 모든 명령은 끝나면 0~255의 종료 코드를 남기는데, 0이 성공, 0이 아니면 실패다. 조건문은 이 종료 코드를 보고 분기한다. grep "root" /etc/passwd; echo $?처럼 직전 명령의 성패를 즉시 확인할 수 있다.
⚠️ 함정: 문자열 비교는 =, !=를, 숫자 비교는 -eq, -ne를 쓴다. 이걸 섞으면 안 된다. [ "$a" = "$b" ]는 문자열로, [ "$a" -eq "$b" ]는 숫자로 비교한다. 또 하나 — [ ] 안에서는 대괄호 양옆과 모든 연산자 양옆에 반드시 공백이 있어야 한다. [$a=$b]나 [ $a=$b ]는 문법 오류다. 그리고 변수는 빈 값일 때 문법이 깨지지 않도록 "$a"처럼 큰따옴표로 감싸는 것이 안전하다.
bash에는 [ ]보다 강력한 [[ ]](이중 대괄호)도 있다. [[ ]]는 bash 전용 확장으로, 내부에서 &&·||·패턴 매칭(== 와일드카드)·정규표현식(=~)을 쓸 수 있고, 변수의 단어 분리·글로빙이 자동으로 억제되어 더 안전하다. 다만 POSIX sh 호환이 필요하면 [ ]를 써야 한다. 1급에서는 두 표기를 모두 알아 두는 것이 좋다.
#!/bin/bashscore=$1if [ "$score" -ge 90 ]; then echo "A 등급"elif [ "$score" -ge 80 ]; then echo "B 등급"elif [ "$score" -ge 70 ]; then echo "C 등급"else echo "재시험"fi
문법의 뼈대는 if 조건; then ... elif 조건; then ... else ... fi다.
fi는 if를 거꾸로 쓴 것으로 블록의 끝을 표시한다(같은 방식으로 case는 esac로 닫는다).
# 파일 존재 여부로 분기하는 실전 예if [ -f /etc/nginx/nginx.conf ]; then echo "설정 파일이 있습니다"else echo "설정 파일이 없습니다" >&2 # 에러는 stderr로 exit 1fi
if는 사실 [ ]만 받는 게 아니라 임의의 명령을 받아 그 종료 코드로 분기한다. if grep -q "root" /etc/passwd; then ...처럼 명령을 직접 조건으로 쓸 수 있다. 종료 코드 0이면 then, 0이 아니면 else로 간다. [ ] 자체가 test라는 명령이기에 가능한 일이다.
#!/bin/bashcase "$1" in start) echo "서비스 시작" ;; stop) echo "서비스 중지" ;; restart|reload) # | 로 여러 패턴 매칭 echo "서비스 재시작" ;; *) # 그 외 모든 경우 (default) echo "사용법: $0 {start|stop|restart}" exit 1 ;;esac
각 패턴 블록은 ;;로 끝나고, *)는 모든 나머지 경우를 받는 기본값이다.
case의 패턴에는 와일드카드(*, ?, [ ])를 쓸 수 있다.
리눅스 init 스크립트(/etc/init.d/의 서비스 제어 스크립트)는 거의 모두 위 형태의 case "$1" in start|stop|restart구조로 작성돼 있다. 그래서 service nginx start나 /etc/init.d/nginx restart 같은 명령이 동작한다. case 문은 이런 "명령어 디스패처"를 만드는 표준 패턴이다.
#!/bin/bash# 목록 순회for fruit in apple banana cherry; do echo "과일: $fruit"done# 와일드카드로 파일 순회for file in *.log; do echo "처리 중: $file" gzip "$file"done# 시퀀스 순회for i in $(seq 1 5); do echo "반복 $i"done# C 스타일 (bash 확장)for ((i=1; i<=5; i++)); do echo "카운트 $i"done
#!/bin/bashcount=1while [ "$count" -le 5 ]; do echo "횟수: $count" count=$((count + 1)) # 산술 확장으로 증가done# 파일을 한 줄씩 읽기 (매우 자주 쓰는 관용구)while read line; do echo "줄: $line"done < /etc/hosts
until은 while의 반대로, 조건이 거짓인 동안 반복하다 참이 되면 멈춘다.
count=1until [ "$count" -gt 5 ]; do # count가 5 초과가 될 때까지 echo "$count" count=$((count + 1))done
반복 제어 키워드도 알아 둔다.
키워드
동작
break
반복문을 즉시 빠져나감
continue
현재 회차를 건너뛰고 다음 회차로
exit n
스크립트 전체를 종료(종료 코드 n)
while read line; do ...; done < file에서 입력을 done 뒤에 <로 연결하는 위치를 헷갈리기 쉽다. 또한 cat file | while read line처럼 파이프로 while에 넣으면, 파이프의 오른쪽이 서브셸에서 돌아 루프 안에서 바꾼 변수가 루프 밖에 반영되지 않는다(서브셸 변수 격리). 그래서 파일을 읽을 때는 파이프 대신 done < file 리다이렉션을 쓰는 것이 안전하다.
#!/bin/bash# 함수 정의 (두 가지 문법)greet() { echo "안녕하세요, $1님" # $1은 함수의 첫 인자}function backup() { # function 키워드 방식 (bash) cp "$1" "$1.bak" return 0 # 종료 코드 반환}# 함수 호출 — 괄호 없이 인자를 공백으로greet "관리자"backup /etc/hostsecho "백업 결과 코드: $?" # 함수의 return 값을 $?로 확인
함수의 인자도 스크립트처럼 $1, $2, $#로 받는다.
함수 안의 $1은 스크립트의 $1이 아니라 함수에 넘긴 인자다.
셸 함수는 값을 return으로 돌려줄 때 0~255의 종료 코드만 반환할 수 있다(일반 데이터를 반환하지 못함). 문자열이나 계산 결과를 돌려주려면 함수 안에서 echo로 출력하고 호출 측에서 명령 치환으로 받는다: result=$(my_func). return은 성공/실패 신호용, echo+$()는 데이터 전달용이라는 역할 분담을 기억해야 한다.
함수 안에서 선언한 변수도 기본적으로는 전역이라 스크립트 전체에 영향을 준다. 함수 안에서만 유효한 지역 변수를 만들려면 local var=값처럼 local 키워드를 붙인다. 큰 스크립트에서 함수가 의도치 않게 전역 변수를 덮어쓰는 버그를 막으려면 함수 내부 변수에 local을 쓰는 습관이 중요하다.
실기 대비의 핵심은 직접 작성하고 실행하며 오류를 만나 보는 것이다. 내일은 Week 3 전체를 종합 복습한다.