变量 IFS 和带有循环的列表文件的不同结果

变量 IFS 和带有循环的列表文件的不同结果

我想获取当前目录及其子目录中的文件列表(我想使用单行脚本):

IFS=$(echo -en "\n\b");
for FILE in $(find -type f); do echo "$FILE"; done

通常,它会按预期工作,但最近,我的文件列表:

file_.doc
file_0.doc
file_[2006_02_25].doc
file_[2016_06_16].odt
file_[2016_06_16].pdf
file_[16-6-2006].doc
file_.pdf
file_ 4-4-2006.doc

输出是:

./file_.doc                                                                                                                                                 
./file_0.doc                                                                                                                                                
./file_0.doc
./file_[2016_06_16].odt
./file_[2016_06_16].pdf
./file_0.doc
./file_.pdf
./file_ 4-4-2006.doc

如果我将变量 IFS 更改为:

IFS=$(echo -en "\n");

那么输出将是(更正后的):

./file_.doc
./file_0.doc
./file_[2006_02_25].doc
./file_[2016_06_16].odt
./file_[2016_06_16].pdf
./file_[16-6-2006].doc
./file_.pdf
./file_ 4-4-2006.doc

我读过的'\b'必要的,并找到了一个解决方案使用printf, 代替echo

我的问题是:

1)你能解释一下是什么让这些输出不同吗?

2)使用上面的解决方案printf可以替代echo -en "\n\b"

答案1

命令替换的输出会受到分词的影响(您可以通过设置来处理IFS),文件名通配。其[abc]构造是像往常一样“匹配任何字符a, b, c”,并 [2006_02_25].doc匹配0.doc


在 Bash/ksh/zsh 中,可以使用双星来获取目录树中的所有文件(递归地,不仅仅是当前目录)。这应该找到与您的示例相同的文件:

shopt -s globstar      # in Bash
# set -o globstar      # in ksh
for file in **/* ; do
    [[ -f $file ]] || continue     # check it's a regular file, like find -type f
    ...
done

当然,find 功能强大,所以如果你有很多条件,使用它可能会更容易。如果这样做,set -f除了修复之外,您还应该禁用文件名通配IFS

set -f
IFS=$'\n'
for file in $(find -type f -some -other -conditions) ; do
    ...
done

或者使用while read带有进程替换的循环:

while IFS= read -r file ; do 
    ...
done < <(find -type f -some -other -conditions)

(上面与 类似find ... | while ...,但它绕过了管道最后一部分在子 shell 中执行的问题。)

这两个都假设文件名不包含换行符,因为它们用作find. (并且因为$(..)吃掉了最后的换行符。)

至少在 Bash 中,实际上有一种方法可以使文件名中的换行符也起作用。将分隔符设置read为空字符串可以有效地使用 NUL 字节作为分隔符。所以:

while IFS= read -d '' -r file ; do 
    ...
done < <(find -type f -some -other -conditions -print0)

尽管这开始变得令人讨厌,因为您确实需要所有这些选项才能read使其工作而不破坏输入。


至于IFS... 设置IFS=$(echo -en "\n");设置IFS为空字符串(因为命令替换会吃掉尾随换行符),导致分裂。在这种情况下,输出似乎是正确的,因为您一次性获得了所有输出find,而不是逐行获得。这也掩盖了文件名通配的问题,因为完整的多行字符串与任何文件名不匹配并且按原样传递。

如果您执行其他操作而不只是打印循环值,您就会看到差异。尝试添加一些分隔符:

IFS=$(echo -en "\n")       # same as IFS=
for FILE in $(find -type f); do echo "<$FILE>"; done

到目前为止,IFS除了最简单的标准 shell 之外,在任何其他 shell 中设置换行符的最简单方法是IFS=$'\n'.

答案2

不要这样做$( find ... )。它将调用文件名生成(通配符),并且某些文件名将被解释为与其他文件名匹配的通配符模式。例如,模式file_[2006_02_25].docfile_[16-6-2006].doc匹配file_0.doc,这就是出现此文件名而不是这两个模式的原因。

此外,您的循环将不会开始迭代,直到find命令替换中的命令生成了其所有路径名,这在一般情况下可能会占用相当多的内存,并且实际上并不是优雅的

相反,只需使用find(并且不要修改IFS):

find . type -f -print

如果您想对这些文件执行其他操作,那么您可以在以下位置执行此操作-exec

find . -type f -exec sh -c 'printf "Found the file %s\n" "$@"' sh {} +

如果你只想处理当前目录下的文件,你可以简单地

for name in *; do
    printf 'Found the name %s\n' "$name"
done

有关的:

相关内容