Linux Shell 函数完全指南:从基础定义到工程化复用
概述
在编写 Shell 脚本时,你是否遇到过这些问题?
- 同一段代码在脚本中重复出现(如日志打印、参数校验),修改时需同步改多处;
- 脚本逻辑混乱,几百行代码没有分层,难以定位问题;
- 多个脚本需要使用相同功能(如计算阶乘、批量文件处理),只能复制粘贴代码。
Shell 函数 正是为解决这些问题而生 —— 它将一段独立功能的代码封装成 “可调用的模块”,实现 “一次定义,多次调用”,让脚本更简洁、易维护、可复用。
简单来说,函数的核心价值是:
- 去冗余:重复代码只写一次,减少维护成本;
- 强内聚:将复杂逻辑拆分为多个小函数,脚本结构更清晰;
- 高复用:可在多个脚本中引用函数,甚至在命令行直接使用。
一、Shell 函数基础:定义与调用
Shell 函数的定义语法灵活,调用方式与普通命令一致,先掌握最核心的 “定义 - 调用” 流程。
1. 两种定义语法(推荐第二种)
Shell 支持两种函数定义格式,功能完全一致,可根据习惯选择:
格式 1:function 关键字定义(直观易懂)
# 语法:function 函数名 { 命令序列; }
function print_hello {
echo "Hello, Shell Function!"
}
格式 2:括号简化定义(更简洁,推荐)
更接近 Python、C 等语言的函数风格,用 () 标识函数,无需 function 关键字:
# 语法:函数名() { 命令序列; }
print_hello() {
echo "Hello, Shell Function!"
}
定义注意事项
- 函数名必须唯一:若重复定义同名函数,后定义的会覆盖前一个(无报错提示);
- 命令序列需用 {} 包裹:左括号 { 后必须加空格或换行,右括号 } 可单独成行;
- 函数需先定义后调用:若在函数定义前调用,Shell 会提示 “命令未找到”(见下文示例)。
2. 调用函数:像用命令一样简单
定义函数后,直接输入函数名即可调用,无需加括号(与其他语言不同),支持在循环、条件判断中调用。
示例 1:基础调用
#!/bin/bash
# 定义函数
print_hello() {
echo "Hello, Shell Function!"
}
# 调用函数(直接写函数名)
print_hello
执行效果:
Hello, Shell Function!
示例 2:循环中调用函数
#!/bin/bash
# 定义函数:打印当前迭代次数
print_iter() {
echo "第 $count 次调用函数"
}
# 循环调用 5 次
count=1
while [ $count -le 5 ]; do
print_iter # 调用函数
count=$[count + 1]
done
执行效果:
第 1 次调用函数
第 2 次调用函数
第 3 次调用函数
第 4 次调用函数
第 5 次调用函数
错误示例:未定义先调用
#!/bin/bash
# 错误:在函数定义前调用
print_hello # 此时 Shell 还未识别 print_hello 是函数
# 定义函数
print_hello() {
echo "Hello, Shell Function!"
}
执行效果:
./test.sh: line 2: print_hello: command not found # 报错:命令未找到
二、函数的返回值:3 种核心方式
Shell 函数不像其他语言那样直接用 return 返回任意值,需通过 “退出状态码”“输出捕获” 等方式获取结果,其中 输出捕获 是最常用、最灵活的方式。
1. 方式 1:默认退出状态码(不推荐)
Shell 函数默认将 “最后一条命令的退出状态码” 作为自身返回值,通过 $? 变量获取(0 表示成功,非 0 表示失败)。
示例:默认返回值的问题
#!/bin/bash
# 定义函数:执行 ls 查看不存在的文件,再打印提示
func_test() {
ls -l badfile # 这条命令执行失败(文件不存在),退出状态码为 1
echo "函数执行结束" # 这条命令执行成功,退出状态码为 0
}
# 调用函数
func_test
# 获取函数返回值(实际是最后一条命令的状态码)
echo "函数返回值:$?"
执行效果:
ls: cannot access 'badfile': No such file or directory
函数执行结束
函数返回值:0 # 虽然后续命令失败,但最后一条 echo 成功,返回 0
问题:无法准确判断函数整体执行结果,仅能获取最后一条命令的状态,实用性极低。
2. 方式 2:return 命令返回状态码(有限制)
用 return 数值 手动指定函数的退出状态码,需满足 0 ≤ 数值 ≤ 255(超出范围会自动取模,如 return 300 实际返回 44),仅适合返回 “成功 / 失败标识” 或小数值。
示例:用 return 返回计算结果
#!/bin/bash
# 定义函数:返回输入值的 2 倍(限 0-255)
double_num() {
read -p "请输入一个数字(0-127):" num
return $[num * 2] # 返回 2 倍值
}
# 调用函数
double_num
# 获取返回值(必须立即获取,否则会被后续命令覆盖)
echo "结果:$?"
执行效果(输入 100):
请输入一个数字(0-127):100
结果:200 # 100*2=200,在 0-255 范围内,正确返回
执行效果(输入 200):
请输入一个数字(0-127):200
结果:144 # 200*2=400,400-256=144(超出范围取模),结果错误
限制:无法返回字符串或超过 255 的数值,仅适合简单场景。
3. 方式 3:捕获函数输出(推荐,灵活通用)
将函数的 “标准输出(echo 打印的内容)” 通过 命令替换($(函数名))捕获到变量中,支持返回任意类型(字符串、大数值、浮点数),是生产环境的首选方式。
示例 1:返回大数值
#!/bin/bash
# 定义函数:计算输入值的 2 倍(用 echo 输出结果)
double_num() {
read -p "请输入一个数字:" num
echo $[num * 2] # 用 echo 输出结果,供外部捕获
}
# 捕获函数输出到变量
result=$(double_num)
echo "结果:$result"
执行效果(输入 1000):
请输入一个数字:1000
结果:2000 # 支持大数值,无范围限制
示例 2:返回字符串
#!/bin/bash
# 定义函数:拼接输入的姓和名
full_name() {
read -p "请输入姓氏:" last_name
read -p "请输入名字:" first_name
echo "${last_name}${first_name}" # 输出拼接后的字符串
}
# 捕获函数输出
name=$(full_name)
echo "您的全名是:$name"
执行效果:
请输入姓氏:张
请输入名字:三
您的全名是:张三
三、函数与变量:作用域与参数传递
在函数中使用变量时,需注意 “作用域”(变量的有效范围)和 “参数传递”(向函数传值),这是避免变量污染、实现函数灵活调用的关键。
1. 向函数传递参数:用位置变量
Shell 函数不支持 “显式参数列表”(如 func(a, b)),而是通过 位置变量($1、$2、$# 等)接收外部传入的参数,与脚本接收命令行参数的方式完全一致。
位置变量说明
变量 | 含义 |
---|---|
$0 | 函数名(而非脚本名) |
$1~$n | 第 1 到第 n 个参数(如 $1 是第一个参数,$2 是第二个参数) |
$# | 传入函数的参数总数 |
$@ | 所有参数的列表(保留参数中的空格,推荐用 "$@" 包裹) |
$* | 所有参数合并为一个字符串(不保留空格,不推荐) |
示例:函数接收多个参数并计算
#!/bin/bash
# 定义函数:计算参数的和(支持 1-2 个参数)
add_num() {
# 判断参数个数
if [ $# -eq 0 ] || [ $# -gt 2 ]; then
echo "错误:请传入 1 或 2 个参数"
return 1 # 返回错误状态码
elif [ $# -eq 1 ]; then
echo $[ $1 + $1 ] # 1 个参数:自身相加
else
echo $[ $1 + $2 ] # 2 个参数:两者相加
fi
}
# 调用函数(传入不同参数)
echo "传入 10 和 15:$(add_num 10 15)"
echo "传入 20:$(add_num 20)"
echo "传入 3 个参数:$(add_num 10 20 30)"
执行效果:
传入 10 和 15:25
传入 20:40
传入 3 个参数:错误:请传入 1 或 2 个参数
注意:脚本参数与函数参数的区别
函数的位置变量仅作用于函数内部,与脚本的命令行参数($1、$2)完全独立,需手动传递脚本参数到函数:
#!/bin/bash
# 定义函数:计算两个数的乘积
mult_num() {
echo $[ $1 * $2 ] # 函数的 $1、$2 是传入的参数
}
# 脚本接收命令行参数(如 ./test.sh 10 20)
if [ $# -ne 2 ]; then
echo "用法:$0 数字1 数字2"
exit 1
fi
# 将脚本的命令行参数($1、$2)传给函数
result=$(mult_num $1 $2)
echo "乘积:$result"
执行效果(./test.sh 10 20):
乘积:200
2. 函数中变量的作用域:全局 vs 局部
默认情况下,函数中定义的变量是 全局变量(脚本内任何地方都能访问),可能导致 “变量污染”(函数修改外部变量)。用 local 关键字可定义 局部变量(仅函数内部有效),推荐优先使用局部变量。
示例 1:全局变量的问题(变量污染)
#!/bin/bash
# 定义函数:修改 temp 变量
func_test() {
temp=$[temp + 5] # temp 是全局变量,会修改外部值
echo "函数内 temp:$temp"
}
# 外部定义 temp 变量
temp=10
echo "调用前 temp:$temp"
# 调用函数
func_test
# 外部查看 temp 变量(已被函数修改)
echo "调用后 temp:$temp"
执行效果:
调用前 temp:10
函数内 temp:15
调用后 temp:15 # 外部变量被函数修改,造成污染
示例 2:用 local 定义局部变量(推荐)
#!/bin/bash
# 定义函数:用 local 定义局部变量
func_test() {
local temp=$[temp + 5] # temp 是局部变量,仅函数内有效
echo "函数内 temp:$temp"
}
# 外部定义 temp 变量
temp=10
echo "调用前 temp:$temp"
# 调用函数
func_test
# 外部查看 temp 变量(未被修改)
echo "调用后 temp:$temp"
执行效果:
调用前 temp:10
函数内 temp:15
调用后 temp:10 # 外部变量未被修改,避免污染
最佳实践:函数中仅将 “需对外暴露的变量” 设为全局,其余变量全部用 local 定义为局部变量。
四、进阶:数组与函数、递归、函数库
掌握基础后,进一步学习数组与函数的配合、递归函数、函数库复用,应对更复杂的场景。
1. 数组与函数:传递与返回
数组无法直接作为参数传给函数,需先将数组拆分为 “空格分隔的字符串”,再在函数内重组为数组;返回数组时,同样通过 echo 输出数组元素,外部再重组。
示例:向函数传递数组并计算总和
#!/bin/bash
# 定义函数:计算数组所有元素的和
sum_array() {
local sum=0
local arr=("$@") # 将参数重组为数组("$@" 保留元素中的空格)
# 遍历数组求和
for num in "${arr[@]}"; do
sum=$[sum + num]
done
echo $sum # 返回总和
}
# 定义外部数组
my_arr=(10 20 30 40 50)
echo "原始数组:${my_arr[@]}"
# 传递数组:将数组拆分为字符串(${my_arr[@]})
total=$(sum_array "${my_arr[@]}")
echo "数组总和:$total"
执行效果:
原始数组:10 20 30 40 50
数组总和:150
示例:从函数返回数组
#!/bin/bash
# 定义函数:将数组每个元素翻倍后返回
double_array() {
local arr=("$@")
local new_arr=()
# 遍历数组,每个元素翻倍
for num in "${arr[@]}"; do
new_arr+=($[num * 2])
done
echo "${new_arr[@]}" # 输出新数组元素
}
# 外部数组
my_arr=(1 2 3 4 5)
echo "原始数组:${my_arr[@]}"
# 捕获函数输出,重组为数组
result_arr=($(double_array "${my_arr[@]}"))
echo "翻倍后数组:${result_arr[@]}"
执行效果:
原始数组:1 2 3 4 5
翻倍后数组:2 4 6 8 10
2. 递归函数:函数调用自身
递归函数是 “调用自身的函数”,需满足两个条件:
- 基准条件:存在一个终止递归的条件(避免无限循环);
- 递推关系:将问题拆解为更小的子问题,逐步逼近基准条件。
经典场景:计算阶乘(n! = n × (n-1) × ... × 1)。
示例:递归计算阶乘
#!/bin/bash
# 定义递归函数:计算 n 的阶乘
factorial() {
local n=$1
# 基准条件:n=1 时返回 1
if [ $n -eq 1 ]; then
echo 1
else
# 递推关系:n! = n × (n-1)!(调用自身计算 (n-1)!)
local sub_result=$(factorial $[n - 1])
echo $[n * sub_result]
fi
}
# 接收用户输入
read -p "请输入一个正整数:" num
# 调用递归函数
result=$(factorial $num)
echo "$num 的阶乘是:$result"
执行效果(输入 5):
请输入一个正整数:5
5 的阶乘是:120
递归过程解析(以 5! 为例):
- factorial(5) → 调用 factorial(4),计算 5 × 4!;
- factorial(4) → 调用 factorial(3),计算 4 × 3!;
- factorial(3) → 调用 factorial(2),计算 3 × 2!;
- factorial(2) → 调用 factorial(1),计算 2 × 1!;
- factorial(1) → 触发基准条件,返回 1;
- 反向计算:2×1=2 → 3×2=6 → 4×6=24 → 5×24=120,最终返回 120。
3. 函数库:实现跨脚本复用
如果多个脚本需要使用相同的函数(如日志打印、参数校验),无需在每个脚本中重复定义,可将函数统一放在 “函数库文件” 中,通过 source 命令(或 . 符号)在脚本中引用,实现 “一次定义,多脚本复用”。
步骤 1:创建函数库文件(my_funcs.lib)
# my_funcs.lib:通用函数库(仅定义函数,无执行逻辑)
# 1. 日志打印函数:带时间戳
log_info() {
local msg=$1
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $msg"
}
# 2. 数值相加函数
add() {
echo $[ $1 + $2 ]
}
# 3. 数值相乘函数
multiply() {
echo $[ $1 * $2 ]
}
步骤 2:在脚本中引用函数库
用 source 函数库路径 或 . 函数库路径(. 是 source 的别名)加载函数库,随后即可调用库中的函数。
示例脚本(test_lib.sh):
#!/bin/bash
# 引用函数库(若库文件与脚本不在同一目录,需写绝对路径,如 /home/roger/my_funcs.lib)
source ./my_funcs.lib
# 调用库中的函数
log_info "开始执行脚本"
# 调用 add 函数
sum=$(add 10 20)
log_info "10 + 20 = $sum"
# 调用 multiply 函数
product=$(multiply 10 20)
log_info "10 × 20 = $product"
log_info "脚本执行结束"
步骤 3:执行脚本验证
# 给脚本执行权限
chmod +x test_lib.sh
# 执行脚本
./test_lib.sh
执行效果:
[2024-06-08 15:30:00] [INFO] 开始执行脚本
[2024-06-08 15:30:00] [INFO] 10 + 20 = 30
[2024-06-08 15:30:00] [INFO] 10 × 20 = 200
[2024-06-08 15:30:00] [INFO] 脚本执行结束
关键注意事项
- 函数库文件无需执行权限(chmod +x),因为它是被 source 加载到当前 shell 中,而非作为独立脚本运行;
- 引用路径需正确:若脚本与库文件不在同一目录,建议用绝对路径(如 /usr/local/lib/my_funcs.lib),避免路径错误;
- 避免库文件有执行逻辑:函数库仅定义函数,不写 echo、read 等执行语句,防止加载时意外触发。
五、命令行中使用函数:提升运维效率
除了在脚本中使用,函数还可直接在命令行定义和调用,甚至通过 .bashrc 持久化,成为 “自定义命令”,大幅提升日常运维效率。
1. 命令行临时定义函数
适合临时使用的简单函数,退出 shell 后函数会消失。
方式 1:单行定义(简单函数)
用分号分隔命令,直接在命令行输入:
# 定义函数:计算两个数的和
function add { echo $[ $1 + $2 ]; }
# 调用函数
add 100 200
# 输出:300
方式 2:多行定义(复杂函数)
输入函数开头后,Shell 会用次提示符(>)提示继续输入,直到输入右括号 } 结束定义:
# 定义函数:打印带时间戳的日志
function log {
> local msg=$1
> echo "[$(date '+%Y-%m-%d %H:%M:%S')] $msg"
> }
# 调用函数
log "备份数据库完成"
# 输出:[2024-06-08 16:00:00] 备份数据库完成
2. 持久化函数:写入 .bashrc
若希望函数在每次登录 shell 后都能使用,可将函数写入用户主目录的 .bashrc 文件(bash 每次启动都会加载该文件)。
步骤 1:编辑 .bashrc 文件
# 用 vim 打开 .bashrc
vim ~/.bashrc
步骤 2:添加函数到文件末尾
在 .bashrc 末尾添加自定义函数(示例:批量压缩 .log 文件的函数):
# 自定义函数:压缩指定目录下的所有 .log 文件(保留原文件)
compress_logs() {
local dir=$1
# 若未指定目录,默认用当前目录
if [ -z "$dir" ]; then
dir=$(pwd)
fi
# 检查目录是否存在
if [ ! -d "$dir" ]; then
echo "错误:目录 $dir 不存在"
return 1
fi
# 压缩所有 .log 文件
echo "正在压缩 $dir 下的 .log 文件..."
for log_file in "$dir"/*.log; do
# 跳过不存在的文件(避免通配符未匹配时的空值)
[ -f "$log_file" ] || continue
gzip "$log_file" # 用 gzip 压缩,生成 .log.gz 文件
echo "压缩完成:$log_file → $log_file.gz"
done
echo "所有 .log 文件压缩结束"
}
步骤 3:加载 .bashrc 使函数生效
无需重启 shell,用 source 命令加载更新后的 .bashrc:
source ~/.bashrc
步骤 4:在命令行调用持久化函数
# 压缩 /var/log 目录下的 .log 文件(需 sudo 权限,因 /var/log 属主是 root)
sudo compress_logs /var/log
执行效果:
正在压缩 /var/log 下的 .log 文件...
压缩完成:/var/log/syslog.log → /var/log/syslog.log.gz
压缩完成:/var/log/auth.log → /var/log/auth.log.gz
所有 .log 文件压缩结束
六、常见问题与避坑指南
使用 Shell 函数时,新手常因语法细节或逻辑漏洞导致问题,以下是高频问题的解决方案:
问题现象 | 可能原因 | 解决方案 |
---|---|---|
函数调用时报 “command not found” | 1. 函数未定义; 2. 未先定义就调用; 3. 函数库引用路径错误 | 1. 检查函数名是否拼写正确; 2. 确保函数定义在调用之前; 3. 验证函数库路径(用 ls 路径 确认文件存在)。 |
函数参数含空格时被拆分为多个参数 | 传递参数时未加双引号 | 传递含空格的参数时,用 "$参数" 包裹(如 func "hello world");函数内用 "$@" 接收参数。 |
递归函数陷入无限循环 | 缺少基准条件或基准条件错误 | 确保递归函数有明确的终止条件(如阶乘中的 n==1),并检查递推关系是否逐步逼近基准条件。 |
函数库加载后仍无法调用函数 | 1. 函数库文件有语法错误; 2. 用 sh 而非 bash 执行脚本(sh 不支持部分 bash 特性) | 1. 单独执行 source 函数库路径,查看是否有语法报错; 2. 脚本开头用 #!/bin/bash,而非 #!/bin/sh。 |
局部变量在函数外仍能访问 | 忘记用 local 关键字定义变量 | 函数内的变量全部用 local 声明(如 local temp=10),避免全局变量污染。 |
七、总结:函数的最佳实践
- 命名规范:函数名用小写字母 + 下划线(如 log_info),避免与系统命令重名(可先用 which 函数名 检查是否存在同名命令);
- 参数校验:函数开头先检查参数个数、类型(如是否为数字),避免非法输入导致错误;
- 局部优先:函数内变量优先用 local 定义,仅对外暴露必要的全局变量;
- 返回值统一:优先用 “捕获输出”(echo+$(func))返回结果,避免混用 return 和默认状态码;
- 库文件拆分:按功能拆分函数库(如 log_lib.sh 存日志函数,math_lib.sh 存计算函数),避免单库过大;
- 注释清晰:函数开头加注释说明功能、参数含义、返回值(示例如下):
# 功能:计算两个数的和
# 参数:$1 - 第一个数字;$2 - 第二个数字
# 返回值:标准输出打印两数之和
add() {
echo $[ $1 + $2 ]
}
掌握 Shell 函数,不仅能让你的脚本从 “杂乱的代码堆” 变成 “模块化的工具集”,还能通过函数库和命令行持久化,将重复的运维操作转化为 “一键命令”,大幅提升工作效率。从现在开始,尝试用函数重构你的旧脚本吧!