본문 바로가기
빌드노트 검색

빌드노트

Bash heredoc으로 만든 설정 파일에서 $변수가 이미 바뀌었다

Bash heredoc의 종료 표식을 인용하지 않으면 설정 파일 생성 시점에 변수·명령·산술식이 확장된다. 인용 전후의 파일 내용을 비교해 본문 안의 작은따옴표가 확장을 막지 못하는 이유와 <<'EOF'로 표현식을 보존하는 방법을 설명하고, 이후 Bash가 파일을 읽을 때의 해석을 구분한다.

Bash 스크립트에서 cat <<EOF로 설정 파일을 만들었는데, 남겨 두려던 $SITE가 생성하는 셸의 값으로 바뀔 수 있다. 설정 파일을 읽는 프로그램이 실행되기 전에도 일어나는 변화다. heredoc 본문을 그대로 저장하려면 시작 부분을 <<'EOF'로 바꾼다. 끝을 표시하는 줄은 따옴표 없이 EOF로 둔다.

먼저 생성 스크립트에서 << 다음의 종료 표식(delimiter)을 확인한다. <<EOF처럼 인용하지 않으면 Bash가 본문의 변수 확장, 명령 치환, 산술 확장을 수행한 뒤 그 결과를 cat의 표준 입력으로 보낸다. GNU Bash 5.3 매뉴얼의 Here Documents 설명 (새 탭에서 열림)에 명시된 동작이다.

같은 본문을 넣고 종료 표식만 바꿨다

아래 입력은 변수 값과 명령 출력을 고정한 별도 재현이다. Linux 또는 WSL의 Bash에서 cat을 쓸 수 있어야 한다. 새 빈 임시 폴더를 작업 위치로 잡고 실행한다. 두 파일을 새로 만드는 예제이므로 기존 설정 폴더에서는 실행하지 않는다. 이 비교로 확인할 것은 본문을 보호하는 위치가 내용 안의 따옴표인지, 시작 종료 표식의 따옴표인지다.

SITE=writer-node
COUNT=7

cat > unquoted.conf <<EOF
endpoint=$SITE
single='$SITE'
command=$(printf 'fixed-command')
sum=$((COUNT+1))
EOF

cat > quoted.conf <<'EOF'
endpoint=$SITE
single='$SITE'
command=$(printf 'fixed-command')
sum=$((COUNT+1))
EOF

printf '%s\n' '[unquoted.conf]'
cat unquoted.conf
printf '%s\n' '[quoted.conf]'
cat quoted.conf

직접 실행한 출력은 다음과 같다. 앞쪽은 생성 시점의 값으로 바뀌었고, 뒤쪽은 달러 기호를 포함한 입력이 남았다.

[unquoted.conf]
endpoint=writer-node
single='writer-node'
command=fixed-command
sum=8
[quoted.conf]
endpoint=$SITE
single='$SITE'
command=$(printf 'fixed-command')
sum=$((COUNT+1))

single='$SITE' 줄이 특히 구별하기 쉽다. 보통 Bash 명령의 작은따옴표를 생각하면 보호될 것 같지만, 인용하지 않은 heredoc 본문에서는 이 작은따옴표가 $SITE의 확장을 막지 못했다. 저장된 것은 single='writer-node'였다. 명령 치환과 산술식도 각각 fixed-command, 8로 바뀌었다. 내용을 생성하는 Bash가 이미 처리한 결과다.

GNU 문서에 따르면 종료 표식의 일부라도 인용하면 본문을 확장하지 않는다. 위 예제에서는 <<'EOF'로 본문을 보존했다. 닫는 줄에는 인용을 제거한 표식인 EOF만 단독으로 쓴다. 이 예제의 <<에서는 그 줄에 들여쓰기나 뒤 공백도 넣지 않는다.

예제 결과가 다르면 실행한 셸이 Bash인지, 생성 스크립트를 실행하기 전에 바깥 명령이나 템플릿 도구가 본문을 바꿨는지 확인한다. EOF를 찾지 못했다는 경고가 나오면 먼저 닫는 줄의 따옴표·공백을 확인한다. 생성 직후의 파일에도 $SITE가 남아 있다면, 적어도 그 생성 단계에서 값으로 치환된 상황은 아니다. 이후 파일을 읽는 프로그램의 규칙을 살펴볼 차례다.

문자 그대로 저장한 뒤 누가 해석하는가

종료 표식을 인용했다고 해서 나중에 자동으로 변수가 채워지지는 않는다. 위에서 cat quoted.conf는 $SITE와 $(printf ...)를 그대로 출력했다. 파일을 읽어 표시하는 일과 그 내용을 Bash 코드로 실행하는 일은 다르다.

이 차이를 보기 위해 방금 직접 만든 합성 파일만 별도 Bash에서 읽었다. 다음 명령의 .은 파일의 명령을 현재 셸에서 실행 (새 탭에서 열림)하므로 출처를 모르는 설정 파일에는 적용하지 않는다. 확인할 것은 생성 때 보존한 표현식이 읽는 Bash의 값으로 평가되는지다.

SITE=reader-node COUNT=40 bash --noprofile --norc -c \
  '. ./quoted.conf; printf "endpoint=%s, single=%s, sum=%s\n" "$endpoint" "$single" "$sum"'

실제 출력은 다음과 같았다.

endpoint=reader-node, single=$SITE, sum=41

endpoint와 sum은 읽는 Bash의 값으로 평가됐다. 이 단계에서는 single='$SITE'가 일반 셸 대입문이므로 작은따옴표가 작동해 값에 $SITE라는 문자가 남았다. 같은 조건으로 unquoted.conf를 읽었을 때는 endpoint=writer-node, single=writer-node, sum=8이었다. 생성 시점에 저장한 값은 읽는 쪽의 변수를 바꿔도 되돌아오지 않았다. 어느 쪽을 읽든 파일 자체의 바이트가 바뀌는 것은 아니었다.

따라서 지금 생성하는 셸의 값을 넣을 것인지, 생성된 파일에 표현식을 남길 것인지를 먼저 정한다. 지금의 값을 넣는 것이 목적이면 인용하지 않은 heredoc을 쓸 수 있지만 본문의 명령 치환도 함께 실행된다는 점을 검토해야 한다. 표현식을 남겨야 한다면 종료 표식을 인용하고, 다음 소비자가 그 표현식을 어떤 문법으로 해석하는지 별도로 확인한다. 일반 설정 파일이 Bash와 같은 확장을 한다고 가정하지 않는다.

이 비교는 WSL2의 Ubuntu 24.04.4 LTS, Bash 5.2.21에서 합성 ASCII 입력과 LF 개행으로 실행했다. 별도 Bash가 읽는 단계까지 확인했으며, 실제 서비스의 설정 파서, 다른 셸, SSH·템플릿 도구를 거치는 생성 경로는 실행하지 않았다.

← 전체 글 보기