第 17 章 创建函数

本章内容

  • 基本的脚本函数
  • 返回值
  • 在函数中使用变量
  • 数组变量和函数
  • 函数递归
  • 创建库
  • 在命令行上使用函数

在编写shell脚本时,你经常会发现在多个地方使用了同一段代码。如果只是一小段代码, 一般也无关紧要。但要在shell脚本中多次重写大块代码段就太累人了。bash shell提供的用户自定义函数功能可以解决这个问题。可以将shell脚本代码放进函数中封装起来,这样就能在脚本中的任何地方多次使用它了。本章将会带你逐步了解如何创建自己的shell脚本函数,并演示如何在shell脚本应用中使用它们。

17.1 基本的脚本函数

在开始编写较复杂的shell脚本时,你会发现自己重复使用了部分能够执行特定任务的代码。这些代码有时很简单,比如显示一条文本消息,或者从脚本用户那里获得一个答案;有时则会比较复杂,需要作为大型处理过程中的一部分被多次使用。
在后一类情况下,在脚本中一遍又一遍地编写同样的代码会很烦人。如果能只写一次,随后在脚本中可多次引用这部分代码就好了。
bash shell提供了这种功能。函数是一个脚本代码块,你可以为其命名并在代码中任何位置重用。要在脚本中使用该代码块时,只要使用所起的函数名就行了(这个过程称为调用函数)。本节将会介绍如何在shell脚本中创建和使用函数。

17.1.1 创建函数

有两种格式可以用来在bash shell脚本中创建函数。第一种格式采用关键字function,后跟分配给该代码块的函数名。

  1. function name {
  2. commands
  3. }

name属性定义了赋予函数的唯一名称。脚本中定义的每个函数都必须有一个唯一的名称。commands是构成函数的一条或多条bash shell命令。在调用该函数时,bash shell会按命令在函数中出现的顺序依次执行,就像在普通脚本中一样。
在bash shell脚本中定义函数的第二种格式更接近于其他编程语言中定义函数的方式。

name() {
commands
}

函数名后的空括号表明正在定义的是一个函数。这种格式的命名规则和之前定义shell脚本函数的格式一样。

17.1.2 使用函数

要在脚本中使用函数,只需要像其他shell命令一样,在行中指定函数名就行了。

$ cat test1
#!/bin/bash
# using a function in a script
function func1 {
echo "This is an example of a function"
}
count=1
while [ $count -le 5 ]
do
  func1
  count=$[ $count + 1 ]
done
echo "This is the end of the loop"
func1
echo "Now this is the end of the script"
$
$ ./test1
This is an example of a function
This is an example of a function
This is an example of a function
This is an example of a function
This is an example of a function
This is the end of the loop
This is an example of a function
Now this is the end of the script
$

每次引用函数名func1时,bash shell会找到func1函数的定义并执行你在那里定义的命令。函数定义不一定非得是shell脚本中首先要做的事,但一定要小心。如果在函数被定义前使用函数,你会收到一条错误消息。

$ cat test2
#!/bin/bash
# using a function located in the middle of a script
count=1
echo "This line comes before the function definition"
function func1 {
  echo "This is an example of a function"
}
while [ $count -le 5 ]
do
  func1
  count=$[ $count + 1 ]
done
echo "This is the end of the loop"
func2
echo "Now this is the end of the script"
function func2 {
  echo "This is an example of a function"
}
$
$ ./test2
This line comes before the function definition
This is an example of a function
This is an example of a function
This is an example of a function
This is an example of a function
This is an example of a function
This is the end of the loop
./test2: func2: command not found
Now this is the end of the script
$

第一个函数func1的定义出现在脚本中的几条语句之后,这当然没任何问题。当func1函数在脚本中被使用时,shell知道去哪里找它。
然而,脚本试图在func2函数被定义之前使用它。由于func2函数还没有定义,脚本运行函数调用处时,产生了一条错误消息。
你也必须注意函数名。记住,函数名必须是唯一的,否则也会有问题。如果你重定义了函数, 新定义会覆盖原来函数的定义,这一切不会产生任何错误消息

$ cat test3
#!/bin/bash
# testing using a duplicate function name
function func1 {
echo "This is the first definition of the function name"
}
func1
function func1 {
echo "This is a repeat of the same function name"
}
func1
echo "This is the end of the script"
$
$ ./test3
This is the first definition of the function name
This is a repeat of the same function name
This is the end of the script
$

