shell教程

第23章. 函数

和"真正的"编程语言一样,Bash也有函数,虽然在某些实现方面稍有些限制.一个函数是一个子程序,用于实现一串操作的代码块(codeblock),它是完成特定任务的"黑盒子".当有重复代码,当一个任务只需要很少的修改就被重复几次执行时,这时你应考虑使用函数.functionfunction_name{command...}或function_name(){command...}第二种格式的写法更深得

"真正的"编程语言一样, Bash也有函数,虽然在某些实现方面稍有些限制. 一个函数是一个子程序,用于实现一串操作的代码块(code block),它是完成特定任务的"黑盒子". 当有重复代码, 当一个任务只需要很少的修改就被重复几次执行时, 这时你应考虑使用函数.

function function_name {
command...
}

function_name () {
command...
}

第二种格式的写法更深得C程序员的喜欢(并且也是更可移植的).

因为在C中,函数的左花括号也可以写在下一行中.

function_name ()
{
command...
}

函数被调用或被触发, 只需要简单地用函数名调用.


例子 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\"."
}

f1  #  虽然在它定义前被引用过,
    #+ 函数"f2"实际到这儿才被调用.
    #  这样是允许的.

    # Thanks, S.C.

在一个函数内嵌套另一个函数也是可以的,但是不常用.

f1 ()
{

  f2 () # nested
  {
    echo "Function \"f2\", inside \"f1\"."
  }

}

f2  #  引起错误.
    #  就是你先"declare -f f2"了也没用.

echo

f1  #  什么也不做,因为调用"f1"不会自动调用"f2".
f2  #  现在,可以正确的调用"f2"了,
    #+ 因为之前调用"f1"使"f2"在脚本中变得可见了.

    # Thanks, S.C.

函数声明可以出现在看上去不可能出现的地方,那些不可能的地方本该由一个命令出现的地方.

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; }     # 在"and-list"(and列表)中定义函数.
# 如果 $NO_EXIT 是 1,声明函数"exit ()".
# 把"exit"取别名为"true"将会禁用内建的"exit".

exit  # 调用"exit ()"函数, 而不是内建的"exit".

# Thanks, S.C.

函数可以处理传递给它的参数并且能返回它的退出状态码(exit status)给脚本后续使用.

function_name $arg1 $arg2

函数以位置来引用传递过来的参数(就好像他们是位置参数(positional parameters)), 例如$1, $2,以此类推.


例子 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-$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

shift命令可以工作在传递给函数的参数 (参考例子 33-15).

但是,传给脚本的命令行参数怎么办?在函数内部可以看到它们吗?好,让我们来弄清楚.


例子 23-3. 函数和被传给脚本的命令行参数

#!/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

与别的编程语言相比,shell脚本一般只传递值给函数,变量名(实现上是指针)如果作为参数传递给函数会被看成是字面上字符串的意思。函数解释参数是以字面上的意思来解释的.

间接变量引用(Indirect variable references) (参考例子 34-2)提供了传递变量指针给函数的一个笨拙的机制.


例子 23-4. 传递间接引用给函数

#!/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

下一个逻辑问题是:在传递参数给函数之后是否能解除参数的引用.


例子 23-5. 解除传递给函数的参数引用

#!/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


例子 23-6. 再次尝试解除传递给函数的参数引用

#!/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

退出和返回

退出状态(exit status)

函数返回一个被称为退出状态的值. 退出状态可以由return来指定statement, 否则函数的退出状态是函数最后一个执行命令的退出状态(0表示成功,非0表示出错代码). 退出状态(exit status)可以在脚本中由$? 引用. 这个机制使脚本函数也可以像C函数一样有一个"返回值".

return

终止一个函数.return 命令[1]可选地带一个整数参数,这个整数作为函数的"返回值"返回给调用此函数的脚本,并且这个值也被赋给变量$?.


例子 23-7. 两个数中的最大者

#!/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

#  练习 (容易):
#  ---------------
#  把这个脚本转化成交互式的脚本,
#+ 也就是说,让脚本可以要求调用者输入两个整数.

为了函数可以返回字符串或是数组,用一个可在函数外可见的变量.

