ls *.txt에서 별을 처리하는 건 ls가 아니다 — 셸과 확장

ls *.txt를 친다. ls*.txt를 처리할까? 아니다. 셸이 먼저 *.txta.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/shbash를 가리킨다(RHEL 전통). 단, sh로 호출하면 POSIX 제한 모드로 동작한다.

셸이 명령을 실행하기 전 거치는 확장 — 따옴표가 핵심

셸은 명령을 실행하기 전 여러 확장(expansion)을 거친다. 이 순서를 모르면 이상한 동작의 원인을 못 찾는다:

  1. 브레이스 확장: a{b,c}ab ac
  2. 틸드 확장: ~ → 홈 디렉토리
  3. 변수/명령/산술 확장: $VAR, $(cmd), $((1+2))
  4. 워드 분리(IFS 기준)
  5. 경로명 확장(glob): *, ?, [abc]
  6. 리다이렉션/파이프 적용
  7. 최종 명령 실행

파일 이름에 공백이 있을 때 재앙이 일어나는 이유

초보자가 자주 겪는 재앙: rm $file을 쳤는데 파일 이름에 공백이 있었다. 셸이 워드 분리를 해버려 my file.txtmyfile.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 cmdtype 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 -eif/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(공백 없음). $?는 직전 종료 코드.

생각해 볼 문제

  1. ls *.txt를 칠 때 *를 처리하는 주체는 누구인가?
  2. '...'"..."의 차이는? 파일 이름에 공백이 있을 때 왜 "..."를 써야 하는가?
  3. 셸 변수와 환경변수의 차이. 자식 프로세스는 무엇을 상속받는가?
  4. 현재 디렉토리가 PATH에 없는 이유(보안)는?
  5. cmd > /dev/null 2>&1의 의미를 풀어 설명하라.
  6. &&;의 차이는?
  7. [ ][[ ]]의 차이. 현대 bash에서 어느 것을 권장하는가?
  8. 스크립트에 set -e가 있을 때의 효과와 한계는?

참고

'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