在编写复杂脚本时,我们常常会遇到重复使用的代码块(如多次验证输入、反复执行相同的文件操作等)。如果每次都重复编写这些代码,不仅冗余,还会导致脚本难以维护。函数(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
脚本说明:
- 将不同功能封装为独立函数(
create_file、copy_file等),每个函数专注于单一任务 - 函数内部有参数验证,确保输入合法
- 使用局部变量(
local)避免命名冲突 - 主逻辑仅负责解析命令行参数,然后调用对应函数,结构清晰
- 支持创建文件、复制文件(带备份)、查看文件信息、批量处理等实用功能
运行示例:
# 创建文件
./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)。
562

被折叠的 条评论
为什么被折叠?



