Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1076760
  • 博文数量: 242
  • 博客积分: 10209
  • 博客等级: 上将
  • 技术积分: 3028
  • 用 户 组: 普通用户
  • 注册时间: 2008-03-12 09:27
文章分类

全部博文(242)

文章存档

2014年(1)

2013年(1)

2010年(51)

2009年(65)

2008年(124)

我的朋友

分类:

2008-04-10 16:54:36

高级Bash脚本编程指南(五)
 
 
 
第四部分 高级
++++++++++++++++
到了这儿,我们将要准备深入脚本编程中一些难的,不寻常的话题.随着话题的展开,我们会
以多种方法和检测边界条件的方式来“打开信封”,看个明白.(当我们涉足未知领域时会发
生什么?).

目录
19. Regular Expressions正则表达式
20. 子shell(Subshells)
21. 受限shell(Restricted Shells)
22. 进程替换
23. 函数
24. 别名(Aliases)
25. 列表结构
26. 数组
27. /dev和/proc
28. 关于Zeros和Nulls
29. 调试
30. 选项
31. 检查遗漏(Gotchas)
32. 脚本编程风格
33. 杂项
34. Bash,版本2和3

第19章 正则表达式
==================
为了充分发挥shell编程的威力, 你需要精通正则表达式. 一些命令和软件包普遍在脚本编程中
使用正则表达式,例如grep, expr, sed和awk.

19.1 一个简要的正则表达式介绍
--------------------------------
一个正式表达式是一个字符串.字符串里的字符被称为元字符,它们可能表示了比它们字面上看
起来的意思更丰富的含义.例如,一个引用符号可能表示引用一个人演讲中的话,或者表示下
面将要讲到的引申表示的意思.正则表达式是一个字符或/和元字符组合成的字符集,它们匹配
(或指定)一个模式.
一个正则表达式包含下面一个或多个项:
 1. 一个字符集.
  这里的字符集里的字符表示的就是它们字面上的意思.正则表达式最简单的情况就是仅
  仅由字符集组成,而没有其他的元字符.
 2. 锚.
  一个锚指明了正则表达式在一行文本中要匹配的位置,例如^和$就是锚.
 3. 修饰符
  它们用于展开或缩小(即是修改了)正则表达式匹配文本行的范围.修饰符包括了星号.
  括号和反斜杠符号.
正则表达是的主要作用是用来文本搜索和字串操作.一个正则表达式匹配一个字符或是一串字
符--完整的一串字符或是另外一个字符串的子串.
星号  -- * -- 匹配前一个字符的任意多次(包括零次).
   "1133*"匹配11 + 一个或更多的3 + 可能的其他字符: 113, 1133, 111312, 等等.
点   -- . -- 匹配除了新行符之外的任意一个字符. [1]
   "13." 匹配13 + 至少一个任意字符(包括空格): 1133, 11333, 但不匹配 13
   (因为少了附加的至少一个任意字符).
脱字符  -- ^ -- 匹配一行的开头,但依赖于上下文环境,可能在正则表达式中表示否定
   一个字符集的意思.
美元符  -- $ -- 在正则表达式中匹配行尾.
   "^$" 匹配空行.
方括号  -- [...] -- 在正则表达式中表示匹配括号中的一个字符.
   "[xyz]" 匹配字符x, y, 或z.
   "[c-n]" 匹配从字符c到n之间的任意一个字符.
   "[B-Pk-y]" 匹配从B到P 或从k到y的任意一个字符.
   "[a-z0-9]" 匹配任意小写字母或数字.
   "[^b-d]" 匹配除了从b到d范围内所有的字符. 这是正则表达式中反转意思或取否
   的一 个例子.(就好像在别的情形中!字符所扮演的角色).
   多个方括号字符集组合使用可以匹配一般的单词和数字模式."[Yy][Ee][Ss]" 匹
   配yes, Yes, YES, yEs, 等等.
   "[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]"匹配社会安全码
   (Social Security number).
反斜杠字符 -- \ -- 转义(escapes) 一个特殊的字符,使这个字符表示原来字面上的意思.
   "\$"表示了原来的字面意思"$",而不是在正则表达式中表达的匹配行尾的意思.
   同样,"\\"也被解释成了字面上的意思"\".
转义(escape)"尖角号" -- \<...\> -- 用于表示单词的边界.
      尖角号必须被转义,因为不这样做的话它们就表示单纯的字面意思
      而已.
      "\" 匹配单词"the",但不匹配"them", "there", "other",
      等等.
   bash$ cat textfile
   This is line 1, of which there is only one instance.
   This is the only instance of line 2.
   This is line 3, another line.
   This is line 4.
  
  
   bash$ grep 'the' textfile
   This is line 1, of which there is only one instance.
   This is the only instance of line 2.
   This is line 3, another line.
  
  
   bash$ grep '\' textfile
   This is the only instance of line 2.
 确定正则表达式能否工作的唯一办法是测试它.
    1 TEST FILE: tstfile                          # 不匹配.
    2                                             # 不匹配.
    3 Run   grep "1133*"  on this file.           # 匹配.
    4                                             # 不匹配.
    5                                             # 不匹配.
    6 This line contains the number 113.          # 匹配.
    7 This line contains the number 13.           # 不匹配.
    8 This line contains the number 133.          # 不匹配.
    9 This line contains the number 1133.         # 匹配.
   10 This line contains the number 113312.       # 匹配.
   11 This line contains the number 1112.         # 不匹配.
   12 This line contains the number 113312312.    # 匹配.
   13 This line contains no numbers at all.       # 不匹配.
  bash$ grep "1133*" tstfile
  Run   grep "1133*"  on this file.           # 匹配.
  This line contains the number 113.          # 匹配.
  This line contains the number 1133.         # 匹配.
  This line contains the number 113312.       # 匹配.
  This line contains the number 113312312.    # 匹配.
扩展的正则表达式. 增加了一些元字符到上面提到的基本的元字符集合里. 它们在egrep,
     awk,和Perl中使用.
问号  -- ? -- 匹配零或一个前面的字符. 它一般用于匹配单个字符.
加号  -- + -- 匹配一个或多个前面的字符.它的作用和*很相似,但唯一的区别是它不
   匹配零个字符的情况.
     1 # GNU 版本的 sed 和 awk 可以使用"+",
     2 # 但它应该转义一下.
     3
     4 echo a111b | sed -ne '/a1\+b/p'
     5 echo a111b | grep 'a1\+b'
     6 echo a111b | gawk '/a1+b/'
     7 # 上面三句都是等价的效果.
     8
     9 # 多谢, S.C.
转义"大括号" -- \{ \} -- 指示前面正则表达式匹配的次数.
    要转义是因为不转义的话大括号只是表示他们字面上的意思.这个用法只是
    技巧上的而不是基本正则表达式的内容.
    "[0-9]\{5\}" 精确匹配5个数字 (从 0 到 9的数字).
    注意: 大括号不能在“经典”(不是POSIX兼容)的正则表达式版本的awk中
      使用. 然而, gawk 有一个选项--re-interval来允许使用大括号
      (不必转义).
       bash$ echo 2222 | gawk --re-interval '/2{3}/'
       2222
      Perl和一些egrep版本不要求转义大括号.
圆括号  -- ( ) -- 括起一组正则表达式. 它和下面要讲的"|"操作符或在用expr进行子字
   符串提取(substring extraction)一起使用很有用.
竖线  -- | -- "或"正则操作符用于匹配一组可选的字符.
    bash$ egrep 're(a|e)d' misc.txt
    People who read seem to be better informed than those who do not.
    The clarinet produces sound by the vibration of its reed.
   注意: 一些sed, ed, 和ex的版本像GNU的软件版本一样支持上面描述的扩展正
     则表达式的版本.
POSIX字符类. [:class:]
    这是另外一个可选的用于指定匹配字符范围的方法.
[:alnum:] 匹配字母和数字.等同于A-Za-z0-9.
[:alpha:] 匹配字母. 等同于A-Za-z.
[:blank:] 匹配一个空格或是一个制表符(tab).
[:cntrl:] 匹配控制字符.
[:digit:] 匹配(十进制)数字. 等同于0-9.
[:graph:] (可打印的图形字符). 匹配 ASCII 码值的33 - 126之间的字符. 这和下面提到的
   [:print:]一样,但是不包括空格字符.
[:lower:] 匹配小写字母. 等同于a-z.
[:print:] (可打印字符). 匹配 ASCII码值 32 - 126之间的字符. 这和上面提到的一样
   [:graph:],但是增多一个空格字符.
[:space:] 匹配空白字符 (空格符和水平制表符).
[:upper:] 匹配大写字母. 等同于A-Z.
[:xdigit:] 匹配十六进制数字. 等同于0-9A-Fa-f.
注意: POSIX字符类一般都要求用引号或是双方括号double brackets ([[ ]])引起来.
   bash$ grep [[:digit:]] test.file
   abc=723
  这些字符类在一个受限的范围内甚至可能用在能用在通配(globbing)中.
   bash$ ls -l ?[[:digit:]][[:digit:]]?
   -rw-rw-r--    1 bozo  bozo         0 Aug 21 14:47 a33b
  为了理解POSIX字符类在脚本中的使用,请参考例子 12-18 和 例子 12-19.
Sed, awk, 和Perl在脚本中被用作过滤器, "过滤"或转换文件/IO流的时候以正则表达式作为参
数.参考例子 A-12和例子 A-17 来理解这种用法.
在正则表达式这个复杂主题的标准参考是Friedl的Mastering Regular Expressions.由
Dougherty和Robbins写的 Sed & Awk也给出了一个清晰的正则表达式论述. 查看参考书目找
到这个主题更多的信息.
注意事项:
[1]  因为sed, awk, 和 grep 通常处理单行,而不能匹配一个新行符. 在要处理多行的一
  个输入时,可以使用点操作符,它可以匹配新行符.
     1 #!/bin/bash
     2
     3 sed -e 'N;s/.*/[&]/' << EOF   # Here Document
     4 line1
     5 line2
     6 EOF
     7 # 输出:
     8 # [line1
     9 # line2]
    10
    11
    12
    13 echo
    14
    15 awk '{ $0=$1 "\n" $2; if (/line.1/) {print}}' << EOF
    16 line 1
    17 line 2
    18 EOF
    19 # 输出:
    20 # line
    21 # 1
    22
    23
    24 # 多谢, S.C.
    25
    26 exit 0
19.1 通配
------------
Bash本身没有正则表达式的功能.在脚本里,使用正则表达式的是命令和软件包 -- 例如sed和
awk -- 它们可以解释正则表达式.
Bash所做的是展开文件名扩展 [1] -- 这就是所谓的通配(globbing) -- 但它不是使用标准的
正则表达式. 而是使用通配符. 通配解释标准的通配符:*和?, 方括号括起来的字符,还有其他
的一些特殊的字符(比如说^用来表示取反匹配).然而通配机制的通配符有很大的局限性. 包含
有*号的字符串将不会匹配以点开头的文件,例如.bashrc. [2] 另外,通配机制的? 字符和正则
表达式中表示的意思不一样.
 bash$ ls -l
 total 2
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 a.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 b.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 c.1
 -rw-rw-r--    1 bozo  bozo       466 Aug  6 17:48 t2.sh
 -rw-rw-r--    1 bozo  bozo       758 Jul 30 09:02 test1.txt
 
 bash$ ls -l t?.sh
 -rw-rw-r--    1 bozo  bozo       466 Aug  6 17:48 t2.sh
 
 bash$ ls -l [ab]*
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 a.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 b.1
 
 bash$ ls -l [a-c]*
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 a.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 b.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 c.1
 
 bash$ ls -l [^ab]*
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 c.1
 -rw-rw-r--    1 bozo  bozo       466 Aug  6 17:48 t2.sh
 -rw-rw-r--    1 bozo  bozo       758 Jul 30 09:02 test1.txt
 
 bash$ ls -l {b*,c*,*est*}
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 b.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 c.1
 -rw-rw-r--    1 bozo  bozo       758 Jul 30 09:02 test1.txt
Bash会对命令行中没有引号引起来的字符尝试文件名扩展. echo 命令可以印证这一点.
 bash$ echo *
 a.1 b.1 c.1 t2.sh test1.txt
 
 bash$ echo t*
 t2.sh test1.txt
注意: 可以改变Bash对通配字符进行解释的行为. set -f 命令可以禁止通配机制, 并且
  shopt的选项nocaseglob和nullglob 能改变通配的行为.
参考例子 10-4.
注意事项:
[1]  文件名扩展意思是扩展包含有特殊字符的文件名模式和模板. 例如,example.???可能
  扩展成example.001和/或example.txt.
[2]  文件名扩展能匹配点开头的文件,但仅在模式字串明确地包含字面意思的点(.)时才
  扩展.
     1 ~/[.]bashrc    #  不会扩展成 ~/.bashrc
     2 ~/?bashrc      #  也不会扩展.
     3                #  通配机制中的通配符和元字符不会扩展点文件
     4                #
     5
     6 ~/.[b]ashrc    #  会扩展成 ~/.bashrc
     7 ~/.ba?hrc      #  也会.
     8 ~/.bashr*      #  也会.
     9
    10 # 可以使用"dotglob"选项把这个特性禁用.
    11
    12 # 多谢, S.C.
第20章 子shell(Subshells)
==========================
运行一个shell脚本时会启动另一个命令解释器. 就好像你的命令是在命令行提示下被解释的一
样, 类似于批处理文件里的一系列命令.每个shell脚本有效地运行在父shell(parent shell)的
一个子进程里.这个父shell是指在一个控制终端或在一个xterm窗口中给你命令指示符的进程.
shell脚本也能启动他自已的子进程. 这些子shell(即子进程)使脚本因为效率而同时进行多个
子任务执行时能做串行处理.
一般来说,脚本里的一个外部命令(external command)能生成(forks)出一个子进程,然而
Bash内建(builtin)的命令却不这样做,因此,内建命令比起外部的等价命令执行起来更快.
圆括号里的命令列表
( 命令1; 命令2; 命令3; ... )
    嵌在圆括号里的一列命令在一个子shell里运行.
注意: 在子shell里的变量不能被这段子shell代码块之外外面的脚本访问.这些变量是不能被
  产生这个子shell的父进程(parent process)存取的,实际上它们是局部变量
  (local variables).
Example 20-1 子shell中的变量作用域
################################Start Script#######################################
 1 #!/bin/bash
 2 # subshell.sh
 3
 4 echo
 5
 6 echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
 7 # Bash, 版本 3, 增加了新的              $BASH_SUBSHELL 变量.
 8 echo
 9
