花拾录
← 返回知识库

Windows 写好的脚本传到 Linux 就失灵:行尾那个看不见的 \r

软件工程 / 工具导入2026/09/220 阅读0 评论

一个 shell 脚本,你在 Windows 上写完、测试通过,信心满满地 scp 到服务器上跑。结果各种诡异:命令被拼错、#!/bin/bash 像是失效了、偶尔还报一句莫名其妙的「找不到命令」。你盯着服务器上的文件看,内容明明和本地一模一样。

现象

常见的几种表现:

  • 执行时报 $'\r': command not found 或 command not found: ...,但报错的命令名看起来是对的;
  • 脚本开头那行 shebang 似乎没生效,被当成普通命令执行;
  • 脚本能跑,但传到子脚本的变量带了看不见的尾巴,导致字符串比较永远不相等;
  • 更隐蔽的:case 语句、if [ "$x" = "y" ] 判断总是走进错误分支。

最让人抓狂的是——用编辑器打开,两边的文件看起来完全相同。

根因

差别在行尾那些看不见的字符。

Windows 的文本文件默认用 CRLF(\r\n)作为行结束符,也就是回车(Carriage Return,\r,字节 0x0D)加换行(Line Feed,\n,字节 0x0A)。而 Unix/Linux 用 LF(\n)单字节。

把 CRLF 文件搬到 Linux 后,每一行末尾都多了一个 \r。Linux 的 shell 把它当成行内容的一部分,而不是行结束符。于是:

  • 变量赋值 FOO=bar 实际变成了 FOO=bar\r,字符串比较、命令参数都会带上这个尾巴;
  • shebang 行 #!/bin/bash 实际是 #!/bin/bash\r,内核去找一个名字里带 \r 的解释器,当然找不到,于是 shebang 失效;
  • 命令行的最后被塞进一个 \r,解析时就可能出现「命令名是 ls\r」这种「看着对、其实找不到」的错误。

解决

上传后先去掉回车再运行:

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

这行的意思是:把每一行末尾的 \r(正则 \r$,$ 表示行尾)替换成空。-i 表示原地修改文件。

如果文件多,可以批量处理:

find . -name '*.sh' -exec sed -i 's/\r$//' {} +

更根本的办法是在源头就存成 LF。 主流的编辑器都支持设置默认换行符:

  • VS Code:右下角点击 CRLF,切换为 LF;也可以在设置里把 files.eol 设为 \n;
  • 在仓库里加一个 .gitattributes,让 Git 自动规范化:
* text=auto eol=lf
*.sh text eol=lf

这样即使本地是 Windows,检出到 Linux 时也是 LF。

还有个更省事的做法:用 dos2unix / unix2dos 工具,名字就是专门干这个的:

dos2unix script.sh

延伸与预防

值得单独说一句的是——这个坑最狠的地方在于它不一定立刻报错。

\r 混进变量之后,很多场景下程序「照常运行」,只是结果错得离谱。比如文件名比较、状态判断、字符串拼接,都可能在带 \r 的情况下静默地走错分支。你以为脚本逻辑有问题,其实是每一行的末尾多了一个看不见的字节。所以凡是遇到「逻辑明明对、结果却不对」的 shell 脚本,除了查逻辑,也要查一遍行尾。

把「换行符」当成文本文件的第一属性来管理,而不是事后补救。

一个实用的判断技巧:当你在 Linux 上看到「文件看起来对、但命令找不到或判断异常」时,第一反应就该是查行尾。快速确认的办法:

file script.sh          # 会提示 "with CRLF line terminators"
cat -A script.sh | head # 行尾的 ^M 就是 \r

cat -A 会把不可见字符显示成可见形式,^M 就代表回车。

顺带说,同样的坑反过来也成立:Linux 脚本传到 Windows、或者在 Windows 上被某些工具读取时,缺了 CRLF 也可能出问题(比如前一篇讲的批处理)。所以跨系统搬运文本,永远先确认一件事:这份文件的换行符,接收方认不认。

评论(0)

  • 还没有评论,来抢沙发~

相关文章