如何在 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 都是相同的,并产生完全相同的结果。

一个实际的例子

这是一个实际的例子,说明循环如何对日常计算有用。 假设您有一组想要发送给朋友的度假照片。 您的照片文件非常大,导致它们太大而无法通过电子邮件发送,并且不方便上传到您的 照片共享服务。 您想要创建较小的 Web 版本的照片,但您有 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` 命令和 `convert` 命令。谢谢

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

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