본문 바로가기
TYLER SONGBlog
← Blog index
CS학습6 min read

Bash: 명령어를 해석해 프로세스를 실행하는 셸과 스크립트 자동화

Bash는 사용자가 입력한 명령어를 해석해 운영체제에 프로세스 실행을 요청하는 유닉스 계열 셸(Shell)이자 스크립트 언어다. 파이프, 리디렉션, 변수, 조건문, 반복문을 조합해 반복 작업을 자동화할 수 있다. 이 글에서는 셸의 동작 원리부터 안전한 스크립트 작성 습관까지 정리한다.

By 송민성

1. 개념

Bash(Bourne Again SHell)는 GNU 프로젝트에서 만든 셸(Shell)이다. 셸은 사용자와 운영체제 커널(Kernel) 사이에서 명령어를 입력받아 해석하고, 프로그램을 실행해 주는 명령어 해석기(command interpreter)다.

Bash는 두 가지 방식으로 쓴다.

  • 대화형(interactive) 모드: 터미널에서 명령어를 한 줄씩 입력하고 결과를 바로 확인한다.
  • 스크립트(script) 모드: 여러 명령어를 파일에 적어 두고 한 번에 실행한다.

대부분의 Linux 배포판에서 기본 로그인 셸로 쓰이며, macOS는 기본 셸이 zsh이지만 Bash도 설치되어 있거나 설치할 수 있다.

2. 왜 사용하는가

  • 자동화: 배포, 백업, 로그 정리처럼 반복되는 작업을 스크립트 한 개로 묶을 수 있다.
  • 조합성: 작은 프로그램을 파이프(|)로 연결해 복잡한 처리를 만든다. 이것이 유닉스 철학이다.
  • 어디에나 있음: 서버, 컨테이너, CI 환경 어디서든 별도 설치 없이 쓸 수 있는 경우가 많다.
  • 원격 작업: SSH로 접속한 서버에서 사용할 수 있는 사실상 기본 도구다.

3. 동작 원리

명령어 한 줄이 실행되는 과정

  1. 입력한 한 줄을 단어(token) 단위로 나눈다.
  2. 변수 치환($VAR), 명령 치환($(...)), 글롭(glob, *.txt) 확장 같은 확장(expansion) 을 수행한다.
  3. 첫 단어가 내장 명령(builtin, 예: cd)인지, 함수인지, 외부 프로그램인지 판별한다. 외부 프로그램이면 환경 변수 PATH에 나열된 디렉터리에서 실행 파일을 찾는다.
  4. 외부 프로그램은 fork()로 자식 프로세스를 만든 뒤 exec 계열 시스템 콜로 해당 프로그램으로 교체해 실행한다.
  5. 부모인 셸은 자식이 끝날 때까지 기다렸다가(wait) 종료 상태(exit status)를 받는다.

cd가 내장 명령인 이유도 여기에 있다. 자식 프로세스에서 디렉터리를 바꾸면 부모 셸의 현재 디렉터리는 바뀌지 않기 때문이다.

표준 입출력과 파이프

모든 프로세스는 파일 디스크립터(file descriptor) 0(표준 입력, stdin), 1(표준 출력, stdout), 2(표준 에러, stderr)를 가진다.

  • >: stdout을 파일로 덮어쓰기
  • >>: 파일에 이어쓰기
  • 2>: stderr를 파일로 보내기
  • |: 앞 명령의 stdout을 뒤 명령의 stdin에 연결

종료 상태

명령은 종료 시 0~255 범위의 정수를 반환한다. 0이 성공, 그 외는 실패를 뜻한다. 직전 명령의 종료 상태는 $?로 읽는다. if, &&, ||는 이 값을 기준으로 동작한다.

4. 코드 예제

기본 명령과 파이프

shell
# 현재 디렉터리의 .log 파일 중 "ERROR"가 들어간 줄의 개수 grep -h "ERROR" *.log | wc -l # 프로세스 목록에서 특정 이름 찾기 ps aux | grep nginx

변수, 조건문, 반복문을 쓰는 스크립트

backup.sh로 저장한다.

shell
#!/usr/bin/env bash set -euo pipefail # 사용법: ./backup.sh <백업할_디렉터리> if [[ $# -lt 1 ]]; then echo "사용법: $0 <백업할_디렉터리>" >&2 exit 1 fi src="$1" if [[ ! -d "$src" ]]; then echo "디렉터리가 없습니다: $src" >&2 exit 2 fi timestamp="$(date +%Y%m%d_%H%M%S)" dest="/tmp/backup_${timestamp}.tar.gz" tar -czf "$dest" -C "$src" . echo "백업 완료: $dest" # 파일 개수 출력 count=0 for f in "$src"/*; do [[ -f "$f" ]] && count=$((count + 1)) done echo "최상위 파일 수: $count"

