当前位置: 首页 > backend >正文

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 823 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 823 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 定义 “局部变量”(仅在函数内有效,函数结束后消失)。
  • returnexit 的区别:return 是退出函数,返回一个状态值给调用者;exit 是直接退出整个脚本,返回状态值给当前 Shell。
  • 外部函数加载:如果函数存放在单独的文件中,需要用 source 文件名. 文件名 加载后才能调用(source. 作用相同)。
2. 带参数的函数执行

调用时在函数名后直接跟参数,格式:

函数名 参数1 参数2  # 参数之间用空格分隔

参数说明

  • 函数内部用 “位置参数” 接收参数:$1 表示第 1 个参数,$2 表示第 2 个参数,$# 表示参数总数,$*$@ 表示所有参数,$? 表示上一条命令的返回值。
  • 临时覆盖父脚本参数:函数执行时,父脚本的参数会被函数参数临时 “掩盖”,函数执行完后,父脚本参数恢复正常。
  • $0 特殊:始终表示父脚本的文件名,不会被函数参数影响。

Shell 函数的基础实践

示例 1:简单的 hello 函数(验证 “先定义后调用”)

实验流程

  1. 编写脚本,先定义 hello 函数(输出 “Hello World !”),再调用函数,观察执行结果。
  2. 编写另一个脚本,先调用 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:调用外部文件中的函数

实验流程

  1. 创建一个存放函数的文件 mylib(定义 hello 函数)。
  2. 编写脚本 fun3.sh,通过 source 加载 mylib 中的函数,然后调用。
  3. 执行脚本,验证能否成功调用外部函数。

详细步骤

# 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:带参数的函数(根据输入输出彩色文字)

实验流程

  1. 定义 print 函数,接收参数 PASS/FAIL/DONE,分别输出绿色、红色、紫色文字;其他参数提示用法。
  2. 脚本中通过 read 命令获取用户输入,传给 print 函数。
  3. 测试不同输入(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:函数参数与脚本参数的关系

实验流程

  1. 编写脚本 fun5.sh,定义 print 函数(逻辑同示例 3),脚本中通过 $2 获取第二个参数传给函数。
  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 检测脚本

实验流程

  1. 定义 usage 函数:提示脚本用法(如参数错误时调用)。
  2. 定义 check_url 函数:用 wget 检测 URL 是否可访问。
  3. 定义 main 函数:检查参数数量,调用 check_url 函数。
  4. 执行脚本,测试有效 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 的和

实验流程

  1. 定义 sum_out 函数:如果 n=1,返回 1;否则返回 n + sum_out(n-1)(自身调用)。
  2. 脚本接收用户输入的整数 n,调用函数计算和并输出。
  3. 测试输入 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)

实验流程

  1. 定义 fact_out 函数:如果 n=1,返回 1;否则返回 n * fact_out(n-1)
  2. 脚本接收用户输入的整数 n,调用函数计算阶乘并输出。
  3. 测试输入 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 加载复用,形成自己的 “函数库”。

http://www.xdnf.cn/news/18473.html

相关文章:

  • 第1.2节:早期AI发展(1950-1980)
  • Mybatis Plus - 代码生成器简单使用
  • Baumer高防护相机如何通过YoloV8深度学习模型实现社交距离的检测识别(python)
  • 【204页PPT】某著名企业信息化规划方案(附下载方式)
  • 【攻防世界】Web_php_include
  • GitLab CI:安全扫描双雄 SAST vs. Dependency Scanning 该如何抉择?
  • 阿德莱德多模态大模型导航能力挑战赛!NavBench:多模态大语言模型在具身导航中的能力探索
  • C++ csignal库详细使用介绍
  • 密码管理中Null 密码
  • 第九届86358贾家庄短片周在山西汾阳贾家庄举办
  • 齐次变换矩阵的逆变换:原理与SymPy实现
  • FIFO核心原理与机制
  • 解决 SymPy Lambdify 中的符号覆盖与语法错误问题
  • PiscCode使用 MediaPipe 检测人脸关键点多样展示
  • 大数据世界的开拓者:深入浅出MapReduce分布式计算经典范式
  • 相似度、距离
  • 一次性密码(OTP)原理及应用
  • OFD格式文件及Python将PDF转换为OFD格式文件
  • Centos 8 管理防火墙
  • 多目标跟踪中基于目标威胁度评估的传感器控制方法复现
  • LeeCode 40.组合总和II
  • SpringBoot -- 集成Spring Security (二)
  • CTFSHOW | 其他篇题解(二)web417 - web437
  • LeetCode第55题 - 跳跃游戏
  • 学习游戏制作记录(合成表UI和技能树的UI)8.22
  • SpringBoot项目创建的五种方式
  • 53 C++ 现代C++编程艺术2-枚举和枚举类
  • C++ unistd.h库文件介绍(文件与目录操作, 进程管理, 系统环境访问, 底层I/O操作, 系统休眠/执行控制)
  • Linux服务测试
  • 【链表 - LeetCode】24. 两两交换链表中的节点