标 题: BASH SHELL 程序设计简介(ZZ)
发信站: 郁金香BBS站 (Wed Apr 18 21:05:52 2007) , 站内信件
BASH SHELL 程序设计简介
译者 : calebjiang mailto:[email protected]
作者 : X_console mailto:[email protected]
出处 : http://www.linuxnewbie.org/nhf/intel/programming/introbashscript.html
如同在 Linux 中其它的 shell,Bourne Again SHell 不只是一个优秀的指令行式的 she
ll,同时也提供 scripting 语言。Shell scripting 允许你完全利用 shell 的功能,将
需要许多指令执行的多项工作自动化。在你的 Linux 机器上有许多程序是 shell 程序。
如果你对学习他们如何运作,或修改他们感兴趣,基本要素是你必须了解 bash 的语法和语意。除此之外,了解
bash 语言,你能够撰写你个人专属的程序,来完成你要做的事情。
PROGRAMMING OR SCRIPTING?
对于刚开始接触程序设计的新手,常会对于 programming 和 scripting 语言间的差异感
到困惑。Programming 语言通常比 Scripting 语言功能更强大,也更快速。例如:C, C+
+, 和 Java 都是 Programming 语言。Programming 语言通常是从原始码 (一个包含最终
程序如何执行的指令集合文字文件) 开始,经过编译 (建立) 成为一个可执行档。这个可执行档不容易移植到不同
的操作系统中。举例来说,如果你曾在 Linux 上写过 C 的程序,你将不能够在窗口 98
系统上执行这个 C 的程序。要这样做,你必须在窗口 98 系统之下,重新编译原始的程序
代码。Scripting 语言也是从原始码开始,但不需编译成可执行档。而是由一个直译器读入原始码档案里的那
些指令,再执行每个指令。不幸地,直译的程序通常比编译的程序慢,因为直译器必须读
入每个指令。主要的优点是,你能轻松地转换原始码档案到任何的操作系统,并且立刻以
直译的方式执行。bash 是一种 scripting 语言。它对撰写小程序而言是不错的,但如果你打算开发大型的应用程
序,programming 语言可能对你比较有益。其它的 scripting 语言有 Perl, Lisp 和 Tc
l 等等。
WHAT DO YOU NEED TO KNOW? / 那些是你需要知道的?
撰写你自己的 shell 程序,需要知道最基本的 Linux 指令。举例来说,你应该知道要如
何拷贝,移动,产生新档案等等。还有一件事你必须知道,该如何使用文书编辑程序。在
Linux 里有三个主要的文字文件编辑程序 - vi, emacs 和 pico。如果对 vi 或 emacs
并不熟悉,可用 pico 或一些其它容易使用文字文件编辑程序。
WARNING!!! / 警告!!!
别以 root 的身份来练习!任何情况都可能发生!若你在撰写程序时发生意外的错误,导
致系统当掉,我并不负责。你已被警告过了!一定要使用没有 root 权限的一般使用者账
户。你甚至可以产生一个新的使用者,专门来练习 shell 程序设计。这样,最糟的情况只
是这个使用者的目录不见了。
YOUR FIRST BASH PROGRAM / 第一个 BASH 程序
我们的第一个程序会是典型的 "Hello World" 程序。不要怀疑,若你已有程序设计的经验
,你现在又必须头疼了。然而,这是传统,我何德何能改变传统呢?"Hello World" 程序
只是将 "Hello World" 这字眼打印到屏幕上。所以赶快打开你的文字文件编辑程序,键入
下列的内容:
#!/bin/bash
echo "Hello World"
第一行是告诉 Linux 用 bash 直译器来执行这个程序。在这个范例,bash 是位在 /bin
目录下。如果在你的系统上,bash 是在不同的目录,请对这行做适当的更改。另外,要特
别说明的是,这个直译器是非常重要,所以请确定目录正确否,它告诉 Linux 哪一个直译
器用来执行程序中的那些指令。下一步是把将程序存盘。称它做 hello.sh 好了。完成后,你需要让此程序可以
执行:
xconsole$ chmod 700 ./hello.sh
如果你不知道该如何更改档案的权限,就参考 chmod 的手册(manual page)。一旦更改完
成后,你仅需输入程序的名字,就能执行了:
xconsole$ ./hello.sh
Hello World
就是这个光!就是这个光!你的第一个程序完成了!真的就是这样无聊,没用,然而每个
人都是这样开始的。只需记得这个程序。撰写程序代码,存成档案,再用 chmod 让它可执
行。
COMMANDS, COMMANDS, COMMANDS / 指令,指令,指令
你的第一个程序要做什么呢?打印 "Hello World" 这两个字到屏幕上。但要怎样做呢?使
用指令。在程序中写的唯一一行程序代码是 echo "Hello World"。好吧,那一个是指令?
echo。echo 程序带有一个参数,并将此参数打印到屏幕。
所谓参数是指在你所键入的程序名称之后的任何东西。在这个案例中,"Hello World" 是
参数,它传进 echo 指令中。若你输入这样的指令 ls /home/root ,那么对 ls 而言,
/home/root 是参数。就竟这代表什么呢?代表如果你有一支程序,可将参数打印到屏幕上
,你就不需使用 echo 程序。我们假定有支称为 foo 的程序,能传入一个参数,一个字符串,并且将其打印到屏
幕上。我们能同样地重写我们的程序:
#!/bin/bash
foo "Hello World"
把它存盘及更改存取模式(chmod),然后执行:
xconsole$ ./hello
Hello World
结果完全一样。究竟有唯一的程序代码吗?没有。你真的能写任何程序?除非你是 echo
程序的作者。你所做的,是将 echo 程序放在你的 shell 程序内,并给一个参数。在现实
的世界中,例子中 echo 指令的另一选择是 printf 指令。printf 提供比较多的控制,如
果你熟悉 C 语言的程序设计就会明了。事实上,要得到完全相同的结果不必写一个 shell 程序:
xconsole$ echo "Hello World"
Hello World
bash shell 程序设计提供了多样的控制方式,而且容易学习。就如你刚看到一样,你用
Linux 指令来写你的 shell 程序了。你的 shell 程序是将其它的程序聚集在一起,来执
行特定的任务。
A MORE USEFUL PROGRAM / 更有用的程序
我们将撰写一个程序,功能是移动所有的档案到一个目录内,然后删除此目录及其内容,
然后再产生此目录。这可由下列的指令来完成:
xconsole$ mkdir trash
xconsole$ mv * trash
xconsole$ rm -rf trash
xconsole$ mkdir trash
不需在交谈式的 shell 上敲进所有指令,改用撰写个 shell 程序:
#!/bin/bash
mkdir trash
mv * trash
rm -rf trash
mkdir trash
echo "Deleted all files!"
把它以 clean.sh 存档,现在你所要做的是执行 clean.sh,它会移动所有的档案到一个目
录,删除他们,再产生目录,而且打印一个讯息告诉你,它已成功地删除所有的档案。所
以记得,如果你发现要做的某些事,要一而再,再而做三地做下去,考虑用个 shell 程序
来自动执行。
COMMENTS / 批注
批注可让你的程序更容易明了,这样并不影响程序的输出结果。目的就是来特别帮你明了
程序。所有在 bash 里的批注,第一个字符都是用井字符号(hash symbol):"#",除了第
一个行 (#!/bin/bash) 外。 第一行并不是批注。在第一行之后,其余以 "#" 开始的任何
一行都是批注。看下列的程序片段:
#!/bin/bash
# 这个程序从 1 记录到 10:
for i in 1 2 3 4 5 6 7 8 9 10; do
echo $i
done
即使你不知道 bash 的程序设计,因为批注的关系,也能立刻知道上述的程序在做什么事
。使用批注是很好的习惯。你将发现若未来需要维护你的程序,有批注会比较容易。
VARIABLES / 变数
变量基本上是储存数值的 "箱子(boxes)" 。有许多理由需要产生变量。做为储存使用者输
入,参数或数字的数值。举下列的程序代码来说:
#!/bin/bash
x=12
echo "The value of variable x is $x"
刚才所做的,是给 x 变数一个数值 12,而 echo "The value of variable x is $x" 这
行则是打印出目前变量 x 的值。当你定义一个变量时,不能有任何的空白在设定操作数:
"=" 之间。语法为:
variable_name=this_value
存取变数的数值藉由变量名称前加一个钱字符号: "$" 进行。如同上面的例子,我们用
echo $x 来存取变量 x 的数值。
变量有二种类型。区域变量,和环境变量。环境变量是由系统设定的,通常藉由使用 env
指令来运用。环境变量保有特别的数值。举例来说,如果你键入:
xconsole$ echo $SHELL
/bin/bash
你会取得到目前执行 shell 的名字。环境变量被定义在 /etc/profile 和 ~/.bash_prof
ile 中。echo 指令适合用来检查目前区域和环境变量的数值。若你对为何需要使用变量仍
有疑问,在这里有个相当好的例子:
#!/bin/bash
echo "The value of x is 12."
echo "I have 12 pencils."
echo "He told me that the value of x is 12."
echo "I am 12 years old."
echo "How come the value of x is 12?"
好,现在假设你决定要将 x 的数值用 8 代替 12,你要怎么做?你必须改变所有的程序代
码中有 x is 12 的那几行。等等... 程序代码中也有其它行有数字 12。你也应该改变吗
?不,因为他们与 x 无关。感到困惑了?现在,在这里有相同的例子,但它使用变量:
#!/bin/bash
x=12 # 设定变量 x 的值为 12
echo "The value of x is $x."
echo "I have 12 pencils."
echo "He told me that the value of x is $x."
echo "I am 12 years old." echo "How come the value of x is $x?"
在这里,我们看见 $x 会打印出变量 x 目前的数值 12。所以现在,如果你要改变 x 的数
值为 8,必须做的是将 x=12 这行改成 x=8,程序会自动地将所有有 $x 那几行显示成 8
,代替原本的 12。其它行并不受影响。变量也有其它重要的用途,稍后会看得到。
CONTROL STRUCTURES / 控制性结构
控制性结构让你的程序可做判断而且使他们更紧凑。更重要的是,它让我们可做错误的检
查。到目前为止,我们所写的程序是从头到尾执行,直到没有指令为止。举例来说:
#!/bin/bash
cp /etc/foo .
echo "Done."
这个短短的 shell 程序,称它 bar.sh 好了,拷贝一个名为 /etc/foo 的档案到目前的目
录内,并打印 "Done" 到屏幕上。这个程序只在一个条件之下可运作,就是你必须有个名
为 /etc/foo 的档案,否则会发生什么:
xconsole$ ./bar.sh
cp: /etc/foo: No such file or directory
Done.
就像你看到的,有问题。并不是执行你的程序的每个人,他们的系统里有 /etc/foo 这个
档。如果你的程序能检查是否有 /etc/foo 档案存在,然后若真的存在,再着手进行拷贝
,否则,就离开。这样或许会是比较好的。在虚拟的程序代码中,将会是像这样:
if /etc/code exists, then
copy /etc/code to the current directory
print "Done." to the screen.
otherwise,
print "This file does not exist." to the screen
exit
能在 bash 中实作吗?当然可以!bash 的控制性结构计有,if, while, until, for 和
case。每个结构都是成对的,意思是起始用个 "标签",终止也用个 "标签"。举例来说,
if 结构开始为 if,结束时为 fi。控制性结构并不是在你系统上的程序,他们是在 bash
中的一项功能(feature)。这意谓着,你将可写自己的程序代码,并不只是将其它程序包在 shell 程序中。
if ... else ... elif ... fi
最常用的结构之一就是 if 结构。它允许你的程序下判断,就像是 "如果这条件存在,执
行它,其余的,执行其它的东西"。要发挥 if 结构的功效,我们就必须使用 test 指令。
test 检查条件的状况,也就是说,存在的档案,权限,或类似和不同等状况。以下为重写
过的 bar.sh:
#!/bin/bash
if test -f /etc/foo
then
# 档案存在,则拷贝档案然后打印出一个讯息
cp /etc/foo .
echo "Done."
else
# 档案不存在,则打印出一个讯息然后离开
echo "This file does not exist."
exit
fi
注意我们在 then 和 else 之后的叙述用锯齿状排列。锯齿状排列是选择性的,但这样感
觉上容易明了,哪些叙述在哪一条件下被执行。现在执行这个程序。如果你有 /etc/foo
这个档案,那么它将会拷贝档案,否则,它将会打印错误讯息。test 检查是否档案 /etc
/foo 存在。选项 -f 检查参数是否是一般性的档案。以下是 test 的选项:
-d 检查此 file 是否是一个目录
-e 检查此 file 是否存在
-f 检查此 file 是否为一般的档案
-g 检查此 file 是否有 SGID 权限
-r 检查此 file 是否可以读取
-s 检查此 file 大小是否不为 0
-u 检查此 file 是否有 SUID 权限
-w 检查此 file 是否可以写入
-x 检查此 file 是否可以执行
else 是用在若第一个条件不匹配时,还要你的程序做别的东西。另有个 elif,可用在 i
f 结构中还有另一个 if 的时候。基本上 elif 代表 "else if",当第一个条件不匹配,
而你想要测试另外的条件时,可用它。
若你认为下列 if 和 test 的格式不易阅读:
if test -f /etc/foo
then
然后,你可以像这样做:
if [ -f /etc/foo ]; then
用中括号形式的 test。若你有 C 语言的程序设计经验,这种语法可能感觉更亲切。需注
意必须用空白(white space)围住两个中括号。分号 ";" 是用来告诉 shell 指令到此为止
。分号后的所有叙述会被当成新的一行叙述处理。基本上这样做比较容易阅读,当然啰,
也可以不这样做;若你喜欢,可把 then 放在下一行上。
当变数和 test 一起时,最好的作法是用双引号将变数围住。例如:
if [ "$name" -eq 5 ]; then
while ... do ... done
while 语法结构是一个递归结构。基本上它的运作是 "当这个条件为真的时候,做这直到
条件不再为真时"。让我们看这个例子:
#!/bin/bash
while true; do
echo "Press CTRL-C to quit."
done
true 实际上是支程序。它所做的是不断地让回路不会停止。使用 true 是为了让程序慢一
些,因 shell 程序必须先呼叫它起来然后在执行它。也可用另外一个选择 - 冒号 ":" 指
令:
#!/bin/bash
while :; do
echo "Press CTRL-C to quit."
done
这样也可达到相同的结果,但是会比较快些,因它是内建在 bash 的一项功能(feature)。
唯一的不同是你为了速度而牺牲了可读性。选一个你感觉较亲切的用吧。或许这里有一个
更有用的例子,使用变量:
#!/bin/bash
x=0; # 设定 x 初值为 0
while [ "$x" -le 10 ]; do
echo "Current value of x: $x"
# 增加 x 的数值:
x=$(expr $x + 1)
sleep 1
done
就如你所见的,我们正在这儿使用 test (用中括号的形式) 检查变量 x 的状态。选项 -
le 检查是否 x 小于,或等于与数值 10。上面的程序代码换做口语来说,即 "当 x 小于
10 或与 10相同时,打印出 x 目前的数值,然后将 x 目前的数值增加 1 。" 。sleep
1 的作用只是把程序暂停一秒,你也可以移除这行。在此,你可发现我们在这里所做的是测试相等状况。检查是
否变量等于某一数值,若是如此,则执行动作。以下列出相等状况的测试:
检查在数目之间的相等:
x -eq y 检查是否 x 和 y 相等
x -ne y 检查是否 x 和 y 不相等
x -gt y 检查是否 x 大于 y
x -lt y 检查是否 x 小于 y
检查在字符串之间的相等:
x = y 检查是否 x 与 y 相同
x != y 检查是否 x 与 y 不相同
-n x 若 x 不是空字符串(null)则为真
-z x 若 x 是空字符串(null)则为真
上面我们写的递归叙述应该不难了解,除了这一行:
x=$(expr $x + 1)
上面的批注告诉我们,那是让 x 每次增加 1。但 $(...) 是什么意思呢?它是变数吗?事
实上,它是告诉 shell 你要执行指令 expr $x + 1,而且要将结果设定到 x。任何包含在
$(...) 的指令将会被执行:
#!/bin/bash
me=$(whoami)
echo "I am $me."
去试试,你会了解我的意思。上述的程序代码能用下列的叙述,获得相同的结果:
#!/bin/bash
echo "I am $(whoami)."
你可自行决定哪一个对你是比较容易的阅读。有另外的方法也可以执行指令或将指令的结
果设给变量。这稍后再详细解释。现在,先使用 $(...)。
until ... do ... done
until 的语法结构与 while 的语法结构是非常相似的。唯一的不同是条件判断相反。whi
le 语法结构,当条件为真的时候才执行循环。until 语法结构,执行循环直到条件为真的
时候停止。所以基本上它是 "执行它,直到这个条件是真的"。这里有个例子:
#!/bin/bash
x=0
until [ "$x" -ge 10 ]; then
echo "Current value of x: $x"
x=$(expr $x + 1)
sleep 1
done
这段程序代码可能看起来很熟悉。试一试,看看结果为何。基本上,until 会继续循环,
一直到 x 大于或等于 10。当到达数值 10 的时候,回路将停止。因此,最后一次打印出
x 的数值将会是 9。
for ... in ... do ... done
for 的语法结构是,当你须透过变量的范围执行循环时所使用。举例来说,你可写个小程
序于每秒打印出 10 点:
#!/bin/bash
echo -n "Checking system for errors"
for dots in 1 2 3 4 5 6 7 8 9 10; do
echo -n "."
echo "System clean."
done
此例中,你可能不知道,-n 选项是让 echo 自动地加一行。试一试,一次有 -n 选项,一
次没有,就会了解我的意思。变量 dots 从 1 循环到 10,而且在每个数值印出一个点。
再试下面这个例子,看看我所谓的变量透过这些值执行循环的涵义:
#!/bin/bash
for x in paper pencil pen; do
echo "The value of variable x is: $x"
sleep 1
done
当你执行这个程序时,会看见 x 的数值首先是 paper,然后变为下一个数值 pencil, 然
后再下一个数值 pen。当发现没有数值时,循环就结束。
这里有个更实用的例子。下面的程序加扩展名 .html 到目前目录里的所有档案:
#!/bin/bash
for file in *; do
echo "Adding .html extension to $file..."
mv $file $file.html
sleep 1
done
* 是个万用字符。它代表 "在目前目录的所有东西",在这个例子中,是指目前目录里的所
有档案。目前目录里的所有档案会加入 .html 的扩展名。回想变量 file 会递归过所有的
数值,即目前目录里的所有档案。然后用 mv 来重新命名变量 file,增加 .html 扩展名
。
case ... in ... done
case 的语法结构是非常相似于 if 的语法结构。基本上它是有许多条件被检查,而你不想
一而再地使用 if 时用的。看下面这段程序:
#!/bin/bash
x=5 # 设定 x 初值为 5
# 现在检查 x 的数值:
case $x in
0) echo "Value of x is 0."
;;
5) echo "Value of x is 5."
;;
9) echo "Value of x is 9."
;;
*) echo "Unrecognized value."
esac
这个 case 语法结构将检查 x 数值 3 种可能性。此例中,首先它检查是否 x 有数值为
0,然后检查数值是否是 5,再检查数值是否是 9。最后,如果所有的检查失败,它会产生
讯息 "Unrecognized value."。记得 "*" 表示 "每件事物",而在这里代表 "除了被叙述
的东西之外,任何其它的数值"。如果 x 是任何其它的数值而非 0, 5,或 9,这数值会落进入 * 类别中。当使
用 case 的时候,每个条件必须以二个分号结束。当你能使用 if 的时候,为什么要烦恼
使用 case 呢?这里有相同结果的程序,是以 if 写成。看哪一个写得比较快,比较容易
阅读:
#!/bin/bash
x=5 # # 设定 x 初值为 5
if [ "$x" -eq 0 ]; then
echo "Value of x is 0."
elif [ "$x" -eq 5 ]; then
echo "Value of x is 5."
elif [ "$x" -eq 9 ]; then
echo "Value of x is 9."
else
echo "Unrecognized value."
fi
QUOTATIONS / 引号
引号在 shell 程序设计中扮演一个重要的角色,总共有三种形式的引号。他们分别是双引
号(double quote): ",单引号(forward quote): ',和反单引号(back quote): `。每
一种都有不同的涵义吗?答案是对的。
双引号主要被用来保有一串字并留住空白。举例来说,"This string contains whitespa
ce." - 这个字符串包含空白,被两个双引号所围住的字符串,会被当做一个自变量处理。
例如下面这些例子:
xconsole$ mkdir hello world
xconsole$ ls -F
hello/ world/
在这里我们产生二个新的目录。mkdir 将字符串 hello 和 world 当做两个自变量,因此
产生二个新的目录。现在,你如下这样做,看看会发生什么事:
xconsole$ mkdir "hello world"
xconsole$ ls -F
hello/ hello world/ world/
它产生了一个有二个字的新目录。双引号将二个字成为一个自变量。没有双引号,mkdir
认为 hello 是第一个自变量,world 是第二个。
单引号主要被用来处理变量。如果一个变量被围在双引号中,它的数值将会被解释出。如
果它是被单引号围住,它的数值将不会被解释。想弄得更清处些,试试下面的例子:
#!/bin/bash
x=5 # 设定 x 初值为 5
# 使用双引号
echo "Using double quotes, the value of x is: $x"
# 使用单引号
echo 'Using forward quotes, the value of x is: $x'
看见不同了吗?若你不打算在一字符串中使用变量,可用双引号围住字符串。此例中你是
否觉得奇怪呢?对的,单引号能用来保留住空白,如同双引号一般:
xconsole$ mkdir 'hello world'
xconsole$ ls -F
hello world/
反单引号与双引号及单引号是完全地不同,它们不是用来保护空白。若你回想起在之前我
们用的这个叙述:
x=$(expr $x + 1)
就如你所知的,指令 expr $x + 1 的结果被指定为变量 x。相同的结果能用反单引号来完
成:
x=`expr $x + 1`
你应该使用哪一个呢?无论那一个你较喜欢。会发现反单引号用得比较 $(...) 还多。然
而,我发现 $(...) 比较容易阅读,尤其是有如这样的一个例子时:
$!/bin/bash
echo "I am `whoami`"
ARITHMETIC WITH BASH / 用 BASH 算术
bash 允许你执行算术语法。如你所见,算术以 expr 指令方式执行。然而,这就像 true
的指令,执行速度较慢。原因是为了要执行 true 和 expr,shell 必须去启动他们。比
较好的方法是使用 shell 内建功能(feature),会较快。如同之前所见,true 指令的另一
选择是 ":" 指令。而 expr 指令的另一选择是将算数表达式围在 $((...)) 之内。这不同于 $(...),括号的数
目已告诉你了。让我们试试:
#!/bin/bash
x=8 # 设定 x 初值为 8
y=4 # 设定 y 初值为 4
# 现在我们将 x 和 y 的总合之值设定到 z:
z=$(($x + $y))
echo "The sum of $x + $y is $z"
如同以往,无论你选择那一个,纯粹地是由你决定。如果你感觉使用 expr 比 $((...))
亲切,就使用它吧。
bash 能够执行加减乘除和余数运算。每个动作有相对应的操作数:
ACTION / 动作 OPERATOR / 操作数
Addition /加 +
Subtraction / 减 -
Multiplication / 乘 *
Division / 除 /
Modulus / 余数 %
每个人应该很熟悉前面的四则运算。你不知道余数是什么?就是当二个数值相除时,所剩
余的数值。这里有个在 bash 中算数的例子:
#!/bin/bash
x=5 # 设定 x 初值为 5
y=3 # 设定 y 初值为 3
add=$(($x + $y)) # 将 x 和 y 相加,并将其和设定到变量 add
sub=$(($x - $y)) # 将 x 数值减 y 数值,并将其差设定到变量 sub
mul=$(($x * $y)) # 将 x 和 y 相乘,并将其积设定到变量 mul
div=$(($x / $y)) # 将 x 数值除以 y 数值,并将其商数设定到变量 div
mod=$(($x % $y)) # 将 x 数值除以 y 数值,并将其余数设定到变量 mod
# 印出这些答案:
echo "Sum: $add"
echo "Difference: $sub"
echo "Product: $mul"
echo "Quotient: $div"
echo "Remainder: $mod"
上面的程序代码也能够改用 expr。例如,你可用 add=$(expr $x + $y) 或 add=`expr $
x + $y` 来代替 add=$(($x + $y))。
READING USER INPUT / 读取使用者输入
现在我们已进入有趣的部份了。你可让程序与使用者产生互动,而取得使用者输入的指令
为 read。read 是内建在 bash 的指令,需要使用变量,如下面范例一样:
#!/bin/bash
# 取得到使用者的名字,印出问候语
echo -n "Enter your name: "
read user_name
echo "Hello $user_name!"
在这里变量为 user_name。当然啰,你能换成你想要的。read 会等待使用者输入一些东西
,然后按 ENTER。如果使用者不输入任何东西,read 会继续等候直到 ENTER 键被按下。
如果 ENTER 键被按下时并没有任何东西被输入,read 仍会执行下一行程序代码。试一试
。这里有个相同的例子,只是这次我们检查看看是否使用者有输入任何东西:
#!/bin/bash
# 取得到使用者的名字,印出问候语
echo -n "Enter your name: "
read user_name
# 使用者没有输入任何东西:
if [ -z "$user_name" ]; then
echo "You did not tell me your name!"
exit
fi
echo "Hello $user_name!"
在这程序中,如果使用者没有输入任何东西就按下 Enter键,我们的程序会抱怨而且退出
。否则,它会打印出问候语。取得使用者输入,对需要使用者输入某事物的交谈式程序而
言,是非常有用的。举例来说,你能够建立简单的数据库,并且让使用者输入数据加入到
你的数据库中。
FUNCTIONS / 函式
函式能让程序撰写变得更容易,也较容易维护。基本上它是将程序打散成多个小段落。一
个函式执行一项你所定义的功能,若你想要它传回一个值也行。在我继续介绍之前,这里
有个 shell 程序使用函式的例子:
#!/bin/bash
# 函式 hello() 只是打印出一个讯息
hello()
{
echo "You are in function hello()"
}
echo "Calling function hello()..."
# 呼叫 hello() 函式:
hello
echo "You are now out of function hello()"
试试去执行上述程序。函式 hello() 只有一个目的,那就是打印一个讯息。函式当然是拿
来做比较复杂的工作。在上述的例子中,我们像下面那行藉由函式名称来呼叫 hello() 函
式:
hello
当这行被执行时,bash 会搜寻程序中 hello() 这行。发现它在程序上端,然后执行它的
内容。
函式总藉由它的名字来呼叫,就如我们在上述程序中看到的一样。当撰写函式时,你可用
function_name() 开始,如我们在上面范例所做的一样,或者用更清楚的表示方式 func
tion function_name()。这里是函式 hello() 的另一种写法:
function hello()
{
echo "You are in function hello()"
}
函式总有一组空的左右小括号: "()",再接一组大括号: "{...}"。这组大括号标示此函
式的开始与结束。包括在这组大括号中的任何程序代码都会被执行,而且只属于此函式。
函式在被呼叫之前,应该先被定义。让我们再次看看上述的程序,只是这次在函式被定义
之前,我们先呼叫它:
#!/bin/bash
echo "Calling function hello()..."
# 呼叫 hello() 函式:
hello echo "You are now out of function hello()"
# 函式 hello() 只是打印出一个讯息
hello()
{
echo "You are in function hello()"
}
以下是我们试着执行它所得的结果:
xconsole$ ./hello.sh
Calling function hello()...
./hello.sh: hello: command not found
You are now out of function hello()
就如你所见的,我们得到一个错误讯息。因此,你的函式会在你的程序代码的开始,或者
至少在你呼叫函式之前。这里有另一个使用函式的例子:
#!/bin/bash
# admin.sh - administrative tool
# 函式 new_user() 建立一个新的使用者账号
new_user()
{
echo "Preparing to add a new user..."
sleep 2
adduser # 执行 adduser 程序
}
echo "1. Add user"
echo "2. Exit"
echo "Enter your choice: "
read choice
case $choice in
1) adduser # 呼叫 adduser() 函式
;;
*) exit
;;
esac
为了要能正常的执行,你需要登入成 root 使用者,因为 adduser 只有 root 才能执行。
希望这个例子 (虽然很短) 能展现出函式的功用。
TRAPPING
你可以在程序中使用内建的 trap 指令捕捉信号。这是优美地退出程序的好方法。举例来
说,如果你有个程序正在执行,按下 CTRL-C 将会送给程序一个岔断信号,因而砍掉此程
序。trap 允许你去抓取这个信号,并且让你有机会让程序继续,或告诉使用者,程序将要
离开。trap 使用下列的语法:
trap action signal
action 为当信号被启动时你所要做的动作,而 signal 是你要寻找的信号。信号的列表能
藉由使用指令 trap -l 取得。当在 shell 程序使用信号的时候,会省略信号最前面的三
个字母,通常是 SIG。举例来说,岔断信号(interrupt signal) 是 SIGINT。而在 shell
程序,只使用 INT。也可使用伴随着信号名称旁的信号编号。举例来说,SIGINT 数字型式信号的数值为 2。试
试下列的程序:
#!/bin/bash
# 使用 trap 指令
# trap CTRL-C 执行 sorry() 函式:
trap sorry INT
# 函式 sorry() 打印出一个讯息
sorry()
{
echo "I'm sorry Dave. I can't do that."
sleep 3
}
# 从 10 数到 1:
for i in 10 9 8 7 6 5 4 3 2 1; do
$i seconds until system failure."
sleep 1
done
echo "System failure."
现在,当程序正在执行并且往下数的时候,按 CTRL-C,将会送一个岔断信号给程序。然而
,此信号将会被 trap 指令捕捉,而执行 sorry() 函式。你可用 "''" 置换动作,来忽略
信号。也可用减号: "-",让 trap 重新设定。举例来说:
# 如果 SIGINT 被捕捉﹐执行 sorry() 函式:
trap sorry INT
# 重新设定 trap:
trap - INT
# 当 SIGINT 被捕捉时,什么也不做:
trap '' INT
当你重新设定 trap 的时候,内定值为最初的动作,即中断程序而且砍掉它。当你设成什
么也不做的时候,就是这样,什么事也不做。程序将继续执行,忽略讯号。
AND & OR
我们已看过控制性结构的使用,和他们的妙用。还有二件额外的东西要来探讨。那就是 A
ND: "&&" 及 OR: "||" 叙述。AND 叙述看起来像这样:
condition_1 && condition_2
AND 叙述首先检查最左边的条件。如果它是真的,然后检查第二个条件。如果也是真的,
那么其余的程序代码会被执行。如果 condition_1 响应是错的,那么 condition_2 将不
会被执行。换句话说:
if(如果) condition_1 是真的,AND(而且) if(如果) condition_2 是真的,then(然后)
...
在这里有使用 AND 叙述的例子:
#!/bin/bash
x=5
y=10
if [ "$x" -eq 5 ] && [ "$y" -eq 10 ]; then
echo "Both conditions are true."
else
echo "The conditions are not true."
fi
我们发现 x 和 y 两者的值都是我们要检查的,所以这些条件都是真的。假如果改变 x=5
的数值为 x=12,然后重新执行程序,会发现条件现在是错的。
OR 叙述的用法相似。唯一的不同就是它检查是否最左边的叙述是错的,如果是错的,然后
到下一个叙述,再继续下一个:
condition_1 || condition_2
在虚拟的程序代码(pseudo code)中,这将翻成下列叙述:
if(如果) condition_1 是真的,OR(或者) if(如果) condition_2 是真的,then(然后).
..
因此,至少其中一个测试条件是真的,后面的程序代码就会被执行:
#!/bin/bash
x=3
y=2
if [ "$x" -eq 5 ] || [ "$y" -eq 2 ]; then
echo "One of the conditions is true."
else
echo "None of the conditions are true."
fi
在这里,你看到其中一个条件是真的。然而,改变 y 的数值再执行程序,你会看见没有条
件是真的。
想想看,若 if 的结构能用来代替 AND 及 OR,将需要用巢状的叙述。巢状的意思是 if
结构中还有另一个 if 结构。巢状的情况当然也可能是在其它的控制性结构上。这里有个
巢状 if 结构的例子,同之前的 AND 程序代码:
#!/bin/bash
x=5
y=10
if [ "$x" -eq 5 ]; then
if [ "$y" -eq 10 ]; then
echo "Both conditions are true."
else
echo "The conditions are not true."
fi
fi
也可达成与 AND 叙述相同的目的,但比较难以了解,且需写较多的程序代码。省点力气吧
,去用 AND 及 OR 叙述。
USING ARGUMENTS
你可能已经注意到 Linux 里的大部分程序都不是交互式的。你需要键入参数,否则会得到
"usage" 的讯息。拿 more 指令来说,如果你不输入文件名称在它之后,会响应一个 "u
sage" 的讯息。你的 shell 程序有可能运用到参数,因此,你需要知道 "$#" 变量。这个
变量代表这个程序的参数个数。举例来说,执行一个程序如下:
xconsole$ foo argument
$# 的数值是 1,因为程序只有一个参数。若有二个参数,那么 $# 的数值是 2。除此之外
,命令列上的每个字,即程序的名称 (在此例为 foo),和参数,能被参考成 shell 程序
中的变量。foo 会是 $0,argument 会是 $1,最多可达 9 个变数,接在 $0 (为程序名称
) 之后,$1 到 $9 对应每个参数。让我们瞧瞧这个动作:
#!/bin/bash
# 打印第一个参数
# 首先检查是否有一个参数:
if [ "$#" -ne 1 ]; then
echo "usage: $0 "
fi
echo "The argument is $1"
这个程序预期在执行时有一个,而且是只有一个参数。如果输入少于一个参数,或多过一
个,程序会打印用法讯息。否则,会有一个参数传进程序,而这 shell 程序会印出你传进
的参数。回想一下,$0 是程序的名称。这就是它为什么被用在 "usage" 讯息的原因。最
后一行使用到 $1。再回想一下,$1 的数值是传进此程序的参数。
REDIRECTION AND PIPING / 转向及管线
一般来说,当你执行指令的时候,它的输出被打印到屏幕上头。举例来说:
xconsole$ echo "Hello World"
Hello World
转向允许将输出结果转到别处,通常像是档案。">" 操作数用来做转向输出。想想它就像
个箭头,告诉输出该往哪里去。这里有个重新转向输出到档案的例子:
xconsole$ echo "Hello World"> foo.file
xconsole$ cat foo.file
Hello World
这里, echo "Hello World" 的输出会被重新转向到 foo.file 的档案。我们读取档案的
内容时候,看到了原本的输出。 ">" 操作数有个问题,它将会覆盖任何的档案内容。若你
要用附加的方式该怎么做?你就必须使用附加操作数: ">>"。用法与转向操作数相同,但
它将不会覆盖档案的内容,而是附加于后。
最后,是有关管线。管线允许你将一个程序的输出结果,当做另外一个程序的输入。管线
的运作是靠使用管线操作数: "|"。注意这不是 "L" 的小写字母。管线符号的输入是按下
SHIFT- 这个组合键。下面有个管线的例子:
xconsole$ cat /etc/passwd | grep xconsole
xconsole:x:1002:100:X_console,,,:/home/xconsole:/bin/bash
此处,我们读入整个 /etc/passwd 然后将输出用管线送到 grep,grep 依次搜寻有包含字
符串 xconsole 的那一整行,然后将其结果打印到屏幕。你也能混用转向的功能,将最后
的输出存成一个档案:
xconsole$ cat /etc/passwd | grep xconsole > foo.file
xconsole$ cat foo.file
xconsole:x:1002:100:X_console,,,:/home/xconsole:/bin/bash
成功了。/etc/passwd 被读取,然后整个的输出被管线送进 grep 搜寻字符串 xconsole。
最后的输出被转向储存进 foo.file 档案内。将来你会发现转向及管线对写自己的 shell
程序来说,是相当有用的工具。
TEMPORARY FILES / 暂存档
通常,你会有需要产生临时文件的时候。这个档案可能是暂时保存一些数据,或只是与程
序一起运作。一旦程序的目的被完成,此档案通常会被删除。当产生档案的时候,必须给
它一个档名。问题是,你产生的档案,不能是在所要产生档案的目录中已存在。否则,有
可能会覆盖掉重要的数据。为了要产生唯一档名的暂时档案,你需要使用 "$$" 符号接在文件名的前缀或字尾。举
例来说,你要用 hello 这个名称产生暂时的档案。而此时刚好有个使用者执行你的程序,
而有个名为 hello 的档案,所以会与你的程序临时文件相冲突。藉由产生一个称为 hell
o.$$ 或 $$hello 的档案,将会产生出唯一档名的档案。试试:
xconsole$ touch hello
xconsole$ ls
hello
xconsole$ touch hello.$$
xconsole$ ls
hello hello.689
你的暂存档就在那里。
RETURN VALUES / 传回数值
大多数的程序会依他们如何结束离开而传回一个数值。举例来说,如果你仔细看 grep 的
手册,它会告诉我们如果搜寻字符串有被找到,grep 会传回一 0 值,没被找到,则传回
一 1 的值。我们为什么要关切程序的传回值呢?有各种不同的理由。假设我们要检查是否
系统上存在个特定的使用者。方法之一是在 /etc/passwd 檔中 grep 此位使用者的名字。让我们假设使用者的名
字是 foobar:
xconsole$ grep "foobar" /etc/passwd
xconsole$
没有输出。这表示 grep 没有发现匹配的字符串。然而若有个讯息印出说没有找到匹配的
字符串,会更有帮助的。这就是你需要抓取程序的传回值的时候。有个特别的变量保存程
序的传回值。这个变量就是 $?。看看下列一段程序代码:
#!/bin/bash
# grep 使用者 foobar,并用导引所有输出到 /dev/null:
grep "foobar" > /dev/null 2>&1
# 抓取传回值,并且做些动作:
if [ "$?" -eq 0 ]; then
echo "Match found."
exit
else
echo "No match found."
fi
现在当你执行此程序时,它会抓取 grep 的传回值。如果值等于 0,那么有匹配的字符串
被找到,而且适当的讯息会被列出。否则,它将会列出没有匹配的字符串被找到。这是个
从程序取得传回值非常基本的使用范例。当你继续练习的时候,会发现有很多时候你会需
要用程序的传回值来做你所要的。
现在,如果你的 shell 程序要离开时,要传回什么数值呢? exit 指令可带一个参数,一
个数字传回。一般来说,数字 0 用来指成功地离开,没有错误发生。其它高于或低于 0
的数字表示有错误发生。这是由你,程序设计者决定的。让我们来看这个程序:
#!/bin/bash
if [ -f "/etc/passwd" ]; then
echo "Password file exists."
exit 0
else
echo "No such file."
exit 1
fi
藉由于离开时指定的传回值,其它你所写的 shell 程序若有用到此程序,将可抓取它的传
回值。
函式也可传回数值。是利用 return 的指令来施行,可传回一个参数,数字。除了它只适
用于函式以外,用法就如同 exit 一样。如以下的例子:
check_passwd()
{
# 检查是否 passwd 档案存在:
if [ -f "/etc/passwd" ]; then
echo "Password file exists."
# 有找到,传回一个 0 值:
return 0
else
# 找不到,传回一个 1 值:
echo "No such file."
return 1
fi
}
# 从函式 check_passwd 取得传回值:
foo=check_passwd
# 检查数值:
if [ "$foo" -eq 0 ]; then
echo "File exists."
exit 0
else
echo "No such file."
exit 1
fi
注意看这程序代码,应不难了解。我们于一开始时,使用一个叫做 foo 的变量,来储存函
式 check_passwd() 的传回值。函式 check_passwd(),用来检查看看是否 /etc/passwd
档案存在。如果存在,我们返回一 0 值,否则返回一 1 值。现在,如果档案存在而且一
0 值被传回,那么变数 foo 的数值会是 0。如果一 1 值被传回,那么变数 foo 的数值将会是 1。接下来的动
作是检查变量 foo 的数值,打印出适当的讯息,然后离开,并传回一数值 0 (成功) 或
1 (失败) 。
CONCLUSION / 结论
虽然已读完了本篇 bash 程序设计的简介,但你的程序设计研习并未完成,还有很多要学
习。如我所言,这只是个简介。然而,这足以让你开始修改并且撰写属于你自己的 shell
程序。如果你真地要精通 shell 程序,我推荐买本 O'Reilly & Associates, Inc. 出版
的 Learning the bash shell, 2nd Edition。bash 程序对日常的管理使用相当好用,但是若你用在大型的计划
上,用更强大的语言像 C 或 Perl 较适当。祝好运。
X_console [email protected]
--
※ 修改:·xpmo 於 04月18日21:08:14 修改本文·[FROM: 郁金香BBS站]
※ 来源:.郁金香BBS站 http://bbs.stu.edu.cn[FROM: 10.^_^.8.11]
--
※ 来源:·郁金香BBS站 bbs.stu.edu.cn·[FROM: 郁金香BBS站]
--------------------------------------------------------------------------------
[转寄][转贴][删除文章][修改文章][本讨论区][上一篇][下一篇][回文章][同主题阅读]