逝雪蓝冰

《Linux程序设计(第4版)》第2章 shell程序设计
自己买了这本书,看到第2章不错的样子,在网上找到原版,这章刚好是图书出版方主动开放的免费章节,所以贴出来给自己看看...
扫描右侧二维码阅读全文
09
2018/11

《Linux程序设计(第4版)》第2章 shell程序设计

自己买了这本书,看到第2章不错的样子,在网上找到原版,这章刚好是图书出版方主动开放的免费章节,所以贴出来给自己看看,随便练练自己的 Markdown 排版技术。

《Linux程序设计(第4版)》

我们在本书的开始刚刚介绍了用C语言进行Linux程序设计,现在却要调转方向学习编写shell程序,这是为什么?在其他的一些操作系统中,命令行界面只是对图形化界面的一个补充。但对于Linux而言,却并非如此。作为Linux灵感来源的UNIX系统最初根本就没有图形化界面,所有的任务都是通过命令行来完成的。因此,UNIX的命令行系统得到了很大的发展,并且成为一个功能强大的系统。Linux系统沿袭了这一特点,许多强大的功能都可以从shell中轻松实现。因为shell对Linux是如此的重要,并且对自动化简单的任务非常有用,所以我们认为应该尽早介绍shell程序设计。

在本章中,我们将通过一些交互性(基于屏幕)的例子来向读者展示编写shell程序时要用到的语法、结构和命令。这些内容将成为对shell主要特性及其效果的一个很有用的概要介绍。同时,我们也顺便介绍两个在shell中经常用到的特别有用的命令行工具:grepfind。在介绍grep时,我们还将介绍正则表达式的基础知识,它在Linux的工具和程序设计语言(如Perl、Ruby和PHP)中都有应用。在本章的最后,你将学习如何编写一个真正的脚本程序,本书的后续章节里将用C语言对它进行重写和扩充。本章将介绍以下内容:

  • 什么是shell
  • 基本思路
  • 微妙的语法:变量、条件判断和程序控制
  • 命令列表
  • 函数
  • 命令和命令的执行
  • here文档
  • 调试
  • grep命令和正则表达式
  • find命令  

因此,无论你是在系统管理工作中正面对着复杂的shell脚本,或是想实现自己最新的了不起(但其实是非常简单)的想法,或只是想加快完成一些重复性的任务,本章对你都很适用。

2.1 为什么使用shell编程

使用shell进行程序设计的原因之一是,你可以快速、简单地完成编程。而且,即使是最基本的Linux安装也会提供一个shell。因此,如果你有一个简单的构想,则可以通过它来检查自己的想法是否可行。shell也非常适合于编写一些执行相对简单的任务的小工具,因为它们更强调的是易于配置、易于维护和可移植性,而不是很看重执行的效率。你还可以使用shell对进程控制进行组织,使命令按照预定顺序在前一阶段命令成功完成的前提下顺序执行。

虽然shell表面上和Windows的命令提示符相似,但是它具备更强大的功能以完成相当复杂的程序。你不仅可以通过它执行命令、调用Linux工具,还可以自己编写程序。shell执行shell程序,这些程序通常被称为脚本,它们是在运行时解释执行的。这使得调试工作比较容易进行,因为你可以逐行地执行指令,而且节省了重新编译的时间。然而,这也使得shell不适合用来完成时间紧迫型和处理器忙碌型的任务。

2.2 一点哲学

现在,我们来关注一点UNIX(当然也是Linux)的哲学。UNIX架构非常依赖于代码的高度可重用性。如果你编写了一个小巧而简单的工具,其他人就可以将它作为一根链条上的某个环节来构成一条命令。Linux让用户满意的原因之一就是它提供了各种各样的优秀工具。下面是一个简单的例子:

$ ls -al | more

这个命令使用了lsmore工具并通过管道实现了文件列表的分屏显示。每个工具就是一个组成部件。通常你可以将许多小巧的脚本程序组合起来以创建一个庞大而复杂的程序。

例如,如果你想打印bash使用手册的参考副本,可以使用如下命令:

$ man bash | col -b | lpr

此外,因为Linux具备自动文件类型处理功能,所以使用这些工具的用户一般不必了解它们是用哪种语言编写的。如果想要这些工具运行得更快,常见的做法是首先在shell中实现工具的原型,一旦确定值得这么做,然后再用C或C++、Perl、Python或者其他执行得更快速的语言来重新实现它们。相反,如果在shell中这些工具工作得已足够好,就不必再重新实现它们。

是否需要重新实现脚本程序取决于你是否需要对它进行优化,是否需要将程序移植到其他系统,是否需要让它更易于修改以及它是否偏离了其最初的设计目的(这种情况经常发生)。

如果你对shell脚本充满好奇,Linux系统中已经装有许多的shell脚本例子,包括软件包安装程序、.xinitrcstartx文件以及/etc/rc.d目录中用于启动时配置系统的脚本程序。

2.3 什么是shell

在开始讨论如何使用shell进行程序设计之前,我们先来回顾一下shell的作用以及Linux系统中提供的各种shell。shell是一个作为用户与Linux系统间接口的程序,它允许用户向操作系统输入需要执行的命令。这点与Windows的命令提示符类似,但正如先前所提到的,Linux shell的功能更强大。例如,我们可以使用<>对输入输出进行重定向,使用|在同时执行的程序之间实现数据的管道传递,使用$(...)获取子进程的输出。在Linux中安装多个shell是完全可行的,用户可以挑选一种自己喜欢的shell来使用。图2-1显示了shell(实际上是两种shell:bash和csh)和其他程序环绕在Linux内核的四周。


图 2-1

由于Linux是高度模块化的系统,所以你可以从各种不同的shell中选择一种来使用,虽然它们中的大多数都是从最初的Bourne shell演变而来的。在Linux系统中,总是作为/bin/sh安装的标准shell是GNU工具集中的bash(GNU Bourne-Again Shell)。因为它作为一个优秀的shell,总是安装在Linux系统上,而且它是开源的并且可以被移植到几乎所有的类UNIX系统上,所以我们把它作为将要使用的shell。在本章中,我们将使用bash的第3版,并且在大多数情况下只使用那些所有POSIX兼容的shell都具备的功能。我们假设bash被安装为/bin/sh并且它是你的登录所使用的默认shell。在大多数Linux发行版中,默认的shell程序/bin/sh实际上是对程序/bin/bash的一个连接。

