如何在 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 语句之间。 例如,假设你想将每张处理过的照片直接复制到你的网络主机上的共享照片目录,并从你的本地系统删除照片文件

$ 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

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