개요


셸 스크립트 = 셸 명령을 파일에 모아 순차 실행하는 스크립트. 어떤 셸(인터프리터)로 실행할지는 로그인 셸이 아니라 파일 첫 줄 shebang이 결정한다.

  • bash = 리눅스 표준 셸. 스크립트 배포의 사실상 표준.
  • POSIX sh(dash/busybox)만 있는 최소 환경(alpine 등)도 있으니, 이식성이 필요하면 sh 문법만 쓴다.

실행 방법


#!/usr/bin/env bash
set -euo pipefail
 
echo "hello"
  • #!/usr/bin/env bash = shebang. 이 파일을 bash 로 실행하라는 지정. env 를 거치면 PATH 에서 bash 를 찾아 이식성이 높다.
    • shebang(셔뱅) = 스크립트 첫 줄 #! 로 시작하는 줄. 커널이 이 파일을 실행할 때 #! 뒤 인터프리터로 파일을 넘긴다. #!/bin/bash 면 bash가, #!/usr/bin/python3 면 python3가 이 파일을 읽어 실행.
    • /usr/bin/env 는 env 라는 실제 프로그램의 절대경로. shebang 엔 절대경로가 와야 하는데, bash 위치는 시스템마다 다르니(/bin/bash, macOS Homebrew는 /opt/homebrew/bin/bash…) #!/bin/bash 로 하드코딩해둬서 그 경로에 bash 가 없는 시스템에서 깨지는 대신 #!/usr/bin/env bash 로 env 를 절대경로로 부르고, env 가 PATH 를 뒤져 어디에 bash 가 있든 찾아 쓰므로 여러 시스템에서 동작한다는 뜻.
    • 트레이드오프: env 방식은 PATH 따라 다른 bash 가 잡힐 수 있어 예측성이 약간 낮다. 정확한 bash 강제가 필요하면 절대경로. 일반적으론 env 권장.
  • 실행: chmod +x script.sh 로 실행권한을 준 뒤 ./script.sh. 또는 bash script.sh 로 직접 넘긴다.
  • set -euo pipefail = 안전 옵션(관용).
    • -e = 명령이 실패(non-zero)하면 즉시 종료.
    • -u = 정의 안 된 변수를 쓰면 에러.
    • -o pipefail = 파이프 중간 명령이 실패해도 전체를 실패로 취급.

명령 탐색: PATH

셸이 ls / git 같은 외부 명령을 실행할 때, PATH 환경변수의 디렉터리들을 왼쪽부터 뒤져 처음 발견된 실행파일을 쓴다.

echo $PATH
# /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin
  • : 로 디렉터리를 구분. 왼쪽이 우선(앞에서 찾으면 뒤는 안 봄).
  • 그래서 export PATH="$HOME/.local/bin:$PATH" 는 내 바이너리를 맨 앞에 붙여 우선 검색하게 한다.
  • shebang #!/usr/bin/env bashenv 도 이 PATH 를 뒤져 bash 를 찾아 실행한다(경로만 반환하는 게 아니라 찾아서 실행).

type vs which vs file

type env
# env is /usr/bin/env
type cd
# cd is a shell builtin
  • type = 셸 빌트인. 이름을 명령으로 쓰면 무엇이 실행되는지(builtin/함수/alias/외부 경로) 알려준다. builtin/alias 까지 정확
  • which = 외부 실행파일의 경로만 찾는다. builtin/alias 는 못 봐서 type 이 더 정확
file /usr/bin/env
# /usr/bin/env: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]
# /usr/bin/env (for architecture x86_64): Mach-O 64-bit executable x86_64
# /usr/bin/env (for architecture arm64e): Mach-O 64-bit executable arm64e
  • file = 파일 내용(매직 바이트)으로 종류를 판별한다. 확장자가 아니라 실제 내용 기준.
  • Mach-O = macOS 실행 포맷(리눅스 ELF, 윈도우 PE). universal binary = 인텔/애플실리콘 코드를 한 파일에 담아 어느 맥에서나 동작.

source vs 실행

  • source file (= . file) = 파일을 현재 셸에서 실행
    • 그 안의 변수/export/함수가 현재 셸에 남는다
    • 설정 파일 로드에 사용
  • ./file = 자식 셸(subshell)에서 실행
    • 그 안의 변경은 현재 셸에 반영 안 됨
    • 환경변수를 지금 셸에 심으려면 반드시 source

