原文链接

Vladimir Likic 2018年9月28日

是否曾考虑过为什么Bash编程这样难?Bash尽管拥有常见编程语言的结构,但是写起来它的逻辑会显得难懂一些。

Bourne-Again SHell(Bash)是由GNU项目下的Free Software Foundation开发,因此它在开源社区格外受欢迎。如今Bash是大多数Linux发行版的默认shell。Bash尽管是诸多UNIX shell之一,但得益于Linux的流行他最为大众周知。

开发UNIX shell的主要目的是使人可以通过命令行高效地同系统进行交互。shell最常见的行为是调用程序使kernel启动一个新的进程。shell可以将一个程序的输出作为另外一个程序的输入,也可以操作文件系统。例如,用户可以将某个文件或一个程序的输出写入一个文件。

尽管Bash首先是一个命令解释器,但他也是一个编程语言。他支持变量、函数、控制流,如条件跳转和循环。其编程风格会有一些怪异之处,这是因为他在同时扮演两个角色:命令解释器和编程语言,而这两者在一起并不十分融洽。

包括Bash在内的所有UNIX shell首先是个命令解释器。这个特性可以追溯至第一版UNIX系统的第一个shell。陆续,UNIX shell开始添加一些用作编程的特性,为了实现用作编程的目的也就出现了一些看似奇怪的特点。对于那些具有其他编程语言经验的人而言,Bash的编程结构会让人感觉困惑,从大量在Bash论坛上的吐槽可以窥知大家对此的看法。

在这篇文章中,我会讨论Bash的编程结构同常规编程语言的不同之处。要想真正理解Bash,应先去了解UNIX shell是如何被引入的。因此,我首先会回顾一下相关历史,然后再介绍几个Bash的特性。除此之外,这篇文章的重点是讲述那些奇怪的特性是如何在兼顾命令解释器和编程语言双重功能的过程中折衷的。

Bash的历史

“shell”一词起源于麻省理工和General Electric and Bell Telephone Laboratories(贝尔实验室的前身)的MULTICS项目,该项目着力于开发下一代分时操作系统。贝尔实验室不满于项目的开发进度,在1969年从该项目中撤出,撤出的团队继续开发他们自己的系统:UNIX。

Bash最老的祖先是Thompson shell,也是第一个UNIX命令解释器,由Ken Thompson于1971年开发。下图截自《UNIX Programming Manual,1st edition》(1971年11月出版),它解释了Thompson Shell

understandingbashelementsofprogrammingpicture1

在1973年到1975年之间,John R. Mashey扩展了原始的Thompson Shell,增加了几个编程的功能,使之成为一个更高层的编程语言。Mashey是这样解释的:

改动的目的是提高shell的可用性。。。同时让其作为一个高等编程语言。遵循UNIX软件的原则:为了避免在无意之间破坏系统的稳定和优雅,除非被使用者确信有必要,否则不增加任何功能。
——J. Mashey 《Using a Command Language as a High-level Programming Language》CSE '76 Proceedings of the 2nd International Conference on Software engineering, 1976.

Stephen Bourne在1976年早期开始开发一个新的shell。Bourne shell吸取了Mashey shell引入的概念,并增加了一些新的创意。Bourne shell在1979年的 UNIX Version 7中被正式引入。

最早的Thompson shell,Mashey shell和Bourne shell都被叫做“sh”,他们在1970年到1976年是作为演进关系,逐渐增加了新的特性。20世纪70年代中,UNIX主要是由贝尔实验室开发,与此同时,加州大学伯克利分校也在进行独立开发(其UNIX变体叫做BSD)。伴随UNIX的发展,shell被持续开发和改进。在Bourne shell已经被使用的时候,伯克利的Bill Joy开发了C shell(csh)。C shell是第一个真正意义上的新的UNIX shell,他伴随伯克利UNIX的2BSD版本发布。在80年代早期,David Korn开发了Korn shell(ksh)。同Bourne shell对比,C shell更侧重于命令解释器角色,而Korn shell增加了更多可编程能力。

