魅力博客

魅力Linux|魅力空间|魅力博客|学习Linux|ubuntu日记|电脑教程|手机软件

深入学习shell脚本艺术-高级Bash脚本编程指南6



10.4 测试与分支(case和select结构)
---------------------------------
case和select结构在技术上说不是循环,因为它们并不对可执行的代码块进行迭代.但是和循环
相似的是,它们也依靠在代码块的顶部或底部的条件判断来决定程序的分支.

在代码块中控制程序分支

case (in) / esac
    在shell中的case同C/C++中的switch结构是相同的.它允许通过判断来选择代码块中多条
    路径中的一条.

    case "$variable" in

    "$condition1")
    command...
    ;;

    "$condition1")
    command...
    ;;

    esac

    注意:    对变量使用""并不是强制的,因为不会发生单词分离.
            每句测试行,都以右小括号)结尾.
            每个条件块都以两个分号结尾;;.
            case块的结束以esac(case的反向拼写)结尾.

Example 10-24 使用case
################################Start Script#######################################
 1 #!/bin/bash
 2 # 测试字符串范围
 3
 4 echo; echo "Hit a key, then hit return."
 5 read Keypress
 6
 7 case "$Keypress" in
 8   [[:lower:]]   ) echo "Lowercase letter";;
 9   [[:upper:]]   ) echo "Uppercase letter";;
10   [0-9]         ) echo "Digit";;
11   *             ) echo "Punctuation, whitespace, or other";;
12 esac      #  Allows ranges of characters in [square brackets],
12 esac      #  允许字符串的范围出现在[]中,
13           #+ or POSIX ranges in [[double square brackets.
13           #+ 或者POSIX范围在[[中.
14
15 #  在这个例子的第一个版本中,
16 #+ 测试大写和小写字符串使用的是
17 #+ [a-z] 和 [A-Z].
18 #  这种用法将不会在某些特定的场合或Linux发行版中正常工作.
19 #  POSIX 更具可移植性.
20 #  感谢Frank Wang 指出这点.
21
22 #  练习:
23 #  -----
24 #  就像这个脚本所表现的,它只允许单个按键,然后就结束了.
25 #  修改这个脚本,让它能够接受重复输入,
26 #+ 报告每个按键,并且只有在"X"被键入时才结束.
27 #  暗示: 将这些代码都用"while"循环圈起来.
28
29 exit 0
################################End Script#########################################

Example 10-25 使用case来创建菜单
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 未经处理的地址资料
 4
 5 clear # 清屏.
 6
 7 echo "          Contact List"
 8 echo "          ------- ----"
 9 echo "Choose one of the following persons:"
10 echo
11 echo "[E]vans, Roland"
12 echo "[J]ones, Mildred"
13 echo "[S]mith, Julie"
14 echo "[Z]ane, Morris"
15 echo
16
17 read person
18
19 case "$person" in
20 # 注意,变量是被引用的.
21
22   "E" | "e" )
23   # 接受大写或小写输入.
24   echo
25   echo "Roland Evans"
26   echo "4321 Floppy Dr."
27   echo "Hardscrabble, CO 80753"
28   echo "(303) 734-9874"
29   echo "(303) 734-9892 fax"
30   echo "revans@zzy.net"
31   echo "Business partner & old friend"
32   ;;
33 # 注意,在每个选项后边都需要以;;结尾.
34
35   "J" | "j" )
36   echo
37   echo "Mildred Jones"
38   echo "249 E. 7th St., Apt. 19"
39   echo "New York, NY 10009"
40   echo "(212) 533-2814"
41   echo "(212) 533-9972 fax"
42   echo "milliej@loisaida.com"
43   echo "Ex-girlfriend"
44   echo "Birthday: Feb. 11"
45   ;;
46
47 # 后边的Smith和Zane的信息在这里就省略了.
48
49           * )
50    # 默认选项.
51    # 空输入(敲RETURN).
52    echo
53    echo "Not yet in database."
54   ;;
55
56 esac
57
58 echo
59
60 #  练习:
61 #  -----
62 #  修改这个脚本,让它能够接受多输入,
63 #+ 并且能够显示多个地址.
64
65 exit 0
################################End Script#########################################

    一个case的特殊用法,用来测试命令行参数.
################################Start Script#######################################
 1 #! /bin/bash
 2
 3 case "$1" in
 4 "") echo "Usage: ${0##*/} <filename>"; exit $E_PARAM;;  # 没有命令行参数,
 5                                                         # 或者第一个参数为空.
 6 # Note that ${0##*/} is ${var##pattern} param substitution. Net result is $0.
 6 # 注意:${0##*/} 是${var##pattern} 这种模式的替换. 得到的结果是$0.
 7
 8 -*) FILENAME=./$1;;   #  如果传递进来的文件名参数($1)以一个破折号开头,
 9                       #+ 那么用./$1来代替
10                       #+ 这样后边的命令将不会把它作为一个选项来解释.
11
12 * ) FILENAME=$1;;     # 否则, $1.
13 esac
################################End Script#########################################

    这是一个更容易懂的命令行参数处理的一个例子.
################################Start Script#######################################
 1 #! /bin/bash
 2
 3
 4 while [ $# -gt 0 ]; do    # 直到你用完所有的参数...
 5   case "$1" in
 6     -d|--debug)
 7               # "-d" or "--debug" parameter?
 8               DEBUG=1
 9               ;;
10     -c|--conf)
11               CONFFILE="$2"
12               shift
13               if [ ! -f $CONFFILE ]; then
14                 echo "Error: Supplied file doesn't exist!"
15                 exit $E_CONFFILE     # 文件没发现错误.
16               fi
17               ;;
18   esac
19   shift       # 检查剩下的参数.
20 done
21
22 #  来自Stefano Falsetto的 "Log2Rot" 脚本,
23 #+ 他的"rottlog" 包的一部分.
24 #  授权使用.
################################End Script#########################################

Example 10-26 使用命令替换来产生case变量
################################Start Script#######################################
 1 #!/bin/bash
 2 # case-cmd.sh: 使用命令替换来产生"case"变量
 3
 4 case $( arch ) in   # "arch" 返回机器的类型.
 5                     # 等价于 'uname -m' ...
 6 i386 ) echo "80386-based machine";;
 7 i486 ) echo "80486-based machine";;
 8 i586 ) echo "Pentium-based machine";;
 9 i686 ) echo "Pentium2+-based machine";;
10 *    ) echo "Other type of machine";;
11 esac
12
13 exit 0
################################End Script#########################################

    case结构也可以过滤globbing模式的字符串.
Example 10-27 简单字符串匹配
################################Start Script#######################################
 1 #!/bin/bash
 2 # match-string.sh: 简单字符串匹配
 3
 4 match_string ()
 5 {
 6   MATCH=0
 7   NOMATCH=90
 8   PARAMS=2     # 函数需要2个参数.
 9   BAD_PARAMS=91
10
11   [ $# -eq $PARAMS ] || return $BAD_PARAMS
12
13   case "$1" in
14   "$2") return $MATCH;;
15   *   ) return $NOMATCH;;
16   esac
17
18 }  
19
20
21 a=one
22 b=two
23 c=three
24 d=two
25
26
27 match_string $a     # 参数个数错误.
28 echo $?             # 91
29
30 match_string $a $b  # 不匹配
31 echo $?             # 90
32
33 match_string $b $d  # 匹配
34 echo $?             # 0
35
36
37 exit 0    
################################End Script#########################################

