Shell编程基础(转)

http://blog.csdn.net/jemmy/archive/2006/04/14/663657.aspx

为什么要使用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中内建的命令。这样我们就可以解决一些不会被编译器所看到的真正的程序问题。


命令
我们从一个Shell脚本的内部执行两种类型的命令。也就是通常(normal)的命令,这样的命令我们也可以在命令行的方式下来运行,称为处部命令,另一种就是我们前面所说的内建(built-in)命令,称之为内部命令。内建命令是在Shell的内部来实现的而不能为外部程序所调用。然而大多数的内部命令也会作为相对独立的单一程序来提供,而这也是POSIX 标准所要求的一部分。通常来说内部命令与外部命令并没有太大的区别,除非是这个内部运行得更为高效。
然而在这里我们只会讨论一些主要的命令,包括我们编写一下脚本时所需要用到的外部与内部命令。作为一个Linux用户我们会知道一些其他的可以在命令行来运行的命令。我们应该记住除了我们在这里所提到一些内建命令以后我们还可以在脚本中使用我们所知道的一些其他的命令。
break
当我们要从一个for,while或是until循环中退出时我们可以使用这个命令。我们也可以传递给break另外一个数字参数,而这个数字参数正是我们要退出的循环次数。因为这样会使得我们的脚本变得难于阅读,所以我们并不推荐大家来使用这样的方式。在默认的情况下,break只会退出单一的层次。如下面的例子:
#!/bin/sh
rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4
for file in fred*
do
    if [ -d “$file” ]; then
        break;
    fi
done
echo first directory starting fred was $file
rm -rf fred*
exit 0
:命令
冒号命令只是一个空命令。这个命令用来作为true的别名而简化逻辑条件是相当有用的。因为他是一个内建的命令,所以他运行得要比true快速,然而他的输出却并不是易于理解的。
我们可以看到在while循环中使用这个命令。while :可以替代更为常见的while true来实现无限的循环。
:结构在变量的设置条件中也是相当有用的,例如:
: ${var:=value}
如果没有:,shell会试着将$var作为一个命令来对待。
#!/bin/sh
rm -f fred
if [ -f fred ]; then
    :
else
    echo file fred did not exist
fi
exit 0
continue
与C语言相类似,这个命令可以使得for,while,until中的变量使用列表中的下一个值继续执行下一次循环。
#!/bin/sh
rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4
for file in fred*
do
    if [ -d “$file” ]; then
          echo “skipping directory $file”
        continue
    fi
    echo file is $file
done
rm -rf fred*
exit 0
continue命令后可以跟一个用来作为一个可选参数的数字,这样我们就可以部分的跳出嵌套循环。然而这样的参数并不常用,因为这样会使得我们的脚本难于理解。例如:
for x in 1 2 3
do
   echo before $x
   continue 1
   echo after $x
done
这个脚本的输出结果如下:
before 1
before 2
before 3
。命令
。命令会在当前的Shell中执行命令:
。 。/shell_script
通常情况下,当一个脚本执行一个外部的命令或是脚本时,就会创建一个新的循环或是子Shell,这个外部命令会在新的环境下运行,然而这个新的环境就会无视返回给父Shell的返回代码。但是外部的资源和。命令使得被调用脚本中所列出的命令在同一个环境下来运行。
这样就意味着在通常的情况下命令对环境变量所做的修改会丢失。而另一方面,。命令可以允许被执行的命令改变当前的运行循环。当我们要使用一个脚本作为包装来为后来一些其他命令的运行设置环境时是相当有用的。例如:如果我们同时在几个不同的工程上进行工作,有时我们就会发现我们需要使用一个不同的参数来调用命令,也许是调用一个老版本的编译器来维护一个古老的程序。
在Shell脚本中,。(dot)命令的工作方式与C或是C++中的#include的工作方式相类似。虽然他并不真正包含一个脚本,但是他确实是在当前的条件下运行命令,所以我们可以使用这个命令在一个脚本中进行变量或是函数定义的合并。
在下面的这个例子中,我们在命令行的方式下使用dot命令,但是我们也可以在一个脚本中使用这个命令。
1 假设我们有两个文件,而这两个文件所包含是为两个不同的开发环境所做的环境设置。要为古老的,经典的命令设置环境,classic_set,我们可以使用下面的命令:
#!/bin/sh
version=classic
PATH=/usr/local/old_bin:/usr/bin:/bin:.
PS1=”classic> “
2而对于新的命令我们使用latest_set:
#!/bin/sh
version=latest
PATH=/usr/local/new_bin:/usr/bin:/bin:.
PS1=” latest version> “
我们可以使用dot命令将这些脚本进行合并来设置环境,如下面的操作:
$ . ./classic_set
classic> echo $version
classic
classic> . latest_set
latest version> echo $version
latest
latest version>
echo
仅管在现代的Shell中使用printf命令,但是在这里我们仍然遵循通常的习惯来使用echo命令后跟新行字符的字符串。
一个通常的问题就是如何来禁止一个新行字符。不幸的,不同版本的Unix实现了不同的解决方法。在Linux中通用的作法是:
echo -n “string to output”
但是也许我们常会看到下面的情况:
echo -e “string to output\c”
在第二种方法,echo -e确保允许解释一些转义字符,例如新\t解释为tab,而将\n解释为回车换行。这在通常的情况下是默认的设置
eval
eval命令允许我们进行参数的赋值。他是Shell的内建命令而并不作为一个单独的命令而存在。下面一个从X/Open所借用来的简短的例子可以很好的说明这个问题。
foo=10
x=foo
y=’$’$x
echo $y
这个例子的输出结果为$foo,然而如果我们用下面的例子:
foo=10
x=foo
eval y=’$’$x
echo $y
这样的输出结果就为10.这样eval就有一些像另外的一个$:他会提供给我们一个变量的值。
eval命令是非常有用的,他会允许我们随时创建和运行代码。他确实会将脚本的高度复杂化,但是他却会允许我们做一些平常来说非常困难甚到是不可能的事情。
exec
exec命令有两种不同的用法。他最通常的用法是用一个不同的程序来替换当前的Shell。例如;
exec wall “Thanks for all the fish”
在脚本中会使用wall命令来替换当前的Shell。在exec命令之后的脚本内容不会处理,因为执行这个脚本的Shell已经不存在了。
exec的第二种用法就是用来修改当前文件的描述符:
exec 3< afile
这个结果是是为了从文件afile文件中读入内容而打开文件描述符3.这样有用法并不常见。
exit n
exit命令会使得脚本返回返回代码。如果我们是在交互的Shell中运行这个命令,他就会使得我们退出会话。如果我们不指定我们的脚本不指定返回状态而退出,那么脚本中上一次命令的执行结果就会作为返回值。通常指定返回值是一个很好的做法。
在Shell编程中,返回代码0为成功,1到125是错误代码,这些错误代码可以为脚本所使用。保留的值具有保留的含义:
126    文件是不可执行的
127    命令没有找到
128及以上    发生信号
使用0作为成功代码在许多的C或是C++程序员看来会有一些的不同。这最大的优点就是在脚本中允许我们使用125种用户定义的错误代码而不需要全局错误代码变量。
在下面这个例子中,如果在当前的目录下存在。profile文件就回成功值。
#!/bin/sh
if [ -f .profile ]; then
    exit 0
