Bash 是一种强大的编程语言,非常适合在命令行和 shell 脚本中使用。这个基于我的三卷 Linux 自学课程的三部分系列文章,探讨了如何在命令行界面 (CLI) 上使用 Bash 作为编程语言。
本系列中的第一篇文章探讨了使用 Bash 进行一些简单的命令行编程,包括使用变量和控制操作符。第二篇文章研究了文件类型、字符串、数字和各种逻辑运算符,这些运算符提供了 Bash 中的执行流程控制逻辑和不同类型的 shell 扩展。这第三篇(也是最后一篇)文章将探讨循环的使用,以执行各种类型的迭代操作以及控制这些循环的方法。
循环
我用过的每种编程语言都至少有几种类型的循环结构,它们提供了执行重复操作的各种功能。我经常使用 for 循环,但也觉得 while 和 until 循环很有用。
for 循环
在我看来,Bash 对 for 命令的实现比大多数语言更灵活,因为它能够处理非数值类型的值;相比之下,例如,标准的 C 语言 for 循环只能处理数值类型的值。
Bash 版本 for 命令的基本结构很简单
for Var in list1 ; do list2 ; done
这可以翻译为:“对于 list1 中的每个值,将 $Var 设置为该值,然后使用该值执行 list2 中的程序语句;当 list1 中的所有值都已使用完毕后,循环结束,退出循环。” list1 中的值可以是简单的显式字符串值,也可以是命令替换的结果(在系列文章的第二篇中描述)。我经常使用这种结构。
为了尝试一下,请确保 ~/testdir 仍然是当前工作目录 (PWD)。清理目录,然后查看一个简单的 for 循环示例,从显式的值列表开始。此列表是字母数字值的混合—但不要忘记所有变量都是字符串,可以这样处理。
[student@studentvm1 testdir]$ rm *
[student@studentvm1 testdir]$ for I in a b c d 1 2 3 4 ; do echo $I ; done
a
b
c
d
1
2
3
4
这是一个更有用的版本,变量名称更有意义
[student@studentvm1 testdir]$ for Dept in "Human Resources" Sales Finance "Information Technology" Engineering Administration Research ; do echo "Department $Dept" ; done
Department Human Resources
Department Sales
Department Finance
Department Information Technology
Department Engineering
Department Administration
Department Research
创建一些目录(并在执行此操作时显示一些进度信息)
[student@studentvm1 testdir]$ for Dept in "Human Resources" Sales Finance "Information Technology" Engineering Administration Research ; do echo "Working on Department $Dept" ; mkdir "$Dept" ; done
Working on Department Human Resources
Working on Department Sales
Working on Department Finance
Working on Department Information Technology
Working on Department Engineering
Working on Department Administration
Working on Department Research
[student@studentvm1 testdir]$ ll
total 28
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Administration
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Engineering
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Finance
drwxrwxr-x 2 student student 4096 Apr 8 15:45 'Human Resources'
drwxrwxr-x 2 student student 4096 Apr 8 15:45 'Information Technology'
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Research
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Sales
$Dept 变量必须用引号括在 mkdir 语句中;否则,由两部分组成的部门名称(例如“Information Technology”)将被视为两个单独的部门。 这突出了我喜欢遵循的最佳实践:所有文件和目录名称都应为单个单词。 尽管大多数现代操作系统都可以处理名称中的空格,但系统管理员需要付出额外的工作来确保在脚本和 CLI 程序中考虑这些特殊情况。(即使它们很烦人,也几乎肯定应该考虑它们,因为您永远不知道会遇到什么文件。)
因此,再次删除 ~/testdir 中的所有内容—再来一次
[student@studentvm1 testdir]$ rm -rf * ; ll
total 0
[student@studentvm1 testdir]$ for Dept in Human-Resources Sales Finance Information-Technology Engineering Administration Research ; do echo "Working on Department $Dept" ; mkdir "$Dept" ; done
Working on Department Human-Resources
Working on Department Sales
Working on Department Finance
Working on Department Information-Technology
Working on Department Engineering
Working on Department Administration
Working on Department Research
[student@studentvm1 testdir]$ ll
total 28
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Administration
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Engineering
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Finance
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Human-Resources
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Information-Technology
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Research
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Sales
假设有人要求提供特定 Linux 计算机上所有 RPM 的列表以及每个 RPM 的简短描述。 这在我为北卡罗来纳州工作时发生过。 由于当时开源未获得州机构“批准”使用,并且我只在我的台式计算机上使用 Linux,因此尖头老板 (PHB) 需要一份安装在我计算机上的每件软件的列表,以便他们可以“批准”例外情况。
您将如何处理? 这是一种方法,首先要知道 rpm –qa 命令提供了 RPM 的完整描述,包括 PHB 想要的两个项目:软件名称和简要摘要。
逐步构建最终结果。 首先,列出所有 RPM
[student@studentvm1 testdir]$ rpm -qa
perl-HTTP-Message-6.18-3.fc29.noarch
perl-IO-1.39-427.fc29.x86_64
perl-Math-Complex-1.59-429.fc29.noarch
lua-5.3.5-2.fc29.x86_64
java-11-openjdk-headless-11.0.ea.28-2.fc29.x86_64
util-linux-2.32.1-1.fc29.x86_64
libreport-fedora-2.9.7-1.fc29.x86_64
rpcbind-1.2.5-0.fc29.x86_64
libsss_sudo-2.0.0-5.fc29.x86_64
libfontenc-1.1.3-9.fc29.x86_64
<snip>
添加 sort 和 uniq 命令以对列表进行排序并打印唯一的 RPM(因为可能安装了一些名称相同的 RPM)
[student@studentvm1 testdir]$ rpm -qa | sort | uniq
a2ps-4.14-39.fc29.x86_64
aajohan-comfortaa-fonts-3.001-3.fc29.noarch
abattis-cantarell-fonts-0.111-1.fc29.noarch
abiword-3.0.2-13.fc29.x86_64
abrt-2.11.0-1.fc29.x86_64
abrt-addon-ccpp-2.11.0-1.fc29.x86_64
abrt-addon-coredump-helper-2.11.0-1.fc29.x86_64
abrt-addon-kerneloops-2.11.0-1.fc29.x86_64
abrt-addon-pstoreoops-2.11.0-1.fc29.x86_64
abrt-addon-vmcore-2.11.0-1.fc29.x86_64
<snip>
由于这给出了您想要查看的 RPM 的正确列表,因此您可以将其用作循环的输入列表,该循环将打印每个 RPM 的所有详细信息
[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done
此代码生成的数据量远远超出您的需求。 请注意,循环已完成。 下一步是仅提取 PHB 请求的信息。 因此,添加一个 egrep 命令,该命令用于选择 ^Name 或 ^Summary。 插入符号 (^) 指定行的开头; 因此,将显示任何以 Name 或 Summary 开头的行。
[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done | egrep -i "^Name|^Summary"
Name : a2ps
Summary : Converts text and other types of files to PostScript
Name : aajohan-comfortaa-fonts
Summary : Modern style true type font
Name : abattis-cantarell-fonts
Summary : Humanist sans serif font
Name : abiword
Summary : Word processing program
Name : abrt
Summary : Automatic bug detection and reporting tool
<snip>
您可以在上面的命令中尝试使用 grep 而不是 egrep,但这将不起作用。 您也可以通过 less 过滤器管道传输此命令的输出以探索结果。 最终命令序列如下所示
[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done | egrep -i "^Name|^Summary" > RPM-summary.txt
此命令行程序在一行中使用管道、重定向和 for 循环。 它将您的小型 CLI 程序的输出重定向到一个文件,该文件可以在电子邮件中使用或用作其他用途的输入。
这种逐步构建程序的过程使您可以查看每个步骤的结果,并确保它按预期工作并提供所需的结果。
通过此练习,PHB 收到了一份包含 1,900 多个单独 RPM 包的列表。 我非常怀疑有人读过这份列表。 但我给了他们他们要求的东西,之后再也没有听到他们对此事的任何消息。
其他循环
Bash 中还有另外两种类型的循环结构:while 和 until 结构,它们在语法和功能上非常相似。 这些循环结构的基本语法很简单
while [ expression ] ; do list ; done
和
until [ expression ] ; do list ; done
第一个的逻辑是:“当表达式评估为真时,执行程序语句列表。 当表达式评估为假时,退出循环。” 第二个的逻辑是:“在表达式评估为真之前,执行程序语句列表。 当表达式评估为真时,退出循环。”
While 循环
while 循环用于在逻辑表达式评估为真时(只要)执行一系列程序语句。 您的 PWD 应该仍然是 ~/testdir。
while 循环最简单的形式是永远运行的循环。 以下形式使用 true 语句始终生成“真”返回代码。 您也可以使用简单的“1”——效果相同——但这说明了 true 语句的用法
[student@studentvm1 testdir]$ X=0 ; while [ true ] ; do echo $X ; X=$((X+1)) ; done | head
0
1
2
3
4
5
6
7
8
9
[student@studentvm1 testdir]$
现在您已经研究了它的各个部分,这个 CLI 程序应该更有意义了。 首先,它将 $X 设置为零,以防它有之前程序或 CLI 命令遗留下来的值。 然后,由于逻辑表达式 [ true ] 始终评估为 1(即真),因此 do 和 done 之间的程序指令列表将永远执行下去—或者直到您按下 Ctrl+C 或以其他方式向程序发送信号 2。 这些指令是一个算术扩展,用于打印 $X 的当前值,然后将其递增 1。
系统管理员的 Linux 哲学 的宗旨之一是力求优雅,而实现优雅的一种方法是简洁。 您可以使用变量递增运算符 ++ 来简化此程序。 在第一个实例中,打印变量的当前值,然后递增变量。 这通过将 ++ 运算符放在变量之后来指示
[student@studentvm1 ~]$ X=0 ; while [ true ] ; do echo $((X++)) ; done | head
0
1
2
3
4
5
6
7
8
9
现在从程序末尾删除 | head,然后再次运行它。
在此版本中,变量在其值被打印之前递增。 这通过将 ++ 运算符放在变量之前来指定。 您能看到区别吗?
[student@studentvm1 ~]$ X=0 ; while [ true ] ; do echo $((++X)) ; done | head
1
2
3
4
5
6
7
8
9
您已将两个语句简化为一个语句,该语句打印变量的值并递增该值。 还有一个递减运算符 --。
您需要一种在特定数字处停止循环的方法。 为此,请将 true 表达式更改为实际的数值评估表达式。 让程序循环到 5 并停止。 在下面的示例代码中,您可以看到 -le 是“小于或等于”的逻辑数值运算符。 这意味着:“只要 $X 小于或等于 5,循环将继续。 当 $X 递增到 6 时,循环终止。”
[student@studentvm1 ~]$ X=0 ; while [ $X -le 5 ] ; do echo $((X++)) ; done
0
1
2
3
4
5
[student@studentvm1 ~]$
Until 循环
until 命令与 while 命令非常相似。 区别在于它将继续循环,直到逻辑表达式评估为“真”。 查看此结构的最简单形式
[student@studentvm1 ~]$ X=0 ; until false ; do echo $((X++)) ; done | head
0
1
2
3
4
5
6
7
8
9
[student@studentvm1 ~]$
它使用逻辑比较来计数到特定值
[student@studentvm1 ~]$ X=0 ; until [ $X -eq 5 ] ; do echo $((X++)) ; done
0
1
2
3
4
[student@studentvm1 ~]$ X=0 ; until [ $X -eq 5 ] ; do echo $((++X)) ; done
1
2
3
4
5
[student@studentvm1 ~]$
总结
本系列探讨了许多强大的工具,用于构建 Bash 命令行程序和 shell 脚本。 但它仅仅触及了您可以使用 Bash 完成的许多有趣事情的表面; 剩下的就取决于您了。
我发现学习 Bash 编程的最佳方法是实践。 找到一个需要多个 Bash 命令的简单项目,并从中创建一个 CLI 程序。 系统管理员执行许多适合 CLI 编程的任务,因此我确信您会很容易找到要自动化的任务。
多年前,尽管熟悉其他 shell 语言和 Perl,但我还是决定将 Bash 用于我所有的系统管理自动化任务。 我发现—有时需要进行一些搜索—我可以使用 Bash 来完成我需要的一切。
4 条评论