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

442

请问能否提供以下功能的代码: 假设有一个文件目录,其中所有文件都需要通过程序运行。该程序将输出结果到标准输出。我需要一个脚本,能够进入该目录,在每个文件上执行该命令,并将输出连接成一个大的输出文件。

例如,对于一个文件运行该命令:

$ cmd [option] [filename] > results.out

3
我想补充一下这个问题。可以使用xargs完成吗?例如, ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out - Ozair Kafray
3
дҪ еҸҜд»ҘдҪҝз”Ёlsе‘Ҫд»ӨпјҢдҪҶжҳҜжңҖеҘҪдёҚиҰҒз”Ёе®ғжқҘй©ұеҠЁxargsгҖӮеҰӮжһңcmdе‘Ҫд»ӨеҶҷеҫ—еҫҲеҘҪпјҢдҪ еҸҜд»ҘзӣҙжҺҘиҫ“е…Ҙcmd <йҖҡй…Қз¬Ұ>гҖӮ - tripleee
10个回答

613
下面的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
如果/dir/目录下没有任何文件,那么循环仍然会运行一次,并将$file的值设置为星号(*),这可能是不希望看到的结果。为了避免这种情况,可以在循环期间启用nullglob。在循环之前添加这行代码:shopt -s nullglob,并在循环之后添加这行代码:shopt -u nullglob #将nullglob恢复到默认状态 - Stew-au
如果循环内输出文件相同,将其重定向到循环外部 done >results.out 会更加高效(可能您可以覆盖而不是追加,就像我在这里假设的那样)。 - tripleee
但是,您如何确定要先执行哪个文件、第二个文件、第三个文件,因为您通常可以按任意顺序从各个文件运行几个命令。顺序很重要。 - indianwebdevil
3
使用此命令处理大量文件时请小心,建议使用find -exec代替。 - kolisko
2
使用此命令处理大量目录中的文件时要小心。建议使用find -exec代替。为什么呢? - That Brazilian Guy
显示剩余3条评论

259

这个怎么样:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1参数防止find递归进入任何子目录。(如果你希望处理此类嵌套目录,可以省略此参数。)
  • -type -f指定只处理普通文件。
  • -exec cmd option {}告诉find对于找到的每个文件,使用指定的option运行cmd命令,并将文件名替换为{}
  • \;表示命令的结束。
  • 最后,所有单独的cmd执行的输出都被重定向到results.out

但是,如果您在意文件被处理的顺序,最好编写循环。我认为find按照inode顺序处理文件(尽管我可能错了),这可能不是您想要的。


2
这是处理文件的正确方式。使用for循环容易出错,原因有很多。另外,排序可以通过使用其他命令,如statsort来完成,当然这取决于排序的标准是什么。 - tuxdna
2
如果我想运行两个命令,我应该如何在“-exec”选项后链接它们?我需要将它们包装在单引号中吗? - frei
“find”总是最好的选择,因为您可以使用选项“-name”按文件名模式进行过滤,并且可以在单个命令中完成。 - João Pimentel Ferreira
8
回答您的问题的答案在这里:https://dev59.com/pG435IYBdhLWcg3w9E9Y#6043896,基本上只需要添加`-exec`选项:`find . -name "*.txt" -exec echo {} ; -exec grep banana {} ;` - João Pimentel Ferreira
3
你如何将文件名作为选项进行引用? - Toskan
显示剩余2条评论

128

我正在使用树莓派的命令行完成这个操作,方法是运行:

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

6
虽然这个答案可能是在生产环境下做这件事的"正确"方式,但对于日常使用的方便性而言,这个一行代码更胜一筹! - rinogo
如果想将修改后的文件名作为参数(例如用于输出文件的名称),可以在 $i 部分之后添加任何内容,这样就会得到一个新字符串。假设有一个虚构的命令 ppp -i raw.txt -o processed.txt,则可以使用以下命令:for i in *; do ppp -i "$i" -o "$i changed"; done。这将对每个文件执行 ppp 命令,并且每次执行的结果文件的名称都将类似于输入文件的名称,末尾加上 " changed"。 - Aleksandar

21

你可以使用xarg

ls | xargs -L 1 -d '\n' your-desired-command 
  • -L 1 表示一次只处理一个项目

  • -d '\n' 根据换行符(\n)将 ls 命令的输出分割成若干行。


