Shell脚本精读 · S08-01 | 函数定义与调用:`function` 与 `()`

模块:S08 函数与脚本结构
篇号:S08-01 / 42
预计阅读:40 分钟
主线:Bash


本篇目标

掌握 Bash 函数的定义与调用:name() { ... } 与可选的 function name 关键字。会在函数里用 $1 "$@" $# 接收参数,用 return 返回退出码,并区分 returnexit。能写出 main "$@" 入口,读懂脚本里的 usagedielog 等辅助函数。


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. returnexit

关键字作用
return结束当前函数,回到调用者
exit结束整个脚本(或 source 时的 Shell 会话)
fatal() {
  echo "fatal: $*" >&2
  exit 1          # 整个脚本退出
}

skip_bad() {
  [[ -f "$1" ]] || return 1   # 仅本函数失败,脚本可继续
}

usageexit(结束脚本):

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
}

localS08-02 详讲;递归前务必限制深度或避免符号链接环。


12. 读脚本检查清单

  • 函数名是否遮蔽lsrm 等系统命令?
  • 传参是否用 "$@" 转发?
  • 失败用 return 还是 exit?是否该只结束函数?
  • usage$0 是否指向脚本名?
  • main "$@" 是否在函数定义之后

练习

判断题

  1. myfunc() { echo hi; }function myfunc() { echo hi; } 在 Bash 中通常都能用。
  2. 函数里 $0 是函数名。
  3. return 1 只会结束函数,不会结束脚本。
  4. 函数可以像命令一样出现在 if 后面:if check_file "$f"; then
  5. main "$@" 写在文件第一行,后面再定义 main,也能正常运行。
参考答案
  1. 对。
  2. 错($0 仍是脚本名或 shell 名)。
  3. 对(除非函数被 source 且在特殊上下文,一般如此)。
  4. 对。
  5. 错(调用时 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 "$@"
参考
  1. main $*main "$@"(最后一行已对,若曾写 main $* 会破坏参数)。
  2. log 定义在 main 调用之后 → 所有函数移到 main "$@" 之前
  3. logecho $*echo "$*"echo "$@"(按意图)。
  4. rungrep pattern $1grep pattern -- "$1"
  5. runexit 1 应在 grep 失败时执行 → grep ... || return 1,否则永远退出脚本。

下一篇预告

S08-02:《作用域:local 与全局变量》— 函数内变量不污染外层、local 与默认值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值