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

빌드노트

스크립트가 있는데 bad interpreter가 나오면 shebang의 CR을 본다

실행 권한이 있는 Linux 스크립트도 shebang 끝의 CR 때문에 인터프리터를 찾지 못할 수 있다. LF·CRLF의 execve errno와 첫 줄 바이트를 비교하고, 없는 경로·권한 오류를 구별해 해당 파일만 LF로 고치는 방법을 설명한다.

Linux에서 스크립트가 분명히 있고 실행 권한도 있는데 bad interpreter나 required file not found가 나오면, 첫 줄의 줄바꿈을 확인한다. 첫 줄이 #!/bin/sh이고 CRLF로 끝나면 커널이 찾는 인터프리터 경로에 CR 문자가 붙을 수 있다. 이 경우 필요한 수정은 문제 파일의 줄바꿈을 LF로 바꾸는 것이다. chmod +x를 다시 실행해도 경로에 붙은 CR은 사라지지 않는다.

파일이 없다는 메시지가 어느 경로를 뜻하는가

스크립트를 ./crlf.sh처럼 직접 실행하면 Linux는 첫 줄의 #! 뒤에서 실행할 인터프리터를 읽는다. 이 첫 줄을 shebang이라고 부른다. 스크립트 자체를 찾았더라도 그곳에 적힌 인터프리터를 찾지 못하면 실행에 실패한다. execve(2) 문서 (새 탭에서 열림)는 스크립트 경로나 인터프리터가 없을 때 모두 ENOENT가 날 수 있다고 설명한다. 파일·인터프리터의 실행 권한 거부나 noexec 마운트는 EACCES에 해당한다.

CRLF는 줄 끝을 CR과 LF 두 바이트로 표시한다. #!/bin/sh 바로 뒤에 CRLF가 있으면 인터프리터 이름이 /bin/sh가 아니라 /bin/sh\r로 해석된다. 여기서 \r는 눈에 보이지 않는 CR 한 바이트를 표기한 것이다. Linux v6.18의 shebang 처리 코드 (새 탭에서 열림)는 LF를 줄 끝으로 찾고 공백과 탭을 잘라내지만, CR은 그 대상에 넣지 않는다.

권한은 같아도 줄바꿈에 따라 결과가 달랐다

실제 장애 기록이 아니라 새 Linux 컨테이너에 합성 파일을 만들어 비교했다. Debian 12, Linux 6.18.40.1-microsoft-standard-WSL2, Bash 5.2.15 환경이었고 일반 사용자 UID 1000으로 실행했다. /bin/sh의 실제 대상은 dash였다.

두 파일의 내용은 아래 두 줄로 같게 만들고, 줄바꿈만 LF와 CRLF로 나눴다. 둘 다 권한은 0755였다.

#!/bin/sh
printf 'OK\n'

자식 프로세스에서 execve를 직접 호출해 실패의 errno를 확인하고, Bash에서 직접 실행했을 때의 종료 상태도 별도로 기록했다.

  • LF 파일: OK를 출력하고 종료 0.
  • CRLF 파일: ENOENT(2), Bash 종료 127.
  • 같은 LF 파일에서 실행 비트만 뺀 0644 파일: EACCES(13), Bash 종료 126.
  • 첫 줄을 #!/definitely-missing-crlf-lab/sh로 쓴 LF 파일: 파일은 0755로 존재하지만 ENOENT(2), Bash 종료 127.
  • 스크립트 경로 자체가 없는 경우: ENOENT(2), Bash 종료 127.

CRLF 파일을 Bash에서 실행했을 때의 메시지는 다음과 같았다. 이 환경에서는 bad interpreter라는 문구 대신 아래 문구가 나왔다.

bash: line 1: ./crlf.sh: cannot execute: required file not found

오류 메시지나 종료 127만으로 CRLF라고 단정할 수는 없다. 없는 인터프리터를 지정한 대조군도 같은 결과였다. 스크립트의 존재·권한과 첫 줄의 실제 바이트를 함께 봐야 한다.

자기 파일에서는 첫 줄을 바이트로 본다

아래는 첫 줄이 #!/bin/sh인 파일을 확인하는 명령이다. Linux 셸에서 GNU sed와 od가 필요하며, 여기서는 GNU sed 4.9와 GNU coreutils 9.1로 실행했다. ./crlf.sh는 자신의 문제 파일 경로로 바꾸고, 첫 줄의 인터프리터가 다르면 /bin/sh도 그 경로로 바꾼다.

ls -l -- ./crlf.sh /bin/sh
sed -n '1p' ./crlf.sh | od -An -tx1