Example 10-28 检查是否是字母输入
################################Start Script#######################################
  1 #!/bin/bash
  2 # isalpha.sh: 使用"case"结构来过滤字符串.
  3
  4 SUCCESS=0
  5 FAILURE=-1
  6
  7 isalpha ()  # 检查输入的*第一个字符*是不是字母表上的字符.
  8 {
  9 if [ -z "$1" ]                # 没有参数传进来?
 10 then
 11   return $FAILURE
 12 fi
 13
 14 case "$1" in
 15 [a-zA-Z]*) return $SUCCESS;;  # 以一个字母开头?
 16 *        ) return $FAILURE;;
 17 esac
 18 }             # 同C语言的"isalpha()"函数相比较.
 19
 20
 21 isalpha2 ()   # 测试是否*整个字符串*为字母表字符.
 22 {
 23   [ $# -eq 1 ] || return $FAILURE
 24
 25   case $1 in
 26   *[!a-zA-Z]*|"") return $FAILURE;;
 27                *) return $SUCCESS;;
 28   esac
 29 }
 30
 31 isdigit ()    # 测试是否*整个字符串*都是数字.
 32 {             # 换句话说就是测试是否是整数变量.
 33   [ $# -eq 1 ] || return $FAILURE
 34
 35   case $1 in
 36   *[!0-9]*|"") return $FAILURE;;
 37             *) return $SUCCESS;;
 38   esac
 39 }
 40
 41
 42
 43 check_var ()  # 测试 isalpha ().
 44 {
 45 if isalpha "$@"
 46 then
 47   echo "\"$*\" begins with an alpha character."
 48   if isalpha2 "$@"
 49   then        # 不需要测试第一个字符是否是non-alpha.
 50     echo "\"$*\" contains only alpha characters."
 51   else
 52     echo "\"$*\" contains at least one non-alpha character."
 53   fi  
 54 else
 55   echo "\"$*\" begins with a non-alpha character."
 56               # 如果没有参数传递进来,也是"non-alpha".
 57 fi
 58
 59 echo
 60
 61 }
 62
 63 digit_check ()  # 测试 isdigit ().
 64 {
 65 if isdigit "$@"
 66 then
 67   echo "\"$*\" contains only digits [0 - 9]."
 68 else
 69   echo "\"$*\" has at least one non-digit character."
 70 fi
 71
 72 echo
 73
 74 }
 75
 76 a=23skidoo
 77 b=H3llo
 78 c=-What?
 79 d=What?
 80 e=`echo $b`   # 命令替换.
 81 f=AbcDef
 82 g=27234
 83 h=27a34
 84 i=27.34
 85
 86 check_var $a
 87 check_var $b
 88 check_var $c
 89 check_var $d
 90 check_var $e
 91 check_var $f
 92 check_var     # 没有参数传进来,将发生什么?
 93 #
 94 digit_check $g
 95 digit_check $h
 96 digit_check $i
 97
 98
 99 exit 0        # S.C改进过这个脚本.
100
101 # Exercise:
102 # --------
103 #  编写一个 'isfloat ()'函数来测试浮点数.
104 #  暗示: 这个函数基本上与'isdigit ()'一样,
105 #+ 但是要添加一部分小数点的处理.
################################End Script#########################################


