Shell函数

编写shell脚本时,经常需要在多个地方使用了同一段代码。如果只是一小段代码,一般也无关紧要。但要在shell脚本中多次重写大块代码段就太累人了。bash shell提供的用户自定义函数功能可以解决这个问题。可以将shell脚本代码放进函数中封装起来,这样就能在脚本中的任何地方多次使用。除了代码重用,在shell中使用函数可以有以下优点:

  1. 代码重用:在脚本的不同部分多次调用,避免了代码的冗余。
  2. 模块化:将复杂脚本分解成多个独立模块,提高代码的可维护性和可读性。
  3. 可测试性:便于对各个功能编写单元测试,提高脚本质量。

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

基本脚本函数

创建函数、调用函数

  1. 第一种方式采用function关键字,后面跟函数名以及代码块

    function name {
        commands
    }

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

  2. 第二种方式更接近其他编程语言的定义方式

    name() {
        commands
    }
#!/bin/bash
# 演示函数定义与调用

# 注意:直接在这里调用 greet 会报错,因为函数尚未定义
# greet

greet() {
    echo "[提示] 你好,欢迎学习 Shell 函数!"
}

# 正确的调用方式:在定义之后调用
greet
  • 当函数在被定义之前使用的话会产生错误信息
#!/bin/bash
# 演示函数在脚本中的位置以及调用顺序

count=1
echo "[提示] 这一行在函数定义之前执行"

func1() {
    echo "[函数1] 这是一个函数示例,正在执行第 $count 次循环"
}

while [ $count -le 3 ]
do
    func1
    count=$((count + 1))
done

echo "[完成] 循环运行结束"

# 下面演示如果调用未定义的函数会发生什么
# func2 

echo "[完成] 脚本运行结束"

func2() {
    echo "[函数2] 脚本末尾定义的函数"
}

作业1:修改以下代码,通过函数实现模拟掷骰子的功能。

#!/bin/bash

sides=6
result=$((RANDOM % sides + 1))
echo "You rolled a $result!"

返回值

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

默认退出状态码

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

#!/bin/bash
# 演示函数的默认退出状态码

check_file() {
    # 模拟一个失败的命令
    ls -l badfile 2>/dev/null
    echo "[函数运行中] 已尝试列出文件"
}

check_file
# $? 总是返回函数中最后一条执行完毕的命令的退出状态码
echo "[结果] 函数的退出状态码为: $?"

如果注释掉函数末尾的 echo 语句,由于前面的 ls 命令执行失败,该函数的退出状态码将是非零值。反之,若最后一条语句执行成功,状态码则为 0

使用return命令

bash shell使用return命令来退出函数并返回特定的退出状态码。return命令允许指定一个整数值来定义函数的退出状态码

#!/bin/bash
# 演示使用 return 命令返回结果

dbl() {
    read -p "[交互] 请输入一个数值: " value
    echo "[计算中] 正在执行翻倍运算..."
    return $((value * 2))
}
dbl
# 注意:$? 的取值范围是 0-255,超过 255 会产生溢出(结果不对)
echo "[结果] 函数通过 return 返回的值等于 $?"

运行脚本对比:

ubuntu@sh:~$ bash test.sh
[交互] 请输入一个数值: 100
[计算中] 正在执行翻倍运算...
[结果] 函数通过 return 返回的值等于 200

# 演示溢出:200 * 2 = 400 (400 - 256 = 144)
ubuntu@sh:~$ bash test.sh
[交互] 请输入一个数值: 200
[计算中] 正在执行翻倍运算...
[结果] 函数通过 return 返回的值等于 144

使用函数输出

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

result=`dbl`

这个命令会将dbl函数的输出赋给$result变量。

#!/bin/bash
# 演示通过 echo 输出将函数结果保存到变量中

calc_double() {
    read -p "[交互] 请输入需要翻倍的数值: " value
    # echo 的输出可以被命令替换 $(...) 捕获
    echo $((value * 2))
}

