빌드노트
Bash while read 안에서는 늘던 변수가 루프 밖에서 0이다
파이프 오른쪽 while read가 바꾼 변수는 별도 셸에 남을 수 있다. 실제 BASHPID와 줄 수를 비교해 파일 리다이렉션·프로세스 치환을 고르고, lastpipe와 job control에 따른 예외를 확인한다.
Bash에서 파이프로 받은 줄을 while read로 세면, 루프 안에서는 1·2로 늘던 count가 루프 뒤에서는 0일 수 있다. 기본 설정에서는 파이프 오른쪽의 루프가 별도 서브셸에서 실행돼 그쪽 변수만 바꾸기 때문이다. 입력이 파일이면 done < 파일, 명령의 출력이면 Bash의 done < <(명령)으로 바꾸면 루프의 값을 현재 셸에 남길 수 있다.
다만 파이프가 언제나 이 결과를 내는 것은 아니다. Bash의 lastpipe 옵션과 job control에 따라 마지막 단계의 실행 위치가 달라진다. 합성 텍스트 두 줄로 실행 위치와 변수 값을 함께 비교했다.
루프 안팎의 BASHPID를 비교한다
아래 입력은 Bash와 env·grep이 있는 Linux 또는 WSL용이다. 쓰기 가능한 빈 실습 폴더에서 env -i PATH=/usr/bin:/bin LC_ALL=C bash --noprofile --norc로 새 Bash를 열고 실행한다. 뒤의 예제는 이 폴더에 sample.txt를 쓰므로 같은 이름의 기존 파일이 없는 곳을 고른다. 비교 설정은 이 새 셸에서만 바꾼다.
set +m
shopt -u lastpipe
shopt lastpipe
set -o | grep '^monitor'
count=0
printf 'before pid=%s count=%s\n' "$BASHPID" "$count"
printf 'alpha\nbeta\n' | while IFS= read -r line; do
count=$((count + 1))
printf 'inside pid=%s count=%s\n' "$BASHPID" "$count"
done
printf 'after pid=%s count=%s\n' "$BASHPID" "$count"
set +m은 job control을 끄고, shopt -u lastpipe는 마지막 파이프 단계를 현재 셸에서 실행하는 옵션을 끈다. 바로 뒤의 두 명령은 실제 설정을 출력한다. WSL2의 Ubuntu 24.04.4, Bash 5.2.21에서 관찰한 출력은 다음과 같았다. PID 숫자는 실행할 때마다 달라진다.
lastpipe off
monitor off
before pid=3494 count=0
inside pid=3498 count=1
inside pid=3498 count=2
after pid=3494 count=0
입력은 두 번 읽혔고, 루프 안에서는 값이 2까지 올랐다. 하지만 앞뒤 출력은 루프와 다른 PID에서 나왔고 그쪽 값은 계속 0이었다. 두 출력은 서로 다른 실행 환경의 변수를 보여 준다. Bash의 실행 환경 설명 (새 탭에서 열림)도 서브셸에서 바꾼 상태는 호출한 셸에 반영되지 않는다고 명시한다.
이 진단에는 $$보다 BASHPID를 쓴다. $$는 이런 서브셸에서도 호출한 셸의 PID를 가리킬 수 있다. 별도 비교에서는 루프 안팎의 $$가 같았는데 BASHPID는 달랐다. 두 변수의 정의는 BASHPID (새 탭에서 열림)와 특수 매개변수 (새 탭에서 열림)에 나와 있다.
파일은 루프에 직접 연결하고, 명령 출력은 프로세스 치환으로 받는다
처리할 입력이 이미 파일이라면 cat 파일 | while ...을 만들 필요가 없다. 같은 새 Bash에서 다음을 이어 실행한다. 첫 줄은 비교용 입력을 직접 만든다.
printf 'alpha\nbeta\n' > sample.txt
count=0
while IFS= read -r line; do
count=$((count + 1))
done < sample.txt
printf 'file after=%s\n' "$count"
file after=2
< sample.txt는 루프의 표준 입력을 파일로 연결한다. 루프를 파이프의 별도 단계로 만들지 않으므로 count가 현재 셸에 남았다. 상세 비교에서도 루프 안팎의 BASHPID가 같았다.
입력을 만드는 명령이 있다면 Bash에서는 다음처럼 받을 수 있다. 여기서는 외부 작업 대신 앞과 같은 두 줄을 출력하는 printf를 썼다. 자기 코드에 적용할 때는 괄호 안을 필요한 입력 생성 명령으로 바꾼다.
count=0
while IFS= read -r line; do
count=$((count + 1))
done < <(printf 'alpha\nbeta\n')
printf 'process after=%s\n' "$count"
process after=2
< <(...)에는 역할이 다른 두 부분이 있다. 앞의 <는 입력 리다이렉션이고, <(...)는 명령 출력을 읽을 수 있는 파일 이름처럼 제공하는 프로세스 치환이다. <( 사이에는 공백을 넣지 않는다. 입력 생성 명령은 따로 실행되지만, 위 루프는 현재 셸에 남았다. 생성 명령의 PID까지 출력한 추가 비교에서도 생성자만 다른 PID였다. Bash 프로세스 치환 문서 (새 탭에서 열림)에서 이 연결 방식을 확인할 수 있다.
파일을 읽는 데는 파일 리다이렉션을 고르면 된다. 명령 출력을 중간 파일 없이 읽고 변수도 남겨야 한다면 프로세스 치환을 고를 수 있다. 이 문법을 POSIX sh 전체에서 쓸 수 있다고 가정하면 안 된다. 위 명령은 Bash로 실행하며, 시스템의 FIFO 또는 /dev/fd 지원도 필요하다.
파이프인데 값이 남는 환경도 있다
Bash의 lastpipe 설명 (새 탭에서 열림)에 따르면, lastpipe가 켜져 있고 job control이 꺼져 있으면 백그라운드가 아닌 파이프의 마지막 명령을 현재 셸에서 실행한다. 같은 Bash 버전의 새 프로세스마다 설정을 달리해 파이프 예제를 비교했다.
lastpipe=off,monitor=off: 루프 뒤count=0.lastpipe=on,monitor=off: 루프 뒤count=2, 루프 안팎의BASHPID도 같았다.lastpipe=off,monitor=on: 루프 뒤count=0.lastpipe=on,monitor=on: 루프 뒤count=0.
여기서 monitor는 set -o 출력에 나오는 job control 설정 이름이다. shopt -s lastpipe만 추가해도 모든 터미널에서 같아지는 것은 아니다. 파일 리다이렉션과 프로세스 치환은 위 네 조합 모두에서 2가 남았다. 스크립트가 다른 실행 환경에서도 같은 방식으로 상태를 남겨야 한다면 루프에 입력을 직접 연결하는 편이 설정 의존을 줄인다. 기존 셸의 옵션을 바꾸기 전에, 문제가 나는 스크립트 자체에서 옵션과 BASHPID를 확인한다.
export count나 set -o pipefail을 추가한 별도 실험도 기본 파이프에서는 루프 뒤 0이었다. export는 자식에게 값을 전달하는 표시이고, pipefail은 파이프의 종료 상태를 고르는 설정이다. 어느 쪽도 서브셸의 변경분을 부모 변수에 합치지 않는다.
프로세스 치환으로 값이 남았다고 입력 생성까지 성공했다는 뜻은 아니다. 두 줄을 출력한 뒤 7로 끝나는 생성 함수를 연결하자 count=2였지만, 루프 바로 뒤에서 읽은 $?는 0이었다. 이때 pipefail도 켜져 있었다. 입력 생성 실패를 먼저 판정해야 한다면 명령 결과를 별도 파일에 저장하고, 그 명령의 종료 상태가 성공일 때만 파일 리다이렉션으로 읽는 방식을 고려한다.
이 관찰은 비대화형 새 Bash의 전경 루프로 확인했다. 예제 입력은 두 줄 모두 LF로 끝난다. 개행 없는 마지막 데이터나 NUL이 든 입력, 다른 셸·Bash 버전, 실제 작업 명령은 검증하지 않았다. 참고 매뉴얼은 5.3판이고 실행한 버전은 5.2.21이다. 입력 리다이렉션도 루프를 감싼 ( ... )나 별도 bash 호출의 경계를 없애지는 않으므로, 값이 필요한 위치와 루프가 같은 셸인지 확인한다. 비교를 마치면 exit로 실습용 Bash에서 나온다.