我有一个名为 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
我指定的原因是非空白上面 - \n
ewline 是$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
。