一、Shell脚本为什么容易出错?

Shell脚本作为Linux系统管理员的瑞士军刀,用起来确实方便,但写起来却处处是坑。很多新手甚至老手都会在不经意间踩到这些雷区。最常见的问题莫过于忘记在变量赋值时等号两边不能有空格,或者在条件判断时少写了一个空格。

举个例子,我们来看一个典型的错误示范:

#!/bin/bash
# 错误示例:等号两边有空格
var = "hello"  # 这会导致语法错误
echo $var

正确的写法应该是:

#!/bin/bash
# 正确示例
var="hello"  # 等号两边不能有空格
echo "$var"  # 变量引用最好用双引号包裹

另一个常见错误是在条件判断时搞错括号的使用。比如:

# 错误示例
if [$var -eq "hello"]; then  # 方括号两边需要空格,且内部操作符也需要空格
    echo "匹配"
fi

正确的写法是:

# 正确示例
if [ "$var" = "hello" ]; then  # 方括号两边和内部都需要空格
    echo "匹配"
fi

二、变量使用的常见陷阱

变量处理是Shell脚本中最容易出错的部分之一。很多人不知道Shell中的变量默认都是全局的,在函数内部修改会影响外部同名变量。

来看一个典型的变量作用域问题:

#!/bin/bash
var="outer"

function test() {
    var="inner"  # 这会修改全局变量
    echo "函数内: $var"
}

test
echo "函数外: $var"  # 输出会是inner,这可能不是我们想要的

解决方法是用local关键字声明局部变量:

#!/bin/bash
var="outer"

function test() {
    local var="inner"  # 使用local声明局部变量
    echo "函数内: $var"
}

test
echo "函数外: $var"  # 输出保持outer

另一个常见问题是未定义的变量会导致脚本异常退出。比如:

#!/bin/bash
# 危险示例:未设置-u选项时,未定义变量不会报错
echo "文件名是: $filename"  # $filename未定义,但脚本会继续执行

安全做法是开启严格模式:

#!/bin/bash
set -euo pipefail  # -e:出错退出 -u:未定义变量报错 -o pipefail:管道命令失败退出
echo "文件名是: $filename"  # 现在会报错退出

三、字符串处理的正确姿势

Shell中的字符串处理看似简单,实则暗藏玄机。很多人不知道双引号和单引号的区别,导致脚本行为异常。

来看一个引号使用的例子:

#!/bin/bash
name="John Doe"

# 单引号示例 - 不展开变量
echo 'Hello, $name!'  # 输出: Hello, $name!

# 双引号示例 - 展开变量
echo "Hello, $name!"  # 输出: Hello, John Doe!

字符串拼接也有讲究:

#!/bin/bash
first="Hello"
second="World"

# 正确拼接方式
result="$first $second"  # 使用双引号包裹
echo "$result"

# 错误拼接方式
result=$first $second  # 这会导致语法错误

字符串比较时,很多人会忽略空格的影响:

#!/bin/bash
str1="hello"
str2="hello "

# 错误比较方式
if [ "$str1" == "$str2" ]; then  # 注意str2末尾有空格
    echo "相等"
else
    echo "不相等"  # 会输出这个
fi

四、数组操作的最佳实践

Shell数组是个强大的功能,但语法却相当反直觉。很多人不知道如何正确声明和遍历数组。

来看一个数组声明和遍历的例子:

#!/bin/bash
# 声明数组的几种方式
arr1=(1 2 3)  # 最常用
arr2=([0]=1 [1]=2 [2]=3)  # 指定下标
arr3[0]=1; arr3[1]=2; arr3[2]=3  # 逐个赋值

# 遍历数组的正确方式
for i in "${arr1[@]}"; do  # 使用@和双引号确保正确处理空格
    echo "$i"
done

获取数组长度也是个常见问题:

#!/bin/bash
arr=(a b c d e)

# 正确获取数组长度
length=${#arr[@]}  # 使用#获取长度
echo "数组长度: $length"

# 错误方式
length=${#arr}  # 这只会获取第一个元素的长度

五、函数编写的高级技巧

Shell函数虽然简单,但要写出健壮的函数需要考虑很多细节。比如参数传递、返回值处理等。

来看一个带参数的函数示例:

#!/bin/bash
# 定义函数
greet() {
    local name="$1"  # 第一个参数
    local times="${2:-1}"  # 第二个参数,默认值1

    for ((i=0; i<times; i++)); do
        echo "Hello, $name!"
    done
}

# 调用函数
greet "Alice" 3  # 带两个参数
greet "Bob"     # 只带一个参数

函数返回值处理也很重要:

#!/bin/bash
# 返回值的正确处理方式
get_status() {
    if [ "$1" -gt 10 ]; then
        return 0  # 成功
    else
        return 1  # 失败
    fi
}

# 调用并检查返回值
if get_status 15; then
    echo "状态正常"
else
    echo "状态异常"
fi

六、错误处理和日志记录

健壮的脚本必须有良好的错误处理和日志记录机制。很多人忽略这一点,导致问题难以排查。

来看一个基本的错误处理框架:

#!/bin/bash
set -euo pipefail  # 开启严格模式

# 定义日志函数
log() {
    local level="$1"
    local message="$2"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" >> script.log
}

# 使用示例
log "INFO" "脚本开始执行"

# 模拟一个可能失败的操作
if ! some_command; then
    log "ERROR" "命令执行失败"
    exit 1
fi

log "INFO" "脚本执行完成"

信号处理也很重要:

#!/bin/bash
# 信号处理示例
cleanup() {
    echo "正在清理..."
    # 执行清理操作
    exit 1
}

trap cleanup SIGINT SIGTERM  # 捕获中断信号

# 主循环
while true; do
    echo "运行中..."
    sleep 1
done

七、实战:编写一个健壮的备份脚本

让我们把这些技巧应用到一个实际的备份脚本中:

#!/bin/bash
set -euo pipefail  # 严格模式
PATH="/usr/local/bin:/usr/bin:/bin"  # 设置安全PATH

# 配置
BACKUP_DIR="/var/backups"
LOG_FILE="/var/log/backup.log"
KEEP_DAYS=7

# 日志函数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 执行备份
log "开始备份..."
backup_file="${BACKUP_DIR}/backup-$(date +%Y%m%d).tar.gz"
if tar -czf "$backup_file" /path/to/backup 2>> "$LOG_FILE"; then
    log "备份成功: $backup_file"
else
    log "备份失败!"
    exit 1
fi

# 清理旧备份
log "清理超过${KEEP_DAYS}天的旧备份..."
find "$BACKUP_DIR" -name 'backup-*.tar.gz' -mtime +$KEEP_DAYS -delete

log "备份任务完成"

这个脚本展示了我们讨论的多个最佳实践:

  1. 开启严格模式
  2. 设置安全的PATH
  3. 完善的日志记录
  4. 错误处理
  5. 使用find清理旧文件

八、总结与建议

通过以上示例和分析,我们可以总结出编写健壮Shell脚本的几个关键点:

  1. 总是开启严格模式(set -euo pipefail)
  2. 变量使用要小心:引用时加双引号,函数内使用local
  3. 字符串处理注意引号区别
  4. 数组操作使用正确的语法
  5. 函数要考虑参数和返回值
  6. 实现完善的错误处理和日志记录
  7. 考虑信号处理和超时机制

记住,Shell脚本虽然灵活,但也很容易写出难以维护的代码。对于复杂的任务,可能需要考虑使用Python等更强大的脚本语言。但对于系统管理、自动化等场景,掌握这些Shell脚本编写规范将大大提高你的工作效率和脚本可靠性。