Shell 函数:语法、传参与递归调用一次讲透

脚本里同一段代码抄了三遍,改一处要改三处------这是函数存在的原因。这篇把 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

📚 参考资料(官方文档 · 中英双语)

附:命令英文全称速查表

命令 / 缩写 英文全称 / 原意 作用
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 与循环,它们和函数组合起来,才是运维脚本的常规形态。

相关推荐
东鹏特饮1 天前
15个实用Linux运维脚本
linux·运维·shell·脚本·redhat·常用生产脚本
Zoom2 天前
【开源】7 天,我用 Rust 复刻了 FinalShell!Rhost v1.0.0 发布
rust·electron·shell
for_ever_love__5 天前
函数——参数、lambda、*args与作用域
python·函数·大模型开发·编程基础
青柠之夏cc5 天前
递归揭秘:从原理到陷阱
递归·函数·乘
Logic1017 天前
关于汉诺塔问题的分析
c语言·算法·递归·函数·解题思路·递归函数·汉诺塔问题
Lsetea10 天前
OpenSSL s_client退出码为0却证书验证失败:严格校验与Shell管道排查
https·shell·ssl证书·openssl·tls
fox_charon12 天前
Windows 下 CLI 参数的引号陷阱:为什么 --resume 'uuid' 会失败
windows·shell·cmd·cli·引号
huisheng_qaq13 天前
【Python基础篇-04】深入理解python的函数、参数与作用域
python·ai·函数·参数作用域
j7~13 天前
【Python】(篇六)《基础语法(函数、列表和元组、字典、文件)》---详解
开发语言·python·文件·函数·字典·编程学习·元组与列表