理解 Bash:编程要素
有没有想过为什么 Bash 编程如此困难?Bash 采用了与传统编程语言相同的结构;然而,在底层,逻辑却截然不同。
Bourne-Again Shell (Bash) 是由自由软件基金会 (FSF) 在 GNU 项目下开发的,这使其在开源社区中享有某种特殊的声誉。如今,Bash 是大多数 Linux 安装上的默认用户 shell。虽然 Bash 只是几个著名的 UNIX shell 之一,但它在 Linux 中的广泛分布使其成为一项重要的工具。
UNIX shell 的主要目的是允许用户通过命令行有效地与系统交互。常见的 shell 操作是调用可执行文件,这反过来会导致内核创建一个新的运行进程。Shell 具有将一个程序的输出发送到另一个程序作为输入的机制,以及与文件系统交互的工具。例如,用户可以遍历文件系统或将程序的输出定向到文件。
虽然 Bash 主要是一个命令解释器,但它也是一种编程语言。Bash 支持变量、函数,并具有控制流结构,例如条件语句和循环。然而,这一切都带有一些不寻常的怪癖。这是因为 Bash 试图同时履行两个角色:既要成为命令解释器,又要成为编程语言——而这两者之间存在张力。
包括 Bash 在内的所有 UNIX shell 主要都是命令解释器。这个特性有着悠久的历史,可以追溯到第一个 shell 和第一个 UNIX 系统。随着时间的推移,UNIX shell 通过演变获得了编程能力,这导致了编程环境的一些不寻常的解决方案。由于许多人在接触 Bash 之前已经具有一些传统编程语言的背景,因此 Bash 在编程结构方面采取的不寻常视角是造成许多困惑的根源,Bash 论坛上发布的许多问题就证明了这一点。
在本文中,我将讨论 Bash 中的编程结构与传统编程语言有何不同。为了真正理解 Bash,了解 UNIX shell 的演变过程很有用,因此我首先回顾相关的历史,然后介绍几个 Bash 功能。本文的大部分内容展示了 Bash 编程的不寻常方面是如何源于将命令解释器功能与编程语言的功能无缝融合的需求。
Bash 历史“shell”一词起源于 MULTICS 项目,该项目是麻省理工学院 (MIT)、通用电气和贝尔电话实验室(以下简称贝尔实验室)之间的合作项目,旨在开发下一代分时操作系统。由于对进展不满意,贝尔实验室于 1969 年退出了该项目,而贝尔实验室的 MULTICS 团队继续开发了自己的操作系统:UNIX。
Bash 的祖先是 Thompson shell,第一个 UNIX 命令解释器,由 Ken Thompson 于 1971 年开发。图 1 显示了《UNIX 编程手册》第一版中的摘录,其中描述了 Thompson shell。

