Quick CRLF-to-LF Conversion in PowerShell Without `dos2unix`
Although this topic has been discussed so often, I think I can still contribute something regarding CRLF/LF.
You write a shell script on Windows, copy it to a Linux server, run it – and get:
-bash: ./deploy.sh: /bin/bash^M: bad interpreter: No such file or directory(Newer bash versions are even less helpful: cannot execute: required file not found.)
The culprit is Windows line endings: **CRLF** (\r\n) instead of **LF** (\n). Bash treats the trailing \r as part of the command. If dos2unix is not at hand, PowerShell handles the conversion in a single line that isn’t particularly easy to remember:
[IO.File]::WriteAllText($p, ([IO.File]::ReadAllText($p) -replace "`r`n", "`n"))## How it works
– [IO.File]::ReadAllText($p) reads the whole file into a single string – line endings included.
– -replace "`r`n", "`n" replaces every CRLF with LF. Inside double quotes, `r and `n are PowerShell escape sequences for CR and LF.
– [IO.File]::WriteAllText() writes the string back as it is.
Why not simply Get-Content / Set-Content? Without -Raw, Get-Content splits the file into lines and drops the line endings, and Set-Content joins them again with the Windows default \r\n – exactly what you wanted to get rid of.
## Three gotchas
**1. Use an absolute path.** .NET resolves relative paths against the process working directory, not against your current PowerShell location. After a Set-Location, .\deploy.sh may point somewhere else – or nowhere. Resolve it first:
$p = Convert-Path .\deploy.sh**2. Mind the encoding.** ReadAllText() assumes UTF-8 (unless a BOM says otherwise), WriteAllText() writes UTF-8 **without** BOM. For UTF-8 or plain ASCII scripts that is exactly what you want – a UTF-8 BOM is dropped on the way, which Linux tools appreciate anyway. **ANSI files (Windows-1252) with umlauts, however, get corrupted:** ü turns into �. Pass the encoding explicitly in that case:
$enc = [Text.Encoding]::GetEncoding(1252)
[IO.File]::WriteAllText($p, ([IO.File]::ReadAllText($p, $enc) -replace "`r`n", "`n"), $enc)**3. The whole file goes into memory.** Fine for scripts and config files, not for multi-GB logs.
## Converting a whole directory
Get-ChildItem -Filter *.sh -Recurse -File | ForEach-Object {
$t = [IO.File]::ReadAllText($_.FullName)
if ($t.Contains("`r`n")) {
[IO.File]::WriteAllText($_.FullName, $t.Replace("`r`n", "`n"))
"converted: $($_.FullName)"
}
}FullName is always absolute (gotcha #1 solved), and files that are already clean are not touched. .Replace() is the plain string method – a literal replacement without the regex engine, same result.
## Alternatives
**PowerShell cmdlets:**
(Get-Content -Raw $p) -replace "`r`n", "`n" | Set-Content -NoNewline $p-Raw keeps the file as one string, -NoNewline stops Set-Content from appending a trailing CRLF. Works fine in PowerShell 7 (default: UTF-8 without BOM). In Windows PowerShell 5.1 both cmdlets default to ANSI, and -Encoding UTF8 writes a BOM – check the result there.
**dos2unix / sed** – Git Bash (part of Git for Windows) ships dos2unix, and so do most Linux boxes and WSL distributions:
“`bash
dos2unix deploy.sh
sed -i 's/\r$//' deploy.sh## Better: don’t create CRLF in the first place
### VS Code
The status bar shows CRLF or LF in the bottom right corner – click it to switch the current file, then save. The default for new files is set in settings.json, either globally or just for shell scripts:
{
// all new files
"files.eol": "\n",
// or only shell scripts
"[shellscript]": {
"files.eol": "\n"
}
}**Note:** files.eol only applies to **new** files. Existing files keep their line endings.
### git
The personal setting:
git config --global core.autocrlf inputinput converts CRLF to LF on commit and leaves the files alone on checkout. The Git for Windows installer defaults to `true`, which checks files out with CRLF – a common way CRLF ends up in your scripts in the first place.
The team-wide solution is a .gitattributes file in the repository. It is versioned and wins over the local core.autocrlf of every clone:
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlftext=auto lets git detect binary files and leave them alone, eol=lf enforces LF in the working tree – on Windows, too. Batch files keep CRLF, because cmd.exe can stumble over LF-only files.
Files that are already committed with CRLF get fixed with:
git add --renormalize .
git commit -m "Normalize line endings"This fixes the repository. Your own working copy keeps the CRLF files until they are checked out again – or until you run the one-liner from above. `git ls-files –eol` shows the line endings of every file in the index (`i/`) and in the working tree (`w/`).
## In summary
– **PowerShell one-liner:** no extra tools, leaves everything but the line endings alone (a UTF-8 BOM excepted) – use absolute paths and mind the encoding.
– **dos2unix / sed:** the classics, whenever Git Bash, WSL or a Linux shell is at hand.
– **VS Code files.eol:** prevents CRLF in new files.
– **.gitattributes with eol=lf:** the team-wide fix that makes the problem go away for good.