Guides · Tooling & Windows gotchas · Published 2026-09-12 · 2 min read

CRLF Line Endings on Windows — Why grep Says Your File Has None, Why Multi-Line Replacements Silently Fail, and a Node Pattern That Handles Both

Git Bash tools on Windows read files in text mode and hide carriage returns, so grep reports zero CRLFs in a CRLF file and string replacements that span lines never match. How to detect line endings reliably with Node, and a read-normalise-replace-restore pattern that keeps files consistent.

Half a day of our lives went to this one. A multi-line search-and-replace in a Node script refused to match text that was visibly identical in the editor. The cause was carriage returns that every tool we used to inspect the file was quietly hiding.

The symptom

On a Windows project where files are saved with CRLF endings:

grep -c $'\r' app.js   # prints 0
cat -A app.js | head   # shows $ at line ends, no ^M

Both say "no carriage returns." Yet a Node replacement like this never matches:

s = s.replace("const a = 1;\nconst b = 2;", "…");

Why

Git Bash's grep, cat, sed and friends on Windows open files in text mode and translate \r\n to \n before you see anything. The file on disk still has \r\n, so your \n-only pattern is one byte short on every line boundary.

Detect line endings with Node, not the shell

const fs = require('fs');
const s = fs.readFileSync(file, 'utf8');
console.log('crlf', (s.match(/\r\n/g) || []).length, 'lf-only', (s.match(/[^\r]\n/g) || []).length);

Trust this over anything a Git Bash tool prints.

The pattern that works on both kinds of file

Read, normalise to \n in memory, do your replacement, then restore the original convention when writing:

const raw = fs.readFileSync(file, 'utf8');
const crlf = /\r\n/.test(raw);
let s = raw.replace(/\r\n/g, '\n');
s = s.replace('old\ntext', 'new\ntext');
if (crlf) s = s.replace(/\n/g, '\r\n');
fs.writeFileSync(file, s);

Skipping the last step is how projects end up with files that contain both endings, which then confuses diffs and some tools.

Keep the repo consistent

Decide on one convention per project and enforce it with .gitattributes, for example * text=auto eol=crlf for a Windows-first project or eol=lf for a Unix-first one. Editors then stop fighting Git, and scripts can assume one ending after the normalisation step above.

Heredocs and escaping in Bash on Windows

A related trap: writing a Node script through a Bash heredoc can mangle backslashes, so a regex like /\\/g arrives as /\/g and throws a syntax error. For anything with escapes, write the script to a file with an editor or a file-writing tool rather than through the shell.

Common mistakes

Summary

On Windows, check line endings with Node, normalise in memory, replace, and restore the original ending on write. Pin the convention in .gitattributes so the question stops coming up.

Related guides