你可以使用如下命令来查看bash的版本号:

$ /bin/bash --version
GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

如果你想要切换到另一个shell(例如,bash不是你的系统中默认的shell),你只需直接执行需要的shell程序(例如,/bin/bash)就可以运行新的shell并且改变命令提示符了。如果你使用的是UNIX系统并且bash没有被安装,你可以从GNU Web网站www.gnu.org上免费下载它。它的源代码具有高度的可移植性,它在你的UNIX版本上编译成功几乎不会有什么问题。

当你创建Linux用户时,你可以设置这个用户要使用的shell,这个工作既可以在创建用户时完成,也可以在创建用户之后,通过修改用户信息来完成。图2-2显示了使用Fedora选择用户shell的界面。


图 2-2

还有许多免费的或商业的shell可以使用,表2-1对常用的shell做了一个简单的总结。

表 2-1

shell名称相关历史
sh(Bourne)源于UNIX早期版本的最初的shell
csh、tcsh、zshC shell及其变体,最初是由Bill Joy在Berkeley UNIX上创建的。它可能是继bash和Korn shell之后第三个最流行的shell
ksh、pdkshkorn shell和它的公共域兄弟pdksh(public domain korn shell)由David Korn编写,它是许多商业版本UNIX的默认shell
bash来自GNU项目的bash或Bourne Again Shell是Linux的主要shell。它的优点是可以免费获取其源代码,即使你的UNIX系统目前没有运行它,它也很可能已经被移植到该系统中。bash与Korn shell有许多相似之处

除了C shell和少数变体以外,所有这些shell都很相似,并且都与X/Open 4.2和POSIX 1003.2规范中对于shell的规定非常一致。POSIX 1003.2对于shell的规定很少,但在X/Open中的扩展规定则提供了一个更加友好、功能更加强大的shell。X/Open通常是一个提出更多要求的规范,但遵循它的系统也更加友好。

2.4 管道和重定向

在深入探讨shell程序设计的细节之前,我们需要先介绍一下如何才能对Linux程序(不仅仅是shell程序)的输入输出进行重定向。

2.4.1 重定向输出

读者可能已经对某些类型的重定向比较熟悉了,例如:

$ ls -l > lsoutput.txt

这条命令把ls命令的输出保存到文件lsoutput.txt中。

然而,重定向所包含的内容可比这个简单的例子所显示的要多得多。你将在第3章学习更多关于标准文件描述符的内容,现在你只需知道文件描述符0代表一个程序的标准输入,文件描述符1代表标准输出,而文件描述符2代表标准错误输出。你可以单独地重定向其中任何一个。事实上,你还可以重定向其他文件描述符,但对标准文件描述符0、1、2以外的文件描述符进行重定向的情况很少见。

上面的例子通过>操作符把标准输出重定向到一个文件。在默认情况下,如果该文件已经存在,它的内容将被覆盖。如果你想改变默认行为,你可以使用命令set –o noclobber(或set –C)设置noclobber选项,从而阻止重定向操作对一个已有文件的覆盖。你可以使用set +o noclobber命令取消该选项。你将在本章后面的内容中看到更多的set命令选项。

你可以用>>操作符将输出内容附加到一个文件中。例如:

$ ps >> lsoutput.txt

这条命令会将ps命令的输出附加到指定文件的尾部。

如果想对标准错误输出进行重定向,你需要把想要重定向的文件描述符编号加在>操作符的前面。因为标准错误输出的文件描述符编号是2,所以使用2>操作符。当需要丢弃错误信息并阻止它显示在屏幕上时,这个方法很有用。

假设你想用kill命令在一个脚本程序里终止一个进程,那么总是存在这种可能性,即在kill命令执行之前,那个需要终止的进程就已经结束了。如果出现这种情况,kill命令将向标准错误输出写一条错误信息,并且在默认情况下,这条信息将会显示在屏幕上。通过对标准输出和标准错误输出都进行重定向,你就可以阻止kill命令向屏幕上写任何内容了。

下面的命令将把标准输出和标准错误输出分别重定向到不同的文件中:

$ kill -HUP 1234 >killout.txt 2>killerr.txt

如果你想把两组输出都重定向到一个文件中,你可以用>&操作符来结合两个输出。如下所示:

$ kill -1 1234 >killouterr.txt 2>&1

这条命令将把标准输出和标准错误输出都重定向到同一个文件中。请注意操作符出现的顺序。这条命令的含义是“将标准输出重定向到文件killouterr.txt,然后将标准错误输出重定向到与标准输出相同的地方。”如果顺序有误,重定向将不会按照你预期的那样执行。

因为可以通过返回码(我们将在本章的后面对其进行详细介绍)来了解kill命令的执行结果,所以通常并不需要保存标准输出或标准错误输出的内容。你可以用Linux的通用“回收站”/dev/null来有效地丢弃所有的输出信息,如下所示:

$ kill -1 1234 >/dev/null 2>&1

2.4.2 重定向输入

你不仅可以重定向标准输出,还可以重定向标准输入。例如:

$ more < killout.txt

很明显,在Linux下这样做意义不大,因为Linux的more命令可以接受文件名作为参数,这与Windows命令行中对应的命令不同。

2.4.3 管道

你可以用管道操作符|来连接进程。Linux与MS-DOS不同,在Linux下通过管道连接的进程可以同时运行,并且随着数据流在它们之间的传递可以自动地进行协调。举一个简单的例子,你可以使用sort命令对ps命令的输出进行排序。

如果不使用管道,你就必须分几个步骤来完成这个任务,如下所示:

$ ps > psout.txt
$ sort psout.txt > pssort.out

一个更精巧的解决方案是用管道来连接进程,如下所示:

$ ps | sort > pssort.out

如果想在屏幕上分页显示输出结果,你可以再连接第三个进程more,将它们都放在同一个命令行上,如下所示:

$ ps | sort | more

允许连接的进程数目是没有限制的。假设你想看看系统中运行的所有进程的名字,但不包括shell本身,可以使用下面的命令:

$ ps –xo comm | sort | uniq | grep -v sh | more

这个命令首先按字母顺序排序ps命令的输出,再用uniq命令去除名字相同的进程,然后用grep –v sh命令删除名为sh的进程,最终将结果分页显示在屏幕上。

