为什么要使用Shell编程?
Linux操作系统是由UNIX操作系统发展起来的.UNIX操作系统中所体现出来的思想和哲学深沉的影响了现代其他的操作统.在UNIX系统中提供了许多不同的Shell程序.大多数的商业UNIX提供了Korn Shell,当然了我们也还有其他的Shell可以来用.虽然Shell看起来与Windows的命令行相类似,但是他显得更为的强大,可以以他的方式来运行更为复杂的程序.我们可以使用Shell进行更为快速和简单的编辑.另外在大多数的Linux基系统的安装提供了Shell,所以我们可能很方便的来检测我们的工作是否可以正常的进行工作.而且Shell可以提供许多的实用程序,我们可以用这样的内容来很好的进行我们的工作,而且这样的程序易于维护和移植.
一些哲学:
现在我们来认识一下UNIX和Linux哲学.UNIX是建立在代码高度重用的基础上的.我们可以建立一个简单的实用程序,那么其他的人就可以用字符串链接或是其他的形式来使用我们的程序.Linux的一大优点就是提供了许多优秀的工具.如:
$ ls -al | more
这个命令使用了ls和more命令,而且使用了管道来重定向输出.我们可以使用简单的脚本来创建立大的复杂的程序.
例如,如果我们要打印bash的man手册页我们可以用下面的命令:
$ man bash | col -b | lpr
因为Linux的自动文件处理,这些程序的使用者一般并不要知道这些程序是以哪种语言写成的.
管道和重定向:
在我们详细的说明Shell编程之前,我们先来说明一下Linux的程序(不仅是Shell程序)如何来重定向输入和输出.
重定向输出:
也许我们已经对输出重定向较为熟悉了.如:
$ ls -l > lsoutput.txt
这个命令会将ls命令的输出保存期在一个名为lsoutput.txt的文件中.
然而事实上要有比我们在这个例子中显示的还要多的重定向.现在我们要知道的就是文件修饰0是程序的标准的输入,1为标准输出,2为标准的错误输出.我们可以独立的重定向这些中的任何一个.事实上我们也可以重定向其他的文件修饰,但是一般的情况下为0,1,2.
在上面的这个例子中,我们使用>修饰符来重定向标准输出到一个文件.在默认的情况,如果这个文件已经存在了,他就会被重写.
如果要增加文件内容,我们可以使用>>运算符.如:
$ ps >> lsoutput.txt
这个就会在文件后面增加新的输出内容.
如果要重定向标准错误输出,我们可以使用>运算符和我们要使用的文件修饰符来进行重定向.因为标准的错误输出修饰符为2,我们就可以使用2>运算符.这在忽略错误信息而又不显示在屏幕上时就会显得尤为有用.
假如我们要在脚本中使用kill命令来杀掉一个进程,然而却是常用这样的情况,在这个命令运行之前这个进程已经不存在了.如果是这样的情况,kill命令就会产生一个标准的错误输出,而在默认的情况下,这个输出要显示在屏幕上.通过重定向输出和错误,我们就可以阻止在屏幕上显示任何内容.如下面的命令:
$ kill -HUP 1234 >killout.txt 2>killerr.txt
这个命令就会将输出和错误信息存放在一个单独的文件中.
如果我们要将这两个输出放在一个文件中,我们可以使用>&来组合这两种输出.如:
$ kill -1 1234 >killouterr.txt 2>&1
这个命令就可以将所有的输出放在同一个文件中.在这里我们要注意的就是命令的顺序.这个命令的顺序可以解释为重定向标准输出到文件killouterr.然后重定向标准错误输出到与标准输出同一的地方.如果我们弄错了顺序,我们就不会得到我们希望的输出.
在这里我们会看到kill命令的结果使用了返回代码,然而常常是我们并不需要保存标准输出或是标准错误输出.我们可以使用UNIX中的/dev/null来忽略所有的错误输出.如:
$ kill -1 1234 >/dev/null 2>&1
重定向输入:
与重定向输出相类似,我们也可以重定向输入.如:
$ more < killout.txt
管道:
我们可以使用管道符|来连接进程.在Linux系统中,由管道连接起来的进程可以自动运行,就如同在他们有一个数据流一样.在下面的这个例子中,我们要使用sort命令来排序ps的输出.而如果我们不使用管道,我们就要分几步来完成:
$ ps > psout.txt
$ sort psout.txt >pssort.out
一个更好的办法就是可以用管道来处理:
$ ps | sort > pssort.out
因为我们要在屏幕上看到他们,我们要使用第三个进程:
$ ps | sort | more
使用的管道数并没有一个量的限制.如果我们要看到正在运行的除了shell以外的不同名字的进程,我们可以用下面的命令:
$ ps -xo comm | sort | uniq | grep -v sh | more
在这个命令中,使用了ps的输出,将这个输出以字母的顺序进行排序,使用uniq来解压进程,使用grep -v sh来移除名为sh的进程,最后在屏幕上显示结果.
在这里我们就可以看到,这样的方式式要比单个执行的命令好得多.在这里我们要注意的一点点就是,在这个命令中我们不要两次使用同一个文件.如下面的命令:
$ cat mydate.txt | sort | uniq | >mydate.txt
这样我们就会得到一个空文件,因为在我们读取这个之前已经改写了这个文件.
Shell作为编程语言:
现在我们已经知道了一些基本的Shell操作,下面我们就进入脚本编程.有两种写Shell程序的方法:我们可以输入命令队列,让Shell来交互的执行他们,或者是将这些命令存放在一个文件中,然后作为程序进行调用.
交互程序:
在命令行输入脚本是可以快速方便的试出小的代码段,如果我们正在学习或是进行测试这是一个相当好的方式.假如我们有许多的C文件,而我们希望找出其中含有POSIX字符串的文件.我们可以如下面的样子进行整体的操作:
$ for file in *
> do
> if grep -l POSIX $file
> then
> more $file
> fi
> done
在这里我们就会看到Shell提示符由$变成了>.我们可以输入命令由Shell来决定如何时停止并且立即执行脚本程序.
在这个例子中,grep命令查找其中含有POSIX字符串的文件,然后more将这个文件中的内容打印在屏幕上.最后Shell返回提示符.
Shell也会允许我们使用通配符进行扩展.我们可以使用*来匹配字符串,我们还可以使用?来匹配单个的字符,而[set]可以允许检测在这里列出的任何一个单个字符.[^set]则正好相反,要除去在这里所列出的字符.我们还可以使用花括号{}进行扩展,这可以允许我们将任意的字符串放在一起.如下面的例子:
$ ls my_{finger,toe}s
这个命令会列出文件my_fingers,my_toes.
有经验的用户也许会用一种更有效率的方式来运行这些命令.也许我们会使用下面的命令:
$ more `grep -l POSIX *`
或者是下面的命令:
$ more $(grep -l POSIX *)
$ grep -l POSIX * | more
这些命令都会打印出含有POSIX的文件名.
然而事实上如果我们每一次要完成这样的任务就要输入一系列命令的做法是相当麻烦的一件事.我们需要就是将这些命令放在一个文件中,作为一个Shell脚本来引用,这样就可以在我们需要的时候来运行他了.
创建一个脚本:
首先我们可以使用任何一个我们喜欢的文本编辑来创建一个含有下面命令的文件,命名为first:
#!/bin/bash
# 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
在这个文件中以#开始的行被看作是注释,在通常的情况下,我们会将#放在第一列.在这里我们要注意的是第一行的注释,#! /bash/bash是一个特殊格式的注释.#!后面的字符告诉系统我们要执行这个文件的程序.在当前的情况下,/bin/bash是默认的Shell程序.
在这里指定的绝对路径最好的做法就是要少于32个字符,因为在一些老版本的UNIX系统有着这样的限制,我们这样做可以很好的做到向后兼容.
因为这个脚本被看作是标准的Shell输入,所以可以包含任何的Linux命令.
exit命令可以返回一个较为敏感的退出代码.如果这个程序是单独运行的,我们就没有必要来检测这个程序的返回代码,而如果我们要在另一个程序中进行调用,进行返回代码的检测以确定这个程序是否成功执行就显得尤为重要.虽然有时我们并不希望我们的程序被其他的程序调用,我们也要返回一个合理的代码.因为也许有一天我们的程序就会作为其他脚本的一部分而被重用.
0则表明这个程序成功执行.因为脚本并不会检测任何的失败,所以我们总是返回成功代码.
我们要注意的另外一点就是在这个文件中我们并没有使用任何的扩展名或是前缀.在Linux或是UNIX系统中并不依靠文件的扩展名来判断文件的类型.如果我们希望可以为这个文件加一个.sh或是其他的扩展名,但是Shell并不会在意这些.大多数预先安装的脚本并没有扩展名,而最好的用来检测文件类型的办法就是使用file命令.
使用脚本可执行:
现在我们就有了我们的脚本程序,我们可以用两种方式来运行.最简单的办法就是将脚本文件作为参数使用Shell进行调用:
$ /bin/bash first
这样就可以正常的工作了.如果我们可以将其与其他的Linux命令相分离而是直接输入命令文件名就可以运行这个程序就显得更好一些.如果我们要这样的做,我们首先要使用下面的命令来为他加上可执行属性:
$ chmod +x first
现在我们就可以用下面的命令来运行了:
$ ./first
Shell语法
变量:
在Shell中,我们在使用变量之前并不需要进行声明.相反我们可以在需要的时候进行简单的使用就可以了.在默认的情况下,所有的变量都是作为字符串进行存储的,虽然有时我们会用数字为其赋值.Shell以及其他的一些实用的转换程序会将数字字符串转换成相应的值为进行操作.在Linux系统中是要区分大小的,所以在Shell看来变量foo与Foo是不同的.
在Shell中我们要访问变量的值,我们要在变量前加上一个$.当我们要为变量赋值时,我们可以只使用变量的名字,Shell会在需要的时候进行动态创建.检测变量内容一个简单的办法就是在终端进行输出,这时要在变量前加上一个$.
在命令行中我们可以用下面的方法来设置和检测变量的值:
$ 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?
引号:
在继续我们的学习之前我们要清楚引号的作用.
通常脚本中的参数是由空白字符来分隔的,如空格,Tab或是回车.如果我们要我们的参数包含一个或是更多个参数,我们就要使用引号了.
例如变量$foo的行为要看我们使用的引号的类型了.如果我们是用双引号,在这一行执行时就会用他的值进行替换,而如果我们使用单引号就不会发生这样的替换.我们还可以使用转义字符\来除去$的特殊意义.
在通常的情况下,我们双引号来包含字符串,这样就可以防止变量被空白符所分隔,而且会用变量的值进行替换.
在下面的这个例子中我们就会看到引号对于变量输出的影响:
#!/bin/bash
myvar=”Hi there”
echo $myvar
echo “$myvar”
echo ‘$myvar’
echo \$myvar
echo Enter some text
read myvar
echo ‘$myvar’ now equals $myvar
exit 0
这个程序的输出为:
Hi there
Hi there
$myvar
$myvar
Enter some text
Hello World
$myvar now equals Hello World
工作原理
我们创建了变理myvar,并赋值为Hi there.变量的内容由命令echo显示出来,从而可以看出$字符扩展对变量内容的影响.从这输出我们可以看出双引号并不会影响变量的替换,而单引号和反斜线却会有这样的影响.我们同时使用一个read命令来从用户得到输入.
环境变量
当启动一个Shell脚本时,一些变量会由环境中的值进行初始化.在脚本中这些变量通常为大写字母,从而与用户定义的变量进行区分,而用户定义的变理常用小写字母来表示.创建的变量依赖于我们个人的配置.其中的许多列在手册页中,但是基本的一些列在下面的表中:
$HOME 当前用户的主目录
$PATH 用来进行命令查找的由冒号分隔的目录列表
$PS1 命令提示,通常为$,但是在bash中我们可以使用更为复杂的值.例如,字符串[\u@\h \W]$是流行的默认用法来告诉我们当前的用户,机器名称以及当前的工作目录,同时给出$提示.
$PS2 第二提示符,当提示额外的输入时使用,通常为>
$IFS 输入区域分隔符.当Shell读入输入时会使用一个字符列表来分隔输入的单词,通常是空格,tab和新行字符.
$0 Shell脚本的名称.
$# 传递的参数个数.
$$ 脚本的进程ID,通常用在一个脚本内部用来建立唯一的一个临时文件,如/tmp/tmp-file_$$.
如果我们的脚本调用一些参数,那么会建立一些其他的变量.即使没有传递参数,环境变量$#仍然存在,但是值却为0.
参数变量列在下面这个表中:
$1,$2,... 传递给脚本的参数.
$* 以单变量的形式显示所有的参数列表,由环境变量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.sh foo bar baz
Hello
The program ./try_var.sh is now running
The second parameter was bar
The first parameter list was foo bar baz
The user's home directory is /home/mylxiaoyi
Please enter a new greeting
hello
The script is now complete
工作原理:
这个脚本创建了一个名为salutation的变量并显示他的内空,然后显示了各种参数变量,而环境变量$HOME已经存在并且已经有适当的值.
函数:
所有程序语言的基本原则是测试条件并在这些测试的基础上进行各种不同的操作.在我们讨论这个话题之前,我们先来看一下在Shell脚本中我们会用到的函数构造以及我们要使用的控制结构.
一个Shell脚本可以测试由命令行调用的任何命令的返回代码,包括我们自己书写的脚本.这就是我们在每一个Shell脚本最后包含exit代码的重要原因.
test或[命令:
事实上,大多数的脚本大量的使用了Shell真假检测的test或是[命令.在大多数的系统上,[和test命令是同义的,但是当使用了一个[命令时而同时为了可读在末尾使用了一个]命令.使用[命令看起来有一点奇怪,但是这个命令在代码中会使得命令的语法看起来要简单,整洁,并且与其他的程序语言很相像.
ls -l /usr/bin/[
-rwxr-xr-x 1 root root 25040 2005-11-16 21:17 /usr/bin/[
我们会使用一个简单的测试例子来介绍test命令:检测一个文件是否存在.用于这个目的的命令是test -f <filename>,所以我们可以用下面的脚本:
if test -f fred.c
then
...
fi
我们也可以像下面的样子来写:
if [ -f fred.c ]
then
...
fi
test命令的返回代码(条件是否满足)决定于条件代码是否运行.
在这里我们要注意是我们必须在[和条件之间用空格进行分隔.我们可以用下面的方法来记住这一点:[是test命令的另一种写法,而我们要在test命令后输入空格.
如果我们喜欢将then与if放在同一行,我们必须要加一个冒号来与then进行分隔:
if [ -f fred.c ]; then
...
fi
我们可以用的test命令的条件类型有以下的三种:字符串比较,算术比较和文件条件.下面的三张表展示了这些条件类型:
字符串比较:
string1 = string2 如果相等则为真
string1 != string2 如果不等则为真
-n string 如果不空则为真
-z string 如果为空则为真
算术比较:
expression1 -eq expression2 如果相等则为真
expression1 -ne expression2 如果不等则为真
expression1 -gt expression2 如果大于则为真
expression1 -ge expression2 大于等于则为真
expression1 -lt expression2 如果小于则为真
expression1 -le expression2 小于等于则为真
!expression 如查为假则为真
文件:
-d file 如果为目录则为真
-e file 如果存在则为真(在这里要注意的是,由于历史原因,-e选项并不可移植,所以常用的是-f选项
-f file 如果为常规文件则为真
-g file 如果设置了组ID则为真
-r file 如果文件可读则为真
-s file 如果文件大小不为零则为真
-u file 如果设置了用户ID则为真
-w file 如果文件可写则为真
-x file 如果文件可执行则为真
现在我们似乎走得有一点的太前了,但是接下来的是一个例子.在这里我们要测试文件/usr/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,我们可以用命令help test得到详细的信息.
控制结构
Shell有一结构控制集合,我们再一次说明他们与其他的程序语言非常相像.
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
这会产生下面的输出:
$ ./if.sh
Is is morning? please answer yes or no
yes
Good morning
这个脚本使用[命令来测试变量timeofday的值.这个结果会被if命令来使用,从而会执行不同的程序代码.
elif
但是不是幸的是,对于这个简单的脚本却有着许多的问题.他会将yes以外的任何答案理解为no.为了防止这样的问题出现我们可以使用elif结构,这样就会允许我们在if执行else部分时添加第二个条件.
我们可以修改上面的脚本以使用当用户输入yes或no以外的内容时报告错误信息.我们的做法是用elif来代替else从而添加另一个条件.
#!/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
工作原理:
这与上一个例子较为相似,但是在这一次如果if的测试条件不为真则使用elif命令测试变量的值.如果这些没有一个成功,则会打印出一条错误信息并且脚本退出并返回代码1,而返回值则可以用作另一个调用此脚本的程序查看脚本是否成功.
变量的问题:
现在修正了一个最明显的问题,但是却潜伏着另一个更细小的问题.我们可以试一下这个新脚本,但是我们仅是输入回车(或是其他的内容)而不回答这个问题,我们就会得到下面的错误信息:
[: =: 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
这样对于只是回车的答案来说就是一个安全的脚本了.
for
我们用for结构在任何字符串的集合的值范围内进行循环.他们可以简单的列在程序中,或是更为一般的,可以是文件名的Shell扩展结果.
语法如下:
for variable in values
do
statements
done
在下面的这个例子中,值为普通的字符,所以我们的脚本如下:
#!/bin/sh
for foo in bar fud 43
do
echo $foo
done
exit 0
我们会得到下面的结果:
./for.sh
bar
fud
43
工作原理:
这个例子创建了一个变量foo,并且在for循环中每次赋于一个不同的值.因为Shell在默认的情况下认为所有的变量都包含字符串,所以在例子中就会像使用字符串fud一样的来使用43.
使用通配符
正如我们在前面的那样,通常情况下我们会在for循环中使用文件名的Shell扩展.我们这样说的意思是在字符串值中使用通配符,并且使得Shell在运行时填充所有的值.
我们已经在我们最初的例子中看到这个例子.这个脚本使用了Shell扩展,*扩展成为当前目录下的所有文件名.这些中的每一个在for循环中轮流用作变量$i的值.
下面我们看一下另一个通配符的例子.想像一下如果我们要打印所有当前的目录下文件名中含有f字符的文件,而且我们知道我们所有的脚本以.sh结尾.我们可以用下面的脚本来完成我们的工作
#!/bin/sh
for file in $(ls f*.sh); do
lpr $file
done
exit 0
工作原理:
这里我们展示了$(command)的语法.基本来说,for命令所使用的参数列表是由包含在$()中的命令的输出来提供的.这个脚本将f*.sh扩展成所有与这个模式匹配的内容.
while
因为在默认的情况下认为Shell值为字符串,因而for循环对于在一系列的字符间进行循环是一个不错的选择,但是对于处理确定次数的循环命令来说就显得有一些笨拙了.
下面这个例了向我们展示了当我们用for循环来在20个数之间进行循环是一件多么麻烦的事情:
#!/bin/sh
for foo in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
do
echo “here we go again”
done
exit 0
即使是我们使用通配符进行扩展,我们有时也会遇到不能确定我们所需要的循环次数.在这样的情况下,我们可以使用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语句的用法.在do和done之间的语句将会被连续执行直到我们的测试条件不再为 真为止.在我们的输出例子中我们检测到trythis的值与secret并不相等,循环将会继续直到$trythis与secret相等为止.然后我们就 会执行done后的语句.
将while语句与算术运算进行组合我们就可以执行确定次数的循环操作.这要比我们刚才所看到的for的例子要简单得多.
#!/bin/sh
foo=1
while [ “$foo” -le 20 ]
do
echo “Here we go again”
foo=$(($foo+1))
done
exit 0
工作原理:
这个脚本使用[命令来检测foo的值并与20进行对比,如果小于或是等于则要执行循环体.在这个while循环中,(($foo+1))语法用来执行花括号内的算术运算,所以foo的值会在每一个循环后增加1.
因为foo不可以是一个空串,所以我们在在测试他的值时不需要用双引号来保护.我们这样做是因为这是一个好习惯.
until
until语句语法格式如下:
until condition
do
statements
done
这与while循环十分相似,所不同的只是测试条件的相反.换句说,until是循环继续直到条件为真,而不是while的条件为真时才进行循环.
until语句适用于我们希望进行循环直到某件事发生时为止的情况.作为一个例子,我们可以考虑下面的情况:当我们在命令行中输入另一个用户名进行登陆时发出响铃.
#!/bin/sh
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
下面我们要说是case结构.case结构比起我们已经讨论这些内容来说要显得有一些复杂.他的语法如下:
case variable in
pattern [ | pattern] ...) statements;;
pattern [ | pattern] ...) statements;;
...
esac
这种结构看起来有一些吓人,但是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就会为每一个可能的语句检测一些不同的字符串.这样就会使得我们的脚本更为短小, 而且实际的来说也更为易读.我们同时展示了*通配符的用法,即使这也许会匹配不希望的情况.例如,如果用户输入了never,这样就会匹配n*并且会显示 Good afternoon,而这并不是我们所希望的行为.在这里我们要注意如果*通配符使用了引号就不会起作用了.
最后,如果我们要使得这个脚本可以重用,但使用最后一个匹配模式时我们需要一个不同的返回值.在这里我们同时也加入set结构.
#!/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语言那样.
为了使得case的匹配更为强大的,我们可以用下面的形式:
[yY] | [Yy][Ee][Ss] )
这在允许多个答案时会限制允许输入的字符并且比*通配符有着更多的控制.
Lists:
有时我们会连接一系列的命令.例如我们在执行一个语句前需要多个限制条件,如下面的例子:
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 && ...
这些命令会由左边开始执行每一个命令,如果返回值为真会执行接下来的右边的命令.这个过程会连续进行执行一个命令的返回值为假,在这之后这个列表的中命令 也不再执行.&&会检测前一个命令的执行结果.每一个语句可以单独执行,这样就会允许我们在一个列表中执行不同的命令,就如下面的例子所 显示那样.如果这个列表中的所有命令都执行成功,那么这就是一个成功的命令,否则就是失败的.
在下面的这个脚本中,我们创建了file_one(先检查是否存在,如果不存在就先创建这个文件)然后移除file_two.然后AND命令列会检测每一个文件是否存在并会输入命令之间一个文本.
#!/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
touch和rm命令来保证当前目录下的文件在一个已知的状态.&&表接下来执行[ -f file_one ]语句,这个语句是会成功的,因为我们刚刚保证这个文件的存在.因为前面一个语句成功了,所以执行echo命令.这也会成功(echo通常会返回真).然 后执行第三个语句,[ -f file_two ].这个语句不会成功,因为这个文件已经不存在.因为最后一个命令失败了,所以最后的echo语句并不会执行.&&表的结果为假,因为在 这个列表中有一个命令失败了,所以if语句会执行else条件.
OR表:
OR表的结构我可以允许我们执行一系列命令,直到有一个命令成功,然后就不再执行更多的命令.他的语法结构如下:
statement1 || statement2 || statement3 || ...
由左开始依次执行每一个语句.如果他的返回值为假,那么就会执行接下来的语句.这个过程会继续下去直到有一个语句返回真值,这样以后就不会再执行接下来的列表中的命令.
||列表与&&列表是非常相像的,所不同的只是执行下一个语句的条件.
如下面的一个例子:
#!/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命令.令人惊奇的是这会返回一个真值,所以在||命令列表就不会再有命令执行.因为在这个列表中有一 个命令返回真值,所以if语句成功.
这两个结构的结果是上一个要执行的语句的结果.
如果要进行多个条件的测试,我们可以偈在C语言中一样的来使用这些列表类的结构.我们可以将这两种结构结合在一起;
[ -f file_one ] && command for true || command for false
如果测试成功将会执行第一个语句否则则会执行第二个语句.我们可以试验这些并不太常用的列表结构,而在通常情况下我们需要使用花括号来强制执行的顺序.
语句块:
如果我们要在一个地方使用多个语句但却只允许一个的时候,例如在AND或是OR列表中,我们可以用花括号将他们括起来形成一个语句块.
函数:
我们可以在Shell中定义函数,如果我们希望形成任何尺寸的脚本文件,我们可以用函数来结构化我们的代码.
要定义一个函数,我们只要写上函数的名字并跟随一个空的括号并将语句括在一个花括号内:
function_name () {
statements
}
如下面的一个简单的函数:
#!/bin/sh
foo() {
echo “Function foo is executing”
}
echo “script starting”
foo
echo “script ended”
exit 0
执行这个脚本我们会得到下面的输出:
script starting
Function foo is executing
script ending
工作原理:
这个脚本是由顶部开始执行的,所以在这里并没有什么不同的.但是当他发现foo(){结构,他就会知道在这里定义了一个函数调用.他会将foo指向一个函 数的情况进行存储并在}后继续执行.当执行foo()行时,Shell知道这里要执行一个前面定义的函数.当这个函数执行结束后,脚本就会在foo行后继 续执行.
我们必须在调用一个函数之前进行定义,这种风格有一些像Pascal的函数定义,所不同的只是在Shell并需要在前面进行声明.这并不是一个问题,因为 所有的脚本都是由顶部开始执行的,所以我们只需要简单的将我们要定义的全部函数放在这些函数中第一次调用之前,这样我们就解决了函数要先定义用调用的问 题.
当调用一个函数时,脚本中的参数,$*,$@,$#,$1,$2等等将会被函数中的参数所替换.这也是我们读取传递给函数参数的方法.当函数结束时,他们就会恢复成以前的值.
我们可以使用return命令来使得函数返回数字值.常用的做法是将函数返回的字符串存放在一个变量中,这样就可以在函数结束以后使用.
在这里我们要注意的是我们可以用local关键字在Shell函数中定义局部变量.这样这个变量就会只在这个函数中起作用.否则,一个函数可以访问在另一 个函数中定义的全局变量.如果一个局部变量与全局变量同名,那么在一个函数中这个变量就会覆盖掉全局变量.例如下面的这个列子:
#!/bin/sh
sample_text=”global variable”
foo() {
local sample_text=”local variable”
echo “Function foo is executing”
echo $sample_text
}
echo “script starting”
echo $sample_text
foo
echo “script ended”
echo $sample_text
exit 0
在没有用return命令返回值的情况下,函数会返回最后命令执行时的返回状态.
在我们下面的例子中,我们将会展示如何向函数传递一个参数以及一个函数如何返回一个真或是假的结果。我们可以使用一个名字作为参数来调用这个脚本。
1在脚本头后我们定义了一个yes_or_no函数:
#!/bin/sh
yes_or_no() {
echo “Is your name $* ?”
while true
do
echo -n “Enter yes or no: “
read x
case “$x” in
y | yes ) return 0;;
n | no ) return 1;;
*) echo “Answer yes or no”
esac
done
}
2接下的是这个程序的主要部分:
echo “Original parameters are $*”
if yes_or_no “$1”
then
echo “Hi $1, nice name”
else
echo “Never mind”
fi
exit 0
当我们运行这个脚本时我们会得到下面的输出:
$ ./my_name Rick Neil
Original parameters are Rick Neil
Is your name Rick ?
Enter yes or no: yes
Hi Rick, nice name
$
工作原理:
当这个脚本执行时,定义了yes_or_no函数但是却并没有运行这个函数。在if语句中,这个脚本执行了了我们所定义的这个函数,并且在将$1用传递给 原脚本的第一个参数Rick进行替换,在这以后将句子的其他部分作为参数传递给函数。这个函数使用这些参数并将他们存放在位置参数$1,$2等当中,并向 调用者提供一个返回值。依据这个返回值,if结构语句可以执行相应的操作。
正如我们所看到的,Shell有一个丰富的控制和条件结构语句。我们需要学习一些Shell中内建的命令。这样我们就可以解决一些不会被编译器所看到的真正的程序问题。