흔한 Bash 스크립팅 함정과 이를 피하는 방법
더 안전한 오류 처리, 인용, 배열, 트랩 및 인수 구문 분석을 통해 일반적인 Bash 스크립팅 버그를 피하세요.
일반적인 Bash 스크립팅 함정과 피하는 방법
Bash 스크립팅 함정은 일반적으로 스크립트가 실제 파일 이름, 누락된 변수, 실패한 명령 또는 예상치 못한 입력을 만날 때 나타납니다. 노트북에서 잘 작동하는 스크립트가 느슨한 기본값에 의존하면 CI나 프로덕션에서 깨질 수 있습니다.
모든 셸 스크립트를 복잡하게 만들 필요는 없습니다. 확장을 인용하고, 의도적으로 실패를 확인하고, 공백이 포함된 이름으로 테스트해야 합니다.
더 안전한 기본값을 신중하게 설정
많은 스크립트가 다음으로 시작합니다:
#!/usr/bin/env bash
set -euo pipefail
이는 많은 자동화 스크립트에 좋은 기준이지만, 각 옵션에는 날카로운 모서리가 있습니다:
set -e는 단순 명령이 실패하면 종료되지만,if테스트,&&및||목록의 일부, 일부 명령 대체와 같은 경우는 제외됩니다.set -u는 설정되지 않은 변수를 확장하면 종료됩니다.set -o pipefail은 파이프라인의 모든 명령이 실패하면 파이프라인이 실패하게 하며, 마지막 명령만 실패하는 것이 아닙니다.
조기 실패가 계속하는 것보다 안전할 때 이러한 옵션을 사용하세요. 실패가 예상되는 명령의 경우 상태를 명시적으로 처리하세요.
if ! grep -q "ready" status.txt; then
echo "서비스가 아직 준비되지 않았습니다"
exit 1
fi
변수 확장을 인용하세요
인용되지 않은 변수는 가장 흔한 Bash 버그입니다. Bash는 인용되지 않은 확장에 대해 단어 분할 및 글로브 확장을 수행하므로, release notes/*.txt와 같은 경로가 여러 인수가 되거나 의도하지 않은 파일과 일치할 수 있습니다.
file="release notes.txt"
# 나쁨: 값이 두 단어로 분할되어 깨집니다.
rm $file
# 좋음: 정확히 하나의 인수를 전달합니다.
rm -- "$file"
명령이 지원하는 경우 사용자 제어 파일 이름 앞에 --를 사용하세요. 이렇게 하면 -rf와 같은 파일 이름이 옵션으로 해석되는 것을 방지할 수 있습니다.
인수 목록에 배열 사용
인수가 있는 명령을 하나의 문자열에 저장하고 실행하지 마세요. 인용이 빠르게 깨지기 쉽습니다.
# 나쁨
flags="-a --exclude node_modules"
rsync $flags "$src" "$dest"
# 좋음
flags=(-a --exclude "node_modules")
rsync "${flags[@]}" "$src" "$dest"
배열은 인수 경계를 유지합니다. 이는 인수에 공백, 와일드카드 문자 또는 대시로 시작하는 값이 포함된 경우 중요합니다.
백틱보다 $(...) 선호
백틱은 중첩하기 어렵고 잘못 읽기 쉽습니다. 명령 대체에는 $(...)를 사용하세요.
current_branch="$(git rev-parse --abbrev-ref HEAD)"
echo "브랜치 빌드 중: $current_branch"
의도적으로 단어 분할을 원하지 않는 한 명령 대체를 인용된 상태로 유지하세요.
데이터 손실 없이 파일 읽기
이 패턴은 무해해 보이지만 공백에서 깨지고 백슬래시를 망칠 수 있습니다:
for line in $(cat hosts.txt); do
echo "$line"
done
대신 while IFS= read -r로 파일을 읽으세요.
while IFS= read -r host; do
echo "$host 확인 중"
done < hosts.txt
IFS=는 선행 및 후행 공백을 유지합니다. -r은 백슬래시 이스케이프가 해석되는 것을 방지합니다.
임시 파일을 mktemp와 trap으로 처리
하드코딩된 임시 경로는 다른 프로세스와 충돌하거나 오래된 파일을 남길 수 있습니다. 고유한 경로를 만들고 종료 시 정리하세요.
tmp_file="$(mktemp)"
cleanup() {
rm -f "$tmp_file"
}
trap cleanup EXIT
printf '%s\n' "작업 데이터" > "$tmp_file"
디렉토리의 경우 mktemp -d를 사용하고 정리 함수에서 디렉토리를 제거하세요.
옵션을 getopts로 구문 분석
수동 인수 구문 분석은 종종 가장자리 경우를 놓칩니다. 짧은 옵션의 경우 Bash 내장 getopts가 일반적으로 충분합니다.
verbose=false
output=""
while getopts ":vo:" opt; do
case "$opt" in
v) verbose=true ;;
o) output="$OPTARG" ;;
:)
echo "옵션 -$OPTARG는 인수가 필요합니다" >&2
exit 2
;;
\?)
echo "알 수 없는 옵션: -$OPTARG" >&2
exit 2
;;
esac
done
shift "$((OPTIND - 1))"
getopts는 -v 및 -o file과 같은 짧은 플래그를 처리합니다. 스크립트에 --output과 같은 긴 옵션이 필요한 경우 신중한 파서를 작성하거나 더 강력한 인수 구문 분석 라이브러리가 있는 언어를 사용하세요.
실패할 수 있는 명령 확인
명령이 무언가를 출력했다고 해서 작동했다고 가정하지 마세요. 중요한 작업의 출력을 사용하기 전에 확인하세요.
if ! archive="$(tar -czf app.tar.gz app 2>&1)"; then
echo "아카이브 실패: $archive" >&2
exit 1
fi
파이프라인의 경우, 중간에서 실패하면 전체 파이프라인이 실패해야 할 때 pipefail을 활성화하세요.
set -o pipefail
journalctl -u api.service | grep -i "error"
pipefail이 없으면 파이프라인 상태는 일반적으로 마지막 명령에서 가져옵니다.
이식성이 중요할 때 Bash 피하기
스크립트가 배열, [[ ... ]], mapfile 또는 pipefail을 사용한다면 Bash 스크립트입니다. 다음으로 시작하세요:
#!/usr/bin/env bash
POSIX sh 이식성이 필요하다면 Bash 전용 기능을 피하고 대상 시스템이 사용하는 셸로 테스트하세요. #!/bin/sh로 Bash 스크립트를 작성하고 모든 곳에서 동일하게 작동하기를 기대하지 마세요.
결론
Bash 스크립트를 개선하는 가장 빠른 방법은 지저분한 입력(파일 이름의 공백, 누락된 변수, 빈 파일, 실패하는 명령)으로 테스트하는 것입니다. 확장을 인용하고, 인수 목록에 배열을 사용하고, trap으로 임시 파일을 정리하고, 실패 경로를 명시적으로 만드세요. 미래의 당신은 완벽한 입력에서만 작동하는 스크립트를 디버깅하는 데 시간을 덜 쓰게 될 것입니다.