일반적인 Bash 스크립트 설정 문제 해결

Bash 스크립트의 설정 문제를 해결하는 기술을 마스터하세요. 이 가이드는 환경 종속성, 부적절한 인용 및 단어 분할과 같은 일반적인 구문 함정, 중요한 실행 오류에 초점을 맞춘 필수 디버깅 기술을 자세히 설명합니다. 강력한 플래그(`set -euo pipefail`) 사용, 인수 구문 분석 오류 처리, DOS 줄 끝 및 잘못된 PATH 변수와 같은 일반적인 문제를 해결하여 자동화 스크립트가 모든 환경에서 안정적으로 실행되도록 하는 방법을 알아보세요.

일반적인 Bash 스크립트 설정 문제 해결

Bash 설정 문제는 일반적으로 모호하게 나타납니다. 터미널에서는 작동하지만 cron에서는 실패하는 스크립트, kubectl을 찾을 수 없는 배포 스크립트, 공백이 있는 설정 파일 경로가 한 고객에게만 문제를 일으키는 경우 등이 있습니다. 버그는 주 로직에 있는 경우가 거의 없습니다. 환경, 인수, 인용, 권한 또는 실제로 파일을 실행한 셸에 대한 가정에 있습니다.

Bash 스크립트 문제를 해결할 때 먼저 네 가지 질문에 답하려고 합니다. 어떤 셸이 실행 중인가? 어떤 환경을 받았는가? 어떤 입력을 구문 분석했는가? 어떤 명령이 먼저 실패했는가? 이 순서는 증상을 쫓는 것을 방지합니다.

셸 및 실행 컨텍스트 확인

Bash 구문으로 시작하지만 sh에서 실행되는 스크립트는 이상한 방식으로 실패할 수 있습니다. 배열, [[ ... ]], source, 프로세스 대체 및 set -o pipefail은 Bash 기능입니다. 파일에서 이를 사용하는 경우 shebang은 Bash를 지정해야 합니다:

#!/usr/bin/env bash

그런 다음 자동화가 실행하는 방식과 동일하게 실행합니다. 다음은 동일하지 않습니다:

./deploy.sh
bash deploy.sh
sh deploy.sh

./deploy.sh는 shebang을 사용합니다. bash deploy.sh는 Bash를 강제합니다. sh deploy.sh는 시스템에 따라 dash, BusyBox ash 또는 다른 셸을 사용할 수 있습니다. 프로덕션에서 sh deploy.sh를 호출하면 완벽한 Bash shebang도 도움이 되지 않습니다.

Cron, systemd, CI 실행기, SSH 강제 명령 및 Docker 엔트리포인트는 모두 다른 환경을 제공합니다. 대화식으로 작동하는 스크립트는 로그인 셸이 실행 전에 PATH, AWS_PROFILE, NVM_DIR 또는 언어 버전 관리자를 설정했기 때문에 실패할 수 있습니다.

상단 근처에 임시 진단 블록을 추가하세요:

printf 'shell=%s\n' "$BASH_VERSION" >&2
printf 'user=%s pwd=%s\n' "$(id -un)" "$PWD" >&2
printf 'PATH=%s\n' "$PATH" >&2

답을 얻으면 제거하거나 게이트하세요. 진단은 유용하지만 환경 값을 로그에 유출하면 비밀이 노출될 수 있습니다.

엄격 모드를 신중하게 사용하고 맹목적으로 사용하지 마세요

set -euo pipefail은 많은 자동화 스크립트에 강력한 기본값이지만 가장자리 사례가 있습니다. set -u는 누락된 변수를 포착합니다. pipefail은 파이프라인 실패를 표시합니다. set -e는 많은 명령 실패 후 중지되지만 조건문, 파이프라인 및 복합 명령 내에서 새로운 Bash 사용자가 예상하는 것과 다르게 동작합니다.

실용적인 시작점은 다음과 같습니다:

set -Eeuo pipefail
trap 'printf "Error on line %s: %s\n" "$LINENO" "$BASH_COMMAND" >&2' ERR

실패한 명령이 스크립트를 중지해야 할 때 사용하세요. 의도적으로 명령을 프로브하고 계속하는 스크립트에서 무심코 사용하지 마세요. 예상되는 실패의 경우 조건을 명시적으로 작성하세요:

if ! grep -q '^enabled=true$' "$config_file"; then
  printf '기능이 비활성화되었습니다.\n'
fi

이는 set -e에서 grep이 실패하고 스크립트가 종료된 이유를 궁금해하는 것보다 명확합니다.

파일을 읽기 전에 인수 검증