10 outer_variable=Outer
11
12 (
13 echo "Subshell level INSIDE subshell = $BASH_SUBSHELL"
14 inner_variable=Inner
15
16 echo "From subshell, \"inner_variable\" = $inner_variable"
17 echo "From subshell, \"outer\" = $outer_variable"
18 )
19
20 echo
21 echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
22 echo
23
24 if [ -z "$inner_variable" ]
25 then
26   echo "inner_variable undefined in main body of shell"
27 else
28   echo "inner_variable defined in main body of shell"
29 fi
30
31 echo "From main body of shell, \"inner_variable\" = $inner_variable"
32 #  $inner_variable 会以没有初始化的变量来打印
33 #+ 因为变量是在子shell里定义的"局部变量".
34 #  这个有办法补救的吗?
35
36 echo
37
38 exit 0
################################End Script#########################################
参考例子 31-2.
+
在子shell中的目录更改不会影响到父shell.
Example 20-2 列出用户的配置文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # allprofs.sh: 打印所有用户的配置文件
 3
 4 # 由 Heiner Steven编写, 并由本书作者修改.
 5
 6 FILE=.bashrc  #  在一般的脚本里,包含用户配置的文件是".profile".
 7               #
 8
 9 for home in `awk -F: '{print $6}' /etc/passwd`
10 do
11   [ -d "$home" ] || continue    # 如果没有家目录,跳过此次循环.
12   [ -r "$home" ] || continue    # 如果目录没有读权限,跳过此次循环.
13   (cd $home; [ -e $FILE ] && less $FILE)
14 done
15
16 #  当脚本终止时,不必用'cd'命令返回原来的目录,
17 #+ 因为'cd $home'是在子shell中发生的,不影响父shell.
18
19 exit 0
################################End Script#########################################
子shell可用于为一组命令设定临时的环境变量.
   1 COMMAND1
   2 COMMAND2
   3 COMMAND3
   4 (
   5   IFS=:
   6   PATH=/bin
   7   unset TERMINFO
   8   set -C
   9   shift 5
  10   COMMAND4
  11   COMMAND5
  12   exit 3 # 只是从子shell退出.
  13 )
  14 # 父shell不受影响,变量值没有更改.
  15 COMMAND6
  16 COMMAND7
它的一个应用是测试是否一个变量被定义了.
   1 if (set -u; : $variable) 2> /dev/null
   2 then
   3   echo "Variable is set."
   4 fi     #  变量已经在当前脚本中被设置,
   5        #+ 或是Bash的一个内部变量,
   6        #+ 或是可见环境变量(指已经被导出的环境变量).
   7
   8 # 也可以写成            [[ ${variable-x} != x || ${variable-y} != y ]]
   9 # 或                    [[ ${variable-x} != x$variable ]]
  10 # 或                    [[ ${variable+x} = x ]]
  11 # 或                    [[ ${variable-x} != x ]]
另一个应用是检查一个加锁的文件:
   1 if (set -C; : > lock_file) 2> /dev/null
   2 then
   3   :   # lock_file 不存在,还没有用户运行这个脚本
   4 else
   5   echo "Another user is already running that script."
   6 exit 65
   7 fi
   8
   9 #  由St閜hane Chazelas编程
  10 #+ 由Paulo Marcel Coelho Aragao修改.
进程在不同的子shell中可以串行地执行.这样就允许把一个复杂的任务分成几个小的子问题来
同时地处理.
Example 20-3 在子shell里进行串行处理
################################Start Script#######################################
 1  (cat list1 list2 list3 | sort | uniq > list123) &
 2  (cat list4 list5 list6 | sort | uniq > list456) &
 3  #列表的合并和排序同时进.
 4  #放到后台运行可以确保能够串行执行.
 5  #
 6  #和下面的有相同的作用:
 7  #   cat list1 list2 list3 | sort | uniq > list123 &
 8  #   cat list4 list5 list6 | sort | uniq > list456 &
 9  
10  wait   #在所有的子shell执行完成前不再执行后面的命令.
11  
12  diff list123 list456
################################End Script#########################################
用"|"管道操作把I/O流重定向到子shell,例如ls -al | (command).
注意:  在一个花括号内的代码块不会运行一个子shell.
  { command1; command2; command3; ... }

第21章 受限shell(Restricted Shells)
====================================
在受限shell中禁用的命令
    在受限shell中运行的脚本或脚本的个代码断会禁用一些正常shell中可以执行的命令.这是
 限制脚本用户的权限和最小化运行脚本导致的破坏的安全措施.
    使用cd 命令更改工作目录.
    更改环境变量$PATH, $SHELL, $BASH_ENV,或$ENV 的值.
    读或更改shell环境选项变量$SHELLOPTS的值.
    输出重定向.
    调用的命令路径中包括有一个或更多个/字符.
    调用exec来把当前的受限shell替换成另外一个不同的进程.
    脚本中许多其他无意中能破坏或捣乱的命令.
    在脚本中企图脱离受限shell模式的操作.
Example 21-1 在受限的情况下运行脚本
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  脚本开头以"#!/bin/bash -r"来调用
 4 #+ 会使整个脚本在受限模式下运行.
 5
 6 echo
 7
 8 echo "Changing directory."
 9 cd /usr/local
10 echo "Now in `pwd`"
11 echo "Coming back home."
12 cd
13 echo "Now in `pwd`"
14 echo
15
16 # 不受限的模式下,所有操作都能正常成功.
17
18 set -r
19 # set --restricted    也能起相同的作用.
20 echo "==> Now in restricted mode. <=="
21
22 echo
23 echo
24
25 echo "Attempting directory change in restricted mode."
26 cd ..
27 echo "Still in `pwd`"
28
29 echo
30 echo
31
32 echo "\$SHELL = $SHELL"
33 echo "Attempting to change shell in restricted mode."
34 SHELL="/bin/ash"
35 echo
36 echo "\$SHELL= $SHELL"
37
38 echo
39 echo
40
41 echo "Attempting to redirect output in restricted mode."
42 ls -l /usr/bin > bin.files
43 ls -l bin.files    # Try to list attempted file creation effort.
44
45 echo
46
47 exit 0
################################End Script#########################################

第22章 进程替换
================
进程替换与命令替换(command substitution)很相似. 命令替换把一个命令的结果赋给一个
变量,例如 dir_contents=`ls -al`或xref=$( grep word datafile). 进程替换则是把一个进
程的输出回馈给另一个进程 (换句话说,它把一个命令的结果发送给另一个命令).
命令替换的一般形式
由圆括号括起的命令
 >(command)
 <(command)
 启动进程替换. 它是用/dev/fd/文件把在圆括号内的进程的处理结果发送给另外一个进
 程. [1] (译者注:实际上现代的UNIX类操作系统提供的/dev/fd/n文件是与文件描述相关
 的,整数n指的就是在进程运行时对应数字的文件描述符)
 注意: 在"<" 或or ">" 与圆括号之间是没有空格的. 如果加了空格将会引起错误信息.
 bash$ echo >(true)
 /dev/fd/63
 
 bash$ echo <(true)
 /dev/fd/63
 Bash在两个文件描述符(file descriptors)之间创建了一个管道, --fIn 和 fOut--. true
命令的标准输入被连接到fOut(dup2(fOut, 0)), 然后Bash把/dev/fd/fIn作为参数传给echo.
如果系统的/dev/fd/文件不够时,Bash会使用临时文件. (Thanks, S.C.)
进程替换能比较两个不同命令之间的输出,或者甚至相同命令不同选项的输出.
 bash$ comm <(ls -l) <(ls -al)
 total 12
-rw-rw-r--    1 bozo bozo       78 Mar 10 12:58 File0
-rw-rw-r--    1 bozo bozo       42 Mar 10 12:58 File2
-rw-rw-r--    1 bozo bozo      103 Mar 10 12:58 t2.sh
        total 20
        drwxrwxrwx    2 bozo bozo     4096 Mar 10 18:10 .
        drwx------   72 bozo bozo     4096 Mar 10 17:58 ..
        -rw-rw-r--    1 bozo bozo       78 Mar 10 12:58 File0
        -rw-rw-r--    1 bozo bozo       42 Mar 10 12:58 File2
        -rw-rw-r--    1 bozo bozo      103 Mar 10 12:58 t2.sh
用进程替换来比较两个不同目录的内容 (考察哪些文件名是相同的,哪些是不同的):
   1 diff <(ls $first_directory) <(ls $second_directory)
其他一些进程替换的用法和技巧:
   1 cat <(ls -l)
   2 # 等同于     ls -l | cat
   3
   4 sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin)
   5 # 列出系统中3个主要的'bin'目录的所有文件,并且按文件名排序.
   6 # 注意是三个明显不同的命令输出回馈给'sort'.
   7
   8 
   9 diff <(command1) <(command2)    # 给出两个命令输出的不同之处.
  10
  11 tar cf >(bzip2 -c > file.tar.bz2) $directory_name
  12 # 调用"tar cf /dev/fd/?? $directory_name",和"bzip2 -c > file.tar.bz2".
  13 #
  14 # 因为/dev/fd/的系统属性,
  15 # 所以两个命令之间的管道不必是命名的.
  16 #
  17 # 这种效果可以模仿出来.
  18 #
  19 bzip2 -c < pipe > file.tar.bz2&
  20 tar cf pipe $directory_name
  21 rm pipe
  22 #        或者
  23 exec 3>&1
  24 tar cf /dev/fd/4 $directory_name 4>&1 >&3 3>&- | bzip2 -c > file.tar.bz2 3>&-
  25 exec 3>&-
  26
  27
  28 # Thanks, St`phane Chazelas
有个读者给我发来下面关于进程替换的有趣例子A.
   1 # 摘自SuSE发行版中的代码片断:
   2
   3 while read  des what mask iface; do
   4 # 这里省略了一些命令 ...
   5 done < <(route -n) 
   6
   7
   8 # 为了测试它,我们来做些动作.
   9 while read  des what mask iface; do
  10   echo $des $what $mask $iface
  11 done < <(route -n) 
  12
  13 # 输出:
  14 # Kernel IP routing table
  15 # Destination Gateway Genmask Flags Metric Ref Use Iface
  16 # 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo
  17
  18
  19
  20 # 由 St閜hane Chazelas给出的,一个更容易理解的等价代码是:
  21 route -n |
  22   while read des what mask iface; do   # 管道的输出被赋给了变量.
  23     echo $des $what $mask $iface
  24   done  #  这样就取回了和上面一样的输出.
  25         #  但是, Ulrich Gayer指出 . . .
  26         #+ 这个简单版本的等价代码在while循环中使用了一个子shell,
  27         #+ 因此当管道结束后变量会被毁掉.
  28  
  29
  30  
  31 #  更进一步, Filip Moritz解释了上面两个例子之间有一个细微的不同之处
  32 #+ 如下所示.
  33
  34 (
  35 route -n | while read x; do ((y++)); done
  36 echo $y # $y 仍然没有被声明或设置
  37
  38 while read x; do ((y++)); done < <(route -n)
  39 echo $y # $y的值为 route -n 输出的行数
  40 )
  41
  42 # 一般来说
  43 (
  44 : | x=x
  45 # 看上去是启动了一个子shell
  46 : | ( x=x )
  47 # 但
  48 x=x < <(:)
  49 # 实际上不是
  50 )
  51
  52 # 当解析csv或类似的东西时非常有用.
  53 # 事实上,这就是SuSE原本的代码片断所要实现的功能.
注意事项:
[1]  这与命名管道(named pipe)(临时文件)有相同的作用, 事实上命名管道同样在进程
  替换中被使用.

第23章 函数
============
和"真正的"编程语言一样, Bash也有函数,虽然在某些实现方面稍有些限制. 一个函数是一个
子程序,用于实现一串操作的代码块(code block),它是完成特定任务的"黑盒子". 当有重复
代码, 当一个任务只需要很少的修改就被重复几次执行时, 这时你应考虑使用函数.
function function_name {
command...
}
function_name () {
command...
}
第二种格式的写法更深得C程序员的喜欢(并且也是更可移植的).
因为在C中,函数的左花括号也可以写在下一行中.
function_name ()
{
command...
}
函数被调用或被触发, 只需要简单地用函数名调用.
Example 23-1 简单函数
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 JUST_A_SECOND=1
 4
 5 funky ()
 6 { # 这是一个最简单的函数.
 7   echo "This is a funky function."
 8   echo "Now exiting funky function."
 9 } # 函数必须在调用前声明.
10
11
12 fun ()
13 { # 一个稍复杂的函数.
14   i=0
15   REPEATS=30
16
17   echo
18   echo "And now the fun really begins."
19   echo
20
21   sleep $JUST_A_SECOND    # 嘿, 暂停一秒!
22   while [ $i -lt $REPEATS ]
23   do
24     echo "----------FUNCTIONS---------->"
25     echo "<------------ARE-------------"
26     echo "<------------FUN------------>"
27     echo
28     let "i+=1"
29   done
30 }
31
32   # 现在,调用两个函数.
33
34 funky
35 fun
36
37 exit 0
################################End Script#########################################
函数定义必须在第一次调用函数前完成.没有像C中的函数“声明”方法.
   1 f1
   2 # 因为函数"f1"还没有定义,这会引起错误信息.
   3
   4 declare -f f1      # 这样也没用.
   5 f1                 # 仍然会引起错误.
   6
   7 # 然而...
   8
   9   
  10 f1 ()
  11 {
  12   echo "Calling function \"f2\" from within function \"f1\"."
  13   f2
  14 }
  15
  16 f2 ()
  17 {
  18   echo "Function \"f2\"."
  19 }
  20
  21 f1  #  虽然在它定义前被引用过,
  22     #+ 函数"f2"实际到这儿才被调用.
  23     #  这样是允许的.
  24    
  25     # Thanks, S.C.
在一个函数内嵌套另一个函数也是可以的,但是不常用.
   1 f1 ()
   2 {
   3
   4   f2 () # nested
   5   {
   6     echo "Function \"f2\", inside \"f1\"."
   7   }
   8
   9 } 
  10
  11 f2  #  引起错误.
  12     #  就是你先"declare -f f2"了也没用.
  13
  14 echo   
  15
  16 f1  #  什么也不做,因为调用"f1"不会自动调用"f2".
  17 f2  #  现在,可以正确的调用"f2"了,
  18     #+ 因为之前调用"f1"使"f2"在脚本中变得可见了.
  19
  20     # Thanks, S.C.
函数声明可以出现在看上去不可能出现的地方,那些不可能的地方本该由一个命令出现的地方.
   1 ls -l | foo() { echo "foo"; }  # 允许,但没什么用.
   2
   3
   4
   5 if [ "$USER" = bozo ]
   6 then
   7   bozo_greet ()   # 在if/then结构中定义了函数.
   8   {
   9     echo "Hello, Bozo."
  10   }
  11 fi 
  12
  13 bozo_greet        # 只能由Bozo运行, 其他用户会引起错误.
  14
  15
  16
  17 # 在某些上下文,像这样可能会有用.
  18 NO_EXIT=1   # 将会打开下面的函数定义.
  19
  20 [[ $NO_EXIT -eq 1 ]] && exit() { true; }     # 在"and-list"(and列表)中定义函数.
  21 # 如果 $NO_EXIT 是 1,声明函数"exit ()".
  22 # 把"exit"取别名为"true"将会禁用内建的"exit".
  23
  24 exit  # 调用"exit ()"函数, 而不是内建的"exit".
  25
  26 # Thanks, S.C.

