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

빌드노트

Bash while read가 첫 줄만 처리한다면 자식의 stdin을 확인한다

목록을 읽는 while read 안의 자식 명령이 같은 stdin을 소비하면 루프가 첫 항목에서 끝날 수 있다. cat으로 남은 두 줄이 읽히는 출력을 비교하고, 빈 입력 제공과 read -u 3을 통한 입력 분리를 선택한다.

Bash에서 목록 파일을 while read로 읽는데 첫 항목을 처리한 뒤 루프가 끝난다면, 본문 안에서 실행하는 명령의 표준 입력부터 확인한다. 자식 명령도 루프와 같은 stdin을 물려받아 남은 목록을 먼저 읽을 수 있다. 그 명령에 입력이 필요 없다면 < /dev/null을 붙인다. 입력이 필요한 명령이라면 목록을 별도 파일 디스크립터로 읽어 두 입력을 분리한다.

done < list.txt로 파일을 연결해도 이 문제는 생긴다. 이 리다이렉션은 루프의 표준 입력을 바꾸며, 그 안의 자식에게 줄마다 독립된 입력을 만들어 주지는 않는다. 아래에서는 네트워크 명령 대신 cat으로 같은 현상을 재현한다.

세 줄 중 두 줄을 cat이 읽는다

Linux 또는 WSL에서 Bash와 cat을 사용할 수 있어야 한다. 쓰기 가능한 빈 폴더에서 아래 코드를 실행한다. list.txt는 이 예제가 새로 만드는 파일이며, 세 줄 모두 LF 개행으로 끝난다. loop= 출력의 개수와 마지막 count를 보면 실제 반복 횟수를 알 수 있다.

printf 'alpha\nbeta\ngamma\n' > list.txt
n=0
while IFS= read -r item; do
    n=$((n + 1))
    printf 'loop=%s item=%s\n' "$n" "$item"
    cat
done < list.txt
printf 'count=%s\n' "$n"

WSL2 Ubuntu 24.04의 Bash 5.2.21, GNU coreutils 9.4에서 실행한 출력이다.

loop=1 item=alpha
beta
gamma
count=1

화면에 세 항목이 보이지만 루프는 한 번만 돌았다. beta와 gamma에는 loop=가 없다. 이 두 줄은 인자 없이 실행한 cat이 표준 입력을 읽어 그대로 출력한 것이다. 같은 코드에서 cat 줄을 뺀 대조 실행은 loop=1부터 loop=3까지 출력하고 count=3으로 끝났다.

read가 alpha를 읽은 다음, 자식 cat이 같은 입력에서 나머지 두 줄을 EOF까지 읽었다. 그 뒤 루프 조건의 read에는 읽을 내용이 남지 않았다. Bash의 명령 실행 환경 설명 (새 탭에서 열림)은 외부 명령에 셸의 열린 파일과 명령별 리다이렉션이 전달된다고 설명한다. 이 예제의 출력은 그 입력을 자식이 먼저 소비했음을 보여 준다. 목록 파일 자체를 지운 것은 아니다.

입력이 필요 없는 명령에는 빈 입력을 준다

먼저 본문 안의 cat 한 줄을 cat < /dev/null로 바꾸고, n=0부터 루프를 다시 실행한다. 전체 코드는 다음과 같다. 앞에서 만든 list.txt를 그대로 읽는다.

n=0
while IFS= read -r item; do
    n=$((n + 1))
    printf 'loop=%s item=%s\n' "$n" "$item"
    cat < /dev/null
done < list.txt
printf 'count=%s\n' "$n"
loop=1 item=alpha
loop=2 item=beta
loop=3 item=gamma
count=3

이번에는 cat이 즉시 EOF를 받아 아무것도 출력하지 않았고, 목록은 세 번 모두 read가 읽었다. < /dev/null은 파일 디스크립터 0을 닫는 대신 빈 입력을 연결한다. 적용 범위는 그 자식 명령이므로 다음 반복의 read는 계속 목록을 읽는다. 번호 없는 <가 표준 입력인 FD 0에 적용되는 규칙은 Bash 입력 리다이렉션 문서 (새 탭에서 열림)에 있다.

