38·Shell 基础进阶

Shell 函数定义与使用

functionsbash

第 38 课 - Shell 函数定义与使用

所属模块: Shell 基础
难度: Intermediate
标签: functions, bash
上一课: for 与 while 循环
下一课: 输入输出重定向


1. 学习目标

完成本课学习后,你将能够:

  1. 掌握 Shell 函数的定义与调用语法。
  2. 理解并正确使用函数参数(位置参数)和特殊变量。
  3. 掌握函数返回值的两种处理方式(returnecho)。
  4. 理解函数内的变量作用域,并正确使用 local 关键字。
  5. 学会利用函数来组织、复用和简化 Shell 脚本。

2. 核心概念

为什么需要函数?

想象一下,你在脚本中需要多次执行“检查磁盘使用率并报警”的操作。如果每次都在主流程中写一大段代码,脚本会变得冗长且难以维护。函数(Function) 就像一个小工具或子程序,它将一系列命令封装起来,起个名字。需要时,只需要“呼叫”它的名字(即调用函数),就能执行那段封装好的代码。这带来了代码复用、模块化和可读性的巨大好处。

函数定义与调用

定义函数的通用语法有两种,推荐第一种:

# 语法一(推荐)
function_name () {
    # 函数体(要执行的命令)
    命令1
    命令2
    ...
}

# 语法二
function function_name {
    # 函数体
    命令1
    命令2
    ...
}

调用函数非常简单,只需在脚本中使用函数名即可。

function_name  # 调用函数

参数传递

函数可以像脚本一样接收参数。在函数内部,通过 $1, $2, $3, ... $N 来访问传入的第1个、第2个、第3个...第N个参数。$0 代表脚本名,但在函数中通常不使用。$# 代表传入参数的总个数。

返回值

Shell 函数有两种“返回”结果的方式:

  1. return 命令:返回一个整数状态码(0-255),通常用 0 表示成功,非 0 表示失败。我们可以通过 $? 来获取上一个命令(或函数)的返回状态码。
  2. echo 命令:通过标准输出打印一个字符串。这是返回字符串或计算结果的常用方式。调用函数时,通常使用命令替换 $( ) 来捕获这个输出。

变量作用域

默认情况下,函数内定义的变量是全局变量,会影响整个脚本。这可能在大型脚本中导致意外的变量覆盖。使用 local 关键字可以在函数内声明局部变量,它的作用域仅限于该函数内部及其调用的子函数。

3. 代码示例

示例1:基础函数 - 参数传递与返回值

#!/bin/bash

# 定义一个函数,用于计算两个数字的和
# 使用 echo 输出结果,而不是 return
calculate_sum () {
    # 检查是否提供了两个参数
    if [ $# -ne 2 ]; then
        echo "Error: 请提供两个数字参数"
        return 1 # 返回非0状态码表示错误
    fi

    local num1=$1
    local num2=$2
    local sum=$(( num1 + num2 ))

    echo $sum # 将结果输出到标准输出
}

# 定义一个函数,用于检查数字是否为偶数
# 使用 return 返回状态码(0: 是偶数,1: 不是)
is_even () {
    local num=$1
    if [ $(( num % 2 )) -eq 0 ]; then
        return 0 # 成功,是偶数
    else
        return 1 # 失败,不是偶数
    fi
}

# --- 主脚本开始 ---
echo "=== 测试 calculate_sum 函数 ==="
result=$(calculate_sum 10 20) # 使用命令替换捕获函数的输出
echo "10 + 20 = $result"

echo ""
echo "=== 测试 is_even 函数 ==="
number=7
if is_even $number; then
    echo "$number 是偶数"
else
    echo "$number 不是偶数" # 这里会执行
fi

number=12
is_even $number
echo "函数 is_even 对数字 $number 的返回状态码是: $?"

示例2:实用函数 - 简化日志记录

#!/bin/bash

# 定义一个日志函数,自动添加时间戳和级别
log_msg () {
    local level=$1
    local message=$2
    local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
    # 使用 local 定义局部变量,避免污染全局
    echo "[$timestamp] [$level] $message"
}

# 主脚本使用
log_msg "INFO" "脚本开始运行"
sleep 1 # 模拟一些操作
log_msg "WARNING" "磁盘空间使用率已达到 85%"
log_msg "INFO" "脚本执行结束"

输出将类似于:

[2023-10-27 14:30:45] [INFO] 脚本开始运行
[2023-10-27 14:30:46] [WARNING] 磁盘空间使用率已达到 85%
[2023-10-27 14:30:46] [INFO] 脚本执行结束

4. 实践练习

练习1:基本函数调用

编写一个函数 say_hello,它接受一个参数(名字),并输出问候语 “你好,[名字]!”。在主脚本中调用它两次,分别传入你的名字和“Linux”。

练习2:参数与返回值

编写一个函数 get_max,它接收两个数字作为参数,使用 echo 输出其中较大的那个数。编写主脚本调用 get_max 45 78 并打印结果 “最大的数字是:78”

练习3:综合应用(挑战)

编写一个脚本,包含一个函数 check_service,该函数接收一个服务名(如 nginx, sshd)作为参数。 函数功能:

  1. 使用 systemctl is-active $service_name 命令检查服务状态。
  2. 根据命令的返回状态码($?),使用 log_msg 函数(参考示例2)输出一条日志,格式为 INFOWARNING。 主脚本中,依次调用 check_service 函数检查 sshdcups 服务的状态。

5. 常见错误

  1. 函数定义后忘记调用:定义了函数,但在脚本中没有写下调用它的语句,函数体内的代码永远不会执行。

    my_function () {
        echo "这个不会显示"
    }
    # 下面这行不能少!
    my_function
    
  2. 混淆 returnecho 的用途

    • 试图用 return 返回一个字符串或计算结果(如 return “hello”return $((a+b)))是错误的,return 只能返回 0-255 的整数状态码。
    • 应该用 echo 输出结果,然后用 $(函数名) 来捕获。
  3. 变量作用域污染:在函数内意外使用了全局变量,或者没有用 local 声明,导致函数内部的操作影响了外部的同名变量。

    value=10
    my_function () {
        value=20 # 这里修改了全局变量 value
        echo "函数内部 value: $value"
    }
    my_function
    echo "函数外部 value: $value" # 输出 20,可能不是预期行为
    
  4. 参数传递错误:调用函数时忘记传递参数,或者传递的参数顺序不对,导致函数逻辑出错。建议在函数开头对参数数量进行检查(if [ $# -eq 0 ])。

6. 小结

  • 函数是脚本组织的基石:它通过封装代码块,实现了逻辑复用、模块化和更好的可读性。
  • 定义与调用:使用 function_name() { ... } 定义,直接使用 function_name 调用。
  • 参数与返回值
    • 参数通过 $1, $2$# 访问。
    • 整数状态码用 return,通过 $? 获取。
    • 字符串或复杂数据用 echo 输出,通过 $( ) 捕获。
  • 变量作用域:默认为全局。在函数内声明变量时,务必使用 local 关键字,以避免难以调试的变量冲突问题。

掌握函数,你就拥有了编写健壮、可维护的 Shell 脚本的关键能力。在接下来的运维脚本编写中,尝试识别并封装重复的逻辑为函数吧!

练习编辑器

rust
Loading...

继续学习

完成本课后,建议继续学习下一课「输入输出重定向」 以巩固所学知识。