24.1 复杂函数和函数复杂性

函数可以处理传递给它的参数,并且能返回它的退出状态码给脚本,以便后续处理。

  1. function_name $arg1 $arg2

函数通过位置来引用传递过来的参数(就好像它们是位置参数),例如,$1, $2,等等。

例子 24-2. 带参数的函数

  1. #!/bin/bash
  2. # 函数和参数
  3. DEFAULT=default # 默认参数值。D
  4. func2 () {
  5. if [ -z "$1" ] # 第一个参数长度是否为零?
  6. then
  7. echo "-Parameter #1 is zero length.-" # 或者没有参数传递进来。
  8. else
  9. echo "-Parameter #1 is \"$1\".-"
  10. fi
  11. variable=${1-$DEFAULT}
  12. echo "variable = $variable" # 这里的参数替换
  13. #+ 表示什么?
  14. # ---------------------------
  15. # 为了区分没有参数的情况
  16. #+ 和只有一个null参数的情况。
  17. if [ "$2" ]
  18. then
  19. echo "-Parameter #2 is \"$2\".-"
  20. fi
  21. return 0
  22. }
  23. echo
  24. echo "Nothing passed."
  25. func2 # 不带参数调用
  26. echo
  27. echo "Zero-length parameter passed."
  28. func2 "" # 使用0长度的参数进行调用
  29. echo
  30. echo "Null parameter passed."
  31. func2 "$uninitialized_param" # 使用未初始化的参数进行调用
  32. echo
  33. echo "One parameter passed."
  34. func2 first # 带一个参数的调用
  35. echo
  36. echo "Two parameters passed."
  37. func2 first second # 带两个参数的调用
  38. echo
  39. echo "\"\" \"second\" passed."
  40. func2 "" second # 第一个调用参数为0长度参数,
  41. echo # 第二个是ASCII码的字符串参数。
  42. exit 0

