Bash脚本进阶技巧与自动化实践指南

1. 为什么你需要掌握Bash脚本进阶技巧

在Linux和Unix系统中,Bash脚本就像一把瑞士军刀,能帮你把重复性工作自动化。我见过太多运维工程师每天花几个小时手动执行相同的命令序列,而一个精心编写的脚本可能只需要几秒钟就能完成同样的工作。

Bash脚本的强大之处在于它的普遍性——几乎所有的Linux发行版都预装了Bash,这意味着你写的脚本可以几乎无障碍地在不同系统间迁移。从简单的文件批量重命名到复杂的系统监控任务,Bash都能胜任。

提示:不要被"脚本"这个词吓到,它其实就是一系列命令的集合,和你平时在终端手动输入的命令没有本质区别,只是把它们保存到一个文件中自动执行而已。

2. Bash变量:脚本的基石

2.1 变量定义与使用

变量是任何编程语言的基础,Bash也不例外。定义变量时,变量名和值之间不能有空格:

name="John" age=30

使用变量时要在前面加$符号:

echo $name # 输出John echo $age # 输出30

注意:Bash变量默认都是字符串类型,即使你给它们赋数字值。这在数学运算时需要特别注意。

2.2 特殊变量

Bash提供了一些特殊变量,在脚本中非常有用:

  • $0:当前脚本名称
  • $1-$9:脚本参数
  • $#:参数个数
  • $?:上一条命令的退出状态
  • $$:当前脚本的进程ID
#!/bin/bash echo "脚本名: $0" echo "第一个参数: $1" echo "参数总数: $#"

2.3 变量扩展技巧

Bash提供了丰富的变量扩展功能:

# 默认值 echo ${name:-"默认名字"} # 如果name未设置,使用"默认名字" # 字符串长度 str="hello" echo ${#str} # 输出5 # 子字符串 echo ${str:1:3} # 从索引1开始取3个字符,输出"ell"

3. 条件判断:让脚本更智能

3.1 if语句基础

Bash中的if语句基本结构:

if [ 条件 ]; then # 条件为真时执行的命令 elif [ 其他条件 ]; then # 其他条件为真时执行的命令 else # 所有条件都不满足时执行的命令 fi

3.2 常用测试条件

  • 文件测试:

    [ -f file ] # 文件存在且是普通文件 [ -d dir ] # 目录存在 [ -r file ] # 文件可读
  • 字符串比较:

    [ "$str1" = "$str2" ] # 字符串相等 [ "$str1" != "$str2" ] # 字符串不等 [ -z "$str" ] # 字符串为空
  • 数值比较:

    [ $num1 -eq $num2 ] # 等于 [ $num1 -lt $num2 ] # 小于 [ $num1 -gt $num2 ] # 大于

3.3 case语句

当有多个条件分支时,case语句比多个if更清晰:

case $变量 in 模式1) 命令1 ;; 模式2) 命令2 ;; *) 默认命令 ;; esac

4. 循环结构:批量处理的利器

4.1 for循环

最基本的for循环形式:

for 变量 in 列表 do 命令 done

示例:

# 遍历数字 for i in {1..5}; do echo $i done # 遍历文件 for file in *.txt; do echo "处理文件: $file" done

4.2 while循环

while循环在条件为真时持续执行:

while [ 条件 ] do 命令 done

示例:读取文件内容

while read line; do echo "行内容: $line" done < file.txt

4.3 until循环

until循环与while相反,条件为假时执行:

until [ 条件 ] do 命令 done

4.4 循环控制

  • break:跳出循环
  • continue:跳过本次循环
  • exit:退出整个脚本

5. 函数:模块化你的脚本

5.1 定义和调用函数

function 函数名 { 命令 return 返回值 } # 或者 函数名() { 命令 return 返回值 }

调用函数:

函数名 参数1 参数2

5.2 函数参数和返回值

函数内部通过$1,$2等访问参数,$#获取参数个数:

greet() { echo "Hello, $1" return 0 } greet "World"

返回值通过return指定,只能是0-255的整数。要返回其他数据,可以使用全局变量或输出到标准输出。

6. 实用脚本示例

6.1 系统监控脚本

#!/bin/bash # 监控CPU使用率 cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') # 监控内存使用 mem_total=$(free -m | awk '/Mem:/ {print $2}') mem_used=$(free -m | awk '/Mem:/ {print $3}') mem_percent=$((mem_used*100/mem_total)) # 监控磁盘空间 disk_usage=$(df -h / | awk '/\// {print $5}' | tr -d '%') # 输出报告 echo "系统监控报告 - $(date)" echo "CPU使用率: ${cpu_usage}%" echo "内存使用: ${mem_used}MB/${mem_total}MB (${mem_percent}%)" echo "根分区使用率: ${disk_usage}%" # 如果CPU或内存使用超过90%,发出警告 if [ ${cpu_usage%.*} -gt 90 ] || [ $mem_percent -gt 90 ]; then echo "警告: 系统资源使用过高!" >&2 exit 1 fi

6.2 日志分析脚本

#!/bin/bash # 分析指定日志文件中的错误 log_file=$1 output_file="error_report_$(date +%Y%m%d).txt" if [ ! -f "$log_file" ]; then echo "错误: 日志文件 $log_file 不存在" >&2 exit 1 fi # 统计错误数量 error_count=$(grep -i "error" "$log_file" | wc -l) # 提取错误详情 echo "错误报告 - $(date)" > "$output_file" echo "日志文件: $log_file" >> "$output_file" echo "总错误数: $error_count" >> "$output_file" echo "" >> "$output_file" echo "错误详情:" >> "$output_file" grep -i "error" "$log_file" >> "$output_file" echo "错误报告已生成: $output_file"

7. 调试和优化技巧

