12.Shell脚本修炼手册--函数的基础认知与实战演练(fock炸弹!!)
Shell 函数的知识与实践
文章目录
- Shell 函数的知识与实践
- Shell 函数介绍
- Shell 函数的语法
- Shell 函数的执行
- 1. 不带参数的函数执行
- 2. 带参数的函数执行
- Shell 函数的基础实践
- 示例 1:简单的 hello 函数(验证 “先定义后调用”)
- 示例 2:调用外部文件中的函数
- 示例 3:带参数的函数(根据输入输出彩色文字)
- 示例 4:函数参数与脚本参数的关系
- 企业级 URL 检测脚本
- 函数的递归调用(函数调用自身)
- 示例 1:递归求 1+2+...+n 的和
- 示例 2:递归求 n 的阶乘(1*2*...*n)
- 示例 3:fork 炸弹(危险!仅作原理了解)
- 总结
Shell 函数介绍
在学习 Shell 函数之前,我们先回忆一下 Linux 中的 alias
(别名)功能。比如我们常用 ll
代替 ls -l --color=auto
,就是通过别名实现的:
# 直接执行详细列表命令,显示/home目录内容(--color=auto自动为文件着色)
[bq@controller shell 14:15:42]$ ls -l --color=auto /home
总用量 0
drwx------. 5 bq bq 124 8月 23 14:15 bq# 创建别名:用ll代替ls -l --color=auto
[bq@shell ~]$ alias ll='ls -l --color=auto'# 用别名执行,效果和原命令完全一致
[bq@controller shell 14:25:23]$ ll /home/
总用量 0
drwx------. 5 bq bq 124 8月 23 14:15 bq
Shell 函数和别名类似,都能简化操作,但功能更强大。简单来说,函数就是把一段重复使用的代码 “打包”,起一个名字。之后想使用这段代码时,直接调用这个名字就行。如果需要修改这段代码,只改 “打包” 好的那一份,所有调用的地方都会同步更新。我们也可以把函数存到单独的文件里,需要时再加载使用。
使用 Shell 函数的好处:
- 减少重复代码:一段代码多次用,定义成函数后不用反复写,提高开发效率。
- 增强可读性:用有意义的函数名代替一堆命令,代码更易懂、易维护。
- 实现模块化:把功能拆分成函数,让脚本更通用,方便移植到其他场景。
小贴士:Linux 系统中的近 2000 个命令,其实都可以理解为 Shell 的 “内置函数”,可见函数在 Shell 中的重要性。
Shell 函数的语法
Shell 函数有多种定义方式,核心都是 “函数名 + 代码块”,以下是常见格式:
标准写法:
function 函数名 () {指令... # 函数要执行的代码return n # 可选,返回一个状态值(0-255,0表示成功)
}
简化写法 1:省略小括号 ()
function 函数名 { # 去掉了函数名后的(),其他和标准写法一致指令...return n
}
简化写法 2:省略 function
关键字
函数名 () { # 去掉了function,保留()和代码块指令...return n
}
三种写法功能完全一样,实际使用中可以根据习惯选择。
Shell 函数的执行
Shell 函数分为 “不带参数” 和 “带参数” 两种,执行方式略有不同,下面详细说明:
1. 不带参数的函数执行
直接输入函数名即可(注意:函数名后不要加小括号),格式:
函数名 # 直接调用函数
重要说明(必看):
- 调用函数时,不要加
function
关键字,也不要加小括号(比如定义了hello()
,调用时直接写hello
)。 - 函数必须 “先定义,后调用”:如果在调用之后才定义函数,Shell 会提示 “命令未找到”。
- 执行优先级:Shell 执行程序的顺序是「系统别名 → 函数 → 系统命令 → 可执行文件」。比如如果有一个别名
ll
、一个函数ll
和系统命令ll
,执行ll
时会先执行别名。 - 变量共享:函数和调用它的脚本会共用变量,但可以用
local
定义 “局部变量”(仅在函数内有效,函数结束后消失)。 return
和exit
的区别:return
是退出函数,返回一个状态值给调用者;exit
是直接退出整个脚本,返回状态值给当前 Shell。- 外部函数加载:如果函数存放在单独的文件中,需要用
source 文件名
或. 文件名
加载后才能调用(source
和.
作用相同)。
2. 带参数的函数执行
调用时在函数名后直接跟参数,格式:
函数名 参数1 参数2 # 参数之间用空格分隔
参数说明:
- 函数内部用 “位置参数” 接收参数:
$1
表示第 1 个参数,$2
表示第 2 个参数,$#
表示参数总数,$*
或$@
表示所有参数,$?
表示上一条命令的返回值。 - 临时覆盖父脚本参数:函数执行时,父脚本的参数会被函数参数临时 “掩盖”,函数执行完后,父脚本参数恢复正常。
$0
特殊:始终表示父脚本的文件名,不会被函数参数影响。
Shell 函数的基础实践
示例 1:简单的 hello 函数(验证 “先定义后调用”)
实验流程:
- 编写脚本,先定义
hello
函数(输出 “Hello World !”),再调用函数,观察执行结果。 - 编写另一个脚本,先调用
hello
函数,再定义函数,观察错误结果。
详细步骤:
# 脚本1:先定义函数,再调用
[bq@shell ~]$ cat fun1.sh
#!/bin/bash
# 定义hello函数:输出Hello World !
function hello () {echo "Hello World !"
}
# 调用hello函数(直接写函数名)
hello# 执行脚本,成功输出结果
[bq@shell ~]$ bash fun1.sh
Hello World !# 脚本2:先调用函数,再定义(错误示范)
[bq@shell ~]$ cat fun2.sh
#!/bin/bash
# 先调用hello函数(此时函数还未定义)
hello
# 后定义hello函数
function hello () {echo "Hello World !"
}# 执行脚本,提示“hello: 命令未找到”(因为调用时函数还不存在)
[bq@shell ~]$ bash fun2.sh
fun2.sh: line 2: hello: command not found
结论:函数必须先定义,后调用,否则会报错。
示例 2:调用外部文件中的函数
实验流程:
- 创建一个存放函数的文件
mylib
(定义hello
函数)。 - 编写脚本
fun3.sh
,通过source
加载mylib
中的函数,然后调用。 - 执行脚本,验证能否成功调用外部函数。
详细步骤:
# 1. 创建存放函数的文件mylib
[bq@shell ~]$ cat >> mylib << 'EOF' # 用here document写入内容,'EOF'表示内容中的变量不解析
function hello () {echo "Hello World !" # 函数功能:输出Hello World !
}
EOF# 2. 编写调用脚本fun3.sh
[bq@shell ~]$ cat fun3.sh
#!/bin/bash
# 检查mylib文件是否存在且可读(-r选项:判断文件存在且可读)
if [ -r mylib ];thensource mylib # 加载mylib文件中的函数(source等同于. mylib)
elseecho "mylib文件不存在或不可读" # 如果文件不存在,提示错误exit 1 # 退出脚本,返回状态码1(表示执行失败)
fi
hello # 调用加载的hello函数# 3. 执行脚本,成功调用外部函数
[bq@shell ~]$ bash fun3.sh
Hello World !
结论:通过 source
可以加载外部文件中的函数,实现代码复用。
示例 3:带参数的函数(根据输入输出彩色文字)
实验流程:
- 定义
print
函数,接收参数PASS
/FAIL
/DONE
,分别输出绿色、红色、紫色文字;其他参数提示用法。 - 脚本中通过
read
命令获取用户输入,传给print
函数。 - 测试不同输入(
PASS
/FAIL
/DONE
/ 其他文字),观察输出结果。
详细步骤:
# 编写脚本fun4.sh
[bq@shell ~]$ cat fun4.sh
#!/bin/bash
# 定义print函数:根据参数输出彩色文字
function print () {# 判断参数是否为PASS:输出绿色文字(\033[1;32m是绿色高亮,\033[0;39m恢复默认颜色)if [ "$1" == "PASS" ];thenecho -e '\033[1;32mPASS\033[0;39m' # -e选项:解析转义字符(如颜色控制符)# 判断参数是否为FAIL:输出红色文字elif [ "$1" == "FAIL" ];thenecho -e '\033[1;31mFAIL\033[0;39m'# 判断参数是否为DONE:输出紫色文字elif [ "$1" == "DONE" ];thenecho -e '\033[1;35mDONE\033[0;39m'# 其他参数:提示用法elseecho "Usage: print PASS|FAIL|DONE"fi
}
# 交互式读取用户输入,-p选项:显示提示文字
read -p "请输入你想要打印的内容:" str
# 调用print函数,传入用户输入的参数
print $str# 测试1:输入PASS(输出绿色PASS)
[bq@shell ~]$ bash fun4.sh
请输入你想要打印的内容:PASS
PASS # 实际显示为绿色高亮# 测试2:输入FAIL(输出红色FAIL)
[bq@shell ~]$ bash fun4.sh
请输入你想要打印的内容:FAIL
FAIL # 实际显示为红色高亮# 测试3:输入DONE(输出紫色DONE)
[bq@shell ~]$ bash fun4.sh
请输入你想要打印的内容:DONE
DONE # 实际显示为紫色高亮# 测试4:输入其他文字(提示用法)
[bq@shell ~]$ bash fun4.sh
请输入你想要打印的内容:hello
Usage: print PASS|FAIL|DONE
补充说明:
read -p "提示信息" 变量名
:用于交互式获取用户输入,-p
显示提示文字。- 颜色控制符:
\033[1;32m
中,1
表示高亮,32
表示绿色(31 = 红,35 = 紫),\033[0;39m
用于恢复默认颜色,避免后续文字也变色。
示例 4:函数参数与脚本参数的关系
实验流程:
- 编写脚本
fun5.sh
,定义print
函数(逻辑同示例 3),脚本中通过$2
获取第二个参数传给函数。 - 测试脚本传入不同参数,观察函数是否使用脚本的参数,以及
$0
的值(脚本名还是函数名)。
详细步骤:
# 编写脚本fun5.sh
[bq@shell ~]$ cat fun5.sh
#!/bin/bash
function print () {if [ "$1" == "PASS" ];thenecho -e '\033[1;32mPASS\033[0;39m'elif [ "$1" == "FAIL" ];thenecho -e '\033[1;31mFAIL\033[0;39m'elif [ "$1" == "DONE" ];thenecho -e '\033[1;35mDONE\033[0;39m'else# $0始终表示脚本名(而非函数名)echo "Usage: $0 PASS|FAIL|DONE"fi
}
str=$2 # 脚本的第二个参数赋值给变量str
print $str # 函数使用脚本的第二个参数# 测试1:脚本传入两个参数(PASS和FAIL)
[bq@shell ~]$ bash fun5.sh PASS FAIL
FAIL # 函数接收的是脚本的第二个参数(FAIL),输出红色FAIL# 测试2:脚本不传入参数(触发else分支)
[bq@shell ~]$ bash fun5.sh
Usage: fun5.sh PASS|FAIL|DONE # $0显示为脚本名fun5.sh,而非函数名print
结论:
- 函数参数需要显式从脚本参数中传递(如脚本的
$2
传给函数的$1
)。 $0
始终表示当前脚本的文件名,和函数名无关。
企业级 URL 检测脚本
实验流程:
- 定义
usage
函数:提示脚本用法(如参数错误时调用)。 - 定义
check_url
函数:用wget
检测 URL 是否可访问。 - 定义
main
函数:检查参数数量,调用check_url
函数。 - 执行脚本,测试有效 URL(如百度)和无效 URL,观察结果。
详细步骤:
# 编写检测脚本check_url.sh
[bq@shell ~]$ cat check_url.sh
#!/bin/bash
# 用法提示函数:当参数错误时调用
function usage () {echo "usage: $0 url" # 提示正确用法:脚本名 + URLexit 1 # 退出脚本,状态码1表示错误
}# URL检测函数:接收URL参数,判断是否可访问
function check_url () {# wget选项说明:# --spider:模拟爬虫(只检查URL是否存在,不下载内容)# -q:安静模式(不输出日志)# -o /dev/null:将日志输出到“黑洞”(彻底不显示)# --tries=1:尝试1次(失败不重试)# -T 5:超时时间5秒wget --spider -q -o /dev/null --tries=1 -T 5 $1# 判断上一条命令的返回值($?):0表示成功,非0表示失败[ $? -eq 0 ] && echo "$1 is accessable" || echo "$1 is not accessable"
}# 主函数:处理参数,调用检测函数
function main () {[ $# -ne 1 ] && usage # 如果参数数量不是1,调用usage函数提示用法check_url $1 # 调用检测函数,传入URL参数
}# 执行主函数,$*表示所有参数(传给main函数)
main $*# 测试1:检测有效URL(百度)
[bq@shell ~]$ bash check_url.sh www.baidu.com
www.baidu.com is accessable # 可访问# 测试2:检测无效URL(不存在的域名)
[bq@shell ~]$ bash check_url.sh www.bq.com
www.bq.com is not accessable # 不可访问# 测试3:参数错误(不传参数)
[bq@shell ~]$ bash check_url.sh
usage: check_url.sh url # 调用usage函数提示用法
实战价值:可批量检测网站可用性,结合定时任务(crontab
)实现自动监控。
函数的递归调用(函数调用自身)
递归是指函数自己调用自己,适用于有明确终止条件的问题(如求和、阶乘)。
示例 1:递归求 1+2+…+n 的和
实验流程:
- 定义
sum_out
函数:如果n=1
,返回 1;否则返回n + sum_out(n-1)
(自身调用)。 - 脚本接收用户输入的整数
n
,调用函数计算和并输出。 - 测试输入
10
(预期结果 55),验证正确性。
详细步骤:
# 编写求和脚本sum.sh
[bq@shell ~]$ cat sum.sh
#!/bin/bash
# 递归求和函数:计算1+2+...+$1的和
function sum_out() {# 终止条件:当参数为1时,和为1if [ $1 -eq 1 ];thensum=1else# 递归调用:n的和 = n + (n-1)的和(通过$(sum_out $[ $1 - 1 ])获取n-1的和)sum=$[ $1 + $(sum_out $[ $1 - 1 ]) ]fiecho $sum # 输出计算结果
}
# 读取用户输入的整数
read -p "输入一个你想计算和的整数:" num
# 调用递归函数,传入用户输入的数字
sum_out $num# 测试:输入10(1+2+...+10=55)
[bq@shell ~]$ bash sum.sh
输入一个你想计算和的整数:10
55 # 结果正确
示例 2:递归求 n 的阶乘(12…*n)
实验流程:
- 定义
fact_out
函数:如果n=1
,返回 1;否则返回n * fact_out(n-1)
。 - 脚本接收用户输入的整数
n
,调用函数计算阶乘并输出。 - 测试输入
10
(预期结果 3628800),验证正确性。
详细步骤:
# 编写阶乘脚本fact.sh
[bq@shell ~]$ cat fact.sh
#!/bin/bash
# 递归阶乘函数:计算1*2*...*$1的积
function fact_out() {# 终止条件:当参数为1时,阶乘为1if [ $1 -eq 1 ];thensum=1else# 递归调用:n的阶乘 = n * (n-1)的阶乘sum=$[ $1 * $(fact_out $[ $1 - 1 ]) ]fiecho $sum # 输出计算结果
}
# 读取用户输入的整数
read -p "输入一个你想计算阶乘的整数:" num
# 调用递归函数,传入用户输入的数字
fact_out $num# 测试:输入10(10! = 3628800)
[bq@shell ~]$ bash fact.sh
输入一个你想计算阶乘的整数:10
3628800 # 结果正确
示例 3:fork 炸弹(危险!仅作原理了解)
fork 炸弹是一种通过递归创建大量进程耗尽系统资源的恶意代码,原理是函数不断自我复制并后台运行。
代码解析:
:(){ :|:& };: # fork炸弹核心代码
逐部分解释:
-
:()
:定义一个函数,函数名是:
(冒号)。 -
{ :|:& }
:函数体内容:
:
:调用函数自身(递归)。|
:管道符,将左边的输出作为右边的输入,同时触发两次函数调用。&
:将进程放入后台运行,允许同时创建更多子进程。
-
;
:结束函数定义。 -
:
:调用函数,触发 “爆炸”。
危害:函数会无限制创建子进程,很快耗尽 CPU、内存等资源,导致系统卡死。
防御措施:限制用户可创建的最大进程数(临时生效):
# 限制当前用户最多创建100个进程(ulimit -u 限制用户最大进程数)
[bq@shell ~]$ ulimit -u 100
警告:切勿在生产环境中执行 fork 炸弹代码!
总结
Shell 函数通过 “封装重复代码” 提升了脚本的简洁性和可维护性,掌握函数的定义、参数传递、递归调用等技巧,能大幅提高 Shell 脚本开发效率。实际使用中,建议将通用函数整理到单独的文件中,通过 source
加载复用,形成自己的 “函数库”。