和"真正的"编程语言一样, Bash也有函数,虽然在某些实现方面稍有些限制. 一个函数是一个子程序,用于实现一串操作的代码块(code block),它是完成特定任务的"黑盒子". 当有重复代码, 当一个任务只需要很少的修改就被重复几次执行时, 这时你应考虑使用函数.
function function_name {
command...
}
function_name () {
command...
}
第二种格式的写法更深得C程序员的喜欢(并且也是更可移植的).
因为在C中,函数的左花括号也可以写在下一行中.
function_name ()
{
command...
}
函数被调用或被触发, 只需要简单地用函数名调用.
例子 23-1. 简单函数
1 #!/bin/bash 2 3 JUST_A_SECOND=1 4 5 funky () 6 { # 这是一个最简单的函数. 7 echo "This is a funky function." 8 echo "Now exiting funky function." 9 } # 函数必须在调用前声明. 10 11 12 fun () 13 { # 一个稍复杂的函数. 14 i=0 15 REPEATS=30 16 17 echo 18 echo "And now the fun really begins." 19 echo 20 21 sleep $JUST_A_SECOND # 嘿, 暂停一秒! 22 while [ $i -lt $REPEATS ] 23 do 24 echo "----------FUNCTIONS---------->" 25 echo "<------------ARE-------------" 26 echo "<------------FUN------------>" 27 echo 28 let "i+=1" 29 done 30 } 31 32 # 现在,调用两个函数. 33 34 funky 35 fun 36 37 exit 0 |
函数定义必须在第一次调用函数前完成.没有像C中的函数“声明”方法.
1 f1 2 # 因为函数"f1"还没有定义,这会引起错误信息. 3 4 declare -f f1 # 这样也没用. 5 f1 # 仍然会引起错误. 6 7 # 然而... 8 9 10 f1 () 11 { 12 echo "Calling function \"f2\" from within function \"f1\"." 13 f2 14 } 15 16 f2 () 17 { 18 echo "Function \"f2\"." 19 } 20 21 f1 # 虽然在它定义前被引用过, 22 #+ 函数"f2"实际到这儿才被调用. 23 # 这样是允许的. 24 25 # Thanks, S.C. |
在一个函数内嵌套另一个函数也是可以的,但是不常用.
1 f1 () 2 { 3 4 f2 () # nested 5 { 6 echo "Function \"f2\", inside \"f1\"." 7 } 8 9 } 10 11 f2 # 引起错误. 12 # 就是你先"declare -f f2"了也没用. 13 14 echo 15 16 f1 # 什么也不做,因为调用"f1"不会自动调用"f2". 17 f2 # 现在,可以正确的调用"f2"了, 18 #+ 因为之前调用"f1"使"f2"在脚本中变得可见了. 19 20 # Thanks, S.C. |
函数声明可以出现在看上去不可能出现的地方,那些不可能的地方本该由一个命令出现的地方.
1 ls -l | foo() { echo "foo"; } # 允许,但没什么用. 2 3 4 5 if [ "$USER" = bozo ] 6 then 7 bozo_greet () # 在if/then结构中定义了函数. 8 { 9 echo "Hello, Bozo." 10 } 11 fi 12 13 bozo_greet # 只能由Bozo运行, 其他用户会引起错误. 14 15 16 17 # 在某些上下文,像这样可能会有用. 18 NO_EXIT=1 # 将会打开下面的函数定义. 19 20 [[ $NO_EXIT -eq 1 ]] && exit() { true; } # 在"and-list"(and列表)中定义函数. 21 # 如果 $NO_EXIT 是 1,声明函数"exit ()". 22 # 把"exit"取别名为"true"将会禁用内建的"exit". 23 24 exit # 调用"exit ()"函数, 而不是内建的"exit". 25 26 # Thanks, S.C. |
函数可以处理传递给它的参数并且能返回它的退出状态码(exit status)给脚本后续使用.
1 function_name $arg1 $arg2 |
函数以位置来引用传递过来的参数(就好像他们是位置参数(positional parameters)), 例如$1, $2,以此类推.
例子 23-2. 带着参数的函数
1 #!/bin/bash 2 # 函数和参数 3 4 DEFAULT=default # 默认的参数值. 5 6 func2 () { 7 if [ -z "$1" ] # 第一个参数是否长度为零? 8 then 9 echo "-Parameter #1 is zero length.-" # 则没有参数传递进来. 10 else 11 echo "-Param #1 is \"$1\".-" 12 fi 13 14 variable=${1-$DEFAULT} # 15 echo "variable = $variable" # 参数替换会表现出什么? 16 # --------------------------- 17 # 它用于分辨没有参数和一个只有NULL值的参数. 18 # 19 20 if [ "$2" ] 21 then 22 echo "-Parameter #2 is \"$2\".-" 23 fi 24 25 return 0 26 } 27 28 echo 29 30 echo "Nothing passed." 31 func2 # 没有参数来调用 32 echo 33 34 35 echo "Zero-length parameter passed." 36 func2 "" # 以一个长度为零的参数调用 37 echo 38 39 echo "Null parameter passed." 40 func2 "$uninitialized_param" # 以未初始化的参数来调用 41 echo 42 43 echo "One parameter passed." 44 func2 first # 用一个参数来调用 45 echo 46 47 echo "Two parameters passed." 48 func2 first second # 以二个参数来调用 49 echo 50 51 echo "\"\" \"second\" passed." 52 func2 "" second # 以第一个参数为零长度,而第二个参数是一个ASCII码组成的字符串来调用. 53 echo # 54 55 exit 0 |
但是,传给脚本的命令行参数怎么办?在函数内部可以看到它们吗?好,让我们来弄清楚.
例子 23-3. 函数和被传给脚本的命令行参数
1 #!/bin/bash 2 # func-cmdlinearg.sh 3 # 以一个命令行参数来调用这个脚本, 4 #+ 类似 $0 arg1来调用. 5 6 7 func () 8 9 { 10 echo "$1" 11 } 12 13 echo "First call to function: no arg passed." 14 echo "See if command-line arg is seen." 15 func 16 # 不!命令行参数看不到. 17 18 echo "============================================================" 19 echo 20 echo "Second call to function: command-line arg passed explicitly." 21 func $1 22 # 现在可以看到了! 23 24 exit 0 |
与别的编程语言相比,shell脚本一般只传递值给函数,变量名(实现上是指针)如果作为参数传递给函数会被看成是字面上字符串的意思。函数解释参数是以字面上的意思来解释的.
间接变量引用(Indirect variable references) (参考例子 34-2)提供了传递变量指针给函数的一个笨拙的机制.
例子 23-4. 传递间接引用给函数
1 #!/bin/bash 2 # ind-func.sh: 传递间接引用给函数. 3 4 echo_var () 5 { 6 echo "$1" 7 } 8 9 message=Hello 10 Hello=Goodbye 11 12 echo_var "$message" # Hello 13 # 现在,让我们传递一个间接引用给函数. 14 echo_var "${!message}" # Goodbye 15 16 echo "-------------" 17 18 # 如果我们改变"hello"变量的值会发生什么? 19 Hello="Hello, again!" 20 echo_var "$message" # Hello 21 echo_var "${!message}" # Hello, again! 22 23 exit 0 |
下一个逻辑问题是:在传递参数给函数之后是否能解除参数的引用.
例子 23-5. 解除传递给函数的参数引用
1 #!/bin/bash 2 # dereference.sh 3 # 给函数传递不同的参数. 4 # Bruce W. Clare编写. 5 6 dereference () 7 { 8 y=\$"$1" # 变量名. 9 echo $y # $Junk 10 11 x=`eval "expr \"$y\" "` 12 echo $1=$x 13 eval "$1=\"Some Different Text \"" # 赋新值. 14 } 15 16 Junk="Some Text" 17 echo $Junk "before" # Some Text before 18 19 dereference Junk 20 echo $Junk "after" # Some Different Text after 21 22 exit 0 |
例子 23-6. 再次尝试解除传递给函数的参数引用
1 #!/bin/bash 2 # ref-params.sh: 解除传递给函数的参数引用. 3 # (复杂例子) 4 5 ITERATIONS=3 # 取得输入的次数. 6 icount=1 7 8 my_read () { 9 # 用my_read varname来调用, 10 #+ 输出用括号括起的先前的值作为默认值, 11 #+ 然后要求输入一个新值. 12 13 local local_var 14 15 echo -n "Enter a value " 16 eval 'echo -n "[$'$1'] "' # 先前的值. 17 # eval echo -n "[\$$1] " # 更好理解, 18 #+ 但会丢失用户输入在尾部的空格. 19 read local_var 20 [ -n "$local_var" ] && eval $1=\$local_var 21 22 # "and列表(And-list)": 如果变量"local_var"测试成功则把变量"$1"的值赋给它. 23 } 24 25 echo 26 27 while [ "$icount" -le "$ITERATIONS" ] 28 do 29 my_read var 30 echo "Entry #$icount = $var" 31 let "icount += 1" 32 echo 33 done 34 35 36 # 多谢Stephane Chazelas提供的示范例子. 37 38 exit 0 |
函数返回一个被称为退出状态的值. 退出状态可以由return来指定statement, 否则函数的退出状态是函数最后一个执行命令的退出状态(0表示成功,非0表示出错代码). 退出状态(exit status)可以在脚本中由$? 引用. 这个机制使脚本函数也可以像C函数一样有一个"返回值".
终止一个函数.return 命令[1]可选地带一个整数参数,这个整数作为函数的"返回值"返回给调用此函数的脚本,并且这个值也被赋给变量$?.
例子 23-7. 两个数中的最大者
1 #!/bin/bash 2 # max.sh: 两个整数中的最大者. 3 4 E_PARAM_ERR=-198 # 如果传给函数的参数少于2个时的返回值. 5 EQUAL=-199 # 如果两个整数值相等的返回值. 6 # 任一个传给函数的参数值溢出 7 # 8 9 max2 () # 返回两个整数的较大值. 10 { # 注意: 参与比较的数必须小于257. 11 if [ -z "$2" ] 12 then 13 return $E_PARAM_ERR 14 fi 15 16 if [ "$1" -eq "$2" ] 17 then 18 return $EQUAL 19 else 20 if [ "$1" -gt "$2" ] 21 then 22 return $1 23 else 24 return $2 25 fi 26 fi 27 } 28 29 max2 33 34 30 return_val=$? 31 32 if [ "$return_val" -eq $E_PARAM_ERR ] 33 then 34 echo "Need to pass two parameters to the function." 35 elif [ "$return_val" -eq $EQUAL ] 36 then 37 echo "The two numbers are equal." 38 else 39 echo "The larger of the two numbers is $return_val." 40 fi 41 42 43 exit 0 44 45 # 练习 (容易): 46 # --------------- 47 # 把这个脚本转化成交互式的脚本, 48 #+ 也就是说,让脚本可以要求调用者输入两个整数. |
为了函数可以返回字符串或是数组,用一个可在函数外可见的变量.
|
例子 23-8. 把数字转化成罗马数字
1 #!/bin/bash 2 3 # 阿拉伯数字转化为罗马数字 4 # 转化范围: 0 - 200 5 # 这是比较粗糙的,但可以工作. 6 7 # 扩展可接受的范围来作为脚本功能的扩充,这个作为练习完成. 8 9 # 用法: roman number-to-convert 10 11 LIMIT=200 12 E_ARG_ERR=65 13 E_OUT_OF_RANGE=66 14 15 if [ -z "$1" ] 16 then 17 echo "Usage: `basename $0` number-to-convert" 18 exit $E_ARG_ERR 19 fi 20 21 num=$1 22 if [ "$num" -gt $LIMIT ] 23 then 24 echo "Out of range!" 25 exit $E_OUT_OF_RANGE 26 fi 27 28 to_roman () # 在第一次调用函数前必须先定义. 29 { 30 number=$1 31 factor=$2 32 rchar=$3 33 let "remainder = number - factor" 34 while [ "$remainder" -ge 0 ] 35 do 36 echo -n $rchar 37 let "number -= factor" 38 let "remainder = number - factor" 39 done 40 41 return $number 42 # 练习: 43 # -------- 44 # 解释这个函数是怎么工作的. 45 # 提示: 靠不断地除来分割数字. 46 } 47 48 49 to_roman $num 100 C 50 num=$? 51 to_roman $num 90 LXXXX 52 num=$? 53 to_roman $num 50 L 54 num=$? 55 to_roman $num 40 XL 56 num=$? 57 to_roman $num 10 X 58 num=$? 59 to_roman $num 9 IX 60 num=$? 61 to_roman $num 5 V 62 num=$? 63 to_roman $num 4 IV 64 num=$? 65 to_roman $num 1 I 66 67 echo 68 69 exit 0 |
也参考例子 10-28.
函数最大可返回的正整数为255. return 命令与退出状态(exit status)的概念联系很紧密,而退出状态的值受此限制。幸运地是有多种(工作区workarounds)来对付这种要求函数返回大整数的情况. 例子 23-9. 测试函数最大的返回值
一种获取大整数的"返回值"的办法是简单地将要返回的值赋给一个全局变量.
更优雅的做法是在函数用 echo 打印"返回值到标准输出",然后使用命令替换(command substitution)捕捉此值. 参考33.7节中这种用法的讨论. 例子 23-10. 比较两个大整数
这是另一个捕捉函数"返回值"的例子. 理解这个例子需要有一些awk的知识.
也参考例子 A-7. 练习: 用我们已经学到的扩展先前罗马数字那个例子脚本能接受任意大的输入. |
函数本质上是一个代码块(code block), 这样意思着它的标准输入可以被重定向(就像在例子 3-1中显示的).
例子 23-11. 用户名的真实名Real name from username
1 #!/bin/bash 2 # realname.sh 3 # 4 # 由用户名而从/etc/passwd取得"真实名". 5 6 7 ARGCOUNT=1 # 需要一个参数. 8 E_WRONGARGS=65 9 10 file=/etc/passwd 11 pattern=$1 12 13 if [ $# -ne "$ARGCOUNT" ] 14 then 15 echo "Usage: `basename $0` USERNAME" 16 exit $E_WRONGARGS 17 fi 18 19 file_excerpt () # 以要求的模式来扫描文件,然后打印文件相关的部分. 20 { 21 while read line # "while" does not necessarily need "[ condition ]" 22 do 23 echo "$line" | grep $1 | awk -F":" '{ print $5 }' # awk指定使用":"为界定符. 24 done 25 } <$file # 重定向函数的标准输入. 26 27 file_excerpt $pattern 28 29 # Yes, this entire script could be reduced to 30 # grep PATTERN /etc/passwd | awk -F":" '{ print $5 }' 31 # or 32 # awk -F: '/PATTERN/ {print $5}' 33 # or 34 # awk -F: '($1 == "username") { print $5 }' # real name from username 35 # 但是,这些可能起不到示例的作用. 36 37 exit 0 |
还有一个办法,可能是更好理解的重定向函数标准输入方法。它为函数内的一个括号内的代码块调用标准输入重定向.
1 # 用下面的代替: 2 Function () 3 { 4 ... 5 } < file 6 7 # 也试一下这个: 8 Function () 9 { 10 { 11 ... 12 } < file 13 } 14 15 # 同样, 16 17 Function () # 可以工作. 18 { 19 { 20 echo $* 21 } | tr a b 22 } 23 24 Function () # 这个不会工作 25 { 26 echo $* 27 } | tr a b # 这儿的内嵌代码块是强制的. 28 29 30 # Thanks, S.C. |
[1] | return命令是Bash内建(builtin)的. |