23.1. 复杂函数和函数复杂性
--------------------------
函数可以处理传递给它的参数并且能返回它的退出状态码(exit status)给脚本后续使用.
   1 function_name $arg1 $arg2
函数以位置来引用传递过来的参数(就好像他们是位置参数(positional parameters)), 例如
$1, $2,以此类推.
Example 23-2 带着参数的函数
################################Start Script#######################################
 1 #!/bin/bash
 2 # 函数和参数
 3
 4 DEFAULT=default                             # 默认的参数值.
 5
 6 func2 () {
 7    if [ -z "$1" ]                           # 第一个参数是否长度为零?
 8    then
 9      echo "-Parameter #1 is zero length.-"  # 则没有参数传递进来.
10    else
11      echo "-Param #1 is \"$1\".-"
12    fi
13
14    variable=${1-$DEFAULT}                   # 
15    echo "variable = $variable"              #  参数替换会表现出什么?
16                                             #  ---------------------------
17                                             #  它用于分辨没有参数和一个只有NULL值的参数.
18                                             #
19
20    if [ "$2" ]
21    then
22      echo "-Parameter #2 is \"$2\".-"
23    fi
24
25    return 0
26 }
27
28 echo
29   
30 echo "Nothing passed."  
31 func2                          # 没有参数来调用
32 echo
33
34
35 echo "Zero-length parameter passed."
36 func2 ""                       # 以一个长度为零的参数调用
37 echo
38
39 echo "Null parameter passed."
40 func2 "$uninitialized_param"   # 以未初始化的参数来调用
41 echo
42
43 echo "One parameter passed."  
44 func2 first           # 用一个参数来调用
45 echo
46
47 echo "Two parameters passed."  
48 func2 first second    # 以二个参数来调用
49 echo
50
51 echo "\"\" \"second\" passed."
52 func2 "" second       # 以第一个参数为零长度,而第二个参数是一个ASCII码组成的字符串来调用.
53 echo                  #
54
55 exit 0
################################End Script#########################################
注意:  shift命令可以工作在传递给函数的参数 (参考例子 33-15).
但是,传给脚本的命令行参数怎么办?在函数内部可以看到它们吗?好,让我们来弄清楚.
Example 23-3 函数和被传给脚本的命令行参数
################################Start Script#######################################
 1 #!/bin/bash
 2 # func-cmdlinearg.sh
 3 #  以一个命令行参数来调用这个脚本,
 4 #+ 类似 $0 arg1来调用.
 5
 6
 7 func ()
 8
 9 {
10 echo "$1"
11 }
12
13 echo "First call to function: no arg passed."
14 echo "See if command-line arg is seen."
15 func
16 # 不!命令行参数看不到.
17
18 echo "============================================================"
19 echo
20 echo "Second call to function: command-line arg passed explicitly."
21 func $1
22 # 现在可以看到了!
23
24 exit 0
################################End Script#########################################
与别的编程语言相比,shell脚本一般只传递值给函数,变量名(实现上是指针)如果作为参数传递给函数会被看成是字面上字符串的意思.函数解释参数是以字面上的意思来解释的.
间接变量引用(Indirect variable references) (参考例子 34-2)提供了传递变量指针给函数的一个笨拙的机制.
Example 23-4 传递间接引用给函数
################################Start Script#######################################
 1 #!/bin/bash
 2 # ind-func.sh: 传递间接引用给函数.
 3
 4 echo_var ()
 5 {
 6 echo "$1"
 7 }
 8
 9 message=Hello
10 Hello=Goodbye
11
12 echo_var "$message"        # Hello
13 # 现在,让我们传递一个间接引用给函数.
14 echo_var "${!message}"     # Goodbye
15
16 echo "-------------"
17
18 # 如果我们改变"hello"变量的值会发生什么?
19 Hello="Hello, again!"
20 echo_var "$message"        # Hello
21 echo_var "${!message}"     # Hello, again!
22
23 exit 0
################################End Script#########################################
下一个逻辑问题是:在传递参数给函数之后是否能解除参数的引用.
Example 23-5 解除传递给函数的参数引用
################################Start Script#######################################
 1 #!/bin/bash
 2 # dereference.sh
 3 # 给函数传递不同的参数.
 4 # Bruce W. Clare编写.
 5
 6 dereference ()
 7 {
 8      y=\$"$1"   # 变量名.
 9      echo $y    # $Junk
10
11      x=`eval "expr \"$y\" "`
12      echo $1=$x
13      eval "$1=\"Some Different Text \""  # 赋新值.
14 }
15
16 Junk="Some Text"
17 echo $Junk "before"    # Some Text before
18
19 dereference Junk
20 echo $Junk "after"     # Some Different Text after
21
22 exit 0
################################End Script#########################################
Example 23-6 再次尝试解除传递给函数的参数引用
################################Start Script#######################################
 1 #!/bin/bash
 2 # ref-params.sh: 解除传递给函数的参数引用.
 3 #                (复杂例子)
 4
 5 ITERATIONS=3  # 取得输入的次数.
 6 icount=1
 7
 8 my_read () {
 9   #  用my_read varname来调用,
10   #+ 输出用括号括起的先前的值作为默认值,
11   #+ 然后要求输入一个新值.
12
13   local local_var
14
15   echo -n "Enter a value "
16   eval 'echo -n "[$'$1'] "'  #  先前的值.
17 # eval echo -n "[\$$1] "     #  更好理解,
18                              #+ 但会丢失用户输入在尾部的空格.
19   read local_var
20   [ -n "$local_var" ] && eval $1=\$local_var
21
22   # "and列表(And-list)": 如果变量"local_var"测试成功则把变量"$1"的值赋给它.
23 }
24
25 echo
26
27 while [ "$icount" -le "$ITERATIONS" ]
28 do
29   my_read var
30   echo "Entry #$icount = $var"
31   let "icount += 1"
32   echo
33 done 
34
35
36 # 多谢Stephane Chazelas提供的示范例子.
37
38 exit 0
################################End Script#########################################
退出和返回
退出状态(exit status)
    函数返回一个被称为退出状态的值. 退出状态可以由return来指定statement, 否则函数的
 退出状态是函数最后一个执行命令的退出状态(0表示成功,非0表示出错代码). 退出状态
 (exit status)可以在脚本中由$? 引用. 这个机制使脚本函数也可以像C函数一样有一个"
 返回值".
return
    终止一个函数.return 命令[1]可选地带一个整数参数,这个整数作为函数的"返回值"返回
 给调用此函数的脚本,并且这个值也被赋给变量$?.
Example 23-7 两个数中的最大者
################################Start Script#######################################
 1 #!/bin/bash
 2 # max.sh: 两个整数中的最大者.
 3
 4 E_PARAM_ERR=-198    # 如果传给函数的参数少于2个时的返回值.
 5 EQUAL=-199          # 如果两个整数值相等的返回值.
 6 #  任一个传给函数的参数值溢出
 7 #
 8
 9 max2 ()             # 返回两个整数的较大值.
