如何在 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 代表 file)。 然后定义您希望变量循环遍历的数据集。 在这种情况下,使用 * 通配符循环遍历当前目录中的所有文件(* 通配符匹配所有内容)。 然后用分号 (;) 结束这个介绍性子句。

$ 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` 命令和 `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.