在 Linux 中像 MSYS2 一样处理带有 CRLF(回车符)的 Bash 脚本?

在 Linux 中像 MSYS2 一样处理带有 CRLF(回车符)的 Bash 脚本?

假设我有以下简单的脚本tmp.sh

echo "testing"
stat .
echo "testing again"

尽管它很简单,但它以\r\n(即 CRLF,即回车+换行)作为行结尾。由于网页不会保留行结尾,因此这里是一个十六进制转储:

$ hexdump -C tmp.sh 
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0d 0a  |echo "testing"..|
00000010  73 74 61 74 20 2e 0d 0a  65 63 68 6f 20 22 74 65  |stat ...echo "te|
00000020  73 74 69 6e 67 20 61 67  61 69 6e 22 0d 0a        |sting again"..|
0000002e

现在,它具有 CRLF 行结尾,因为该脚本是在 Windows 上的 MSYS2 下启动和开发的。因此,当我在 Windows 10 上的 MSYS2 中运行它时,我得到了预期的结果:

$ bash tmp.sh
testing
  File: .
  Size: 0               Blocks: 40         IO Block: 65536  directory
Device: 8e8b98b6h/2391513270d   Inode: 281474976761067  Links: 1
Access: (0755/drwxr-xr-x)  Uid: (197609/      USER)   Gid: (197121/    None)
Access: 2020-04-03 10:42:53.210292000 +0200
Modify: 2020-04-03 10:42:53.210292000 +0200
Change: 2020-04-03 10:42:53.210292000 +0200
 Birth: 2019-02-07 13:22:11.496069300 +0100
testing again

但是,如果我将此脚本复制到 Ubuntu 18.04 计算机并在那里运行它,我会得到其他内容:

$ bash tmp.sh
testing
stat: cannot stat '.'$'\r': No such file or directory
testing again

在具有相同行结尾的其他脚本中,我在 Ubuntu bash 中也遇到了此错误:

line 6: $'\r': command not found

...可能来自空行。

所以,很明显,Ubuntu 中的某些东西在回车时会被卡住。我见过BASH 和回车行为:

它与 Bash 没有任何关系: \r 和 \n 由终端解释,而不是由 Bash 解释

...但是,我想这仅适用于在命令行上逐字输入的内容;这里的\r\n已经在脚本本身中输入了,所以 Bash 一定会解释这里\r的。

这是 Ubuntu 中 Bash 的版本:

$ bash --version
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)

...这里是 MSYS2 中的 Bash 版本:

$ bash --version
GNU bash, version 4.4.23(2)-release (x86_64-pc-msys)

(看起来他们的差距并没有那么大……)

无论如何,我的问题是 - 有没有办法说服 Ubuntu/Linux 上的 Bash 忽略\r,而不是试图将其解释为(可以这么说)“可打印字符”(在本例中,意味着一个可能是有效命令的一部分,bash 如此解释)?编辑:没有必须转换脚本本身(所以它保持不变,带有 CRLF 行结尾,如果以这种方式检查,例如在 git 中)

EDIT2:我更喜欢这种方式,因为与我一起工作的其他人可能会在 Windows 文本编辑器中重新打开脚本,可能会\r\n再次重新引入脚本并提交它;然后我们可能会得到无休止的提交流,这可能只不过是污染存储库的\r\n转换\n

编辑2:@Kusalananda 在评论中提到dos2unixsudo apt install dos2unix);请注意,只需这样写:

$ dos2unix tmp.sh 
dos2unix: converting file tmp.sh to Unix format...

...将就地转换文件;要将其输出到 stdout,必须设置 stdin 重定向:

$ dos2unix <tmp.sh | hexdump -C
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0a 73  |echo "testing".s|
00000010  74 61 74 20 2e 0a 65 63  68 6f 20 22 74 65 73 74  |tat ..echo "test|
00000020  69 6e 67 20 61 67 61 69  6e 22 0a                 |ing again".|
0000002b

...然后,原则上,可以在 Ubuntu 上运行它,这似乎在这种情况下有效:

$ dos2unix <tmp.sh | bash
testing
  File: .
  Size: 20480       Blocks: 40         IO Block: 4096   directory
Device: 816h/2070d  Inode: 1572865     Links: 27
Access: (1777/drwxrwxrwt)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-04-03 11:11:00.309160050 +0200
Modify: 2020-04-03 11:10:58.349139481 +0200
Change: 2020-04-03 11:10:58.349139481 +0200
 Birth: -
testing again

然而,除了需要记住的稍微混乱的命令之外,这也改变了 bash 语义,因为 stdin 不再是终端;这可能适用于这个简单的例子,但请参阅例如https://stackoverflow.com/questions/23257247/pipe-a-script-into-bash例如更大的问题。

答案1

据我所知,没有办法告诉 Bash 接受 Windows 风格的行结尾。

在涉及 Windows 的情况下,常见的做法是依靠 Git 在提交时使用autocrlf配置标志自动转换行结尾的功能。参见示例GitHub 有关行结尾的文档,这不是 GitHub 特有的。这样,文件就会在存储库中以 Unix 风格的行结尾提交,并根据每个客户端平台进行适当的转换。

(相反的问题不是问题:MSYS2 在 Windows 上可以很好地处理 Unix 风格的行结尾。)