7.1 调试技巧

  • 使用-x选项运行脚本,显示执行的每条命令:

    bash -x script.sh
  • 在脚本中插入set -xset +x来启用/禁用部分代码的调试:

    set -x # 要调试的代码 set +x
  • 使用trap捕获信号:

    trap 'echo "脚本被中断"; exit' INT TERM

7.2 性能优化

  • 减少子进程创建:每个|管道或$(command)都会创建新进程
  • 使用内置命令代替外部命令:如[[ ]][ ]更高效
  • 避免不必要的循环:能用findgrep完成的就不要用循环
  • 使用awksed处理大文件比逐行读取更高效

8. 常见问题与解决方案

8.1 变量扩展问题

问题:变量中包含空格或特殊字符时出错

解决方案:总是用双引号引用变量:

# 错误 rm $file # 正确 rm "$file"

8.2 权限问题

问题:脚本无法执行

解决方案:

chmod +x script.sh # 添加执行权限 ./script.sh # 执行脚本

8.3 路径问题

问题:脚本在不同目录执行结果不同

解决方案:在脚本开头获取绝对路径:

script_dir=$(cd "$(dirname "$0")" && pwd)

8.4 兼容性问题

问题:脚本在不同系统表现不同

解决方案:

  • 明确指定解释器:#!/bin/bash
  • 避免使用特定版本的Bash特性
  • 测试脚本在不同环境的表现

9. 进阶技巧

9.1 数组操作

# 定义数组 fruits=("apple" "banana" "orange") # 访问元素 echo ${fruits[0]} # apple # 遍历数组 for fruit in "${fruits[@]}"; do echo $fruit done # 数组长度 echo ${#fruits[@]} # 3

9.2 关联数组(Bash 4.0+)

declare -A user user[name]="John" user[age]=30 echo ${user[name]} # John

9.3 进程替换

# 比较两个命令的输出 diff <(command1) <(command2)

9.4 并行处理

# 使用&后台运行和wait for i in {1..10}; do command $i & done wait # 等待所有后台进程完成

10. 实际应用场景

10.1 自动化部署

#!/bin/bash # 检查参数 if [ $# -ne 2 ]; then echo "用法: $0 项目目录 目标服务器" >&2 exit 1 fi project_dir=$1 server=$2 backup_dir="/var/backups/$(date +%Y%m%d)" # 在目标服务器创建备份目录 ssh "$server" "mkdir -p $backup_dir" # 备份现有项目 echo "备份现有项目..." ssh "$server" "tar czf $backup_dir/project_backup.tar.gz /var/www/html" # 部署新版本 echo "部署新版本..." rsync -avz --delete "$project_dir/" "$server:/var/www/html/" # 重启服务 echo "重启服务..." ssh "$server" "systemctl restart apache2" echo "部署完成"

10.2 日志轮转

#!/bin/bash log_dir="/var/log/myapp" max_files=10 # 轮转日志 for logfile in "$log_dir"/*.log; do # 跳过非普通文件 [ -f "$logfile" ] || continue # 压缩旧日志 gzip "$logfile" mv "$logfile.gz" "$logfile.$(date +%Y%m%d).gz" done # 删除多余的日志文件 ls -t "$log_dir"/*.gz | tail -n +$((max_files + 1)) | xargs rm -f

10.3 批量图片处理

#!/bin/bash input_dir="images" output_dir="processed" width=800 mkdir -p "$output_dir" # 使用find和xargs并行处理 find "$input_dir" -name "*.jpg" -print0 | xargs -0 -P 4 -I {} convert "{}" -resize "${width}x" "$output_dir/$(basename "{}")" echo "图片处理完成"

11. 安全最佳实践

11.1 输入验证

# 验证参数是数字 if ! [[ "$1" =~ ^[0-9]+$ ]]; then echo "错误: 参数必须是数字" >&2 exit 1 fi

11.2 权限最小化

# 检查是否是root用户 if [ "$(id -u)" -ne 0 ]; then echo "错误: 需要root权限" >&2 exit 1 fi

11.3 敏感信息处理

# 从配置文件中读取密码 password=$(grep '^password=' config.ini | cut -d= -f2) # 使用后立即清除 unset password

11.4 使用set -euo pipefail

#!/bin/bash set -euo pipefail # -e: 命令失败时立即退出 # -u: 使用未定义变量时报错 # -o pipefail: 管道中任一命令失败则整个管道失败

12. 资源推荐

12.1 在线学习资源

  • Bash官方文档:https://www.gnu.org/software/bash/manual/
  • Bash脚本教程:https://linuxconfig.org/bash-scripting-tutorial
  • ShellCheck(在线语法检查):https://www.shellcheck.net/

12.2 实用工具

  • jq:处理JSON数据
  • yq:处理YAML数据
  • parallel:并行执行命令
  • tmux:终端多路复用器

12.3 进阶书籍

  • 《Linux命令行与shell脚本编程大全》
  • 《Bash Cookbook》
  • 《Shell脚本学习指南》

13. 个人经验分享

在实际工作中,我发现Bash脚本最适合处理那些重复性的系统管理任务。比如我曾经写过一个脚本,自动收集多台服务器的系统信息并生成汇总报告,把原本需要半天的手工工作缩短到了几分钟。

一个重要的经验是:脚本写完后,一定要在各种边界条件下测试。我曾经因为没考虑文件名中包含空格的情况,导致一个删除旧日志的脚本误删了重要文件。现在我会在脚本开头加上set -euo pipefail,并尽可能用双引号引用所有变量。

另一个建议是:即使是很小的脚本也要写注释。你可能觉得简单的逻辑不需要注释,但几个月后再看时,可能完全不记得当初为什么要这样写。好的注释应该解释"为什么"而不是"做什么"。