如你所见,与使用一系列单独的命令并且每个命令都带有自己的临时文件相比,这是一个更精巧的解决方案。但这里有一点需要引起注意:如果你有一系列的命令需要执行,相应的输出文件是在这一组命令被创建的同时立刻被创建或写入的,所以决不要在命令流中重复使用相同的文件名。如果你尝试执行如下命令:

cat mydata.txt | sort | uniq > mydata.txt

你最终将得到一个空文件,因为你在读取文件mydata.txt之前就已经覆盖了这个文件的内容。

2.5 作为程序设计语言的shell

现在你已了解了一些基本的shell操作,是时候开始介绍一些真正的shell脚本程序了。编写shell脚本程序有两种方式。你可以输入一系列命令让shell交互地执行它们,也可以把这些命令保存到一个文件中,然后将该文件作为一个程序来调用。

2.5.1 交互式程序

在命令行上直接输入shell脚本是一种测试短小代码段的简单而快捷的方式。如果你正在学习shell脚本或仅仅是为了进行测试,使用这种方式是非常有用的。

假设你想要从大量C语言源文件中查找包含字符串POSIX的文件。与其使用grep命令在每个文件中搜索字符串,然后再分别列出包含该字符串的文件,不如用下面的交互式脚本来执行整个操作:

$ for file in *
> do
> if grep -l POSIX $file
> then
> more $file
> fi
> done
posix
This is a file with POSIX in it - treat it well
$

请注意,当shell期待进一步的输入时,正常的$shell提示符将改变为>提示符。你可以一直输入下去,由shell来判断何时输入完毕并立刻执行脚本程序。

在这个例子中,grep命令输出它找到的包含POSIX字符串的文件,然后more命令将文件的内容显示在屏幕上。最后,返回shell提示符。还要注意的是,你用shell变量来处理每个文件,以使该脚本自文档化。你也可以将变量名起为i,但是变量名file更容易理解。

shell还提供了通配符扩展(通常称为globbing)。你一定已注意到可以用通配符*来匹配一个字符串。但是你可能不知道可以用通配符?来匹配单个字符,而[set]允许匹配方括号中任何一个单个字符,[^set]对方括号中的内容取反,即匹配任何没有出现在给出的字符集中的字符。扩展的花括号{}(只能用在部分shell中,其中包括bash)允许你将任意的字符串组放在一个集合中,以供shell进行扩展。例如:

$ ls my_{finger,toe}s

这个命令将列出文件my_fingersmy_toes,它使用shell来检查当前目录下的每个文件。当我们在本章结尾详细介绍grep命令和正则表达式的强大功能时,我们将回过头来再次研究匹配模式中的这些规则。

有经验的Linux用户可能会用一种更有效的方式来执行这个简单的操作。也许使用如下的命令:

$ more `grep -l POSIX *`

或使用功能相同的另一种命令形式:

$ more $(grep -l POSIX *)

此外,下面的命令将输出包含POSIX字符串的文件名:

$ grep -l POSIX * | more

在上面的脚本中,你看到shell利用其他命令(如grepmore)来完成主要的工作。shell本身只是允许你将几个现有的命令结合在一起,以构成一个新的功能强大的命令。你将在后面的脚本示例中看到通配符扩展的多次应用,并且我们还将在本章中介绍grep命令和正则表达式时详细讨论整个扩展的细节。

如果每次想要执行一系列命令时,你都要经过这么一个冗长的输入过程,将非常令人烦恼。你需要将这些命令保存到一个文件中,即我们常说的shell脚本,这样你就可以在需要的时候随时执行它们了。

2.5.2 创建脚本

首先,你必须用一个文本编辑器来创建一个包含命令的文件,将其命名为first,它的内容如下所示:

#!/bin/sh

# first
# This file looks through all the files in the current
# directory for the string POSIX, and then prints the names of
# those files to the standard output.

for file in *
do
  if grep -q POSIX $file
  then
    echo $file
  fi
done

exit 0

程序中的注释以#符号开始,一直持续到该行的结束。按照惯例,我们通常把#放在第一列。在作出这样一个笼统的陈述之后,请注意第一行#!/bin/sh,它是一种特殊形式的注释,#!字符告诉系统同一行上紧跟在它后面的那个参数是用来执行本文件的程序。在这个例子中,/bin/sh是默认的shell程序。

请注意注释中使用的是绝对路径。考虑到向后兼容性,这个路径按惯例最好不要超过32个字符,因为一些老版本的UNIX在使用# !时只能使用这个限制之内的字符数,虽然Linux通常不存在这样的限制。

因为脚本程序本质上被看作是shell的标准输入,所以它可以包含任何能够通过你的PATH环境变量引用到的Linux命令。

exit命令的作用是确保脚本程序能够返回一个有意义的退出码(在本章的后面将对此进行详细介绍)。当程序以交互方式运行时,我们很少需要检查它的退出码,但如果你打算从另一个脚本程序里调用这个脚本程序并查看它是否执行成功,那么返回一个适当的退出码就很重要了。即使你从来也没打算允许你的脚本程序被另一个脚本程序调用,你也应该在退出时返回一个合理的退出码。请相信自己的脚本程序是有用的,它总有一天会作为其他脚本程序的一部分而被重用。

在shell程序设计里,0表示成功。因为这个脚本程序并不能检查到任何错误,所以它总是返回一个表示成功的退出码。我们将在本章后面详细介绍exit命令时,再回过头来解释用0表示成功的原因。

请注意,这个脚本没有使用任何的文件扩展名或后缀。一般情况下,Linux和UNIX很少利用文件扩展名来决定文件的类型。你可以为脚本使用.sh或者其他扩展名,但shell并不关心这一点。大多数预安装的脚本程序并没有使用任何文件扩展名,检查这些文件是否是脚本程序的最好方法是使用file命令,例如,file firstfile /bin/bash。你可以使用任何适用于你的工作环境或适合于你的方式。

2.5.3 把脚本设置为可执行

现在你已经有了自己的脚本文件,运行它有两种方法。比较简单的方法是调用shell,并把脚本文件名当成一个参数,如下所示:

$ /bin/sh first

这可以工作,但如果能像对待其他Linux命令那样,只输入脚本程序的名字就可以调用它就更好了。你可以使用chmod命令来改变这个文件的模式,使得这个文件可以被所有用户执行,如下所示:

$ chmod +x first

当然,这并不是使用chmod命令将一个文件设置为可执行的唯一方式,请用man chmod命令查看它的八进制参数和其他选项用法。

然后你可以用下面的命令来执行它:

$ first

