遍历带有空格的文件列表

Posted

技术标签:

【中文标题】遍历带有空格的文件列表【英文标题】:Iterate over a list of files with spaces 【发布时间】:2011-10-25 17:32:51 【问题描述】:

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

如何避免空格分割?

【问题讨论】:

这基本上是When to wrap quotes around a shell variable?的具体子案例 【参考方案1】:

您可以将基于单词的迭代替换为基于行的迭代:

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

【讨论】:

这非常干净。并且让我感觉比结合 for 循环更改 IFS 更好 这将分割一个包含 \n 的文件路径。好的,这些不应该存在,但可以创建:touch "$(printf "foo\nbar")" 为了防止对输入的任何解释(反斜杠、前导和尾随空格),请改用IFS= while read -r f 这个answer 显示了find 和while 循环的更安全组合。 似乎指出了显而易见的问题,但在几乎所有简单的情况下,-exec 将比显式循环更干净:find . -iname "foo*" -exec echo "File found: " \;。另外,在许多情况下,您可以将最后一个 \; 替换为 + 以将大量文件放在一个命令中。【参考方案2】:

有几种可行的方法来实现这一点。

如果您想紧贴原来的版本,可以这样做:

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)

如果您对&lt; &lt;(command) 语法不熟悉,您应该阅读process substitution。与for file in $(find ...) 相比,它的优势在于可以正确处理带有空格、换行符和其他字符的文件。这是因为find-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 的主体就在一个子shell 中,这可能不是你想要的。

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"

【讨论】:

接受了你的回答:最完整和最有趣的——我不知道$IFS&lt; &lt;(cmd) 语法。还有一件事对我来说仍然很模糊,为什么$'\0' 中的$?非常感谢。 +1,但您应该添加 ...while IFS= read... 来处理以空格开头或结尾的文件。 进程替换解决方案有一个警告。如果您在循环中有任何提示(或正在以任何其他方式从 STDIN 读取),则输入将由您输入循环的内容填充。 (也许这应该添加到答案中?) @uvsmtid:这个问题被标记为bash,所以我觉得使用 bash 特定的功能很安全。进程替换不能移植到其他 shell(sh 本身不太可能收到如此重要的更新)。 IFS=$'\n'for 结合可防止行内部分词,但仍会使生成的行受到通配符的影响,因此这种方法并不完全可靠(除非您也关闭通配符第一的)。虽然read -d $'\0' 有效,但它有点误导,因为它建议您可以使用$'\0' 创建NUL - 您不能:ANSI C-quoted string 中的\0 有效地终止字符串,因此-d $'\0'-d '' 实际上相同。【参考方案3】:

还有一个非常简单的解决方案:依赖 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 上测试)。

【讨论】:

【参考方案4】:
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

【讨论】:

附带说明,这仅在您要执行命令时才有效。内置的 shell 不会以这种方式工作。【参考方案5】:
find . -name "fo*" -print0 | xargs -0 ls -l

man xargs

【讨论】:

【参考方案6】:

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

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

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

【讨论】:

【参考方案7】:

find 有一个 -exec 参数,它循环查找结果并执行任意命令。例如:

find . -iname "foo*" -exec echo "File found: " \;

这里 代表找到的文件,将其包装在"" 中允许生成的shell 命令处理文件名中的空格。

在许多情况下,您可以用\+ 替换最后一个\;(它启动一个新命令),这会将多个文件放在一个命令中(但不一定要同时放置所有文件,请参阅man find了解更多详情)。

【讨论】:

【参考方案8】:

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

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

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

测试目录内:

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

【讨论】:

【参考方案9】:

在某些情况下,如果您只需要复制或移动文件列表,您也可以将该列表通过管道传输到 awk。 重要的是 \"" "\" 字段周围的 $0 (简而言之,您的文件,一个行列表 = 一个文件)。

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

【讨论】:

【参考方案10】:

好的 - 我在 Stack Overflow 上的第一篇文章!

虽然我的问题一直在 csh 而不是 bash 我提出的解决方案,但我敢肯定,两者都适用。问题在于 shell 对“ls”返回的解释。我们可以通过简单地使用 * 通配符的 shell 扩展来从问题中删除“ls”——但是如果当前(或指定的文件夹)中没有文件,则会出现“不匹配”错误——为了解决这个问题,我们只需将扩展扩展为包含点文件:* .* - 这将始终产生结果,因为 files .和 .. 将永远存在。所以在 csh 中我们可以使用这个构造...

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

希望这会有所帮助!

【讨论】:

【参考方案11】:

工作的另一种解决方案...

目标是:

在目录中递归地选择/过滤文件名 处理每个名称(路径中的任何空格...)
#!/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


【讨论】:

感谢建设性的评论,但是:1-这是一个实际问题,2-外壳可能会随着时间的推移而发展......正如我所假设的那样; 3- 上面没有一个答案可以满足 pb 的直接分辨率而不改变问题或论文:-)

以上是关于遍历带有空格的文件列表的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Python 中将列表转换为带有空格的字符串?

QTextBrowser 显示带有多余空格的嵌套 HTML 列表

将列表添加到 Excel 列

python利用列表文件遍历

递归遍历带有列表的嵌套字典,并替换匹配的值

遍历文件时,如何将每个文件名附加到列表中? [复制]