CRLF vs LF The Line Ending Bug That Breaks CI

Опубликовано

Ваш скрипт работает локально, но в CI возникает ошибка "плохой интерпретатор: /bin/bash^M". Это ошибка завершения строк CRLF. Изучите, что вызывает эту ошибку, как её обнаружить и как навсегда исправить с помощью .gitattributes.

CRLF vs LF: The Line Ending Bug That Breaks CI 1
Реклама · УДАЛИТЬ?

Your shell script runs perfectly on your laptop. You push it to GitHub. CI fails with a cryptic /bin/bash^M: bad interpreter error. You’ve just been bitten by the most invisible bug in software development: the wrong line ending.

This guide explains what CRLF and LF actually are, why mixing them silently corrupts scripts and configs, and the exact steps to prevent line ending bugs from ever reaching your pipeline again.

What Are CRLF and LF?

Every text file needs a way to mark the end of a line. Two conventions exist, inherited from the era of physical teletype machines:

  • LF (Line Feed) — a single \n character (byte 0x0A). Used by Linux, macOS, and every Unix-derived system.
  • CRLF (Carriage Return + Line Feed) — two characters, \r\n (bytes 0x0D 0x0A). Used by Windows and MS-DOS.

The names come from typewriter mechanics. A carriage return moved the print head back to the start of the line. A line feed advanced the paper one row. Windows kept both; Unix dropped the redundant carriage return.

Why CRLF Breaks CI Pipelines

On Linux (where virtually all CI runners execute), the \r is not whitespace — it’s a literal character. When a shell script is saved with CRLF endings, each line ends with \r before the newline. The kernel interprets the shebang line as #!/bin/bash\r and looks for a binary literally named bash\r. That binary does not exist.

The resulting error looks like this:

: bad interpreter: /bin/bash^M: No such file or directory

The ^M is how terminals display the carriage return character. It’s invisible in most text editors, which is what makes this bug so disorienting.

Other Places CRLF Silently Causes Damage

  • Dockerfiles — A RUN instruction with CRLF will inject \r into every command, breaking string comparisons and file paths.
  • Python scriptsSyntaxError: unexpected character after line continuation character when \ is followed by \r\n.
  • .env files — Environment variable values pick up a trailing \r, so APP_ENV=production\r never matches the expected production.
  • CSV and data files — Parsers that read line by line may include \r in the last field of every row.
  • SSH authorized_keys — A CRLF-encoded key file will be silently rejected by the SSH daemon.
  • Git diffs — Every line appears changed, burying real changes in noise.

How to Detect Line Ending Problems

Most editors hide line endings by default. Here are reliable ways to check:

Using cat or hexdump

# Show ^M characters
cat -A yourfile.sh | head -5

# Hex dump to see 0x0d (CR) characters
hexdump -C yourfile.sh | head -10

Using file command

file yourfile.sh
# CRLF output: yourfile.sh: ASCII text, with CRLF line terminators
# LF output:   yourfile.sh: ASCII text

Using grep

# Returns exit code 0 (found) if CRLF endings exist
grep -rlP "\r" . --include="*.sh" --include="*.py" --include="*.yml"

How to Fix CRLF Line Endings

Option 1: dos2unix (fastest one-off fix)

dos2unix strips carriage returns from files. It’s available on all major Linux distributions:

# Fix a single file
dos2unix yourscript.sh

# Fix all shell scripts recursively
find . -name "*.sh" -exec dos2unix {} \;

# Reverse: convert LF to CRLF (unix2dos)
unix2dos yourfile.sh

Option 2: sed (no extra tools needed)

# Remove carriage returns in-place
sed -i 's/\r//' yourscript.sh

# Or using tr
tr -d '\r' < input.sh > output.sh

Option 3: Fix in VS Code or JetBrains

In VS Code, the line ending mode is shown in the status bar (bottom right). Click it to switch between CRLF and LF for the current file. To change the default for new files, set "files.eol": "\n" in your settings.json.

In JetBrains IDEs, go to File → Line Separators to change the current file, or set the default in Editor → Code Style → Line separator.

The Right Fix: .gitattributes

One-off fixes don’t scale. The correct solution is a .gitattributes file that tells Git exactly which line endings to enforce, regardless of what editor or OS contributors use.

# .gitattributes — commit this to the root of your repository

# Default: normalize all text files to LF in the repo
* text=auto eol=lf

# Explicitly enforce LF for scripts and configs
*.sh    text eol=lf
*.bash  text eol=lf
*.py    text eol=lf
*.rb    text eol=lf
*.yml   text eol=lf
*.yaml  text eol=lf
*.json  text eol=lf
*.env   text eol=lf
Dockerfile text eol=lf
Makefile   text eol=lf

# Windows-only files can keep CRLF
*.bat  text eol=crlf
*.cmd  text eol=crlf
*.ps1  text eol=crlf

# Binary files — never touch line endings
*.png  binary
*.jpg  binary
*.gif  binary
*.zip  binary
*.pdf  binary

After adding this file, run the following to re-normalize your entire repository in one shot:

git add --renormalize .
git commit -m "chore: normalize line endings via .gitattributes"

Git’s autocrlf Setting — and Why It Often Makes Things Worse

Git has a core.autocrlf setting that attempts to convert line endings automatically:

  • core.autocrlf=true — converts LF to CRLF on checkout (Windows), CRLF to LF on commit. Intended for Windows users.
  • core.autocrlf=input — converts CRLF to LF on commit, does nothing on checkout. Safer for Mac/Linux.
  • core.autocrlf=false — Git does nothing. Whatever the editor saves is what gets committed.

The problem: core.autocrlf is a local setting stored in ~/.gitconfig. Every developer on your team has a different value, so commits from different machines produce different line endings. This creates constant noise in diffs and intermittent CI failures depending on who last touched a file.

The rule of thumb: use .gitattributes to set line ending policy in the repository. Let core.autocrlf be whatever each developer has — .gitattributes overrides it.

Adding a CI Guard

Even with .gitattributes in place, it’s worth adding an explicit check in CI to catch any files that slip through. A two-line step covers most cases:

# In your CI workflow (GitHub Actions example)
- name: Check for CRLF line endings
  run: |
    if grep -rlP "\r" . --include="*.sh" --include="*.py" --include="*.yml" --include="Dockerfile"; then
      echo "ERROR: CRLF line endings found. Run dos2unix on the above files."
      exit 1
    fi

This step fails loudly at the source — the PR — rather than silently at deployment.

Quick Reference: CRLF vs LF

LF (\n)CRLF (\r\n)
Bytes0x0A0x0D 0x0A
Used byLinux, macOS, UnixWindows, MS-DOS
Safe for shell scriptsДаNo — breaks shebang
Safe for DockerfileДаНет
Safe for .env filesДаNo — adds trailing \r to values
Git recommendationNormalize to LF in repoOnly for .bat/.cmd/.ps1
Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?