Skip to content

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

函数接收参数、用 localecho 输出告警、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 脚本处理文本的主力工具。