你可能会看到一条错误信息告诉你未找到命令。这种情况很可能发生,因为shell环境变量PATH并没有被设置为在当前目录下查找要执行的命令。要解决这个问题,一种办法是在命令行上直接输入命令PATH=$PATH:.或编辑你的.bash_profile文件,将刚才这条命令添加到文件的末尾,然后退出登录后再重新登录进来。另外,你也可以在保存脚本程序的目录中输入命令./first,该命令的作用是把脚本程序的完整的相对路径告诉shell。

./来指定路径还有另一个好处,它能够保证你不会意外执行系统中与你的脚本文件同名的另一个命令。

你不应该用这种方法来修改超级用户(一般其用户名为root)的PATH变量。这是一个安全方面的漏洞,因为以root用户身份登录的系统管理员可能会因此误调用了某个标准命令的伪装版本。本书其中一位作者就曾经这样做过一次,目的当然是为了向系统管理员指出这一点!即使对于普通用户,把当前目录包括在PATH变量中也多少有些危险。因此,如果你非常关心系统的安全,最好的办法是养成在执行当前目录中的所有命令时,在其前面都加上一个./的好习惯。

在确信你的脚本程序能够正确执行后,你可以把它从当前目录移到一个更合适的地方去。如果这个命令只供你本人使用,你可以在自己的家目录中创建一个bin目录,并且将该目录添加到你自己的PATH变量中。如果你想让其他人也能够执行这个脚本程序,你可以将/usr/local/bin或其他系统目录作为添加新程序的适当位置。如果你在系统上没有root权限,你可以要求系统管理员帮你复制你的文件,当然你首先必须让他们相信这些程序的价值才行。为了防止其他用户修改脚本程序,哪怕只是意外地修改,你也应该去掉脚本程序的写权限。系统管理员用来设置文件属主和访问权限的一系列命令如下所示:

# cp first /usr/local/bin
# chown root /usr/local/bin/first
# chgrp root /usr/local/bin/first
# chmod 755 /usr/local/bin/first

注意,你在这里不是修改访问权限标志的特定部分,而是使用chmod命令的绝对格式,因为你清楚地知道你需要的访问权限。

如果你愿意,还可以使用chmod命令相对长一些但可能含义更明确的格式,如下所示:

# chmod u=rwx,go=rx /usr/local/bin/first

更多chmod命令的详细资料请参考它的使用手册。

在Linux系统中,如果你拥有包含某个文件的目录的写权限,就可以删除这个文件。为安全起见,应该确保只有超级用户才能对你想保证文件安全的目录执行写操作。因为目录只是另一种类型的文件,所以拥有对一个目录文件写权限的用户可以添加和删除目录文件中的名称。

2.6 shell的语法

现在你已看过一个简单的shell程序示例,是时候来深入研究shell强大的程序设计能力了。shell是一种很容易学习的程序设计语言,它可以在把各个小程序段组合为一个大程序之前就能很容易地对它们分别进行交互式的测试。你还可以用bash shell编写出相当庞大的结构化程序。在接下来的几节里,我们将学习以下内容:

  • 变量:字符串、数字、环境和参数
  • 条件:shell中的布尔值
  • 程序控制:if、elif、for、while、until、case
  • 命令列表
  • 函数
  • shell内置命令
  • 获取命令的执行结果
  • here文档

2.6.1 变量

在shell里,使用变量之前通常并不需要事先为它们做出声明。你只是通过使用它们(比如当你给它们赋初始值时)来创建它们。在默认情况下,所有变量都被看作字符串并以字符串来存储,即使它们被赋值为数值时也是如此。shell和一些工具程序会在需要时把数值型字符串转换为对应的数值以对它们进行操作。Linux是一个区分大小写的系统,因此shell认为变量fooFoo是不同的,而这两者与FOO又是不同的。

在shell中,你可以通过在变量名前加一个$符号来访问它的内容。无论何时你想要获取变量内容,你都必须在它前面加一个$字符。当你为变量赋值时,你只需要使用变量名,该变量会根据需要被自动创建。一种检查变量内容的简单方式就是在变量名前加一个$符号,再用echo命令将它的内容输出到终端上。

在命令行上,你可以通过设置和检查变量salutation的不同值来实际查看变量的使用:

$ salutation=Hello
$ echo $salutation
Hello
$ salutation="Yes Dear"
$ echo $salutation
Yes Dear
$ salutation=7+5
$ echo $salutation
7+5

注意,如果字符串里包含空格,就必须用引号把它们括起来。此外,等号两边不能有空格。

你可以使用read命令将用户的输入赋值给一个变量。这个命令需要一个参数,即准备读入用户输入数据的变量名,然后它会等待用户输入数据。通常情况下,在用户按下回车键时,read命令结束。当从终端上读取一个变量时,你一般不需要使用引号,如下所示:

$ read salutation
Wie geht's?
$ echo $salutation
Wie geht's?

1.使用引号

在继续学习之前,你先需要弄清楚shell的一个特点:引号的使用。

一般情况下,脚本文件中的参数以空白字符分隔(例如,一个空格、一个制表符或者一个换行符)。如果你想在一个参数中包含一个或多个空白字符,你就必须给参数加上引号。

$foo这样的变量在引号中的行为取决于你所使用的引号类型。如果你把一个$变量表达式放在双引号中,程序执行到这一行时就会把变量替换为它的值;如果你把它放在单引号中,就不会发生替换现象。你还可以通过在$字符前面加上一个\字符以取消它的特殊含义。

字符串通常都被放在双引号中,以防止变量被空白字符分开,同时又允许$扩展。

实验 变量的使用

这个例子显示了引号在变量输出中的作用:

#!/bin/sh

myvar="Hi there"

echo $myvar
echo "$myvar"
echo '$myvar'
echo \$myvar

echo Enter some text
read myvar

echo '$myvar' now equals $myvar
exit 0

输出结果如下:

$ ./variable
Hi there
Hi there
$myvar
$myvar
Enter some text
Hello World
$myvar now equals Hello World

实验解析

变量myvar在创建时被赋值字符串Hi there。你用echo命令显示该变量的内容,同时显示了在变量名前加一个$符号就能得到变量的内容。你看到使用双引号并不影响变量的替换,但使用单引号和反斜线就不进行变量的替换。你还使用read命令从用户那里读入一个字符串。

2.环境变量

