为什么使用命令替换时换行符会丢失?

为什么使用命令替换时换行符会丢失?

我有一个名为 links.txt 的文本文件,如下所示

link1
link2
link3

我想逐行循环遍历这个文件并对每一行执行一个操作。我知道我可以使用 while 循环来做到这一点,但由于我正在学习,我想使用 for 循环。我实际上使用了这样的命令替换

a=$(cat links.txt)

然后使用这样的循环

for i in $a; do ###something###;done

我也可以做这样的事情

for i in $(cat links.txt); do ###something###; done

现在我的问题是,当我将 cat 命令输出替换为变量 a 时,link1 link2 和 link3 之间的新行字符被删除并替换为空格

echo $a

输出

链接1 链接2 链接3

然后我使用了for循环。当我们进行命令替换时,新行是否总是被空格替换?

问候

答案1

换行符在某些时候会被替换,因为它们是特殊字符。为了保留它们,您需要通过使用引号确保它们始终被解释:

$ a="$(cat links.txt)"
$ echo "$a"
link1
link2
link3

现在,由于我在操作数据时都使用引号,因此换行符 ( \n) 总是由 shell 解释,因此保留下来。如果您在某个时候忘记使用它们,这些特殊字符将会丢失。

如果您在包含空格的行上使用循环,也会发生完全相同的行为。例如,给定以下文件...

mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

输出将取决于您是否使用引号:

$ for i in $(cat links.txt); do echo $i; done
mypath1/file
with
spaces.txt
mypath2/filewithoutspaces.txt

$ for i in "$(cat links.txt)"; do echo "$i"; done
mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

现在,如果您不想使用引号,可以使用一个特殊的 shell 变量来更改 shell 字段分隔符 ( IFS)。如果将此分隔符设置为换行符,则可以解决大多数问题。

$ IFS=$'\n'; for i in $(cat links.txt); do echo $i; done
mypath1/file with spaces.txt
mypath2/filewithoutspaces.txt

为了完整起见,这里是另一个示例,它不依赖于命令输出替换。一段时间后,我发现由于该实用程序的行为,大多数用户认为这种方法更可靠read

$ cat links.txt | while read i; do echo $i; done

read以下是的手册页的摘录:

读取实用程序应从标准输入读取一行。

由于read逐行获取输入,因此您确信只要出现空格,它就不会中断。只需cat通过管道将输出传递给它,它就会很好地迭代您的行。

编辑:我从其他答案和评论中可以看出,人们在使用cat.作为杰森·瑞安在他的评论中说,更多恰当的在 shell 中读取文件的方法是使用流重定向 ( <),如您在val0x00ff 的答案在这里。然而,由于问题不是“如何在shell编程中读取/处理文件“,我的回答更多关注的是报价行为,而不是其余部分。

答案2

换行符丢失了,因为 shell 已经执行了场分裂命令替换后。

在 POSIX 中命令替换部分:

shell 应通过在子 shell 环境中执行命令(请参阅 Shell 执行环境)并用命令的标准输出替换命令替换(命令文本加上“$()”或反引号)来扩展命令替换,删除替换末尾的一个或多个字符的序列。输出结束前嵌入的字符不得被去除;但是,它们可能被视为字段分隔符并在字段拆分期间被消除,具体取决于 IFS 的值和有效的引用。如果输出包含任何空字节,则行为未指定。

默认IFS值(至少在 中bash):

$ printf '%q\n' "$IFS"
$' \t\n'

在您的情况下,您不设置IFS或使用双引号,因此换行符将在字段拆分期间被消除。

您可以保留换行符,例如通过设置IFS为空:

$ IFS=
$ a=$(cat links.txt)
$ echo "$a"
link1
link2
link3

答案3

为了强调我的重点,for循环迭代。如果您的文件是:

one two
three four

然后这会发出线路:

for word in $(cat file); do echo "$word"; done

迭代线文件的,执行以下操作:

while IFS= read -r line; do
    # do something with "$line" <-- quoted almost always
done < file

答案4

换行符被空格替换,因为这就是echo工作原理 - 它将其参数连接到空格上。echo用空格替换参数分隔符。事实上,您可以迭代for任何您想要的内容,但您必须首先指定字段分隔符:

string=abababababababababababa IFS=a        
for c in $string
do printf %s "$c"
done

输出

bbbbbbbbbbb

但这并不是for循环独有的行为 - 任何字段拆分扩展都会发生这种情况:

printf %s $string
bbbbbbbbbbb

例如,如果您只想打印文件中任何非空行的前 10 个字节...

###original:
first "line"
<second>"line"
<second>"line"
<second>line and so on%
(IFS='                                                       
'; printf %.10s\\n $(cat file))
###output
first "lin
<second>"l
<second>"l
<second>li

我指定的原因是非空白上面 - \newline 是$IFS.当连续出现两个或多个时,其他所有内容都会给你一个空参数,但任何空格、制表符或换行符序列只能计算为单个字段。

例如:

(IFS=0;printf 'ten lines!%s\n' $(printf "%010d"))

ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!
ten lines!

但...

(IFS=\ ;printf 'one line%s\n' $(printf "%010s"))
one line

在这两种情况下printf都会打印 10 个填充字符 - 在第一种情况下它会打印 10 个零,在第二个情况下会打印 10 个空格。在第一种情况下,每个 0 生成一个空字段,而第二种情况则printf获得 10 个空参数,为每个参数写入其格式字符串,但在第二种情况下打印的所有空格根本没有任何意义。

您应该注意,这不是仅有的shell 将使用不带引号的扩展来生成字段类型 - 默认情况下它也会全局。做这样的事情:

for line in $(cat file)

可能会导致非常意外的结果,因为其中一些行很可能包含与真实文件匹配的 shell 全局变量 - 并且突然$line不再引用输入行,而是引用磁盘上的文件名。

如果您打算$IFS用于任何拆分,那么它是总是一个好主意是:

set -f

...首先,这将指示 shell 在您执行操作时不要进行 glob 操作。完成后,您可以使用 重新启用它set +f

相关内容