실행 방법은 다음과 같다.

shell
chmod +x backup.sh ./backup.sh ~/projects

함수와 종료 상태 활용

shell
#!/usr/bin/env bash check_port() { local host="$1" local port="$2" # /dev/tcp는 Bash 고유 기능이다 if (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null; then echo "${host}:${port} 열림" return 0 else echo "${host}:${port} 닫힘" return 1 fi } check_port localhost 22 || echo "SSH 포트에 접근할 수 없습니다"

5. 시간 복잡도 또는 성능 특성

Bash에는 자료구조처럼 고정된 시간복잡도가 없으므로, 비용이 어디서 발생하는지로 이해한다.

  • 프로세스 생성 비용: 외부 명령을 호출할 때마다 fork와 exec가 일어난다. 반복문 안에서 sed, awk, grep 같은 외부 명령을 수만 번 호출하면 느려진다. 간단한 문자열 처리는 Bash 내장 기능(${var#prefix}, [[ ]], $(( )))으로 대체하면 프로세스 생성 비용을 피할 수 있다.
  • 파이프라인: 파이프로 연결된 명령들은 각각 별도 프로세스로 동시에 실행되며, 데이터는 스트림으로 흘러간다. 큰 파일도 전체를 메모리에 올리지 않고 처리할 수 있다.
  • 용도의 한계: 대량의 계산이나 복잡한 자료구조 처리에는 적합하지 않다. 이런 작업은 Python 같은 언어가 낫다.

6. 실무 사용 사례

  • 배포·CI/CD: 빌드, 테스트, 도커 이미지 푸시 등 파이프라인 단계를 셸 스크립트로 작성한다.
  • 로그 분석: grep, awk, sort, uniq를 파이프로 연결해 빠르게 집계한다.
  • 서버 운영: cron과 결합해 정기 백업, 디스크 정리, 헬스 체크를 수행한다.
  • 개발 환경 구성: 프로젝트 초기 설정이나 환경 변수 로딩을 스크립트로 통일한다.
  • 컨테이너 진입점: Docker의 ENTRYPOINT 스크립트로 시작 전 설정을 처리한다.

로그에서 상위 요청 경로를 뽑는 예시는 다음과 같다. 접근 로그의 7번째 필드가 경로라고 가정한다.

shell
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -5

7. 주의할 점

  • 변수는 항상 따옴표로 감싼다: rm $file처럼 쓰면 값에 공백이 있을 때 여러 인자로 쪼개진다. rm -- "$file"처럼 쓴다.
  • 엄격 모드를 켠다: set -euo pipefail은 실패한 명령에서 중단(-e), 정의되지 않은 변수 사용 시 오류(-u), 파이프 중간 실패 감지(pipefail)를 의미한다. 다만 -e는 조건문 안 등 일부 문맥에서는 동작하지 않는 예외가 있으므로 만능으로 믿지 않는다.
  • 이식성: [[ ]], 배열, /dev/tcp 등은 Bash 확장 기능이다. sh(POSIX 셸)로 실행하면 동작하지 않을 수 있다. 셔뱅(shebang)에 #!/usr/bin/env bash를 명시한다.
  • 공백 규칙: 변수 할당은 name=value처럼 = 앞뒤에 공백이 없어야 한다. [[ ... ]] 안쪽에는 공백이 필요하다.
  • 위험한 삭제: rm -rf "$dir/"에서 dir이 비어 있으면 /를 지우는 상황이 될 수 있다. ${dir:?}로 빈 값을 막는다.
  • 복잡해지면 다른 언어로: 스크립트가 수백 줄을 넘거나 JSON, 자료구조를 많이 다루면 Python 등으로 옮기는 것이 유지보수에 낫다.

8. 핵심 정리

  • Bash는 명령어를 해석해 fork/exec로 프로세스를 실행하는 셸이자 스크립트 언어다.
  • 명령은 종료 상태 0이면 성공, 0이 아니면 실패이며, 조건문과 &&, ||는 이 값을 기준으로 한다.
  • 표준 입력/출력/에러(0/1/2)와 리디렉션, 파이프가 조합성의 핵심이다.
  • 반복문 안의 외부 명령 호출은 프로세스 생성 비용이 크므로 내장 기능을 우선 고려한다.
  • 변수는 따옴표로 감싸고, set -euo pipefail과 셔뱅을 습관으로 삼는다.