일반적인 설정 버그는 $1이 없을 때 있는 것으로 취급하는 것입니다. set -u에서 누락된 $1을 참조하면 즉시 종료됩니다. set -u가 없으면 빈 문자열이 됩니다.

작은 사용법 블록을 사용하세요:

usage() {
  printf '사용법: %s <설정-파일> [환경]\n' "${0##*/}" >&2
}

if (( $# < 1 )); then
  usage
  exit 2
fi

config_file=$1
environment=${2:-dev}

if [[ ! -r $config_file ]]; then
  printf '설정 파일을 읽을 수 없습니다: %s\n' "$config_file" >&2
  exit 1
fi

environment에는 기본값이 있지만 config_file에는 없습니다. 기본값은 선택적 값에 유용하고 필수 값에는 위험합니다. 스크립트는 프로덕션 배포를 위해 매우 의도적인 경우가 아니라면 조용히 ./config.yml로 대체해서는 안 됩니다.

설정의 경로 및 값 인용

대부분의 Bash 스크립트는 결국 설정 파일이나 환경 변수에서 경로를 읽습니다. 해당 값이 인용되지 않으면 Bash는 단어 분할 및 글로브 확장을 수행합니다.

backup_dir="/mnt/backups/May reports"

# 잘못됨: 여러 인수가 됩니다.
cp $backup_dir/latest.tar.gz /restore/

# 올바름.
cp "$backup_dir/latest.tar.gz" /restore/

동일한 규칙이 명령 대체에 적용됩니다:

release_name=$(git describe --tags --always)
printf '배포 중: %s\n' "$release_name"

의도적으로 여러 인수가 필요한 경우 문자열 대신 배열을 사용하세요:

rsync_opts=(-a --delete --exclude '.git')
rsync "${rsync_opts[@]}" "$src/" "$dest/"

이는 opts="-a --delete" 다음에 rsync $opts ...의 취약한 패턴을 피합니다.

PATH 및 외부 명령 종속성 확인

command not found는 일반적으로 컨텍스트 문제입니다. 터미널은 /opt/homebrew/bin/aws에서 aws를 찾을 수 있지만 cron은 /usr/bin:/bin만 있습니다.

시작 시 필요한 도구를 확인하세요:

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || {
    printf '필요한 명령을 찾을 수 없습니다: %s\n' "$1" >&2
    exit 127
  }
}

require_cmd docker
require_cmd jq
require_cmd aws

중요한 시스템 유틸리티의 경우 절대 경로가 괜찮을 수 있습니다. 다른 위치에 설치된 개발자 도구의 경우 명확한 오류가 있는 종속성 검사가 일반적으로 유지 관리하기 쉽습니다.

스크립트가 systemd에 의해 실행되는 경우 사용자의 .bashrc에 의존하는 대신 유닛 또는 환경 파일에 환경을 설정하세요. 비대화형 셸이 터미널과 동일한 시작 파일을 반드시 읽는 것은 아닙니다.

환경 변수를 명시적으로 구문 분석

환경 기반 설정은 편리하지만 비어 있음과 설정되지 않음이 항상 같은 것은 아닙니다. Bash 매개변수 확장을 사용하면 정확하게 지정할 수 있습니다:

: "${APP_ENV:?APP_ENV가 설정되어야 합니다}"
log_level=${LOG_LEVEL:-INFO}

${APP_ENV:?message}는 변수가 설정되지 않았거나 비어 있으면 실패합니다. ${LOG_LEVEL:-INFO}는 설정되지 않았거나 비어 있으면 기본값을 사용합니다. 빈 문자열이 스크립트에서 의미가 있는 경우 콜론이 없는 형식(예: ${VAR-default})을 사용하세요.

문제 해결 중에 전체 환경을 로그에 덤프하지 마세요. 토큰, 데이터베이스 비밀번호 또는 클라우드 자격 증명을 인쇄하기 쉽습니다.

CRLF 줄 끝 및 보이지 않는 문자 주의

Windows에서 편집된 스크립트에는 CRLF 줄 끝이 포함될 수 있습니다. 전형적인 증상은 ^M이 포함된 오류 또는 인터프리터가 존재하지 않는 것처럼 보이는 shebang 실패입니다.

다음으로 확인하세요:

file deploy.sh
sed -n 'l' deploy.sh | head

다음 중 하나로 수정하세요:

dos2unix deploy.sh
# 또는 dos2unix를 사용할 수 없는 경우:
sed -i 's/\r$//' deploy.sh

또한 복사된 설정 값에서 후행 공백을 확인하세요. prod처럼 보이지만 실제로는 prod 인 변수는 case 분기를 놓쳐서 빙글빙글 돌게 할 수 있습니다.

첫 번째 실패 명령 디버깅

set -x는 확장 후 명령을 표시합니다. 이것이 인용 및 설정 버그에 정확히 필요한 것입니다:

PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
# 실패 섹션 여기
set +x

비밀 주변에서 xtrace를 활성화하지 마세요. 스크립트가 비밀번호, 토큰, 서명된 URL 또는 개인 키를 처리하는 경우 필요한 좁은 섹션만 추적하세요.

설정 파일의 경우 해결된 값과 적용하려는 테스트를 인쇄하세요:

printf 'config_file 사용: %q\n' "$config_file" >&2
[[ -r $config_file ]] || exit 1

%q는 공백을 셸 친화적인 방식으로 표시하므로 디버깅에 유용합니다.

권한도 설정으로 처리

때로는 스크립트가 올바르지만 실행하는 계정이 설정을 읽거나, 도우미를 실행하거나, 출력 디렉토리에 쓸 수 없습니다.

실제 사용자를 확인하세요:

id
namei -l "$config_file"

namei -l은 경로의 모든 디렉토리에 실행 권한이 필요하기 때문에 특히 유용합니다. 접근할 수 없는 상위 디렉토리 내의 읽을 수 있는 파일은 여전히 접근할 수 없습니다.

실행 가능한 스크립트의 경우 패키징 또는 이미지 빌드 중에 권한과 줄 끝을 함께 설정하세요:

chmod 0755 /usr/local/bin/deploy

스크립트가 sudo에서만 작동하는 경우 권한이 필요한 파일 또는 명령을 식별하세요. 하나의 잘못된 소유권 설정을 감추기 위해 전체 스크립트를 루트로 실행하지 마세요.

신뢰할 수 있는 문제 해결 패스

Bash 설정 문제가 명확하지 않은 경우 다음 패스를 순서대로 실행하세요:

  1. Bash 기능을 사용하는 경우 스크립트가 Bash에서 실행 중인지 확인합니다.
  2. 실패 컨텍스트의 작업 디렉토리, 사용자 및 PATH를 인쇄합니다.
  3. 기본 로직 전에 필수 인수 및 설정 파일을 검증합니다.
  4. 의도적으로 분할하려는 경우가 아니면 모든 확장을 인용합니다.
  5. command -v로 필요한 외부 명령을 확인합니다.
  6. 비밀이 보호된 실패 섹션 주변에서만 set -x를 사용합니다.
  7. 비즈니스 로직을 변경하기 전에 권한 및 줄 끝을 확인합니다.

이 시퀀스는 스크립트를 미스터리 소설로 만들지 않고 대부분의 실제 실패를 포착합니다. Bash는 작지만 실행 컨텍스트는 큽니다. 컨텍스트를 먼저 문제 해결하세요.

설정 로딩과 실행 분리

설정 로딩이 자체 단계일 때 스크립트 문제를 해결하기 더 쉽습니다. 하나의 긴 블록에서 파일을 읽고, 변수를 내보내고, 디렉토리를 만들고, 서비스를 다시 시작하지 마세요. 먼저 값을 확인합니다. 그런 다음 검증합니다. 그런 다음 작업을 실행합니다.

load_config() {
  local file=$1
  [[ -r $file ]] || {
    printf '설정을 읽을 수 없습니다: %s\n' "$file" >&2
    return 1
  }

  # 의도적으로 간단한 KEY=VALUE 파일의 예.
  # 완전히 신뢰하지 않는 파일을 소싱하지 마세요.
  while IFS='=' read -r key value; do
    [[ -z $key || $key == \#* ]] && continue
    case $key in
      APP_PORT) APP_PORT=$value ;;
      APP_ENV) APP_ENV=$value ;;
      *) printf '알 수 없는 설정 키 무시: %s\n' "$key" >&2 ;;
    esac
  done < "$file"
}