10 {                   # 注意: 参与比较的数必须小于257.
11 if [ -z "$2" ]
12 then
13   return $E_PARAM_ERR
14 fi
15
16 if [ "$1" -eq "$2" ]
17 then
18   return $EQUAL
19 else
20   if [ "$1" -gt "$2" ]
21   then
22     return $1
23   else
24     return $2
25   fi
26 fi
27 }
28
29 max2 33 34
30 return_val=$?
31
32 if [ "$return_val" -eq $E_PARAM_ERR ]
33 then
34   echo "Need to pass two parameters to the function."
35 elif [ "$return_val" -eq $EQUAL ]
36   then
37     echo "The two numbers are equal."
38 else
39     echo "The larger of the two numbers is $return_val."
40 fi 
41
42  
43 exit 0
44
45 #  练习 (容易):
46 #  ---------------
47 #  把这个脚本转化成交互式的脚本,
48 #+ 也就是说,让脚本可以要求调用者输入两个整数.
################################End Script#########################################
 注意: 为了函数可以返回字符串或是数组,用一个可在函数外可见的变量.
    1 count_lines_in_etc_passwd()
    2 {
    3   [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
    4   #  如果/etc/passwd可读,则把REPLY设置成文件的行数.
    5   #  返回一个参数值和状态信息.
    6   #  'echo'好像没有必要,但 . . .
    7   #+ 它的作用是删除输出中的多余空白字符.
    8 }
    9
   10 if count_lines_in_etc_passwd
   11 then
   12   echo "There are $REPLY lines in /etc/passwd."
   13 else
   14   echo "Cannot count lines in /etc/passwd."
   15 fi 
   16
   17 # Thanks, S.C.
Example 23-8 把数字转化成罗马数字
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 阿拉伯数字转化为罗马数字
 4 # 转化范围: 0 - 200
 5 # 这是比较粗糙的,但可以工作.
 6
 7 # 扩展可接受的范围来作为脚本功能的扩充,这个作为练习完成.
 8
 9 # 用法: roman number-to-convert
10
11 LIMIT=200
12 E_ARG_ERR=65
13 E_OUT_OF_RANGE=66
14
15 if [ -z "$1" ]
16 then
17   echo "Usage: `basename $0` number-to-convert"
18   exit $E_ARG_ERR
19 fi 
20
21 num=$1
22 if [ "$num" -gt $LIMIT ]
23 then
24   echo "Out of range!"
25   exit $E_OUT_OF_RANGE
26 fi 
27
28 to_roman ()   # 在第一次调用函数前必须先定义.
29 {
30 number=$1
31 factor=$2
32 rchar=$3
33 let "remainder = number - factor"
34 while [ "$remainder" -ge 0 ]
35 do
36   echo -n $rchar
37   let "number -= factor"
38   let "remainder = number - factor"
39 done 
40
41 return $number
42        # 练习:
43        # --------
44        # 解释这个函数是怎么工作的.
45        # 提示: 靠不断地除来分割数字.
46 }
47   
48
49 to_roman $num 100 C
50 num=$?
51 to_roman $num 90 LXXXX
52 num=$?
53 to_roman $num 50 L
54 num=$?
55 to_roman $num 40 XL
56 num=$?
57 to_roman $num 10 X
58 num=$?
59 to_roman $num 9 IX
60 num=$?
61 to_roman $num 5 V
62 num=$?
63 to_roman $num 4 IV
64 num=$?
65 to_roman $num 1 I
66
67 echo
68
69 exit 0
################################End Script#########################################
 请参考例子 10-28.
 注意: 函数最大可返回的正整数为255. return 命令与退出状态(exit status)的概念联
   系很紧密,而退出状态的值受此限制.幸运地是有多种(工作区workarounds)来对
   付这种要求函数返回大整数的情况.
Example 23-9 测试函数最大的返回值
################################Start Script#######################################
 1 #!/bin/bash
 2 # return-test.sh
 3
 4 # 一个函数最大可能返回的值是255.
 5
 6 return_test ()         # 无论传给函数什么都返回它.
 7 {
 8   return $1
 9 }
10
11 return_test 27         # o.k.
12 echo $?                # 返回 27.
13  
14 return_test 255        # 仍然 o.k.
15 echo $?                # 返回 255.
16
17 return_test 257        # 错误!
18 echo $?                # 返回 1 (返回代码指示错误).
19
20 # ======================================================
21 return_test -151896    # 能够返回这个非常大的负数么?
22 echo $?                # 会返回-151896?
23                        # 不! 它将返回168.
24 #  2.05b版本之前的Bash是允许
25 #+ 超大负整数作为返回值的.
26 #  但是比它更新一点的版本修正了这个漏洞.
27 #  这将破坏比较老的脚本.
28 #  慎用!
29 # ======================================================
30
31 exit 0
################################End Script#########################################
 如果你非常想使用超大整数作为"返回值"的话, 那么只能通过将你想返回的返回值直接的
 传递到一个全局变量中的手段来达到目的.
    1 Return_Val=   # 全局变量, 用来保存函数中需要返回的超大整数.
    2
    3 alt_return_test ()
    4 {
    5   fvar=$1
    6   Return_Val=$fvar
    7   return   # Returns 0 (success).
    8 }
    9
   10 alt_return_test 1
   11 echo $?                              # 0
   12 echo "return value = $Return_Val"    # 1
   13
   14 alt_return_test 256
   15 echo "return value = $Return_Val"    # 256
   16
   17 alt_return_test 257
   18 echo "return value = $Return_Val"    # 257
   19
   20 alt_return_test 25701
   21 echo "return value = $Return_Val"    #25701
 一种更优雅的方法是让函数echo出它的返回值, 输出到stdout上, 然后再通过"命令替换"
 的手段来捕获它. 参考Section 33.7关于这个问题的讨论.
Example 23-10 比较两个大整数
################################Start Script#######################################
 1 #!/bin/bash
 2 # max2.sh: 取两个超大整数中最大的.
 3
 4 #  这个脚本与前面的"max.sh"例子作用相同,
 5 #+ 经过修改可以适用于比较超大整数.
 6
 7 EQUAL=0             # 如果两个参数相同的返回值.
 8 E_PARAM_ERR=-99999  # 没有足够的参数传递到函数中.
 9 #           ^^^^^^    也可能是传递到函数中的某个参数超出范围了.
10
11 max2 ()             # 从这两个数中"返回"更大一些的.
12 {
13 if [ -z "$2" ]
14 then
15   echo $E_PARAM_ERR
16   return
17 fi
18
19 if [ "$1" -eq "$2" ]
20 then
21   echo $EQUAL
22   return
23 else
24   if [ "$1" -gt "$2" ]
25   then
26     retval=$1
27   else
28     retval=$2
29   fi
30 fi
31
32 echo $retval        # echo(到stdout), 而不是使用返回值.
33                     # 为什么?
34 }
35
36
37 return_val=$(max2 33001 33997)
38 #            ^^^^             函数名
39 #                 ^^^^^ ^^^^^ 这是传递进来的参数
40 #  这事实上是一个命令替换的形式:
41 #+ 会把这个函数当作一个命令来处理,
42 #+ 并且分配这个函数的stdout到变量"return_val"中.
43
44
45 # ========================= OUTPUT ========================
46 if [ "$return_val" -eq "$E_PARAM_ERR" ]
47   then
48   echo "Error in parameters passed to comparison function!"
49 elif [ "$return_val" -eq "$EQUAL" ]
50   then
51     echo "The two numbers are equal."
52 else
53     echo "The larger of the two numbers is $return_val."
54 fi
55 # =========================================================
56  
57 exit 0
58
59 #  练习:
60 #  -----
61 #  1) 找出一种更优雅的方法来测试
62 #+    传递到函数中的参数.
63 #  2) 在"OUTPUT"的时候简化if/then结构.
64 #  3) 重写这个脚本使其能够从命令行参数中来获取输入.
################################End Script#########################################
 下边是获得一个函数的"返回值"的另一个例子. 想要了解这个例子需要一些awk的知识.
    1 month_length ()  # 以月份数作为参数.
    2 {                # 返回这个月有几天.
    3 monthD="31 28 31 30 31 30 31 31 30 31 30 31"  # 作为局部变量来声明?
    4 echo "$monthD" | awk '{ print $'"${1}"' }'    # 有技巧的.
    5 #                             ^^^^^^^^^
    6 # 先将参数传递到函数中  ($1 -- 月份号), 然后就到awk了.
    7 # Awk将会根据传递进来的月份号来决定打印"print $1 . . . print $12"中的哪个 (依赖于月份号)
    8 # 传递参数到内嵌awk脚本的模版:
    9 #                                 $'"${script_parameter}"'
   10
   11 #  需要错误检查来修正参数的范围(1-12)
   12 #+ 并且要处理闰年的特殊的2月.
   13 }
   14
   15 # ----------------------------------------------
   16 # 用例:
   17 month=4        # 拿4月来举个例子.
   18 days_in=$(month_length $month)
   19 echo $days_in  # 30
   20 # ----------------------------------------------
 也参考例子 A-7.
 练习: 用我们已经学到的扩展先前罗马数字那个例子脚本能接受任意大的输入.
重定向
重定向函数的标准输入
 函数本质上是一个代码块(code block), 这样意思着它的标准输入可以被重定向
 (就像在例子 3-1中显示的).
Example 23-11 用户名的真实名
################################Start Script#######################################
 1 #!/bin/bash
 2 # realname.sh
 3 #
 4 # 由用户名而从/etc/passwd取得"真实名".
 5
 6
 7 ARGCOUNT=1       # 需要一个参数.
 8 E_WRONGARGS=65
 9
10 file=/etc/passwd
11 pattern=$1
12
13 if [ $# -ne "$ARGCOUNT" ]
14 then
15   echo "Usage: `basename $0` USERNAME"
16   exit $E_WRONGARGS
17 fi 
18
19 file_excerpt ()  # 以要求的模式来扫描文件,然后打印文件相关的部分.
20 {
21 while read line  # "while" does not necessarily need "[ condition ]"
22 do
23   echo "$line" | grep $1 | awk -F":" '{ print $5 }'  # awk指定使用":"为界定符.
24 done
25 } <$file  # 重定向函数的标准输入.
26
27 file_excerpt $pattern
28
29 # Yes, this entire script could be reduced to
30 #       grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
31 # or
32 #       awk -F: '/PATTERN/ {print $5}'
33 # or
34 #       awk -F: '($1 == "username") { print $5 }' # real name from username
35 # 但是,这些可能起不到示例的作用.
36
37 exit 0
################################End Script#########################################
 还有一个办法,可能是更好理解的重定向函数标准输入方法.它为函数内的一个括号内的
 代码块调用标准输入重定向.
   1 # 用下面的代替:
   2 Function ()
   3 {
   4  ...
   5  } < file
   6
   7 # 也试一下这个:
   8 Function ()
   9 {
  10   {
  11     ...
  12    } < file
  13 }
  14
  15 # 同样,
  16
  17 Function ()  # 可以工作.
  18 {
  19   {
  20    echo $*
  21   } | tr a b
  22 }
  23
  24 Function ()  # 这个不会工作
  25 {
  26   echo $*
  27 } | tr a b   # 这儿的内嵌代码块是强制的.
  28
  29
  30 # Thanks, S.C.
注意事项:
[1]  return命令是Bash内建(builtin)的.
23.2. 局部变量
--------------
怎么样使一个变量变成局部的?
局部变量
 如果变量用local来声明,那么它只能在该变量声明的代码块(block of code)中可见.
 这个代码块就是局部"范围". 在一个函数内,局部变量意味着只能在函数代码块内它才
 有意义.
Example 23-12 局部变量的可见范围
################################Start Script#######################################
 1 #!/bin/bash
 2 # 在函数内部的全局和局部变量.
 3
 4 func ()
 5 {
 6   local loc_var=23       # 声明为局部变量.
 7   echo                   # 使用内建的'local'关键字.
 8   echo "\"loc_var\" in function = $loc_var"
 9   global_var=999         # 没有声明为局部变量.
10                          # 默认为全局变量.
11   echo "\"global_var\" in function = $global_var"
12 } 
13
14 func
15
16 # 现在,来看看是否局部变量"loc_var"能否在函数外面可见.
17
18 echo
19 echo "\"loc_var\" outside function = $loc_var"
20                                       # $loc_var outside function =
21                                       # 不, $loc_var不是全局可访问的.
22 echo "\"global_var\" outside function = $global_var"
23                                       # $global_var outside function = 999
24                                       # $global_var 是全局可访问的.
25 echo         
26
27 exit 0
28 #  与In contrast to C相比, 在函数内声明的Bash变量只有在
29 #+ 它被明确声明成局部的变量时才是局部的.
################################End Script#########################################
 注意: 在函数调用之前,所有在函数内声明且没有明确声明为local的变量都可在函数体
   外可见.
    1 #!/bin/bash
    2
    3 func ()
    4 {
    5 global_var=37    #  在函数还没有被调用前
    6                  #+ 变量只在函数内可见.
    7 }                #  函数结束
    8
    9 echo "global_var = $global_var"  # global_var =
   10                                  #  函数"func"还没有被调用,
   11                                  #+ 所以变量$global_var还不能被访问.
   12
   13 func
   14 echo "global_var = $global_var"  # global_var = 37
   15                                  # 已经在函数调用时设置了值.
23.2.1. 局部变量使递归变得可能.
-------------------------------
局部变量可以递归, [1] 但这个办法会产生大量的计算,因此它在shell脚本中是被明确表明
不推荐的. [2]
Example 23-13 用局部变量来递归
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #               阶乘
 4 #               ---------
 5
 6
 7 # bash允许递归吗?
 8 # 嗯, 允许, 但是...
 9 # 它太慢以致你难以忍受.
10
11
12 MAX_ARG=5
13 E_WRONG_ARGS=65
14 E_RANGE_ERR=66
15
16
17 if [ -z "$1" ]
18 then
19   echo "Usage: `basename $0` number"
20   exit $E_WRONG_ARGS
21 fi
22
23 if [ "$1" -gt $MAX_ARG ]
24 then
25   echo "Out of range (5 is maximum)."
26   #  现在让我们来了解实际情况.
27   #  如果你想求比这个更大的范围的阶乘,
28   #+ 应该重新用一个真正的编程语言来写.
29   exit $E_RANGE_ERR
30 fi 
31
32 fact ()
33 {
34   local number=$1
35   #  变量"number"必须声明为局部,
36   #+ 否则它不会工作.
37   if [ "$number" -eq 0 ]
38   then
39     factorial=1    # 0的阶乘为1.
40   else
41     let "decrnum = number - 1"
42     fact $decrnum  # 递归调用(函数内部调用自己本身).
43     let "factorial = $number * $?"
44   fi
45
46   return $factorial
47 }
48
49 fact $1
50 echo "Factorial of $1 is $?."
51
52 exit 0
################################End Script#########################################
也请参考例子 A-16的脚本递归的例子. 必须意识到递归也意味着巨大的资源消耗和缓慢的运
行,因此它不适合在脚本中使用.
注意事项:
[1]  Herbert Mayer 给递归下的定义是". . . expressing an algorithm by using a
  simpler version of that same algorithm(用一个相同算法的版本来表示一个算法)
  . . ." 递归函数是调用它自己本身的函数.
[2]  太多层的递归可能会引起脚本段错误而崩溃.
     1 #!/bin/bash
     2
     3 #  警告: 运行这个脚本可能使你的系统失去响应!
     4 #  如果你运气不错,在它使用完所有可用内存之前会段错误而退出.
     5
     6 recursive_function ()    
     7 {
     8 echo "$1"     # 使函数做些事情以加速产生段错误.
     9 (( $1 < $2 )) && recursive_function $(( $1 + 1 )) $2;
    10 #  当第一个参数比第二个参数少时,
    11 #+ 把第1个参数增1再次递归.
    12 }
    13
    14 recursive_function 1 50000  # 递归 50,000 次!
    15 #  非常可能段错误 (依赖于栈的大小,它由ulimit -m设置).
    16
    17 #  这种深度的递归甚至可能由于耗尽栈的内存大小而引起C程序的段错误.
    18 #
    19
    20
    21 echo "This will probably not print."
    22 exit 0  # 这个脚本将不会从这儿正常退出.
    23
    24 #  多谢, St`phane Chazelas.
23.3. 不使用局部变量的递归
--------------------------
函数甚至可以不使用局部变量来调用自己.
Example 23-14 汉诺塔
################################Start Script#######################################
  1 #! /bin/bash
  2 #
  3 # 汉诺塔(The Towers Of Hanoi)
  4 # Bash script
  5 # Copyright (C) 2000 Amit Singh. All Rights Reserved.
  6 #
  7 #
  8 # 在bash version 2.05b.0(13)-release下测试通过
  9 #
 10 #  经过作者同意后在"Advanced Bash Scripting Guide"书中使用
 11 #
 12 #  由ABS的作者做了少许修改.
 13
 14 #=================================================================#
 15 #  汉诺塔是由Edouard Lucas提出的数学谜题 ,
 16 #+ 他是19世纪的法国数学家.
 17 #
 18 #  有三个直立的柱子竖在地面上.
 19 #  第一个柱子有一组的盘子套在上面.
 20 #  这些盘子是平整的,中间带着孔,
 21 #+ 因此它们才能套在柱子上面.
 22 #  这组盘子有不同的直径,它们是依照直径从小到大来从高到低放置.
 23 #
 24 #  最小的盘在最高,最大的盘在最底部.
 25 #
 26 #  现在的任务是要把这一组的盘子从一个柱子全部地搬到另一个柱子上.
 27 #
 28 #  你只能一次从一个柱子上移动一个盘子到另一个柱子.
 29 #  允许把盘子重新移回到它原来的最初位置.
 30 #  你可以把一个小的盘子放在大的盘子上面,
 31 #+ 但不能把大的盘子放在小的盘子上面.
 32 #  请注意这一点.
 33 #
 34 #  对于这一组盘子,数量少时,只需要移动很少的次数就能达到要求.
 35 #+ 但随着这组盘子的数量的增加,
 36 #+ 移动的次数几乎成倍增长的,
 37 #+ 而移动的策略变得愈加复杂.
 38 #
 39 #  想了解更多的信息, 请访问 .
 40 #
 41 #
 42 #         ...                   ...                    ...
 43 #         | |                   | |                    | |
 44 #        _|_|_                  | |                    | |
 45 #       |_____|                 | |                    | |
 46 #      |_______|                | |                    | |
 47 #     |_________|               | |                    | |
 48 #    |___________|              | |                    | |
 49 #   |             |             | |                    | |
 50 # .--------------------------------------------------------------.
 51 # |**************************************************************|
 52 #          #1                   #2                      #3
 53 #
 54 #=================================================================#
 55
 56
 57 E_NOPARAM=66  # 没有参数传给脚本.
 58 E_BADPARAM=67 # 传给脚本的盘子数不合法.
 59 Moves=        # 保存移动次数的全局变量.
 60               # 这儿修改了原脚本.
 61
 62 dohanoi() {   # 递归函数.
 63     case $1 in
 64     0)
 65         ;;
 66     *)
 67         dohanoi "$(($1-1))" $2 $4 $3
 68         echo move $2 "-->" $3
 69  let "Moves += 1"  # 这儿修改了原脚本.
 70         dohanoi "$(($1-1))" $4 $3 $2
 71         ;;
 72     esac
 73 }
 74
 75 case $# in
 76 1)
 77     case $(($1>0)) in     # 至少要有一个盘子.
 78     1)
 79         dohanoi $1 1 3 2
 80         echo "Total moves = $Moves"
 81         exit 0;
 82         ;;
 83     *)
 84         echo "$0: illegal value for number of disks";
 85         exit $E_BADPARAM;
 86         ;;
 87     esac
 88     ;;
 89 *)
 90     echo "usage: $0 N"
 91     echo "       Where \"N\" is the number of disks."
 92     exit $E_NOPARAM;
 93     ;;
 94 esac
 95
 96 # 练习:
 97 # ---------
 98 # 1) 从现在这个位置以下的命令会不会总是被执行?
 99 #    为什么? (容易)
100 # 2) 解释这个可运行的"dohanoi"函数的原理.
101 #    (难)
################################End Script#########################################

第24章 别名(Aliases)
=====================
Bash别名本质上是一个简称, 缩写, 这可避免键入过长的命令序列. 例如,如果我们添加
alias lm="ls -l | more" 这一行到文件~/.bashrc file里, 然后每次在命令行键入lm 将会
自动被替换成ls -l | more. 这使用户在命令行不必键冗长的命令序列也避免了记忆复杂的命
令及众多选项. 设置alias rm="rm -i" (交互式删除)可以使你犯下错误时不必过度悲伤,它
能避免你不小心删除重要文件.
在脚本里,别名机制不是非常的有用. 如果把别名机制想像成C预处理器的某些功能将会非常
好,比如宏扩展,但是,不幸的是Bash不能在别名中扩展参数. [1] 而且,别名不能在“混合
型的结构”中使用,比如if/then语句, 循环, 和函数. 还有一个限制是别名不能递归地扩展.
大多数情况Almost invariably, 我们想让别名完成的工作都能被函数更高效地完成.
Example 24-1 脚本中的别名
################################Start Script#######################################
 1 #!/bin/bash
 2 # alias.sh
 3
 4 shopt -s expand_aliases
 5 # 必须设置这个选项,否则脚本不会扩展别名功能.
 6
 7
 8 # 首先, 来点有趣的.
 9 alias Jesse_James='echo "\"Alias Jesse James\" was a 1959 comedy starring Bob Hope."'