2
使用xargs很好,因为如果添加-P 8标志(最多同时运行8个进程),它允许您并行运行所需的命令。 - Nick Crews
2
对于 macOS,-d 选项不可用。您可以先通过 brew install findutils 安装 findutils,然后使用 gxargs 替代 xargs 来解决此问题。 - Wit

18
被接受/获得高票的答案很好,但它们缺少一些细节。本帖子介绍了如何更好地处理 shell 路径名扩展(glob)失败,文件名包含嵌入式换行符/-符号以及将命令输出重定向移出 for 循环并将结果写入文件的情况。运行 shell glob 扩展使用 * 时,如果目录中没有文件,则存在扩展失败的可能性,并且未扩展的 glob 字符串将传递给要运行的命令,这可能会产生不良影响。bash shell 通过使用 nullglob 提供了一个扩展的 shell 选项来解决这个问题。因此,在包含您的文件的目录中,循环基本上变为以下形式。
 shopt -s nullglob

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

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

或者以符合POSIX标准的方式(nullglob是特定于bash的)

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

这让您在表达式失败一次并且条件[ -f "$file" ]检查未展开的字符串./*是否是该目录中的有效文件名时进入循环。因此,在此条件失败时,使用continue我们回到不会连续运行的for循环。

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

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


正确双引用文件名可以解决名称包含Glob字符或空格的情况。但*nix文件名也可以包含其中的换行符。因此,我们使用唯一一个不能成为有效文件名的字符来限定文件名——空字节(\0)。

由于bash在内部使用C样式字符串,在其中空字节用于指示字符串的结尾,因此它是正确的候选项。

所以使用shell的printf选项来使用read命令的-d选项以该NULL字节分隔文件,我们可以执行以下操作

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

nullglobprintf 被包裹在 (..) 中,这意味着它们基本上在子 shell(child shell)中运行,因为在命令退出后避免 nullglob 选项对父 shell 产生影响。 read 命令的 -d '' 选项不符合 POSIX 标准,所以需要使用 bash shell 执行。可以使用 find 命令来完成此操作。

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

对于不支持-print0选项的find实现(除了GNU和FreeBSD的实现),可以使用printf命令模拟。

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
检查文件是否存在,如果在不存在的目录中搜索,则$file包含正则表达式字符串“/invald_dir/*”,而不是有效的文件名。 - cdalxndr

6

有时候完成任务的一个快速且简单的方法是:

find directory/ | xargs  Command 

例如,要在当前目录中查找所有文件中的行数,您可以执行以下操作:
find . | xargs wc -l

8
@Hubert 为什么你的文件名中有换行符?! - musicin3d
3
这不是一个“为什么”的问题,而是一个正确性的问题 - 文件名不必包含可打印字符,甚至不必是有效的UTF-8序列。此外,“换行符”在不同编码中的定义也不同,有些编码 ♀ 是另一种编码的换行符。请看代码页437。 - Hubert Kario
3
你确定吗?这个方法99.9%的时间都有效,而且他确实说了“快速而简单”。 - Edoardo
1
我不喜欢“快速而肮脏”(又称“破碎的”)Bash脚本。迟早会出现像著名的“移动〜/ .local / share / steam。运行steam。它删除了用户拥有的系统上的所有内容。”这样的错误报告。 - reducing activity
这也无法处理文件名中带有空格的文件。 - Shamas S

1

如果文件名中包含换行符,则此方法将无法正常工作。 - Hubert Kario
2
@HubertKario 你可能想要了解更多关于find命令中的-print0xargs命令中的-0,它们使用空字符而不是任何空格(包括换行符)。 - tuxdna
是的,使用“-print0”是有帮助的,但整个管道需要使用类似这样的东西,“sort”不够。 - Hubert Kario

1

我需要将一个目录中的所有 .md 文件复制到另一个目录中,以下是我的操作步骤。

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

这段代码很难读懂,所以我们来逐步分解它。

首先进入包含文件的目录,

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

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

rm -r ../docs/"$i" 删除由 mkdir -p 创建的额外文件夹。

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

echo "$i -> ../docs/$i" 回显您的操作

; done 生活得幸福快乐


注意:要使 ** 生效,需要设置 globstar shell 选项:shopt -s globstar - Hubert Kario

1

Maxdepth

我发现它与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

-1

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

sh /dir/* > ./result.txt

3
你是否正确理解了问题?这只是尝试通过Shell运行目录中的每个文件,就好像它们是脚本一样。 - rdas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接