shell基础编程实例讲解


一些 shell 编程的例子 Example 2-1 清除:清除/var/log 下的 log 文件 ################################Start Script####################################### # Cleanup # 当然要使用 root 身份来运行这个脚本 cd /var/log cat /dev/null > messages cat /dev/null > wtmp echo "Logs cleaned up." ################################End Script######################################### Example 2-2 清除:一个改良的清除脚本 ################################Start Script####################################### #!/bin/bash # 一个 Bash 脚本的正确的开头部分. # Cleanup, 版本 2 # 当然要使用 root 身份来运行. # 在此处插入代码,来打印错误消息,并且在不是 root 身份的时候退出. LOG_DIR=/var/log # 如果使用变量,当然比把代码写死的好. cd $LOG_DIR cat /dev/null > messages cat /dev/null > wtmp echo "Logs cleaned up." exit # 这个命令是一种正确并且合适的退出脚本的方法. ################################End Script######################################### Example 2-3. cleanup:一个增强的和广义的删除 logfile 的脚本 ################################Start Script####################################### #!/bin/bash # 清除, 版本 3 # Warning: # ------- # 这个脚本有好多特征,这些特征是在后边章节进行解释的,大概是进行到本书的一半的 # 时候, # 你就会觉得它没有什么神秘的了. # LOG_DIR=/var/log ROOT_UID=0 # $UID 为 0 的时候,用户才具有根用户的权限 LINES=50 # 默认的保存行数 E_XCD=66 # 不能修改目录? E_NOTROOT=67 # 非根用户将以 error 退出 # 当然要使用根用户来运行 if [ "$UID" -ne "$ROOT_UID" ] then echo "Must be root to run this script." exit $E_NOTROOT fi if [ -n "$1" ] # 测试是否有命令行参数(非空). then lines=$1 else lines=$LINES # 默认,如果不在命令行中指定 fi # Stephane Chazelas 建议使用下边 #+ 的更好方法来检测命令行参数. #+ 但对于这章来说还是有点超前. # # E_WRONGARGS=65 # 非数值参数(错误的参数格式) # # case "$1" in # "" ) lines=50;; # *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;; # * ) lines=$1;; # esac # #* 直到"Loops"的章节才会对上边的内容进行详细的描述. cd $LOG_DIR if [ `pwd` != "$LOG_DIR" ] # 或者 if[ "$PWD" != "$LOG_DIR" ] # 不在 /var/log 中? then echo "Can't change to $LOG_DIR." exit $E_XCD fi # 在处理 log file 之前,再确认一遍当前目录是否正确. # 更有效率的做法是 # # cd /var/log || { # echo "Cannot change to necessary directory." >&2 # exit $E_XCD; # } tail -$lines messages > mesg.temp # 保存 log file 消息的最后部分. mv mesg.temp messages # 变为新的 log 目录. # cat /dev/null > messages #* 不再需要了,使用上边的方法更安全. cat /dev/null > wtmp # ': > wtmp' 和 '> wtmp'具有相同的作用 echo "Logs cleaned up." exit 0 # 退出之前返回 0,返回 0 表示成功. # ################################End Script######################################### 因为你可能希望将系统 log 全部消灭,这个版本留下了 log 消息最后的部分.你将不断地找到 新 的方法来完善这个脚本,并提高效率. 要注意,在每个脚本的开头都使用"#!",这意味着告诉你的系统这个文件的执行需要指定一个 解 释器.#!实际上是一个 2 字节[1]的魔法数字,这是指定一个文件类型的特殊标记, 换句话说, 在 这种情况下,指的就是一个可执行的脚本(键入 man magic 来获得关于这个迷人话题的更多详 细 信息).在#!之后接着是一个路径名.这个路径名指定了一个解释脚本中命令的程序,这个程序 可 以是 shell,程序语言或者是任意一个通用程序.这个指定的程序从头开始解释并且执行脚本中 的命令(从#!行下边的一行开始),忽略注释.[2] 如: #!/bin/sh #!/bin/bash #!/usr/bin/perl #!/usr/bin/tcl #!/bin/sed -f #!/usr/awk -f 上边每一个脚本头的行都指定了一个不同的命令解释器,如果是/bin/sh,那么就是默认 shell (在 Linux 系统中默认是 Bash).[3]使用#!/bin/sh,在大多数商业发行的 UNIX 上,默认是 Bourne shell,这将让你的脚本可以正常的运行在非 Linux 机器上,虽然这将会牺牲 Bash 一些独特的特 征. 脚本将与 POSIX[4] 的 sh 标准相一致. 注意: #! 后边给出的路径名必须是正确的,否则将会出现一个错误消息,通常是 "Command not found",这将是你运行这个脚本时所得到的唯一结果. 当然"#!"也可以被忽略,不过这样你的脚本文件就只能是一些命令的集合,不能够使用 shell 内 建 的指令了,如果不能使用变量的话,当然这也就失去了脚本编程的意义了. 注意:这个例子鼓励你使用模块化的方式来编写脚本,平时也要注意收集一些零碎的代 码, 这些零碎的代码可能用在你将来编写的脚本中.这样你就可以通过这些代码片段来 构 造一个较大的工程用例. 以下边脚本作为序,来测试脚本被调用的参数是否正确. ################################Start Script####################################### E_WRONG_ARGS=65 script_parameters="-a -h -m -z" # -a = all, -h = help, 等等. if [ $# -ne $Number_of_expected_args ] then echo "Usage: `basename $0` $script_parameters" # `basename $0`是这个脚本的文件名 exit $E_WRONG_ARGS fi ################################End Script######################################### Example 3-1. 代码块和 I/O 重定向 ################################Start Script####################################### #!/bin/bash # 从 /etc/fstab 中读行 File=/etc/fstab { read line1 read line2 } < $File echo "First line in $File is:" echo "$line1" echo echo "Second line in $File is:" echo "$line2" exit 0 # 现在,你怎么分析每行的分割域 # 暗示: 使用 awk. ################################End Script######################################### Example 3-2. 将一个代码块的结果保存到文件 ################################Start Script####################################### #!/bin/bash # rpm-check.sh # 这个脚本的目的是为了描述,列表,和确定是否可以安装一个 rpm 包. # 在一个文件中保存输出. # # 这个脚本使用一个代码块来展示 SUCCESS=0 E_NOARGS=65 if [ -z "$1" ] then echo "Usage: `basename $0` rpm-file" exit $E_NOARGS fi { echo echo "Archive Description:" rpm -qpi $1 # 查询说明 echo echo "Archive Listing:" rpm -qpl $1 # 查询列表 echo rpm -i --test $1 # 查询 rpm 包是否可以被安装 if [ "$?" -eq $SUCCESS ] then echo "$1 can be installed." else echo "$1 cannot be installed." fi echo } > "$1.test" # 把代码块中的所有输出都重定向到文件中 echo "Results of rpm test in file $1.test" # 查看 rpm 的 man 页来查看 rpm 的选项 exit 0 ################################End Script######################################### 注意: 与()中的命令不同的是,{}中的代码块将不能正常地开启一个新 shell.[2] {} \; 路径名.一般都在 find 命令中使用.这不是一个 shell 内建命令. 注意: ";"用来结束 find 命令序列的-exec 选项. [] test. test 的表达式将在[]中. 值得注意的是[是 shell 内建 test 命令的一部分,并不是/usr/bin/test 中的扩展命令 的一个连接. [[]] test. test 表达式放在[[]]中.(shell 关键字) 具体查看[[]]结构的讨论. [] 数组元素 Array[1]=slot_1 echo ${Array[1]} [] 字符范围 在正则表达式中使用,作为字符匹配的一个范围 (()) 数学计算的扩展 在(())结构中可以使用一些数字计算. 具体参阅((...))结构. >&>>&>>< 重定向. scriptname >filename 重定向脚本的输出到文件中.覆盖文件原有内容. command &>filename 重定向 stdout 和 stderr 到文件中 command >&2 重定向 command 的 stdout 到 stderr scriptname >>filename 重定向脚本的输出到文件中.添加到文件尾端,如果没有文件, 则创建这个文件. 进程替换,具体见"进程替换部分",跟命令替换极其类似. (command)> <(command) <和> 可用来做字符串比较 <和> 可用在数学计算比较 << 重定向,用在"here document" <<< 重定向,用在"here string" <,> ASCII 比较 1 veg1=carrots 2 veg2=tomatoes 3 4 if [[ "$veg1" < "$veg2" ]] 5 then 6 echo "Although $veg1 precede $veg2 in the dictionary," 7 echo "this implies nothing about my culinary preferences." 8 else 9 echo "What kind of dictionary are you using, anyhow?" 10 fi \<,\> 正则表达式中的单词边界.如: bash$grep '\' textfile | 管道.分析前边命令的输出,并将输出作为后边命令的输入.这是一种产生命令链的 好方法. 1 echo ls -l | sh 2 # 传递"echo ls -l"的输出到 shell 中, 3 #+ 与一个简单的"ls -l"结果相同. 4 5 6 cat *.lst | sort | uniq 7 # 合并和排序所有的".lst"文件,然后删除所有重复的行. 管道是进程间通讯的一个典型办法,将一个进程的 stdout 放到另一个进程的 stdin 中. 标准的方法是将一个一般命令的输出,比如cat或echo,传递到一个过滤命令中(在这 个 过滤命令中将处理输入),得到结果,如: cat $filename1 | $filename2 | grep $search_word 当然输出的命令也可以传递到脚本中.如: ################################Start Script####################################### #!/bin/bash # uppercase.sh : 修改输出,全部转换为大写 tr 'a-z' 'A-Z' # 字符范围必须被""引用起来 #+ 来阻止产生单字符的文件名. exit 0 ################################End Script######################################### Example 3-3. 在后台运行一个循环 ################################Start Script####################################### #!/bin/bash #background-loop.sh 3 for i in 1 2 3 4 5 6 7 8 9 10 #第一个循环 do 6 echo -n "$i" done& #在后台运行这个循环 8 #在第 2 个循环之后,将在某些时候执行. echo #这个'echo'某些时候将不会显示. for i in 11 12 13 14 15 16 17 18 19 20 #第二个循环 do echo -n "$i" done 16 echo #这个'echo'某些时候将不会显示. 18 #-------------------------------------------------------- #期望的输出应该是 #1 2 3 4 5 6 7 8 9 10 #11 12 13 14 15 16 17 18 19 20 #然而实际的结果有可能是 #11 12 13 14 15 16 17 18 19 20 #1 2 3 4 5 6 7 8 9 10 bozo $ #(第 2 个'echo'没执行,为什么?) #也可能是 #1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #(第 1 个'echo'没执行,为什么?) #非常少见的执行结果,也有可能是: #11 12 13 1 2 3 4 5 6 7 8 9 10 14 15 16 17 18 19 20 #前台的循环先于后台的执行 exit 0 # Nasimuddin Ansari 建议加一句 sleep 1 #+ 在 6 行和 14 行的 echo -n "$i"之后加 #+ 将看到一些乐趣 ################################End Script######################################### 注意:在一个脚本内后台运行一个命令,有可能造成这个脚本的挂起,等待一个按键 响应.幸运的是,我们可以在 Example 11-24 附近,看到这个问题的解决办法. && 与-逻辑操作. - 选项,前缀.在所有的命令内如果想使用选项参数的话,前边都要加上"-". COMMAND -[Option1][Option2][...] ls -al sort -dfu $filename set -- $variable 1 if [ $file1 -ot $file2 ] 2 then 3 echo "File $file1 is older than $file2." 4 fi 5 6 if [ "$a" -eq "$b" ] 7 then 8 echo "$a is equal to $b." 9 fi 10 11 if [ "$c" -eq 24 -a "$d" -eq 47 ] 12 then 13 echo "$c equals 24 and $d equals 47." 14 fi - 用于重定向 stdin 或 stdout. ################################Start Script####################################### (cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -) # 从一个目录移动整个目录树到另一个目录 # [courtesy Alan Cox , with a minor change] # 1) cd /source/directory 源目录 # 2) && 与操作,如果 cd 命令成功了,那么就执行下边的命令 # 3) tar cf - . 'c'创建一个新文档,'f'后边跟'-'指定目标文件作为 stdout # '-'后边的'f'(file)选项,指明作为 stdout 的目标文件. # 并且在当前目录('.')执行. # 4) | 管道... # 5) ( ... ) 一个子 shell # 6) cd /dest/directory 改变当前目录到目标目录. # 7) && 与操作,同上. # 8) tar xpvf - 'x'解档,'p'保证所有权和文件属性, # 'v'发完整消息到 stdout # 'f'后边跟'-',从 stdin 读取数据 # # 注意:'x' 是一个命令, 'p', 'v', 'f' 是选项. # Whew! # 更优雅的写法应该是 # cd source/directory # tar cf - . | (cd ../dest/directory; tar xpvf -) # # 当然也可以这么写: # cp -a /source/directory/* /dest/directory # 或者: # cp -a /source/directory/* /source/directory/.[^.]* /dest/directory # 如果在/source/directory 中有隐藏文件的话. ################################End Script######################################### ################################Start Script####################################### bunzip2 linux-2.6.13.tar.bz2 | tar xvf - # --未解压的 tar 文件-- | --然后把它传递到"tar"中-- # 如果 "tar" 没能够正常的处理"bunzip2", # 这就需要使用管道来执行 2 个单独的步骤来完成它. # 这个练习的目的是解档"bzipped"的 kernel 源文件. ################################End Script######################################### Example 3-4. 备份最后一天所有修改的文件. ################################Start Script####################################### #!/bin/bash # 在一个"tarball"中(经过 tar 和 gzip 处理过的文件) #+ 备份最后 24 小时当前目录下 d 所有修改的文件. BACKUPFILE=backup-$(date +%m-%d-%Y) # 在备份文件中嵌入时间. # Thanks, Joshua Tschida, for the idea. archive=${1:-$BACKUPFILE} # 如果在命令行中没有指定备份文件的文件名, #+ 那么将默认使用"backup-MM-DD-YYYY.tar.gz". tar cvf - `find . -mtime -1 -type f -print` > $archive.tar gzip $archive.tar echo "Directory $PWD backed up in archive file \"$archive.tar.gz\"." # Stephane Chazelas 指出上边代码, #+ 如果在发现太多的文件的时候,或者是如果文件 #+ 名包括空格的时候,将执行失败. # Stephane Chazelas 建议使用下边的两种代码之一 # ------------------------------------------------------------------- # find . -mtime -1 -type f -print0 | xargs -0 tar rvf "$archive.tar" # 使用 gnu 版本的 find. # find . -mtime -1 -type f -exec tar rvf "$archive.tar" '{}' \; # 对于其他风格的 UNIX 便于移植,但是比较慢. # ------------------------------------------------------------------- exit 0 ################################End Script######################################### 注意:以"-"开头的文件名在使用"-"作为重定向操作符的时候,可能会产生问题. 应该写一个脚本来检查这个问题,并给这个文件加上合适的前缀.如: ./-FILENAME, $PWD/-FILENAME,或$PATHNAME/-FILENAME. 如果变量的值以"-"开头,可能也会引起问题. 1 var="-n" 2 echo $var 3 #具有"echo -n"的效果了,这样什么都不会输出的. - 之前工作的目录."cd -"将回到之前的工作目录,具体请参考"$OLDPWD"环境变量. 注意:一定要和之前讨论的重定向功能分开,但是只能依赖上下文区分. - 算术减号. = 算术等号,有时也用来比较字符串. 1 a=28 2 echo $a # 28 + 算术加号,也用在正则表达式中. + 选项,对于特定的命令来说使用"+"来打开特定的选项,用"-"来关闭特定的选项. % 算术取模运算.也用在正则表达式中. ~ home 目录.相当于$HOME 变量.~bozo 是 bozo 的 home 目录,并且 ls ~bozo 将列出 其中的 内容. ~/就是当前用户的 home 目录,并且 ls ~/将列出其中的内容,如: bash$ echo ~bozo /home/bozo bash$ echo ~ /home/bozo bash$ echo ~/ /home/bozo/ bash$ echo ~: /home/bozo: bash$ echo ~nonexistent-user ~nonexistent-user ~+ 当前工作目录,相当于$PWD 变量. ~- 之前的工作目录,相当于$OLDPWD 内部变量. =~ 用于正则表达式,这个操作将在正则表达式匹配部分讲解,只有 version3 才支持. ^ 行首,正则表达式中表示行首."^"定位到行首. 控制字符 修改终端或文本显示的行为.控制字符以 CONTROL + key 组合. 控制字符在脚本中不能正常使用. Ctl-B 光标后退,这应该依赖于 bash 输入的风格,默认是 emacs 风格的. Ctl-C Break,终止前台工作. Ctl-D 从当前 shell 登出(和 exit 很像) "EOF"(文件结束符).这也能从 stdin 中终止输入. 在 console 或者在 xterm window 中输入的时候,Ctl-D 将删除光标下字 符. 当没有字符时,Ctrl-D将退出当前会话.在xterm window也有关闭窗口 的效果. Ctl-G beep.在一些老的终端,将响铃. Ctl-H backspace,删除光标前边的字符.如: 1 #!/bin/bash 2 # 在一个变量中插入 Ctl-H 3 4 a="^H^H" # 两个 Ctl-H (backspaces). 5 echo "abcdef" # abcdef 6 echo -n "abcdef$a " # abcd f 7 # 注意结尾的空格 ^ ^ 两个 twice. 8 echo -n "abcdef$a" # abcdef 9 # 结尾没有空格 没有 backspace 的效果了(why?). 10 # 结果并不像期望的那样 11 echo; echo Ctl-I 就是 tab 键. Ctl-J 新行. Ctl-K 垂直 tab.(垂直 tab?新颖,没听过) 作用就是删除光标到行尾的字符. Ctl-L clear,清屏. Ctl-M 回车 ################################Start Script####################################### #!/bin/bash # Thank you, Lee Maschmeyer, for this example. read -n 1 -s -p $'Control-M leaves cursor at beginning of this line. Press Enter. \x0d' #当然,'0d'就是二进制的回车. echo >&2 # '-s'参数使得任何输入都不将回显出来 #+ 所以,明确的重起一行是必要的. read -n 1 -s -p $'Control-J leaves cursor on next line. \x0a' echo >&2 # Control-J 是换行. ### read -n 1 -s -p $'And Control-K\x0bgoes straight down.' echo >&2 # Control-K 是垂直制表符. # 关于垂直制表符效果的一个更好的例子见下边: var=$'\x0aThis is the bottom line\x0bThis is the top line\x0a' echo "$var" # 这句与上边的例子使用的是同样的办法,然而: echo "$var" | col # 这将造成垂直制表符右边的部分在左边部分的上边. # 这也解释了为什么我们要在行首和行尾加上一个换行符-- #+ 来避免一个混乱的屏幕输出. # Lee Maschmeyer 的解释: # --------------------- # In the [first vertical tab example] . . . the vertical tab # 在这里[第一个垂直制表符的例子中] . . . 这个垂直制表符 #+ makes the printing go straight down without a carriage return. # This is true only on devices, such as the Linux console, #+ that can't go "backward." # The real purpose of VT is to go straight UP, not down. # It can be used to print superscripts on a printer. # 它可以用来在一个打印机上打印上标. # col 的作用,可以用来模仿 VT 的合适的行为. exit 0 ################################End Script######################################### Example 4-1. 变量赋值和替换 ################################Start Script####################################### #!/bin/bash # 变量赋值和替换 a=375 hello=$a #------------------------------------------------------------------------- # 强烈注意,在赋值的前后一定不要有空格. # 如果有空格会发生什么? # 如果"VARIABLE =value", # ^ #+ 脚本将尝试运行一个"VARIABLE"的命令,带着一个"=value"参数. # 如果"VARIABLE= value", # ^ #+ script tries to run "value" command with #+ 脚本将尝试运行一个"value"的命令,带着 #+ the environmental variable "VARIABLE" set to "". #+ 一个被赋成""值的环境变量"VARIABLE". #------------------------------------------------------------------------- echo hello # 没有变量引用,不过是个 hello 字符串 echo $hello echo ${hello} # 同上 echo "$hello" echo "${hello}" echo hello="A B C D" echo $hello # A B C D echo "$hello" # A B C D # 就象你看到的 echo $hello 和 echo "$hello" 将给出不同的结果. # ^ ^ # Quoting a variable preserves whitespace. # 引用一个变量将保留其中的空白,当然,如果是变量替换就不会保留了. echo echo '$hello' # $hello # ^ ^ # 全引用的作用 #+ 将导致"$"变成一个单独的字符. # 注意两种引用不同的效果 hello= # 设置为空值 echo "\$hello (null value) = $hello" # 注意设置一个变量为空,与 unset 它,不是一回事,虽然看起来一样 # # -------------------------------------------------------------- # 可以在同一行上设置多个变量. #+ 要以空白分隔 # 小心,这会降低可读性,和可移植性. var1=21 var2=22 var3=$V3 echo echo "var1=$var1 var2=$var2 var3=$var3" # 在老版本的"sh"上,可能会有问题. # -------------------------------------------------------------- echo; echo numbers="one two three" # ^ ^ other_numbers="1 2 3" # ^ ^ # 如果变量中有空白,那么引用就必要了. # echo "numbers = $numbers" echo "other_numbers = $other_numbers" # other_numbers = 1 2 3 echo echo "uninitialized_variable = $uninitialized_variable" # Uninitialized 变量为空值(根本就没赋值). uninitialized_variable= # 声明,但是没被初始化 #+ 其实和前边设置为空值得作用是一样的. echo "uninitialized_variable = $uninitialized_variable" # 还是一个空值 uninitialized_variable=23 # 赋值 unset uninitialized_variable # Unset it. echo "uninitialized_variable = $uninitialized_variable" # 还是空值 echo exit 0 ################################End Script######################################### 注意: 一个空值变量,或者是根本就没声明的变量,在赋值之前使用它可能会引起问题. 但是还是可以用来做算术运算 ################################Start Script####################################### echo "$uninitialized" # (blank line) let "uninitialized += 5" # Add 5 to it. echo "$uninitialized" # 5 # 结论: # 对于一个空值变量在做算术操作的时候,就好像它的值为 0 一样. # This is undocumented (and probably non-portable) behavior. # 这并没被文档化(可能是不可移植)的行为. ################################End Script######################################### Example 4-2. 一般的变量赋值 ################################Start Script####################################### #!/bin/bash # "裸体"变量 echo # 变量什么时候是"裸体"的,比如前边少了$的时候. # 当它被赋值的时候,而不是被引用的时候. # 赋值 a=879 echo "The value of \"a\" is $a." # 使用 let 赋值 let a=16+5 echo "The value of \"a\" is now $a." echo # 在 for 循环中 echo -n "Values of \"a\" in the loop are: " for a in 7 8 9 11 do echo -n "$a " done echo echo # 在 read 命令状态中 echo -n "Enter \"a\" " read a echo "The value of \"a\" is now $a." echo exit 0 ################################End Script######################################### Example 4-3. 变量赋值,一般的和比较特殊的 ################################Start Script####################################### #!/bin/bash a=23 # Simple case echo $a b=$a echo $b # 现在让我们来点小变化 a=`echo Hello!` # 把 echo 命令的结果传给变量 a echo $a # 注意,如果在命令扩展结构中使用一个(!)的话,在命令行中将不能工作 #+ 因为这触发了 Bash 的"历史机制". # 但是,在校本里边使用的话,历史功能是被关闭的,所以就能够正常运行. 15 a=`ls -l` # 把 ls -l 的结果给 a echo $a # 别忘了,这么引用的话,ls 的结果中的所有空白部分都没了(包括换行) echo echo "$a" # 这么引用就正常了,保留了空白 # (具体参阅章节"引用") exit 0 ################################End Script######################################### Example 4-4 整型还是 string? ################################Start Script####################################### #!/bin/bash # int-or-string.sh: 整形还是 string? a=2334 # 整型 let "a += 1" echo "a = $a " # a = 2335 echo # 还是整型 b=${a/23/BB} # 将 23 替换成 BB # 这将把 b 变量从整型变为 string echo "b = $b" # b = BB35 declare -i b # 即使使用 declare 命令也不会对此有任何帮助,9.4 节有解释 echo "b = $b" # b = BB35 let "b += 1" # BB35 + 1 = echo "b = $b" # b = 1 echo c=BB34 echo "c = $c" # c = BB34 d=${c/BB/23} # S 将 BB 替换成 23 # 这使得$d 变为一个整形 echo "d = $d" # d = 2334 let "d += 1" # 2334 + 1 = echo "d = $d" # d = 2335 echo # 关于空变量怎么样? e="" echo "e = $e" # e = let "e += 1" # 算术操作允许一个空变量? echo "e = $e" # e = 1 echo # 空变量将转换成一个整型变量 # 关于未声明的变量怎么样? echo "f = $f" # f = let "f += 1" # 算术操作允许么? echo "f = $f" # f = 1 echo # 未声明的变量将转换成一个整型变量 # 所以说 Bash 中的变量都是无类型的. exit 0 ################################End Script######################################### Example 4-5 位置参数 ################################Start Script####################################### #!/bin/bash # 作为用例,调用这个脚本至少需要 10 个参数,如 # ./scriptname 1 2 3 4 5 6 7 8 9 10 MINPARAMS=10 echo echo "The name of this script is \"$0\"." # 添加./是为了当前目录 echo "The name of this script is \"`basename $0`\"." # 去掉目录信息,具体见'basename'命令 echo if [ -n "$1" ] # 测试变量被被引用 then echo "Parameter #1 is $1" # "#"没被转义 fi if [ -n "$2" ] then echo "Parameter #2 is $2" fi if [ -n "$3" ] then echo "Parameter #3 is $3" fi # ... if [ -n "${10}" ] # 大于 9 的参数必须出现在{}中. then echo "Parameter #10 is ${10}" fi echo "-----------------------------------" echo "All the command-line parameters are: "$*"" if [ $# -lt "$MINPARAMS" ] #$#是传到脚本里的位置参数的个数 then echo echo "This script needs at least $MINPARAMS command-line arguments!" fi echo exit 0 ################################End Script######################################### {}标记法是一种很好的使用位置参数的方法.这也需要间接引用(见 Example 34-2) 1 args=$# # 位置参数的个数 2 lastarg=${!args} 3 # 或: lastarg=${!#} 4 # 注意 lastarg=${!$#} 将报错 一些脚本可能会依赖于使用不同的调用名字,而表现出不同的行为,这样一般都需 要 判断$0,而其他的名字都是通过 ln 命令产生的链接.(具体参见 Example 12-2) 如果脚本需要一个命令行参数,而调用的时候,没用这个参数,这就有可能造成分配 一个 空变量,这样估计就会引起问题.一种解决办法就是在这个位置参数,和相关的变量 后 边,都添加一个额外的字符.具体见下边的例子. ################################Start Script####################################### variable1_=$1_ # 而不是 variable1=$1 # 这将阻止一个错误,即使在调用时没使用这个位置参数. critical_argument01=$variable1_ # 这个扩展的字符是可以被消除掉的,就像这样. variable1=${variable1_/_/} # 副作用就是$variable1_多了一个下划线 # 这里使用了一个参数替换模版(后边会有具体的讨论) # (Leaving out the replacement pattern results in a deletion.) # (在一个删除动作中,节省了一个替换模式) # 一个解决这种问题的更简单的做法就是,判断一下这个位置参数是否传递下来了 if [ -z $1 ] then exit $E_MISSING_POS_PARAM fi # 但是上边的方法将可能产生一个意外的副作用 # 参数替换的更好的办法应该是: # ${1:-$DefaultVal} # 具体察看"Parameter Substition"节 #+ 在第 9 章 ################################End Script######################################### Example 4-6 wh,whois 节点名字查询 ################################Start Script####################################### #!/bin/bash # ex18.sh # Does a 'whois domain-name' lookup on any of 3 alternate servers: # ripe.net, cw.net, radb.net # 把这个脚本重命名为'wh',然后放到/usr/local/bin 下 # 需要 3 个符号链接 # ln -s /usr/local/bin/wh /usr/local/bin/wh-ripe # ln -s /usr/local/bin/wh /usr/local/bin/wh-cw # ln -s /usr/local/bin/wh /usr/local/bin/wh-radb E_NOARGS=65 if [ -z "$1" ] then echo "Usage: `basename $0` [domain-name]" exit $E_NOARGS fi # Check script name and call proper server. # 检查脚本名字,然后调用合适的服务器 case `basename $0` in # Or: case ${0##*/} in "wh" ) whois $1@whois.ripe.net;; "wh-ripe") whois $1@whois.ripe.net;; "wh-radb") whois $1@whois.radb.net;; "wh-cw" ) whois $1@whois.cw.net;; * ) echo "Usage: `basename $0` [domain-name]";; esac exit $? ################################End Script######################################### Example 4-7 使用 shift ################################Start Script####################################### #!/bin/bash # 使用'shift'来穿过所有的位置参数. # 把这个脚本命名为 shft, #+ 并且使用一些参数来调用它,如: # ./shft a b c def 23 skidoo until [ -z "$1" ] # 知道所有参数都用光 do echo -n "$1 " shift done echo # 额外的换行. exit 0 ################################End Script######################################### Example 5-1 echo 一些诡异的变量 ################################Start Script####################################### #!/bin/bash # weirdvars.sh: echo 诡异的变量 var="'(]\\{}\$\"" echo $var # '(]\{}$" echo "$var" # '(]\{}$" 并没有什么不同 echo IFS='\' echo $var # '(] {}$" \ 转换成空格了?明显和 IFS 有关系么!又不傻! echo "$var" # '(]\{}$" exit 0 ################################End Script######################################### Example 5-2 转义符 ################################Start Script####################################### #!/bin/bash # escaped.sh: 转义符 echo; echo echo "\v\v\v\v" # 逐字的打印\v\v\v\v . # 使用-e 选项的 echo 命令来打印转义符 echo "=============" echo "VERTICAL TABS" echo -e "\v\v\v\v" # Prints 4 vertical tabs. echo "==============" echo "QUOTATION MARK" echo -e "\042" # 打印" (引号, 8 进制的 ASCII 码就是 42). echo "==============" # The $'\X' construct makes the -e option unnecessary. # 如果使用$'\X'结构,那-e 选项就不必要了 echo; echo "NEWLINE AND BEEP" echo $'\n' # 新行. echo $'\a' # Alert (beep). echo "===============" echo "QUOTATION MARKS" # 版本 2 以后 Bash 允许使用$'\nnn'结构 # 注意这种情况,'\nnn\是 8 进制 echo $'\t \042 \t' # Quote (") framed by tabs. # 当然,也可以使用 16 进制的值,使用$'\xhhh' 结构 echo $'\t \x22 \t' # Quote (") framed by tabs. # 早一点的 Bash 版本允许'\x022'这种形式 echo "===============" echo # 分配 ASCII 字符到变量中 # --------------------- quote=$'\042' # \042 是",分配到变量中 echo "$quote This is a quoted string, $quote and this lies outside the quotes." echo # Concatenating ASCII chars in a variable. # 变量中的连续的 ASCII char. triple_underline=$'\137\137\137' # 137 是 8 进制的 ASCII 码'_'. echo "$triple_underline UNDERLINE $triple_underline" echo ABC=$'\101\102\103\010' # 101, 102, 103 是 8 进制的码 A, B, C. echo $ABC echo; echo escape=$'\033' # 033 是 8 进制码 for escape. echo "\"escape\" echoes as $escape" #"escape" echoes as 没有变量被输出 echo; echo exit 0 ################################End Script######################################### 另一个关于$''字符串扩展结果的例子见 Example 34-1 \" 表达引号本身 1 echo "Hello" # Hello 2 echo "\"Hello\", he said." # "Hello", he said. \$ $号本身,跟在\$后的变量名,将不能扩展 1 echo "\$variable01" # 结果是$variable01 \\ \号本身. 1 echo "\\" # 结果是\ 2 3 # 相反的 . . . 4 5 echo "\" # 这会出现第 2 个命令提示符,说白了就是提示你命令不全,你再补个" 就 6 # 好了.如果是在脚本里,就会给出一个错误. 注意:\的行为依赖于它是否被转义,被"",或者是否在"命令替换"和"here document"中. ################################Start Script####################################### # 简单的转义和"" echo \z # z echo \\z # \z echo '\z' # \z echo '\\z' # \\z echo "\z" # \z echo "\\z" # \z # 命令替换 echo `echo \z` # z echo `echo \\z` # z echo `echo \\\z` # \z echo `echo \\\\z` # \z echo `echo \\\\\\z` # \z echo `echo \\\\\\\z` # \\z echo `echo "\z"` # \z echo `echo "\\z"` # \z # Here document cat < 4 )) # true echo "Exit status of \"(( 5 > 4 ))\" is $?." # 0 (( 5 > 9 )) # false echo "Exit status of \"(( 5 > 9 ))\" is $?." # 1 (( 5 - 5 )) # 0 echo "Exit status of \"(( 5 - 5 ))\" is $?." # 1 (( 5 / 4 )) # 除法也行 echo "Exit status of \"(( 5 / 4 ))\" is $?." # 0 (( 1 / 2 )) # 出发结果<1 echo "Exit status of \"(( 1 / 2 ))\" is $?." # 结果将为 0 # 1 (( 1 / 0 )) 2>/dev/null # 除数为 0 的错误 # ^^^^^^^^^^^ echo "Exit status of \"(( 1 / 0 ))\" is $?." # 1 # What effect does the "2>/dev/null" have? # "2>/dev/null"的作用是什么? # 如果删除"2>dev/null"将会发生什么? # Try removing it, then rerunning the script. # 尝试删除它,然后再运行脚本. exit 0 ################################End Script######################################### Example 7-4 test 死的链接文件 ################################Start Script####################################### #!/bin/bash # broken-link.sh # Written by Lee bigelow # Used with permission. #一个真正有用的 shell 脚本来找出死链接文件并且输出它们的引用 #以便于它们可以被输入到 xargs 命令中进行处理 :) #比如: broken-link.sh /somedir /someotherdir|xargs rm # #这里,不管怎么说,是一种更好的方法 # #find "somedir" -type l -print0|\ #xargs -r0 file|\ #grep "broken symbolic"| #sed -e 's/^\|: *broken symbolic.*$/"/g' # #但这不是一个纯粹的 bash,最起码现在不是. #小心:小心/proc 文件系统和任何的循环链接文件. ############################################################## #如果没对这个脚本传递参数,那么就使用当前目录. #否则就使用传递进来的参数作为目录来搜索. # #################### [ $# -eq 0 ] && directorys=`pwd` || directorys=$@ #建立函数 linkchk 来检查传进来的目录或文件是否是链接和是否存在, #并且打印出它们的引用 #如果传进来的目录有子目录, #那么把子目录也发送到 linkchk 函数中处理,就是递归目录. ########## linkchk () { for element in $1/*; do [ -h "$element" -a ! -e "$element" ] && echo \"$element\" [ -d "$element" ] && linkchk $element # Of course, '-h' tests for symbolic link, '-d' for directory. # 当然'-h'是测试链接,'-d'是测试目录. done } #如果是个可用目录,那就把每个从脚本传递进来的参数都送到 linkche 函数中. #如果不是,那就打印出错误消息和使用信息. # ################ for directory in $directorys; do if [ -d $directory ] then linkchk $directory else echo "$directory is not a directory" echo "Usage: $0 dir1 dir2 ..." fi done exit 0 ################################End Script######################################### Example 7-5 数字和字符串比较 ################################Start Script####################################### #!/bin/bash a=4 b=5 # 这里的变量 a 和 b 既可以当作整型也可以当作是字符串. # 这里在算术比较和字符串比较之间有些混淆, #+ 因为 Bash 变量并不是强类型的. # Bash 允许对整型变量操作和比较 #+ 当然变量中只包含数字字符. # 但是还是要考虑清楚再做. echo if [ "$a" -ne "$b" ] then echo "$a is not equal to $b" echo "(arithmetic comparison)" fi echo if [ "$a" != "$b" ] then echo "$a is not equal to $b." echo "(string comparison)" # "4" != "5" # ASCII 52 != ASCII 53 fi # 在这个特定的例子中,"-ne"和"!="都可以. echo exit 0 ################################End Script######################################### Example 7-6 测试字符串是否为 null ################################Start Script####################################### #!/bin/bash # str-test.sh: 测试 null 字符串和非引用字符串, #+ but not strings and sealing wax, not to mention cabbages and kings . . . #+ 上边这句没看懂 # Using if [ ... ] # 如果一个字符串没被初始化,那么它就没有定义的值(像这种话,总感觉像屁话) # 这种状态叫做"null"(与 zero 不同) if [ -n $string1 ] # $string1 没被声明和初始化 then echo "String \"string1\" is not null." else echo "String \"string1\" is null." fi # 错误的结果. # 显示$string1 为非空,虽然他没被初始化. echo # 让我们再试一下. if [ -n "$string1" ] # 这次$string1 被引用了. then echo "String \"string1\" is not null." else echo "String \"string1\" is null." fi # ""的字符串在[]结构中 echo if [ $string1 ] # 这次$string1 变成"裸体"的了 then echo "String \"string1\" is not null." else echo "String \"string1\" is null." fi # 这工作得很好. # 这个[]test 操作检测 string 是否为 null. # 然而,使用("$string1")是一种很好的习惯 # # As Stephane Chazelas points out, # if [ $string1 ] 有 1 个参数 "]" # if [ "$string1" ] 有 2 个参数,空的"$string1"和"]" echo string1=initialized if [ $string1 ] # 再来,$string1"裸体了" then echo "String \"string1\" is not null." else echo "String \"string1\" is null." fi # 再来,给出了正确的结果. # 不过怎么说("$string1")还是好很多,因为. . . string1="a = b" if [ $string1 ] # 再来,$string1 再次裸体了. then echo "String \"string1\" is not null." else echo "String \"string1\" is null." fi # 非引用的"$string1"现在给出了一个错误的结果! exit 0 # Thank you, also, Florian Wisser, for the "heads-up". ################################End Script######################################### Example 7-7 zmore ################################Start Script####################################### #!/bin/bash # zmore #使用'more'来查看 gzip 文件 NOARGS=65 NOTFOUND=66 NOTGZIP=67 if [ $# -eq 0 ] # 与 if [ -z "$1" ]同样的效果 # 应该是说前边的那句注释有问题,$1 是可以存在的,比如:zmore "" arg2 arg3 then echo "Usage: `basename $0` filename" >&2 # 错误消息到 stderr exit $NOARGS # 脚本返回 65 作为退出码. fi filename=$1 if [ ! -f "$filename" ] # 将$filename ""起来,来允许可能的空白 then echo "File $filename not found!" >&2 # 错误消息到 stderr exit $NOTFOUND fi if [ ${filename##*.} != "gz" ] # 在变量替换中使用中括号 then echo "File $1 is not a gzipped file!" exit $NOTGZIP fi zcat $1 | more # 使用过滤命令'more' # 如果你想的话也可使用'less' exit $? # 脚本将返回 pipe 的结果作为退出码 # 事实上,不用非的有"exit $?",但是不管怎么说,有了这句,能正规一些 # 将最后一句命令的执行状态作为退出码返回 ################################End Script######################################### Example 8-1 最大公约数 ################################Start Script####################################### #!/bin/bash # gcd.sh: 最大公约数 # 使用 Euclid's 算法 # 最大公约数,就是 2 个数能够同时整除的最大的数. # # Euclid's 算法采用连续除法. # 在每个循环中 #+ 被除数 <--- 除数 #+ 除数 <--- 余数 #+ 直到余数= 0. #+ 在最后的循环中 The gcd = 被除数 # # 关于这个算法更精彩的讨论 # 见 Jim Loy's site, http://www.jimloy.com/number/euclids.htm. # ------------------------------------------------------ # 参数检查 ARGS=2 E_BADARGS=65 if [ $# -ne "$ARGS" ] then echo "Usage: `basename $0` first-number second-number" exit $E_BADARGS fi # ------------------------------------------------------ gcd () { dividend=$1 # 随便给值 divisor=$2 #+ 即使$2 大,也没关系. # Why not? remainder=1 # 如果再循环中使用为初始化的变量. #+ 那将在第一次循环中产生一个错误消息. until [ "$remainder" -eq 0 ] do let "remainder = $dividend % $divisor" dividend=$divisor # 现在使用 2 个最小的数重复. divisor=$remainder done # Euclid's algorithm } # Last $dividend is the gcd. } # 最后的$dividend 就是 gcd. gcd $1 $2 echo; echo "GCD of $1 and $2 = $dividend"; echo # 练习: # -------- # 检查命令行参数来确定它们都是整数, #+ and exit the script with an appropriate error message if not. #+ 否则就选择合适的错误消息退出. exit 0 ################################End Script######################################### Example 8-2 使用算术操作符 ################################Start Script####################################### #!/bin/bash # Counting to 11 in 10 different ways. n=1; echo -n "$n " let "n = $n + 1" # let "n = n + 1" 这么写也行 echo -n "$n " : $((n = $n + 1)) # ":" 是必须的,这是因为,如果没有":"的话,Bash 将 #+ 尝试把"$((n = $n + 1))"解释成一个命令 echo -n "$n " (( n = n + 1 )) # 对于上边的方法的一个更简单的选则. # Thanks, David Lombard, for pointing this out. echo -n "$n " n=$(($n + 1)) echo -n "$n " : $[ n = $n + 1 ] # ":" 是必须的,这是因为,如果没有":"的话,Bash 将 #+ 尝试把"$[ n = $n + 1 ]" 解释成一个命令 # 即使"n"被初始化成为一个字符串,这句也能工作. echo -n "$n " n=$[ $n + 1 ] # 即使"n"被初始化成为一个字符串,这句也能工作. #* Avoid this type of construct, since it is obsolete and nonportable. #* 尽量避免这种类型的结果,因为这已经被废弃了,并且不具可移植性. # Thanks, Stephane Chazelas. echo -n "$n " # 现在来个 C 风格的增量操作. # Thanks, Frank Wang, for pointing this out. let "n++" # let "++n" also works. echo -n "$n " (( n++ )) # (( ++n ) also works. echo -n "$n " : $(( n++ )) # : $(( ++n )) also works. echo -n "$n " : $[ n++ ] # : $[ ++n ]] also works echo -n "$n " echo exit 0 ################################End Script######################################### Example 8-3 使用&&和||进行混合状态的 test ################################Start Script####################################### #!/bin/bash a=24 b=47 if [ "$a" -eq 24 ] && [ "$b" -eq 47 ] then echo "Test #1 succeeds." else echo "Test #1 fails." fi # 错误: if [ "$a" -eq 24 && "$b" -eq 47 ] #+ 尝试执行' [ "$a" -eq 24 ' #+ 因为没找到']'所以失败了. # # 注意: 如果 [[ $a -eq 24 && $b -eq 24 ]] 能够工作. # 那这个[[]]的 test 结构就比[]结构更灵活了. # # (在 17 行的"&&"与第 6 行的"&&"意义不同) # Thanks, Stephane Chazelas, for pointing this out. if [ "$a" -eq 98 ] || [ "$b" -eq 47 ] then echo "Test #2 succeeds." else echo "Test #2 fails." fi # -a 和-o 选项提供了 #+ 一种可选的混合 test 方法. # Thanks to Patrick Callahan for pointing this out. if [ "$a" -eq 24 -a "$b" -eq 47 ] then echo "Test #3 succeeds." else echo "Test #3 fails." fi if [ "$a" -eq 98 -o "$b" -eq 47 ] then echo "Test #4 succeeds." else echo "Test #4 fails." fi a=rhino b=crocodile if [ "$a" = rhino ] && [ "$b" = crocodile ] then echo "Test #5 succeeds." else echo "Test #5 fails." fi exit 0 ################################End Script######################################### Example 8-4 数字常量的处理 ################################Start Script####################################### #!/bin/bash # numbers.sh: 数字常量的几种不同的表示法 # 10 进制: 默认 let "dec = 32" echo "decimal number = $dec" # 32 # 一切都很正常 # 8 进制: 以'0'(零)开头 let "oct = 032" echo "octal number = $oct" # 26 # 表达式的结果用 10 进制表示. # # 16 进制表示:数字以'0x'或者'0X'开头 let "hex = 0x32" echo "hexadecimal number = $hex" # 50 # 表达式的结果用 10 进制表示. # 其它进制: BASE#NUMBER # BASE between 2 and 64. # 2 到 64 进制都可以. # NUMBER 必须在 BASE 的范围内,具体见下边. let "bin = 2#111100111001101" echo "binary number = $bin" # 31181 let "b32 = 32#77" echo "base-32 number = $b32" # 231 let "b64 = 64#@_" echo "base-64 number = $b64" # 4031 # 这种 64 进制的表示法中的每位数字都必须在 64 进制表示法的限制字符内. # 10 个数字+ 26 个小写字母+ 26 个大写字母+ @ + _ echo echo $((36#zz)) $((2#10101010)) $((16#AF16)) $((53#1aA)) # 1295 170 44822 3375 # 重要的注意事项: # --------------- # 如果使用的每位数字超出了这个进制表示法规定字符的范围的话, #+ 将给出一个错误消息. let "bad_oct = 081" # (部分的) 错误消息输出: # bad_oct = 081: too great for base (error token is "081") # Octal numbers use only digits in the range 0 - 7. exit 0 # Thanks, Rich Bartell and Stephane Chazelas, for clarification. ################################End Script######################################### Example 9-1 $IFS 和空白 ################################Start Script####################################### #!/bin/bash # $IFS 处理空白的方法,与处理其它字符不同. output_args_one_per_line() { for arg do echo "[$arg]" done } echo; echo "IFS=\" \"" echo "-------" IFS=" " var=" a b c " output_args_one_per_line $var # output_args_one_per_line `echo " a b c "` # # [a] # [b] # [c] echo; echo "IFS=:" echo "-----" IFS=: var=":a::b:c:::" # 与上边的一样,但是用" "替换了":" output_args_one_per_line $var # # [] # [a] # [] # [b] # [c] # [] # [] # [] # 同样的事情也会发生在 awk 中的"FS"域分隔符. # Thank you, Stephane Chazelas. echo exit 0 ################################End Script######################################### Example 12-37 也是使用$IFS 的另一个启发性的例子. $IGNOREEOF 忽略 EOF: 告诉 shell 在 log out 之前要忽略多少文件结束符(control-D). $LC_COLLATE 常在.bashrc 或/etc/profile 中设置,这个变量用来在文件名扩展和模式匹配校对顺序. 如果$LC_COLLATE 被错误的设置,那么将会在 filename globbing 中引起错误的结 果. 注意:在 2.05 以后的 Bash 版本中,filename globbing 将不在对[]中的字符区分大小写. 比如:ls [A-M]* 将即匹配 File1.txt 也会匹配 file1.txt.为了恢复[]的习惯用法, 设置$LC_COLLATE 的值为 c,使用 export LC_COLLATE=c 在/etc/profile 或者 是 ~/.bashrc 中. $LC_CTYPE 这个内部变量用来控制 globbing 和模式匹配的字符串解释. $LINENO 这个变量记录它所在的 shell 脚本中它所在行的行号.这个变量一般用于调试目的. 1 # *** BEGIN DEBUG BLOCK *** 2 last_cmd_arg=$_ # Save it. 3 4 echo "At line number $LINENO, variable \"v1\" = $v1" 5 echo "Last command argument processed = $last_cmd_arg" 6 # *** END DEBUG BLOCK *** $MACHTYPE 系统类型 提示系统硬件 bash$ echo $MACHTYPE i686 $OLDPWD 老的工作目录("OLD-print-working-directory",你所在的之前的目录) $OSTYPE 操作系统类型. bash$ echo $OSTYPE linux $PATH 指向 Bash 外部命令所在的位置,一般为/usr/bin,/usr/X11R6/bin,/usr/local/bin 等. 当给出一个命令时,Bash 将自动对$PATH 中的目录做一张 hash 表.$PATH 中以":" 分隔的 目录列表将被存储在环境变量中.一般的,系统存储的$PATH 定义在/ect/processed 或 ~/.bashrc 中(见 Appendix G). bash$ echo $PATH /bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin:/sbin:/usr/sbin PATH=${PATH}:/opt/bin 将把/opt/bin 目录附加到$PATH 变量中.在脚本中,这是一 个 添加目录到$PATH 中的便捷方法.这样在这个脚本退出的时候,$PATH 将会恢复 (因为这个 shell 是个子进程,像这样的一个脚本是不会将它的父进程的环境变量修改的) 注意:当前的工作目录"./"一般都在$PATH 中被省去. $PIPESTATUS 数组变量将保存最后一个运行的前台管道的退出码.有趣的是,这个退出码和最后 一个命令 运行的退出码并不一定相同. bash$ echo $PIPESTATUS 0 bash$ ls -al | bogus_command bash: bogus_command: command not found bash$ echo $PIPESTATUS 141 bash$ ls -al | bogus_command bash: bogus_command: command not found bash$ echo $? 127 $PIPESTATUS 数组的每个成员都会保存一个管道命令的退出码,$PIPESTATUS[0] 保存第 一个管道命令的退出码,$PIPESTATUS[1]保存第 2 个,以此类推. 注意:$PIPESTATUS 变量在一个 login shell 中可能会包含一个错误的 0 值(3.0 以下 版本) tcsh% bash bash$ who | grep nobody | sort bash$ echo ${PIPESTATUS[*]} 0 包含在脚本中的上边这行将会产生一个期望的输出 0 1 0. 注意:在某些上下文$PIPESTATUS 可能不会给出正确的结果. bash$ echo $BASH_VERSION 3.00.14(1)-release bash$ $ ls | bogus_command | wc bash: bogus_command: command not found 0 0 0 bash$ echo ${PIPESTATUS[@]} 141 127 0 Chet Ramey 把上边输出不成确原因归咎于 ls 的行为.因为如果把 ls 的结果放到管 道上, 并且这个输出没被读取,那么 SIGPIPE 将会 kill 掉它,并且退出码变为 141,而不是我 们期 望的 0.这种情况也会发生在 tr 命令中. 注意:$PIPESTATUS 是一个"volatile"变量.在任何命令插入之前,并且在 pipe 询问之 后, 这个变量需要立即被捕捉. bash$ $ ls | bogus_command | wc bash: bogus_command: command not found 0 0 0 bash$ echo ${PIPESTATUS[@]} 0 127 0 bash$ echo ${PIPESTATUS[@]} 0 $PPID 一个进程的$PPID 就是它的父进程的进程 id(pid).[1] 使用 pidof 命令对比一下. $PROMPT_COMMAND 这个变量保存一个在主提示符($PS1)显示之前需要执行的命令. $PS1 主提示符,具体见命令行上的显示. $PS2 第 2 提示符,当你需要额外的输入的时候将会显示,默认为">". $PS3 第 3 提示符,在一个 select 循环中显示(见 Example 10-29). $PS4 第 4 提示符,当使用-x 选项调用脚本时,这个提示符将出现在每行的输出前边. 默认为"+". $PWD 工作目录(你当前所在的目录). 与 pwd 内建命令作用相同. ################################Start Script####################################### #!/bin/bash E_WRONG_DIRECTORY=73 clear # 清屏. TargetDirectory=/home/bozo/projects/GreatAmericanNovel cd $TargetDirectory echo "Deleting stale files in $TargetDirectory." if [ "$PWD" != "$TargetDirectory" ] then # 防止偶然删除错误的目录 echo "Wrong directory!" echo "In $PWD, rather than $TargetDirectory!" echo "Bailing out!" exit $E_WRONG_DIRECTORY fi rm -rf * rm .[A-Za-z0-9]* # Delete dotfiles. rm .[A-Za-z0-9]* # 删除"."文件(隐含文件). # rm -f .[^.]* ..?* 为了删除以多个"."开头的文件. # (shopt -s dotglob; rm -f *) 也行. # Thanks, S.C. for pointing this out. # 文件名能够包含 0-255 范围的所有字符,除了"/". # 删除以各种诡异字符开头的文件将作为一个练习留给大家. # 这里预留给其他的必要操作. echo echo "Done." echo "Old files deleted in $TargetDirectory." echo exit 0 ################################End Script######################################### $REPLY read 命令如果没有给变量,那么输入将保存在$REPLY 中.在 select 菜单中也可用,但 是只 提供选择的变量的项数,而不是变量本身的值. ################################Start Script####################################### #!/bin/bash # reply.sh # REPLY 是'read'命令结果保存的默认变量. echo echo -n "What is your favorite vegetable? " read echo "Your favorite vegetable is $REPLY." # 当且仅当在没有变量提供给"read"命令时, #+ REPLY 才保存最后一个"read"命令读入的值. echo echo -n "What is your favorite fruit? " read fruit echo "Your favorite fruit is $fruit." echo "but..." echo "Value of \$REPLY is still $REPLY." # $REPLY 还是保存着上一个 read 命令的值, #+ 因为变量$fruit 被传入到了这个新的"read"命令中. echo exit 0 ################################End Script######################################### $SECONDS 这个脚本已经运行的时间(单位为秒). ################################Start Script####################################### #!/bin/bash TIME_LIMIT=10 INTERVAL=1 echo echo "Hit Control-C to exit before $TIME_LIMIT seconds." echo while [ "$SECONDS" -le "$TIME_LIMIT" ] do if [ "$SECONDS" -eq 1 ] then units=second else units=seconds fi echo "This script has been running $SECONDS $units." # 在一台比较慢的或者是负载很大的机器上,这个脚本可能会跳过几次循环 #+ 在一个 while 循环中. sleep $INTERVAL done echo -e "\a" # Beep! exit 0 ################################End Script######################################### Example 9-2 时间输入 ################################Start Script####################################### 1 #!/bin/bash 2 # timed-input.sh 3 4 # TMOUT=3 在新版本的 Bash 上也能工作. 5 6 7 TIMELIMIT=3 # 在这个例子上是 3 秒,也可以设其他的值. 8 9 PrintAnswer() 10 { 11 if [ "$answer" = TIMEOUT ] 12 then 13 echo $answer 14 else # 别想混合着两个例子. 15 echo "Your favorite veggie is $answer" 16 kill $! # kill 将不再需要 TimerOn 函数运行在后台. 17 # $! 是运行在后台的最后一个工作的 PID. 18 fi 19 20 } 21 22 23 24 TimerOn() 25 { 26 sleep $TIMELIMIT && kill -s 14 $$ & 27 # 等待 3 秒,然后发送一个信号给脚本. 28 } 29 30 Int14Vector() 31 { 32 answer="TIMEOUT" 33 PrintAnswer 34 exit 14 35 } 36 37 trap Int14Vector 14 # 为了我们的目的,时间中断(14)被破坏了. 38 39 echo "What is your favorite vegetable " 40 TimerOn 41 read answer 42 PrintAnswer 43 44 45 # 很明显的,这是一个拼凑的实现. 46 #+ 然而使用"-t"选项来"read"的话,将会简化这个任务. 47 # 见"t-out.sh",在下边. 48 49 # 如果你需要一个真正的幽雅的写法... 50 #+ 建议你使用 c/c++来写这个应用, 51 #+ 使用合适的库来完成这个任务,比如'alarm'和'setitimer'. 52 53 exit 0 ################################End Script######################################### Example 9-3 再来一个时间输入 ################################Start Script####################################### #!/bin/bash # timeout.sh # Stephane Chazelas 编写, #+ 本书作者进行了一些修改. INTERVAL=5 # timeout 间隔 timedout_read() { timeout=$1 varname=$2 old_tty_settings=`stty -g` stty -icanon min 0 time ${timeout}0 eval read $varname # 或者就是 read $varname stty "$old_tty_settings" # 察看"stty"的 man 页. } echo; echo -n "What's your name? Quick! " timedout_read $INTERVAL your_name # 这种方法可能不是每个终端类型都可以正常使用的. # 最大的 timeout 依赖于具体的终端. #+ (一般都是 25.5 秒). echo if [ ! -z "$your_name" ] # If name input before timeout... then echo "Your name is $your_name." else echo "Timed out." fi echo # 这个脚本的行为可能与"timed-input.sh"有点不同. # 在每次按键的时候,计数器都会重置. exit 0 ################################End Script######################################### Example 9-4 Timed read ################################Start Script####################################### #!/bin/bash # t-out.sh # "syngin seven"的一个很好的提议 (thanks). TIMELIMIT=4 # 4 seconds read -t $TIMELIMIT variable <&1 # ^^^ # 在这个例子中,对于 Bash 1.x 和 2.x 就需要使用"<&1" # 但对于 Bash 3.x 就不需要. echo if [ -z "$variable" ] # Is null? then echo "Timed out, variable still unset." else echo "variable = $variable" fi exit 0 ################################End Script######################################### Example 9-5 我是 root? ################################Start Script####################################### #!/bin/bash # am-i-root.sh: 我是不是 root 用户? ROOT_UID=0 # Root 的$UID 是 0. if [ "$UID" -eq "$ROOT_UID" ] # 是否是 root 用户,请站出来. then echo "You are root." else echo "You are just an ordinary user (but mom loves you just the same)." fi exit 0 # ============================================================= # # 下边的代码将不被执行,因为脚本已经退出了. # 检验是 root 用户的一种可选方法: ROOTUSER_NAME=root username=`id -nu` # Or... username=`whoami` if [ "$username" = "$ROOTUSER_NAME" ] then echo "Rooty, toot, toot. You are root." else echo "You are just a regular fella." fi ################################End Script######################################### Example 9-6 arglist:通过$*和$@列出所有的参数 ################################Start Script####################################### #!/bin/bash # arglist.sh # 多使用几个参数来调用这个脚本,比如"one tow three". E_BADARGS=65 if [ ! -n "$1" ] then echo "Usage: `basename $0` argument1 argument2 etc." exit $E_BADARGS fi echo index=1 # 初始化数量. echo "Listing args with \"\$*\":" for arg in "$*" # 如果"$*"不被""引用,那么将不能正常地工作 do echo "Arg #$index = $arg" let "index+=1" done # $* sees all arguments as single word. done # $* 认为所有的参数为一个单词 echo "Entire arg list seen as single word." echo index=1 # 重置数量. # 如果你忘了这句会发生什么? echo "Listing args with \"\$@\":" for arg in "$@" do echo "Arg #$index = $arg" let "index+=1" done # $@ 认为每个参数都一个单独的单词. echo "Arg list seen as separate words." echo index=1 # 重置数量. echo "Listing args with \$* (unquoted):" for arg in $* do echo "Arg #$index = $arg" let "index+=1" done # 未""引用的$*把参数作为独立的单词. echo "Arg list seen as separate words." exit 0 ################################End Script######################################### Example 9-7 不一致的$*和$@行为 ################################Start Script####################################### 1 #!/bin/bash 2 3 # "$*"和"$@"的古怪行为, 4 #+ 依赖于它们是否被""引用. 5 # 单词拆分和换行的不一致处理. 6 7 8 set -- "First one" "second" "third:one" "" "Fifth: :one" 9 # 设置这个脚本参数,$1,$2,等等. 10 11 echo 12 13 echo 'IFS unchanged, using "$*"' 14 c=0 15 for i in "$*" # 引用 16 do echo "$((c+=1)): [$i]" # 这行在下边的每个例子中都一样. 17 # Echo 参数. 18 done 19 echo --- 20 21 echo 'IFS unchanged, using $*' 22 c=0 23 for i in $* # 未引用 24 do echo "$((c+=1)): [$i]" 25 done 26 echo --- 27 28 echo 'IFS unchanged, using "$@"' 29 c=0 30 for i in "$@" 31 do echo "$((c+=1)): [$i]" 32 done 33 echo --- 34 35 echo 'IFS unchanged, using $@' 36 c=0 37 for i in $@ 38 do echo "$((c+=1)): [$i]" 39 done 40 echo --- 41 42 IFS=: 43 echo 'IFS=":", using "$*"' 44 c=0 45 for i in "$*" 46 do echo "$((c+=1)): [$i]" 47 done 48 echo --- 49 50 echo 'IFS=":", using $*' 51 c=0 52 for i in $* 53 do echo "$((c+=1)): [$i]" 54 done 55 echo --- 56 57 var=$* 58 echo 'IFS=":", using "$var" (var=$*)' 59 c=0 60 for i in "$var" 61 do echo "$((c+=1)): [$i]" 62 done 63 echo --- 64 65 echo 'IFS=":", using $var (var=$*)' 66 c=0 67 for i in $var 68 do echo "$((c+=1)): [$i]" 69 done 70 echo --- 71 72 var="$*" 73 echo 'IFS=":", using $var (var="$*")' 74 c=0 75 for i in $var 76 do echo "$((c+=1)): [$i]" 77 done 78 echo --- 79 80 echo 'IFS=":", using "$var" (var="$*")' 81 c=0 82 for i in "$var" 83 do echo "$((c+=1)): [$i]" 84 done 85 echo --- 86 87 echo 'IFS=":", using "$@"' 88 c=0 89 for i in "$@" 90 do echo "$((c+=1)): [$i]" 91 done 92 echo --- 93 94 echo 'IFS=":", using $@' 95 c=0 96 for i in $@ 97 do echo "$((c+=1)): [$i]" 98 done 99 echo --- 100 101 var=$@ 102 echo 'IFS=":", using $var (var=$@)' 103 c=0 104 for i in $var 105 do echo "$((c+=1)): [$i]" 106 done 107 echo --- 108 109 echo 'IFS=":", using "$var" (var=$@)' 110 c=0 111 for i in "$var" 112 do echo "$((c+=1)): [$i]" 113 done 114 echo --- 115 116 var="$@" 117 echo 'IFS=":", using "$var" (var="$@")' 118 c=0 119 for i in "$var" 120 do echo "$((c+=1)): [$i]" 121 done 122 echo --- 123 124 echo 'IFS=":", using $var (var="$@")' 125 c=0 126 for i in $var 127 do echo "$((c+=1)): [$i]" 128 done 129 130 echo 131 132 # 用 ksh 或者 zsh -y 来试试这个脚本. 133 134 exit 0 135 136 # This example script by Stephane Chazelas, 137 # and slightly modified by the document author. ################################End Script######################################### Example 9-8 当$IFS 为空时的$*和$@ ################################Start Script####################################### #!/bin/bash # 如果$IFS 被设置为空时, #+ 那么"$*" 和"$@" 将不会象期望那样 echo 出位置参数. mecho () # Echo 位置参数. { echo "$1,$2,$3"; } IFS="" # 设置为空. set a b c # 位置参数. mecho "$*" # abc,, mecho $* # a,b,c mecho $@ # a,b,c mecho "$@" # a,b,c # 当$IFS 设置为空时,$* 和$@ 的行为依赖于 #+ 正在运行的 Bash 或者 sh 的版本. # 所以在脚本中使用这种"feature"不是明智的行为. # Thanks, Stephane Chazelas. exit 0 ################################End Script######################################### Example 9-9 下划线变量 ################################Start Script####################################### #!/bin/bash echo $_ # /bin/bash # 只是调用/bin/bash 来运行这个脚本. du >/dev/null # 将没有命令的输出 echo $_ # du ls -al >/dev/null # 没有命令输出 echo $_ # -al (最后的参数) : echo $_ # : ################################End Script######################################### Example 9-10 在一个文本文件的段间插入空行 ################################Start Script####################################### #!/bin/bash # paragraph-space.sh # 在一个不空行的文本文件的段间插入空行. # Usage: $0 "$filename.$SUFFIX" # 转换为新的文件名. rm -f $file # 转换完毕后删除原有的文件. echo "$filename.$SUFFIX" # 从 stdout 输出反馈. done exit 0 # 练习: # -------- # 就像它现在这个样子,这个脚本把当前目录的所有文件都转换了. # # 修改这个脚本,让他只转换以".mac"为后缀的文件. ################################End Script######################################### Example 9-12 模仿 getopt 命令 ################################Start Script####################################### #!/bin/bash # getopt-simple.sh # Author: Chris Morgan # 授权使用在 ABS Guide 中. getopt_simple() { echo "getopt_simple()" echo "Parameters are '$*'" until [ -z "$1" ] do echo "Processing parameter of: '$1'" if [ ${1:0:1} = '/' ] then tmp=${1:1} # 去掉开头的'/' . . . parameter=${tmp%%=*} # 提取名字. value=${tmp##*=} # 提取值. echo "Parameter: '$parameter', value: '$value'" eval $parameter=$value fi shift done } # 传递所有的选项到 getopt_simple(). getopt_simple $* echo "test is '$test'" echo "test2 is '$test2'" exit 0 --- sh getopt_example.sh /test=value1 /test2=value2 Parameters are '/test=value1 /test2=value2' Processing parameter of: '/test=value1' Parameter: 'test', value: 'value1' Processing parameter of: '/test2=value2' Parameter: 'test2', value: 'value2' test is 'value1' test2 is 'value2' ################################End Script######################################### Example 9-13 提取字符串的一种可选的方法 ################################Start Script####################################### #!/bin/bash # substring-extraction.sh String=23skidoo1 # 012345678 Bash # 123456789 awk # 注意,对于 awk 和 Bash 来说,它们使用的是不同的 string 索引系统: # Bash 的第一个字符是从'0'开始记录的. # Awk 的第一个字符是从'1'开始记录的. echo ${String:2:4} # 位置 3 (0-1-2), 4 个字符长 # skid # awk 中等价于${string:pos:length}的命令是 substr(string,pos,length). echo | awk ' { print substr("'"${String}"'",3,4) # skid } ' # 使用一个空的"echo"通过管道给 awk 一个假的输入, #+ 这样可以不用提供一个文件名. exit 0 ################################End Script######################################### 9.2.2 更深的讨论 ~~~~~~~~~~~~~~~~ 关于在脚本中使用字符串更深的讨论,请参考 9.3 节,h 和 expr 命令列表的相关章节. 关于脚本的例子,见: Example 12-9 Example 9-16 Example 9-17 Example 9-18 Example 9-20 注意事项: [1] 这适用于命令行参数和函数参数. 9.3 参数替换 ------------ 操作和扩展变量 ${parameter} 与$parameter 相同,就是 parameter 的值.在特定的上下文中,只有少部分会产生 ${parameter}的混淆.可以组合起来一起赋指给字符串变量. 1 your_id=${USER}-on-${HOSTNAME} 2 echo "$your_id" 3 # 4 echo "Old \$PATH = $PATH" 5 PATH=${PATH}:/opt/bin #Add /opt/bin to $PATH for duration of script. 6 echo "New \$PATH = $PATH" ${parameter-default},${parameter:-default} 如果 parameter 没被 set,那么就使用 default. 1 echo ${username-`whoami`} 2 # echo `whoami`的结果,如果没 set username 变量的话. 注意:${parameter-default}和${parameter:-default}大部分时候是相同的. 额外的":"在 parameter 被声明的时候(而且被赋空值),会有一些不同. ################################Start Script####################################### #!/bin/bash # param-sub.sh # 一个变量是否被声明 #+ 将会影响默认选项的触发 #+ 甚至于这个变量被设为空. username0= echo "username0 has been declared, but is set to null." echo "username0 = ${username0-`whoami`}" # 将不会 echo. echo echo username1 has not been declared. echo "username1 = ${username1-`whoami`}" # 将会 echo. username2= echo "username2 has been declared, but is set to null." echo "username2 = ${username2:-`whoami`}" # ^ # 将会 echo 因为使用的是:-而不是 -. # 和前边的第一个例子好好比较一下. # # 再来一个: variable= # 变量已经被声明了,但是被设置为空. echo "${variable-0}" # (no output) echo "${variable:-1}" # 1 # ^ unset variable echo "${variable-2}" # 2 echo "${variable:-3}" # 3 exit 0 ################################End Script######################################### 9.2.2 更深的讨论 如果脚本中并没有传入命令行参数,那么 default parameter 将被使用. 1 DEFAULT_FILENAME=generic.data 2 filename=${1:-$DEFAULT_FILENAME} 3 # 如果没有参数被传递进来,那么下边的命令快将操作 4 #+ 文件"generic.data" 5 # 6 # 后续命令. 另外参见 Example 3-4,Example 28-2,和 Example A-6. 与"使用一个与列表来支持一个默认的命令行参数"的方法相比较. ${parameter=default},${parameter:=default} 如果 parameter 未设置,那么就设置为 default. 这两种办法绝大多数时候用法都一样,只有在$parameter 被声明并设置为空的时候, 才会有区别,[1]和上边的行为一样. 1 echo ${username=`whoami`} 2 # Variable "username" is now set to `whoami`. 2 # 变量"username"被赋值为`whoami`. ${parameter+alt_value},${parameter:+alt_value} 如果 parameter 被 set 了,那就使用 alt_value,否则就使用 null 字符串. 这两种办法绝大多数时候用法都一样,只有在$parameter 被声明并设置为空的时候, 会有区别,见下. ################################Start Script####################################### echo "###### \${parameter+alt_value} ########" echo a=${param1+xyz} echo "a = $a" # a = param2= a=${param2+xyz} echo "a = $a" # a = xyz param3=123 a=${param3+xyz} echo "a = $a" # a = xyz echo echo "###### \${parameter:+alt_value} ########" echo a=${param4:+xyz} echo "a = $a" # a = param5= a=${param5:+xyz} echo "a = $a" # a = # 与 a=${param5+xyz}有不同的结果. param6=123 a=${param6+xyz} echo "a = $a" # a = xyz ################################End Script######################################### Example 9-14 使用参数替换和 error messages ################################Start Script####################################### #!/bin/bash # 检查一些系统的环境变量. # 这是个好习惯. # 比如,如果$USER(在 console 上的用户名)没被 set, #+ 那么系统就不会认你. : ${HOSTNAME?} ${USER?} ${HOME?} ${MAIL?} echo echo "Name of the machine is $HOSTNAME." echo "You are $USER." echo "Your home directory is $HOME." echo "Your mail INBOX is located in $MAIL." echo echo "If you are reading this message," echo "critical environmental variables have been set." echo echo # ------------------------------------------------------ # ${variablename?} 结果也可以用来 #+ 在一个脚本中检查变量是否被 set. ThisVariable=Value-of-ThisVariable # 注意,顺便提一下,这个字符串变量可能在它们的名字中会被设置 #+ 非法字符 : ${ThisVariable?} echo "Value of ThisVariable is $ThisVariable". echo echo : ${ZZXy23AB?"ZZXy23AB has not been set."} # 如果 ZZXy23AB 没被 set, #+ 那么这个脚本将以一个 error message 终止. # 你可以指定错误消息. # : ${variablename?"ERROR MESSAGE"} # 同样的结果: dummy_variable=${ZZXy23AB?} # dummy_variable=${ZZXy23AB?"ZXy23AB has not been set."} # # echo ${ZZXy23AB?} >/dev/null # 同"set -u"命令来比较这些检查变量是否被 set 的方法. # echo "You will not see this message, because script already terminated." HERE=0 exit $HERE # Will NOT exit here. # 事实上,这个脚本将返回值 1 作为退出状态(echo $?). ################################End Script######################################### Example 9-15 参数替换和"usage"messages ################################Start Script####################################### #!/bin/bash # usage-message.sh : ${1?"Usage: $0 ARGUMENT"} # 如果没有命令行参数,那么脚本将在此处退出. #+ 并且打出如下的错误消息. # usage-message.sh: 1: Usage: usage-message.sh ARGUMENT echo "These two lines echo only if command-line parameter given." echo "command line parameter = \"$1\"" exit 0 # 如果有命令行参数,那么将在此处退出. # 测试这个脚本,第 1 次测试带参数,第 2 次测试不带参数. # 如果有参数,那么"$?"就是 0. # 如果没有,那么"$?"就是 1. ################################End Script######################################### Example 9-16 变量长度 ################################Start Script####################################### #!/bin/bash # length.sh E_NO_ARGS=65 if [ $# -eq 0 ] # 这个 demo 脚本必须有命令行参数. then echo "Please invoke this script with one or more command-line arguments." exit $E_NO_ARGS fi var01=abcdEFGH28ij echo "var01 = ${var01}" echo "Length of var01 = ${#var01}" # 现在,让我们试试在里边嵌入一个空格. var02="abcd EFGH28ij" echo "var02 = ${var02}" echo "Length of var02 = ${#var02}" echo "Number of command-line arguments passed to script = ${#@}" echo "Number of command-line arguments passed to script = ${#*}" exit 0 ################################End Script######################################### Example 9-17 参数替换中的模式匹配 ################################Start Script####################################### #!/bin/bash # patt-matching.sh # 使用# ## % %%来进行参数替换操作的模式匹配. var1=abcd12345abc6789 pattern1=a*c # * (通配符) 匹配 a - c 之间的任何字符. echo echo "var1 = $var1" # abcd12345abc6789 echo "var1 = ${var1}" # abcd12345abc6789 # (alternate form) echo "Number of characters in ${var1} = ${#var1}" echo echo "pattern1 = $pattern1" # a*c (everything between 'a' and 'c') echo "--------------" echo '${var1#$pattern1} =' "${var1#$pattern1}" # d12345abc6789 # 最短的可能匹配, 去掉 abcd12345abc6789 的前 3 个字符 # |-| ^^^ echo '${var1##$pattern1} =' "${var1##$pattern1}" # 6789 # 最远的匹配,去掉 abcd12345abc6789 的前 12 个字符. # |----------| ^^^^ echo; echo; echo pattern2=b*9 # 'b' 到'9'之间的任何字符 echo "var1 = $var1" # 还是 abcd12345abc6789 echo echo "pattern2 = $pattern2" echo "--------------" echo '${var1%pattern2} =' "${var1%$pattern2}" # abcd12345a # 最近的匹配, 去掉 abcd12345abc6789 的最后 6 个字符 # |----| ^^^^ echo '${var1%%pattern2} =' "${var1%%$pattern2}" # a # 最远匹配, 去掉 abcd12345abc6789 的最后 12 个字符 # |-------------| ^^^^^^ # 记住, # 和## 从字符串的左边开始,并且去掉左边的字符串, # % 和 %% 从字符串的右边开始,并且去掉右边的子串. echo exit 0 ################################End Script######################################### Example 9-18 重命名文件扩展名 ################################Start Script####################################### #!/bin/bash # rfe.sh: 重命名文件扩展名. # # 用法: rfe old_extension new_extension # # 例子: # 将指定目录的所有 *.gif 文件都重命名为 *.jpg, # 用法: rfe gif jpg E_BADARGS=65 case $# in 0|1) # "|" 在这里的意思是或操作. echo "Usage: `basename $0` old_file_suffix new_file_suffix" exit $E_BADARGS # 如果只有 0 个或 1 个参数,那么就退出. ;; esac for filename in *.$1 # 以第一个参数为扩展名的全部文件的列表 do mv $filename ${filename%$1}$2 # 从筛选出的文件中先去掉以第一参数结尾的扩展名部门, #+ 然后作为扩展名把第 2 个参数添加上. done exit 0 ################################End Script######################################### Example 9-19 使用模式匹配来分析比较特殊的字符串 ################################Start Script####################################### #!/bin/bash var1=abcd-1234-defg echo "var1 = $var1" t=${var1#*-*} echo "var1 (with everything, up to and including first - stripped out) = $t" # t=${var1#*-} 在这个例子中作用是一样的, #+ 因为 # 匹配这个最近的字符串, #+ 并且 * 匹配前边的任何字符串,包括一个空字符. # (Thanks, Stephane Chazelas, for pointing this out.) t=${var1##*-*} echo "If var1 contains a \"-\", returns empty string... var1 = $t" t=${var1%*-*} echo "var1 (with everything from the last - on stripped out) = $t" echo # ------------------------------------------- path_name=/home/bozo/ideas/thoughts.for.today # ------------------------------------------- echo "path_name = $path_name" t=${path_name##/*/} echo "path_name, stripped of prefixes = $t" # 在这个特定的例子中,与 t=`basename $path_name` 的作用一致. # t=${path_name%/}; t=${t##*/} 是一个更一般的解决办法, #+ 但有时还是不行. # 如果 $path_name 以一个新行结束, 那么`basename $path_name` 将不能工作, #+ 但是上边这个表达式可以. # (Thanks, S.C.) t=${path_name%/*.*} # 与 t=`dirname $path_name` 效果相同. echo "path_name, stripped of suffixes = $t" # 在某些情况下将失效,比如 "../", "/foo////", # "foo/", "/". # 删除后缀,尤其是在 basename 没有后缀的时候, #+ 但是 dirname 还是会使问题复杂化. # (Thanks, S.C.) echo t=${path_name:11} echo "$path_name, with first 11 chars stripped off = $t" t=${path_name:11:5} echo "$path_name, with first 11 chars stripped off, length 5 = $t" echo t=${path_name/bozo/clown} echo "$path_name with \"bozo\" replaced by \"clown\" = $t" t=${path_name/today/} echo "$path_name with \"today\" deleted = $t" t=${path_name//o/O} echo "$path_name with all o's capitalized = $t" t=${path_name//o/} echo "$path_name with all o's deleted = $t" exit 0 ################################End Script######################################### Example 9-20 对字符串的前缀或后缀使用匹配模式 ################################Start Script####################################### #!/bin/bash # var-match.sh: # 对字符串的前后缀使用匹配替换的一个样本 v0=abc1234zip1234abc # 原始变量. echo "v0 = $v0" # abc1234zip1234abc echo # 匹配字符串的前缀 v1=${v0/#abc/ABCDEF} # abc1234zip1234abc # |-| echo "v1 = $v1" # ABCDEF1234zip1234abc # |----| # 匹配字符串的后缀 v2=${v0/%abc/ABCDEF} # abc1234zip123abc # |-| echo "v2 = $v2" # abc1234zip1234ABCDEF # |----| echo # ---------------------------------------------------- # 必须在开头或结尾匹配,否则, #+ 将不会产生替换结果. # ---------------------------------------------------- v3=${v0/#123/000} # 匹配上了,但不是在字符串的开头 echo "v3 = $v3" # abc1234zip1234abc # 没替换. v4=${v0/%123/000} # 匹配上了,但不是在字符串结尾. echo "v4 = $v4" # abc1234zip1234abc # 没替换. exit 0 ################################End Script######################################### Example 9-21 使用 declare 来指定变量的类型 ################################Start Script####################################### #!/bin/bash func1 () { echo This is a function. } declare -f # 列出之前的所有函数. echo declare -i var1 # var1 是个整形. var1=2367 echo "var1 declared as $var1" var1=var1+1 # 变量声明不需使用'let'命令. echo "var1 incremented by 1 is $var1." # 尝试将变量修改为整形. echo "Attempting to change var1 to floating point value, 2367.1." var1=2367.1 # 结果将是一个错误消息,并且变量并没有被修改. echo "var1 is still $var1" echo declare -r var2=13.36 # 'declare' 允许设置变量的属性, #+ 并且同时分配变量的值. echo "var2 declared as $var2" # 尝试修改只读变量. var2=13.37 # 产生一个错误消息,并且从脚本退出了. echo "var2 is still $var2" # 这行将不会被执行. exit 0 # 脚本将不会在此处退出. ################################End Script######################################### Example 9-22 间接引用 ################################Start Script####################################### #!/bin/bash # ind-ref.sh: 间接变量引用 # 存取一个变量的值的值(这里翻译得有点拗口,不过凑合吧) a=letter_of_alphabet # 变量"a"的值是另一个变量的名字. letter_of_alphabet=z echo # 直接引用. echo "a = $a" # a = letter_of_alphabet # 间接引用. eval a=\$$a echo "Now a = $a" # Now a = z echo # 现在,让我们试试修改第 2 个引用的值. t=table_cell_3 table_cell_3=24 echo "\"table_cell_3\" = $table_cell_3" # "table_cell_3" = 24 echo -n "dereferenced \"t\" = "; eval echo \$$t # 解引用 "t" = 24 # 在这个简单的例子中,下边的表达式也能正常工作(为什么?). # eval t=\$$t; echo "\"t\" = $t" echo t=table_cell_3 NEW_VAL=387 table_cell_3=$NEW_VAL echo "Changing value of \"table_cell_3\" to $NEW_VAL." echo "\"table_cell_3\" now $table_cell_3" echo -n "dereferenced \"t\" now "; eval echo \$$t # "eval" 将获得两个参数 "echo" 和 "\$$t" (与$table_cell_3 等价) echo # (Thanks, Stephane Chazelas, 澄清了上边的行为.) # 另一个方法是使用${!t}符号,见"Bash, 版本 2"小节. # 也请参阅 ex78.sh. exit 0 ################################End Script######################################### 间接应用到底有什么应用价值?它给 Bash 添加了一种类似于 C 语言指针的功能,在 Example 34-3 中有例子.并且,还有一些其它的有趣的应用.... Nils Radtke 展示了如何建立一个"dynamic"变量名字并且取出其中的值.当 sourcing(包含)配 置 文件时,这很有用. ################################Start Script####################################### #!/bin/bash # --------------------------------------------- # 这部分内容可能来自于单独的文件. isdnMyProviderRemoteNet=172.16.0.100 isdnYourProviderRemoteNet=10.0.0.10 isdnOnlineService="MyProvider" # --------------------------------------------- remoteNet=$(eval "echo \$$(echo isdn${isdnOnlineService}RemoteNet)") remoteNet=$(eval "echo \$$(echo isdnMyProviderRemoteNet)") remoteNet=$(eval "echo \$isdnMyProviderRemoteNet") remoteNet=$(eval "echo $isdnMyProviderRemoteNet") echo "$remoteNet" # 172.16.0.100 # ================================================================ # 同时,它甚至能更好. # # 考虑下边的脚本,给出了一个变量 getSparc, #+ 但是没给出变量 getIa64: chkMirrorArchs () { arch="$1"; if [ "$(eval "echo \${$(echo get$(echo -ne $arch | sed 's/^\(.\).*/\1/g' | tr 'a-z' 'A-Z'; echo $arch | sed 's/^.\(.*\)/\1/g')):-false}")" = true ] then return 0; else return 1; fi; } getSparc="true" unset getIa64 chkMirrorArchs sparc echo $? # 0 # True chkMirrorArchs Ia64 echo $? # 1 # False # 注意: # ----- # Even the to-be-substituted variable name part is built explicitly. # The parameters to the chkMirrorArchs calls are all lower case. # The variable name is composed of two parts: "get" and "Sparc" . . . ################################End Script######################################### Example 9-23 传递一个间接引用给 awk ################################Start Script####################################### #!/bin/bash # "column totaler"脚本的另一个版本 #+ 这个版本在目标文件中添加了一个特殊的列(数字的). # 这个脚本使用了间接引用. ARGS=2 E_WRONGARGS=65 if [ $# -ne "$ARGS" ] # 检查命令行参数是否是合适的个数. then echo "Usage: `basename $0` filename column-number" exit $E_WRONGARGS fi filename=$1 column_number=$2 #===== 上边的这部分,与原来的脚本一样 =====# # 一个多行的 awk 脚本被调用,通过 ' ..... ' # awk 脚本开始. # ------------------------------------------------ awk " { total += \$${column_number} # 间接引用. } END { print total } " "$filename" # ------------------------------------------------ # awk 脚本结束. # 间接的变量引用避免了在一个内嵌的 awk 脚本中引用 #+ 一个 shell 变量的问题. # Thanks, Stephane Chazelas. exit 0 ################################End Script######################################### Example 9-24 产生随机数 ################################Start Script####################################### 1 #!/bin/bash 2 3 # $RANDOM 在每次调用的时候,返回一个不同的随机整数. 4 # 指定的范围是: 0 - 32767 (有符号的 16-bit 整数). 5 6 MAXCOUNT=10 7 count=1 8 9 echo 10 echo "$MAXCOUNT random numbers:" 11 echo "-----------------" 12 while [ "$count" -le $MAXCOUNT ] # 产生 10 ($MAXCOUNT) 个随机整数. 13 do 14 number=$RANDOM 15 echo $number 16 let "count += 1" # 数量加 1. 17 done 18 echo "-----------------" 19 20 # 如果你需要在一个特定范围内产生一个随机 int,那么使用'modulo'(模)操作. 21 # 这将返回一个除法操作的余数. 22 23 RANGE=500 24 25 echo 26 27 number=$RANDOM 28 let "number %= $RANGE" 29 # ^^ 30 echo "Random number less than $RANGE --- $number" 31 32 echo 33 34 35 36 # 如果你需要产生一个比你指定的最小边界大的随机数, 37 #+ 那么建立一个 test 循环,来丢弃所有产生对比这个数小的随机数. 38 39 FLOOR=200 40 41 number=0 #initialize 42 while [ "$number" -le $FLOOR ] 43 do 44 number=$RANDOM 45 done 46 echo "Random number greater than $FLOOR --- $number" 47 echo 48 49 # 让我们对上边的循环尝试一个小改动,也就是 50 # 让"number = $RANDOM + $FLOOR" 51 # 这将不再需要那个 while 循环,并且能够运行得更快. 52 # 但是, 这可能会产生一个问题.那么这个问题是什么呢?(译者:这很简单,有可能溢出) 53 54 55 56 # 结合上边两个例子的技术,来达到获得在指定的上下限之间来产生随机数. 57 number=0 #initialize 58 while [ "$number" -le $FLOOR ] 59 do 60 number=$RANDOM 61 let "number %= $RANGE" # 让$number 依比例落在$RANGE 范围内. 62 done 63 echo "Random number between $FLOOR and $RANGE --- $number" 64 echo 65 66 67 68 # 产生一个二元选择,就是"true"和"false"两个值. 69 BINARY=2 70 T=1 71 number=$RANDOM 72 73 let "number %= $BINARY" 74 # 注意,让"number >>= 14" 将给出一个更好的随机分配 75 #+ (右移 14 位将把所有为全部清空,除了第 15 位,因为有符号,所以第 16 位是符号位). 76 if [ "$number" -eq $T ] 77 then 78 echo "TRUE" 79 else 80 echo "FALSE" 81 fi 82 83 echo 84 85 86 # 抛骰子 87 SPOTS=6 # 模 6 给出的范围就是 0-5. 88 # 加 1 就会得到期望的范围 1 - 6. 89 # Thanks, Paulo Marcel Coelho Aragao, for the simplification. 90 die1=0 91 die2=0 92 # 是否让 SPOTS=7 比加 1 更好呢?解释行或者不行的原因? 93 94 # 每次抛骰子,都会给出均等的机会. 95 96 let "die1 = $RANDOM % $SPOTS +1" # 抛第一次. 97 let "die2 = $RANDOM % $SPOTS +1" # 抛第二次. 98 # 上边的那个算术操作,具有更高的优先级呢 -- 99 #+ 模操作(%)还是加法操作(+)? 100 101 102 let "throw = $die1 + $die2" 103 echo "Throw of the dice = $throw" 104 echo 105 106 107 exit 0 ################################End Script######################################### Example 9-25 从一副扑克牌中取出一张随机的牌 ################################Start Script####################################### #!/bin/bash # pick-card.sh # 这是一个从数组中取出随机元素的一个例子. # 取出一张牌,任何一张. Suites="Clubs Diamonds Hearts Spades" Denominations="2 3 4 5 6 7 8 9 10 Jack Queen King Ace" # 注意变量的多行展开. suite=($Suites) # 读到数组变量中. denomination=($Denominations) num_suites=${#suite[*]} # 计算有多少个元素. num_denominations=${#denomination[*]} echo -n "${denomination[$((RANDOM%num_denominations))]} of " echo ${suite[$((RANDOM%num_suites))]} # $bozo sh pick-cards.sh # Jack of Clubs # Thank you, "jipe," for pointing out this use of $RANDOM. exit 0 ################################End Script######################################### Example 9-26 两个指定值之间的随机数 ################################Start Script####################################### 1 #!/bin/bash 2 # random-between.sh 3 # 在两个指定值之间的随机数. 4 # Bill Gradwohl 编写的本脚本,本文作者作了较小的修改. 5 # 允许使用. 6 7 8 randomBetween() { 9 # 产生一个正的或者负的随机数. 10 #+ 在$max 和$max 之间 11 #+ 并且可被$divisibleBy 整除的. 12 # 给出一个合理的随机分配的返回值. 13 # 14 # Bill Gradwohl - Oct 1, 2003 15 16 syntax() { 17 # 在函数中内嵌函数 18 echo 19 echo "Syntax: randomBetween [min] [max] [multiple]" 20 echo 21 echo "Expects up to 3 passed parameters, but all are completely optional." 22 echo "min is the minimum value" 23 echo "max is the maximum value" 24 echo "multiple specifies that the answer must be a multiple of this value." 25 echo " i.e. answer must be evenly divisible by this number." 26 echo 27 echo "If any value is missing, defaults area supplied as: 0 32767 1" 28 echo "Successful completion returns 0, unsuccessful completion returns" 29 echo "function syntax and 1." 30 echo "The answer is returned in the global variable randomBetweenAnswer" 31 echo "Negative values for any passed parameter are handled correctly." 32 } 33 34 local min=${1:-0} 35 local max=${2:-32767} 36 local divisibleBy=${3:-1} 37 # 默认值分配,用来处理没有参数传递进来的时候. 38 39 local x 40 local spread 41 42 # 确认 divisibleBy 是正值. 43 [ ${divisibleBy} -lt 0 ] && divisibleBy=$((0-divisibleBy)) 44 45 # 完整性检查. 46 if [ $# -gt 3 -o ${divisibleBy} -eq 0 -o ${min} -eq ${max} ]; then 47 syntax 48 return 1 49 fi 50 51 # 察看是否 min 和 max 颠倒了. 52 if [ ${min} -gt ${max} ]; then 53 # 交换它们. 54 x=${min} 55 min=${max} 56 max=${x} 57 fi 58 59 # 如果 min 自己并不能够被$divisibleBy 整除, 60 #+ 那么就调整 min 的值,使其能够被$divisibleBy 整除,前提是不能放大范围. 61 if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ]; then 62 if [ ${min} -lt 0 ]; then 63 min=$((min/divisibleBy*divisibleBy)) 64 else 65 min=$((((min/divisibleBy)+1)*divisibleBy)) 66 fi 67 fi 68 69 # 如果 min 自己并不能够被$divisibleBy 整除, 70 #+ 那么就调整 max 的值,使其能够被$divisibleBy 整除,前提是不能放大范围. 71 if [ $((max/divisibleBy*divisibleBy)) -ne ${max} ]; then 72 if [ ${max} -lt 0 ]; then 73 max=$((((max/divisibleBy)-1)*divisibleBy)) 74 else 75 max=$((max/divisibleBy*divisibleBy)) 76 fi 77 fi 78 79 # --------------------------------------------------------------------- 80 # 现在,来做真正的工作. 81 82 # 注意,为了得到对于端点来说合适的分配, 83 #+ 随机值的范围不得不落在 84 #+ 0 和 abs(max-min)+divisibleBy 之间, 而不是 abs(max-min)+1. 85 86 # 对于端点来说, 87 #+ 这个少量的增加将会产生合适的分配. 88 89 # 修改这个公式,使用 abs(max-min)+1 来代替 abs(max-min)+divisibleBy 的话, 90 #+ 也能够产生正确的答案, 但是在这种情况下生成的随机值对于正好为端点倍数 91 #+ 的这种情况来说将是不完美的,因为在正好为端点倍数的情况的随机率比较低, 92 #+ 因为你才加 1 而已,这比正常的公式所产生的机率要小得多(正常为加 divisibleBy) 93 # --------------------------------------------------------------------- 94 95 spread=$((max-min)) 96 [ ${spread} -lt 0 ] && spread=$((0-spread)) 97 let spread+=divisibleBy 98 randomBetweenAnswer=$(((RANDOM%spread)/divisibleBy*divisibleBy+min)) 99 100 return 0 101 102 # 然而,Paulo Marcel Coelho Aragao 指出 103 #+ 当$max 和$min 不能被$divisibleBy 整除时, 104 #+ 这个公式将会失败. 105 # 106 # 他建议使用如下的公式: 107 # rnumber = $(((RANDOM%(max-min+1)+min)/divisibleBy*divisibleBy)) 108 109 } 110 111 # 让我们测试一下这个函数. 112 min=-14 113 max=20 114 divisibleBy=3 115 116 117 # 产生一个数组 answers,answers 的下标用来表示在范围内可能出现的值, 118 #+ 而内容记录的是对于这个值出现的次数,如果我们循环足够多次,一定会得到 119 #+ 一次出现机会. 120 declare -a answer 121 minimum=${min} 122 maximum=${max} 123 if [ $((minimum/divisibleBy*divisibleBy)) -ne ${minimum} ]; then 124 if [ ${minimum} -lt 0 ]; then 125 minimum=$((minimum/divisibleBy*divisibleBy)) 126 else 127 minimum=$((((minimum/divisibleBy)+1)*divisibleBy)) 128 fi 129 fi 130 131 132 # 如果 maximum 自己并不能够被$divisibleBy 整除, 133 #+ 那么就调整 maximum 的值,使其能够被$divisibleBy 整除,前提是不能放大范围. 134 135 if [ $((maximum/divisibleBy*divisibleBy)) -ne ${maximum} ]; then 136 if [ ${maximum} -lt 0 ]; then 137 maximum=$((((maximum/divisibleBy)-1)*divisibleBy)) 138 else 139 maximum=$((maximum/divisibleBy*divisibleBy)) 140 fi 141 fi 142 143 144 # 我们需要产生一个下标全为正的数组, 145 #+ 所以我们需要一个 displacement 来保正都为正的结果. 146 147 148 displacement=$((0-minimum)) 149 for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do 150 answer[i+displacement]=0 151 done 152 153 154 # 现在我们循环足够多的次数来得到我们想要的答案. 155 loopIt=1000 # 脚本作者建议 100000, 156 #+ 但是这实在是需要太长的时间了. 157 158 for ((i=0; i<${loopIt}; ++i)); do 159 160 # 注意,我们在这里调用 randomBetween 函数时,故意将 min 和 max 颠倒顺序 161 #+ 我们是为了测试在这种情况下,此函数是否还能得到正确的结果. 162 163 randomBetween ${max} ${min} ${divisibleBy} 164 165 # 如果答案不是我们所预期的,那么就报告一个错误. 166 [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] && echo MIN or MAX error - ${randomBetweenAnswer}! 167 [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] && echo DIVISIBLE BY error - ${randomBetweenAnswer}! 168 169 # 将统计值存到 answer 之中. 170 answer[randomBetweenAnswer+displacement]=$((answer[randomBetweenAnswer+displa cement]+1)) 171 done 172 173 174 175 # 让我们察看一下结果 176 177 for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do 178 [ ${answer[i+displacement]} -eq 0 ] && echo "We never got an answer of $i." || echo "${i} occurred ${answer[i+displacement]} times." 179 done 180 181 182 exit 0 ################################End Script######################################### Example 9-27 使用随机数来摇一个骰子 ################################Start Script####################################### #!/bin/bash # RANDOM 到底有多 random? RANDOM=$$ # 使用脚本的进程 ID 来作为随机数的产生种子. PIPS=6 # 一个骰子有 6 面. MAXTHROWS=600 # 如果你没别的事干,那么可以增加这个数值. throw=0 # 抛骰子的次数. ones=0 # 必须把所有 count 都初始化为 0, twos=0 #+ 因为未初始化的变量为 null,不是 0. threes=0 fours=0 fives=0 sixes=0 print_result () { echo echo "ones = $ones" echo "twos = $twos" echo "threes = $threes" echo "fours = $fours" echo "fives = $fives" echo "sixes = $sixes" echo } update_count() { case "$1" in 0) let "ones += 1";; # 因为骰子没有 0,所以给 1. 1) let "twos += 1";; # 对 tows 做同样的事. 2) let "threes += 1";; 3) let "fours += 1";; 4) let "fives += 1";; 5) let "sixes += 1";; esac } echo while [ "$throw" -lt "$MAXTHROWS" ] do let "die1 = RANDOM % $PIPS" update_count $die1 let "throw += 1" done print_result exit 0 # 如果 RANDOM 是真正的随机,那么摇出来结果应该平均的. # $MAXTHROWS 设为 600,那么每面都应该为 100,上下的出入不应该超过 20. # # 记住 RANDOM 毕竟只是一个伪随机数, #+ 并且不是十分完美的. # 随机数的产生是一个深奥并复杂的问题. # 足够长的随机序列,不但会展现杂乱无章的一面, #+ 而且会展现机会均等的一面. # 一个很简单的练习: # ----------------- # 重写这个例子,做成抛 1000 次硬币的形式. # 分为正反两面. ################################End Script######################################### Example 9-28 重新分配随机数种子 ################################Start Script####################################### #!/bin/bash # seeding-random.sh: 设置 RANDOM 变量作为种子. MAXCOUNT=25 # 决定产生多少个随机数. random_numbers () { count=0 while [ "$count" -lt "$MAXCOUNT" ] do number=$RANDOM echo -n "$number " let "count += 1" done } echo; echo RANDOM=1 # 为随机数的产生设置 RANDOM 种子. random_numbers echo; echo RANDOM=1 # 设置同样的种子... random_numbers # ...将会和上边产生的随机数列相同. # # 复制一个相同的随机数序列在什么时候有用呢? echo; echo RANDOM=2 # 再试一下,但这次使用不同的种子... random_numbers # 将给出一个不同的随机数序列. echo; echo # RANDOM=$$ 使用脚本的进程 id 作为随机数的种子. # 从'time'或'date'命令中取得 RANDOM 作为种子也是很常用的办法. # 一个有想象力的方法... SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }') # 首先从/dev/urandom(系统伪随机设备文件)中取出 1 行, #+ 然后着这个可打印行转换为(8 进制)数,通过使用"od"命令, #+ 最后使用"awk"来获得一个数, #+ 这个数将作为随机数产生的种子. RANDOM=$SEED random_numbers echo; echo exit 0 ################################End Script######################################### Example 9-29 使用 awk 产生伪随机数 ################################Start Script####################################### #!/bin/bash # random2.sh: 产生一个范围 0 - 1 的为随机数. # 使用 awk 的 rand()函数. AWKSCRIPT=' { srand(); print rand() } ' # Command(s) / 传到 awk 中的参数 # 注意,srand()函数用来产生 awk 的随机数种子. echo -n "Random number between 0 and 1 = " echo | awk "$AWKSCRIPT" # 如果你省去'echo'那么将发生什么? exit 0 # Exercises: # 练习: # ----- # 1) 使用循环结构,打印出 10 个不同的随机数. # (提示: 在循环的每次执行过程中,你必须使用"srand()"函数来生成不同的 #+ 种子.如果你没做这件事那么将发生什么? # 2) 使用一个整数乘法作为一个放缩因子,在 10 到 100 的范围之间, #+ 来产生随机数. # 3) 同上边的练习 #2,但这次产生随机整数. ################################End Script######################################### Example 9-30 C 风格的变量处理 ################################Start Script####################################### 1 #!/bin/bash 2 # 处理一个变量,C 风格,使用((...))结构. 3 4 5 echo 6 7 (( a = 23 )) # 给一个变量赋值,从"="两边的空格就能看出这是 c 风格的处理. 8 echo "a (initial value) = $a" 9 10 (( a++ )) # 变量'a'后加 1,C 风格. 11 echo "a (after a++) = $a" 12 13 (( a-- )) # 变量'a'后减 1,C 风格. 14 echo "a (after a--) = $a" 15 16 17 (( ++a )) # 变量'a'预加 1,C 风格. 18 echo "a (after ++a) = $a" 19 20 (( --a )) # 变量'a'预减 1,C 风格. 21 echo "a (after --a) = $a" 22 23 echo 24 25 ######################################################## 26 # 注意:在 C 语言中,预减和后减操作 27 #+ 会有些不同的副作用. 28 29 n=1; let --n && echo "True" || echo "False" # False 30 n=1; let n-- && echo "True" || echo "False" # True 31 32 # Thanks, Jeroen Domburg. 33 ######################################################## 34 35 echo 36 37 (( t = a<45?7:11 )) # C 风格的 3 元操作. 38 echo "If a < 45, then t = 7, else t = 11." 39 echo "t = $t " # Yes! 40 41 echo 42 43 44 # ---------------- 45 # 复活节彩蛋注意! 46 # ---------------- 47 # Chet Ramey 显然的偷偷摸摸的做了一些未公开的 C 风格的结构 48 #+ 放在 Bash 中(准确地说是根据 ksh 来改写的,这更接近些) 49 # 在 Bash 文档中,Ramey 调用((...))shell 算法, 50 #+ 但是它可以走得更远. 51 # 对不起, Chet, 现在秘密被公开了. 52 53 # See also "for" and "while" loops using the ((...)) construct. 53 # 也参考一些"for"和"while"循环中使用((...))结构的例子. 54 55 # 这些只能工作在 2.04 或者更高版本的 Bash 中. 56 57 exit 0 ################################End Script######################################### Example 10-1 循环的一个简单例子 ################################Start Script####################################### #!/bin/bash # 列出所有行星. for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto do echo $planet # Each planet on a separate line. done echo for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto" # 所有的行星都在同一行上. # 完整的'list'作为一个变量都封在""中 do echo $planet done exit 0 ################################End Script######################################### Example 10-2 每个[list]元素带两个参数的 for 循环 ################################Start Script####################################### #!/bin/bash # 还是行星. # 分配行星的名字和它距太阳的距离. for planet in "Mercury 36" "Venus 67" "Earth 93" "Mars 142" "Jupiter 483" do set -- $planet # 解析变量"planet"并且设置位置参数. # "--" 将防止$planet 为空,或者是以一个破折号开头. # 可能需要保存原始的位置参数,因为它们被覆盖了. # 一种方法就是使用数组, # original_params=("$@") echo "$1 $2,000,000 miles from the sun" #-------two tabs---把后边的 0 和$2 连接起来 done # (Thanks, S.C., for additional clarification.) exit 0 ################################End Script######################################### Example 10-3 文件信息:对包含在变量中的文件列表进行操作 ################################Start Script####################################### #!/bin/bash # fileinfo.sh FILES="/usr/sbin/accept /usr/sbin/pwck /usr/sbin/chroot /usr/bin/fakefile /sbin/badblocks /sbin/ypbind" # 你关心的文件列表. # 扔进去一个假文件, /usr/bin/fakefile. echo for file in $FILES do if [ ! -e "$file" ] # 检查文件是否存在. then echo "$file does not exist."; echo continue # 继续下一个. fi ls -l $file | awk '{ print $9 " file size: " $5 }' # 打印 2 个域. whatis `basename $file` # 文件信息. # 注意 whatis 数据库需要提前建立好. # 要想达到这个目的, 以 root 身份运行/usr/bin/makewhatis. echo done exit 0 ################################End Script######################################### Example 10-4 在 for 循环中操作文件 ################################Start Script####################################### #!/bin/bash # list-glob.sh: 产生 [list] 在 for 循环中, 使用 "globbing" echo for file in * # ^ 在表达式中识别 file globbing 时, #+ Bash 将执行文件名扩展 do ls -l "$file" # 列出所有在$PWD(当前目录)中的所有文件. # 回想一下,通配符"*"能够匹配所有文件, #+ 然而,在"globbing"中,是不能比配"."文件的. # If the pattern matches no file, it is expanded to itself. # 如果没匹配到任何文件,那它将扩展成自己. # 为了不让这种情况发生,那就设置 nullglob 选项 #+ (shopt -s nullglob). # Thanks, S.C. done echo; echo for file in [jx]* do rm -f $file # 只删除当前目录下以"j"或"x"开头的文件. echo "Removed file \"$file\"". done echo exit 0 ################################End Script######################################### Example 10-5 在 for 循环中省略[list] ################################Start Script####################################### #!/bin/bash # 使用两种方法来调用这个脚本,一种是带参数的情况,另一种不带参数. #+ 观察此脚本的行为各是什么样的? for a do echo -n "$a " done # The 'in list' missing, therefore the loop operates on '$@' # 没有[list],所以循环将操作'$@' #+ (包括空白的命令参数列表). echo exit 0 ################################End Script######################################### Example 10-6 使用命令替换来产生 for 循环的[list] ################################Start Script####################################### #!/bin/bash # for-loopcmd.sh: 带[list]的 for 循环 #+ [list]是由命令替换产生的. NUMBERS="9 7 3 8 37.53" for number in `echo $NUMBERS` # for number in 9 7 3 8 37.53 do echo -n "$number " done echo exit 0 ################################End Script######################################### Example 10-7 对于二进制文件的一个 grep 替换 ################################Start Script####################################### #!/bin/bash # bin-grep.sh: 在一个二进制文件中定位匹配字串. # 对于二进制文件的一个 grep 替换 # 与"grep -a"的效果相似 E_BADARGS=65 E_NOFILE=66 if [ $# -ne 2 ] then echo "Usage: `basename $0` search_string filename" exit $E_BADARGS fi if [ ! -f "$2" ] then echo "File \"$2\" does not exist." exit $E_NOFILE fi IFS="\n" # 由 Paulo Marcel Coelho Aragao 提出的建议. for word in $( strings "$2" | grep "$1" ) # "strings" 命令列出二进制文件中的所有字符串. # 输出到管道交给"grep",然后由 grep 命令来过滤字符串. do echo $word done # S.C. 指出, 行 23 - 29 可以被下边的这行来代替, # strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]' # 试试用"./bin-grep.sh mem /bin/ls"来运行这个脚本. exit 0 ################################End Script######################################### Example 10-8 列出系统上的所有用户 ################################Start Script####################################### #!/bin/bash # userlist.sh PASSWORD_FILE=/etc/passwd n=1 # User number for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" ) # 域分隔 = : ^^^^^^ # 打印出第一个域 ^^^^^^^^ # 从 password 文件中取得输入 ^^^^^^^^^^^^^^^^^ do echo "USER #$n = $name" let "n += 1" done # USER #1 = root # USER #2 = bin # USER #3 = daemon # ... # USER #30 = bozo exit 0 # 练习 : # ------ # 一个普通用户(或者是一个普通用户运行的脚本) #+ 怎么能读取/etc/password 呢? # 这是否是一个安全漏洞? 为什么是?为什么不是? ################################End Script######################################### Example 10-9 在目录的所有文件中查找源字串 ################################Start Script####################################### #!/bin/bash # findstring.sh: # 在一个指定目录的所有文件中查找一个特定的字符串. directory=/usr/bin/ fstring="Free Software Foundation" # 查看那个文件中包含 FSF. for file in $( find $directory -type f -name '*' | sort ) do strings -f $file | grep "$fstring" | sed -e "s%$directory%%" # 在"sed"表达式中, #+ 我们必须替换掉正常的替换分隔符"/", #+ 因为"/"碰巧是我们需要过滤的字串之一. # 如果不用"%"代替"/"作为分隔符,那么这个操作将失败,并给出一个错误消息.(试试) done exit 0 # 练习 (easy): # ------------ # 将内部用的$directory 和$fstring 变量,用从 #+ 命令行参数代替. ################################End Script######################################### Example 10-10 列出目录中所有的符号连接文件 ################################Start Script####################################### #!/bin/bash # symlinks.sh: 列出目录中所有的符号连接文件. directory=${1-`pwd`} # 如果没有其他的特殊指定, #+ 默认为当前工作目录. # 下边的代码块,和上边这句等价. # ---------------------------------------------------------- # ARGS=1 # 需要一个命令行参数. # # if [ $# -ne "$ARGS" ] # 如果不是一个参数的话... # then # directory=`pwd` # 当前工作目录 # else # directory=$1 # fi # ---------------------------------------------------------- echo "symbolic links in directory \"$directory\"" for file in "$( find $directory -type l )" # -type l 就是符号连接文件 do echo "$file" done | sort # 否则列出的文件将是未排序的 # 严格上说,此处并不一定非要一个循环不可, #+ 因为"find"命令的结果将被扩展成一个单词. # 然而,这种方式很容易理解和说明. # Dominik 'Aeneas' Schnitzer 指出, #+ 如果没将 $( find $directory -type l )用""引用起来的话 #+ 那么将会把一个带有空白部分的文件名拆成以空白分隔的两部分(文件名中允许有空白). # 即使这只将取出每个参数的第一个域. exit 0 # Jean Helou 建议使用下边的方法: echo "symbolic links in directory \"$directory\"" # 当前 IFS 的备份.要小心使用这个值. OLDIFS=$IFS IFS=: for file in $(find $directory -type l -printf "%p$IFS") do # ^^^^^^^^^^^^^^^^ echo "$file" done|sort ################################End Script######################################### Example 10-11 将目录中的符号连接文件名保存到一个文件中 ################################Start Script####################################### #!/bin/bash # symlinks.sh: 列出目录中所有的符号连接文件. OUTFILE=symlinks.list # 保存的文件 directory=${1-`pwd`} # 如果没有其他的特殊指定, #+ 默认为当前工作目录. echo "symbolic links in directory \"$directory\"" > "$OUTFILE" echo "---------------------------" >> "$OUTFILE" for file in "$( find $directory -type l )" # -type l 为符号链接 do echo "$file" done | sort >> "$OUTFILE" # 循环的输出 # ^^^^^^^^^^^^^ 重定向到一个文件中 exit 0 ################################End Script######################################### Example 10-12 一个 C 风格的 for 循环 ################################Start Script####################################### #!/bin/bash # 两种循环到 10 的方法. echo # 标准语法. for a in 1 2 3 4 5 6 7 8 9 10 do echo -n "$a " done echo; echo # +==========================================+ # 现在, 让我们用 C 风格的语法做同样的事. LIMIT=10 for ((a=1; a <= LIMIT ; a++)) # Double parentheses, and "LIMIT" with no "$". for ((a=1; a <= LIMIT ; a++)) # 双圆括号, 并且"LIMIT"变量前边没有 "$". do echo -n "$a " done # 这是一个借用'ksh93'的结构. echo; echo # +===================================================================== ====+ # 让我们使用 C 的逗号操作符,来同时增加两个变量的值. for ((a=1, b=1; a <= LIMIT ; a++, b++)) # 逗号将同时进行 2 条操作. do echo -n "$a-$b " done echo; echo exit 0 ################################End Script######################################### Example 10-13 在 batch mode 中使用 efax ################################Start Script####################################### #!/bin/bash # Faxing ('fax' 必须已经被安装过了). EXPECTED_ARGS=2 E_BADARGS=65 if [ $# -ne $EXPECTED_ARGS ] # 检查命令行参数的个数是否正确. then echo "Usage: `basename $0` phone# text-file" exit $E_BADARGS fi if [ ! -f "$2" ] then echo "File $2 is not a text file" exit $E_BADARGS fi fax make $2 # 从文本文件中创建传真格式的文件. for file in $(ls $2.0*) # 连接转换过的文件. # 在变量列表中使用通配符. do fil="$fil $file" done efax -d /dev/ttyS3 -o1 -t "T$1" $fil # 干活的地方. # S.C. 指出, 通过下边的命令可以省去 for 循环. # efax -d /dev/ttyS3 -o1 -t "T$1" $2.0* # 但这并不十分有讲解意义[嘿嘿]. exit 0 ################################End Script######################################### Example 10-14 简单的 while 循环 ################################Start Script####################################### #!/bin/bash var0=0 LIMIT=10 while [ "$var0" -lt "$LIMIT" ] do echo -n "$var0 " # -n 将会阻止产生新行. # ^ 空格,数字之间的分隔. var0=`expr $var0 + 1` # var0=$(($var0+1)) 也可以. # var0=$((var0 + 1)) 也可以. # let "var0 += 1" 也可以. done # 使用其他的方法也行. echo exit 0 ################################End Script######################################### Example 10-15 另一个 while 循环 ################################Start Script####################################### #!/bin/bash echo # 等价于: while [ "$var1" != "end" ] # while test "$var1" != "end" do echo "Input variable #1 (end to exit) " read var1 # 为什么不使用'read $var1'? echo "variable #1 = $var1" # 因为包含"#"字符,所以需要"" # 如果输入为'end',那么就在这里 echo. # 不在这里判断结束,在循环顶判断. echo done exit 0 ################################End Script######################################### Example 10-16 多条件的 while 循环 ################################Start Script####################################### #!/bin/bash var1=unset previous=$var1 while echo "previous-variable = $previous" echo previous=$var1 [ "$var1" != end ] # 记录之前的$var1. # 这个"while"循环中有 4 个条件, 但是只有最后一个能控制循环. # 退出状态由第 4 个条件决定. do echo "Input variable #1 (end to exit) " read var1 echo "variable #1 = $var1" done # 尝试理解这个脚本的运行过程. # 这里还是有点小技巧的. exit 0 ################################End Script######################################### Example 10-17 C 风格的 while 循环 ################################Start Script####################################### #!/bin/bash # wh-loopc.sh: 循环 10 次的 while 循环. LIMIT=10 a=1 while [ "$a" -le $LIMIT ] do echo -n "$a " let "a+=1" done # 到目前为止都没什么令人惊奇的地方. echo; echo # +=================================================================+ # 现在, 重复 C 风格的语法. ((a = 1)) # a=1 # 双圆括号允许赋值两边的空格,就像 C 语言一样. while (( a <= LIMIT )) # 双圆括号, 变量前边没有"$". do echo -n "$a " ((a += 1)) # let "a+=1" # Yes, 看到了吧. # 双圆括号允许像 C 风格的语法一样增加变量的值. done echo # 现在,C 程序员可以在 Bash 中找到回家的感觉了吧. exit 0 ################################End Script######################################### Example 10-18 until 循环 ################################Start Script####################################### #!/bin/bash END_CONDITION=end until [ "$var1" = "$END_CONDITION" ] # 在循环的顶部判断条件. do echo "Input variable #1 " echo "($END_CONDITION to exit)" read var1 echo "variable #1 = $var1" echo done exit 0 ################################End Script######################################### Example 10-19 嵌套循环 ################################Start Script####################################### #!/bin/bash # nested-loop.sh: 嵌套的"for" 循环. outer=1 # 设置外部循环计数. # 开始外部循环. for a in 1 2 3 4 5 do echo "Pass $outer in outer loop." echo "---------------------" inner=1 # 重设内部循环的计数. # =============================================== # 开始内部循环. for b in 1 2 3 4 5 do echo "Pass $inner in inner loop." let "inner+=1" # 增加内部循环计数. done # 内部循环结束. # =============================================== let "outer+=1" # 增加外部循环的计数. echo # 每次外部循环之间的间隔. done # 外部循环结束. exit 0 ################################End Script######################################### Example 10-20 break 和 continue 命令在循环中的效果 ################################Start Script####################################### #!/bin/bash LIMIT=19 # 上限 echo echo "Printing Numbers 1 through 20 (but not 3 and 11)." a=0 while [ $a -le "$LIMIT" ] do a=$(($a+1)) if [ "$a" -eq 3 ] || [ "$a" -eq 11 ] # 除了 3 和 11. then continue # 跳过本次循环剩下的语句. fi echo -n "$a " # 在$a 等于 3 和 11 的时候,这句将不会执行. done # 练习: # 为什么循环会打印出 20? echo; echo echo Printing Numbers 1 through 20, but something happens after 2. ################################################################## # Same loop, but substituting 'break' for 'continue'. # 同样的循环, 但是用'break'来代替'continue'. a=0 while [ "$a" -le "$LIMIT" ] do a=$(($a+1)) if [ "$a" -gt 2 ] then break # 将会跳出整个循环. fi echo -n "$a " done echo; echo; echo exit 0 ################################End Script######################################### Example 10-21 多层循环的退出 ################################Start Script####################################### #!/bin/bash # break-levels.sh: 退出循环. # "break N" 退出 N 层循环. for outerloop in 1 2 3 4 5 do echo -n "Group $outerloop: " # -------------------------------------------------------- for innerloop in 1 2 3 4 5 do echo -n "$innerloop " if [ "$innerloop" -eq 3 ] then break # 试试 break 2 来看看发生什么. # (内部循环和外部循环都被退出了.) fi done # -------------------------------------------------------- echo done echo exit 0 ################################End Script######################################### Example 10-22 多层循环的 continue ################################Start Script####################################### #!/bin/bash # "continue N" 命令, 将让 N 层的循环全部被 continue. for outer in I II III IV V # 外部循环 do echo; echo -n "Group $outer: " # -------------------------------------------------------------------- for inner in 1 2 3 4 5 6 7 8 9 10 # 内部循环 do if [ "$inner" -eq 7 ] then continue 2 # continue 2 层, 也就是到 outer 循环上. # 将"continue 2"替换为一个单独的"continue" # 来看一下一个正常循环的行为. fi echo -n "$inner " # 7 8 9 10 将不会被 echo done # -------------------------------------------------------------------- 22 #译者注:如果在此处添加 echo 的话,当然也不会输出. done echo; echo # 练习: # 准备一个有意义的"continue N"的使用,放在脚本中. exit 0 ################################End Script######################################### Example 10-23 在实际的任务中使用"continue N" ################################Start Script####################################### # Albert Reiner 给出了一个关于使用"continue N"的例子: # --------------------------------------------------- # Suppose I have a large number of jobs that need to be run, with #+ any data that is to be treated in files of a given name pattern in a #+ directory. There are several machines that access this directory, and #+ I want to distribute the work over these different boxen. Then I #+ usually nohup something like the following on every box: while true do for n in .iso.* do [ "$n" = ".iso.opts" ] && continue beta=${n#.iso.} [ -r .Iso.$beta ] && continue [ -r .lock.$beta ] && sleep 10 && continue lockfile -r0 .lock.$beta || continue echo -n "$beta: " `date` run-isotherm $beta date ls -alF .Iso.$beta [ -r .Iso.$beta ] && rm -f .lock.$beta continue 2 done break done # The details, in particular the sleep N, are particular to my #+ application, but the general pattern is: while true do for job in {pattern} do {job already done or running} && continue {mark job as running, do job, mark job as done} continue 2 done break # Or something like `sleep 600' to avoid termination. done # This way the script will stop only when there are no more jobs to do #+ (including jobs that were added during runtime). Through the use #+ of appropriate lockfiles it can be run on several machines #+ concurrently without duplication of calculations [which run a couple #+ of hours in my case, so I really want to avoid this]. Also, as search #+ always starts again from the beginning, one can encode priorities in #+ the file names. Of course, one could also do this without `continue 2', #+ but then one would have to actually check whether or not some job #+ was done (so that we should immediately look for the next job) or not #+ (in which case we terminate or sleep for a long time before checking #+ for a new job). ################################End Script######################################### Example 10-24 使用 case ################################Start Script####################################### #!/bin/bash # 测试字符串范围 echo; echo "Hit a key, then hit return." read Keypress case "$Keypress" in [[:lower:]] ) echo "Lowercase letter";; [[:upper:]] ) echo "Uppercase letter";; [0-9] ) echo "Digit";; * ) echo "Punctuation, whitespace, or other";; esac # Allows ranges of characters in [square brackets], esac # 允许字符串的范围出现在[]中, #+ or POSIX ranges in [[double square brackets. #+ 或者 POSIX 范围在[[中. # 在这个例子的第一个版本中, #+ 测试大写和小写字符串使用的是 #+ [a-z] 和 [A-Z]. # 这种用法将不会在某些特定的场合或 Linux 发行版中正常工作. # POSIX 更具可移植性. # 感谢 Frank Wang 指出这点. # 练习: # ----- # 就像这个脚本所表现的,它只允许单个按键,然后就结束了. # 修改这个脚本,让它能够接受重复输入, #+ 报告每个按键,并且只有在"X"被键入时才结束. # 暗示: 将这些代码都用"while"循环圈起来. exit 0 ################################End Script######################################### Example 10-25 使用 case 来创建菜单 ################################Start Script####################################### #!/bin/bash # 未经处理的地址资料 clear # 清屏. echo " Contact List" echo " ------- ----" echo "Choose one of the following persons:" echo echo "[E]vans, Roland" echo "[J]ones, Mildred" echo "[S]mith, Julie" echo "[Z]ane, Morris" echo read person case "$person" in # 注意,变量是被 引用的 . "E" | "e" ) # 接受大写或小写输入 . echo echo "Roland Evans" echo "4321 Floppy Dr." echo "Hardscrabble, CO 80753" echo "(303) 734-9874" echo "(303) 734-9892 fax" echo "revans@zzy.net" echo "Business partner & old friend" ;; # 注意,在每个选项后边都需要以 ;;结尾. "J" | "j" ) echo echo "Mildred Jones" echo "249 E. 7th St., Apt. 19" echo "New York, NY 10009" echo "(212) 533-2814" echo "(212) 533-9972 fax" echo "milliej@loisaida.com" echo "Ex-girlfriend" echo "Birthday: Feb. 11" ;; # 后边的 Smith 和 Zane 的信息在这里就省略了 . * ) # 默认选项 . # 空输入 (敲 RETURN). echo echo "Not yet in database." ;; esac echo # 练习: # ----- # 修改这个脚本,让它能够接受多输入, #+ 并且能够显示多个地址. exit 0 ################################End Script######################################### 一个 case 的特殊用法,用来测试命令行参数. ################################Start Script####################################### #! /bin/bash case "$1" in "") echo "Usage: ${0##*/} "; exit $E_PARAM;; # 没有命令行参数, # 或者第一个参数为空. # Note that ${0##*/} is ${var##pattern} param substitution. Net result is $0. # 注意:${0##*/} 是${var##pattern} 这种模式的替换. 得到的结果是$0. -*) FILENAME=./$1;; # 如果传递进来的文件名参数($1)以一个破折号开头, #+ 那么用./$1 来代替 #+ 这样后边的命令将不会把它作为一个选项来解释. * ) FILENAME=$1;; # 否则, $1. esac ################################End Script######################################### 这是一个更容易懂的命令行参数处理的一个例子. ################################Start Script####################################### #! /bin/bash while [ $# -gt 0 ]; do # 直到你用完所有的参数... case "$1" in -d|--debug) # "-d" or "--debug" parameter? DEBUG=1 ;; -c|--conf) CONFFILE="$2" shift if [ ! -f $CONFFILE ]; then echo "Error: Supplied file doesn't exist!" exit $E_CONFFILE # 文件没发现错误. fi ;; esac shift # 检查剩下的参数. done # 来自 Stefano Falsetto 的 "Log2Rot" 脚本, #+ 他的"rottlog" 包的一部分. # 授权使用. ################################End Script######################################### Example 10-26 使用命令替换来产生 case 变量 ################################Start Script####################################### #!/bin/bash # case-cmd.sh: 使用命令替换来产生"case"变量 case $( arch ) in # "arch" 返回机器的类型. # 等价于 'uname -m' ... i386 ) echo "80386-based machine";; i486 ) echo "80486-based machine";; i586 ) echo "Pentium-based machine";; i686 ) echo "Pentium2+-based machine";; * ) echo "Other type of machine";; esac exit 0 ################################End Script######################################### Example 10-27 简单字符串匹配 ################################Start Script####################################### #!/bin/bash # match-string.sh: 简单字符串匹配 match_string () { MATCH=0 NOMATCH=90 PARAMS=2 # 函数需要 2 个参数. BAD_PARAMS=91 [ $# -eq $PARAMS ] || return $BAD_PARAMS case "$1" in "$2") return $MATCH;; * ) return $NOMATCH;; esac } a=one b=two c=three d=two match_string $a # 参数个数错误. echo $? # 91 match_string $a $b # 不匹配 echo $? # 90 match_string $b $d # 匹配 echo $? # 0 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######################################### Example 10-29 用 select 来创建菜单 ################################Start Script####################################### #!/bin/bash PS3='Choose your favorite vegetable: ' # 设置提示符字串. echo select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas" do echo echo "Your favorite veggie is $vegetable." echo "Yuck!" echo break # 如果这里没有'break'会发生什么? done exit 0 ################################End Script######################################### Example 10-30 用函数中 select 结构来创建菜单 ################################Start Script####################################### #!/bin/bash PS3='Choose your favorite vegetable: ' echo choice_of() { select vegetable # [in list] 被忽略, 所以'select'用传递给函数的参数. do echo echo "Your favorite veggie is $vegetable." echo "Yuck!" echo break done } choice_of beans rice carrots radishes tomatoes spinach # $1 $2 $3 $4 $5 $6 # 传递给 choice_of() 函数的参数 exit 0 ################################End Script######################################### Example 11-1 一个 fork 出多个自己实例的脚本 ################################Start Script####################################### #!/bin/bash # spawn.sh PIDS=$(pidof sh $0) # 这个脚本不同实例的进程 ID. P_array=( $PIDS ) # 把它们放到数组里(为什么?). echo $PIDS # 显示父进程和子进程的进程 ID. let "instances = ${#P_array[*]} - 1" # 计算元素个数,至少为 1. # 为什么减 1? echo "$instances instance(s) of this script running." echo "[Hit Ctl-C to exit.]"; echo sleep 1 # 等. sh $0 # 再来一次. exit 0 # 没必要: 脚本永远不会走到这里. # 为什么走不到这里? # 在使用 Ctl-C 退出之后, #+ 是否所有产生的进程都会被 kill 掉? # 如果是这样的话, 为什么? # 注意: # ---- # 小心,不要让这个脚本运行太长时间. # 它最后将吃掉你大部分的系统资源. # 对于用脚本产生大量的自身实例来说, #+ 是否有适当的脚本技术. # 为什么是为什么不是? ################################End Script######################################### 一般的,脚本中的内建命令在执行时将不会 fork 出一个子进程.但是脚本中的外部或过滤命令 通常会 fork 一个子进程. 一个内建命令通常与一个系统命令同名,但是 Bash 在内部重新实现了这些命令.比如,Bash 的 echo 命令与/bin/echo 就不尽相同,虽然它们的行为绝大多数情况下是一样的. #!/bin/bash 2 echo "This line uses the \"echo\" builtin." /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####################################### # 嵌入一个换行? echo "Why doesn't this string \n split on two lines?" # 上边这句的\n 将被打印出来.达不到换行的目的. # 让我们在试试其他方法. echo echo $"A line of text containing a linefeed." # 打印出 2 个独立的行,(潜入换行成功了). # 但是,"$"前缀是否是必要的? echo echo "This string splits on two lines." # 不用非得有"$"前缀. echo echo "---------------" echo echo -n $"Another line of text containing a linefeed." # 打印出 2 个独立的行,(潜入换行成功了). # 即使-n 选项,也没能阻止换行(译者:-n 阻止了第 2 个换行) echo echo echo "---------------" echo echo # 然而,下边的代码就没能像期望的那样运行. # Why not? Hint: Assignment to a variable. # 为什么失败? 提示: 因为分配到了变量. string1=$"Yet another line of text containing a linefeed (maybe)." echo $string1 # Yet another line of text containing a linefeed (maybe). # ^ # 换行变成了空格. # Thanks, Steve Parker, for pointing this out. ################################End Script######################################### Example 11-2 printf ################################Start Script####################################### #!/bin/bash # printf demo PI=3.14159265358979 DecimalConstant=31373 Message1="Greetings," Message2="Earthling." echo printf "Pi to 2 decimal places = %1.2f" $PI echo printf "Pi to 9 decimal places = %1.9f" $PI # 都能正确地结束. printf "\n" # 打印一个换行, # 等价于 'echo' . . . printf "Constant = \t%d\n" $DecimalConstant # 插入一个 tab (\t). printf "%s %s \n" $Message1 $Message2 echo # ==========================================# # 模仿 C 函数, sprintf(). # 使用一个格式化的字符串来加载一个变量. echo Pi12=$(printf "%1.12f" $PI) echo "Pi to 12 decimal places = $Pi12" Msg=`printf "%s %s \n" $Message1 $Message2` echo $Msg; echo $Msg # 向我们看到的一样,现在'sprintf'函数可以 #+ 作为一个可被加载的模块 #+ 但这是不可移植的. exit 0 ################################End Script######################################### Example 11-3 使用 read,变量分配 ################################Start Script####################################### #!/bin/bash # "Reading" 变量. echo -n "Enter the value of variable 'var1': " # -n 选项,阻止换行. read var1 # 注意在 var1 前面没有'$',因为变量正在被设置. echo "var1 = $var1" echo # 一个'read'命令可以设置多个变量. echo -n "Enter the values of variables 'var2' and 'var3' (separated by a space or tab): " read var2 var3 echo "var2 = $var2 var3 = $var3" # 如果你只输入了一个值,那么其他的变量还是未设置(null). exit 0 ################################End Script######################################### Example 11-4 当使用一个不带变量参数的 read 命令时,将会发生什么? ################################Start Script####################################### #!/bin/bash # read-novar.sh echo # -------------------------- # echo -n "Enter a value: " read var echo "\"var\" = "$var"" # 到这里为止,都与期望的相同. # -------------------------- # echo # ------------------------------------------------------------------- # echo -n "Enter another value: " read # 没有变量分配给'read'命令,因此... #+ 输入将分配给默认变量,$REPLY. var="$REPLY" echo "\"var\" = "$var"" # 这部分代码和上边的代码等价. # ------------------------------------------------------------------- # echo exit 0 ################################End Script######################################### Example 11-5 read 命令的多行输入 ################################Start Script####################################### #!/bin/bash echo echo "Enter a string terminated by a \\, then press ." echo "Then, enter a second string, and again press ." read var1 # "\"将会阻止产生新行,当 read $var1 时. # first line \ # second line echo "var1 = $var1" # var1 = first line second line # For each line terminated by a "\" # 对于每个一个"\"结尾的行 #+ 你都会看到一个下一行的提示符,让你继续向 var1 输入内容. echo; echo echo "Enter another string terminated by a \\ , then press ." read -r var2 # -r 选项将会让"\"转义. # first line \ echo "var2 = $var2" # var2 = first line \ # 第一个就会结束 var2 变量的录入. echo exit 0 ################################End Script######################################### Example 11-6 检测方向键 ################################Start Script####################################### #!/bin/bash # arrow-detect.sh: 检测方向键,和一些非打印字符的按键. # Thank you, Sandro Magi 告诉了我怎么做. # -------------------------------------------- # 按键产生的字符编码. arrowup='\[A' arrowdown='\[B' arrowrt='\[C' arrowleft='\[D' insert='\[2' delete='\[3' # -------------------------------------------- SUCCESS=0 OTHER=65 echo -n "Press a key... " # 如果不是上边列表所列出的按键,可能还是需要按回车.(译者:因为一般按键是一个字符) read -n3 key # 读 3 个字符. echo -n "$key" | grep "$arrowup" #检查输入字符是否匹配. if [ "$?" -eq $SUCCESS ] then echo "Up-arrow key pressed." exit $SUCCESS fi echo -n "$key" | grep "$arrowdown" if [ "$?" -eq $SUCCESS ] then echo "Down-arrow key pressed." exit $SUCCESS fi echo -n "$key" | grep "$arrowrt" if [ "$?" -eq $SUCCESS ] then echo "Right-arrow key pressed." exit $SUCCESS fi echo -n "$key" | grep "$arrowleft" if [ "$?" -eq $SUCCESS ] then echo "Left-arrow key pressed." exit $SUCCESS fi echo -n "$key" | grep "$insert" if [ "$?" -eq $SUCCESS ] then echo "\"Insert\" key pressed." exit $SUCCESS fi echo -n "$key" | grep "$delete" if [ "$?" -eq $SUCCESS ] then echo "\"Delete\" key pressed." exit $SUCCESS fi echo " Some other key pressed." exit $OTHER # 练习: # ----- # 1) 通过使用'case'结构来代替'if'结构 #+ 来简化这个脚本. # 2) Add detection of the "Home," "End," "PgUp," and "PgDn" keys. # 2) 添加"Home," "End," "PgUp," 和 "PgDn"这些按键的检查. ################################End Script######################################### Example 11-7 通过文件重定向来使用 read ################################Start Script####################################### #!/bin/bash read var1 ; ... To force variable substitution try: $export WEBROOT_PATH=/usr/local/webroot $sed 's//$WEBROOT_PATH/' < test.pl > out But this just gives: my $WEBROOT = $WEBROOT_PATH; However: $export WEBROOT_PATH=/usr/local/webroot $eval sed 's%\%$WEBROOT_PATH%' < test.pl > out # ==== That works fine, and gives the expected substitution: my $WEBROOT = /usr/local/webroot; ### Correction applied to original example by Paulo Marcel Coelho Aragao. ################################End Script######################################### Example 11-15 使用 set 来改变脚本的位置参数 ################################Start Script####################################### #!/bin/bash # script "set-test" # 使用 3 个命令行参数来调用这个脚本, # 比如, "./set-test one two three". echo echo "Positional parameters before set \`uname -a\` :" #uname 命令打印操作系统名 echo "Command-line argument #1 = $1" echo "Command-line argument #2 = $2" echo "Command-line argument #3 = $3" set `uname -a` # 把`uname -a`的命令输出设置 # 为新的位置参数. echo $_ # 这要看你的 unmae -a 输出了,这句打印出的就是输出的最后一个单词. # 在脚本中设置标志. echo "Positional parameters after set \`uname -a\` :" # $1, $2, $3, 等等. 这些位置参数将被重新初始化为`uname -a`的结果 echo "Field #1 of 'uname -a' = $1" echo "Field #2 of 'uname -a' = $2" echo "Field #3 of 'uname -a' = $3" echo --- echo $_ # --- echo exit 0 ################################End Script######################################### Example 11-16 重新分配位置参数 ################################Start Script####################################### #!/bin/bash variable="one two three four five" set -- $variable # 将位置参数的内容设为变量"$variable"的内容. first_param=$1 second_param=$2 shift; shift # Shift past first two positional params. remaining_params="$*" echo echo "first parameter = $first_param" # one echo "second parameter = $second_param" # two echo "remaining parameters = $remaining_params" # three four five echo; echo # 再来一次. set -- $variable first_param=$1 second_param=$2 echo "first parameter = $first_param" # one echo "second parameter = $second_param" # two # ====================================================== set -- # Unsets positional parameters if no variable specified. # 如果没指定变量,那么将会 unset 所有的位置参数. first_param=$1 second_param=$2 echo "first parameter = $first_param" # (null value) echo "second parameter = $second_param" # (null value) exit 0 ################################End Script######################################### Example 11-17 Unset 一个变量 ################################Start Script####################################### #!/bin/bash # unset.sh: Unset 一个变量. variable=hello # 初始化. echo "variable = $variable" unset variable # Unset. # 与 variable= 的效果相同. echo "(unset) variable = $variable" # $variable 设为 null. exit 0 ################################End Script######################################### Example 11-18 使用 export 命令传递一个变量到一个内嵌 awk 的脚本中 ################################Start Script####################################### #!/bin/bash # 这是"求列的和"脚本的另外一个版本(col-totaler.sh) #+ 那个脚本可以把目标文件中的指定的列上的所有数字全部累加起来,求和. # 这个版本将把一个变量通过 export 的形式传递到'awk'中 . . . #+ 并且把 awk 脚本放到一个变量中. ARGS=2 E_WRONGARGS=65 if [ $# -ne "$ARGS" ] # 检查命令行参数的个数. then echo "Usage: `basename $0` filename column-number" exit $E_WRONGARGS fi filename=$1 column_number=$2 #===== 上边的这部分,与原始脚本完全一样 =====# export column_number # 将列号通过 export 出来,这样后边的进程就可用了. # ----------------------------------------------- awkscript='{ total += $ENVIRON["column_number"] } END { print total }' # 是的,一个变量可以保存一个 awk 脚本. # ----------------------------------------------- # 现在,运行 awk 脚本. awk "$awkscript" "$filename" # Thanks, Stephane Chazelas. exit 0 ################################End Script######################################### ################################Start Script####################################### while getopts ":abcde:fg" Option # Initial declaration. # 开始的声明. # a, b, c, d, e, f, 和 g 被认为是选项(标志). # e 选项后边的:提示,这个选项带一个参数. do case $Option in a ) # Do something with variable 'a'. a ) # 对选项'a'作些操作. b ) # 对选项'b'作些操作. ... e) # Do something with 'e', and also with $OPTARG, e) # 对选项'e'作些操作, 同时处理一下$OPTARG, # which is the associated argument passed with option 'e'. # 这个变量里边将保存传递给选项"e"的参数. ... g ) # 对选项'g'作些操作. esac done shift $(($OPTIND - 1)) # 将参数指针向下移动. # 所有这些远没有它看起来的那么复杂.<嘿嘿> ################################End Script######################################### Example 11-19 使用 getopts 命令来读取传递给脚本的选项/参数.(我测试的结果与说明不同, 我使用 ./scriptname -mnp,但是$OPTIND 的值居然是 1 1 2) ################################Start Script####################################### #!/bin/bash # 练习 getopts 和 OPTIND # 在 Bill Gradwohl 的建议下,这个脚本于 10/09/03 被修改. # 这里我们将学习 'getopts'如何处理脚本的命令行参数. # 参数被作为"选项"(标志)被解析,并且分配参数. # 试一下通过如下方法来调用这个脚本 # 'scriptname -mn' # 'scriptname -oq qOption' (qOption 可以是任意的哪怕有些诡异字符的字符串.) # 'scriptname -qXXX -r' # # 'scriptname -qr' - 意外的结果, "r" 将被看成是选项 "q" 的参数. # 'scriptname -q -r' - 意外的结果, 同上. # 'scriptname -mnop -mnop' - 意外的结果 # (OPTIND is unreliable at stating where an option came from). # # 如果一个选项需要一个参数("flag:"),那么它应该 #+ 取得在命令行上挨在它后边的任何字符. NO_ARGS=0 E_OPTERROR=65 if [ $# -eq "$NO_ARGS" ] # 不带命令行参数就调用脚本? then echo "Usage: `basename $0` options (-mnopqrs)" exit $E_OPTERROR # 如果没有参数传进来,那就退出,并解释用法. fi # 用法: 脚本名 -选项名 # 注意: 破折号(-)是必须的 while getopts ":mnopq:rs" Option do case $Option in m ) echo "Scenario #1: option -m- [OPTIND=${OPTIND}]";; n | o ) echo "Scenario #2: option -$Option- [OPTIND=${OPTIND}]";; p ) echo "Scenario #3: option -p- [OPTIND=${OPTIND}]";; q ) echo "Scenario #4: option -q-\ with argument \"$OPTARG\" [OPTIND=${OPTIND}]";; # 注意,选项'q'必须分配一个参数, #+ 否则默认将失败. r | s ) echo "Scenario #5: option -$Option-";; * ) echo "Unimplemented option chosen.";; # DEFAULT esac done shift $(($OPTIND - 1)) # 将参数指针减 1,这样它将指向下一个参数. # $1 现在引用的是命令行上的第一个非选项参数 #+ 如果有一个这样的参数存在的话. exit 0 # 像 Bill Gradwohl 所说, # "The getopts mechanism allows one to specify: scriptname -mnop -mnop #+ but there is no reliable way to differentiate what came from where #+ by using OPTIND." ################################End Script######################################### Example 11-20 "Including"一个数据文件 ################################Start Script####################################### #!/bin/bash . data-file # 加载一个数据文件. # 与"source data-file"效果相同,但是更具可移植性. # 文件"data-file"必须存在于当前工作目录, #+ 因为这个文件时使用'basename'来引用的. # 现在,引用这个数据文件中的一些数据. echo "variable1 (from data-file) = $variable1" echo "variable3 (from data-file) = $variable3" let "sum = $variable2 + $variable4" echo "Sum of variable2 + variable4 (from data-file) = $sum" echo "message1 (from data-file) is \"$message1\"" # 注意 : 将双引号转义 print_message This is the message-print function in the data-file. exit 0 ################################End Script######################################### Example 11-20 使用的 data-file.见上边,这个文件必须和上边的脚本放在同一目录下. ################################Start Script####################################### # 这是需要被脚本加载的 data file. # 这种文件可以包含变量,函数,等等. # 在脚本中可以通过'source'或者'.'命令来加载. # 让我们初始化一些变量. variable1=22 variable2=474 variable3=5 variable4=97 message1="Hello, how are you?" message2="Enough for now. Goodbye." print_message () { # Echo 出传递进来的任何消息. if [ -z "$1" ] then return 1 # 如果没有参数的话,出错. fi echo until [ -z "$1" ] do # 循环处理传递到函数中的参数. echo -n "$1" # 每次 Echo 一个参数, -n 禁止换行. echo -n " " # 在参数间插入空格. shift # 下一个. done echo return 0 } ################################End Script######################################### Example 11-21 一个没什么用的,source 自身的脚本 ################################Start Script####################################### #!/bin/bash # self-source.sh: 一个脚本递归的 source 自身. # 来自于"Stupid Script Tricks," 卷 II. MAXPASSCNT=100 # source 自身的最大数量. echo -n "$pass_count " # 在第一次运行的时候,这句只不过 echo 出 2 个空格, #+ 因为$pass_count 还没被初始化. let "pass_count += 1" # 假定这个为初始化的变量 $pass_count #+ 可以在第一次运行的时候+1. # 这句可以正常工作于 Bash 和 pdksh,但是 #+ 它依赖于不可移植(并且可能危险)的行为. # 更好的方法是在使用$pass_count 之前,先把这个变量初始化为 0. while [ "$pass_count" -le $MAXPASSCNT ] do . $0 # 脚本"sources" 自身, 而不是调用自己. # ./$0 (应该能够正常递归) 但是不能在这正常运行. 为什么? done # 这里发生的动作并不是真正的递归, #+ 因为脚本成功的展开了自己,换句话说, #+ 在每次循环的过程中 #+ 在每个'source'行(第 20 行)上 # 都产生了新的代码. # # 当然,脚本会把每个新'sourced'进来的文件的"#!"行 #+ 都解释成注释,而不会把它看成是一个新的脚本. echo exit 0 # 最终的效果就是从 1 数到 100. # 让人印象深刻. # 练习: # ----- # 使用这个小技巧编写一些真正能干些事情的脚本. ################################End Script######################################### Example 11-22 exec 的效果 ################################Start Script####################################### #!/bin/bash exec echo "Exiting \"$0\"." # 脚本将在此退出. # ---------------------------------- # 下边的部分将执行不到. echo "This echo will never echo." exit 99 # 脚本不会在这退出. # 脚本退出后检查一下退出码 #+ 使用'echo $?'命令. # 肯定不是 99. ################################End Script######################################### Example 11-23 一个 exec 自身的脚本 ################################Start Script####################################### #!/bin/bash # self-exec.sh echo echo "This line appears ONCE in the script, yet it keeps echoing." echo "The PID of this instance of the script is still $$." # 上边这句用来根本没产生子进程. echo "==================== Hit Ctl-C to exit ====================" sleep 1 exec $0 # 产生了本脚本的另一个实例, #+ 并且这个实例代替了之前的那个. echo "This line will never echo!" # 当然会这样. exit 0 ################################End Script######################################### Example 11-24 在继续处理之前,等待一个进程的结束 ################################Start Script####################################### #!/bin/bash ROOT_UID=0 # 只有$UID 为 0 的用户才拥有 root 权限. E_NOTROOT=65 E_NOPARAMS=66 if [ "$UID" -ne "$ROOT_UID" ] then echo "Must be root to run this script." # "Run along kid, it's past your bedtime." exit $E_NOTROOT fi if [ -z "$1" ] then echo "Usage: `basename $0` find-string" exit $E_NOPARAMS fi echo "Updating 'locate' database..." echo "This may take a while." updatedb /usr & # 必须使用 root 身份来运行. wait # 将不会继续向下运行,除非 'updatedb'命令执行完成. # 你希望在查找文件名之前更新 database. locate $1 # 如果没有'wait'命令的话,而且在比较糟的情况下, #+ 脚本可能在'updatedb'命令还在运行的时候退出, #+ 这将会导致'updatedb'成为一个孤儿进程. exit 0 ################################End Script######################################### Example 11-25 一个结束自身的脚本. ################################Start Script####################################### #!/bin/bash # self-destruct.sh kill $$ # 脚本将在此处结束自己的进程. # Recall that "$$" is the script's PID. # 回忆一下,"$$"就是脚本的 PID. echo "This line will not echo." # 而且 shell 将会发送一个"Terminated"消息到 stdout. exit 0 # 在脚本结束自身进程之后, #+ 它返回的退出码是什么? # # sh self-destruct.sh # echo $? # 143 # # 143 = 128 + 15 # 结束信号 ################################End Script######################################### Example 12-1 使用 ls 命令来创建一个烧录 CDR 的内容列表 ################################Start Script####################################### #!/bin/bash # ex40.sh (burn-cd.sh) # 自动刻录 CDR 的脚本. SPEED=2 # 如果你的硬件支持的话,你可以选用更高的速度. IMAGEFILE=cdimage.iso CONTENTSFILE=contents DEVICE=cdrom # DEVICE="0,0" 为了使用老版本的 CDR DEFAULTDIR=/opt # 这是包含需要被刻录内容的目录. # 必须保证目录存在. # 小练习: 测试一下目录是否存在. # Uses Joerg Schilling's "cdrecord" package: # 使用 Joerg Schilling 的 "cdrecord"包: # http://www.fokus.fhg.de/usr/schilling/cdrecord.html # 如果一般用户调用这个脚本的话,可能需要 root 身份 #+ chmod u+s /usr/bin/cdrecord # 当然, 这会产生安全漏洞, 虽然这是一个比较小的安全漏洞. if [ -z "$1" ] then IMAGE_DIRECTORY=$DEFAULTDIR # 如果命令行没指定的话, 那么这个就是默认目录. else IMAGE_DIRECTORY=$1 fi # 创建一个内容列表文件. ls -lRF $IMAGE_DIRECTORY > $IMAGE_DIRECTORY/$CONTENTSFILE # "l" 选项将给出一个"长"文件列表. # "R" 选项将使这个列表递归. # "F" 选项将标记出文件类型 (比如: 目录是以 /结尾, 而可执行文件以 *结尾). echo "Creating table of contents." # 在烧录到 CDR 之前创建一个镜像文件. mkisofs -r -o $IMAGEFILE $IMAGE_DIRECTORY echo "Creating ISO9660 file system image ($IMAGEFILE)." # 烧录 CDR. echo "Burning the disk." echo "Please be patient, this will take a while." cdrecord -v -isosize speed=$SPEED dev=$DEVICE $IMAGEFILE exit $? ################################End Script######################################### Example 12-2 Hello or Good-bye ################################Start Script####################################### #!/bin/bash # hello.sh: 显示"hello" 还是 "goodbye" #+ 依赖于脚本是如何被调用的. # 在当前目录下($PWD)为这个脚本创建一个链接: # ln -s hello.sh goodbye # 现在, 通过如下两种方法来调用这个脚本: # ./hello.sh # ./goodbye HELLO_CALL=65 GOODBYE_CALL=66 if [ $0 = "./goodbye" ] then echo "Good-bye!" # 当然, 在这里你也可以添加一些其他的 goodbye 类型的命令.Some other goodbye-type commands, as appropriate. exit $GOODBYE_CALL fi echo "Hello!" # 当然, 在这里你也可以添加一些其他的 hello 类型的命令. exit $HELLO_CALL ################################End Script######################################### Example 12-3 删除当前目录下文件名中包含一些特殊字符(包括空白)的文件.. ################################Start Script####################################### #!/bin/bash # badname.sh # 删除当前目录下文件名中包含一些特殊字符的文件. for filename in * do badname=`echo "$filename" | sed -n /[\+\{\;\"\\\=\?~\(\)\<\>\&\*\|\$]/p` # badname=`echo "$filename" | sed -n '/[+{;"\=?~()<>&*|$]/p'` 这句也行. # 删除文件名包含这些字符的文件: + { ; " \ = ? ~ ( ) < > & * | $ # rm $badname 2>/dev/null # ^^^^^^^^^^^ 错误消息将被抛弃. done # 现在, 处理文件名中以任何方式包含空白的文件. find . -name "* *" -exec rm -f {} \; # "find"命令匹配到的目录名将替换到{}的位置. # '\' 是为了保证 ';'被正确的转义, 并且放到命令的结尾. exit 0 #--------------------------------------------------------------------- # 这行下边的命令将不会运行, 因为 "exit" 命令. # 这句是上边脚本的一个可选方法: find . -name '*[+{;"\\=?~()<>&*|$ ]*' -exec rm -f '{}' \; # (Thanks, S.C.) ################################End Script######################################### Example 12-4 通过文件的 inode 号来删除文件 ################################Start Script####################################### #!/bin/bash # idelete.sh: 通过文件的 inode 号来删除文件. # 当文件名以一个非法字符开头的时候, 这就非常有用了, #+ 比如 ? 或 -. ARGCOUNT=1 # 文件名参数必须被传递到脚本中. E_WRONGARGS=70 E_FILE_NOT_EXIST=71 E_CHANGED_MIND=72 if [ $# -ne "$ARGCOUNT" ] then echo "Usage: `basename $0` filename" exit $E_WRONGARGS fi if [ ! -e "$1" ] then echo "File \""$1"\" does not exist." exit $E_FILE_NOT_EXIST fi inum=`ls -i | grep "$1" | awk '{print $1}'` # inum = inode (索引节点) 号. # -------------------------------------------------------- # 每个文件都有一个 inode 号, 这个号用来记录文件物理地址信息. # -------------------------------------------------------- echo; echo -n "Are you absolutely sure you want to delete \"$1\" (y/n)? " # 'rm' 命令的 '-v' 选项也会问这句话. read answer case "$answer" in [nN]) echo "Changed your mind, huh?" exit $E_CHANGED_MIND ;; *) echo "Deleting file \"$1\".";; esac find . -inum $inum -exec rm {} \; # ^^ # 大括号就是"find"命令 #+ 用来替换文本输出的地方. echo "File "\"$1"\" deleted!" exit 0 ################################End Script######################################### Example 12-5 Logfile: 使用 xargs 来监控系统 log ################################Start Script####################################### #!/bin/bash # 从 /var/log/messagesGenerates 的尾部开始 # 产生当前目录下的一个 lof 文件. # 注意: 如果这个脚本被一个一般用户调用的话, # /var/log/messages 必须是全部可读的. # #root chmod 644 /var/log/messages LINES=5 ( date; uname -a ) >>logfile # 时间和机器名 echo --------------------------------------------------------------------- >>logfile tail -$LINES /var/log/messages | xargs | fmt -s >>logfile echo >>logfile echo >>logfile exit 0 # 注意: # ----- # 像 Frank Wang 所指出, #+ 在原文件中的任何不匹配的引号(包括单引号和双引号) #+ 都会给 xargs 造成麻烦. # # 他建议使用下边的这行来替换上边的第 15 行: # tail -$LINES /var/log/messages | tr -d "\"'" | xargs | fmt -s >>logfile # 练习: # ----- # 修改这个脚本, 使得这个脚本每个 20 分钟 #+ 就跟踪一下 /var/log/messages 的修改记录. # 提示: 使用 "watch" 命令. ################################End Script######################################### Example 12-6 把当前目录下的文件拷贝到另一个文件中 ################################Start Script####################################### #!/bin/bash # copydir.sh # 拷贝 (verbose) 当前目录($PWD)下的所有文件到 #+ 命令行中指定的另一个目录下. E_NOARGS=65 if [ -z "$1" ] # 如果没有参数传递进来那就退出. then echo "Usage: `basename $0` directory-to-copy-to" exit $E_NOARGS fi ls . | xargs -i -t cp ./{} $1 # ^^ ^^ ^^ # -t 是 "verbose" (输出命令行到 stderr) 选项. # -i 是"替换字符串"选项. # {} 是输出文本的替换点. # 这与在"find"命令中使用{}的情况很相像. # # 列出当前目录下的所有文件(ls .), #+ 将 "ls" 的输出作为参数传递到 "xargs"(-i -t 选项) 中, #+ 然后拷贝(cp)这些参数({})到一个新目录中($1). # # 最终的结果和下边的命令等价, #+ cp * $1 #+ 除非有文件名中嵌入了"空白"字符. exit 0 ################################End Script######################################### Example 12-7 通过名字 Kill 进程 ################################Start Script####################################### #!/bin/bash # kill-byname.sh: 通过名字 kill 进程. # 与脚本 kill-process.sh 相比较. # 例如, #+ 试一下 "./kill-byname.sh xterm" -- #+ 并且查看你系统上的所有 xterm 都将消失. # 警告: # ----- # 这是一个非常危险的脚本. # 运行它的时候一定要小心. (尤其是以 root 身份运行时) #+ 因为运行这个脚本可能会引起数据丢失或产生其他一些不好的效果. E_BADARGS=66 if test -z "$1" # 没有参数传递进来? then echo "Usage: `basename $0` Process(es)_to_kill" exit $E_BADARGS fi PROCESS_NAME="$1" ps ax | grep "$PROCESS_NAME" | awk '{print $1}' | xargs -i kill {} 2&>/dev/null # ^^ ^^ # ----------------------------------------------------------- # 注意: # -i 参数是 xargs 命令的"替换字符串"选项. # 大括号对的地方就是替换点. # 2&>/dev/null 将会丢弃不需要的错误消息. # ----------------------------------------------------------- exit $? ################################End Script######################################### Example 12-8 使用 xargs 分析单词出现的频率 ################################Start Script####################################### #!/bin/bash # wf2.sh: Crude word frequency analysis on a text file. # 使用 'xargs' 将文本行分解为单词. # 于后边的 "wf.sh" 脚本相比较. # 检查命令行上输入的文件. ARGS=1 E_BADARGS=65 E_NOFILE=66 if [ $# -ne "$ARGS" ] # 纠正传递到脚本中的参数个数? then echo "Usage: `basename $0` filename" exit $E_BADARGS fi if [ ! -f "$1" ] # 检查文件是否存在. then echo "File \"$1\" does not exist." exit $E_NOFILE fi ################################################################# cat "$1" | xargs -n1 | \ # 列出文件, 每行一个单词. tr A-Z a-z | \ # 将字符转换为小写. sed -e 's/\.//g' -e 's/\,//g' -e 's/ /\ /g' | \ # 过滤掉句号和逗号, #+ 并且将单词间的空格修改为换行, sort | uniq -c | sort -nr # 最后统计出现次数,把数字显示在第一列,然后显示单词,并按数字排序. ################################################################# # 这个例子的作用与"wf.sh"的作用是一样的, #+ 但是这个例子比较臃肿, 并且运行起来更慢一些(为什么?). exit 0 ################################End Script######################################### Example 12-9 使用 expr ################################Start Script####################################### 1 #!/bin/bash 2 3 # 展示一些 'expr'的使用 4 # ===================== 5 6 echo 7 8 # 算术 操作 9 # ---- ---- 10 11 echo "Arithmetic Operators" 12 echo 13 a=`expr 5 + 3` 14 echo "5 + 3 = $a" 15 16 a=`expr $a + 1` 17 echo 18 echo "a + 1 = $a" 19 echo "(incrementing a variable)" 20 21 a=`expr 5 % 3` 22 # 取模操作 23 echo 24 echo "5 mod 3 = $a" 25 26 echo 27 echo 28 29 # 逻辑 操作 30 # ---- ---- 31 32 # true 返回 1 ,false 返回 0 , 33 #+ 而 Bash 的使用惯例则相反. 34 35 echo "Logical Operators" 36 echo 37 38 x=24 39 y=25 40 b=`expr $x = $y` # 测试相等. 41 echo "b = $b" # 0 ( $x -ne $y ) 42 echo 43 44 a=3 45 b=`expr $a \> 10` 46 echo 'b=`expr $a \> 10`, therefore...' 47 echo "If a > 10, b = 0 (false)" 48 echo "b = $b" # 0 ( 3 ! -gt 10 ) 49 echo 50 51 b=`expr $a \< 10` 52 echo "If a < 10, b = 1 (true)" 53 echo "b = $b" # 1 ( 3 -lt 10 ) 54 echo 55 # Note escaping of operators. 56 57 b=`expr $a \<= 3` 58 echo "If a <= 3, b = 1 (true)" 59 echo "b = $b" # 1 ( 3 -le 3 ) 60 # 也有 "\>=" 操作 (大于等于). 61 62 63 echo 64 echo 65 66 67 68 # 字符串 操作 69 # ------ ---- 70 71 echo "String Operators" 72 echo 73 74 a=1234zipper43231 75 echo "The string being operated upon is \"$a\"." 76 77 # 长度: 字符串长度 78 b=`expr length $a` 79 echo "Length of \"$a\" is $b." 80 81 # 索引: 从字符串的开头查找匹配的子串, 82 # 并取得第一个匹配子串的位置. 83 b=`expr index $a 23` 84 echo "Numerical position of first \"2\" in \"$a\" is \"$b\"." 85 86 # substr: 从指定位置提取指定长度的字串. 87 b=`expr substr $a 2 6` 88 echo "Substring of \"$a\", starting at position 2,\ 89 and 6 chars long is \"$b\"." 90 91 92 # 'match' 操作的默认行为就是 93 #+ 从字符串的开始进行搜索,并匹配第一个匹配的字符串. 94 # 95 # 使用正则表达式 96 b=`expr match "$a" '[0-9]*'` # 数字的个数. 97 echo Number of digits at the beginning of \"$a\" is $b. 98 b=`expr match "$a" '\([0-9]*\)'` # 注意需要转义括号 99 # == == + 这样才能触发子串的匹配. 100 echo "The digits at the beginning of \"$a\" are \"$b\"." 101 102 echo 103 104 exit 0 ################################End Script######################################### 注意: ":" 操作可以替换 match. 比如, b=`expr $a : [0-9]*`与上边所使用的 b=`expr match $a [0-9]*` 完全等价. ################################Start Script####################################### #!/bin/bash echo echo "String operations using \"expr \$string : \" construct" echo "===================================================" echo a=1234zipper5FLIPPER43231 echo "The string being operated upon is \"`expr "$a" : '\(.*\)'`\"." # 转义括号对操作. == == # *************************** #+ 转移括号对 #+ 用来匹配一个子串 # *************************** # 如果不转义括号的话... #+ 那么 'expr' 将把 string 操作转换为一个整数. echo "Length of \"$a\" is `expr "$a" : '.*'`." # 字符串长度 echo "Number of digits at the beginning of \"$a\" is `expr "$a" : '[0-9]*'`." # ------------------------------------------------------------------------- # echo echo "The digits at the beginning of \"$a\" are `expr "$a" : '\([0-9]*\)'`." # == == echo "The first 7 characters of \"$a\" are `expr "$a" : '\(.......\)'`." # ===== == == # 再来一个, 转义括号对强制一个子串匹配. # echo "The last 7 characters of \"$a\" are `expr "$a" : '.*\(.......\)'`." # ==== end of string operator ^^ # (最后这个模式的意思是忽略前边的任何字符,直到最后 7 个字符, #+ 最后 7 个点就是需要匹配的任意 7 个字符的字串) echo exit 0 ################################End Script######################################### Example 12-10 使用 date 命令 ################################Start Script####################################### #!/bin/bash # 练习 'date' 命令 echo "The number of days since the year's beginning is `date +%j`." # 需要在调用格式的前边加上一个 '+' 号. # %j 给出今天是本年度的第几天. echo "The number of seconds elapsed since 01/01/1970 is `date +%s`." # %s 将产生从 "UNIX 元年" 到现在为止的秒数,yields number of seconds since "UNIX epoch" began, #+ 但是这东西有用么? prefix=temp suffix=$(date +%s) # 'date'命令的 "+%s" 选项是 GNU-特性. filename=$prefix.$suffix echo $filename # 这是一种非常好的产生 "唯一" 的临时文件的办法, #+ 甚至比使用 $$ 都强. # 如果想了解 'date' 命令的更多选项, 请查阅这个命令的 man 页. exit 0 ################################End Script######################################### Example 12-11 分析单词出现的频率 ################################Start Script####################################### #!/bin/bash # wf.sh: 分析文本文件中自然词汇出现的频率. # "wf2.sh" 是一个效率更高的版本. # 从命令行中检查输入的文件. ARGS=1 E_BADARGS=65 E_NOFILE=66 if [ $# -ne "$ARGS" ] # 检验传递到脚本中参数的个数. then echo "Usage: `basename $0` filename" exit $E_BADARGS fi if [ ! -f "$1" ] # 检查传入的文件参数是否存在. then echo "File \"$1\" does not exist." exit $E_NOFILE fi ######################################################## # main () sed -e 's/\.//g' -e 's/\,//g' -e 's/ /\ /g' "$1" | tr 'A-Z' 'a-z' | sort | uniq -c | sort -nr # ========================= # 检查单词出现的频率 # 过滤掉句号和逗号, #+ 并且把单词间的空格转化为换行, #+ 然后转化为小写, #+ 最后统计出现的频率并按频率排序. # Arun Giridhar 建议将上边的代码修改为: # . . . | sort | uniq -c | sort +1 [-f] | sort +0 -nr # 这句添加了第 2 个排序主键, 所以 #+ 这个与上边等价的例子将按照字母顺序进行排序. # 就像他所解释的: # "这是一个有效的根排序, 首先对频率最少的 #+ 列进行排序 #+ (单词或者字符串, 忽略大小写) #+ 然后对频率最高的列进行排序." # # 像 Frank Wang 所解释的那样, 上边的代码等价于: #+ . . . | sort | uniq -c | sort +0 -nr #+ 用下边这行也行: #+ . . . | sort | uniq -c | sort -k1nr -k ######################################################## exit 0 # 练习: # ----- # 1) 使用 'sed' 命令来过滤其他的标点符号, #+ 比如分号. # 2) 修改这个脚本, 添加能够过滤多个空格或者 # 空白的能力. ################################End Script######################################### Example 12-12 那个文件是脚本? ################################Start Script####################################### #!/bin/bash # script-detector.sh: 在一个目录中检查所有的脚本文件. TESTCHARS=2 # 测试前两个字节. SHABANG='#!' # 脚本都是以 "sha-bang." 开头的. for file in * # 遍历当前目录下的所有文件. do if [[ `head -c$TESTCHARS "$file"` = "$SHABANG" ]] # head -c2 #! # '-c' 选项将从文件头输出指定个数的字符, #+ 而不是默认的行数. then echo "File \"$file\" is a script." else echo "File \"$file\" is *not* a script." fi done exit 0 # 练习: # ----- # 1) 将这个脚本修改为可以指定目录 #+ 来扫描目录下的脚本. #+ (而不是只搜索当前目录). # # 2) 就目前看来, 这个脚本将不能正确识别出 #+ Perl, awk, 和其他一些脚本语言的脚本文件. # 修正这个问题. ################################End Script######################################### Example 12-13 产生 10 进制随机数 ################################Start Script####################################### #!/bin/bash # rnd.sh: 输出一个 10 进制随机数 # Script by Stephane Chazelas. head -c4 /dev/urandom | od -N4 -tu4 | sed -ne '1s/.* //p' # =================================================================== # # 分析 # ---- # head: # -c4 选项将取得前 4 个字节. # od: # -N4 选项将限制输出为 4 个字节. # -tu4 选项将使用无符号 10 进制格式来输出. # sed: # -n 选项, 使用 "s" 命令与 "p" 标志组合的方式, # 将会只输出匹配的行. # 本脚本作者解释 'sed' 命令的行为如下. # head -c4 /dev/urandom | od -N4 -tu4 | sed -ne '1s/.* //p' # ----------------------------------> | # 假设一直处理到 "sed"命令时的输出--> | # 为 0000000 1198195154\n # sed 命令开始读取字串: 0000000 1198195154\n. # 这里它发现一个换行符, #+ 所以 sed 准备处理第一行 (0000000 1198195154). # sed 命令开始匹配它的 . 第一个匹配的并且只有这一个匹配的: # range action # 1 s/.* //p # 因为行号在 range 中, 所以 sed 开始执行 action: #+ 替换掉以空格结束的最长的字符串, 在这行中这个字符串是 # ("0000000 ") ,用空字符串(//)将这个匹配到的字串替换掉, 如果成功, 那就打印出结果 # ("p" 在这里是 "s" 命令的标志, 这与单独的 "p" 命令是不同的). # sed 命令现在开始继续读取输入. (注意在继续之前, #+ continuing, 如果没使用 -n 选项的话, sed 命令将再次 #+ 将这行打印一遍). # 现在, sed 命令读取剩余的字符串, 并且找到文件的结尾. # sed 命令开始处理第 2 行(这行也被标记为 '$' # 因为这已经是最后一行). # 所以这行没被匹配到 中, 这样 sed 命令就结束了. # 这个 sed 命令的简短的解释是: # "在第一行中删除第一个空格左边全部的字符, #+ 然后打印出来." # 一个更好的来达到这个目的的方法是: # sed -e 's/.* //;q' # 这里, 分别是 (也可以写成 # sed -e 's/.* //' -e q): # range action # nothing (matches line) s/.* // # nothing (matches line) q (quit) # 这里, sed 命令只会读取第一行的输入. # 将会执行 2 个命令, 并且会在退出之前打印出(已经替换过的)这行(因为 "q" action), #+ 因为没使用 "-n" 选项. # =================================================================== # # 也可以使用如下一个更简单的语句来代替: # head -c4 /dev/urandom| od -An -tu4 exit 0 ################################End Script######################################### Example 12-14 使用 tail 命令来监控系统 log ################################Start Script####################################### #!/bin/bash filename=sys.log cat /dev/null > $filename; echo "Creating / cleaning out file." # 如果文件不存在的话就创建文件, #+ 然后将这个文件清空. # : > filename 和 > filename 也可以完成这个工作. tail /var/log/messages > $filename # /var/log/messages 必须具有全局可读权限才行. echo "$filename contains tail end of system log." exit 0 ################################End Script######################################### Example 12-15 在一个脚本中模仿 "grep" 的行为 ################################Start Script####################################### #!/bin/bash # grp.sh: 一个非常粗糙的 'grep' 的实现. E_BADARGS=65 if [ -z "$1" ] # 检查传递给脚本的参数. then echo "Usage: `basename $0` pattern" exit $E_BADARGS fi echo for file in * # 遍历 $PWD 下的所有文件. do output=$(sed -n /"$1"/p $file) # 命令替换. if [ ! -z "$output" ] # 如果"$output" 不加双引号将会发生什么? then echo -n "$file: " echo $output fi # sed -ne "/$1/s|^|${file}: |p" 这句与上边这段等价. echo done echo exit 0 # 练习: # ----- # 1) 在任何给定的文件中,如果有超过一个匹配的话, 在输出中添加新行. # 2) 添加一些特征. ################################End Script######################################### Example 12-16 在 1913 年的韦氏词典中查找定义 ################################Start Script####################################### #!/bin/bash # dict-lookup.sh # 这个脚本在 1913 年的韦氏词典中查找定义. # 这本公共词典可以通过不同的 #+ 站点来下载,包括 #+ Project Gutenberg (http://www.gutenberg.org/etext/247). # # 在通过本脚本使用之前, #+ 先要将这本字典由 DOS 格式转换为 UNIX 格式(只以 LF 作为行结束符). # 将这个文件存储为纯文本形式, 并且保证是未压缩的 ASCII 格式. # 将 DEFAULT_DICTFILE 变量以 path/filename 形式设置好. E_BADARGS=65 MAXCONTEXTLINES=50 # 显示的最大行数. DEFAULT_DICTFILE="/usr/share/dict/webster1913-dict.txt" # 默认的路径和文件名. # 在必要的时候可以进行修改. # 注意: # ----- # 这个特定的 1913 年版的韦氏词典 #+ 在每个入口都是以大写字母开头的 #+ (剩余的字符都是小写). # 只有每部分的第一行是以这种形式开始的, #+ 这也就是为什么搜索算法是下边的这个样子. if [[ -z $(echo "$1" | sed -n '/^[A-Z]/p') ]] # 必须指定一个要查找的单词, #+ 并且这个单词必须以大写字母开头. then echo "Usage: `basename $0` Word-to-define [dictionary-file]" echo echo "Note: Word to look up must start with capital letter," echo "with the rest of the word in lowercase." echo "--------------------------------------------" echo "Examples: Abandon, Dictionary, Marking, etc." exit $E_BADARGS fi if [ -z "$2" ] # 也可以指定不同的词典 #+ 作为这个脚本的第 2 个参数传递进来. then dictfile=$DEFAULT_DICTFILE else dictfile="$2" fi # --------------------------------------------------------- Definition=$(fgrep -A $MAXCONTEXTLINES "$1 \\" "$dictfile") # 以 "Word \..." 这种形式定义 # # 当然, 即使搜索一个特别大的文本文件的时候 #+ "fgrep" 也是足够快的. # 现在, 剪掉定义块. echo "$Definition" | sed -n '1,/^[A-Z]/p' | # 从输出的第一行 #+ 打印到下一部分的第一行. sed '$d' | sed '$d' # 删除输出的最后两行 Delete last two lines of output #+ (空行和下一部分的第一行). # --------------------------------------------------------- exit 0 # 练习: # ----- # 1) 修改这个脚本, 让它具备能够处理任何字符形式的输入 # + (大写, 小写, 或大小写混合), 然后将其转换为 # + 能够处理的统一形式. # # 2) 将这个脚本转化为一个 GUI 应用, # + 使用一些比如像 "gdialog"的东西 . . . # 这样的话, 脚本将不再从命令行中 # + 取得这些参数. # # 3) 修改这个脚本让它具备能够分析另外一个 # + 公共词典的能力,比如 U.S. Census Bureau Gazetteer. ################################End Script######################################### Example 12-17 检查列表中单词的正确性 ################################Start Script####################################### #!/bin/bash # lookup: 对指定数据文件中的每个单词都做一遍字典查询.. file=words.data # 指定的要搜索的数据文件. echo while [ "$word" != end ] # 数据文件中最后一个单词. do read word # 从数据文件中读, 因为在循环的后边重定向了. look $word > /dev/null # 不想将字典文件中的行显示出来. lookup=$? # 'look' 命令的退出状态. if [ "$lookup" -eq 0 ] then echo "\"$word\" is valid." else echo "\"$word\" is invalid." fi done <"$file" # 将 stdin 重定向到 $file, 所以 "reads" 来自于 $file. echo exit 0 # ---------------------------------------------------- # 下边的代码行将不会执行, 因为上边已经有 "exit"命令了. # Stephane Chazelas 建议使用下边更简洁的方法: while read word && [[ $word != end ]] do if look "$word" > /dev/null then echo "\"$word\" is valid." else echo "\"$word\" is invalid." fi done <"$file" exit 0 ################################End Script######################################### Example 12-18 转换大写: 把一个文件的内容全部转换为大写. ################################Start Script####################################### #!/bin/bash # 把一个文件的内容全部转换为大写. E_BADARGS=65 if [ -z "$1" ] # 检查命令行参数. then echo "Usage: `basename $0` filename" exit $E_BADARGS fi tr a-z A-Z <"$1" # 与上边的作用相同, 但是使用了 POSIX 字符集标记方法: # tr '[:lower:]' '[:upper:]' <"$1" # Thanks, S.C. exit 0 # 练习: # 重写这个脚本, 通过选项可以控制脚本或者 #+ 转换为大写或者转换为小写. ################################End Script######################################### Example 12-19 转换小写: 将当前目录下的所有文全部转换为小写. ################################Start Script####################################### #!/bin/bash # # 将当前目录下的所有文全部转换为小写. # # 灵感来自于 John Dubois 的脚本, #+ 转换为 Bash 脚本, #+ 然后被本书作者精简了一下. for filename in * # 遍历当前目录下的所有文件. do fname=`basename $filename` n=`echo $fname | tr A-Z a-z` # 将名字修改为小写. if [ "$fname" != "$n" ] # 只对那些文件名不是小写的文件进行重命名. then mv $fname $n fi done exit $? # 下边的代码将不会被执行, 因为上边的 "exit". #-------------------------------------------# # 删除上边的内容,来运行下边的内容. # 对于那些文件名中包含空白和新行的文件, 上边的脚本就不能工作了. # Stephane Chazelas 因此建议使用下边的方法: for filename in * # 不必非得使用 basename 命令, # 因为 "*" 不会返回任何包含 "/" 的文件. do n=`echo "$filename/" | tr '[:upper:]' '[:lower:]'` # POSIX 字符集标记法. # 添加的斜线是为了在文件名结尾换行不会被 # 命令替换删掉. # 变量替换: n=${n%/} # 从文件名中将上边添加在结尾的斜线删除掉. [[ $filename == $n ]] || mv "$filename" "$n" # 检查文件名是否已经是小写. done exit $? ################################End Script######################################### Example 12-20 Du: DOS 到 UNIX 文本文件的转换. ################################Start Script####################################### #!/bin/bash # Du.sh: DOS 到 UNIX 文本文件的转换. E_WRONGARGS=65 if [ -z "$1" ] then echo "Usage: `basename $0` filename-to-convert" exit $E_WRONGARGS fi NEWFILENAME=$1.unx CR='\015' # 回车 Carriage return. # 015 是 8 进制的 ASCII 码的回车. # DOS 中文本文件的行结束符是 CR-LF. # UNIX 中文本文件的行结束符只是 LF. tr -d $CR < $1 > $NEWFILENAME # 删除回车并且写到新文件中. echo "Original DOS text file is \"$1\"." echo "Converted UNIX text file is \"$NEWFILENAME\"." exit 0 # 练习: # ----- # 修改上边的脚本完成从 UNIX 到 DOS 的转换. ################################End Script######################################### Example 12-21 rot13: rot13, 弱智加密. ################################Start Script####################################### #!/bin/bash # rot13.sh: 典型的 rot13 算法, # 使用这种方法加密可能可以愚弄一下 3 岁小孩. # 用法: ./rot13.sh filename # 或 ./rot13.sh (不太了解这句的内容, 应该是有特 定的含义) key=ETAOINSHRDLUBCFGJMQPVWZYXK # "key" 不过是一个乱序的字母表. # 修改 "key" 就会修改加密的结果. # The 'cat "$@"' construction gets input either from stdin or from files. # 如果使用 stdin, 那么要想结束输入就使用 Control-D. # 否则就要在命令行上指定文件名. cat "$@" | tr "a-z" "A-Z" | tr "A-Z" "$key" # | 转化为大写 | 加密 # 小写, 大写, 或混合大小写, 都可以正常工作. # 但是传递进来的非字母字符将不会起任何变化. # 用下边的语句试试这个脚本: # "Nothing so needs reforming as other people's habits." # --Mark Twain # # 输出为: # "CFPHRCS QF CIIOQ MINFMBRCS EQ FPHIM GIFGUI'Q HETRPQ." # --BEML PZERC # 解密: # cat "$@" | tr "$key" "A-Z" # 这个简单的密码可以轻易的被一个 12 岁的小孩 #+ 用铅笔和纸破解. exit 0 # 练习: # ----- # 修改这个脚本, 让它可以用命令行参数 #+ 来决定加密或解密. ################################End Script######################################### Example 12-23 格式化文件列表. ################################Start Script####################################### #!/bin/bash WIDTH=40 # 设为 40 列宽. b=`ls /usr/local/bin` # 取得文件列表... echo $b | fmt -w $WIDTH # 也可以使用如下方法,作用相同 # echo $b | fold - -s -w $WIDTH exit 0 ################################End Script######################################### Example 12-24 使用 column 来格式化目录列表 ################################Start Script####################################### #!/bin/bash # 这是"column" man 页中的一个例子, 作者对这个例子做了很小的修改. (printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \ ; ls -l | sed 1d) | column -t # 管道中的 "sed 1d" 删除输出的第一行, #+ 第一行将是 "total N", #+ 其中 "N" 是 "ls -l" 找到的文件总数. # "column" 中的 -t 选项用来转化为易于打印的表形式. exit 0 ################################End Script######################################### Example 12-25 nl: 一个自己计算行号的脚本. ################################Start Script####################################### #!/bin/bash # line-number.sh # 这个脚本将会 echo 自身两次, 并显示行号. # 'nl' 命令显示的时候你将会看到, 本行是第 4 行, 因为它不计空行. # 'cat -n' 命令显示的时候你将会看到, 本行是第 6 行. nl `basename $0` echo; echo # 下边, 让我们试试 'cat -n' cat -n `basename $0` # 区别就是 'cat -n' 对空行也进行计数. # 注意 'nl -ba' 也会这么做. exit 0 # ----------------------------------------------------------------- ################################End Script######################################### Example 12-26 manview: 查看格式化的 man 页 ################################Start Script####################################### #!/bin/bash # manview.sh: 将 man 页源文件格式化以方便查看. # 当你想阅读 man 页的时候, 这个脚本就有用了. # 它允许你在运行的时候查看 #+ 中间结果. E_WRONGARGS=65 if [ -z "$1" ] then echo "Usage: `basename $0` filename" exit $E_WRONGARGS fi # --------------------------- groff -Tascii -man $1 | less # 来自于 groff man 页. # --------------------------- # 如果 man 业中包括表或者等式, #+ 那么上边的代码就够呛了. # 下边的这行代码可以解决上边的这个问题. # # gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man # # Thanks, S.C. exit 0 ################################End Script######################################### Example 12-27 使用 cpio 来拷贝一个目录树 ################################Start Script####################################### #!/bin/bash # 使用 'cpio' 拷贝目录树. # 使用 'cpio' 的优点: # 加速拷贝. 比通过管道使用 'tar' 命令快一些. # 很适合拷贝一些 'cp' 命令 #+ 搞不定的的特殊文件(比如名字叫 pipes 的文件, 等等) ARGS=2 E_BADARGS=65 if [ $# -ne "$ARGS" ] then echo "Usage: `basename $0` source destination" exit $E_BADARGS fi source=$1 destination=$2 find "$source" -depth | cpio -admvp "$destination" # ^^^^^ ^^^^^ # 阅读 'find' 和 'cpio' 的 man 页来了解这些选项的意义. # 练习: # ----- # 添加一些代码来检查 'find | cpio' 管道命令的退出码($?) #+ 并且如果出现错误的时候输出合适的错误码. exit 0 ################################End Script######################################### Example 12-28 解包一个 rpm 归档文件 ################################Start Script####################################### #!/bin/bash # de-rpm.sh: 解包一个 'rpm' 归档文件 : ${1?"Usage: `basename $0` target-file"} # 必须指定 'rpm' 归档文件名作为参数. TEMPFILE=$$.cpio # Tempfile 必须是一个"唯一"的名字. # $$ 是这个脚本的进程 ID. rpm2cpio < $1 > $TEMPFILE # 将 rpm 归档文件转换为 cpio 归档文件. cpio --make-directories -F $TEMPFILE -i # 解包 cpio 归档文件. rm -f $TEMPFILE # 删除 cpio 归档文件. exit 0 # 练习: # 添加一些代码来检查 1) "target-file" 是否存在 #+ 2) 这个文件是否是一个 rpm 归档文件. # 暗示: 分析 'file' 命令的输出. ################################End Script######################################### Example 12-29 从 C 文件中去掉注释 ################################Start Script####################################### #!/bin/bash # strip-comment.sh: 去掉 C 程序中的注释 (/* 注释 */) E_NOARGS=0 E_ARGERROR=66 E_WRONG_FILE_TYPE=67 if [ $# -eq "$E_NOARGS" ] then echo "Usage: `basename $0` C-program-file" >&2 # 将错误消息发到 stderr. exit $E_ARGERROR fi # 检查文件类型是否正确. type=`file $1 | awk '{ print $2, $3, $4, $5 }'` # "file $1" echoe 出文件类型 . . . # 然后 awk 会删掉第一个域, 就是文件名 . . . # 然后结果将会传递到变量 "type" 中. correct_type="ASCII C program text" if [ "$type" != "$correct_type" ] then echo echo "This script works on C program files only." echo exit $E_WRONG_FILE_TYPE fi # 相当隐秘的 sed 脚本: #-------- sed ' /^\/\*/d /.*\*\//d ' $1 #-------- # 如果你花上几个小时来学习 sed 语法的话, 上边这个命令还是很好理解的. # 如果注释和代码在同一行上, 上边的脚本就不行了. #+ 所以需要添加一些代码来处理这种情况. # 这是一个很重要的练习. # 当然, 上边的代码也会删除带有 "*/" 的非注释行 -- #+ 这也不是一个令人满意的结果. exit 0 # ---------------------------------------------------------------- # 下边的代码不会执行, 因为上边已经 'exit 0' 了. # Stephane Chazelas 建议使用下边的方法: usage() { echo "Usage: `basename $0` C-program-file" >&2 exit 1 } WEIRD=`echo -n -e '\377'` # or WEIRD=$'\377' [[ $# -eq 1 ]] || usage case `file "$1"` in *"C program text"*) sed -e "s%/\*%${WEIRD}%g;s%\*/%${WEIRD}%g" "$1" \ | tr '\377\n' '\n\377' \ | sed -ne 'p;n' \ | tr -d '\n' | tr '\377' '\n';; *) usage;; esac # 如果是下列的这些情况, 还是很糟糕: # printf("/*"); # or # /* /* buggy embedded comment */ # # 为了处理上边所有这些特殊情况(字符串中的注释, 含有 \", \\" ... #+ 的字符串中的注释) 唯一的方法还是写一个 C 分析器 #+ (或许可以使用 lex 或者 yacc ?). exit 0 ################################End Script######################################### Example 12-30 Exploring /usr/X11R6/bin ################################Start Script####################################### #!/bin/bash # 在 /usr/X11R6/bin 中的所有神秘的 2 进制文件都是什么东西? DIRECTORY="/usr/X11R6/bin" # 也试试 "/bin", "/usr/bin", "/usr/local/bin", 等等. for file in $DIRECTORY/* do whatis `basename $file` # 将会 echo 出这个 2 进制文件的信息. done exit 0 # 你可能希望将这个脚本的输出重定向, 像这样: # ./what.sh >>whatis.db # 或者一页一页的在 stdout 上查看, # ./what.sh | less ################################End Script######################################### Example 12-31 一个"改进过"的 strings 命令 ################################Start Script####################################### #!/bin/bash # wstrings.sh: "word-strings" (增强的 "strings" 命令) # # 这个脚本将会过滤 "strings" 命令的输出. #+ 通过排除标准单词列表的形式检查来过滤输出. # 这将有效的过滤掉无意义的字符, #+ 并且指挥输出可以识别的字符. # =========================================================== # 脚本参数的标准检查 ARGS=1 E_BADARGS=65 E_NOFILE=66 if [ $# -ne $ARGS ] then echo "Usage: `basename $0` filename" exit $E_BADARGS fi if [ ! -f "$1" ] # 检查文件是否存在. then echo "File \"$1\" does not exist." exit $E_NOFILE fi # =========================================================== MINSTRLEN=3 # 最小的字符串长度. WORDFILE=/usr/share/dict/linux.words # 字典文件. # 也可以指定一个不同的 #+ 单词列表文件, #+ 但这种文件必须是以每个单词一行的方式进行保存. wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \ tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '` # 将'strings' 命令的输出通过管道传递到多个 'tr' 命令中. # "tr A-Z a-z" 全部转换为小写字符. # "tr '[:space:]'" 转换空白字符为多个 Z. # "tr -cs '[:alpha:]' Z" 将非字母表字符转换为多个 Z, #+ 然后去除多个连续的 Z. # "tr -s '\173-\377' Z" 把所有 z 后边的字符都转换为 Z. #+ 并且去除多余重复的Z.(注意173(123 ascii "{")和377(255 ascii 最后一个字符)都是8进制) #+ 这样处理之后, 我们所有之前需要处理的令我们头痛的字符 #+ 就全都转换为字符 Z 了. # 最后"tr Z ' '" 将把所有的 Z 都转换为空格, #+ 这样我们在下边循环中用到的变量 wlist 中的内容就全部以空格分隔了. # **************************************************************** # 注意, 我们使用管道来将多个 'tr' 的输出传递到下一个 'tr' 时 #+ 每次都使用了不同的参数. # **************************************************************** for word in $wlist # 重要: # $wlist 这里不能使用双引号. # "$wlist" 不能正常工作. # 为什么不行? do strlen=${#word} # 字符串长度. if [ "$strlen" -lt "$MINSTRLEN" ] # 跳过短的字符串. then continue fi grep -Fw $word "$WORDFILE" # 只匹配整个单词. # ^^^ # "固定字符串" 和 #+ "整个单词" 选项. done exit $? ################################End Script######################################### Example 12-32 在一个脚本中使用 cmp 来比较 2 个文件. ################################Start Script####################################### #!/bin/bash ARGS=2 # 脚本需要 2 个参数. E_BADARGS=65 E_UNREADABLE=66 if [ $# -ne "$ARGS" ] then echo "Usage: `basename $0` file1 file2" exit $E_BADARGS fi if [[ ! -r "$1" || ! -r "$2" ]] then echo "Both files to be compared must exist and be readable." exit $E_UNREADABLE fi cmp $1 $2 &> /dev/null # /dev/null 将会禁止 "cmp" 命令的输出. # cmp -s $1 $2 与上边这句结果相同 ("-s" 选项是安静标志) # Thank you Anders Gustavsson for pointing this out. # # 用 'diff' 命令也可以, 比如, diff $1 $2 &> /dev/null if [ $? -eq 0 ] # 测试 "cmp" 命令的退出码. then echo "File \"$1\" is identical to file \"$2\"." else echo "File \"$1\" differs from file \"$2\"." fi exit 0 ################################End Script######################################### Example 12-33 basename 和 dirname ################################Start Script####################################### #!/bin/bash a=/home/bozo/daily-journal.txt echo "Basename of /home/bozo/daily-journal.txt = `basename $a`" echo "Dirname of /home/bozo/daily-journal.txt = `dirname $a`" echo echo "My own home is `basename ~/`." # `basename ~` also works. echo "The home of my home is `dirname ~/`." # `dirname ~` also works. exit 0 ################################End Script######################################### Example 12-34 检查文件完整性 ################################Start Script####################################### 1 #!/bin/bash 2 # file-integrity.sh: 检查一个给定目录下的文件 3 # 是否被改动了. 4 5 E_DIR_NOMATCH=70 6 E_BAD_DBFILE=71 7 8 dbfile=File_record.md5 9 # 存储记录的文件名 (数据库文件). 10 11 12 set_up_database () 13 { 14 echo ""$directory"" > "$dbfile" 15 # 把目录名写到文件的第一行. 16 md5sum "$directory"/* >> "$dbfile" 17 # 在文件中附上 md5 checksums 和 filenames. 18 } 19 20 check_database () 21 { 22 local n=0 23 local filename 24 local checksum 25 26 # ------------------------------------------- # 27 # 这个文件检查其实是不必要的, 28 #+ 但是能安全一些. 29 30 if [ ! -r "$dbfile" ] 31 then 32 echo "Unable to read checksum database file!" 33 exit $E_BAD_DBFILE 34 fi 35 # ------------------------------------------- # 36 37 while read record[n] 38 do 39 40 directory_checked="${record[0]}" 41 if [ "$directory_checked" != "$directory" ] 42 then 43 echo "Directories do not match up!" 44 # 换个目录试一下. 45 exit $E_DIR_NOMATCH 46 fi 47 48 if [ "$n" -gt 0 ] # 不是目录名. 49 then 50 filename[n]=$( echo ${record[$n]} | awk '{ print $2 }' ) 51 # md5sum 向后写记录, 52 #+ 先写 checksum, 然后写 filename. 53 checksum[n]=$( md5sum "${filename[n]}" ) 54 55 56 if [ "${record[n]}" = "${checksum[n]}" ] 57 then 58 echo "${filename[n]} unchanged." 59 60 elif [ "`basename ${filename[n]}`" != "$dbfile" ] 61 # 跳过 checksum 数据库文件, 62 #+ 因为在每次调用脚本它都会被修改. 63 # --- 64 # 这不幸的意味着当我们在 $PWD 中运行这个脚本 65 #+ 时, 修改这个 checksum 数 66 #+ 据库文件将不会被检测出来. 67 # 练习: 修复这个问题. 68 then 69 echo "${filename[n]} : CHECKSUM ERROR!" 70 # 因为最后的检查, 文件已经被修改. 71 fi 72 73 fi 74 75 76 77 let "n+=1" 78 done <"$dbfile" # 从 checksum 数据库文件中读. 79 80 } 81 82 # =================================================== # 83 # main () 84 85 if [ -z "$1" ] 86 then 87 directory="$PWD" # 如果没制定参数, 88 else #+ 那么就使用当前的工作目录. 89 directory="$1" 90 fi 91 92 clear # 清屏. 93 echo " Running file integrity check on $directory" 94 echo 95 96 # ------------------------------------------------------------------ # 97 if [ ! -r "$dbfile" ] # 是否需要建立数据库文件? 98 then 99 echo "Setting up database file, \""$directory"/"$dbfile"\"."; echo 100 set_up_database 101 fi 102 # ------------------------------------------------------------------ # 103 104 check_database # 调用主要处理函数. 105 106 echo 107 108 # 你可能想把这个脚本的输出重定向到文件中, 109 #+ 尤其在这个目录中有很多文件的时候. 110 111 exit 0 112 113 # 如果要对数量非常多的文件做完整性检查, 114 #+ 可以考虑一下 "Tripwire" 包, 115 #+ http://sourceforge.net/projects/tripwire/. 116 ################################End Script######################################### Example 12-35 Uudecod 编码后的文件 ################################Start Script####################################### #!/bin/bash # 在当前目录下 uudecode 所有用 uuencode 编码的文件. lines=35 # 允许读头部的 35 行(范围很宽). for File in * # Test 所有 $PWD 下的文件. do search1=`head -$lines $File | grep begin | wc -w` search2=`tail -$lines $File | grep end | wc -w` # Uuencode 过的文件在文件开始的地方有个 "begin", #+ 在文件结尾的地方有个 "end". if [ "$search1" -gt 0 ] then if [ "$search2" -gt 0 ] then echo "uudecoding - $File -" uudecode $File fi fi done # 小心不要让这个脚本运行自己, #+ 因为它也会把自身也认为是一个 uuencoded 文件, #+ 这都是因为这个脚本自身也包含 "begin" 和 "end". # 练习: # ----- # 修改这个脚本, 让它可以检查一个新闻组的每个文件, #+ 并且如果下一个没找的话就跳过. exit 0 ################################End Script######################################### Example 12-36 查找滥用的连接来报告垃圾邮件发送者 ################################Start Script####################################### #!/bin/bash # spam-lookup.sh: 查找滥用的连接来报告垃圾邮件发送者. # 感谢 Michael Zick. # 检查命令行参数. ARGCOUNT=1 E_WRONGARGS=65 if [ $# -ne "$ARGCOUNT" ] then echo "Usage: `basename $0` domain-name" exit $E_WRONGARGS fi dig +short $1.contacts.abuse.net -c in -t txt # 也试试: # dig +nssearch $1 # 尽量找到 "可信赖的名字服务器" 并且显示 SOA 记录. # 下边这句也可以: # whois -h whois.abuse.net $1 # ^^ ^^^^^^^^^^^^^^^ 指定主机. # 使用这个命令也可以查找多个垃圾邮件发送者, 比如:" # whois -h whois.abuse.net $spamdomain1 $spamdomain2 . . . # 练习: # ----- # 扩展这个脚本的功能, #+ 让它可以自动发送 e-mail 来通知 #+ 需要对此负责的 ISP 的联系地址. # 暗示: 使用 "mail" 命令. exit $? # spam-lookup.sh chinatietong.com # 一个已知的垃圾邮件域.(译者: 中国铁通. . .) # "crnet_mgr@chinatietong.com" # "crnet_tec@chinatietong.com" # "postmaster@chinatietong.com" # 如果想找到这个脚本的一个更详尽的版本, #+ 请访问 SpamViz 的主页, http://www.spamviz.net/index.html. ################################End Script######################################### Example 12-37 分析一个垃圾邮件域 ################################Start Script####################################### 1 #! /bin/bash 2 # is-spammer.sh: 鉴别一个垃圾邮件域 3 4 # $Id: is-spammer, v 1.4 2004/09/01 19:37:52 mszick Exp $ 5 # 上边这行是 RCS ID 信息. 6 # 7 # 这是附件中捐献脚本 is_spammer.bash 8 #+ 的一个简单版本. 9 10 # is-spammer 11 12 # 使用外部程序: 'dig' 13 # 测试版本: 9.2.4rc5 14 15 # 使用函数. 16 # 使用 IFS 来分析分配在数组中的字符串. 17 # 检查 e-mail 黑名单. 18 19 # 使用来自文本体中的 domain.name: 20 # http://www.good_stuff.spammer.biz/just_ignore_everything_else 21 # ^^^^^^^^^^^ 22 # 或者使用来自任意 e-mail 地址的 domain.name: 23 # Really_Good_Offer@spammer.biz 24 # 25 # 并将其作为这个脚本的唯一参数. 26 #(另: 你的 Inet 连接应该保证连接) 27 # 28 # 这样, 在上边两个实例中调用这个脚本: 29 # is-spammer.sh spammer.biz 30 31 32 # Whitespace == :Space:Tab:Line Feed:Carriage Return: 33 WSP_IFS=$'\x20'$'\x09'$'\x0A'$'\x0D' 34 35 # No Whitespace == Line Feed:Carriage Return 36 No_WSP=$'\x0A'$'\x0D' 37 38 # 域分隔符为点分 10 进制 ip 地址 39 ADR_IFS=${No_WSP}'.' 40 41 # 取得 dns 文本资源记录. 42 # get_txt 43 get_txt() { 44 45 # 分析在"."中分配的 $1. 46 local -a dns 47 IFS=$ADR_IFS 48 dns=( $1 ) 49 IFS=$WSP_IFS 50 if [ "${dns[0]}" == '127' ] 51 then 52 # 查看此处是否有原因. 53 echo $(dig +short $2 -t txt) 54 fi 55 } 56 57 # 取得 dns 地址资源记录. 58 # chk_adr 59 chk_adr() { 60 local reply 61 local server 62 local reason 63 64 server=${1}${2} 65 reply=$( dig +short ${server} ) 66 67 # 假设应答可能是一个错误码 . . . 68 if [ ${#reply} -gt 6 ] 69 then 70 reason=$(get_txt ${reply} ${server} ) 71 reason=${reason:-${reply}} 72 fi 73 echo ${reason:-' not blacklisted.'} 74 } 75 76 # 需要从名字中取得 IP 地址. 77 echo 'Get address of: '$1 78 ip_adr=$(dig +short $1) 79 dns_reply=${ip_adr:-' no answer '} 80 echo ' Found address: '${dns_reply} 81 82 # 一个可用的应答至少是 4 个数字加上 3 个点. 83 if [ ${#ip_adr} -gt 6 ] 84 then 85 echo 86 declare query 87 88 # 分析点中的分配. 89 declare -a dns 90 IFS=$ADR_IFS 91 dns=( ${ip_adr} ) 92 IFS=$WSP_IFS 93 94 # Reorder octets into dns query order. 95 rev_dns="${dns[3]}"'.'"${dns[2]}"'.'"${dns[1]}"'.'"${dns[0]}"'.' 96 97 # 参见: http://www.spamhaus.org (Conservative, well maintained) 98 echo -n 'spamhaus.org says: ' 99 echo $(chk_adr ${rev_dns} 'sbl-xbl.spamhaus.org') 100 101 # 参见: http://ordb.org (Open mail relays) 102 echo -n ' ordb.org says: ' 103 echo $(chk_adr ${rev_dns} 'relays.ordb.org') 104 105 # 参见: http://www.spamcop.net/ (你可以在这里报告 spammer) 106 echo -n ' spamcop.net says: ' 107 echo $(chk_adr ${rev_dns} 'bl.spamcop.net') 108 109 # # # 其他的黑名单操作 # # # 110 111 # 参见: http://cbl.abuseat.org. 112 echo -n ' abuseat.org says: ' 113 echo $(chk_adr ${rev_dns} 'cbl.abuseat.org') 114 115 # 参见: http://dsbl.org/usage (Various mail relays) 116 echo 117 echo 'Distributed Server Listings' 118 echo -n ' list.dsbl.org says: ' 119 echo $(chk_adr ${rev_dns} 'list.dsbl.org') 120 121 echo -n ' multihop.dsbl.org says: ' 122 echo $(chk_adr ${rev_dns} 'multihop.dsbl.org') 123 124 echo -n 'unconfirmed.dsbl.org says: ' 125 echo $(chk_adr ${rev_dns} 'unconfirmed.dsbl.org') 126 127 else 128 echo 129 echo 'Could not use that address.' 130 fi 131 132 exit 0 133 134 # 练习: 135 # ----- 136 137 # 1) 检查脚本的参数, 138 # 并且如果必要的话使用合适的错误消息退出. 139 140 # 2) 检查调用这个脚本的时候是否在线, 141 # 并且如果必要的话使用合适的错误消息退出. 142 143 # 3) Substitute generic variables for "hard-coded" BHL domains. 144 145 # 4) 通过对 'dig' 命令使用 "+time=" 选项 146 来给这个脚本设置一个暂停. ################################End Script######################################### Example 12-38 获得一份股票报价 ################################Start Script####################################### #!/bin/bash # quote-fetch.sh: 下载一份股票报价. E_NOPARAMS=66 if [ -z "$1" ] # 必须指定需要获取的股票(代号). then echo "Usage: `basename $0` stock-symbol" exit $E_NOPARAMS fi stock_symbol=$1 file_suffix=.html # 获得一个 HTML 文件, 所以要正确命名它. URL='http://finance.yahoo.com/q?s=' # Yahoo 金融板块, 后缀是股票查询. # ----------------------------------------------------------- wget -O ${stock_symbol}${file_suffix} "${URL}${stock_symbol}" # ----------------------------------------------------------- # 在 http://search.yahoo.com 上查询相关材料: # ----------------------------------------------------------- # URL="http://search.yahoo.com/search?fr=ush-news&p=${query}" # wget -O "$savefilename" "${URL}" # ----------------------------------------------------------- # 保存相关 URL 的列表. exit $? # 练习: # ----- # # 1) 添加一个测试来验证用户正在线. # (暗示: 对 "ppp" 或 "connect" 来分析 'ps -ax' 的输出. # # 2) 修改这个脚本, 让这个脚本具有获得本地天气预报的能力, #+ 将用户的 zip code 作为参数. ################################End Script######################################### Example 12-39 更新 Fedora 4 ################################Start Script####################################### 1 #!/bin/bash 2 # fc4upd.sh 3 4 # 脚本作者: Frank Wang. 5 # 本书作者作了少量修改. 6 # 授权在本书中使用. 7 8 9 # 使用 rsync 命令从镜像站点上下载 Fedora 4 的更新. 10 # 为了节省空间, 如果有多个版本存在的话, 11 #+ 只下载最新的包. 12 13 URL=rsync://distro.ibiblio.org/fedora-linux-core/updates/ 14 # URL=rsync://ftp.kddilabs.jp/fedora/core/updates/ 15 # URL=rsync://rsync.planetmirror.com/fedora-linux-core/updates/ 16 17 DEST=${1:-/var/www/html/fedora/updates/} 18 LOG=/tmp/repo-update-$(/bin/date +%Y-%m-%d).txt 19 PID_FILE=/var/run/${0##*/}.pid 20 21 E_RETURN=65 # 某些意想不到的错误. 22 23 24 # 一搬 rsync 选项 25 # -r: 递归下载 26 # -t: 保存时间 27 # -v: verbose 28 29 OPTS="-rtv --delete-excluded --delete-after --partial" 30 31 # rsync include 模式 32 # Leading slash causes absolute path name match. 33 INCLUDE=( 34 "/4/i386/kde-i18n-Chinese*" 35 # ^ ^ 36 # 双引号是必须的, 用来防止 file globbing. 37 ) 38 39 40 # rsync exclude 模式 41 # 使用 "#" 临时注释掉一些不需要的包. 42 EXCLUDE=( 43 /1 44 /2 45 /3 46 /testing 47 /4/SRPMS 48 /4/ppc 49 /4/x86_64 50 /4/i386/debug 51 "/4/i386/kde-i18n-*" 52 "/4/i386/openoffice.org-langpack-*" 53 "/4/i386/*i586.rpm" 54 "/4/i386/GFS-*" 55 "/4/i386/cman-*" 56 "/4/i386/dlm-*" 57 "/4/i386/gnbd-*" 58 "/4/i386/kernel-smp*" 59 # "/4/i386/kernel-xen*" 60 # "/4/i386/xen-*" 61 ) 62 63 64 init () { 65 # 让管道命令返回可能的 rsync 错误, 比如, 网络延时(stalled network). 66 set -o pipefail 67 68 TMP=${TMPDIR:-/tmp}/${0##*/}.$$ # 保存精炼的下载列表. 69 trap "{ 70 rm -f $TMP 2>/dev/null 71 }" EXIT # 删除存在的临时文件. 72 } 73 74 75 check_pid () { 76 # 检查进程是否存在. 77 if [ -s "$PID_FILE" ]; then 78 echo "PID file exists. Checking ..." 79 PID=$(/bin/egrep -o "^[[:digit:]]+" $PID_FILE) 80 if /bin/ps --pid $PID &>/dev/null; then 81 echo "Process $PID found. ${0##*/} seems to be running!" 82 /usr/bin/logger -t ${0##*/} \ 83 "Process $PID found. ${0##*/} seems to be running!" 84 exit $E_RETURN 85 fi 86 echo "Process $PID not found. Start new process . . ." 87 fi 88 } 89 90 91 # 根据上边的模式, 92 #+ 设置整个文件的更新范围, 从 root 或 $URL 开始. 93 set_range () { 94 include= 95 exclude= 96 for p in "${INCLUDE[@]}"; do 97 include="$include --include \"$p\"" 98 done 99 100 for p in "${EXCLUDE[@]}"; do 101 exclude="$exclude --exclude \"$p\"" 102 done 103 } 104 105 106 # 获得并提炼 rsync 更新列表. 107 get_list () { 108 echo $$ > $PID_FILE || { 109 echo "Can't write to pid file $PID_FILE" 110 exit $E_RETURN 111 } 112 113 echo -n "Retrieving and refining update list . . ." 114 115 # 获得列表 -- 为了作为单个命令来运行 rsync 需要 'eval'. 116 # $3 和 $4 是文件创建的日期和时间. 117 # $5 是完整的包名字. 118 previous= 119 pre_file= 120 pre_date=0 121 eval /bin/nice /usr/bin/rsync \ 122 -r $include $exclude $URL | \ 123 egrep '^dr.x|^-r' | \ 124 awk '{print $3, $4, $5}' | \ 125 sort -k3 | \ 126 { while read line; do 127 # 获得这段运行的秒数, 过滤掉不用的包. 128 cur_date=$(date -d "$(echo $line | awk '{print $1, $2}')" +%s) 129 # echo $cur_date 130 131 # 取得文件名. 132 cur_file=$(echo $line | awk '{print $3}') 133 # echo $cur_file 134 135 # 如果可能的话, 从文件名中取得 rpm 的包名字. 136 if [[ $cur_file == *rpm ]]; then 137 pkg_name=$(echo $cur_file | sed -r -e \ 138 's/(^([^_-]+[_-])+)[[:digit:]]+\..*[_-].*$/\1/') 139 else 140 pkg_name= 141 fi 142 # echo $pkg_name 143 144 if [ -z "$pkg_name" ]; then # 如果不是一个 rpm 文件, 145 echo $cur_file >> $TMP #+ 然后添加到下载列表里. 146 elif [ "$pkg_name" != "$previous" ]; then # 发现一个新包. 147 echo $pre_file >> $TMP # 输出最新的文件. 148 previous=$pkg_name # 保存当前状态. 149 pre_date=$cur_date 150 pre_file=$cur_file 151 elif [ "$cur_date" -gt "$pre_date" ]; then # 如果是相同的包, 但是更新一些, 152 pre_date=$cur_date #+ 那么就更新最新的. 153 pre_file=$cur_file 154 fi 155 done 156 echo $pre_file >> $TMP # TMP 现在包含所有 157 #+ 提炼过的列表. 158 # echo "subshell=$BASH_SUBSHELL" 159 160 } # 这里的打括号是为了让最后这句"echo $pre_file >> $TMP" 161 # 也能与整个循环一起放到同一个子 shell ( 1 )中. 162 163 RET=$? # 取得管道命令的返回码. 164 165 [ "$RET" -ne 0 ] && { 166 echo "List retrieving failed with code $RET" 167 exit $E_RETURN 168 } 169 170 echo "done"; echo 171 } 172 173 # 真正的 rsync 的下载部分. 174 get_file () { 175 176 echo "Downloading..." 177 /bin/nice /usr/bin/rsync \ 178 $OPTS \ 179 --filter "merge,+/ $TMP" \ 180 --exclude '*' \ 181 $URL $DEST \ 182 | /usr/bin/tee $LOG 183 184 RET=$? 185 186 # --filter merge,+/ is crucial for the intention. 187 # + modifier means include and / means absolute path. 188 # Then sorted list in $TMP will contain ascending dir name and 189 #+ prevent the following --exclude '*' from "shortcutting the circuit." 190 191 echo "Done" 192 193 rm -f $PID_FILE 2>/dev/null 194 195 return $RET 196 } 197 198 # ------- 199 # Main 200 init 201 check_pid 202 set_range 203 get_list 204 get_file 205 RET=$? 206 # ------- 207 208 if [ "$RET" -eq 0 ]; then 209 /usr/bin/logger -t ${0##*/} "Fedora update mirrored successfully." 210 else 211 /usr/bin/logger -t ${0##*/} "Fedora update mirrored with failure code: $RET" 212 fi 213 214 exit $RET ################################End Script######################################### Example 12-40 使用 ssh ################################Start Script####################################### #!/bin/bash # remote.bash: 使用 ssh. # 这个例子是 Michael Zick 编写的. # 授权使用. # 假设: # ----- # fd-2(文件描述符 2) 并没有被抛弃 ( '2>/dev/null' ). # ssh/sshd 假设 stderr ('2') 将会被显示给用户. # # sshd 正运行在你的机器上. # 对于大多数 '标准' 的发行版, 是应该有的, #+ 并且没有一些稀奇古怪的 ssh-keygen. # 在你的机器上从命令行中试一下 ssh: # # $ ssh $HOSTNAME # 不同特殊的准备, 你将被要求输入你的密码. # 输入密码 # 完成后, $ exit # # 好使了么? 如果好使了, 你可以做好准备来获取更多的乐趣了. # 在你的机器上用 'root'身份来试试 ssh: # # $ ssh -l root $HOSTNAME # 当询问密码时, 输入 root 的密码, 别输入你的密码. # Last login: Tue Aug 10 20:25:49 2004 from localhost.localdomain # 完成后键入 'exit'. # 上边的动作将会给你一个交互的 shell. # 在 'single command' 模式下建立 sshd 是可能的, #+ 不过这已经超出本例的范围了. # 唯一需要注意的事情是下面都可以工作在 #+ 'single command' 模式. # 一个基本的写输出(本地)命令. ls -l # 现在在远端机器上使用同样的基本命令. # 使用一套不同的 'USERNAME' 和 'HOSTNAME' : USER=${USERNAME:-$(whoami)} HOST=${HOSTNAME:-$(hostname)} # 现在在远端主机上运行上边的命令行命令, #+ 当然, 所有的传输都被加密了. ssh -l ${USER} ${HOST} " ls -l " # 期望的结果就是在远端主机上列出你的 #+ username 主目录的所有文件. # 如果想看点不一样的, 那就 #+ 在别的地方运行这个脚本, 别再你的主目录上运行这个脚本. # 换句话说, Bash 命令已经作为一个引用行 #+ 被传递到远端的 shell 中了,这样就可以在远端的机器上运行它了. # 在这种情况下, sshd 代表你运行了 ' bash -c "ls -l" '. # 对于每个命令行如果想不输入密码的话, #+ 对于这种类似的议题, 可以参阅 #+ man ssh #+ man ssh-keygen #+ man sshd_config. exit 0 ################################End Script######################################### Example 12-41 一个可以 mail 自己的脚本 ################################Start Script####################################### #!/bin/sh # self-mailer.sh: mail 自己的脚本 adr=${1:-`whoami`} # 如果不指定的话, 默认是当前用户. # 键入 'self-mailer.sh wiseguy@superdupergenius.com' #+ 发送这个脚本到这个地址. # 如果只键入 'self-mailer.sh' (不给参数) 的话, 那么这脚本就会被发送给 #+ 调用者, 比如 bozo@localhost.localdomain. # # 如果想了解 ${parameter:-default} 结构的更多细节, #+ 请参见第 9 章 变量重游中的 #+ 第 3 节 参数替换. # ====================================================================== ====== cat $0 | mail -s "Script \"`basename $0`\" has mailed itself to you." "$adr" # ====================================================================== ====== # -------------------------------------------- # 来自 self-mailing 脚本的一份祝福. # 一个喜欢恶搞的家伙运行了这个脚本, #+ 这导致了他自己收到了这份 mail. # 显然的, 有些人确实没什么事好做, #+ 就只能浪费他们自己的时间玩了. # -------------------------------------------- echo "At `date`, script \"`basename $0`\" mailed to "$adr"." exit 0 ################################End Script######################################### Example 12-42 按月偿还贷款 ################################Start Script####################################### #!/bin/bash # monthlypmt.sh: 计算按月偿还贷款的数量. # 这份代码是一份修改版本, 原始版本在 "mcalc" (贷款计算)包中, #+ 这个包的作者是 Jeff Schmidt 和 Mendel Cooper (本书作者). # http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz [15k] echo echo "Given the principal, interest rate, and term of a mortgage," echo "calculate the monthly payment." bottom=1.0 echo echo -n "Enter principal (no commas) " read principal echo -n "Enter interest rate (percent) " # 如果是 12%, 那就键入 "12", 别输入 ".12". read interest_r echo -n "Enter term (months) " read term interest_r=$(echo "scale=9; $interest_r/100.0" | bc) # 转换成小数. # "scale" 指定了有效数字的个数. interest_rate=$(echo "scale=9; $interest_r/12 + 1.0" | bc) top=$(echo "scale=9; $principal*$interest_rate^$term" | bc) echo; echo "Please be patient. This may take a while." let "months = $term - 1" # ==================================================================== for ((x=$months; x > 0; x--)) do bot=$(echo "scale=9; $interest_rate^$x" | bc) bottom=$(echo "scale=9; $bottom+$bot" | bc) # bottom = $(($bottom + $bot")) done # ==================================================================== # -------------------------------------------------------------------- # Rick Boivie 给出了一个对上边循环的修改, #+ 这个修改更加有效率, 将会节省大概 2/3 的时间. # for ((x=1; x <= $months; x++)) # do # bottom=$(echo "scale=9; $bottom * $interest_rate + 1" | bc) # done # 然后他又想出了一个更加有效率的版本, #+ 将会节省 95% 的时间! # bottom=`{ # echo "scale=9; bottom=$bottom; interest_rate=$interest_rate" # for ((x=1; x <= $months; x++)) # do # echo 'bottom = bottom * interest_rate + 1' # done # echo 'bottom' # } | bc` # 在命令替换中嵌入一个 'for 循环'. # -------------------------------------------------------------------------- # On the other hand, Frank Wang suggests: # bottom=$(echo "scale=9; ($interest_rate^$term-1)/($interest_rate-1)" | bc) # 因为 . . . # 在循环后边的算法 #+ 事实上是一个等比数列的求和公式. # 求和公式是 e0(1-q^n)/(1-q), #+ e0 是第一个元素 并且 q=e(n+1)/e(n) #+ 和 n 是元素的数量. # -------------------------------------------------------------------------- # let "payment = $top/$bottom" payment=$(echo "scale=2; $top/$bottom" | bc) # 使用 2 位有效数字来表示美元和美分. echo echo "monthly payment = \$$payment" # 在总和的前边显示美元符号. echo exit 0 # 练习: # 1) 处理输入允许本金总数中的逗号. # 2) 处理输入允许按照百分号和小数点的形式输入利率. # 3) 如果你真正想好好编写这个脚本, # 那么就扩展这个脚本让它能够打印出完整的分期付款表. ################################End Script######################################### Example 12-43 数制转换 ################################Start Script####################################### 1 #!/bin/bash 2 ########################################################################## 3 # 脚本 : base.sh - 用不同的数值来打印数字 (Bourne Shell) 4 # 作者 : Heiner Steven (heiner.steven@odn.de) 5 # 日期 : 07-03-95 6 # 类型 : 桌面 7 # $Id: base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $ 8 # ==> 上边这行是 RCS ID 信息. 9 ########################################################################## 10 # 描述 11 # 12 # Changes 13 # 21-03-95 stv fixed error occuring with 0xb as input (0.2) 14 ########################################################################## 15 16 # ==> 在本书中使用这个脚本通过了作者的授权. 17 # ==> 注释是本书作者添加的. 18 19 NOARGS=65 20 PN=`basename "$0"` # 程序名 21 VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2` # ==> VER=1.2 22 23 Usage () { 24 echo "$PN - print number to different bases, $VER (stv '95) 25 usage: $PN [number ...] 26 27 If no number is given, the numbers are read from standard input. 28 A number may be 29 binary (base 2) starting with 0b (i.e. 0b1100) 30 octal (base 8) starting with 0 (i.e. 014) 31 hexadecimal (base 16) starting with 0x (i.e. 0xc) 32 decimal otherwise (i.e. 12)" >&2 33 exit $NOARGS 34 } # ==> 打印出用法信息的函数. 35 36 Msg () { 37 for i # ==> 省略 [list] . 38 do echo "$PN: $i" >&2 39 done 40 } 41 42 Fatal () { Msg "$@"; exit 66; } 43 44 PrintBases () { 45 # 决定数值的数制 46 for i # ==> 省略 [list]... 47 do # ==> 所以是对命令行参数进行操作. 48 case "$i" in 49 0b*) ibase=2;; # 2 进制 50 0x*|[a-f]*|[A-F]*) ibase=16;; # 16 进制 51 0*) ibase=8;; # 8 进制 52 [1-9]*) ibase=10;; # 10 进制 53 *) 54 Msg "illegal number $i - ignored" 55 continue;; 56 esac 57 58 # 去掉前缀, 将 16 进制数字转换为大写(bc 需要大写) 59 number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'` 60 # ==>使用":" 作为 sed 分隔符, 而不使用"/". 61 62 # 将数字转换为 10 进制 63 dec=`echo "ibase=$ibase; $number" | bc` # ==> 'bc' 是个计算工具. 64 case "$dec" in 65 [0-9]*) ;; # 数字没问题 66 *) continue;; # 错误: 忽略 67 esac 68 69 # 在一行上打印所有的转换后的数字. 70 # ==> 'here document' 提供命令列表给'bc'. 71 echo `bc < 这里必须使用一个 "while 循环", 84 # ==>+ 因为所有的 case 都可能退出循环或者 85 # ==>+ 结束脚本. 86 # ==> (感谢, Paulo Marcel Coelho Aragao.) 87 do 88 case "$1" in 89 --) shift; break;; 90 -h) Usage;; # ==> 帮助信息. 91 -*) Usage;; 92 *) break;; # 第一个数字 93 esac # ==> 对于非法输入更严格检查是非常有用的. 94 shift 95 done 96 97 if [ $# -gt 0 ] 98 then 99 PrintBases "$@" 100 else # 从标准输入中读取 101 while read line 102 do 103 PrintBases $line 104 done 105 fi 106 107 108 exit 0 ################################End Script######################################### Example 12-44 使用 "here document" 来调用 bc ################################Start Script####################################### #!/bin/bash # 使用命令替换来调用 'bc' # 并与 'here document' 相结合. var1=`bc << EOF 18.33 * 19.78 EOF ` echo $var1 # 362.56 # $( ... ) 这种标记法也可以. v1=23.53 v2=17.881 v3=83.501 v4=171.63 var2=$(bc << EOF scale = 4 a = ( $v1 + $v2 ) b = ( $v3 * $v4 ) a * b + 15.35 EOF ) echo $var2 # 593487.8452 var3=$(bc -l << EOF scale = 9 s ( 1.7 ) EOF ) # 返回弧度为 1.7 的正弦. # "-l" 选项将会调用 'bc' 算数库. echo $var3 # .991664810 # 现在, 在函数中试一下... hyp= # 声明全局变量. hypotenuse () # 计算直角三角形的斜边. { hyp=$(bc -l << EOF scale = 9 sqrt ( $1 * $1 + $2 * $2 ) EOF ) # 不幸的是, 不能从 bash 函数中返回浮点值. } hypotenuse 3.68 7.31 echo "hypotenuse = $hyp" # 8.184039344 exit 0 ################################End Script######################################### Example 12-45 计算圆周率 ################################Start Script####################################### 1 #!/bin/bash 2 # cannon.sh: 通过开炮来取得近似的圆周率值. 3 4 # 这事实上是一个"Monte Carlo"蒙特卡洛模拟的非常简单的实例: 5 #+ 蒙特卡洛模拟是一种由现实事件抽象出来的数学模型, 6 #+ 由于要使用随机抽样统计来估算数学函数, 所以使用伪随机数来模拟真正的随机. 7 8 # 想象有一个完美的正方形土地, 边长为 10000 个单位. 9 # 在这块土地的中间有一个完美的圆形湖, 10 #+ 这个湖的直径是 10000 个单位. 11 # 这块土地的绝大多数面积都是水, 当然只有 4 个角上有一些土地. 12 # (可以把这个湖想象成为使这个正方形的内接圆.) 13 # 14 # 我们将使用老式的大炮和铁炮弹 15 #+ 向这块正方形的土地上开炮. 16 # 所有的炮弹都会击中这块正方形土地的某个地方. 17 #+ 或者是打到湖上, 或者是打到 4 个角的土地上. 18 # 因为这个湖占据了这个区域大部分地方, 19 #+ 所以大部分的炮弹都会"扑通"一声落到水里. 20 # 而只有很少的炮弹会"砰"的一声落到 4 个 21 #+ 角的土地上. 22 # 23 # 如果我们发出的炮弹足够随机的落到这块正方形区域中的话, 24 #+ 那么落到水里的炮弹与打出炮弹的总数的比率, 25 #+ 大概非常接近于 PI/4. 26 # 27 # 原因是所有的炮弹事实上都 28 #+ 打在了这个土地的右上角, 29 #+ 也就是, 笛卡尔坐标系的第一象限. 30 # (之前的解释只是一个简化.) 31 # 32 # 理论上来说, 如果打出的炮弹越多, 就越接近这个数字. 33 # 然而, 对于 shell 脚本来说一定会作些让步的, 34 #+ 因为它肯定不能和那些内建就支持浮点运算的编译语言相比. 35 # 当然就会降低精度. 36 37 38 DIMENSION=10000 # 这块土地的边长. 39 # 这也是所产生的随机整数的上限. 40 41 MAXSHOTS=1000 # 开炮次数. 42 # 10000 或更多次的话, 效果应该更好, 但有点太浪费时间了. 43 PMULTIPLIER=4.0 # 接近于 PI 的比例因子. 44 45 get_random () 46 { 47 SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }') 48 RANDOM=$SEED # 来自于 "seeding-random.sh" 49 #+ 的例子脚本. 50 let "rnum = $RANDOM % $DIMENSION" # 范围小于 10000. 51 echo $rnum 52 } 53 54 distance= # 声明全局变量. 55 hypotenuse () # 从 "alt-bc.sh" 例子来的, 56 { # 计算直角三角形的斜边的函数. 57 distance=$(bc -l << EOF 58 scale = 0 59 sqrt ( $1 * $1 + $2 * $2 ) 60 EOF 61 ) 62 # 设置 "scale" 为 0 , 好让结果四舍五入为整数值, 63 #+ 这是这个脚本中必须折中的一个地方. 64 # 不幸的是, 这将降低模拟的精度. 65 } 66 67 68 # main() { 69 70 # 初始化变量. 71 shots=0 72 splashes=0 73 thuds=0 74 Pi=0 75 76 while [ "$shots" -lt "$MAXSHOTS" ] # 主循环. 77 do 78 79 xCoord=$(get_random) # 取得随机的 X 与 Y 坐标. 80 yCoord=$(get_random) 81 hypotenuse $xCoord $yCoord # 直角三角形斜边 = 82 #+ distance. 83 ((shots++)) 84 85 printf "#%4d " $shots 86 printf "Xc = %4d " $xCoord 87 printf "Yc = %4d " $yCoord 88 printf "Distance = %5d " $distance # 到湖中心的 89 #+ 距离 -- 90 # 起始坐标点 -- 91 #+ (0,0). 92 93 if [ "$distance" -le "$DIMENSION" ] 94 then 95 echo -n "SPLASH! " 96 ((splashes++)) 97 else 98 echo -n "THUD! " 99 ((thuds++)) 100 fi 101 102 Pi=$(echo "scale=9; $PMULTIPLIER*$splashes/$shots" | bc) 103 # 将比例乘以 4.0. 104 echo -n "PI ~ $Pi" 105 echo 106 107 done 108 109 echo 110 echo "After $shots shots, PI looks like approximately $Pi." 111 # 如果不太准的话, 那么就提高一下运行的次数. . . 112 # 可能是由于运行错误和随机数随机程度不高造成的. 113 echo 114 115 # } 116 117 exit 0 118 119 # 要想知道一个 shell 脚本到底适不适合作为 120 #+ 一种需要对复杂和精度都有要求的计算应用的模拟的话. 121 # 122 # 一般至少需要两个判断条件. 123 # 1) 作为一种概念的验证: 来显示它可以做到. 124 # 2) 在使用真正的编译语言来实现一个算法之前, 125 #+ 使用脚本来测试和验证这个算法. ################################End Script######################################### Example 12-46 将 10 进制数字转换为 16 进制数字 ################################Start Script####################################### #!/bin/bash # hexconvert.sh: 将 10 进制数字转换为 16 进制数字 E_NOARGS=65 # 缺命令行参数错误. BASE=16 # 16 进制. if [ -z "$1" ] then echo "Usage: $0 number" exit $E_NOARGS # 需要一个命令行参数. fi # 练习: 添加命令行参数检查. hexcvt () { if [ -z "$1" ] then echo 0 return # 如果没有参数传递到这个函数中就 "return" 0. fi echo ""$1" "$BASE" o p" | dc # "o" 设置输出的基数(数制). # "p" 打印栈顶. # 察看 dc 的 man 页来了解其他的选项. return } hexcvt "$1" exit 0 ################################End Script######################################### Example 12-47 因子分解 ################################Start Script####################################### #!/bin/bash # factr.sh: 分解约数 MIN=2 # 如果比这个数小就不行了. E_NOARGS=65 E_TOOSMALL=66 if [ -z $1 ] then echo "Usage: $0 number" exit $E_NOARGS fi if [ "$1" -lt "$MIN" ] then echo "Number to factor must be $MIN or greater." exit $E_TOOSMALL fi # 练习: 添加类型检查 (防止非整型的参数). echo "Factors of $1:" # --------------------------------------------------------------------------------- echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc # --------------------------------------------------------------------------------- # 上边这行代码是 Michel Charpentier 编写的. # 在此使用经过授权 (thanks). exit 0 ################################End Script######################################### Example 12-48 计算直角三角形的斜边 ################################Start Script####################################### #!/bin/bash # hypotenuse.sh: 返回直角三角形的斜边. # ( 直角边长的平方和,然后对和取平方根) ARGS=2 # 需要将 2 个直角边作为参数传递进来. E_BADARGS=65 # 错误的参数值. if [ $# -ne "$ARGS" ] # 测试传递到脚本中的参数值. then echo "Usage: `basename $0` side_1 side_2" exit $E_BADARGS fi AWKSCRIPT=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } ' # 命令 / 传递给 awk 的参数 # 现在, 将参数通过管道传递给 awk. echo -n "Hypotenuse of $1 and $2 = " echo $1 $2 | awk "$AWKSCRIPT" exit 0 ################################End Script######################################### Example 12-49 使用 seq 来产生循环参数 ################################Start Script####################################### #!/bin/bash # 使用 "seq" echo for a in `seq 80` # 或者 for a in $( seq 80 ) # 与 " for a in 1 2 3 4 5 ... 80 "相同 (少敲了好多字!). # 也可以使用 'jot' (如果系统上有的话). do echo -n "$a " done # 1 2 3 4 5 ... 80 # 这也是一个通过使用命令的输出 # 来产生 "for"循环中 [list] 列表的例子. echo; echo COUNT=80 # 当然, 'seq' 也可以使用一个可替换的参数. for a in `seq $COUNT` # 或者 for a in $( seq $COUNT ) do echo -n "$a " done # 1 2 3 4 5 ... 80 echo; echo BEGIN=75 END=80 for a in `seq $BEGIN $END` # 传给 "seq" 两个参数, 从第一个参数开始增长, #+ 一直增长到第二个参数为止. do echo -n "$a " done # 75 76 77 78 79 80 echo; echo BEGIN=45 INTERVAL=5 END=80 for a in `seq $BEGIN $INTERVAL $END` # 传给 "seq" 三个参数从第一个参数开始增长, #+ 并以第二个参数作为增量, #+ 一直增长到第三个参数为止. do echo -n "$a " done # 45 50 55 60 65 70 75 80 echo; echo exit 0 ################################End Script######################################### Example 12-50 字母统计 ################################Start Script####################################### #!/bin/bash # letter-count.sh: 统计一个文本文件中字母出现的次数. # 由 Stefano Palmeri 编写. # 经过授权使用在本书中. # 本书作者做了少许修改. MINARGS=2 # 本脚本至少需要 2 个参数. E_BADARGS=65 FILE=$1 let LETTERS=$#-1 # 制定了多少个字母 (作为命令行参数). # (从命令行参数的个数中减 1.) show_help(){ echo echo Usage: `basename $0` file letters echo Note: `basename $0` arguments are case sensitive. echo Example: `basename $0` foobar.txt G n U L i N U x. echo } # 检查参数个数. if [ $# -lt $MINARGS ]; then echo echo "Not enough arguments." echo show_help exit $E_BADARGS fi # 检查文件是否存在. if [ ! -f $FILE ]; then echo "File \"$FILE\" does not exist." exit $E_BADARGS fi # 统计字母出现的次数. for n in `seq $LETTERS`; do shift if [[ `echo -n "$1" | wc -c` -eq 1 ]]; then # 检查参数. echo "$1" -\> `cat $FILE | tr -cd "$1" | wc -c` # 统计. else echo "$1 is not a single char." fi done exit $? # 这个脚本在功能上与 letter-count2.sh 完全相同, #+ 但是运行得更快. # 为什么? ################################End Script######################################### Example 12-51 使用 getopt 来分析命令行选项 ################################Start Script####################################### #!/bin/bash # 使用 getopt. # 尝试使用下边的不同的方法来调用这脚本: # sh ex33a.sh -a # sh ex33a.sh -abc # sh ex33a.sh -a -b -c # sh ex33a.sh -d # sh ex33a.sh -dXYZ # sh ex33a.sh -d XYZ # sh ex33a.sh -abcd # sh ex33a.sh -abcdZ # sh ex33a.sh -z # sh ex33a.sh a # 解释上面每一次调用的结果. E_OPTERR=65 if [ "$#" -eq 0 ] then # 脚本需要至少一个命令行参数. echo "Usage $0 -[options a,b,c]" exit $E_OPTERR fi set -- `getopt "abcd:" "$@"` # 为命令行参数设置位置参数. # 如果使用 "$*" 来代替 "$@" 的话会发生什么? while [ ! -z "$1" ] do case "$1" in -a) echo "Option \"a\"";; -b) echo "Option \"b\"";; -c) echo "Option \"c\"";; -d) echo "Option \"d\" $2";; *) break;; esac shift done # 通常来说在脚本中使用内建的 'getopts' 命令, #+ 会比使用 'getopt' 好一些. # 参见 "ex33.sh". exit 0 ################################End Script######################################### Example 12-52 一个拷贝自身的脚本 ################################Start Script####################################### #!/bin/bash # self-copy.sh # 这个脚本将会拷贝自身. file_subscript=copy dd if=$0 of=$0.$file_subscript 2>/dev/null # 阻止 dd 产生的消息: ^^^^^^^^^^^ exit $? ################################End Script######################################### Example 12-53 练习 dd ################################Start Script####################################### #!/bin/bash # exercising-dd.sh # 由 Stephane Chazelas 编写. # 本文作者做了少量修改. input_file=$0 # 脚本本身. output_file=log.txt n=3 p=5 dd if=$input_file of=$output_file bs=1 skip=$((n-1)) count=$((p-n+1)) 2> /dev/null # 从脚本中把位置 n 到 p 的字符提取出来. # ------------------------------------------------------- echo -n "hello world" | dd cbs=1 conv=unblock 2> /dev/null # 垂直的 echo "hello world" . exit 0 ################################End Script######################################### Example 12-54 记录按键 ################################Start Script####################################### #!/bin/bash # dd-keypress.sh: 记录按键, 不需要按回车. keypresses=4 # 记录按键的个数. old_tty_setting=$(stty -g) # 保存老的终端设置. echo "Press $keypresses keys." stty -icanon -echo # 禁用标准模式. # 禁用本地 echo. keys=$(dd bs=1 count=$keypresses 2> /dev/null) # 如果不指定输入文件的话, 'dd' 使用标准输入. stty "$old_tty_setting" # 恢复老的终端设置. echo "You pressed the \"$keys\" keys." # 感谢 Stephane Chazelas, 演示了这种方法. exit 0 ################################End Script######################################### Example 12-55 安全的删除一个文件 ################################Start Script####################################### #!/bin/bash # blot-out.sh: 删除一个文件所有的记录. # 这个脚本会使用随即字节交替的覆盖 #+ 目标文件, 并且在最终删除这个文件之前清零. # 这么做之后, 即使你通过传统手段来检查磁盘扇区 #+ 也不能把文件原始数据重新恢复. PASSES=7 # 破坏文件的次数. # 提高这个数字会减慢脚本运行的速度, #+ 尤其是对尺寸比较大的目标文件进行操作的时候. BLOCKSIZE=1 # 带有 /dev/urandom 的 I/O 需要单位块尺寸, #+ 否则你可能会获得奇怪的结果. E_BADARGS=70 # 不同的错误退出码. E_NOT_FOUND=71 E_CHANGED_MIND=72 if [ -z "$1" ] # 没指定文件名. then echo "Usage: `basename $0` filename" exit $E_BADARGS fi file=$1 if [ ! -e "$file" ] then echo "File \"$file\" not found." exit $E_NOT_FOUND fi echo; echo -n "Are you absolutely sure you want to blot out \"$file\" (y/n)? " read answer case "$answer" in [nN]) echo "Changed your mind, huh?" exit $E_CHANGED_MIND ;; *) echo "Blotting out file \"$file\".";; esac flength=$(ls -l "$file" | awk '{print $5}') # 5 是文件长度. pass_count=1 chmod u+w "$file" # Allow overwriting/deleting the file. echo while [ "$pass_count" -le "$PASSES" ] do echo "Pass #$pass_count" sync # 刷新 buffer. dd if=/dev/urandom of=$file bs=$BLOCKSIZE count=$flength # 使用随机字节进行填充. sync # 再刷新 buffer. dd if=/dev/zero of=$file bs=$BLOCKSIZE count=$flength # 用 0 填充. sync # 再刷新 buffer. let "pass_count += 1" echo done rm -f $file # 最后, 删除这个已经被破坏得不成样子的文件. sync # 最后一次刷新 buffer. echo "File \"$file\" blotted out and deleted."; echo exit 0 # 这是一种真正安全的删除文件的办法, #+ 但是效率比较低, 运行比较慢. # GNU 的文件工具包中的 "shred" 命令, #+ 也可以完成相同的工作, 不过更有效率. # 使用普通的方法是不可能重新恢复这个文件了. # 然而 . . . #+ 这个简单的例子是不能够抵抗 #+ 那些经验丰富并且正规的分析. # 这个脚本可能不会很好的运行在日志文件系统上.(译者注: JFS) # 练习 (很难): 像它做的那样修正这个问题. # Tom Vier 的文件删除包可以更加彻底 #+ 的删除文件, 比这个简单的例子厉害得多. # http://www.ibiblio.org/pub/Linux/utils/file/wipe-2.0.0.tar.bz2 # 如果想对安全删除文件这一论题进行深度的分析, #+ 可以参见 Peter Gutmann 的页面, #+ "Secure Deletion of Data From Magnetic and Solid-State Memory". # http://www.cs.auckland.ac.nz/~pgut001/pubs/secure_del.html ################################End Script######################################### Example 12-56 文件名产生器 ################################Start Script####################################### #!/bin/bash # tempfile-name.sh: 临时文件名产生器 BASE_STR=`mcookie` # 32-字符的 magic cookie. POS=11 # 字符串中随便的一个位置. LEN=5 # 取得 $LEN 长度连续的字符串. prefix=temp # 最终的一个临时文件. # 如果想让这个文件更加唯一, #+ 可以对这个前缀也使用下边的方法来生成. suffix=${BASE_STR:POS:LEN} # 提取从第 11 个字符之后的长度为 5 的字符串. temp_filename=$prefix.$suffix # 构造文件名. echo "Temp filename = "$temp_filename"" # sh tempfile-name.sh # Temp filename = temp.e19ea # 与使用 'date' 命令(参考 ex51.sh)来创建唯一文件名 #+ 的方法相比较. exit 0 ################################End Script######################################### Example 12-57 将米转换为英里 ################################Start Script####################################### #!/bin/bash # unit-conversion.sh convert_units () # 通过参数取得需要转换的单位. { cf=$(units "$1" "$2" | sed --silent -e '1p' | awk '{print $2}') # 除了真正需要转换的部分保留下来外,其他的部分都去掉. echo "$cf" } Unit1=miles Unit2=meters cfactor=`convert_units $Unit1 $Unit2` quantity=3.73 result=$(echo $quantity*$cfactor | bc) echo "There are $result $Unit2 in $quantity $Unit1." # 如果你传递了两个不匹配的单位会发生什么? #+ 比如分别传入英亩和英里? exit 0 ################################End Script######################################### Example 12-58 使用 m4 ################################Start Script####################################### #!/bin/bash # m4.sh: 使用 m4 宏处理器 # 字符操作 string=abcdA01 echo "len($string)" | m4 # 7 echo "substr($string,4)" | m4 # A01 echo "regexp($string,[0-1][0-1],\&Z)" | m4 # 01Z # 算术操作 echo "incr(22)" | m4 # 23 echo "eval(99 / 3)" | m4 # 33 exit 0 ################################End Script######################################### Example 13-1 设置一个新密码 ################################Start Script####################################### #!/bin/bash # setnew-password.sh: 只用于说明目的. # 如果真正运行这个脚本并不是一个好主意. # 这个脚本必须以 root 身份运行. ROOT_UID=0 # Root 的 $UID 0. E_WRONG_USER=65 # 不是 root? E_NOSUCHUSER=70 SUCCESS=0 if [ "$UID" -ne "$ROOT_UID" ] then echo; echo "Only root can run this script."; echo exit $E_WRONG_USER else echo echo "You should know better than to run this script, root." echo "Even root users get the blues... " echo fi username=bozo NEWPASSWORD=security_violation # 检查 bozo 是否在这里. grep -q "$username" /etc/passwd if [ $? -ne $SUCCESS ] then echo "User $username does not exist." echo "No password changed." exit $E_NOSUCHUSER fi echo "$NEWPASSWORD" | passwd --stdin "$username" # 'passwd'命令 '--stdin' 选项允许 #+ 从 stdin(或者管道)中获得一个新的密码. echo; echo "User $username's password changed!" # 在脚本中使用'passwd'命令是很危险的. exit 0 ################################End Script######################################### Example 13-2 设置一个擦除字符 ################################Start Script####################################### #!/bin/bash # erase.sh: 在读取输入时使用"stty"来设置一个擦除字符. echo -n "What is your name? " read name # 试试退格键 #+ 来删除输入的字符. # 有什么问题? echo "Your name is $name." stty erase '#' # 将 "hashmark" (#) 设置为退格字符. echo -n "What is your name? " read name # 使用#来删除最后键入的字符. echo "Your name is $name." # 警告: 即使在脚本退出后, 新的键值还是保持设置.(译者: 使用 stty erase '^?' 恢复) exit 0 ################################End Script######################################### Example 13-3 关掉终端对于密码的 echo ################################Start Script####################################### #!/bin/bash # secret-pw.sh: 保护密码不被显示 echo echo -n "Enter password " read passwd echo "password is $passwd" echo -n "If someone had been looking over your shoulder, " echo "your password would have been compromised." echo && echo # 在一个"与列表"中产生 2 个换行. stty -echo # 关闭屏幕的 echo. echo -n "Enter password again " read passwd echo echo "password is $passwd" echo stty echo # 恢复屏幕的 echo. exit 0 # 详细的阅读 stty 命令的 info 页, 以便于更好的掌握这个有用并且狡猾的工具. ################################End Script######################################### Example 13-4 按键检测 ################################Start Script####################################### #!/bin/bash # keypress.sh: 检测用户按键 ("hot keys"). echo old_tty_settings=$(stty -g) # 保存老的设置(为什么?). stty -icanon Keypress=$(head -c1) # 或者 $(dd bs=1 count=1 2> /dev/null) # 在非 GNU 的系统上 echo echo "Key pressed was \""$Keypress"\"." echo stty "$old_tty_settings" # 恢复老的设置. # 感谢, Stephane Chazelas. exit 0 ################################End Script######################################### Example 13-5 Checking a remote server for identd ################################Start Script####################################### #! /bin/sh ## Duplicate DaveG's ident-scan thingie using netcat. Oooh, he'll be p*ssed. ## Args: target port [port port port ...] ## Hose stdout _and_ stderr together. ## ## 优点: runs slower than ident-scan, giving remote inetd less cause ##+ for alarm, and only hits the few known daemon ports you specify. ## 缺点: requires numeric-only port args, the output sleazitude, ##+ and won't work for r-services when coming from high source ports. # 脚本作者: Hobbit # 授权使用在本书中. # --------------------------------------------------- E_BADARGS=65 # 至少需要两个参数. TWO_WINKS=2 # 需要睡多长时间. THREE_WINKS=3 IDPORT=113 # Authentication "tap ident" port. RAND1=999 RAND2=31337 TIMEOUT0=9 TIMEOUT1=8 TIMEOUT2=4 # --------------------------------------------------- case "${2}" in "" ) echo "Need HOST and at least one PORT." ; exit $E_BADARGS ;; esac # Ping 'em once and see if they *are* running identd. nc -z -w $TIMEOUT0 "$1" $IDPORT || { echo "Oops, $1 isn't running identd." ; exit 0 ; } # -z scans for listening daemons. # -w $TIMEOUT = How long to try to connect. # Generate a randomish base port. RP=`expr $$ % $RAND1 + $RAND2` TRG="$1" shift while test "$1" ; do nc -v -w $TIMEOUT1 -p ${RP} "$TRG" ${1} < /dev/null > /dev/null & PROC=$! sleep $THREE_WINKS echo "${1},${RP}" | nc -w $TIMEOUT2 -r "$TRG" $IDPORT 2>&1 sleep $TWO_WINKS # 这个脚本看起来是不是一个瘸腿脚本, 或者其它更差的什么东西? # ABS Guide 作者注释: "并不是真的那么差, #+ 事实上相当清楚." kill -HUP $PROC RP=`expr ${RP} + 1` shift done exit $? # 注意事项: # --------- # 尝试注释一下第 30 行的程序, 并且使用"localhost.localdomain 25" #+ 作为参数来运行这个脚本. # For more of Hobbit's 'nc' example scripts, #+ look in the documentation: #+ the /usr/share/doc/nc-X.XX/scripts directory. ################################End Script######################################### Example 13-6 pidof 帮助杀掉一个进程 ################################Start Script####################################### #!/bin/bash # kill-process.sh NOPROCESS=2 process=xxxyyyzzz # 使用不存在的进程. # 只不过是为了演示... # ... 并不想在这个脚本中杀掉任何真正的进程. # # 如果, 举个例子, 你想使用这个脚本来断线 Internet, # process=pppd t=`pidof $process` # 取得$process 的 pid(进程 id). # 'kill'必须使用 pid(不能用程序名). if [ -z "$t" ] # 如果没这个进程, 'pidof' 返回空. then echo "Process $process was not running." echo "Nothing killed." exit $NOPROCESS fi kill $t # 对于顽固的进程可能需要'kill -9'. # 这里需要做一个检查, 看看进程是否允许自身被 kill. # 或许另一个 " t=`pidof $process` " 或者 ... # 整个脚本都可以使用下边这句来替换: # kill $(pidof -x process_name) # 但是这就没有教育意义了. exit 0 ################################End Script######################################### Example 13-7 检查一个 CD 镜像 ################################Start Script####################################### # 以 root 身份... mkdir /mnt/cdtest # 如果没有的话,准备一个 mount 点. mount -r -t iso9660 -o loop cd-image.iso /mnt/cdtest # mount 这个镜像. # "-o loop" option equivalent to "losetup /dev/loop0" cd /mnt/cdtest # 现在检查这个镜像. ls -alR # 列出目录树中的文件. # 等等. ################################End Script######################################### Example 13-8 在一个文件中创建文件系统 ################################Start Script####################################### SIZE=1000000 # 1M head -c $SIZE < /dev/zero > file # 建立指定尺寸的文件. losetup /dev/loop0 file # 作为 loopback 设备来建立. mke2fs /dev/loop0 # 创建文件系统. mount -o loop /dev/loop0 /mnt # Mount 它. # Thanks, S.C. ################################End Script######################################### Example 13-9 添加一个新的硬盘驱动器 ################################Start Script####################################### #!/bin/bash # 在系统上添加第二块硬盘驱动器. # 软件配置. 假设硬件已经安装了. # 来自于本书作者的一篇文章. # 在"Linux Gazette"的问题#38 上, http://www.linuxgazette.com. ROOT_UID=0 # 这个脚本必须以 root 身份运行. E_NOTROOT=67 # 非 root 用户将会产生这个错误. if [ "$UID" -ne "$ROOT_UID" ] then echo "Must be root to run this script." exit $E_NOTROOT fi # 要非常谨慎的小心使用! # 如果某步错了, 可能会彻底摧毁你当前的文件系统. NEWDISK=/dev/hdb # 假设/dev/hdb 空白. 检查一下! MOUNTPOINT=/mnt/newdisk # 或者选择另外的 mount 点. fdisk $NEWDISK mke2fs -cv $NEWDISK1 # 检查坏块, 详细输出. # 注意: /dev/hdb1, *不是* /dev/hdb! mkdir $MOUNTPOINT chmod 777 $MOUNTPOINT # 让所有用户都具有全部权限. # 现在, 测试一下... # mount -t ext2 /dev/hdb1 /mnt/newdisk # 尝试创建一个目录. # 如果工作起来了, umount 它, 然后继续. # 最后一步: # 将下边这行添加到/etc/fstab. # /dev/hdb1 /mnt/newdisk ext2 defaults 1 1 exit 0 ################################End Script######################################### Example 13-10 使用 umask 来将输出文件隐藏起来 ################################Start Script####################################### #!/bin/bash # rot13a.sh: 与"rot13.sh"脚本相同, 但是会将输出写道"安全"文件中. # 用法: ./rot13a.sh filename # 或 ./rot13a.sh $OUTFILE # ^^ 从 stdin 或文件中输入. ^^^^^^^^^^ 输出重定向到文件中. exit 0 ################################End Script######################################### Example 13-11 killall, 来自于 /etc/rc.d/init.d ################################Start Script####################################### #!/bin/sh # --> 本书作者所作的注释全部以"# -->"开头. # --> 这是由 Miquel van Smoorenburg 所编写的 # --> 'rc'脚本包的一部分, . # --> 这个特殊的脚本看起来是是为 Red Hat / FC 所特定的, # --> (在其它的发行版中可能不会出现). # 停止所有正在运行的不必要的服务 #+ (there shouldn't be any, so this is just a sanity check) for i in /var/lock/subsys/*; do # --> 标准的 for/in 循环, 但是由于"do"在同一行上, # --> 所以必须添加";". # 检查脚本是否在那. [ ! -f $i ] && continue # --> 这是一种使用"与列表"的聪明的方法, 等价于: # --> if [ ! -f "$i" ]; then continue # 取得子系统的名字. subsys=${i#/var/lock/subsys/} # --> 匹配变量名, 在这里就是文件名. # --> 与 subsys=`basename $i`完全等价. # --> 从锁定文件名中获得 # -->+ (如果那里有锁定文件的话, # -->+ 那就证明进程正在运行). # --> 参考一下上边所讲的"锁定文件"的内容. # 终止子系统. if [ -f /etc/rc.d/init.d/$subsys.init ]; then /etc/rc.d/init.d/$subsys.init stop else /etc/rc.d/init.d/$subsys stop # --> 挂起运行的作业和幽灵进程. # --> 注意"stop"只是一个位置参数, # -->+ 并不是 shell 内建命令. fi done ################################End Script######################################### Example 14-1 愚蠢的脚本策略 ################################Start Script####################################### #!/bin/bash # stupid-script-tricks.sh: 朋友, 别在家这么做. # 来自于"Stupid Script Tricks," 卷 I. dangerous_variable=`cat /boot/vmlinuz` # 这是压缩过的 Linux 内核本身. echo "string-length of \$dangerous_variable = ${#dangerous_variable}" # 这个字符串变量的长度是 $dangerous_variable = 794151 # (不要使用'wc -c /boot/vmlinuz'来计算长度.) # echo "$dangerous_variable" # 千万别尝试这么做! 这样将挂起这个脚本. # 文档作者已经意识到将二进制文件设置到 #+ 变量中是一个没用的应用. exit 0 ################################End Script######################################### Example 14-2 从循环的输出中产生一个变量 ################################Start Script####################################### #!/bin/bash # csubloop.sh: 从循环的输出中产生一个变量. variable1=`for i in 1 2 3 4 5 do echo -n "$i" # 对于这里的命令替换来说 done` #+ 这个'echo'命令是非常关键的. echo "variable1 = $variable1" # variable1 = 12345 i=0 variable2=`while [ "$i" -lt 10 ] do echo -n "$i" # 再来一个, 'echo'是必须的. let "i += 1" # 递增. done` echo "variable2 = $variable2" # variable2 = 0123456789 # 这就证明了在一个变量声明中 #+ 嵌入一个循环是可行的. exit 0 ################################End Script######################################### Example 14-3 找 anagram(回文构词法, 可以将一个有意义的单词, 变换为 1 个或多个有意义 的单词, 但是还是原来的子母集合) ################################Start Script####################################### #!/bin/bash # agram2.sh # 关于命令替换嵌套的例子. # 使用"anagram"工具 #+ 这是作者的"yawl"文字表包中的一部分. # http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz # http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz E_NOARGS=66 E_BADARG=67 MINLEN=7 if [ -z "$1" ] then echo "Usage $0 LETTERSET" exit $E_NOARGS # 脚本需要一个命令行参数. elif [ ${#1} -lt $MINLEN ] then echo "Argument must have at least $MINLEN letters." exit $E_BADARG fi FILTER='.......' # 必须至少有 7 个字符. # 1234567 Anagrams=( $(echo $(anagram $1 | grep $FILTER) ) ) # | | 嵌套的命令替换 | | # ( 数组分配 ) echo echo "${#Anagrams[*]} 7+ letter anagrams found" echo echo ${Anagrams[0]} # 第一个 anagram. echo ${Anagrams[1]} # 第二个 anagram. # 等等. # echo "${Anagrams[*]}" # 在一行上列出所有的 anagram . . . # 考虑到后边还有"数组"作为单独的一章进行讲解, #+ 这里就不深入了. # 可以参阅 agram.sh 脚本, 这也是一个找出 anagram 的例子. exit $? ################################End Script######################################### Example 16-1 使用 exec 重定向标准输入 ################################Start Script####################################### #!/bin/bash # 使用'exec'重定向标准输入. exec 6<&0 # 将文件描述符#6 与 stdin 链接起来. # 保存了 stdin. exec < data-file # stdin 被文件"data-file"所代替. read a1 # 读取文件"data-file"的第一行. read a2 # 读取文件"data-file"的第二行. echo echo "Following lines read from file." echo "-------------------------------" echo $a1 echo $a2 echo; echo; echo exec 0<&6 6<&- # 现在将 stdin 从 fd #6 中恢复, 因为刚才我们把 stdin 重定向到#6 了, #+ 然后关闭 fd #6 ( 6<&- ), 好让这个描述符继续被其他进程所使用. # # <&6 6<&- 这么做也可以. echo -n "Enter data " read b1 # 现在"read"已经恢复正常了, 就是从 stdin 中读取. echo "Input read from stdin." echo "----------------------" echo "b1 = $b1" echo exit 0 ################################End Script######################################### Example 16-2 使用 exec 来重定向 stdout ################################Start Script####################################### #!/bin/bash # reassign-stdout.sh LOGFILE=logfile.txt exec 6>&1 # 将 fd #6 与 stdout 相连接. # 保存 stdout. exec > $LOGFILE # stdout 就被文件"logfile.txt"所代替了. # ----------------------------------------------------------- # # 在这块中所有命令的输出就都发向文件 $LOGFILE. echo -n "Logfile: " date echo "-------------------------------------" echo echo "Output of \"ls -al\" command" echo ls -al echo; echo echo "Output of \"df\" command" echo df # ----------------------------------------------------------- # exec 1>&6 6>&- # 恢复 stdout, 然后关闭文件描述符#6. echo echo "== stdout now restored to default == " echo ls -al echo exit 0 ################################End Script######################################### Example 16-3 使用 exec 在同一脚本中重定向 stdin 和 stdout ################################Start Script####################################### #!/bin/bash # upperconv.sh # 将一个指定的输入文件转换为大写. E_FILE_ACCESS=70 E_WRONG_ARGS=71 if [ ! -r "$1" ] # 判断指定的输入文件是否可读? then echo "Can't read from input file!" echo "Usage: $0 input-file output-file" exit $E_FILE_ACCESS fi # 即使输入文件($1)没被指定 #+ 也还是会以相同的错误退出(为什么?). if [ -z "$2" ] then echo "Need to specify output file." echo "Usage: $0 input-file output-file" exit $E_WRONG_ARGS fi exec 4<&0 exec < $1 # 将会从输入文件中读取. exec 7>&1 exec > $2 # 将写到输出文件中. # 假设输出文件是可写的(添加检查?). # ----------------------------------------------- cat - | tr a-z A-Z # 转换为大写. # ^^^^^ # 从 stdin 中读取.Reads from stdin. # ^^^^^^^^^^ # 写到 stdout 上. # 然而, stdin 和 stdout 都被重定向了. # ----------------------------------------------- exec 1>&7 7>&- # 恢复 stout. exec 0<&4 4<&- # 恢复 stdin. # 恢复之后, 下边这行代码将会如期望的一样打印到 stdout 上. echo "File \"$1\" written to \"$2\" as uppercase conversion." exit 0 ################################End Script######################################### Example 16-4 避免子 shell ################################Start Script####################################### #!/bin/bash # avoid-subshell.sh # Matthew Walker 提出的建议. Lines=0 echo cat myfile.txt | while read line; # (译者注: 管道会产生子 shell) do { echo $line (( Lines++ )); # 增加这个变量的值 #+ 但是外部循环却不能存取. # 子 shell 问题. } done echo "Number of lines read = $Lines" # 0 # 错误! echo "------------------------" exec 3<> myfile.txt while read line <&3 do { echo "$line" (( Lines++ )); # 增加这个变量的值 #+ 现在外部循环就可以存取了. # 没有子 shell, 现在就没问题了. } done exec 3>&- echo "Number of lines read = $Lines" # 8 echo exit 0 # 下边这些行是脚本的结果, 脚本是不会走到这里的. $ cat myfile.txt Line 1. Line 2. Line 3. Line 4. Line 5. Line 6. Line 7. Line 8. ################################End Script######################################### Example 17-1 广播: 发送消息给每个登录上的用户 ################################Start Script####################################### #!/bin/bash wall <就行, #+ 事实上它是键. # Bram Moolenaar 指出这种方法不能正常地用在'vim'上, (译者注: Bram Moolenaar 是 vim 作 者) #+ 因为可能会有终端的相互影响问题. exit 0 ################################End Script######################################### Example 17-3 使用 cat 的多行消息 ################################Start Script####################################### #!/bin/bash # 'echo' 对于打印单行消息是非常好的, #+ 但是在打印消息块时可能就有点问题了. # 'cat' here document 可以解决这个限制. cat < $Newfile < $OUTFILE # ----------------------------------------------------------- # 将'limit string'引用起来将会阻止上边 #+ here document 的消息体中的变量扩展. # 这会使得输出文件中的内容保持 here document 消息体中的原文. if [ -f "$OUTFILE" ] then chmod 755 $OUTFILE # 让所产生的文件具有可执行权限. else echo "Problem in creating file: \"$OUTFILE\"" fi # 这个方法也用来产生 #+ C 程序代码, Perl 程序代码, Python 程序代码, makefile, #+ 和其他的一些类似的代码. # (译者注: 中间一段没译的注释将会被 here document 打印出来) exit 0 ################################End Script######################################### Example 17-9 Here documents 与函数 ################################Start Script####################################### #!/bin/bash # here-function.sh GetPersonalData () { read firstname read lastname read address read city read state read zipcode } # 这个函数无疑的看起来就一个交互函数, 但是... # 给上边的函数提供输入. GetPersonalData < $file.new echo "Modified file is $file.new" exit 0 # 下边是'man bash'中的一段: # Here Strings # here document 的一种变形,形式如下: # # << list123) & (cat list4 list5 list6 | sort | uniq > list456) & #列表的合并和排序同时进. #放到后台运行可以确保能够串行执行. # #和下面的有相同的作用: # cat list1 list2 list3 | sort | uniq > list123 & # cat list4 list5 list6 | sort | uniq > list456 & wait #在所有的子 shell 执行完成前不再执行后面的命令. diff list123 list456 ################################End Script######################################### Example 21-1 在受限的情况下运行脚本 ################################Start Script####################################### #!/bin/bash # 脚本开头以"#!/bin/bash -r"来调用 #+ 会使整个脚本在受限模式下运行. echo echo "Changing directory." cd /usr/local echo "Now in `pwd`" echo "Coming back home." cd echo "Now in `pwd`" echo # 不受限的模式下 ,所有操作都能正常成功 . set -r # set --restricted 也能起相同的作用 . echo "==> Now in restricted mode. <==" echo echo echo "Attempting directory change in restricted mode." cd .. echo "Still in `pwd`" echo echo echo "\$SHELL = $SHELL" echo "Attempting to change shell in restricted mode." SHELL="/bin/ash" echo echo "\$SHELL= $SHELL" echo echo echo "Attempting to redirect output in restricted mode." ls -l /usr/bin > bin.files ls -l bin.files # Try to list attempted file creation effort. echo exit 0 ################################End Script######################################### Example 23-1 简单函数 ################################Start Script####################################### #!/bin/bash JUST_A_SECOND=1 funky () { # 这是一个最简单的函数. echo "This is a funky function." echo "Now exiting funky function." } # 函数必须在调用前声明. fun () { # 一个稍复杂的函数. i=0 REPEATS=30 echo echo "And now the fun really begins." echo sleep $JUST_A_SECOND # 嘿, 暂停一秒! while [ $i -lt $REPEATS ] do echo "----------FUNCTIONS---------->" echo "<------------ARE-------------" echo "<------------FUN------------>" echo let "i+=1" done } # 现在,调用两个函数. funky fun exit 0 ################################End Script######################################### Example 23-2 带着参数的函数 ################################Start Script####################################### #!/bin/bash # 函数和参数 DEFAULT=default # 默认的参数值. func2 () { if [ -z "$1" ] # 第一个参数是否长度为零? then echo "-Parameter #1 is zero length.-" # 则没有参数传递进来. else echo "-Param #1 is \"$1\".-" fi variable=${1-$DEFAULT} # echo "variable = $variable" # 参数替换会表现出什么? # --------------------------- # 它用于分辨没有参数和一个只有 NULL 值的参数. # if [ "$2" ] then echo "-Parameter #2 is \"$2\".-" fi return 0 } echo echo "Nothing passed." func2 # 没有参数来调用 echo echo "Zero-length parameter passed." func2 "" # 以一个长度为零的参数调用 echo echo "Null parameter passed." func2 "$uninitialized_param" # 以未初始化的参数来调用 echo echo "One parameter passed." func2 first # 用一个参数来调用 echo echo "Two parameters passed." func2 first second # 以二个参数来调用 echo echo "\"\" \"second\" passed." func2 "" second # 以第一个参数为零长度,而第二个参数是一个 ASCII 码组成的字符串来 调用. echo # exit 0 ################################End Script######################################### Example 23-3 函数和被传给脚本的命令行参数 ################################Start Script####################################### #!/bin/bash # func-cmdlinearg.sh # 以一个命令行参数来调用这个脚本, #+ 类似 $0 arg1 来调用. func () { echo "$1" } echo "First call to function: no arg passed." echo "See if command-line arg is seen." func # 不!命令行参数看不到. echo "============================================================" echo echo "Second call to function: command-line arg passed explicitly." func $1 # 现在可以看到了! exit 0 ################################End Script######################################### Example 23-4 传递间接引用给函数 ################################Start Script####################################### #!/bin/bash # ind-func.sh: 传递间接引用给函数. echo_var () { echo "$1" } message=Hello Hello=Goodbye echo_var "$message" # Hello # 现在,让我们传递一个间接引用给函数. echo_var "${!message}" # Goodbye echo "-------------" # 如果我们改变"hello"变量的值会发生什么? Hello="Hello, again!" echo_var "$message" # Hello echo_var "${!message}" # Hello, again! exit 0 ################################End Script######################################### Example 23-5 解除传递给函数的参数引用 ################################Start Script####################################### #!/bin/bash # dereference.sh # 给函数传递不同的参数. # Bruce W. Clare 编写. dereference () { y=\$"$1" # 变量名. echo $y # $Junk x=`eval "expr \"$y\" "` echo $1=$x eval "$1=\"Some Different Text \"" # 赋新值. } Junk="Some Text" echo $Junk "before" # Some Text before dereference Junk echo $Junk "after" # Some Different Text after exit 0 ################################End Script######################################### Example 23-6 再次尝试解除传递给函数的参数引用 ################################Start Script####################################### #!/bin/bash # ref-params.sh: 解除传递给函数的参数引用. # (复杂例子) ITERATIONS=3 # 取得输入的次数. icount=1 my_read () { # 用 my_read varname 来调用, #+ 输出用括号括起的先前的值作为默认值, #+ 然后要求输入一个新值. local local_var echo -n "Enter a value " eval 'echo -n "[$'$1'] "' # 先前的值. # eval echo -n "[\$$1] " # 更好理解, #+ 但会丢失用户输入在尾部的空格. read local_var [ -n "$local_var" ] && eval $1=\$local_var # "and 列表(And-list)": 如果变量"local_var"测试成功则把变量"$1"的值赋给它. } echo while [ "$icount" -le "$ITERATIONS" ] do my_read var echo "Entry #$icount = $var" let "icount += 1" echo done # 多谢 Stephane Chazelas 提供的示范例子. exit 0 ################################End Script######################################### Example 23-7 两个数中的最大者 ################################Start Script####################################### #!/bin/bash # max.sh: 两个整数中的最大者. E_PARAM_ERR=-198 # 如果传给函数的参数少于 2 个时的返回值. EQUAL=-199 # 如果两个整数值相等的返回值. # 任一个传给函数的参数值溢出 # max2 () # 返回两个整数的较大值. { # 注意: 参与比较的数必须小于 257. if [ -z "$2" ] then return $E_PARAM_ERR fi if [ "$1" -eq "$2" ] then return $EQUAL else if [ "$1" -gt "$2" ] then return $1 else return $2 fi fi } max2 33 34 return_val=$? if [ "$return_val" -eq $E_PARAM_ERR ] then echo "Need to pass two parameters to the function." elif [ "$return_val" -eq $EQUAL ] then echo "The two numbers are equal." else echo "The larger of the two numbers is $return_val." fi exit 0 # 练习 (容易): # --------------- # 把这个脚本转化成交互式的脚本, #+ 也就是说,让脚本可以要求调用者输入两个整数. ################################End Script######################################### Example 23-8 把数字转化成罗马数字 ################################Start Script####################################### #!/bin/bash # 阿拉伯数字转化为罗马数字 # 转化范围: 0 - 200 # 这是比较粗糙的,但可以工作. # 扩展可接受的范围来作为脚本功能的扩充,这个作为练习完成. # 用法: roman number-to-convert LIMIT=200 E_ARG_ERR=65 E_OUT_OF_RANGE=66 if [ -z "$1" ] then echo "Usage: `basename $0` number-to-convert" exit $E_ARG_ERR fi num=$1 if [ "$num" -gt $LIMIT ] then echo "Out of range!" exit $E_OUT_OF_RANGE fi to_roman () # 在第一次调用函数前必须先定义. { number=$1 factor=$2 rchar=$3 let "remainder = number - factor" while [ "$remainder" -ge 0 ] do echo -n $rchar let "number -= factor" let "remainder = number - factor" done return $number # 练习: # -------- # 解释这个函数是怎么工作的. # 提示: 靠不断地除来分割数字. } to_roman $num 100 C num=$? to_roman $num 90 LXXXX num=$? to_roman $num 50 L num=$? to_roman $num 40 XL num=$? to_roman $num 10 X num=$? to_roman $num 9 IX num=$? to_roman $num 5 V num=$? to_roman $num 4 IV num=$? to_roman $num 1 I echo exit 0 ################################End Script######################################### Example 23-9 测试函数最大的返回值 ################################Start Script####################################### #!/bin/bash # return-test.sh # 一个函数最大可能返回的值是 255. return_test () # 无论传给函数什么都返回它. { return $1 } return_test 27 # o.k. echo $? # 返回 27. return_test 255 # 仍然 o.k. echo $? # 返回 255. return_test 257 # 错误! echo $? # 返回 1 (返回代码指示错误). # ====================================================== return_test -151896 # 能够返回这个非常大的负数么? echo $? # 会返回-151896? # 不! 它将返回 168. # 2.05b 版本之前的 Bash 是允许 #+ 超大负整数作为返回值的. # 但是比它更新一点的版本修正了这个漏洞. # 这将破坏比较老的脚本. # 慎用! # ====================================================== exit 0 ################################End Script######################################### Example 23-10 比较两个大整数 ################################Start Script####################################### #!/bin/bash # max2.sh: 取两个超大整数中最大的. # 这个脚本与前面的"max.sh"例子作用相同, #+ 经过修改可以适用于比较超大整数. EQUAL=0 # 如果两个参数相同的返回值. E_PARAM_ERR=-99999 # 没有足够的参数传递到函数中. # ^^^^^^ 也可能是传递到函数中的某个参数超出范围了. max2 () # 从这两个数中"返回"更大一些的. { if [ -z "$2" ] then echo $E_PARAM_ERR return fi if [ "$1" -eq "$2" ] then echo $EQUAL return else if [ "$1" -gt "$2" ] then retval=$1 else retval=$2 fi fi echo $retval # echo(到 stdout), 而不是使用返回值. # 为什么? } return_val=$(max2 33001 33997) # ^^^^ 函数名 # ^^^^^ ^^^^^ 这是传递进来的参数 # 这事实上是一个命令替换的形式: #+ 会把这个函数当作一个命令来处理, #+ 并且分配这个函数的 stdout 到变量"return_val"中. # ========================= OUTPUT ======================== if [ "$return_val" -eq "$E_PARAM_ERR" ] then echo "Error in parameters passed to comparison function!" elif [ "$return_val" -eq "$EQUAL" ] then echo "The two numbers are equal." else echo "The larger of the two numbers is $return_val." fi # ========================================================= exit 0 # 练习: # ----- # 1) 找出一种更优雅的方法来测试 #+ 传递到函数中的参数. # 2) 在"OUTPUT"的时候简化 if/then 结构. # 3) 重写这个脚本使其能够从命令行参数中来获取输入. ################################End Script######################################### Example 23-11 用户名的真实名 ################################Start Script####################################### #!/bin/bash # realname.sh # # 由用户名而从/etc/passwd 取得"真实名". ARGCOUNT=1 # 需要一个参数. E_WRONGARGS=65 file=/etc/passwd pattern=$1 if [ $# -ne "$ARGCOUNT" ] then echo "Usage: `basename $0` USERNAME" exit $E_WRONGARGS fi file_excerpt () # 以要求的模式来扫描文件,然后打印文件相关的部分. { while read line # "while" does not necessarily need "[ condition ]" do echo "$line" | grep $1 | awk -F":" '{ print $5 }' # awk 指定使用":"为界定符. done } <$file # 重定向函数的标准输入. file_excerpt $pattern # Yes, this entire script could be reduced to # grep PATTERN /etc/passwd | awk -F":" '{ print $5 }' # or # awk -F: '/PATTERN/ {print $5}' # or # awk -F: '($1 == "username") { print $5 }' # real name from username # 但是,这些可能起不到示例的作用. exit 0 ################################End Script######################################### Example 23-12 局部变量的可见范围 ################################Start Script####################################### #!/bin/bash # 在函数内部的全局和局部变量. func () { local loc_var=23 # 声明为局部变量. echo # 使用内建的'local'关键字. echo "\"loc_var\" in function = $loc_var" global_var=999 # 没有声明为局部变量. # 默认为全局变量. echo "\"global_var\" in function = $global_var" } func # 现在,来看看是否局部变量"loc_var"能否在函数外面可见. echo echo "\"loc_var\" outside function = $loc_var" # $loc_var outside function = # 不, $loc_var 不是全局可访问的. echo "\"global_var\" outside function = $global_var" # $global_var outside function = 999 # $global_var 是全局可访问的. echo exit 0 # 与 In contrast to C 相比, 在函数内声明的 Bash 变量只有在 #+ 它被明确声明成局部的变量时才是局部的. ################################End Script######################################### Example 23-13 用局部变量来递归 ################################Start Script####################################### #!/bin/bash # 阶乘 # --------- # bash 允许递归吗? # 嗯, 允许, 但是... # 它太慢以致你难以忍受. MAX_ARG=5 E_WRONG_ARGS=65 E_RANGE_ERR=66 if [ -z "$1" ] then echo "Usage: `basename $0` number" exit $E_WRONG_ARGS fi if [ "$1" -gt $MAX_ARG ] then echo "Out of range (5 is maximum)." # 现在让我们来了解实际情况. # 如果你想求比这个更大的范围的阶乘, #+ 应该重新用一个真正的编程语言来写. exit $E_RANGE_ERR fi fact () { local number=$1 # 变量"number"必须声明为局部, #+ 否则它不会工作. if [ "$number" -eq 0 ] then factorial=1 # 0 的阶乘为 1. else let "decrnum = number - 1" fact $decrnum # 递归调用(函数内部调用自己本身). let "factorial = $number * $?" fi return $factorial } fact $1 echo "Factorial of $1 is $?." exit 0 ################################End Script######################################### 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 # http://hanoi.kernelthread.com 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 # 想了解更多的信息, 请访问 http://hanoi.kernelthread.com. 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######################################### Example 24-1 脚本中的别名 ################################Start Script####################################### #!/bin/bash # alias.sh shopt -s expand_aliases # 必须设置这个选项,否则脚本不会扩展别名功能. # 首先, 来点有趣的. alias Jesse_James='echo "\"Alias Jesse James\" was a 1959 comedy starring Bob Hope."' Jesse_James echo; echo; echo; alias ll="ls -l" # 可以使用单引号(')或双引号(")来定义一个别名. echo "Trying aliased \"ll\":" ll /usr/X11R6/bin/mk* #* 别名工作了. echo directory=/usr/X11R6/bin/ prefix=mk* # 看通配符会不会引起麻烦. echo "Variables \"directory\" + \"prefix\" = $directory$prefix" echo alias lll="ls -l $directory$prefix" echo "Trying aliased \"lll\":" lll # 详细列出在/usr/X11R6/bin 目录下所有以 mk 开头的文件. # 别名能处理连接变量 -- 包括通配符 -- o.k. TRUE=1 echo if [ TRUE ] then alias rr="ls -l" echo "Trying aliased \"rr\" within if/then statement:" rr /usr/X11R6/bin/mk* #* 引起错误信息! # 别名不能在混合结构中使用. echo "However, previously expanded alias still recognized:" ll /usr/X11R6/bin/mk* fi echo count=0 while [ $count -lt 3 ] do alias rrr="ls -l" echo "Trying aliased \"rrr\" within \"while\" loop:" rrr /usr/X11R6/bin/mk* #* 在这儿,别名也不会扩展. # alias.sh: line 57: rrr: command not found let count+=1 done echo; echo alias xyz='cat $0' # 脚本打印自身内容. # 注意是单引号(强引用). xyz # 虽然 Bash 的文档它是不会工作的,但好像它是可以工作的. # # # 然而,就像 Steve Jacobson 指出, #+ 参数"$0"立即扩展成了这个别名的声明. exit 0 ################################End Script######################################### Example 24-2 unalias: 设置和删除别名 ################################Start Script####################################### #!/bin/bash # unalias.sh shopt -s expand_aliases # 打开别名功能扩展. alias llm='ls -al | more' llm echo unalias llm # 删除别名. llm # 引起错误信息,因为'llm'已经不再有效了. exit 0 ################################End Script######################################### Example 25-1 使用"与列表(and list)"来测试命令行参数 ################################Start Script####################################### #!/bin/bash # "and list" if [ ! -z "$1" ] && echo "Argument #1 = $1" && [ ! -z "$2" ] && echo "Argument #2 = $2" then echo "At least 2 arguments passed to script." # 所有连接起来的命令都返回真. else echo "Less than 2 arguments passed to script." # 整个命令列表中至少有一个命令返回假值. fi # 注意"if [ ! -z $1 ]" 可以工作,但它是有所假定的等价物, # if [ -n $1 ] 不会工作. # 但是, 加引用可以让它工作. # if [ -n "$1" ] 就可以了. # 小心! # 最好总是引起要测试的变量. # 这是使用"纯粹"的 if/then 语句完成的同等功能. if [ ! -z "$1" ] then echo "Argument #1 = $1" fi if [ ! -z "$2" ] then echo "Argument #2 = $2" echo "At least 2 arguments passed to script." else echo "Less than 2 arguments passed to script." fi # 这会更长且不如"与列表"精致. exit 0 ################################End Script######################################### Example 25-2 用"与列表"的另一个命令行参数测试 ################################Start Script####################################### #!/bin/bash ARGS=1 # 期望的参数个数. E_BADARGS=65 # 如果用户给出不正确的参数个数的退出码. test $# -ne $ARGS && echo "Usage: `basename $0` $ARGS argument(s)" && exit $E_BADARGS # 如果 条件 1 测试为真(表示传给脚本的参数不对), #+ 则余下的命令会被执行,并且脚本结束运行. # 下面的代码只有当上面的测试失败时才会执行. echo "Correct number of arguments passed to this script." exit 0 # 为了检查退出码,脚本结束后用"echo $?"来查看退出码. ################################End Script######################################### Example 25-3 "或列表"和"与列表"的结合使用 ################################Start Script####################################### #!/bin/bash # delete.sh, 不是很聪明的文件删除功能. # 用法: delete filename E_BADARGS=65 if [ -z "$1" ] then echo "Usage: `basename $0` filename" exit $E_BADARGS # 没有参数? 跳出脚本. else file=$1 # 设置文件名. fi [ ! -f "$file" ] && echo "File \"$file\" not found. \ Cowardly refusing to delete a nonexistent file." # 与列表, 用于文件不存在时给出一个错误信息. # 注意 echo 命令的参数用了一个转义符继续使第二行也是这个命令的参数. [ ! -f "$file" ] || (rm -f $file; echo "File \"$file\" deleted.") # 或列表, 用于存在文件时删除此文件. # 注意上面两个相反的逻辑. # 与列表为真时才执行, 或列表为假时执行. exit 0 ################################End Script######################################### Example 26-1 简单的数组用法 ################################Start Script####################################### #!/bin/bash area[11]=23 area[13]=37 area[51]=UFOs # 数组成员不必一定要连贯或连续的. # 数组的一部分成员允许不被初始化. # 数组中空缺元素是允许的. # 实际上,保存着稀疏数据的数组(“稀疏数组”)在电子表格处理软件中非常有用. # echo -n "area[11] = " echo ${area[11]} # {大括号}是需要的. echo -n "area[13] = " echo ${area[13]} echo "Contents of area[51] are ${area[51]}." # 没有初始化内容的数组元素打印空值(NULL 值). echo -n "area[43] = " echo ${area[43]} echo "(area[43] unassigned)" echo # 两个数组元素的和被赋值给另一个数组元素 area[5]=`expr ${area[11]} + ${area[13]}` echo "area[5] = area[11] + area[13]" echo -n "area[5] = " echo ${area[5]} area[6]=`expr ${area[11]} + ${area[51]}` echo "area[6] = area[11] + area[51]" echo -n "area[6] = " echo ${area[6]} # 这里会失败是因为整数和字符串相加是不允许的. echo; echo; echo # ----------------------------------------------------------------- # 另一个数组, "area2". # 另一种指定数组元素的值的办法... # array_name=( XXX YYY ZZZ ... ) area2=( zero one two three four ) echo -n "area2[0] = " echo ${area2[0]} # 啊哈, 从 0 开始计数(即数组的第一个元素是[0], 而不是 [1]). echo -n "area2[1] = " echo ${area2[1]} # [1] 是数组的第二个元素. # ----------------------------------------------------------------- echo; echo; echo # ----------------------------------------------- # 第三种数组, "area3". # 第三种指定数组元素值的办法... # array_name=([xx]=XXX [yy]=YYY ...) area3=([17]=seventeen [24]=twenty-four) echo -n "area3[17] = " echo ${area3[17]} echo -n "area3[24] = " echo ${area3[24]} # ----------------------------------------------- exit 0 ################################End Script######################################### Example 26-2 格式化一首诗 ################################Start Script####################################### #!/bin/bash # poem.sh: 排印出作者喜欢的一首诗. # 诗的行数 (一小节诗). Line[1]="I do not know which to prefer," Line[2]="The beauty of inflections" Line[3]="Or the beauty of innuendoes," Line[4]="The blackbird whistling" Line[5]="Or just after." # 出处. Attrib[1]=" Wallace Stevens" Attrib[2]="\"Thirteen Ways of Looking at a Blackbird\"" # 此诗是公众的 (版权期已经到期了). echo for index in 1 2 3 4 5 # 5 行. do printf " %s\n" "${Line[index]}" done for index in 1 2 # 打印两行出处行. do printf " %s\n" "${Attrib[index]}" done echo exit 0 # 练习: # -------- # 修改这个脚本使其从一个文本文件中提取内容打印一首行. ################################End Script######################################### Example 26-3 多种数组操作 ################################Start Script####################################### #!/bin/bash # array-ops.sh: 数组更多有趣的用法. array=( zero one two three four five ) # 元素 0 1 2 3 4 5 echo ${array[0]} # zero echo ${array:0} # zero # 第一个元素的参数扩展, #+ 从位置 0 开始 (即第一个字符). echo ${array:1} # ero # 第一个元素的参数扩展, #+ 从位置 1 开始 (即第二个字符). echo "--------------" echo ${#array[0]} # 4 # 数组第一个元素的长度. echo ${#array} # 4 # 数组第一个元素的长度. # (另一种写法) echo ${#array[1]} # 3 # 数组第二个元素的长度. # Bash 的数组是 0 开始索引的. echo ${#array[*]} # 6 # 数组中元素的个数. echo ${#array[@]} # 6 # 数组中元素的个数. echo "--------------" array2=( [0]="first element" [1]="second element" [3]="fourth element" ) echo ${array2[0]} # 第一个元素 echo ${array2[1]} # 第二个元素 echo ${array2[2]} # # 因为初始化时没有指定,因此值为空(null). echo ${array2[3]} # 第四个元素 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####################################### #!/bin/bash # script-array.sh: 把此脚本的内容传进数组. # 从 Chris Martin 的 e-mail 中得到灵感 (多谢!). script_contents=( $(cat "$0") ) # 把这个脚本($0)的内容存进数组. # for element in $(seq 0 $((${#script_contents[@]} - 1))) do # ${#script_contents[@]} #+ 表示数组中元素的个数. # # 问题: # 为什么需要 seq 0 ? # 试试更改成 seq 1. echo -n "${script_contents[$element]}" # 将脚本的每行列成一个域. echo -n " -- " # 使用" -- "作为域分隔符. done echo exit 0 # 练习: # -------- # 修改这个脚本使它能按照它原本的格式输出, #+ 连同空白符,换行,等等. # ################################End Script######################################### Example 26-6 一些数组专用的工具 ################################Start Script####################################### #!/bin/bash declare -a colors # 所有脚本后面的命令都会把 #+ 变量"colors"作为数组对待. echo "Enter your favorite colors (separated from each other by a space)." read -a colors # 键入至少 3 种颜色以用于下面的示例. # 指定'read'命令的选项, #+ 允许指定数组元素. echo element_count=${#colors[@]} # 专用语法来提取数组元素的个数. # element_count=${#colors[*]} 也可以. # # "@"变量允许分割引号内的单词 #+ (依靠空白字符来分隔变量). # # 这就像"$@" 和"$*"在位置参数中表现出来的一样. # index=0 while [ "$index" -lt "$element_count" ] do # List all the elements in the array. echo ${colors[$index]} let "index = $index + 1" done # 每个数组元素被列为单独的一行. # 如果这个没有要求, 可以用 echo -n "${colors[$index]} " # # 可以用一个"for"循环来做: # for i in "${colors[@]}" # do # echo "$i" # done # (Thanks, S.C.) echo # 再次列出数组中所有的元素, 但使用更优雅的做法. echo ${colors[@]} # echo ${colors[*]} 也可以. echo # "unset"命令删除一个数组元素或是整个数组. unset colors[1] # 删除数组的第二个元素. # 作用等同于 colors[1]= echo ${colors[@]} # 再列出数组,第二个元素没有了. unset colors # 删除整个数组. # unset colors[*] 或 #+ unset colors[@] 都可以. echo; echo -n "Colors gone." echo ${colors[@]} # 再列出数组, 则为空了. exit 0 ################################End Script######################################### 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######################################### Example 26-8 初始化数组 ################################Start Script####################################### #! /bin/bash # array-assign.bash # 数组操作是 Bash 特有的, #+ 因此脚本名用".bash"结尾. # Copyright (c) Michael S. Zick, 2003, All rights reserved. # 许可证: 没有任何限制,可以用于任何目的的反复使用. # Version: $ID$ # # 由 William Park 添加注释. # 基于 Stephane Chazelas 提供在本书中的一个例子 # # 'times' 命令的输出格式: # User CPU <空格> System CPU # User CPU of dead children <空格> System CPU of dead children # Bash 赋一个数组的所有元素给新的数组变量有两种办法. # # 在 Bash 版本 2.04, 2.05a 和 2.05b, #+ 这两种办法都对 NULL 的值的元素全部丢弃. # 另一种数组赋值办法是维护[下标]=值之间的关系将会在新版本的 Bash 支持. # # 可以用外部命令来构造一个大数组, #+ 但几千个元素的数组如下就可以构造了. # declare -a bigOne=( /dev/* ) echo echo 'Conditions: Unquoted, default IFS, All-Elements-Of' echo "Number of elements in array is ${#bigOne[@]}" # set -vx echo echo '- - testing: =( ${array[@]} ) - -' times declare -a bigTwo=( ${bigOne[@]} ) # ^ ^ times echo echo '- - testing: =${array[@]} - -' times declare -a bigThree=${bigOne[@]} # 这次没有用括号. times # 正如 Stephane Chazelas 指出的那样比较输出的数组可以了解第二种格式的赋值比第三和 第四的 times 的更快 # # # William Park 解释 explains: #+ bigTwo 数组是被赋值了一个单字符串, #+ bigThree 则赋值时一个一个元素的赋值. # 所以, 实际上的情况是: # bigTwo=( [0]="... ... ..." ) # bigThree=( [0]="..." [1]="..." [2]="..." ... ) # 我在本书的例子中仍然会继续用第一种格式, #+ 因为我认为这会对说明清楚更有帮助. # 我的例子中的可复用的部分实际上还是会使用第二种格式, #+ 因为这种格式更快一些. # MSZ: 很抱歉早先的失误(应是指本书的先前版本). # 注: # ---- # 在 31 和 43 行的"declare -a"语句不是必须的, #+ 因为会在使用 Array=( ... )赋值格式时暗示它是数组. # # 但是, 省略这些声明会导致后面脚本的相关操作更慢一些. # # 试一下, 看有什么变化. exit 0 ################################End Script######################################### Example 26-9 复制和连接数组 ################################Start Script####################################### #! /bin/bash # CopyArray.sh # # 由 Michael Zick 编写. # 在本书中使用已得到许可. # 怎么传递变量名和值处理,返回就用使用该变量, #+ 或说"创建你自己的赋值语句". CpArray_Mac() { # 创建赋值命令语句 echo -n 'eval ' echo -n "$2" # 目的变量名 echo -n '=( ${' echo -n "$1" # 源名字 echo -n '[@]} )' # 上面的全部会合成单个命令. # 这就是函数所有的功能. } declare -f CopyArray # 函数"指针" CopyArray=CpArray_Mac # 建立命令 Hype() { # 要复制的数组名为 $1. # (接合数组,并包含尾部的字符串"Really Rocks".) # 返回结果的数组名为 $2. local -a TMP local -a hype=( Really Rocks ) $($CopyArray $1 TMP) TMP=( ${TMP[@]} ${hype[@]} ) $($CopyArray TMP $2) } declare -a before=( Advanced Bash Scripting ) declare -a after echo "Array Before = ${before[@]}" Hype before after echo "Array After = ${after[@]}" # 有多余的字符串? echo "What ${after[@]:3:2}?" declare -a modest=( ${after[@]:2:1} ${after[@]:3:2} ) # ---- 子串提取 ---- echo "Array Modest = ${modest[@]}" # 'before'变量变成什么了 ? echo "Array Before = ${before[@]}" 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####################################### #!/bin/bash # bubble.sh: 排序法之冒泡排序. # 回忆冒泡排序法. 在这个版本中要实现它... # 靠连续地多次比较数组元素来排序, #+ 比较两个相邻的元素,如果排序顺序不对,则交换两者的顺序. # 当第一轮比较结束后,最"重"的元素就被排到了最底部. # 当第二轮比较结束后,第二"重"的元素就被排到了次底部的位置. # 以此类推. # 这意味着每轮的比较不需要比较先前已"沉淀"好的数据. # 因此你会注意到后面数据的打印会比较快一些. exchange() { # 交换数组的两个元素. local temp=${Countries[$1]} # 临时保存要交换的一个元素. # Countries[$1]=${Countries[$2]} Countries[$2]=$temp return } declare -a Countries # 声明数组, #+ 在此是可选的,因为下面它会被按数组来初始化. # 是否允许用转义符(\)将数组的各变量值放到几行上? # # 是的. Countries=(Netherlands Ukraine Zaire Turkey Russia Yemen Syria \ Brazil Argentina Nicaragua Japan Mexico Venezuela Greece England \ Israel Peru Canada Oman Denmark Wales France Kenya \ Xanadu Qatar Liechtenstein Hungary) # "Xanadu" 是个虚拟的充满美好的神话之地. # clear # 开始之前清除屏幕. echo "0: ${Countries[*]}" # 从 0 索引的元素开始列出整个数组. number_of_elements=${#Countries[@]} let "comparisons = $number_of_elements - 1" count=1 # 传递数字. while [ "$comparisons" -gt 0 ] # 开始外部的循环 do index=0 # 每轮开始前重设索引值为 0. while [ "$index" -lt "$comparisons" ] # 开始内部循环 do if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ] # 如果原来的排序次序不对... # 回想一下 \> 在单方括号里是 is ASCII 码的比较操作符. # # if [[ ${Countries[$index]} > ${Countries[`expr $index + 1`]} ]] #+ 也可以. then exchange $index `expr $index + 1` # 交换. fi let "index += 1" done # 内部循环结束 # ---------------------------------------------------------------------- # Paulo Marcel Coelho Aragao 建议使用更简单的 for-loops. # # for (( last = $number_of_elements - 1 ; last > 1 ; last-- )) # do # for (( i = 0 ; i < last ; i++ )) # do # [[ "${Countries[$i]}" > "${Countries[$((i+1))]}" ]] \ # && exchange $i $((i+1)) # done # done # ---------------------------------------------------------------------- let "comparisons -= 1" # 因为最"重"的元素冒到了最底部, #+ 我们可以每轮少做一些比较. echo echo "$count: ${Countries[@]}" # 每轮结束后,打印一次数组. echo let "count += 1" # 增加传递计数. done # 外部循环结束 # 完成. exit 0 ################################End Script######################################### Example 26-12 内嵌数组和间接引用 ################################Start Script####################################### #!/bin/bash # embedded-arrays.sh # 内嵌数组和间接引用. # 由 Dennis Leeuw 编写. # 已获使用许可. # 由本文作者修改. ARRAY1=( VAR1_1=value11 VAR1_2=value12 VAR1_3=value13 ) ARRAY2=( VARIABLE="test" STRING="VAR1=value1 VAR2=value2 VAR3=value3" ARRAY21=${ARRAY1[*]} ) # 把 ARRAY1 数组嵌到这个数组里. function print () { OLD_IFS="$IFS" IFS=$'\n' # 这是为了在每个行打印一个数组元素. # TEST1="ARRAY2[*]" local ${!TEST1} # 试下删除这行会发生什么. # 间接引用. # 这使 $TEST1 只在函数内存取. # # 我们看看还能干点什么. echo echo "\$TEST1 = $TEST1" # 变量的名称. echo; echo echo "{\$TEST1} = ${!TEST1}" # 变量的内容. # 这就是间接引用的作用. # echo echo "-------------------------------------------"; echo echo # 打印变量 echo "Variable VARIABLE: $VARIABLE" # 打印一个字符串元素 IFS="$OLD_IFS" TEST2="STRING[*]" local ${!TEST2} # 间接引用 (像上面一样). echo "String element VAR2: $VAR2 from STRING" # 打印一个字符串元素 TEST2="ARRAY21[*]" local ${!TEST2} # 间接引用 (像上面一样). echo "Array element VAR1_1: $VAR1_1 from ARRAY21" } print echo exit 0 # 脚本作者注, #+ "你可以很容易地将其扩展成 Bash 的一个能创建 hash 的脚本." # (难) 留给读者的练习: 实现它. ################################End Script######################################### 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######################################### 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######################################### Example 26-15 复杂的数组应用: 列出一种怪异的数学序列 ################################Start Script####################################### #!/bin/bash # Douglas Hofstadter 的有名的"Q-series": # Q(1) = Q(2) = 1 # Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), 当 n>2 时 # 这是令人感到陌生的也是没有规律的"乱序"整数序列. # 序列的头 20 个如下所示: # 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12 # 参考 Hofstadter 的书, "Goedel, Escher, Bach: An Eternal Golden Braid", #+ 页码 137. LIMIT=100 # 计算数的个数. LINEWIDTH=20 # 很行要打印的数的个数. Q[1]=1 # 序列的头 2 个是 1. Q[2]=1 echo echo "Q-series [$LIMIT terms]:" echo -n "${Q[1]} " # 打印头 2 个数. echo -n "${Q[2]} " for ((n=3; n <= $LIMIT; n++)) # C 风格的循环条件. do # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]] 当 n>2 时 # 需要将表达式分步计算, #+ 因为 Bash 不擅长处理此类复杂计算. let "n1 = $n - 1" # n-1 let "n2 = $n - 2" # n-2 t0=`expr $n - ${Q[n1]}` # n - Q[n-1] t1=`expr $n - ${Q[n2]}` # n - Q[n-2] T0=${Q[t0]} # Q[n - Q[n-1]] T1=${Q[t1]} # Q[n - Q[n-2]] Q[n]=`expr $T0 + $T1` # Q[n - Q[n-1]] + Q[n - Q[n-2]] echo -n "${Q[n]} " if [ `expr $n % $LINEWIDTH` -eq 0 ] # 格式化输出. then # ^ 取模操作 echo # 把行分成内部的块. fi done echo exit 0 # 这是 Q-series 问题的迭代实现. # 更直接明了的递归实现留给读者完成. # 警告: 递归地计算这个序列会花很长的时间. ################################End Script######################################### 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######################################### Example 27-1 利用/dev/tcp 来检修故障 ################################Start Script####################################### #!/bin/bash # dev-tcp.sh: 用/dev/tcp 重定向来检查 Internet 连接. # Troy Engel 编写. # 已得到作者允许. TCP_HOST=www.dns-diy.com # 一个已知的 ISP. TCP_PORT=80 # http 的端口是 80 . # 尝试连接. (有些像 'ping' . . .) echo "HEAD / HTTP/1.0" >/dev/tcp/${TCP_HOST}/${TCP_PORT} MYEXIT=$? : <From the bash reference: /dev/tcp/host/port If host is a valid hostname or Internet address, and port is an integer port number or service name, Bash attempts to open a TCP connection to the corresponding socket. EXPLANATION if [ "X$MYEXIT" = "X0" ]; then echo "Connection successful. Exit code: $MYEXIT" else echo "Connection unsuccessful. Exit code: $MYEXIT" fi exit $MYEXIT ################################End Script######################################### Example 27-2 搜索与一个 PID 相关的进程 ################################Start Script####################################### #!/bin/bash # pid-identifier.sh: 给出指定 PID 的进程的程序全路径. ARGNO=1 # 此脚本期望的参数个数. E_WRONGARGS=65 E_BADPID=66 E_NOSUCHPROCESS=67 E_NOPERMISSION=68 PROCFILE=exe if [ $# -ne $ARGNO ] then echo "Usage: `basename $0` PID-number" >&2 # 帮助信息重定向到标准出错. exit $E_WRONGARGS fi pidno=$( ps ax | grep $1 | awk '{ print $1 }' | grep $1 ) # 搜索命令"ps"输出的第一列. # 然后再次确认是真正我们要寻找的进程,而不是这个脚本调用而产生的进程. # 后一个"grep $1"会滤掉这个可能产生的进程. # # pidno=$( ps ax | awk '{ print $1 }' | grep $1 ) # 也可以, 由 Teemu Huovila 指出. if [ -z "$pidno" ] # 如果过滤完后结果是一个空字符串, then # 没有对应的 PID 进程在运行. echo "No such process running." exit $E_NOSUCHPROCESS fi # 也可以用: # if ! ps $1 > /dev/null 2>&1 # then # 没有对应的 PID 进程在运行. # echo "No such process running." # exit $E_NOSUCHPROCESS # fi # 为了简化整个进程,使用"pidof". if [ ! -r "/proc/$1/$PROCFILE" ] # 检查读权限. then echo "Process $1 running, but..." echo "Can't get read permission on /proc/$1/$PROCFILE." exit $E_NOPERMISSION # 普通用户不能存取/proc 目录的某些文件. fi # 最后两个测试可以用下面的代替: # if ! kill -0 $1 > /dev/null 2>&1 # '0'不是一个信号, # 但这样可以测试是否可以 # 向该进程发送信号. # then echo "PID doesn't exist or you're not its owner" >&2 # exit $E_BADPID # fi exe_file=$( ls -l /proc/$1 | grep "exe" | awk '{ print $11 }' ) # 或 exe_file=$( ls -l /proc/$1/exe | awk '{print $11}' ) # # /proc/pid-number/exe 是进程程序全路径的符号链接. # if [ -e "$exe_file" ] # 如果 /proc/pid-number/exe 存在 ... then # 则相应的进程存在. echo "Process #$1 invoked by $exe_file." else echo "No such process running." fi # 这个被详细讲解的脚本几乎可以用下面的命令代替: # ps ax | grep $1 | awk '{ print $5 }' # 然而, 这样并不会工作... # 因为'ps'输出的第 5 列是进程的 argv[0](即命令行第一个参数,调用时程序用的程序路径本 身), # 但不是可执行文件. # # 然而, 下面的两个都可以工作. # find /proc/$1/exe -printf '%l\n' # lsof -aFn -p $1 -d txt | sed -ne 's/^n//p' # 由 Stephane Chazelas 附加注释. exit 0 ################################End Script######################################### Example 27-3 网络连接状态 ################################Start Script####################################### #!/bin/bash PROCNAME=pppd # ppp 守护进程 PROCFILENAME=status # 在这儿寻找信息. NOTCONNECTED=65 INTERVAL=2 # 两秒刷新一次. pidno=$( ps ax | grep -v "ps ax" | grep -v grep | grep $PROCNAME | awk '{ print $1 }' ) # 搜索 ppp 守护进程'pppd'的进程号. # 一定要过滤掉由搜索进程产生的该行进程. # # 正如 Oleg Philon 指出的那样, #+ 使用"pidof"命令会相当的简单. # pidno=$( pidof $PROCNAME ) # # 颇有良心的建议: #+ 当命令序列变得复杂的时候,去寻找更简洁的办法. . if [ -z "$pidno" ] # 如果没有找到此进程号,则进程没有运行. then echo "Not connected." exit $NOTCONNECTED else echo "Connected."; echo fi while [ true ] # 死循环,这儿可以有所改进. do if [ ! -e "/proc/$pidno/$PROCFILENAME" ] # 进程运行时,对应的"status"文件会存在. then echo "Disconnected." exit $NOTCONNECTED fi netstat -s | grep "packets received" # 取得一些连接统计. netstat -s | grep "packets delivered" sleep $INTERVAL echo; echo done exit 0 # 当要停止它时,可以用 Control-C 终止. # 练习: # --------- # 改进这个脚本,使它能按"q"键退出. # 给脚本更友好的界面. ################################End Script######################################### Example 28-1 隐藏 cookie 而不再使用 ################################Start Script####################################### if [ -f ~/.netscape/cookies ] # 如果存在则删除. then rm -f ~/.netscape/cookies fi ln -s /dev/null ~/.netscape/cookies # 现在所有的 cookies 都会丢入黑洞而不会保存在磁盘上了. ################################End Script######################################### Example 28-2 用/dev/zero 创建一个交换临时文件 ################################Start Script####################################### #!/bin/bash # 创建一个交换文件. ROOT_UID=0 # Root 用户的 $UID 是 0. E_WRONG_USER=65 # 不是 root? FILE=/swap BLOCKSIZE=1024 MINBLOCKS=40 SUCCESS=0 # 这个脚本必须用 root 来运行. if [ "$UID" -ne "$ROOT_UID" ] then echo; echo "You must be root to run this script."; echo exit $E_WRONG_USER fi blocks=${1:-$MINBLOCKS} # 如果命令行没有指定, #+ 则设置为默认的 40 块. # 上面这句等同如: # -------------------------------------------------- # if [ -n "$1" ] # then # blocks=$1 # else # blocks=$MINBLOCKS # fi # -------------------------------------------------- if [ "$blocks" -lt $MINBLOCKS ] then blocks=$MINBLOCKS # 最少要有 40 个块长. fi echo "Creating swap file of size $blocks blocks (KB)." dd if=/dev/zero of=$FILE bs=$BLOCKSIZE count=$blocks # 把零写入文件. mkswap $FILE $blocks # 将此文件建为交换文件(或称交换分区). swapon $FILE # 激活交换文件. echo "Swap file created and activated." exit $SUCCESS ################################End Script######################################### Example 28-3 创建 ramdisk ################################Start Script####################################### #!/bin/bash # ramdisk.sh # "ramdisk"是系统 RAM 内存的一段, #+ 它可以被当成是一个文件系统来操作. # 它的优点是存取速度非常快 (包括读和写). # 缺点: 易失性, 当计算机重启或关机时会丢失数据. #+ 会减少系统可用的 RAM. # # 那么 ramdisk 有什么作用呢? # 保存一个较大的数据集在 ramdisk, 比如一张表或字典, #+ 这样可以加速数据查询, 因为在内存里查找比在磁盘里查找快得多. E_NON_ROOT_USER=70 # 必须用 root 来运行. ROOTUSER_NAME=root MOUNTPT=/mnt/ramdisk SIZE=2000 # 2K 个块 (可以合适的做修改) BLOCKSIZE=1024 # 每块有 1K (1024 byte) 的大小 DEVICE=/dev/ram0 # 第一个 ram 设备 username=`id -nu` if [ "$username" != "$ROOTUSER_NAME" ] then echo "Must be root to run \"`basename $0`\"." exit $E_NON_ROOT_USER fi if [ ! -d "$MOUNTPT" ] # 测试挂载点是否已经存在了, then #+ 如果这个脚本已经运行了好几次了就不会再建这个目录了 mkdir $MOUNTPT #+ 因为前面已经建立了. fi dd if=/dev/zero of=$DEVICE count=$SIZE bs=$BLOCKSIZE # 把 RAM 设备的内容用零填 充. # 为何需要这么做? mke2fs $DEVICE # 在 RAM 设备上创建一个 ext2 文件系统. mount $DEVICE $MOUNTPT # 挂载设备. chmod 777 $MOUNTPT # 使普通用户也可以存取这个 ramdisk. 40 # 但是,只能由 root 来缷载它. echo "\"$MOUNTPT\" now available for use." # 现在 ramdisk 即使普通用户也可以用来存取文件了. # 注意, ramdisk 是易失的, 所以当计算机系统重启或关机时 ramdisk 里的内容会消失. # # 拷贝所有你想保存文件到一个常规的磁盘目录下. # 重启之后, 运行这个脚本再次建立起一个 ramdisk. # 仅重新加载 /mnt/ramdisk 而没有其他的步骤将不会正确工作. # 如果加以改进, 这个脚本可以放在 /etc/rc.d/rc.local, #+ 以使系统启动时能自动设立一个 ramdisk. # 这样很合适速度要求高的数据库服务器. exit 0 ################################End Script######################################### Example 29-1 一个错误的脚本 ################################Start Script####################################### #!/bin/bash # ex74.sh # 这是一个错误的脚本. # 哪里有错? a=37 if [$a -gt 27 ] then echo $a fi exit 0 ################################End Script######################################### Example 29-2 丢失关键字(keyword) ################################Start Script####################################### #!/bin/bash # missing-keyword.sh: 会产生什么样的错误信息? for a in 1 2 3 do echo "$a" # done # 第 7 行的必需的关键字 'done' 被注释掉了. exit 0 ################################End Script######################################### Example 29-3 另一个错误脚本 ################################Start Script####################################### #!/bin/bash # 这个脚本目的是为了删除当前目录下的所有文件,包括文件名含有空格的文件. # # 但不能工作. # 为什么? badname=`ls | grep ' '` # 试试这个: # echo "$badname" rm "$badname" exit 0 ################################End Script######################################### Example 29-4 用"assert"测试条件 ################################Start Script####################################### #!/bin/bash # assert.sh assert () # 如果条件测试失败, { #+ 则打印错误信息并退出脚本. E_PARAM_ERR=98 E_ASSERT_FAILED=99 if [ -z "$2" ] # 没有传递足够的参数. then return $E_PARAM_ERR # 什么也不做就返回. fi lineno=$2 if [ ! $1 ] then echo "Assertion failed: \"$1\"" echo "File \"$0\", line $lineno" exit $E_ASSERT_FAILED # else # return # 返回并继续执行脚本后面的代码. fi } a=5 b=4 condition="$a -lt $b" # 会错误信息并从脚本退出. # 把这个“条件”放在某个地方, #+ 然后看看有什么现象. assert "$condition" $LINENO # 脚本以下的代码只有当"assert"成功时才会继续执行. # 其他的命令. # ... echo "This statement echoes only if the \"assert\" does not fail." # ... # 余下的其他命令. exit 0 ################################End Script######################################### Example 29-5 捕捉 exit ################################Start Script####################################### #!/bin/bash # 用 trap 捕捉变量值. trap 'echo Variable Listing --- a = $a b = $b' EXIT # EXIT 是脚本中 exit 命令产生的信号的信号名. # # 由"trap"指定的命令不会被马上执行,只有当发送了一个适应的信号时才会执行. # echo "This prints before the \"trap\" --" echo "even though the script sees the \"trap\" first." echo a=39 b=36 exit 0 # 注意到注释掉上面一行的'exit'命令也没有什么不同, #+ 这是因为执行完所有的命令脚本都会退出. ################################End Script######################################### Example 29-6 在 Control-C 后清除垃圾 ################################Start Script####################################### #!/bin/bash # logon.sh: 简陋的检查你是否还处于连线的脚本. umask 177 # 确定临时文件不是全部用户都可读的. TRUE=1 LOGFILE=/var/log/messages # 注意 $LOGFILE 必须是可读的 #+ (用 root 来做:chmod 644 /var/log/messages). TEMPFILE=temp.$$ # 创建一个"唯一的"临时文件名, 使用脚本的进程 ID. # 用 'mktemp' 是另一个可行的办法. # 举例: # TEMPFILE=`mktemp temp.XXXXXX` KEYWORD=address # 上网时, 把"remote IP address xxx.xxx.xxx.xxx"这行 # 加到 /var/log/messages. ONLINE=22 USER_INTERRUPT=13 CHECK_LINES=100 # 日志文件中有多少行要检查. trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT # 如果脚本被 control-c 中断了,则清除临时文件. echo while [ $TRUE ] #死循环. do tail -$CHECK_LINES $LOGFILE> $TEMPFILE # 保存系统日志文件的最后 100 行到临时文件. # 这是需要的, 因为新版本的内核在登录网络时产生许多日志文件信息. search=`grep $KEYWORD $TEMPFILE` # 检查"IP address" 短语是不是存在, #+ 它指示了一次成功的网络登录. if [ ! -z "$search" ] # 引号是必须的,因为变量可能会有一些空白符. then echo "On-line" rm -f $TEMPFILE # 清除临时文件. exit $ONLINE else echo -n "." # -n 选项使 echo 不会产生新行符, #+ 这样你可以从该行的继续打印. fi sleep 1 done # 注: 如果你更改 KEYWORD 变量的值为"Exit", #+ 这个脚本就能用来在网络登录后检查掉线 # # 练习: 修改脚本,像上面所说的那样,并修正得更好 # exit 0 # Nick Drage 建议用另一种方法: while true do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0 echo -n "." # 在连接上之前打印点 (.....). sleep 2 done # 问题: 用 Control-C 来终止这个进程可能是不够的. #+ (点可能会继续被打印.) # 练习: 修复这个问题. # Stephane Chazelas 也提出了另一个办法: CHECK_INTERVAL=1 while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD" do echo -n . sleep $CHECK_INTERVAL done echo "On-line" # 练习: 讨论这几个方法的优缺点. # ################################End Script######################################### Example 29-7 跟踪变量 ################################Start Script####################################### #!/bin/bash trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG # 在每个命令行显示变量$variable 的值. variable=29 echo "Just initialized \"\$variable\" to $variable." let "variable *= 3" echo "Just multiplied \"\$variable\" by 3." exit $? # "trap 'command1 . . . command2 . . .' DEBUG" 的结构适合复杂脚本的环境 #+ 在这种情况下多次"echo $variable"比较没有技巧并且也耗时. # # # Thanks, Stephane Chazelas 指出这一点. 脚本的输出: VARIABLE-TRACE> $variable = "" VARIABLE-TRACE> $variable = "29" Just initialized "$variable" to 29. VARIABLE-TRACE> $variable = "29" VARIABLE-TRACE> $variable = "87" Just multiplied "$variable" by 3. VARIABLE-TRACE> $variable = "87" ################################End Script######################################### Example 29-8 运行多进程 (在多处理器的机器里) ################################Start Script####################################### 1 #!/bin/bash 2 # parent.sh 3 # 在多处理器的机器里运行多进程. 4 # 作者: Tedman Eng 5 6 # 这是要介绍的两个脚本的第一个, 7 #+ 这两个脚本都在要在相同的工作目录下. 8 9 10 11 12 LIMIT=$1 # 要启动的进程总数 13 NUMPROC=4 # 当前进程数 (forks?) 14 PROCID=1 # 启动的进程 ID 15 echo "My PID is $$" 16 17 function start_thread() { 18 if [ $PROCID -le $LIMIT ] ; then 19 ./child.sh $PROCID& 20 let "PROCID++" 21 else 22 echo "Limit reached." 23 wait 24 exit 25 fi 26 } 27 28 while [ "$NUMPROC" -gt 0 ]; do 29 start_thread; 30 let "NUMPROC--" 31 done 32 33 34 while true 35 do 36 37 trap "start_thread" SIGRTMIN 38 39 done 40 41 exit 0 42 43 44 45 # ======== 下面是第二个脚本 ======== 46 47 48 #!/bin/bash 49 # child.sh 50 # 在多处理器的机器里运行多进程. 51 # 这个脚本由 parent.sh 脚本调用(即上面的脚本). 52 # 作者: Tedman Eng 53 54 temp=$RANDOM 55 index=$1 56 shift 57 let "temp %= 5" 58 let "temp += 4" 59 echo "Starting $index Time:$temp" "$@" 60 sleep ${temp} 61 echo "Ending $index" 62 kill -s SIGRTMIN $PPID 63 64 exit 0 65 66 67 # ======================= 脚本作者注 ======================= # 68 # 这不是完全没有 bug 的脚本. 69 # 我运行 LIMIT = 500 ,在过了开头的一二百个循环后, 70 #+ 这些进程有一个消失了! 71 # 不能确定是不是因为捕捉信号产生碰撞还是其他的原因. 72 # 一但信号捕捉到,在下一个信号设置之前, 73 #+ 会有一个短暂的时间来执行信号处理程序, 74 #+ 这段时间内很可能会丢失一个信号捕捉,因此失去生成一个子进程的机会. 75 76 # 毫无疑问会有人能找出这个 bug 的原因,并且修复它 77 #+ . . . 在将来的某个时候. 78 79 80 81 # ===================================================================== # 82 83 84 85 # ----------------------------------------------------------------------# 86 87 88 89 ################################################################# 90 # 下面的脚本由 Vernia Damiano 原创. 91 # 不幸地是, 它不能正确工作. 92 ################################################################# 93 94 #!/bin/bash 95 96 # 必须以最少一个整数参数来调用这个脚本 97 #+ (这个整数是协作进程的数目). 98 # 所有的其他参数被传给要启动的进程. 99 100 101 INDICE=8 # 要启动的进程数目 102 TEMPO=5 # 每个进程最大的睡眼时间 103 E_BADARGS=65 # 没有参数传给脚本的错误值. 104 105 if [ $# -eq 0 ] # 检查是否至少传了一个参数给脚本. 106 then 107 echo "Usage: `basename $0` number_of_processes [passed params]" 108 exit $E_BADARGS 109 fi 110 111 NUMPROC=$1 # 协作进程的数目 112 shift 113 PARAMETRI=( "$@" ) # 每个进程的参数 114 115 function avvia() { 116 local temp 117 local index 118 temp=$RANDOM 119 index=$1 120 shift 121 let "temp %= $TEMPO" 122 let "temp += 1" 123 echo "Starting $index Time:$temp" "$@" 124 sleep ${temp} 125 echo "Ending $index" 126 kill -s SIGRTMIN $$ 127 } 128 129 function parti() { 130 if [ $INDICE -gt 0 ] ; then 131 avvia $INDICE "${PARAMETRI[@]}" & 132 let "INDICE--" 133 else 134 trap : SIGRTMIN 135 fi 136 } 137 138 trap parti SIGRTMIN 139 140 while [ "$NUMPROC" -gt 0 ]; do 141 parti; 142 let "NUMPROC--" 143 done 144 145 wait 146 trap - SIGRTMIN 147 148 exit $? 149 150 : <> $LOGFILE # 现在, 执行操作. exec $OPERATION "$@" # 在操作之前记录日志是必须的. # 为什么? ################################End Script######################################### Example 33-4 包装 awk 的脚本 ################################Start Script####################################### #!/bin/bash # pr-ascii.sh: 打印 ASCII 码的字符表. START=33 # 可打印的 ASCII 字符的范围 (十进制). END=125 echo " Decimal Hex Character" # 表头. echo " ------- --- ---------" for ((i=START; i<=END; i++)) do echo $i | awk '{printf(" %3d %2x %c\n", $1, $1, $1)}' # 在这个上下文,不会运行 Bash 的内建 printf 命令: # printf "%c" "$i" done exit 0 # Decimal Hex Character # ------- --- --------- # 33 21 ! # 34 22 " # 35 23 # # 36 24 $ # # . . . # # 122 7a z # 123 7b { # 124 7c | # 125 7d } # 把脚本的输出重定向到一个文件或是管道给 more 命令来查看: #+ sh pr-asc.sh | more ################################End Script######################################### Example 33-5 另一个包装 awk 的脚本 ################################Start Script####################################### #!/bin/bash # 给目标文件增加一列由数字指定的列. ARGS=2 E_WRONGARGS=65 if [ $# -ne "$ARGS" ] # 检查命令行参数个数是否正确. then echo "Usage: `basename $0` filename column-number" exit $E_WRONGARGS fi filename=$1 column_number=$2 # 传递 shell 变量给脚本的 awk 部分需要一点技巧. # 方法之一是在 awk 脚本中使用强引用来引起 bash 脚本的变量 # # $'$BASH_SCRIPT_VAR' # ^ ^ # 这个方法在下面的内嵌的 awk 脚本中出现. # 参考 awk 文档了解更多的细节. # 多行的 awk 脚本调用格式为: awk ' ..... ' # 开始 awk 脚本. # ----------------------------- awk ' { total += $'"${column_number}"' } END { print total } ' "$filename" # ----------------------------- # awk 脚本结束. # 把 shell 变量传递给 awk 变量可能是不安全的, #+ 因此 Stephane Chazelas 提出了下面另外一种方法: # --------------------------------------- # awk -v column_number="$column_number" ' # { total += $column_number # } # END { # print total # }' "$filename" # --------------------------------------- exit 0 ################################End Script######################################### Example 33-6 把 Perl 嵌入 Bash 脚本 ################################Start Script####################################### #!/bin/bash # Shell 命令可以包含 Perl 脚本. echo "This precedes the embedded Perl script within \"$0\"." echo "===============================================================" perl -e 'print "This is an embedded Perl script.\n";' # 像 sed 脚本, Perl 也使用"-e"选项. echo "===============================================================" echo "However, the script may also contain shell and system commands." exit 0 ################################End Script######################################### Example 33-7 Bash 和 Perl 脚本联合使用 ################################Start Script####################################### #!/bin/bash # bashandperl.sh echo "Greetings from the Bash part of the script." # 下面可以有更多的 Bash 命令. exit 0 # 脚本的 Bash 部分结束. # ======================================================= #!/usr/bin/perl # 脚本的这个部分必须用-x 选项来调用. print "Greetings from the Perl part of the script.\n"; # 下面可以有更多的 Perl 命令. # 脚本的 Perl 部分结束. ################################End Script######################################### Example 33-8 递归调用自己本身的(无用)脚本 ################################Start Script####################################### #!/bin/bash # recurse.sh # 脚本能否递归地调用自己? # 是的, 但这有什么实际的用处吗? # (看下面的.) RANGE=10 MAXVAL=9 i=$RANDOM let "i %= $RANGE" # 产生一个从 0 到 $RANGE - 1 之间的随机数. if [ "$i" -lt "$MAXVAL" ] then echo "i = $i" ./$0 # 脚本递归地调用再生成一个和自己一样的实例. fi # 每个子脚本做的事都一样, #+ 直到产生的变量 $i 和变量 $MAXVAL 相等. # 用"while"循环代替"if/then"测试会引起错误. # 解释为什么会这样. exit 0 # 注: # ---- # 脚本要正确地工作必须有执行权限. # 这是指用"sh"命令来调用这个脚本而没有设置正确权限导致的问题. # 请解释原因. ################################End Script######################################### Example 33-9 递归调用自己本身的(有用)脚本 ################################Start Script####################################### #!/bin/bash # pb.sh: 电话本(phone book) # 由 Rick Boivie 编写,已得到使用许可. # 由 ABS 文档作者修改. MINARGS=1 # 脚本需要至少一个参数. DATAFILE=./phonebook # 在当前目录下名为"phonebook"的数据文件必须存在 # PROGNAME=$0 E_NOARGS=70 # 没有参数的错误值. if [ $# -lt $MINARGS ]; then echo "Usage: "$PROGNAME" data" exit $E_NOARGS fi if [ $# -eq $MINARGS ]; then grep $1 "$DATAFILE" # 如果$DATAFILE 文件不存在,'grep' 会打印一个错误信息. else ( shift; "$PROGNAME" $* ) | grep $1 # 脚本递归调用本身. fi exit 0 # 脚本在这儿退出. # 因此 Therefore, 从这行开始可以写没有#开头的的注释行 # # ------------------------------------------------------------------------ "phonebook"文件的例子: John Doe 1555 Main St., Baltimore, MD 21228 (410) 222-3333 Mary Moe 9899 Jones Blvd., Warren, NH 03787 (603) 898-3232 Richard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567 Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678 Zoe Zenobia 4481 N. Baker St., San Francisco, SF 94338 (415) 501-1631 # ------------------------------------------------------------------------ $bash pb.sh Roe Richard Roe 856 E. 7th St., New York, NY 10009 (212) 333-4567 Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678 $bash pb.sh Roe Sam Sam Roe 956 E. 8th St., New York, NY 10009 (212) 444-5678 # 当超过一个参数传给这个脚本时, #+ 它只打印包含所有参数的行. ################################End Script######################################### Example 33-10 另一个递归调用自己本身的(有用)脚本 ################################Start Script####################################### #!/bin/bash # usrmnt.sh, 由 Anthony Richardson 编写 # 得到允许在此使用. # usage: usrmnt.sh # 描述: 挂载设备, 调用者必须列在/etc/sudoers 文件的 MNTUSERS 组里 # # ---------------------------------------------------------- # 这是一个用户挂载设备的脚本,它用 sudo 来调用自己. # 只有拥有合适权限的用户才能用 # usermount /dev/fd0 /mnt/floppy # 来代替 # sudo usermount /dev/fd0 /mnt/floppy # 我使用相同的技术来处理我所有的 sudo 脚本, #+ 因为我觉得它很方便. # ---------------------------------------------------------- # 如果 SUDO_COMMAND 变量没有设置,我们不能通过 sudo 来运行脚本本身. #+ 传递用户的真实 ID 和组 ID . . . if [ -z "$SUDO_COMMAND" ] then mntusr=$(id -u) grpusr=$(id -g) sudo $0 $* exit 0 fi # 如果我们以 sudo 来调用运行,就会运行这儿. /bin/mount $* -o uid=$mntusr,gid=$grpusr exit 0 # 附注 (由脚本作者加注): # ------------------------------------------------- # 1) Linux 允许在/etc/fstab 文件中使用"users"选项 # 以使任何用户能挂载可移动的介质. # 但是, 在一个服务器上, # 我只想有限的几个用户可以存取可移动介质. # 我发现使用 sudo 可以有更多的控制. # 2) 我也发现 sudo 能通过组更方便地达成目的. # # 3) 这个方法使给予任何想给合适权限的人使用 mount 命令 # 所以要小心使用. # 你也可以开发类似的脚本 mntfloppy, mntcdrom,和 mntsamba 来使 mount命令得到更好的 控制 # # # ################################End Script######################################### Example 33-11 一个 "彩色的" 地址资料库 ################################Start Script####################################### #!/bin/bash # ex30a.sh: ex30.sh 的"彩色" 版本. # 没有加工处理的地址资料库 clear # 清除屏幕. echo -n " " echo -e '\E[37;44m'"\033[1mContact List\033[0m" # 白色为前景色,蓝色为背景色 echo; echo echo -e "\033[1mChoose one of the following persons:\033[0m" # 粗体 tput sgr0 echo "(Enter only the first letter of name.)" echo echo -en '\E[47;34m'"\033[1mE\033[0m" # 蓝色 tput sgr0 # 把色彩设置为"常规" echo "vans, Roland" # "[E]vans, Roland" echo -en '\E[47;35m'"\033[1mJ\033[0m" # 红紫色 tput sgr0 echo "ones, Mildred" echo -en '\E[47;32m'"\033[1mS\033[0m" # 绿色 tput sgr0 echo "mith, Julie" echo -en '\E[47;31m'"\033[1mZ\033[0m" # 红色 tput sgr0 echo "ane, Morris" echo read person case "$person" in # 注意变量被引起来了. "E" | "e" ) # 接受大小写的输入. echo echo "Roland Evans" echo "4321 Floppy Dr." echo "Hardscrabble, CO 80753" echo "(303) 734-9874" echo "(303) 734-9892 fax" echo "revans@zzy.net" echo "Business partner & old friend" ;; "J" | "j" ) echo echo "Mildred Jones" echo "249 E. 7th St., Apt. 19" echo "New York, NY 10009" echo "(212) 533-2814" echo "(212) 533-9972 fax" echo "milliej@loisaida.com" echo "Girlfriend" echo "Birthday: Feb. 11" ;; # 稍后为 Smith 和 Zane 增加信息. * ) # 默认选项 Default option. # 空的输入(直接按了回车) 也会匹配这儿. echo echo "Not yet in database." ;; esac tput sgr0 # 把色彩重设为"常规". echo exit 0 ################################End Script######################################### Example 33-12 画盒子 ################################Start Script####################################### 1 #!/bin/bash 2 # Draw-box.sh: 用 ASCII 字符画一个盒子. 3 4 # Stefano Palmeri 编写,文档作者作了少量编辑. 5 # 征得作者同意在本书使用. 6 7 8 ###################################################################### 9 ### draw_box 函数的注释 ### 10 11 # "draw_box" 函数使用户可以在终端上画一个盒子. 12 # 13 # 14 # 用法: draw_box ROW COLUMN HEIGHT WIDTH [COLOR] 15 # ROW 和 COLUMN 定位要画的盒子的左上角. 16 # 17 # ROW 和 COLUMN 必须要大于 0 且小于目前终端的尺寸. 18 # 19 # HEIGHT 是盒子的行数,必须 > 0. 20 # HEIGHT + ROW 必须 <= 终端的高度. 21 # WIDTH 是盒子的列数,必须 > 0. 22 # WIDTH + COLUMN 必须 <= 终端的宽度. 23 # 24 # 例如: 如果你当前终端的尺寸是 20x80, 25 # draw_box 2 3 10 45 是合法的 26 # draw_box 2 3 19 45 的 HEIGHT 值是错的 (19+2 > 20) 27 # draw_box 2 3 18 78 的 WIDTH 值是错的 (78+3 > 80) 28 # 29 # COLOR 是盒子边框的颜色. 30 # 它是第 5 个参数,并且它是可选的. 31 # 0=黑色 1=红色 2=绿色 3=棕褐色 4=蓝色 5=紫色 6=青色 7=白色. 32 # 如果你传给这个函数错的参数, 33 #+ 它就会以代码 65 退出, 34 #+ 没有其他的信息打印到标准出错上. 35 # 36 # 在画盒子之前要清屏. 37 # 函数内不包含有清屏命令. 38 # 这使用户可以画多个盒子,甚至叠接多个盒子. 39 40 ### draw_box 函数注释结束 ### 41 ###################################################################### 42 43 draw_box(){ 44 45 #=============# 46 HORZ="-" 47 VERT="|" 48 CORNER_CHAR="+" 49 50 MINARGS=4 51 E_BADARGS=65 52 #=============# 53 54 55 if [ $# -lt "$MINARGS" ]; then # 如果参数小于 4,退出. 56 exit $E_BADARGS 57 fi 58 59 # 搜寻参数中的非数字的字符. 60 # 能用其他更好的办法吗 (留给读者的练习?). 61 if echo $@ | tr -d [:blank:] | tr -d [:digit:] | grep . &> /dev/null; then 62 exit $E_BADARGS 63 fi 64 65 BOX_HEIGHT=`expr $3 - 1` # -1 是需要的,因为因为边角的"+"是高和宽共有的部分. 66 BOX_WIDTH=`expr $4 - 1` # 67 T_ROWS=`tput lines` # 定义当前终端长和宽的尺寸, 68 T_COLS=`tput cols` # 69 70 if [ $1 -lt 1 ] || [ $1 -gt $T_ROWS ]; then # 如果参数是数字就开始检查有效性. 71 exit $E_BADARGS # 72 fi 73 if [ $2 -lt 1 ] || [ $2 -gt $T_COLS ]; then 74 exit $E_BADARGS 75 fi 76 if [ `expr $1 + $BOX_HEIGHT + 1` -gt $T_ROWS ]; then 77 exit $E_BADARGS 78 fi 79 if [ `expr $2 + $BOX_WIDTH + 1` -gt $T_COLS ]; then 80 exit $E_BADARGS 81 fi 82 if [ $3 -lt 1 ] || [ $4 -lt 1 ]; then 83 exit $E_BADARGS 84 fi # 参数检查完毕. 85 86 plot_char(){ # 函数内的函数. 87 echo -e "\E[${1};${2}H"$3 88 } 89 90 echo -ne "\E[3${5}m" # 如果传递了盒子边框颜色参数,则设置它. 91 92 # start drawing the box 93 94 count=1 # 用 plot_char 函数画垂直线 95 for (( r=$1; count<=$BOX_HEIGHT; r++)); do # 96 plot_char $r $2 $VERT 97 let count=count+1 98 done 99 100 count=1 101 c=`expr $2 + $BOX_WIDTH` 102 for (( r=$1; count<=$BOX_HEIGHT; r++)); do 103 plot_char $r $c $VERT 104 let count=count+1 105 done 106 107 count=1 # 用 plot_char 函数画水平线 108 for (( c=$2; count<=$BOX_WIDTH; c++)); do # 109 plot_char $1 $c $HORZ 110 let count=count+1 111 done 112 113 count=1 114 r=`expr $1 + $BOX_HEIGHT` 115 for (( c=$2; count<=$BOX_WIDTH; c++)); do 116 plot_char $r $c $HORZ 117 let count=count+1 118 done 119 120 plot_char $1 $2 $CORNER_CHAR # 画盒子的角. 121 plot_char $1 `expr $2 + $BOX_WIDTH` + 122 plot_char `expr $1 + $BOX_HEIGHT` $2 + 123 plot_char `expr $1 + $BOX_HEIGHT` `expr $2 + $BOX_WIDTH` + 124 125 echo -ne "\E[0m" # 恢复最初的颜色. 126 127 P_ROWS=`expr $T_ROWS - 1` # 在终端的底部打印提示符. 128 129 echo -e "\E[${P_ROWS};1H" 130 } 131 132 133 # 现在, 让我们来画一个盒子. 134 clear # 清屏. 135 R=2 # 行 136 C=3 # 列 137 H=10 # 高 138 W=45 # 宽 139 col=1 # 颜色(红) 140 draw_box $R $C $H $W $col # 画盒子. 141 142 exit 0 143 144 # 练习: 145 # -------- 146 # 增加可以在盒子里打印文本的选项 ################################End Script######################################### Example 33-13 显示彩色文本 ################################Start Script####################################### #!/bin/bash # color-echo.sh: 用彩色来显示文本. # 依照需要修改这个脚本. # 这比手写彩色的代码更容易一些. black='\E[30;47m' red='\E[31;47m' green='\E[32;47m' yellow='\E[33;47m' blue='\E[34;47m' magenta='\E[35;47m' cyan='\E[36;47m' white='\E[37;47m' alias Reset="tput sgr0" # 把文本属性重设回原来没有清屏前的 # cecho () # Color-echo. # 参数 $1 = 要显示的信息 # 参数 $2 = 颜色 { local default_msg="No message passed." # 不是非要一个本地变量. message=${1:-$default_msg} # 默认的信息. color=${2:-$black} # 如果没有指定,默认使用黑色. echo -e "$color" echo "$message" Reset # 重设文本属性. return } # 现在,让我们试试. # ---------------------------------------------------- cecho "Feeling blue..." $blue cecho "Magenta looks more like purple." $magenta cecho "Green with envy." $green cecho "Seeing red?" $red cecho "Cyan, more familiarly known as aqua." $cyan cecho "No color passed (defaults to black)." # 缺失 $color (色彩)参数. cecho "\"Empty\" color passed (defaults to black)." "" # 空的 $color (色彩)参数. cecho # $message(信息) 和 $color (色彩)参数都缺失. cecho "" "" # 空的 $message (信息)和 $color (色彩)参数. # ---------------------------------------------------- echo exit 0 # 练习: # --------- # 1) 为'cecho ()'函数增加粗体的效果. # 2) 增加可选的彩色背景. ################################End Script######################################### Example 33-14 "赛马" 游戏 ################################Start Script####################################### 1 #!/bin/bash 2 # horserace.sh: 非常简单的赛马模拟. 3 # 作者: Stefano Palmeri 4 # 已取得使用许可. 5 6 ################################################################ 7 # 脚本目的: 8 # 使用转义字符和终端颜色. 9 # 10 # 练习: 11 # 编辑脚本使其更具有随机性, 12 #+ 设置一个假的赌场 . . . 13 # 嗯 . . . 嗯 . . . 这个开始使我想起了一部电影 . . . 14 # 15 # 脚本给每匹马一个随机的障碍. 16 # 不均等会以障碍来计算 17 #+ 并且用一种欧洲风格表达出来. 18 # 例如: 机率(odds)=3.75 意味着如果你押 1 美元赢, 19 #+ 你可以赢得 3.75 美元. 20 # 21 # 脚本已经在 GNU/Linux 操作系统上测试过 OS, 22 #+ 测试终端有 xterm 和 rxvt, 及 konsole. 23 # 测试机器有 AMD 900 MHz 的处理器, 24 #+ 平均比赛时间是 75 秒. 25 # 在更快的计算机上比赛时间应该会更低. 26 # 所以, 如果你想有更多的悬念,重设 USLEEP_ARG 变量的值. 27 # 28 # 由 Stefano Palmeri 编写. 29 ################################################################ 30 31 E_RUNERR=65 32 33 # 检查 md5sum 和 bc 是不是安装了. 34 if ! which bc &> /dev/null; then 35 echo bc is not installed. 36 echo "Can\'t run . . . " 37 exit $E_RUNERR 38 fi 39 if ! which md5sum &> /dev/null; then 40 echo md5sum is not installed. 41 echo "Can\'t run . . . " 42 exit $E_RUNERR 43 fi 44 45 # 更改下面的变量值可以使脚本执行的更慢. 46 # 它会作为 usleep 的参数 (man usleep) 47 #+ 并且它的单位是微秒 (500000 微秒 = 半秒). 48 USLEEP_ARG=0 49 50 # 如果脚本接收到 ctrl-c 中断,清除临时目录, 恢复终端光标和颜色 51 # 52 trap 'echo -en "\E[?25h"; echo -en "\E[0m"; stty echo;\ 53 tput cup 20 0; rm -fr $HORSE_RACE_TMP_DIR' TERM EXIT 54 # 参考调试的章节了解'trap'的更多解释 55 56 # 给脚本设置一个唯一(实际不是绝对唯一的)的临时目录名. 57 HORSE_RACE_TMP_DIR=$HOME/.horserace-`date +%s`-`head -c10 /dev/urandom | md5sum | head -c30` 58 59 # 创建临时目录,并切换到该目录下. 60 mkdir $HORSE_RACE_TMP_DIR 61 cd $HORSE_RACE_TMP_DIR 62 63 64 # 这个函数把光标移动到行为 $1 列为 $2 然后打印 $3. 65 # 例如: "move_and_echo 5 10 linux" 等同于 66 #+ "tput cup 4 9; echo linux", 但是用一个命令代替了两个. 67 # 注: "tput cup" 表示在终端左上角的 0 0 位置, 68 #+ echo 是在终端的左上角的 1 1 位置. 69 move_and_echo() { 70 echo -ne "\E[${1};${2}H""$3" 71 } 72 73 # 产生 1-9 之间伪随机数的函数. 74 random_1_9 () { 75 head -c10 /dev/urandom | md5sum | tr -d [a-z] | tr -d 0 | cut -c1 76 } 77 78 # 画马时模拟运动的两个函数. 79 draw_horse_one() { 80 echo -n " "//$MOVE_HORSE// 81 } 82 draw_horse_two(){ 83 echo -n " "\\\\$MOVE_HORSE\\\\ 84 } 85 86 87 # 取得当前的终端尺寸. 88 N_COLS=`tput cols` 89 N_LINES=`tput lines` 90 91 # 至少需要 20-行 X 80-列 的终端尺寸. 检查一下. 92 if [ $N_COLS -lt 80 ] || [ $N_LINES -lt 20 ]; then 93 echo "`basename $0` needs a 80-cols X 20-lines terminal." 94 echo "Your terminal is ${N_COLS}-cols X ${N_LINES}-lines." 95 exit $E_RUNERR 96 fi 97 98 99 # 开始画赛场. 100 101 # 需要一个 80 个字符的字符串,看下面的. 102 BLANK80=`seq -s "" 100 | head -c80` 103 104 clear 105 106 # 把前景和背景颜色设置成白色的. 107 echo -ne '\E[37;47m' 108 109 # 把光标移到终端的左上角. 110 tput cup 0 0 111 112 # 画六条白线. 113 for n in `seq 5`; do 114 echo $BLANK80 # 线是用 80 个字符组成的字符串. 115 done 116 117 # 把前景色设置成黑色. 118 echo -ne '\E[30m' 119 120 move_and_echo 3 1 "START 1" 121 move_and_echo 3 75 FINISH 122 move_and_echo 1 5 "|" 123 move_and_echo 1 80 "|" 124 move_and_echo 2 5 "|" 125 move_and_echo 2 80 "|" 126 move_and_echo 4 5 "| 2" 127 move_and_echo 4 80 "|" 128 move_and_echo 5 5 "V 3" 129 move_and_echo 5 80 "V" 130 131 # 把前景色设置成红色. 132 echo -ne '\E[31m' 133 134 # 一些 ASCII 艺术. 135 move_and_echo 1 8 "..@@@..@@@@@...@@@@@.@...@..@@@@..." 136 move_and_echo 2 8 ".@...@...@.......@...@...@.@......." 137 move_and_echo 3 8 ".@@@@@...@.......@...@@@@@.@@@@...." 138 move_and_echo 4 8 ".@...@...@.......@...@...@.@......." 139 move_and_echo 5 8 ".@...@...@.......@...@...@..@@@@..." 140 move_and_echo 1 43 "@@@@...@@@...@@@@..@@@@..@@@@." 141 move_and_echo 2 43 "@...@.@...@.@.....@.....@....." 142 move_and_echo 3 43 "@@@@..@@@@@.@.....@@@@...@@@.." 143 move_and_echo 4 43 "@..@..@...@.@.....@.........@." 144 move_and_echo 5 43 "@...@.@...@..@@@@..@@@@.@@@@.." 145 146 147 # 把前景和背景颜色设为绿色. 148 echo -ne '\E[32;42m' 149 150 # 画 11 行绿线. 151 tput cup 5 0 152 for n in `seq 11`; do 153 echo $BLANK80 154 done 155 156 # 把前景色设为黑色. 157 echo -ne '\E[30m' 158 tput cup 5 0 159 160 # 画栅栏. 161 echo "++++++++++++++++++++++++++++++++++++++\ 162 ++++++++++++++++++++++++++++++++++++++++++" 163 164 tput cup 15 0 165 echo "++++++++++++++++++++++++++++++++++++++\ 166 ++++++++++++++++++++++++++++++++++++++++++" 167 168 # 把前景和背景色设回白色. 169 echo -ne '\E[37;47m' 170 171 # 画 3 条白线. 172 for n in `seq 3`; do 173 echo $BLANK80 174 done 175 176 # 把前景色设为黑色. 177 echo -ne '\E[30m' 178 179 # 创建 9 个文件来保存障碍物. 180 for n in `seq 10 7 68`; do 181 touch $n 182 done 183 184 # 设置脚本要画的马的类型为第一种类型. 185 HORSE_TYPE=2 186 187 # 为每匹马创建位置文件和机率文件. 188 #+ 在这些文件里保存了该匹马当前的位置, 189 #+ 类型和机率. 190 for HN in `seq 9`; do 191 touch horse_${HN}_position 192 touch odds_${HN} 193 echo \-1 > horse_${HN}_position 194 echo $HORSE_TYPE >> horse_${HN}_position 195 # 给马定义随机的障碍物. 196 HANDICAP=`random_1_9` 197 # 检查 random_1_9 函数是否返回了有效值. 198 while ! echo $HANDICAP | grep [1-9] &> /dev/null; do 199 HANDICAP=`random_1_9` 200 done 201 # 给马定义最后的障碍的位置. 202 LHP=`expr $HANDICAP \* 7 + 3` 203 for FILE in `seq 10 7 $LHP`; do 204 echo $HN >> $FILE 205 done 206 207 # 计算机率. 208 case $HANDICAP in 209 1) ODDS=`echo $HANDICAP \* 0.25 + 1.25 | bc` 210 echo $ODDS > odds_${HN} 211 ;; 212 2 | 3) ODDS=`echo $HANDICAP \* 0.40 + 1.25 | bc` 213 echo $ODDS > odds_${HN} 214 ;; 215 4 | 5 | 6) ODDS=`echo $HANDICAP \* 0.55 + 1.25 | bc` 216 echo $ODDS > odds_${HN} 217 ;; 218 7 | 8) ODDS=`echo $HANDICAP \* 0.75 + 1.25 | bc` 219 echo $ODDS > odds_${HN} 220 ;; 221 9) ODDS=`echo $HANDICAP \* 0.90 + 1.25 | bc` 222 echo $ODDS > odds_${HN} 223 esac 224 225 226 done 227 228 229 # 打印机率. 230 print_odds() { 231 tput cup 6 0 232 echo -ne '\E[30;42m' 233 for HN in `seq 9`; do 234 echo "#$HN odds->" `cat odds_${HN}` 235 done 236 } 237 238 # 在起跑线上画马. 239 draw_horses() { 240 tput cup 6 0 241 echo -ne '\E[30;42m' 242 for HN in `seq 9`; do 243 echo /\\$HN/\\" " 244 done 245 } 246 247 print_odds 248 249 echo -ne '\E[47m' 250 # 等待回车按键开始赛马. 251 # 转义序列'\E[?25l'禁显了光标. 252 tput cup 17 0 253 echo -e '\E[?25l'Press [enter] key to start the race... 254 read -s 255 256 # 禁用了终端的常规显示功能. 257 # 这避免了赛跑时不小心按了按键键入显示字符而弄乱了屏幕. 258 # 259 stty -echo 260 261 # -------------------------------------------------------- 262 # 开始赛跑. 263 264 draw_horses 265 echo -ne '\E[37;47m' 266 move_and_echo 18 1 $BLANK80 267 echo -ne '\E[30m' 268 move_and_echo 18 1 Starting... 269 sleep 1 270 271 # 设置终点线的列数. 272 WINNING_POS=74 273 274 # 记录赛跑开始的时间. 275 START_TIME=`date +%s` 276 277 # COL 是由下面的"while"结构使用的. 278 COL=0 279 280 while [ $COL -lt $WINNING_POS ]; do 281 282 MOVE_HORSE=0 283 284 # 检查 random_1_9 函数是否返回了有效值. 285 while ! echo $MOVE_HORSE | grep [1-9] &> /dev/null; do 286 MOVE_HORSE=`random_1_9` 287 done 288 289 # 取得随机取得的马的类型和当前位置. 290 HORSE_TYPE=`cat horse_${MOVE_HORSE}_position | tail -1` 291 COL=$(expr `cat horse_${MOVE_HORSE}_position | head -1`) 292 293 ADD_POS=1 294 # 检查当前的位置是否是障碍物的位置. 295 if seq 10 7 68 | grep -w $COL &> /dev/null; then 296 if grep -w $MOVE_HORSE $COL &> /dev/null; then 297 ADD_POS=0 298 grep -v -w $MOVE_HORSE $COL > ${COL}_new 299 rm -f $COL 300 mv -f ${COL}_new $COL 301 else ADD_POS=1 302 fi 303 else ADD_POS=1 304 fi 305 COL=`expr $COL + $ADD_POS` 306 echo $COL > horse_${MOVE_HORSE}_position # 保存新位置. 307 308 # 选择要画的马的类型. 309 case $HORSE_TYPE in 310 1) HORSE_TYPE=2; DRAW_HORSE=draw_horse_two 311 ;; 312 2) HORSE_TYPE=1; DRAW_HORSE=draw_horse_one 313 esac 314 echo $HORSE_TYPE >> horse_${MOVE_HORSE}_position # 保存当前类型. 315 316 # 把前景色设为黑,背景色设为绿. 317 echo -ne '\E[30;42m' 318 319 # 把光标位置移到新的马的位置. 320 tput cup `expr $MOVE_HORSE + 5` `cat horse_${MOVE_HORSE}_position | head -1` 321 322 # 画马. 323 $DRAW_HORSE 324 usleep $USLEEP_ARG 325 326 # 当所有的马都越过 15 行的之后,再次打印机率. 327 touch fieldline15 328 if [ $COL = 15 ]; then 329 echo $MOVE_HORSE >> fieldline15 330 fi 331 if [ `wc -l fieldline15 | cut -f1 -d " "` = 9 ]; then 332 print_odds 333 : > fieldline15 334 fi 335 336 # 取得领头的马. 337 HIGHEST_POS=`cat *position | sort -n | tail -1` 338 339 # 把背景色重设为白色. 340 echo -ne '\E[47m' 341 tput cup 17 0 342 echo -n Current leader: `grep -w $HIGHEST_POS *position | cut -c7`" " 343 344 done 345 346 # 取得赛马结束的时间. 347 FINISH_TIME=`date +%s` 348 349 # 背景色设为绿色并且启用闪动的功能. 350 echo -ne '\E[30;42m' 351 echo -en '\E[5m' 352 353 # 使获胜的马闪动. 354 tput cup `expr $MOVE_HORSE + 5` `cat horse_${MOVE_HORSE}_position | head -1` 355 $DRAW_HORSE 356 357 # 禁用闪动文本. 358 echo -en '\E[25m' 359 360 # 把前景和背景色设为白色. 361 echo -ne '\E[37;47m' 362 move_and_echo 18 1 $BLANK80 363 364 # 前景色设为黑色. 365 echo -ne '\E[30m' 366 367 # 闪动获胜的马. 368 tput cup 17 0 369 echo -e "\E[5mWINNER: $MOVE_HORSE\E[25m"" Odds: `cat odds_${MOVE_HORSE}`"\ 370 " Race time: `expr $FINISH_TIME - $START_TIME` secs" 371 372 # 恢复光标和最初的颜色. 373 echo -en "\E[?25h" 374 echo -en "\E[0m" 375 376 # 恢复回显功能. 377 stty echo 378 379 # 删除赛跑的临时文件. 380 rm -rf $HORSE_RACE_TMP_DIR 381 382 tput cup 19 0 383 384 exit 0 ################################End Script######################################### Example 33-15 返回值技巧 ################################Start Script####################################### #!/bin/bash # multiplication.sh multiply () # 传递乘数. { # 能接受多个参数. local product=1 until [ -z "$1" ] # 直到所有参数都处理完毕... do let "product *= $1" shift done echo $product # 不会打印到标准输出, } #+ 因为要把它赋给一个变量. mult1=15383; mult2=25211 val1=`multiply $mult1 $mult2` echo "$mult1 X $mult2 = $val1" # 387820813 mult1=25; mult2=5; mult3=20 val2=`multiply $mult1 $mult2 $mult3` echo "$mult1 X $mult2 X $mult3 = $val2" # 2500 mult1=188; mult2=37; mult3=25; mult4=47 val3=`multiply $mult1 $mult2 $mult3 $mult4` echo "$mult1 X $mult2 X $mult3 X $mult4 = $val3" # 8173300 exit 0 ################################End Script######################################### Example 33-16 整型还是 string? ################################Start Script####################################### #!/bin/bash # sum-product.sh # 函数可以"返回"多个值. sum_and_product () # 计算所传参数的总和与乘积. { echo $(( $1 + $2 )) $(( $1 * $2 )) # 打印每个计算的值到标准输出,各值用空格分隔开. } echo echo "Enter first number " read first echo echo "Enter second number " read second echo retval=`sum_and_product $first $second` # 把函数的输出赋值给变量. sum=`echo "$retval" | awk '{print $1}'` # 把第一个域的值赋给 sum 变量. product=`echo "$retval" | awk '{print $2}'` # 把第二个域的值赋给 product 变量. echo "$first + $second = $sum" echo "$first * $second = $product" echo exit 0 ################################End Script######################################### Example 33-17 传递和返回数组 ################################Start Script####################################### #!/bin/bash # array-function.sh: 传递一个数组给函数并且... # 从函数"返回"一个数组 Pass_Array () { local passed_array # 局部变量. passed_array=( `echo "$1"` ) echo "${passed_array[@]}" # 列出新数组中的所有元素 #+ 新数组是在函数内声明和赋值的. } original_array=( element1 element2 element3 element4 element5 ) echo echo "original_array = ${original_array[@]}" # 列出最初的数组元素. # 下面是传递数组给函数的技巧. # ********************************** argument=`echo ${original_array[@]}` # ********************************** # 把原数组的所有元素用空格分隔开合成一个字符串并赋给一个变量 # # # 注意:只是把数组本身传给函数是不会工作的. # 下面是允许数组作为"返回值"的技巧. # ***************************************** returned_array=( `Pass_Array "$argument"` ) # ***************************************** # 把函数的输出赋给数组变量. echo "returned_array = ${returned_array[@]}" echo "=============================================================" # 现在,再试一次 Now, try it again, #+ 尝试在函数外存取(列出)数组. Pass_Array "$argument" # 函数本身可以列出数组,但... #+ 函数外存取数组被禁止. echo "Passed array (within function) = ${passed_array[@]}" # 因为变量是函数内的局部变量,所以只有 NULL 值. echo exit 0 ################################End Script######################################### Example 33-18 anagrams 游戏 ################################Start Script####################################### #!/bin/bash # agram.sh: 用 anagrams 玩游戏. # 寻找 anagrams ... LETTERSET=etaoinshrdlu FILTER='.......' # 最小有多少个字母? # 1234567 anagram "$LETTERSET" | # 找出这串字符中所有的 anagrams ... grep "$FILTER" | # 至少 7 个字符, grep '^is' | # 以'is'开头 grep -v 's$' | # 不是复数的(指英文单词复数) grep -v 'ed$' # 不是过去式的(当然也是英文单词) # 可以加许多组合条件和过滤器. # 使用 "anagram" 软件 #+ 它是作者 "yawl" 单词列表软件包的一部分. # http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz # http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz exit 0 # 代码结束. bash$ sh agram.sh islander isolate isolead isotheral # 练习: # --------- # 修改这个脚本使 LETTERSET 能作为命令行参数来接受. # 能够传递参数给第 11 - 13 行的过滤器(就像 $FILTER), #+ 以便能靠传递参数来指定一种功能. # 参考 agram2.sh 了解些微不同的 anagram 的一种方法 # ################################End Script######################################### Example 33-19 在 shell 脚本中调用的窗口部件 ################################Start Script####################################### #!/bin/bash # dialog.sh: 使用 'gdialog' 窗口部件. # 必须在你的系统里安装'gdialog'才能运行此脚本. # 版本 1.1 (04/05/05 修正) # 这个脚本的灵感源自下面的文章. # "Scripting for X Productivity," by Marco Fioretti, # LINUX JOURNAL, Issue 113, September 2003, pp. 86-9. # Thank you, all you good people at LJ. # 在窗口中的输入错误. E_INPUT=65 # 输入窗口显示的尺寸. HEIGHT=50 WIDTH=60 # 输出文件名 (由脚本名构建而来). OUTFILE=$0.output # 把这个脚本的内容显示在窗口中. gdialog --title "Displaying: $0" --textbox $0 $HEIGHT $WIDTH # 现在,保存输入到输出文件中. echo -n "VARIABLE=" > $OUTFILE gdialog --title "User Input" --inputbox "Enter variable, please:" \ $HEIGHT $WIDTH 2>> $OUTFILE if [ "$?" -eq 0 ] # 检查退出状态是一个好习惯. then echo "Executed \"dialog box\" without errors." else echo "Error(s) in \"dialog box\" execution." # 或者, 点击"Cancel", 而不是"OK" 按钮. rm $OUTFILE exit $E_INPUT fi # 现在,我们重新取得并显示保存的变量. . $OUTFILE # 'Source' 保存的文件(即执行). echo "The variable input in the \"input box\" was: "$VARIABLE"" rm $OUTFILE # 清除临时文件. # 有些应用可能需要保留这些文件. exit $? ################################End Script######################################### Example 34-1 字符串扩展 ################################Start Script####################################### #!/bin/bash # 字符串扩展. # Bash 版本 2 引入的特性. # 具有$'xxx'格式的字符串 #+ 将会解释里面的标准的转义字符. echo $'Ringing bell 3 times \a \a \a' # 可能在一些终端只能响铃一次. echo $'Three form feeds \f \f \f' echo $'10 newlines \n\n\n\n\n\n\n\n\n\n' echo $'\102\141\163\150' # Bash # 八进制相等的字符. exit 0 ################################End Script######################################### Example 34-2 间接变量引用 - 新方法 ################################Start Script####################################### #!/bin/bash # 间接变量引用. # 这有点像 C++的引用属性. a=letter_of_alphabet letter_of_alphabet=z echo "a = $a" # 直接引用. echo "Now a = ${!a}" # 间接引用. # ${!variable} 形式比老的"eval var1=\$$var2"更高级 echo t=table_cell_3 table_cell_3=24 echo "t = ${!t}" # t = 24 table_cell_3=387 echo "Value of t changed to ${!t}" # 387 # 这在用来引用数组或表格的成员时非常有用, #+ 或用来模拟多维数组. # 如果有可索引的选项 (类似于指针运算) #+ 会更好. 唉. exit 0 ################################End Script######################################### Example 34-3 使用间接变量引用的简单数据库应用 ################################Start Script####################################### #!/bin/bash # resistor-inventory.sh # 使用间接变量引用的简单数据库应用. # ============================================================== # # 数据 B1723_value=470 # 值 B1723_powerdissip=.25 # 是什么 B1723_colorcode="yellow-violet-brown" # 色彩带宽 B1723_loc=173 # 它们存在哪儿 B1723_inventory=78 # 有多少 B1724_value=1000 B1724_powerdissip=.25 B1724_colorcode="brown-black-red" B1724_loc=24N B1724_inventory=243 B1725_value=10000 B1725_powerdissip=.25 B1725_colorcode="brown-black-orange" B1725_loc=24N B1725_inventory=89 # ============================================================== # echo PS3='Enter catalog number: ' echo select catalog_number in "B1723" "B1724" "B1725" do Inv=${catalog_number}_inventory Val=${catalog_number}_value Pdissip=${catalog_number}_powerdissip Loc=${catalog_number}_loc Ccode=${catalog_number}_colorcode echo echo "Catalog number $catalog_number:" echo "There are ${!Inv} of [${!Val} ohm / ${!Pdissip} watt] resistors in stock." echo "These are located in bin # ${!Loc}." echo "Their color code is \"${!Ccode}\"." break done echo; echo # 练习: # --------- # 1) 重写脚本,使其从外部文件里读数据. # 2) 重写脚本,用数组代替间接变量引用 # # 用数组会更简单明了 # 注: # ----- # Shell 脚本除了最简单的数据应用,其实并不合适数据库应用, #+ 它过多地依赖实际工作的环境和命令. # 写数据库应用更好的还是用一门自然支持数据结构的语言, #+ 如 C++ 或 Java (或甚至是 Perl). exit 0 ################################End Script######################################### Example 34-4 用数组和其他的小技巧来处理四人随机打牌 ################################Start Script####################################### 1 #!/bin/bash 2 3 # Cards: 4 # 处理四人打牌. 5 6 UNPICKED=0 7 PICKED=1 8 9 DUPE_CARD=99 10 11 LOWER_LIMIT=0 12 UPPER_LIMIT=51 13 CARDS_IN_SUIT=13 14 CARDS=52 15 16 declare -a Deck 17 declare -a Suits 18 declare -a Cards 19 # 用一个三维数据来描述数据会更容易实现也更明了一些. 20 # 21 # 可能 Bash 将来的版本会支持多维数组. 22 23 24 initialize_Deck () 25 { 26 i=$LOWER_LIMIT 27 until [ "$i" -gt $UPPER_LIMIT ] 28 do 29 Deck[i]=$UNPICKED # 把整副牌的每张牌都设为没人持牌. 30 let "i += 1" 31 done 32 echo 33 } 34 35 initialize_Suits () 36 { 37 Suits[0]=C #梅花 38 Suits[1]=D #方块 39 Suits[2]=H #红心 40 Suits[3]=S #黑桃 41 } 42 43 initialize_Cards () 44 { 45 Cards=(2 3 4 5 6 7 8 9 10 J Q K A) 46 # 另一种初始化数组的方法. 47 } 48 49 pick_a_card () 50 { 51 card_number=$RANDOM 52 let "card_number %= $CARDS" 53 if [ "${Deck[card_number]}" -eq $UNPICKED ] 54 then 55 Deck[card_number]=$PICKED 56 return $card_number 57 else 58 return $DUPE_CARD 59 fi 60 } 61 62 parse_card () 63 { 64 number=$1 65 let "suit_number = number / CARDS_IN_SUIT" 66 suit=${Suits[suit_number]} 67 echo -n "$suit-" 68 let "card_no = number % CARDS_IN_SUIT" 69 Card=${Cards[card_no]} 70 printf %-4s $Card 71 # 优雅地打印各张牌. 72 } 73 74 seed_random () # 随机产生牌上数值的种子. 75 { # 如果你没有这么做会有什么发生? 76 seed=`eval date +%s` 77 let "seed %= 32766" 78 RANDOM=$seed 79 # 其他的产生随机用的种子的方法还有什么 W? 80 # 81 } 82 83 deal_cards () 84 { 85 echo 86 87 cards_picked=0 88 while [ "$cards_picked" -le $UPPER_LIMIT ] 89 do 90 pick_a_card 91 t=$? 92 93 if [ "$t" -ne $DUPE_CARD ] 94 then 95 parse_card $t 96 97 u=$cards_picked+1 98 # 改回 1 步进的索引(临时的). 为什么? 99 let "u %= $CARDS_IN_SUIT" 100 if [ "$u" -eq 0 ] # 内嵌的 if/then 条件测试. 101 then 102 echo 103 echo 104 fi 105 # Separate hands. 106 107 let "cards_picked += 1" 108 fi 109 done 110 111 echo 112 113 return 0 114 } 115 116 117 # 结构化编程: 118 # 整个程序逻辑模块化. 119 120 #================ 121 seed_random 122 initialize_Deck 123 initialize_Suits 124 initialize_Cards 125 deal_cards 126 #================ 127 128 exit 0 129 130 131 132 # 练习 1: 133 # 把这个脚本完整地做注释. 134 135 # 练习 2: 136 # 增加一个处理例程 (函数) 来以花色排序打印出每个人手中的牌. 137 # 如果你高兴,可增加你喜欢的各种酷的代码. 138 139 # 练习 3: 140 # 简化和理顺脚本的逻辑. ################################End Script#########################################  高效 awk 编程第 3 版 $1 开始使用 AWK  BASH shell set 命令详解  awk 使用手册  gawk 使用手册  脚本实现 vi 前自动备份源文件  关于固定元字符的一个问题  自动扫描 C 段段内主机的 shell  4:变量  自动增加号段脚本  将 MySQL 数据库中数据直接在浏览器  " [ ] "来表示条件测试  一个检查日志文件的脚本  5:参数  一个脚本的多种实现方法  随机播放多个目录下音频文件的 Shel  Linux/Unix 系统通用的 Shell 脚本中  一个获取代理的脚本  超简单的邮件快速登录脚本制作实现
还剩348页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

wyc_cs

贡献于2015-04-20

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf