Shell 脚本函数:化繁为简的代码魔法

引言:为什么要使用函数?

想象一下你正在编写一个Shell脚本来自动化部署网站:

  1. 需要多次检查磁盘空间
  2. 需要多次记录日志到同一个文件
  3. 需要多次验证用户输入

如果没有函数,你的代码可能会是这样:

#!/bin/bash
# 部署前检查磁盘
if [ $(df / --output=pcent | tail -1 | tr -d '% ') -gt 90 ]; then
echo "$(date): 磁盘空间不足,部署中止" >> deploy.log
exit 1
fi

# 一些部署操作...

# 部署后再次检查磁盘
if [ $(df / --output=pcent | tail -1 | tr -d '% ') -gt 90 ]; then
echo "$(date): 警告:磁盘空间紧张" >> deploy.log
fi

# 更多操作...

你会发现相同的代码重复出现了多次。这带来了几个问题:

  • 代码冗余:同样功能的代码写了多遍
  • 维护困难:如果需要修改检查逻辑,必须修改所有地方
  • 可读性差:主要逻辑被重复的代码淹没

这就是函数要解决的问题!函数就像是一个代码的乐高积木,你把常用的功能打包成一个个独立的模块,然后可以在需要的地方反复使用。


一、函数基础:定义与调用

1.1 如何定义函数

在Shell中,定义函数有几种方式,最常用的是这两种:

# 方式一:使用函数名后跟括号(推荐,更清晰)
function deploy_alert {
echo "=== 部署警报 ==="
echo "时间: $(date)"
echo "详情: $1"
}

# 方式二:省略function关键字
check_disk() {
local threshold=90
local usage=$(df / --output=pcent | tail -1 | tr -d '% ')

if [ $usage -gt $threshold ]; then
echo "磁盘使用率: ${usage}% > ${threshold}%"
return 1
fi
return 0
}

1.2 如何调用函数

调用函数就像使用一个普通的命令一样简单:

#!/bin/bash

# 先定义函数
check_disk() {
# 函数体
}

# 在需要的地方调用函数
echo "开始部署前检查..."
check_disk

if [ $? -eq 0 ]; then
echo "磁盘检查通过"
else
echo "磁盘检查失败"
exit 1
fi

# 传递参数给函数
deploy_alert "开始部署前端代码"

二、函数参数:与外界沟通的桥梁

函数可以接收参数,这让它们更加灵活和强大。

2.1 传递和接收参数

#!/bin/bash

# 定义带参数的函数
create_backup() {
local source_dir=$1
local backup_dir=$2
local max_backups=$3

echo "正在备份 $source_dir$backup_dir"
echo "最多保留 $max_backups 个备份"
# 实际的备份逻辑...
}

# 调用函数并传递参数
create_backup "/home/user/documents" "/backups" 5

2.2 特殊参数变量

函数内部有一些特殊变量来处理参数:

变量 描述
$1-$9 第1个到第9个参数
${10} 第10个参数(需要加大括号)
$# 传递给函数的参数个数
$@ 所有参数的列表(每个参数都是独立的引用)
$* 所有参数的列表(所有参数作为一个整体)

示例:处理多个参数

print_args() {
echo "一共传递了 $# 个参数"
echo "所有参数: $@"

# 遍历所有参数
local count=1
for arg in "$@"; do
echo "参数 $count: $arg"
count=$((count + 1))
done
}

print_args "apple" "banana" "cherry" "date"

三、返回值:函数的”回答”

3.1 返回状态码 vs 返回数据

这是Shell函数的一个重要概念:

  • 返回状态码:表示函数执行成功(0)还是失败(非0),使用 return 语句
  • 返回数据:函数产生的实际输出,使用 echoprintf
# 这个函数返回状态码
is_file_exists() {
if [ -f "$1" ]; then
return 0 # 成功
else
return 1 # 失败
fi
}

# 这个函数返回数据
get_file_size() {
if [ -f "$1" ]; then
du -h "$1" | awk '{print $1}'
return 0
else
return 1
fi
}

# 使用示例
if is_file_exists "important.txt"; then
echo "文件存在"
size=$(get_file_size "important.txt")
echo "文件大小: $size"
fi

3.2 捕获函数输出

# 函数通过echo返回数据
get_server_info() {
echo "主机名: $(hostname)"
echo "运行时间: $(uptime | awk '{print $3}')"
echo "内存使用: $(free -h | awk '/Mem:/ {print $3"/"$2}')"
}

# 调用函数并捕获所有输出
server_info=$(get_server_info)
echo "服务器信息:"
echo "$server_info"

四、变量作用域:避免意外的”串门”

4.1 局部变量与全局变量

默认情况下,Shell函数中定义的变量是全局的,这可能会导致意外的问题:

#!/bin/bash

modify_var() {
# 这个variable会修改全局的同名变量!
variable="在函数内修改了"
}

variable="初始值"
echo "修改前: $variable" # 输出: 初始值
modify_var
echo "修改后: $variable" # 输出: 在函数内修改了

4.2 使用local关键字

为了避免这种问题,应该使用 local 关键字声明局部变量:

#!/bin/bash

safe_function() {
local local_var="我是局部的"
global_var="我是全局的" # 没有local关键字,仍然是全局的

echo "函数内: local_var = $local_var"
echo "函数内: global_var = $global_var"
}

local_var="外部局部变量"
global_var="外部全局变量"

