下面是一个脚本及其输出,描述了我今天发现的问题。即使ls
输出被引用,bash
仍然会在空格处中断。我改用for file in *.txt
,只是想知道为什么bash
这样做。
[chau@archlinux example]$ cat a.sh
#!/bin/bash
FILES=$(ls --quote-name *.txt)
echo "Value of $FILES:"
echo $FILES
echo
echo "Loop output:"
for file in $FILES
do
echo $file
done
[chau@archlinux example]$ ./a.sh
Value of $FILES:
"b.txt" "File with space in name.txt"
Loop output:
"b.txt"
"File
with
space
in
name.txt"
为什么 bash 忽略了 ls 输出中的引号?
因为单词拆分发生在变量扩展的结果上。
在计算语句时,shell 会经历不同的阶段,称为 shell 扩展。其中一个阶段是"分词"。从字面上看,单词拆分确实将您的变量拆分为单独的单词,引用了 bash 手册:
shell 扫描参数扩展、命令替换和算术扩展的结果,这些结果未在双引号内发生,以进行单词拆分。
shell 将$IFS的每个字符视为分隔符,并将其他扩展的结果拆分为使用这些字符作为字段终止符的单词。如果未设置 IFS,或者其值正好是
<space><tab><newline>
,则默认值,则忽略先前扩展结果开头和结尾的<space>
、<tab>
和<newline>
序列,并且任何不在开头或结尾的 IFS 字符序列都用于分隔单词。...
当 shell 有一个不在双引号内的$FILES
时,它首先进行"参数扩展"。它将$FILES
扩展到字符串"b.txt" "File with space in name.txt"
。然后发生分词。因此,使用默认IFS
,生成的字符串在空格,制表符或换行符上拆分/分隔。
为了防止单词拆分,$FILES
必须在双引号内,而不是$FILES
的值。
好吧,你可以这样做(不安全(:
ls -1 --quote-name *.txt |
while IFS= read -r file; do
eval file="$file"
ls -l "$file"
done
- 告诉ls输出换行符分隔列表
-1
- 逐行阅读列表
- 重新评估变量以删除带有
evil
的引号。我的意思是eval
- 我在循环中使用
ls -l "$file"
来检查"$file"
是否是有效的文件名。
由于ls
,这仍然不适用于所有文件名。带有不可读字符的文件名只是被我的ls
忽略,就像touch "c.txt"$'x01'
一样。带有嵌入式换行符的文件名会出现类似ls $'n'"c.txt"
.
这就是为什么建议忘记脚本中的ls
-ls
仅用于在终端中漂亮打印。在脚本中使用find
.
如果您的文件名中没有嵌入换行符,您可以:
find . -mindepth 1 -maxdepth 1 -name '*.txt' |
while IFS= read -r file; do
ls -l "$file"
done
如果您的文件名只是任何内容,请使用以 null 结尾的流:
find . -mindepth 1 -maxdepth 1 -name '*.txt' -print0 |
while IFS= read -r -d'' file; do
ls -l "$file"
done
许多 unix 实用程序(grep -z
、xargs -0
、cut -z
、sort -z
(都支持处理以零结尾的字符串/流,只是为了处理所有你可以拥有的奇怪文件名。
你可以试试下面的片段:
#!/bin/bash
while read -r file; do
echo "$file"
done < <(ls --quote-name *.txt)