第二十三章 函数
和真正的编程语言一样,Bash也有函数,虽然在某些实现方面稍有些限制。一个函数是一个子程序,用于实现一串操作的代码块,它是完成特定任务的"黑盒子"。当有重复代码,当一个任务只需要很少的修改就被重复几次执行时,这时应考虑使用函数。
function function_name{
command...
}
#或
function_name () {
command...
}
Example 23-1 简单函数
#!/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
函数定义必须在第一次调用函数前完成。没有像C中的函数"声明"方法。
f1
#这会引起错误,因为函数"f1"还没有定义
declare -f f1
f1
#这样也没办法,仍然会引起错误
f1 ()
{
echo "Calling function \"f2\" from within function \"f1\"."
f2
}
f2 ()
{
echo "Function \"f2\"."
}
#虽然在它定义前被引用过,函数"f2"实际到这儿才被调用.这样是允许的.
在一个函数内嵌套另一个函数也是可以的,但是不常用
f1 ()
{
f2 ()
{
echo "Function \"f2\", inside \"f1\"."
}
}
f2 #引起错误,就是你先"declare -f f2"了也没有。
echo
f1 #什么也不做,因为调用"f1"不会自动调用"f2"
f2 #现在可以正确的调用"f2"了,因为之前调用"f1"使"f2"在脚本中变的可见了。
函数声明可以出现在看上去不可能出现的地方,那些不可能的地方本该由一个命令出现的地方
ls -l | foo() {echo "foo";} #允许,但是没什么用
if [ "$USER" = bozo ];then
bozo_greet() # 在 if/then 结构中定义了函数.
{
echo "Hello, Bozo."
}
fi
bozo_greet # 只能由 Bozo 运行, 其他用户会引起错误.
#在某些上下文,像这样可能会有用
NO_EXIT=1 #将会打开下面的函数定义
[[ $NO_EXIT -eq 1 ]] && exit () {true;}
#如果 $NO_EXIT 是 1,声明函数"exit ()". :把"exit"取别名为"true"将会禁用内建的"exit".
exit # 调用"exit ()"函数, 而不是内建的"exit".
23.1 复杂函数和函数复杂性
函数可以处理传递给它的参数并且能返回它的退出状态码给脚本后续使用。
function_name $arg1 $arg2
函数以位置来引用传递过来的参数(就好像它们是位置参数),例如$1,$2,以此类推。
Example 23-2 带着参数的函数
#!/bin/bash
#
DEFAULT=default
func2 () {
if [ -z "$1" ];then
echo "-Parameter #1 is zero length.-"
else
echo "-Param #1 is \"$1\".-"
fi
variable=${1-$DEFUALT}
echo "variable = $variable"
if [ "$2" ];then
echo "-Parameter #2 is \"$2\".-"
fi
return 0
}
echo
echo "没有参数来调用"
func2
echo
echo "以一个长度为零的参数调用"
func2 ""
echo
echo "以未初始化的参数来调用"
func2 "$uninitialized_param"
echo
echo "用一个参数来调用"
func2 first
echo
echo "以两个参数来调用"
func2 first second
echo
echo "以第一个参数为零长度,而第二个参数是一个 ASCII 码组成的字符串来调用"
func2 "" second
echo
exit 0
注意: shift 命令可以工作在传递给函数的参数 (参考例子 33-15)
但是,传给脚本的命令行参数怎么办?在函数内部可以看到它们吗?
Example 23-3 函数和被传给脚本的命令行参数
#!/bin/bash
#
func (){
echo "$1"
}
echo "对函数的第一次调用:没有传递参数。"
echo "查看是否看到命令行"
func #不!命令行参数看不到
echo
echo "对函数的第二次调用:显式传递了命令行参数。"
func $1 #现在可以看到了!
exit 0
与别的编程语言相比,shell脚本一般只传递值给函数,变量名如果作为参数传递给函数会被看成是字面上字符串的意思。函数解释参数是以字面上的意思来解释的。
间接变量引用(参考例子34-2)提供了传递变量指针给函数的一个笨拙的机制。
Example 23-4 传递间接引用给函数
#!/bin/bash
#
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
下一个逻辑问题是:在传递参数给函数之后是否能解除参数的引用。
Example 23-5 解除传递给函数的参数引用
#!/bin/bash
#
#给函数传递不同的参数
dereference ()
{
y=\$"$1"
echo $y # $Junk
x=`eval "expr \"$y\""`
echo $1=$x # Junk=Some Text
eval "$1=\"Some Different Text\""
}
Junk="Some Text"
echo $Junk "before"
dereference Junk
echo $Junk "after"
exit 0
Example 23-6 再次尝试解除传递给函数的参数引用
#!/bin/bash
#
ITERATIONS=3
icount=1
my_read () {
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
}
echo
while [ "$icount" -le "$ITERATIONS" ]
do
my_read var
echo "Entry #$icount = $var"
let "icount += 1"
echo
done
exit 0
退出和返回
退出状态
函数返回一个被称为退出状态的值。退出状态可以由return来指定statement,否则函数的退出状态是函数最后一个执行命令的退出状态。
return
终止一个函数
return命令可选地带是一个整数参数,这个整数作为函数的"返回值"返回给调用此函数的脚本,并且这个值也被赋给变量$?。
Example 23-7 两个数中的最大者
#!/bin/bash
#
E_PARAM_ERR=198 # 如果传给函数的参数少于 2 个时的返回值.
EQUAL=199 # 如果两个整数值相等的返回值.
max2 ()
{
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 "需要向函数传递两个参数。."
elif [ "$return_val" -eq $EQUAL ];then
echo "这两个数字相等."
else
echo "两个数字中较大的一个是$return_val."
fi
exit 0
注意:为了函数可以返回字符串或是数组,用一个可在函数外可见的变量。
#!/bin/bash
count_lines_in_etc_passwd ()
{
[[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
#echo'好像没有必要,但 . . .它的作用是删除输出中的多余空白字符.
}
if count_lines_in_etc_passwd
then
echo "There are $REPLY lines in /etc/passwd."
else
echo "Cannot count lines in /etc/passwd."
fi
Example 23-8 把数字转化成罗马数字
#!/bin/bash
#
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=$?
echo
exit 0
请参考例子10-28
注意:函数最大可返回的正整数为255.return命令与退出状态的概念联系很紧密,而退出状态的值受此限制。幸运的是有多种来对付这种要求函数返回大整数的情况。
Example 23-9 测试函数最大的返回值
#!/bin/bash
#
return_test ()
{
return $1 #无论传给函数什么都返回它.
}
return_test 27
echo $? #27
return_test 255
echo $? #255
return_test 256
echo $? #0
return_test 257
echo $? #1
#return_test -151896 #2.05b 版本之前的 Bash 是允许超大负整数作为返回值的.
#echo $?
exit 0
如果你非常想使用超大整数作为"返回值"的话,那么只能通过将你想返回的返回值直接的传递到一个全局变量中的手段来达到目的
#!/bin/bash
return_val=
alt_return_test ()
{
fvar=$1
return_val=$fvar
return
}
alt_return_test 1
echo $?
echo "return value = $return_val"
alt_return_test 256
echo "return value = $return_val"
alt_return_test 257
echo "return value = $return_val"
alt_return_test 25702
echo "return value = $return_val"
exit 0
一种更优雅的方法是让函数echo出它的返回值,输出到stdout上,然后通过"命令替换"的手段来捕获它。参考Section 33.7 关于这个问题的讨论。
Example 23-10 比较两个大整数
#!/bin/bash
#
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
}
return_val=$(max2 33001 33997)
#这事实上是一个命令替换的形式:会把这个函数当作一个命令来处理,\
# 并且分配这个函数的 stdout 到变量"return_val"中.
if [ "$return_val" -eq "$E_PARAM_ERR" ];then
echo "传递给比较函数的参数出错!"
elif [ "$return_val" -eq "$EQUAL" ];then
echo "这两个数字相等。"
else
echo "两个数字中较大的一个是$return_val。"
fi
exit 0
下边是获得一个函数的"返回值"的另一个例子。想要了解这个例子需要一些awk的知识
#!/bin/bash
#
month_length()
{
monthD="31 28 31 30 31 30 31 31 30 31 30 31"
echo "$monthD" | awk '{print $'"${1}"'}'
}
#需要错误检查来修正参数的范围(1-12),并且要处理闰年的特殊的 2 月.
#传递参数到内嵌 awk 脚本的模版:$'"${script_parameter}"'
if [ -z "$1" ];then
echo "用法:`basename $0` 1-12."
exit 4
fi
days_in=$(month_length "$1")
echo $days_in
exit 0
重定向
重定向函数的标准输入
函数本质上是一个代码块,这样意味着它的标准输入可以被重定向
Example 23-11 用户名的真实名
#!/bin/bash
#
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
do
echo "$line" | grep $1 | awk -F":" '{print $5}'
done
} < $file #重定向函数的标准输入
file_excerpt $pattern
exit 0
还有一个办法,可能是更好理解的重定向函数标准输入方法。它为函数内的一个括号内的代码块调用标准输入重定向。
#用下面的代替:
Function () {
...
} < file
#也试一下这个:
Function () {
{
...
} < file
}
#同样
Function () { #也会工作
{
echo $*
} | tr a b
}
Function () { #这个不会工作
echo $*
} | tr a b #这里的内嵌代码块是强制的。
23.2 局部变量
如果变量用local来声明,那么它只能在该变量声明的代码块中可见。这个代码块就是局部"范围"。在一个函数内,局部变量意味着只能在函数代码块内它才有意义。
Example 23-12 局部变量的可见范围
#!/bin/bash
#
func ()
{
local loc_var=23 #声明为局部变量
echo
echo "\"local_var\" in function = $loc_var"
global_var=999 #没有声明为局部变量,默认是全局变量
echo "\"global_var\" in function = $global_var"
}
func
echo
echo "\"loc_var\" outside function = $loc_var"
echo "\"global_var\" outside function = $global_var"
echo
exit 0
注意:在函数调用之前,所有在函数内声明且没有明确声明为local的变量都可在函数体外可见。
#!/bin/bash
#
func ()
{
global_var=37 #在函数还没有调用前,变量只能在函数内可见。
}
echo "global_var = $global_var" #函数"func"还没有被调用,所以$global_var还不能被访问
func
echo "global_var = $global_var" #已经在函数调用时设置了值。
23.2.1 局部变量使递归变得可能
局部变量可以递归,但这个办法会产生大量的计算,因此它在shell脚本中是被明确表明不推荐的。
Example 23-13 用局部变量来递归
#!/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
if [ "$number" -eq 0 ];then
factorial=1
else
let "decrnum = number -1"
fact $decrnum #递归调用(函数内部调用自己本身)
let "factorial = $number * $?"
fi
return $factorial
}
fact $1
echo "Factorial of $1 is $?"
exit 0
也请参考例子A-16的脚本递归的例子。必须意识到递归也意味这巨大的资源消耗和缓慢的运行,因此它不适合在脚本中使用。
23.3 不使用局部变量的递归
函数甚至可以不使用局部变量来调用自己
Exampl 23-14 汉诺塔(没看懂)
#!/bin/bash
#
#汉诺塔是由爱德华·卢卡斯提出的数学谜题,他是 19 世纪的法国数学家。
E_NOPARAM=66 # 没有参数传给脚本.
E_BADPARAM=67 # 传给脚本的盘子数不合法.
Moves= # 保存移动次数的全局变量.
dohanoi() { # 递归函数.
case $1 in
0)
;;
*)
dohanoi "$(($1-1))" $2 $4 $3
echo move $2 "-->" $3
let "Moves += 1"
dohanoi "$(($1-1))" $4 $3 $2
;;
esac
}
case $# in
1)
case $(($1>0)) in # 至少要有一个盘子.
1)
dohanoi $1 1 3 2
echo "Total moves = $Moves"
exit 0;
;;
*)
echo "$0: illegal value for number of disks";
exit $E_BADPARAM;
;;
esac
;;
*)
echo "usage: $0 N"
echo " Where \"N\" is the number of disks."
exit $E_NOPARAM;
;;
esac