欢迎光临
我们一直在努力

Shell 从入门到精通:保姆级教程(七)

第 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 …

三条铁律:

  • function 关键字可省略,() 不能省(也可以写成 function name { … });
  • 必须先定义后调用——Shell 逐行执行,函数只有在定义之后才存在;
  • return 只能返回 0—255 的整数,且必须紧跟数字;想返回字符串,用 echo + 命令替换。
  • 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 是取第一个参数后把参数列表前移的标准写法。

    赞(0)
    未经允许不得转载:171主机测评 » Shell 从入门到精通:保姆级教程(七)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址