select
    select结构是建立菜单的另一种工具,这种结构是从ksh中引入的.

    select variable [in list]
    do
    command...
    break
    done

    提示用户选择的内容比如放在变量列表中.注意:select命令使用PS3提示符[默认为(#? )]
    但是可以修改PS3.
Example 10-29 用select来创建菜单
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 PS3='Choose your favorite vegetable: ' # 设置提示符字串.
 4
 5 echo
 6
 7 select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
 8 do
 9   echo
10   echo "Your favorite veggie is $vegetable."
11   echo "Yuck!"
12   echo
13   break  # 如果这里没有'break'会发生什么?
14 done
15
16 exit 0
################################End Script#########################################

    如果忽略了in list列表,那么select命令将使用传递到脚本的命令行参数,或者是函数参数
    前提是将select写到这个函数中.

    与for variable [in list]结构在忽略[in list]时的行为相比较.
Example 10-30 用函数中select结构来创建菜单
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 PS3='Choose your favorite vegetable: '
 4
 5 echo
 6
 7 choice_of()
 8 {
 9 select vegetable
10 # [in list] 被忽略, 所以'select'用传递给函数的参数.
11 do
12   echo
13   echo "Your favorite veggie is $vegetable."
14   echo "Yuck!"
15   echo
16   break
17 done
18 }
19
20 choice_of beans rice carrots radishes tomatoes spinach
21 #         $1    $2   $3      $4       $5       $6
22 #         传递给choice_of() 函数的参数
23
24 exit 0
################################End Script#########################################
    参见Example 34-3.


第11章    内部命令与内建
======================
内建命令指的就是包含在Bash工具集中的命令.这主要是考虑到执行效率的问题--内建命令将
比外部命令的执行得更快,外部命令通常需要fork出一个单独的进程来执行.另外一部分原因
是特定的内建命令需要直接存取shell内核部分.

当一个命令或者是shell本身需要初始化(或者创建)一个新的子进程来执行一个任务的时候,这
种行为被称为forking.这个新产生的进程被叫做子进程,并且这个进程是从父进程中分离出来
的.当子进程执行它的任务时,同时父进程也在运行.

注意:当父进程取得子进程的进程ID的时候,父进程可以传递给子进程参数,而反过来则不行.
这将产生不可思议的并且很难追踪的问题.

Example 11-1 一个fork出多个自己实例的脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # spawn.sh
 3
 4
 5 PIDS=$(pidof sh $0)  # 这个脚本不同实例的进程ID.
 6 P_array=( $PIDS )    # 把它们放到数组里(为什么?).
 7 echo $PIDS           # 显示父进程和子进程的进程ID.
 8 let "instances = ${#P_array[*]} - 1"  # 计算元素个数,至少为1.
 9                                       # 为什么减1?
10 echo "$instances instance(s) of this script running."
11 echo "[Hit Ctl-C to exit.]"; echo
12
13
14 sleep 1              # 等.
15 sh $0                # 再来一次.
16
17 exit 0               # 没必要: 脚本永远不会走到这里.
18                      # 为什么走不到这里?
19
20 #  在使用Ctl-C退出之后,
21 #+ 是否所有产生的进程都会被kill掉?
22 #  如果是这样的话, 为什么?
23
24 # 注意:
25 # ----
26 # 小心,不要让这个脚本运行太长时间.
27 # 它最后将吃掉你大部分的系统资源.
28
29 #  对于用脚本产生大量的自身实例来说,
30 #+ 是否有适当的脚本技术.
31 #  为什么是为什么不是?
################################End Script#########################################
一般的,脚本中的内建命令在执行时将不会fork出一个子进程.但是脚本中的外部或过滤命令
通常会fork一个子进程.

一个内建命令通常与一个系统命令同名,但是Bash在内部重新实现了这些命令.比如,Bash的
echo命令与/bin/echo就不尽相同,虽然它们的行为绝大多数情况下是一样的.
1 #!/bin/bash
2
3 echo "This line uses the \"echo\" builtin."
4 /bin/echo "This line uses the /bin/echo system command."

关键字的意思就是保留字.对于shell来说关键字有特殊的含义,并且用来构建shell的语法结构.
比如,"for","while","do"和"!"都是关键字.与内建命令相同的是,关键字也是Bash的骨干部分,
但是与内建命令不同的是,关键字自身并不是命令,而是一个比较大的命令结构的一部分.[1]


I/O类

echo
    打印(到stdout)一个表达式或变量(见Example 4-1).
    1 echo Hello
    2 echo $a

    echo需要使用-e参数来打印转移字符.见Example 5-2.
    一般的每个echo命令都会在终端上新起一行,但是-n选项将会阻止新起一行.

    注意:echo命令可以用来作为一系列命令的管道输入.
    1 if echo "$VAR" | grep -q txt   # if [[ $VAR = *txt* ]]
    2 then
    3   echo "$VAR contains the substring sequence \"txt\""
    4 fi

    注意:echo命令与命令替换相组合可以用来设置一个变量.
    a=`echo "HELLO" | tr A-Z a-z`
    参见Example 12-19,Example 12-3,Example 12-42,和Example 12-43.

    注意:echo `command`将会删除任何有命令产生的换行符.

    $IFS(内部域分隔符)一般都会将\n(换行符)包含在它的空白字符集合中.Bash因此会根据
    参数中的换行来分离命令的输出.然后echo将以空格代替换行来输出这些参数.

    bash$ ls -l /usr/share/apps/kjezz/sounds
    -rw-r--r--    1 root     root         1407 Nov  7  2000 reflect.au
    -rw-r--r--    1 root     root          362 Nov  7  2000 seconds.au




    bash$ echo `ls -l /usr/share/apps/kjezz/sounds`
    total 40 -rw-r--r-- 1 root root 716 Nov 7 2000 reflect.au -rw-r--r-- 1 root root 362 Nov 7 2000 seconds.au
     

    所以,我们怎么才能在一个需要echo出来的字符串中嵌入换行呢?
################################Start Script#######################################
 1 # 嵌入一个换行?
 2 echo "Why doesn't this string \n split on two lines?"
 3 # 上边这句的\n将被打印出来.达不到换行的目的.
 4
 5 # 让我们在试试其他方法.
 6
 7 echo
 8          
 9 echo $"A line of text containing
10 a linefeed."
11 # 打印出2个独立的行,(潜入换行成功了).
12 # 但是,"$"前缀是否是必要的?
13
14 echo
15
16 echo "This string splits
17 on two lines."
18 # 不用非得有"$"前缀.
19
20 echo
21 echo "---------------"
22 echo
23
24 echo -n $"Another line of text containing
25 a linefeed."
26 # 打印出2个独立的行,(潜入换行成功了).
27 # 即使-n选项,也没能阻止换行(译者:-n 阻止了第2个换行)
28
29 echo
30 echo
31 echo "---------------"
32 echo
33 echo
34
35 # 然而,下边的代码就没能像期望的那样运行.
36 # Why not? Hint: Assignment to a variable.
36 # 为什么失败? 提示: 因为分配到了变量.
37 string1=$"Yet another line of text containing
38 a linefeed (maybe)."
39
40 echo $string1
41 # Yet another line of text containing a linefeed (maybe).
42 #                                    ^
43 # 换行变成了空格.
44
45 # Thanks, Steve Parker, for pointing this out.
################################End Script#########################################
    注意: 这个命令是shell的一个内建命令,与/bin/echo不同,虽然行为相似.
    bash$ type -a echo
    echo is a shell builtin
    echo is /bin/echo

printf
    printf命令,格式化输出,是echo命令的增强.它是C语言printf()库函数的一个有限的变形,
    并且在语法上有些不同.

    printf format-string... parameter...
    这是Bash的内建版本,与/bin/printf或/usr/bin/printf命令不同.想更深入的了解,请
    察看printf(系统命令)的man页.

    注意:老版本的Bash可能不支持printf.
Example 11-2 printf
################################Start Script#######################################
 1 #!/bin/bash
 2 # printf demo
 3
 4 PI=3.14159265358979
 5 DecimalConstant=31373
 6 Message1="Greetings,"
 7 Message2="Earthling."
 8
 9 echo
10
11 printf "Pi to 2 decimal places = %1.2f" $PI
12 echo
13 printf "Pi to 9 decimal places = %1.9f" $PI  # 都能正确地结束.
14
15 printf "\n"                                  # 打印一个换行,
16                                              # 等价于 'echo' . . .
17
18 printf "Constant = \t%d\n" $DecimalConstant  # 插入一个 tab (\t).
19
20 printf "%s %s \n" $Message1 $Message2
21
22 echo
23
24 # ==========================================#
25 # 模仿C函数, sprintf().
26 # 使用一个格式化的字符串来加载一个变量.
27
28 echo
29
30 Pi12=$(printf "%1.12f" $PI)
31 echo "Pi to 12 decimal places = $Pi12"
32
33 Msg=`printf "%s %s \n" $Message1 $Message2`
34 echo $Msg; echo $Msg
35
36 #  向我们看到的一样,现在'sprintf'函数可以
37 #+ 作为一个可被加载的模块
38 #+ 但这是不可移植的.
39
40 exit 0
################################End Script#########################################

    使用printf的最主要的应用就是格式化错误消息.
     1 E_BADDIR=65
     2
     3 var=nonexistent_directory
     4
     5 error()
     6 {
     7   printf "$@" >&2
     8   # 格式化传递进来的位置参数,并把它们送到stderr.
     9   echo
    10   exit $E_BADDIR
    11 }
    12
    13 cd $var || error $"Can't cd to %s." "$var"
    14
    15 # Thanks, S.C.

read
    从stdin中读取一个变量的值,也就是与键盘交互取得变量的值.使用-a参数可以取得数组
    变量(见Example 26-6).
Example 11-3 使用read,变量分配
################################Start Script#######################################
 1 #!/bin/bash
 2 # "Reading" 变量.
 3
 4 echo -n "Enter the value of variable 'var1': "
 5 # -n选项,阻止换行.
 6
 7 read var1
 8 # 注意在var1前面没有'$',因为变量正在被设置.
 9
10 echo "var1 = $var1"
11
12
13 echo
14
15 # 一个'read'命令可以设置多个变量.
16 echo -n "Enter the values of variables 'var2' and 'var3' (separated by a space or tab): "
17 read var2 var3
18 echo "var2 = $var2      var3 = $var3"
19 # 如果你只输入了一个值,那么其他的变量还是未设置(null).
20
21 exit 0
################################End Script#########################################

    一个不带变量参数的read命令,将把来自键盘的输入存入到专用变量$REPLY中.
Example 11-4 当使用一个不带变量参数的read命令时,将会发生什么?
################################Start Script#######################################
 1 #!/bin/bash
 2 # read-novar.sh
 3
 4 echo
 5
 6 # -------------------------- #
 7 echo -n "Enter a value: "
 8 read var
 9 echo "\"var\" = "$var""
10 # 到这里为止,都与期望的相同.
11 # -------------------------- #
12
13 echo
14
15 # ------------------------------------------------------------------- #
16 echo -n "Enter another value: "
17 read           #  没有变量分配给'read'命令,因此...
18                #+ 输入将分配给默认变量,$REPLY.
19 var="$REPLY"
20 echo "\"var\" = "$var""
21 # 这部分代码和上边的代码等价.
22 # ------------------------------------------------------------------- #
23
24 echo
25
26 exit 0
################################End Script#########################################

    通常情况下,在使用read命令时,输入一个\然后回车,将会阻止产生一个新行.-r选项将会
    让\转义.
Example 11-5 read命令的多行输入
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 echo
 4
 5 echo "Enter a string terminated by a \\, then press <ENTER>."
 6 echo "Then, enter a second string, and again press <ENTER>."
 7 read var1     # "\"将会阻止产生新行,当read $var1时.
 8               #     first line \
 9               #     second line
10
11 echo "var1 = $var1"
12 #     var1 = first line second line
13
14 #  For each line terminated by a "\"
14 #  对于每个一个"\"结尾的行
15 #+ 你都会看到一个下一行的提示符,让你继续向var1输入内容.
16
17 echo; echo
18
19 echo "Enter another string terminated by a \\ , then press <ENTER>."
20 read -r var2  # -r选项将会让"\"转义.
21               #     first line \
22
23 echo "var2 = $var2"
24 #     var2 = first line \
25
26 # 第一个<ENTER>就会结束var2变量的录入.
27
28 echo
29
30 exit 0
################################End Script#########################################

    read命令有些有趣的选项,这些选项允许打印出一个提示符,然后在不输入<ENTER>的情况
    下,可以读入你的按键字符.
     1 # Read a keypress without hitting ENTER.
     1 # 不敲回车,读取一个按键字符.
     2
     3 read -s -n1 -p "Hit a key " keypress
     4 echo; echo "Keypress was "\"$keypress\""."
     5
     6 # -s 选项意味着不打印输入.
     7 # -n N 选项意味着直接受N个字符的输入.
     8 # -p 选项意味着在读取输入之前打印出后边的提示符.
     9
    10 # 使用这些选项是有技巧的,因为你需要使用正确的循序来使用它们.

    read的-n选项也可以检测方向键,和一些控制按键.
Example 11-6 检测方向键
################################Start Script#######################################
 1 #!/bin/bash
 2 # arrow-detect.sh: 检测方向键,和一些非打印字符的按键.
 3 # Thank you, Sandro Magi告诉了我怎么做.
 4
 5 # --------------------------------------------
 6 # 按键产生的字符编码.
 7 arrowup='\[A'
 8 arrowdown='\[B'
 9 arrowrt='\[C'
10 arrowleft='\[D'
11 insert='\[2'
12 delete='\[3'
13 # --------------------------------------------
14
15 SUCCESS=0
16 OTHER=65
17
18 echo -n "Press a key...  "
19 # 如果不是上边列表所列出的按键,可能还是需要按回车.(译者:因为一般按键是一个字符)
20 read -n3 key                      # 读3个字符.
21
22 echo -n "$key" | grep "$arrowup"  #检查输入字符是否匹配.
23 if [ "$?" -eq $SUCCESS ]
24 then
25   echo "Up-arrow key pressed."
26   exit $SUCCESS
27 fi
28
29 echo -n "$key" | grep "$arrowdown"
30 if [ "$?" -eq $SUCCESS ]
31 then
32   echo "Down-arrow key pressed."
33   exit $SUCCESS
34 fi
35
36 echo -n "$key" | grep "$arrowrt"
37 if [ "$?" -eq $SUCCESS ]
38 then
39   echo "Right-arrow key pressed."
40   exit $SUCCESS
41 fi
42
43 echo -n "$key" | grep "$arrowleft"
44 if [ "$?" -eq $SUCCESS ]
45 then
46   echo "Left-arrow key pressed."
47   exit $SUCCESS
48 fi
49
50 echo -n "$key" | grep "$insert"
51 if [ "$?" -eq $SUCCESS ]
52 then
53   echo "\"Insert\" key pressed."
54   exit $SUCCESS
55 fi
56
57 echo -n "$key" | grep "$delete"
58 if [ "$?" -eq $SUCCESS ]
59 then
60   echo "\"Delete\" key pressed."
61   exit $SUCCESS
62 fi
63
64
65 echo " Some other key pressed."
66
67 exit $OTHER
68
69 #  练习:
70 #  -----
71 #  1) 通过使用'case'结构来代替'if'结构
72 #+    来简化这个脚本.
73 #  2) Add detection of the "Home," "End," "PgUp," and "PgDn" keys.
73 #  2) 添加"Home," "End," "PgUp," 和 "PgDn"这些按键的检查.
################################End Script#########################################
    注意: 对read命令来说,-n 选项将不会检测ENTER(新行)键.

    read命令的-t选项允许时间输入(见Example 9-4).

    read命令也可以从重定向的文件中读入变量的值.如果文件中的内容超过一行,那么只有第
    一行被分配到这个变量中.如果read命令有超过一个参数,那么每个变量都会从文件中取得
    以定义的空白分隔的字符串作为变量的值.小心!
Example 11-7 通过文件重定向来使用read
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 read var1 <data-file
 4 echo "var1 = $var1"
 5 # var1将会把data-file的第一行的全部内容都作为它的值.
 6
 7 read var2 var3 <data-file
 8 echo "var2 = $var2   var3 = $var3"
 9 # 注意,这里"read"命令将会产生一种不直观的行为.
10 # 1) 重新从文件的开头开始读入变量.
11 # 2) 每个变量都设置成了以空白分割的字符串,
12 #    而不是之前的以整行的内容作为变量的值.
13 # 3) 而最后一个变量将会取得第一行剩余的全部部分(不管是否以空白分割).
14 # 4) 如果需要赋值的变量的个数比文件中第一行一空白分割的字符串的个数多的话,
15 #    那么这些变量将会被赋空值.
16
17 echo "------------------------------------------------"
18
19 # 如何用循环来解决上边所提到的问题:
20 while read line
21 do
22   echo "$line"
23 done <data-file
24 # Thanks, Heiner Steven for pointing this out.
25
26 echo "------------------------------------------------"
27
28 # 使用$IFS (内部域分隔变量)来将每行的输入单独的放到"read"中,
29 # 如果你不想使用默认空白的话.
30
31 echo "List of all users:"
32 OIFS=$IFS; IFS=:       # /etc/passwd 使用 ":" 作为域分隔符.
33 while read name passwd uid gid fullname ignore
34 do
35   echo "$name ($fullname)"
36 done </etc/passwd   # I/O 重定向.
37 IFS=$OIFS              # 恢复原始的 $IFS.
38 # 这个代码片段也是Heiner Steven写的.
39
40
41
42 #  在循环内部设置$IFS变量
43 #+ 而不用把原始的$IFS
44 #+ 保存到临时变量中.
45 #  Thanks, Dim Segebart, for pointing this out.
46 echo "------------------------------------------------"
47 echo "List of all users:"
48
49 while IFS=: read name passwd uid gid fullname ignore
50 do
51   echo "$name ($fullname)"
52 done </etc/passwd   # I/O 重定向.
53
54 echo
55 echo "\$IFS still $IFS"
56
57 exit 0
################################End Script#########################################

    注意:管道输出到一个read命令中,使用管道echo输出到read会失败.
        然而使用管道cat输出看起来能够正常运行.
    1 cat file1 file2 |
    2 while read line
    3 do
    4 echo $line
    5 done

    但是,像Bjon Eriksson指出的:
Example 11-8 管道输出到read中的问题
################################Start Script#######################################
 1 #!/bin/sh
 2 # readpipe.sh
 3 # 这个例子是Bjon Eriksson捐献的.
 4
 5 last="(null)"
 6 cat $0 |
 7 while read line
 8 do
 9     echo "{$line}"
10     last=$line
11 done
12 printf "\nAll done, last:$last\n"
13
14 exit 0  # 代码结束.
15         # 下边是这个脚本的部分输出.
16         # 打印出了多余的大括号.
17
18 #############################################
19
20 ./readpipe.sh
21
22 {#!/bin/sh}
23 {last="(null)"}
24 {cat $0 |}
25 {while read line}
26 {do}
27 {echo "{$line}"}
28 {last=$line}
29 {done}
30 {printf "nAll done, last:$lastn"}
31
32
33 All done, last:(null)
34
35 变量(last)是设置在子shell中的而没设在外边.
################################End Script#########################################

    在许多linux发行版上,gendiff脚本通常在/usr/bin下,将find的输出使用管道传递到一个
    while循环中.
    1 find $1 \( -name "*$2" -o -name ".*$2" \) -print |
    2 while read f; do
    3 . . .

文件系统类

cd
    cd,修改目录命令,在脚本中用得最多的时候就是,命令需要在指定目录下运行时,需要用cd
    修改当前工作目录.
    1 (cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)
    [之前有个例子,Alan Cox写的]

    -P(physical)选项的作用是忽略符号连接.

    cd - 将把工作目录改为$OLDPWD,就是之前的工作目录.

    注意:当我们用两个/来作为cd命令的参数时,结果却出乎我们的意料.
    bash$ cd //
    bash$ pwd
    //

    输出应该,并且当然是/.无论在命令行下还是在脚本中,这都是个问题.

pwd
    打印当前的工作目录.这将给用户(或脚本)当前的工作目录(见Example 11-9).使用这个
    命令的结果和从内键变量$PWD中读取的值是相同的.

pushd, popd, dirs
    这几个命令可以使得工作目录书签化,就是可以按顺序向前或向后移动工作目录.
    压栈的动作可以保存工作目录列表.选项可以允许对目录栈作不同的操作.

    pushd dir-name 把路径dir-name压入目录栈,同时修改当前目录到dir-name.

    popd 将目录栈中最上边的目录弹出,同时修改当前目录到弹出来的那个目录.

    dirs 列出所有目录栈的内容(与$DIRSTACK便两相比较).一个成功的pushd或者popd将会
        自动的调用dirs命令.

    对于那些并没有对当前工作目录做硬编码,并且需要对当前工作目录做灵活修改的脚本来说
    ,使用这些命令是再好不过的了.注意内建$DIRSTACK数组变量,这个变量可以在脚本内存取,
    并且它们保存了目录栈的内容.

Example 11-9 修改当前的工作目录
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 dir1=/usr/local
 4 dir2=/var/spool
 5
 6 pushd $dir1
 7 # 将会自动运行一个 'dirs' (把目录栈的内容列到stdout上).
 8 echo "Now in directory `pwd`." # Uses back-quoted 'pwd'.
 9
10 # 现在对'dir1'做一些操作.
11 pushd $dir2
12 echo "Now in directory `pwd`."
13
14 # 现在对'dir2'做一些操作.
15 echo "The top entry in the DIRSTACK array is $DIRSTACK."
16 popd
17 echo "Now back in directory `pwd`."
18
19 # 现在,对'dir1'做更多的操作.
20 popd
21 echo "Now back in original working directory `pwd`."
22
23 exit 0
24
25 # 如果你不使用 'popd'将会发生什么 -- 然后退出这个脚本?
26 # 你最后将落在那个目录中?为什么?
################################End Script#########################################


变量类

let
    let命令将执行变量的算术操作.在许多情况下,它被看作是复杂的expr版本的一个简化版.
Example 11-10 用"let"命令来作算术操作.
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 echo
 4
 5 let a=11            # 与 'a=11' 相同
 6 let a=a+5           # 等价于let "a = a + 5"
 7                     # (双引号和空格是这句话更具可读性.)
 8 echo "11 + 5 = $a"  # 16
 9
10 let "a <<= 3"       # 等价于let "a = a << 3"
11 echo "\"\$a\" (=16) left-shifted 3 places = $a"
12                     # 128
13
14 let "a /= 4"        # 等价于let "a = a / 4"
15 echo "128 / 4 = $a" # 32
16
17 let "a -= 5"        # 等价于let "a = a - 5"
18 echo "32 - 5 = $a"  # 27
19
20 let "a *=  10"      # 等价于let "a = a * 10"
21 echo "27 * 10 = $a" # 270
22
23 let "a %= 8"        # 等价于let "a = a % 8"
24 echo "270 modulo 8 = $a  (270 / 8 = 33, remainder $a)"
25                     # 6
26
27 echo
28
29 exit 0
################################End Script#########################################

eval
    eval arg1 [arg2] ... [argN]

    将表达式中的参数,或者表达式列表,组合起来,并且评估它们.包含在表达式中的任何变量
    都将被扩展.结果将会被转化到命令中.这对于从命令行或者脚本中产生代码是很有用的.
    bash$ process=xterm
    bash$ show_process="eval ps ax | grep $process"
    bash$ $show_process
    1867 tty1     S      0:02 xterm
    2779 tty1     S      0:00 xterm
    2886 pts/1    S      0:00 grep xterm

Example 11-11 显示eval命令的效果
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 y=`eval ls -l`  #  与 y=`ls -l` 很相似
 4 echo $y         #+ 但是换行符将被删除,因为echo的变量未被""引用.
 5 echo
 6 echo "$y"       #  用""将变量引用,换行符就不会被空格替换了.
 7
 8 echo; echo
 9
10 y=`eval df`     #  与 y=`df` 很相似
11 echo $y         #+ 换行符又被空格替换了.
12
13 #  当没有LF(换行符)出现时,对于使用"awk"这样的工具来说,
14 #+ 可能分析输出的结果更容易一些.
15
16 echo
17 echo "==========================================================="
18 echo
19
20 # Now, showing how to "expand" a variable using "eval" . . .
20 # 现在,来看一下怎么用"eval"命令来扩展一个变量. . .
21
22 for i in 1 2 3 4 5; do
23   eval value=$i
24   #  value=$i 将具有同样的效果. "eval"并不非得在这里使用.
25   #  一个缺乏特殊含义的变量将被评估为自身 --
26   #+ 也就是说,这个变量除了能够被扩展成自身所表示的字符,不能扩展成任何其他的含义.
27   echo $value
28 done
29
30 echo
31 echo "---"
32 echo
33
34 for i in ls df; do
35   value=eval $i
36   #  value=$i has an entirely different effect here.
36   #  value=$i 在这里就与上边这句有了本质上的区别.
37   #  "eval" 将会评估命令 "ls" 和 "df" . . .
38   #  术语 "ls" 和 "df" 就具有特殊含义,
39   #+ 因为它们被解释成命令,
40   #+ 而不是字符串本身.
41   echo $value
42 done
43
44
45 exit 0
################################End Script#########################################

Example 11-12 强制登出(log-off)
################################Start Script#######################################
 1 #!/bin/bash
 2 # 结束ppp进程来强制登出log-off.
 3
 4 # 脚本应该以根用户的身份来运行.
 5
 6 killppp="eval kill -9 `ps ax | awk '/ppp/ { print $1 }'`"
 7 #                     -------- ppp 的进程ID       -------  
 8
 9 $killppp                  # 这个变量现在成为了一个命令.
10
11
12 # 下边的命令必须以根用户的身份来运行.
13
14 chmod 666 /dev/ttyS3      # 恢复读写权限,否则什么?
15 #  因为在ppp上执行一个SIGKILL将会修改串口的权限,
16 #+ 我们把权限恢复到之前的状态.
17
18 rm /var/lock/LCK..ttyS3   # 删除串口琐文件.为什么?
19
20 exit 0
21
22 # 练习:
23 # -----
24 # 1) 编写一个脚本来验证是否跟用户正在运行它.
25 # 2) 做一个检查,检查一下将要杀掉的进程
26 #+   再杀掉这个进程之前,它是否正在运行.
27 # 3) 基于'fuser'来编写达到这个目的的另一个版本的脚本
28 #+      if [ fuser -s /dev/modem ]; then . . .
################################End Script#########################################

Example 11-13 另一个"rot13"的版本
################################Start Script#######################################
 1 #!/bin/bash
 2 # 使用'eval'的一个"rot13"的版本,(译者:rot13就是把26个字母,从中间分为2瓣,各13个)
 3 # 与脚本"rot13.sh" 比较一下.
 4
 5 setvar_rot_13()              # "rot13" 函数
 6 {
 7   local varname=$1 varvalue=$2
 8   eval $varname='$(echo "$varvalue" | tr a-z n-za-m)'
 9 }
10
11
12 setvar_rot_13 var "foobar"   # 用"foobar" 传递到rot13函数中.
13 echo $var                    # 结果是sbbone
14
15 setvar_rot_13 var "$var"     # 传递"sbbone" 到rot13函数中.
16                              # 又变成了原始值.
17 echo $var                    # foobar
18
19 # 这个例子是Segebart Chazelas编写的.
20 # 作者又修改了一下.
21
22 exit 0
################################End Script#########################################

    Rory Winston捐献了下编的脚本,关于使用eval命令.
Example 11-14 在Perl脚本中使用eval命令来强制变量替换
################################Start Script#######################################
 1 In the Perl script "test.pl":
 2         ...        
 3         my $WEBROOT = <WEBROOT_PATH>;
 4         ...
 5
 6 To force variable substitution try:
 7         $export WEBROOT_PATH=/usr/local/webroot
 8         $sed 's/<WEBROOT_PATH>/$WEBROOT_PATH/' < test.pl > out
 9
10 But this just gives:
11         my $WEBROOT = $WEBROOT_PATH;
12
13 However:
14         $export WEBROOT_PATH=/usr/local/webroot
15         $eval sed 's%\<WEBROOT_PATH\>%$WEBROOT_PATH%' < test.pl > out
16 #        ====
17
18 That works fine, and gives the expected substitution:
19         my $WEBROOT = /usr/local/webroot;
20
21
22 ### Correction applied to original example by Paulo Marcel Coelho Aragao.
################################End Script#########################################

    eval命令是有风险的,如果有更合适的方法来实现功能的话,尽量要避免使用它.
    eval命令将执行命令的内容,如果命令中有rm -rf*这种东西,可能就不是你想要的了.
    如果在一个不熟悉的人编写的脚本中使用eval命令将是危险的.

set
    set命令用来修改内部脚本变量的值.一个作用就是触发选项标志位来帮助决定脚本的行
    为.另一个应用就是以一个命令的结果(set `command`)来重新设置脚本的位置参数.脚本
    将会从命令的输出中重新分析出位置参数.
Example 11-15 使用set来改变脚本的位置参数
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # script "set-test"
 4
 5 # 使用3个命令行参数来调用这个脚本,
 6 # 比如, "./set-test one two three".
 7
 8 echo
 9 echo "Positional parameters before  set \`uname -a\` :" #uname命令打印操作系统名
10 echo "Command-line argument #1 = $1"
11 echo "Command-line argument #2 = $2"
12 echo "Command-line argument #3 = $3"
13
14
15 set `uname -a` # 把`uname -a`的命令输出设置
16                # 为新的位置参数.
17
18 echo $_        # 这要看你的unmae -a输出了,这句打印出的就是输出的最后一个单词.
19 # 在脚本中设置标志.
20
21 echo "Positional parameters after  set \`uname -a\` :"
22 # $1, $2, $3, 等等. 这些位置参数将被重新初始化为`uname -a`的结果
23 echo "Field #1 of 'uname -a' = $1"
24 echo "Field #2 of 'uname -a' = $2"
25 echo "Field #3 of 'uname -a' = $3"
26 echo ---
27 echo $_        # ---
28 echo
29
30 exit 0
################################End Script#########################################

    不使用任何选项或参数来调用set命令的话,将会列出所有的环境变量和其他所有的已经
    初始化过的命令.
    bash$ set
    AUTHORCOPY=/home/bozo/posts
    BASH=/bin/bash
    BASH_VERSION=$'2.05.8(1)-release'
    ...
    XAUTHORITY=/home/bozo/.Xauthority
    _=/etc/bashrc
    variable22=abc
    variable23=xzy

    使用参数--来调用set命令的话,将会明确的分配位置参数.如果--选项后边没有跟变量名
    的话,那么结果就使所有位置参数都比unset了.

Example 11-16 重新分配位置参数
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 variable="one two three four five"
 4
 5 set -- $variable
 6 # 将位置参数的内容设为变量"$variable"的内容.
 7
 8 first_param=$1
 9 second_param=$2
10 shift; shift        # Shift past first two positional params.
11 remaining_params="$*"
12
13 echo
14 echo "first parameter = $first_param"             # one
15 echo "second parameter = $second_param"           # two
16 echo "remaining parameters = $remaining_params"   # three four five
17
18 echo; echo
19
20 # 再来一次.
21 set -- $variable
22 first_param=$1
23 second_param=$2
24 echo "first parameter = $first_param"             # one
25 echo "second parameter = $second_param"           # two
26
27 # ======================================================
28
29 set --
30 # Unsets positional parameters if no variable specified.
30 # 如果没指定变量,那么将会unset所有的位置参数.
31
32 first_param=$1
33 second_param=$2
34 echo "first parameter = $first_param"             # (null value)
35 echo "second parameter = $second_param"           # (null value)
36
37 exit 0
################################End Script#########################################
    见Example 10-2,和Example 12-51.

unset
    unset命令用来删除一个shell变量,效果就是把这个变量设为null.注意:这个命令对位置
    参数无效.
    bash$ unset PATH

    bash$ echo $PATH


    bash$

Example 11-17 Unset一个变量
################################Start Script#######################################
 1 #!/bin/bash
 2 # unset.sh: Unset一个变量.
 3
 4 variable=hello                       # 初始化.
 5 echo "variable = $variable"
 6
 7 unset variable                       # Unset.
 8                                      # 与 variable= 的效果相同.
 9 echo "(unset) variable = $variable"  # $variable 设为 null.
10
11 exit 0
################################End Script#########################################

export
    export命令将会使得被export的变量在运行的脚本(或shell)的所有的子进程中都可用.
    不幸的是,没有办法将变量export到父进程(就是调用这个脚本或shell的进程)中.
    关于export命令的一个重要的使用就是用在启动文件中,启动文件是用来初始化并且
    设置环境变量,让用户进程可以存取环境变量.
Example 11-18 使用export命令传递一个变量到一个内嵌awk的脚本中
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  这是"求列的和"脚本的另外一个版本(col-totaler.sh)
 4 #+ 那个脚本可以把目标文件中的指定的列上的所有数字全部累加起来,求和.
 5 #  这个版本将把一个变量通过export的形式传递到'awk'中 . . .
 6 #+ 并且把awk脚本放到一个变量中.
 7
 8
 9 ARGS=2
10 E_WRONGARGS=65
11
12 if [ $# -ne "$ARGS" ] # 检查命令行参数的个数.
13 then
14    echo "Usage: `basename $0` filename column-number"
15    exit $E_WRONGARGS
16 fi
17
18 filename=$1
19 column_number=$2
20
21 #===== 上边的这部分,与原始脚本完全一样 =====#
22
23 export column_number
24 # 将列号通过export出来,这样后边的进程就可用了.
25
26
27 # -----------------------------------------------
28 awkscript='{ total += $ENVIRON["column_number"] }
29 END { print total }'
30 # 是的,一个变量可以保存一个awk脚本.
31 # -----------------------------------------------
32
33 # 现在,运行awk脚本.
34 awk "$awkscript" "$filename"
35
36 # Thanks, Stephane Chazelas.
37
38 exit 0
################################End Script#########################################

    注意:可以在一个操作中同时赋值和export变量,如: export var1=xxx.

    然而,像Greg Keraunen指出的,在某些情况下使用上边这种形式,将与先设置变量,然后
    export变量效果不同.

    bash$ export var=(a b); echo ${var[0]}
    (a b)



    bash$ var=(a b); export var; echo ${var[0]}
    a


declare, typeset
    declare和typeset命令被用来指定或限制变量的属性.

readonly
    与declare -r作用相同,设置变量的只读属性,也可以认为是设置常量.设置了这种属性之后
    如果你还要修改它,那么你将得到一个错误消息.这种情况与C语言中的const常量类型的情
    况是相同的.

getopts
    可以说这是分析传递到脚本的命令行参数的最强力工具.这个命令与getopt外部命令,和
    C语言中的库函数getopt的作用是相同的.它允许传递和连接多个选项[2]到脚本中,并能分
    配多个参数到脚本中.

    getopts结构使用两个隐含变量.$OPTIND是参数指针(选项索引),和$OPTARG(选项参数)
    (可选的)可以在选项后边附加一个参数.在声明标签中,选项名后边的冒号用来提示
    这个选项名已经分配了一个参数.

    getopts结构通常都组成一组放在一个while循环中,循环过程中每次处理一个选项和参数,
    然后增加隐含变量$OPTIND的值,再进行下一次的处理.

    注意: 1.通过命令行传递到脚本中的参数前边必须加上一个减号(-).这是一个前缀,这样
            getopts命令将会认为这个参数是一个选项.事实上,getopts不会处理不带"-"前缀
            的参数,如果第一个参数就没有"-",那么将结束选项的处理.

          2.使用getopts的while循环模版还是与标准的while循环模版有些不同.没有标准
            while循环中的[]判断条件.

          3.getopts结构将会取代getopt外部命令.

################################Start Script#######################################
 1 while getopts ":abcde:fg" Option
 2 # Initial declaration.
 2 # 开始的声明.
 3 # a, b, c, d, e, f, 和 g 被认为是选项(标志).
 4 # e选项后边的:提示,这个选项带一个参数.
 5 do
 6   case $Option in
 7     a ) # Do something with variable 'a'.
 7     a ) # 对选项'a'作些操作.
 8     b ) # 对选项'b'作些操作.
 9     ...
10     e)  # Do something with 'e', and also with $OPTARG,
10     e)  # 对选项'e'作些操作, 同时处理一下$OPTARG,
11         # which is the associated argument passed with option 'e'.
11         # 这个变量里边将保存传递给选项"e"的参数.
12     ...
13     g ) # 对选项'g'作些操作.
14   esac
15 done
16 shift $(($OPTIND - 1))
17 # 将参数指针向下移动.
18
19 # 所有这些远没有它看起来的那么复杂.<嘿嘿>
20           
################################End Script#########################################

