遍历带空格的文件列表


201

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

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


Answers:


253

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

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

31
这非常干净。而且让我比结合for循环更改IFS感觉更好
Derrick

15
这将拆分一个包含\ n的文件路径。好吧,这些不应该在周围,但可以创建:touch "$(printf "foo\nbar")"
Ollie Saunders 2013年

4
为防止对输入进行任何解释(反斜杠,前导和尾随空格),请IFS= while read -r f改用。
mklement0 '16

2
答案显示find了while循环的更安全组合。
moi

5
似乎指出了明显的-exec循环,但几乎在所有简单的情况下,它都将比显式循环更干净:find . -iname "foo*" -exec echo "File found: {}" \;。另外,在很多情况下,您可以用替换最后一个\;+以在一个命令中放入很多文件。
naught101 '16

152

有几种可行的方法可以完成此任务。

如果您想紧贴原始版本,可以这样进行:

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)语法不熟悉,则应阅读有关进程替换的信息。这样做的好处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
}

是否保留了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"

1
接受你的答案:最完整和最有趣的-我不知道$IFS< <(cmd)语法。不过有一点仍不清楚我,为什么$$'\0'?非常感谢。
gregseth

2
+1,但您应该添加... while IFS= read以处理以空格开头或结尾的文件。
戈登·戴维森

1
流程替代解决方案有一个警告。如果循环内有任何提示(或正在以其他任何方式从STDIN读取),则输入内容将由您填充到循环中的内容填充。(也许应该将其添加到答案中?)
andsens 2013年

2
@uvsmtid:标记了此问题,bash因此使用bash特定功能使我感到安全。进程替换不能移植到其他shell(sh本身不可能收到如此重要的更新)。
sorpigal

2
IFS=$'\n'与组合使用for可防止行内单词拆分,但仍会使生成的行容易受到干扰,因此这种方法并不十分可靠(除非您也先关闭了干扰)。虽然read -d $'\0'可行,但它在某种程度上令人误解,因为它暗示您可以$'\0'用来创建NUL-您不能:\0ANSI C引号中的字符串有效终止该字符串,因此-d $'\0'实际上与相同-d ''
mklement0 '16

29

还有一个非常简单的解决方案:依靠bash通配

$ 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上进行了测试)。



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

请参阅man xargs


6

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

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

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


3

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

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

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

在测试目录中:

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

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

假设我们要给这些文件起更好的名字...

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

2

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

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

此处{}代表找到的文件,将其包装起来即可""使生成的shell命令处理文件名中的空格。

在许多情况下,您可以用替换最后一个\;(启动一个新命令)\+,这将在一个命令中放置多个文件(尽管不一定要一次全部存储,man find有关更多详细信息,请参阅)。


0

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

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

0

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

尽管我的问题一直存在于csh中,但我确信我提出的解决方案在这两者中都可以使用。问题在于shell对“ ls”返回的解释。我们可以通过简单地使用*通配符的shell扩展从问题中删除“ ls”,但是如果当前(或指定的文件夹)中没有文件,则会出现“ no match”错误-为了解决这个问题,我们只需扩展扩展以包含点文件,因此:* .*-自从文件开始,这将始终产生结果。和..将始终存在。因此,在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
}

希望这可以帮助!


0

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

目标是:

  • 在目录中递归选择/过滤文件名
  • 处理每个名称(路径中的任何空格...)
#!/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的直接分辨率,而不会更改问题或分散说明:-)
Vince B,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.