如何在Bash中编写循环

使用for循环和find命令自动对多个文件执行一组操作。

人们想要学习Unix shell的一个常见原因是释放批处理的功能。如果要对许多文件执行某些操作,一种方法是构造一个遍历这些文件的命令来实现。在编程术语中,这称为执行控制,最常见的示例之一是for循环。

for循环是一个配方,详细说明了您希望计算机对指定的每个数据对象(例如文件)执行什么操作。

1. 经典的循环

Linux终端适用于Linux的7大终端仿真器用于Linux中进行数据分析的10个命令行工具立即下载:SSH备忘单高级Linux命令备忘单Linux命令行教程一个简单的循环是分析文件集合的循环。这本身可能不是一个有用的循环,但它是一种安全的方法,可以向您证明自己有能力分别处理目录中的每个文件。首先,通过创建目录并将一些文件的某些副本放入其中来创建一个简单的测试环境。一开始的时候使用任何文件都可以,但是以后的示例需要图形文件(例如JPEG、PNG或类似文件)。您可以使用文件管理器或在终端中创建文件夹并将文件复制到其中:

$ mkdir example
$ cp ~/Pictures/vacation/*.{png,jpg} example

将目录更改为新文件夹,然后列出其中的文件以确认测试环境符合您的期望:

$ cd example
$ ls -1
cat.jpg
design_maori.png
otago.jpg
waterfall.png

在一个循环中逐个遍历每个文件的语法是:创建一个变量。然后定义您要变量循环通过的数据集。在这种情况下,请使用通配符循环浏览当前目录中的所有文件(通配符匹配所有内容)。然后以分号(;)终止此介绍性子句。

$ for f in * ;

根据您的喜好,您可以选择按此处返回。在语法上完成之前,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

多行和单行格式对于您的外壳都是相同的,并且产生完全相同的结果。

2. 一个实际的例子

这是一个循环如何对日常计算有用的实际示例。假设您有要发送给朋友的度假照片集。您的照片文件很大,太大而无法通过电子邮件发送,并且不便上传到您的照片共享服务。您想为照片创建较小的网络版本,但是您有100张照片,不想浪费时间一张一张地缩小每张照片。

首先,在Linux,BSD或Mac上使用包管理器安装ImageMagick命令。例如,在Fedora和RHEL上:

$ sudo dnf install ImageMagick

在Ubuntu或Debian上:

$ sudo apt install ImageMagick

在BSD上,使用端口或pkgsrc。在Mac上,使用Homebrew或MacPorts。

安装ImageMagick后,您将拥有一组用于对照片进行操作的新命令。

为您要创建的文件创建目标目录:

$ mkdir tmp

要将每张照片缩小到其原始大小的33%,请尝试以下循环:

$ for f in * ; do convert $f -scale 33% tmp/$f ; done

然后在tmp文件夹中查看缩放后的照片。

您可以在循环中使用任意数量的命令,因此,如果您需要对一批文件执行复杂的操作,则可以将整个工作流放在for循环的do和done语句之间。例如,假设您要将每张处理过的照片直接复制到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条命令,还会节省同样多的时间。

3. 限制循环

**
并不一定总是要查看每个文件。您可能只想处理示例目录中的JPEG文件:

$ for f in *.jpg ; do convert $f -scale 33% tmp/$f ; done
$ ls -m tmp
cat.jpg, otago.jpg

做完了 ls -m tmpcat.jpg,otago.jpg或者,您可能需要重复执行特定次数的操作,而不是处理文件。for循环的变量由您提供的任何数据定义,因此您可以创建一个循环访问迭代数字而不是文件的循环:

$ for n in {0..4}; do echo $n ; done
0
1
2
3
4

4. 更多的循环

您现在已经足够了解创建自己的循环了。在对循环感到满意之前,请在要处理的文件副本上使用它们,并尽可能多地使用带有内置保护措施的命令,以防止您破坏数据并造成不可弥补的错误,例如意外重命名整个文件,相同名称的文件目录,彼此覆盖。

有关高级for循环主题,请继续阅读。

并非所有的shell都是Bash

for关键字内置在Bash shell中。许多相似的shell使用相同的关键字和语法,但是某些shell(例如tcsh)使用不同的关键字(例如foreach)来代替。

在tcsh中,语法本质上相似,但比Bash严格。在以下代码示例中,是否不键入字符串foreach?在第2行和第3行中。它是辅助提示,提醒您仍在构建循环的过程中。

$ 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中,foreach和end都必须单独出现在单独的行中,因此不能像使用Bash和类似的shell那样在一行上创建for循环。

5. 使用find命令执行for循环

从理论上讲,您可能会发现一个不提供for循环函数的shell,或者您可能只是更喜欢使用带有附加功能的其他命令。
find命令是实现for循环功能的另一种方法,因为它提供了几种方法来定义要包含在循环中的文件范围以及并行处理选项。
find命令旨在帮助您在硬盘驱动器上查找文件。它的语法很简单:您提供要搜索的位置的路径,并找到所有文件和目录:

$ find .
.
./cat.jpg
./design_maori.png
./otago.jpg
./waterfall.png

你可以通过添加name的一部分来过滤搜索结果:

$ 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子句中,括号字符{}代表正在处理的任何项(换句话说,已定位的任何以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子目录中的文件。在某些情况下,您可能想要搜索当前目录以及其中的所有其他目录(以及其中的所有目录)。它可以是功能强大的递归处理工具,尤其是在复杂的文件结构中(例如,音乐艺术家的目录中包含充满音乐文件的专辑目录),但是您可以使用-maxdepth选项对其进行限制。

只查找当前目录下的PNG文件(不包括子目录):

$ find . -maxdepth 1 -name "*png"

要在当前目录以及其他子目录级别中查找和处理文件,请将最大深度增加1:

$ find . -maxdepth 2 -name "*png"

它的默认值是进入所有子目录。

6. 小延伸

使用循环的次数越多,节省的时间和精力就越多,可以处理的任务也就越大。您只是一个用户,但是经过深思熟虑的循环,您可以使计算机完成艰苦的工作。

您可以并且应该像对待其他任何命令一样对待循环,以便在需要对多个文件重复执行一个或两个操作时可以将其放在手边。但是,它也是进行认真编程的合法途径,因此,如果您必须对任意数量的文件执行复杂的任务,请抽出一些时间来计划工作流程。如果您可以在一个文件上实现目标,那么将该可重复过程包装在for循环中是相对简单的,并且唯一需要的“编程”是了解变量的工作方式以及足够的组织以将未处理的文件与已处理的文件分开。只需做一些练习,您就可以从一个Linux用户转移到知道如何编写循环的Linux用户!

7. Shell脚本关于循环的一些总结

不管是哪一门计算机语言,循环都是不可绕开的一个话题,Shell 当然也不是例外。下面总结一些 Shell 脚本里常用的循环相关的知识点,新手朋友可以参考。

1)、for 循环

Shell 脚本里最简单的循环当属 for 循环,有编程基础的朋友应该都有使用过 for 循环。最简单的 for 循环如下所示,你只需将变量值依次写在 in 后面即可:

#!/bin/bash
for num in 1 2 3 4
do
    echo $num
done

如果要循环的内容是字母表里的连续字母或连续数字,那么就可以按以下语法来写脚本:

#!/bin/bash
for x in {a..z}
do
    echo $x
done

2)、while 循环

除了 for 循环,Shell 同样提供了 while 循环。对于其它语言,如果你见过 for 循环却没见过 while 循环,那么你一定是学了个假语言。

在 while 循环里,每进行一次循环,条件都会被判断一次,来确定本次循环是否该继续。其实在循环次数比较少的情况下,for 循环与 while 循环效果差不多,但如果循环次数比较多,比如 10 万次,那么 while 循环的优势就体现出来了。

#!/bin/bash
n=1
while [ $n -le 4 ]
do
    echo $n
    ((n++))
done

3)、循环套循环

像其它高级语言一样,循环是可以互相嵌套的。比如下面这个例子,我们在 while 循环里再套入一个 for 循环:

#!/bin/bash
n=1
while [ $n -lt 6 ]
do
    for l in {a..d}
    do
        echo $n$l
    done
    ((n++))
done

这个脚本执行的结果应该是 1a, 1b, 1c, 1d, 2a, 2b … 5d。

4)、循环的内容是变化的

我们上面提到的 for 循环,循环变量要赋的值都列在了 in 后面的列表里了。但这样灵活性太差,因为在很多情况下,循环变量要获得的值是不固定的。

就比如,有个变量要获得当前系统上所有用户,但因为每台电脑用户都不一样,我们根本就没办法将这个变量写死。

在这种情况下,我们可以使用 ls 命令将 /home 目录下所有用户都列出来,然后用循环变量依次获取它们。完整代码如下:

#!/bin/bash
for user in `ls /home`
do
    echo $user
done

当然,除了 ls ,Shell 还支持其它命令。比如我们可以使用 date 命令获取当前系统时间,再依次打印出来:

$ for word in `date`
> do
>     echo $word
> done
Thu
Apr
9
08:12:09
CST
2020

5)、变量值检查

我们在使用 while 循环时,经常需要判断一个变量的值是否大于或者小于某个数。有时候这个数也是用另一个变量来表示,那么我们就需要判断这个变量的值是否是数字。有三种判断方法:

#!/bin/bash
echo -n "How many times should I say hello? "
read ans
if [ "$ans" -eq "$ans" ]; then
    echo ok1
fi
if [[ $ans = *[[:digit:]]* ]]; then
    echo ok2
fi
if [[ "$ans" =~ ^[0-9]+$ ]]; then
    echo ok3
fi

第一种方法看起来似乎是个废话,但实际上,-eq 只能用于数值间判断,如果是字符串则判断不通过,所以这就保证了 ans 是个数值型变量。

第二种方法是直接使用 Shell 的通配符对变量进行判断。

第三种方法就更直接了,使用正则表达式对变量进行判断。

我们直接来看一个例子:

#!/bin/bash
echo -n "How many times should I say hello? "
read ans
if [ "$ans" -eq "$ans" ]; then
  n=1
  while [ $n -le $ans ]
  do
    echo hello
    ((n++))
  done
fi

在这个脚本里,我将要循环的次数传入到 ans 变量,然后脚本就具体打印几次 hello 。为了保证我们传入的内容是数字,我们使用了 if [ "$ans" -eq "$ans" ] 语句来判断。如果我们传入的不是数字,则不会进入 while 循环。

6)、循环输出文本文件内容

如果你想按行依次循环输出文本文件的内容,可以这样操作:

#!/bin/bash
echo -n "File> "
read file
n=0
while read line; do
  ((n++))
  echo "$n: $line"
done < $file

在这里,我们使用 read 命令将文本文件的内容读取存入 file 变量,然后再使用重定向(上述脚本最后一行)将 file 内容依次传入 while 循环处理再打印出来。

7)、死循环

有时候我们需要一直永远循环做某件事,那么我们就可以使用死循环。达到这个目的很简单,只需使用while true 即可。

#!/bin/bash
while true
do
    echo -n "Still running at "
    date
    sleep 1
done

在以上这个脚本里,将每隔 1 秒打印一次 Still running at 具体时间 ,直到你按 Ctrl + C 终止这个脚本。
————————————————
原文链接:https://blog.csdn.net/boazheng/article/details/105446510

posted @ 2021-04-07 10:15  直角漫步  阅读(416)  评论(0编辑  收藏  举报