linux shell编程实战 06 函数与模块化复用

在编写复杂脚本时,我们常常会遇到重复使用的代码块(如多次验证输入、反复执行相同的文件操作等)。如果每次都重复编写这些代码,不仅冗余,还会导致脚本难以维护。函数(Function)正是为解决这类问题而设计的——它可以将一段代码封装成一个独立的模块,通过名称重复调用,实现代码的复用和模块化。本章将详细介绍Shell函数的定义、调用、参数传递及实战应用,帮助你编写更简洁、易维护的脚本。

6.1 函数的基本概念:什么是函数及为什么需要函数

6.1.1 函数的定义

函数是一组预先定义好的命令集合,通过一个名称来标识,需要时可以通过名称调用这组命令。

可以用生活中的例子理解函数:

  • 函数 = 一个“工具”(如螺丝刀、计算器)
  • 函数名 = 工具的名称(如“十字螺丝刀”)
  • 函数体 = 工具的使用步骤(如“对准螺丝→旋转→拧紧”)
  • 调用函数 = 使用工具(需要时拿出来用,不用重复造工具)

6.1.2 为什么需要函数

假设编写一个脚本时,需要3次验证用户输入是否为数字,不使用函数的写法是:

# 第一次验证
read num1
if ! [[ $num1 =~ ^[0-9]+$ ]]; then
    echo "错误:必须输入数字"
    exit 1
fi

# 第二次验证(重复代码)
read num2
if ! [[ $num2 =~ ^[0-9]+$ ]]; then
    echo "错误:必须输入数字"
    exit 1
fi

# 第三次验证(重复代码)
read num3
if ! [[ $num3 =~ ^[0-9]+$ ]]; then
    echo "错误:必须输入数字"
    exit 1
fi

这段代码中,验证逻辑被重复了3次,修改时需要改3处,效率低下。

使用函数的写法:

# 定义验证函数(一次编写,多次使用)
validate_number() {
    local input=$1
    if ! [[ $input =~ ^[0-9]+$ ]]; then
        echo "错误:必须输入数字"
        return 1
    fi
    return 0
}

# 调用函数(简洁,只需一行)
read num1
validate_number "$num1" || exit 1

read num2
validate_number "$num2" || exit 1

read num3
validate_number "$num3" || exit 1

函数的核心优势

  • 代码复用:一次定义,多次调用,减少重复代码
  • 模块化:将复杂脚本拆分成多个函数,逻辑更清晰
  • 易维护:修改函数即可影响所有调用处,无需逐个修改
  • 可读性:函数名能直观表达功能(如validate_number一眼就知道是验证数字)

6.2 函数的定义与调用:创建和使用你的第一个函数

Shell函数的定义和调用非常灵活,语法简单,无需声明返回值类型。

6.2.1 函数的定义方式

Shell中定义函数有两种常用格式:

格式1:标准格式(推荐)

函数名() {
    # 函数体:要执行的命令
}

格式2:带function关键字

function 函数名 {
    # 函数体:要执行的命令
}

说明

  • 函数名的命名规则与变量相同(字母、数字、下划线,不能以数字开头)
  • 函数体用{}包裹,{后和}前建议留空格
  • 函数必须先定义后调用(通常放在脚本开头)

示例:定义一个简单的函数

#!/bin/bash

# 定义函数(标准格式)
say_hello() {
    echo "Hello, World!"
}

# 定义函数(带function关键字)
function say_goodbye {
    echo "Goodbye!"
}

6.2.2 函数的调用

调用函数只需使用函数名即可,不需要加括号(与其他编程语言不同)。

语法

函数名  # 直接写函数名即可调用

示例:调用上面定义的函数

#!/bin/bash

# 定义函数
say_hello() {
    echo "Hello, World!"
}

function say_goodbye {
    echo "Goodbye!"
}

# 调用函数
echo "第一次调用:"
say_hello
say_goodbye

echo -e "\n第二次调用:"
say_hello
say_goodbye

输出结果

第一次调用:
Hello, World!
Goodbye!

第二次调用:
Hello, World!
Goodbye!

注意:函数必须在调用前定义,否则会报错“command not found”。

6.3 函数的参数传递:向函数传递数据

和脚本接收命令行参数一样,函数也可以接收参数,通过$1、$2、$#等特殊变量访问。

6.3.1 传递和访问参数

调用函数时传递参数

函数名 参数1 参数2 参数3 ...

函数内部访问参数

  • $1:第一个参数
  • $2:第二个参数
  • $#:参数个数
  • $*/$@:所有参数
  • $0:当前脚本名(注意:不是函数名)

示例:带参数的函数

#!/bin/bash

