33.7. 各种小技巧
为了记录在一个实际的会话期或多个会话期内运行的用户脚本,可以加下面的代码到每个你想追踪记录的脚本里. 这会记录下连续的脚本名记录和调用的次数.
1 # 添加(>>)下面几行到你想追踪记录的脚本末尾处. 2 3 whoami>> $SAVE_FILE # 记录调用脚本的用户. 4 echo $0>> $SAVE_FILE # 记录脚本名. 5 date>> $SAVE_FILE # 记录日期和时间. 6 echo>> $SAVE_FILE # 空行作为分隔行. 7 8 # 当然, SAVE_FILE 变量应在~/.bashrc中定义并导出(export) 9 #+ (变量值类似如 ~/.scripts-run)
The>>操作符可以在文件尾添加内容. 如果你想在文件头添加内容,那应该怎么办?
1 file=data.txt 2 title="***This is the title line of data text file***" 3 4 echo $title | cat - $file >$file.new 5 # "cat -" 连接标准输出的内容和$file的内容. 6 # 最后的结果就是生成了一个新文件, 7 #+ 文件的头添加了 $title 的值,后跟$file的内容.
这是早先例子 17-13中的简化变体. 当然, , sed也可以办到.
脚本也可以像内嵌到另一个shell脚本的普通命令一样调用, 如 Tcl或wish脚本, 甚至可以是Makefile. 它们可以作为外部shell命令用C语言的system()函数调用, 例如., system("script_name");.
把内嵌的 sed或 awk脚本的内容赋值给一个变量可以增加包装脚本(shell wrapper)的可读性. 参考 例子 A-1和 例子 11-18.
把你最喜欢和最有用的定义和函数放在一些文件中. 当需要的使用的时候, 在脚本中使用dot(.) 或 source命令来"包含(include)"这些"库文件"的一个或多个.
1 # 脚本库 2 # ------ ------- 3 4 # 注: 5 # 本文件没有"#!"开头. 6 # 也没有真正做执行动作的代码. 7 8 9 # 有用的变量定义 10 11 ROOT_UID=0 # Root用户的 $UID 值是0. 12 E_NOTROOT=101 # 非root用户出错代码. 13 MAXRETVAL=255 # 函数最大的的返回值(正值). 14 SUCCESS=0 15 FAILURE=-1 16 17 18 19 # 函数 20 21 Usage () # "Usage:" 信息(即帮助信息). 22 { 23 if [ -z "$1" ] # 没有传递参数. 24 then 25 msg=filename 26 else 27 msg=$@ 28 fi 29 30 echo "Usage: `basename $0` "$msg"" 31 } 32 33 34 Check_if_root () # 检查是不是root在运行脚本. 35 { # 取自例子"ex39.sh". 36 if [ "$UID" -ne "$ROOT_UID" ] 37 then 38 echo "Must be root to run this script." 39 exit $E_NOTROOT 40 fi 41 } 42 43 44 CreateTempfileName () # 创建一个"唯一"的临时文件. 45 { # 取自例子"ex51.sh". 46 prefix=temp 47 suffix=`eval date +%s` 48 Tempfilename=$prefix.$suffix 49 } 50 51 52 isalpha2 () # 测试字符串是不是都是字母组成的. 53 { # 取自例子"isalpha.sh". 54 [ $# -eq 1 ] || return $FAILURE 55 56 case $1 in 57 *[!a-zA-Z]*|"") return $FAILURE;; 58 *) return $SUCCESS;; 59 esac # Thanks, S.C. 60 } 61 62 63 abs () # 绝对值. 64 { # 注意: 最大的返回值 = 255. 65 E_ARGERR=-999999 66 67 if [ -z "$1" ] # 要传递参数. 68 then 69 return $E_ARGERR # 返回错误. 70 fi 71 72 if [ "$1" -ge 0 ] # 如果非负的值, 73 then # 74 absval=$1 # 绝对值是本身. 75 else # 否则, 76 let "absval = (( 0 - $1 ))" # 改变它的符号. 77 fi 78 79 return $absval 80 } 81 82 83 tolower () # 把传递的字符串转为小写 84 { # 85 86 if [ -z "$1" ] # 如果没有传递参数, 87 then #+ 打印错误信息 88 echo "(null)" #+ (C风格的void指针的错误信息) 89 return #+ 然后从函数中返回. 90 fi 91 92 echo "$@" | tr A-Z a-z 93 # 转换传递过来的所有参数($@). 94 95 return 96 97 # 用命令替换功能把函数的输出赋给变量. 98 # 例如: 99 # oldvar="A seT of miXed-caSe LEtTerS" 100 # newvar=`tolower "$oldvar"` 101 # echo "$newvar" # 一串混合大小写的字符转换成了全部小写字符 102 # 103 # 练习: 重写这个函数把传递的参数变为大写 104 # ... toupper() [容易]. 105 }
在脚本中添加特殊种类的注释开头标识有助于条理清晰和可读性.
1 ## 表示注意. 2 rm -rf *.zzy ## "rm"命令的"-rf"组合选项非常的危险, 3 ##+ 尤其是对通配符而言. 4 5 #+ 表示继续上一行. 6 # 这是第一行 7 #+ 这是多行的注释, 8 #+ 这里是最后一行. 9 10 #* 表示标注. 11 12 #o 表示列表项. 13 14 #> 表示另一个观点. 15 while [ "$var1" != "end" ] #> while test "$var1" != "end"
if-test结构的一种聪明用法是用来注释一块代码块.
1 #!/bin/bash 2 3 COMMENT_BLOCK= 4 # 给上面的变量设置某个值就会产生讨厌的结果 5 # 6 7 if [ $COMMENT_BLOCK ]; then 8 9 Comment block -- 10 ================================= 11 This is a comment line. 12 This is another comment line. 13 This is yet another comment line. 14 ================================= 15 16 echo "This will not echo." 17 18 Comment blocks are error-free! Whee! 19 20 fi 21 22 echo "No more comments, please." 23 24 exit 0
把这种方法和使用here documents来注释代码块作一个比较.
测试$? 退出状态变量, 因为一个脚本可能想要测试一个参数是否只包含数字,以便后面可以把它当作一个整数.
1 #!/bin/bash 2 3 SUCCESS=0 4 E_BADINPUT=65 5 6 test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null 7 # 整数要么等于零要么不等于零. 8 # 2>/dev/null 可以抑制错误信息. 9 10 if [ $? -ne "$SUCCESS" ] 11 then 12 echo "Usage: `basename $0` integer-input" 13 exit $E_BADINPUT 14 fi 15 16 let "sum = $1 + 25" # 如果$1不是整数就会产生错误. 17 echo "Sum = $sum" 18 19 # 任何变量,而不仅仅命令行参数可用这种方法来测试. 20 21 exit 0
0 - 255 范围的函数返回值是个严格的限制. 用全局变量和其他方法常常出问题. 函数内返回值给脚本主体的另一个办法是让函数写值到标准输出(通常是用echo) 作为"返回值",并且将其赋给一个变量. 这实际是命令替换(command substitution)的变体.
例子 33-15. 返回值技巧
1 #!/bin/bash 2 # multiplication.sh 3 4 multiply () # 传递乘数. 5 { # 能接受多个参数. 6 7 local product=1 8 9 until [ -z "$1" ] # 直到所有参数都处理完毕... 10 do 11 let "product *= $1" 12 shift 13 done 14 15 echo $product # 不会打印到标准输出, 16 } #+ 因为要把它赋给一个变量. 17 18 mult1=15383; mult2=25211 19 val1=`multiply $mult1 $mult2` 20 echo "$mult1 X $mult2 = $val1" 21 # 387820813 22 23 mult1=25; mult2=5; mult3=20 24 val2=`multiply $mult1 $mult2 $mult3` 25 echo "$mult1 X $mult2 X $mult3 = $val2" 26 # 2500 27 28 mult1=188; mult2=37; mult3=25; mult4=47 29 val3=`multiply $mult1 $mult2 $mult3 $mult4` 30 echo "$mult1 X $mult2 X $mult3 X $mult4 = $val3" 31 # 8173300 32 33 exit 0
相同的技术也可用在字符串中. 这意味着函数可以"返回"一个非数字的值.
1 capitalize_ichar () # 把传递来的参数字符串的第一个字母大写 2 { # 3 4 string0="$@" # 能接受多个参数. 5 6 firstchar=${string0:0:1} # 第一个字符. 7 string1=${string0:1} # 余下的字符. 8 9 FirstChar=`echo "$firstchar" | tr a-z A-Z` 10 # 第一个字符转换成大写字符. 11 12 echo "$FirstChar$string1" # 打印到标准输出. 13 14 } 15 16 newstring=`capitalize_ichar "every sentence should start with a capital letter."` 17 echo "$newstring" # Every sentence should start with a capital letter.
用这种办法甚至可能"返回"多个值.
例子 33-16. 返回多个值的技巧
1 #!/bin/bash 2 # sum-product.sh 3 # 函数可以"返回"多个值. 4 5 sum_and_product () # 计算所传参数的总和与乘积. 6 { 7 echo $(( $1 + $2 )) $(( $1 * $2 )) 8 # 打印每个计算的值到标准输出,各值用空格分隔开. 9 } 10 11 echo 12 echo "Enter first number " 13 read first 14 15 echo 16 echo "Enter second number " 17 read second 18 echo 19 20 retval=`sum_and_product $first $second` # 把函数的输出赋值给变量. 21 sum=`echo "$retval" | awk '{print $1}'` # 把第一个域的值赋给sum变量. 22 product=`echo "$retval" | awk '{print $2}'` # 把第二个域的值赋给product变量. 23 24 echo "$first + $second = $sum" 25 echo "$first * $second = $product" 26 echo 27 28 exit 0
下一个技巧是传递数组给函数的技术, 然后 "返回"一个数组给脚本.
用 变量替换(command substitution)把数组的所有元素用空格分隔开来并赋给一个变量就可以实现给函数传递数组. 用先前介绍的方法函数内echo一个数组并"返回此值",然后调用命令替换用( ... )操作符赋值给一个数组.
例子 33-17. 传递和返回数组
1 #!/bin/bash 2 # array-function.sh: 传递一个数组给函数并且... 3 # 从函数"返回"一个数组 4 5 6 Pass_Array () 7 { 8 local passed_array # 局部变量. 9 passed_array=( `echo "$1"` ) 10 echo "${passed_array[@]}" 11 # 列出新数组中的所有元素 12 #+ 新数组是在函数内声明和赋值的. 13 } 14 15 16 original_array=( element1 element2 element3 element4 element5 ) 17 18 echo 19 echo "original_array = ${original_array[@]}" 20 # 列出最初的数组元素. 21 22 23 # 下面是传递数组给函数的技巧. 24 # ********************************** 25 argument=`echo ${original_array[@]}` 26 # ********************************** 27 # 把原数组的所有元素用空格分隔开合成一个字符串并赋给一个变量 28 # 29 # 30 # 注意:只是把数组本身传给函数是不会工作的. 31 32 33 # 下面是允许数组作为"返回值"的技巧. 34 # ***************************************** 35 returned_array=( `Pass_Array "$argument"` ) 36 # ***************************************** 37 # 把函数的输出赋给数组变量. 38 39 echo "returned_array = ${returned_array[@]}" 40 41 echo "=============================================================" 42 43 # 现在,再试一次Now, try it again, 44 #+ 尝试在函数外存取(列出)数组. 45 Pass_Array "$argument" 46 47 # 函数本身可以列出数组,但... 48 #+ 函数外存取数组被禁止. 49 echo "Passed array (within function) = ${passed_array[@]}" 50 # 因为变量是函数内的局部变量,所以只有NULL值. 51 52 echo 53 54 exit 0
在例子 A-10中有一个更精心制作的给函数传递数组的例子.
利用双括号结构,使在for和 while循环中可以使用C风格的语法来设置和增加变量. 参考例子 10-12和 例子 10-17.
在脚本开头设置 path和 umask增加脚本的"可移植性" -- 在某些把 $PATH和 umask弄乱的系统里也可以运行.
1 #!/bin/bash 2 PATH=/bin:/usr/bin:/usr/local/bin ; export PATH 3 umask 022 # 脚本的创建的文件有 755 的权限设置. 4 5 # 多谢Ian D. Allen提出这个技巧.
一个有用的脚本技术是:重复地把一个过滤器的输出回馈(用管道)给另一个相同过滤器,但过滤器有不同的参数和/或选项. 尤其对 tr和 grep更合适.
1 # 取自例子"wstrings.sh". 2 3 wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \ 4 tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`
例子 33-18. anagrams游戏
1 #!/bin/bash 2 # agram.sh: 用anagrams玩游戏. 3 4 # 寻找 anagrams ... 5 LETTERSET=etaoinshrdlu 6 FILTER='.......' # 最小有多少个字母? 7 # 1234567 8 9 anagram "$LETTERSET" | # 找出这串字符中所有的 anagrams ... 10 grep "$FILTER" | # 至少7个字符, 11 grep '^is' | # 以'is'开头 12 grep -v 's$' | # 不是复数的(指英文单词复数) 13 grep -v 'ed$' # 不是过去式的(当然也是英文单词) 14 # 可以加许多组合条件和过滤器. 15 16 # 使用 "anagram" 软件 17 #+ 它是作者 "yawl" 单词列表软件包的一部分. 18 # http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz 19 # http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz 20 21 exit 0 # 代码结束. 22 23 24 bash$ sh agram.sh 25 islander 26 isolate 27 isolead 28 isotheral 29 30 31 32 # 练习: 33 # --------- 34 # 修改这个脚本使 LETTERSET 能作为命令行参数来接受. 35 # 能够传递参数给第 11 - 13 行的过滤器(就像 $FILTER), 36 #+ 以便能靠传递参数来指定一种功能. 37 38 # 参考agram2.sh了解些微不同的anagram的一种方法 39 #
See also Example 27-3, Example 12-22, and Example A-9.
使用"匿名的 here documents"来注释代码块,这样避免了对代码块的每一块单独用#来注释了. 参考例子 17-11.
当依赖某个命令脚本在一台没有安装该命令的机器上运行时会出错. 使用 whatis命令可以避免此问题.
1 CMD=command1 # 第一选择First choice. 2 PlanB=command2 # 第二选择Fallback option. 3 4 command_test=$(whatis "$CMD" | grep 'nothing appropriate') 5 # 如果'command1'没有在系统里发现 , 'whatis'会返回: 6 #+ "command1: nothing appropriate." 7 # 8 # 另一种更安全的办法是: 9 # command_test=$(whereis "$CMD" | grep \/) 10 # 但后面的测试判断应该翻转过来, 11 #+ 因为$command_test只有当系统存在$CMD命令时才有内容. 12 # 13 # (Thanks, bojster.) 14 15 16 if [[ -z "$command_test" ]] # 检查命令是否存在. 17 then 18 $CMD option1 option2 # 调用command1. 19 else # 否则, 20 $PlanB #+ 调用command2. 21 fi
在发生错误的情况下 if-grep test可能不会返回期望的结果,因为文本是打印在标准出错而不是标准输出上.
1 if ls -l nonexistent_filename | grep -q 'No such file or directory' 2 then echo "File \"nonexistent_filename\" does not exist." 3 fi
把标准出错重定向到标准输出上可以修改这个.
1 if ls -l nonexistent_filename 2>&1 | grep -q 'No such file or directory' 2 # ^^^^ 3 then echo "File \"nonexistent_filename\" does not exist." 4 fi 5 6 # 多谢Chris Martin指出.
The run-parts命令很容易依次运行一组命令脚本,特别是和 cron或 at组合起来.
在shell脚本里能调用 X-Windows 的窗口小部件将多么美好. 已经存在有几种工具包实现这个了, 它们称为Xscript, Xmenu, 和widtools. 头两个已经不再维护. 幸运地是仍然可以从这儿下载widtools.
widtools(widget tools) 工具包要求安装了 XForms库. 另外, 它的 Makefile在典型的Linux系统上安装前需要做一些合适的编辑. 最后, 提供的6个部件有3个不能工作 (事实上会发生段错误).
dialog工具集提供了shell脚本使用一种称为"对话框"的窗口部件. 原始的 dialog软件包工作在文本模式的控制台下, 但它的后续软件 gdialog, Xdialog, 和 kdialog使用基于X-Windows的窗口小部件集.
例子 33-19. 在shell脚本中调用的窗口部件
1 #!/bin/bash 2 # dialog.sh: 使用 'gdialog' 窗口部件. 3 # 必须在你的系统里安装'gdialog'才能运行此脚本. 4 # 版本 1.1 (04/05/05 修正) 5 6 # 这个脚本的灵感源自下面的文章. 7 # "Scripting for X Productivity," by Marco Fioretti, 8 # LINUX JOURNAL, Issue 113, September 2003, pp. 86-9. 9 # Thank you, all you good people at LJ. 10 11 12 # 在窗口中的输入错误. 13 E_INPUT=65 14 # 输入窗口显示的尺寸. 15 HEIGHT=50 16 WIDTH=60 17 18 # 输出文件名 (由脚本名构建而来). 19 OUTFILE=$0.output 20 21 # 把这个脚本的内容显示在窗口中. 22 gdialog --title "Displaying: $0" --textbox $0 $HEIGHT $WIDTH 23 24 25 26 # 现在,保存输入到输出文件中. 27 echo -n "VARIABLE=" > $OUTFILE 28 gdialog --title "User Input" --inputbox "Enter variable, please:" \ 29 $HEIGHT $WIDTH 2>> $OUTFILE 30 31 32 if [ "$?" -eq 0 ] 33 # 检查退出状态是一个好习惯. 34 then 35 echo "Executed \"dialog box\" without errors." 36 else 37 echo "Error(s) in \"dialog box\" execution." 38 # 或者, 点击"Cancel", 而不是"OK" 按钮. 39 rm $OUTFILE 40 exit $E_INPUT 41 fi 42 43 44 45 # 现在,我们重新取得并显示保存的变量. 46 . $OUTFILE # 'Source' 保存的文件(即执行). 47 echo "The variable input in the \"input box\" was: "$VARIABLE"" 48 49 50 rm $OUTFILE # 清除临时文件. 51 # 有些应用可能需要保留这些文件. 52 53 exit $?
其他的在脚本中使用窗口的工具还有 Tk或 wish(Tcl派生物), PerlTk(Perl 的 Tk 扩展), tksh(ksh 的 Tk 扩展), XForms4Perl(Perl 的 XForms 扩展), Gtk-Perl(Perl 的 Gtk 扩展), 或 PyQt(Python 的 Qt 扩展).
为了对复杂的脚本做多次的版本修订管理, 可以使用rcs软件包.
使用这个软件包的好处之一是会自动地升级ID头标识.在 rcs的co命令处理一些预定义的关键字参数替换,例如,代替脚本里头#$Id$的,如类似下面的行:
1 #$Id: hello-world.sh,v 1.1 2004/10/16 02:43:05 bozo Exp $