我想遍历一个文件列表。这个列表是一个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

我能做些什么来避免空格分割?


当前回答

好的,这是我在Stack Overflow上的第一篇文章!

Though my problems with this have always been in csh not bash the solution I present will, I'm sure, work in both. The issue is with the shell's interpretation of the "ls" returns. We can remove "ls" from the problem by simply using the shell expansion of the * wildcard - but this gives a "no match" error if there are no files in the current (or specified folder) - to get around this we simply extend the expansion to include dot-files thus: * .* - this will always yield results since the files . and .. will always be present. So in csh we can use this construct ...

foreach file (* .*)
   echo $file
end

如果你想过滤掉标准的点文件,那很简单…

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

在这个线程的第一篇文章中的代码将这样写:-

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

希望这能有所帮助!

其他回答

find . -name "fo*" -print0 | xargs -0 ls -l

见xargs先生。

你可以用基于行的迭代替换基于单词的迭代:

find . -iname "foo*" | while read f
do
    # ... loop body
done
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

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

我也喜欢marchelbling愚蠢的文件示例。:)

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

在test目录中:

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个元素。变量周围的引号对于保留空格很重要。

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

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

有几种可行的方法可以做到这一点。

如果你想要坚持你的原始版本,可以这样做:

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)
}

如果您不熟悉< <(命令)语法,您应该阅读有关进程替换的内容。与$(find…)中的for file相比,这样做的优点是可以正确处理带有空格、换行符和其他字符的文件。这是因为find with -print0将使用null(又名\0)作为每个文件名的结束符,并且与换行符不同,null不是文件名中的合法字符。

这是相对于几乎相同版本的优势

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

while循环体中的任何变量赋值都被保留。也就是说,如果你像上面那样管道到while,那么while的主体就在一个子壳中,这可能不是你想要的。

进程替换版本的优势在于…-print0 | xargs -0是最小值:如果你只需要打印一行或对文件执行一个操作,那么xargs版本是可以的,但如果你需要执行多个步骤,循环版本更容易。

编辑:这里有一个很好的测试脚本,这样您就可以了解解决这个问题的不同尝试之间的差异

#!/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"