Example 11-19 使用getopts命令来读取传递给脚本的选项/参数.
(我测试的结果与说明不同,我使用 ./scriptname -mnp,但是$OPTIND的值居然是1 1 2)
################################Start Script#######################################
 1 #!/bin/bash
 2 # 练习 getopts 和 OPTIND
 3 # 在Bill Gradwohl的建议下,这个脚本于 10/09/03 被修改.
 4
 5
 6 # 这里我们将学习 'getopts'如何处理脚本的命令行参数.
 7 # 参数被作为"选项"(标志)被解析,并且分配参数.
 8
 9 # 试一下通过如下方法来调用这个脚本
10 # 'scriptname -mn'
11 # 'scriptname -oq qOption' (qOption 可以是任意的哪怕有些诡异字符的字符串.)
12 # 'scriptname -qXXX -r'
13 #
14 # 'scriptname -qr'    - 意外的结果, "r" 将被看成是选项 "q" 的参数.
15 # 'scriptname -q -r'  - 意外的结果, 同上.
16 # 'scriptname -mnop -mnop'  - 意外的结果
17 # (OPTIND is unreliable at stating where an option came from).
18 #
19 #  如果一个选项需要一个参数("flag:"),那么它应该
20 #+ 取得在命令行上挨在它后边的任何字符.
21
22 NO_ARGS=0
23 E_OPTERROR=65
24
25 if [ $# -eq "$NO_ARGS" ]  # 不带命令行参数就调用脚本?
26 then
27   echo "Usage: `basename $0` options (-mnopqrs)"
28   exit $E_OPTERROR        # 如果没有参数传进来,那就退出,并解释用法.
29 fi  
30 # 用法: 脚本名 -选项名
31 # 注意: 破折号(-)是必须的
32
33
34 while getopts ":mnopq:rs" Option
35 do
36   case $Option in
37     m     ) echo "Scenario #1: option -m-   [OPTIND=${OPTIND}]";;
38     n | o ) echo "Scenario #2: option -$Option-   [OPTIND=${OPTIND}]";;
39     p     ) echo "Scenario #3: option -p-   [OPTIND=${OPTIND}]";;
40     q     ) echo "Scenario #4: option -q-\
41  with argument \"$OPTARG\"   [OPTIND=${OPTIND}]";;
42     #  注意,选项'q'必须分配一个参数,
43     #+ 否则默认将失败.
44     r | s ) echo "Scenario #5: option -$Option-";;
45     *     ) echo "Unimplemented option chosen.";;   # DEFAULT
46   esac
47 done
48
49 shift $(($OPTIND - 1))
50 #  将参数指针减1,这样它将指向下一个参数.
51 #  $1 现在引用的是命令行上的第一个非选项参数
52 #+ 如果有一个这样的参数存在的话.
53
54 exit 0
55
56 #   像 Bill Gradwohl 所说,<rojy bug>
57 #  "The getopts mechanism allows one to specify:  scriptname -mnop -mnop
58 #+  but there is no reliable way to differentiate what came from where
59 #+  by using OPTIND."
################################End Script#########################################