count_lines_in_etc_passwd()
{
  [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
  #  如果/etc/passwd可读,则把REPLY设置成文件的行数.
  #  返回一个参数值和状态信息.
  #  '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

# Thanks, S.C.


例子 23-8. 把数字转化成罗马数字

#!/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

也参考例子 10-28.

函数最大可返回的正整数为255. return 命令与退出状态(exit status)的概念联系很紧密,而退出状态的值受此限制。幸运地是有多种(工作区workarounds)来对付这种要求函数返回大整数的情况.


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

#!/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.
#  Bash 2.05b以前的版本允许返回大负数.
#
#  更新的Bash版本取消了这个问题.
#  请小心! 这可能会使原先的脚本出现问题.
#
# ======================================================

exit 0

一种获取大整数的"返回值"的办法是简单地将要返回的值赋给一个全局变量.

Return_Val=   # 用于保存函数返回巨大值的全局变量.

alt_return_test ()
{
  fvar=$1
  Return_Val=$fvar
  return   # 返回 0 (指示成功).
}

alt_return_test 1
echo $?                              # 0
echo "return value = $Return_Val"    # 1

alt_return_test 256
echo "return value = $Return_Val"    # 256

alt_return_test 257
echo "return value = $Return_Val"    # 257

alt_return_test 25701
echo "return value = $Return_Val"    #25701

更优雅的做法是在函数用 echo 打印"返回值到标准输出",然后使用命令替换(command substitution)捕捉此值. 参考33.7节这种用法的讨论.


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

#!/bin/bash
# max2.sh: 求两个大整数的较大值.

#  这是先前 "max.sh" 的修改版本,
#+ 以允许比较大整数.

EQUAL=0             # 如果两个值相等返回的值.
E_PARAM_ERR=-99999  # 没有足够的参数传递给函数N.
#           ^^^^^^    参数的值超出范围是可以接受的.

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返回值.
                    # 为什么?
}


return_val=$(max2 33001 33997)
#            ^^^^             函数名
#                 ^^^^^ ^^^^^ 传递的参数
#  这是命令替换格式的一种:
#+ 可以把函数当成一个命令看待,
#+ 并把函数的标准输出赋值给变量"return_val."


# ========================= 输出 ========================
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) 简化"输出"段的if/then结构
#  3) 重写脚本使脚本能从命令行参数中取得要比较的整数.

这是另一个捕捉函数"返回值"的例子. 理解这个例子需要有一些awk的知识.

month_length ()  # 把月份数字作为参数.
{                # 返回该月份的天数.
monthD="31 28 31 30 31 30 31 31 30 31 30 31"  # 要不要声明为?
echo "$monthD" | awk '{ print $'"${1}"' }'    # 小技巧.
#                             ^^^^^^^^^
# 参数被传给函数  ($1 -- 月份数字), 然后传给.
# Awk把参数解释为"print $1 . . . print $12" (这依赖于月份数)
# 传一个参数给内嵌的awk脚本的模板:
#                                 $'"${script_parameter}"'

#  需要作一些错误检查来保证参数在正确的范围(1-12)
#+ 并且也需要检查闰年的二月.
}

# ----------------------------------------------
# 使用例子:
month=4        # 例如四月份.
days_in=$(month_length $month)
echo $days_in  # 30
# ----------------------------------------------

也参考例子 A-7.

练习: 用我们已经学到的扩展先前罗马数字那个例子脚本能接受任意大的输入.

重定向

重定向函数的标准输入

函数本质上是一个代码块(code block), 这样意思着它的标准输入可以被重定向(就像在例子 3-1中显示的).


例子 23-11. 用户名的真实名Real name from username

#!/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

还有一个办法,可能是更好理解的重定向函数标准输入方法。它为函数内的一个括号内的代码块调用标准输入重定向.

# 用下面的代替:
Function ()
{
 ...
 } < file

# 也试一下这个:
Function ()
{
  {
    ...
   } < file
}

# 同样,

Function ()  # 可以工作.
{
  {
   echo $*
  } | tr a b
}

Function ()  # 这个不会工作
{
  echo $*
} | tr a b   # 这儿的内嵌代码块是强制的.


# Thanks, S.C.

[1]

return命令是Bash内建(builtin)的.

© 内存溢出 OutOfMemory.CN