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

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


当前回答

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

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

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"

其他回答

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

另一个解决工作的方法是……

目标是:

递归地选择/过滤目录中的文件名 处理每个名称(路径…中的任意空格)

#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}