빌드노트
Bash에서 마지막 줄을 거부했는데 앞선 줄은 이미 처리됐다
모든 줄에 LF가 필수인 Bash 입력에서 마지막 줄을 거부해도 앞선 작업은 남을 수 있다. 작은 파일을 배열에 모아 전체 검증 뒤 처리하는 코드와 실제 기록 1개→0개 비교로 부분 처리를 막는 범위와 한계를 설명한다.
Bash의 while read에서 마지막 LF 개행이 없는 줄을 오류로 처리해도, 앞선 줄의 작업은 이미 실행됐을 수 있다. 모든 레코드에 LF가 필수이고 잘못된 파일에서는 작업을 하나도 시작하면 안 된다면, 파일 전체를 검증한 뒤 처리해야 한다. 루프가 끝난 뒤 오류만 반환해서는 앞선 작업을 취소할 수 없다.
read는 EOF에서 실패 상태를 반환하면서도 마지막 문자열을 변수에 남길 수 있다. 그래서 while IFS= read -r line은 그 값을 본문으로 넘기지 않는다. 마지막 개행이 선택 사항이면 잔여값을 받는 조건을 쓸 수 있지만, 여기서는 LF가 필수인 작은 파일을 작업 전에 거부하는 경우를 다룬다. EOF의 비영 상태와 while의 성공 조건은 Debian이 제공하는 GNU Bash 5.2 매뉴얼 (새 탭에서 열림)에서 확인할 수 있다.
같은 오류 종료라도 남은 작업은 다르다
Linux 또는 WSL의 Bash에서, 읽는 동안 바뀌지 않는 작은 일반 텍스트 파일을 대상으로 한다. 직접 비교하려면 쓰기 가능한 빈 폴더에서 마지막 LF만 다른 입력 두 개를 만든다.
printf 'alpha\nomega\n' > with-lf.txt
printf 'alpha\nomega' > without-lf.txt
이 입력으로 합성 실험을 했다. read가 성공할 때마다 작업하고 루프 뒤에서 남은 값을 거부하면, without-lf.txt는 종료 상태 2가 됐지만 alpha 작업은 이미 한 번 실행됐다. 실제 작업 위치를 실습용 기록 파일 추가로 바꾼 비교에서도 alpha 기록 한 개가 남았다. 먼저 배열에 모으고 검증이 끝난 뒤 작업하는 방식은 같은 종료 2에 기록 0개였다. 두 방식 모두 with-lf.txt에서는 두 값을 처리하고 종료 0이었다.
read 직후의 상태와 값을 따로 기록하니 LF 없는 입력은 0 / alpha, 1 / omega였다. LF가 있으면 0 / alpha, 0 / omega, 1 / 빈 값이었다. 이 정확한 상태와 값은 직접 관찰한 결과다. -r를 붙여도 EOF 반환 상태가 성공으로 바뀌지는 않는다.
읽는 루프에서는 모으기만 한다
다음 코드는 성공한 read의 값만 배열에 넣고, 실패 후 남은 값이 있으면 처리 루프에 도달하기 전에 종료한다. file을 자기 파일 경로로 바꾸면 적용할 수 있다. 괄호는 exit가 현재 터미널을 닫지 않도록 별도 셸에서 실행한다.
(
file='./without-lf.txt'
records=()
while IFS= read -r line; do
records+=("$line")
done < "$file" || exit 1
if [[ -n $line ]]; then
printf 'REJECT value=%q\n' "$line" >&2
exit 2
fi
for line in "${records[@]}"; do
printf 'APPLY value=%q\n' "$line" || exit 1
done
)
without-lf.txt에서는 표준 출력이 비어 있고 표준 오류에 REJECT value=omega만 나온다. 블록 직후 printf 'exit=%d\n' "$?"로 확인한 상태는 2였다. file='./with-lf.txt'로 바꾸면 APPLY value=alpha, APPLY value=omega 두 줄을 출력하고 상태는 0이다. 출력이나 상태가 다르면 입력 파일과 실행한 Bash를 먼저 확인한다.
실제 작업은 for 안의 출력 위치에 넣는다. 첫 루프에는 작업을 넣지 않는다. 검증한 배열을 그대로 처리하므로 검증 뒤 파일을 다시 열 필요도 없다. records+=("$line")과 "${records[@]}"의 인용을 유지해야 공백을 포함한 원소가 하나의 값으로 전달된다. Bash 배열의 추가와 원소별 확장 (새 탭에서 열림)
공백만 있는 꼬리도 데이터이므로 IFS=를 유지한다. -r는 역슬래시가 이스케이프로 해석되는 것을 막는다. %q는 경계를 드러내는 Bash 진단 형식이며, 실제 작업에는 계속 "$line"을 전달한다. 진단 출력을 다시 실행하지 않는다.
경계 입력도 같은 제어 흐름으로 비교했다.
- 빈 파일은 종료 0·작업 0회였다. LF 하나인 파일은 종료 0·빈 값으로 작업 1회였다. 이 코드는 빈 파일과 빈 줄을 허용한다.
- LF 없는 공백·탭·역슬래시·혼합 꼬리는 모두 종료 2·작업 0회였다. 루프 안에서 먼저 작업하던 방식에는
alpha기록 한 개가 남았다. - 공백·역슬래시·혼합 값에도 LF가 있으면 원래 값으로 처리했다. 양끝 공백 두 개가 있는
tail\path를 배열에서 꺼내printf '%s' "$line" | od -An -tx1로 확인한 바이트는20 20 74 61 69 6c 5c 70 61 74 68 20 20이었다.20은 공백,5c는 역슬래시다.
검증 뒤 작업의 실패까지 되돌리지는 않는다
이 방법은 마지막 LF 누락을 뒤늦게 발견해 정상 접두부만 처리하는 일을 막는다. 빈 줄 금지나 필드 형식 같은 규칙이 더 있다면 첫 루프에서 함께 검사해야 한다. 전체 검증을 통과한 뒤 작업 자체가 실패하는 경우는 별개다. 두 번째 작업을 실패시킨 합성 실험에서는 종료 1과 함께 첫 alpha 기록이 남았다. 선검증은 트랜잭션이 아니다.
또한 read의 실패 원인은 EOF만이 아니다. 위 코드는 빈 값과 함께 끝난 실패를 정상 EOF로 다루며 모든 I/O 오류를 구별하지 않는다. 파일을 열지 못하면 리다이렉션과 || exit 1에서 중단하지만, 읽는 도중의 I/O 오류 감지·복구는 검증하지 않았다. 이 보장까지 필요하면 읽기 오류를 별도로 보고하는 입력 처리기가 필요하다.
직접 실행한 환경은 WSL2 Ubuntu 24.04.4, GNU Bash 5.2.21, LC_ALL=C였다. LF 없는 65,536바이트 값은 작업 0회로 거부했고, 65,535바이트 값 뒤에 LF를 붙인 파일은 원래 값을 한 번 처리했다. 이는 허용 크기 권장값이 아니다. 배열에는 파일 전체가, read 변수에는 한 줄이 모이므로 파일 총량·레코드 길이·개수에 별도 제한이 필요하다. 본문 코드는 이 제한을 구현하지 않는다. 큰 파일, 파이프, 동시 수정, CRLF 변환, NUL을 포함한 바이너리 보존과 다른 Bash 버전은 검증하지 않았다.