# 调用函数并将输出结果赋给变量 result
result=$(calc_double)

echo "[结果] 函数计算得到的翻倍值为:$result"
ubuntu@sh:~$ bash test.sh
[交互] 请输入需要翻倍的数值: 200
[结果] 函数计算得到的翻倍值为:400

作业2:编写以上脚本使用命令替换保存函数输出。

函数中使用变量

向函数传递参数

bash shell会将函数当作小型脚本来对待。这意味着可以像普通脚本那样向函数传递参数。

函数可以使用标准的参数环境变量来表示命令行上传给函数的参数。例如,函数名会在$0变量中定义,函数命令行上的任何参数都会通过$1$2等定义。也可以用特殊变量$#来判断传给函数的参数数目。

#!/bin/bash

# 定义一个函数,接收两个参数并输出它们
print_two_args() {
    echo "[参数] 第一个参数是:$1"
    echo "[参数] 第二个参数是:$2"
    echo "[状态] 参数总量:$#"
}

# 调用函数并传递两个参数
print_two_args "参数1" "参数2"

}

调用函数,并传递两个参数

print_two_args “hello” “world”


```shell
第一个参数是:hello
第二个参数是:world

在函数中处理变量

作用域是变量可见的区域。函数中定义的变量与普通变量的作用域不同。也就是说,对脚本的其他部分而言,它们是隐藏的。

全局变量

对于全局变量,可以直接在函数中使用或修改。例如:

#!/bin/bash

# 定义全局变量
name="Tom"

# 定义函数
my_func() {
  echo "Hello, $name"
  name="Jerry"
}

# 调用函数
my_func
echo "Name is $name" # 输出 "Name is Jerry"

局部变量

#!/bin/bash

# 定义函数
my_func() {
  # 定义局部变量
  local count=0

  # 修改局部变量
  count=$((count + 1))

  # 输出局部变量
  echo "Count is $count"
}

# 调用函数
my_func # 输出 "Count is 1"

需要注意的是,在函数中使用局部变量时,变量名前必须加上 **local**关键字,否则会被认为是全局变量。如果函数中定义了一个与全局变量同名的局部变量,则在函数中访问该变量时,会优先使用局部变量。

#!/bin/bash
# 在函数中使用局部变量不会影响到脚本主体中的全局变量的值
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

作业1:编写以上脚本,掌握函数内局部变量的定义与作用范围。

在命令行上使用函数

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

在命令行上创建函数

  • 单行方式定义函数

    ubuntu@sh:~$ addem() { echo $(($1 + $2)); }
    ubuntu@sh:~$ addem 1 2
    3
  • 多行方式定义函数

    ubuntu@sh:~$ multem() {
    > echo $(($1 * $2))
    > }
    ubuntu@sh:~$ multem 2 4
    8
    ubuntu@sh:~$

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

在.bashrc文件中定义函数

在命令行上直接定义shell函数的明显缺点是退出shell时,函数就消失了,这对于一些复杂函数并不适用。此时可以将函数写入一个特定位置并且在shell启动时由shell重新载入。.bashrc文件会在每次shell启动时在主目录下被查找并调用,因此可以在主目录下的.bashrc文件中定义函数。

将以下内容写入到.bashrc文件的末尾:

addem() {
echo $(($1 + $2))
}

创建库

通过创建函数库文件可以方便在多个脚本中使用同一段代码。以下代码保存在myfuncs文件中:

addem() {
    echo $(($1 + $2))
}
multem() {
    echo $(($1 * $2))
}
divem() {
    if [ "$2" -ne 0 ]
    then
        echo $(($1 / $2))
    else
        echo -1
    fi
}

在另一个脚本中执行myfuncs

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

输出信息

addem: command not found
The result is

使用函数库的关键在于source命令。source命令会在当前shell上下文中执行命令,而不是创建一个新shell。可以用source命令来在shell脚本中运行库文件脚本。这样脚本就可以使用库中的函数了。

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

作业2:编写以上脚本掌握库文件的使用