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
第一个的逻辑是:“只要表达式的计算结果为 true,就执行程序语句列表。 当表达式的计算结果为 false 时,退出循环。” 第二个是:“在表达式的计算结果为 true 之前,执行程序语句列表。 当表达式的计算结果为 true 时,退出循环。”
While 循环
当(只要)逻辑表达式的计算结果为 true 时,while 循环用于执行一系列程序语句。 你的 PWD 仍然应该是 ~/testdir。
while 循环的最简单形式是永远运行的循环。 以下形式使用 true 语句来始终生成“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(即 true),因此 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 命令非常相似。 不同之处在于,它将继续循环,直到逻辑表达式的计算结果为“true”。 查看此结构的最简单形式
[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 条评论