🔍搜索⌘K

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! 为例):

  1. factorial(5) → 调用 factorial(4),计算 5 × 4!;
  2. factorial(4) → 调用 factorial(3),计算 4 × 3!;
  3. factorial(3) → 调用 factorial(2),计算 3 × 2!;
  4. factorial(2) → 调用 factorial(1),计算 2 × 1!;
  5. factorial(1) → 触发基准条件,返回 1;
  6. 反向计算: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),避免全局变量污染。

七、总结:函数的最佳实践

  1. 命名规范:函数名用小写字母 + 下划线(如 log_info),避免与系统命令重名(可先用 which 函数名 检查是否存在同名命令);
  2. 参数校验:函数开头先检查参数个数、类型(如是否为数字),避免非法输入导致错误;
  3. 局部优先:函数内变量优先用 local 定义,仅对外暴露必要的全局变量;
  4. 返回值统一:优先用 “捕获输出”(echo+$(func))返回结果,避免混用 return 和默认状态码;
  5. 库文件拆分:按功能拆分函数库(如 log_lib.sh 存日志函数,math_lib.sh 存计算函数),避免单库过大;
  6. 注释清晰:函数开头加注释说明功能、参数含义、返回值(示例如下):
# 功能:计算两个数的和
# 参数:$1 - 第一个数字;$2 - 第二个数字
# 返回值:标准输出打印两数之和
add() {
  echo $[ $1 + $2 ]
}

把 Shell 函数用熟了,你的脚本就能从一堆”杂乱的代码”变成”模块化的工具集”,再配合函数库和命令行持久化,那些重复的运维操作都能变成”一键命令”,效率蹭蹭往上涨。现在就开始,试着用函数把你的旧脚本重构一遍吧!