当一个shell脚本程序开始执行时,一些变量会根据环境设置中的值进行初始化。这些变量通常用大写字母做名字,以便把它们和用户在脚本程序里定义的变量区分开来,后者按惯例都用小写字母做名字。具体创建的变量取决于你的个人配置。在系统的使用手册中列出了许多这样的环境变量,表2-2列出了其中一些主要的变量。

表 2-2

环境变量说明
$HOME当前用户的家目录
$PATH以冒号分隔的用来搜索命令的目录列表
$PS1命令提示符,通常是$字符,但在bash中,你可以使用一些更复杂的值。例如,字符串[\u@\h \W]$就是一个流行的默认值,它给出用户名、机器名和当前目录名,当然也包括一个$提示符
$PS2二级提示符,用来提示后续的输入,通常是>字符
$IFS输入域分隔符。当shell读取输入时,它给出用来分隔单词的一组字符,它们通常是空格、制表符和换行符
$0shell脚本的名字
$#传递给脚本的参数个数
$$shell脚本的进程号,脚本程序通常会用它来生成一个唯一的临时文件,如/tmp/tmpfile_$$

如果想通过执行env <command>命令来查看程序在不同环境下是如何工作的,请查阅env命令的使用手册。你也将在本章的后面看到如何使用export命令在子shell中设置环境变量。

3.参数变量

如果脚本程序在调用时带有参数,一些额外的变量就会被创建。即使没有传递任何参数,环境变量$#也依然存在,只不过它的值是0罢了。

参数变量见表2-3。

表 2-3

参数变量说明
$1, $2, ...脚本程序的参数
$*在一个变量中列出所有的参数,各个参数之间用环境变量IFS中的第一个字符分隔开。如果IFS被修改了,那么$*将命令行分割为参数的方式就将随之改变
$@它是$*的一种精巧的变体,它不使用IFS环境变量,所以即使IFS为空,参数也不会挤在一起

通过下面的例子,你可以很容易地看出$@$*之间的区别:

$ IFS=''
$ set foo bar bam
$ echo "$@"
foo bar bam
$ echo "$*"
foobarbam
$ unset IFS
$ echo "$*"
foo bar bam

如你所见,双引号里面的$@把各个参数扩展为彼此分开的域,而不受IFS值的影响。一般来说,如果你想访问脚本程序的参数,使用$@是明智的选择。

除了使用echo命令查看变量的内容外,你还可以使用read命令来读取它们。

实验 使用参数和环境变量

下面的脚本程序演示了一些简单的变量操作。当输入脚本程序的内容并把它保存为文件try_var后,别忘了用chmod +x try_var命令把它设置为可执行。

#!/bin/sh

salutation="Hello"
echo $salutation
echo "The program $0 is now running"
echo "The second parameter was $2"
echo "The first parameter was $1"
echo "The parameter list was $*"
echo "The user's home directory is $HOME"

echo "Please enter a new greeting"
read salutation

echo $salutation
echo "The script is now complete"
exit 0

运行这个脚本程序,你将得到如下所示的输出结果:

$ ./try_var foo bar baz
Hello
The program ./try_var is now running
The second parameter was bar
The first parameter was foo
The parameter list was foo bar baz
The user's home directory is /home/rick
Please enter a new greeting
Sire
Sire
The script is now complete
$

实验解析

这个脚本程序创建变量salutation并显示它的内容,然后显示各种参数变量以及环境变量$HOME都已存在并有了适当的值。

我们将在后面进一步介绍参数替换。

2.6.2 条件

所有程序设计语言的基础是对条件进行测试判断,并根据测试结果采取不同行动的能力。在讨论它之前,我们先来看看在shell脚本程序里可以使用的条件结构,然后再来看看使用这些条件的控制结构。

一个shell脚本能够对任何可以从命令行上调用的命令的退出码进行测试,其中也包括你自己编写的脚本程序。这也就是为什么要在所有自己编写的脚本程序的结尾包括一条返回值的exit命令的重要原因。

