Appearance
09|函数
写到现在,脚本越来越长,有些代码块重复出现——比如多处都要"判断服务是否在运行""发告警通知"。重复的代码改一处忘改另一处就出 bug。
函数把一段代码封装起来,起个名字,调用时传参。本篇讲 Shell 函数的定义、参数、返回值和作用域。
一、定义和调用
Shell 函数两种写法,效果一样:
bash
# 推荐
greet() {
echo "hello"
}
# 带 function 关键字
function greet {
echo "hello"
}
# 调用
greet函数要先定义后调用,所以一般把函数定义放脚本前面,主体逻辑放后面。
二、函数参数
函数内的 $1、$2 是调用函数时传的参数,不是脚本的参数:
bash
greet() {
echo "hello, $1"
}
greet deploy # hello, deploy
greet admin # hello, admin函数里 $0 仍是脚本名,不是函数名。$# $@ $* 在函数内指函数的参数。
三、返回值
Shell 函数的"返回值"有两个概念,容易混。
标准输出——函数里 echo 的内容可以被 $() 捕获:
bash
get_port() {
echo 8080
}
port=$(get_port)
echo "端口: $port" # 端口: 8080退出码——return 返回 0-255 的整数,表示成功失败,不是返回数据:
bash
is_running() {
if systemctl is-active --quiet "$1"; then
return 0 # 成功
else
return 1 # 失败
fi
}
if is_running nginx; then
echo "nginx 在跑"
fi规律:要返回数据用 echo + $(),要返回状态用 return + $?。return 的值超过 255 会回绕,所以别用 return 传数据。
四、局部变量
默认 Shell 变量是全局的,函数里改了会影响外面:
bash
count=0
inc() {
count=$((count + 1))
}
inc
inc
echo $count # 2,被改了函数内部用 local 声明局部变量,不污染外层:
bash
inc() {
local step=1
echo "step=$step"
}
inc
echo "step=$step" # 空,外层没有 step规律:函数里用的临时变量都加 local,避免和外层冲突。这是写函数的好习惯,尤其脚本变大后,不加 local 的变量互相干扰,排查极难。
五、函数组合的例子
把前面的知识串起来——写个检查磁盘并告警的函数:
bash
check_disk() {
local mount=$1
local threshold=${2:-80}
local usage
usage=$(df "$mount" | awk 'NR==2 {gsub(/%/,""); print $5}')
if (( usage >= threshold )); then
echo "告警: $mount 使用 ${usage}% 超过 ${threshold}%" >&2
return 1
fi
return 0
}
# 调用
for m in / /data /var; do
if ! check_disk "$m" 70; then
echo "需要关注 $m" >&2
fi
done函数接收参数、用 local、echo 输出告警、return 返回状态。调用方用 if 判断返回值。这是个干净的函数用法。
六、函数库
常用函数多了,可以放进一个文件,多个脚本共用。建个 lib.sh:
bash
# lib.sh
log() {
echo "[$(date '+%F %T')] $*" >&2
}
check_disk() {
# ... 同上
}
send_alert() {
local msg=$1
log "告警: $msg"
# 实际发通知的逻辑
}别的脚本引用它:
bash
#!/usr/bin/env bash
set -euo pipefail
source /usr/local/lib/lib.sh # 引入函数库
log "开始巡检"
check_disk / 80 || send_alert "根分区满"
log "巡检完成"source(或 .)在当前 shell 执行文件,把里面定义的函数和变量引进来。这样公共逻辑只写一处,多个脚本复用。
七、递归
函数能调用自己,处理可分解的问题。比如算目录总大小(实际用 du 更好,这里只演示递归):
bash
factorial() {
local n=$1
if (( n <= 1 )); then
echo 1
else
local prev
prev=$(factorial $((n - 1)))
echo $((n * prev))
fi
}
echo $(factorial 5) # 120递归在 Shell 里用得少,循环通常更合适。知道有这个能力即可。
八、函数的几个坑
函数名和命令重名——定义了 ls 函数会覆盖 ls 命令,慎用常见命令名做函数名。
return 和 exit 混用——exit 退出整个脚本,return 只退出函数。函数里该用 return 的地方写了 exit,会把整个脚本退掉。
local 赋值不能和命令替换分开:
bash
local usage # 声明为局部
usage=$(df ...) # 这样 OK
local usage=$(df ...) # 一行也 OK但下面这样,local 的退出码总是 0,掩盖了命令的退出码:
bash
local usage=$(some_command) # some_command 失败也看不出,因为 local 成功了要判断命令成败,分开写:
bash
local usage
usage=$(some_command) || { echo "失败"; return 1; }学会了之后
函数让脚本能封装复用代码。参数用 $1、数据返回用 echo + $()、状态返回用 return、临时变量加 local——这套规则写出来的函数干净好维护。多个脚本共用函数用 source 引入库。
语言基础到这里就齐了:变量、命令替换、参数、条件、循环、数组、字符串、函数。下一篇开始讲文本处理三件套 grep、sed、awk——Shell 脚本处理文本的主力工具。