06 - 函数
06 - 函数
6.1 函数定义
Bash 中有两种函数定义语法:
# 语法一:function 关键字(Bash 扩展)
function greet {
echo "Hello, $1!"
}
# 语法二:POSIX 兼容(推荐)
greet() {
echo "Hello, $1!"
}
# 语法三:function + ()(冗余但合法)
function greet() {
echo "Hello, $1!"
}
💡 推荐:使用
name()语法,兼容性最好。
函数命名规范
# ✅ 推荐:小写+下划线
validate_input() { :; }
process_file() { :; }
get_user_info() { :; }
# ✅ 可接受:驼峰
validateInput() { :; }
processFile() { :; }
# ❌ 避免:与系统命令重名
# ls() { ... } # 会覆盖 ls 命令!
# cd() { ... } # 会覆盖 cd 命令!
# ✅ 但有时有意为之(包装/增强)
ls() {
command ls --color=auto -h "$@" # 使用 command 绕过函数
}
6.2 函数参数
Bash 函数的参数不使用括号传递,而是通过位置参数 $1, $2, … $N。
# 基本参数传递
greet() {
local name="$1"
local greeting="${2:-Hello}" # 第二个参数,默认值
echo "$greeting, $name!"
}
greet "张三" # 输出: Hello, 张三!
greet "张三" "你好" # 输出: 你好, 张三!
# 参数个数检查
validate_args() {
if [[ $# -lt 2 ]]; then
echo "用法: ${FUNCNAME[0]} <name> <age>" >&2
return 1
fi
echo "姓名: $1, 年龄: $2"
}
# 使用 "$@" 传递所有参数
wrapper() {
# 将所有参数转发给另一个函数
greet "$@"
}
wrapper "World" "Hi"
# 命名参数模拟(通过关联数组)
deploy() {
declare -A params=()
while [[ $# -gt 0 ]]; do
case "$1" in
--env|-e) params[env]="$2"; shift 2 ;;
--version|-v) params[version]="$2"; shift 2 ;;
--force|-f) params[force]="true"; shift ;;
*) echo "未知参数: $1" >&2; return 1 ;;
esac
done
echo "环境: ${params[env]:-dev}"
echo "版本: ${params[version]:-latest}"
echo "强制: ${params[force]:-false}"
}
deploy --env prod --version 2.0 --force
6.3 返回值
Bash 函数有两种"返回"机制:退出码(return)和输出(echo)。
return 语句
# return 返回退出码(0-255)
is_running() {
local pid="$1"
if kill -0 "$pid" 2>/dev/null; then
return 0 # 成功/真
else
return 1 # 失败/假
fi
}
if is_running 1234; then
echo "进程运行中"
else
echo "进程未运行"
fi
# return 省略时返回最后一条命令的退出码
check_file() {
[[ -f "$1" && -r "$1" ]]
# 隐式 return $?
}
# ⚠️ 注意:return 的范围是 0-255
# 如需返回大数值,使用 echo + 命令替换
echo + 命令替换(推荐用于返回数据)
# 通过 echo 输出结果,用 $() 捕获
get_home_dir() {
local user="$1"
local home_dir
home_dir=$(getent passwd "$user" | cut -d: -f6)
echo "$home_dir"
}
home=$(get_home_dir "root")
echo "Root 主目录: $home"
# 返回多个值
get_user_info() {
local user="$1"
local uid gid home
IFS=: read -r _ _ uid gid _ home _ < <(getent passwd "$user")
echo "$uid|$gid|$home" # 用分隔符
}
# 解析多值返回
IFS='|' read -r uid gid home < <(get_user_info "root")
echo "UID=$uid, GID=$gid, HOME=$home"
# 返回数组(通过 nameref)
get_files() {
local -n result=$1 # Bash 4.3+ nameref
result=()
while IFS= read -r file; do
result+=("$file")
done < <(find /tmp -maxdepth 1 -name "*.log" -type f 2>/dev/null)
}
get_files log_files
echo "找到 ${#log_files[@]} 个日志文件"
6.4 局部变量
# 函数中必须使用 local 声明局部变量
process_data() {
local input="$1" # 参数拷贝
local temp_file # 局部变量
local -a results=() # 局部数组
local -i count=0 # 局部整数
local -r config="/etc/app.conf" # 局部只读
temp_file=$(mktemp)
# 使用变量...
while IFS= read -r line; do
results+=("$line")
((count++))
done < "$input"
echo "处理了 $count 行"
# 清理
rm -f "$temp_file"
}
# ⚠️ 忘记 local 会污染全局
bad_example() {
x=42 # 这是全局变量!
}
# ⚠️ 即使函数退出,全局变量依然存在
bad_example
echo "$x" # 输出: 42
6.5 递归
# 阶乘
factorial() {
local n=$1
if ((n <= 1)); then
echo 1
else
local sub_result
sub_result=$(factorial $((n - 1)))
echo $((n * sub_result))
fi
}
echo "5! = $(factorial 5)" # 输出: 120
# ⚠️ Bash 的递归性能很差,递归深度有限
# 超过约 200-300 层就会栈溢出
# 文件系统递归遍历
walk_dir() {
local dir="$1"
local indent="${2:-0}"
local prefix
prefix=$(printf '%*s' "$indent" '')
for item in "$dir"/*; do
[[ -e "$item" ]] || continue
if [[ -d "$item" ]]; then
echo "${prefix}📁 $(basename "$item")/"
walk_dir "$item" $((indent + 2))
else
echo "${prefix}📄 $(basename "$item")"
fi
done
}
walk_dir "/etc" 0 | head -30
# 目录大小计算(递归)
dir_size() {
local dir="$1"
local total=0
while IFS= read -r -d '' file; do
local size
size=$(stat -c%s "$file" 2>/dev/null || echo 0)
((total += size))
done < <(find "$dir" -type f -print0 2>/dev/null)
echo "$total"
}
6.6 函数作为参数(回调)
# 将函数名作为参数传递
apply_to_each() {
local func="$1"
shift
local item
for item in "$@"; do
"$func" "$item"
done
}
to_upper() { echo "${1^^}"; }
to_lower() { echo "${1,,}"; }
apply_to_each to_upper "hello" "world" # HELLO WORLD
apply_to_each to_lower "HELLO" "WORLD" # hello world
# 数组处理框架
map() {
local func="$1"
shift
local item
for item in "$@"; do
"$func" "$item"
done
}
filter() {
local predicate="$1"
shift
local item
for item in "$@"; do
if "$predicate" "$item"; then
echo "$item"
fi
done
}
is_even() { (( $1 % 2 == 0 )); }
double() { echo $(( $1 * 2 )); }
numbers=(1 2 3 4 5 6 7 8 9 10)
# 过滤偶数
echo "偶数: $(filter is_even "${numbers[@]}")"
# 映射加倍
echo "加倍: $(map double "${numbers[@]}")"
# 重试包装器
retry() {
local max_attempts="$1"
local delay="$2"
shift 2
local attempt=1
while ((attempt <= max_attempts)); do
if "$@"; then
return 0
fi
echo "第 $attempt 次尝试失败,${delay}s 后重试..."
sleep "$delay"
((attempt++))
done
echo "所有 $max_attempts 次尝试均失败" >&2
return 1
}
# 使用重试包装器
retry 3 5 curl -sf "https://api.example.com/health"
6.7 业务场景:模块化脚本框架
#!/bin/bash
# app.sh —— 模块化应用脚本
set -euo pipefail
# ---- 日志模块 ----
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
CURRENT_LOG_LEVEL=$LOG_LEVEL_INFO
log::debug() { [[ $CURRENT_LOG_LEVEL -le $LOG_LEVEL_DEBUG ]] && echo "[DEBUG] $*" >&2; }
log::info() { [[ $CURRENT_LOG_LEVEL -le $LOG_LEVEL_INFO ]] && echo "[INFO] $*" >&2; }
log::warn() { [[ $CURRENT_LOG_LEVEL -le $LOG_LEVEL_WARN ]] && echo "[WARN] $*" >&2; }
log::error() { [[ $CURRENT_LOG_LEVEL -le $LOG_LEVEL_ERROR ]] && echo "[ERROR] $*" >&2; }
# ---- 工具模块 ----
util::confirm() {
local prompt="${1:-确认执行?}"
local answer
read -rp "$prompt (y/N): " answer
[[ "$answer" =~ ^[yY] ]]
}
util::require_command() {
local cmd="$1"
if ! command -v "$cmd" &>/dev/null; then
log::error "必需命令 '$cmd' 未安装"
return 1
fi
}
util::temp_dir() {
local tmpdir
tmpdir=$(mktemp -d)
# 注册清理
trap "rm -rf '$tmpdir'" RETURN
echo "$tmpdir"
}
# ---- 参数解析模块 ----
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) CURRENT_LOG_LEVEL=$LOG_LEVEL_DEBUG; shift ;;
-q|--quiet) CURRENT_LOG_LEVEL=$LOG_LEVEL_ERROR; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) log::error "未知选项: $1"; exit 1 ;;
*) break ;;
esac
done
ARGS=("$@")
}
usage() {
cat << 'EOF'
用法: app.sh [选项] <command> [args...]
选项:
-v, --verbose 详细输出
-q, --quiet 仅错误输出
-h, --help 显示帮助
命令:
build 构建项目
test 运行测试
deploy 部署服务
EOF
}
# ---- 主逻辑 ----
main() {
parse_args "$@"
local command="${ARGS[0]:-}"
case "$command" in
build)
log::info "开始构建..."
# build logic
log::info "构建完成"
;;
test)
log::info "运行测试..."
# test logic
log::info "测试完成"
;;
deploy)
util::require_command docker
log::info "开始部署..."
# deploy logic
log::info "部署完成"
;;
"")
usage
exit 1
;;
*)
log::error "未知命令: $command"
usage
exit 1
;;
esac
}
main "$@"
6.8 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
忘记 local | 变量泄漏到全局 | 函数内始终使用 local |
| return vs echo | return 只能返回 0-255 | 数据用 echo + $() |
| 函数定义顺序 | 函数必须在调用前定义 | 将函数放在脚本开头 |
return 在函数外 | return 只能在函数中使用 | 函数外用 exit |
| 递归深度 | Bash 递归性能差 | 复杂递归用 Python/Go |
| 子 Shell 中的函数 | 管道中的函数在子 Shell 执行 | 使用 < <() |