test[命令

在实际工作中,大多数脚本程序都会广泛使用shell的布尔判断命令[test。在一些系统上,这两个命令的作用是一样的,只是为了增强可读性,当使用[命令时,我们还使用符号]来结尾。把[符号当作一条命令多少有点奇怪,但它在代码中确实会使命令的语法看起来更简单、更明确、更像其他的程序设计语言。

在一些老版本的UNIX shell中,这些命令调用的是一个外部程序,但在较新的shell版本中,它们已成为shell的内置命令。我们将在本章后面介绍各种命令时再次讨论这个问题。

因为test命令在shell脚本程序以外用得很少,所以那些很少编写shell脚本的Linux用户往往会将自己编写的简单程序命名为test。如果程序不能正常工作,很可能是因为它与shell中的test命令发生了冲突。要想查看系统中是否有一个指定名称的外部命令,你可以尝试使用which test这样的命令来检查执行的是哪一个test命令,或者使用./test这种执行方式以确保你执行的是当前目录下的脚本程序。如有疑问,你只需养成在调用脚本的前面加上./的习惯即可。

我们以一个最简单的条件为例来介绍test命令的用法:检查一个文件是否存在。用于实现这一操作的命令是test –f <filename>,所以在脚本程序里,你可以写出如下所示的代码:

if test -f fred.c
then
...
fi

你还可以写成下面这样:

if [ -f fred.c ]
then
...
fi

test命令的退出码(表明条件是否被满足)决定是否需要执行后面的条件代码。

注意,你必须在[符号和被检查的条件之间留出空格。要记住这一点,你可以把[符号看作和test命令一样,而test命令之后总是应该有一个空格。

如果你喜欢把thenif放在同一行上,就必须要用一个分号把test语句和then分隔开。如下所示:

       if [ -f fred.c ]; then
       ...
       fi

test命令可以使用的条件类型可以归为3类:字符串比较、算术比较和与文件有关的条件测试,表2-4、表2-5和表2-6描述了这3种条件类型。

表 2-4

字符串比较结果
string1 = string2如果两个字符串相同则结果为真
string1 != string2如果两个字符串不同则结果为真
-n string如果字符串不为空则结果为真
-z string如果字符串为null(一个空串)则结果为真

表 2-5

算术比较结果
expression1 -eq expression2如果两个表达式相等则结果为真
expression1 -ne expression2如果两个表达式不等则结果为真
expression1 -gt expression2如果expression1大于expression2则结果为真
expression1 -ge expression2如果expression1大于等于expression2则结果为真
expression1 -lt expression2如果expression1小于expression2则结果为真
expression1 -le expression2如果expression1小于等于expression2则结果为真
! expression如果表达式为假则结果为真,反之亦然

表 2-6

文件条件测试结果
-d file如果文件是一个目录则结果为真
-e file如果文件存在则结果为真。要注意的是,历史上-e选项不可移植,所以通常使用的是-f选项
-f file如果文件是一个普通文件则结果为真
-g file如果文件的set-group-id位被设置则结果为真
-r file如果文件可读则结果为真
-s file如果文件的大小不为0则结果为真
-u file如果文件的set-user-id位被设置则结果为真
-w file如果文件可写则结果为真
-x file如果文件可执行则结果为真

读者可能想知道什么是set-group-idset-user-id(也叫做set-gidset-uid)位。set-uid位授予了程序其拥有者的访问权限而不是其使用者的访问权限,而set-gid位授予了程序其所在组的访问权限。这两个特殊位是通过chmod命令的选项ug设置的。set-gidset-uid标志对shell脚本程序不起作用,它们只对可执行的二进制文件有用。

我们稍微超前了一些,但是接下来的测试/bin/bash文件状态的例子可以让你看出如何使用它们:

#!/bin/sh

if [ -f /bin/bash ]
then
  echo "file /bin/bash exists"
fi

if [ -d /bin/bash ]
then
  echo "/bin/bash is a directory"
else
  echo "/bin/bash is NOT a directory"
fi

各种与文件有关的条件测试的结果为真的前提是文件必须存在。上述列表仅仅列出了test命令比较常用的选项,完整的选项清单请查阅它的使用手册。如果你使用的是bash,那么test命令是shell的内置命令,使用help test命令可以获得test命令更详细的信息。我们将在本章后面用到这里给出的部分选项。

现在你已学习了“条件”,下面你将看到使用它们的控制结构。

2.6.3 控制结构

shell有一组控制结构,它们与其他程序设计语言中的控制结构很相似。

在下面的各小节中,各语句的语法中的statements表示whenwhileuntil测试条件满足时,将要执行的一系列命令。

1.if语句

if语句非常简单:它对某个命令的执行结果进行测试,然后根据测试结果有条件地执行一组语句。如下所示:

if condition
then
  statements
else
  statements
fi

实验 使用if语句

if语句的一个常见用法是提一个问题,然后根据回答作出决定,如下所示:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

if [ $timeofday = "yes" ]; then
  echo "Good morning"
else
  echo "Good afternoon"
fi

exit 0

这将给出如下所示的输出:

Is it morning? Please answer yes or no
yes
Good morning
$

这个脚本程序用[命令对变量timeofday的内容进行测试,测试结果由if命令判断,由它来决定执行哪部分代码。

请注意,你用额外的空白符来缩进if结构内部的语句。这只是为了照顾人们的阅读习惯,shell会忽略这些多余的空白符。

elif语句

遗憾的是,上面这个非常简单的脚本程序存在几个问题。其中一个问题是,它会把所有不是yes的回答都看做是no。你可以通过使用elif结构来避免出现这样的情况,它允许你在if结构的else部分被执行时增加第二个检查条件。

实验 用elif结构做进一步检查

你可以对上面的脚本程序做些修改,让它在用户输入yesno以外的其他任何数据时报告一条出错信息。这是通过将else替换为elif并且增加另一个测试条件的方法来完成的。

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

if [ $timeofday = "yes" ]
then
  echo "Good morning"

elif [ $timeofday = "no" ]; then
  echo "Good afternoon"
else
  echo "Sorry, $timeofday not recognized. Enter yes or no"
  exit 1
fi

exit 0

实验解析

这个脚本程序与上一个例子很相似,但新增的elif命令会在第一个if条件不满足的情况下进一步测试变量。如果两次测试的结果都不成功,就打印一条出错信息并以1为退出码结束脚本程序,调用者可以在调用程序中利用这个退出码来检查脚本程序是否执行成功。

3.一个与变量有关的问题

刚才所做的修改弥补了一个非常明显的缺陷,但这个脚本程序还潜藏着一个更隐蔽的问题。运行这个新的脚本程序,但是这次不回答问题,而是直接按下回车键(或是某些键盘上的Return键)。你将看到如下所示的出错信息;

[: =: unary operator expected

哪里出问题了呢?问题就在第一个if语句中。在对变量timeofday进行测试的时候,它包含一个空字符串,这使得if语句成为下面这个样子:

if [ = "yes" ]

而这不是一个合法的条件。为了避免出现这种情况,你必须给变量加上引号,如下所示:

if [ "$timeofday" = "yes" ]

这样,一个空变量提供的就是一个合法的测试了:

if [ "" = "yes" ]

新脚本程序如下所示:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

if [ "$timeofday" = "yes" ]
then
  echo "Good morning"
elif [ "$timeofday" = "no" ]; then
  echo "Good afternoon"
else
  echo "Sorry, $timeofday not recognized. Enter yes or no"

  exit 1
fi

exit 0

这个脚本对用户直接按下回车键来回答问题的情况也能够应付自如了。

如果你想让echo命令去掉每一行后面的换行符,可移植性最好的办法是使用printf命令(请见本章后面的printf一节)而不是echo命令。有的shell用echo -e命令来完成这一任务,但并不是所有的系统都支持该命令。bash使用echo -n命令来去除换行符,所以如果确信自己的脚本程序只运行在bash上,你就可以使用如下的语法:

echo -n "Is it morning? Please answer yes or no: "

请注意,你需要在结束引号前留出一个额外的空格,这使得在用户输入响应前有一个间隙,从而看起来更加整洁。

4.for语句

我们可以用for结构来循环处理一组值,这组值可以是任意字符串的集合。它们可以在程序里被列出,更常见的做法是使用shell的文件名扩展结果。

它的语法很简单:

for variable in values
do
  statements
done

实验 使用固定字符串的for循环

循环值通常是字符串,所以你可以这样写程序:

#!/bin/sh

for foo in bar fud 43
do
  echo $foo
done
exit 0

该程序的输出结果如下所示:

bar
fud
43

如果你把第一行由for foo in bar fud 43修改为for foo in "bar fud 43"会怎样呢?别忘了,加上引号就等于告诉shell把引号之间的一切东西都看作是一个字符串。这是在变量里保留空格的一种办法。

实验解析

这个例子创建了一个变量foo,然后在for循环里每次给它赋一个不同的值。因为shell在默认情况下认为所有变量包含的都是字符串,所以字符串43在使用中与字符串fud是一样合法有效的。

实验 使用通配符扩展的for循环

正如我们前面所提到的,for循环经常与shell的文件名扩展一起使用。这意味着在字符串的值中使用一个通配符,并由shell在程序执行时填写出所有的值。

你已经在最早的first例子中见过这种做法了。该脚本程序用shell扩展把*扩展为当前目录中所有文件的名字,然后它们依次作为for循环中的变量$file使用。

我们来快速地看看另外一个通配符扩展的例子。假设你想打印当前目录中所有以字母f开头的脚本文件,并且你知道自己的所有脚本程序都以.sh结尾,你就可以这样做:

#!/bin/sh

for file in $(ls f*.sh); do
  lpr $file
done
exit 0

实验解析

这个例子演示了$(command)语法的用法,我们将在后面的内容中对它做更详细地介绍(参见2.6.6节)。简单地说,for命令的参数表来自括在$()中的命令的输出结果。

shell扩展f*.sh给出所有匹配此模式的文件的名字。

请记住,shell脚本程序中所有的变量扩展都是在脚本程序被执行时而不是在编写它时完成的。所以,变量声明中的语法错误只有在执行时才会被发现,就像前面我们给空变量加引号的例子中看到的那样。

5.while语句

因为在默认情况下,所有的shell变量值都被认为是字符串,所以for循环特别适合于对一系列字符串进行循环处理,但如果你事先并不知道循环要执行的次数,那么它就显得不是那么有用了。

如果需要重复执行一个命令序列,但事先又不知道这个命令序列应该执行的次数,你通常会使用一个while循环,它的语法如下所示:

while condition; do
  statements
done

请看下面的例子,这是一个非常简陋的密码检查程序:

#!/bin/sh

echo "Enter password"
read trythis

while [ "$trythis" != "secret" ]; do
  echo "Sorry, try again"
  read trythis
done
exit 0

这个脚本程序的一个输出示例如下所示:

Enter password
password
Sorry, try again
secret
$

很明显,这不是一种询问密码的非常安全的办法,但它确实演示了while语句的作用。dodone之间的语句将反复执行,直到条件不再为真。在这个例子中,你检查的条件是变量trythis的值是否等于secret。循环将一直执行直到$trythis等于secret。随后你将继续执行脚本程序中紧跟在done后面的语句。

6.until语句

until语句的语法如下所示:

until condition
do
  statements
done

它与while循环很相似,只是把条件测试反过来了。换句话说,循环将反复执行直到条件为真,而不是在条件为真时反复执行。

一般来说,如果需要循环至少执行一次,那么就使用while循环;如果可能根本都不需要执行循环,就使用until循环。

下面是一个until循环的例子,你设置一个警报,当某个特定的用户登录时,该警报就会启动,你通过命令行将用户名传递给脚本程序。如下所示:

#!/bin/bash

until who | grep "$1" > /dev/null
do
   sleep 60
done

# now ring the bell and announce the expected user.

echo -e '\a'
echo "**** $1 has just logged in ****"

exit 0

如果用户已经登录,那么循环就不需要执行。所以在这种情况下,使用until语句比使用while语句更自然。

7.case语句

case结构比你目前为止见过的其他结构都要稍微复杂一些。它的语法如下所示:

case variable in
  pattern [ | pattern] ...) statements;;
  pattern [ | pattern] ...) statements;;
  ...
esac

这看上去有些令人生畏,但case结构允许你通过一种比较复杂的方式将变量的内容和模式进行匹配,然后再根据匹配的模式去执行不同的代码。这要比使用多条ifelifelse语句来执行多个条件检查要简单得多。

请注意,每个模式行都以双分号(;;)结尾。因为你可以在前后模式之间放置多条语句,所以需要使用一个双分号来标记前一个语句的结束和后一个模式的开始。

因为case结构具备匹配多个模式然后执行多条相关语句的能力,这使得它非常适合于处理用户的输入。弄明白case工作原理的最好方法就是通过例子来进行说明。我们将使用3个实验例子逐步深入地对它进行介绍,每次都对模式匹配进行改进。

你在case结构的模式中使用如*这样的通配符时要小心。因为case将使用第一个匹配的模式,即使后续的模式有更加精确的匹配也是如此。

实验 case示例一:用户输入

你可以用case结构编写一个新版的输入测试脚本程序,让它更具选择性并且对非预期输入也更宽容:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
    yes)  echo "Good Morning";;
    no )  echo "Good Afternoon";;
    y  )  echo "Good Morning";;
    n  )  echo "Good Afternoon";;
    *  )  echo "Sorry, answer not recognized";;
