Shell学习笔记(7)——构建函数

基本的脚本函数

函数是一个脚本代码块,用户可以为其命名并在代码中任何位置重用。要在脚本中使用该代码块时,只要使用所起的函数名就行了(这个过程称为调用函数)。

创建函数

创建函数有2种方式:第一种,使用关键字function,如下所示:

1
2
3
function name{
commands
}

name属性定义了赋予函数的唯一名称。脚本中定义的每个函数都必须有一个唯一的名称。commands是构成函数的一条或多条bash shell命令。在调用该函数时,bash shell会按命令在函数中出现的顺序依次执行,就像在普通脚本中一样。

第二种构建函数的格式如下:

1
2
3
name() {
commands
}

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

使用函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/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"

运行结果如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/funciton$ bash test1.sh
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函数的定义并执行你在那里定义的命令。如果在函数被定义前使用函数,会收到一条错误消息,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/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 scripts"
function func2 {
echo "This is an example of a function"
}

运行结果如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/funciton$ bash test2.sh
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.sh: line 17: func2: command not found
Now this is the end of the scripts

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/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 end of the script"

运行结果如下所示:

1
2
3
4
biotest@ubuntu:~/funciton$ bash test3.sh
This is the first definition of the function name
This is a repeat of the same function name
This is end of the script

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

返回值

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

默认退出状态码

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

1
2
3
4
5
6
7
8
9
10
11
#!/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 : $?"

运行结果如下所示:

1
2
3
4
5
biotest@ubuntu:~/funciton$ bash test4.sh
testing the function:
Trying to display a non-existent file
ls: cannot access 'badfile': No such file or directory
The exit status is : 2

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

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# testing the exit status of a fucntion
func1() {
ls -l badfile
echo "This was a test of a bad command"
}
echo "Testing the function: "
func1
echo "The exit status is: $?"

运行结果如下所示:

1
2
3
4
5
biotest@ubuntu:~/funciton$ bash test4b.sh
Testing the function:
ls: cannot access 'badfile': No such file or directory
This was a test of a bad command
The exit status is: 0

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

使用return命令

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

1
2
3
4
5
6
7
8
9
10
11
#!/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 $?"

运行结果如下所示:

1
2
3
4
biotest@ubuntu:~/funciton$ bash test5.sh
Enter a value:5
doubling the value
The new value is 10

dbl函数会将$value变量中用户输入的值翻倍,然后用return命令返回结果。脚本用$?变量显示了该值。但当用这种方法从函数中返回值时,需要注意两点:第一,函数一结束就取返回值;第二,退出状态码必须是0~255。如果在用$?变量提取函数返回值之前执行了其他命令,函数的返回值就会丢失。$?变量会返回执行的最后一条命令的退出状态码。第二个问题界定了返回值的取值范围。由于退出状态码必须小于256,函数的结果必须生成一个小于256的整数值。任何大于256的值都会产生一个错误值,如下所示:

1
2
3
4
biotest@ubuntu:~/funciton$ bash test5.sh
Enter a value:200
doubling the value
The new value is 144

使用函数的输出

将函数的份输出保存到shell变量中,就能获取任何类型的函数输出,例如result='dbl就是将dbl函数的输出赋值给$result,如下所示:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
# using the return command in a function
function dbl {
read -p "Enter a value:" value
echo $[ $value*2 ]
}
result=$(dbl)
echo "The new value is $result"

运行结果如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash test5b.sh
Enter a value:200
The new value is 400

新函数会用echo语句来显示计算的结果。该脚本会获取dbl函数的输出,而不是查看退出状态码。在这个例子中,dbl函数实际上输出了两条消息。read命令输出了一条简短的消息来向用户询问输入值。bash shell脚本并不将其作为STDOUT输出的一部分,并且忽略掉它。如果你用echo语句生成这条消息来向用户查询,那么它会与输出值一起被读进shell变量中。通过这种技术,还可以返回浮点值和字符串值。

