Springuptech
← Back to Insights

When a Script Written on Windows Misbehaves Only on Linux — Fixing Invisible Line Endings (CRLF vs LF)

A plain-language explanation of why shell scripts saved on Windows throw "bad interpreter" errors or pass strange arguments on Linux, and how to detect, fix and prevent CRLF line ending problems.

Jaeyong RohPublished 3 min read

Have you ever edited a script on Windows, uploaded it to a Linux server, and watched it suddenly behave strangely? The error messages make no sense, the code looks fine, and you can lose hours.

The culprit is usually an invisible line ending character. Based on a case we actually ran into, here is what causes it and how to fix it.

What actually happened

On a Linux server, we ran a Java program through a script called run.sh. The script was simple:

java -cp classpath -Dprop=value my.app.Main $1 $2

Yet when we ran ./run.sh with no arguments at all, the Java program reported args.length as 1, not 0. Printing args[0] showed what looked like an empty string.

So we printed the code of each character:

for (char c : args[0].toCharArray()) {
    System.out.println("ASCII: " + (int) c);
}

The output was ASCII: 13 — the carriage return (CR, \r), an invisible character.

The cause: operating systems use different line endings

The marker that says "a new line starts here" is itself a character, and it differs between operating systems.

Operating system Line ending Name
Windows \r\n (CR + LF) CRLF
Linux, macOS \n (LF) LF

It is a leftover from typewriters, where "return to the start of the line" (CR) and "advance the paper one line" (LF) were separate actions.

Linux only treats \n as the end of a line. When a script saved in Windows format (CRLF) runs on Linux, a \r is left at the end of every line and treated as part of a command or argument. In our case, the \r after $2 was passed as an actual argument.

Symptoms to watch for

  • /bin/bash^M: bad interpreter: No such file or directory
  • $'\r': command not found
  • "File not found" errors because an invisible character is stuck to the end of a path or variable
  • A program receiving an unexpected number of arguments, as in our case

How to check

① Reveal invisible characters with cat -A

cat -A run.sh

If you see ^M$ at the end of lines, the file uses CRLF ($ marks the line end and ^M is the \r). On macOS, use cat -v.

② Use the file command

file run.sh
# run.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators

If it says with CRLF line terminators, you have found it.

③ Trace the execution

bash -x ./run.sh

Each command is printed as it runs, so you can see odd values like $'\r' attached to arguments.

How to fix it

Option 1. dos2unix (simplest)

dos2unix run.sh

If it is not installed, use sudo apt install dos2unix (Ubuntu) or sudo yum install dos2unix (CentOS family).

Option 2. Remove it with sed

sed -i 's/\r$//' run.sh

Works on most Linux systems without installing anything. (On macOS: sed -i '' 's/\r$//' run.sh.)

Option 3. Change it in your editor

  • VS Code: click CRLF in the status bar at the bottom right, choose LF, then save
  • Notepad++: Edit → EOL Conversion → Unix (LF)

Run the script again after converting, and args.length correctly returns 0.

Never again: prevention settings

Even after fixing it, saving the file on Windows again brings the problem back. For a team, these settings are the most reliable cure.

① Lock line endings in Git (.gitattributes)

Create .gitattributes at the repository root with:

* text=auto
*.sh text eol=lf

Shell scripts are then always checked out with LF, whatever operating system someone uses. If you have Windows-only batch files, add *.bat text eol=crlf.

② Align editor settings (.editorconfig)

root = true

[*.sh]
end_of_line = lf

Most editors read this file and apply the right line endings automatically.

③ Change the VS Code default

Set Files: Eol to \n in settings so new files are saved with LF.

Summary

  • Windows ends lines with CRLF (\r\n); Linux uses LF (\n).
  • Scripts saved on Windows keep a stray \r on Linux and fail in confusing ways.
  • Detect it with cat -A or file; convert with dos2unix or sed.
  • Set team-wide rules with .gitattributes and .editorconfig to stop it from coming back.

Once you know the cause, this takes a minute to fix — if you don't, it can take a whole day. We hope this helps anyone working across Windows and Linux.

LinuxBashGitTroubleshooting
J

Jaeyong Roh

Lead engineer at Springuptech · 22 years in software engineering

Have a project related to this topic?

Even if it is just an idea, we will help you work out what is possible.