esac

exit 0

实验解析

case语句被执行时,它会把变量timeofday的内容与各字符串依次进行比较。一旦某个字符串与输入匹配成功,case命令就会执行紧随右括号)后面的代码,然后就结束。

case命令会对用来做比较的字符串进行正常的通配符扩展,因此你可以指定字符串的一部分并在其后加上一个*通配符。只使用一个单独的*表示匹配任何可能的字符串,所以我们总是在其他匹配字符串之后再加上一个*以确保如果没有字符串得到匹配,case语句也会执行某个默认动作。之所以能够这样做是因为case语句是按顺序比较每一个字符串,它不会去查找最佳匹配,而仅仅是查找第一个匹配。因为默认条件通常都是些“最不可能出现”的条件,所以使用*对脚本程序的调试很有帮助。

实验 case示例二:合并匹配模式

上面例子中的case结构明显比多个if语句的版本更精致,但通过合并匹配模式,你可以编写一个更加清晰的版本。如下所示:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
    yes | y | Yes | YES )      echo "Good Morning";;
    n* | N* )                  echo "Good Afternoon";;
    * )                        echo "Sorry, answer not recognized";;
esac

exit 0

实验解析

这个脚本程序在每个case条目中都使用了多个字符串,case将对每个条目中的多个不同的字符串进行测试,以决定是否需要执行相应的语句。这使得脚本程序不仅长度变短,而且实际上也更容易阅读。这个脚本程序同时还显示了*通配符的用法,虽然这样做有可能匹配意料之外的模式。例如,如果用户输入never,它就会匹配n*并显示出Good Afternoon,而这并不是我们希望的行为。另外需要注意的是*通配符扩展在引号中不起作用。

实验 case示例三:执行多条语句

