第 9 章 · 04 函数:让命令可复用


文档摘要

第 9 章 · 04 函数:让命令可复用 本节摘要:当你的脚本长到几十行,开始出现「同样的几行被复制粘贴」时,就该用函数了。函数是「脚本里的脚本」——它有名字、有参数( / / )、有返回值(退出码 )、有作用域( )。本节要讲透四个关键点:一是函数的两种定义写法( 和 ,推荐前者);二是参数机制——函数内 、 是「传给函数的参数」,和脚本顶层的 、 (传给脚本的)是两套;三是返回值只能用退出码 (0-255),想「返回字符串」要用全局变量或命令替换 ;四是作用域,默认变量是全局的,想限定在函数内必须用 。建立「函数是无名小脚本」这个心智模型后,你就能像组织 Python/JS 代码一样组织 Shell 脚本。

第 9 章 · 04 函数:让命令可复用

本节摘要:当你的脚本长到几十行,开始出现「同样的几行被复制粘贴」时,就该用函数了。函数是「脚本里的脚本」——它有名字、有参数($1/$2/$@)、有返回值(退出码 return)、有作用域(local)。本节要讲透四个关键点:一是函数的两种定义写法(name() {...}function name {...},推荐前者);二是参数机制——函数内 $1$2 是「传给函数的参数」,和脚本顶层的 $1$2(传给脚本的)是两套;三是返回值只能用退出码 return(0-255),想「返回字符串」要用全局变量或命令替换 $(func);四是作用域,默认变量是全局的,想限定在函数内必须用 local。建立「函数是无名小脚本」这个心智模型后,你就能像组织 Python/JS 代码一样组织 Shell 脚本。

内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. 用两种语法定义函数,理解 name() { ... } 是通用推荐写法。
  2. 调用函数并传递参数,区分函数内的 $1/$@ 和脚本顶层的 $1/$@
  3. return 返回退出码,用命令替换 $(func) 或全局变量「返回」字符串/数据。
  4. local 限制变量作用域,避免函数意外污染全局状态。
  5. 把重复逻辑抽成函数,把长脚本拆成可复用的小块。
  6. 在函数里递归调用(了解边界)和组合调用其他函数。

一、设计动机(为什么这样设计)

函数是「脚本里的脚本」