func1函数最初的定义工作正常,但重新定义该函数后,后续的函数调用都会使用第二个定义

17.2 返回值

bash shell会把函数当作一个小型脚本,运行结束时会返回一个退出状态码(参见第11章)。有3种不同的方法来为函数生成退出状态码。

17.2.1 默认退出状态码

默认情况下,函数的退出状态码是函数中最后一条命令返回的退出状态码。在函数执行结束后,可以用标准变量$?来确定函数的退出状态码。

$ cat test4
#!/bin/bash
# testing the exit status of a function
func1() {
  echo "trying to display a non-existent file"
  ls -l badfile
}
echo "testing the function: "
func1
echo "The exit status is: $?"
$
$ ./test4
testing the function:
trying to display a non-existent file
ls: badfile: No such file or directory
The exit status is: 1
$

函数的退出状态码是1,这是因为函数中的最后一条命令没有成功运行。但你无法知道函数中其他命令中是否成功运行。看下面的例子。

$ cat test4b
#!/bin/bash
# testing the exit status of a function
func1() {
  ls -l badfile
  echo "This was a test of a bad command"
}
echo "testing the function:"
func1
echo "The exit status is: $?"
$
$ ./test4b
testing the function:
ls: badfile: No such file or directory
This was a test of a bad command
The exit status is: 0
$

这次,由于函数最后一条语句echo运行成功,该函数的退出状态码就是0,尽管其中有一条命令并没有正常运行。使用函数的默认退出状态码是很危险的。幸运的是,有几种办法可以解决这个问题。

17.2.2 使用 return 命令

bash shell使用return命令来退出函数并返回特定的退出状态码。return命令允许指定一个整数值来定义函数的退出状态码,从而提供了一种简单的途径来编程设定函数退出状态码。

$ cat test5
#!/bin/bash
# using the return command in a function
function dbl {
read -p "Enter a value: " value
echo "doubling the value"
return $[ $value * 2 ]
}
dbl
echo "The new value is $?"
$

dbl函数会将$value变量中用户输入的值翻倍,然后用return命令返回结果。脚本用$?变量显示了该值。
但当用这种方法从函数中返回值时,要小心了。记住下面两条技巧来避免问题:

  • 记住,函数一结束就取返回值;
  • 记住,退出状态码必须是0~255。

如果在用$?变量提取函数返回值之前执行了其他命令,函数的返回值就会丢失。记住,$? 变量会返回执行的最后一条命令的退出状态码。
17
第二个问题界定了返回值的取值范围。由于退出状态码必须小于256,函数的结果必须生成一个小于256的整数值。任何大于256的值都会产生一个错误值。

$ ./test5
Enter a value: 200
doubling the value
The new value is 1
$

要返回较大的整数值或者字符串值的话,你就不能用这种返回值的方法了。我们在下一节中将会介绍另一种方法。

17.2.3 使用函数输出

正如可以将命令的输出保存到shell变量中一样,你也可以对函数的输出采用同样的处理办法。可以用这种技术来获得任何类型的函数输出,并将其保存到变量中:

result='dbl'

这个命令会将dbl函数的输出赋给$result变量。下面是在脚本中使用这种方法的例子。

$ cat test5b
#!/bin/bash
# using the echo to return a value
function dbl {
read -p "Enter a value: " value
echo $[ $value * 2 ]
}
result=$(dbl)
echo "The new value is $result"
$
$ ./test5b
Enter a value: 200
The new value is 400
$
$ ./test5b
Enter a value: 1000
The new value is 2000
$

新函数会用echo语句来显示计算的结果。该脚本会获取dbl函数的输出,而不是查看退出状态码。
这个例子中演示了一个不易察觉的技巧。你会注意到dbl函数实际上输出了两条消息。read 命令输出了一条简短的消息来向用户询问输入值。bash shell脚本非常聪明,并不将其作为STDOUT 输出的一部分,并且忽略掉它。如果你用echo语句生成这条消息来向用户查询,那么它会与输出值一起被读进shell变量中。

说明 通过这种技术,你还可以返回浮点值和字符串值。这使它成为一种获取函数返回值的强大方法。

17.3 在函数中使用变量

