#9: 定义使用函数
当你的bash脚本越来越大,代码也就越来越混乱。
而且很多部分的代码都是重复的。
可以通过使用bash中的函数来重构代码,使其变得更有组织性和可读性。
本节将学习如何在bash脚本中创建函数、返回函数值和传递参数给函数。
除此之外还将学习变量作用域的工作原理以及如何定义递归函数。
创建函数
声明bash函数有两种不同的语法。下面这种是创建函数最常用的语法:
function_name () {
commands
}
第二种语法不太常用,保留关键词function开头,后面仅函数名没有括号,也就是不接收后面会提到的传入参数:
function function_name {
commands
}
使用函数需要了解以下几个问题:
- 除非你调用了函数,否则函数永远不会执行。
- 函数必须定义在其被调用之前。
每当你想要运行一个函数时,只需要在调用该函数时引用它的函数名就可以。
看看下面的fun.sh脚本:
#!/bin/bash
hello () {
echo "Hello World"
}
hello
hello
hello
上面的代码中,定义了一个名为hello的函数,该函数向终端回显“hello world”。注意,hello函数被调用了三次,所以在运行这个脚本的时候,打印了三次“hello world”:
sean@debian-12:~$ ./fun.sh
Hello World
Hello World
Hello World

返回函数值
在其他编程语言中,函数在被调用时会返回一个值;但是bash不这样,它没有返回值。
在bash中的函数执行完成后,它返回的是一个$?变量,0代表执行成功,如果返回的是非零的正整数(1-255),就是代表执行失败。
使用return语句可以改变函数的退出状态。比如下面error.sh的代码:
#! /bin/bash
error () {
blabla
return 0
}
error
echo "The return status of the error function is: $?"
运行error.sh脚本,你会惊讶函数可以执行成功,后面的内容也可以正常的执行。
sean@debian-12:~$ ./error.sh
./error.sh: line 4: blabla: command not found
The return status of the error function is: 0
如果没有return 0这一句,error函数是不会返回非零的退出状态的,因为blabla会导致报错“command not found”。
综上所述,就算bash的函数没有返回值,也能通过添加return语句来改变退出状态解决返回的问题。

注意:return也可以用来终止函数的执行。
传参给函数
你可以向函数传递参数,有点类似于在执行脚本的时候向脚本传递参数的形式,在调用函数时包含参数即可。
让我们来看看下面iseven.sh这个脚本
#!/bin/bash
iseven () {
if [ $(($1 % 2)) -eq 0 ]; then
echo "$1 is even."
else
echo "$1 is odd."
fi
}
iseven 3
iseven 4
iseven 20
iseven 111
函数iseven用于测试一个数是偶数还是奇数,脚本中对iseven函数进行了四次的调用,每次调用都提供一个数字,这个数字就是传递给函数的参数,在函数里面,这个参数被$1变量引用。
运行iseven.sh脚本验证它可以正常运行:
sean@debian-12:~$ ./iseven.sh
3 is odd.
4 is even.
20 is even.
111 is odd.

需要弄懂一个概念,脚本的参数和函数的参数是两个不同的东西,用下面funarg.sh脚本来理解两者的区别:
#!/bin/bash
fun () {
echo "$1 is the first argument to fun()"
echo "$2 is the second argument to fun()"
}
echo "$1 is the first argument to the script."
echo "$2 is the second argument to the script."
fun Yes 7
运行这个带参数的脚本并且仔细理解它的输出结果为什么是这样:
sean@debian-12:~$ ./funarg.sh Cool Stuff
Cool is the first argument to the script.
Stuff is the second argument to the script.
Yes is the first argument to fun()
7 is the second argument to fun()

现在可以理解了吧?同样是$1和$2的变量引用传进来的参数,在函数的里面和外面调用他们,得到的结果是不一样的。
全局变量和局部变量
bash变量可以是具有全局作用域(称为全局变量)和局部作用域(称为局部变量)两种。你可以在脚本的任何地方访问到全局变量,但是局部变量只能在定义它的函数里面引用。
请看下面的scope.sh脚本:
#!/bin/bash
v1='A'
v2='B'
myfun() {
local v1='C'
v2='D'
echo "Inside myfun(): v1: $v1, v2: $v2"
}
echo "Before calling myfun(): v1: $v1, v2: $v2"
myfun
echo "After calling myfun(): v1: $v1, v2: $v2"
首先定义了两个全局变量v1和v2,在在myfun()函数中使用了local关键字定义了一个局部变量v1;并且修改了全局变量v2。
注意:你可以在不同的函数里面对局部变量使用相同的变量名。
现在我们运行一下脚本:
sean@debian-12:~$ ./scope.sh
Before calling myfun(): v1: A, v2: B
Inside myfun(): v1: C, v2: D
After calling myfun(): v1: A, v2: D
从脚本的输出结果,可以得到以下的结论:
- 在函数里面,与全局变量同名的局部变量会优先在函数体内的全局变量
- 可以在函数里面修改全局变量

