Bash 程序设计
控制结构:控制结构有 if...then 、 for...in 、 while 、 until 、 case 语句 。与控制结构配合使用的还有break 和continue 语句也可用于调整shell 脚本中的命令执行顺序。
if...then :
if test-command
then
command
fi
test 内置命令:if 测试test-command 返回状态,并基于这个状态转移控制。if 语句的结束由fi 标记。
echo -n “word 1:”
read word1
echo -n “word 2:”
read word2
if test “$word1” = ”$word2”
then
echo “Match”
fi
echo “End of program.”
上面echo 利用'-n' 选项来表示输出echo 参数后不用换行。
在bash 中test 是一个内置命令,也就是它是shell 的一部分。同时还有一个单独的工具test 。通常某个命令的工具版本不存在,而内置命令版本可用时,使用后者。test 测试语句中使用的'=' 两边的空白字符不可以省略。
test 的操作符'-eq' 比较两个整数,特殊变量$# 表示命令行参数的个数。
if test $# -eq 0
then
echo “You must supply at least one argument.”
exit 1
fi
echo “Program running.”
可以利用'info test' 得到完整的test 用法信息:
测试文件类型:'-b FILE' '-c FILE' '-d FILE' '-f FILE' '-h FILE' '-L FILE' '-p FILE' '-S FILE' ‘-t FD’
访问权限测试:'-g FILE' '-k FILE' '-r FILE' '-u FILE' '-w FILE' '-x FILE' '-O FILE'
'-G FILE'
文件特征比较:'-e FILE' '-s FILE' 'FILE1 -nt FILE2' 'FILE1 -ot FILE2'
'FILE1 -ef FILE2'
字符串测试:'-z STRING' '-n STRING' 'STRING' 'STRING1 = STRING2'
'STRING1 != STRING2'
整数测试:'ARG1 -eq ARG2' 'ARG1 -ne ARG2' 'ARG1 -lt ARG2' 'ARG1 -le ARG2'
'ARG1 -gt ARG2' 'ARG1 -ge ARG2'
测试条件连接词:'! EXPR ' 'EXPR1 -a EXPR2 ' 'EXPR1 -o EXPR2 '( 注意中间的空白符号)
[] 与test 同义 可以把test 的参数用方括号括起来,以代替在脚本中使用关键字test 。括号两边必须有空白符
if [ $# -eq 0 ]
then
ehco “Usage : chkarg2 argument...” 1>&2
exit 1
fi
echo “Program running.”
exit 0
用法信息 上面例子显示的信息称为用法信息,使用 '1>&2' 标记可把这个输出重定位到标准错误输出 。在脚本正常运行后一般退出状态为 0 ,如果退出状态非 0 表示遇到一个错误。
if...then...else
if test-command
then
commands
else
commands
fi
if test-command;then
commands
else
commands
fi
下面的shell 脚本,参数为文件名,该脚本将文件内容显示在终端上。如果第一个参数为'-v' ,该脚本将使用less 命令分页显示。
if [ $# -eq 0 ]
then
echo “Usage: out [-v] filename...” 1>&2
exit 1
fi
if [ “$1” = “-v” ]
then
shift
less -- “$@”
else
cat -- “$@”
fi
在上面的脚本中,cat 和 less 命令的 '--' 参数通知调用这些工具的命令行它后面再没有选项了,即不再把 '--' 后面以连字符开头的参数作为选项。‘ rm -- -example.sh’( 删除文件 -example.sh).
shift 和 set 对位置参数的操作。
if … then … elif :
if test-command
then
commands
elif test-command
then
commands
…
else
commands
fi
示例:
echo -n “Word 1:”
read word1
echo -n “Word 2:”
read word2
echo -n “Word 3:”
read word3
if [ “$word1” = “$word2” -a “$word1” = “$word3” ]
then
echo “Match : words 1,2 & 3”
elif [ “$word1” = “$word2” ]
then
echo “Match : word 1 & 2”
elif [ “$word2” = “$word3” ]
then
echo “Match : word 2 & 3”
else
echo “No Match”
fi
上面的脚本第一个if 语句使用布尔操作符 AND(-a) 作为 test 的一个参数。注意 '=' '[' 和 ']' 两边的空白符不可省略。
1 #!/bin/bash
2 # Identify links to a file
3 # Usage: lnks file [directory]
4 if [ $# -eq 0 -o $# -gt 2 ]
5 then
6 echo "Usage:lnks file [directory]" 1>&2
7 exit 1
8 fi
9
10 if [ -d "$1" ]
11 then
12 echo "First argument cannot be a directory." 1>&2
13 exit 1
14 else
15 file="$1"
16 fi
17 if [ $# -eq 1 ]
18 then
19 directory="."
20 elif [ -d "$2" ]
21 then
22 directory="$2"
23 else
24 echo "Optional second argument must be a directory." 1>&2
25 echo "Usage:lnks file [directory]." 1>&2
26 exit 1
27 fi
28
29 #check that file exists and is a regular file:
30 if [ ! -f "$file" ]
31 then
32 echo "lnks:$file not found or special file" 1>&2
33 exit 1
34 fi
35 #check link count on file
36 set -- $(ls -l "$file")
37 linkcnt=$2
38 if [ "$linkcnt" -eq 1 ]
39 then
40 echo "lnks : no other hard links to $file" 1>&2
41 exit 0
42 fi
43 #get the inode of the given file
44 set $(ls -i "$file")
45
46 inode=$1
47 #find and print the files with that incite number
48 echo "lnks:using find to search for links..." 1>&2
49 find "$directory" -xdev -inum $inode -print
50
黑体黄底命令行使用了命令替换 $(command)
确定shell ,用法消息,注释,测试参数。
内置命令 set 使用命令替换机制把位置参数设置为 ls -l 命令的输出。 输出的第二个字段就是链接的数目,因此就将linkcnt 就被设置为$2 。set 中的参数'--' 防止把'-ls -l'( 第一个输出字段是对文件的访问许可,一般以字符‘-’ 开头) 产生的信息作为选项。
比较与文件名关联的inode 来确定多个文件名是否链接到同一个文件,这是一种好方法。
find 工具用于查找满足其参数所指定条件的文件,查找的位置由第一个参数($directory) 指定并搜索其所有子目录。余下的参数指定把值为$inode 的文件的文件名送到标准输出。由于不同文件系统中的文件可能拥有相同的inode 号,并且没有链接,find 必须只查找同一文件系统中的目录。参数-xdev 防止find 命令搜索其他文件系统。
可以使用 -x 选项来帮助调试脚本。该选项使得 shell 执行每条命令前把命令显示出来。
可以在带参数'-x' 的shell 中运行上面的shell 脚本,在执行每条命令前先将该命令显示出来。要么为当前shell 设置'-x' 选项( 使用 set -x 命令) ,以使所有脚本的命令在执行前都显示出来。要么只在当前执行脚本中通过shell 使用'-x' 选项。
>bash -x lnks.sh example
脚本中的每条命令前都会加上PS4 变量的值,默认是加号'+'. 因此可通过该值来区分是脚本输出还是调试输出。
通过在脚本的开始位置加入带有 '-x' 参数的 set ,就可以设置运行该脚本的 shell 。 'set -x'
若要关闭调试则引应使用命令:'set +x'
'set -x' 同 'set -xtrace'
控制结构for...in 的语法如下:
for loop-index in argument-list
do
commands
done
依照argument-list 中的每个参数( 将对应的每一个参数赋值给loop-index 变量) 重复执行do 和done 语句之间的命令。
例子1 :
1 for fruit in apples oranges pears bananas
2 do
3 echo "$fruit"
4 done
例子2 :
1 for i in *
2 do
3 if [ -d "$i" ]
4 then
5 echo "$i"
6 fi
7 done
'*' 匹配当前目录下所有可见文件名,与运行该 shell 脚本的当前目录有关,而与 shell 脚本所在目录无关。
for 控制结构语法如下:
for loop-index
do
commands
done
loop-index 用命令行参数中的每个参数值取代 ,重复执行do 和done 之间的语句。除了参数来源不同,该结构语法同'for...in' 结构语法。
例子:
>cat -n for_test.sh
1 for arg
2 do
3 echo "$arg"
4 done
5
>chmod a+rwx for_test.sh
>./for_test.sh example example2 example4 example6
'for arg' 隐含表示 'for arg in “$@”' ,这里将” $@” 扩展为一个命令行参数列表, 如” $1””$2””$3” 等。
>cat -n whos.sh
1 #!/bin/bash
2 # adapted from finger.sh by Lee Sailer
3 # UNIX/WORLD
4
5 if [ $# -eq 0 ]
6 then
7 echo "Usage: whos id ..." 1>&2
8 exit 1
9 fi
10
11 for id
12 do
13 gawk -F: ' {print $1,$5} ' /etc/passwd |
14 grep -i "$id"
15 done
16
>./whos.sh root “terry Zeng”
脚本whos 演示了for 结构中$@ 符号所代表的含义。whos 脚本后面可以跟一个或几个用户名作为参数,执行whos 脚本就可以将这些与用户相关的信息显示出来。
在这个脚本中的 for 循环中隐含的 $@ 的使用特别有效果,因为它使得 for 循环可以把带空格的参数视为一个单独的参数 ( 如参数” terry Zeng”) 。
对于每个命令行参数,whos 搜索/etc/passwd 文件。在for 循环结构中gawk 的作用是从文件/etc/passwd 的行中提取第一个($1) 和第5 个($5) 字段的内容。第1 个和第5 个字段的内容通过一个管道传递给grep 。grep 在其输入中搜索内容为$id($id 就是输入的命令行参数) 的对象。选项'-i' 的作用是让grep 在搜索过程中忽略其他的事件,grep 在其输入中按照每行的格式显示包含$id 内容的对象。
行末的| 即便在管道标志后面还有换行,管道也能照常工作。
while :
while 控制结构语法如下:
while test-command
do
commands
done
只要测试条件的返回值为真,while 结构语句就要执行do 与done 语句之间的命令。
例子:
>cat -n count.sh
1 #!/bin/bash
2 number=0
3 while [ "$number" -lt 10 ]
4 do
5 echo -n "$number"
6 ((number += 1))
7 done
8 echo
test 内置命令:脚本使用了'-lt' 来执行数值比较测试。对于数值比较测试由以下几种测试选项:'-ne','-eq','-gt','-ge','-lt','-le' 。对于字符串的比较可以用'=' 或者'!=' 来进行测试比较。
使用 '-n' 选项用来防止 echo 在其输出之后输出换行。最后的 echo 使脚本在标准输出上输出一个新的字符行。 ((number += 1)) 赋值表达式。
例子:
1 #!/bin/bash
2 # remove correct spellings from aspell output
3
4 if [ $# -ne 2 ]
5 then
6 echo "Usage spell_check.sh file1 file2" 1>&2
7 echo "file1 list of correct spellings" 1>&2
8 echo "file2 file to be checked" 1>&2
9 exit 1
10 fi
11
12 if [ ! -r "$1" ]
13 then
14 echo "spell ..check; $1 is not readable" 1>&2
15 exit 1
16 fi
17
18 if [ ! -r "$2" ]
19 then
20 echo "spell ..check; $2 is not readable" 1>&2
21 exit 1
22 fi
23
24 aspell -l < "$2" |
25 while read line
26 do
27 if ! grep "^$line$" "$1" > /dev/null
28 then
29 echo $line
30 fi
31 done
32
上面的脚本中'-r' 参数判断一个文件是否是可读的,括号里的感叹号是对跟着的操作符进行相反的运算,与'-r' 参数联合起来用以判断一个文件是否是不可读的。
上面的脚本把aspell 脚本的输出( 使用参数-l 可以让aspell 脚本把所检查出的错误单词的列表送到标准输出上) 通过一个管道送到while 结构的标准输入上,while 结构从他的标准输入上一次读入一行内容( 每行只有一个单词) 。只要测试条件( 也就是read line) 能从标准输入上得到一个单词,那么它就返回一个true 状态。
在while 循环中if 语句用来检测grep 条件测试的返回值,grep 是用来判断被读取行是否在用户的正确单词列表中。grep 搜索的模式( 也就是$line) 前后都有特殊字符,这些字符分别用来指明一行的开始和结束( 分别是'^' 和'$' ;'^$line$' 理解为 '^---$line---$' 其中 '^' 代表行的开始,第一个 '$' 代表对变量 'line' 的引用,第二个 '$' 代表行的结束 ) 。 这些特殊符号的作用是确保grep 搜索时只有当变量$line 的内容与用户输入的正确单词列表的一整行的内容相同时才形成匹配。grep 的标准输出被重定向到文件/dev/null 中,因为我们不关心输出结果,只关心返回状态。
' if ! grep "^$line$" "$1" > /dev/null ' 可以替换为 ' if ! grep -qw “$line” “$1” ' 。 其中'-q' 抑制了grep 的输出,这样只返回退出状态。'-w' 使得grep 只匹配整个单词。
until
until 与while 语句的语法结构相似。区别仅在于条件语句的测试位置:一个在语句的开始测试,一个在语句的结束测试。
until test-commands
do
commands
done
例子:
1 #! /bin/bash
2 secretname=jenny
3 name=noname
4 echo "Try to guess the secret name!"
5 echo
6 until [ "$name" = "$secretname" ]
7 do
8 echo -n "Your guess:"
9 read name
10 done
11 echo "Very good."
12
例子:
1 #!/bin/bash
2 # UNIX/WORLD
3 trap ' ' 1 2 3 18
4 stty -echo
5 echo -n "Key: "
6 read key_1
7 echo
8 echo -n "Again:"
9 read key_2
10 echo
11 key3=
12 if [ "$key_1" = "$key_2" ]
13 then
14 tput clear
15 until [ "$key_3" = "$key_2" ]
16 do
17 read key_3
18 done
19 else
20 echo "locktty: keys do not match" 1>&2
21 fi
22 stty echo
23
如果运行上面的例子而忘记了脚本密码,可以登录另一个虚拟终端并终止该进程。
trap 内置命令:利用 trap 内置命令可以防止用户通过发送中断的方式来终止脚本的运行。通过捕获信号 18 可以保证用户无法用 CONTROL+Z 组合键来挂起运行该脚本的进程。 stty -echo 命令来防止终端把键盘输入的字符显示出来 ,这样可以保证输入的密码不被显示出来。
break 与continue :同c 语言可以用来中断for while until 语句的执行
1 #!/bin/bash
2 for index in 1 2 3 4 5 6 7 8 9
3 do
4 if [ $index -le 3 ]
5 then
6 echo "continue"
7 continue
8 fi
9 echo $index
10 if [ $index -ge 8 ]
11 then
12 echo "break"
13 break
14 fi
15 done
16
case :一种多分支选择机制,同C 语言。
case test-string in
pattern-1)
commands-1
;;
pattern-2)
commands-2
;;
pattern-3)
commands-3
;;
...
esac
case 结构中的匹配类型类似于一个模糊文件引用。
字符‘*’: 匹配任意字符串,用作默认的case 匹配。
字符‘?’: 匹配 单个字符。
[…]: 定义一个字符类,对处于方括号中的每个字符依次进行单字符匹配;两个字符之间的连字符用来指定字符范围。
| : 分离带有选择的选项,这些选项满足case 结构的一个特别的分支。
例子:
1 echo -n "Enter A,B,or C:"
2 read letter
3 case "$letter" in
4 a|A)
5 echo "You Entered A"
6 ;;
7 b|B)
8 echo "You Entered B"
9 ;;
10 c|C)
11 echo "You Entered C"
12 ;;
13 *)
14 echo "You did not enter A,B,or C"
15 ;;
16 esac
例子:
1 #!/bin/bash
2
3 echo -e "/n COMMAND MENU/n"
4 echo "a.Current date and time"
5 echo "b.Users currently logged in"
6 echo "c.Name of the working directory"
7 echo -e "d.Contents of the working directory/n"
8 echo -n "Enter a,b,c,or d:"
9 read answer
10 echo
11 case "$answer" in
12 a)
13 date
14 ;;
15 b)
16 who
17 ;;
18 c)
19 pwd
20 ;;
21 d)
22 ls
23 ;;
24 *)
25 echo "There is no selection:$answer"
26 ;;
27 esac
'echo -e' 选项 -e 使 echo 把后面的 '/n' 解释为一个换行符,如果 echo 后面不加这个参数 '-e' , echo 就会输出两个字符 '/n' ,而不是一个空行。参数 '-e' 使得 echo 解释由反斜杠 '/' 转义的字符。 带有反斜杠的字符一定要引起来,否则反斜杠就要由 shell 来解释而不会传到 echo 由 echo 解释。
例子:
>cat -n safedit
1 #!/bin/bash
2 PATH=/bin:/usr/bin
3 script=$(basename $0)
4 case $# in
5 0)
6 vim
7 exit 0
8 ;;
9 1)
10 if [ ! -f "$1" ]
11 then
12 vim "$1"
13 exit 0
14 fi
15 if [ ! -r "$1" -o ! -w "$1" ]
16 then
17 echo "$script:check permissions on $1" 1>&2
18 exit 1
19 else
20 editfile="$1"
21 fi
22 if [ ! -w "." ]
23 then
24 echo "$script:backup cannont be"/
25 "created in the working directory" 1>&2
26 exit 1
27 fi
28 ;;
29 *)
30 echo "Usage:$script [file-to-edit]" 1>&2
31 exit 1
32 ;;
33 esac
34
35 tempfile=/tmp/$$.$script
36 cp $editfile $tempfile
37 if vim $editfile
38 then
39 mv $tempfile bak.$(basename $editfile)
40 echo "$script:backup file created"
41 else
42 mv $tempfile editerr
43 echo "$script:edit error--copy of" /
44 "original file is in editerr"1>&2
45 fi
46
>chmod u+x safedit
>./safedit example.sh
再另一个控制台上进入tmp 目录可以查看到一个nxxn.safedit 的临时文件,回到原来的控制台退出vim 编辑器,查看当前控制台当前工作目录下会生成一个备份文件格式bak.example.sh 。再打开新的控制台查看/tmp 目录发现最开始生成的临时文件已经被删除。
>ln safedit safedit.ln
>./safedit.ln example.sh
注意观察该命令行运行的过程,生成的中间文件,最后结果与上面文字描述的不同。
设置 PATH 变量:该脚本设置了 PATH 变量,目的是为了保证脚本中执行的命令是系统目录中的标准命令。避免了用户自己可能设置的 PATH 包含自己定义的目录,而在该自定义目录下,用户可能编写了一些与脚本中调用命令同名的脚本或程序。
程序名: basename 去掉前导的目录部分后打印名称,因此脚本中将 script 设置为运行脚本的基本名称。通过命令替换实现。 $0 存储脚本被调用时的命令。好处在于对脚本进行重命名或则创建链接后运行该脚本会得到正确的提示信息。
给临时文件命名:脚本中设置变量 tempfile 为临时文件名,以 shell 进程的 PID 号作为开始并以脚本的名字作为结束。使用 PID 号是为了确保文件名的唯一性,脚本名字附在临时文件名后面,为了让用户知道其来源。 PID 号放在前面是由于一些老版本 unix 上对文件名有长度限制,而 PID 号可以确保其唯一性,所以放在前面避免由于文件名字长的限制而被切去。
测试条件:脚本中使用了一个测试条件 vim $editfile 。测试 vim 编辑器编辑文件完成后返回的 exit 代码, if 控制结构就是利用这个 exit 代码来决定分支。成功则返回 0 , then 被执行;不成功则返回非 0 , else 语句被执行。
select :首先显示一个菜单,然后根据用户的选择给变量赋予相应的值,最后执行一系列命令。
select varname [in arg ….]
do
command
done
例子:
1 #! /bin/bash
2 PS3= 'Choose your favorite fruit from these from these possibilities:'
3 select FRUIT in apple banana blueberry kiwi orange watermelon STOP
4 do
5 if [ "$FRUIT" = "" ];then
6 echo -e "Invalid entry./n"
7 continue
8 elif [ "$FRUIT " = STOP ];then
9 echo "Thanks for playing."
10 break
11 fi
12 echo "You Chose $FRUIT as your favorite."
13 echo -e "That is choice number $REPLY ./n"
14 done
15
PS3 :是 select 特有的提示符,在 select 语句输出菜单后,就会显示出 PS3 的值。 slect 不断的发出 PS3 提示并按照用户的输入执行命令,直到有事情使其停止。
select 将 varname 设置为输入的值 ( 例子中是变量 FRUIT) ,同时将用户的响应存储在键盘变量 REPLY 中。如果用用户非法输入, shell 将 varname 设置为空 ($FRUIT) 。