第 7 章 函数
7.1 为什么需要函数
当脚本超过 50 行,你一定会遇到这些问题:同样的检查逻辑写了三遍;改一个路径要改五个地方;出错时不知道是哪一段挂的。
函数就是解药:把一段命令封装成一个名字,调用名字即可执行。Shell 函数有两类:
- 系统函数:系统自带,直接可用,如 basename、dirname;
- 自定义函数:自己定义,用于复用业务逻辑。
7.2 两个必会的系统函数
basename:从路径取文件名
basename [路径] [后缀]
basename /one/two/aa.txt # aa.txt
basename /one/two/aa.txt .txt # aa(去掉指定后缀)
basename /one/two/ # two(只取最后一段)
dirname:从路径取目录
dirname /one/two/aa.txt # /one/two
dirname aa.txt # .(当前目录)
这两个函数在脚本里极其常用,比如“在脚本所在目录下创建日志”:
#!/bin/bash
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
LOG="$SCRIPT_DIR/run.log"
echo "日志路径: $LOG"
⚠️ 直接用 $0 时,如果脚本是通过 PATH 里的软链接调用的,dirname 拿到的是软链接所在目录。加 readlink -f 可以解析出真实路径。
7.3 自定义函数:语法与三条铁律
# 定义
[ function ] 函数名()
{
命令
[return 返回值]
}
调用
函数名 参数1 参数2 …
三条铁律:
7.4 三种函数形态
(1)无参无返回
#!/bin/bash
demo()
{
echo "执行了函数"
}
demo
输出:
执行了函数
(2)无参有返回
#!/bin/bash
sum()
{
echo "求两个数的和…"
read -p "输入第一个数字: " n1
read -p "输入第二个数字: " n2
echo "两个数字分别为 $n1 和 $n2"
return $((n1 + n2))
}
sum
echo "两个数字的和为: $?"
输出:
求两个数的和…
输入第一个数字: 3
输入第二个数字: 5
两个数字分别为 3 和 5
两个数字的和为: 8
⚠️ 如果和超过 255,return 会被截断(比如 300 变成 44)。要返回大数或字符串,别用 return:
add() {
echo $(( $1 + $2 )) # 用 echo 输出结果
}
result=$(add 100 200) # 用命令替换接收
echo $result # 300
(3)有参函数
函数内部用 $1、$2 取参数,$# 取个数,"$@" 取全部——和脚本参数完全一样。
#!/bin/bash
funParam(){
echo "第一个参数为 $1"
echo "第二个参数为 $2"
echo "第十个参数(错误写法)为 $10"
echo "第十个参数(正确写法)为 ${10}"
echo "参数总数有 $# 个"
echo "作为一个字符串输出所有参数: $*"
}
funParam 1 2 3 4 5 6 7 8 9 10 22
输出:
第一个参数为 1
第二个参数为 2
第十个参数(错误写法)为 10
第十个参数(正确写法)为 10
参数总数有 11 个
作为一个字符串输出所有参数: 1 2 3 4 5 6 7 8 9 10 22
这里 $10 恰好碰巧对了,因为 $1 是 1、后面拼了个 0。换成 funParam a b … 立刻露馅(输出 a0)。永远写 ${10}。
7.5 函数与脚本的本质区别:进程
#!/bin/bash
demo(){
echo "函数中打印当前进程 ID: $$"
}
echo "脚本中打印当前进程 ID: $$"
demo
输出:
脚本中打印当前进程 ID: 12345
函数中打印当前进程 ID: 12345
两者 PID 相同——函数在当前 Shell 进程内执行,不新开进程。
而脚本里调用另一个脚本文件:
#!/bin/bash
echo "父脚本 PID: $$"
bash child.sh
输出的两个 PID 不同——外部脚本在子 Shell 中执行,会开启独立进程。
这就带来三个实际差异:
| 进程 | 当前进程内 | 新进程 |
| 变量 | 直接读写当前 Shell 变量 | 拿不到父 Shell 的局部变量(除非 export) |
| exit | 会退出整个脚本 | 只退出子脚本 |
| 性能 | 快 | 有 fork 开销 |
⚠️ 函数里慎用 exit:它会终止整个脚本,而不是只退出函数。要在函数里“提前返回”,用 return。
7.6 局部变量:local
函数内定义的变量默认是全局的,会污染外部同名变量。用 local 限制作用域:
#!/bin/bash
name="外部"
demo() {
local name="函数内"
echo "函数内: $name"
}
demo
echo "函数外: $name"
输出:
函数内: 函数内
函数外: 外部
去掉 local,第二行就会变成“函数内”——这是很多“变量莫名被改”的元凶。
7.7 实战:给脚本装上“日志”和“断言”
真实脚本里,函数最大的价值是把“每个脚本都要写一遍”的东西固定下来。
#!/bin/bash
# ============ 工具函数 ============
LOG_FILE="/tmp/deploy.log"
log() {
local level="$1"; shift
local msg="$*"
local ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
printf '[%s] [%-5s] %s\\n' "$ts" "$level" "$msg" | tee -a "$LOG_FILE"
}
die() {
log ERROR "$*"
exit 1
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"
}
============ 主流程 ============
need_cmd tar
need_cmd gzip
log INFO "开始部署"
… 业务逻辑 …
log INFO "部署完成"
输出:
[2026-10-18 22:30:01] [INFO ] 开始部署
[2026-10-18 22:30:01] [INFO ] 部署完成
这段不到 20 行的工具层,几乎可以直接搬到任何生产脚本里。local level="$1"; shift 是取第一个参数后把参数列表前移的标准写法。