fi
exit 1
如果我们是一个甘受惩罚的人或者是需要简洁的脚本,我们可以用我们在前面见过的AND和OR列表来重写我们的脚本,而将所有的内容放在一行:
[ -f .profile ] && exit 0 || exit 1
export
export命令可以使得变量以其子Shell中的参数进行命令.在默认的情效况下,在一个Shell中所创建的变量在另一个所调用的Shell中并不是可见的.export命令可以由他的参数创建一个环境变量,这个变量可以为当前程序中所调用的其他的脚本或是程序可见.更为技术的一点来说,由任何子进程序所引进的环境变量是由这个Shell派生的.这个命令的含义以及用法可以从下面的两个脚本export1和export2来很好的进行演示.
我们先列出export2的脚本内容:
#!/bin/sh
echo “$foo”
echo “$bar”
下面的是export1中的内容,在这个脚本的结尾处我们调用export2:
#!/bin/sh
foo=”The first meta-syntactic variable”
export bar=”The second meta-syntactic variable”
export2
如果我们运行这个脚本我们可以得到下面的结果:
$ export1
The second meta-syntactic variable
$
之所以发生第一行的空行是因为在export2中变量foo并不是可见的,所以$foo被赋值为空值,输出一个空的变量就会得到一个新行.
一旦一个变量由一个Shell引入,那么他在由这个Shell中所派生的或是在这个Shell中所依次调用的脚本中都是可见的.如果脚本export2调用了另一个脚本,那么对于另一个脚本来说这个变量的值仍然是可见的.
命令set -a或是set -allexport可以引入所有的变量.
expr
expr命令会将他的参数作为一个表达式来对待.他最常见的用法是用在如下面形式的简单算术中:
x=`expr $x + 1`
在这里的反引号(`)是将命令expr $x+1的结果作为变量x的值.我们也可以用语法$()来写这个句子而不使用反引号的形式,如下面的形式:
x=$(expr $x + 1)
expr命令是一个相当强大的命令,他可以处理许多表达赋值的问题.他的一些原则如下表所示:
expr1 | expr2    如果expr1不为空则为expr1,否则为expr2
expr1 & expr2    如果expr2或是expr1为零则为零,否则为expr1
expr1 = expr2    相等
expr1 > expr2    大于
expr1 >= expr2    大于等于
expr1 < expr2    小于
expr1 <= expr2    小于等于
expr1 != expr2    不等于
expr1 + expr2    相加
expr1 - expr2    相减
expr1 * expr2    相乘
expr1 / expr2    整数相除
expr1 % expr2    整数取模
printf
printf命令只在现代的Shell中可用.X/Open建议我们最好应使用echo来产生格式化的输出.
语法格式如下:
printf “format string” parameter1 parameter2 ...
字符串的格式与我们在C或是C++中所见到的相类假,只是有一些限制.主要的区别在于Shell并支持浮点数,因为Shell中的所有算术运算都是作为整数来处理的.格式化字符串可以由字母,转义序列和转义符的任何组合来构成.在格式化字符串中除了%和\的所有字符都可以进行精确的输出.
下面列出的是可以支持的转义序列:
\\    反斜线
\a    警告
\b    退格
\f    形成输入字符
\n    新行字符
\r    回车
\t    Tab字符
\v    垂直Tab
\ooo    由八进制表示的单个字符
转义字符是相当复杂的,所以我们在这里只是列出一些我们会常用到的内容.更为详细的内容可以在在线的bash手册中得到.转义字符是%和紧跟其后的要转义的字符组成的.下面列出一些主要的转义字符:
d    输出十进制数
c    输出一个字符
s    输出一个字符串
%    输出%字符
然后我们会使用格式化字符串来解释其余的参数并进行结果的输出.例如:
$ printf “%s\n” hello
hello
$ printf “%s %d\t%s” “Hi There” 15 people
Hi There 15   people
在这里我们要注意我们必须使用""来使得Hi There字符串成为一个单一的参数.
return
return命令会使得一个函数返回.我们在前面的一些函数的用法中已经注意到了这一点.return命令会返回一个单一的数字参数,而这个数字参数在调用这个函数的脚本中是可见的.如果没有指定返回参数,return在默认情况下会返回上一次命令的返回代码.
set
set命令会为Shell设置参数变量.这对于在输出由空格区分的命令中使用域是很有用的一个方法.
假如我们要在我们的Shell脚本中使用当前月份的名字.系统提供了一个日期的命令,这个命令含有字符串形式的月份,但是我们需要将他与其他的区域进行分离.我们可以使用set命令和$(...)结构的组合来执行日期命令并返回结果.日期命令的输出是将月份的字符串作为他的第二个参数.
如下面的例子:
#!/bin/sh
echo the date is $(date)
set $(date)
echo The month is $2
exit 0
这个程序为date命令的输出设置了参数列表,并且使用第二个参数$2来得到月份的名字.
在这里我们要注意的是我们使用date命令来作为一个简单的例子来展示如何解开位置参数.因为date命令是与语言设置有关的,而事实上我们可以使用命令date +%B来得到月份的名字.date还有许多其他的格式选项,我们可以从他的手册页中得到更为详细的说明.
我们还可以使用set命令来控制通过传递Shell参数的Shell执行方式.这最通常的用法是使用命令set -x,这样就会使得一个脚本显示他当前执行命令的轨迹.
shift
shift命令可以使得所有的参数变量值减1,这样$2就会变为$1,而$3就会变为$2,依次类推.这样以后$1的值就会丢弃,而$0的值会保持不变.如果在调用shift命令时指定了一个数字参数,那么这些参数也会移动一些相应的位置.而其他的一些变量,$*,$@,$#也会为新的参数变量的范围进行相应的修改.
shift命令在参数之中进行扫描是相当有用的,如果我们的脚本需要10个或是更多的参数,那么我们就需要使用shift来访问10个或是更多的内容.
作为例子,我们可以像下面的形式来扫描所有的位置参数:
#!/bin/sh
while [ “$1” != “” ]; do
    echo “$1”
    shift
done
exit 0
trap
trap命令可以用来指定在收到信号时所要进行的动作.一个常见的用法是在一个脚本被中断时所做的处理工作.由于历史的原因,Shell总是使用数字来代表信号,但是新的脚本可以通过#include signal.h并且忽略SIG前缀来使用信号的名字.如果我们要查看信号的数字和其相关的名字我们可以在命令提示下输入trap -l命令.
trap命令可以通过紧跟信号的名字的方式来传递将要进行的动作:
trap command signal
我们要记住通常情况下脚本是从上到下进行解释的,我们必须在我们要保护的脚本部分之前来指定trap命令.
要将一个trap的条件设置为默认的情况,我们可以简单的将命令指定为-.要忽略一个信号,我们可以将命令设为空串``.一个没有任何参数的trap命令会打印出当前的动作列表.
下面的表格列出了一些比较重要的标准的X/Open信号,这些信号都可以被捕获.更为详细的内容我们可以从信号手册中得到.
HUP(1)    挂起.通常是当一个终端离线或是用户退出时发出.
INT(2)    中断.通常是通过按下Ctrl+C发出的.
QUIT(3)    退出,通常是由Ctrl+\发出的.
ABRT(6)    中止,通常是由一些严重的运行错误发出的.
ALRM(14) 警告.通常是由处理超时发出的.
TERM(15) 结束.通常是当系统关机时发出的.
下面的脚本模拟了一些简单信号的处理:
#!/bin/sh
trap ‘rm -f /tmp/my_tmp_file_$$’ INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$
echo “press interrupt (CTRL-C) to interrupt ....”
while [ -f /tmp/my_tmp_file_$$ ]; do
    echo File exists
    sleep 1
done
echo The file no longer exists
trap INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$
echo “press interrupt (control-C) to interrupt ....”
while [ -f /tmp/my_tmp_file_$$ ]; do
    echo File exists
    sleep 1
done
echo we never get here
exit 0
如果我们运行这个命令,我们在每一个循环中按下Ctrl+C,我们就会得到下面的输出:
creating file /tmp/my_tmp_file_141
press interrupt (CTRL-C) to interrupt ....
File exists
File exists
File exists
File exists
The file no longer exists
creating file /tmp/my_tmp_file_141
press interrupt (CTRL-C) to interrupt ....
File exists
File exists
File exists
File exists
工作原理:
这个脚本使用trap命令来为命令rm -f /tmp/my_tmp_file_$$执行时接收到INT信号时安排将会执行的动作.然后这个脚本会进入一个循环直到文件存在.当用户按下Ctrl+C时,语句rm -f /tmp/my_tmp_file_$$被执行,然后这个循环会重新开始.因为这个文件已经被删除了,所以第一个while循环会正常的结束.
这个脚本然后会再一次使用trap命令,这一次是要指定当INT信号发生时没有命令被执行.他会重新创建这个文件并进入第二个循环语句.如果用户在这时按下Ctrl+C时,没有配置要执行的语句,所以会发生默认的动作,即立即终止脚本.因为这个脚本是立即被终止的,所以最后的echo和exit命令并不会执行.
unset
unset命令会从当前的环境中移除变量或是函数.但是对于Shell自己定义的只读的变量(例如IFS)不可以进行这样的操作.这个命令并不是经常使用.
例如下面的这个例子:
#!/bin/sh
foo=”Hello World”
echo $foo
unset foo
echo $foo

 


两个更为有用的命令和正则表达式
在我们开始学习新的Shell编程知识之前,我们先来看一下两个更为有用的两个命令,这两个命令虽然并不是Shell的一部分,但是在进行Shell编程时却会经常用到.随后我们会来看一下正则表达式.
find命令
我们先来看的是find命令.这个命令对于我们用来查找文件时是相当有用的,但是对于Linux新手来说却有一些难于使用,在一定程序是由于他所带的选项,测试,动作类型参数,而且一个参数的执行结果会影响接下来的参数.
在我们深入这些选项和参数之前,我们先来看一个非常简单的例子.假如在我们的机子上有一个文件wish.我们来进行这个操作时要以root身份来运行,这样就可以保证我们可以搜索整个机子:
# find / -name wish -print
/usr/bin/wish
#
正如我们可以想到的,他会打印出搜索到的结果.很简单,是不是?
然而,他却需要一定的时间来运行,因为他也会同时搜索网络上的Window机器上的磁盘.Linux机器会挂载大块的Window机器的文件系统.他也会同时那些位置,虽然我们知道我们要查找的文件位于Linux机器上.
这也正是第一个选项的用武之地.如果我们指定了-mount选项,我们就可以告诉find命令不要搜索挂载的目录.
# find / -mount -name wish -print
/usr/bin/wish
#
这样我们仍然可以搜索这个文件,但是这一次并没有搜索挂载的文件系统.
find命令的完整语法如下:
find [path] [options] [tests] [actions]
path是一个很简单的部分:我们可以使用绝对路径,例如/bin,或者是使用相对路径,例如.. .如果我们需要我们还可以指定多个路径,例如 find /var /home
主要的一些选项如下:
-depth    在查看目录本身以前要先搜索目录中的内容
-follow    跟随符号链接
-maxdepths N    在搜索一个目录时至多搜索N层
-mount(或-xdev)    不要搜索其他的文件系统
下面的是一些test的选项.我们可以为find命令指定大量的测试,并且每一个测试会返回真或是假.当find命令工作时,他会考查顺序查找到的文件,并且会在这个文件上按顺序进行他们所定义的测试.如果一个测试返回假,find命令会停止他当前正在考查的文件并继续进行下面的动作.我们在下表中列出的只是一些我们最常用到的测试,我们可以通过查看手册页得到我们可以利用find命令使用的可能的扩展列表项.
-atime N    N天以前访问的文件
-mtime N    N天以前修改的文件
-name pattern    除了路径,与指定的类型匹配的文件名.为了保证指定的类型传递给find命令而并不是立即被Shell赋值,指定的类型必须用引号进行引用.
-newer otherfile    与otherfile文件相比要新的文件
-type C        C类型的文件,而这里的C可以指定的一种类型.最常用的是d代表目录,而f是指普通的文件.对于其他的文件类型,我们可以查看手册页.
-user username    指定的用户所拥有的文件
我们也可以使用运算符进行测试的组合.大多数的有两种格式:短格式和长格式.
!    -not    测试的反
-a    -and    所有的测试必须为真
-o    -or    测试中某一个为真
我们可以使用括号来强行改变测试和运算符的次序.因为这些对于Shell来说有着特殊的意义,所以我们也需要使用反斜线将他们作为一个整体进行引用.另外,如果我们为文件名指定了匹配类型,我们也必须用引号进行引用,这样就可以避免他们被Shell进行扩展,从而可以将他们直接传递给find命令.所以如果我们要写一个这样的测试,要查找比X文件要近或者是以一个范围开头的文件,我们要写成下面的形式:
\(-newer X -o -name “_*” \)
现在我们要试着在当前的目录下查找最近修改日期比while2更近的文件,我们可以用下面的命令:
$ find . -newer while2 -print
.
./elif3
./words.txt
./words2.txt
./_trap
$
我们在上面所用的命令看起来似乎不错,但是我们却同时也搜索了当前的目录文件,而这并不是我们所希望的,我们所感兴趣只是常规文件.所以我们可以加上另外一个测试-type f:
$ find . -newer while2 -type f -print
./elif3
./words.txt
./words2.txt
./_trap
$
工作原理:
这些命令是如何进行工作的呢?我们指定find命令应该在当前的目录下进行查找(.),而我们所要查找的是比while2更新的文件(-newer while2),而且如果已经传递了测试,还要测试这个文件是否为一个常规文件(-type -f).最后,我们使用我们以前用过的动作,-print,仅仅是来验证我们所找到的文件.
下面我们要查找的文件或者是以下划线开头的或者是要比while2文件新的文件,但是也必须为一个常规文件.这个例子可以向我们展示如何来进行测试的组合:
$ find . \( -name “_*” -or -newer while2 \) -type f -print
./elif3
./words.txt
./words2.txt
./_break
./_if
./_set
./_shift
./_trap
./_unset
./_until
$
这时我们可以看到这并不是一件很难的事情,不是这样吗?我们必须转义圆括号,这样他就不会被Shell所保护,同时用引号引用*,这样他就可以直接传递给find命令了.
既然我们现在能够可靠的查找文件,下面我们就来看一下当我们查找指定的文件时我们可以进行的一些协作.我们要再一次强调,我们在这里所列出的只是一些最常用的选项,我们可以查看手册页得到全部的集合.
-exec command    执行一个命令.这是我们最常执行的动作.
-ok command    与-exec相类似,所不同的只是他会提示用户在执行将要执行的命令之前进行命令的确认.
-print        打印出文件名
-ls        使用ls命令列出当前的文件
-exec和-ok命令会同一行的参数子序列作为他的参数的一部分,直到遇到一个终结符\;序列.对于-exec和-ok来说字符串{}是珍上特殊的类型,而且会为当前文件的绝对路径所替换.
这样的解释也许并不是太认人容易理解,但是一个例子也许可以很好的来说明这些.
如下面的一个简单的例子:
$ find . -newer while2 -type f -exec ls -l {} \;
-rwxr-xr-x    1 rick     rick          275 Feb 8 17:07 ./elif3
-rwxr-xr-x    1 rick     rick          336 Feb 8 16:52 ./words.txt
-rwxr-xr-x    1 rick     rick         1274 Feb 8 16:52 ./words2.txt
-rwxr-xr-x    1 rick     rick          504 Feb 8 18:43 ./_trap
$
正如我们现在所看到的,find命令是相当有用的.要用好这个命令只需要一些简单的练习.然而这样的练习也许要付一定的代价,所以我们应做一些find命令的实验.
grep命令
我们将要看到的第二个非常有用的命令为grep命令,这是一个并不常见的名字,他是通用正则表达式解析器的简称(General Regular Expression Parser).我们使用find命令在我们的系统是查找所需的文件,但是我们却要使用grep命令在文件中查找指定的字符串.而事实上,最常用的做法就是当我们在使用find命令时将grep作为一个命令传递给-exec.
grep命令可以带选项,匹配的模式以及我们要在其中查找的文件:
grep [options] PATTERN [FILES]
如果并没有指定文件名,他就会搜索标准输入.
让我们从grep命令的主要的选项开始.我们在这里列出的只是一些主要的选项,我们可以从手册中得到更为详细的内容说明.
-c    打印出匹配行的总数,而不是打印出匹配的行
-E    打开扩展表达式
-h    禁止将在其中查找到匹配内容的文件名作为输出行的前缀
-i    忽略大小写
-l    列出带用匹配行的文件名,而不是输出实际的匹配行
-v    将匹配类型转换为选择不匹配的行而不是匹配的行
如下面的一些例子:
$ grep in words.txt
When shall we three meet again. In thunder, lightning, or in rain?
I come, Graymalkin!
$ grep -c in words.txt words2.txt
words.txt:2
words2.txt:14
$ grep -c -v in words.txt words2.txt
words.txt:9
words2.txt:16
$
工作原理:
第一个例子中并没有指定选项,grep命令只是简单在的words.txt文件中查找字符串in,并且打印出所匹配的行.在这里并没有打印出文件名,这是因为在这里我们只是使用了一个文件.
在第二个例子中打印出在两个不同的中匹配行的总数,在这种情况就要打印出文件名.
在最后的一个例子中我们使用了-v选项来转换查找的条件并且打印出在两个文件中不匹配的总行数.
正则表达式
正是我们所看到的,grep命令的基本用法是比较容易掌握的.现在我们要来看一下基本的正则表达式,这会允许我们做一些更为复杂的匹配.正如我们在前面所提到的,正则表达式是用在Linux或是共他的一些开源中的语言.我们可以在vi或是在编写Perl脚本时使用.
在正则表达式的使用过程中,一些字符会被以不同的方式进行处理.最常见的一些用法如下:
^    在一行的开头
$    在一行的结尾
.    任意一个单一字符
[]    方括号中所包含是字母的范围,其中的任何一个都可以进行匹配,例如a-e的字母范围,或者是我们可以使用^来进行反义.
如果我们要将他们作为普通的字符来使用就要在这些字符前面加上\.所以如果我们要查找一个$字符,我们就要使用\$来进行查找.
下面的是一些可以在方括号中使用的比较有用的特殊匹配:
[:alnum:]    字母数字字符
[:alpha:]    字母
[:ascii:]    ASCII字符
[:blank:]    空格或是Tab
[:cntrl:]    ASCII码控制字符
[:digit:]    数字
[:graph:]    非控制,非空格字符
[:lower:]    小写字母
[:print:]    可打印字符
[:punct:]    标点字符
[:space:]    空白字符,包括垂直Tab
[:upper:]    大写字符
[:xdigit:]    十六进制数字
另外,如果同时使用-E选项指定了扩展匹配,在正则表达式的后面也许会跟一些其他的控制匹配类型组合的字符.如果我们只是想将他们作为普通的字符进行使用,我们也要在其前面加上转义符\.
?    可选的匹配,但是最多匹配一次
*    必须匹配0个或是多个项目
+    必须匹配1个或是多个项目
{n}    必须匹配n次
{n,}    必须匹配n次或是更多次
{n,m}    匹配范围为n次到m次,包括m次
这些内容看起来有一些复杂,但是如果我们循序渐进,我们就会发现事实上这些内容并不如我们在第一眼看到时那样的复杂.最简单的掌握正则表达式的方法就是简单的试一些例子:
如果我们要查找以字符e结尾的行我们可以用下面的命令:
$ grep e$ words2.txt
Art thou not, fatal vision, sensible
I see thee yet, in form as palpable
Nature seems dead, and wicked dreams abuse
$
正如我们所看到的,这个命令会搜索出以e结尾的匹配行.
现在假设我们要查找以字母a结尾的单词.要达到这个目的,我们在方括号中使用特殊的匹配.在这样的情况下,我们要使用[[:blank:]],这会测试一个空格或是一个Tab:
$ grep a[[:blank:]] words2.txt
Is this a dagger which I see before me,
A dagger of the mind, a false creation,
Moves like a ghost. Thou sure and firm-set earth,
$
现在假设我们要查找一个以Th开头的三个字母的单词.在这种情况下,我们需要同时使用[[:space:]]来决定一个单词的结尾并使用.来匹配另外的一个字母:
$ grep Th.[[:space:]] words2.txt
The handle toward my hand? Come, let me clutch thee.
The curtain’d sleep; witchcraft celebrates
Thy very stones prate of my whereabout,
$
最后我们要使用扩展的grep命令来查找10个字符长的小写字母的单词.在这里我们要指定一个字符的范围的来匹配a到z,同时指定字符的10次重复:
$ grep -E [a-z]\{10\} words2.txt
Proceeding from the heat-oppressed brain?
And such an instrument I was to use.
The curtain’d sleep; witchcraft celebrates
Thy very stones prate of my whereabout,
$
我们在这里只是接触正则表达式一些相对来说更为重要的一部分.正如在Linux中的其他的大多数的内容,在这之外会许多的文档来帮助我们要发现更为详细的内容,但是学习正则表达式的最好的方法就是要实验这些表达式.
命令执行:
当我们编写脚本时,我们常常需要在Shell脚本中取得命令执行结果的结果来使用.也就说我们需要执行一个命令并将这个命令的输出结果放在一个变量中.这时我们可以使用我们在前面的set命令的例子中所介绍的$(command)语法.这也是一个相对较老的格式,而最常使用的用法是`command`格式.
所有新的脚本应使用$(...)的格式,这可以用来避免一些相当复杂的在反引号命令中使用$,`,\所造成的转换规则.如果在`...`结构中使用了反引号,我们就需要使用\进行转义.这些相对模糊的字符会使得程序感到迷惑,有时甚至是一些经验丰富的程序也不得不进行一些试验以使得在反引号命令中的引号可以正确的进行工作.
$(command)命令的结果只是简单的命令的输出.在这里我们要注意的是这并不是这个命令的返回状态,而是输出的字符串.如下面的例子:
#!/bin/sh
echo The current directory is $PWD
echo The current users are $(who)
exit 0
因为当前的目录是一个Shell环境变量,所以第一行并不需要使用这种命令执行结构.然而,who命令的执行结果,如果希望他在这个脚本中可见,我们就要使用这种命令结构.
如果我们希望将他们的结果放在一个变量中,我们可以像平常一样将他们赋值给一个变量:
whoisthere=$(who)
echo $whoisthere
将一个命令的执行结果放在一个脚本变量中的能力是相当强大的,因为这样就可以很容易的在脚本中使用现在的命令并取得他们的输出.如果你发现在你正在试着转换一个标准命令在标准输出上的输出结果的参数集合并将他们作为一个程序的参数,你就会发现命令xargs会帮助你完成这一切.可以查看手册页得到更深更详细的内容.
有时会出现的一个问题就是我们要调用的命令会在我们所希望的文本出现之前输出了一些空白符,或者是比我们所希望的更多的内容.在这样的情况下,我们可以使用我们在前面所说到的set命令.
算术扩展
我们已经使用了expr命令,这可以允许处理简单的算术命令,但是他的执行是相当的慢的,因为在处理expr命令时需要调用一个新的Shell.
一个新的更好的替换就是$((...))扩展.通过将我们所希望的表达式包在括号里以便在$((...))中进行赋值,我们可以进行更为有效的简单算术.
如下面的例子:
#!/bin/sh
x=0
while [ “$x” -ne 10 ]; do
    echo $x
    x=$(($x+1))
done
exit 0
参数扩展
我们在前面已经看到了参数分配与扩展的最简单形式,在那里我们是这样写的:
foo=fred
echo $foo
当我们要在一个变量的结尾处加上另外的一个字符时却会发生问题.假设我们要写一个简短的脚本来处理名为1_tmp和2_tmp的文件,我们可以试着用下面的脚本来处理:
#!/bin/sh
for i in 1 2
do
    my_secret_process $i_tmp
done
但是在每一个循环中,我们会得到下面的信息:
my_secret_process: too few arguments
发生了什么错误呢?
问题就在于Shell会试着将变量$i_tmp用他的变量值进行替换,但是却并不存在这个变量.而Shell并不会认为这是一个错误,而只是用空值来进行替换,所以并没有参数传递给my_secret_process.要将$i的扩展保护为变量的一部分,我们需要将i放在一对花括号中:
#!/bin/sh
for i in 1 2
do
    my_secret_process ${i}_tmp
done
这样以后在第一个循环中,i的值会用${i}进行替换,从而给出一个实际的文件名.这样我们就已经将一个参数的值替换为一个字符串了.
我们可以在Shell中进行许多的替换.常常这样的方法会为参数的处理问题提供一个优雅的解决方法.
常用到的一些如下表:
${parm:-default}    如果一个参数为空,则将他设定为一个默认值.
${#parm}        给出参数的长度.
${parm%word}        从末尾开始,移除与word相匹配的最小部分并返回其余的部分.
${parm%%word}        从末尾开始,移除与word相匹配的最长部分并返回其余的部分.
${parm#word}        从开头开始,移除与word相匹配的最小部分并返回其余的部分.
${parm##word}        从开头开始,移除与word相匹配的最长部分并返回其余的部分.
这些替换对于我们要处理字符串来说是相当有用的.而最后的四个可以用来移除字符串中的部分内容,而这对于处理文件名和路径是更为有用的.如下面的一些例子中所示的:
#!/bin/sh
unset foo
echo ${foo:-bar}
foo=fud
echo ${foo:-bar}
foo=/usr/bin/X11/startx
echo ${foo#*/}
echo ${foo##*/}
bar=/usr/local/etc/local/networks
echo ${bar%local*}
echo ${bar%%local*}
exit 0
如果我们运行这个脚本我们会得到下面的输出结果:
bar
fud
usr/bin/X11/startx
startx
/usr/local/etc
/usr
工作原理:
第一个句子,${foo:-bar},会为foo的值指定为bar,因为当这个语句开始执行时并没有为foo指定任何值.foo的值会保持不变直到他遇到unset语句.
在这里我们有一些需要我们注意的内容:
${foo:=bar}将会设置变量$foo.这个字符串运算符会检测foo存在并且不为空值.如果他不为空,则会返回他的值,但是如果是相反的情况,就会将foo的值设为bar并且会返回替换的结果值.
${foo:?bar}会打印出foo: bar,而如果foo并不存在或是他被设为空值则会退出命令.
最后,${foo:+bar},如果foo存在并且不为空则会返回bar.
{foo#*/}语句进行匹配并且只是移除左面的内容(在这里我们要记住*匹配0个或是多个字符).{foo##*/}进行匹配并会移除尽可能多的内容,所以他会移除了最右面的/以及他前面的所有字符.
{bar%local*}语句匹配从右面开始直到第一次出现local的字符,而{bar%%local*}会从右面开始匹配尽可能多的字符,直到第一次发现local.
因为Unix和Linux都比较强的依赖于过滤的概念,所以我们常常要将一个操作的执行结果进行手工重定向.假设我们要使用cjpeg命令将一个GIF的文件转换为JPEG的文件:
$ cjpeg image.gif > image.jpg
也许有时我们会在大量的文件上进行这样的操作.这时我们如何自动重定向?我们可以很容易的这样来做:
#!/bin/sh
for image in *.gif
do
   cjpeg $image > ${image%%gif}jpg
done
这个脚本可以将当前目录下的每一个GIF文件转换成为JPEG文件.

 

Here Documents
从一个Shell脚本传递给一个命令的一个比较特殊的方法就是使用here document.这个文档可以使得执行的命令就像是由文件或是键盘读入的,而事实上,这是由这个脚本读入的.
一个here document是以<<开头的,后面所跟的是要在文档的结尾处重复出现的字符序列.<<是Shell的重定向标签,在这样的情况下,他会强制将命令输入给here document.这个特殊的序列的作用就像是一个标记,来告诉Shell here document要在哪里结束.这个标记充列不可以出现在要传递给命令的行上,所以最好是将他们标记为最不常用的或是难以忘记的内容.
如下面的这个例子:
#!/bin/sh
cat <<!FUNKY!
hello
this is a here
document
!FUNKY!
而这个脚本的输出如下:
hello
this is a here
document
here document看上去也许是一个古怪的特征,但是他们却是非常强大的,因为他可以允许我们调用一个交互的程序,如编辑器,而提供给他们一些预定义的输入.然而他们最常用到的地方却是从一个脚本的内部输出大量的文本,正如我们在前面所看到的一样,从而可以避免在每一行使用echo命令.我们使用了!来标记每一行的标记符以保证不会造成迷惑.
如果我们希望以一个预先定义好的方式来处理一个文件中的一些行,我们可以使用ed编辑器并且可以在一个Shell脚本中使用here document传递编辑的命令.
假如有一个名为a_text_file的文件,他包含下面的一些行:
That is line 1
That is line 2
That is line 3
That is line 4
我们可以用下面的这个脚本来进行处理:
#!/bin/sh
ed a_text_file <<!FunkyStuff!
3
d
.,\$s/is/was/
w
q
!FunkyStuff!
exit 0
在我们运行这个脚本之后,这个文件的内容如下:
That is line 1
That is line 2
That was line 4
工作原理:
这个Shell脚本只是简单的调用了ed编辑,然后将一些要执行的动作命令传递给ed编辑器,这些命令分别是移动到第三行,删除此行,然后在当前行使用what was进行替换.ed命令是由当前脚本中的here document处取得的,也就是在!FunkyStuff!之间的部分.
在这里我们要注意的一点就我们使用了\进行了$的转义.
调试脚本
调试脚本是一件非常容易的事情,但是却并没有其他的特殊的工具来帮助我们进行调试.在这里我们会概要的说明一些常用到的方法.
当发生错误时,Shell通常会打印出包含错误内容的行号.如果错误并没有立即是显示出来,我们可以添加另外的echo语句来显示变量的内容而且可以简单的输入交互Shell中进行代码片段的测试.
因为脚本是被解释的,所以修改和重试一个Shell时并没有编译的动作.跟踪更为复杂的错误的主要方法就是设置各种Shell选项.要这样做,我们可以在调用脚本后使用命令行选项或者是使用set命令.我们在这里列出一些常用到的选项:
命令行选项        set选项            描述
sh -n <script>        set -o noexec        只检查语法错误,并不执行命令
            set -n       
sh -v <script>        set -o verbose        在运行命令之前将命令打印出来
            set -v           
sh -x <script>        set -o xtrace        在命令执行之后将命令打印出来
            set -x       
            set -o nounset        当使用未定义的变量时给出错误信息
            set -u
我们可以设置打开标记选项,使用-o,或是关闭,使用+o,这样就如同在使用缩写版本.我们可以使用xtrace选项进行简单的跟踪执行.作为最初的检查,我们可以使用命令选项,但是在最后的测试时,我们可以在脚本中会出现的问题的代码中加入xtrace标记.执行跟踪会使得Shell在执行Shell脚本中的每一行命令之前将变量的扩展等内容打印出来.
我们可以使用下面的命令来打开xtrace:
set -o xtrace
我们也可以用下面的命令来关闭xtrace:
set +o xtrace
扩展的层次在由每一行的开始的+的个数来表示的(默认情况下).我们可以将+改为一些更有意义的内容,方法是在我们的Shell配置文件中设置Shell变量PS4的值.
在Shell中,我们还会发现我们可以通过取得EXIT的信号来得到一个程序的状态.
trap ‘echo Exiting: critical variable = $critical_variable’ EXIT
进入图形---对话实用程序
在结束我们的Shell脚本的讨论之前,我们还要讨论一个特征,虽然严格来说这并不是Shell的一部分,但是对于Shell程序来说却是相当有用的,所以我们要在这里讨论一下.如果我们知道我们的脚本程序只在Linux文本下运行,我们可以有一个简洁的办法来加亮我们脚本的特色,那就是我们可以使用dialog命令.这个命令会使用文本的模式和颜色,但是他看起来还是有着图形界面的色彩.
dialog是相当简单的,也只是一个有着各种参数及变量的单一程序并且允许我们显示各种类型的图形框,所示的范围也只是有着Yes/No的来进行输入的方框甚至是菜单选择.这个程序通常会在用户进行了输入分类时返回,返回的结果可以由返回的状态得到或者是通过取回标准错误流输入的文本.
在我们在开始更为详细的说明以前,我们先来看一个非常简单的dialog操作.我们可以直接在命令行使用dialog,这可以很好的来展示原型,所以我们先创建一个简单的消息框来显示传统的第一个程序:
dialog --msgbox “Hello World” 9 18
这样就会在屏幕上显示一个图形信息框,仅是一个OK对话框.
现在我们可以看到dialog是一件多么简单的事情.下面我们可以来看一些更为详细的选项:
下表列出我们可以创建的主要的对话框.
选择对话框    --checklist    允许我们显示一个列表项,其中的每一个都可以单独选择
信息对话框    --infobox    可以立即返回的简单对话框,返回后并不会清除屏幕
输入对话框    --inputbox    允许用户输入文本
菜单对话框    --menu        允许用户从列表项中进行单一选择
信息对话框    --msgbox    向用户显示一个有着Ok按钮的信息框
单选对话框    --raidolist    允许用户从列表中选择一项
文本对话框    --textbox    允计我们在一个滚动框中显示一个文件
Yes/No对话框    --yesno        允许我们询问一个问题,可以允许用户选择yes或是no
另外还有一些其他的对话框类型可以用.如果我们想要知道更多的一些不常用的对话框,如以前一样,我们可以从手册页可得到更为详细的内容说明.
要得到任何一个允许我们进行文本输入的对话框的输出或是选择,我们需要得到错误流,通常情况下我们是将他们重定向一个临时文件,这样我们就可以在稍后进行处理了.要得到Yes/No类型的结果,我们只需要查看一下返回代码就可以了,与其他的程序相类似,成功则返回0(例如yes选择),失败则返回1.
--checklist    text height width list-height [tag text status] ...
--infobox    text height width
--inputbox    text height width [initial string]
--menu        text height width menu-height [tag item ] ...
--msgbox    text height width
--radiolist    text height width list-height [tag text status] ...
--textbox    filename height width
--yesno        text height width
另外,所有的对话框都会带一些选项.我们在这里不会列出所有的选项,但是我们却要注意其听两个:--title,允许我们为对话框指定一个标题,--clear,用来清屏.我们可以通地查看手册页得到更为详细的内容.
下面我们开始一个简单的完整的例子.一旦我们懂得了这个例子,那么所有其他的也只就是一件简单的事了.这个例子子会创建一个选择列表类型的对话框,标题为Check me,而说明为Pick Numbers.这个列表对话将会是15个字符高和25个字符宽,而每一个选项为3个字符高.最后我们显示出这些内容,并且在设置了默认情况下的开关选项.
dialog --title “Check me” --checklist “Pick Numbers” 15 25 3 1 “one” “off” 2 “two”“on” 3 “three” “off”
工作原理:
在这个例子中,--checklist的参数告诉我们要创建一个checklist类型的对话框.我们使用--title选项将这个对话框的标题设置为Check me,而下一个参数则是提示的信息,在这里我们将其设为Pick Numbers.
我们接下来的操作是来设置对话框的大小.我们在这里将其设成15行高和25行宽,另外有将有3行用来显示菜单.这并不是一个很好的尺寸,但是在这里却很好的向我们显示了如何来进行布局.
也许这样的选项看上去实在是有一些太奇怪了,但是我们所要记住的只是每一个菜单选项有三个值:
标号,文本以及状态.
所以第一个项目的标号我们设为1,而显示的文本为"one"并且状态为"on".我们可以依此设置以后的各个选项.
很容易是不是?我们只要在命令行上试验一些命令我们就可以看到这个命令是如何容易的来使用了.
为了将这样的对话框放在一个程序中,我们需要能够访问用户输入的结果.这也是相当容易的,我们只要简单的文本输入重定向错误流或者是检查环境变量$?,这也就是我们在前面的命令行中所调用的返回状态.
如下面的这个例子:
#!/bin/sh
# Ask some questions and collect the answer
dialog --title “Questionnaire” --msgbox “Welcome to my simple survey” 9 18
在这里我们只是通过一个简单的对话框显示来告诉用户发生了什么.而我们并不需要得到用户输入的结果,所以这是相当简单的.
我们可以将这个程序加以改进:
dialog --title “Confirm” --yesno “Are you willing to take part?” 9 18
if [ $? != 0 ]; then
   dialog --infobox “Thank you anyway” 5 20
   sleep 2
   dialog --clear
   exit 0
fi
现在我们通过使用一个简单的yes/no对话框来询问用户是否要进行处理.我们使用环境变量$?来检测用户是否选择了yes或是no.如果用户不希望进行处理,我们就会在退出之前使用一个简单的不需要用户有输入的信息对话框.
dialog --title “Questionnaire” --inputbox “Please enter your name” 9 30 2>_1.txt
Q_NAME=$(cat _1.txt)
现在我们使用一个输入对话框来询问用户的姓名.我们重定向标准错误流2到一个临时文件,_1.txt,这样我们就可以用变量Q_NAME来进行处理了.
dialog --menu “$Q_NAME, what music do you like best?” 15 30 4 1 “Classical” 2
“Jazz” 3 “Country” 4 “Other” 2>_1.txt
Q_MUSIC=$(cat _1.txt)
现在我们显示了一个有着四个不同选项的菜单.在这里我们也将重定向标准错误流到一个临时文件并装入到一个变量中.
if [ “$Q_MUSIC” == “1” ]; then
    dialog --msgbox “Good choice!” 12 25
fi
如果用户选择了菜单选项1,这就会被存放在一个临时文件_1.txt中,这样我们可以将结果取出放在一个变量Q_MUSIC中,从而我们可以检测结果.
sleep 5
dialog --clear
exit 0
最后我们清除最后一个对话框并退出程序.
现在如果我们只需要使用Linux文本界面,从一个Shell脚本编写简单的GUI程序,我们就可以使用这样的方法.
组合
现在我们已经看到将Shell做为一门编程语言所具有的主要特征,现在我们可以将我们已经学到的这些内容组合在一起来编写一个简单的程序.

posted @ 2008-01-04 16:07  jambol  阅读(653)  评论(0编辑  收藏  举报