在Shell脚本中,末尾处的换行符“$(...)”会被删除。为什么?

5
有人可以解释一下为什么这两个命令的输出不同吗?
$ echo "NewLine1\nNewLine2\n"
NewLine1
NewLine2
                         <-- Note 2nd newline here
$ echo "$(echo "NewLine1\nNewLine2\n")"
NewLine1
NewLine2
$                        <-- No second newline

有没有好的方法可以在 "$( .... )" 的输出末尾保留换行符? 我考虑过添加一个虚拟字母并将其删除,但我很想知道为什么这些换行符会消失。

回显的本质是消耗空格。我现在没有时间测试,但你在问题测试中使用的双引号可能不是你想要的。也许 echo "$(echo \"NewLine1\nNewLine2\n\")" 可以解决问题。祝好运。 - shellter
谢谢,但是在“$(...)”语句的括号内不需要转义双引号。它被视为一个新命令。这是使用$(...)比反引号``更好的优点之一。上述问题也可以通过使用反引号来重现。 - Henry Stokeley
这是已记录的行为:(http://www.gnu.org/software/bash/manual/bashref.html#Command-Substitution)--“...命令的标准输出,删除任何尾随换行符”。要解决这个问题,您可能需要将输出重定向到临时文件,然后再使用该文件。 - glenn jackman
1
这是一个很好的解释。对我来说有点令人沮丧,因为在 "$( ... )" 语句末尾的换行符对我很重要。就像我上面说的,我可能会添加一个随机字母,然后稍后再将其删除。 - Henry Stokeley
@glennjackman:你应该把它发布为答案。 - Keith Thompson
3个回答

4

因为这是POSIX的规定,并且在Bourne shell中一直如此:

2.6.3 命令替换

命令替换允许将一个命令的输出替换为该命令名称本身。当命令被包含在以下内容中时,应进行命令替换:

$(command)

或者(反引号版本):

`command`

Shell通过在子shell环境中执行命令,扩展命令替换,并用命令的标准输出替换命令替换(即命令文本加上"$()"或反引号括起来的文本),移除一个或多个结尾处的<newline>字符序列。输出的嵌入式<newline>字符在输出结束前不会被移除;但是,它们可能被视为字段分隔符,在字段拆分期间根据IFS和引用的值而消除。如果输出包含任何空字节,行为是未指定的。

保留最后的换行符的一种方法是:
VAR="$(command; echo x)"   # Append x to keep newline(s).
VAR=${VAR%x}               # Chop x.

Vis.:

$ x="$(whoami; echo x)" ; printf '<%s>\n' "$x" "${x%x}"
<user
x>
<user
>

为什么要移除尾随的换行符?因为大多数情况下你都需要这样做。我也在使用Perl编程,有很多次我读取了一行或变量后需要去掉换行符:

while (defined ($string = <>)) {
    chop $string;
    frobnitz($string);
} 

2
VAR="$(command)x" 不保留最终的换行符,因为这是命令替换导致的。 - Stephane Chazelas
@sch 抱歉,已修复。感谢您的提醒。 - Jens

1

命令替换会移除每个结尾的换行符。

移除一个换行符是有道理的。例如:

basename foo/bar

输出 bar\n。输入:

var=$(basename foo/bar)

您希望$var包含bar而不是bar\n

然而,在

var=$(basename $'foo/bar\n')

您希望$var包含bar\n(毕竟,在Unix文件名中换行符与任何字符一样有效)。但是所有shell都会删除每个尾随的换行符。这个缺陷在原始Bourne shell中存在,即使rc已经修复了Bourne的大多数缺陷,但它仍未修复该缺陷。(尽管rc具有``(){cmd}语法来不去除任何换行符)。
在POSIX shell中,为了解决这个问题,您可以执行以下操作:
var=$(basename -- "$file"; echo .)
var=${var%??}

虽然这样做会丢失 basename 的退出状态,但你可以通过以下方式解决:

var=$(basename -- "$file" && echo .) && var=${var%??}

${var%??} 是用来删除最后两个字符的。第一个是我们上面添加的 .,第二个是由 basename 添加的 一个 换行符,我们不会再删除更多,因为命令替换会像其他换行符一样成为我们想要获取基础文件名的文件名的一部分,所以我们需要它们。

在 Bourne shell 中没有 ${var%x} 运算符,你需要走很长而且复杂的路来解决这个问题。


1
如果不删除换行符,则像这样的结构将会出现:

x="$(pwd)/filename"

“would not work usefully, but the people who wrote Unix preferred useful behaviour.”的意思是“可能没有什么用,但编写Unix的人更喜欢有用的行为。”

很久以前(大约在1983年或1984年),我曾经遭受过一个特定变种Unix系统中shell更新的困扰,它没有删除末尾的换行符。这破坏了许多脚本。但问题很快得到解决。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接