在贝尔实验室和伯克利的UNIX开发彼此相互促进并最终融合在一起。80年代,AT&T将UNIX授权给诸多厂商,这导致了那场关于UNIX归属的“战争”。1985年,Richard Stallman建立了Free Software Foundation(FSF),其主要目的是开发一个自由使用的类UNIX系统,该系统将不存在任何知识产权的纠纷。这就是著名的GNU项目(“GNU's not UNIX)。事实上,1983年9月,在Stallman最初发给net.unix-wizards邮件组的邮件中就已经呼吁:解放UNIX。

由于自由的UNIX仍然需要shell,因此shell的开发是GNU项目的优先事项。Free Software Fundation的第一个雇佣程序员,Brain Fox,从1988年开始开发一个新的shell。这最终成为了Bash,beta版在1989年发布。Bash几乎是Bourne shell的翻版,因此称作Bourne-Again,但他同时包含了C shell和Korn shell新增的功能。直到1992年,Brain Fox都是Bash的官方维护者。与此同时,Chet Ramey也已经参与了Bash的开发工作,并于1993年接手成为Bash的官方维护者。在接下来的25年里,Chet Ramey持续维护Bash,他也是Bash的现任维护者。

同时做两件事情

最初的Thompson shell仅仅是一个命令解释器,其使用方法如下:

$ command [ arg1 … [ argN ]

command是可执行文件的名字,可选参数arg1..argN是传递给命令的参数。Thompson shell不支持任何编程操作。这一点在后续的Mashey shell中得到了改变。后续的Bourne shell也沿用了这个改变,在Stephen Bourne 1978年的重要论文《The UNIX Shell》中,Bourne表述如下:

UNIX shell既是一种编程语言又是一种通俗语言。作为编程语言,它包含控制流和字符变量;作为通俗语言,它提供了一个访问操作系统进程的用户接口。
——(S.R Bourne, "The UNIX Shell", The Bell System Technical Journal, Vol 56, No 6, July–August 1978.)

请注意这两个不同功能的侧重点:程序语言性和通俗语言性。实际上,是Mashey和Bourne扩充了Thompson shell作为命令解释器的功能。shell最初的设计角色就是命令解释器,编程特性是后加的。UNIX shell在维护其作为命令解释器的角色的同时,很精妙地增加了一些可编程的特性。

Bash的操作模式

相比原始的Mashey shell和Bourne shell,如今的Bash已经极其强大。但是,shell的存在目的并没有改变。它的最重要的职责就是执行命令,更确切说就是向内核提交可执行文件。这产生了几个很显著的后果,首先,Bash几乎将所有东西都视为命令,看下面的片段:

$ VAR
bash: VAR: command not found
$ 9
bash: 9: command not found
$ 9 + 1
bash: 9: command not found

这表示Bash将输入切分为单词,然后将第一个单词作为命令去执行(如上面的单词VAR9)。这里的“命令”可以是Bash的内置命令(如 cd),或者是一个外置命令(如/bin/ls),或者是一个可执行文件。当字符串9 + 1被输入,Bash将其切分为三个词9,+,1。在Bash中,一切都是字符串,如果不是强制执行数学运算,根本不会出现数字的概念。以下是个简明的总结,Bash的操作遵循:

  1. 以空格(或者多个空格,或者制表符)为标记将输入切割为单词。

  2. 假定第一个单词是一个命令。如果有多个单词,后面的会被当作参数传入命令。

  3. 尝试执行命令(连带传入的参数)。

这种简要的阐释忽略了一些中间步骤。比如,Bash会扫描输入行并执行扩展和替换。它还会检查是否有内置命令并执行。为了首先从整体理解Bash,我经常会忽略这些细节。

因此,Bash的主要目的是执行命令。这是一个重要特点。在Bash的编程结构中,那些第一眼看起来像编程语言的特性,实际上是起源于Bash执行命令的特性。

Bash的内置和外置命令

经常让Bash初学者感到困惑的是Bash中内置和外置命令的区别。在典型的Linux/UNIX系统中,许多常用的命令既被Bash内置,同时它也有一个同名的Bash之外的程序与其对应。例如Bash内置的echo/bin/echo,内置的kill/bin/kill,内置的test/usr/bin/test等。
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进程开始的时候就一经运行了。作为对比,外置程序需要将二进制文件加载到kernel里并执行,这是个较慢的过程。

需要注意的是,有些shell命令是无法作为外置程序的。例如cd命令用作切换当前工作目录,一个外置程序是无法切换shell的当前工作路径的,所以cd必须是被内置。为什么呢?因为被shell唤起的外置程序必然以shell本身作为父进程,子进程是无法切换父进程的工作路径的。

也许你会问:如果echo这类命令是已经被shell内置了,那为什么还要有/bin/echo这种外部程序呢?理由是:首先可能会在shell之外使用echo,另外,没人要求UNIX shell必须有echo这个内置命令,所以需要外置程序/bin/echo兜底。

在实际中,用户经常会遇到这样一个疑问:如何判断执行的命令是内置还是外置的呢?Bash命令type(它本身是一个内置命令)会指示一个命令的来源。例如:

$ type echo
echo is a shell builtin
$ type ls
ls is hashed (/bin/ls)
$

Bash有如下原则:如果一个命令有对应的内置命令,内置命令会被优先执行。如果没有内置命令,Bash会去寻找外置程序并执行。如果你就是想用外置程序,而它恰好有内置命令,则需要指明外置程序的完整路径。

变量赋值

当Bash中输入一个命令,Bash会期望其遇到当第一个单词是一个命令。唯一的例外是:当第一个单词中包含=,Bash此时会执行变量赋值。例如:

$ VAR=7
$

这将会把名为VAR的变量赋值为7。如果想使用这个变量,则需要在变量名前面加一个$。比如,你可以像下面这样,使用echo查看刚才的变量。

$ echo $VAR
7
$

给变量赋值,确保赋值语句是含有=的“连续”的字符串很重要(不能有空格)。下面是错误的示例:

$ VAR = 1
bash: VAR: command not found
$

在这个例子里,Bash将输入VAR = 1切分为3个词VAR,=,1,并尝试把第一个词作为命令来执行。这显然不是我们的预期。

内置变量 "?"

Bash允许你通过简单地赋值来动态创建变量,同时,它也有许多内置变量。比如BASHPID。它保存着Bash shell的进程ID。

$ echo $BASHPID
2141
$

另一个内置变量是?,它也是我将重点讲的。在Bash会话的任意地方,这个变量都保存着最近一次执行的命令的返回值。这个值永远都是整型。(具体的讲,它就是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请求kernel运行了一个外部程序(/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是一个命令,实际上它是Bash的内置命令。

$ type true
true is a shell builtin
$ help true
true: true
    Return a successful result.

    Exit Status:
    Always succeeds.

让我们关注这个事实:true是一个命令。实际上,这个true跟之前将?变量赋值为0的哪个true是相同的命令。所以,当if语句执行的时候,它实际上判断的是true这个命令执行的返回值。如果你不确信这一点,可以将true替换为外部命令/bin/true

$ if /bin/ture; then echo 'Yay true!'; fi
Yay true!
$

再看一下外置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,并且没有任何命令参数,所以输出了一个空行。你也可以为其添加参数:

$ if /bin/echo 'Hi'; then echo 'Yay true!'; fi
Hi
Yay true!
$

两个echo命令的执行是不同的:第一个是外部命令/bin/echo;第二个出现在if语句体中的是shell内置命令echo,当然这个echo也可以被替换为外部命令。

让我们继续,之前我提到过Bash把赋值语句视为一个命令。因此在内部命令或者外部命令出现的地方也可以使用赋值语句:

$ if VAR=99; then echo 'Assignment done!'; fi
Assignment done!
$ echo $VAR
99
$

总结一下,if条件语句的通俗模式是:if CMD1; then CMD2; fi,其中CMD1CMD2是命令。if语句通过判断命令CMD1的返回值确定是否执行CMD2。这跟大多数编程语言的真值判断不同,这是shell让人困惑的一个重要原因。我应该称之为困惑之源第一

false命令

之前提到了true命令,不意外,也有一个false命令,执行跟true相反的操作,也是一个内置命令:

$ 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.

truefalse命令除了返回状态值0或者1之外并没有任何其他操作。因此if条件句通过判断返回值来决定是否执行语句体,if true永远是判真的,if false永远判否。注意true的返回值是0,false的返回值是1。有点反直觉,跟大多数编程语言的真假值相反。比如,Python中的真值判断,0等价于False,1等价于True。

if 用作返回值的判断

让我们通过编写一个返回1的C程序true.c来确认Bash中的if语句只是测试程序的返回值。(注意,真正的程序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的设定,非0的结束状态表示失败。我将此称之为困惑之源第二

更多的精巧之处

考虑以下场景:判断文件是否存在,如果存在则删除。为此可以使用附加-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是内置的。你可能也会想到,同时有一个外置命令/user/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 CMD1; CMD2; ... CMDN; then CMDN+1; CMDN+2; ... CMDN+M; fiif条件句依次执行命令序列,当且仅当最后一个命令返回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后面的三个命令,因为最后一个命令是false,返回的是非0,所以不会执行语句体(rm dum.txt)中的命令。总结一下,在任何可以使用一个命令的地方,你都可以使用一个命令序列。命令序列的返回状态就是最后一个命令的返回状态。我将此成为困惑之源第三

循环语句whileuntil

理解if条件句的行为将会很有帮助,因为whileuntil循环的行为非常类似。看以下例子:

$ 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永远返回0,所以后续会无限循环(直至被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和Bash内置命令echo

如你所料,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后面跟随的命令返回一个成功的结束状态(也就是返回0)。因为false永远都返回非0,所以这是个无限循环。当然,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(the Thompson shell)仅仅是一个命令解释器。后续的Mashey shell和Bourne shell
引入了编程功能。因此Bash可以说是Bourne shell的直系后代,继承了后者所有关于可编程性的核心特性。其中之一就是如何将编程语言和命令解释器融合。也是因为这个目的,UNIX shell是有诸多精巧的设计的。

Bash中,编程结构看起来跟其他传统的编程语言类似。但是,这些结构背后的执行是非常异于常规的。这会让有其他编程基础的人来使用Bash编程并不太习惯(接触Bash的大多数人都在此列)。这里是Bash编程中最主要的三个容易让人困惑的地方:

  1. Bash编程的一个让人意外的特点是,ifwhileuntil结构判断的是命令的返回状态。简单说,这些结构判断的是“执行程序的返回状态是否是0?”。在UNIX体系中,返回值0代表成功,其他值则代表失败。
  2. 返回状态是可执行程序返回的一个整型——可以联想C语言main()函数的返回值。请注意,一个程序顺利执行也许仍然会返回非零状态(我在上文中给出了一个示例)。但是这种写法是极不提倡的。这会破坏系统的约定俗成。因为整个系统都遵守程序运行成功返回0的约定,因此一个例外可能会导致严重后果。
  3. 能使用一个单独命令的地方都可以合法使用一个由分号分割的命令序列。如果使用命令序列,序列中最后一个命令的返回状态被视为这个命令序列的返回状态。

致谢

非常感谢Chet Ramey对这篇文章草稿的意见。同时感谢Isidora C. Likic校对本文的文本和用例。

额外资源

有太多的文章和书籍可以列出,但如果你对进一步学习Bash感兴趣,我们推荐以下 Linux Journal 的文章 ( LJ 的文章也太多来,这里是一些你可以起步的地方)

"Creating the Concentration Game PAIRS with Bash" by Dave Taylor
"Create Dynamic Wallpaper with a Bash Script" by Patrick Wheelan
"Developing Console Applications with Bash" by Andy Carlson
"Hacking a Safe with Bash" by Adam Kosmin
"Ubuntu Linux and Bash as a Windows Program!" by Dave Taylor
"Bash Parameter Expansion" by Mitch Frazier
"Bash Regular Expressions" by Mitch Frazier
"Bash Extended Globbing" by Mitch Frazier
"My Favorite bash Tips and Tricks" by Prentice Bisbal
"Parsing an RSS News Feed with a Bash Script" by Jim Hall

"Getting Loopy with Bash: Using for Loops" by Shawn Powers
"Bash Startup Scripts: bashrc and bash_profile" by Shawn Powers