最后,为了让这个脚本程序具备可重用性,你需要在使用默认模式时给出另外一个退出码。如下所示:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
    yes | y | Yes | YES )
           echo "Good Morning"
           echo "Up bright and early this morning"
           ;;
    [nN]*)
           echo "Good Afternoon"
           ;;
    *)
           echo "Sorry, answer not recognized"
           echo "Please answer yes or no"
           exit 1
           ;;
esac

exit 0

实验解析

为了演示模式匹配的不同用法,这个代码改变了no情况下的匹配方法。你还看到了如何在case语句中为每个模式执行多条语句。注意,你必须很小心地把最精确的匹配放在最开始,而把最一般化的匹配放在最后。这样做很重要,因为case将执行它找到的第一个匹配而不是最佳匹配。如果你把*)放在开头,那不管用户输入的是什么,都会匹配这个模式。

**请注意,esac前面的双分号(;;)是可选的。在C语言程序设计中,即使少一个break语句都算是不好的程序设计做法,但在shell程序设计中,如果最后一个case模式是默认模式,那么省略最后一个双分号(;;)是没有问题的,因为后面没有其他的case模式需要考虑了。

为了让case的匹配功能更强大,你可以使用如下的模式:

[yY] | [Yy][Ee][Ss] )

这限制了允许出现的字母,但它同时也允许多种多样的答案并且提供了比*通配符更多的控制。

8. 命令列表

有时,你想要将几条命令连接成一个序列。例如,你可能想在执行某个语句之前同时满足好几个不同的条件,如下所示:

if [ -f this_file ]; then
    if [ -f that_file ]; then
        if [ -f the_other_file ]; then
            echo "All files present, and correct"
        fi
    fi
fi

或者你可能希望至少在这一系列条件中有一个为真,像下面这样:

if [ -f this_file ]; then
   foo="True"
elif [ -f that_file ]; then
   foo="True"
elif [ -f the_other_file ]; then
   foo="True"
else
   foo="False"
fi
if [ "$foo" = "True" ]; then
   echo "One of the files exists"
fi

虽然这可以通过使用多个if语句来实现,但如你所见,写出来的程序非常笨拙。shell提供了一对特殊的结构,专门用于处理命令列表,它们是AND列表和OR列表。虽然它们通常在一起使用,但我们将分别介绍它们的语法。

AND列表

AND列表结构允许你按照这样的方式执行一系列命令:只有在前面所有的命令都执行成功的情况下才执行后一条命令。它的语法是:

statement1 && statement2 && statement3 && ...

从左开始顺序执行每条命令,如果一条命令返回的是true,它右边的下一条命令才能够执行。如此持续直到有一条命令返回false,或者列表中的所有命令都执行完毕。&&的作用是检查前一条命令的返回值。

每条语句都是独立执行,这就允许你把许多不同的命令混合在一个单独的命令列表中,就像下面的脚本程序显示的那样。AND列表作为一个整体,只有在列表中的所有命令都执行成功时,才算它执行成功,否则就算它失败。

实验 AND列表

在下面的脚本程序中,你执行touch file_one命令(检查文件是否存在,如果不存在就创建它)并删除file_two文件。然后用AND列表检查每个文件是否存在并通过echo命令给出相应的指示。

#!/bin/sh

touch file_one
rm -f file_two

if [ -f file_one ] && echo "hello" && [ -f file_two ] && echo " there"
then
    echo "in if"
else
    echo "in else"
fi

exit 0

执行这个脚本程序,你将看到如下所示的结果:

hello
in else

实验解析

touchrm命令确保当前目录中的有关文件处于已知状态。然后&&列表执行[-f file_one]语句,这条语句肯定会执行成功,因为你已经确保该文件是存在的了。因为前一条命令执行成功,所以echo命令得以执行,它也执行成功(echo命令总是返回true)。当执行第三个测试[-f file_two]时,因为该文件并不存在,所以它执行失败了。这条命令的失败导致最后一条echo语句未被执行。而因为该命令列表中的一条命令失败了,所以&&列表的总的执行结果是falseif语句将执行它的else部分。

OR列表

OR列表结构允许我们持续执行一系列命令直到有一条命令成功为止,其后的命令将不再被执行。它的语法是:

statement1 || statement2 || statement3 || ...

从左开始顺序执行每条命令。如果一条命令返回的是false,它右边的下一条命令才能够被执行。如此持续直到有一条命令返回true,或者列表中的所有命令都执行完毕。

||列表和&&列表很相似,只是继续执行下一条语句的条件现在变为其前一条语句必须执行失败。

实验 OR列表

沿用上一个例子,但要修改下面程序清单里阴影部分的语句:

#!/bin/sh

rm -f file_one

if [ -f file_one ] || echo "hello" || echo " there"
then
    echo "in if"
else
    echo "in else"
fi

exit 0

这个脚本程序的输出是:

hello
in if

实验解析

头两行代码简单的为脚本程序的剩余部分设置好相应的文件。第一条命令[-f file_one]失败了,因为这个文件不存在。接下来执行echo语句,它返回true,因此||列表中的后续命令将不会被执行,因为||列表中有一条命令(echo)返回的是true,所以if语句执行成功并将执行其then部分。

这两种结构的返回结果都等于最后一条执行语句的返回结果。

这些列表类型结构的执行方式与C语言中对多个条件进行测试的执行方式很相似。只需执行最少的语句就可以确定其返回结果。不影响返回结果的语句不会被执行。这通常被称为短路求值(short circuit evaluation)。

将这两种结构结合在一起将更能体现逻辑的魅力。请看:

[ -f file_one ] && command for true || command for false

在上面的语句中,如果测试成功就会执行第一条命令,否则执行第二条命令。你最好用这些不寻常的命令列表来进行实验,但在通常情况下,你应该用括号来强制求值的顺序。

9.语句块

如果你想在某些只允许使用单个语句的地方(比如在AND或OR列表中)使用多条语句,你可以把它们括在花括号{}中来构造一个语句块。例如,在本章后面给出的应用程序中,你将看到如下所示的代码:

get_confirm && {
   grep -v "$cdcatnum" $tracks_file > $temp_file
   cat $temp_file > $tracks_file
   echo
   add_record_tracks
}

2.6.4 函数

待续。。。

转自图灵社区图书前两章预读页面

标签: Linux, 程序设计, shell

Last modification:April 8th, 2019 at 08:01 pm

Leave a Comment