模块:S08 函数与脚本结构
篇号:S08-01 / 42
预计阅读:40 分钟
主线:Bash
文章目录
本篇目标
掌握 Bash 函数的定义与调用:name() { ... } 与可选的 function name 关键字。会在函数里用 $1 "$@" $# 接收参数,用 return 返回退出码,并区分 return 与 exit。能写出 main "$@" 入口,读懂脚本里的 usage、die、log 等辅助函数。
30 秒速览
- 推荐写法:
funcname() { 命令; }(function关键字可省略)。 - 调用:
funcname arg1 arg2,与调用普通命令一样。 - 函数体内
$1$2"$@"$#与脚本顶层规则相同(S03-03)。 return [n]结束函数并返回退出码;省略时返回最后一条命令的退出码。exit结束整个脚本(或被source时的当前 Shell)。- 函数要先定义再调用(定义写在调用之前,或集中在文件前部)。
正文
1. 为什么用函数
把重复逻辑收成一块,便于:
- 同一脚本里多次调用
main入口清晰(S01-04)- 稍后
source到别的脚本(S08-03)
log() {
echo "[$(date +%H:%M:%S)] $*" >&2
}
log "start"
do_work
log "done"
2. 定义语法
2.1 推荐:name() { ... }
greet() {
echo "hello, $1"
}
2.2 可选:function name() { ... }
function greet() {
echo "hello, $1"
}
两种在 Bash 里几乎等价;function 是扩展风格,新脚本可只写 name()。
格式注意:
# 对
myfunc() {
echo ok
}
# 错:缺少 ()
myfunc {
echo ok
}
左花括号 { 与 name() 之间通常要有空格或换行(与 { 开命令组规则一致)。
3. 调用
greet Alice
greet "Bob Smith"
- 函数名与命令名同一命名空间;不要与已有命令同名(如定义
ls()会遮蔽外部ls)。 - 含空格参数要引号:
greet "Bob Smith"。
# 命令替换里调用
out=$(greet World)
# 条件里调用
if is_ready; then
deploy
fi
4. 参数:$1 与 "$@"
函数内位置参数与脚本一致:
| 变量 | 含义 |
|---|---|
$1 $2 … | 第几个参数 |
$# | 参数个数 |
"$@" | 全部参数,逐项保留 |
$0 | 仍是脚本名(不是函数名) |
run() {
echo "argc=$#"
printf ' arg=%s' "$@"
echo
}
run a b "c d"
# argc=3
# arg=a arg=b arg=c d
转发参数(包装器常见):
invoke() {
real_cmd "$@"
}
不要用 $* 代替 "$@" 传参(S03-03)。
5. return 与退出码
is_file() {
[[ -f "$1" ]]
}
if is_file "$CONFIG"; then
source "$CONFIG"
fi
函数里最后一条命令的退出码即函数退出码;也可显式 return:
die() {
echo "error: $*" >&2
return 1
}
deploy() {
build || return 1
publish || return 1
return 0
}
| 写法 | 含义 |
|---|---|
return 0 | 成功 |
return 1(或其它非 0) | 失败 |
return(无参数) | 返回上一条命令的退出码 |
函数末尾无 return | 返回最后一条命令的退出码 |
check() {
grep -q pattern "$1"
}
# check 的退出码 = grep 的退出码
return 只能带 0~255 的整数(与 exit 相同)。
6. return 与 exit
| 关键字 | 作用 |
|---|---|
return | 结束当前函数,回到调用者 |
exit | 结束整个脚本(或 source 时的 Shell 会话) |
fatal() {
echo "fatal: $*" >&2
exit 1 # 整个脚本退出
}
skip_bad() {
[[ -f "$1" ]] || return 1 # 仅本函数失败,脚本可继续
}
usage 常 exit(结束脚本):
usage() {
echo "usage: $0 [-h] <file>" >&2
exit 2
}
被 source 的文件里顶层 exit 会退出当前 Shell(S01-02、S08-03);库函数里更常用 return。
7. main 入口模式
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "usage: $0 <srcdir>" >&2
exit 2
}
main() {
(( $# == 1 )) || usage
process "$1"
}
main "$@"
| 部分 | 作用 |
|---|---|
| 上部 | set、工具函数、usage |
main | 业务主流程 |
最后一行 main "$@" | 把脚本参数传入 |
定义顺序:Bash 按行执行,main "$@" 运行时 main 必须已定义,故函数写在 main "$@" 之前。
8. 多个辅助函数
读脚本时常见命名:
| 函数 | 常见用途 |
|---|---|
usage | 打印用法并 exit 2 |
die / fatal | 打印错误并 exit 1 |
log / info | 带前缀日志 |
require_cmd | 检查依赖命令是否存在 |
die() {
echo "error: $*" >&2
exit 1
}
require_file() {
[[ -f "$1" ]] || die "missing file: $1"
}
main() {
require_file "$CONFIG"
...
}
9. 函数与 set -e
在 set -e 下,函数内某命令失败可能导致整个脚本退出(除非该失败处于 if、 || 等例外上下文,S01-04)。
try_step() {
risky_cmd || return 1 # 不让 set -e 直接杀脚本
}
读脚本:函数是「局部容错」还是「失败即 exit」要分清。
10. 导出函数:export -f(了解)
子 shell 默认不继承函数定义;需要时:
export -f myfunc
bash -c 'myfunc arg'
日常单脚本很少用;远程 ssh 执行复杂命令时偶尔见到。
11. 递归(少见)
walk() {
local d=$1
[[ -d "$d" ]] || return 0
for f in "$d"/*; do
[[ -f "$f" ]] && echo "$f"
[[ -d "$f" ]] && walk "$f"
done
}
local 在 S08-02 详讲;递归前务必限制深度或避免符号链接环。
12. 读脚本检查清单
- 函数名是否遮蔽了
ls、rm等系统命令? - 传参是否用
"$@"转发? - 失败用
return还是exit?是否该只结束函数? -
usage的$0是否指向脚本名? -
main "$@"是否在函数定义之后?
练习
判断题
myfunc() { echo hi; }与function myfunc() { echo hi; }在 Bash 中通常都能用。- 函数里
$0是函数名。 return 1只会结束函数,不会结束脚本。- 函数可以像命令一样出现在
if后面:if check_file "$f"; then。 - 把
main "$@"写在文件第一行,后面再定义main,也能正常运行。
- 对。
- 错(
$0仍是脚本名或 shell 名)。 - 对(除非函数被
source且在特殊上下文,一般如此)。 - 对。
- 错(调用时
main尚未定义会报错)。
实操题
实现 repeat n cmd [args...]:第一个参数为次数(整数),后面为要执行的命令及参数;执行 n 次,任一次失败则 return 1。
repeat 3 echo hello
# hello 打印 3 次
参考答案
repeat() {
local n=$1
shift
local i
for (( i = 0; i < n; i++ )); do
"$@" || return 1
done
}
(local 见 S08-02;此处可用普通变量完成练习。)
改错题
#!/usr/bin/env bash
set -e
main $*
log() {
echo $*
}
run() {
grep pattern $1
exit 1
}
main() {
log starting
run "$1"
}
main "$@"
参考
main $*→main "$@"(最后一行已对,若曾写main $*会破坏参数)。log定义在main调用之后 → 所有函数移到main "$@"之前。log里echo $*→echo "$*"或echo "$@"(按意图)。run里grep pattern $1→grep pattern -- "$1"。run里exit 1应在 grep 失败时执行 →grep ... || return 1,否则永远退出脚本。
下一篇预告
S08-02:《作用域:local 与全局变量》— 函数内变量不污染外层、local 与默认值。
352

被折叠的 条评论
为什么被折叠?



