如何在 Bash 中编写循环

使用 for 循环和 find 命令在多个文件上自动执行一组操作。
246 位读者喜欢这篇文章。
bash logo on green background

Opensource.com

人们想要学习 Unix shell 的一个常见原因是解锁批量处理的强大功能。如果您想对多个文件执行一组操作,其中一种方法是构建一个命令来迭代这些文件。在编程术语中,这称为执行控制,其中最常见的例子之一是 for 循环。

for 循环是一个详细说明您希望计算机对您指定的每个数据对象(例如文件)执行哪些操作的配方。

经典的 for 循环

一个简单的循环尝试是分析文件集合的循环。 这本身可能不是一个有用的循环,但它是一种安全的方式,向您自己证明您有能力单独处理目录中的每个文件。 首先,创建一个简单的测试环境,方法是创建一个目录并将一些文件的副本放入其中。 最初任何文件都可以,但后面的示例需要图形文件(例如 JPEG、PNG 或类似文件)。 您可以使用文件管理器或在终端中创建文件夹并将文件复制到其中

$ mkdir example
	$ cp ~/Pictures/vacation/*.{png,jpg} example

将目录更改为您的新文件夹,然后列出其中的文件以确认您的测试环境符合您的预期

$ cd example
$ ls -1
cat.jpg
design_maori.png
otago.jpg
waterfall.png

循环中单独循环遍历每个文件的语法是:创建一个变量(例如,f 代表文件)。 然后定义您希望变量循环遍历的数据集。 在本例中,使用 * 通配符(* 通配符匹配所有内容)循环遍历当前目录中的所有文件。 然后用分号 (;) 终止此介绍性子句。

$ for f in * ;

根据您的偏好,您可以选择在此处按 Return 键。 Shell 在语法完成之前不会尝试执行循环。

接下来,定义您希望在循环的每次迭代中发生什么。 为了简单起见,使用 file 命令获取有关每个文件的一些数据,这些文件由 f 变量表示(但前面加上 $ 以告诉 shell 将变量的值替换为变量当前包含的任何内容)

do file $f ;

用另一个分号终止子句并关闭循环

done

Return 键开始 shell 循环遍历当前目录中的所有内容for 循环将每个文件逐个分配给变量 f 并运行您的命令

$ for f in * ; do
	> file $f ;
	> done
	cat.jpg: JPEG image data, EXIF standard 2.2
	design_maori.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced
	otago.jpg: JPEG image data, EXIF standard 2.2
	waterfall.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced

您也可以这样写

$ for f in *; do file $f; done
	cat.jpg: JPEG image data, EXIF standard 2.2
	design_maori.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced
	otago.jpg: JPEG image data, EXIF standard 2.2
	waterfall.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced

多行和单行格式对您的 shell 来说是相同的,并产生完全相同的结果。

一个实际的例子

这是一个关于循环如何对日常计算有用的实际示例。 假设您有一系列想要发送给朋友的度假照片。 您的照片文件很大,使其太大而无法通过电子邮件发送,并且不方便上传到您的 照片共享服务。 您想要创建照片的较小网络版本,但您有 100 张照片,并且不想花时间一张一张地缩小每张照片。

首先,在 Linux、BSD 或 Mac 上使用您的软件包管理器安装 ImageMagick 命令。 例如,在 Fedora 和 RHEL 上

$ sudo dnf install ImageMagick

在 Ubuntu 或 Debian 上

$ sudo apt install ImageMagick

在 BSD 上,使用 portspkgsrc。 在 Mac 上,使用 HomebrewMacPorts

安装 ImageMagick 后,您将拥有一组新的命令来操作照片。

为您即将创建的文件创建一个目标目录

$ mkdir tmp

要将每张照片缩小到其原始大小的 33%,请尝试此循环

$ for f in * ; do convert $f -scale 33% tmp/$f ; done

然后查看 tmp 文件夹以查看您缩放的照片。

您可以在循环中使用任意数量的命令,因此如果您需要对一批文件执行复杂的操作,您可以将整个工作流程放在 for 循环的 dodone 语句之间。 例如,假设您想要将每张处理过的照片直接复制到您的 Web 主机上的共享照片目录,并从本地系统中删除照片文件

$ for f in * ; do 
    convert $f -scale 33% tmp/$f
    scp -i seth_web tmp/$f seth@example.com:~/public_html 
    trash tmp/$f ;
  done

对于 for 循环处理的每个文件,您的计算机都会自动运行三个命令。 这意味着,如果您以这种方式处理 10 张照片,您将节省 30 个命令,并且可能至少节省相同的时间。

限制您的循环

循环不一定总是要查看每个文件。 您可能只想处理示例目录中的 JPEG 文件

$ for f in *.jpg ; do convert $f -scale 33% tmp/$f ; done
$ ls -m tmp
cat.jpg, otago.jpg

或者,您可能需要重复执行某个操作特定的次数,而不是处理文件。 for 循环的变量由您提供给它的任何数据定义,因此您可以创建一个循环来迭代数字而不是文件

$ for n in {0..4}; do echo $n ; done
0
1
2
3
4

更多循环

现在您已经了解了足够多的知识来创建自己的循环。 在您对循环感到满意之前,请在您要处理的文件的副本上使用它们,并尽可能使用带有内置保护措施的命令,以防止您损坏数据并犯下不可弥补的错误,例如意外地将整个文件目录重命名为相同的名称,每个文件相互覆盖。

有关高级 for 循环主题,请继续阅读。

并非所有 shell 都是 Bash

for 关键字内置于 Bash shell 中。 许多类似的 shell 使用相同的关键字和语法,但有些 shell(例如 tcsh)使用不同的关键字,例如 foreach

在 tcsh 中,语法在精神上相似,但比 Bash 更严格。 在以下代码示例中,请勿在第 2 行和第 3 行中键入字符串 foreach?。 这是一个辅助提示,提醒您仍在构建循环的过程中。

$ foreach f (*)
foreach? file $f
foreach? end
cat.jpg: JPEG image data, EXIF standard 2.2
design_maori.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced
otago.jpg: JPEG image data, EXIF standard 2.2
waterfall.png: PNG image data, 4608 x 2592, 8-bit/color RGB, non-interlaced

在 tcsh 中,foreachend 都必须单独出现在不同的行上,因此您无法像使用 Bash 和类似 shell 那样在一行上创建 for 循环。

带有 find 命令的 For 循环

理论上,您可以找到一个不提供 for 循环功能的 shell,或者您可能只是更喜欢使用具有附加功能的不同命令。

find 命令是实现 for 循环功能的另一种方法,因为它提供了多种方法来定义要包含在循环中的文件的范围,以及用于 并行 处理的选项。

find 命令旨在帮助您在硬盘驱动器上查找文件。 它的语法很简单:您提供要搜索的位置的路径,find 会查找所有文件和目录

$ find . 
.
./cat.jpg
./design_maori.png
./otago.jpg
./waterfall.png

您可以通过添加名称的某一部分来过滤搜索结果

$ find . -name "*jpg"
./cat.jpg
./otago.jpg

关于 find 的伟大之处在于,它找到的每个文件都可以使用 -exec 标志馈送到循环中。 例如,要仅缩小示例目录中的 PNG 照片

$ find . -name "*png" -exec convert {} -scale 33% tmp/{} \;
$ ls -m tmp
design_maori.png, waterfall.png

-exec 子句中,括号字符 {} 代表 find 正在处理的任何项目(换句话说,任何以 PNG 结尾的文件,一次找到一个)。 -exec 子句必须以分号结尾,但 Bash 通常会尝试自己使用分号。 您使用反斜杠 (\;) “转义”分号,以便 find 知道将该分号视为其终止字符。

find 命令非常擅长它所做的事情,有时可能会太好。 例如,如果您重新使用它来查找另一个照片过程的 PNG 文件,您会收到一些错误

$ find . -name "*png" -exec convert {} -flip -flop tmp/{} \;	
convert: unable to open image `tmp/./tmp/design_maori.png':
No such file or directory @ error/blob.c/OpenBlob/2643.
...

似乎 find 已经找到了所有 PNG 文件——不仅是当前目录 (.) 中的文件,还有您之前处理并放置在 tmp 子目录中的文件。 在某些情况下,您可能希望 find 搜索当前目录以及其中的所有其他目录(以及那些中的所有目录)。 它可以是一个强大的递归处理工具,尤其是在复杂的文件结构中(例如包含专辑目录的音乐艺术家目录,专辑目录中充满了音乐文件),但您可以使用 -maxdepth 选项来限制这一点。

要在当前目录中仅查找 PNG 文件(不包括子目录)

$ find . -maxdepth 1 -name "*png"

要在当前目录以及其他级别的子目录中查找和处理文件,请将最大深度增加 1

$ find . -maxdepth 2 -name "*png"

其默认设置是下降到所有子目录。

为了乐趣和利益而循环

您使用循环的次数越多,节省的时间和精力就越多,并且您可以处理的任务就越大。 您只是一个用户,但是通过精心设计的循环,您可以让您的计算机完成繁重的工作。

您可以并且应该像对待任何其他命令一样对待循环,将其放在手边,以便在您需要对多个文件重复一两个操作时使用。 但是,它也是通往严肃编程的合法途径,因此,如果您必须在任意数量的文件上完成复杂的任务,请花一点时间来计划您的工作流程。 如果您可以在一个文件上实现您的目标,那么将该可重复的过程包装在 for 循环中相对简单,并且唯一需要的“编程”是了解变量如何工作以及足够的组织来将未处理的文件与已处理的文件分开。 稍加练习,您就可以从 Linux 用户转变为知道如何编写循环的 Linux 用户,所以走出去,让您的计算机为您工作吧!

接下来阅读什么
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。 他曾在电影和计算行业工作,而且常常同时进行。

10 条评论

很棒

很棒的小教程。 人们喜欢少量有价值的信息。 在正确的编码场景中使用时,此“for 循环”语句对于提高工作效率非常有帮助。 感谢 Gonca Sousa 的课程。

我的错误和道歉 @Seth Kenlon。 您是作者,我在上面评论中感谢课程的声明中引用了 Gonca Sousa。 对不起 Seth Kenlon,也对 Gonca Sousa 的错误署名表示歉意。 howtopam

回复 作者 howtopam

很棒的文章 - 除了学习循环之外,很高兴了解 `file` 命令和 `covert`。 谢谢

ImageMagick 中的 ``convert`` 命令是我使用的次数远比我意识到的要多的东西之一。 我想如果我必须在 GUI 中一次打开我调整大小或从一种格式转换为另一种格式的每张图像,我就会放弃计算机。

感谢您的阅读,JJ,并感谢您的评论!

回复 作者 JJ

文章写得非常好。 ++ 给作者的点子和执行力。

很棒的文章! 我使用了您描述的循环类型。

我注意到您没有涵盖您有数百甚至
数千个文件要处理的情况,并且您希望它们按顺序运行而不是
并行运行。 以下命令可以轻松重写。

find . -name "*png" -exec convert {} -scale 33% tmp/{} \;

这是新版本。

find . -name "*png" |\
while IFS= read -r file; do
convert "$file" -scale 33% tmp/
done

很好的观点,也是一个很棒的话题。 我确实觉得 IFS 和解析 find 的输出以进行顺序处理对于一篇独立的文章来说已经足够复杂了。 我不想匆忙掠过分析 find 的输出的复杂性以及您需要将 IFS 设置为什么(或者 IFS 一开始是什么),以及如何设置 read 才能使输入合理。

尽管如此,这是一个单独文章的好主意。 我已经注意到了,但是如果您想写,我会随意将此 URL 滑给您...
https://open-source.net.cn/how-submit-article

回复 ,作者是 carltm

知识共享许可本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.