실험의 ls 출력에서는 스크립트가 -rwxr-xr-x였고 /bin/sh는 dash를 가리키는 링크였다. 자기 환경에서도 링크라면 실제 대상의 존재와 실행 권한까지 확인한다. 첫 줄의 hex 출력은 다음처럼 달랐다.

LF:    23 21 2f 62 69 6e 2f 73 68 0a
CRLF:  23 21 2f 62 69 6e 2f 73 68 0d 0a

0d가 CR이고 0a가 LF다. 인터프리터 이름 바로 뒤의 0d 0a가 이 사례를 구별하는 근거다. 편집기 화면에서 #!/bin/sh로 보이는 것만으로는 이 차이를 알 수 없다.

ls부터 파일을 찾지 못하면 실행할 때 쓴 경로와 현재 위치를 먼저 맞춘다. 첫 줄이 LF인데 인터프리터 경로가 없다면 해당 인터프리터를 준비하거나, 스크립트가 실제로 요구하는 인터프리터 경로로 고친다. Bash 문법을 쓰는 파일을 단순히 /bin/sh로 바꾸면 본문 실행에서 다른 오류가 날 수 있다. EACCES라면 파일의 실행 비트뿐 아니라 상위 디렉터리 접근 권한과 마운트 조건도 확인한다.

확인한 파일 하나만 LF로 고친다

아래 명령은 수정 권한이 있는 일반 텍스트 스크립트 하나를 대상으로 한다. 심볼릭 링크라면 먼저 실제 수정할 파일을 정한다. 백업 이름 crlf.sh.bak가 아직 사용되지 않았는지 확인한 뒤 실행한다. GNU sed의 \r 표기를 쓰므로 다른 sed 구현에서는 동일하게 동작한다고 가정하지 않는다.

cp -p -- ./crlf.sh ./crlf.sh.bak &&
sed -i 's/\r$//' ./crlf.sh

줄 끝에 있는 CR만 제거한다. 편집기에서 해당 파일의 줄바꿈을 LF로 선택해 저장하는 방법도 있다. 수정 후에는 첫 줄과 직접 실행 결과를 다시 본다.

sed -n '1p' ./crlf.sh | od -An -tx1
./crlf.sh

실험에서는 첫 줄이 23 21 2f 62 69 6e 2f 73 68 0a로 바뀌었고, 파일 권한은 0755로 유지됐다. 직접 실행은 OK를 출력하고 종료 0으로 바뀌었다. 그 전에 chmod +x만 다시 적용했을 때는 여전히 ENOENT(2)였다.

sh ./crlf.sh로 실행되는지만 보고 수정 완료로 판단하지는 않는다. 이 호출은 shebang으로 인터프리터를 선택하는 단계를 건너뛴다. 같은 CRLF 예제를 /bin/sh에 직접 넘겼을 때는 종료 0이었지만, 출력 바이트는 OK\n\r로 CR이 남아 있었다.

다음 체크아웃에서 다시 생긴다면

Git에서 파일을 받을 때마다 CRLF로 돌아온다면 해당 경로의 줄바꿈 규칙을 정한다. 예를 들어 저장소 루트의 job.sh를 Linux에서 직접 실행해야 한다면, 그 저장소의 .gitattributes에 다음 한 줄을 추가할 수 있다.

job.sh text eol=lf

Git의 gitattributes 문서 (새 탭에서 열림)는 text의 인덱스 정규화와 eol의 작업 파일 줄바꿈을 구분한다. 새 합성 저장소에서 Git 2.39.5에 명령별 core.autocrlf=true를 적용해 비교했다. 인덱스에 LF로 저장한 파일도 속성 없이 다시 꺼내면 CRLF가 됐고, 위 속성을 적용한 뒤 다시 꺼내면 LF가 됐다.

다만 속성을 추가하고 git add를 실행한 직후에는 이미 있던 작업 파일의 CRLF가 그대로였다. 속성 설정은 현재 파일을 곧바로 고치는 명령이 아니다. 현재 문제 파일은 먼저 LF로 수정하고, 속성은 이후 Git으로 파일을 꺼내는 경로의 재발 방지책으로 둔다. 전체 저장소를 일괄 변환하거나 전역 core.autocrlf를 바꿀 필요는 없다.

여기서 확인한 것은 인자가 없는 #!/bin/sh의 LF·CRLF 차이다. #!/usr/bin/env ..., BOM이 붙은 파일, 다른 운영체제의 오류 문구는 비교하지 않았다. 첫 줄에서 CR이 발견되지 않으면 이 수정안을 반복하기보다 실제로 지정된 인터프리터와 실행 경로를 확인한다.

← 전체 글 보기