
1. 同样一段脚本Windows写得爽Linux跑得懵先搞清楚问题长什么样我在运维岗上见过太多这样的场景开发在 Windows 上写了个部署脚本本地跑得好好的一旦丢到 Linux 服务器上要么报command not found要么报syntax error: unexpected end of file更诡异的是有些脚本连报错都不给执行结果却跟预期完全对不上。标题里这句话——“Windows 下编写的脚本无法在 Linux 中运行”——本身是个非常笼统的描述但落到真实环境里它背后往往藏着七八种完全不同的病因。这篇文章我就把这几年踩过的坑、排查过的现场以及最终沉淀下来的一套处理流程从头到尾捋一遍。先说结论绝大多数问题出在三个地方——换行符、解释器声明、以及执行权限。这三样东西在 Windows 下几乎不会被注意到因为 Windows 本身就不按 Linux 那套逻辑来。换行符是头号杀手我统计过自己经手的跨平台脚本问题大概有七成以上跟它有关剩下的两成多才是解释器差异、编码 BOM、路径分隔符这类边角料。所以你不用急着怀疑自己写的语法有问题先按照下面这套路径走一遍九成场景十分钟内能定位到根因。不过说句实话真正让人头疼的不是“跑不了”本身而是它跑起来的姿势千奇百怪。有的脚本第一行就报错有的运行到一半突然抽风有的结果不对但完全不报错。所以这篇文章我不想只给你几个命令就完事我会把每个症状背后的原理也讲透。只有理解了“为什么”遇到没见过的新报错时你才能举一反三。1.1 最常见的两类报错现场先还原一下最典型的两个现场你看看自己中过哪个。现场一把 Windows 里写好的.sh文件通过 FTP 或者 U 盘拷到 Linux执行./deploy.sh终端直接甩出来-bash: ./deploy.sh: /bin/bash^M: bad interpreter: No such file or directory这个^M就是罪魁祸首。它是回车符Carriage ReturnASCII 13在终端里的显示形式。Linux 下的 bash 在解析脚本第一行#!/bin/bash时读到的其实是#!/bin/bash\r也就是路径末尾多了一个回车字符。它去/bin/bash\r这个路径找解释器当然找不到。现场二报错形式变成./test.sh: line 3: $\r: command not found syntax error near unexpected token $\r这个场景通常是脚本里的解释器声明没有换行符污染但后续代码行末尾全带着\r。bash 在解析到回车符时把它当成了一个命令或者语法单元于是要么说$\r: command not found要么在解析 if/for 这类语法结构时报syntax error near unexpected token。有次我帮同事看一个部署脚本他坚持说“代码在 Windows 上跑得好好的”我让他执行一下cat -A script.sh看到每行结尾都是^M$他立刻沉默了。1.2 为什么“看起来一样”的脚本行为却完全不同这里有个特别容易让新手困惑的点你用 Notepad 或者 VSCode 打开同一个脚本文件Windows 和 Linux 下看到的字符内容完全一样肉眼根本看不出区别。原因很简单回车符和换行符都是不可见字符。Windows 文本文件用\r\n回车换行表示一行结束Linux/Unix 只用\n换行。这个差异最早可以追溯到电传打字机时代——\r是把打印头移回行首\n是把纸往上滚一行。Windows 继承了老式 DOS 的做法两个都用Linux 则简化成只需要换行。更麻烦的是你把一个 Linux 的脚本用 Windows 记事本打开再保存一下它也会悄悄把换行全部转成\r\n。也就是说哪怕你最初在 Linux 上写好了脚本只要中间经过一次 Windows 编辑器的“无意识转换”再拷回去照样跑不了。所以这不是一个“一次性”的问题而是一个会反复出现的兼容性问题你必须在开发和编辑环节就把它管住。2. CRLF和LF99%的跨平台脚本问题都出在这个看不见的字符上既然换行符是第一大杀手那我们就把它单独拎出来彻底讲明白。文本文件里每一行的结尾在 Windows 下是\r\n两个字符在 Linux 下是\n一个字符。\r全称 Carriage Return对应十六进制0x0D\n全称 Line Feed对应十六进制0x0A。这俩的称呼也经常被搞混——Linux 下管\n叫换行符Windows 下经常把回车换行合在一起叫而老式的 MacmacOS 9 及以前用的是单独的\r这个虽然现在很少见了但在一些古董文本文件里还能碰到遇到的话同样会导致解析错乱。2.1 一个回车符引发的血案要真正理解为什么\r能让 bash 直接崩溃你得站在解释器的角度想问题。Shell 是一种行解释器它按行读取脚本每读一行就做词法分析。词法分析阶段\r本身不是一个合法的命令分隔符也不是空字符于是 bash 在遇到$\r的时候会把它当作一个 token 去尝试解释。在命令替换、变量赋值、函数定义这些场景里这个额外的 token 就会让解析器彻底懵掉。举个例子。假设你的脚本里有这一行if [ -f /etc/passwd ]; then echo exists fi如果每行结尾都是\r\n那么 bash 实际读到的是if [ -f /etc/passwd ]\r; then\r echo exists\r fi\r[ -f /etc/passwd ]后面跟着一个回车符测试命令的参数列表里多了一个不是合法选项的东西于是要么报[: unexpected argument要么干脆语法错误。更坑的是有些版本的内置命令对尾部\r处理方式不一样有的命令能忽略有的直接报错这就导致同一个脚本在不同 Linux 发行版上的表现可能天差地别特别具有迷惑性。2.2 三种立竿见影的换行符转换方法知道了病因接下来就是怎么治。我推荐三个方法按效率排序。方法一dos2unix命令一键转换。这是最直接的办法大多数发行版默认没装需要先安装# Debian/Ubuntu sudo apt-get install dos2unix # CentOS/RHEL sudo yum install dos2unix然后转换dos2unix script.sh注意它会直接修改源文件。如果你想保留原文件可以用dos2unix -n old.sh new.sh。反向转换Linux 转 Windows就用unix2dos。方法二如果你手头没有权限装软件用sed也能做到sed -i s/\r$// script.sh这条命令的本质是把每行结尾的\r替换成空字符串。-i表示原地修改\r$是正则表达式匹配“行尾的回车符”。我在生产环境上经常用这个因为服务器不一定允许你装东西但sed是几乎每个 Linux 系统都会有的。方法三纯tr命令适合管道场景不改源文件tr -d \r input.sh output.shtr -d是删除指定字符\r就是要删掉所有回车符然后把标准输入重定向到源文件标准输出重定向到新文件。这个方法的好处是简单粗暴缺点是如果你处理的是二进制文件会出问题所以只建议针对纯文本脚本使用。2.3 用file命令和cat -A做诊断做运维的人最爱说一句话先确认再操作。转换之前你得先确认文件确实存在\r。两个命令就够了。file命令会直接告诉你文本文件类型和换行风格file script.sh如果输出里带with CRLF line terminators说明文件是 Windows 风格。正常 Linux 脚本应该显示ASCII text或者带with LF line terminators有些版本不显示 LF因为 LF 是默认。cat -A则会把换行符显形cat -A script.sh看输出每行结尾是$表示这是\n如果$前还有一个^M那说明多了一个\r。我第一次给同事演示这个命令的时候他盯着屏幕上密密麻麻的^M$愣了好几秒然后自己默默把 VSCode 的换行符配置改了。工具只教你一种用法的话你会记住操作但如果你能亲眼看到“看不见的字符”从今往后你写跨平台脚本的时候会本能地留意这个细节。3. Shebang、解释器与权限脚本“明明没错”却执行不了的三个隐藏原因换行符的问题解决了之后你会发现很多脚本还是跑不起来。这时候就得把视线转向另外三个隐蔽性更强的因素。它们不会像^M那样肉眼可见但造成的后果一模一样脚本根本到不了执行代码那一步。3.1 Shebang缺失或写错内核不知道该找谁Shebang 是脚本第一行以#!开头的声明它告诉操作系统要用哪个解释器来执行这个文件。比如#!/bin/bash、#!/usr/bin/env python3。Linux 内核在执行一个文本文件时如果发现文件前两个字节是#!它就会把后面的路径当作解释器然后启动解释器并把脚本文件作为参数传给它。如果脚本没有 Shebang那直接执行./script.sh会怎样这取决于你在哪个 shell 里。在 bash 里通常会用 bash 自己来解释但如果用户默认 shell 是 zsh 或者 sh解析规则可能不一样行为就可能发生变化。不过这个情况其实不算最坑的最坑的是 Shebang 写错了或者指向了一个不存在的解释器路径。举个例子你在 Windows 上写脚本时用记事本建的第一行写的是#! /bin/bash注意#!和/bin/bash之间多了一个空格。绝大多数情况下 bash 能容忍这种写法但某些解释器的处理方式不一样为了保险起见不要加空格。还有一种情况脚本第一行写的是#!/bin/bash\r换行符没清干净那就回到上一节说的bad interpreter问题了。另外一个我在实际碰到的坑有人把脚本传到 Linux 上后习惯性地用sh script.sh来运行即使没有 Shebang 也可以跑。但如果脚本本身是为 bash 写的语法数组、[[ ]]条件、source等而系统默认的sh是 dash 的话就会出现“在 Windows 上写着明明没问题到 Linux 跑就一堆语法错误”的怪象。这个我们下一节详细说。3.2 sh和bash的差异语法没错但跑出不同结果这是我最想让新手注意的一点Linux 上的/bin/sh不一定是 bash。在 Debian 和 Ubuntu 上/bin/sh是指向dash的符号链接dash 是一个精简版的 POSIX shell只支持 POSIX 语法的一个子集不支持数组至少不支持 bash 那种风格、不支持[[ ]]、不支持source要用.。你在 Windows 的 Git Bash 或者 Cygwin 里写的脚本很可能用了 bash 特有的语法一拿到 Debian 上直接用sh xxx.sh跑立刻报错。如果你要确认自己的脚本需要 bash 还是 sh第一要务是检查 Shebang。如果有#!/bin/bash就用./script.sh跑如果没有 Shebang那就说明这个脚本应该遵循 POSIX 标准来写。跨平台脚本我建议全部显式声明#!/bin/bash这样至少行为可预期。当然更严谨一点也可以只写 POSIX 语法但如果你不是被要求必须跑在sh上没必要给自己添这个限制。还有个有趣的坑你从 Windows 上用 FTP 工具比如某些 IDE 自带的部署功能上传脚本时工具可能默认把换行符转换成了 Linux 风格这反而没问题但如果你用了scp或者直接在共享文件夹里编辑脚本就可能是 Windows 风格。很多人困惑“是不是和编辑工具有关”实际上和传输工具有关的情况更多。3.3 可执行权限chmod x不是可选的第三个隐藏因素是文件权限。Windows 没有 Unix 那种“可执行位”的概念test.bat能被运行靠的是扩展名关联而不是文件本身带执行权限。但在 Linux 下一个文件能不能被当成程序直接执行取决于它有没有x权限。你从 Windows 拷贝一个脚本到 Linux常见的目录权限是-rw-r--r--没有执行位。直接执行./script.sh会得到-bash: ./script.sh: Permission denied解决办法很简单chmod x script.sh这个权限位单独看很简单但它引出一个更麻烦的问题如果你通过 FAT32 格式的 U 盘或者某些网络文件系统跨平台拷贝文件整个文件系统的挂载选项里就可能没有执行权限支持哪怕你chmod x成功下次重新挂载又回到没有权限的状态。一次我帮一个同事排查“chmod 了还是 Permission denied”查到最后发现他的脚本放在ntfs-3g挂载的 Windows 分区上需要改/etc/fstab的挂载选项才能解决这是个非常容易忽略的深水区。4. 一套完整的排查链路从报错信息到定位根因的操作顺序这一章我分享一下完整的实操排查链路。直接照顺序做能省下大量时间去网上瞎搜。很多新人遇到“Permission denied”就去搜“Linux 权限”遇到“bad interpreter”就去搜“bash 运行脚本报错”结果查出来的答案零散且不精准。其实只要先做两个基础检查就能把 80% 的问题划分清楚。4.1 第一步先用file确认脚本的真实格式任何脚本拷到 Linux 上之后第一件事不要着急执行先去file一下。我自己的习惯是把它当作像仪器测温一样自然的流程步骤。file /path/to/script.sh这是输出样例script.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators这句话直接告诉你三件事它是一个 Bash 脚本也有可能是 POSIX shell script它已经是可执行文件说明权限位可能是对的或可以加它带 CRLF 换行符这是当前最需要处理的问题如果输出是script.sh: Unicode text, UTF-8 (with BOM) text, with CRLF line terminators那就更麻烦了因为还多了一个 BOM 头的问题下一章细说。看到file的结果以后你对问题的分类就有了准确判断。如果是 CRLF按上述dos2unix或sed处理转换如果一切正常再往下看执行方式。4.2 第二步按报错类型分类处理拿到报错信息后我习惯先做一个分类。你可以对照这个表格快速定位报错形态大概率原因快速处理bad interpreter: /bin/bash^M首行 Shebang 带 CRLF转换换行符$\r: command not found或syntax error near unexpected token代码行带 CRLF转换换行符Permission denied无执行权限chmod xNo such file or directory但文件确实存在Shebang 路径错误或解释器未安装检查#!路径command not found针对命令命令不存在或环境变量 PATH 不对检查命令是否安装、路径/bin/sh: 0: Cant open script.sh用 sh 执行了不存在的文件或路径拼错检查路径和文件名运行后部分功能异常但不报错命令参数差异或内置命令差异用set -x追踪有些时候file的输出已经告诉我们 CRLF 存在了可你去挨个搜索“bad interpreter”是什么意思绕了一大圈才明白。拿着这个表格对照基本能一锤定音。4.3 第三步用set -x和shellcheck辅助排查如果你把换行符、权限、Shebang 都排除了脚本还是跑不出预期结果那就得看脚本内部逻辑了。两个辅助工具非常实用。set -x是 bash 的调试选项它会打印每一条实际执行的命令前面带一个号。用法有两种bash -x script.sh或者在脚本内部第二行写上set -x这个选项会把变量展开之后的结果也显示出来能帮你发现“变量值里带着一个看不见的\r”这类问题。我遇到过一次很典型的案例脚本运行没报错但创建出来的文件名尾都有一个问号最后用set -x才看到变量值末尾藏着一个\r。shellcheck是另一个我必须强烈推荐的工具。它是一个静态检查工具能直接指出脚本里的语法问题、常见陷阱和不规范写法# 安装 sudo apt-get install shellcheck # Debian/Ubuntu sudo yum install shellcheck # CentOS/RHEL # 检查 shellcheck script.sh它甚至能帮你识别出部分换行符问题以及sh和bash混用问题。比如它会警告你在 POSIX 模式下用了 bash 特有的数组语法。对于跨平台脚本这种场景shellcheck 的价值在于把潜在问题提前暴露而不是等用户跑到一半才发现。5. 不只是换行命令差异、路径风格与编码BOM的连环坑如果换行符和权限都处理完脚本还是和你对着干那你就进入了更深的领域。这一层的问题通常比较“隐性”——不会直接报错但行为完全不对。我把踩过的坑集中说一下。5.1 Windows命令与Linux命令不是一一对应很多人以为在 Windows 的 cmd 或者 PowerShell 里用的命令在 Linux 上换了终端就能直接执行。这是个天大的误会。ping、ipconfig、find、tasklist……哪怕名字一样参数和行为也完全不同。举几个常见例子Windows 的find是查找文本内容的Linux 的find是查找文件的Windows 下查找文本用findstrLinux 下才用grep。wget在 Windows 10/11 上其实也有但行为和参数不一定一致Linux 上的wget默认支持递归下载Windows 那个阉割版很多选项不可用。路径分隔符Windows 用\Linux 用/脚本里写死路径的话一跨就废。环境变量语法Windows 是%VAR%cmd或$env:VARPowerShellLinux 是$VAR。如果你的脚本里写了对 Windows 命令的调用比如用 PowerShell 脚本的逻辑改成了 bash要彻底审查一遍命令是否在目标 Linux 环境下存在。特别是脚本中用到的工具路径Windows 下软件装在C:\Program Files\...Linux 下通常在/usr/bin或者/usr/local/bin这个差异很难用文本替换解决需要逐个适配。5.2 路径分隔符和盘符的隐性差异除了命令差异路径问题也非常隐蔽。假设脚本里有这样一行cd C:\Users\admin\scripts拿到 Linux 上当然执行不了。但有些问题不是这么明显的——比如脚本是从配置文件里读路径这个配置文件又是 Windows 软件生成的路径分隔符就是\。在 bash 里\是转义字符一个单独的\在双引号里会被保留在单引号里也会保留但在很多场景下它会吃掉后面的字符导致路径解析错乱。还有盘符的问题。Linux 没有盘符的概念所有路径都从根目录/开始。如果脚本里有C:/或者D:/这种路径在 Linux 上会被当成相对路径处理最终结果完全不可预期。处理这类问题的唯一办法是在脚本里避免硬编码绝对路径改用相对路径或者通过环境变量注入。我记得有一次帮别人排查自动化任务脚本在 Linux 上能跑但是定位不到配置文件。一开始我以为是权限问题一根set -x才发现脚本里通过$APP_HOME拼了一个\config\app.ini的后缀路径而$APP_HOME是从 Windows 环境变量里带过来的分隔符完全没转。所以跨平台脚本中路径统一用/并且尽量用/拼接目录才是真正的“一次编写处处运行”。5.3 UTF-8 BOM头一个看不见的三个字节BOMByte Order Mark是 UTF-8 编码里用来标识文件编码的标记在文件开头写入EF BB BF三个字节。Windows 的记事本保存 UTF-8 文件时默认会加入 BOM而 Linux 下的很多工具不认 BOM会把这个额外的三个字节当作真实内容来解析。这就导致一种很邪门的情况脚本的 Shebang 明明写着#!/bin/bash文件也检查过换行符一切正常但执行时报错说找不到解释器。我用xxd看过这种文件的开头00000000: efbb bf23 212f 6269 6e2f 6261 7368 0a ...#!/bin/bash.看出来了吗#!/bin/bash前面藏着efbb bf也就是 BOM。内核看到的前两个字节不是#!自然是找不到解释器的。处理办法是去掉 BOM。你可以用sedsed -i 1s/^\xEF\xBB\xBF// script.sh这条命令把第一行开头的 BOM 字节删掉。也可以用dos2unix它默认就会处理 BOM。最彻底的办法还是在编辑器层面解决后面我会专门讲怎么配置。另外提醒一句如果你用的是 Python 脚本BOM 会导致 Python 解释器在读取源码时报SyntaxError: invalid character in identifier这类问题在跨平台场景下也经常碰到。所以不仅是 shell 脚本任何文本类脚本都有必要关注 BOM 问题。6. 根治方案在Windows上直接养成Linux友好的脚本编写习惯前面讲的都是“出了问题怎么救”但作为一个踩过无数次坑的人我更想告诉你的是“从一开始就别让问题出现”。跨平台脚本的兼容性不是靠一次转换搞定的而是靠整个开发和编辑链路的习惯。这一章我把自己的整套方法论写出来。6.1 编辑器统一配置从源头保证LF如果你还在用 Windows 记事本写脚本我真诚建议你立刻换掉。不是记事本不能用而是它默认保存为 UTF-8 with BOM CRLF这两样都是 Linux 脚本的灾难。VSCode、Notepad、Sublime Text 都支持自定义换行符和编码直接把默认值改成 LF 和 UTF-8 无 BOM。以 VSCode 为例左下角点击当前行尾风格默认显示CRLF点击后选择LF。打开设置搜索files.eol设为\n。搜索files.encoding设为utf8并且不要在设置里勾选files.autoGuessEncoding避免不确定。如果你希望所有现有文件也统一转换可以在命令面板跑Change End Of Line Sequence然后选 LF。Notepad 的操作类似菜单栏的“编辑” - “档案格式转换” - “转换为 UNIX 格式”然后保存。这里有个小细节VSCode 的右下角行尾风格只对当前文件生效它不会自动把所有文件都转成 LF。所以最稳妥的做法是先打开文件手动执行一次行尾转换然后再在设置里把默认值改了双管齐下。6.2 用Git的core.autocrlf把换行符管起来也许你已经用 Git 管理脚本了但你知道 Git 也在悄悄干预换行符吗Git 在 Windows 上有一个非常重要的配置项core.autocrlf。它的作用是为true时提交到仓库时自动把 CRLF 转成 LF检出到工作目录时自动把 LF 转成 CRLF。为input时提交时转为 LF检出时保持 LF。为false时不做任何转换。如果你在 Windows 上写脚本然后通过 Git 传到 Linux 服务器上建议把core.autocrlf设为false或input同时配合.gitattributes文件来精细化控制。我个人的做法是在仓库根目录放一个.gitattributes* textauto eollf *.sh text eollf *.py text eollf这样不管谁在什么系统上提交只要他和这个仓库打交道换行符在入库和检出时都会被强制成 LF。这个方案能从根本上避免团队协作时的换行符混乱。不过要注意.gitattributes只对之后的操作生效仓库里已有的文件还是要手动转换一次否则 Git 可能认为文件没有变化。你需要做一次“重新规范化”操作大致流程是把文件从索引中移除、重新添加让 Git 按照新的属性重新规范化内容。这个操作网上有很多资料属于 Git 的高级用法但一次配置终身受益。6.3 有条件就上WSL让开发和运行环境一致如果你长期做跨平台开发或者运维脚本编写我强烈建议你在 Windows 上直接启用 WSLWindows Subsystem for Linux在 WSL 里写脚本、跑脚本、再部署到远程 Linux 服务器。这不是绕远路而是从根上消灭“环境差异”的问题。WSL 里运行的是真正的 Linux 内核WSL2 是轻量级虚拟机你写的 bash 脚本在 WSL 里跑和在服务器上跑的行为基本一致。更关键的是你在 WSL 里创建的文件默认就是 LF 换行、无 BOM文件权限也会被正确保留。这样一来Windows 和 Linux 之间的脚本兼容性问题根本不会产生。我的建议工作流是Windows 上用 VSCode 写代码用 Remote-WSL 插件连接到 WSL 环境写完后直接在 WSL 里用 bash 跑一遍测试确认没问题了再通过 Git 或者 scp 部署到远程服务器。听起来多了一层实际上省掉的是大量排障时间。尤其是你如果经常要写部署脚本、定时任务脚本WSL 这套工作流用起来非常顺手。6.4 一套我常用的跨平台脚本开发检查清单最后分享一份我打印出来贴在工位上的检查清单。每次写完跨平台脚本或者从 Windows 拷贝脚本到 Linux 之前按这个顺序过一遍确认编辑器默认保存为 LF 换行、UTF-8 无 BOM。确认 Git 仓库里设置了core.autocrlf false或.gitattributes强制 LF。确认脚本第一行 Shebang 是#!/bin/bash或#!/usr/bin/env bash没有多余空格。用dos2unix或者sed -i s/\r$//显式预处理一次。上传后先执行file script.sh确认没有 CRLF、没有 BOM、脚本类型正确。执行chmod x script.sh赋予执行权限。如果怀疑权限用ls -l script.sh看权限位。小规模执行配合bash -x观察变量展开是否符合预期。静态检查能跑的话跑一下shellcheck script.sh。涉及路径的地方统一使用/使用相对路径或环境变量避免硬编码 Windows 盘符。这一套流程看着繁琐但实际执行下来一分钟都用不到。别嫌多等你哪次部署到生产环境被一个\r搞得满头大汗你就知道这一分钟有多值钱了。我个人在实际操作中的体会是跨平台脚本问题技术难度其实很低难的是你愿不愿意在每个环节多留一个心眼。换行符、编码、权限、解释器每一个单独拿出来都简单到不值一提但它们组合在一起就能把一段逻辑完全正确的代码变成一个让人抓狂的谜题。希望这篇内容能帮你把这条路上的坑提前填平。下次再遇到“Windows 下编写的脚本无法在 Linux 中运行”别急着怀疑语法先按这套流程走一遍多半能快速解决问题。