ls *.txt에서 별을 처리하는 건 ls가 아니다 — 셸과 확장
ls *.txt를 친다. ls가 *.txt를 처리할까? 아니다. 셸이 먼저 *.txt를 a.txt b.txt c.txt로 펼쳐서(확장) ls에 넘긴다. ls는 이미 펼쳐진 파일 이름만 받는다. 이 사실을 모르면 셸 스크립트의 이상한 동작 원인을 평생 못 찾는다.
셸은 단순한 명령 실행기가 아니다. 명령을 해석하고, 변수·glob·산술을 치환(확장)하며, 파이프·리다이렉션·제어문까지 처리하는 인터프리터다. cron, systemd unit, 배포 스크립트, 설정 자동화 — 리눅스 관리의 상당수가 셸 기반이다. 이 글은 셸이 명령을 어떻게 해석하는지, 그리고 그 해석을 스크립트로 어떻게 묶는지를 다룬다.
셸은 명령 번역사다
flowchart LR
U[사용자/스크립트] --> SH[셸 - bash]
SH --> K[커널 시스템 콜]
K --> HW[하드웨어]
사용자(또는 스크립트)가 명령을 치면 → 셸이 해석 → 커널 시스템 콜 → 실행.
- bash(Bourne Again Shell): GNU 프로젝트, 대부분 리눅스의 기본 로그인 셸. Bourne shell(
sh) 호환 + 확장. - Rocky 10의
/bin/sh는 bash를 가리킨다(RHEL 전통). 단,sh로 호출하면 POSIX 제한 모드로 동작한다.
셸이 명령을 실행하기 전 거치는 확장 — 따옴표가 핵심
셸은 명령을 실행하기 전 여러 확장(expansion)을 거친다. 이 순서를 모르면 이상한 동작의 원인을 못 찾는다:
- 브레이스 확장:
a{b,c}→ab ac - 틸드 확장:
~→ 홈 디렉토리 - 변수/명령/산술 확장:
$VAR,$(cmd),$((1+2)) - 워드 분리(IFS 기준)
- 경로명 확장(glob):
*,?,[abc] - 리다이렉션/파이프 적용
- 최종 명령 실행
파일 이름에 공백이 있을 때 재앙이 일어나는 이유
초보자가 자주 겪는 재앙: rm $file을 쳤는데 파일 이름에 공백이 있었다. 셸이 워드 분리를 해버려 my file.txt가 my와 file.txt 두 인자로 쪼개지고, 각각을 지우려다 사고가 난다. 항상 "$file"(큰따옴표)로 감싸야 한다.
" "(큰따옴표): 변수 확장 O, glob X.' '(작은따옴표): 모든 확장 X(문자 그대로).- 따옴표 없음: glob·워드 분리 다 됨(위험).
변수 — 환경변수만 자식에게 간다
| 종류 | 범위 | 선언 | 예 |
|---|---|---|---|
| 셸 변수 | 현재 셸만 | VAR=value |
NAME=rocky |
| 환경변수 | 자식 프로세스에 상속 | export VAR=value |
export PATH=... |
자식 프로세스는 환경변수만 물려받는다. 스크립트 안에서 설정한 셸 변수는 호출한 셸에 영향을 주지 않는다. 부모 셸에 적용하려면 source로 실행해야 한다.
주요 환경변수:
| 변수 | 의미 |
|---|---|
PATH |
명령 검색 디렉토리 목록(: 구분) |
HOME |
현재 사용자 홈 |
USER / LOGNAME |
사용자명 |
SHELL |
로그인 셸 경로 |
PWD |
현재 작업 디렉토리 |
LANG / LC_* |
로케일 |
PATH와 명령 검색
명령을 치면 셸은 PATH의 각 디렉토리를 순서대로 찾는다. 없으면 "command not found". 현재 디렉토리(.)는 보통 PATH에 없다(보안상 — 공격자가 현재 디렉토리에 가짜 ls를 두면 실행될 수 있으므로). 그래서 ./script.sh로 명시한다. 명령 위치는 which cmd나 type cmd로 확인. 한 번 찾은 경로는 hash 캐시에 기억되니, 새 위치로 옮긴 명령이 안 될 땐 hash -r.
입출력 리다이렉션·파이프
파일 디스크립터: 0=stdin, 1=stdout, 2=stderr.
| 표현 | 효과 |
|---|---|
cmd > file |
표준출력을 file로(덮어쓰기) |
cmd >> file |
표준출력을 file에 추가 |
cmd < file |
file에서 표준입력 읽기 |
cmd 2> file |
표준에러를 file로 |
cmd 2>&1 |
표준에러를 표준출력으로 합침 |
cmd &> file |
표준출력+에러 모두 file로(bash) |
cmd > /dev/null 2>&1 |
모두 폐기 |
파이프 cmd1 | cmd2: cmd1의 표준출력을 cmd2의 표준입력으로 연결. ls | grep txt | wc -l. tee는 화면과 파일에 동시 출력(cmd | tee out.log).
메타문자·연산자
| 기호 | 의미 |
|---|---|
* / ? / [abc] |
glob(0개 이상 / 1개 / 문자집합) |
{a,b,c} |
브레이스 확장 |
~ |
홈 |
; |
명령 순차 실행(앞 결과 무관) |
&& |
앞 명령 성공 시 다음(단락 평가) |
|| |
앞 명령 실패 시 다음 |
$(cmd) |
명령 치환(`cmd` 대신 권장) |
$((expr)) |
산술 확장 |
# |
주석 |
스크립트 문법 — 제어 흐름 묶기
shebang과 실행
#!/bin/bash
# 더 이식성 높게: #!/usr/bin/env bash
실행 권한을 주고 ./script.sh, 또는 bash script.sh(실행 권한 불필요). shebang이 없으면 현재 셸로 해석되어 동작이 달라질 수 있으니 항상 명시하라.
변수·인자
#!/bin/bash
greeting="Hello" # 할당 (= 양옆 공백 없음!)
name=$1 # 첫 번째 인자
count=$# # 인자 개수
all="$@" # 모든 인자(따옴표 시 각각 보존)
exit_code=$? # 직전 명령 종료 코드 (0=성공)
echo "$greeting, $name" # 큰따옴표 권장
조건문 — [ ] vs [[ ]]
if grep -q "root" /etc/passwd; then
echo "root 존재"
elif [ ... ]; then
echo "다른 경우"
else
echo "나머지"
fi
[[ $a == *.txt ]] # 패턴 매칭([[ ]]만)
[ -f /etc/hosts ] # 파일 존재
[ -z "$var" ] # 빈 문자열
[ $num -gt 5 ] # 정수 비교(-eq -ne -lt -le -gt -ge)
현대 bash에선 [[ ]]를 권장한다 — 단어 분리가 안 되고, 패턴/정규식을 지원하고, 더 안전하다.
반복문·case
for f in *.txt; do echo "$f"; done
for ((i=0; i<5; i++)); do echo "$i"; done
while [ "$count" -lt 10 ]; do count=$((count+1)); done
while IFS= read -r line; do echo "$line"; done < input.txt
case "$1" in
start) systemctl start nginx ;;
stop) systemctl stop nginx ;;
*) echo "usage: $0 {start|stop}"; exit 1 ;;
esac
함수
greet() {
local name="$1" # local: 함수 내 한정
echo "Hi, $name"
return 0 # 종료 코드만(0~255)
}
msg=$(greet "world") # 출력을 변수로 회수
return은 종료 코드만 반환한다. 데이터를 돌려받으려면 echo + 명령 치환($(...)).
안전 모드 — 모든 스크립트에 권장
set -e # 오류 시 즉시 종료
set -u # 미정의 변수 사용 시 오류
set -o pipefail # 파이프 어느 단계 실패 시 전체 실패
# 한 줄: set -Eeuo pipefail
단, set -e는 if/while 조건문 안의 실패나 파이프 마지막 단계만 검사한다. 파이프 전체를 잡으려면 set -o pipefail로 보완.
Rocky 10에서 직접 확인하기
미검증(출처 인용). 표준 bash라 Ubuntu/WSL에서도 대부분 동일.
echo "$SHELL" # /bin/bash
echo "$BASH_VERSION" # 5.x
ps -p $$ -o comm= # 현재 셸
cat /etc/shells # 사용 가능 셸 목록
env | sort | head
echo "$PATH"
which systemctl
export PATH="$PATH:/opt/myapp/bin" # 현재 세션 임시 추가
실용 스크립트 예(백업):
#!/bin/bash
set -Eeuo pipefail
src="${1:?usage: $0 <src> <dest>}"
dest="${2:?usage: $0 <src> <dest>}"
ts=$(date +%Y%m%d-%H%M%S)
if [[ ! -d "$src" ]]; then
echo "error: $src not a directory" >&2
exit 1
fi
tar -czf "${dest}/backup-${ts}.tar.gz" -C "$src" .
echo "backup done: ${dest}/backup-${ts}.tar.gz"
# ~/.bashrc 에 추가해 별칭·함수 영구화
alias ll='ls -lh'
mkcd() { mkdir -p "$1" && cd "$1"; }
source ~/.bashrc # 적용
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
"VAR = value" |
오류. VAR=value(공백 없음)만 정상 |
"rm $file 안전하다" |
파일명에 공백 있으면 재앙. 항상 "$file" |
"[ $a > $b ] 정수 비교" |
리다이렉션으로 해석(위험). -gt/-lt 또는 (( a > b )) |
"$?는 마지막 결과" |
직전 명령 코드. 사이에 다른 명령 끼면 덮어씀 |
"&&는 항상 다음 실행" |
단락 평가. 앞 실패 시 다음 안 됨. 정리 무조건 필요하면 trap |
"스크립트 안 export가 부모 셸에 영향" |
아니다. 자식에만. 부모에 적용은 source |
| "shebang 없어도 된다" | 현재 셸로 해석될 수 있어 동작 다를 수 있음. 항상 shebang 명시 |
"set -e는 모든 실패를 잡는다" |
if/while 조건문 안 실패나 파이프 마지막 단계만 검사. set -o pipefail로 보완 |
더 깊이
- 프로세스 치환:
<(cmd)는 명령 출력을 임시 파일처럼 쓰게 한다 —diff <(ls dir1) <(ls dir2). 임시 파일 없이 두 명령 출력을 비교. - 배열·연관배열:
arr=(a b c),declare -A m; m[name]="alice"(bash 4+). - trap으로 정리:
trap 'cleanup' EXIT로 스크립트 종료 시(정상/오류/시그널) 항상 정리 함수를 실행. 임시 파일 제거·잠금 해제에 필수.
요약 — 이 글의 결론
- 셸(bash) = 명령 해석기 + 스크립트 언어.
/bin/sh는 bash(RHEL). - 확장 순서: 브레이스 → 틸드 → 변수/명령/산술 → 워드분리 → glob → 리다이렉션 → 실행.
ls *.txt에서 별을 펼치는 건 셸이다. - 따옴표:
" "(변수 O, glob X),' '(모두 X). 파일명엔 항상"..."— 공백 재앙을 막는다. - 환경변수(
export)만 자식 상속. 셸 변수는 현재 셸만.PATH엔.이 없다(보안). - 리다이렉션:
>(덮어쓰기),>>(추가),2>&1(에러를 출력으로),&>(모두). [[ ]]>[ ]: 더 안전, 패턴 매칭 지원.- 안전 모드:
set -Eeuo pipefail. 단set -e는 pipefail로 보완해야 파이프 실패를 잡는다. - 변수 할당:
VAR=value(공백 없음).$?는 직전 종료 코드.
생각해 볼 문제
ls *.txt를 칠 때*를 처리하는 주체는 누구인가?'...'와"..."의 차이는? 파일 이름에 공백이 있을 때 왜"..."를 써야 하는가?- 셸 변수와 환경변수의 차이. 자식 프로세스는 무엇을 상속받는가?
- 현재 디렉토리가
PATH에 없는 이유(보안)는? cmd > /dev/null 2>&1의 의미를 풀어 설명하라.&&와;의 차이는?[ ]와[[ ]]의 차이. 현대 bash에서 어느 것을 권장하는가?- 스크립트에
set -e가 있을 때의 효과와 한계는?
참고
- Bash Reference Manual — GNU - 접근 2026-07-09
- Advanced Bash-Scripting Guide - 접근 2026-07-09
- man page:
man bash,man test,man hier,man printf(1) - POSIX Shell: pubs.opengroup.org
'Tech Artifacts > Linux' 카테고리의 다른 글
| Linux - 09. network (0) | 2026.07.09 |
|---|---|
| Linux - 08. package (0) | 2026.07.09 |
| Linux - 06. io device (0) | 2026.07.09 |
| Linux - 05. filesystem (0) | 2026.07.09 |
| Linux - 04. memory (0) | 2026.07.09 |