. config.env로 설정 파일을 소싱하는 것은 일반적이지만 셸 코드를 실행합니다. 이는 파일이 신뢰되고 코드처럼 소유된 경우에만 허용됩니다. 사용자가 편집할 수 있는 설정의 경우 지원하는 키만 구문 분석하세요.

다음 운영자를 위해 실패를 실행 가능하게 만들기

좋은 오류 메시지는 무엇이 실패했고 어떤 값이 원인인지 알려줍니다. 다음을 비교하세요:

printf '오류\n' >&2

그리고:

printf '백업 디렉토리에 쓸 수 없습니다: %s\n' "$backup_dir" >&2

두 번째 메시지는 다음 사람에게 확인할 내용을 제공합니다. 이는 DevOps 스크립트에서 중요합니다. 실패를 보는 사람이 작성자가 아닐 수 있기 때문입니다. 그들은 당직 중이거나 반쯤 자고 있거나 실패한 배포에서 CI 로그를 보고 있을 수 있습니다.

종료 코드도 의미를 전달할 수 있습니다. 사용법 문제에는 2, 일반 런타임 실패에는 1, 문서화된 이유가 있는 경우 도구별 코드를 사용하세요. 분류법을 만드는 데 하루 종일 보내지 마세요. 하지만 스크립트가 경고를 인쇄했다고 해서 검증 실패 후 성공을 반환하지 마세요.

