🐧 리눅스 Bash 셸 기본 문법 완전 정리, 변수부터 함수까지 한 번에 배우기
💻 초보도 쉽게 배우는 Bash 셸 스크립트 문법 가이드
리눅스 환경에서 업무 자동화나 시스템 관리 작업을 하다 보면, 반복적인 명령어 입력 대신 셸 스크립트를 작성해 처리하는 것이 훨씬 효율적입니다.
특히 Bash 셸은 가장 널리 사용되는 셸로, 기본 문법만 잘 익혀도 파일 처리, 조건 분기, 반복 작업, 함수 정의까지 다양하게 활용할 수 있습니다.
이 글에서는 초보자도 이해할 수 있도록 변수 선언, 조건문 [ ] 활용법, for 반복문, 그리고 함수 정의까지 Bash의 핵심 문법을 하나씩 살펴보겠습니다.
복잡해 보이던 셸 스크립트가 훨씬 친근하게 느껴질 수 있도록 실제 예제와 함께 설명드릴 예정입니다.
단순히 문법만 나열하는 것이 아니라, 실제 개발과 서버 운영 현장에서 자주 사용하는 패턴과 주의사항도 함께 다룰 것입니다.
따라서 이 글을 끝까지 읽으면, 기초를 탄탄히 다지는 것은 물론 실무에도 바로 적용 가능한 스크립트를 작성할 수 있게 됩니다.
리눅스 초보는 물론, Bash를 다시 복습하려는 분들에게도 유용한 자료가 될 것입니다.
📄 Bash 변수 선언과 활용
Bash에서 변수는 자료형을 명시하지 않고 이름에 값을 대입하는 방식으로 선언합니다.
대입 시 등호 주변에 공백이 있으면 안 되며, 값 참조 시에는 보통 $변수명 형태를 사용합니다.
스크립트의 신뢰성을 높이려면 따옴표 규칙과 환경 변수, 매개변수 확장까지 함께 이해하는 것이 중요합니다.
이 파트에서는 기본 대입, 읽기 전용 처리, 환경 내보내기, 명령 치환, 산술 확장, 따옴표, 매개변수 확장, 배열까지 기초를 차근히 정리합니다.
✍️ 기본 선언과 참조
name=linux
echo "$name" # 값 참조
readonly name # 읽기 전용으로 고정(또는 declare -r name)
# name="change" # 오류: readonly 변수는 변경 불가
export PATH="$PATH:/opt/bin" # 환경 변수로 내보내기
tmp=$(date +%F) # 명령 치환: $(...)
sum=$((3 + 4)) # 산술 확장: $(( ... ))
읽기 전용은 readonly 또는 declare -r로 지정합니다.
하위 프로세스에 전달할 값은 export로 환경 변수로 승격합니다.
명령의 실행 결과를 변수에 담을 때는 $(명령) 구문을, 간단한 정수 계산은 $((식))을 사용합니다.
🧩 따옴표 규칙과 이스케이프
| 구분 | 설명 |
|---|---|
| 큰따옴표 ” “ | 변수와 명령 치환을 해석합니다. 공백 보호에 유리합니다. |
| 작은따옴표 ‘ ‘ | 문자 그대로 유지합니다. 변수/치환을 확장하지 않습니다. |
file="My Documents/report.txt"
echo "$file" # 공백 보존
echo '$file' # 그대로 $file 출력
text="hello"
echo "len=${#text}" # 길이: 5
💡 TIP: 확장을 의도할 때는 큰따옴표를, 문자열을 ‘그대로’ 둘 때는 작은따옴표를 사용하세요.
🧮 매개변수 확장과 기본값
name=${name:-"guest"} # 비어있으면 guest 사용
path=${path:="/tmp"} # 비어있으면 /tmp 대입
msg=${msg:?"msg가 필요합니다"} # 비어있으면 오류 후 종료
trim=${text:0:3} # 부분 문자열
len=${#text} # 길이
upper=${text^^} # 대문자 변환(Bash 4+)
lower=${text,,} # 소문자 변환(Bash 4+)
기본값 패턴은 스크립트의 안전성을 높입니다.
특히 ${var:?"에러"}는 필수 인자가 누락됐을 때 즉시 실패하도록 만들어 문제를 조기에 드러나게 합니다.
문자열 조작 연산자(^^, ,,)는 Bash 4 이상에서 지원된다는 점도 함께 기억해 두세요.
📦 배열과 연관배열
# 인덱스 배열
arr=(linux bash script)
echo "${arr[0]}" # linux
echo "${arr[@]}" # 모든 요소
echo "${#arr[@]}" # 길이
# 연관배열(딕셔너리): Bash 4+
declare -A conf
conf[host]="localhost"
conf[port]=8080
echo "${conf[host]}:${conf[port]}"
⚠️ 주의: 연관배열은 Bash 4 이상에서만 동작합니다.
일부 구형 배포판의 /bin/bash가 3.x인 경우가 있으니 버전을 확인하세요.
- ✍️대입은 공백 없이 작성합니다.
예:var=value - 🔒변경되면 안 되는 값은 readonly/declare -r로 보호합니다.
- 🌱자식 프로세스에 필요한 값은 export로 환경 변수화합니다.
- 🧩확장/공백 보존을 위해 큰따옴표 사용을 습관화합니다.
- 🛑필수 인자는
${var:?"필수"}로 검증합니다.
또는 스크립트 시작부에set -u를 고려합니다.
💬 변수 이름은 보통 대문자는 환경 변수, 소문자는 지역 변수 관례를 따릅니다.
의미 있는 이름과 일관된 따옴표 사용 습관이 디버깅 시간을 크게 줄여줍니다.
🔍 조건문 [ ] 기본 문법
Bash에서 조건 분기는 if와 case를 중심으로 이루어지며, 테스트 표현식에는 [ ], [[ ]], (( ))를 사용합니다.
[ ]는 POSIX 호환 테스트 명령이고, [[ ]]는 Bash 확장 테스트로 패턴 매칭과 안전한 비교가 가능합니다.
정수 비교에는 산술 컨텍스트 (( ))가 읽기 쉽습니다.
이 파트에서는 기본 문법, 자주 쓰는 파일·문자열·정수 비교 연산자, 논리 결합, case 분기, 그리고 안전한 따옴표 습관까지 핵심만 정리합니다.
🧱 if, elif, else 구조와 [ ]
file="data.txt"
if [ -f "$file" ]; then
echo "일반 파일입니다"
elif [ -d "$file" ]; then
echo "디렉터리입니다"
else
echo "해당 경로가 없습니다"
fi
중요 포인트는 대괄호 양끝의 공백과 각 연산자 사이 공백을 반드시 지키는 것입니다.
경로와 문자열 비교 시에는 공백 안전성을 위해 항상 큰따옴표로 감싸 주세요.
🧰 자주 쓰는 테스트 연산자 요약
| 분류 | 연산자 | 설명 |
|---|---|---|
| 파일 | -e, -f, -d, -s, -r, -w, -x | 존재, 일반파일, 디렉터리, 크기>0, 읽기/쓰기/실행 가능 |
| 문자열 | =, !=, -n, -z | 같다, 다르다, 길이>0, 길이=0 |
| 정수 | -eq, -ne, -lt, -le, -gt, -ge | 같다, 다르다, <, ≤, >, ≥ |
논리 결합은 [ ... ] && [ ... ], [ ... ] || [ ... ] 형태로 쓰는 것이 가장 안전합니다.
-a, -o 연산자는 [ ]에서 모호성을 유발하므로 피하는 것이 좋습니다.
🧠 [[ ]]와 (( )) 활용
name="report.log"
# Bash 확장 테스트(권장)
if [[ $name == *.log ]]; then # 글롭 패턴 매칭
echo "로그 파일"
fi
# 정규식 매칭
line="id=123"
if [[ $line =~ ^id=([0-9]+)$ ]]; then
echo "캡처된 숫자: ${BASH_REMATCH[1]}"
fi
# 산술 비교
n=5
if (( n >= 3 )); then
echo "3 이상"
fi
💡 TIP: [[ ]] 내부는 단어 분할과 글롭 확장이 덜 일어나 안전합니다.
좌변·우변에 따옴표를 필수로 쓰지 않아도 되는 경우가 많지만, 변수가 비어 있을 수 있다면 큰따옴표를 유지하세요.
🔀 case 분기와 패턴
cmd="$1"
case "$cmd" in
start|up) echo "서비스 시작" ;;
stop|down) echo "서비스 중지" ;;
restart) echo "재시작" ;;
*) echo "사용법: $0 {start|stop|restart}" ; exit 1 ;;
esac
여러 값을 한 줄에서 매칭하거나 와일드카드 *로 기본 분기를 처리할 수 있습니다.
입력 인자는 반드시 따옴표로 감싸서 공백 또는 특수문자에 안전하도록 합니다.
- 📐
[좌우, 연산자 사이에는 공백이 필요합니다. - 📝문자열과 경로 비교 시에는 항상 큰따옴표로 감쌉니다.
- 🧩정수 비교는 (( )), 패턴·정규식은 [[ ]]가 편리합니다.
- 🔗논리 결합은
&&,||로 명확히 표현하세요.
⚠️ 주의: [[ ]]는 Bash/ksh 확장으로 순수 POSIX 셸에서는 동작하지 않습니다.
이식성을 최우선으로 할 때는 [ ]만 사용하거나, 스크립트 해시뱅을 #!/usr/bin/env bash로 명확히 선언하세요.
💬 조건식은 결국 ‘마지막 명령의 종료 상태(0=성공, 그 외=실패)’로 판단합니다.
명령 자체를 if 조건에 넣는 패턴도 유용합니다.
예:if grep -q "ERROR" app.log; then ... fi
🔄 for 반복문 사용법
Bash의 for는 목록을 순회하는 전통형과, 정수 증감을 다루는 C-스타일 두 가지가 널리 쓰입니다.
글롭(와일드카드)·브레이스 확장으로 목록을 만들거나, 명령 치환으로 동적으로 아이템을 공급할 수도 있습니다.
반복문은 파일 처리, 배치 작업, 로그 스캔 등 자동화에서 핵심이므로, 안전한 따옴표 규칙과 IFS·셸 옵션을 함께 이해하면 품질이 크게 향상됩니다.
🧾 전통형 for: 목록 순회
# 1) 명시적 목록
for color in red green blue; do
echo "$color"
done
# 2) 글롭(와일드카드)
for f in *.log; do
echo "처리: $f"
done
# 3) 브레이스 확장
for n in {01..05}; do
echo "job-$n"
done
# 4) 인자 순회
for arg in "$@"; do
echo "ARG: $arg"
done
파일 이름과 인자는 항상 큰따옴표로 감싸 공백·특수문자에 안전하게 처리하세요.
브레이스 확장은 Bash가 문자열을 생성하므로 외부 명령에 의존하지 않는 장점이 있습니다.
🧮 C-스타일 for: 정수 반복
for ((i=0; i<5; i++)); do
printf "i=%d\n" "$i"
done
# 역순/증분 제어
for ((i=10; i>=0; i-=2)); do
echo "$i"
done
산술 비교와 증감이 명확해 숫자 범위를 다룰 때 가독성이 좋습니다.
조건·증감식을 유연하게 구성할 수 있어 인덱스 기반 배열 처리에도 자주 사용됩니다.
🧰 break, continue, nullglob
shopt -s nullglob # 매칭 없을 때 그대로 '*.log'가 아닌 빈 목록
for f in *.log; do
[[ ! -s "$f" ]] && continue # 빈 파일은 건너뛰기
grep -q "ERROR" "$f" && { echo "에러 발견"; break; }
done
shopt -u nullglob
nullglob을 켜면 패턴이 매칭되지 않을 때 루프가 *.log라는 리터럴을 받지 않으므로 예기치 않은 동작을 줄일 수 있습니다.
break는 즉시 탈출, continue는 다음 반복으로 넘어갑니다.
🪄 안전한 목록 생성과 명령 치환
| 방법 | 예시 | 특징 |
|---|---|---|
| 브레이스 | {1..5} |
빠르고 의존성 없음 |
seq |
$(seq 1 5) |
외부 명령 의존, 범위 유연 |
| 글롭 | *.txt |
파일 목록 자동 생성 |
# 명령 치환 결과를 안전하게 순회하려면 IFS 조정 대신 while-read를 고려
# (줄 단위, 공백/특수문자 안전)
while IFS= read -r line; do
printf 'LINE: %s\n' "$line"
done < <(printf '%s\n' "a b" "c,d" "e")
💡 TIP: 공백이 포함된 항목을 안전하게 처리하려면 for word in $(cmd)보다 while IFS= read -r 패턴을 고려하세요.
줄 단위로 안정적입니다.
🧩 배열과 함께 쓰는 for
arr=(alpha "beta gamma" delta)
# 값 순회
for v in "${arr[@]}"; do
echo "$v"
done
# 인덱스 순회
for ((i=0; i<${#arr[@]}; i++)); do
printf '%d:%s\n' "$i" "${arr[i]}"
done
- 🧷항목은 항상 따옴표로 감싸 공백·글롭 확장을 방지합니다.
- 🎛️파일 패턴 순회 시 nullglob을 켜면 의도치 않은 리터럴 아이템을 피할 수 있습니다.
- 🧮숫자 범위는
for (( ... )), 간단 목록은 전통형for가 편리합니다. - 📦배열 값 순회는
"${arr[@]}"를 사용하고,"${arr[*]}"는 한 문자열로 결합됨을 이해합니다.
⚠️ 주의: for x in $(cmd)는 공백·탭·개행으로 단어 분할이 일어나 파일명 파손 위험이 큽니다.
가능하면 while IFS= read -r 또는 널 구분(-print0, -d '') 패턴을 사용하세요.
💬
for는 ‘미리 준비된 목록을 소비’한다는 관점으로 이해하면 쉽습니다.
목록 생성은 브레이스·글롭·명령 치환 등 어떤 방식이든 가능하지만, 안전을 위해 항상 따옴표와 셸 옵션을 점검하세요.
🛠️ Bash 함수 정의와 호출
Bash 함수는 명령어 묶음을 이름으로 재사용하도록 만드는 핵심 도구입니다.
반복되는 패턴을 함수로 캡슐화하면 스크립트 가독성과 유지보수성이 크게 향상됩니다.
이 섹션에서는 함수 정의 문법, 인자 처리, 반환 규칙, 지역 변수 스코프, 오류 전파, 모듈화(라이브러리)까지 실전에 필요한 내용을 차근히 정리합니다.
또한 표준 출력과 종료 상태의 차이, return과 echo의 역할 분리, set -e·pipefail 등 신뢰성 옵션까지 함께 다룹니다.
🧱 기본 문법과 호출
# 두 표기 모두 가능
hello() {
echo "Hello $1"
}
function bye() {
echo "Bye $1"
}
hello "Linux"
bye "Bash"
함수 본문은 중괄호로 감싸며 마지막 명령의 종료 상태가 함수의 종료 상태가 됩니다.
따라서 성공/실패를 호출 측에 명확히 알리고 싶다면 return 또는 실패 시 명령이 0이 아닌 코드를 반환하도록 구성합니다.
🧭 인자, 표준출력, 반환 코드
sum() {
local a="$1" b="$2"
echo $(( a + b )) # 결과는 '표준 출력'으로 반환
}
safe_div() {
local a="$1" b="$2"
(( b == 0 )) && { echo "division by zero" >&2; return 2; }
echo $(( a / b ))
}
res=$(sum 2 3) # 캡처: 표준 출력
if safe_div 10 0 > /dev/null; then
echo "ok"
else
echo "failed, code=$?"
fi
Bash에는 “값 반환” 구문이 없으므로 계산 결과는 보통 표준 출력으로 내보내고 호출 측에서 치환해 받습니다.
성공/실패 신호는 종료 상태($?)로 전달합니다.
오류 메시지는 표준 에러(>&2)로 분리하면 파이프라인에서 데이터와 로그를 구분할 수 있습니다.
🧪 매개변수 처리와 사용법
| 기호 | 의미 | 비고 |
|---|---|---|
$1, $2 ... |
위치 인자 | 항상 큰따옴표 권장 |
$# |
인자 개수 | 필수값 검증에 활용 |
$@ |
각 인자를 개별 보존 | "$@"로 감싸기 |
$* |
한 문자열로 결합 | 잘 쓰지 않음 |
require_two() {
(( $# < 2 )) && { echo "need 2+ args" >&2; return 1; }
echo "OK: $1, $2"
}
print_all() {
for a in "$@"; do
printf '> %s\n' "$a"
done
}
🧪 지역 변수와 스코프
counter=0
bump() {
local counter=100 # 지역 변수(부모의 counter와 별개)
(( counter++ ))
echo "inner=$counter"
}
bump
echo "outer=$counter" # 0
함수 내부에서 local을 사용하면 외부 변수 오염을 방지할 수 있습니다.
배열과 연관배열도 local -a, local -A로 지역화할 수 있습니다.
서브셸 ( ... )을 쓰면 환경 자체를 분리해 부작용을 더 줄일 수 있지만, 변경이 부모 셸에 반영되지 않는 점을 이해해야 합니다.
🛡️ 에러 처리, set -e, trap
set -Eeuo pipefail
# -e: 오류 시 종료, -E: trap ERR 전파, -u: 정의되지 않은 변수 오류, pipefail: 파이프 중간 실패 감지
log_err() { echo "ERR: $*" >&2; }
trap 'log_err "line $LINENO status=$?"' ERR
must_exist() {
[[ -f "$1" ]] || { log_err "file not found: $1"; return 1; }
}
⚠️ 주의: set -e는 서브셸, 조건식, 파이프라인 맥락에서 예외가 있습니다.
중요 로직에서는 명시적 체크(cmd || { ...; return 1; })로 실패를 다루는 습관을 권장합니다.
📦 함수 라이브러리와 재사용
# lib.sh — 공용 함수 모음
die() { echo "ERROR: $*" >&2; exit 1; }
require() { command -v "$1" >/dev/null || die "need '$1'"; }
# main.sh — 라이브러리 포함
#!/usr/bin/env bash
set -euo pipefail
. "./lib.sh" # 또는 source ./lib.sh
require "jq"
echo "ready"
💡 TIP: 대형 스크립트는 기능별로 파일을 나누고 source로 로드하세요.
테스트 용이성과 재사용성이 좋아집니다.
함수 목록은 declare -F로 확인하고, 하위 셸에 내보내려면 export -f func를 사용할 수 있습니다.
- 📌계산 결과는 표준 출력, 성공/실패는 종료 코드로 구분합니다.
- 🧷외부 변수 오염 방지를 위해 local을 적극 사용합니다.
- 🧯오류 메시지는 표준 에러로 분리합니다.
- 🧩라이브러리는 파일로 분리하고
source하여 재사용합니다.
💬 함수는 “데이터는 표준 출력, 제어는 종료 코드”라는 원칙을 지키면 테스트와 조합이 쉬워집니다.
이 원칙 하나만 익혀도 Bash 스크립트의 품질이 눈에 띄게 올라갑니다.
❓ 자주 묻는 질문 (FAQ)
변수는 꼭 미리 선언해야 하나요?
단, 읽기 전용이 필요하면
readonly 또는 declare -r를 사용하고, 하위 프로세스에 전달하려면 export로 환경 변수화하세요.
[ ]와 [[ ]]의 차이는 무엇인가요?
[ ]는 POSIX 테스트 명령으로 이식성이 좋습니다.[[ ]]는 Bash 확장으로 패턴 매칭(== *.log)과 정규식(=~)이 가능하고 단어 분할 위험이 줄어듭니다.순수 POSIX 셸 호환이 필요하면
[ ]만 사용하세요.
정수 비교는 -eq 대신 (( ))를 써도 되나요?
(( ))는 산술 컨텍스트로 (( a > b ))처럼 가독성이 좋습니다.다만
(( ))도 Bash 확장이므로 /bin/sh 이식성을 요구하면 [ ]와 -eq/-lt를 사용하세요.
for 루프에서 공백이 있는 파일명을 안전하게 처리하려면?
for x in $(cmd) 대신 while IFS= read -r 패턴을 사용하세요.파일 목록은
find -print0와 -d '' 조합이 가장 안전합니다.
함수에서 값을 어떻게 반환하나요?
echo)으로 내보내고 호출부에서 $(...)로 받습니다.성공/실패는 종료 코드(
return N 또는 마지막 명령의 상태)로 표현하고, 오류 메시지는 표준 에러(>&2)로 분리하세요.
스크립트 첫 줄의 해시뱅은 꼭 필요할까요?
#!/usr/bin/env bash)이 없으면 기본 셸이 무엇인지 보장되지 않습니다.Bash 기능([[ ]], (( ))) 등을 쓴다면 해시뱅으로 해석기를 명시하세요.
set -e와 pipefail은 언제 써야 하나요?
set -Eeuo pipefail을 고려하세요.다만 조건식/파이프라인의 예외를 이해해야 하며, 중요한 구간은
cmd || { ...; return 1; }처럼 명시적으로 처리하는 습관이 좋습니다.
source와 실행의 차이는 무엇인가요?
source file.sh 또는 . file.sh는 현재 셸 컨텍스트에서 코드를 읽어 실행하여 변수/함수를 그대로 남깁니다.반면
./file.sh는 새 프로세스에서 실행되며 현재 셸의 변수에는 영향을 주지 않습니다.
🧰 Bash 셸 기초, 변수부터 함수까지 핵심만 압축 정리
이 글에서는 리눅스 환경에서 가장 널리 쓰이는 Bash 셸의 기초 문법을 실무 관점에서 정리했습니다.
변수 선언과 참조 규칙, 따옴표 사용, 매개변수 확장과 배열을 통해 안전하고 유연한 데이터 처리를 익혔습니다.
조건문은 [ ], [[ ]], (( ))의 차이와 파일·문자열·정수 비교 연산자를 중심으로, case 패턴까지 실제 예제로 살폈습니다.
반복문은 전통형 for와 C-스타일 for를 비교하며 글롭, 브레이스 확장, nullglob 옵션 등 안전한 목록 순회 요령을 다뤘습니다.
함수 파트에서는 인자 처리, 표준 출력과 종료 코드의 역할 분리, local 스코프, 오류 전파, set -Eeuo pipefail과 trap 사용으로 신뢰성을 높이는 방법을 정리했습니다.
이제 기본기를 바탕으로 스크립트를 작은 함수로 모듈화하고, 입력 검증과 에러 처리 원칙을 지키며 자동화를 확장해 보세요.
🏷️ 관련 태그 : Bash, 리눅스, 셸스크립트, 변수, 조건문, 대괄호테스트, for문, 함수, 배열, 스크립트예제