개요
셸 스크립트 = 셸 명령을 파일에 모아 순차 실행하는 스크립트. 어떤 셸(인터프리터)로 실행할지는 로그인 셸이 아니라 파일 첫 줄 shebang이 결정한다.
bash= 리눅스 표준 셸. 스크립트 배포의 사실상 표준.- POSIX
sh(dash/busybox)만 있는 최소 환경(alpine 등)도 있으니, 이식성이 필요하면sh문법만 쓴다.
Good to know
sh= 원래 Bourne shell. 지금은 “POSIX 표준 최소 셸”을 가리키는 이름. 실제/bin/sh가 무슨 구현으로 링크되는지는 시스템마다 다르다.dash= Debian Almquist Shell. 가볍고 빠른 POSIX sh 구현. Debian/Ubuntu에선/bin/sh가 dash로 링크돼 있다. bash 아님. 그래서 우분투에서#!/bin/sh스크립트에 bash 기능 쓰면 깨진다.busybox= 임베디드/최소 리눅스용으로 수백 개 유닉스 명령을 하나의 작은 바이너리로 뭉친 것. 그 안의sh(ash 기반)도 POSIX 최소 셸.alpine= Alpine Linux. 매우 작은(5MB급) 리눅스 배포판. 도커 베이스 이미지로 인기. glibc 대신 musl, GNU coreutils 대신 busybox를 써서 bash가 기본으로 없고/bin/sh는 busybox ash다.
실행 방법
#!/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 권장.
- shebang(셔뱅) = 스크립트 첫 줄
- 실행:
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 bash의env도 이PATH를 뒤져bash를 찾아 실행한다(경로만 반환하는 게 아니라 찾아서 실행).
type vs which vs file
type env
# env is /usr/bin/env
type cd
# cd is a shell builtintype= 셸 빌트인. 이름을 명령으로 쓰면 무엇이 실행되는지(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 arm64efile= 파일 내용(매직 바이트)으로 종류를 판별한다. 확장자가 아니라 실제 내용 기준.- 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그대로 전달
- e.g.
- 앞서 “따옴표로 감싸면 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 "없음"
fiif 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 [ ]
[ ]= POSIXtest명령어- 사실
[라는 이름의 명령어고 마지막 인자가] sh/dash/bash/zsh어디서나 동작- 명령어라 인자가 단어 분리/glob 을 겪으므로 변수는 반드시 따옴표로 감싼다
- e.g.
[ -z "$x" ]
- e.g.
- 사실
[[ ]]= 셸 키워드bash/zsh/ksh전용, POSIXsh엔 없음- 명령어가 아니라 문법 구조라 단어 분리/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 로 자주 쓰인다