# 定义一个打印用户信息的函数
print_user_info() {
    local name=$1  # 第一个参数:姓名
    local age=$2   # 第二个参数:年龄
    local city=$3  # 第三个参数:城市
    
    echo "姓名:$name"
    echo "年龄:$age"
    echo "城市:$city"
    echo "参数总数:$#"
}

# 调用函数并传递参数
echo "用户1信息:"
print_user_info "张三" 25 "北京"

echo -e "\n用户2信息:"
print_user_info "李四" 30 "上海"

输出结果

用户1信息:
姓名:张三
年龄:25
城市:北京
参数总数:3

用户2信息:
姓名:李四
年龄:30
城市:上海
参数总数:3

6.3.2 参数验证

函数中通常需要验证参数是否符合要求(如数量是否足够、格式是否正确):

示例:带参数验证的函数

#!/bin/bash

# 定义计算两数之和的函数
add() {
    # 验证参数个数
    if [ $# -ne 2 ]; then
        echo "错误:需要2个参数,实际收到$#个"
        return 1  # 返回非0状态码表示错误
    fi
    
    local a=$1
    local b=$2
    
    # 验证参数是否为数字
    if ! [[ $a =~ ^[0-9]+$ ]] || ! [[ $b =~ ^[0-9]+$ ]]; then
        echo "错误:参数必须是数字"
        return 1
    fi
    
    # 计算并返回结果
    echo $((a + b))
    return 0  # 返回0表示成功
}

# 测试函数
echo "测试1(正确参数):"
add 10 20

echo -e "\n测试2(参数不足):"
add 10

echo -e "\n测试3(非数字参数):"
add 10 "abc"

输出结果

测试1(正确参数):
30

测试2(参数不足):
错误:需要2个参数,实际收到1个

测试3(非数字参数):
错误:参数必须是数字

6.4 函数的返回值:获取函数执行结果

Shell函数的“返回值”有两种形式:状态码返回(用于判断成功/失败)和结果输出(用于返回具体数据),初学者容易混淆,需要重点区分。

6.4.1 状态码返回(return命令)

return命令用于返回状态码(0-255的整数),表示函数执行的“成功”或“失败”:

  • return 0:表示成功(默认返回值)
  • return n(n≠0):表示失败(n为错误码)

状态码可以通过$?变量获取。

示例:用return返回状态码

#!/bin/bash

# 定义检查文件是否存在的函数
file_exists() {
    local file=$1
    
    if [ -z "$file" ]; then
        echo "错误:未指定文件"
        return 1  # 错误码1:参数为空
    fi
    
    if [ -f "$file" ]; then
        return 0  # 成功:文件存在
    else
        echo "错误:$file 不存在"
        return 2  # 错误码2:文件不存在
    fi
}

# 调用函数并检查返回值
file_exists "test.txt"
result=$?  # 获取状态码

if [ $result -eq 0 ]; then
    echo "操作成功"
else
    echo "操作失败,错误码:$result"
fi

输出结果(如果test.txt不存在):

错误:test.txt 不存在
操作失败,错误码:2

6.4.2 结果输出(echo命令)

如果函数需要返回具体数据(如计算结果、字符串),通常使用echo输出结果,然后通过命令替换$(函数名))获取。

示例:用echo返回具体结果

#!/bin/bash

# 定义计算平方的函数
square() {
    local num=$1
    echo $((num * num))  # 输出计算结果
}

# 调用函数并获取结果(用命令替换)
result=$(square 5)
echo "5的平方是:$result"

# 直接在表达式中使用
echo "10的平方是:$(square 10)"

输出结果

5的平方是:25
10的平方是:100

6.4.3 两种返回方式的区别与应用

场景推荐方式示例
判断函数是否执行成功return 状态码if func; then ...
返回具体数据(数字、字符串等)echo 输出 + 命令替换result=$(func)

最佳实践

  • 状态码仅用于表示“成功/失败”,不携带具体数据
  • 复杂结果(如多个值)可以用字符串拼接后echo输出,再拆分使用
  • 避免在函数中随意echo无关信息(会混入返回结果)

6.5 函数中的变量:作用域与生命周期

函数中的变量按作用域分为全局变量局部变量,理解它们的区别对编写正确的函数至关重要。

6.5.1 全局变量

定义:在函数外部定义的变量,或在函数内部未用local声明的变量。

特点

  • 在脚本的任何地方(包括所有函数内部)都能访问和修改
  • 生命周期与脚本一致(脚本结束后消失)

示例:全局变量的使用

#!/bin/bash

# 全局变量(函数外部定义)
global_var="全局变量初始值"

# 定义函数修改全局变量
modify_global() {
    global_var="被函数修改后的值"  # 未用local,仍是全局变量
    echo "函数内:global_var = $global_var"
}

