n=0; cat lines.txt | while read -r line; do n=$((n + 1)); done으로 줄 수를 세고 echo "count: $n"으로 값을 찍는 한 줄이 있습니다. lines.txt는 세 줄인데, 이 한 줄을 zsh로 돌리면 3이 나오고 같은 줄을 #!/bin/bash로 시작하는 스크립트 count.sh에 넣어 ./count.sh로 실행하면 0이 나옵니다. 이 글을 읽고 나면 bash에서 루프가 바꾼 변수가 어디서 사라지는지 설명할 수 있고, 두 셸 모두에서 3을 돌려주는 고친 형태를 바로 쓸 수 있습니다.
예제는 2026-11-09에 macOS 27.0.1에 들어 있는 /bin/bash 3.2.57과 /bin/zsh 5.9로 실행했습니다. lines.txt의 세 줄은 alpha, beta, gamma이고, zsh로 쓴 실행 스크립트가 경우마다 셸을 새로 띄워 돌렸습니다.
같은 스크립트가 bash에서는 0, zsh에서는 3
문제의 스크립트 count.sh입니다.
#!/bin/bash
n=0
cat lines.txt | while read -r line; do
n=$((n + 1))
done
echo "count: $n"
/bin/bash count.sh는 count: 0, /bin/zsh count.sh는 count: 3을 찍었습니다. 루프 안에서는 두 셸 모두 n이 3까지 올라갔지만(bash 쪽은 뒤의 중괄호 묶음에서 확인합니다), 파이프가 끝난 뒤 남은 n이 달랐습니다.
터미널에서 쓰는 셸이 스크립트를 돌리는 셸이라는 보장은 없습니다. 이 Mac 계정의 로그인 셸은 /bin/zsh이지만, zsh 스크립트 안에서 ./count.sh로 실행해도 결과는 0이었습니다. 실행 파일로 부르자 첫 줄 #!/bin/bash에 적힌 bash가 돌린 것입니다. 반대로 같은 한 줄을 설정 파일 없이 띄운 대화형 zsh(zsh -f)에 입력하면 3, 설정 파일 없이 띄운 대화형 bash(bash --norc)에 입력하면 0이었습니다. 그래서 터미널의 zsh에서 시험해 3을 확인한 줄이 bash 스크립트 안에서는 0이 될 수 있습니다.