10 Jesse_James
11
12 echo; echo; echo;
13
14 alias ll="ls -l"
15 # 可以使用单引号(')或双引号(")来定义一个别名.
16
17 echo "Trying aliased \"ll\":"
18 ll /usr/X11R6/bin/mk*   #* 别名工作了.
19
20 echo
21
22 directory=/usr/X11R6/bin/
23 prefix=mk*  # 看通配符会不会引起麻烦.
24 echo "Variables \"directory\" + \"prefix\" = $directory$prefix"
25 echo
26
27 alias lll="ls -l $directory$prefix"
28
29 echo "Trying aliased \"lll\":"
30 lll         # 详细列出在/usr/X11R6/bin目录下所有以mk开头的文件.
31 # 别名能处理连接变量 -- 包括通配符 -- o.k.
32
33
34
35
36 TRUE=1
37
38 echo
39
40 if [ TRUE ]
41 then
42   alias rr="ls -l"
43   echo "Trying aliased \"rr\" within if/then statement:"
44   rr /usr/X11R6/bin/mk*   #* 引起错误信息!
45   # 别名不能在混合结构中使用.
46   echo "However, previously expanded alias still recognized:"
47   ll /usr/X11R6/bin/mk*
48 fi 
49
50 echo
51
52 count=0
53 while [ $count -lt 3 ]
54 do
55   alias rrr="ls -l"
56   echo "Trying aliased \"rrr\" within \"while\" loop:"
57   rrr /usr/X11R6/bin/mk*   #* 在这儿,别名也不会扩展.
58                            #  alias.sh: line 57: rrr: command not found
59   let count+=1
60 done
61
62 echo; echo
63
64 alias xyz='cat $0'   # 脚本打印自身内容.
65                      # 注意是单引号(强引用).
66 xyz
67 #  虽然Bash的文档它是不会工作的,但好像它是可以工作的.
68 #
69 #
70 #  然而,就像 Steve Jacobson指出,
71 #+ 参数"$0"立即扩展成了这个别名的声明.
72
73 exit 0
################################End Script#########################################
unalias 命令删除先前设置的别名.
Example 24-2 unalias: 设置和删除别名
################################Start Script#######################################
 1 #!/bin/bash
 2 # unalias.sh
 3
 4 shopt -s expand_aliases  # 打开别名功能扩展.
 5
 6 alias llm='ls -al | more'
 7 llm
 8
 9 echo
10
11 unalias llm              # 删除别名.
12 llm
13 # 引起错误信息,因为'llm'已经不再有效了.
14
15 exit 0
################################End Script#########################################
 bash$ ./unalias.sh
 total 6
drwxrwxr-x    2 bozo     bozo         3072 Feb  6 14:04 .
drwxr-xr-x   40 bozo     bozo         2048 Feb  6 14:04 ..
-rwxr-xr-x    1 bozo     bozo          199 Feb  6 14:04 unalias.sh
./unalias.sh: llm: command not found
注意事项:
[1]  但是, 别名好像能扩展位置参数.

第25章 列表结构
================
"与列表(and list)"和"或列表(or list)" 结构提供一种处理一串连续命令的方法. 它们能有
效地替代复杂的嵌套if/then语句甚至可以代替case语句.
连接命令
与列表(and list)
    1 command-1 && command-2 && command-3 && ... command-n
 如果每个命令都返回真值(0)将会依次执行下去. 当某个命令返回假值(非零值), 整个命
 令链就会结束执行(第一个返回假的命令将会是最后一个执行的命令,后面的都不再执行).
Example 25-1 使用"与列表(and list)"来测试命令行参数
################################Start Script#######################################
 1 #!/bin/bash
 2 # "and list"
 3
 4 if [ ! -z "$1" ] && echo "Argument #1 = $1" && [ ! -z "$2" ] && echo "Argument #2 = $2"
 5 then
 6   echo "At least 2 arguments passed to script."
 7   # 所有连接起来的命令都返回真.
 8 else
 9   echo "Less than 2 arguments passed to script."
10   # 整个命令列表中至少有一个命令返回假值.
11 fi 
12 # 注意"if [ ! -z $1 ]" 可以工作,但它是有所假定的等价物,
13 #   if [ -n $1 ] 不会工作.
14 #     但是, 加引用可以让它工作.
15 #  if [ -n "$1" ] 就可以了.
16 #     小心!
17 # 最好总是引起要测试的变量.
18
19
20 # 这是使用"纯粹"的 if/then 语句完成的同等功能.
21 if [ ! -z "$1" ]
22 then
23   echo "Argument #1 = $1"
24 fi
25 if [ ! -z "$2" ]
26 then
27   echo "Argument #2 = $2"
28   echo "At least 2 arguments passed to script."
29 else
30   echo "Less than 2 arguments passed to script."
31 fi
32 # 这会更长且不如"与列表"精致.
33
34
35 exit 0
################################End Script#########################################
Example 25-2 用"与列表"的另一个命令行参数测试
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 ARGS=1        # 期望的参数个数.
 4 E_BADARGS=65  # 如果用户给出不正确的参数个数的退出码.
 5
 6 test $# -ne $ARGS && echo "Usage: `basename $0` $ARGS argument(s)" && exit $E_BADARGS
 7 #  如果 条件1 测试为真(表示传给脚本的参数不对),
 8 #+ 则余下的命令会被执行,并且脚本结束运行.
 9
10 # 下面的代码只有当上面的测试失败时才会执行.
11 echo "Correct number of arguments passed to this script."
12
13 exit 0
14
15 # 为了检查退出码,脚本结束后用"echo $?"来查看退出码.
################################End Script#########################################
 当然,一个与列表也能给变量设置默认值.
    1 arg1=$@       # 不管怎样,设置变量$arg1为命令行参数.
    2
    3 [ -z "$arg1" ] && arg1=DEFAULT
    4               # 如果没有在命令行上指定参数则把$arg1设置为DEFAULT.
或列表(or list)
    1 command-1 || command-2 || command-3 || ... command-n
 只要前一个命令返回假命令链就会依次执行下去. 一旦有一个命令返回真, 命令链就会结
 束(第一个返回真的命令将会是最后一个执行的命令). 这显然和"与列表"正好相反.
Example 25-3 "或列表"和"与列表"的结合使用
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  delete.sh, 不是很聪明的文件删除功能.
 4 #  用法: delete filename
 5
 6 E_BADARGS=65
 7
 8 if [ -z "$1" ]
 9 then
10   echo "Usage: `basename $0` filename"
11   exit $E_BADARGS  # 没有参数? 跳出脚本.
12 else 
13   file=$1          # 设置文件名.
14 fi 
15
16
17 [ ! -f "$file" ] && echo "File \"$file\" not found. \
18 Cowardly refusing to delete a nonexistent file."
19 # 与列表, 用于文件不存在时给出一个错误信息.
20 # 注意 echo 命令的参数用了一个转义符继续使第二行也是这个命令的参数.
21
22 [ ! -f "$file" ] || (rm -f $file; echo "File \"$file\" deleted.")
23 # 或列表, 用于存在文件时删除此文件.
24
25 # 注意上面两个相反的逻辑.
26 # 与列表为真时才执行, 或列表为假时执行.
27
28 exit 0
################################End Script#########################################
 注意: 如果在与列表的第一个命令返回真时,它会执行.
   1 # ==> 下面的片断摘自Miquel van Smoorenburg写的 /etc/rc.d/init.d/single 脚本
   2 #+==> 示例与和或列表的使用.
   3 # ==> "箭头"的注释由本书作者添加.
   4
   5 [ -x /usr/bin/clear ] && /usr/bin/clear
   6   # ==> 如果 /usr/bin/clear 存在, 则调用它.
   7   # ==> 在调用一个命令前检查它是否存在,
   8   #+==> 以避免产生错误信息和其他难读懂的结果.
   9
  10   # ==> . . .
  11
  12 # 如果他们想在单用户模式下运行某些程序, 可能也会运行这个...
  13 for i in /etc/rc1.d/S[0-9][0-9]* ; do
  14         # 检查脚本是否可执行.
  15         [ -x "$i" ] || continue
  16   # ==> 如果在目录$PWD中相应的文件没有发现,
  17   #+==> 则会跳过此次循环.
  18
  19         # 不接受备份文件和由rpm产生的文件.
  20         case "$1" in
  21                 *.rpmsave|*.rpmorig|*.rpmnew|*~|*.orig)
  22                         continue;;
  23         esac
  24         [ "$i" = "/etc/rc1.d/S00single" ] && continue
  25   # ==> 设置脚本名,但还不执行它.
  26         $i start
  27 done
  28
  29   # ==> . . .
注意: 与列表或是或列表的退出状态是最后一个执行命令的退出状态.
灵活地组合"与"和"或"列表是允许的,但这样逻辑会很容易变得费解并且需要较多的测试.
   1 false && true || echo false         # false
   2
   3 # 结果等同
   4 ( false && true ) || echo false     # false
   5 # 但不同与
   6 false && ( true || echo false )     # (没有输出)
   7
   8 #  注意是从左到右来分组并求值的,
   9 #+ 因为逻辑操作符"&&"和"||"有相同的优先处理权.
  10
  11 #  最好避免这种复杂,除非你确实知道你在做什么.
  12
  13 #  Thanks, S.C.
参考例子 A-7和例子 7-4 演示的使用与/或列表测试变量的例子.

第26章 数组
============
较新的Bash版本支持一维数组. 数组元素可以用符号variable[xx]来初始化. 另外,脚本可以
用declare -a variable语句来清楚地指定一个数组. 要访问一个数组元素,可以使用花括号
来访问,即${variable[xx]}.
Example 26-1 简单的数组用法
################################Start Script#######################################
 1 #!/bin/bash
 2
 3
 4 area[11]=23
 5 area[13]=37
 6 area[51]=UFOs
 7
 8 #  数组成员不必一定要连贯或连续的.
 9