echo "函数调用前:global_var = $global_var"
modify_global
echo "函数调用后:global_var = $global_var"  # 全局变量已被修改

输出结果

函数调用前:global_var = 全局变量初始值
函数内:global_var = 被函数修改后的值
函数调用后:global_var = 被函数修改后的值

6.5.2 局部变量

定义:在函数内部用local关键字声明的变量。

特点

  • 仅在当前函数内部有效,函数外部无法访问
  • 函数执行结束后自动销毁(释放内存)
  • 避免与全局变量或其他函数的变量重名

语法

local 变量名=

示例:局部变量的使用

#!/bin/bash

# 全局变量
count=10

# 定义函数使用局部变量
use_local() {
    local count=20  # 局部变量,与全局变量同名但不冲突
    echo "函数内(局部变量):count = $count"
}

echo "函数调用前(全局变量):count = $count"
use_local
echo "函数调用后(全局变量):count = $count"  # 全局变量未被修改

输出结果

函数调用前(全局变量):count = 10
函数内(局部变量):count = 20
函数调用后(全局变量):count = 10

最佳实践

  • 函数内部的临时变量尽量用local声明(避免污染全局变量)
  • 函数之间共享数据优先通过参数和返回值,而非全局变量
  • 全局变量用于存储脚本级别的配置或状态(如日志路径、是否开启调试模式)

6.6 函数的嵌套与递归:函数调用函数

函数可以调用其他函数(嵌套),甚至调用自身(递归),这让复杂逻辑的实现成为可能。

6.6.1 函数嵌套(函数调用函数)

函数嵌套是指一个函数内部调用另一个函数,这是实现模块化的常用方式。

示例:函数嵌套

#!/bin/bash

# 子函数:计算平方
square() {
    local num=$1
    echo $((num * num))
}

# 子函数:计算和
add() {
    local a=$1
    local b=$2
    echo $((a + b))
}

# 主函数:计算 (a² + b²)
sum_of_squares() {
    local a=$1
    local b=$2
    
    # 调用其他函数(嵌套)
    local a_sq=$(square $a)
    local b_sq=$(square $b)
    local result=$(add $a_sq $b_sq)
    
    echo $result
}

# 调用主函数
echo "计算 (3² + 4²) = $(sum_of_squares 3 4)"  # 结果应为25

输出结果

计算 (3² + 4²) = 25

6.6.2 函数递归(函数调用自身)

递归是指函数调用自身,通常用于解决可以分解为相同子问题的任务(如阶乘、斐波那契数列)。

示例:递归计算阶乘(n! = n × (n-1) × … × 1)

#!/bin/bash

# 递归函数:计算阶乘
factorial() {
    local n=$1
    
    # 基线条件(终止递归的条件)
    if [ $n -eq 1 ] || [ $n -eq 0 ]; then
        echo 1
        return
    fi
    
    # 递归调用(n! = n × (n-1)!)
    local prev=$(factorial $((n - 1)))
    echo $((n * prev))
}

# 测试函数
echo "5的阶乘:$(factorial 5)"  # 5! = 5×4×3×2×1 = 120
echo "3的阶乘:$(factorial 3)"  # 3! = 6

输出结果

5的阶乘:120
3的阶乘:6

注意

  • 递归必须有基线条件(停止递归的条件),否则会无限递归导致脚本崩溃
  • Shell对递归深度有限制(通常不超过1000层),复杂递归任务建议用其他语言

6.7 实战示例:多功能文件处理工具

综合运用函数知识,编写一个包含多个功能的文件处理工具,实现以下功能:

  • 创建文件(带内容)
  • 复制文件(带备份)
  • 查看文件信息(大小、修改时间)
  • 批量操作(对多个文件执行相同命令)
#!/bin/bash
# 多功能文件处理工具

# 函数:显示帮助信息
show_help() {
    echo "用法:$0 [选项] 文件名/目录..."
    echo "文件处理工具,支持以下功能:"
    echo "  -c 内容   创建文件并写入内容"
    echo "  -cp 目标   复制文件(自动备份原有文件)"
    echo "  -info      显示文件信息(大小、修改时间)"
    echo "  -batch 命令 对目录下所有文件执行指定命令"
    echo "  -h         显示帮助信息"
}

# 函数:创建文件并写入内容
create_file() {
    local filename=$1
    local content=$2
    
    if [ -z "$filename" ] || [ -z "$content" ]; then
        echo "错误:创建文件需要文件名和内容"
        return 1
    fi
    
    # 如果文件已存在,提示确认
    if [ -e "$filename" ]; then
        read -p "$filename 已存在,是否覆盖?(y/n):" confirm
        if [ "$confirm" != "y" ]; then
            echo "取消创建"
            return 0
        fi
    fi
    
    # 写入内容
    echo "$content" > "$filename"
    echo "文件创建成功:$filename"
    return 0
}