bash에서는 파이프의 각 명령이 서브셸에서 돈다
GNU Bash 매뉴얼(5.3판)의 Pipelines 절은 여러 명령으로 된 파이프라인에서 각 명령이 자기 서브셸, 곧 별도 프로세스에서 실행된다고 적습니다. bash에서 cat lines.txt | while …을 돌리면, 뒤에서 다룰 lastpipe 예외(옵션을 켜 두었고 작업 제어가 꺼져 있을 때)를 빼고는 cat도, 오른쪽의 while 루프도 원래 셸이 아닌 새 프로세스에서 돕니다. 서브셸은 셸 환경을 복사해 시작하므로 루프 안에서 n은 0에서 출발해 3까지 올라갑니다. 그러나 POSIX.1-2024 셸 명령 언어(2.13 Shell Execution Environment)가 정하듯, 서브셸 환경에서 바꾼 것은 원래 셸 환경에 영향을 주지 않습니다. 파이프가 끝나면 그 프로세스도 끝나고 3은 함께 사라집니다. 원래 셸의 n은 처음부터 끝까지 0이었습니다.
루프가 정말 다른 프로세스에서 돌았는지 보려고 where.sh를 따로 돌렸습니다. where.sh는 echo x | while read -r _로 한 줄만 파이프에 넘기고, 루프 몸에서 sh -c 'echo "loop body started by pid: $PPID"'를 실행합니다. 루프 안에서 띄운 sh가 자기를 띄운 프로세스의 번호를 찍게 한 것입니다. bash에서는 셸의 번호가 50282인데 루프 쪽 번호는 50284였습니다. 루프를 돌린 것이 셸 자신이 아니었다는 뜻입니다(번호는 실행마다 달라집니다). 넘긴 줄이 하나뿐이라 루프 뒤에 찍은 셈은 bash에서 0, zsh에서 1이었습니다.
사라지는 것은 숫자만이 아닙니다. 파이프 오른쪽 루프에서 beta 줄을 만나면 exit 1을 부르는 스크립트를 bash로 돌리자, 그다음 줄의 after the loop: still running이 찍히고 종료 상태 0으로 끝났습니다. exit가 끝낸 것은 루프를 돌리던 서브셸이었습니다. 같은 스크립트를 zsh 5.9로 돌리면 그 줄은 찍히지 않았고 종료 상태는 1이었습니다.
zsh는 파이프 오른쪽 루프를 셸 안에서 돌린다
zsh 5.9로 where.sh를 돌리면 셸 번호와 루프 쪽 번호가 둘 다 50286으로 같았습니다. 2026-11-09 실행에서 zsh 5.9는 파이프 오른쪽의 while 루프를 셸 프로세스 안에서 돌렸고, 그래서 루프가 바꾼 값이 남았습니다.
POSIX는 파이프라인의 각 명령이 서브셸 환경에 있다고 하면서도, 확장으로서 일부 또는 모든 명령을 현재 환경에서 실행해도 된다고 열어 둡니다. bash의 기본 동작도, zsh 5.9에서 본 동작도 이 범위 안에 있습니다. zsh 공식 사이트의 사용자 안내서(A User's Guide to the Z-Shell, 3.8.4)는 파이프라인의 오른쪽을 현재 셸에서 돌린다고 적고, 이것은 zsh에서 보장되지만 다른 셸에서는 기대지 말라고 덧붙입니다. 다만 이 안내서는 zsh 4.0 무렵인 2003년에 쓰였고 레퍼런스 매뉴얼에는 같은 문장이 없어, 5.9의 동작은 실행으로 확인했습니다. 셸마다 답이 다를 수 있는 동작이므로, 어느 셸에서나 같은 결과가 필요한 스크립트는 이 차이에 기대지 않는 편이 안전합니다.
개발 노트
파이프 오른쪽의 루프는 어느 프로세스에서 돌았나
count.sh는 lines.txt 세 줄을 cat | while read로 세고, where.sh는 echo x 한 줄을 넘겨 루프 쪽 프로세스 번호를 찍음 · 2026-11-09 실행
bash 3.2.57: 루프는 복사본에서
count: 0- cat과 while을 각각 서브셸에서 실행
- where.sh: 루프 쪽 pid 50284, 셸은 50282
- count.sh: 루프가 늘린 n=3은 복사본에만
- 파이프가 끝나면 복사본이 사라져 count: 0
zsh 5.9: 루프는 셸 안에서
- where.sh: 루프 쪽 pid도 셸과 같은 50286
- count.sh: 루프가 바꾼 n이 남아 count: 3
- POSIX가 확장으로 허용하는 동작이다
- zsh 사용자 안내서(2003)도 '보장'이라 적음
고친 형태: 루프를 파이프 밖으로
- done < lines.txt: 파일을 루프에 바로 연결
- done < <(cat lines.txt): cat만 따로 실행
- bash 3.2.57과 zsh 5.9 모두 3
- 이 Mac의 sh(POSIX 모드 bash 3.2.57)에서는 < <( ) 줄이 문법 오류
셸 번호는 where.sh가 $$로, 루프 쪽 번호는 루프 몸의 sh가 $PPID로 찍었으며 실행마다 달라진다. macOS 27.0.1, /bin/bash 3.2.57, /bin/zsh 5.9.
lastpipe는 macOS의 bash 3.2에 없다
bash에도 파이프라인의 마지막 명령을 셸 안에서 돌리게 하는 옵션이 있습니다. bash 4.2에서 생긴 lastpipe를 켜면, 작업 제어가 꺼져 있을 때 파이프라인의 마지막 명령을 셸 프로세스가 돌릴 수 있습니다. 작업 제어가 켜져 있으면 효과가 없습니다.
그런데 이 Mac의 /bin/bash는 3.2.57입니다. 스크립트 첫머리에 shopt -s lastpipe를 넣자 shopt: lastpipe: invalid shell option name이 나오고 상태 1로 실패했습니다. set -e가 없는 스크립트라 다음 줄로 넘어갔고 결과는 여전히 0이었습니다. bash 4.2 이상은 이 Mac에 없어서 lastpipe를 켠 bash가 3을 찍는지는 돌려 보지 않았습니다.
파이프 대신 리디렉션으로 고친다
루프를 파이프 밖으로 꺼내면 bash 3.2.57에서도 루프가 셸 안에서 돕니다. 파일을 읽는다면 cat을 빼고 루프 끝에 파일을 바로 연결합니다.
n=0
while read -r line; do
n=$((n + 1))
done < lines.txt
echo "done < lines.txt: $n"
입력이 파일이 아니라 grep이나 find 같은 명령의 출력이라면 프로세스 치환을 씁니다. 루프 끝의 done < lines.txt 줄만 done < <(cat lines.txt)로 바뀝니다. 따로 도는 것은 괄호 안의 명령이고 루프는 셸 안에 남습니다. 두 형태 모두 bash 3.2.57과 zsh 5.9에서 3을 찍었습니다.
두 가지를 더 확인했습니다. 첫째, < <( )를 쓴 스크립트는 sh가 아니라 bash(또는 zsh)로 돌립니다. 두 형태를 차례로 담은 fixed.sh를 sh fixed.sh로 부르자(이 Mac의 sh는 POSIX 모드로 도는 bash 3.2.57) 첫 형태는 3을 찍었지만, 둘째 형태의 done < <(cat lines.txt) 줄(11번째 줄)에서 문법 오류로 멈췄고 종료 상태는 2였습니다. bash 변경 기록에 따르면 bash 5.1부터는 POSIX 모드에서도 프로세스 치환을 받아들이지만, 이 Mac에는 그런 bash가 없어 돌려 보지 않았습니다. sh로 돌릴 스크립트라면 done < 파일 형태를 씁니다. 둘째, 파이프를 꼭 남겨야 한다면 bash에서는 값을 쓰는 줄까지 중괄호로 묶어 같은 서브셸에 넣을 수 있습니다. cat lines.txt | { while …; done; echo "inside { }: $n"; }는 안에서 3을 찍었지만, 파이프가 끝난 뒤의 n은 bash에서 여전히 0이었습니다.

스크립트는 첫 줄에 적힌 셸로 시험한다
bash에서 파이프 오른쪽 루프가 바꾼 변수는 파이프가 끝나면 사라진다고 보고, 루프가 만든 값을 뒤에서 쓸 때는 done < 파일이나 done < <(명령)으로 씁니다. 시험할 때는 터미널에 줄을 입력해 보는 대신 첫 줄의 경로 그대로 /bin/bash count.sh로 부르거나 ./count.sh로 파일을 돌립니다. 그냥 bash count.sh라고 하면 PATH에서 먼저 찾은 다른 bash가 돌 수 있습니다. zsh에서 3이 나왔다는 것은 bash 스크립트도 3을 찍는다는 증거가 되지 못합니다.
이 기록에 대화를 더해 주세요.
궁금한 점, 다른 접근, 직접 해 본 결과를 나눠 주세요.
로그인 상태 확인 중…
댓글을 불러오는 중…