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

빌드노트

Bash에서 VAR=new 명령 "$VAR"가 이전 값을 인자로 넘기는 이유

Bash에서 명령 앞 대입과 같은 줄의 변수 인자가 서로 다른 값을 쓰는 이유를 확인한다. export 전후의 자식 인자·환경·부모 값을 비교하고, 별도 대입 후 호출하거나 자식에서 환경을 읽는 방법을 고른다.

Bash에서 명령 앞에 VAR=new를 붙였는데 같은 명령의 "$VAR" 인자는 이전 값일 수 있다. 호출하는 Bash가 인자를 먼저 확장하고, 명령 앞 대입은 실행할 프로그램의 환경에 적용하기 때문이다. 인자도 새 값이어야 한다면 대입을 별도 명령으로 먼저 끝내거나, 새 환경을 받은 자식 쪽에서 값을 읽도록 해야 한다.

같은 자식이 인자로는 old, 환경으로는 new를 받는다

아래는 Linux의 /bin/bash를 자식 프로그램으로 실행해 두 입력을 비교하는 예제다. WSL2의 Ubuntu 24.04.4 LTS, Bash 5.2.21에서 직접 실행했다. PowerShell 구문이 아니다. /bin/bash가 있는 Linux 터미널에서 env -i /bin/bash --noprofile --norc로 별도 실습 셸을 열고, 아래 입력을 순서대로 실행한다. 이 새 셸만 빈 환경으로 시작하며 사용자 초기화 파일은 수정하지 않는다.

CB_PREFIX_LAB는 이 실습에만 쓰는 변수다. 자동 export를 끄고 같은 이름을 비운 뒤, 부모의 기존 값·자식 인자·자식 환경·호출 뒤 부모 값을 확인한다.

set +a
unset CB_PREFIX_LAB
CB_PREFIX_LAB=old
printf 'parent-before=<%s>\n' "$CB_PREFIX_LAB"
CB_PREFIX_LAB=new /bin/bash --noprofile --norc -c 'printf "arg1=<%s>\nenv=<%s>\n" "$1" "$CB_PREFIX_LAB"' probe "$CB_PREFIX_LAB"
printf 'parent-after=<%s>\n' "$CB_PREFIX_LAB"

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

parent-before=<old>
arg1=<old>
env=<new>
parent-after=<old>

호출 줄의 맨 끝 "$CB_PREFIX_LAB"는 부모 Bash가 확장한다. 반면 작은따옴표 안의 코드는 자식 Bash가 실행한다. 그 안의 $1은 전달받은 인자이고, $CB_PREFIX_LAB는 자식이 환경에서 받아들인 값이다. probe는 bash -c의 $0 자리이므로 그다음 값이 $1이 된다. 이 인자 배치는 Bash의 -c 옵션 설명 (새 탭에서 열림)에 나와 있다.

Bash 매뉴얼의 Simple Command Expansion 절 (새 탭에서 열림)은 명령 이름 앞의 대입을 나중에 처리하도록 보관하고, 그 밖의 단어를 먼저 확장한다고 설명한다. 이 예제에 적용하면 다음 순서다.

  1. CB_PREFIX_LAB=new를 명령 앞 대입으로 구분해 둔다.
  2. 부모의 "$CB_PREFIX_LAB"를 old로 확장해 인자를 만든다.
  3. 실행할 자식의 환경에는 CB_PREFIX_LAB=new를 넣는다.

자식이 시작될 때는 인자가 이미 old로 정해져 있다. 환경에 new가 있어도 그 인자를 다시 확장하지 않는다. 이 외부 명령을 실행한 뒤 부모의 값도 old로 남았다. 큰따옴표는 유지해야 한다. 큰따옴표 안에서도 변수 확장은 일어나며, 인용을 없애서 이 순서를 고칠 수는 없다.

자기 명령에서도 인자가 이전 값이고 환경은 새 값이라면, 아래 두 수정 방법 중 프로그램의 입력 방식에 맞는 것을 적용한다. 위 입력 자체가 다른 결과를 내면 /bin/bash --version으로 실행 파일을 확인하고, 대입과 호출을 같은 실습 셸에서 순서대로 실행했는지 먼저 확인한다.