# 函数:复制文件(带备份)
copy_file() {
    local src=$1
    local dest=$2
    
    if [ -z "$src" ] || [ -z "$dest" ]; then
        echo "错误:复制需要源文件和目标文件"
        return 1
    fi
    
    if [ ! -e "$src" ]; then
        echo "错误:源文件 $src 不存在"
        return 1
    fi
    
    # 如果目标文件已存在,创建备份
    if [ -e "$dest" ]; then
        local backup="${dest}.bak"
        cp "$dest" "$backup"
        echo "已备份目标文件到:$backup"
    fi
    
    # 执行复制
    cp "$src" "$dest"
    echo "复制成功:$src$dest"
    return 0
}

# 函数:显示文件信息
show_file_info() {
    local file=$1
    
    if [ -z "$file" ] || [ ! -e "$file" ]; then
        echo "错误:文件 $file 不存在"
        return 1
    fi
    
    # 获取文件信息
    local size=$(du -h "$file" | awk '{print $1}')  # 大小
    local mtime=$(stat -c "%y" "$file" | cut -d' ' -f1-2)  # 修改时间
    
    echo "文件信息:$file"
    echo "  大小:$size"
    echo "  修改时间:$mtime"
    echo "  类型:$(file "$file" | cut -d: -f2-)"  # 文件类型
    return 0
}

# 函数:批量处理文件
batch_process() {
    local dir=$1
    local cmd=$2
    
    if [ -z "$dir" ] || [ -z "$cmd" ] || [ ! -d "$dir" ]; then
        echo "错误:批量处理需要有效目录和命令"
        return 1
    fi
    
    echo "对 $dir 下的文件执行命令:$cmd"
    for file in "$dir"/*; do
        if [ -f "$file" ]; then  # 只处理普通文件
            echo -e "\n处理文件:$file"
            eval "$cmd \"$file\""  # eval执行命令(注意转义)
        fi
    done
    return 0
}

# 主逻辑:解析参数并调用对应函数
if [ $# -eq 0 ]; then
    show_help
    exit 0
fi

# 解析选项
case $1 in
    -c)
        create_file "$2" "$3"
        ;;
    -cp)
        copy_file "$2" "$3"
        ;;
    -info)
        show_file_info "$2"
        ;;
    -batch)
        batch_process "$2" "$3"
        ;;
    -h)
        show_help
        ;;
    *)
        echo "错误:未知选项 $1"
        show_help
        exit 1
        ;;
esac

脚本说明

  1. 将不同功能封装为独立函数(create_filecopy_file等),每个函数专注于单一任务
  2. 函数内部有参数验证,确保输入合法
  3. 使用局部变量(local)避免命名冲突
  4. 主逻辑仅负责解析命令行参数,然后调用对应函数,结构清晰
  5. 支持创建文件、复制文件(带备份)、查看文件信息、批量处理等实用功能

运行示例

# 创建文件
./file_tool.sh -c test.txt "这是测试内容"

# 复制文件
./file_tool.sh -cp test.txt test_copy.txt

# 查看文件信息
./file_tool.sh -info test.txt

# 批量查看目录下文件信息
./file_tool.sh -batch ./docs "ls -l"

小结

本章详细介绍了Shell函数的使用,主要内容包括:

  • 函数的基本概念:函数是封装的命令集合,用于实现代码复用和模块化
  • 函数的定义与调用:两种定义格式(函数名()function 函数名),调用时直接使用函数名
  • 参数传递:通过$1、$2、$#等特殊变量访问参数,支持参数验证
  • 返回值
    • return:返回状态码(0表示成功,非0表示失败)
    • echo+命令替换:返回具体数据(数字、字符串等)
  • 变量作用域
    • 全局变量:脚本内所有地方可访问
    • 局部变量:用local声明,仅在函数内部有效
  • 函数嵌套与递归:函数可调用其他函数(嵌套)或自身(递归)

函数是编写复杂Shell脚本的必备工具,它能让代码更简洁、易维护、可扩展。学习的关键是理解函数的封装思想——将复杂任务拆分成多个小函数,每个函数只做一件事,然后通过函数调用组合实现整体功能。

到本章为止,我们已经学习了Shell编程的核心基础知识(变量、条件判断、循环、数组、函数)。这些知识足以编写大多数日常所需的脚本,解决实际工作中的自动化问题。后续可以结合具体需求,进一步学习更高级的技巧和工具(如文本三剑客grep、sed、awk)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云计算练习生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值