10 #  数组的一部分成员允许不被初始化.
11 #  数组中空缺元素是允许的.
12 #  实际上,保存着稀疏数据的数组(“稀疏数组”)在电子表格处理软件中非常有用.
13 #
14
15
16 echo -n "area[11] = "
17 echo ${area[11]}    #  {大括号}是需要的.
18
19 echo -n "area[13] = "
20 echo ${area[13]}
21
22 echo "Contents of area[51] are ${area[51]}."
23
24 # 没有初始化内容的数组元素打印空值(NULL值).
25 echo -n "area[43] = "
26 echo ${area[43]}
27 echo "(area[43] unassigned)"
28
29 echo
30
31 # 两个数组元素的和被赋值给另一个数组元素
32 area[5]=`expr ${area[11]} + ${area[13]}`
33 echo "area[5] = area[11] + area[13]"
34 echo -n "area[5] = "
35 echo ${area[5]}
36
37 area[6]=`expr ${area[11]} + ${area[51]}`
38 echo "area[6] = area[11] + area[51]"
39 echo -n "area[6] = "
40 echo ${area[6]}
41 # 这里会失败是因为整数和字符串相加是不允许的.
42
43 echo; echo; echo
44
45 # -----------------------------------------------------------------
46 # 另一个数组, "area2".
47 # 另一种指定数组元素的值的办法...
48 # array_name=( XXX YYY ZZZ ... )
49
50 area2=( zero one two three four )
51
52 echo -n "area2[0] = "
53 echo ${area2[0]}
54 # 啊哈, 从0开始计数(即数组的第一个元素是[0], 而不是 [1]).
55
56 echo -n "area2[1] = "
57 echo ${area2[1]}    # [1] 是数组的第二个元素.
58 # -----------------------------------------------------------------
59
60 echo; echo; echo
61
62 # -----------------------------------------------
63 # 第三种数组, "area3".
64 # 第三种指定数组元素值的办法...
65 # array_name=([xx]=XXX [yy]=YYY ...)
66
67 area3=([17]=seventeen [24]=twenty-four)
68
69 echo -n "area3[17] = "
70 echo ${area3[17]}
71
72 echo -n "area3[24] = "
73 echo ${area3[24]}
74 # -----------------------------------------------
75
76 exit 0
################################End Script#########################################
注意: Bash 允许把变量当成数组来操作,即使这个变量没有明确地被声明为数组.
     1 string=abcABC123ABCabc
     2 echo ${string[@]}               # abcABC123ABCabc
     3 echo ${string[*]}               # abcABC123ABCabc
     4 echo ${string[0]}               # abcABC123ABCabc
     5 echo ${string[1]}               # 没有输出!
     6                                 # 为什么?
     7 echo ${#string[@]}              # 1
     8                                 # 数组中只有一个元素.
     9                                 # 且是这个字符串本身.
    10
    11 # Thank you, Michael Zick, for pointing this out.
  类似的示范请参考Bash variables are untyped.
Example 26-2 格式化一首诗
################################Start Script#######################################
 1 #!/bin/bash
 2 # poem.sh: 排印出作者喜欢的一首诗.
 3
 4 # 诗的行数 (一小节诗).
 5 Line[1]="I do not know which to prefer,"
 6 Line[2]="The beauty of inflections"
 7 Line[3]="Or the beauty of innuendoes,"
 8 Line[4]="The blackbird whistling"
 9 Line[5]="Or just after."
10
11 # 出处.
12 Attrib[1]=" Wallace Stevens"
13 Attrib[2]="\"Thirteen Ways of Looking at a Blackbird\""
14 # 此诗是公众的 (版权期已经到期了).
15
16 echo
17
18 for index in 1 2 3 4 5    # 5行.
19 do
20   printf "     %s\n" "${Line[index]}"
21 done
22
23 for index in 1 2          # 打印两行出处行.
24 do
25   printf "          %s\n" "${Attrib[index]}"
26 done
27
28 echo
29
30 exit 0
31
32 # 练习:
33 # --------
34 # 修改这个脚本使其从一个文本文件中提取内容打印一首行.
################################End Script#########################################
数组元素有它们独有的语法, 并且甚至Bash命令和操作符有特殊的选项可以支持数组使用.
Example 26-3 多种数组操作
################################Start Script#######################################
 1 #!/bin/bash
 2 # array-ops.sh: 数组更多有趣的用法.
 3
 4
 5 array=( zero one two three four five )
 6 # 元素    0   1   2    3     4    5
 7
 8 echo ${array[0]}       #  zero
 9 echo ${array:0}        #  zero
10                        #  第一个元素的参数扩展,
11                        #+ 从位置0开始 (即第一个字符).
12 echo ${array:1}        #  ero
13                        #  第一个元素的参数扩展,
14                        #+ 从位置1开始 (即第二个字符).
15
16 echo "--------------"
17
18 echo ${#array[0]}      #  4
19                        #  数组第一个元素的长度.
20 echo ${#array}         #  4
21                        #  数组第一个元素的长度.
22                        #  (另一种写法)
23
24 echo ${#array[1]}      #  3
25                        #  数组第二个元素的长度.
26                        #  Bash的数组是0开始索引的.
27
28 echo ${#array[*]}      #  6
29                        #  数组中元素的个数.
30 echo ${#array[@]}      #  6
31                        #  数组中元素的个数.
32
33 echo "--------------"
34
35 array2=( [0]="first element" [1]="second element" [3]="fourth element" )
36
37 echo ${array2[0]}      # 第一个元素
38 echo ${array2[1]}      # 第二个元素
39 echo ${array2[2]}      #
40                        # 因为初始化时没有指定,因此值为空(null).
41 echo ${array2[3]}      # 第四个元素
42
43
44 exit 0
################################End Script#########################################
大部分标准的字符串操作符 可以用于数组操作.
Example 26-4 用于数组的字符串操作符
################################Start Script#######################################
  1 #!/bin/bash
  2 # array-strops.sh: 用于数组的字符串操作符.
  3 # 由Michael Zick编码.
  4 # 已征得作者的同意.
  5
  6 #  一般来说,任何类似 ${name ... } 写法的字符串操作符
  7 #+ 都能在一个数组的所有字符串元素中使用
  8 #+ 像${name[@] ... } 或 ${name[*] ...} 的写法.
  9
 10
 11 arrayZ=( one two three four five five )
 12
 13 echo
 14
 15 # 提取尾部的子串
 16 echo ${arrayZ[@]:0}     # one two three four five five
 17                         # 所有的元素.
 18
 19 echo ${arrayZ[@]:1}     # two three four five five
 20                         # 在第一个元素 element[0]后面的所有元素.
 21
 22 echo ${arrayZ[@]:1:2}   # two three
 23                         # 只提取在元素 element[0]后面的两个元素.
 24
 25 echo "-----------------------"
 26
 27 #  子串删除
 28 #  从字符串的前部删除最短的匹配,
 29 #+ 匹配字串是一个正则表达式.
 30
 31 echo ${arrayZ[@]#f*r}   # one two three five five
 32                         # 匹配表达式作用于数组所有元素.
 33                         # 匹配了"four"并把它删除.
 34
 35 # 字符串前部最长的匹配
 36 echo ${arrayZ[@]##t*e}  # one two four five five
 37                         # 匹配表达式作用于数组所有元素.
 38                         # 匹配"three"并把它删除.
 39
 40 # 字符串尾部的最短匹配
 41 echo ${arrayZ[@]%h*e}   # one two t four five five
 42                         # 匹配表达式作用于数组所有元素.
 43                         # 匹配"hree"并把它删除.
 44
 45 # 字符串尾部的最长匹配
 46 echo ${arrayZ[@]%%t*e}  # one two four five five
 47                         # 匹配表达式作用于数组所有元素.
 48                         # 匹配"three"并把它删除.
 49
 50 echo "-----------------------"
 51
 52 # 子串替换
 53
 54 # 第一个匹配的子串会被替换
 55 echo ${arrayZ[@]/fiv/XYZ}   # one two three four XYZe XYZe
 56                             # 匹配表达式作用于数组所有元素.
 57
 58 # 所有匹配的子串会被替换
 59 echo ${arrayZ[@]//iv/YY}    # one two three four fYYe fYYe
 60                             # 匹配表达式作用于数组所有元素.
 61
 62 # 删除所有的匹配子串
 63 # 没有指定代替字串意味着删除
 64 echo ${arrayZ[@]//fi/}      # one two three four ve ve
 65                             # 匹配表达式作用于数组所有元素.
 66
 67 # 替换最前部出现的字串
 68 echo ${arrayZ[@]/#fi/XY}    # one two three four XYve XYve
 69                             # 匹配表达式作用于数组所有元素.
 70
 71 # 替换最后部出现的字串
 72 echo ${arrayZ[@]/%ve/ZZ}    # one two three four fiZZ fiZZ
 73                             # 匹配表达式作用于数组所有元素.
 74
 75 echo ${arrayZ[@]/%o/XX}     # one twXX three four five five
 76                             # 为什么?
 77
 78 echo "-----------------------"
 79
 80
 81 # 在从awk(或其他的工具)取得数据之前 --
 82 # 记得:
 83 #   $( ... ) 是命令替换.
 84 #   函数以子进程运行.
 85 #   函数将输出打印到标准输出.
 86 #   用read来读取函数的标准输出.
 87 #   name[@]的写法指定了一个"for-each"的操作.
 88
 89 newstr() {
 90     echo -n "!!!"
 91 }
 92
 93 echo ${arrayZ[@]/%e/$(newstr)}
 94 # on!!! two thre!!! four fiv!!! fiv!!!
 95 # Q.E.D: 替换部分的动作实际上是一个'赋值'.
 96
 97 #  使用"For-Each"型的
 98 echo ${arrayZ[@]//*/$(newstr optional_arguments)}
 99 #  现在Now, 如果if Bash只传递匹配$0的字符串给要调用的函数. . .
100 #
101
102 echo
103
104 exit 0
################################End Script#########################################
命令替换能创建数组的新的单个元素.
Example 26-5 将脚本的内容传给数组
################################Start Script#######################################
 1 #!/bin/bash
 2 # script-array.sh: 把此脚本的内容传进数组.
 3 # 从Chris Martin的e-mail中得到灵感 (多谢!).
 4
 5 script_contents=( $(cat "$0") )  #  把这个脚本($0)的内容存进数组.
 6                                  #
 7
 8 for element in $(seq 0 $((${#script_contents[@]} - 1)))
 9   do                #  ${#script_contents[@]}
10                     #+ 表示数组中元素的个数.
11                     #
12                     #  问题:
13                     #  为什么需要  seq 0  ?
14                     #  试试更改成 seq 1.
15   echo -n "${script_contents[$element]}"
16                     # 将脚本的每行列成一个域.
17   echo -n " -- "    # 使用" -- "作为域分隔符.
18 done
19
20 echo
21
22 exit 0
23
24 # 练习:
25 # --------
26 #  修改这个脚本使它能按照它原本的格式输出,
27 #+ 连同空白符,换行,等等.
28 #
################################End Script#########################################
在数组的环境里, 一些 Bash 内建的命令 含义有一些轻微的改变. 例如, unset 会删除数组
元素, 或甚至删除整个数组.
Example 26-6 一些数组专用的工具
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 declare -a colors
 4 #  所有脚本后面的命令都会把
 5 #+ 变量"colors"作为数组对待.
 6
 7 echo "Enter your favorite colors (separated from each other by a space)."
 8
 9 read -a colors    # 键入至少3种颜色以用于下面的示例.
10 #  指定'read'命令的选项,
11 #+ 允许指定数组元素.
12
13 echo
14
15 element_count=${#colors[@]}
16 # 专用语法来提取数组元素的个数.
17 #     element_count=${#colors[*]} 也可以.
18 #
19 #  "@"变量允许分割引号内的单词
20 #+ (依靠空白字符来分隔变量).
21 #
22 #  这就像"$@" 和"$*"在位置参数中表现出来的一样.
23 #
24
25 index=0
26
27 while [ "$index" -lt "$element_count" ]
28 do    # List all the elements in the array.
29   echo ${colors[$index]}
30   let "index = $index + 1"
31 done
32 # 每个数组元素被列为单独的一行.
33 # 如果这个没有要求, 可以用  echo -n "${colors[$index]} "
34 #
35 # 可以用一个"for"循环来做:
36 #   for i in "${colors[@]}"
37 #   do
38 #     echo "$i"
39 #   done
40 # (Thanks, S.C.)
41
42 echo
43
44 # 再次列出数组中所有的元素, 但使用更优雅的做法.
45   echo ${colors[@]}          # echo ${colors[*]} 也可以.
46
47 echo
48
49 # "unset"命令删除一个数组元素或是整个数组.
50 unset colors[1]              # 删除数组的第二个元素.
51                              # 作用等同于   colors[1]=
52 echo  ${colors[@]}           # 再列出数组,第二个元素没有了.
53
54 unset colors                 # 删除整个数组.
55                              #  unset colors[*] 或
56                              #+ unset colors[@] 都可以.
57 echo; echo -n "Colors gone."     
58 echo ${colors[@]}            # 再列出数组, 则为空了.
59
60 exit 0
################################End Script#########################################
正如在前面的例子中看到的, ${array_name[@]}和${array_name[*]} 都与数组的所有元素相
关. 同样地, 为了计算数组的元素个数, 可以用${#array_name[@]} 或${#array_name[*]}.
${#array_name} 是数组第一个元素${array_name[0]}的长度(字符数) .
Example 26-7 关于空数组和空数组元素
################################Start Script#######################################
  1 #!/bin/bash
  2 # empty-array.sh
  3
  4 #  多谢 Stephane Chazelas 制作这个例子最初的版本,
  5 #+ 并由 Michael Zick 扩展了.
  6
  7
  8 # 空数组不同与含有空值元素的数组.
  9
 10 array0=( first second third )
 11 array1=( '' )   # "array1" 由一个空元素组成.
 12 array2=( )      # 没有元素 . . . "array2" 是空的.
 13
 14 echo
 15 ListArray()
 16 {
 17 echo
 18 echo "Elements in array0:  ${array0[@]}"
 19 echo "Elements in array1:  ${array1[@]}"
 20 echo "Elements in array2:  ${array2[@]}"
 21 echo
 22 echo "Length of first element in array0 = ${#array0}"
 23 echo "Length of first element in array1 = ${#array1}"
 24 echo "Length of first element in array2 = ${#array2}"
 25 echo
 26 echo "Number of elements in array0 = ${#array0[*]}"  # 3
 27 echo "Number of elements in array1 = ${#array1[*]}"  # 1  (惊奇!)
 28 echo "Number of elements in array2 = ${#array2[*]}"  # 0
 29 }
 30
 31 # ===================================================================
 32
 33 ListArray
 34
 35 # 尝试扩展这些数组.
 36
 37 # 增加一个元素到数组.
 38 array0=( "${array0[@]}" "new1" )
 39 array1=( "${array1[@]}" "new1" )
 40 array2=( "${array2[@]}" "new1" )
 41
 42 ListArray
 43
 44 # 或
 45 array0[${#array0[*]}]="new2"
 46 array1[${#array1[*]}]="new2"
 47 array2[${#array2[*]}]="new2"
 48
 49 ListArray
 50
 51 # 当像上面的做法增加数组时,数组像 '栈'
 52 # 上面的做法是 'push(压栈)'
 53 # 栈高是:
 54 height=${#array2[@]}
 55 echo
 56 echo "Stack height for array2 = $height"
 57
 58 # 'pop(出栈)' 是:
 59 unset array2[${#array2[@]}-1]   # 数组是以0开始索引的,
 60 height=${#array2[@]}            #+ 这就意味着第一个元素下标是 0.
 61 echo
 62 echo "POP"
 63 echo "New stack height for array2 = $height"
 64
 65 ListArray
 66
 67 # 只列出数组array0的第二和第三个元素.
 68 from=1   #是以0开始的数字
 69 to=2  #
 70 array3=( ${array0[@]:1:2} )
 71 echo
 72 echo "Elements in array3:  ${array3[@]}"
 73
 74 # 像一个字符串一样处理(字符的数组).
 75 # 试试其他的字符串格式.
 76
 77 # 替换:
 78 array4=( ${array0[@]/second/2nd} )
 79 echo
 80 echo "Elements in array4:  ${array4[@]}"
 81
 82 # 替换所有匹配通配符的字符串.
 83 array5=( ${array0[@]//new?/old} )
 84 echo
 85 echo "Elements in array5:  ${array5[@]}"
 86
 87 # 当你开始觉得对此有把握的时候 . . .
 88 array6=( ${array0[@]#*new} )
 89 echo # 这个可能会使你感到惊奇.
 90 echo "Elements in array6:  ${array6[@]}"
 91
 92 array7=( ${array0[@]#new1} )
 93 echo # 数组array6之后就没有惊奇了.
 94 echo "Elements in array7:  ${array7[@]}"
 95
 96 # 这看起来非常像 . . .
 97 array8=( ${array0[@]/new1/} )
 98 echo
 99 echo "Elements in array8:  ${array8[@]}"
100
101 #  那么我们怎么总结它呢So what can one say about this?
102
103 #  字符串操作在数组var[@]的每一个元素中执行.
104 #
105 #  因此Therefore : 如果结果是一个零长度的字符串,
106 #+ Bash支持字符串向量操作,
107 #+ 元素会在结果赋值中消失不见.
108
109 #  提问, 这些字符串是强还是弱引用?
110
111 zap='new*'
112 array9=( ${array0[@]/$zap/} )
113 echo
114 echo "Elements in array9:  ${array9[@]}"
115
116 # 当你还在想你在Kansas州的何处时 . . .
117 array10=( ${array0[@]#$zap} )
118 echo
119 echo "Elements in array10:  ${array10[@]}"
120
121 # 把 array7 和 array10比较.
122 # 把 array8 和 array9比较.
123
124 # 答案: 必须用弱引用.
125
126 exit 0
################################End Script#########################################
${array_name[@]}和${array_name[*]} 的关系类似于$@ and $*. 这种数组用法非常有用.
   1 # 复制一个数组.
   2 array2=( "${array1[@]}" )
   3 # 或
   4 array2="${array1[@]}"
   5
   6 # 给数组增加一个元素.
   7 array=( "${array[@]}" "new element" )
   8 # 或
   9 array[${#array[*]}]="new element"
  10
  11 # Thanks, S.C.
注意: array=( element1 element2 ... elementN ) 初始化操作, 依赖于命令替换
  (command substitution)使将一个文本内容加载进数组成为可能.
   1 #!/bin/bash
   2
   3 filename=sample_file
   4
   5 #            cat sample_file
   6 #
   7 #            1 a b c
   8 #            2 d e fg
   9
  10
  11 declare -a array1
  12
  13 array1=( `cat "$filename"`)                #  加载$filename文件的内容进数组array1.
  14 #         打印文件到标准输出               #
  15 #
  16 #  array1=( `cat "$filename" | tr '\n' ' '`)
  17 #                            把文件里的换行变为空格.
  18 #  这是没必要的,因为Bash做单词分割时会把换行变为空格.
  19 #
  20
  21 echo ${array1[@]}            # 打印数组.
  22 #                              1 a b c 2 d e fg
  23 #
  24 #  文件中每个由空白符分隔开的“词”都被存在数组的一个元素里
  25 #
  26
  27 element_count=${#array1[*]}
  28 echo $element_count          # 8
出色的技巧使数组的操作技术又多了一种.
Example 26-8 初始化数组
################################Start Script#######################################
 1 #! /bin/bash
 2 # array-assign.bash
 3
 4 #  数组操作是Bash特有的,
 5 #+ 因此脚本名用".bash"结尾.
 6
 7 # Copyright (c) Michael S. Zick, 2003, All rights reserved.
 8 # 许可证: 没有任何限制,可以用于任何目的的反复使用.
 9 # Version: $ID$
10 #
11 # 由William Park添加注释.
12
13 #  基于Stephane Chazelas提供在本书中的一个例子
14 #
15
16 # 'times' 命令的输出格式:
17 # User CPU <空格> System CPU
18 # User CPU of dead children <空格> System CPU of dead children
19
20 #  Bash赋一个数组的所有元素给新的数组变量有两种办法.
21 #
22 #  在Bash版本2.04, 2.05a 和 2.05b,
23 #+ 这两种办法都对NULL的值的元素全部丢弃.
24 #  另一种数组赋值办法是维护[下标]=值之间的关系将会在新版本的Bash支持.
25 #
26
27 #  可以用外部命令来构造一个大数组,
28 #+ 但几千个元素的数组如下就可以构造了.
29 #
30
31 declare -a bigOne=( /dev/* )
32 echo
33 echo 'Conditions: Unquoted, default IFS, All-Elements-Of'
34 echo "Number of elements in array is ${#bigOne[@]}"
35
36 # set -vx
37
38
39
40 echo
41 echo '- - testing: =( ${array[@]} ) - -'
42 times
43 declare -a bigTwo=( ${bigOne[@]} )
44 #                 ^              ^
45 times
46
47 echo
48 echo '- - testing: =${array[@]} - -'
49 times
50 declare -a bigThree=${bigOne[@]}
51 # 这次没有用括号.
52 times
53
54 #  正如Stephane Chazelas指出的那样比较输出的数组可以了解第二种格式的赋值比第三和第四的times的更快
55 #
56 #
57 #  William Park 解释explains:
58 #+ bigTwo 数组是被赋值了一个单字符串,
59 #+ bigThree 则赋值时一个一个元素的赋值.
60 #  所以, 实际上的情况是:
61 #                   bigTwo=( [0]="... ... ..." )
62 #                   bigThree=( [0]="..." [1]="..." [2]="..." ... )
63
64
65 #  我在本书的例子中仍然会继续用第一种格式,
66 #+ 因为我认为这会对说明清楚更有帮助.
67
68 #  我的例子中的可复用的部分实际上还是会使用第二种格式,
69 #+ 因为这种格式更快一些.
70
71 # MSZ: 很抱歉早先的失误(应是指本书的先前版本).
72
73
74 #  注:
75 #  ----
76 #  在31和43行的"declare -a"语句不是必须的,
77 #+ 因为会在使用Array=( ... )赋值格式时暗示它是数组.
78 #
79 #  但是, 省略这些声明会导致后面脚本的相关操作更慢一些.
80 #
81 #  试一下, 看有什么变化.
82
83 exit 0
################################End Script#########################################
注意: 对变量增加 declare -a  语句声明可以加速后面的数组操作速度.
Example 26-9 复制和连接数组
################################Start Script#######################################
 1 #! /bin/bash
 2 # CopyArray.sh
 3 #
 4 # 由 Michael Zick编写.
 5 # 在本书中使用已得到许可.
 6
 7 #  怎么传递变量名和值处理,返回就用使用该变量,
 8 #+ 或说"创建你自己的赋值语句".
 9
10
11 CpArray_Mac() {
12
13 # 创建赋值命令语句
14
15     echo -n 'eval '
16     echo -n "$2"                    # 目的变量名
17     echo -n '=( ${'
18     echo -n "$1"                    # 源名字
19     echo -n '[@]} )'
20
21 # 上面的全部会合成单个命令.
22 # 这就是函数所有的功能.
23 }
24
25 declare -f CopyArray                # 函数"指针"
26 CopyArray=CpArray_Mac               # 建立命令
27
28 Hype()
29 {
30
31 # 要复制的数组名为 $1.
32 # (接合数组,并包含尾部的字符串"Really Rocks".)
33 # 返回结果的数组名为 $2.
34
35     local -a TMP
36     local -a hype=( Really Rocks )
37
38     $($CopyArray $1 TMP)
39     TMP=( ${TMP[@]} ${hype[@]} )
40     $($CopyArray TMP $2)
41 }
42
43 declare -a before=( Advanced Bash Scripting )
44 declare -a after
45
46 echo "Array Before = ${before[@]}"
47
48 Hype before after
49
50 echo "Array After = ${after[@]}"
51
52 # 有多余的字符串?
53
54 echo "What ${after[@]:3:2}?"
55
56 declare -a modest=( ${after[@]:2:1} ${after[@]:3:2} )
57 #                    ----     子串提取       ----
58
59 echo "Array Modest = ${modest[@]}"
60
61 # 'before'变量变成什么了 ?
62
63 echo "Array Before = ${before[@]}"
64
65 exit 0
################################End Script#########################################
Example 26-10 关于连接数组的更多信息
################################Start Script#######################################
  1 #! /bin/bash
  2 # array-append.bash
  3
  4 # Copyright (c) Michael S. Zick, 2003, All rights reserved.
  5 # 许可: 可以无限制的以任何目的任何格式重复使用.
  6 # 版本: $ID$
  7 #
  8 # 格式上由M.C做了轻微的修改.
  9
 10
 11 # 数组操作是Bash特有的属性.
 12 # 原来的 UNIX /bin/sh 没有类似的功能.
 13
 14
 15 #  把此脚本的输出管道输送给 'more'
 16 #+ 以便输出不会滚过终端屏幕.
 17
 18
 19 # 下标依次使用.
 20 declare -a array1=( zero1 one1 two1 )
 21 # 下标有未使用的 ([1] 没有被定义).
 22 declare -a array2=( [0]=zero2 [2]=two2 [3]=three2 )
 23
 24 echo
 25 echo '- Confirm that the array is really subscript sparse. -'
 26 echo "Number of elements: 4"        # 这儿是举例子就用硬编码.
 27 for (( i = 0 ; i < 4 ; i++ ))
 28 do
 29     echo "Element [$i]: ${array2[$i]}"
 30 done
 31 # 也可以参考basics-reviewed.bash更多的常见代码.
 32
 33
 34 declare -a dest
 35
 36 # 组合 (添加) 两个数组到第三个数组.
 37 echo
 38 echo 'Conditions: Unquoted, default IFS, All-Elements-Of operator'
 39 echo '- Undefined elements not present, subscripts not maintained. -'
 40 # # 那些未定义的元素不存在; 组合时会丢弃这些元素.
 41
 42 dest=( ${array1[@]} ${array2[@]} )
 43 # dest=${array1[@]}${array2[@]}     # 奇怪的结果, 或者叫臭虫.
 44
 45 # 现在, 打印出结果.
 46 echo
 47 echo '- - Testing Array Append - -'
 48 cnt=${#dest[@]}
 49
 50 echo "Number of elements: $cnt"
 51 for (( i = 0 ; i < cnt ; i++ ))
 52 do
 53     echo "Element [$i]: ${dest[$i]}"
 54 done
 55
 56 # 把一个数组赋值给另一个数组的单个元素 (两次).
 57 dest[0]=${array1[@]}
 58 dest[1]=${array2[@]}
 59
 60 # 列出结果.
 61 echo
 62 echo '- - Testing modified array - -'
 63 cnt=${#dest[@]}
 64
 65 echo "Number of elements: $cnt"
 66 for (( i = 0 ; i < cnt ; i++ ))
 67 do
 68     echo "Element [$i]: ${dest[$i]}"
 69 done
 70
 71 # 检测第二个元素的改变.
 72 echo
 73 echo '- - Reassign and list second element - -'
 74
 75 declare -a subArray=${dest[1]}
 76 cnt=${#subArray[@]}
 77
 78 echo "Number of elements: $cnt"
 79 for (( i = 0 ; i < cnt ; i++ ))
 80 do
 81     echo "Element [$i]: ${subArray[$i]}"
 82 done
 83
 84 #  用 '=${ ... }' 把整个数组的值赋给另一个数组的单个元素
 85 #+ 使数组所有元素值被转换成了一个字符串,各元素的值由一个空格分开(其实是IFS的第一个字符).
 86 #
 87 #
 88
 89 # 如果原先的元素没有包含空白符 . . .
 90 # 如果原先的数组下标都是连续的 . . .
 91 # 我们就能取回最初的数组结构.
 92
 93 # 恢复第二个元素的修改回元素.
 94 echo
 95 echo '- - Listing restored element - -'
 96
 97 declare -a subArray=( ${dest[1]} )
 98 cnt=${#subArray[@]}
 99
100 echo "Number of elements: $cnt"
101 for (( i = 0 ; i < cnt ; i++ ))
102 do
103     echo "Element [$i]: ${subArray[$i]}"
104 done
105 echo '- - Do not depend on this behavior. - -'
106 echo '- - This behavior is subject to change - -'
107 echo '- - in versions of Bash newer than version 2.05b - -'
108
109 # MSZ: 很抱歉早先时混淆的几个要点(译者注:应该是指本书早先的版本).
110
111 exit 0
################################End Script#########################################
--
数组允许在脚本中实现一些常见的熟悉算法.这是否是必要的好想法在此不讨论,留给读者自
行判断.
Example 26-11 一位老朋友: 冒泡排序
################################Start Script#######################################
 1 #!/bin/bash
 2 # bubble.sh: 排序法之冒泡排序.
 3
 4 # 回忆冒泡排序法. 在这个版本中要实现它...
 5
 6 #  靠连续地多次比较数组元素来排序,
 7 #+ 比较两个相邻的元素,如果排序顺序不对,则交换两者的顺序.
 8 #  当第一轮比较结束后,最"重"的元素就被排到了最底部.
 9 #  当第二轮比较结束后,第二"重"的元素就被排到了次底部的位置.
10 #  以此类推.
11 #  这意味着每轮的比较不需要比较先前已"沉淀"好的数据.
12 #  因此你会注意到后面数据的打印会比较快一些.
13
14
15 exchange()
16 {
17   # 交换数组的两个元素.
18   local temp=${Countries[$1]} #  临时保存要交换的一个元素.
19                               #
20   Countries[$1]=${Countries[$2]}
21   Countries[$2]=$temp
22  
23   return
24 } 
25
26 declare -a Countries  #  声明数组,
27                       #+ 在此是可选的,因为下面它会被按数组来初始化.
28
29 #  是否允许用转义符(\)将数组的各变量值放到几行上?
30 #
31 #  是的.
32
33 Countries=(Netherlands Ukraine Zaire Turkey Russia Yemen Syria \
34 Brazil Argentina Nicaragua Japan Mexico Venezuela Greece England \
35 Israel Peru Canada Oman Denmark Wales France Kenya \
36 Xanadu Qatar Liechtenstein Hungary)
37
38 # "Xanadu" 是个虚拟的充满美好的神话之地.
39 #
40
41
42 clear                      # 开始之前清除屏幕.
43
44 echo "0: ${Countries[*]}"  # 从0索引的元素开始列出整个数组.
45
46 number_of_elements=${#Countries[@]}
47 let "comparisons = $number_of_elements - 1"
48
49 count=1 # 传递数字.
50
51 while [ "$comparisons" -gt 0 ]          # 开始外部的循环
52 do
53
54   index=0  # 每轮开始前重设索引值为0.
55
56   while [ "$index" -lt "$comparisons" ] # 开始内部循环
57   do
58     if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ]
59     #  如果原来的排序次序不对...
60     #  回想一下 \> 在单方括号里是is ASCII 码的比较操作符.
61     #
62
63     #  if [[ ${Countries[$index]} > ${Countries[`expr $index + 1`]} ]]
64     #+ 也可以.
65     then
66       exchange $index `expr $index + 1`  # 交换.
67     fi 
68     let "index += 1"
69   done # 内部循环结束
70
71 # ----------------------------------------------------------------------
72 # Paulo Marcel Coelho Aragao 建议使用更简单的for-loops.
73 #
74 # for (( last = $number_of_elements - 1 ; last > 1 ; last-- ))
75 # do
76 #     for (( i = 0 ; i < last ; i++ ))
77 #     do
78 #         [[ "${Countries[$i]}" > "${Countries[$((i+1))]}" ]] \
79 #             && exchange $i $((i+1))
80 #     done
81 # done
82 # ----------------------------------------------------------------------
83  
84
85 let "comparisons -= 1" #  因为最"重"的元素冒到了最底部,
86                        #+ 我们可以每轮少做一些比较.
87
88 echo
89 echo "$count: ${Countries[@]}"  # 每轮结束后,打印一次数组.
90 echo
91 let "count += 1"                # 增加传递计数.
92
93 done                            # 外部循环结束
94                                 # 完成.
95
96 exit 0
################################End Script#########################################
--
在数组内嵌一个数组有可能做到吗?
   1 #!/bin/bash
   2 # "内嵌" 数组.
   3
   4 #  Michael Zick 提供这个例子,
   5 #+ 由William Park作了些纠正和解释.
   6
   7 AnArray=( $(ls --inode --ignore-backups --almost-all \
   8  --directory --full-time --color=none --time=status \
   9  --sort=time -l ${PWD} ) )  # 命令及选项.
  10
  11 # 空格是有意义的 . . . 不要在上面引号引用任何东西.
  12
  13 SubArray=( ${AnArray[@]:11:1}  ${AnArray[@]:6:5} )
  14 #  这个数组有6个元素:
  15 #+     SubArray=( [0]=${AnArray[11]} [1]=${AnArray[6]} [2]=${AnArray[7]}
  16 #      [3]=${AnArray[8]} [4]=${AnArray[9]} [5]=${AnArray[10]} )
  17 #
  18 #  Bash中的数组像是字符串(char *)型的(循环)链表.
  19 #
  20 #  因此, 这实际上不是内嵌的数组,
  21 #+ 但它的功能是相似的.
  22
  23 echo "Current directory and date of last status change:"
  24 echo "${SubArray[@]}"
  25
  26 exit 0
--
内嵌数组和间接引用(indirect references) 的组合使用产生了一些有趣的用法.
Example 26-12 内嵌数组和间接引用
################################Start Script#######################################
 1 #!/bin/bash
 2 # embedded-arrays.sh
 3 # 内嵌数组和间接引用.
 4
 5 # 由Dennis Leeuw编写.
 6 # 已获使用许可.
 7 # 由本文作者修改.
 8
 9
10 ARRAY1=(
11         VAR1_1=value11
12         VAR1_2=value12
13         VAR1_3=value13
14 )
15
16 ARRAY2=(
17         VARIABLE="test"
18         STRING="VAR1=value1 VAR2=value2 VAR3=value3"
19         ARRAY21=${ARRAY1[*]}
20 )       # 把ARRAY1数组嵌到这个数组里.
21
22 function print () {
23         OLD_IFS="$IFS"
24         IFS=$'\n'       #  这是为了在每个行打印一个数组元素.
25                         #
26         TEST1="ARRAY2[*]"
27         local ${!TEST1} # 试下删除这行会发生什么.
28         #  间接引用.
29  #  这使 $TEST1只在函数内存取.
30  #
31
32
33         #  我们看看还能干点什么.
34         echo
35         echo "\$TEST1 = $TEST1"       #  变量的名称.
36         echo; echo
37         echo "{\$TEST1} = ${!TEST1}"  #  变量的内容.
38                                       #  这就是间接引用的作用.
39                                       #
40         echo
41         echo "-------------------------------------------"; echo
42         echo
43
44
45         # 打印变量
46         echo "Variable VARIABLE: $VARIABLE"
47  
48         # 打印一个字符串元素
49         IFS="$OLD_IFS"
50         TEST2="STRING[*]"
51         local ${!TEST2}      # 间接引用 (像上面一样).
52         echo "String element VAR2: $VAR2 from STRING"
53
54         # 打印一个字符串元素
55         TEST2="ARRAY21[*]"
56         local ${!TEST2}      # 间接引用 (像上面一样).
57         echo "Array element VAR1_1: $VAR1_1 from ARRAY21"
58 }
59
60 print
61 echo
62
63 exit 0
64
65 #   脚本作者注,
66 #+ "你可以很容易地将其扩展成Bash的一个能创建hash的脚本."
67 #   (难) 留给读者的练习: 实现它.
################################End Script#########################################
--
数组使埃拉托色尼素数筛子有了shell脚本的实现. 当然, 如果是追求效率的应用自然应该用
一种编译型的语言,例如用C. 这种脚本运行实在是太慢.
Example 26-13 复杂数组应用: 埃拉托色尼素数筛子
################################Start Script#######################################
  1 #!/bin/bash
  2 # sieve.sh (ex68.sh)
  3
  4 # 埃拉托色尼素数筛子
  5 # 找素数的经典算法.
  6
  7 #  在同等数量的数值内这个脚本比用C写的版本慢很多.
  8 #
  9
 10 LOWER_LIMIT=1       # 从1开始.
 11 UPPER_LIMIT=1000    # 到 1000.
 12 # (如果你很有时间的话,你可以把它设得更高 . . . )
 13
 14 PRIME=1
 15 NON_PRIME=0
 16
 17 let SPLIT=UPPER_LIMIT/2
 18 # 优化:
 19 # 只需要测试中间到最大之间的值 (为什么?).
 20
 21
 22 declare -a Primes
 23 # Primes[] 是一个数组.
 24
 25
 26 initialize ()
 27 {
 28 # 初始化数组.
 29
 30 i=$LOWER_LIMIT
 31 until [ "$i" -gt "$UPPER_LIMIT" ]
 32 do
 33   Primes[i]=$PRIME
 34   let "i += 1"
 35 done
 36 #  假定所有的数组成员都是需要检查的 (素数)
 37 #+ 一直到检查完成前.
 38 }
 39
 40 print_primes ()
 41 {
 42 # 打印出所有Primes[]数组中被标记为素数的元素.
 43
 44 i=$LOWER_LIMIT
 45
 46 until [ "$i" -gt "$UPPER_LIMIT" ]
 47 do
 48
 49   if [ "${Primes[i]}" -eq "$PRIME" ]
 50   then
 51     printf "%8d" $i
 52     # 每个数字打印前先打印8个空格, 数字是在偶数列打印的.
 53   fi
 54  
 55   let "i += 1"
 56  
 57 done
 58
 59 }
 60
 61 sift () # 查出非素数.
 62 {
 63
 64 let i=$LOWER_LIMIT+1
 65 # 我们都知道1是素数, 所以我们从2开始.
 66
 67 until [ "$i" -gt "$UPPER_LIMIT" ]
 68 do
 69
 70 if [ "${Primes[i]}" -eq "$PRIME" ]
 71 # 不要处理已经过滤过的数字 (被标识为非素数).
 72 then
 73
 74   t=$i
 75
 76   while [ "$t" -le "$UPPER_LIMIT" ]
 77   do
 78     let "t += $i "
 79     Primes[t]=$NON_PRIME
 80     # 标识为非素数.
 81   done
 82
 83 fi 
 84
 85   let "i += 1"
 86 done 
 87
 88
 89 }
 90
 91
 92 # ==============================================
 93 # main ()
 94 # 继续调用函数.
 95 initialize
 96 sift
 97 print_primes
 98 # 这就是被称为结构化编程的东西了.
 99 # ==============================================
100
101 echo
102
103 exit 0
104
105
106
107 # -------------------------------------------------------- #
108 # 因为前面的一个'exit',所以下面的代码不会被执行.
109
110 #  下面是Stephane Chazelas写的一个埃拉托色尼素数筛子的改进版本,
111 #+ 运行会稍微快一点.
112
113 # 必须在命令行上指定参数(寻找素数的限制范围).
114
115 UPPER_LIMIT=$1                  # 值来自命令行.
116 let SPLIT=UPPER_LIMIT/2         # 从中间值到最大值.
117
118 Primes=( '' $(seq $UPPER_LIMIT) )
119
120 i=1
121 until (( ( i += 1 ) > SPLIT ))  # 仅需要从中间值检查.
122 do
123   if [[ -n $Primes[i] ]]
124   then
125     t=$i
126     until (( ( t += i ) > UPPER_LIMIT ))
127     do
128       Primes[t]=
129     done
130   fi 
131 done 
132 echo ${Primes[*]}
133
134 exit 0
################################End Script#########################################
比较这个用数组的素数产生器和另一种不用数组的例子 A-16.
--
数组可以做一定程度的扩展,以模拟支持Bash原本不支持的数据结构.
Example 26-14 模拟下推的堆栈
################################Start Script#######################################
  1 #!/bin/bash
  2 # stack.sh: 下推的堆栈模拟
  3
  4 #  类似于CPU栈, 下推的堆栈依次保存数据项,
  5 #+ 但取出时则反序进行, 后进先出.
  6
  7 BP=100            #  栈数组的基点指针.
  8                   #  从元素100开始.
  9
 10 SP=$BP            #  栈指针.
 11                   #  初始化栈底.
 12
 13 Data=             #  当前栈的内容. 
 14                   #  必须定义成全局变量,
 15                   #+ 因为函数的返回整数有范围限制.
 16
 17 declare -a stack
 18
 19
 20 push()            # 把一个数据项压入栈.
 21 {
 22 if [ -z "$1" ]    # 没有可压入的?
 23 then
 24   return
 25 fi
 26
 27 let "SP -= 1"     # 更新堆栈指针.
 28 stack[$SP]=$1
 29
 30 return
 31 }
 32
 33 pop()                    # 从栈中弹出一个数据项.
 34 {
 35 Data=                    # 清空保存数据项中间变量.
 36
 37 if [ "$SP" -eq "$BP" ]   # 已经没有数据可弹出?
 38 then
 39   return
 40 fi                       #  这使SP不会超过100,
 41                          #+ 例如, 这可保护一个失控的堆栈.
 42
 43 Data=${stack[$SP]}
 44 let "SP += 1"            # 更新堆栈指针.
 45 return
 46 }
 47
 48 status_report()          # 打印堆栈的当前状态.
 49 {
 50 echo "-------------------------------------"
 51 echo "REPORT"
 52 echo "Stack Pointer = $SP"
 53 echo "Just popped \""$Data"\" off the stack."
 54 echo "-------------------------------------"
 55 echo
 56 }
 57
 58
 59 # =======================================================
 60 # 现在,来点乐子.
 61
 62 echo
 63
 64 # 看你是否能从空栈里弹出数据项来.
 65 pop
 66 status_report
 67
 68 echo
 69
 70 push garbage
 71 pop
 72 status_report     # 压入garbage, 弹出garbage.     
 73
 74 value1=23; push $value1
 75 value2=skidoo; push $value2
 76 value3=FINAL; push $value3
 77
 78 pop              # FINAL
 79 status_report
 80 pop              # skidoo
 81 status_report
 82 pop              # 23
 83 status_report    # 后进, 先出!
 84
 85 #  注意堆栈指针每次压栈时减,
 86 #+ 每次弹出时加一.
 87
 88 echo
 89
 90 exit 0
 91
 92 # =======================================================
 93
 94
 95 # 练习:
 96 # ---------
 97
 98 # 1)  修改"push()"函数,使其调用一次就能够压入多个数据项.
 99 #
100
101 # 2)  修改"pop()"函数,使其调用一次就能弹出多个数据项.
102 #
103
104 # 3)  给那些有临界操作的函数增加出错检查.
105 #     即是指是否一次完成操作或没有完成操作返回相应的代码,
106 #   + 没有完成要启动合适的处理动作.
107 #
108
109 # 4)  这个脚本为基础,
110 #   + 写一个栈实现的四则运算计算器.
################################End Script#########################################
--
要想操作数组的下标需要中间变量. 如果确实要这么做, 可以考虑使用一种更强功能的编程语
言, 例如 Perl 或 C.
Example 26-15 复杂的数组应用: 列出一种怪异的数学序列
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # Douglas Hofstadter的有名的"Q-series":
 4
 5 # Q(1) = Q(2) = 1
 6 # Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), 当 n>2 时
 7
 8 # 这是令人感到陌生的也是没有规律的"乱序"整数序列.
 9 # 序列的头20个如下所示:
10 # 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12
11
12 #  参考Hofstadter的书, "Goedel, Escher, Bach: An Eternal Golden Braid",
13 #+ 页码 137.
14
15
16 LIMIT=100     # 计算数的个数.
17 LINEWIDTH=20  # 很行要打印的数的个数.
18
19 Q[1]=1        # 序列的头2个是 1.
20 Q[2]=1
21
22 echo
23 echo "Q-series [$LIMIT terms]:"
24 echo -n "${Q[1]} "             # 打印头2个数.
25 echo -n "${Q[2]} "
26
27 for ((n=3; n <= $LIMIT; n++))  # C风格的循环条件.
28 do   # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]]  当 n>2 时
29 #  需要将表达式分步计算,
30 #+ 因为Bash不擅长处理此类复杂计算.
31
32   let "n1 = $n - 1"        # n-1
33   let "n2 = $n - 2"        # n-2
34  
35   t0=`expr $n - ${Q[n1]}`  # n - Q[n-1]
36   t1=`expr $n - ${Q[n2]}`  # n - Q[n-2]
37  
38   T0=${Q[t0]}              # Q[n - Q[n-1]]
39   T1=${Q[t1]}              # Q[n - Q[n-2]]
40
41 Q[n]=`expr $T0 + $T1`      # Q[n - Q[n-1]] + Q[n - Q[n-2]]
42 echo -n "${Q[n]} "
43
44 if [ `expr $n % $LINEWIDTH` -eq 0 ]    # 格式化输出.
45 then   #      ^ 取模操作
46   echo # 把行分成内部的块.
47 fi
48
49 done
50
51 echo
52
53 exit 0
54
55 # 这是Q-series问题的迭代实现.
56 # 更直接明了的递归实现留给读者完成.
57 # 警告: 递归地计算这个序列会花很长的时间.
################################End Script#########################################
--
Bash 只支持一维数组,但有一些技巧可用来模拟多维数组.
Example 26-16 模拟二维数组,并使它倾斜
################################Start Script#######################################
  1 #!/bin/bash
  2 # twodim.sh: 模拟二维数组.
  3
  4 # 一维数组由单行组成.
  5 # 二维数组由连续的行组成.
  6
  7 Rows=5
  8 Columns=5
  9 # 5 X 5 的数组Array.
 10
 11 declare -a alpha     # char alpha [Rows] [Columns];
 12                      # 不必要的声明. 为什么?
 13
 14 load_alpha ()
 15 {
 16 local rc=0
 17 local index
 18
 19 for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y
 20 do     # 如果你高兴,可以使用不同的符号.
 21   local row=`expr $rc / $Columns`
 22   local column=`expr $rc % $Rows`
 23   let "index = $row * $Rows + $column"
 24   alpha[$index]=$i
 25 # alpha[$row][$column]
 26   let "rc += 1"
 27 done 
 28
 29 #  更简单的办法
 30 #+   declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y )
 31 #+ 但这就缺少了二维数组的感觉了.
 32 }
 33
 34 print_alpha ()
 35 {
 36 local row=0
 37 local index
 38
 39 echo
 40
 41 while [ "$row" -lt "$Rows" ]   #  以行顺序为索引打印行的各元素:
 42 do                             #+ 即数组列值变化快,
 43                                #+ 行值变化慢.
 44   local column=0
 45
 46   echo -n "       "            #  依行倾斜打印正方形的数组.
 47  
 48   while [ "$column" -lt "$Columns" ]
 49   do
 50     let "index = $row * $Rows + $column"
 51     echo -n "${alpha[index]} "  # alpha[$row][$column]
 52     let "column += 1"
 53   done
 54
 55   let "row += 1"
 56   echo
 57
 58 done 
 59
 60 # 等同于
 61 #     echo ${alpha[*]} | xargs -n $Columns
 62
 63 echo
 64 }
 65
 66 filter ()     # 过滤出负数的数组索引.
 67 {
 68
 69 echo -n "  "  # 产生倾斜角度.
 70               # 解释怎么办到的.
 71
 72 if [[ "$1" -ge 0 &&  "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]]
 73 then
 74     let "index = $1 * $Rows + $2"
 75     # Now, print it rotated现在,打印旋转角度.
 76     echo -n " ${alpha[index]}"
 77     #           alpha[$row][$column]
 78 fi   
 79
 80 }
 81  
 82
 83
 84
 85 rotate ()  #  旋转数组 45 度 --
 86 {          #+ 在左下角"平衡"图形.
 87 local row
 88 local column
 89
 90 for (( row = Rows; row > -Rows; row-- ))
 91   do       # 从后面步进数组. 为什么?
 92
 93   for (( column = 0; column < Columns; column++ ))
 94   do
 95
 96     if [ "$row" -ge 0 ]
 97     then
 98       let "t1 = $column - $row"
 99       let "t2 = $column"
100     else
101       let "t1 = $column"
102       let "t2 = $column + $row"
103     fi 
104
105     filter $t1 $t2   # 过滤出负数数组索引.
106                      # 如果你不这样做会怎么样?
107   done
108
109   echo; echo
110
111 done
112
113 #  数组旋转灵感源于Herbert Mayer写的
114 #+ "Advanced C Programming on the IBM PC," 的例子 (页码. 143-146)
115 #+ (看参考书目附录).
116 #  这也能看出C能做的事情有多少能用shell脚本做到.
117 #
118
119 }
120
121
122 #---------------   现在, 可以开始了.     ------------#
123 load_alpha     # 加载数组.
124 print_alpha    # 打印数组. 
125 rotate         # 反时钟旋转数组45度.
126 #-----------------------------------------------------#
127
128 exit 0
129
130 # 这是有点做作,不太优雅.
131
132 # 练习:
133 # ---------
134 # 1)  重写数组加载和打印函数,
135 #     使其更直观和容易了解.
136 #
137 # 2)  指出数组旋转函数是什么原理.
138 #     Hint索引: 思考数组从尾向前索引的实现.
139 #
140 # 3)  重写脚本使其可以处理非方形数组Rewrite this script to handle a non-square array,
141 #     例如 6 X 4 的数组.
142 #     尝试旋转数组时做到最小"失真".
################################End Script#########################################
二维数组本质上等同于一维数组, 而只增加了使用行和列的位置来引用和操作元素的寻址模式.
关于二维数组更好的例子, 请参考例子 A-10.
--
另一个有趣的使用数组的脚本:
    * 例子 14-3

阅读(2459) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~