좋아하는 컨텍스트가 아닌 실패 컨텍스트 테스트

systemd가 스크립트를 실행하는 경우 systemd로 테스트하세요. cron이 실행하는 경우 축소된 환경으로 테스트하세요. 빠른 근사치는 다음과 같습니다:

env -i HOME="$HOME" PATH=/usr/bin:/bin bash ./script.sh config.env

이는 대화형 셸의 안전 담요를 제거합니다. 누락된 내보내기 및 PATH 가정이 빠르게 나타납니다.

Docker 엔트리포인트 스크립트의 경우 프로덕션과 동일한 환경 및 마운트로 이미지를 가능한 가깝게 실행하세요:

docker run --rm --env-file app.env -v "$PWD/config:/config:ro" my-image:tag

CI에서만 실패하는 경우 CI 실행기의 작업 디렉토리와 정확한 명령줄을 인쇄하세요. 많은 CI Bash 실패는 체크아웃 후 잘못된 상대 경로일 뿐이며 깊은 셸 문제가 아닙니다.

출시 전 실제 검토 패스

스크립트나 컨테이너 설정이 완료되었다고 부르기 전에 다음 사람이 오전 2시에 디버깅해야 하는 것처럼 한 번 읽어보세요. 그러면 눈에 띄는 것이 달라집니다. 스크립트를 작성하는 동안 이해가 되었던 프롬프트가 CI 로그에 나타나면 모호할 수 있습니다. 명백해 보였던 Docker 서비스 이름이 애플리케이션의 변수 이름과 일치하지 않을 수 있습니다. Bash 기본값은 개발에는 안전하고 프로덕션에는 위험할 수 있습니다.

의도적으로 어색한 값으로 짧은 드라이 런을 하는 것을 좋아합니다. 공백이 있는 경로를 사용하세요. 빈 선택적 값을 사용하세요. 대시로 시작하는 파일 이름을 시도해보세요. 다른 작업 디렉토리에서 스크립트를 실행하세요. 하나의 예상 환경 변수 없이 컨테이너를 시작하세요. 이러한 테스트는 화려하지 않지만 일반적으로 먼저 깨지는 가정을 포착합니다.

또한 실패 메시지를 확인하세요. 유일한 출력이 실패인 경우 문서의 조언이 구현에 반영되지 않은 것입니다. 유용한 실패는 어떤 값이 사용되었는지, 어떤 검사가 실패했는지, 운영자가 무엇을 변경할 수 있는지 알려줍니다. 이는 모든 환경 변수를 덤프하거나 비밀을 인쇄하는 것을 의미하지 않습니다. 구체성이 도움이 되는 곳에서 구체적이어야 함을 의미합니다: 설정 경로, 누락된 명령 이름, 네트워크 이름, 서비스 호스트 이름 또는 프로세스가 바인딩하려고 시도한 포트.

마지막 습관은 예제를 시스템이 실제로 실행되는 방식에 가깝게 유지하는 것입니다. 프로덕션에서 Compose를 사용하는 경우 Compose로 테스트하세요. 스크립트가 systemd에 의해 실행되는 경우 systemd 또는 유사하게 최소 환경으로 테스트하세요. 명령이 복사하여 붙여넣기에 안전해야 하는 경우 예제 자체에 인용, -- 구분 기호 및 검증을 포함하세요. 독자는 경고보다 작동하는 패턴을 더 자주 복사합니다.

그 검토 패스는 관료주의가 아닙니다. 작은 자동화를 지루하게 유지하는 방법입니다. 지루함은 셸 프롬프트, 설정 로더, 변수 확장, 컨테이너 진단 및 Docker 네트워킹에서 원하는 것입니다. 동작이 덜 놀라울수록 다음 운영자가 신뢰하기 쉽습니다.