이미지 로딩 중...
AI Generated
2025. 11. 17. · 2 Views
쉘 스크립트 기본 문법과 구조 완벽 가이드
리눅스 쉘 스크립트의 기본 문법부터 명령어 구조, 공백 처리, 코드 블록, 명령어 치환까지 실무에서 바로 활용할 수 있는 핵심 개념을 초보자도 이해하기 쉽게 설명합니다. 실제 작동하는 예제와 함께 쉘 스크립팅의 기초를 탄탄하게 다질 수 있습니다.
목차
- 명령어와 인자 구조
- 세미콜론과 명령어 구분
- 공백과 탭의 중요성
- 코드 블록과 그룹핑
- [명령어 치환 ($() vs
)](#명령어-치환-($()-vs-)) - 종료 상태 코드 ($?)
1. 명령어와 인자 구조
시작하며
여러분이 리눅스 터미널에서 파일을 복사하거나 디렉토리를 만들 때, "왜 어떤 명령어는 되고 어떤 명령어는 안 되지?"라고 궁금해하신 적 있나요? 예를 들어 cp file1file2는 작동하지 않지만 cp file1 file2는 작동합니다.
이런 문제는 쉘 명령어의 기본 구조를 제대로 이해하지 못해서 발생합니다. 명령어, 옵션, 인자가 어떻게 구분되는지 모르면 오타처럼 보이는 에러를 계속 만나게 됩니다.
바로 이럴 때 필요한 것이 명령어와 인자의 구조 이해입니다. 쉘이 어떻게 여러분의 명령을 해석하는지 알면, 더 이상 시행착오를 겪지 않고 정확한 명령을 작성할 수 있습니다.
개요
간단히 말해서, 쉘 명령어는 레고 블록처럼 조립되는 구조입니다. 첫 번째 블록은 명령어 이름, 그 다음은 옵션(선택사항), 마지막은 인자(대상)로 구성됩니다.
왜 이 구조가 필요할까요? 쉘은 여러분이 입력한 한 줄의 텍스트를 공백으로 나누어 각각의 의미를 파악합니다.
예를 들어, 여러 개의 파일을 한 번에 복사하거나, 특정 옵션으로 명령어의 동작을 바꿀 때 이 구조가 매우 유용합니다. 기존에는 GUI에서 마우스로 클릭했다면, 이제는 명령어 한 줄로 훨씬 빠르게 작업할 수 있습니다.
이 구조의 핵심 특징은 세 가지입니다: 1) 명령어는 항상 맨 앞에 위치, 2) 옵션은 하이픈(-)으로 시작, 3) 인자는 명령어가 작업할 대상입니다. 이러한 특징들이 쉘 명령어를 체계적이고 예측 가능하게 만들어줍니다.
코드 예제
# 기본 형식: 명령어 [옵션] [인자1] [인자2] ...
ls -l /home/user
# 여러 인자를 받는 명령어
cp -r source_dir dest_dir backup_dir
# 옵션을 여러 개 사용
tar -czf archive.tar.gz folder/
# 긴 옵션 사용
ls --all --human-readable
# 인자 없이 명령어만
pwd
# 옵션 없이 명령어와 인자만
cd /var/log
설명
이것이 하는 일: 쉘은 여러분이 입력한 명령줄을 공백 기준으로 분리하여 첫 번째 단어를 실행할 프로그램으로, 나머지를 그 프로그램에 전달할 정보로 인식합니다. 첫 번째로, ls -l /home/user에서 ls는 파일 목록을 보여주는 명령어입니다.
쉘은 이 명령어를 찾아서 실행할 준비를 합니다. 왜 맨 앞에 있어야 할까요?
쉘은 항상 첫 번째 단어를 "무엇을 실행할지"로 해석하기 때문입니다. 그 다음으로, -l은 옵션입니다.
옵션은 명령어의 동작 방식을 바꿉니다. -l은 "자세한(long) 형식으로 보여줘"라는 의미입니다.
하이픈이 붙으면 쉘은 "아, 이건 명령어를 어떻게 실행할지에 대한 설정이구나"라고 이해합니다. 마지막으로, /home/user는 인자입니다.
이것은 "어디에 대해" 또는 "무엇을" 작업할지를 지정합니다. ls 명령어가 이 경로의 파일들을 보여주게 됩니다.
최종적으로 "/home/user 디렉토리의 파일 목록을 자세히 보여주기"라는 완전한 명령이 완성됩니다. 여러분이 이 구조를 이해하면 어떤 명령어든 쉽게 해석하고 작성할 수 있습니다.
tar -czf archive.tar.gz folder/를 보면, tar는 압축 명령어, -czf는 "압축하고(c) gzip으로(z) 파일로(f)" 만들라는 옵션, archive.tar.gz는 생성할 파일명, folder/는 압축할 대상이라는 걸 바로 알 수 있습니다. 또한 명령어를 직접 작성할 때도 실수를 줄일 수 있고, 매뉴얼을 읽을 때도 훨씬 빠르게 이해할 수 있습니다.
실전 팁
💡 옵션을 합칠 수 있습니다: -c -z -f 대신 -czf처럼 짧게 쓸 수 있어요. 하지만 -f처럼 값을 받는 옵션은 항상 마지막에 두세요.
💡 긴 옵션은 더 읽기 쉽습니다: -a 대신 --all처럼 쓰면 나중에 스크립트를 볼 때 무슨 의미인지 바로 알 수 있어요.
💡 공백이 포함된 인자는 따옴표로 감싸세요: cp "my file.txt" backup/처럼 하지 않으면 쉘이 두 개의 인자로 인식합니다.
💡 --help 옵션으로 사용법을 확인하세요: 거의 모든 명령어에서 ls --help처럼 사용하면 옵션과 인자 형식을 바로 볼 수 있습니다.
💡 인자 순서가 중요합니다: cp source dest와 cp dest source는 완전히 다른 결과를 만듭니다. 항상 매뉴얼에서 순서를 확인하세요.
2. 세미콜론과 명령어 구분
시작하며
여러분이 서버에서 여러 작업을 연속으로 해야 할 때, 일일이 명령어를 하나씩 입력하고 엔터를 누르는 게 번거롭다고 느껴본 적 있나요? 예를 들어 디렉토리를 만들고, 그 안에 파일을 만들고, 권한을 설정하는 작업을 할 때 말이죠.
이런 반복 작업은 시간 낭비일 뿐 아니라, 중간에 실수로 다른 명령어를 입력할 위험도 있습니다. 특히 자동화 스크립트를 작성할 때는 여러 명령을 하나로 묶을 수 있어야 합니다.
바로 이럴 때 필요한 것이 세미콜론(;)을 이용한 명령어 구분입니다. 한 줄에 여러 명령어를 작성하여 순차적으로 실행할 수 있게 해줍니다.
개요
간단히 말해서, 세미콜론은 여러 문장을 하나로 합치는 마침표와 같은 역할을 합니다. 마치 책에서 문장과 문장을 구분하듯, 쉘 명령어도 세미콜론으로 구분할 수 있습니다.
왜 이게 필요할까요? 여러 명령을 자동으로 실행할 때, 일일이 엔터를 누를 수 없습니다.
또한 조건 없이 무조건 순서대로 실행하고 싶을 때 매우 유용합니다. 예를 들어, 백업 폴더를 만들고 파일을 복사한 다음 로그를 남기는 작업을 한 줄로 처리할 수 있습니다.
기존에는 각 명령어를 엔터로 구분하여 실행했다면, 이제는 세미콜론으로 연결하여 한 번에 실행할 수 있습니다. 핵심 특징은 두 가지입니다: 1) 앞 명령어의 성공/실패와 무관하게 다음 명령어가 실행됨, 2) 각 명령어는 독립적으로 작동함.
이러한 특징 덕분에 간단한 순차 작업을 빠르게 처리할 수 있습니다.
코드 예제
# 여러 명령을 순차적으로 실행
cd /tmp; mkdir test_dir; cd test_dir
# 각 명령은 독립적 - 앞이 실패해도 다음 실행됨
rm nonexistent.txt; echo "작업 완료"
# 백업 작업 예시
mkdir -p backup; cp *.txt backup/; ls backup/
# 로그 기록과 함께 실행
date >> log.txt; echo "작업 시작" >> log.txt; ./script.sh
# 한 줄로 개발 환경 준비
cd ~/project; git pull; npm install; npm start
# 정리 작업 자동화
rm -rf temp/; mkdir temp; touch temp/placeholder
설명
이것이 하는 일: 쉘은 세미콜론을 만나면 "여기서 하나의 명령이 끝났구나, 다음 명령을 실행하자"라고 판단합니다. 마치 여러분이 할 일 목록을 순서대로 처리하는 것과 같습니다.
첫 번째로, cd /tmp; mkdir test_dir; cd test_dir에서 쉘은 첫 번째 명령인 cd /tmp를 실행합니다. 이것은 /tmp 디렉토리로 이동하는 명령입니다.
왜 세미콜론을 쓸까요? 엔터를 누르면 쉘이 입력 대기 상태가 되지만, 세미콜론을 쓰면 자동으로 다음 명령으로 넘어갑니다.
그 다음으로, 첫 번째 명령이 완료되면 쉘은 세미콜론 다음의 mkdir test_dir를 실행합니다. 중요한 점은 앞 명령이 실패해도 이 명령은 실행된다는 것입니다.
예를 들어 /tmp가 없어서 cd가 실패해도 mkdir은 현재 디렉토리에서 실행됩니다. 마지막으로, cd test_dir이 실행되어 새로 만든 디렉토리로 이동합니다.
최종적으로 세 개의 명령이 순차적으로 완료되어, /tmp에 test_dir을 만들고 그 안으로 이동하는 작업이 끝납니다. 여러분이 세미콜론을 사용하면 반복 작업을 자동화하고, 여러 단계의 설정을 한 번에 처리할 수 있습니다.
서버 초기 설정이나 빌드 파이프라인에서 특히 유용합니다. 또한 cron 작업이나 일회성 명령을 실행할 때 여러 작업을 하나로 묶어서 관리하기 쉽게 만들 수 있습니다.
다만 에러 처리가 필요하다면 나중에 배울 &&나 || 연산자를 사용하는 게 더 안전합니다.
실전 팁
💡 에러에 민감한 작업에는 && 를 사용하세요: 세미콜론 대신 &&를 쓰면 앞 명령이 성공했을 때만 다음 명령이 실행됩니다.
💡 가독성을 위해 적절히 줄바꿈하세요: 스크립트 파일에서는 세미콜론보다 실제 줄바꿈이 더 읽기 쉽습니다.
💡 디버깅할 때는 명령을 분리하세요: 어느 단계에서 문제가 생겼는지 파악하려면 세미콜론으로 묶지 말고 하나씩 실행해보세요.
💡 set -e로 스크립트 안전성 높이기: 스크립트 시작에 set -e를 추가하면 어떤 명령이 실패하든 즉시 중단됩니다.
3. 공백과 탭의 중요성
시작하며
여러분이 변수를 할당하려고 NAME = "John"이라고 썼는데 "command not found" 에러가 나서 당황한 적 있나요? 또는 조건문에서 [ $x=5 ]라고 썼는데 원하는 대로 작동하지 않았던 경험이 있으신가요?
이런 문제는 쉘 스크립트에서 가장 흔하게 발생하는 실수입니다. 공백 하나 때문에 완전히 다른 의미로 해석되거나 에러가 발생합니다.
프로그래밍 언어 대부분은 공백에 관대하지만, 쉘은 매우 엄격합니다. 바로 이럴 때 필요한 것이 공백과 탭의 규칙 이해입니다.
어디에 공백을 넣고, 어디에 넣으면 안 되는지 알면 신비한 에러들이 모두 사라집니다.
개요
간단히 말해서, 쉘에서 공백은 단순한 여백이 아니라 의미를 구분하는 중요한 구분자입니다. 마치 한국어에서 띄어쓰기가 의미를 바꾸는 것처럼, 쉘에서도 공백이 전혀 다른 명령을 만들어냅니다.
왜 이렇게 엄격할까요? 쉘은 1970년대에 만들어진 오래된 시스템이라 공백을 인자 구분자로 사용합니다.
변수 할당에서 공백을 허용하면 명령어와 구분할 수 없기 때문입니다. 예를 들어, NAME=value는 변수 할당이지만 NAME = value는 NAME이라는 명령어를 실행하려는 시도가 됩니다.
기존 프로그래밍 언어에서는 x = 5 같은 문법이 자연스럽다면, 쉘에서는 반드시 x=5처럼 공백 없이 써야 합니다. 핵심 규칙은 세 가지입니다: 1) 변수 할당 시 등호 양쪽에 공백 금지, 2) 조건문 대괄호 내부에는 반드시 공백 필요, 3) 명령어와 인자는 반드시 공백으로 구분.
이 규칙들을 지키지 않으면 문법 에러나 예상치 못한 동작이 발생합니다.
코드 예제
# 올바른 변수 할당 - 공백 없음
NAME="John Doe"
COUNT=42
# 잘못된 변수 할당 - 에러 발생
# NAME = "John" # NAME이라는 명령어로 인식됨
# 조건문에서는 공백 필수
if [ $COUNT -eq 42 ]; then
echo "정답"
fi
# 공백이 없으면 문자열로 인식됨
# [ $COUNT=42 ] # 항상 참이 되는 버그
# 배열 요소는 공백으로 구분
FRUITS=("apple" "banana" "orange")
# 함수 정의 - 괄호와 중괄호 사이 공백
myfunc() {
echo "함수 실행"
}
설명
이것이 하는 일: 쉘은 공백을 보고 "여기서부터 새로운 단어가 시작된다"고 판단합니다. 따라서 공백의 위치에 따라 명령어, 옵션, 인자, 변수 할당 등을 다르게 해석합니다.
첫 번째로, NAME="John Doe"에서 등호 양쪽에 공백이 없는 이유는 쉘이 이것을 하나의 할당 문장으로 인식하게 하기 위함입니다. 만약 NAME = "John Doe"라고 쓰면 쉘은 "NAME이라는 프로그램을 실행하고, =과 "John Doe"를 인자로 전달하라"고 이해합니다.
왜냐하면 공백이 명령어와 인자의 구분자이기 때문입니다. 그 다음으로, 조건문 [ $COUNT -eq 42 ]에서는 대괄호 안쪽에 공백이 반드시 필요합니다.
[는 실제로 test라는 명령어의 별칭이고, ]는 마지막 인자입니다. 따라서 [$COUNT-eq42]처럼 쓰면 쉘은 [$COUNT-eq42]라는 하나의 문자열로 인식하여 에러가 발생합니다.
공백이 있어야 [, $COUNT, -eq, 42, ]로 제대로 분리됩니다. 마지막으로, 배열 FRUITS=("apple" "banana" "orange")에서 각 요소는 공백으로 구분됩니다.
만약 "apple""banana"처럼 붙여 쓰면 하나의 문자열 "applebanana"가 됩니다. 최종적으로 공백은 단순한 서식이 아니라 쉘 문법의 핵심 부분입니다.
여러분이 이 규칙을 익히면 90%의 초보자 에러를 피할 수 있습니다. 특히 변수 할당과 조건문에서 공백 규칙만 정확히 알아도 디버깅 시간을 크게 줄일 수 있습니다.
또한 다른 사람의 스크립트를 읽을 때도 왜 공백이 그렇게 배치되었는지 이해할 수 있어 코드 리뷰와 유지보수가 훨씬 쉬워집니다.
실전 팁
💡 ShellCheck 도구를 사용하세요: 온라인이나 에디터 플러그인으로 공백 관련 실수를 자동으로 찾아줍니다.
💡 등호 주변 공백은 무조건 제거: VAR=value만 기억하세요. 다른 언어 습관을 버려야 합니다.
💡 조건문은 항상 [ 공백 조건 공백 ] 형식: 이걸 템플릿처럼 외우면 실수가 줄어듭니다.
💡 따옴표 안의 공백은 보존됩니다: "hello world"는 공백 두 개가 그대로 유지되지만, 따옴표 밖에서는 하나로 줄어듭니다.
💡 탭과 공백을 섞지 마세요: 특히 heredoc이나 들여쓰기에서 일관성 있게 하나만 사용하세요.
4. 코드 블록과 그룹핑
시작하며
여러분이 조건문이나 반복문에서 여러 명령어를 실행하고 싶을 때, 어떻게 묶어야 할지 고민한 적 있나요? 또는 여러 명령의 출력을 한 번에 리다이렉트하고 싶은데 방법을 몰라서 고생한 경험이 있으신가요?
이런 상황은 복잡한 스크립트를 작성할 때 자주 발생합니다. 개별 명령어로는 한계가 있고, 여러 명령을 하나의 단위로 다뤄야 할 때가 많습니다.
특히 조건에 따라 여러 작업을 동시에 실행하거나, 파이프라인으로 연결할 때 필수적입니다. 바로 이럴 때 필요한 것이 코드 블록과 그룹핑입니다.
중괄호 {}나 소괄호 ()를 사용하여 여러 명령을 하나로 묶어 관리할 수 있습니다.
개요
간단히 말해서, 코드 블록은 여러 명령어를 하나의 묶음으로 만드는 컨테이너입니다. 마치 물건을 상자에 담아서 한 번에 옮기는 것처럼, 명령어들을 묶어서 한 단위로 처리할 수 있습니다.
왜 이게 필요할까요? 조건문 안에서 여러 작업을 수행하거나, 여러 명령의 출력을 하나의 파일로 보내거나, 서브쉘에서 독립적으로 실행할 때 매우 유용합니다.
예를 들어, 로그 파일을 생성할 때 여러 명령의 출력을 하나의 파일에 모으는 경우에 사용됩니다. 기존에는 각 명령마다 리다이렉션을 따로 써야 했다면, 이제는 그룹으로 묶어서 한 번만 리다이렉션하면 됩니다.
핵심은 두 가지 방식입니다: 1) 중괄호 {}는 현재 쉘에서 실행되고 효율적임, 2) 소괄호 ()는 서브쉘에서 실행되어 변수 변경이 격리됨. 중괄호는 빠르지만 변수가 공유되고, 소괄호는 느리지만 안전하게 격리됩니다.
코드 예제
# 중괄호 그룹 - 현재 쉘에서 실행
{
echo "작업 시작"
date
ls -l
echo "작업 완료"
} > report.txt
# 소괄호 서브쉘 - 변수 변경이 격리됨
DIR="/tmp"
(cd $DIR; rm -f temp*; echo "현재 위치: $PWD")
echo "원래 위치: $PWD" # 원래 디렉토리에 그대로 있음
# 조건문에서 블록 사용
if [ -f "config.txt" ]; then
{
echo "설정 파일 발견"
cat config.txt
echo "설정 로드 완료"
} >> log.txt
fi
# 반복문과 함께 사용
for file in *.txt; do
{
echo "=== $file ==="
cat "$file"
}
done > combined.txt
# 파이프라인과 그룹핑
{ echo "Line 1"; echo "Line 2"; echo "Line 3"; } | grep "2"
설명
이것이 하는 일: 쉘은 중괄호나 소괄호로 묶인 명령들을 하나의 단위로 인식하여, 전체에 대한 리다이렉션이나 파이프를 적용할 수 있게 해줍니다. 첫 번째로, 중괄호 그룹 { 명령들; } > file에서 쉘은 중괄호 안의 모든 명령을 순차적으로 실행하고, 그 출력을 모두 하나의 파일로 보냅니다.
왜 중괄호를 쓸까요? 각 명령마다 >> report.txt를 붙이는 것보다 훨씬 깔끔하고 실수를 줄일 수 있기 때문입니다.
중요한 점은 중괄호 뒤에 세미콜론이나 줄바꿈이 필요하고, 중괄호 안쪽에 공백이 있어야 한다는 것입니다. 그 다음으로, 소괄호 서브쉘 (cd $DIR; rm -f temp*)는 새로운 쉘 프로세스를 만들어서 그 안에서 명령을 실행합니다.
서브쉘 안에서 디렉토리를 변경하거나 변수를 수정해도 원래 쉘에는 영향을 주지 않습니다. 예제에서 서브쉘 안에서 /tmp로 이동했지만, 서브쉘이 끝나면 원래 디렉토리로 자동으로 돌아옵니다.
마지막으로, 조건문이나 반복문에서 블록을 사용하면 여러 작업을 조건에 따라 한꺼번에 처리할 수 있습니다. if 문 안의 중괄호 블록은 모든 로그를 하나의 파일에 추가합니다.
최종적으로 코드 블록은 복잡한 스크립트를 체계적으로 구조화하고, 리다이렉션과 파이프를 효율적으로 사용할 수 있게 해줍니다. 여러분이 그룹핑을 사용하면 스크립트가 훨씬 깔끔해지고, 중복 코드를 줄일 수 있습니다.
특히 로그 생성, 리포트 작성, 복잡한 조건 처리에서 빛을 발합니다. 또한 서브쉘을 활용하면 임시로 환경을 바꿔야 하는 작업을 안전하게 처리할 수 있어, 스크립트의 부작용을 최소화할 수 있습니다.
실전 팁
💡 중괄호는 공백과 세미콜론 필수: { cmd; }처럼 여는 중괄호 뒤와 닫는 중괄호 앞에 공백, 마지막 명령 뒤에 세미콜론이 필요합니다.
💡 성능이 중요하면 중괄호 사용: 서브쉘은 새 프로세스를 만들어서 느리므로, 격리가 필요없다면 중괄호를 쓰세요.
💡 서브쉘로 안전하게 실험: 위험한 명령을 실행하기 전에 (cd /; rm -rf test)처럼 서브쉘로 감싸면 디렉토리 변경이 격리됩니다.
💡 파이프라인에서 그룹핑 활용: { cmd1; cmd2; } | sort처럼 여러 명령의 출력을 한 번에 처리할 수 있습니다.
💡 중괄호 블록은 함수처럼 사용 가능: 자주 쓰는 블록은 함수로 만들어서 재사용하세요.
5. 명령어 치환 ($() vs ``)
시작하며
여러분이 현재 날짜를 파일명에 포함시키거나, 다른 명령어의 출력을 변수에 저장하고 싶을 때 어떻게 해야 할지 막막했던 적 있나요? 예를 들어 backup_2025_01_17.tar.gz 같은 파일명을 자동으로 만들고 싶을 때 말이죠.
이런 상황은 자동화 스크립트에서 아주 흔합니다. 명령어의 실행 결과를 다른 명령어의 입력으로 사용하거나, 동적인 값을 생성해야 할 때 필수적입니다.
하드코딩하면 매번 수정해야 하지만, 명령어 치환을 쓰면 자동으로 값이 생성됩니다. 바로 이럴 때 필요한 것이 명령어 치환(Command Substitution)입니다.
$()나 백틱 ` `을 사용하여 명령어의 출력을 다른 명령어나 변수에 삽입할 수 있습니다.
개요
간단히 말해서, 명령어 치환은 명령어를 실행한 결과를 그 자리에 바로 넣어주는 기능입니다. 마치 수학에서 함수의 결과값을 다른 식에 대입하는 것처럼, 명령어의 출력을 즉시 사용할 수 있습니다.
왜 필요할까요? 스크립트를 작성할 때 고정된 값이 아니라 실행 시점의 동적인 값을 사용해야 하는 경우가 많습니다.
현재 시간, 사용자 이름, 파일 개수, 시스템 정보 등을 자동으로 가져와서 사용할 수 있습니다. 예를 들어, 백업 파일에 자동으로 타임스탬프를 붙이거나, 로그에 현재 사용자 정보를 기록할 때 매우 유용합니다.
기존에는 결과를 수동으로 복사해서 붙여넣었다면, 이제는 명령어 치환으로 자동화할 수 있습니다. 두 가지 문법이 있습니다: 1) $(command) - 현대적이고 중첩이 쉬움, 2) `command` - 구식이지만 여전히 사용됨.
$()가 가독성이 좋고 중첩할 때 이스케이프가 필요 없어서 권장됩니다.
코드 예제
# 현대적 방식 - $()를 사용
CURRENT_DATE=$(date +%Y-%m-%d)
echo "오늘은 $CURRENT_DATE 입니다"
# 파일명에 날짜 포함
tar -czf "backup_$(date +%Y%m%d).tar.gz" /home/user/
# 명령어 결과를 변수에 저장
FILE_COUNT=$(ls -1 | wc -l)
echo "현재 디렉토리에 $FILE_COUNT 개의 파일이 있습니다"
# 중첩 명령어 치환 - $()가 더 깔끔함
OWNER=$(ls -l $(which bash) | awk '{print $3}')
echo "bash의 소유자는 $OWNER 입니다"
# 구식 방법 - 백틱 (권장하지 않음)
# OLD_WAY=`date`
# 조건문에서 사용
if [ $(whoami) = "root" ]; then
echo "관리자 권한으로 실행 중"
fi
# 반복문에서 사용
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "사용자: $user"
done
설명
이것이 하는 일: 쉘은 $()를 만나면 괄호 안의 명령을 먼저 실행하고, 그 출력 결과로 $() 부분을 대체합니다. 마치 계산기에서 괄호 안을 먼저 계산하는 것과 같습니다.
첫 번째로, CURRENT_DATE=$(date +%Y-%m-%d)에서 쉘은 date +%Y-%m-%d 명령을 실행합니다. 이 명령은 "2025-01-17" 같은 형식의 날짜를 출력합니다.
왜 이렇게 할까요? 날짜는 매일 바뀌므로 하드코딩할 수 없기 때문입니다.
쉘은 이 출력을 받아서 CURRENT_DATE="2025-01-17"처럼 변수에 저장합니다. 그 다음으로, tar -czf "backup_$(date +%Y%m%d).tar.gz"에서 쉘은 파일명을 만들 때 $(date +%Y%m%d)를 먼저 실행합니다.
예를 들어 오늘이 2025년 1월 17일이라면, 최종 파일명은 "backup_20250117.tar.gz"가 됩니다. 이렇게 하면 매일 자동으로 다른 파일명으로 백업이 생성됩니다.
마지막으로, 중첩된 명령어 치환 $(ls -l $(which bash))에서는 안쪽 $(which bash)가 먼저 실행되어 "/bin/bash" 같은 경로를 반환하고, 그 결과가 바깥쪽 ls -l 명령의 인자로 들어갑니다. 최종적으로 bash 파일의 상세 정보를 얻을 수 있습니다.
백틱으로 중첩하면 `ls -l \`which bash\ ``처럼 이스케이프가 필요해서 복잡하지만, $()는 그냥 중첩하면 됩니다. 여러분이 명령어 치환을 사용하면 스크립트가 훨씬 유연하고 재사용 가능해집니다.
날짜, 사용자, 시스템 정보 등을 동적으로 가져와서 로그, 백업, 알림 메시지에 활용할 수 있습니다. 특히 자동화 작업에서 매번 다른 값이 필요할 때 수동 수정 없이 자동으로 처리할 수 있어, 시간을 크게 절약하고 실수를 줄일 수 있습니다.
실전 팁
💡 항상 $()를 사용하세요: 백틱은 구식이고 중첩이 어려우므로 $()만 사용하는 습관을 들이세요.
💡 따옴표로 감싸서 공백 문제 방지: "$(command)"처럼 따옴표로 감싸면 결과에 공백이 있어도 안전합니다.
💡 성능 고려: 명령어 치환은 서브쉘을 만들므로 반복문 안에서 너무 많이 사용하면 느려집니다.
💡 에러 처리 추가: RESULT=$(command 2>/dev/null)처럼 에러를 숨기거나, || echo "실패"로 처리하세요.
💡 가독성을 위해 변수에 저장: 복잡한 치환은 먼저 변수에 저장하고 사용하면 코드가 더 읽기 쉽습니다.
6. 종료 상태 코드 ($?)
시작하며
여러분이 스크립트를 실행했는데 아무 에러 메시지 없이 조용히 실패한 경험이 있나요? 또는 명령이 성공했는지 실패했는지 확인하고 싶은데 방법을 몰라서 답답했던 적이 있으신가요?
이런 상황은 자동화 스크립트나 CI/CD 파이프라인에서 치명적입니다. 명령이 실패했는데 다음 단계가 계속 진행되면 예상치 못한 문제가 발생합니다.
특히 파일이 존재하지 않거나, 권한이 없거나, 네트워크 문제로 실패한 경우를 감지해야 합니다. 바로 이럴 때 필요한 것이 종료 상태 코드(Exit Status)입니다.
$? 변수를 통해 마지막 명령이 성공했는지 실패했는지 확인하고, 그에 따라 적절히 대응할 수 있습니다.
개요
간단히 말해서, 종료 상태 코드는 프로그램이 끝날 때 남기는 성적표입니다. 0점이면 성공(합격), 0이 아니면 실패(불합격)를 의미하며, 실패 이유에 따라 다른 숫자를 반환합니다.
왜 이게 필요할까요? 명령어는 항상 조용히 실행되지 않습니다.
성공과 실패를 판단하여 다음 작업을 진행할지 중단할지 결정해야 합니다. 예를 들어, 파일 다운로드가 실패했으면 압축 해제를 시도하면 안 되고, 데이터베이스 연결이 실패했으면 쿼리를 실행하면 안 됩니다.
종료 코드로 이런 조건을 확인할 수 있습니다. 기존에는 에러 메시지를 육안으로 확인했다면, 이제는 종료 코드로 프로그램이 자동으로 판단할 수 있습니다.
핵심 규칙은 간단합니다: 1) 0은 성공, 2) 1-255는 다양한 종류의 실패, 3) $?는 직전 명령의 종료 코드 저장. 일반적으로 1은 일반 에러, 2는 잘못된 사용법, 127은 명령을 찾을 수 없음, 130은 Ctrl+C로 중단 등의 의미를 갖습니다.
코드 예제
# 종료 코드 확인
ls /tmp
echo "ls의 종료 코드: $?" # 0이면 성공
ls /nonexistent
echo "실패한 ls의 종료 코드: $?" # 0이 아니면 실패
# 조건문에서 활용
if cp source.txt dest.txt; then
echo "복사 성공"
else
echo "복사 실패: $?"
fi
# 종료 코드 저장 후 사용
wget https://example.com/file.zip
DOWNLOAD_STATUS=$?
if [ $DOWNLOAD_STATUS -eq 0 ]; then
unzip file.zip
else
echo "다운로드 실패 (코드: $DOWNLOAD_STATUS)"
exit 1
fi
# 스크립트에서 명시적으로 종료 코드 반환
function validate_file() {
if [ -f "$1" ]; then
return 0 # 성공
else
return 1 # 실패
fi
}
validate_file "config.txt"
if [ $? -eq 0 ]; then
echo "설정 파일 존재"
fi
# 에러 시 즉시 종료
set -e # 이후 모든 명령이 실패하면 스크립트 중단
mkdir /tmp/test
cd /tmp/test
touch newfile.txt
설명
이것이 하는 일: 모든 명령어와 프로그램은 종료할 때 0-255 사이의 숫자를 반환하고, 쉘은 이 값을 $? 변수에 자동으로 저장합니다. 이 값으로 성공 여부를 판단할 수 있습니다.
첫 번째로, ls /tmp 명령이 실행되고 성공적으로 파일 목록을 보여주면 종료 코드 0을 반환합니다. 왜 0일까요?
유닉스 철학에서 0은 "문제없음, 모두 정상"을 의미합니다. 쉘은 이 값을 $?에 저장하고, echo "ls의 종료 코드: $?"를 실행하면 0이 출력됩니다.
그 다음으로, ls /nonexistent처럼 존재하지 않는 디렉토리를 조회하면 ls 명령은 에러 메시지를 출력하고 0이 아닌 값(보통 2)을 반환합니다. 중요한 점은 $?는 가장 최근 명령의 종료 코드만 저장한다는 것입니다.
따라서 여러 명령을 연속으로 실행하면 마지막 명령의 결과만 $?에 남습니다. 마지막으로, if cp source.txt dest.txt; then처럼 조건문에서 직접 명령을 실행하면, 쉘은 자동으로 종료 코드를 확인하여 0이면 then 블록을, 그렇지 않으면 else 블록을 실행합니다.
최종적으로 종료 코드는 스크립트의 흐름 제어와 에러 처리의 핵심 메커니즘입니다. 여러분이 종료 코드를 활용하면 견고한 스크립트를 작성할 수 있습니다.
모든 중요한 작업 후에 성공 여부를 확인하고, 실패하면 적절히 대응하거나 사용자에게 알릴 수 있습니다. 특히 자동화 스크립트, 백업 스크립트, 배포 스크립트에서 각 단계의 성공을 보장하고 문제를 조기에 발견할 수 있습니다.
또한 set -e로 어떤 명령이든 실패하면 즉시 중단되게 하여, 연쇄적인 실패를 방지할 수 있습니다.
실전 팁
💡 $?는 즉시 저장하세요: 다른 명령을 실행하면 $?가 덮어써지므로 STATUS=$?처럼 변수에 먼저 저장하세요.
💡 set -e로 자동 중단: 스크립트 시작에 set -e를 추가하면 에러 발생 시 자동으로 중단됩니다.
💡 set -o pipefail로 파이프라인 에러 감지: 파이프라인 중간에 실패해도 감지하려면 이 옵션을 켜세요.
💡 함수에서 return 사용: 함수는 exit 대신 return 0 또는 return 1로 종료 코드를 반환하세요.
💡 의미 있는 종료 코드 사용: 여러분의 스크립트도 0(성공), 1(일반 에러), 2(잘못된 인자) 같은 관례를 따르세요.