使用空格迭代文件列表

gre*_*eth 185 linux bash shell

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

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

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

$ 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
Run Code Online (Sandbox Code Playgroud)

我该怎么做才能避免空格分裂?

mar*_*ton 241

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

find . -iname "foo*" | while read f
do
    # ... loop body
done
Run Code Online (Sandbox Code Playgroud)

  • 这非常干净.并且让我感觉比连接for循环更改IFS更好 (29认同)
  • 这将拆分包含\n的单个文件路径.好吧,那些不应该出现但是可以创建它们:`touch"$(printf"foo \nbar")"` (14认同)
  • 要防止对输入(反斜杠,前导和尾随空格)的任何解释,请使用`IFS = while read -rf`. (3认同)
  • 似乎指出了显而易见的,但在几乎所有简单的情况下,`-exec`将比显式循环更清晰:`find.-iname"foo*" - exec echo"找到的文件:{}"\;`.另外,在许多情况下,您可以将最后一个`\;`替换为`+`以在一个命令中放入大量文件. (3认同)
  • 这个 [answer](http://stackoverflow.com/a/21663203/1116842) 展示了更安全的“find”和 while 循环组合。 (2认同)
  • 即使过了几年,这个答案也有一些主要的错误。(1) - `read f` 在将文件名分配给 `f` 之前从文件名中修剪尾随空格;为了避免这种情况,它应该是`IFS= read f`。(2) - `read f` 在文件名中使用反斜杠 -- 使用 `touch 'foo\bar'` 创建的文件将简单地将 `foobar` 分配给 `f`,而以反斜杠结尾的文件名*结尾*会导致下一行中的无关文件将附加到其名称并作为单行读取。 (2认同)
  • 小心。循环体中对 stdin 的任何读取都会消耗您的一些输入。例如,如果命令需要 y/N 确认,则可能会发生这种情况。(如果接受一行输入的命令没有抱怨,您甚至可能没有注意到!) (2认同)

Sor*_*gal 149

有几种可行的方法可以实现这一目标.

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

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

如果文件名中包含文字换行符,则仍然会失败,但空格不会破坏它.

但是,没有必要弄乱IFS.这是我首选的方法:

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

如果您发现< <(command)语法不熟悉,您应该阅读有关进程替换的内容.这样做的好处for file in $(find ...)是可以正确处理带有空格,换行符和其他字符的文件.这是有效的,因为findwith -print0将使用null(aka \0)作为每个文件名的终止符,并且与换行符不同,null不是文件名中的合法字符.

相比于几乎等同的版本,这样做的优点

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

是否保留了while循环体中的任何变量赋值.也就是说,如果你管道while如上,那么它的主体while是在子壳中,这可能不是你想要的.

流程替换版本的优势find ... -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"
Run Code Online (Sandbox Code Playgroud)

  • +1,但你应该添加...`IFS = read` ...来处理以空格开头或结尾的文件. (2认同)
  • @uvsmtid:这个问题被标记为“bash”,所以我觉得使用 bash 特定的功能是安全的。进程替换不可移植到其他 shell(sh 本身不太可能收到如此重要的更新)。 (2认同)
  • 将'IFS = $'\n'`与`for`结合起来可以防止行内部的字分裂,但仍会使得结果行受到全局处理,因此这种方法并不完全健壮(除非你先关闭globbing) .虽然`read -d $'\ 0'`有效,但它有点误导,因为它表明你可以使用'$'\ 0'`创建NULs - 你不能:在一个ANSI中的`\ 0` C-quoted string](http://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting)有效地_terminates_字符串,所以`-d $'\ 0'`实际上是相同的作为`-d''`. (2认同)

mar*_*ing 30

还有一个非常简单的解决方案:依赖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'
Run Code Online (Sandbox Code Playgroud)

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


Kar*_*ath 13

find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
Run Code Online (Sandbox Code Playgroud)

  • 作为附注,这仅在您想要执行命令时才有效.内置shell不会以这种方式工作. (6认同)

Tor*_*orp 11

find . -name "fo*" -print0 | xargs -0 ls -l
Run Code Online (Sandbox Code Playgroud)

man xargs.


che*_*ner 6

由于您没有使用任何其他类型的过滤find,因此从bash4.0开始可以使用以下内容:

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

**/将匹配零个或多个目录,因此完整的模式将匹配foo*在当前目录或任何子目录.