빌드노트
세 줄처럼 보이는 파일인데 wc -l은 2다
wc -l은 LF 개수를 센다. 마지막 LF만 다른 파일과 빈 파일·빈 줄을 실제 바이트로 비교하고, 미종결 텍스트 레코드를 포함할 때의 awk NR과 CSV·JSON 파서의 적용 경계를 구분한다.
편집기에서 세 줄로 보이는 파일을 wc -l로 세었더니 2가 나올 수 있다. wc -l은 화면의 행이나 업무 레코드가 아니라 개행 문자 LF의 개수를 센다. 마지막 내용 뒤에 LF가 없으면 그 부분은 계수에 들어가지 않는다. 마지막 미종결 부분도 한 레코드로 읽을지, 입력 형식 오류로 볼지는 데이터 형식에 따라 정해야 한다.
파일 끝에 무엇이 있는지 먼저 본다
Linux 또는 WSL의 Bash에서 wc, od, GNU Awk를 사용한다. awk --version으로 GNU Awk인지 확인한다. 다음 명령은 파일을 바꾸지 않고 LF 개수, 실제 바이트, 기본 구분자로 읽은 레코드 수를 차례로 보여 준다. file에는 확인할 파일의 경로를 넣는다. 이 글에서는 ./data.txt가 그 대상이다.
file='./data.txt'
wc -l < "$file"
od -An -t x1 -- "$file"
awk 'END { print NR }' "$file"
A, B, C 사이에만 LF를 넣은 합성 파일에서 실제 출력은 다음과 같았다. 편집기 화면을 측정한 결과가 아니라, 정해진 바이트를 파일로 써서 비교한 결과다.
2
41 0a 42 0a 43
3
41, 42, 43은 각각 A, B, C이고 0a가 LF다. LF는 두 개이며 마지막 바이트는 43이다. 따라서 wc -l의 2는 이 파일에 맞는 결과다. 개행 수를 출력한다는 설명은 GNU wc 매뉴얼을 제공하는 Debian 공식 문서 (새 탭에서 열림)에서도 확인할 수 있다.
반면 여기서 실행한 GNU Awk는 마지막 C도 한 레코드로 읽어 3을 출력했다. 기본 레코드 구분자 RS는 개행이고, NR은 지금까지 읽은 입력 레코드의 누적 개수다. END에서 그 값을 출력한다. 이 명령에는 빈 줄을 제외하는 조건이 없으므로 빈 레코드도 센다. Ubuntu의 GNU Awk 문서: Records와 NR (새 탭에서 열림)
자기 파일에서도 두 값이 하나 차이 난다면 끝 바이트를 함께 본다. 파일이 비어 있지 않고 마지막 바이트가 0a가 아니라면, 이 비교에서는 마지막 미종결 레코드를 포함했는지가 차이의 이유다. 경로·권한 오류가 출력되면 숫자를 해석하기 전에 파일을 읽을 수 있는지부터 확인한다. 두 명령 사이에 계속 바뀌는 파일도 이 비교의 대상에서 제외한다.
마지막 LF만 다른 파일과 빈 파일을 비교한다
차이를 직접 확인하려면 쓰기 가능한 빈 폴더에서 아래 다섯 파일을 만든다. 같은 이름의 기존 파일이 없는 곳에서 실행한다. 마지막 LF의 유무와 빈 파일·빈 줄의 차이를 확인하는 입력이다.
printf 'A\nB\nC' > without-lf.txt
printf 'A\nB\nC\n' > with-lf.txt
: > empty.txt
printf '\n' > blank.txt
printf '\n\n' > two-blanks.txt
앞의 조회 명령에서 file을 각 파일 경로로 바꾸면 된다. 직접 실행한 값은 다음과 같았다. 아래 바이트 표기는 od의 16진수 출력이다.
without-lf.txt:41 0a 42 0a 43→wc -l은 2,NR은 3.with-lf.txt:41 0a 42 0a 43 0a→ 둘 다 3.empty.txt: 0바이트여서od출력도 없고, 두 계수 모두 0.blank.txt:0a→ 내용이 비어 있는 레코드 하나로, 둘 다 1.two-blanks.txt:0a 0a→ 빈 레코드 두 개로, 둘 다 2.
끝 LF 하나는 앞의 C를 종결한다. 그 뒤에 빈 레코드가 하나 더 생기는 것은 아니다. 반대로 LF만 있는 파일은 0바이트 파일과 다르다. “내용이 없어 보이니 0줄”이라고 판단하거나, 모든 wc -l 결과에 1을 더하면 이 입력들에서 틀린다. LF 없이 A 한 바이트만 쓴 추가 비교에서도 wc -l은 0, NR은 1이었다.
LF로 구분된 목록에서 마지막 미종결 부분과 빈 레코드까지 세려면 앞의 awk 'END { print NR }'을 쓸 수 있다. 이는 값이 있는 업무 데이터만 세는 명령은 아니다. 파일 이름을 여러 개 넘기면 NR은 모든 파일의 합계가 되므로, 위처럼 파일 하나씩 조회해야 비교가 쉽다. 모든 레코드가 LF로 끝나야 하는 입력 규약이라면 마지막 LF 누락을 따로 검사한다. 숫자를 맞추려고 원본에 개행을 자동으로 덧붙이지 않는다.
CRLF와 여러 줄 레코드에서는 경계를 다시 확인한다
Linux에서 읽은 CRLF는 바이트로 0d 0a다. CR과 LF가 둘이라고 해서 wc -l이 두 번 세지는 않는다. A\r\nB\r\nC\r\n을 쓴 비교에서는 LF 세 개를 세어 3, 기본 NR도 3이었다. 마지막 CRLF를 뺀 A\r\nB\r\nC는 각각 2와 3이었다. CRLF 파일에서도 마지막 LF 유무라는 판단은 같았다.
다만 기본 GNU Awk가 CR까지 제거한 것은 아니다. 완결된 CRLF 입력에서 각 레코드의 길이는 2로 관찰됐다. A 같은 문자 하나와 CR 하나가 남아 있었기 때문이다. CR만 쓴 A\rB\rC\r은 wc -l이 0, 기본 NR이 1이었다. 이 결과를 모든 편집기의 줄바꿈 인식 규칙으로 일반화할 수는 없다.
업무 레코드가 여러 물리적 줄에 걸칠 수 있다면 두 명령 모두 레코드 계수기로 충분하지 않다. RFC 4180의 CSV 설명 (새 탭에서 열림)은 따옴표로 감싼 필드 안에 줄바꿈이 들어갈 수 있음을 다룬다. 별도로 만든 헤더 없는 입력은 1,"red\r\nblue"\r\n2,"green"\r\n이었다. 여기서 \r\n은 실제 CRLF 바이트를 표시한 것이다. 레코드는 두 개지만 첫 레코드의 필드에 CRLF가 하나 더 들어 있다. wc -l과 기본 NR은 모두 3이었고, Python CSV 파서는 레코드 2개를 읽었다. 이 경우에는 CSV 파서로 읽고 헤더 포함 여부도 따로 정한다.
여러 줄로 정렬한 JSON 배열도 마찬가지다. 항목 두 개를 네 줄에 쓴 합성 입력은 wc -l과 기본 NR이 모두 4, JSON 파서로 읽은 배열 길이는 2였다. RFC 8259의 문법과 배열 정의 (새 탭에서 열림)에 따라 구조를 읽고, 배열 원소처럼 무엇을 셀지 먼저 정해야 한다. 일반 JSON을 한 줄 한 레코드 형식으로 가정하지 않는다.
이번 실행은 WSL2 Ubuntu 24.04.4, Bash 5.2.21, GNU coreutils 9.4, GNU Awk 5.2.1과 Python 3.12.3에서 LC_ALL=C로 고정한 작은 ASCII 파일을 비교했다. 참고한 Debian wc 문서는 9.1판, Ubuntu gawk 문서는 5.2.1판이다. 다른 awk 구현, UTF-16·NUL이 포함된 입력, 쓰는 중인 로그의 일관된 계수는 검증하지 않았다. LF 개수가 필요한지, 마지막 미종결 부분을 포함한 텍스트 레코드 수가 필요한지, 형식 파서가 읽은 항목 수가 필요한지를 정한 뒤 명령을 고르면 된다.