다양한 시스템에서 Bash 스크립트 이식성 보장
GNU, BSD, BusyBox 간 차이를 처리하여 Linux, macOS, CI 환경에서 이식 가능한 Bash 스크립트를 작성합니다.
다양한 시스템에서 Bash 스크립트 이식성 보장
랩톱, Linux 서버, CI 러너에서 작동하는 Bash 스크립트를 작성하는 것은 생각보다 어렵습니다. Bash 스크립트 이식성은 일반적으로 작은 차이에서 깨집니다: Linux에서는 작동하지만 macOS에서는 실패하는 sed -i 플래그, GNU coreutils에만 존재하는 date 옵션, 또는 /bin/bash가 테스트한 버전이라고 가정하는 스크립트 등이 있습니다.
핵심 어려움은 Bash가 환경의 일부일 뿐이라는 점입니다. Linux는 일반적으로 GNU 유틸리티를 제공합니다. macOS는 BSD 계열 유틸리티를 제공합니다. BusyBox 기반 컨테이너는 더 적은 옵션을 가진 더 작은 구현을 제공할 수 있습니다. 스크립트는 필요한 것을 명확히 해야 합니다.
이 가이드는 Bash 스크립트에 중점을 두며, 엄격한 POSIX sh 스크립트는 다루지 않습니다. 진정한 /bin/sh 이식성이 필요하다면 Bash 전용 구문을 완전히 피하고 dash와 같은 셸로 테스트하세요.
명확한 셸 계약으로 시작하기
의도에 맞는 셔뱅(shebang)을 사용하세요. 스크립트가 Bash를 필요로 한다면 다음과 같이 명시하세요:
#!/usr/bin/env bash
/usr/bin/env는 $PATH를 통해 Bash를 찾으며, 사용자가 /bin 외부에 최신 Bash를 설치한 경우 유용합니다. 프로덕션 호스트에서 고정된 인터프리터 경로가 필요하다면 해당 경로를 문서화하고 강제하세요.
엄격 모드는 많은 실수를 조기에 잡아내지만, 만능은 아닙니다:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
이 옵션들은 도움이 되지만 주의사항이 있습니다:
-e: 많은 단순 명령이 0이 아닌 종료 상태를 반환하면 종료합니다.-u: 설정되지 않은 변수를 오류로 처리합니다.pipefail: 파이프라인의 어떤 명령이 실패하면 파이프라인이 실패하도록 만듭니다.
예상되는 실패는 명시적으로 처리하세요:
if ! grep -q "ready" "$log_file"; then
echo "서비스가 아직 준비되지 않았습니다"
fi
Bash 버전 확인하기
대상 시스템에 없는 Bash 기능에 우연히 의존하지 마세요. macOS는 역사적으로 /bin/bash에 오래된 Bash를 제공했으며, 많은 Linux 배포판은 최신 버전을 제공합니다.
주의해서 사용해야 할 기능:
- 연관 배열.
**와 같은 고급 글롭(glob).<(command)와 같은 프로세스 치환.- 최신 매개변수 확장 동작.
최소 Bash 버전이 필요하다면, 스크립트 상단 근처에서 확인하세요:
if (( BASH_VERSINFO[0] < 4 )); then
echo "이 스크립트는 Bash 4 이상이 필요합니다." >&2
exit 1
fi
GNU, BSD, BusyBox 차이 처리하기
가장 큰 이식성 문제는 Bash 자체보다 외부 명령에서 발생하는 경우가 많습니다.
sed -i
GNU sed는 백업 확장자 없이 -i를 허용합니다. macOS의 BSD sed는 -i 뒤에 확장자 인수가 필요하며, 빈 문자열이라도 필요합니다.
file="data.txt"
pattern="s/error/success/g"
case "$(uname -s)" in
Darwin)
sed -i '' "$pattern" "$file"
;;
*)
sed -i "$pattern" "$file"
;;
esac
중요한 스크립트의 경우, 임시 파일에 쓰고 제자리로 이동하는 패턴이 더 안전합니다. 이렇게 하면 제자리 편집 동작에 의존하지 않습니다.
date
날짜 계산은 시스템마다 다릅니다:
| 목표 | GNU date |
macOS의 BSD date |
|---|---|---|
| 30일 전 | date -d "30 days ago" +%Y%m%d |
date -v-30d +%Y%m%d |
스크립트가 복잡한 날짜 계산을 필요로 한다면 Python과 같은 일관된 의존성을 사용하거나, macOS에 GNU coreutils를 설치하고 gdate를 명시적으로 호출하세요. date -d가 존재한다고 묵시적으로 가정하지 마세요.
grep, find, xargs
가능하면 널리 지원되는 옵션을 사용하세요:
egrep대신grep -E를 사용하세요.- PCRE 지원이 있는 GNU grep을 확인하지 않는 한
grep -P를 피하세요. - GNU와 BSD 구현 간에 다른
find조건자에 주의하세요. - 지원되는 경우 파일 이름에 null로 구분된 파이프라인을 선호하세요:
find "$root" -type f -name '*.log' -print0 | xargs -0 rm -f
의존성 및 경로 관리하기
일반 명령 조회에는 $PATH를 사용하되, 작업을 수행하기 전에 필요한 도구를 확인하세요:
check_dependency() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "오류: 필요한 명령 '$1'을(를) 찾을 수 없습니다." >&2
exit 1
fi
}
check_dependency jq
check_dependency curl
which보다 command -v를 선호하세요. 이는 Bash의 내장 명령이며 스크립트에서 더 예측 가능하게 동작합니다.
의도적으로 단어 분할을 원하지 않는 한 변수를 인용하세요:
cp "$source_file" "$target_dir/"
이는 Project Files/report.txt와 같은 경로에 중요하며, 예상치 못한 입력에서 와일드카드 확장으로부터 보호합니다.
임시 파일 안전하게 사용하기
임시 작업에는 mktemp를 사용하세요. 간단하고 이식 가능한 패턴은 하나의 임시 디렉토리를 만들고 그 안에 파일을 넣는 것입니다:
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
tmp_file="$tmp_dir/output.txt"
some_command > "$tmp_file"
작은따옴표로 묶인 트랩은 $tmp_dir이 트랩이 실행될 때까지 확장되지 않도록 합니다. 변수가 여전히 범위 내에 있으므로 정리 시 올바른 디렉토리가 제거됩니다.
줄 끝 및 파일 시스템 대소문자 주의하기
Windows에서 편집된 스크립트는 CRLF 줄 끝을 사용할 수 있습니다. 일반적인 증상은 다음과 같습니다:
/usr/bin/env: bash\r: No such file or directory
편집기를 셸 스크립트에 LF 줄 끝을 저장하도록 설정하거나, 빌드 프로세스에서 dos2unix를 실행하세요.
또한 대부분의 Linux 파일 시스템은 기본적으로 대소문자를 구분하지만, 기본 macOS APFS 설정은 종종 대소문자를 구분하지 않습니다. 스크립트가 Config.yml을 작성하고 나중에 config.yml을 읽는다면 Mac에서는 작동하지만 Linux에서는 실패할 수 있습니다.
지원하는 시스템에서 테스트하기
가장 좋은 이식성 확인은 작은 테스트 매트릭스입니다:
- GNU 유틸리티를 사용하는 Linux.
- BSD 유틸리티를 사용하는 macOS.
- 스크립트가 Alpine 또는 BusyBox 환경에서 실행되는 경우 최소 컨테이너.
ShellCheck도 실행하세요. 모든 플랫폼 문제를 잡아내지는 못하지만, 사용자가 발견하기 전에 많은 인용, 정의되지 않은 변수, 취약한 명령 패턴을 잡아냅니다.
결론
Bash 스크립트 이식성은 가정을 명시적으로 만드는 데서 비롯됩니다. 셸을 선택하고, 의존성을 확인하고, 변수를 인용하고, 필요하지 않은 한 GNU 전용 플래그를 피하고, 사용자가 실행하는 동일한 운영 체제에서 테스트하세요. Linux와 macOS를 포함한 작은 CI 매트릭스는 자동화가 프로덕션에 도달하기 전에 대부분의 이식성 버그를 잡아냅니다.