答案2

你应该使用binfmt_misc为此[1]。

首先,定义一个处理以 开头的文件的 magic #! /bin/bash<CR><LF>,然后为其创建一个可执行解释器。解释器可以是另一个脚本:

INTERP=/path/to/bash-crlf

echo ",bash-crlf,M,,#! /bin/bash\x0d\x0a,,$INTERP," > /proc/sys/fs/binfmt_misc/register
cat > "$INTERP" <<'EOT'; chmod 755 "$INTERP"
#! /bin/bash
script=$1; shift; exec bash <(sed 's/\r$//' "$script") "$@"
EOT

测试一下:

$ printf '%s\r\n' '#! /bin/bash' pwd >/tmp/foo; chmod 755 /tmp/foo
$ cat -v /tmp/foo
#! /bin/bash^M
pwd^M
$ /tmp/foo
/tmp

示例解释器有两个问题:1.由于它通过不可查找的文件(管道)传递脚本,bash 将逐字节读取它,效率非常低,并且2.任何错误消息都将引用/dev/fd/63或类似的名称而不是原始脚本的名称。

[1] 当然,您可以创建一个/bin/bash^M指向解释器的符号链接,而不是使用 binfmt_misc,这也适用于 OpenBSD 等其他系统:

ln -s /path/to/bash-crlf $'/bin/bash\r'

但在 Linux 上,shebanged 可执行文件比 binfmt_misc 没有任何优势,并且将垃圾放入系统目录中并不是正确的策略,并且会让任何系统管理员摇头;-)

答案3

好的,我找到了一些解决方法,通过:

“联结”符号链接

现代 UNIX 系统有一种方法可以使任意数据显示为文件,而与存储方式无关:保险丝。使用 FUSE,对文件的每个操作(创建、打开、读取、写入、列出目录等)都会调用程序中的某些代码,并且该代码可以执行您想要的任何操作。看创建一个实际上是命令的虚拟文件。你可以尝试一下脚本文件系统或者熔丝,或者如果您雄心勃勃,也可以自己动手。

... 和创建一个实际上是命令的虚拟文件

您可能正在寻找命名管道

因此,方法是:创建一个命名管道,向其dos2unix输出,然后调用bash该命名管道。

这里我有原始的tmp.shCRLF 行结尾为/tmp;首先,让我们创建命名管道:

tmp$ mkfifo ftmp.sh

现在,如果您运行以下命令:

tmp$ dos2unix <tmp.sh >ftmp.sh

...你会注意到它会阻塞;如果你这样做了,请说:

~$ cat /tmp/ftmp.sh | hexdump -C
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0a 73  |echo "testing".s|
00000010  74 61 74 20 2e 0a 65 63  68 6f 20 22 74 65 73 74  |tat ..echo "test|
00000020  69 6e 67 20 61 67 61 69  6e 22 0a                 |ing again".|
0000002b

...您会注意到转换已完成 - 并且在命令cat运行完毕后,dos2unix <tmp.sh >ftmp.sh先前阻止的命令已退出。

dos2unix因此,我们可以在“无限”while 循环中设置对命名管道的写入:

tmp$ while [ 1 ] ; do dos2unix <tmp.sh >ftmp.sh ; done

...即使它是一个“紧密”循环,也不应该成为问题,因为大多数时候 while 循环内的命令是阻塞的。

然后我可以这样做:

~$ bash /tmp/ftmp.sh
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 801h/2049d  Inode: 5276132     Links: 7
...
testing again
$

...显然,脚本运行良好。

这种方法的好处是我可以tmp.sh在文本编辑器中打开原始文件;并编写新代码 - 以 CRLF 结尾 - 然后保存tmp.sh;在Linux下运行bash /tmp/ftmp.sh将运行最新保存的版本。

这样做的问题是,像read -p "Enter user: " user这样依赖于实际终端 stdin 的命令将会失败;或者更确切地说,不要失败,但如果你尝试,请说成/tmp/tmp.sh

echo "testing"
stat .
echo "testing again"
read -p "Enter user: " user
echo "user is: $user"

...然后将输出:

$ bash /tmp/ftmp.sh
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
...
 Birth: -
testing again
Enter user: tyutyu
user is: tyutyu
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
...
 Birth: -
testing again
Enter user: asd
user is: asd
testing
...

...等等 - 也就是说,终端中键盘的 stdin 被正确解释,但由于某种原因,脚本开始循环,并从头开始一遍又一遍地执行(如果我们没有命令,则不会发生这种情况read -p ...)原本的tmp.sh)。也许有一些重定向的东西(例如,在循环命令中添加一些0>1&或其他内容while;实际上,我有一个.sh脚本也开始像这样循环,并且简单地在脚本末尾wget添加显式似乎可以停止脚本循环)这也可以处理这个问题,-但到目前为止,我需要使用的脚本没有类似的命令,所以这种方法可能对我有用。exit.shread -p

答案4

您可以在 bash 脚本中每行的末尾插入井号 (#)。这样,Unix 上的 shell 就会将 CR 视为注释,而不会关心它。

“十六进制”,任何行都应以

0x23 0x0D 0x0A

例子:

echo "testing" #
stat . #
echo "testing again" #

相关内容