如何在 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

回复 by howtopam

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

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

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

回复 by 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

回复 by carltm

Creative Commons License本作品已获得 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.