iBetter Books
수정

set -x와 디버깅 기법

스크립트가 예상과 다르게 작동할 때 가장 먼저 해볼 것은 set -x입니다. 이 옵션을 켜면 Bash가 각 명령어를 실행하기 전에 그 명령어를 화면에 출력합니다. 무슨 명령어가 어떤 순서로 실행되는지, 변수가 어떤 값으로 치환되는지 모두 볼 수 있습니다.

set -x — 실행 추적

# 파일: trace_demo.sh#!/usr/bin/env bashset -xNAME="홍길동"GREETING="안녕하세요, ${NAME}님"echo "$GREETING"

실행 결과입니다.

+ NAME=홍길동
+ GREETING='안녕하세요, 홍길동님'
+ echo '안녕하세요, 홍길동님'
안녕하세요, 홍길동님

+ 기호로 시작하는 줄이 추적 출력입니다. 변수가 실제 값으로 치환된 명령어가 보입니다. 스크립트가 자신이 실행한 명령어를 먼저 보여주고, 그 다음에 실제 출력이 나옵니다.

set -x를 끄려면 set +x를 씁니다.

PS4 커스터마이징 — 파일명:줄번호:함수명 표시

기본 +는 단조롭습니다. PS4를 수정하면 추적 출력에 파일명, 줄 번호, 함수명을 포함시킬 수 있습니다.

# 파일: ps4_demo.sh#!/usr/bin/env bash# PS4 설정: 파일명:줄번호:함수명: 형식export PS4='+${BASH_SOURCE[0]##*/}:${LINENO}:${FUNCNAME[0]:-main}: 'set -xgreet() {    local name="$1"    echo "안녕하세요, ${name}님"}USER_NAME="홍길동"greet "$USER_NAME"

실행 결과입니다.

+ps4_demo.sh:11:main: USER_NAME=홍길동
+ps4_demo.sh:12:main: greet 홍길동
+ps4_demo.sh:8:greet: local name=홍길동
+ps4_demo.sh:9:greet: echo '안녕하세요, 홍길동님'
안녕하세요, 홍길동님

각 줄 앞에 파일명:줄번호:함수명이 붙어서 오류가 어디서 났는지 바로 찾을 수 있습니다. ${BASH_SOURCE[0]##*/}에서 ##*/는 경로에서 파일명만 추출하는 매개변수 확장입니다.

특정 구간만 디버깅

스크립트 전체에 set -x를 적용하면 출력이 너무 많아집니다. 의심스러운 구간만 추적할 때는 set -xset +x로 구간을 지정합니다.

# 파일: selective_debug.sh#!/usr/bin/env bashecho "일반 구간 - 추적 없음"# 디버그 구간 시작set -xRESULT=$(ls /tmp | wc -l)echo "파일 수: $RESULT"set +x# 디버그 구간 끝echo "다시 일반 구간 - 추적 없음"

실행 결과입니다.

일반 구간 - 추적 없음
+ls /tmp
+wc -l
+RESULT=42
+echo '파일 수: 42'
파일 수: 42
다시 일반 구간 - 추적 없음

bash -x로 외부에서 디버깅

스크립트 내부를 수정하지 않고 외부에서 디버깅 모드로 실행하는 방법도 있습니다.

# 스크립트 내부에 set -x 없이 외부에서 켜기bash -x script.sh# 다른 인터프리터 설정 무시하고 강제로 bash -x로 실행bash -x ./script.sh 인수1 인수2

이미 배포된 스크립트를 수정하지 않고 임시로 디버그 정보를 보고 싶을 때 유용합니다.

디버그 모드 플래그

환경변수로 디버그 모드를 켜고 끄는 패턴입니다. 일반 실행 시에는 추적 없이 실행하고, 문제가 생겼을 때만 DEBUG=1을 설정합니다.

# 파일: debug_flag.sh#!/usr/bin/env bash# DEBUG 환경변수가 설정되어 있으면 추적 활성화[[ "${DEBUG:-}" ]] && set -xecho "작업 시작"RESULT=$(ls /tmp | wc -l)echo "파일 수: $RESULT"echo "완료"

사용 방법입니다.

# 일반 실행./debug_flag.sh# 디버그 모드로 실행DEBUG=1 ./debug_flag.sh# PS4도 함께 설정DEBUG=1 PS4='+${BASH_SOURCE[0]##*/}:${LINENO}: ' ./debug_flag.sh

라이브러리 파일에서는 이 패턴을 더 세밀하게 적용할 수 있습니다. DEBUG_VERBOSE=1로 특정 모듈만 추적하는 방식도 가능합니다.

실습 — 버그가 있는 스크립트를 set -x로 추적하여 수정

버그가 있는 스크립트입니다. 어디서 문제가 생기는지 set -x로 추적하고 수정합니다.

# 파일: buggy_script.sh#!/usr/bin/env bash# 특정 확장자를 가진 파일 개수를 세는 스크립트# 버그: 실행해보면 항상 0을 출력함TARGET_DIR="$1"EXTENSION="$2"count_files() {    local dir=$1    local ext=$2    local count    count=$(find $dir -name "*.$ext" | wc -l)    echo $count}TOTAL=$(count_files $TARGET_DIR $EXTENSION)echo "${EXTENSION} 파일 개수: $TOTAL"

set -x와 수정된 PS4로 추적합니다.

export PS4='+${BASH_SOURCE[0]##*/}:${LINENO}:${FUNCNAME[0]:-main}: 'bash -x buggy_script.sh /tmp "sh"

추적 출력을 보면 find $dir -name "*.$ext"에서 따옴표가 없어 공백이 있는 디렉토리 경로에서 인수가 분리되고, 변수 참조에 따옴표가 없어 글로빙 문제가 생길 수 있음을 알 수 있습니다.

수정된 스크립트입니다.

#!/usr/bin/env bash# 수정: buggy_script.shset -euo pipefailcount_files() {    local dir="$1"    local ext="$2"    local count    count=$(find "$dir" -name "*.${ext}" | wc -l)    echo "$count"}if [[ $# -ne 2 ]]; then    echo "사용법: $0 <디렉토리> <확장자>" >&2    exit 2fiTARGET_DIR="$1"EXTENSION="$2"if [[ ! -d "$TARGET_DIR" ]]; then    echo "오류: 디렉토리를 찾을 수 없습니다: $TARGET_DIR" >&2    exit 3fiTOTAL=$(count_files "$TARGET_DIR" "$EXTENSION")echo "${EXTENSION} 파일 개수: $TOTAL"

수정 포인트는 크게 두 가지입니다. 변수를 항상 큰따옴표로 감싸고, 인수 검증 코드를 추가했습니다. set -x 추적으로 실제 실행되는 명령어를 보면 어디서 따옴표가 빠졌는지 한눈에 확인할 수 있습니다.