递归函数
递归函数是一种自己调用自己的函数!通常用于解决一个问题可以分解为更小的子问题的编程。
下面factorial.sh脚本中的factorial函数是一个经典的递归函数的例子。
#!/bin/bash
factorial () {
if [ $1 -le 1 ]; then
echo 1
else
last=$(factorial $(( $1 -1)))
echo $(( $1 * last ))
fi
}
echo -n "4! is: "
factorial 4
echo -n "5! is: "
factorial 5
echo -n "6! is: "
factorial 6
任何递归函数都必须以基线条件开始,基线条件会结束递归函数的调用链。在factorial()函数中,基线条件的定义如下:
if [ $1 -le 1 ]; then
echo 1
现在我们来推导factorial函数的递归情况。要计算n的阶乘,其中n是一个大于1的正数,你可以调用函数,然后”n“乘以”n-1“的阶乘:
factorial(n) = n * factorial(n-1)
让我们用上面的公式来写这个递归的例子:
last=$(factorial $(( $1 -1)))
echo $(( $1 * last ))
现在运行脚本,并且观察输出结果是否正确:
sean@debian-12:~$ ./factorial.sh
4! is: 24
5! is: 120
6! is: 720

请尝试编写一个用于计算第n个数的斐波那契数列的递归函数。提示:首先提出基线条件,然后是递归条件。
练习题
自己动手做练习可以帮助你记忆刚学到的东西,下面是几到简单的练习题,用到了上面提到的内容。如果您是初学者,建议您可以自己动手做一下。
练习 1:它是正整数吗
从简单的开始,重构前面的判断是正整数还是负整数的脚本,将其写在一个函数里面。
难度级别:容易
提示:使用read命令接收用户输入。
练习 2:函数乘法表
写一个shell脚本,打印给定数字的乘法表。必须使用函数。
难度级别:容易
提示:使用for循环可以更高效。
练习 3:是素数吗
编写一个shell脚本,接收一个数字并检查它是否是一个素数(质数),必须使用函数。
难度级别:中等
提示:质数只能被1和数字本身整除。23是质数,21不是质数。1和2也是质数。
练习 4:斐波那契数列
编写shell脚本,接收大于3的数字,并打印到该数字的斐波那契数列。不要使用递归函数。
难度级别:中等到困难
提示:斐波那契数列从0和1开始,下一项是前两项的和。
输出:用10来检查输出的结果,应该打印 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
练习 5:使用递归的斐波那契数列
编写shell脚本,接收大于3的数字,并打印到该数字的斐波那契数列。必须使用递归函数。
难度级别:困难
提示:斐波那契数列从0和1开始,下一项是前两项的和。
输出:用10来检查输出的结果,应该打印 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
练习题答案
答案 1:它是正整数吗
从简单的开始,重构前面的判断是正整数还是负整数的脚本,将其写在一个函数里面。
#!/bin/bash
read -p "Enter number: " num
ispositive () {
if [ "$1" -le 0 ]; then
echo "Number $1 is negative"
else
echo "Number $1 is positive"
fi
}
ispositive $num
答案 2:函数乘法表
写一个shell脚本,打印给定数字的乘法表。必须使用函数。
#!/bin/bash
read -p "Enter number: " num
table () {
for i in {1..10}; do
echo $(($1*$i))
done
}
table $num
答案 3:是素数吗
编写一个shell脚本,接收一个数字并检查它是否是一个素数(质数),必须使用函数。
#!/bin/bash
read -p "Enter number: " num
isprime () {
for((i=2; i<=num/2; i++)); do
if [ $((num%i)) -eq 0 ]; then
echo "$num is not a prime number."
exit
fi
done
echo "$num is prime number."
}
isprime $num
答案 4:斐波那契数列
编写shell脚本,接收大于3的数字,并打印到该数字的斐波那契数列。不要使用递归函数。
#!/bin/bash
read -p "Enter number greater than 3: " num
if [ "$num" -lt 3 ]; then
echo "Number must be greater than 3"
exit
fi
#initialize first and second terms
t1=0
t2=1
#initialize the next term (3rd term)
nextTerm=$(($t1 + $t2))
echo -n "$t1, $t2"
fibo () {
for ((i = 3; i <= $num; ++i)); do
echo -n ", $nextTerm";
t1=$t2;
t2=$nextTerm;
nextTerm=$(($t1 + $t2));
done
echo ""
}
fibo $num
答案 5:使用递归的斐波那契数列
编写shell脚本,接收大于3的数字,并打印到该数字的斐波那契数列。必须使用递归函数。
#!/bin/bash
read -p "Enter number greater than 3: " num
if [ "$num" -lt 3 ]; then
echo "Number must be greater than 3"
exit
fi
echo -n "Fibonacci series for $num terms is: "
fibo() {
if [ "$1" -le 1 ]; then
echo -n "$1"
else
echo -n $(( $(fibo $(($1 - 1)) ) + $(fibo $(($1 - 2)) ) ))
fi
}
for (( i=0; i<$num; ++i)); do
fibo $i
# Print , and space between the numbers except the last one
if [ "$i" -lt $(($num -1)) ]; then
echo -n ", "
fi
done
echo ""