最近在部署一个Java应用时,执行启动脚本start_app.sh时遇到了奇怪的报错:
code复制start_app.sh: line 2: $'\r': command not found
start_app.sh: line 5: $'\r': command not found
start_app.sh: line 11: $'\r': command not found
进程...所有旧的 no_database_demo-1.0-SNAPSHOT
start_app.sh: line 33: syntax error: unexpected end of file
这个错误看起来非常诡异——脚本中明明没有调用什么\r命令,但系统却提示找不到这个"命令"。更奇怪的是,最后还出现了语法错误,提示文件意外结束。
经过排查,这实际上是Linux/Unix系统中非常经典的一个问题:脚本文件的换行符格式不正确。具体来说,这个脚本是在Windows环境下编写或编辑过的,使用了Windows的CRLF(\r\n)换行符,而Linux系统只能正确识别Unix格式的LF(\n)换行符。
提示:CRLF是"Carriage Return + Line Feed"的缩写,即回车+换行,对应ASCII码是\r\n;而LF是"Line Feed"的缩写,即换行,对应ASCII码是\n。
当Linux的shell(如bash)尝试执行一个脚本时,它会逐行读取并解释脚本内容。如果脚本使用的是CRLF换行符:
\r字符\r在Unix中是一个合法的字符(回车符),shell会尝试将其作为命令执行\r的命令,因此报错"command not found"CRLF换行符还会导致脚本解析异常:
\r视为行内容的一部分\r而无法正确匹配在Linux系统上,有几种方法可以检查文件的换行符格式:
使用file命令:
bash复制file start_app.sh
如果是DOS格式,输出会包含"with CRLF line terminators"
使用cat -v显示不可见字符:
bash复制cat -v start_app.sh
Windows换行符会显示为^M$,Unix换行符只显示$
使用hexdump查看原始字节:
bash复制hexdump -C start_app.sh | head
CRLF会显示为0d 0a,LF只显示0a
对于已经上传到Linux服务器的文件,最直接的解决方法是使用vim进行格式转换:
使用vim打开脚本文件:
bash复制vim start_app.sh
进入命令模式(按ESC键)
输入以下命令设置文件格式:
code复制:set fileformat=unix
保存并退出:
code复制:wq
注意:vim中也可以使用简写命令
:set ff=unix。如果想查看当前文件格式,可以使用:set ff?命令。
Linux系统通常预装了dos2unix工具,专门用于转换文件格式:
安装dos2unix(如果尚未安装):
bash复制# Ubuntu/Debian
sudo apt-get install dos2unix
# CentOS/RHEL
sudo yum install dos2unix
转换文件格式:
bash复制dos2unix start_app.sh
验证转换结果:
bash复制file start_app.sh
应该显示"ASCII text",而不是"with CRLF line terminators"
如果没有dos2unix工具,可以使用sed命令删除CR字符:
bash复制sed -i 's/\r$//' start_app.sh
这个命令会原地修改文件(-i选项),将所有行尾的\r字符替换为空。
如果文件还在Windows系统上,可以使用Notepad++进行预处理:
对于使用Git管理的项目,可以配置自动换行符转换:
全局配置(推荐):
bash复制git config --global core.autocrlf input
这样在Windows上检出时转换为CRLF,提交时转换为LF
或者针对特定仓库:
bash复制git config core.autocrlf input
还可以在项目根目录添加.gitattributes文件:
code复制* text=auto
*.sh text eol=lf
开发时应在编辑器中设置默认使用Unix换行符:
除了换行符,还要确保脚本有可执行权限:
bash复制chmod +x start_app.sh
否则会报"Permission denied"错误,而不是换行符相关的错误。
dos2unix预处理除了换行符,文件编码也可能导致类似问题:
UTF-8 with BOM:某些Windows编辑器会添加BOM头
bash复制# 移除BOM头
sed -i '1s/^\xEF\xBB\xBF//' script.sh
非UTF-8编码:确保脚本使用UTF-8无BOM编码
行尾的空格也可能导致脚本执行异常:
bash复制# 删除所有行尾空格
sed -i 's/[[:space:]]*$//' script.sh
不同的shell对换行符的容忍度不同:
在Windows上创建脚本:
bash复制echo "echo Hello World" > test.sh
上传到Linux服务器后执行:
bash复制bash test.sh
报错:test.sh: line 1: $'\r': command not found
当遇到脚本执行报错时,建议按以下步骤排查:
ls -l script.shfile script.sh或cat -v script.shbash -n script.shbash -x script.shenv与脚本中使用的变量是否匹配对于复杂的脚本,可以:
set -x启用调试模式trap命令捕获信号和错误bash复制shellcheck script.sh
在Git仓库中添加pre-commit钩子,自动检查文件格式:
安装pre-commit:
bash复制pip install pre-commit
创建.pre-commit-config.yaml:
yaml复制repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: mixed-line-ending
args: [--fix=lf]
安装钩子:
bash复制pre-commit install
在持续集成流程中添加文件格式检查:
yaml复制# .gitlab-ci.yml示例
check_script_format:
script:
- git ls-files -z '*.sh' | xargs -0 dos2unix --info
- if [ $? -ne 0 ]; then echo "Found DOS line endings"; exit 1; fi
编写简单的校验脚本:
bash复制#!/bin/bash
check_files() {
local files=$(find . -type f -name "*.sh")
local has_error=0
for file in $files; do
if file "$file" | grep -q "CRLF"; then
echo "ERROR: $file contains CRLF line endings"
has_error=1
fi
done
return $has_error
}
check_files || exit 1
不同操作系统使用不同换行符的原因可以追溯到早期的计算机和打字机:
现代工具和语言运行时通常能智能处理换行符:
open()函数的newline参数控制换行符处理System.lineSeparator()返回平台相关的换行符许多网络协议明确规定使用CRLF:
这也是为什么Web开发中经常需要处理CRLF换行符。