脚本里同一段代码抄了三遍,改一处要改三处------这是函数存在的原因。这篇把 Shell 函数的定义语法、传参规则、
return与exit的区别讲清楚,再用递归调用收尾,让你能写出「一次定义、到处调用」的脚本。<br /><br />✍️ by zhb
本文概要:
| 项目 | 内容 |
|---|---|
| 文章主题 | Shell 函数:三种定义语法、参数传递机制、递归调用与防护 |
| 所属类别 | Linux 系统运维 / Shell 编程基础 |
| 知识关键字 | function、函数传参、$1/$#/$@、return 与 exit、local、source、递归、fork 炸弹 |
本文脉络:先看函数与 alias 的关系、为什么要用函数;再讲三种定义语法;接着是执行与传参(这里有几个反直觉的点,专门拎出来说);然后用四个基础示例练手;递归单独一章,最后用一个企业级 URL 检测脚本把函数拆解落到实处。文中命令名均标注英文全称,完整清单见文末《附:命令英文全称速查表》。
一、为什么用函数
先看 alias 的效果------把一长串命令塞进一个短名字:
bash
[zhb@zhb-m2 ~]$ alias ll='ls -l --color=auto'
[zhb@zhb-m2 ~]$ ll /home/
total 4
drwx------. 3 devops devops 78 Nov 29 13:56 devops
drwx------. 7 zhb zhb 4096 Dec 6 11:50 zhb
drwx------. 6 student student 169 Nov 29 13:56 student
函数的作用类似,但强得多:把程序里多次被调用的相同代码组合起来(函数体)并取一个名字,其它地方只需调这个名字。要改这段逻辑,只改函数体内一份即可。函数还能独立写到文件里,需要时再加载。
用函数的三个好处:
- 减少整个程序的代码量,提升开发效率。
- 增加程序可读性,提升管理效率。
- 实现功能模块化,使程序具备通用性(可移植性)。
小细节:对 Shell 而言,Linux 系统里近 2000 个命令都可以看作 Shell 函数,所以函数无处不在。
二、三种定义语法
bash
# 标准写法:function + 函数名 + () + {}
function 函数名 () {
指令...
return n
}
# 简化写法1:不写 ()
function 函数名 {
指令...
return n
}
# 简化写法2:不写 function(最常用)
函数名 () {
指令...
return n
}
2.1 定义语法速查表
| 写法 | 语法 | 说明 |
|---|---|---|
| 标准写法 | function 名 () { ... } |
关键字与小括号都在,最正式 |
| 简化写法1 | function 名 { ... } |
省略小括号 |
| 简化写法2 | 名 () { ... } |
省略 function,实际最常用 |
return n |
返回退出码 | 只能返回 0~255 的整数,取代 exit 退出函数 |
三、函数执行与传参
3.1 执行不带参数的函数
直接输入函数名即可,注意不带小括号:
bash
函数名
执行函数有几条重要规则:
- 执行时,函数名前的
function和函数后的()都不要带。 - 函数的定义必须在调用之前(先定义、后调用)。
- Shell 执行各类程序的顺序为:系统别名 → 函数 → 系统命令 → 可执行文件。
- 函数与调用它的脚本共用变量 ,也可以为函数设定
local局部变量及特殊位置参数。 return与exit不同:return退出函数 ,exit退出脚本文件 ;且return把返回值交给调用它的当前程序,exit把返回值交给执行程序的当前 Shell。- 函数存放在独立文件被脚本加载时,要用
source加载。 - 函数内一般用
local定义局部变量,离开函数后就消失。
3.2 执行带参数的函数
bash
函数名 参数1 参数2
传参要点:
- Shell 的位置参数(
$1、$2...、$#、$*、$?、$@)都可以作为函数的参数。 - 此时父脚本的参数被函数参数临时掩盖/隐藏。
$0比较特殊,它仍然是父脚本的名称,不会变成函数名。- 函数执行完成后,原来的命令行参数即恢复。
- 函数的参数变量是在函数体里面定义的。
3.3 函数关键要素速查表
| 要素 | 含义 | 说明 |
|---|---|---|
function |
定义函数的关键字 | 可省略 |
() |
参数占位符 | 定义时写、调用时不写 |
{ } |
函数体边界 | 左括号后要有空格或换行 |
return n |
返回退出码 | 取值范围 0~255;中断函数执行 |
exit n |
退出脚本 | 中断整个脚本,不只是函数 |
local |
局部变量声明 | 变量离开函数即失效 |
source |
加载外部函数库 | 等价于 .,如 source mylib |
$0 |
脚本名 | 函数内依然是父脚本名 |
$1/$2 |
函数参数 | 覆盖父脚本的同名位置参数 |
四、基础实践
4.1 示例:hello 函数与「先定义后调用」
bash
# 正确:先定义、后调用
[zhb@zhb-m2 ~]$ cat fun1.sh
#!/bin/bash
function hello () {
echo "Hello World ! "
}
hello
[zhb@zhb-m2 ~]$ bash fun1.sh
Hello World !
# 错误:先调用、后定义
[zhb@zhb-m2 ~]$ cat fun2.sh
#!/bin/bash
hello
function hello () {
echo "Hello World !"
}
[zhb@zhb-m2 ~]$ bash fun2.sh
fun2.sh: line 2: hello: command not found
# 中文:找不到命令 hello ------ 说明函数必须先定义后调用
4.2 示例:调用外部函数库
把函数写到单独文件,用 source 加载:
bash
[zhb@zhb-m2 ~]$ cat >> mylib << 'EOF'
function hello () {
echo "Hello World !"
}
EOF
[zhb@zhb-m2 ~]$ cat fun3.sh
#!/bin/bash
if [ -r mylib ];then
source mylib
hello
else
echo mylib is not exist
exit 1
fi
[zhb@zhb-m2 ~]$ bash fun3.sh
Hello World !
4.3 示例:带参数的函数(彩色输出)
bash
[zhb@zhb-m2 ~]$ cat fun4.sh
#!/bin/bash
function print () {
if [ "$1" == "PASS" ];then
echo -e '\033[1;32mPASS\033[0;39m'
elif [ "$1" == "FAIL" ];then
echo -e '\033[1;31mFAIL\033[0;39m'
elif [ "$1" == "DONE" ];then
echo -e '\033[1;35mDONE\033[0;39m'
else
echo "Usage: -bash PASS|FAIL|DONE"
fi
}
print $1
[zhb@zhb-m2 ~]$ bash fun4.sh
Usage: -bash PASS|FAIL|DONE
[zhb@zhb-m2 ~]$ bash fun4.sh PASS
# 输出绿色 PASS
4.4 示例:验证 $0、$1、$2 的生效特性
bash
[zhb@zhb-m2 ~]$ cat fun5.sh
#!/bin/bash
function print () {
if [ "$1" == "PASS" ];then
echo -e '\033[1;32mPASS\033[0;39m'
elif [ "$1" == "FAIL" ];then
echo -e '\033[1;31mFAIL\033[0;39m'
elif [ "$1" == "DONE" ];then
echo -e '\033[1;35mDONE\033[0;39m'
else
echo "Usage: $0 PASS|FAIL|DONE"
fi
}
str=$2
print $str
# 脚本第一个参数没传给函数内部,函数拿到的是脚本的 $2
[zhb@zhb-m2 ~]$ bash fun5.sh PASS FAIL
FAIL
# $0 仍然是脚本名,而非函数名
[zhb@zhb-m2 ~]$ bash fun5.sh
Usage: fun5.sh PASS|FAIL|DONE
⚠️ 注意 :函数内的
$1是函数自己的第一个参数 ,不是脚本的第一个参数。所以fun5.sh PASS FAIL里,print $str传进去的是FAIL,函数里的$1就是FAIL。这是最容易绕晕的地方。
4.5 示例:用函数做密码强度校验
把「有没有数字 / 字母 / 特殊字符 / 长度够不够」各写成一个函数,主流程只需 && 串起来,逻辑一目了然:
bash
#!/bin/bash
# Name: str.sh
has_num() { [[ $1 =~ [0-9] ]]; }
has_alpha() { [[ $1 =~ [a-zA-Z] ]]; }
has_special() { [[ $1 =~ [^0-9a-zA-Z] ]]; }
check_len() { [[ ${#1} -ge 12 ]]; }
while true; do
read -sp "请提供您的密码字符串(包含字母、数字、特殊字符,长度≥12): " str
if has_num "$str" && has_alpha "$str" && has_special "$str" && check_len "$str"; then
echo "输入合法:$str"
break
fi
has_num "$str" || echo "字符串不包含数字"
has_alpha "$str" || echo "字符串不包含字母"
has_special "$str" || echo "字符串不包含特殊字符"
check_len "$str" || echo "字符串长度不足 12"
echo "请重新输入!"
done
⚠️ 注意 :四个函数都用「
[[ 判据 ]]作为函数体最后一条命令」的写法,函数的退出码 就是这条判断的结果,所以能直接当条件用。has_num "$str" || echo ...是「不满足就提示」的惯用式------函数返回真时||右边不执行,正好只在失败时提示。read -sp的-s让输入不回显(secure),适合密码场景。
五、递归调用
函数调用自身即递归。Shell 没有递归深度限制,但每次递归都会新开子 Shell,慎用大数。

5.1 示例:求 1+2+3+...+n 的和
bash
#!/bin/bash
# Name: sum_fun.sh
function sum_out() {
if [ $1 -eq 1 ];then
sum=1
else
sum=$[ $1 + $(sum_out $[ $1 - 1 ] ) ]
fi
echo 'sum='$sum
}
sum_out $1
bash
[zhb@zhb-m2 ~]$ bash sum_fun.sh 9
sum=1
sum=3
sum=6
sum=10
sum=15
sum=21
sum=28
sum=36
sum=45
[zhb@zhb-m2 ~]$ bash sum_fun.sh 10
...(最后一行)
sum=55
⚠️ 注意 :每次递归都
echo 'sum='$sum,所以 9 层递归会打印 9 行------这是故意 的,用来观察「基线先算出、再逐层回传求和」的过程。真正只要结果的版本把echo挪到函数外即可。
5.2 示例:拼出「1+2+3+...+n」的表达式串
换个思路:不在递归里算数,而是自下而上拼出算式字符串 ,最后交给 bc 一次算完:
bash
#!/bin/bash
# Name: 1.sh
expr=""
s() {
if [ "$1" -le 1 ]; then
expr="1"
return
fi
s $(( $1 - 1 ))
expr="$1 + $expr"
}
s "$1"
result=$(echo "$expr" | bc)
echo "$expr = $result"
bash
[zhb@zhb-m2 ~]$ bash 1.sh 5
5 + 4 + 3 + 2 + 1 = 15
⚠️ 注意 :
return是基线出口 ------$1不大于 1 时把expr设为"1"并直接返回,不再递归。expr="$1 + $expr"写在递归调用之后 ,所以是「最内层先赋值、外层再往前拼」;bc是任意精度计算器,专门处理这类纯算术字符串。这题展示了递归的第二种用法:不只算值,还能组织数据。
5.3 示例:求 n 的阶乘(1×2×3×...×n)
bash
#!/bin/bash
function fact_out() {
if [ $1 -eq 1 ];then
sum=1
else
sum=$[ $1 * $(fact_out $[ $1 - 1 ] ) ]
fi
echo $sum
}
fact_out $1
bash
[zhb@zhb-m2 ~]$ bash fact_fun.sh 5
120
[zhb@zhb-m2 ~]$ bash fact_fun.sh 10
3628800
5.4 fork 炸弹与防护
bash
:(){ :|:& };:
逐行拆解:
bash
:() # 定义函数,函数名为 ":",输入 ":" 时就调用 {} 内代码
{ # ":" 函数起始
: # 递归调用 ":" 函数本身
| # 用管道一次产生两个函数调用
: # 另一次递归调用的 ":"
& # 放后台运行,以便产生更多子进程
} # ":" 函数终止
; # 定义结束后执行的操作
: # 调用 ":" 函数,"引爆" fork 炸弹
成因 :无限制地启用新进程,直到耗尽所有计算资源。对策:限制用户进程数量。
bash
# 限制当前用户最多 100 个进程
[zhb@zhb-m2 ~]$ ulimit -u 100
⚠️ 注意 :fork 炸弹只在自己的虚拟机或试验环境里演示 ,绝不要在共享的生产机上敲。
ulimit -u只能限制当前会话的进程数,重启会话即失效,永久限制要写进/etc/security/limits.conf。
六、企业案例:分函数拆解的 URL 检测脚本
把「用法提示」「具体检测」「主流程」各拆成一个函数,职责清晰、可复用:
bash
[zhb@zhb-m2 ~]$ cat check_url.sh
#!/bin/bash
function usage () {
echo "usage: $0 url"
exit 1
}
function check_url () {
wget --spider -q -o /dev/null --tries=1 -T 5 $1
[ $? -eq 0 ] && echo $1 is accessable || echo $1 is not accessable
}
function main () {
[ $# -ne 1 ] && usage
check_url $1
}
main $*
bash
# 测试结果
[zhb@zhb-m2 ~]$ bash check_url.sh www.baidu.com
www.baidu.com is accessable
[zhb@zhb-m2 ~]$ bash check_url.sh www.zhb.com
www.zhb.com is not accessable
6.1 wget 检测参数速查表
| 参数 | 含义 | 英文原意 |
|---|---|---|
wget |
命令行下载工具 | web get |
--spider |
只检查链接存在,不下载 | spider(爬虫模式) |
-q |
静默,不输出进度 | quiet |
-o /dev/null |
把日志丢弃到空设备 | output |
--tries=1 |
只重试 1 次 | tries |
-T 5 |
超时 5 秒 | timeout |
$1 |
目标 URL(位置参数) | --- |
6.2 本篇其他命令选项速查
| 命令 / 选项 | 含义 | 英文原意 |
|---|---|---|
ls -l |
长格式列出(权限/属主/大小/时间) | long listing |
ls --color=auto |
按文件类型自动着色 | color |
echo -e |
解析字符串里的转义序列(如 \033 颜色码) |
enable escapes |
[ -r 文件 ] |
测试文件可读 | readable |
📚 参考资料(官方文档 · 中英双语)
- Bash 官方手册 · Shell 函数
- Bash 官方手册 · 位置参数与特殊参数
- Bash 内建命令 source / 局部变量 local
- 姊妹篇:Shell 条件判断:if、elif、else 全解与监控脚本落地、Shell case 分支 + while/until 循环:从菜单脚本到守护进程
附:命令英文全称速查表
| 命令 / 缩写 | 英文全称 / 原意 | 作用 |
|---|---|---|
function |
function | 定义函数的关键字 |
return |
return | 从函数返回退出码 |
exit |
exit | 退出当前脚本/Shell |
local |
local | 声明函数内局部变量 |
source |
source | 在当前 Shell 加载并执行脚本 |
alias |
alias | 命令别名 |
ulimit |
user limit | 查看/设置用户资源限制 |
wget |
web get | 命令行下载工具 |
bash |
Bourne Again SHell | Shell 解释器 |
总结 :函数的骨架就三件事------三选一的定义语法、位置参数传参、
return收尾。真正要记牢的是两个反直觉点:$0在函数里仍是脚本名、return只退出函数而exit退出整个脚本。下一篇讲case与循环,它们和函数组合起来,才是运维脚本的常规形态。

