1. 当脚本变成"复制粘贴"地狱时

运维工程师小王最近遇到件烦心事:每天要维护二十多个相似的备份脚本。每次修改参数就像玩"大家来找茬",稍不留神就会漏改某个文件。上周因为某个脚本里的路径没更新,导致整个数据库备份失败。这不正是典型的代码复用性差导致的运维事故吗?

2. 函数:给代码碎片穿针引线

(技术栈:Bash Shell 5.0+)

让我们从最简单的日志备份需求开始。原始脚本可能是这样的:

#!/bin/bash
# 备份应用日志
LOG_DIR="/var/log/myapp"
BACKUP_DIR="/backup/$(date +%Y%m%d)"
mkdir -p $BACKUP_DIR
tar -czf $BACKUP_DIR/app_logs.tar.gz $LOG_DIR/*.log

# 备份系统日志
SYSLOG_DIR="/var/log/system"
mkdir -p $BACKUP_DIR
tar -czf $BACKUP_DIR/system_logs.tar.gz $SYSLOG_DIR/*.log

当我们需要复用打包逻辑时,可以用函数改造:

#!/bin/bash

# 定义打包函数
package_logs() {
    local src_dir=$1       # 源目录(第一个参数)
    local prefix=$2        # 备份文件前缀(第二个参数)
    local backup_root="/backup/$(date +%Y%m%d)"
    
    mkdir -p "$backup_root"
    tar -czf "${backup_root}/${prefix}_logs.tar.gz" "${src_dir}"/*.log
}

# 调用函数执行实际任务
package_logs "/var/log/myapp" "application"
package_logs "/var/log/system" "system"

应用场景:适合处理重复操作但参数不同的任务,比如批量文件处理、多服务启停等

技术优势

  • 消除重复代码块
  • 参数化配置提高灵活性
  • 修改只需调整函数定义

注意事项

  • 使用local定义局部变量避免污染全局空间
  • 函数应保持单一职责原则
  • 通过return code传递执行状态

3. 模块化:把脚本变成乐高积木

(技术栈:Bash Shell 4.0+)

当项目规模扩大时,可以把通用功能拆分到独立文件。假设我们创建logging_utils.sh

#!/bin/bash
# 日志工具模块

init_backup_dir() {
    export BACKUP_ROOT="/backup/$(date +%Y%m%d)"
    mkdir -p "$BACKUP_ROOT"
}

compress_files() {
    local target_dir=$1
    local output_name=$2
    
    tar -czf "${BACKUP_ROOT}/${output_name}.tar.gz" -C "$target_dir" .
}

clean_old_backups() {
    local retention_days=$1
    find "/backup" -type d -mtime +$retention_days -exec rm -rf {} \;
}

主脚本通过source引入:

#!/bin/bash
source "$(dirname "$0")/logging_utils.sh"

init_backup_dir
compress_files "/var/log/myapp" "app_logs"
compress_files "/var/log/system" "sys_logs"
clean_old_backups 7

应用场景:适合跨项目共享代码、构建工具链、封装复杂操作

技术优势

  • 实现真正的代码复用
  • 各模块独立维护
  • 通过命名空间管理功能

注意事项

  • 注意文件路径引用问题
  • 避免循环引用
  • 使用unset及时释放资源

4. 参数魔术:让脚本学会七十二变

(技术栈:Bash Shell 4.0+)

通过参数化改造提升灵活性:

#!/bin/bash
# 通用备份脚本

# 解析参数
while getopts "s:p:r:" opt; do
    case $opt in
        s) source_dir="$OPTARG" ;;
        p) prefix_name="$OPTARG" ;;
        r) retention_days="$OPTARG" ;;
        *) echo "Invalid option"; exit 1 ;;
    esac
done

# 参数校验
[[ -z "$source_dir" ]] && { echo "必须指定源目录"; exit 1; }

# 使用带默认值的参数
: ${prefix_name:=default_backup}
: ${retention_days:=7}

# 主逻辑
backup_dir="/backup/$(date +%Y%m%d)"
mkdir -p "$backup_dir"
tar -czf "${backup_dir}/${prefix_name}.tar.gz" -C "$source_dir" .
find "/backup" -type d -mtime +$retention_days -exec rm -rf {} \;

应用场景:需要频繁调整配置参数的场景,如定时任务、CI/CD流水线

技术优势

  • 提升脚本通用性
  • 降低使用门槛
  • 支持配置默认值

注意事项

  • 必须包含参数校验逻辑
  • 重要参数建议设置默认值
  • 使用getopts规范参数解析

5. 组合拳:构建你的脚本兵器库

将上述方法结合使用:

#!/bin/bash
# 主程序:backup_manager.sh

# 加载工具库
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib"
source "${LIB_DIR}/logging_utils.sh"
source "${LIB_DIR}/notify_utils.sh"

# 主函数
main() {
    local config_file=$1
    source "$config_file" || exit 1
    
    init_backup_system
    perform_backups
    send_notification "Backup completed"
}

# 启动程序
main "$@"

技术优势

  • 分层架构清晰
  • 功能模块可插拔
  • 配置与逻辑分离

6. 技术方案的AB面

优点对比

  • 函数封装:开发成本低,适合小型项目
  • 模块化:维护性好,适合团队协作
  • 参数化:灵活性高,适合通用工具

潜在缺陷

  • 函数间存在作用域污染风险
  • 模块加载增加启动耗时
  • 过度抽象可能降低可读性

避坑指南

  1. 使用shellcheck进行静态检查
  2. 重要操作添加dry-run模式
  3. 保持函数不超过50行
  4. 为共享函数添加版本控制

7. 总结:让脚本拥有超能力

通过函数封装、模块拆分、参数优化三板斧,我们可以让Bash脚本焕发新生。就像给传统武术加上现代格斗技巧,既保留Shell脚本的轻量优势,又获得现代编程的工程化特性。记住:当脚本超过300行时,就该考虑用Python重写了——但在此之前,这些优化技巧足以帮你打造出高效的Shell武器库。

下次当你的手指准备按下Ctrl+C/Ctrl+V时,不妨停下来想想:这个代码片段值得被封装吗?毕竟,优秀的工程师都在用脑子写代码,而不是用手。