用空格遍历文件列表

我想迭代一个文件列表。 这个列表是find命令的结果,所以我想出了:

 getlist() { for f in $(find . -iname "foo*") do echo "File found: $f" # do something useful done } 

没关系,除非文件名中有空格:

 $ ls foo_bar_baz.txt foo bar baz.txt $ getlist File found: foo_bar_baz.txt File found: foo File found: bar File found: baz.txt 

我能做些什么来避免在空间上的分裂?

你可以用一个基于行的replace基于单词的迭代:

 find . -iname "foo*" | while read f do # ... loop body done 

有几个可行的方法来完成这一点。

如果你想紧贴原始版本,可以这样做:

 getlist() { IFS=$'\n' for file in $(find . -iname 'foo*') ; do printf 'File found: %s\n' "$file" done } 

这将仍然失败,如果文件名中有文字换行符,但空格不会打破它。

但是,与IFS混合是没有必要的。 这是我的首选方法:

 getlist() { while IFS= read -d $'\0' -r file ; do printf 'File found: %s\n' "$file" done < <(find . -iname 'foo*' -print0) } 

如果你发现< <(command)语法不熟悉,你应该阅读关于进程replace 。 这for file in $(find ...)中的文件的优点是具有空格,换行符和其他字符的文件被正确处理。 这是有效的,因为用-print0 find将会使用null (aka \0 )作为每个文件名的终结符,而不像换行符,null在文件名中不是合法的字符。

这几乎相当于版本的优势

 getlist() { find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do printf 'File found: %s\n' "$file" done } 

是否在while循环的主体中的任何variables赋值被保留。 也就是说,如果你像上面那样pipe道,那么这个时候的身体就在一个子壳里, while这个子壳可能并不是你想要的。

进程replace版本的优点是find ... -print0 | xargs -0 find ... -print0 | xargs -0是最小的:如果你需要的只是打印一行或者对文件执行一个单独的操作,那么xargs版本就没有问题,但是如果你需要执行多个步骤,循环版本会更容易。

编辑 :这是一个很好的testing脚本,所以你可以得到解决这个问题的不同尝试之间的区别的想法

 #!/usr/bin/env bash dir=/tmp/getlist.test/ mkdir -p "$dir" cd "$dir" touch 'file not starting foo' foo foobar barfoo 'foo with spaces'\ 'foo with'$'\n'newline 'foo with trailing whitespace ' # while with process substitution, null terminated, empty IFS getlist0() { while IFS= read -d $'\0' -r file ; do printf 'File found: '"'%s'"'\n' "$file" done < <(find . -iname 'foo*' -print0) } # while with process substitution, null terminated, default IFS getlist1() { while read -d $'\0' -r file ; do printf 'File found: '"'%s'"'\n' "$file" done < <(find . -iname 'foo*' -print0) } # pipe to while, newline terminated getlist2() { find . -iname 'foo*' | while read -r file ; do printf 'File found: '"'%s'"'\n' "$file" done } # pipe to while, null terminated getlist3() { find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do printf 'File found: '"'%s'"'\n' "$file" done } # for loop over subshell results, newline terminated, default IFS getlist4() { for file in "$(find . -iname 'foo*')" ; do printf 'File found: '"'%s'"'\n' "$file" done } # for loop over subshell results, newline terminated, newline IFS getlist5() { IFS=$'\n' for file in $(find . -iname 'foo*') ; do printf 'File found: '"'%s'"'\n' "$file" done } # see how they run for n in {0..5} ; do printf '\n\ngetlist%d:\n' $n eval getlist$n done rm -rf "$dir" 

还有一个非常简单的解决scheme:依靠bash globbing

 $ mkdir test $ cd test $ touch "stupid file1" $ touch "stupid file2" $ touch "stupid file 3" $ ls stupid file 3 stupid file1 stupid file2 $ for file in *; do echo "file: '${file}'"; done file: 'stupid file 3' file: 'stupid file1' file: 'stupid file2' 

请注意,我不确定这种行为是默认的行为,但我没有在我的shopt中看到任何特殊的设置,所以我会去说,它应该是“安全的”(在osx和Ubuntu上testing)。

 find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:" 
 find . -name "fo*" -print0 | xargs -0 ls -l 

看到man xargs

由于您没有使用find进行任何其他types的筛选,因此您可以使用以下bash 4.0:

 shopt -s globstar getlist() { for f in **/foo* do echo "File found: $f" # do something useful done } 

**/将匹配零个或多个目录,所以完整模式将匹配当前目录或任何子目录中的foo*

在某些情况下,如果您只需要复制或移动文件列表,则可以将该列表pipe理为awk。
重要的\"" "\"字段$0 (简而言之,您的文件,一个行列表=一个文件)。

 find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }' 

我真的很喜欢循环和数组迭代,所以我想我会添加这个答案的混合…

我也喜欢marchelbling愚蠢的文件的例子。 🙂

 $ mkdir test $ cd test $ touch "stupid file1" $ touch "stupid file2" $ touch "stupid file 3" 

在testing目录里面:

 readarray -t arr <<< "`ls -A1`" 

这会将每个文件列表行添加到名为arr的bash数组中,并删除任何尾随的换行符。

比方说,我们想给这些文件更好的名字…

 for i in ${!arr[@]} do newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/ */_/g'`; mv "${arr[$i]}" "$newname" done 

$ {!arr [@]}扩展为0 1 2,所以“$ {arr [$ i]}”是数组的 i 元素。 variables周围的引号对于保存空间很重要。

结果是三个重命名的文件:

 $ ls -1 smarter_file1 smarter_file2 smarter_file_3