shell 脚本对编码和行尾敏感吗?

Posted

技术标签:

【中文标题】shell 脚本对编码和行尾敏感吗?【英文标题】:Are shell scripts sensitive to encoding and line endings? 【发布时间】:2021-09-09 19:30:21 【问题描述】:

我正在 Mac 上制作 NW.js 应用程序,并希望通过双击图标以开发模式运行该应用程序。第一步,我正在尝试使我的 shell 脚本工作。

在 Windows 上使用 VSCode(我想争取时间),我在项目的根目录下创建了一个 run-nw 文件,其中包含以下内容:

#!/bin/bash

cd "src"
npm install

cd ..
./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &

但我得到了这个输出:

$ sh ./run-nw

: command not found  
: No such file or directory  
: command not found  
: No such file or directory  

Usage: npm <command>

where <command> is one of:  (snip commands list)

(snip npm help)

npm@3.10.3 /usr/local/lib/node_modules/npm  
: command not found  
: No such file or directory  
: command not found

我真的不明白:

它似乎将空行作为命令。在我的编辑器 (VSCode) 中,我尝试将 \r\n 替换为 \n(以防 \r 产生问题),但它没有任何改变。 似乎找不到文件夹(有或没有dirname 指令),或者它不知道cd 命令? 似乎不理解installnpm 的参数 真正让我感到奇怪的是,它仍在运行应用程序(如果我手动执行了 npm install)...

无法使其正常工作,并且怀疑文件本身有些奇怪,我直接在 Mac 上创建了一个新文件,这次使用 vim。我输入了完全相同的说明,然后……现在它可以正常工作了。 两个文件的差异显示完全零差异。

有什么区别?什么会使第一个脚本不起作用?我怎样才能知道?

更新

按照接受的答案的建议,在错误的行尾出现后,我检查了多项内容。原来,由于我从我的 Windows 机器上复制了我的~/.gitconfig,所以我有autocrlf=true,所以每次我在 Windows 下修改 bash 文件时,它都会将行尾重新设置为\r\n。 因此,除了运行 dos2unix(您必须在 Mac 上使用 Homebrew 安装)之外,如果您使用的是 Git,请检查您的配置。

【问题讨论】:

如果你在 Linux 上运行一个 shell 脚本,至少到目前为止我遇到的所有 shell 实现,如果他们在某个地方发现了一个 \r,就会感到不安。不,你说你已经删除了\r,我希望你证实它们真的消失了。为了安全起见,您应该以十六进制级别查看文件,以确保其中没有其他奇怪的字符。下一步是使用sh -x ./run-nw 执行脚本,以获取更多信息。 另一个在文本文件中查找奇怪字符的好命令是LC_ALL=C cat -vet /path/to/file。如果文件是正常的,它会看起来很正常(除了每行末尾有一个“$”)。任何异常都应该很好地突出。 DOS/Windows 文件的行尾将有“^M$”。 你不需要安装dos2unix; tr 命令就足够了,它是标准操作系统安装的一部分。下面的答案之一显示了如何使用它,并且可能值得更多的支持。 dd 中还有一个功能可以执行此 IIRC,但可以说它太晦涩无法给出答案。 tr 无法使用 BOM 修复 UTF-8(无论如何这都是可憎的);也许另请参阅***.com/a/38038099/4957508 了解背景和***.com/questions/45240387/… 了解如何删除它。至少 一些 版本的dos2unix 可以解决这个问题,但我猜不是全部。 【参考方案1】:

对于 IntelliJ 用户,这里是编写 Linux 脚本的解决方案。 使用 LF - Unix 和 masOS (\n)

【讨论】:

【参考方案2】:

我在使用 git 和 WSL 时遇到了这个问题。 git 有一个功能,它可以根据您使用的操作系统更改文件的行尾,在 Windows 上,它确保行尾为 \r\n,这与仅使用 \n 的 Linux 不兼容。

您可以通过将文件名.gitattributes 添加到您的 git 根目录并添加以下行来解决此问题:

