#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.

传参给函数1

需要弄懂一个概念,脚本的参数和函数的参数是两个不同的东西,用下面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()

传参给函数2

现在可以理解了吧?同样是$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 ""