如何在 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极客、自由文化倡导者、独立多媒体艺术家和龙与地下城爱好者。他曾在电影和计算行业工作,通常同时进行。

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.