변수


name="meatsby"        # = 양옆에 공백 금지
echo "$name"          # 참조는 항상 큰따옴표 권장
echo "${name}_suffix" # 변수명 경계가 모호하면 중괄호
  • 대입은 이름=값, = 주위에 공백을 넣으면 안 된다(name = "x"name 이라는 명령 실행으로 해석됨)
  • 참조는 $name, best practice: 거의 항상 "$name" 으로 큰따옴표를 감싼다(이유는 아래 단어 분리/glob)

$name vs ${name}

  • 기능은 동일. ${} 는 변수명 경계가 모호할 때 필요하다.
    • $name_suffix -> 셸이 name_suffix 라는 변수로 해석. ${name}_suffix 로 경계를 끊어야 한다.
  • ${} 는 확장 기능에도 쓰인다: ${var:-기본값} (없으면 기본값), ${var/a/b} (치환) 등. 아래 파라미터 확장 참고.

파라미터 확장

값을 꺼내면서 가공하는 문법. 원본 변수는 안 바뀌고 가공된 값이 그 자리에 확장된다.

치환 (bash/zsh 확장, sh 엔 없음) - pat 은 정규식이 아니라 glob 패턴이다.

s="a.b.c"
echo "${s/./_}"    # a_b.c   첫 매치만 치환
echo "${s//./_}"   # a_b_c   모든 매치 치환 (//)
echo "${s/.c/}"    # a.b     치환문자를 비우면 삭제
  • ${var/pat/rep} 첫 매치
  • ${var//pat/rep} 모든 매치
  • ${var/#pat/rep} 맨 앞
  • ${var/%pat/rep} 맨 끝

접두/접미 제거 - 경로/확장자에 자주 쓰고 POSIX 라 sh 에서도 된다.

f="/tmp/report.txt"
echo "${f##*/}"    # report.txt   앞에서 최장 제거 → basename
echo "${f%/*}"     # /tmp         뒤에서 최단 제거 → dirname
echo "${f%.txt}"   # /tmp/report  .txt 접미 제거
  • #/## 앞에서 제거(최단/최장)
  • %/%% 뒤에서 제거
  • 기본값:
    • ${var:-기본값} 없으면 기본값 사용
    • ${var:=기본값} 없으면 대입까지

단어 분리와 glob

변수를 확장한 직후 셸이 하는 두 처리. 이게 참조를 따옴표로 감싸야 하는 이유다.

단어 분리(word splitting) = 확장 결과를 공백으로 쪼개 여러 인자로 나눔.

files="a.txt b.txt"
rm $files      # → rm a.txt b.txt   (인자 2개)
rm "$files"    # → rm "a.txt b.txt" (인자 1개, 공백 포함 파일명)

glob = * ? [ ] 와일드카드를 실제 파일명으로 확장.

x="*.txt"
echo $x        # → 현재 폴더 txt 파일들로 확장 (의도치 않게)
echo "$x"      # → 문자열 *.txt 그대로

파일명 매칭용 패턴 문자들. 셸이 이걸 실제 존재하는 파일명으로 확장한다 (globbing).

  • * = 0개 이상의 임의 문자
    • 단, 경로 구분자 / 와 숨김파일 앞 . 은 기본 제외
  • ? = 정확히 1개의 임의 문자
  • [ ] = 대괄호 안 문자 집합 중 하나
    • [abc] = a/b/c 중 하나
    • [a-z] [0-9] = 범위
    • [!abc](또는 [^abc]) = 부정(그 외)
  • 정규식이 아니다. glob은 셸 레벨의 더 단순한 패턴이다.
  • 매치되는 파일이 있을 때만 확장된다. bash 기본에선 매치가 하나도 없으면 패턴이 문자 그대로 남는다
    • e.g. ls *.xyz 없으면 *.xyz 그대로 전달
  • 앞서 “따옴표로 감싸면 glob 이 안 일어난다”가 바로 이 확장을 막는 것. 값이 원형 그대로 유지된다.

셸 변수 vs 환경변수

  • 셸 변수 = 현재 셸 프로세스 안에서만 존재. 자식 프로세스는 못 본다.
  • 환경변수 = export 된 변수. 자식 프로세스에 복사되어 상속된다.
    export PATH="$HOME/.local/bin:$PATH"  # 자식이 이 PATH 를 봐야 하므로 export
  • scope: 둘 다 그 셸이 죽으면 사라진다(영속 아님). 그래서 매번 자동 세팅하려고 .zshrc/.zshenv 같은 시작 파일에 export 를 적어둔다.
    • 셸 변수 scope = 그 셸 하나.
    • 환경변수 scope = 그 셸 + 거기서 태어난 자식/손자(상속). 부모/형제엔 안 감.
  • 왜 분리? 모든 변수를 자식에 넘기면 환경이 오염되고 무거워진다. 임시 변수(루프 카운터 등)까지 상속할 이유가 없어, 기본은 셸 내부용이고 자식이 봐야 하는 것만 골라 export 한다.

조건문


if [[ -f "$file" ]]; then
  echo "있음"
elif [[ -d "$file" ]]; then
  echo "디렉터리"
else
  echo "없음"
fi
  • if COMMAND; then ... fi = 명령을 실행하고 그 종료 코드(0=성공=참)로 분기한다. if 자체는 test 기능이 없다.
  • [[ ... ]] / [ ... ] 이 조건을 평가해 참/거짓을 돌려주고, if는 그 결과만 본다.

test 연산자

파일 검사

  • -e 존재(종류 무관)
  • -f 일반 파일
  • -d 디렉터리
  • -L 심볼릭 링크
  • -r / -w / -x 읽기 / 쓰기 / 실행 권한
  • -s 존재 + 크기 > 0

문자열 검사

  • -z 비어 있음
  • -n 비어 있지 않음
  • str1 = str2 / str1 != str2 같다 / 다르다
    • [ ] / [[ ]] 공통
    • [[ ]] 안에선 = 와 == 가 완전히 동일 (둘 다 문자열 비교/패턴 매칭). 가독성상 ==를 흔히 쓴다
  • [[ str == pat* ]] 패턴 매칭
  • [[ str =~ regex ]] 정규식
    • [[ ]] 전용

숫자 비교

  • -eq -ne -lt -le -gt -ge = ==, !=, <, <=, >, >=

[[ ]] vs [ ]

  • [ ] = POSIX test 명령어
    • 사실 [ 라는 이름의 명령어고 마지막 인자가 ]
    • sh/dash/bash/zsh 어디서나 동작
    • 명령어라 인자가 단어 분리/glob 을 겪으므로 변수는 반드시 따옴표로 감싼다
      • e.g. [ -z "$x" ]
  • [[ ]] = 셸 키워드
    • bash/zsh/ksh 전용, POSIX sh 엔 없음
    • 명령어가 아니라 문법 구조라 단어 분리/glob 이 안 일어나 더 안전하다
    • &&||<> 를 직접 쓸 수 있고, == 로 패턴 매칭, =~ 로 정규식 매칭도 지원
  • = vs ==: [[ ]] 안에선 둘이 동일(관례상 ==). [ ](POSIX)에선 = 만 표준이고 == 는 비표준.
  • 결론: bash/zsh 스크립트는 [[ ]], 이식성(#!/bin/sh)이 필요하면 [ ].

[ ] 에서 따옴표를 빼면 값에 따라 깨진다:

x=""            # [ -z $x ]  → [ -z ]      인자 소실, 의도와 다르게 동작
x="a b"         # [ -z $x ]  → [ -z a b ]  too many arguments 에러

[ -z "$x" ] 로 감싸면 $x 가 뭐든 하나의 인자로 유지돼 안전. [[ -z $x ]] 는 단어 분리가 없어 따옴표 없이도 안전하지만 best practice 는 언제나 따옴표로 감싼 [[ -z "$x" ]] 를 사용하자.

논리 연산


[ -f "$config" ] && source "$config"   # 앞이 참일 때만 뒤 실행
mkdir dir || echo "실패"                # 앞이 실패할 때만 뒤 실행
  • A && B = A가 성공(참)일 때만 B 실행 (short-circuit)
  • A || B = A가 실패(거짓)일 때만 B 실행
  • [ 조건 ] && 명령한 줄짜리 if 로 자주 쓰인다

References