对目录中的所有文件执行命令

Execute command on all files in a directory

提问人:themaestro 提问时间:5/10/2012 最后编辑:codeforesterthemaestro 更新时间:7/20/2022 访问量:555201

问:

有人可以提供代码来执行以下操作: 假设有一个文件目录,所有这些文件都需要通过程序运行。程序将结果输出为标准输出。我需要一个脚本,该脚本将进入一个目录,对每个文件执行命令,并将输出压缩到一个大的输出文件中。

例如,要对 1 个文件运行命令:

$ cmd [option] [filename] > results.out
Bash 脚本

评论

3赞 Ozair Kafray 5/10/2012
我想补充一下这个问题。可以使用 xargs 完成吗?例如,ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
3赞 tripleee 10/31/2017
它可以,但您可能不想使用 ls 来驱动 .如果写得很称职,也许你可以简单地做.xargscmdcmd <wildcard>

答:

260赞 Jim Lewis 5/10/2012 #1

这个怎么样:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1参数阻止 find 以递归方式下降到 任何子目录。(如果您希望处理此类嵌套目录,可以省略此项。
  • -type -f指定仅处理纯文件。
  • -exec cmd option {}告诉它使用找到的每个文件的指定运行,并将文件名替换为cmdoption{}
  • \;表示命令的结束。
  • 最后,所有单独执行的输出被重定向到cmdresults.out

但是,如果您关心文件的处理顺序,则可以 写一个循环可能会更好。我认为处理文件 按 inode 顺序(尽管我可能错了),这可能不是什么 你想要。find

评论

2赞 tuxdna 12/25/2013
这是处理文件的正确方法。由于多种原因,使用 for 循环容易出错。此外,还可以使用其他命令(例如 和 )进行排序,这当然取决于排序条件。statsort
2赞 frei 11/20/2017
如果我想运行两个命令,如何在选项之后链接它们?我必须用单引号或其他东西包装它们吗?-exec
0赞 João Pimentel Ferreira 12/8/2017
find始终是最佳选项,因为您可以使用选项按文件名模式进行过滤,并且可以在单个命令中完成。-name
8赞 João Pimentel Ferreira 12/8/2017
@frei您的问题的答案在这里:stackoverflow.com/a/6043896/1243247 但基本上只是添加选项:-execfind . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
3赞 Toskan 4/11/2019
如何引用文件名作为选项?
614赞 Andrew Logvinov 5/10/2012 #2

以下 bash 代码会将$file传递给命令,其中 $file 将表示 /dir 中的每个文件

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt

评论

37赞 Stew-au 9/19/2012
如果 中不存在任何文件,则循环仍以 '*' for 的值运行一次,这可能是不希望的。为避免这种情况,请在循环期间启用 nullglob。在循环之前添加此行,在循环后添加此行。/dir/$fileshopt -s nullglobshopt -u nullglob #revert nullglob back to it's normal default state
0赞 tripleee 10/31/2017
如果输出文件在循环内是相同的,那么在循环外重定向会更有效(也许这样你就可以覆盖而不是追加,就像我在这里假设的那样)。done >results.out
0赞 Timothy Swan 11/13/2017
如何获得自定义命名的单个结果文件?
3赞 kolisko 2/28/2019
对于 dir 中的大量文件,请使用此命令时要小心。请改用 find -exec。
2赞 That Brazilian Guy 10/17/2020
“对于目录中的大量文件,请使用此命令时要小心。请改用 find -exec”。但是为什么?
1赞 tripleee 12/13/2023
@Timo 此页面上的解决方案通常可以很好地做到这一点。xargs
1赞 tuxdna 12/25/2013 #3

根据 @Jim Lewis 的方法:

这是一个快速的解决方案,使用并按修改日期对文件进行排序:find

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

有关排序,请参阅:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time

评论

0赞 Hubert Kario 11/12/2018
如果文件的名称中有换行符,这将不起作用
2赞 tuxdna 11/15/2018
@HubertKario 您可能想阅读有关 for 和 for 的更多信息,请使用 null 字符而不是任何空格(包括换行符)。-print0find-0xargs
0赞 Hubert Kario 11/16/2018
是的,使用是有帮助的,但整个管道需要使用这样的东西,而不是-print0sort
128赞 robgraves 5/20/2016 #4

我正在从命令行在我的 Raspberry Pi 上执行此操作,方法是运行:

for i in *; do cmd "$i"; done

评论

6赞 rinogo 4/9/2021
虽然这个答案可能是在生产环境中执行此操作的“正确”方法,但为了日常使用方便,这种单行方法胜出!
0赞 Aleksandar 1/9/2023
如果想使用修改后的文件名作为参数(例如,输出文件的名称),您可以在部件之后添加任何内容,并且您将有一个新字符串。虚构命令的示例如下: - 这将对每个文件执行命令,并且每次执行的结果文件将像输入文件一样命名,并在末尾添加“changed”。$ippp -i raw.txt -o processed.txtfor i in *; do ppp -i "$i" -o "$i changed"; doneppp
0赞 tripleee 12/13/2023
你要避免讨厌的极端情况,即扩展为一个看起来像命令选项的字符串(如果你的文件名称以破折号开头,它会这样做;这并不常见,但并不像你希望的那样罕见)。for i in ./**
1赞 Eric Wooley 11/2/2017 #5

我需要将所有 .md 文件从一个目录复制到另一个目录,所以这就是我所做的。

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

这很难读,所以让我们分解一下。

第一张 CD 进入包含您的文件的目录,

for i in **/*.md;对于模式中的每个文件

mkdir -p ../docs/"$i"将该目录设在包含文件的文件夹之外的 docs 文件夹中。这将创建一个与该文件同名的额外文件夹。

rm -r ../docs/"$i"删除由于以下原因而创建的额外文件夹mkdir -p

cp "$i" "../docs/$i"复制实际文件

echo "$i -> ../docs/$i"回声你做了什么

; done从此过上幸福的生活

评论

0赞 Hubert Kario 11/12/2018
注意:要工作,需要设置shell选项:**globstarshopt -s globstar
6赞 Rahul 2/10/2018 #6

有时完成工作的一种快速而肮脏的方法是:

find directory/ | xargs  Command 

例如,要查找当前目录中所有文件中的行数,可以执行以下操作:

find . | xargs wc -l

评论

8赞 musicin3d 12/2/2018
@Hubert 为什么你的文件名中有换行符?!
4赞 Hubert Kario 12/7/2018
这不是“为什么”的问题,而是正确性的问题——文件名不必包含可打印字符,甚至不必是有效的 UTF-8 序列。此外,换行符在很大程度上依赖于编码,一种编码是另一种编码♀的换行符。请参阅代码页 437
3赞 Edoardo 1/24/2019
来吧,真的吗?这确实在 99.9% 的情况下有效,他确实说过“又快又脏”
1赞 reducing activity 1/27/2019
我不喜欢“快速而肮脏”(又名“破碎”)的 Bash 脚本。它迟早会以著名的“感动.跑了蒸汽。它删除了用户拥有的系统上的所有内容。~/.local/share/steam
0赞 Shamas S 9/12/2019
这也不适用于名称中包含空格的文件。
19赞 Inian 4/16/2019 #7

被接受/高票的答案很棒,但它们缺少一些细节。这篇文章介绍了如何更好地处理 shell 路径名扩展 (glob) 失败、文件名包含嵌入的换行符/破折号以及在将结果写入文件时将命令输出重定向移出 for 循环的情况。

使用 运行 shell glob 扩展时,如果目录中没有文件,并且未扩展的 glob 字符串将传递给要运行的命令,则扩展可能会失败,这可能会产生不良结果。shell 为此提供了一个扩展的 shell 选项,使用 。因此,在包含文件的目录中,循环基本上如下所示*bashnullglob

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

这样,当表达式不返回任何文件(如果目录为空)时,可以安全地退出 for 循环./*

或以符合 POSIX 的方式(特定)nullglobbash

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

这样一来,当表达式失败一次时,您可以进入循环,条件检查未展开的字符串是否是该目录中的有效文件名,但事实并非如此。因此,在这种情况下失败,使用我们恢复到随后不会运行的循环。[ -f "$file" ]./*continuefor

还要注意 just 在传递文件名参数之前的用法。这是必需的,因为如前所述,shell 文件名可以包含文件名中任何位置的破折号。一些 shell 命令会解释这一点,并在名称正确引用时将它们视为命令选项,并执行命令,认为是否提供了标志。--

在这种情况下,该信号表示命令行选项的结束,这意味着该命令不应将超出此点的任何字符串解析为命令标志,而只能解析为文件名。--


双引号文件名可以正确解决名称包含全形字符或空格的情况。但是 *nix 文件名也可以包含换行符。因此,我们使用唯一不能成为有效文件名一部分的字符 - 空字节 () 来限制文件名。由于内部使用样式字符串,其中 null 字节用于指示字符串的末尾,因此它是正确的候选者。\0bashC

因此,使用 shell 选项使用命令选项用这个 NULL 字节分隔文件,我们可以在下面执行以下操作printf-dread

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

和 the 被包裹起来,这意味着它们基本上在子 shell(子 shell)中运行,因为一旦命令退出,为了避免在父 shell 上反射的选项。命令选项不符合 POSIX,因此需要一个 shell 来完成此操作。使用命令,这可以按以下方式完成nullglobprintf(..)nullglob-d ''readbashfind

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

对于不支持的实现(除了 GNU 和 FreeBSD 实现),可以使用以下方法进行模拟find-print0printf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

另一个重要的修复是将重定向移出 for 循环,以减少大量文件 I/O。在循环中使用时,shell 必须对 for 循环的每次迭代执行两次系统调用,一次用于打开,一次用于关闭与文件关联的文件描述符。这将成为运行大型迭代的性能瓶颈。建议将其移出循环。

使用此修复程序扩展上述代码,您可以执行

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

这基本上会将文件输入的每次迭代的命令内容放入 stdout,当循环结束时,打开目标文件一次以写入 stdout 的内容并保存它。相同的等效版本将是find

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out

评论

1赞 cdalxndr 2/20/2020
+1 用于检查文件是否存在。如果在不存在的目录中搜索,$file包含的正则表达式字符串“/invald_dir/*”不是有效的文件名。
-2赞 yovie 4/16/2019 #8

我认为简单的解决方案是:

sh /dir/* > ./result.txt

评论

3赞 rdas 4/16/2019
你正确理解了这个问题吗?这只会尝试通过 shell 运行目录中的每个文件 - 就好像它是一个脚本一样。
1赞 eQ19 6/11/2019 #9

最大深度

我发现它与 Jim Lewis 的答案配合得很好,只需添加如下内容:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

排序顺序

如果要按排序顺序执行,请按如下方式修改:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

仅举例来说,这将按以下顺序执行:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

无限深度

如果你想在一定条件下无限深度地执行,你可以使用这个:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

然后放在子目录中的每个文件之上,如下所示:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

在父文件正文中的某处:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
21赞 Al Mamun 11/26/2019 #10

您可以使用:xarg

ls | xargs -L 1 -d '\n' your-desired-command 
  • -L 1原因一次传递 1 个项目

  • -d '\n'拆分 基于换行的输出。ls

评论

2赞 Nick Crews 8/23/2022
使用 xargs 很好,因为如果您添加标志(最多同时 8 个进程),它允许您并行运行 your-desired-command。-P 8
2赞 Wit 11/16/2022
对于 macOS,该选项不可用。您可以先修复它,然后使用而不是-dbrew install findutilsgxargsxargs