빌드노트
Bash read 뒤에 경로의 역슬래시와 양끝 공백이 사라진다면
read -r는 역슬래시를 보존하지만 양끝 공백까지 지키려면 IFS=도 필요하다. 실제 바이트와 루프 실행 횟수를 비교하고, 값이 할당돼도 EOF에서 실패하는 마지막 개행 없는 입력의 처리 조건을 확인한다.
Bash에서 파일의 한 줄을 read line으로 받았는데 C:\new\folder가 C:newfolder로 바뀌거나 양끝 공백이 사라질 수 있다. read가 입력을 그대로 복사하지 않고 역슬래시와 필드 구분자를 해석하기 때문이다. 줄 내용을 한 변수에 보존하려면 IFS= read -r line을 사용한다. 다만 마지막 개행이 없는 값까지 처리할지는 루프 조건에서 따로 정해야 한다.
-r만으로 공백까지 보존되지는 않는다
아래는 실제 경로를 열지 않고 경로 모양의 문자열만 읽는 비교다. Linux 또는 WSL에서 bash --noprofile --norc로 새 Bash를 열고 실행한다. mktemp가 새 임시 파일 하나를 만들며, 같은 셸에서 뒤의 예제까지 이어 실행한다. 직접 확인한 환경은 WSL2의 Ubuntu 24.04.4 LTS, Bash 5.2.21이다.
export LC_ALL=C
IFS=$' \t\n'
sample=$(mktemp)
printf '%s\n' ' C:\new\folder ' > "$sample"
read line < "$sample"
printf 'read: <%s> bytes=%d\n' "$line" "${#line}"
read -r line < "$sample"
printf 'read -r: <%s> bytes=%d\n' "$line" "${#line}"
IFS= read -r line < "$sample"
printf 'IFS= read -r: <%s> bytes=%d\n' "$line" "${#line}"
read: <C:newfolder> bytes=11
read -r: <C:\new\folder> bytes=13
IFS= read -r: < C:\new\folder > bytes=17
입력의 경로 앞뒤에는 공백이 두 개씩 있고 마지막에는 LF 개행이 하나 있다. printf의 고정 형식 %s로 문자열을 넣었으므로 입력을 만들 때 역슬래시를 해석하지 않는다. 출력의 꺾쇠는 값의 경계를 보이기 위해 붙였다. 이번 입력은 ASCII이고 LC_ALL=C이므로 ${#line}으로 표시한 길이를 바이트 수와 비교할 수 있다. 마지막 개행은 세 경우 모두 변수에 들어가지 않았다.
기본 read는 역슬래시를 이스케이프 문자로 다룬다. 여기서 \n을 줄바꿈으로 바꾼 것은 아니다. 역슬래시를 제거하고 다음 문자 n을 남겼다. -r는 이 처리를 끈다. 역슬래시 뒤에 실제 개행이 있을 때 두 줄을 이어 읽는 동작도 막는다. GNU Bash 매뉴얼의 read 설명 (새 탭에서 열림)
양끝 공백이 사라지는 이유는 별개다. 위에서 설정한 기본 IFS는 공백·탭·개행이며, read는 이 값을 기준으로 입력을 필드로 나눈다. 변수 이름이 하나여도 양끝의 IFS 공백은 제거된다. IFS=는 이 read 호출의 구분자를 빈 값으로 만든다. unset IFS는 기본 분리 규칙으로 돌아가므로 같은 대안이 아니다. Debian의 Bash 5.2 매뉴얼: Word Splitting (새 탭에서 열림)
줄 경계도 별도로 비교했다. 경로 한 줄, 끝에 역슬래시가 붙은 alpha 한 줄, beta 한 줄, 공백·탭·공백만 있는 한 줄, 빈 줄, 양끝에 공백이 두 개씩 있는 tail\end 한 줄을 합성 입력으로 만들었다. 여섯 줄 모두 개행으로 끝낼 때 일반 while read 본문은 5회 실행됐고, 변수에 들어온 내용의 합은 27바이트였다. alpha 다음의 역슬래시와 실제 개행이 제거돼 alphabeta 한 값이 된 것이다. read -r는 6회·31바이트, IFS= read -r는 6회·42바이트였다. 이 바이트 합계에는 각 줄을 끝내는 개행은 포함하지 않았다.
값이 들어왔어도 EOF에서 종료 상태는 1이다
IFS= read -r로 바꾼 뒤에도 마지막 값만 처리되지 않으면 파일 끝의 개행을 확인한다. 앞서 만든 임시 파일을 같은 문자열로 덮되, 이번에는 끝에 개행을 쓰지 않는다.
printf '%s' ' C:\new\folder ' > "$sample"
if IFS= read -r line < "$sample"; then rc=0; else rc=$?; fi
printf 'status=%d bytes=%d value=<%s>\n' "$rc" "${#line}" "$line"
status=1 bytes=17 value=< C:\new\folder >
같은 값에 마지막 개행이 있을 때는 상태가 0이었다. 개행이 없을 때는 EOF를 만나 1을 반환했지만 변수에는 17바이트가 이미 할당돼 있었다. read의 실패 상태를 곧바로 “아무 값도 읽지 못했다”로 해석하면 안 된다. 공식 매뉴얼도 EOF를 비영 종료 상태의 조건으로 설명한다. GNU Bash 매뉴얼의 종료 상태 설명 (새 탭에서 열림)
while IFS= read -r line; do ...; done은 상태가 0인 경우에만 본문을 실행한다. 따라서 개행 없는 마지막 값은 변수에 들어와도 본문으로 넘어가지 않는다. 위의 여섯 줄 입력에서 마지막 개행만 뺀 비교에서도 본문 실행은 6회에서 5회로 줄었다.
마지막 개행 없는 값도 한 레코드로 받아들이려면 다음 조건을 쓴다. 앞의 개행 없는 임시 파일에 그대로 실행할 수 있다.
while IFS= read -r line || [[ -n $line ]]; do
printf '<%s>\n' "$line"
done < "$sample"
< C:\new\folder >
read가 실패해도 남은 값이 비어 있지 않으면 본문을 한 번 실행한다. 여섯 줄 비교에서는 마지막 개행의 유무와 관계없이 6회·42바이트를 처리했다. 추가 비교에서 빈 파일은 0회, 개행 하나만 있는 파일은 빈 값으로 1회, 개행 없이 공백·탭·공백만 있는 파일은 3바이트 값으로 1회였다. IFS=를 빼면 마지막 공백 값이 먼저 빈 문자열로 바뀌므로 이 조건만 추가해서는 보존할 수 없다.
파일 형식이 모든 레코드에 마지막 개행을 요구한다면, 이 조건으로 불완전한 입력을 자동 수용하기보다 생산자가 개행을 쓰도록 고치거나 수신 시 형식 오류로 구분한다. 반대로 마지막 개행이 선택 사항인 텍스트 목록이면 위 조건이 맞는다. 이 조건은 EOF와 다른 읽기 오류를 구별하는 오류 처리기까지 제공하지는 않는다.
읽은 뒤에도 값을 전달할 때는 "$line"처럼 인용한다. 이 예제의 printf '%s' "$line" 형태를 유지하면 출력 단계에서 공백 분리나 역슬래시 해석이 다시 끼어드는 것을 피할 수 있다. 결과가 다르면 읽은 직후나 루프 본문 안에서 printf '%s' "$line" | od -An -tx1로 변수의 바이트를 확인하고 od -An -tx1 "$sample"의 파일 바이트와 대조한다. 보존된 경로 값의 바이트는 20 20 43 3a 5c 6e 65 77 5c 66 6f 6c 64 65 72 20 20이었다. 20은 공백, 5c는 역슬래시이며, 파일의 마지막 LF는 0a다.
이번 실험은 LF로 구분한 ASCII 텍스트와 일반 파일 입력을 확인했다. CRLF 변환이나 NUL을 포함한 데이터의 무손실 처리는 검증하지 않았다. 특히 기본 read는 NUL을 무시하므로 IFS= read -r를 임의의 바이너리 복사 방법으로 쓰면 안 된다. 문서 설명은 유지관리자가 제공하는 GNU Bash 5.3 매뉴얼과 Debian의 Bash 5.2 매뉴얼을 읽어 대조했고, 위 출력은 Bash 5.2.21에서 직접 얻었다. GNU Bash 매뉴얼의 NUL 처리 조건 (새 탭에서 열림)