假设我有以下简单的脚本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 在评论中提到dos2unix
(sudo 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.sh
CRLF 行结尾为/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
.sh
read -p
答案4
您可以在 bash 脚本中每行的末尾插入井号 (#)。这样,Unix 上的 shell 就会将 CR 视为注释,而不会关心它。
“十六进制”,任何行都应以
0x23 0x0D 0x0A
例子:
echo "testing" #
stat . #
echo "testing again" #