在函数中使用变量

在test5例子的脚本里,在函数里用了一个叫作$value的变量来保存处理后的值。在函数中使用变量时,用户需要注意它们的定义方式以及处理方式。这是shell脚本中常见错误的根源。

向函数传递参数

函数使用两种类型的变量,分别为全局变量和局部变量。

全局变量

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

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# using a global variable to pass a value
function dbl {
value=$[ $value*2 ]
}
read -p "Enter a vluae: " value
dbl
echo "The new value is : $value"

运行结果如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash test8.sh
Enter a vluae: 300
The new value is : 600

$value变量在函数外定义并被赋值。当dbl函数被调用时,该变量及其值在函数中都依然有效。如果变量在函数内被赋予了新值,那么在脚本中引用该变量时,新值也依然有效。但这操作其实很危险,因为如果是不同的脚本都使用该函数,有可能造成冲突,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
# demonstrating a bad use of variable
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

运行结果如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash badtest2.sh
The result is 22
temp is larger

由于函数中用到了$temp变量,它的值在脚本中使用时受到了影响,产生了意想不到的后果。

局部变量

在函数内部,通常无需使用全局变量,使用局部变量即可,要实现这一点,只要在变量声明的前面加上 local 关键字就可以了,例如local temp,local关键字保证了变量只局限在该函数中。如果脚本中在该函数之外有同样名字的变量, 那么shell将会保持这两个变量的值是分离的。现在你就能很轻松地将函数变量和脚本变量隔离开了,只共享需要共享的变量。如下所示:

1
2

运行结果如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash test9.sh
The result is 22
temp is smaller

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

数组变量和函数

向函数传数组参数

如果你试图将该数组变量作为函数参数,函数只会取数组变量的第一个值,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# Trying to pass an arraya 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

结果运行如下:

1
2
3
4
biotest@ubuntu:~/funciton$ bash badtest3b.sh
The original array is: 1 2 3 4 5
The parameters are: 1
The received array is 1

要解决上述问题,用户必须将该数组变量的值分解成单个的值,然后将这些值作为函数参数使用。在函数内部,可以将所有的参数重新组合成一个新的变量,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/funciton$ cat test10.sh
#!/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[*]}

运行结果如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash test10.sh
The original array is 1 2 3 4 5
The new array value is: 1 2 3 4 5

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/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"

运行结果如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash test11.sh
The original array is : 1 2 3 4 5
The result is 15

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

从函数返回数组

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/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[*]}"

运行结果如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash test12.sh
The original array is: 1 2 3 4 5
The new array is: 2 4 6 8 10

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

函数递归

函数可以调用函数自身,这个过程就称为函数的递归。下面看一个函数,这个函数就是通过递归来计算阶乘,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/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"

运行结果如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash test13.sh
Enter value: 5
The factorial of 5 is: 120

在创建了函数后,可以在其他的脚本中调用。

创建库

如果用户要在多个脚本中使用同一段代码的话,这就需要创建函数库文件,然后在多个脚本中引用该库文件。这个过程的第一步是创建一个包含脚本中所需函数的公用库文件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
# 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定义这三个函数,但当你运行另外一个要用到这些函数的脚本时,它们是无法使用的。这同样适用于脚本。如果你尝试像普通脚本文件那样运行库文件,函数并不会出现在脚本中,如下所示:

1
2
3
4
5
6
#!/bin/bash
# using a library file the wrong way
bash myfuncs.sh
result=$(addem 10 15)
echo "The result is $result"

运行后如下所示:

1
2
3
biotest@ubuntu:~/funciton$ bash badtest4.sh
badtest4.sh: line 5: addem: command not found
The result is