echo "调用前: local_var = $local_var" # 输出: 外部局部变量
echo "调用前: global_var = $global_var" # 输出: 外部全局变量

safe_function

echo "调用后: local_var = $local_var" # 输出: 外部局部变量(未被修改)
echo "调用后: global_var = $global_var" # 输出: 我是全局的(被修改了!)

五、高级函数技巧

5.1 递归函数

函数可以调用自身,这在处理递归数据结构时非常有用:

#!/bin/bash

# 计算阶乘的递归函数
factorial() {
local n=$1
if [ $n -eq 0 ]; then
echo 1
else
local prev=$(factorial $((n-1)))
echo $((n * prev))
fi
}

# 计算5的阶乘
result=$(factorial 5)
echo "5! = $result" # 输出: 120

5.2 函数库:代码复用的高级形式

你可以创建包含常用函数的库文件,然后在多个脚本中重复使用:

mylib.sh(函数库文件):

#!/bin/bash

# 日志函数
log_info() {
echo "[INFO] $(date): $1"
}

log_error() {
echo "[ERROR] $(date): $1" >&2
}

# 检查命令是否可用
check_command() {
if command -v "$1" >/dev/null 2>&1; then
log_info "命令 $1 可用"
return 0
else
log_error "命令 $1 不可用"
return 1
fi
}

main.sh(主脚本文件):

#!/bin/bash

# 导入函数库
source mylib.sh

# 使用库中的函数
log_info "脚本开始执行"

if check_command "git"; then
log_info "开始执行Git操作..."
# Git操作...
else
log_error "Git不可用,中止执行"
exit 1
fi

log_info "脚本执行完成"

六、实战案例:完整的部署脚本

让我们用一个综合案例展示函数的强大之处:

#!/bin/bash

# 导入函数库
source utils.sh

# 配置变量
readonly BACKUP_DIR="/backups"
readonly DEPLOY_DIR="/var/www/html"
readonly LOG_FILE="/var/log/deploy.log"

# 主部署函数
deploy() {
local version=$1

log_info "开始部署版本 $version"

# 检查系统状态
if ! check_disk_space; then
log_error "磁盘空间检查失败"
return 1
fi

if ! check_dependencies; then
log_error "依赖检查失败"
return 1
fi

# 创建备份
if ! create_backup; then
log_error "备份创建失败"
return 1
fi

# 执行部署
if ! extract_package "$version"; then
log_error "包解压失败"
restore_backup
return 1
fi

if ! run_migrations; then
log_error "数据库迁移失败"
restore_backup
return 1
fi

# 清理旧备份
cleanup_old_backups

log_success "版本 $version 部署成功"
return 0
}

# 检查依赖
check_dependencies() {
local required_commands=("git" "tar" "systemctl")

for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
log_error "必需命令 $cmd 未安装"
return 1
fi
done

return 0
}

# 创建备份
create_backup() {
local backup_name="backup_$(date +%Y%m%d_%H%M%S).tar.gz"

if tar -czf "$BACKUP_DIR/$backup_name" -C "$DEPLOY_DIR" . 2>/dev/null; then
log_info "备份创建成功: $backup_name"
return 0
else
log_error "备份创建失败"
return 1
fi
}

# 主程序
main() {
local version=${1:-"latest"}

log_info "=== 开始部署流程 ==="

if deploy "$version"; then
log_info "=== 部署完成 ==="
exit 0
else
log_error "=== 部署失败 ==="
exit 1
fi
}

# 只有直接执行此脚本时才运行main函数
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

七、最佳实践与常见陷阱

7.1 最佳实践

  1. 总是使用local变量:避免意外的全局变量污染
  2. 使用有意义的函数名:动词+名词,如 create_backupvalidate_input
  3. 保持函数简短:一个函数只做一件事
  4. 使用return码表示成功失败:0表示成功,非0表示失败
  5. 使用echo返回数据:而不是修改全局变量
  6. 添加注释说明:说明函数的目的、参数和返回值

7.2 常见陷阱

  1. 忘记local关键字:意外修改全局变量

    # 错误
    set_username() {
    username=$1 # 这会修改全局的username变量!
    }

    # 正确
    set_username() {
    local username=$1 # 这是局部变量
    }
  2. 在管道中使用函数:函数在子shell中运行,无法修改父shell的变量

    count_lines() {
    local file=$1
    wc -l < "$file"
    }

    # 这样是有效的
    line_count=$(count_lines "file.txt")

    # 这样是无效的,无法修改父shell的变量
    total=0
    count_lines "file.txt" | while read count; do
    total=$((total + count)) # 这个修改不会影响外部的total
    done
  3. 过度使用函数:对于简单的任务,直接写代码可能更清晰


总结

Shell函数是将复杂脚本转化为模块化、可维护代码的关键工具。它们提供了:

  • 代码复用:避免重复,提高开发效率
  • 模块化:将复杂问题分解为小问题
  • 可读性:通过有意义的函数名让代码自文档化
  • 可维护性:修改功能只需修改一个地方
  • 封装性:隐藏实现细节,暴露简洁接口

从简单的代码封装到复杂的递归算法,从基本的参数传递到高级的函数库组织,Shell函数为我们提供了构建健壮、可靠脚本所需的一切工具。

记住这个简单的原则:如果你发现自己在复制粘贴代码,就该创建一个函数了! 通过合理使用函数,你的Shell脚本将从一堆命令的集合,进化成真正优雅、强大的程序。