export를 추가해도 인자의 확장 시점은 같다

앞선 실습 셸에서 값은 그대로 두고 export 표시만 추가한 뒤, 같은 호출을 다시 실행했다.

export CB_PREFIX_LAB
CB_PREFIX_LAB=new /bin/bash --noprofile --norc -c 'printf "arg1=<%s>\nenv=<%s>\n" "$1" "$CB_PREFIX_LAB"' probe "$CB_PREFIX_LAB"
printf 'parent-after=<%s>\n' "$CB_PREFIX_LAB"
arg1=<old>
env=<new>
parent-after=<old>

export 전후 모두 인자는 old였다. 별도 대조에서는 접두 대입 없이 실행한 자식의 환경값이 export 전에는 없고, export 후에는 old인 것도 확인했다. export의 전달 효과와 인자 확장의 시점은 따로 봐야 한다. Environment 절 (새 탭에서 열림)이 설명하는 명령 앞 대입만으로도 이 자식에는 new가 전달된다. 이 증상에서 export를 반복해 붙이는 것은 인자를 바꾸는 해결책이 되지 않는다.

인자가 필요하면 먼저 대입하고, 환경이 필요하면 자식에서 읽는다

프로그램이 명령행 인자를 요구한다면 대입을 독립된 명령으로 끝낸 뒤 다음 명령에서 인자를 만든다. 다음은 같은 출력 명령이 인자와 환경 모두에서 new를 받게 하는 수정이다. 여기서는 부모의 old를 보존하려고 괄호 안에서 대입한다.

(
  export CB_PREFIX_LAB=new
  /bin/bash --noprofile --norc -c 'printf "arg1=<%s>\nenv=<%s>\n" "$1" "$CB_PREFIX_LAB"' probe "$CB_PREFIX_LAB"
)
printf 'parent-after=<%s>\n' "$CB_PREFIX_LAB"
arg1=<new>
env=<new>
parent-after=<old>

이제 인자를 확장하는 셸에는 이미 new가 대입돼 있다. 괄호는 별도 서브셸 범위를 만들므로 밖의 부모 값은 보존된다. 이 범위는 명령 묶기 설명 (새 탭에서 열림)에서도 확인할 수 있다. 이후 명령에도 새 값을 유지하려는 경우에는 괄호 없이 별도 export CB_PREFIX_LAB=new 다음에 호출하면 된다. 이 형태도 직접 실행했으며 인자·자식 환경·호출 뒤 부모 값이 모두 new였다.

프로그램이 환경변수를 직접 읽을 수 있다면, 호출하는 셸에서 굳이 같은 값을 인자로 만들 필요가 없다. 아래는 새 환경을 받은 자식 Bash가 값을 읽도록 바꾼 입력이다.

CB_PREFIX_LAB=old
CB_PREFIX_LAB=new /bin/bash --noprofile --norc -c 'printf "used=<%s>\n" "$CB_PREFIX_LAB"'
printf 'parent-after=<%s>\n' "$CB_PREFIX_LAB"
used=<new>
parent-after=<old>

이 호출에는 값을 넘기는 위치 인자가 없다. 작은따옴표로 감싼 고정 코드를 자식이 실행하면서 자기 환경의 new를 읽었다. 여기에 큰따옴표로 감싼 명령 문자열을 쓰면 부모가 그 안의 변수까지 먼저 확장할 수 있으므로 인용 범위를 유지한다. 실제 프로그램에서도 명령행 옵션을 요구하는지, 환경변수를 읽는지에 따라 두 방법 중 하나를 고르면 된다.

이 글은 기본 모드 Bash가 /bin/bash라는 외부 실행 파일을 호출한 경우를 검증했다. 셸 함수나 특수 내장 명령의 대입 범위로 일반화하지 않는다. 특히 특수 내장 명령 설명 (새 탭에서 열림)에는 POSIX 모드에서 명령 앞 대입이 호출 뒤에도 남는 규칙이 따로 있다. 함수·특수 내장 명령·다른 셸은 이번 실험에서 실행하지 않았다. 참고 문서는 Bash 5.3판이며 실제 실행 버전 5.2.21과 구분한다. 실습을 마치면 별도 셸에서 exit로 나온다.

← 전체 글 보기