理解 Shell 函数最简洁的模型是:一个函数就是一个嵌在脚本里的小脚本。它和脚本一样:有参数($1/$2/$@/$#)、有退出码(return)、有标准流(stdout/stderr)。区别只是它没有独立的文件,而是嵌在父脚本里。

#!/usr/bin/env bash # 顶层脚本 greet() { echo "Hello, $1" # 这里的 $1 是「传给 greet 的参数」 } greet Alice # 调用,参数是 Alice greet Bob # 再调用,参数是 Bob

注意:调用函数时不加括号,直接 greet Alice 而不是 greet(Alice)。括号只在定义时出现。

两种定义写法

# 写法 1:POSIX 标准写法(推荐) greet() { echo "Hello, $1" } # 写法 2:Bash 扩展写法 function greet { echo "Hello, $1" }

两者等价。推荐写法 1:它是 POSIX 标准,兼容所有 POSIX Shell(sh/dash/bash/zsh)。写法 2 只在 Bash 里能用。

⚠️ 注意:函数体的 { 后面要换行或加 ;} 前必须有 ; 或换行。否则 Shell 无法识别函数体边界。

ok() { echo hi; } # 对!单行写法,{ 后有空格、} 前有 ; bad(){echo hi} # 错!没有空格和分号

参数:函数内的 $1 和脚本顶层的 $1 是两套

这是新手最迷惑的点。Shell 的参数变量 $1/$2/$@ 是「动态」的——它们指向「当前正在执行的那一层」的参数:

#!/usr/bin/env bash echo "脚本参数: $1" # 这里 $1 是脚本的第一个参数 foo() { echo "函数参数: $1" # 这里 $1 是传给 foo 的第一个参数 } foo X Y # 调用 foo,传 X Y echo "回到脚本: $1" # 回到顶层,$1 又变回脚本的参数

运行 ./script.sh A B,输出:

脚本参数: A 函数参数: X 回到脚本: A

关键概念$1/$@/$# 不是「脚本参数」也不是「函数参数」,而是「当前调用层的参数」。函数一调用,它就被重新指向函数的参数;函数返回,又恢复成上一层的。理解这一点,你就不会在函数里误用脚本的参数。

返回值:退出码 vs 字符串

Shell 函数的「返回」有两种含义,新手常常混淆:

  1. 退出码(return)return N,N 是 0-255 的整数。0 表成功,非 0 表失败。这是真正的「函数返回值」,但它只能传 0-255。
  2. 输出(stdout):函数里 echo/printf 的内容会出现在 stdout,调用方用 $(func) 捕获。这是「返回字符串」的实际做法。
# 返回退出码(用于成功/失败判断) is_root() { [[ $EUID -eq 0 ]] } if is_root; then echo "是 root" fi # 返回字符串(用命令替换捕获) today() { date +%F } d=$(today) echo "今天是 $d" # 返回数值(也走 stdout,再算术展开接住) add() { echo $(($1 + $2)) } sum=$(add 3 5) echo $sum # 8

💡 技巧:Shell 函数「返回数据」的惯用法是 echo + $()。函数像一条小命令,你把它当命令用,它的输出就是「返回值」。退出码 return 只用来表达「成功还是失败」。

变量作用域:默认全局,必须用 local 才局部

Shell 的变量默认是全局的——函数里赋值的变量,函数返回后依然存在:

count=0 inc() { count=$((count + 1)) # 这是全局的 count! } inc inc echo $count # 2(全局被改了)

这通常是 bug 的温床。要限制作用域,用 local

safe_inc() { local n # 局部变量,函数外看不到 n=$((n + 1)) }

⚠️ 注意函数里所有「中间变量」都必须用 local 声明,否则它会污染调用者的命名空间。这是 Shell 函数健壮性的第一条规则。养成习惯:函数体里看到变量赋值,先想「它该不该是 local」。

二、高频组合与实战

1. 封装重复的命令序列

log() { echo "[$(date +%F\ %T)] $*" >> /var/log/myapp.log } log "服务启动" log "处理文件 $file 失败,错误码 $code"

$* 把所有参数连成一个字符串,很适合「日志消息」这种场景。

2. 带返回值的工具函数

# 判断文件是否超过 N 分钟没动 is_stale() { local file=$1 local minutes=$2 local age age=$(( ($(date +%s) - $(stat -c %Y "$file")) / 60 )) [[ $age -gt $minutes ]] } if is_stale /tmp/lock 30; then echo "锁文件超过 30 分钟没更新,可能僵死了" fi

函数把「复杂判断」封装成一句 if,调用方完全不用关心实现。

3. 给脚本写 usage 帮助(函数的典型用途)

usage() { cat <<EOF 用法: $0 [-o 输出目录] 源目录 -o DIR 指定输出目录(默认 ./out) 源目录 必填 示例: $0 -o /backup /home/alice EOF } # 参数解析(简化版,第 07 节会用 getopts 重写) if [[ $# -lt 1 ]]; then usage exit 1 fi

usage 写成函数,能在多处复用(缺参数时、-h 时、出错时)。

4. 数据返回:用 stdout + 命令替换

# 提取文件的某个部分 header() { grep '^#' "$1" | head -5 } h=$(header config.yaml) echo "配置头部: $h" # 多行返回也行 list_files() { find "$1" -type f -name '*.txt' } while IFS= read -r f; do echo "找到 $f" done < <(list_files /data)

5. 函数调用函数

check_disk() { local usage usage=$(df --output=pcent "$1" | tail -1 | tr -dc '0-9') [[ $usage -gt 90 ]] } alert() { echo "[ALERT] $*" >&2 } monitor() { if check_disk /; then alert "根分区使用率超过 90%" fi } monitor

小函数组合成大逻辑,这就是「函数让命令可复用」的精髓。

6. 递归(了解即可)

factorial() { local n=$1 if (( n <= 1 )); then echo 1 else local sub sub=$(factorial $((n - 1))) echo $((n * sub)) fi } factorial 5 # 120

Shell 递归不常用(性能差、栈浅),知道能这么写就行,复杂算法别用 Shell。

三、踩坑与排错

坑 1:调用函数加了括号

greet(Alice) # 错!Shell 把 greet 当命令、(Alice) 当子 Shell greet Alice # 对!参数直接空格分隔

坑 2:函数里忘了 local,污染全局

process() { i=0 # 坑!如果外层也有 i,这里把它覆盖了 for i in *.txt; do ... } # 外层循环 for i in 1 2 3; do process # process 里的 i=0 把外层的 i 搞乱 done

规则:函数内的所有循环变量、中间变量,一律 local 声明。

坑 3:把 return 当成「返回字符串」

add() { return $(($1 + $2)) # 错!return 只能 0-255,且只是退出码 } add 100 200 # return 300 → 截断成 44(300 mod 256) # 正确做法:用 echo + 命令替换 add() { echo $(($1 + $2)) } result=$(add 100 200)

坑 4:{} 的语法

f() { echo hi; } # 对 f() { echo hi } # 错!} 前必须有 ; 或换行 f() { echo hi } # 对,换行也可以

坑 5:函数定义必须在使用之前

greet # 错!此时 greet 还没定义 greet() { echo hi; }

Shell 是「逐行解释」的,函数必须先定义后使用。习惯把所有函数定义放在脚本顶部,主逻辑放底部。

本节要点回顾

  1. 函数是「脚本里的脚本」:有名字、有参数($1/$@)、有退出码(return)、有 stdout(用 $() 捕获)。
  2. 两种定义写法name() {...}(POSIX,推荐)和 function name {...}(Bash 扩展)。
  3. $1/$@ 是「当前调用层的参数」——函数调用时指向函数参数,函数返回后恢复成上一层。这是动态绑定的,不要混淆。
  4. 返回数据用 echo + $(),退出码 return 只表达成功/失败(0-255)。把 return 当返回字符串是常见误区。
  5. 变量默认全局,所有中间变量必须 local 声明,否则函数会污染调用者,引发诡异 bug。
  6. 调用不加括号greet Alice 而不是 greet(Alice)
  7. 函数必须先定义后使用,习惯把函数集中放在脚本顶部。

至此你掌握了脚本的全部语法要素:变量、条件、循环、函数。但一个「能跑」的脚本和「能上线」的脚本之间,还差三件法宝——健壮性三件套。下一节开始讲第一件:set -euo pipefail。这一行加在脚本开头,能让你的脚本「遇错即停、拒绝未定义变量、让管道失败穿透」,是业余脚本和生产脚本的分水岭。


发布者: 作者: 灏天文库 转发
评论区 (0)
U