图 1. 《UNIX 编程手册》第一版摘录,出版于 1971 年,描述了原始的 Thompson Shell
在 1973 年至 1975 年间,John R. Mashey 扩展了原始的 Thompson shell,并添加了几个编程功能,使其成为一种高级编程语言。用 Mashey 自己的话说
修改旨在改进 shell 的使用……并使其更方便地用作高级编程语言。与许多现有 UNIX 软件的理念一致,我们尝试仅在实际用户体验表明有必要时才添加新功能,以避免通过“蔓延的功能主义”污染紧凑、优雅的系统。(摘自 J. Mashey,“将命令语言用作高级编程语言”,CSE '76 第二届国际软件工程会议论文集,1976 年。)
Stephen Bourne 于 1976 年初开始开发新的 shell。Bourne shell 受益于 Mashey shell 引入的概念,并带来了自己的一些新想法。Bourne shell 正式在 1979 年发布的 UNIX 版本 7 中推出。
最初的 Thompson shell、Mashey shell 和 Bourne shell 都被称为 sh,它们在 1970 年至 1976 年期间相互重叠或相互取代,因为它们不断改进并获得了额外的功能。在整个 1970 年代,UNIX 主要在贝尔实验室和加利福尼亚大学伯克利分校(称为 BSD 的变体)开发。随着 UNIX 的发展,shell 也不断开发和改进。在 Bourne shell 已经投入使用时,Bill Joy 在伯克利开发了 C shell (csh)。C shell 是第一个真正意义上的替代 UNIX shell,它被纳入 Berkeley UNIX 的 2BSD 版本。在 1980 年代初期,David Korn 开发了 Korn shell (ksh)。与 Bourne shell 相比,C shell 强调命令解释器模式,而 Korn shell 则具有更广泛的编程功能。
贝尔实验室和伯克利的 UNIX 开发工作相互丰富,这两个版本后来合并了。在 1980 年代,AT&T 将 UNIX 授权给多家商业供应商,这导致了 UNIX 市场主导地位的破坏性战争。1985 年,Richard Stallman 成立了自由软件基金会 (FSF),其主要倡议是构建一个免费使用的类 UNIX 系统,一个不受围绕 UNIX 的知识产权问题困扰的系统。这就是著名的 GNU 项目(“GNU's not UNIX”)。事实上,Stallman 于 1983 年 9 月在 net.unix-wizards 邮件列表中发送的原始信件以呼吁开始:“Free Unix!”
由于没有 shell 就无法拥有免费的 UNIX,因此 shell 是 GNU 项目的优先事项。Brian Fox,自由软件基金会的第一个有偿程序员,于 1988 年开始开发 shell。这就是后来的 Bash,于 1989 年首次发布 beta 版。Bash 主要是一个 Bourne shell 的克隆(因此称为“Bourne-Again”),但它也包含受 C shell 和 Korn shell 启发的其他功能。Brian Fox 是 Bash 的官方维护者,直到 1992 年。当时,Chet Ramey 已经参与了 Bash 的工作,并于 1993 年成为官方维护者。Chet Ramey 在接下来的 25 年里继续维护和开发 Bash,并且他仍然是 Bash 当前的维护者。
同时做两件不同的事情最初的 Thompson shell 是一个简单的命令解释器,其操作模式如下
$ command [ arg1 ... [ argN ]
其中 command
是可执行文件的名称(即要执行的命令),可选参数 arg1 ... argN
传递给该命令。Thompson shell 没有编程功能。这种情况随着 Mashey shell(以及后来的 Bourne shell)的开发而改变。Stephen Bourne 在他 1978 年发表的开创性论文“The UNIX Shell”中写道
UNIX shell 既是一种编程语言,也是一种命令语言。作为一种编程语言,它包含控制流原语和字符串值变量。作为一种命令语言,它为 UNIX 操作系统与进程相关的功能提供了用户界面。(S.R Bourne,“The UNIX Shell”,《贝尔系统技术期刊》,第 56 卷,第 6 期,1978 年 7 月至 8 月。)
请注意对不同功能的强调:编程语言和命令语言。事实上,正是 Mashey shell 和 Bourne shell 将 Thompson shell 的功能扩展到了命令解释器之外。shell 最初的角色是命令解释器,shell 的编程功能是后来添加的。UNIX shell 演变出了一些巧妙的方法,将编程功能与原始的命令解释器角色结合起来。
Bash 操作模式今天的 Bash 比最初的 Mashey shell 和 Bourne shell 更强大。然而,shell 的目的仍然完全相同。可以说,shell 最重要的功能是运行命令(即,将可执行文件提交给内核执行)。这有几个深远的含义。首先,Bash 将(几乎)任何给定的内容都视为命令。考虑以下 Bash 会话
$ VAR
bash: VAR: command not found
$ 9
bash: 9: command not found
$ 9 + 1
bash: 9: command not found
$
这表明 Bash 将输入拆分为单词,然后尝试将第一个单词作为命令执行(“单词”VAR
和 9
)。在这里,“命令”可以是 Bash 内置命令(例如 cd
)、实用程序(例如 /bin/ls
)或其他一些可执行文件。当输入字符串 9 + 1
时,Bash 将其拆分为三个“单词”:9
、+
和 1
。重要的是要注意,Bash 将所有单词都保留为字符串,并且在被迫进行算术求值之前没有数字的概念。作为相当简化的总结,Bash 的操作方式如下
- 接收输入并根据空格(空格或制表符)将其拆分为单词。
- 假设第一个单词是命令。如果第一个单词之后有任何内容,则假设它们是要传递给命令的参数。
- 尝试执行命令(并将其参数传递给它,如果有的话)。
此视图忽略了几个中间步骤。例如,Bash 扫描输入行并执行各种扩展和替换。它还检查是否有名为给定名称的内置命令,如果存在,则执行该命令。为了不忽略大局,我经常忽略这些细节。
因此,Bash 最重要的目的是执行命令,这有一些深远的含义。值得注意的是,Bash 中的编程结构,乍一看可能像一种编程语言,但实际上是从这种操作模式派生出来的。这就是本文的中心主题。
Bash 内置命令与外部命令Bash 新手经常感到困惑的一点是 Bash 内置命令和外部命令之间的区别。在典型的 Linux/UNIX 系统上,许多常用命令既内置在 Bash 中,也以相同的名称作为独立的exe存在。例如,echo
(内置)和 /bin/echo
、kill
(内置)和 /bin/kill
、test
(内置)和 /usr/bin/test
(还有更多)。考虑一下 Bash 内置命令 echo
和 /bin/echo
的行为非常相似
$ echo 'Echoed with a built-in!'
Echoed with a built-in!
$ /bin/echo 'Echoed with external program!'
Echoed with external program!
$
但是,也存在细微的差异(尝试 echo --version
)。为什么要重复命令?原因有几个。内置版本通常出于性能原因而存在:Bash 内置命令在已经运行的 shell 进程中执行。相比之下,执行外部实用程序涉及内核加载和执行外部二进制文件,这是一个慢得多的过程。
此时,值得注意的是,某些 shell 命令本质上不能是外部实用程序(换句话说,它们必须是 shell 内置命令)。考虑更改当前工作目录的 cd
命令。外部实用程序无法更改 shell 的当前工作目录,因此 cd
必须是 Bash 内置命令。为什么?因为将命令作为外部实用程序调用会使 shell 成为其父进程,而子进程无法更改父进程的当前工作目录。
您可以反过来问这个问题,“如果 echo
已经内置在 shell 中,为什么外部实用程序 /bin/echo
会存在?” 这是因为人们并不总是通过 shell 工作,可能需要在没有中介 shell 进程的情况下调用 echo
。其次,原则上,没有什么可以强制 UNIX shell 必须将 echo
作为内置命令,因此,拥有外部实用程序 /bin/echo
作为后备非常重要。
用户经常面临的一个实际问题是:如何知道您刚刚调用的命令是 shell 内置命令还是同名的外部实用程序?Bash 命令 type
(它本身是一个 shell 内置命令)指示如果执行将使用哪个命令。例如
$ type echo
echo is a shell builtin
$ type ls
ls is hashed (/bin/ls)
$
基本规则如下:如果存在给定名称的内置命令,则将执行该命令。如果内置命令不存在,Bash 将搜索外部程序,如果找到,则执行它。如果您想确保使用恰好与 shell 内置命令同名的可执行文件,请使用完整路径调用该可执行文件。
变量赋值当在 Bash 中输入命令时,Bash 期望遇到的第一个单词是命令。但是,有一个例外:如果第一个单词包含 =
,Bash 将尝试执行变量赋值。例如
$ VAR=7
$
这会将值 7 分配给名为 VAR
的变量。要检索变量的值,您需要以美元符号为变量名添加前缀。因此,要查看变量的值,您可以将美元符号前缀与 echo
结合使用
$ echo $VAR
7
$
对于变量赋值,包含 =
的连续字符串很重要。以下操作将失败
$ VAR = 1
bash: VAR: command not found
$
在这种情况下,Bash 将输入 VAR = 1
拆分为三个“单词”(VAR
、=
和 1
),然后尝试将第一个单词作为命令执行。这显然不是这里的本意。
虽然 Bash 允许您通过简单地为变量赋值来动态创建任意变量,但它也有许多内置变量。内置变量的一个示例是 BASHPID
。它包含 Bash shell 本身的进程 ID
$ echo $BASHPID
2141
$
另一个内置变量(我在这里广泛介绍的变量)是 ?
。在 Bash 会话中的任何时候,此变量都包含上次执行命令的返回值。返回值始终是整数。(具体来说,这是 C 程序函数 main()
的返回值。注意:在任何 C 程序中,函数 main()
必须返回一个整数。)按照 UNIX 约定,返回值 0 表示成功,任何其他值表示失败。例如,考虑实用程序 /bin/ls
$ touch NEWFILE
$ /bin/ls NEWFILE
NEWFILE
$ echo $?
0
$
按照约定,实用程序 /bin/ls
在成功时返回 0,您可以通过检查 ?
的值来看到这一点。如果 ls
无法执行(例如,无法访问文件),它将返回值 >0
$ /bin/ls DOESNOTEXIST
ls: cannot access 'DOESNOTEXIST': No such file or directory
$ echo $?
2
$ echo $?
0
$
在最后一个示例中,请注意第一个 ?
设置为 2,第二个 ?
设置为 0。为什么?因为第二个 ?
包含 echo
命令的退出状态(该命令已成功执行)。请记住,?
变量包含上次执行命令的退出状态。您可以使用命令 true
和 false
将 ?
的值分别设置为 0 或 1
$ false
$ echo $?
1
$ false
$ true
$ echo $?
0
$
乍一看可能有点傻,但请继续阅读。
Bash 混合行为现在让我们考虑一下 Bash 如何提供无缝集成的命令环境的印象,即使它执行的任务本质上是完全不同的。首先,请注意,运行 Bash 内置命令对 ?
变量产生的影响与运行外部程序相同
$ false # set ? to 1
$ echo 'Calling a built-in command'
Calling a built-in command
$ echo $?
0
此示例表明,调用内置命令 echo
将 ?
更改为 0(要确认这一点,请首先运行 false
命令,该命令将 ?
设置为 1)。关键是它的行为与调用外部程序 echo
相同
$ false # set ? to 1
$ /bin/echo 'Calling external program'
Calling external program
$ echo $?
0
然而,这两种情况截然不同。在第一种情况下,Bash 调用了内部命令 echo
;在第二个示例中,Bash 请求内核运行外部可执行文件 (/bin/echo
) 并暂停自身等待可执行文件完成。对 ?
变量的影响完全相同。
即使对于变量赋值,Bash 也会相应地设置 ?
变量
$ false # set ? to 1
$ VAR=one
$ echo $?
0
$
由此可见,Bash 将变量赋值视为命令。如果变量赋值不成功,?
将设置为大于 0 的值。例如,内置变量 BASHPID
是只读的,您无法更改它(也就是说,Bash 无法更改其自身的进程 ID)。所以这会失败
$ true # set ? to 0
$ BASHPID=99
$ echo $?
1
$
尝试执行不存在的命令也会将 ?
设置为指示失败
$ true # set ? to 0
$ DUMMY
bash: DUMMY: command not found
$ echo $?
127
$
在这种情况下,Bash 用数字 127 填充了特殊变量 ?
。这个数字在 Bash 中是硬编码的,它专门表示“未找到命令”。
总而言之,以上示例显示了三种完全不同的情况:调用内部 Bash 命令、运行外部程序和变量赋值。然而,Bash 将这三种情况都视为命令执行,并在 ?
特殊变量方面提供了通用行为。掌握了这些见解,现在让我们研究 Bash 中的三个基本编程结构:if
语句、while
循环和 until
循环。
if
语句
几乎每种编程语言的基本要素都是条件 if
语句。在 C 语言中,它看起来像这样
if (TRUTH_TEST) {
statements to execute
}
这里 TRUTH_TEST
是一个根据 C 语言规则评估为真或假的测试。这有时称为“真值测试”。这是 Python 中的一个示例
if True:
print('Yay true!')
在 Bash 中,相同的示例看起来像这样
if true
then
echo 'Yay true!'
fi
您可以使用 ; 重新格式化它,以提供方便的单行输入
$ if true; then echo 'Yay true!'; fi
Yay true!
$
这看起来很像任何编程语言中的 if
条件语句。但是,事实并非如此。在上面的示例中,true
是一个命令。事实上,true
是一个 shell 内置命令
$ type true
true is a shell builtin
$ help true
true: true
Return a successful result.
Exit Status:
Always succeeds.
请仔细体会:true
是一个命令。事实上,这与上面从命令行运行以设置 ?
变量值的 true
命令相同。那么 if
语句正在评估什么?它正在评估 true
命令的返回值。如果您不相信,请考虑可以将 true
替换为外部实用程序 /bin/true
$ if /bin/true; then echo 'Yay true!'; fi
Yay true!
$
其中
$ man true
TRUE(1) User Commands TRUE(1)
NAME
true - do nothing, successfully
SYNOPSIS
true [ignored command line arguments]
true OPTION
DESCRIPTION
Exit with a status code indicating success.
如果 true
是一个命令,您可以在那里放置任何命令,对吗?没错
$ if /bin/echo; then echo 'Yay true!'; fi
Yay true!
$
请注意在字符串 Yay true!
之前打印了空行。那是因为 if
语句实际上执行了命令 /bin/echo
,并且在没有任何参数的情况下,这将打印一个换行符。您实际上可以为 echo
命令提供一个参数
$ if /bin/echo 'Hi'; then echo 'Yay true!'; fi
Hi
Yay true!
$
此处执行的两个 echo
命令是不同的:第一个是外部实用程序 /bin/echo
;第二个,出现在 if
语句体中的 echo
是 shell 内置命令。显然,第二个 echo
也可以替换为外部实用程序。
继续,我之前提到过 Bash 会将变量赋值视为命令。因此,变量赋值可以与内置命令或外部可执行文件在同一位置使用
$ if VAR=99; then echo 'Assignment done!'; fi
Assignment done!
$ echo $VAR
99
$
总结一下,if
条件语句的通用形式是:if CMD1; then CMD2; fi
,其中 CDM1
和 CMD2
是命令。if
语句通过评估命令 CMD1
的退出代码来控制流程:如果 CMD1
成功(根据退出状态 0 判断),则执行 CMD2
。这与大多数传统编程语言中的真值测试截然不同,并且是造成许多困惑的根源。我将其称为困惑的来源 1。
false
命令
我刚刚描述了 true
是一个命令。因此,毫不奇怪,有一个 false
,它是 true
的完全相反。对于 Bash 内置命令
$ type false
false is a shell builtin
$ help false
false: false
Return an unsuccessful result.
Exit Status:
Always fails.
$
并且,有一个具有相同功能的外部实用程序
$ man false
FALSE(1) User Commands FALSE(1)
NAME
false - do nothing, unsuccessfully
SYNOPSIS
false [ignored command line arguments]
false OPTION
DESCRIPTION
Exit with a status code indicating failure.
命令 true
和 false
什么都不做,但分别以状态 0 或 1 退出。由于 if
语句在决定是否执行主体时评估退出代码,因此 if true
始终成功,而 if false
始终失败。请注意,true
的退出值为 0,false
的退出值为 1。这有点违反直觉,并且与大多数编程语言完全相反。例如,在 Python 真值测试中,0 等同于 False(布尔值),而 1 等同于 True(布尔值)。
if
正在测试退出值
让我们通过编写一个简单的 C 程序 true.c 来确认 Bash 中的 if
语句仅测试程序的退出值,该程序返回 1(注意,真正的实用程序 true
返回 0,或成功!)
int main() {
return 1;
}
此程序没有做太多事情;它只是返回 1 作为退出状态。根据 UNIX 约定,退出状态 1 表示失败(无论程序是否运行良好!)。让我们编译并执行此程序,并确认它向 shell 返回“不成功”的退出状态
$ gcc true.c -o true
$ ./true
$ echo $?
1
$
因此,如果您在此程序的 if
语句中使用它,则输出将不是您可能期望的
$ if ./true; then echo 'Yay true!'; fi
$
换句话说,true
命令“失败”了。此示例证实了 if
语句所做的只是评估退出状态。程序运行良好,完全符合预期,但这并不重要;从 Bash 的角度来看,非零退出状态表示失败。我将其称为困惑的来源 2。
考虑以下任务:测试文件是否存在,如果存在,则删除它。为此,您可以使用带有 -e
标志的 Bash 内置 test
命令
$ rm dum.txt # make sure file 'dum.txt' doesn't exist
$ test -e dum.txt # test if file 'dum.txt' exists
$ echo $? # confirm that the command test failed
1
$ touch dum.txt # now create file 'dum.txt'
$ test -e dum.txt # test if file 'dum.txt' exists
$ echo $? # confirm the command test was successful
0
$
因此,要测试文件是否存在,如果存在,则删除它
$ touch dum.txt # create file 'dum.txt'
$ if test -e dum.txt; then rm dum.txt; fi # file deleted
$
这里需要注意的关键是 if test -e dum.txt; then rm dum.txt; fi
实际上执行了命令 test -e dum.txt
。在这种情况下,test
是 Bash 内置命令。您可能会怀疑,有一个 /usr/bin/test
实用程序可以执行相同的操作,并且可以达到相同的效果
$ touch dum.txt # create file 'dum.txt'
$ if /usr/bin/test -e dum.txt; then rm dum.txt; fi
↪# file deleted
$
现在,Bash 将 [ ]
实现为内置 test
命令的同义词
$ test -e dum.txt # command successful if file exists
$ [ -e dum.txt ] # exactly the same as previous example!
请注意,[ -e dum.txt ]
是一个命令。当然,这会在成功时返回 0,在失败时返回 1。让我们确认一下
$ rm dum.txt
$ [ -e dum.txt ]
$ echo $?
1
$ touch dum.txt
$ [ -e dum.txt ]
$ echo $?
0
$
有了这种理解,您可以使用 [ -e ... ]
结构重复上面的示例
$ touch dum.txt # create file 'dum.txt'
$ if [ -e dum.txt ]; then rm dum.txt; fi # file deleted
$
最后一个结构看起来更像大多数传统编程语言中的 if
控制语句。但是,事实并非如此。[ ]
是一个命令——基本上是调用内置 test
命令的另一种方式。
惊喜并没有完全到此结束。在 Bash 中,if
语句可以在关键字 if
之后和用关键字 then
表示的主体之前接受任意数量的以分号分隔的命令。类似于这样:if CMD1; CMD2; ... CMDN; then CMDN+1; CMDN+2; CMDN+M; fi
。if
语句按顺序评估所有命令,并且仅当最后一个命令的退出状态为 0(按照约定,表示成功)时才执行循环体。考虑以下示例
$ if false; true; then echo 'Yay true!'; fi
↪# body will execute
Yay true!
$ if true; false; then echo 'Yay true!'; fi
↪# body will not execute
$
因此,在 Bash 中,编写如下内容是完全合法的
$ if [ -e dum.txt ]; echo 'Hi'; false; then rm dum.txt; fi
Hi
$
这将执行 if
之后给出的三个命令,并且永远不会执行主体 (rm dum.txt
),因为最后一个命令是 false
,它总是失败(更准确地说,返回非零状态)。总而言之,您可以将命令列表用作单个命令的替代。此类命令列表的总体退出状态由列表中最后一个命令的退出状态给出。我将其称为困惑的来源 3。
while
和 until
理解 if
语句的行为非常有用,因为相同的行为也适用于 while
和 until
循环。考虑以下示例
$ while true; do echo 'Hi, while looping ...'; done
Hi, while looping ...
Hi, while looping ...
Hi, while looping ...
^C
$
让我们确切地了解这里发生了什么。首先,while
循环执行 true
命令并评估其退出状态。由于 true
的退出状态始终为 0,因此它执行了循环体 (echo 'Hi, while looping ...'
)。然后它返回以进行另一个相同的循环。由于 true
命令始终成功运行,因此这创建了一个无限循环(已通过 Ctrl-C 中断)。由于 true
是一个命令,您可以将其替换为任何命令。例如
$ while /bin/echo 'ECHO'; do echo 'Hi, while looping ...'; done
ECHO
Hi, while looping ...
ECHO
Hi, while looping ...
ECHO
Hi, while looping ...
^C
$
因此,此 while
循环仅交替执行两个 echo
命令:/bin/echo
(外部可执行文件)和 echo
(Bash 内置命令)。
您可能会怀疑,while
结构可以接受命令列表,在这种情况下,它将根据列表中最后一个命令的退出状态继续执行循环体。换句话说,while
循环的通用形式如下:while CMD1; CMD2; ... CMDN; do CMDN+1; CMDN+2; CMDN+M; done
。例如
$ while true; false; do echo 'Hi, looping ...'; done
$
在此示例中,循环体未执行,因为最后一个命令是 false
(它总是失败)。until
循环的工作方式类似
$ until false; do echo 'Hi, until looping ...'; done
Hi, until looping ...
Hi, until looping ...
Hi, until looping ...
^C
$
在 until
循环的情况下,只要关键字 until
后列出的命令返回非零退出状态,循环体就会执行。由于命令 false
每次都返回非零退出状态,因此上面的示例导致了无限循环。当然,在通用形式中,until
循环可以接受命令列表:until CMD1; CMD2; ... CMDN; do CMDN+1; CMDN+2; CMDN+M; done
。
您可能会问,如果这些循环仅执行两个命令(或两个命令列表),那么在实践中它到底有什么用?循环中测试的命令可能取决于某些动态条件(例如,写入文件的字节数或网络流量类型等等)。条件的变化可能导致命令失败或成功。您还可以在循环体中修改 Bash 变量,这会导致循环的使用方式与此处所示类似
$ i=1
$ while [ $i -le 3 ]; do echo $i; i=$((i+1)); done
1
2
3
$
这里 ((i + 1))
强制 Bash 算术求值,$((i + 1))
返回结果值;结构 [ $i -le 3 ]
是 test $i -le 3
的同义词,它执行算术比较。请注意,从 Bash 的角度来看,这是一个成功执行或未成功执行的命令
$ i=1
$ [ $i -le 3 ]
$ echo $?
0
$ i=9
$ [ $i -le 3 ]
$ echo $?
1
$
这就是为什么 [ $i -le 3 ]
结构可以在 while
关键字之后使用,该关键字期望一个命令(或命令列表)。
Bash 是 GNU 项目生成的 Bourne shell 的独立实现衍生产品,其增强功能受到 C shell 和 Korn shell 的启发。最初的 UNIX shell(Thompson shell)是一个简单的命令解释器。随后,Mashey shell 和 Bourne shell 融入了编程功能。由于 Bash 是 Bourne shell 的直接后代,因此它继承了编程环境如何工作的所有关键思想。这包括它如何将编程语言与命令解释器融合在一起。为此,UNIX shell 已经演变出了一些巧妙的解决方案。
在 Bash 中,编程结构看起来类似于传统编程语言中发现的结构。然而,这些编程结构固有的工作方式却截然不同。这对于具有传统编程语言知识的人来说可能相当令人困惑(Bash 用户通常就是这种情况)。以下是 Bash 编程中三个主要的困惑来源
- Bash 编程的令人惊讶之处在于结构
if
、while
和until
评估命令的退出状态。基本上,这些结构评估以下内容:“退出状态是否为零?” 按照 UNIX 约定,退出状态 0 表示成功,任何其他状态表示失败。 - 退出状态是可执行文件返回的整数——可以将其视为 C 函数
main()
返回的值。请注意,运行良好的程序可能会返回非零退出状态(我在上面展示了一个示例)。但是,不建议编写此类程序。这将打破约定,并且很可能会破坏其他东西,因为整个环境都严重依赖此约定。 - 单个命令可以替换为以分号分隔的命令列表。在这种情况下,命令列表的退出状态是最后一个执行的命令返回的状态。
衷心感谢 Chet Ramey 对本文草稿的反馈。我还要感谢 Isidora C. Likic 检查文本和示例。
资源关于此主题的文章和书籍太多,无法在此处一一列举,但如果您有兴趣了解更多信息,我们推荐以下 Linux Journal 文章(实际上 LJ 文章也太多,无法在此处一一列出,但以下是一些入门文章)
- Dave Taylor 撰写的“使用 Bash 创建专注游戏 PAIRS”
- Patrick Wheelan 撰写的“使用 Bash 脚本创建动态壁纸”
- Andy Carlson 撰写的“使用 Bash 开发控制台应用程序”
- Adam Kosmin 撰写的“使用 Bash 破解保险箱”
- Dave Taylor 撰写的“Ubuntu Linux 和 Bash 作为 Windows 程序!”
- Mitch Frazier 撰写的“Bash 参数扩展”
- Mitch Frazier 撰写的“Bash 正则表达式”
- Mitch Frazier 撰写的“Bash 扩展 Globbing”
- Prentice Bisbal 撰写的“我最喜欢的 bash 提示和技巧”
- Jim Hall 撰写的“使用 Bash 脚本解析 RSS 新闻源”
Bash 视频
有关更多此类编程文章,请查看 Linux Journal 2018 年 10 月刊的“编程深度探索”部分。