notice 也可以使用shift命令来处理传递给函数的参数(请参考例子 33-18.
但是,传递给脚本的命令行参数怎么办?在函数内部,可以看见这些命令行参数么?好,现在让我们弄清楚这个困惑。

例子 34-3. 函数以及传递给脚本的命令行参数。

  1. #!/bin/bash
  2. # func-cmdlinearg.sh
  3. # 带一个命令行参数来执行这个脚本,
  4. #+ 类似于 $0 arg1.
  5. func ()
  6. {
  7. echo "$1" # 显示传递给这个函数的第一个参数。
  8. } # 命令行参数可以么?
  9. echo "First call to function: no arg passed."
  10. echo "See if command-line arg is seen."
  11. func
  12. # 不! 没有见到命令行参数.
  13. echo "============================================================"
  14. echo
  15. echo "Second call to function: command-line arg passed explicitly."
  16. func $1
  17. # 现在,见到命令行参数了!
  18. exit 0

和其它的编程语言相比,shell脚本一般只会传值给函数。如果把变量名(事实上就是指针)作为参数传递给函数的话,那将被解释为字面含义,也就是被看做字符串。 函数只会以字面含义来解释函数参数。

变量的间接引用(请参考例子 37-2)提供了一种笨拙的机制,来将变量指针传递给函数。

例子 24-4. 将一个间接引用传递给函数

  1. #!/bin/bash
  2. # ind-func.sh: 将一个间接引用传递给函数。
  3. echo_var ()
  4. {
  5. echo "$1"
  6. }
  7. message=Hello
  8. Hello=Goodbye
  9. echo_var "$message" # Hello
  10. # 现在,让我们传递一个间接引用给函数。
  11. echo_var "${!message}" # Goodbye
  12. echo "-------------"
  13. # 如果我们改变“hello”的值会发生什么?
  14. Hello="Hello, again!"
  15. echo_var "$message" # Hello
  16. echo_var "${!message}" # Hello, again!
  17. exit 0

接下来的一个逻辑问题就是,将参数传递给函数之后,参数能否被解除引用。

例子 24-5. 对一个传递给函数的参数进行解除引用的操作

  1. #!/bin/bash
  2. # dereference.sh
  3. # 对一个传递给函数的参数进行解除引用的操作。
  4. # 此脚本由Bruce W. Clare编写.
  5. dereference ()
  6. {
  7. y=\$"$1" # 变量名(而不是值).
  8. echo $y # $Junk
  9. x=`eval "expr \"$y\" "`
  10. echo $1=$x
  11. eval "$1=\"Some Different Text \"" # 赋新值.
  12. }
  13. Junk="Some Text"
  14. echo $Junk "before" # Some Text before
  15. dereference Junk
  16. echo $Junk "after" # Some Different Text after
  17. exit 0

例子 24-6. 再来一次,对一个传递给函数的参数进行解除引用的操作

  1. #!/bin/bash
  2. # ref-params.sh: 解除传递给函数的参数引用。
  3. # (复杂的例子C)
  4. ITERATIONS=3 # 取得输入的次数。
  5. icount=1
  6. my_read () {
  7. # 用my_read varname这种形式来调用,
  8. #+ 将之前用括号括起的值作为默认值输出,
  9. #+ 然后要求输入一个新值.
  10. local local_var
  11. echo -n "Enter a value "
  12. eval 'echo -n "[$'$1'] "' # 之前的值.
  13. # eval echo -n "[\$$1] " # 更容易理解,
  14. #+ 但会丢失用户在尾部输入的空格。
  15. read local_var
  16. [ -n "$local_var" ] && eval $1=\$local_var
  17. # "与列表": 如果 "local_var" 的测试结果为true,则把变量"$1"的值赋给它。
  18. }
  19. echo
  20. while [ "$icount" -le "$ITERATIONS" ]
  21. do
  22. my_read var
  23. echo "Entry #$icount = $var"
  24. let "icount += 1"
  25. echo
  26. done
  27. # 感谢Stephane Chazelas 提供这个例子。
  28. exit 0

退出与返回码

退出状态码

函数返回一个值,被称为退出状态码。这和一条命令返回的退出状态码类似。退出状态码可以由return 命令明确指定,也可以由函数中最后一条命令的退出状态码来指定(如果成功,则返回0,否则返回非0值)。可以在脚本中使用$?来引用退出状态码。 因为有了这种机制,所以脚本函数也可以像C函数一样有“返回值”。

return

终止一个函数。一个return命令1 可选的允许带一个整形参数,这个整形参数将作为函数的“退出状态码”返回给调用这个函数的脚本,并且这个证书也被赋值给变量$?.

例子 24-7. 取两个数中的最大值

  1. #!/bin/bash
  2. # max.sh: 取两个Maximum of two integers.
  3. E_PARAM_ERR=250 # 如果传给函数的参数少于两个时,就返回这个值。
  4. EQUAL=251 # 如果两个参数相等时,就返回这个值。
  5. # 任意超出范围的
  6. #+ 参数值都可能传递到函数中。
  7. max2 () # 返回两个数中的最大值。
  8. { # 注意:参与比较的数必须小于250.
  9. if [ -z "$2" ]
  10. then
  11. return $E_PARAM_ERR
  12. fi
  13. if [ "$1" -eq "$2" ]
  14. then
  15. return $EQUAL
  16. else
  17. if [ "$1" -gt "$2" ]
  18. then
  19. return $1
  20. else
  21. return $2
  22. fi
  23. fi
  24. }
  25. max2 33 34
  26. return_val=$?
  27. if [ "$return_val" -eq $E_PARAM_ERR ]
  28. then
  29. echo "Need to pass two parameters to the function."
  30. elif [ "$return_val" -eq $EQUAL ]
  31. then
  32. echo "The two numbers are equal."
  33. else
  34. echo "The larger of the two numbers is $return_val."
  35. fi
  36. exit 0
  37. # 练习 (easy):
  38. # ---------------
  39. # 把这个脚本转化为交互脚本,
  40. #+ 也就是,修改这个脚本,让其要求调用者输入2个数。

info 为了让函数可以返回字符串或者是数组,可以使用一个在函数外可见的专用全局变量。

  1. count_lines_in_etc_passwd()
  2. {
  3. [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
  4. # 如果 /etc/passwd 可读,让 REPLY 等于 文件的行数.
  5. # 这样就可以同时返回参数值与状态信息。
  6. # 'echo' 看上去没什么用,可是...
  7. #+ 它的作用是删除输出中的多余空白符。
  8. }
  9. if count_lines_in_etc_passwd
  10. then
  11. echo "There are $REPLY lines in /etc/passwd."
  12. else
  13. echo "Cannot count lines in /etc/passwd."
  14. fi
  15. # 感谢, S.C.

例子 24-8. 将阿拉伯数字转化为罗马数字

  1. #!/bin/bash
  2. # 将阿拉伯数字转化为罗马数字。
  3. # 范围:0 - 200
  4. # 比较粗糙,但可以正常工作。
  5. # 扩展范围, 并且完善这个脚本, 作为练习.
  6. # 用法: roman number-to-convert
  7. LIMIT=200
  8. E_ARG_ERR=65
  9. E_OUT_OF_RANGE=66
  10. if [ -z "$1" ]
  11. then
  12. echo "Usage: `basename $0` number-to-convert"
  13. exit $E_ARG_ERR
  14. fi
  15. num=$1
  16. if [ "$num" -gt $LIMIT ]
  17. then
  18. echo "Out of range!"
  19. exit $E_OUT_OF_RANGE
  20. fi
  21. to_roman () # 在第一次调用函数前必须先定义它。
  22. {
  23. number=$1
  24. factor=$2
  25. rchar=$3
  26. let "remainder = number - factor"
  27. while [ "$remainder" -ge 0 ]
  28. do
  29. echo -n $rchar
  30. let "number -= factor"
  31. let "remainder = number - factor"
  32. done
  33. return $number
  34. # 练习:
  35. # ---------
  36. # 1) 解释这个函数如何工作
  37. # 提示: 依靠不断的除,来分割数字。
  38. # 2) 扩展函数的范围:
  39. # 提示: 使用echo和substitution命令.
  40. }
  41. to_roman $num 100 C
  42. num=$?
  43. to_roman $num 90 LXXXX
  44. num=$?
  45. to_roman $num 50 L
  46. num=$?
  47. to_roman $num 40 XL
  48. num=$?
  49. to_roman $num 10 X
  50. num=$?
  51. to_roman $num 9 IX
  52. num=$?
  53. to_roman $num 5 V
  54. num=$?
  55. to_roman $num 4 IV
  56. num=$?
  57. to_roman $num 1 I
  58. # 成功调用了转换函数。
  59. # 这真的是必须的么? 这个可以简化么?
  60. echo
  61. exit

也可以参见例子 11-29

notice 函数所能返回最大的正整数是255. return命令和退出状态码的概念紧密联系在一起,并且退出状态码的值受此限制。 幸运的是,如果想让函数返回大整数的话,有好多种不同的变通方法 能够应对这个情况。

例子24-9. 测试函数最大的返回值

  1. #!/bin/bash
  2. # return-test.sh
  3. # 函数所能返回的最大正整数为255.
  4. return_test () # 传给函数什么值,就返回什么值。
  5. {
  6. return $1
  7. }
  8. return_test 27 # o.k.
  9. echo $? # 返回27.
  10. return_test 255 # Still o.k.
  11. echo $? # 返回 255.
  12. return_test 257 # 错误!
  13. echo $? # 返回 1 (对应各种错误的返回码).
  14. # =========================================================
  15. return_test -151896 # 能返回一个大负数么?
  16. echo $? # 能否返回 -151896?
  17. # 不行! 返回的是 168.
  18. # Bash 2.05b 之前的版本
  19. #+ 允许返回大负数。
  20. # 这可能是个有用的特性。
  21. # Bash之后的新版本修正了这个漏洞。
  22. # 这可能会影响以前所编写的脚本。
  23. # 一定要小心!
  24. # =========================================================
  25. exit 0

如果你想获得大整数“返回值”的话,简单的方法就是将“要返回的值”保存到一个全局变量中。

  1. Return_Val= # 用于保存函数特大返回值的全局变量。
  2. alt_return_test ()
  3. {
  4. fvar=$1
  5. Return_Val=$fvar
  6. return # 返回 0 (成功).
  7. }
  8. alt_return_test 1
  9. echo $? #0
  10. echo "return value = $Return_Val" #1
  11. alt_return_test 256
  12. echo "return value = $Return_Val" # 256
  13. alt_return_test 257
  14. echo "return value = $Return_Val" # 257
  15. alt_return_test 25701
  16. echo "return value = $Return_Val" #25701

一种更优雅的做法是在函数中使用echo命令将”返回值输出到stdout“,然后用命令替换来捕捉此值。请参考36.7小节关于这种用法的讨论

例子 24-10. 比较两个大整数

  1. #!/bin/bash
  2. # max2.sh: 取两个大整数中的最大值。
  3. # 这是前一个例子 "max.sh" 的修改版,
  4. #+ 这个版本可以比较两个大整数。
  5. EQUAL=0 # 如果两个值相等,那就返回这个值。
  6. E_PARAM_ERR=-99999 # 没有足够多的参数,那就返回这个值。
  7. # ^^^^^^ 任意超出范围的参数都可以传递进来。
  8. max2 () # "返回" 两个整数中最大的那个。
  9. {
  10. if [ -z "$2" ]
  11. then
  12. echo $E_PARAM_ERR
  13. return
  14. fi
  15. if [ "$1" -eq "$2" ]
  16. then
  17. echo $EQUAL
  18. return
  19. else
  20. if [ "$1" -gt "$2" ]
  21. then
  22. retval=$1
  23. else
  24. retval=$2
  25. fi
  26. fi
  27. echo $retval # 输出 (到 stdout), 而没有用返回值。
  28. # 为什么?
  29. }
  30. return_val=$(max2 33001 33997)
  31. # ^^^^ 函数名
  32. # ^^^^^ ^^^^^ 传递进来的参数
  33. # 这其实是命令替换的一种形式:
  34. #+ 可以把函数看作一个命令,
  35. #+ 然后把函数的stdout赋值给变量“return_val".
  36. # ========================= OUTPUT ========================
  37. if [ "$return_val" -eq "$E_PARAM_ERR" ]
  38. then
  39. echo "Error in parameters passed to comparison function!"
  40. elif [ "$return_val" -eq "$EQUAL" ]
  41. then
  42. echo "The two numbers are equal."
  43. else
  44. echo "The larger of the two numbers is $return_val."
  45. fi
  46. # =========================================================
  47. exit 0
  48. # 练习:
  49. # ---------
  50. # 1) 找到一种更优雅的方法,
  51. #+ 去测试传递给函数的参数。
  52. # 2) 简化”输出“段的if/then结构。
  53. # 3) 重写这个脚本,使其能够从命令行参数中获得输入。

这是另一个能够捕捉函数”返回值“的例子。要想搞明白这个例子,需要一些awk的知识。

  1. month_length () # 把月份作为参数。
  2. { # 返回该月包含的天数。
  3. monthD="31 28 31 30 31 30 31 31 30 31 30 31" # 作为局部变量声明?
  4. echo "$monthD" | awk '{ print $'"${1}"' }' # 小技巧.
  5. # ^^^^^^^^^
  6. # 传递给函数的参数 ($1 -- 月份), 然后传给 awk.
  7. # Awk 把参数解释为"print $1 . . . print $12" (这依赖于月份号)
  8. # 这是一个模板,用于将参数传递给内嵌awk的脚本:
  9. # $'"${script_parameter}"'
  10. # 这里是一个简单的awk结构:
  11. # echo $monthD | awk -v month=$1 '{print $(month)}'
  12. # 使用awk的-v选项,可以把一个变量值赋给
  13. #+ awk程序块的执行体。
  14. # 感谢 Rich.
  15. # 需要做一些错误检查,来保证月份好正确,在范围(1-12)之间,
  16. #+ 别忘了检查闰年的二月。
  17. }
  18. # ----------------------------------------------
  19. # 用例:
  20. month=4 # 以四月为例。
  21. days_in=$(month_length $month)
  22. echo $days_in # 30
  23. # ----------------------------------------------

也请参考例子 A-7例子A-37.

练习:使用目前我们已经学到的知识,来扩展之前的例子 将阿拉伯数字转化为罗马数字,让它能够接受任意大的输入。

重定向
重定向函数的stdin
函数本质上其实就是一个代码块,这就意味着它的stdin可以被重定向(比如例子3-1)。

例子 24-11. 从username中取得用户的真名

  1. #!/bin/bash
  2. # realname.sh
  3. #
  4. # 依靠username,从/etc/passwd 中获得“真名”.
  5. ARGCOUNT=1 # 需要一个参数.
  6. E_WRONGARGS=85
  7. file=/etc/passwd
  8. pattern=$1
  9. if [ $# -ne "$ARGCOUNT" ]
  10. then
  11. echo "Usage: `basename $0` USERNAME"
  12. exit $E_WRONGARGS
  13. fi
  14. file_excerpt () # 按照要求的模式来扫描文件,
  15. { #+ 然后打印文件的相关部分。
  16. while read line # "while" 并不一定非得有 [ 条件 ] 不可。
  17. do
  18. echo "$line" | grep $1 | awk -F":" '{ print $5 }'
  19. # awk用":" 作为界定符。
  20. done
  21. } <$file # 重定向到函数的stdin。
  22. file_excerpt $pattern
  23. # 是的,整个脚本其实可以被缩减为
  24. # grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
  25. # or
  26. # awk -F: '/PATTERN/ {print $5}'
  27. # or
  28. # awk -F: '($1 == "username") { print $5 }' # 从username中获取真名
  29. # 但是,这些起不到示例的作用。
  30. exit 0

还有一个办法,或许能够更好的理解重定向函数的stdin。 它在函数内添加了一对大括号,并且将重定向stdin的行为放在这对添加的大括号上。

  1. # 用下面的方法来代替它:
  2. Function ()
  3. {
  4. ...
  5. } < file
  6. # 试试这个:
  7. Function ()
  8. {
  9. {
  10. ...
  11. } < file
  12. }
  13. # 同样的,
  14. Function () # 没问题.
  15. {
  16. {
  17. echo $*
  18. } | tr a b
  19. }
  20. Function () # 不行.
  21. {
  22. echo $*
  23. } | tr a b # 这儿的内嵌代码块是被强制的。
  24. # 感谢, S.C.

extra Emmanuel Rouat的 sample bash 文件包含了一些很有指导性意义的函数例子。