줄바꿈 문자 CRLF와 LF의 차이: 실제로 깨지는 네 가지
CRLF와 LF의 차이는 1바이트입니다. LF는 \n(0x0A) 한 바이트로 줄을 끝내고, Linux와 macOS가 이 방식을 씁니다. CRLF는 \r\n(0x0D 0x0A) 두 바이트를 쓰고, Windows가 여기에 해당합니다.
대부분의 글이 틀리는 대목은 그다음입니다. CRLF 줄바꿈 문자가 들어간 셸 스크립트는 보통 실패하지 않습니다. 잘 돌아가고, 예상한 대로 출력하고, 종료 코드 0으로 끝납니다. 정작 깨지는 것은 변수에 담긴 값입니다. 대입문이 뒤에 붙은 \r를 그대로 가져갔는데 아무도 문제 삼지 않았기 때문입니다.
실측한 동작은 네 가지 양상으로 갈립니다. 알아차리기 어려운 것부터 놓으면 이 순서입니다.
- 조용한 성공. 출력은 멀쩡하고, 변수에는 보이지 않는
\r가 들어 있고, 종료 코드는 0입니다. - 아무것도 멈추지 못하는 오류. 빈 줄에서
: command not found가 뜨지만 스크립트는 끝까지 실행되고 종료 코드는 0입니다. - 문법 오류.
if,for, 함수 정의가 깨지고 종료 코드는 2입니다. bad interpreter. 셔뱅 줄이\r를 물고 있고 종료 코드는 126입니다.
먼저 file yourfile부터 실행하세요. 한 줄짜리 출력만 보고도 지금 CRLF 쪽인지 LF 쪽인지 가려낼 수 있습니다.
아래 내용은 모두 macOS(Darwin arm64)에서 bash 3.2.57(1)-release, zsh 5.9, dash, git 2.55.0, node v26.7.0, Python 3.14.6으로 측정했고,
docker run --rm bash:5(GNU bash 5.3.15(1)-release, aarch64-unknown-linux-musl)로 Linux에서 교차 확인했습니다.
1. CRLF, LF, CR가 실제로 어떤 바이트인가
CRLF는 carriage return line feed의 약자이고, 말 그대로 두 글자를 붙여 놓은 것입니다.
| 이름 | 이스케이프 | 바이트 |
|---|---|---|
| 캐리지 리턴(carriage return) | \r | 0x0D |
| 라인 피드(line feed) | \n | 0x0A |
| CRLF | \r\n | 0x0D 0x0A |
플랫폼별로 실제로 쓰는 값은 이렇습니다.
| 플랫폼 | 줄바꿈 문자 |
|---|---|
| Windows | CRLF(\r\n) |
| Linux, 최신 macOS | LF(\n) |
| Mac Classic, OS 9 이하 | CR(\r) 단독 |
이름은 기계 장치에서 그대로 왔습니다. 텔레타이프에서 캐리지 리턴은 인쇄 헤드를 왼쪽 여백으로 되돌리는 동작이었고, 라인 피드는 종이를 한 줄 밀어 올리는 동작이었습니다. Windows는 두 동작을 두 바이트로 그대로 남겼고, Unix는 하나면 충분하다고 판단했습니다. 단독 CR는 오래된 내보내기 파일에서 여전히 나오는데, 그런 파일은 대부분의 Unix 도구 눈에 거대한 한 줄로 보입니다.
명령 하나면 어느 쪽인지 알 수 있습니다
file은 바이트를 읽고 종료 문자의 이름을 알려 줍니다.
$ file d1.txt
d1.txt: ASCII text, with CRLF line terminators
$ file d2.txt
d2.txt: ASCII text ← no suffix means pure LF
$ file d3.txt
d3.txt: ASCII text, with CRLF, LF line terminators ← mixed, file lists both
두 번째 출력에 접미사가 없다는 것은 순수 LF라는 뜻이고, 세 번째는 두 종류가 다 잡혔다는 뜻입니다. 눈여겨볼 것은 세 번째입니다. 줄바꿈 문자가 섞인 파일은 설정이 다른 두 도구가 번갈아 쓴 결과입니다. 편집기가 LF로 저장해 둔 파일에 스크립트가 CRLF로 덧붙였거나, 병합이 두 버전을 이어 붙인 것입니다. od -c는 그 경계를 바이트 단위로 보여 줍니다.
$ od -c d3.txt
0000000 a \r \n b \n
0000005
줄바꿈 문자는 바이트 층에 사는 문제이고, 그 바로 옆이 문자 인코딩입니다. 한 층 위에서 무슨 일이 벌어지는지는 UTF-8과 UTF-16 인코딩 가이드에서 다룹니다.
2. 네 가지 실패 양상: 조용한 성공부터 종료 코드 126까지
같은 \r\n 파일인데 줄 내용에 따라 결과가 다섯 갈래로 갈립니다.
| 스크립트 내용 | 실제로 벌어지는 일 | 종료 코드 |
|---|---|---|
echo hello 같은 단순 명령 | 조용한 성공, 출력도 정확함 | 0 |
X=abc 대입 | 조용한 성공, 다만 값 끝에 \r가 붙음 | 0 |
빈 줄, 줄 끝의 \ 이어 쓰기 | : command not found, 스크립트는 끝까지 실행됨 | 0 |
if/fi, for/do/done, f() { | syntax error near unexpected token | 2 |
\r가 붙은 셔뱅 줄 | bad interpreter: No such file or directory | 126 |
“CRLF 때문에 command not found가 난다”는 말은 어디서나 반복되지만, 표의 첫 두 행이 보여 주듯 실제로 벌어지는 일은 그게 아닙니다. 단순 명령은 아무 불평도 하지 않습니다.
조용한 성공이 가장 위험합니다
$ printf 'echo hello\r\necho world\r\n' > win.sh && bash win.sh
hello
world
[exit=0]
두 줄을 넣었더니 두 줄이 나왔고 종료 코드는 0입니다. 붙잡고 디버깅할 대상이 없고, 로그를 뒤져도 검색할 문자열이 없습니다. 같은 스크립트에 비교를 한 번 시키면 이야기가 달라집니다.
$ printf 'V=1.2.3\r\ntest "$V" = "1.2.3" && echo MATCH || echo NO-MATCH\r\n' > v.sh
$ bash v.sh
NO-MATCH
[exit=0]
$V에 담긴 값은 1.2.3이 아니라 1.2.3\r입니다. macOS와 Linux에서 결과가 같습니다. 버전 게이트, if [ "$ENV" = "prod" ], 파일에서 읽어 온 기능 플래그가 하나같이 조용히 잘못된 분기를 타면서 종료 코드는 0을 냅니다. 빌드는 초록불이고 로그는 깨끗하니, CI에서 잡아내기 가장 어려운 줄바꿈 문자 버그입니다.
아무것도 멈추지 못하는 오류
CRLF 파일에서 빈 줄은 \r 하나만 들어 있는 줄입니다. 셸은 이것을 실행해야 할 명령으로 봅니다. 실행에 실패해 메시지를 찍고, 스크립트는 다음 줄로 넘어가 종료 코드 0으로 끝납니다. 정확한 문구는 셸마다 다릅니다.
줄 끝 역슬래시로 이어 쓴 줄도 같은 식으로 깨집니다. \r가 역슬래시와 줄바꿈 사이에 끼어들기 때문에 이어 쓰기가 이어 쓰기 노릇을 못 하고, 다음 줄이 독립된 명령으로 실행됩니다.
문법 오류, 그리고 fi가 fi가 아닌 이유
c_if.sh: line 5: syntax error: unexpected end of file
[exit=2]
Linux의 bash 5.3.15는 같은 파일을 두고 조금 더 말해 줍니다.
i.sh: line 5: syntax error: unexpected end of file from `if' command on line 2
반복문과 함수 정의는 마지막 줄이 아니라 1번 줄에서 깨집니다.
c_for.sh: line 1: syntax error near unexpected token `do'
c_for.sh: line 1: `for i in 1 2; do'
c_func.sh: line 1: syntax error near unexpected token `{'
c_func.sh: line 1: `f() {'
원리를 알고 나면 이 부류는 전부 예측할 수 있습니다. bash는 블록을 닫는 낱말을 fi가 아니라 fi\r로 읽습니다. fi\r는 키워드 fi가 아니므로 if 블록은 영영 닫히지 않고, bash는 파일이 바닥날 때까지 계속 읽습니다. 오류가 깨진 줄이 아니라 마지막 줄을 가리키는 이유가 여기 있습니다.
bad interpreter와 종료 코드 126
$ ./s.sh
bash: ./s.sh: /bin/bash^M: bad interpreter: No such file or directory
[exit=126]
macOS와 Linux가 같은 문구를 찍고 둘 다 126으로 끝납니다. 메시지 안의 경로를 읽어 보세요. /bin/bash^M입니다. 커널은 #! 다음부터 줄바꿈 전까지를 인터프리터 경로로 받아들이는데, \r가 그 경로의 일부입니다. 그런 파일은 존재하지 않으므로 스크립트의 첫 줄이 실행되기도 전에 exec가 실패합니다.
종료 코드 126은 “찾았지만 실행할 수 없음”까지 함께 덮습니다. 그러니 시작을 거부하는 스크립트가 무조건 줄바꿈 문자 문제인 것은 아닙니다. 파일 권한도 같은 계열의 실패를 냅니다. 둘을 갈라 주는 단서는 경로 안의 ^M입니다.
\r가 눈에 띄지 않는 이유
출력을 읽는 것만으로는 소용이 없습니다. 이 바이트는 화면에 아무 흔적도 남기지 않습니다. 억지로 눈앞에 끌어내야 합니다.
$ bash d.sh # script: HOST=example.com / echo "connecting to $HOST:8080"
connecting to example.com:8080 ← what you get
$ bash d.sh | cat -v
connecting to example.com^M:8080^M ← what is actually there
$ bash d.sh | od -c
0000000 c o n n e c t i n g t o e x
0000020 a m p l e . c o m \r : 8 0 8 0 \r
0000040 \n
\r 두 개가 평범한 출력에서는 보이지 않은 채, 곧 호스트 이름으로 쓰일 문자열 안에 들어앉아 있습니다. 이 바이트가 인자를 타고 흘러가 엉뚱한 도구가 누명을 쓰는 일도 있습니다.
$ bash v.sh # the script pipes through: ... | head -2
head: illegal line count -- 2\r
head는 제대로 동작한 것입니다. 스크립트가 2\r를 건넸을 뿐입니다. 값에 \r가 끼어 있는 오류 메시지는 전부 이 버그가 남의 이름을 달고 나타난 것입니다.
3. 내 오류 메시지가 인터넷에서 본 것과 딴판인 이유
printf 'echo a\r\n\r\necho b\r\n'로 만든 파일, 그러니까 2번 줄이 \r 하나뿐인 빈 줄인 파일을 셸 네 개에서 실행했습니다.
| 셸 | 버전 | 정확한 메시지 |
|---|---|---|
| bash(macOS 기본 탑재) | 3.2.57(1)-release | s_blank.sh: line 2: : command not found |
| bash(주류 Linux) | 5.3.15(1)-release | t.sh: line 2: $'\r': command not found |
| zsh | 5.9 | s_blank.sh:2: command not found: ^M |
| dash | — | s_blank.sh: 2: : not found |
검색 결과는 거의 전부 두 번째 행을 인용합니다. bash 4와 5는 출력할 수 없는 문자를 ANSI-C 인용 방식으로 찍기 때문에 캐리지 리턴이 $'\r'로 나옵니다. macOS에 딸려 오는 bash는 3.2라 그렇게 하지 않으므로, 콜론과 공백 사이가 텅 빈 문구를 보게 됩니다. zsh는 ^M을 찍습니다. dash는 아예 command라는 낱말을 뺍니다.
원인은 하나인데 메시지는 넷입니다. 자기가 본 오류 문구를 그대로 검색창에 붙여 넣었는데 쓸 만한 결과가 나오지 않았다면 이유가 이것입니다. 밋밋한 : command not found도 그 유명한 오류와 같은 버그입니다.
4. Git: core.autocrlf가 저장소에 실제로 넣는 것
git autocrlf 설명은 대개 정의에서 멈춥니다. 막상 알아야 할 것은 커밋에 무엇이 들어가고 동료가 체크아웃하면 무엇을 받느냐입니다. 저장된 blob은 git cat-file -p HEAD:f.txt로, 작업 사본은 rm f.txt && git checkout -- f.txt로 측정했습니다.
core.autocrlf | 원본 파일 | 저장소의 blob | 체크아웃 후 작업 사본 |
|---|---|---|---|
true | CRLF | LF | CRLF |
true | LF | LF | CRLF |
input | CRLF | LF | LF |
input | LF | LF | LF |
false | CRLF | CRLF | CRLF |
false | LF | LF | LF |
표에서 읽어 낼 것은 이렇습니다.
true와input은 둘 다 저장소 안이 LF임을 보장합니다. 차이는 체크아웃뿐입니다.true는 CRLF로 되돌리고,input은 파일을 건드리지 않습니다.- 커밋에 CRLF를 집어넣는 값은
false하나뿐입니다. 캐리지 리턴을 누가 커밋했느냐는 물음의 답이 이 행입니다. - 두 번째 행이 뜻밖입니다.
true에서는 디스크에 LF로 있던 파일이 체크아웃 뒤 CRLF로 돌아옵니다.
Git은 다시 쓰기 전에 두 가지 형태 중 하나로 미리 알립니다.
warning: in the working copy of 'f.txt', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'f.txt', CRLF will be replaced by LF the next time Git touches it
”아무것도 안 건드렸는데 git은 파일 전체가 바뀌었다고 합니다”
또 두 번째 행입니다. git config core.autocrlf true이면 체크아웃이 작업 디렉터리의 LF 파일을 CRLF로 다시 씁니다. 이제 모든 줄이 blob과 1바이트씩 어긋나므로 git diff는 파일 전체를 수정으로 보고하고, 풀 리퀘스트에는 아무도 손대지 않은 파일이 통째로 다시 쓰인 것처럼 뜹니다. 범인은 체크아웃 필터입니다.
반대 방향도 같은 소음을 냅니다. false인 동료가 CRLF를 커밋하고 내가 input이면, 열어 본 적도 없는 파일이 내 diff에 올라옵니다.
5. .gitattributes가 팀 단위의 답입니다
core.autocrlf는 기계 한 대에만 걸리는 설정이라 남들에게는 보이지 않습니다. .gitattributes는 저장소 안에 들어 있는 파일이므로 클론할 때마다 따라갑니다. 둘이 어긋나면 속성 파일이 이깁니다. 네 가지 형태를 모두 시험했습니다.
.gitattributes | core.autocrlf | 원본 | blob | 체크아웃 후 | 승자 |
|---|---|---|---|---|---|
* text=auto | false | CRLF | LF | LF | 속성 |
* text eol=crlf | input | LF | LF | CRLF | 속성 |
* -text | true | CRLF | CRLF | CRLF | 속성 |
* text eol=lf | true | CRLF | LF | LF | 속성 |
세 번째 행은 기억해 둘 만합니다. -text는 변환을 통째로 끄고, 그러면서도 core.autocrlf=true를 이깁니다. 바이트가 손 타지 않고 그대로 살아남아야 하는 파일은 이렇게 지킵니다.
그대로 가져다 쓸 수 있는 .gitattributes
* text=auto
*.sh text eol=lf
*.bash text eol=lf
Makefile text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.png -text
*.jpg -text
*.pdf -text
*.zip -text
text=auto는 Git이 텍스트로 판정한 것을 저장소 안에서 LF로 정규화합니다. 명시적인 eol=lf 줄들은 누가 쓰든 반드시 LF여야 하는 파일을 담당합니다. CRLF가 들어간 .sh는 종료 코드 126을 예약해 둔 것이나 마찬가지이기 때문입니다. Windows 스크립트 확장자는 반대 이유로 eol=crlf를 받고, 바이너리 패턴은 -text를 받아 아무 변환도 겪지 않습니다.
core.safecrlf와 core.eol
core.autocrlf 옆에 함께 등장하지만 하는 일이 다른 설정 두 가지입니다.
- **
core.safecrlf**는 변환기가 아니라 감시자입니다. 변환이 왕복하지 못할 때, 즉 정규화하면 정보가 사라지는 혼합 파일 같은 경우에true는 작업을 거부하고warn은 경고만 남긴 채 통과시킵니다. 어떤 바이트가 저장될지는 절대 건드리지 않습니다. 손실이 생기는 변환을 조용히 해치우지 않을 뿐입니다. - **
core.eol**은core.autocrlf가false일 때text로 표시된 파일을 작업 디렉터리에 어떤 줄바꿈 문자로 쓸지 고릅니다. 값은lf,crlf,native입니다.core.autocrlf가 이 값을 덮어쓰기 때문에, autocrlf를 켜 둔 기계에서core.eol을 설정하면 아무 일도 일어나지 않은 것처럼 보이기 쉽습니다.
.gitattributes를 고쳐도 이미 커밋된 파일은 그대로입니다
속성은 Git이 파일을 쓰거나 읽는 시점에 적용되므로, 기존 내용은 무언가가 다시 쓰기 전까지 그대로 남습니다. 그 과정을 직접 강제하세요.
$ git add --renormalize .
$ git commit -m "Normalize line endings"
거대한 diff 하나가 나오는데, 그게 정상입니다. 전용 브랜치에서 실행해 커밋 하나로 병합하고, 들어가기 전에 반드시 팀 전체에 알리세요.
6. CRLF를 LF로 변환하기, 그리고 되돌리기
Darwin에서 \r가 실제로 지워지는지 하나씩 확인한 네 가지 방법입니다.
| 명령 | 결과 |
|---|---|
tr -d '\r' < f > f.out | 동작함 |
perl -pi -e 's/\r\n/\n/g' f | 동작함 |
sed -i '' -e 's/\r$//' f(BSD 형식) | 동작함 |
sed -i -e 's/\r$//' f(GNU 형식, macOS에서 실행) | 동작하지만 쓰레기 파일을 하나 남김 |
macOS의 GNU sed -i 함정
BSD sed는 -i 뒤에 백업 접미사가 따라오기를 요구합니다. Linux 튜토리얼을 그대로 베껴 오면 BSD sed는 -e를 그 접미사로 삼켜 버립니다. 편집은 그래도 이루어지고, 덤으로 이런 것이 남습니다.
a5.txt
a5.txt-e
a5.txt-e는 백업 사본입니다. sed가 -e를 사용자가 요청한 접미사로 읽었기 때문에 생겼습니다. macOS에서 올바른 형식은 빈 문자열을 명시적으로 넘기는 sed -i '' -e 's/\r$//' f입니다. -e 파일이 커밋되어 있는 저장소는 누군가 Mac에서 GNU 한 줄짜리를 실행한 저장소입니다.
macOS에는 dos2unix가 없습니다
기본 상태의 macOS에서 command -v dos2unix는 아무것도 내놓지 않습니다. 이 바이너리는 brew install dos2unix로 들어옵니다. 인터넷에서 가장 많이 복사되는 답이 정작 수많은 개발자가 앉아 있는 기계에서 실패하는 이유가 이것입니다. tr -d '\r'는 설치가 필요 없고 같은 일을 합니다.
반대 방향인 unix2dos도 없기는 마찬가지이고, sed -e 's/$/\r/'가 대체품으로 동작합니다.
파일이 깨끗한 LF로 돌아오면 그때부터 다시 줄의 목록으로 다뤄도 안전합니다. 줄을 통째로 문자열로 비교하는 작업이라면 특히 중요합니다. 텍스트 줄 정렬과 중복 줄 제거는 둘 다 value\r와 value를 서로 다른 줄로 읽으므로, 캐리지 리턴 하나가 조용히 중복 제거를 무력화합니다.
편집기에서
VS Code는 현재 파일의 줄바꿈 문자를 오른쪽 아래 상태 표시줄에 CRLF 또는 LF로 보여 주고, 그 표시를 클릭하면 파일이 전환됩니다. files.eol 설정은 새 파일의 기본값을 정하는데, 작업 공간별로 설정해 두면 구성이 제각각인 팀도 같은 값을 쓰게 됩니다. 다른 편집기에도 이름만 다를 뿐 같은 조작 두 가지가 있습니다. 사람들이 놓치는 부분은 파일별 표시와 기본 설정이 서로 별개라는 점입니다. 한쪽을 바꿔도 다른 쪽은 그대로입니다.
7. 코드에서 줄바꿈 문자 다루기
line1\r\nline2\r\n 파일 하나를 일곱 가지 경로로 읽었습니다.
| 진입점 | 읽히는 값 | \r |
|---|---|---|
Node fs.readFileSync(f,"utf8") | "line1\r\nline2\r\n" | 유지 |
Node, 같은 값에 .split("\n") | ["line1\r","line2\r",""] | 모든 줄에 유지 |
Node readline + crlfDelay:Infinity | ["line1","line2"] | 제거 |
Python open(f), 기본 모드 | 'line1\nline2\n' | 변환 |
Python open(f).readlines() | ['line1\n','line2\n'] | 변환 |
Python open(f, newline="") | 'line1\r\nline2\r\n' | 유지 |
Python open(f,"rb") | b'line1\r\nline2\r\n' | 유지 |
익숙한 버그 리포트 하나가 이 표에 그대로 들어 있습니다. Python에서는 되는데 Node에서는 깨진다는 그 이야기입니다. Python의 기본 텍스트 모드는 universal newlines를 적용해 \r\n을 \n으로 옮긴 뒤에야 값을 건넵니다. Node는 바이트를 있는 그대로 건넵니다. 어느 쪽도 틀리지 않았지만, 같은 파일을 읽는 순간 둘의 결과는 갈립니다.
rstrip("\n")은 \r를 남깁니다
original: 'line1\r\n' | rstrip("\n"): 'line1\r' | strip(): 'line1'
rstrip("\n")은 목록에 적어 준 문자만 정확히 제거하는데, \r는 그 목록에 없었습니다. 그래서 남은 값은 일치해야 할 상대와 끝내 같아지지 않습니다. “분명히 제거했는데 여전히 같지 않다”는 말의 정직한 답이 이것입니다. strip()이나 인자 없는 rstrip()을 쓰면 캐리지 리턴을 포함해 뒤쪽 공백이 전부 사라집니다.
두 플랫폼 모두에서 안전하게 줄 나누기
JavaScript에서는 양쪽 줄바꿈 문자를 모두 견디는 패턴 text.split(/\r?\n/)으로 나누세요. Python에서는 기본 텍스트 모드에 머물러 universal newlines에 맡기거나, \r\n과 \n, 단독 \r까지 처리하는 splitlines()를 호출하세요.
쓰기가 나머지 절반입니다. Node는 건네준 바이트를 그대로 쓰므로, 문자열은 \n으로 조립하고 디스크에 무엇이 떨어질지는 .gitattributes가 정하게 두세요. Python의 open(path, "w")는 newline=""을 넘기지 않는 한 \n을 플랫폼 줄바꿈 문자로 옮깁니다. CSV 작성기가 바로 이 이유로 요구하는 그 옵션입니다.
줄 끝의 캐리지 리턴과 파일 앞머리의 바이트 순서 표시(BOM)는 양 끝에서 나타나는 같은 부류의 버그입니다. 복사·붙여넣기에도 살아남아 동등성 검사를 깨뜨리는, 눈에 보이지 않는 바이트입니다. 반대쪽 끝은 UTF-8 BOM 문제 해결 가이드에서 다룹니다.
8. 줄바꿈 문자가 또 무는 곳
CSV와 Excel. RFC 4180은 레코드 구분자로 CRLF를 규정합니다. CSV는 CRLF가 결함이 아니라 정답인 몇 안 되는 자리입니다. LF만 가정하고 쓴 파서는 모든 행의 마지막 필드에 \r를 남기므로, CSV를 JSON으로 변환한 값이 눈으로는 멀쩡한데 비교만 하면 틀린다면 그 바이트부터 확인하세요.
Docker. CRLF가 든 .sh를 이미지에 복사해 넣으면 네 번째 실패 양상이 됩니다. COPY는 바이트를 보존하고, 셔뱅은 \r를 그대로 물고 있고, 컨테이너는 126으로 끝납니다. .gitattributes의 *.sh text eol=lf 한 줄이면 이 부류 전체를 막습니다.
diff와 풀 리퀘스트. 눈에 보이는 편집이 하나도 없는데 파일 전체가 바뀐 것으로 표시된다면, 4절에서 본 메커니즘이 코드 리뷰까지 따라온 것입니다. 두 버전을 텍스트 비교로 맞대 보면 몇 초 만에 확인되고, 텍스트 diff 가이드가 결과 읽는 법을 안내합니다.
혼합 파일. ASCII text, with CRLF, LF line terminators는 설정이 다른 두 작성자가 있었다는 뜻입니다. 마침 편집한 줄만 손보지 말고 파일 전체를 정규화하세요. 그러지 않으면 다음 diff도 똑같이 시끄럽습니다.
FAQ
CRLF와 LF의 차이는 무엇인가요?
CRLF는 \r\n(0x0D 0x0A) 두 바이트이고, LF는 \n(0x0A) 한 바이트입니다. Windows는 CRLF를, Linux와 macOS는 LF를 쓰며 둘 다 줄의 끝을 표시합니다. 편집기에서는 글자가 똑같아 보이고, 차이는 바이트와 문자열 비교, diff에서만 드러납니다.
스크립트에 CRLF 줄바꿈 문자가 있는데 왜 오류가 나지 않나요?
단순 명령은 바이트 하나가 더 붙어도 살아남기 때문입니다. 뒤에 \r가 붙은 echo hello는 그대로 실행되고 0으로 끝납니다. 오류는 파서가 신경 쓰는 자리에서만 나타납니다. 빈 줄, fi 같은 키워드, 그리고 셔뱅입니다. 위험한 쪽은 대입문입니다. 성공하면서 \r를 변수에 담아 두기 때문입니다.
$'\r': command not found는 무슨 뜻이고, 왜 저에게는 안 보이나요?
캐리지 리턴 하나만 들어 있는 줄을 셸이 실행하려 했다는 뜻입니다. bash 4와 5는 그 문자를 ANSI-C 인용으로 찍어 $'\r'가 됩니다. macOS에 딸려 오는 bash 3.2는 콜론 사이에 아무것도 찍지 않고, zsh는 대신 ^M을 찍습니다. 원인은 하나인데 메시지는 세 가지입니다.
core.autocrlf는 true, input, false 중 무엇으로 해야 하나요?
Linux와 macOS에서는 input, Windows에서는 true를 쓰되, 둘보다 .gitattributes를 앞세우세요. 실측 결과 true와 input은 둘 다 저장소에 LF를 저장했고, CRLF가 커밋에 들어가도록 허용하는 값은 false 하나뿐이었습니다. true는 여기에 더해 체크아웃 때 작업 사본의 LF 파일을 CRLF로 다시 씁니다.
.gitattributes와 core.autocrlf가 어긋나면 어느 쪽이 이기나요?
.gitattributes가 이깁니다. 시험한 네 가지 형태 * text=auto, * text eol=crlf, * -text, * text eol=lf가 모두 로컬 core.autocrlf 값을 눌렀습니다. 이것이 속성 파일을 쓰는 근거입니다. 속성은 커밋되어 모두에게 적용되지만, core.autocrlf는 눈에 보이지도 않고 강제할 수도 없는 기계별 설정입니다.
파일이 CRLF인지 LF인지 어떻게 확인하나요?
file yourfile을 실행하세요. CRLF면 ASCII text, with CRLF line terminators가 나오고, 순수 LF면 접미사 없이 ASCII text만 나오고, 혼합 파일이면 with CRLF, LF line terminators가 나옵니다. 바이트 수준까지 확실히 하려면 od -c를 실행해 \n 앞에 \r가 있는지 보세요.
CRLF는 실제로 언제 써야 하나요?
형식이나 프로토콜이 요구할 때입니다. RFC 4180은 CSV의 레코드 구분자로 CRLF를 정의하고, HTTP 헤더와 SMTP도 전송 구간에서 같은 규칙을 씁니다. Windows 배치 파일과 PowerShell 파일도 CRLF 쪽이 안전합니다. 그 밖의 모든 곳, 그러니까 소스 코드와 셸 스크립트와 설정 파일은 LF입니다.