脚本行为

source, . (点命令)
    这个命令在命令行上执行的时候,将会执行一个脚本.在一个文件内一个source file-name
    将会加载file-name文件.source一个文件(或点命令)将会在脚本中引入代码,并附加到脚
    本中(与C语言中的#include指令的效果相同).最终的结果就像是在使用"sourced"行上插
    入了相应文件的内容.这在多个脚本需要引用相同的数据,或函数库时非常有用.
Example 11-20 "Including"一个数据文件
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 . data-file    # 加载一个数据文件.
 4 # 与"source data-file"效果相同,但是更具可移植性.
 5
 6 #  文件"data-file"必须存在于当前工作目录,
 7 #+ 因为这个文件时使用'basename'来引用的.
 8
 9 # 现在,引用这个数据文件中的一些数据.
10
11 echo "variable1 (from data-file) = $variable1"
12 echo "variable3 (from data-file) = $variable3"
13
14 let "sum = $variable2 + $variable4"
15 echo "Sum of variable2 + variable4 (from data-file) = $sum"
16 echo "message1 (from data-file) is \"$message1\""
17 # 注意 :                            将双引号转义
18
19 print_message This is the message-print function in the data-file.
20
21
22 exit 0
################################End Script#########################################
    Example 11-20使用的data-file.见上边,这个文件必须和上边的脚本放在同一目录下.
################################Start Script#######################################
 1 # 这是需要被脚本加载的data file.
 2 # 这种文件可以包含变量,函数,等等.
 3 # 在脚本中可以通过'source'或者'.'命令来加载.
 4
 5 # 让我们初始化一些变量.
 6
 7 variable1=22
 8 variable2=474
 9 variable3=5
10 variable4=97
11
12 message1="Hello, how are you?"
13 message2="Enough for now. Goodbye."
14
15 print_message ()
16 {
17 # Echo出传递进来的任何消息.
18
19   if [ -z "$1" ]
20   then
21     return 1
22     # 如果没有参数的话,出错.
23   fi
24
25   echo
26
27   until [ -z "$1" ]
28   do
29     # 循环处理传递到函数中的参数.
30     echo -n "$1"
31     # 每次Echo 一个参数, -n禁止换行.
32     echo -n " "
33     # 在参数间插入空格.
34     shift
35     # 下一个.
36   done  
37
38   echo
39
40   return 0
41 }  
################################End Script#########################################

    如果引入的文件本身就是一个可执行脚本的话,那么它将运行起来,当它return的时候,控制
    权又重新回到了引用它的脚本中.一个用source引入的脚本可以使用return 命令来达到这
    个目的.

    也可以向需要source的脚本中传递参数.这些参数在source脚本中被认为是位置参数.
    1 source $filename $arg1 arg2

    你甚至可以在脚本文件中source脚本文件自身,虽然看不出有什么实际的应用价值.
Example 11-21 一个没什么用的,source自身的脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # self-source.sh: 一个脚本递归的source自身.
 3 # 来自于"Stupid Script Tricks," 卷 II.
 4
 5 MAXPASSCNT=100    # source自身的最大数量.
 6
 7 echo -n  "$pass_count  "
 8 #  在第一次运行的时候,这句只不过echo出2个空格,
 9 #+ 因为$pass_count还没被初始化.
10
11 let "pass_count += 1"
12 #  假定这个为初始化的变量 $pass_count
13 #+ 可以在第一次运行的时候+1.
14 #  这句可以正常工作于Bash和pdksh,但是
15 #+ 它依赖于不可移植(并且可能危险)的行为.
16 #  更好的方法是在使用$pass_count之前,先把这个变量初始化为0.
17
18 while [ "$pass_count" -le $MAXPASSCNT ]
19 do
20   . $0   # 脚本"sources" 自身, 而不是调用自己.
21          # ./$0 (应该能够正常递归) 但是不能在这正常运行. 为什么?
22 done  
23
24 #  这里发生的动作并不是真正的递归,
25 #+ 因为脚本成功的展开了自己,换句话说,
26 #+ 在每次循环的过程中
27 #+ 在每个'source'行(第20行)上
28 #  都产生了新的代码.
29 #
30 #  当然,脚本会把每个新'sourced'进来的文件的"#!"行
31 #+ 都解释成注释,而不会把它看成是一个新的脚本.
32
33 echo
34
35 exit 0   # 最终的效果就是从1数到100.
36          # 让人印象深刻.
37
38 # 练习:
39 # -----
40 # 使用这个小技巧编写一些真正能干些事情的脚本.
################################End Script#########################################

exit
    绝对的停止一个脚本的运行.exit命令有可以随便找一个整数变量作为退出脚本返回shell
    时的退出码.使用exit 0对于退出一个简单脚本来说是种好习惯,表明成功运行.

    注意: 如果不带参数的使用exit来退出,那么退出码将是脚本中最后一个命令的退出码.
        等价于exit $?.

exec
    这个shell内建命令将使用一个特定的命令来取代当前进程.一般的当shell遇到一个命令,
    它会fork off一个子进程来真正的运行命令.使用exec内建命令,shell就不会fork了,并
    且命令的执行将会替换掉当前shell.因此,当我们在脚本中使用它时,当命令实行完毕,
    它就会强制退出脚本.[3]
Example 11-22 exec的效果
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 exec echo "Exiting \"$0\"."   # 脚本将在此退出.
 4
 5 # ----------------------------------
 6 # 下边的部分将执行不到.
 7
 8 echo "This echo will never echo."
 9
10 exit 99                       #  脚本不会在这退出.
11                               #  脚本退出后检查一下退出码
12                               #+ 使用'echo $?'命令.
13                               #  肯定不是99.
################################End Script#########################################

Example 11-23 一个exec自身的脚本
################################Start Script#######################################
 1 #!/bin/bash
 2 # self-exec.sh
 3
 4 echo
 5
 6 echo "This line appears ONCE in the script, yet it keeps echoing."
 7 echo "The PID of this instance of the script is still $$."
 8 #     上边这句用来根本没产生子进程.
 9
10 echo "==================== Hit Ctl-C to exit ===================="
11
12 sleep 1
13
14 exec $0   #  产生了本脚本的另一个实例,
15           #+ 并且这个实例代替了之前的那个.
16
17 echo "This line will never echo!"  # 当然会这样.
18
19 exit 0
################################End Script#########################################
    exec命令还能用于重新分配文件描述符.比如: exec <zzz-file将会用zzz-file来代替
    stdin.

    注意: find命令的 -exec选项与shell内建的exec命令是不同的.

shopt
    这个命令允许shell在空闲时修改shell选项(见Example 24-1和Example 24-2).它经常出
    现在启动脚本中,但是在一般脚本中也可用.需要Bash 2.0版本以上.
    1 shopt -s cdspell
    2 # Allows minor misspelling of directory names with 'cd'
    2 # 使用'cd'命令时,允许产生少量的拼写错误.
    3
    4 cd /hpme  # 噢! 应该是'/home'.
    5 pwd       # /home
    6           # 拼写错误被纠正了.

caller
    将caller命令放到函数中,将会在stdout上打印出函数调用者的信息.
     1 #!/bin/bash
     2
     3 function1 ()
     4 {
     5   # 在 function1 () 内部.
     6   caller 0   # 显示调用者信息.
     7 }
     8
     9 function1    # 脚本的第9行.
    10
    11 # 9 main test.sh
    12 # ^                 函数调用者所在的行号.
    13 #   ^^^^            从脚本的"main"部分调用的.
    14 #        ^^^^^^^    调用脚本的名字
    15
    16 caller 0     # 没效果,因为这个命令不再函数中.

    caller命令也可以返回在一个脚本中被source的另一个脚本的信息.象函数一样,这是一个
    "子例程调用",你会发现这个命令在调试的时候特别有用.

命令类

ture
    一个返回成功(就是返回0)退出码的命令,但是除此之外什么事也不做.
    1 # 死循环
    2 while true   # 这里的true可以用":"替换
    3 do
    4    operation-1
    5    operation-2
    6    ...
    7    operation-n
    8    # 需要一种手段从循环中跳出来,或者是让这个脚本挂起.
    9 done

flase
    一个返回失败(非0)退出码的命令,但是除此之外什么事也不做.
     1 # 测试 "false"
     2 if false
     3 then
     4   echo "false evaluates \"true\""
     5 else
     6   echo "false evaluates \"false\""
     7 fi
     8 # 失败会显示"false"
     9
    10
    11 # while "false" 循环 (空循环)
    12 while false
    13 do
    14    # 这里边的代码将不会走到.
    15    operation-1
    16    operation-2
    17    ...
    18    operation-n
    19    # 什么事都没发生!
    20 done   

type[cmd]
    与which扩展命令很相像,type cmd将给出"cmd"的完整路径.与which命令不同的是,type命
    令是Bash内建命令.一个很有用的选项是-a选项,使用这个选项可以鉴别所识别的参数是关
    键字还是内建命令,也可以定位同名的系统命令.
    bash$ type '['
    [ is a shell builtin
    bash$ type -a '['
    [ is a shell builtin
    [ is /usr/bin/[

hash[cmds]
    在shell的hash表中[4],记录指定命令的路径名,所以在shell或脚本中在调用这个命令的
    话,shell或脚本将不需要再在$PATH中重新搜索这个命令了.如果不带参数的调用hash命
    令,它将列出所有已经被hash的命令.-r选项会重新设置hash表.

bind
    bind内建命令用来显示或修改readline[5]的键绑定.

help
    获得shell内建命令的一个小的使用总结.这与whatis命令比较象,但是help是内建命令.
    bash$ help exit
    exit: exit [n]
       Exit the shell with a status of N.  If N is omitted, the exit status
       is that of the last command executed.


11.1 作业控制命令
-----------------
下边的作业控制命令需要一个"作业标识符"作为参数.见这章结尾的表.

jobs
    在后台列出所有正在运行的作业,给出作业号.

    注意: 进程和作业的概念太容易混淆了.特定的内建命令,比如kill,disown和wait即可以
        接受一个作业号作为参数也可以接受一个作为参数.但是fg,bg和jobs命令只能接受
        作业号作为参数.
        bash$ sleep 100 &
        [1] 1384

        bash $ jobs
        [1]+  Running                 sleep 100 &

        注意: "1"是作业号(作业是被当前shell所维护的),而"1384"是进程号(进程是被系统
            维护的).为了kill掉作业/进程,或者使用 kill %1命令或者使用kill 1384命令,
            这两个命令都可以.

            感谢,S.C.

disown
    从shell的当前作业表中,删除作业.

fg,bg
    fg命令可以把一个在后台运行的作业放到前台来运行.而bg命令将会重新启动一个挂起的
    作业,并且在后台运行它.如果使用fg或者bg命令的时候没指定作业号,那么默认将对当前
    正在运行的作业做操作.

wait
    停止脚本的运行,直到后台运行的所有作业都结束为止,或者直到指定作业号或进程号为选
    项的作业结束为止.

    你可以使用wait命令来防止在后台作业没完成(这会产生一个孤儿进程)之前退出脚本.

Example 11-24 在继续处理之前,等待一个进程的结束
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 ROOT_UID=0   # 只有$UID 为0的用户才拥有root权限.
 4 E_NOTROOT=65
 5 E_NOPARAMS=66
 6
 7 if [ "$UID" -ne "$ROOT_UID" ]
 8 then
 9   echo "Must be root to run this script."
10   # "Run along kid, it's past your bedtime."
11   exit $E_NOTROOT
12 fi  
13
14 if [ -z "$1" ]
15 then
16   echo "Usage: `basename $0` find-string"
17   exit $E_NOPARAMS
18 fi
19
20
21 echo "Updating 'locate' database..."
22 echo "This may take a while."
23 updatedb /usr &     # 必须使用root身份来运行.
24
25 wait
26 # 将不会继续向下运行,除非 'updatedb'命令执行完成.
27 # 你希望在查找文件名之前更新database.
28
29 locate $1
30
31 #  如果没有'wait'命令的话,而且在比较糟的情况下,
32 #+ 脚本可能在'updatedb'命令还在运行的时候退出,
33 #+ 这将会导致'updatedb'成为一个孤儿进程.
34
35 exit 0
################################End Script#########################################

    当然,wait 也可以接受一个作业标识符作为参数,比如,wait %1或wait $PPID.见"作业标识
    符表".

    注意: 在一个脚本中,使用一个后台运行的命令(使用&)可能会使这个脚本挂起,直到敲
        回车,挂起才会被恢复.看起来只有这个命令的结果需要输出到stdout的时候才会发
        生这种现象.这会是一个很烦人的现象.
          1 #!/bin/bash
          2 # test.sh          
          3
          4 ls -l &
          5 echo "Done."

        bash$ ./test.sh
        Done.
        [bozo@localhost test-scripts]$ total 1
        -rwxr-xr-x    1 bozo     bozo           34 Oct 11 15:09 test.sh
        _
                

        看起来在这个后台运行命令的后边放上一个wait命令可能会解决这个问题.
          1 #!/bin/bash
          2 # test.sh          
          3
          4 ls -l &
          5 echo "Done."
          6 wait

        bash$ ./test.sh
        Done.
        [bozo@localhost test-scripts]$ total 1
        -rwxr-xr-x    1 bozo     bozo           34 Oct 11 15:09 test.sh
                       
        如果把这个后台运行命令的输出重定向到文件中或者重定向到/dev/null中,也能解决
        这个问题.

suspend
    这个命令的效果与Control-Z很相像,但是它挂起的是这个shell(这个shell的父进程应该
    在合适的时候重新恢复它).

logout
    退出一个登陆的shell,也可以指定一个退出码.

times
    给出执行命令所占的时间,使用如下形式输出:
     0m0.020s 0m0.020s
    这是一种很有限的能力,因为这不常出现于shell脚本中.

kill
    通过发送一个适当的结束信号,来强制结束一个进程(见Example 13-6).
Example 11-25 一个结束自身的脚本.
################################Start Script#######################################
 1 #!/bin/bash
 2 # self-destruct.sh
 3
 4 kill $$  # 脚本将在此处结束自己的进程.
 5          # Recall that "$$" is the script's PID.
 5          # 回忆一下,"$$"就是脚本的PID.
 6
 7 echo "This line will not echo."
 8 # 而且shell将会发送一个"Terminated"消息到stdout.
 9
10 exit 0
11
12 #  在脚本结束自身进程之后,
13 #+ 它返回的退出码是什么?
14 #
15 # sh self-destruct.sh
16 # echo $?
17 # 143
18 #
19 # 143 = 128 + 15
20 #             结束信号
################################End Script#########################################
    注意: kill -l将列出所有信号. kill -9 是"必杀"命令,这个命令将会结束哪些顽固的
        不想被kill掉的进程.有时候kill -15也可以干这个活.一个僵尸进程不能被登陆的
        用户kill掉, -- 因为你不能杀掉一些已经死了的东西 -- ,但是init进程迟早会
        把它清除干净.僵尸进程就是子进程已经结束掉,而父进程却没kill掉这个子进程,
        那么这个子进程就成为僵尸进程.

command
    command命令会禁用别名和函数的查找.它只查找内部命令以及搜索路径中找到的脚本或可
    执行程序.(译者,只在要执行的命令与函数或别名同名时使用,因为函数的优先级比内建命
    令的优先级高)

    (译者:注意一下bash执行命令的优先级:
        1.别名
        2.关键字
        3.函数
        4.内置命令
        5.脚本或可执行程序($PATH)
    )


    注意: 当象运行的命令或函数与内建命令同名时,由于内建命令比外部命令的优先级高,而
        函数比内建命令优先级高,所以bash将总会执行优先级比较高的命令.这样你就没有选
        择的余地了.所以Bash提供了3个命令来让你有选择的机会.command命令就是这3个命
        令之一.
        另外两个是builtin和enable.

builtin
    在"builtin"后边的命令将只调用内建命令.暂时的禁用同名的函数或者是同名的扩展命令.

enable
    这个命令或者禁用内建命令或者恢复内建命令.如: enable -n kill将禁用kill内建命令,
    所以当我们调用kill时,使用的将是/bin/kill外部命令.

    -a选项将会恢复相应的内建命令,如果不带参数的话,将会恢复所有的内建命令.
    选项-f filename将会从适当的编译过的目标文件[6]中以共享库(DLL)的形式来加载一个
    内建命令.

autoload
    这是从ksh的autoloader命令移植过来的.一个带有"autoload"声明的函数,在它第一次被
    调用的时候才会被加载.[7] 这样做会节省系统资源.

    注意: autoload命令并不是Bash安装时候的核心命令的一部分.这个命令需要使用命令
        enable -f(见上边enable命令)来加载.
    

Table 11-1 作业标识符
==========================================================
 记法  | 含义
==========================================================
 %N    | 作业号[N]
==========================================================
 %S    | 以字符串S开头的被(命令行)调用的作业
==========================================================
 %?S   | 包含字符串S的被(命令行)调用的作业
==========================================================
 %%    | 当前作业(前台最后结束的作业,或后台最后启动的作业)
==========================================================
 %+    | 当前作业(前台最后结束的作业,或后台最后启动的作业)
==========================================================
 %-    | 最后的作业
==========================================================
 $!    | 最后的后台进程
==========================================================


注意事项:
[1]        一个例外就是time命令,Bash官方文档说这个命令是一个关键字.
[2]        一个选项就是一个行为上比较象标志位的参数,可以用来打开或关闭脚本的某些行为.
        而和某个特定选项相关的参数就是用来控制这个选项功能是开启还是关闭的.
[3]        除非exec被用来重新分配文件描述符.
[4]        hash是一种处理存储在表中数据的方法,这种方法就是为表中的数据建立查找键.
        而数据项本身是不规则的,这样就可以通过一个简单的数学算法来产生一个数字,
        这个数字被用来作为查找键.

        使用hash的一个最有利的地方就是提高了速度.而缺点就是会产生"冲撞" -- 也就是
        说,可能会有多个数据元素使用同一个主键.

        关于hash的例子见 Example A-21 和 Example A-22.
[5]        在一个交互的shell中,readline库就是Bash用来读取输入的.
        (译者: 比如默认的Emacs风格的输入,当然也可以改为vi风格的输入)
[6]        一些可加载的内建命令的C源代码都放在/usr/share/doc/bash-?.??/functions下.
        注意: enable命令的-f选项并不是对所有系统都支持的(看移没移植上).
[7]        typeset -fu可以达到和autoload命令相同的作用.

返回顶部

发表评论:

Powered By Z-BlogPHP 1.7.3


知识共享许可协议
本作品采用知识共享署名 3.0 中国大陆许可协议进行许可。
网站备案号粤ICP备15104741号-1