使用函数库的需要用到source命令。source命令会在当前shell上下文中执行命令,而不是创建一个新shell。可以用source命令来在shell脚本中运行库文件脚本。这样脚本就可以使用库中的函数了。source命令有个快捷的别名,称作点操作符(dotoperator)。要在shell脚本中运行myfuncs库文件,只需添加这一行,即. ./myfuncs(两个点之间有空格),这个例子是假定myfuncs库文件与shell脚本位于同一目录,如果不是,则需要输入全路径。使用source命令的脚本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
# using a library file the wrong way
. ./myfuncs.sh
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"

运行结果如下所示:

1
2
3
4
biotest@ubuntu:~/funciton$ bash test14.sh
The result of adding them is: 15
The result of multiplying them is: 50
The result of dividing them is: 2

在命令行上使用函数

在命令行界面中也可以使用函数,一旦在shell中定义了函数,用户就可以在整个系统中使用它了,无需担心脚本是不是在PATH环境变量里。

在命令行上创建函数

第1种方法:直接定义函数

这一种方法使用的是单行试,需要函数主体部分的每个命令后面加上分号,如下所示:

1
2
3
biotest@ubuntu:~/funciton$ function divem { echo $[ $1/$2 ]; }
biotest@ubuntu:~/funciton$ divem 100 5
20

再看一个案例:

1
2
3
4
biotest@ubuntu:~/funciton$ function doubleit { read -p "Enter value: " value; echo $[ $value*2 ]; }
biotest@ubuntu:~/funciton$ doubleit
Enter value: 12
24

第2种方法:多行形式

还可以采用多行方式定义函数,此种情况下,bash shell会用提示符提示输入更多的命令,此种方法不需要在命令后面加分号,直接回车就行,如下所示:

1
2
3
4
5
biotest@ubuntu:~/funciton$ function multem {
> echo $[ $1*$2 ]
> }
biotest@ubuntu:~/funciton$ multem 2 5
10

.bashrc文件中定义函数

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

直接定义函数

打开.bashrc文件,下拉在文件的末尾处写入函数即可,如下所示:

1
2
3
4
5
6
7
if [ -r /etc/bashrc ];then
. /etc/bashrc
fi
function addem {
echo $[ $1 + $2 ]
}

运行结果如下所示:

1
2
biotest@ubuntu:~/funciton$ addem 10 2
12

也可以将库文件写入到.bashrc文件中,如下所示:

1
2
3
4
5
if [ -r /etc/bashrc ];then
. /etc/bashrc
fi
. /home/biotest/funciton/myfuncs.sh

然后source ~/.bashrc,这样在shell中就可以使用myfuncs.sh中的函数了,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/funciton$ addem 10 5
15
biotest@ubuntu:~/funciton$ multem 10 5
50
biotest@ubuntu:~/funciton$ divem 10 5
2

案例

shtool库提供了一些简单的shell脚本函数,可以用来完成日常的shell功能,例如处理临时文件和目录或者格式化输出显示。

下载及安装

1
2
wget ftp://ftp.gnu.org/gnu/shtool/shtool-2.0.8.tar.gz
tar -zxvf shtool-2.0.8.tar.gz

构建库

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

1
2
biotest@ubuntu:~/Downloads/shtool-2.0.8$ ./configure
biotest@ubuntu:~/Downloads/shtool-2.0.8$ make

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

1
make test

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

1
biotest@ubuntu:~/Downloads/shtool-2.0.8$ sudo make install

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

shtool 库函数

shtool库提供了大量方便的、可用于shell脚本的函数。如下所示:

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

shtool函数的使用格式为shtool [options] [function [options] [args]]

使用库

下面是一个在shell脚本中使用platform函数的例子,如下所示:

1
2
#!/bin/bash
shtool platform

运行后如下所示:

1
2
biotest@ubuntu:~/funciton$ bash test16.sh
Ubuntu 16.04 (AMD64)

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

1
2
biotest@ubuntu:~/funciton$ ls -al /usr/bin | shtool prop -p "waiting..."
waiting...

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