Shell 脚本函数:化繁为简的代码魔法 引言:为什么要使用函数? 想象一下你正在编写一个Shell脚本来自动化部署网站:
需要多次检查磁盘空间
需要多次记录日志到同一个文件
需要多次验证用户输入
如果没有函数,你的代码可能会是这样:
#!/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 " } 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 语句
返回数据 :函数产生的实际输出,使用 echo 或 printf
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 捕获函数输出 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="初始值" echo "修改前: $variable " modify_var echo "修改后: $variable "
4.2 使用local关键字 为了避免这种问题,应该使用 local 关键字声明局部变量:
#!/bin/bash safe_function () { local local_var="我是局部的" global_var="我是全局的" 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 } result=$(factorial 5) echo "5! = $result "
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.shlog_info "脚本开始执行" if check_command "git" ; then log_info "开始执行Git操作..." else log_error "Git不可用,中止执行" exit 1 fi log_info "脚本执行完成"
六、实战案例:完整的部署脚本 让我们用一个综合案例展示函数的强大之处:
#!/bin/bash source utils.shreadonly 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 } if [[ "${BASH_SOURCE[0]} " == "${0} " ]]; then main "$@ " fi
七、最佳实践与常见陷阱 7.1 最佳实践
总是使用local变量 :避免意外的全局变量污染
使用有意义的函数名 :动词+名词,如 create_backup、validate_input
保持函数简短 :一个函数只做一件事
使用return码表示成功失败 :0表示成功,非0表示失败
使用echo返回数据 :而不是修改全局变量
添加注释说明 :说明函数的目的、参数和返回值
7.2 常见陷阱
忘记local关键字 :意外修改全局变量
set_username () { username=$1 } set_username () { local username=$1 }
在管道中使用函数 :函数在子shell中运行,无法修改父shell的变量
count_lines () { local file=$1 wc -l < "$file " } line_count=$(count_lines "file.txt" ) total=0 count_lines "file.txt" | while read count; do total=$((total + count)) done
过度使用函数 :对于简单的任务,直接写代码可能更清晰
总结 Shell函数是将复杂脚本转化为模块化、可维护代码的关键工具。它们提供了:
代码复用 :避免重复,提高开发效率
模块化 :将复杂问题分解为小问题
可读性 :通过有意义的函数名让代码自文档化
可维护性 :修改功能只需修改一个地方
封装性 :隐藏实现细节,暴露简洁接口
从简单的代码封装到复杂的递归算法,从基本的参数传递到高级的函数库组织,Shell函数为我们提供了构建健壮、可靠脚本所需的一切工具。
记住这个简单的原则:如果你发现自己在复制粘贴代码,就该创建一个函数了! 通过合理使用函数,你的Shell脚本将从一堆命令的集合,进化成真正优雅、强大的程序。