자기 스크립트에서도 입력을 사용하지 않는 명령에 이 변경을 적용했을 때 반복 횟수가 회복되는지 확인한다. 회복된다면 그 명령의 stdin 사용이 원인이라는 근거가 된다. 반대로 명령이 표준 입력으로 작업 데이터를 받아야 한다면 이 수정을 그대로 쓰면 안 된다. 그 데이터까지 EOF로 바뀐다.

자식에게 줄 입력이 있다면 목록을 FD 3으로 옮긴다

다음 비교에서는 목록을 FD 3으로 열고, 자식의 표준 입력에는 별도의 한 줄을 연결한다. 같은 빈 폴더에 child-stdin.txt를 추가하며, FD 3을 다른 용도로 쓰지 않는 Bash에서 실행한다. read -u 3으로 읽는 대상만 바꾸었는지, 자식의 child-input이 목록 항목으로 섞이지 않는지 확인한다.

printf 'child-input\n' > child-stdin.txt
n=0
while IFS= read -r -u 3 item; do
    n=$((n + 1))
    printf 'loop=%s item=%s\n' "$n" "$item"
    cat 3<&-
done 3< list.txt < child-stdin.txt
printf 'count=%s\n' "$n"
loop=1 item=alpha
child-input
loop=2 item=beta
loop=3 item=gamma
count=3

3< list.txt는 목록을 읽기 전용으로 FD 3에 열고, < child-stdin.txt는 FD 0에 자식용 입력을 연결한다. Bash의 read 설명 (새 탭에서 열림)에 따라 -u 3을 지정한 read는 FD 0 대신 FD 3을 읽는다. 따라서 cat이 표준 입력을 끝까지 읽어도 목록의 다음 항목이 남는다.

cat 3<&-의 3<&-는 그 자식에 전달될 FD 3만 닫는다. 부모 루프의 FD 3은 그대로다. 목록용 FD를 자식에게 넘길 이유가 없어 닫았으며, 이것이 자식의 표준 입력을 끊는 것은 아니다. 닫기 문법은 Bash 파일 디스크립터 리다이렉션 문서 (새 탭에서 열림)에서 확인할 수 있다. 위 코드는 FD 0을 복제한 것이 아니라, 두 파일을 서로 다른 FD에 각각 연 것이다.

출력에서 child-input은 한 번만 나온다. 첫 cat이 자식용 입력을 끝까지 읽었기 때문에 다음 cat들은 그쪽 EOF를 만난다. FD 분리는 입력 경로를 나눌 뿐, 자식에게 매번 새 데이터를 공급하지는 않는다. 매 반복마다 같은 입력 파일 전체가 필요하다면 그 파일의 입력 리다이렉션을 자식 명령에 붙여 매번 새로 열어야 한다.

이 글의 재현에는 파이프가 없고, 루프 뒤 변수도 실제 반복 횟수인 1 또는 3으로 남았다. 루프 안에서는 여러 번 실행됐는데 밖의 변수만 원래 값인 문제와 구분한다. 입력 마지막 줄에도 개행이 있으므로 마지막 LF 누락을 다룬 실험도 아니다. IFS=와 -r는 공백·역슬래시 처리에 관한 설정이며, 자식이 같은 입력을 읽는 것을 막지는 않는다.

확인한 문서는 Bash 5.3판이고 실제 실행은 5.2.21이었다. 합성 파일과 cat으로 입력 소비 원인을 비교했으며 실제 SSH·서비스, 다른 셸, 바이너리 입력은 검증하지 않았다. 입력을 분리한 뒤에도 루프가 끝난다면 본문 안의 break·exit, 명령 실패에 따른 종료, 실제 파일 내용을 이어 확인해야 한다. 자식의 입력 소비만으로 모든 조기 종료를 설명할 수는 없다.

← 전체 글 보기