config/* text eol=lf
run.sh text eol=lf

在此示例中,config 目录中的所有文件都将只有换行符换行符和run.sh 文件。

【讨论】:

这是一个非常聪明的解决方案,无需额外的seds、trs 和类似的代码给后续代码带来负担。 Notepad++ 或 Idea 等优秀的文本编辑器不会将其转回 crlfs(如果他们这样做了,它将在 git commit 时出现)。【参考方案3】:

如果您使用read 命令从(或可能)DOS/Windows 格式的文件(或管道)中读取,您可以利用以下事实: read 将修剪行首和行尾的空白。如果你告诉它回车是空格(通过将它们添加到IFS 变量中),它会从行尾修剪它们。

在 bash(或 zsh 或 ksh)中,这意味着您将替换此标准习语:

IFS= read -r somevar    # This will not trim CR

用这个:

IFS=$'\r' read -r somevar    # This *will* trim CR

(注意:-r 选项与此无关,避免破坏反斜杠通常是个好主意。)

如果您不使用 IFS= 前缀(例如,因为您想将数据拆分为字段),那么您将替换它:

read -r field1 field2 ...    # This will not trim CR

用这个:

IFS=$' \t\n\r' read -r field1 field2 ...    # This *will* trim CR

如果您使用的 shell 不支持 $'...' 引用模式(例如 dash,某些 Linux 发行版上的默认 /bin/sh),或者您的脚本甚至可能用这样的shell运行,那么你需要变得更复杂一点:

cr="$(printf '\r')"
IFS="$cr" read -r somevar    # Read trimming *only* CR
IFS="$IFS$cr" read -r field1 field2 ...    # Read trimming CR and whitespace, and splitting fields

注意,正常情况下,更改IFS时,应尽快恢复正常,以免产生奇怪的副作用;但在所有这些情况下,它都是read 命令的前缀,所以它只影响那个命令,之后不必重新设置。

【讨论】:

【参考方案4】:

由于正在使用 VS Code,我们可以在右下角看到 CRLF 或 LF,具体取决于所使用的内容,如果我们单击它,我们可以在它们之间进行更改(在下面的示例中使用了 LF):

我们还可以使用命令托盘中的“更改行尾顺序”命令。因为它们的功能相同,所以更容易记住。

【讨论】:

【参考方案5】:

为了完整起见,我指出another solution可以永久解决这个问题,而无需一直运行dos2unix:

sudo ln -s /bin/bash `printf 'bash\r'`

【讨论】:

虽然这适用于 python,但它通常不适用于 bash,因为 bash 默认情况下在 IFS 中不包含 \r (因此它将被视为真实字符而不是空格)所以 \ shebang 行以外的 r 字符仍然会导致问题【参考方案6】:

如果您使用的是 BBEdit 之类的文本编辑器,您可以在状态栏中进行操作。有一个可以切换的选项。

【讨论】:

【参考方案7】:

我试图从 Windows 启动我的 docker 容器并得到了这个:

Bash script and /bin/bash^M: bad interpreter: No such file or directory

我使用的是 git bash,问题出在 git config 上,然后我只执行了以下步骤,它就成功了。它将配置 Git 在结帐时不转换行尾:

    git config --global core.autocrlf input 删除您的本地存储库 再次克隆它。

非常感谢此链接中的 Jason Harmon: https://forums.docker.com/t/error-while-running-docker-code-in-powershell/34059/6

在此之前,我试过这个,但没有用:

    dos2unix scriptname.sh sed -i -e 's/\r$//' scriptname.sh sed -i -e 's/^M$//' scriptname.sh

【讨论】:

我有同样的问题,dos2unix 没有缓解。你的命令有效!非常感谢! 谢谢你的回答,这个答案一定有更多的投票权,这个命令对我也很有效【参考方案8】:

是的。 Bash 脚本对行尾敏感,无论是在脚本本身还是在它处理的数据中。它们应该有 Unix 风格的行尾,即每一行都以换行符结束(十进制 10,ASCII 中的十六进制 0A)。

脚本中的 DOS/Windows 行尾

对于 Windows 或 DOS 样式的行尾,每行都以回车符后跟换行符结束。您可以在cat -v yourfile 的输出中看到这个原本不可见的字符:

$ cat -v yourfile
#!/bin/bash^M
^M
cd "src"^M
npm install^M
^M
cd ..^M
./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &^M

在这种情况下,回车符(插入符号表示法中的^M 或 C 转义表示法中的 \r)不会被视为空格。 Bash 将 shebang(由单个回车符组成)之后的第一行解释为要运行的命令/程序的名称。

由于没有名为^M 的命令,它打印: command not found 由于没有名为"src"^M(或src^M)的目录,它打印: No such file or directory 它将install^M 而不是install 作为参数传递给npm,这会导致npm 抱怨。

输入数据中的 DOS/Windows 行结尾

如上,如果你有一个带回车的输入文件:

hello^M
world^M

那么它在编辑器中和将其写入屏幕时看起来完全正常,但工具可能会产生奇怪的结果。例如,grep 将无法找到明显存在的行:

$ grep 'hello$' file.txt || grep -x "hello" file.txt
(no match because the line actually ends in ^M)

附加文本将覆盖该行,因为回车会将光标移动到行首:

$ sed -e 's/$/!/' file.txt
!ello
!orld

字符串比较似乎会失败,即使在写入屏幕时字符串看起来相同:

$ a="hello"; read b < file.txt
$ if [[ "$a" = "$b" ]]
  then echo "Variables are equal."
  else echo "Sorry, $a is not equal to $b"
  fi

Sorry, hello is not equal to hello

解决方案

解决方案是将文件转换为使用 Unix 样式的行尾。有多种方法可以实现:

    这可以使用dos2unix 程序来完成:

    dos2unix filename
    

    有能力的文本编辑器(Sublime、Notepad++,而不是 Notepad)中打开文件并将其配置为以 Unix 行结尾保存文件,例如,使用 Vim,在 (re) 之前运行以下命令保存:

    :set fileformat=unix
    

    如果您有支持-i--in-place 选项的sed 实用程序版本,例如GNU sed,您可以运行以下命令来去除尾随回车符:

    sed -i 's/\r$//' filename
    

    对于其他版本的sed,您可以使用输出重定向来写入新文件。确保为重定向目标使用不同的文件名(以后可以重命名)。

    sed 's/\r$//' filename > filename.unix
    

    同样,tr 翻译过滤器可用于从其输入中删除不需要的字符:

    tr -d '\r' <filename >filename.unix
    

Cygwin Bash

对于 Cygwin 的 Bash 端口,有一个自定义的 igncr 选项可以设置为忽略行尾的回车(可能是因为它的许多用户使用本地 Windows 程序来编辑他们的文本文件)。 这可以通过运行 set -o igncrcurrent shell 启用。

设置此选项仅适用于 当前 shell 进程,因此在 采购 带有无关回车符的文件时很有用。如果您经常遇到带有 DOS 行尾的 shell 脚本并希望永久设置此选项,您可以设置一个名为 SHELLOPTS(全大写字母)的环境变量以包含 igncr。 Bash 在启动时(在读取任何启动文件之前)使用此环境变量设置 shell 选项。

实用工具

file 实用程序可用于快速查看文本文件中使用了哪些行结尾。以下是它为每种文件类型打印的内容:

Unix 行尾:Bourne-Again shell script, ASCII text executable Mac 行尾:Bourne-Again shell script, ASCII text executable, with CR line terminators DOS 行尾:Bourne-Again shell script, ASCII text executable, with CRLF line terminators

cat 实用程序的 GNU 版本有一个 -v, --show-nonprinting 选项,用于显示非打印字符。

dos2unix 实用程序专门用于在 Unix、Mac 和 DOS 行尾之间转换文本文件。

有用的链接

***有一个excellent article,涵盖了标记文本行结尾的多种不同方式、此类编码的历史以及在不同操作系统、编程语言和互联网协议(例如,FTP)中如何处理换行符。

具有经典 Mac OS 行结尾的文件

对于Classic Mac OS(OS X 之前的版本),每行都以回车符(十进制 13,ASCII 中的十六进制 0D)结束。如果脚本文件以这样的行结尾保存,Bash 只会看到一个长行,如下所示:

#!/bin/bash^M^Mcd "src"^Mnpm install^M^Mcd ..^M./tools/nwjs-sdk-v0.17.3-osx-x64/nwjs.app/Contents/MacOS/nwjs "src" &^M

由于这一长行以八角 (#) 开头,Bash 将这一行(和整个文件)视为一条注释。

注意:2001 年,Apple 推出了基于 BSD 派生的NeXTSTEP 操作系统的 Mac OS X。因此,OS X 也使用 Unix 风格的 LF-only 行结尾,从那时起,以 CR 结尾的文本文件变得极为罕见。不过,我认为值得展示 Bash 将如何尝试解释此类文件。

【讨论】:

dos2unix 为我完成了这项工作。感谢您节省了数百万小时。 很好的解释,这里只少了一点:这些天有什么真正的理由让真正的 bash 继续治疗 @987654368 @作为行尾有意义的字符? @AlexCohn 没有令人信服的功能原因,但更改此行为可能会破坏现有脚本。我敢肯定这一定是维护者多次提出并拒绝的。如果你能设计一个好的过渡计划,让它现在是可选的,将来是强制性的,它可能会获得一些支持;但我可以预测很多老前辈会告诉你“教年轻人不要使用 Windows 编辑器”。 感谢@tripleee 的澄清。我已经开始研究 Alex 的问题的答案,并打算使用带有 igncr 选项集的 Cygwin Bash 来试验脚本,但我已经很长时间没有方便地访问 Windows 操作系统了。 @AlexCohn It isn't bash, it's the Linux kernel.【参考方案9】:

来自重复文件,如果问题是您的文件的名称 末尾包含^M,您可以将它们重命名为

for f in *$'\r'; do
    mv "$f" "$f%$'\r'"
done

您确实希望首先修复导致这些文件名称损坏的任何原因(可能创建它们的脚本应该是 dos2unixed 然后重新运行?)但有时这是不可行的。

$'\r' 语法是 Bash 特有的;如果您有不同的外壳,也许您需要使用其他符号。或许也可以看看Difference between sh and bash

【讨论】:

我没有遇到过这个问题,但是考虑到很多用户会从重复的问题中来到这里,这个答案值得更多的关注。我赞成它开始在答案列表中移动。【参考方案10】:

在 JetBrains 产品(PyCharm、phpStorm、IDEA 等)上,您需要单击 CRLF/LF切换 在两种类型的行分隔符(\r\n\n)之间。

【讨论】:

在 Windows 上的 IntelliJ 上,打开设置 (Ctrl+Alt+S) |编辑 |代码风格。在右侧选择 Unix and macOS (\n) 作为 Line Separator。这是更改每个文件设置的替代方法。【参考方案11】:

MAC/Linux 上最简单的方法 - 使用“touch”命令创建文件,使用 VI 或 VIM 编辑器打开此文件,粘贴代码并保存。这将自动删除 windows 字符。

【讨论】:

这不是最简单的方法,也不一定会删除 Windows 字符,它们是有效字符。 没错,但在 vi/vim 中复制/粘贴并不是我所说的“最简单”的方法:D 不过,我不会反对。 同意,对于像我这样不是 shell 脚本专家的人来说,这是一种生活黑客 :) touch 是一个程序【参考方案12】:

去除不需要的 CR ('\r') 字符的另一种方法是运行 tr 命令,例如:

$ tr -d '\r' < dosScript.py > nixScript.py

【讨论】:

应该注意的是,新用户可能会认为他们也可以使用tr -d '\r' &lt; myFile &gt; myFile,这不是一个好主意,因为他们的myFile 现在将被删除或至少被截断。使用&lt; infile &gt; outFile 重定向时,始终为infileoutfile 使用不同的文件名。然后,您可以根据需要重命名。祝大家好运。 另外,tr 的不寻常之处在于它拒绝接受文件名参数;你必须使用tr x y &lt;inputfile(不是tr x y inputfile)这样的重定向

以上是关于shell 脚本对编码和行尾敏感吗?的主要内容,如果未能解决你的问题,请参考以下文章

从windows到linux的shell脚本编码和格式问题

Shell脚本批量修改文件编码为UTF-8

Base64编码是加密算法吗?

text Fish Shell脚本设置创建并更新为云服务编码的视频的时间

对shell脚本进行加密

shell脚本--代码风格规范及技巧