你可能已经注意到,在17.2.3节的test5例子中,我们在函数里用了一个叫作$value的变量来保存处理后的值。在函数中使用变量时,你需要注意它们的定义方式以及处理方式。这是shell 脚本中常见错误的根源。本节将会介绍一些处理shell脚本函数内外变量的方法。

17.3.1 向函数传递参数

我们在17.2节中提到过,bash shell会将函数当作小型脚本来对待。这意味着你可以像普通脚本那样向函数传递参数(参见第14章)。
函数可以使用标准的参数环境变量来表示命令行上传给函数的参数。例如,函数名会在$0 变量中定义,函数命令行上的任何参数都会通过$1、$2等定义。也可以用特殊变量$#来判断传给函数的参数数目。
在脚本中指定函数时,必须将参数和函数放在同一行,像这样:

func1 $value1 10

然后函数可以用参数环境变量来获得参数值。这里有个使用此方法向函数传值的例子。

$ cat test6
#!/bin/bash
# passing parameters to a function
function addem {
  if [ $# -eq 0 ] || [ $# -gt 2 ]
  then
    echo -1
  elif [ $# -eq 1 ]
  then
    echo $[ $1 + $1 ]
  else
    echo $[ $1 + $2 ]
  fi
}
echo -n "Adding 10 and 15: "
value=$(addem 10 15)
echo $value
echo -n "Let's try adding just one number: "
value=$(addem 10)
echo $value
echo -n "Now trying adding no numbers: "
value=$(addem)
echo $value
echo -n "Finally, try adding three numbers: "
value=$(addem 10 15 20)
echo $value
$
$ ./test6
Adding 10 and 15: 25
Let's try adding just one number: 20
Now trying adding no numbers: -1
Finally, try adding three numbers: -1
$

text6脚本中的addem函数首先会检查脚本传给它的参数数目。如果没有任何参数,或者参数多于两个,addem会返回值-1。如果只有一个参数,addem会将参数与自身相加。如果有两个参数,addem会将它们进行相加。
由于函数使用特殊参数环境变量作为自己的参数值,因此它无法直接获取脚本在命令行中的参数值。下面的例子将会运行失败。

$ cat badtest1
#!/bin/bash
# trying to access script parameters inside a function
function badfunc1 {
  echo $[ $1 * $2 ]
}
if [ $# -eq 2 ]
then
  value=$(badfunc1)
  echo "The result is $value"
else
  echo "Usage: badtest1 a b"
fi
$
$ ./badtest1
Usage: badtest1 a b
$ ./badtest1 10 15
./badtest1: * : syntax error: operand expected (error token is "*
")
The result is
$

尽管函数也使用了$1和$2变量,但它们和脚本主体中的$1和$2变量并不相同。要在函数中使用这些值,必须在调用函数时手动将它们传过去。

$ cat test7
#!/bin/bash
# trying to access script parameters inside a function
function func7 {
  echo $[ $1 * $2 ]
}
if [ $# -eq 2 ]
then
  value=$(func7 $1 $2)
  echo "The result is $value"
else
  echo "Usage: badtest1 a b"
fi
$
$ ./test7
Usage: badtest1 a b
$ ./test7 10 15
The result is 150
$

通过将$1和$2变量传给函数,它们就能跟其他变量一样供函数使用了。

17.3.2 在函数中处理变量

给shell脚本程序员带来麻烦的原因之一就是变量的作用域。作用域是变量可见的区域。函数中定义的变量与普通变量的作用域不同。也就是说,对脚本的其他部分而言,它们是隐藏的。
函数使用两种类型的变量:

  • 全局变量
  • 局部变量

下面几节将会介绍这两种类型的变量在函数中的用法。

  1. 全局变量

全局变量是在shell脚本中任何地方都有效的变量。如果你在脚本的主体部分定义了一个全局变量,那么可以在函数内读取它的值。类似地,如果你在函数内定义了一个全局变量,可以在脚本的主体部分读取它的值。
默认情况下,你在脚本中定义的任何变量都是全局变量。在函数外定义的变量可在函数内正常访问。

$ cat test8
#!/bin/bash
# using a global variable to pass a value
function dbl {
  value=$[ $value * 2 ]
}
read -p "Enter a value: " value
dbl
echo "The new value is: $value"
$
$ ./test8
Enter a value: 450
The new value is: 900
$

$value变量在函数外定义并被赋值。当dbl函数被调用时,该变量及其值在函数中都依然有效。如果变量在函数内被赋予了新值,那么在脚本中引用该变量时,新值也依然有效。
但这其实很危险,尤其是如果你想在不同的shell脚本中使用函数的话。它要求你清清楚楚地知道函数中具体使用了哪些变量,包括那些用来计算非返回值的变量。这里有个例子可说明事情是如何搞砸的。

$ cat badtest2
#!/bin/bash
# demonstrating a bad use of variables
function func1 {
  temp=$[ $value + 5 ]
  result=$[ $temp * 2 ]
}
temp=4
value=6
func1
echo "The result is $result"
if [ $temp -gt $value ]
then
  echo "temp is larger"
else
  echo "temp is smaller"
fi
$
$ ./badtest2
The result is 22
temp is larger
$

由于函数中用到了$temp变量,它的值在脚本中使用时受到了影响,产生了意想不到的后果。有个简单的办法可以在函数中解决这个问题,下节将会介绍。

  1. 局部变量

无需在函数中使用全局变量,函数内部使用的任何变量都可以被声明成局部变量。要实现这一点,只要在变量声明的前面加上local关键字就可以了。

local temp

也可以在变量赋值语句中使用local关键字:

local temp=$[ $value + 5 ]

local关键字保证了变量只局限在该函数中。如果脚本中在该函数之外有同样名字的变量, 那么shell将会保持这两个变量的值是分离的。现在你就能很轻松地将函数变量和脚本变量隔离开了,只共享需要共享的变量。

$ cat test9
#!/bin/bash
# demonstrating the local keyword
function func1 {
  local temp=$[ $value + 5 ]
  result=$[ $temp * 2 ]
}
temp=4
value=6
func1
echo "The result is $result"
if [ $temp -gt $value ]
then
  echo "temp is larger"
else
  echo "temp is smaller"
fi
$
$ ./test9
The result is 22
temp is smaller
$

现在,在func1函数中使用$temp变量时,并不会影响在脚本主体中赋给$temp变量的值。

17.4 数组变量和函数

第6章讨论了使用数组来在单个变量中保存多个值的高级用法。在函数中使用数组变量值有点麻烦,而且还需要一些特殊考虑。本节将会介绍一种方法来解决这个问题。

17.4.1 向函数传数组参数

向脚本函数传递数组变量的方法会有点不好理解。将数组变量当作单个参数传递的话,它不会起作用。

$ cat badtest3
#!/bin/bash
# trying to pass an array variable
function testit {
  echo "The parameters are: $@"
  thisarray=$1
  echo "The received array is ${thisarray[*]}"
}
myarray=(1 2 3 4 5)
echo "The original array is: ${myarray[*]}"
testit $myarray
$
$ ./badtest3
The original array is: 1 2 3 4 5
The parameters are: 1
The received array is 1
$

如果你试图将该数组变量作为函数参数,函数只会取数组变量的第一个值。
要解决这个问题,你必须将该数组变量的值分解成单个的值,然后将这些值作为函数参数使用。在函数内部,可以将所有的参数重新组合成一个新的变量。下面是个具体的例子。

$ cat test10
#!/bin/bash
# array variable to function test
function testit {
  local newarray
  newarray=(;'echo "$@"')
  echo "The new array value is: ${newarray[*]}"
}
myarray=(1 2 3 4 5)
echo "The original array is ${myarray[*]}"
testit ${myarray[*]}
$
$ ./test10
The original array is 1 2 3 4 5
The new array value is: 1 2 3 4 5
$

该脚本用$myarray变量来保存所有的数组元素,然后将它们都放在函数的命令行上。该函 数随后从命令行参数中重建数组变量。在函数内部,数组仍然可以像其他数组一样使用。

$ cat test11
#!/bin/bash
# adding values in an array
function addarray {
  local sum=0
  local newarray
  newarray=($(echo "$@"))
  for value in ${newarray[*]}
  do
    sum=$[ $sum + $value ]
  done
  echo $sum
}
myarray=(1 2 3 4 5)
echo "The original array is: ${myarray[*]}"
arg1=$(echo ${myarray[*]})
result=$(addarray $arg1)
echo "The result is $result"
$
$ ./test11
The original array is: 1 2 3 4 5
The result is 15
$

addarray函数会遍历所有的数组元素,将它们累加在一起。你可以在myarray数组变量中 放置任意多的值,addarry函数会将它们都加起来。

17.4.2 从函数返回数组

从函数里向shell脚本传回数组变量也用类似的方法。函数用echo语句来按正确顺序输出单个数组值,然后脚本再将它们重新放进一个新的数组变量中。

$ cat test12
#!/bin/bash
# returning an array value
function arraydblr {
  local origarray
  local newarray
  local elements
  local i
  origarray=($(echo "$@"))
  newarray=($(echo "$@"))
  elements=$[ $# - 1 ]
  for (( i = 0; i <= $elements; i++ ))
  {
    newarray[$i]=$[ ${origarray[$i]} * 2 ]
  }
  echo ${newarray[*]}
}
myarray=(1 2 3 4 5)
echo "The original array is: ${myarray[*]}"
arg1=$(echo ${myarray[*]})
result=($(arraydblr $arg1))
echo "The new array is: ${result[*]}"
$
$ ./test12
The original array is: 1 2 3 4 5
The new array is: 2 4 6 8 10

该脚本用$arg1变量将数组值传给arraydblr函数。arraydblr函数将该数组重组到新的数 组变量中,生成该输出数组变量的一个副本。然后对数据元素进行遍历,将每个元素值翻倍,并将结果存入函数中该数组变量的副本。
arraydblr函数使用echo语句来输出每个数组元素的值。脚本用arraydblr函数的输出来重新生成一个新的数组变量。
17

17.5 函数递归

局部函数变量的一个特性是自成体系。除了从脚本命令行处获得的变量,自成体系的函数不需要使用任何外部资源。
这个特性使得函数可以递归地调用,也就是说,函数可以调用自己来得到结果。通常递归函数都有一个最终可以迭代到的基准值。许多高级数学算法用递归对复杂的方程进行逐级规约,直到基准值定义的那级。
递归算法的经典例子是计算阶乘。一个数的阶乘是该数之前的所有数乘以该数的值。因此, 要计算5的阶乘,可以执行如下方程:

5! = 1 * 2 * 3 * 4 * 5 = 120

使用递归,方程可以简化成以下形式:

x!  =  x  *  (x-1)!

也就是说,x的阶乘等于x乘以x1的阶乘。这可以用简单的递归脚本表达为:

function factorial {
if [ $1 -eq 1 ]
then
  echo 1
else
  local temp=$[ $1 - 1 ]
  local result='factorial $temp'
  echo $[ $result * $1 ]
fi
}

阶乘函数用它自己来计算阶乘的值:

$ cat test13
#!/bin/bash
# using recursion
function factorial {
  if [ $1 -eq 1 ]
  then
    echo 1
  else
    local temp=$[ $1 - 1 ]
    local result=$(factorial $temp)
    echo $[ $result * $1 ]
  fi
}
read -p "Enter value: " value
result=$(factorial $value)
echo "The factorial of $value is: $result"
$
$ ./test13
Enter value: 5
The factorial of 5 is: 120
$

使用阶乘函数很容易。创建了这样的函数后,你可能想把它用在其他脚本中。接下来,我们来看看如何有效地利用函数。

17.6 创建库

使用函数可以在脚本中省去一些输入工作,这一点是显而易见的。但如果你碰巧要在多个脚本中使用同一段代码呢?显然,为了使用一次而在每个脚本中都定义同样的函数太过麻烦。
有个方法能解决这个问题!bash shell允许创建函数库文件,然后在多个脚本中引用该库文件。这个过程的第一步是创建一个包含脚本中所需函数的公用库文件。这里有个叫作myfuncs的库文件,它定义了3个简单的函数。

$ cat myfuncs
# my script functions
function addem {
  echo $[ $1 + $2 ]
}
function multem {
  echo $[ $1 * $2 ]
}
function divem {
  if [ $2 -ne 0 ]
  then
    echo $[ $1 / $2 ]
  else
    echo -1
  fi
}
$

下一步是在用到这些函数的脚本文件中包含myfuncs库文件。从这里开始,事情就变复杂了。问题出在shell函数的作用域上。和环境变量一样,shell函数仅在定义它的shell会话内有效。
如果你在shell命令行界面的提示符下运行myfuncs shell脚本,shell会创建一个新的shell并在其中运行这个脚本。它会为那个新shell定义这三个函数,但当你运行另外一个要用到这些函数的脚本时,它们是无法使用的。
这同样适用于脚本。如果你尝试像普通脚本文件那样运行库文件,函数并不会出现在脚本中。

$ cat badtest4
#!/bin/bash
# using a library file the wrong way
./myfuncs
result=$(addem 10 15)
echo "The result is $result"
$
$ ./badtest4
./badtest4: addem: command not found
The result is
$

使用函数库的关键在于source命令。source命令会在当前shell上下文中执行命令,而不是创建一个新shell。可以用source命令来在shell脚本中运行库文件脚本。这样脚本就可以使用库中的函数了。
source命令有个快捷的别名,称作点操作符(dot operator)。要在shell脚本中运行myfuncs库文件,只需添加下面这行:

. ./myfuncs

这个例子假定myfuncs库文件和shell脚本位于同一目录。如果不是,你需要使用相应路径访问该文件。这里有个用myfuncs库文件创建脚本的例子。

$ cat test14
#!/bin/bash
# using functions defined in a library file
. ./myfuncs
value1=10
value2=5
result1=$(addem $value1 $value2)
result2=$(multem $value1 $value2)
result3=$(divem $value1 $value2)
echo "The result of adding them is: $result1"
echo "The result of multiplying them is: $result2"
echo "The result of dividing them is: $result3"
$
$ ./test14
The result of adding them is: 15
The result of multiplying them is: 50
The result of dividing them is: 2
$

该脚本成功地使用了myfuncs库文件中定义的函数。

17.7 在命令行上使用函数

可以用脚本函数来执行一些十分复杂的操作。有时也很有必要在命令行界面的提示符下直接使用这些函数。
和在shell脚本中将脚本函数当命令使用一样,在命令行界面中你也可以这样做。这个功能很不错,因为一旦在shell中定义了函数,你就可以在整个系统中使用它了,无需担心脚本是不是在PATH环境变量里。重点在于让shell能够识别这些函数。有几种方法可以实现。

17.7.1 在命令行上创建函数

因为shell会解释用户输入的命令,所以可以在命令行上直接定义一个函数。有两种方法。一种方法是采用单行方式定义函数。

$ function divem { echo $[ $1 / $2 ]; }
$ divem 100 5
20
$

当在命令行上定义函数时,你必须记得在每个命令后面加个分号,这样shell就能知道在哪里是命令的起止了。

$ function doubleit { read -p "Enter value: " value; echo $[
$value * 2 ]; }
$
$ doubleit
Enter value: 20
40
$

另一种方法是采用多行方式来定义函数。在定义时,bash shell会使用次提示符来提示输入更多命令。用这种方法,你不用在每条命令的末尾放一个分号,只要按下回车键就行。

$ function multem {
> echo $[ $1 * $2 ]
> }
$ multem 2 5
10
$

在函数的尾部使用花括号,shell就会知道你已经完成了函数的定义。

警告 在命令行上创建函数时要特别小心。如果你给函数起了个跟内建命令或另一个命令相同的名字,函数将会覆盖原来的命令。

17.7.2 在.bashrc 文件中定义函数

在命令行上直接定义shell函数的明显缺点是退出shell时,函数就消失了。对于复杂的函数来说,这可是个麻烦事。
一个非常简单的方法是将函数定义在一个特定的位置,这个位置在每次启动一个新shell的时候,都会由shell重新载入。
最佳地点就是.bashrc文件。bash shell在每次启动时都会在主目录下查找这个文件,不管是交互式shell还是从现有shell中启动的新shell。

  1. 直接定义函数

可以直接在主目录下的.bashrc文件中定义函数。许多Linux发行版已经在.bashrc文件中定义了一些东西,所以注意不要误删了。把你写的函数放在文件末尾就行了。这里有个例子。

$ cat .bashrc
# .bashrc
# Source global definitions
if [ -r /etc/bashrc ]; then
  . /etc/bashrc
fi
function addem {
  echo $[ $1 + $2 ]
}
$

该函数会在下次启动新bash shell时生效。随后你就能在系统上任意地方使用这个函数了。

  1. 读取函数文件

只要是在shell脚本中,都可以用source命令(或者它的别名点操作符)将库文件中的函数添加到你的.bashrc脚本中。

$ cat .bashrc
# .bashrc
# Source global definitions
if [ -r /etc/bashrc ]; then
  . /etc/bashrc
fi
. /home/rich/libraries/myfuncs
$

要确保库文件的路径名正确,以便bash shell能够找到该文件。下次启动shell时,库中的所有函数都可在命令行界面下使用了。

$ addem 10 5
15
$ multem 10 5
50
$ divem 10 5
2
$

更好的是,shell还会将定义好的函数传给子shell进程,这样一来,这些函数就自动能够用于该shell会话中的任何shell脚本了。你可以写个脚本,试试在不定义或使用source的情况下, 直接使用这些函数。

$ cat test15
#!/bin/bash
# using a function defined in the .bashrc file
value1=10
value2=5
result1=$(addem $value1 $value2)
result2=$(multem $value1 $value2)
result3=$(divem $value1 $value2)
echo "The result of adding them is: $result1"
echo "The result of multiplying them is: $result2"
echo "The result of dividing them is: $result3"
$
$ ./test15
The result of adding them is: 15
The result of multiplying them is: 50
The result of dividing them is: 2
$

甚至都不用对库文件使用source,这些函数就可以完美地运行在shell脚本中。

17.8 实例

函数的应用绝不仅限于创建自己的函数自娱自乐。在开源世界中,共享代码才是关键,而这一点同样适用于脚本函数。你可以下载大量各式各样的函数,并将其用于自己的应用程序中。
本节介绍了如何下载、安装、使用GNU shtool shell脚本函数库。shtool库提供了一些简单的shell脚本函数,可以用来完成日常的shell功能,例如处理临时文件和目录或者格式化输出显示。

17.8.1 下载及安装

首先是将GNU shtool库下载并安装到你的系统中,这样你才能在自己的shell脚本中使用这些库函数。要完成这项工作,可以使用FTP客户端或者图像化桌面中的浏览器。shtool软件包的下载地址是:

ftp://ftp.gnu.org/gnu/shtool/shtool-2.0.8.tar.gz

将文件shtool-2.0.8.tar.gz下载到下载目录中。然后你可以使用命令行工具cp或是Linux发行版中的图形化文件管理器(如Ubuntu中的Nautius)将文件复制到主目录中。
完成复制操作后,使用tar命令提取文件。

tar -zxvf shtool-2.0.8.tar.gz

该命令会将打包文件中的内容提取到shtool-2.0.8目录中。接下来就可以构建shell脚本库文件了。

17.8.2 构建库

shtool文件必须针对特定的Linux环境进行配置。配置工作必须使用标准的configure和make命令,这两个命令常用于C编程环境。要构建库文件,只要输入:

$ ./confifgure
$ make

configure命令会检查构建shtool库文件所必需的软件。一旦发现了所需的工具,它会使用工具路径修改配置文件。
make命令负责构建shtool库文件。最终的结果(shtool)是一个完整的库软件包。你也可以使用make命令测试这个库文件。
17

$ make test
Running test suite:
echo...........ok
mdate..........ok
table..........ok
prop...........ok
move...........ok
install........ok
mkdir..........ok
mkln...........ok
mkshadow.......ok
fixperm........ok
rotate.........ok
tarball........ok
subst..........ok
platform.......ok
arx............ok
slo............ok
scpp...........ok
version........ok
path...........ok
OK: passed: 19/19
$

测试模式会测试shtool库中所有的函数。如果全部通过测试,就可以将库安装到Linux系统中的公用位置,这样所有的脚本就都能够使用这个库了。要完成安装,需要使用make命令的install选项。不过你得以root用户的身份运行该命令。

$ su
Password:
# make install
./shtool mkdir -f -p -m 755 /usr/local
./shtool mkdir -f -p -m 755 /usr/local/bin
./shtool mkdir -f -p -m 755 /usr/local/share/man/man1
./shtool mkdir -f -p -m 755 /usr/local/share/aclocal
./shtool mkdir -f -p -m 755 /usr/local/share/shtool
...
./shtool install -c -m 644 sh.version /usr/local/share/shtool/sh.version
./shtool install -c -m 644 sh.path /usr/local/share/shtool/sh.path
#

现在就能在自己的shell脚本中使用这些函数了。

17.8.3 shtool 库函数

shtool库提供了大量方便的、可用于shell脚本的函数。表17-1列出了库中可用的函数。
表17-1 shtool库函数

函 数 描 述
Arx 创建归档文件(包含一些扩展功能)
Echo 显示字符串,并提供了一些扩展构件
fixperm 改变目录树中的文件权限
install 安装脚本或文件
mdate 显示文件或目录的修改时间
mkdir 创建一个或更多目录
Mkln 使用相对路径创建链接
mkshadow 创建一棵阴影树
move 带有替换功能的文件移动
Path 处理程序路径
platform 显示平台标识
Prop 显示一个带有动画效果的进度条
rotate 转置日志文件
Scpp 共享的C预处理器
Slo 根据库的类别,分离链接器选项
Subst 使用sed的替换操作
Table 以表格的形式显示由字段分隔(field-separated)的数据
tarball 从文件和目录中创建tar文件
version 创建版本信息文件

每个shtool函数都包含大量的选项和参数,你可以利用它们改变函数的工作方式。下面是shtool函数的使用格式:

shtool [options] [function [options] [args]]

17.8.4 使用库

可以在命令行或自己的shell脚本中直接使用shtool函数。下面是一个在shell脚本中使用
platform函数的例子。

$ cat test16
#!/bin/bash
shtool platform
$ ./test16
Ubuntu 14.04 (iX86)
$

platform函数会返回Linux发行版以及系统所使用的CPU硬件的相关信息。我喜欢的一个函数prop函数。它可以使用\、|、/和-字符创建一个旋转的进度条。这是一个非常漂亮的工具,可以告诉shell脚本用户目前正在进行一些后台处理工作。
要使用prop函数,只需要将希望监看的输出管接到shtool脚本就行了。

$ ls –al /usr/bin | shtool prop –p "waiting..."
waiting...
$

prop函数会在处理过程中不停地变换进度条字符。在本例中,输出信息来自于ls命令。你能看到多少进度条取决于CPU能以多快的速度列出/usr/bin中的文件!-p选项允许你定制输出文本,这段文本会出现在进度条字符之前。好了,尽情享受吧!

17.9 小结

shell脚本函数允许你将脚本中多处用到的代码放到一个地方。可以创建一个包含该代码块的函数,然后在脚本中通过函数名来引用这块代码,而不用一次次地重写那段代码。bash shell只要看到函数名,就会自动跳到对应的函数代码块处。
甚至可以创建能返回值的函数。这样你的函数就能够同脚本进行交互,返回数字和字符串数据。脚本函数可以用函数中最后一条命令的退出状态码或return命令来返回数值。return命令可以基于函数的结果,通过编程的方式将函数的退出状态码设为特定值。
函数也可以用标准的echo语句来返回值。可以跟其他shell命令一样用反引号来获取输出的数据。这样你就能从函数中返回任意类型的数据了(包括字符串和浮点数)。
可以在函数中使用shell变量,对其赋值以及从中取值。这样你就能将任何类型的数据从主体脚本程序的脚本函数中传入传出。函数也支持定义只能在函数内部访问的局部变量。局部变量使得用户可以创建自成体系的函数,这样就不会影响到shell脚本主体中变量或处理过程了。
函数也可以调用包括它自身在内的其他函数。函数的自调用行为称为递归。递归函数通常有个作为函数终结条件的基准值。函数在调用自身的同时会不停地减少参数值,直到达到基准值。
如果需要在shell脚本中使用大量函数,可以创建脚本函数库文件。库文件可以用source命令(或该命令的别名)在任何shell脚本文件中引用,这也称为sourcing。shell不会运行库文件, 但会使这些函数在运行该脚本的shell中生效。可以用同样的方法创建在普通shell命令行上使用的函数。你可以直接在命令行上定义函数,或者将它们加到.bashrc文件中,这样每次启动新的shell 会话时就可以使用这些函数了。这是一种创建实用工具的简便方法,不管PATH环境变量设置成 什么,都可以直接拿来使用。
下一章将会介绍脚本中文本图形的使用。在现代化图形界面普及的今天,只有普通的文本界面有时是不够的。bash shell提供了一些轻松的方法来将简单的图形功能加入到你的脚本中。