흔한 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은 백슬래시 이스케이프가 해석되는 것을 방지합니다.

임시 파일을 mktemptrap으로 처리

하드코딩된 임시 경로는 다른 프로세스와 충돌하거나 오래된 파일을 남길 수 있습니다. 고유한 경로를 만들고 종료 시 정리하세요.

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으로 임시 파일을 정리하고, 실패 경로를 명시적으로 만드세요. 미래의 당신은 완벽한 입력에서만 작동하는 스크립트를 디버깅하는 데 시간을 덜 쓰게 될 것입니다.