强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 echoreturn 只能返回 0-255数据用 echo + $()
函数定义顺序函数必须在调用前定义将函数放在脚本开头
return 在函数外return 只能在函数中使用函数外用 exit
递归深度Bash 递归性能差复杂递归用 Python/Go
子 Shell 中的函数管道中的函数在子 Shell 执行使用 < <()

6.9 扩展阅读