调试与测试
调试是编写可靠脚本的关键技能。Shell 脚本没有像高级语言那样的完善调试工具,但 Bash 提供了多种调试机制。本章介绍 Shell 脚本的调试技巧、错误处理和测试方法。
调试选项
set 命令选项
set 命令可以控制 Shell 的行为,对于调试非常有用。
| 选项 | 含义 |
|---|---|
-e | 命令失败时立即退出 |
-u | 使用未定义变量时报错 |
-x | 打印执行的命令 |
-v | 打印读取的行 |
-o pipefail | 管道中任何命令失败时返回失败 |
脚本开头添加选项
#!/bin/bash
set -e # 遇错退出
set -u # 未定义变量报错
set -o pipefail # 管道失败检测
set -x # 打印执行的命令
# 或者合并写法
set -euxo pipefail
命令行调试
# 打印执行的命令
bash -x script.sh
# 打印读取的行
bash -v script.sh
# 同时使用多个选项
bash -xv script.sh
# 检查语法错误(不执行)
bash -n script.sh
调试特定代码段
#!/bin/bash
echo "Starting script"
# 开启调试
set -x
for i in 1 2 3; do
echo "Number: $i"
done
set +x
echo "Debug section ended"
trap 调试
捕获 DEBUG 信号
#!/bin/bash
# 在每条命令执行前打印信息
trap 'echo "Executing: $BASH_COMMAND at line $LINENO"' DEBUG
x=1
y=2
echo $((x + y))
捕获 ERR 信号
#!/bin/bash
# 命令失败时打印信息
trap 'echo "Error at line $LINENO: $BASH_COMMAND"' ERR
false # 这个命令会失败
echo "This won't print if set -e is enabled"
打印调用栈
#!/bin/bash
print_stack() {
echo "Call stack:"
local i
for ((i=${#FUNCNAME[@]}-1; i>=0; i--)); do
echo " ${FUNCNAME[$i]}() at ${BASH_SOURCE[$i]}:${BASH_LINENO[$i-1]}"
done
}
func_c() {
print_stack
}
func_b() {
func_c
}
func_a() {
func_b
}
func_a
错误处理
检查命令结果
#!/bin/bash
# 方式一:使用 if
if ! command; then
echo "Command failed"
exit 1
fi
# 方式二:使用 && 和 ||
command && echo "Success" || echo "Failed"
# 方式三:检查 $?
command
if [[ $? -ne 0 ]]; then
echo "Command failed"
exit 1
fi
自定义错误处理函数
#!/bin/bash
error_exit() {
echo "Error: $1" >&2
exit "${2:-1}"
}
# 使用
[[ -f "$file" ]] || error_exit "File not found: $file" 2
完整的错误处理示例
#!/bin/bash
set -euo pipefail
# 错误处理函数
on_error() {
local exit_code=$?
local line_no=$1
local command=$2
echo "Error on line $line_no: '$command'"
echo "Exit code: $exit_code"
# 清理工作
cleanup
exit $exit_code
}
cleanup() {
echo "Cleaning up..."
rm -f "$temp_file"
}
# 设置 trap
trap 'on_error $LINENO "$BASH_COMMAND"' ERR
trap cleanup EXIT
# 创建临时文件
temp_file="/tmp/script_$$"
touch "$temp_file"
# 主逻辑
echo "Working..."
日志记录
简单日志函数
#!/bin/bash
LOG_FILE="script.log"
log() {
local level=$1
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
# 使用
log_info "Starting script"
log_warn "This is a warning"
log_error "This is an error"
带颜色的日志
#!/bin/bash
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $*"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*"
}
log_debug() {
echo -e "${BLUE}[DEBUG]${NC} $*"
}
ShellCheck
ShellCheck 是一个 Shell 脚本静态分析工具,可以检测常见的错误和不良实践。
安装
# Ubuntu/Debian
sudo apt install shellcheck
# macOS
brew install shellcheck
# Windows (via scoop)
scoop install shellcheck
使用
# 检查脚本
shellcheck script.sh
# 指定 Shell 类型
shellcheck -s bash script.sh
# 输出格式
shellcheck -f gcc script.sh # GCC 风格
shellcheck -f json script.sh # JSON 格式
# 忽略特定警告
shellcheck -e SC2086 script.sh
常见警告
# SC2086: 双引号变量以防止词分割
echo $var # 警告
echo "$var" # 正确
# SC2039: 在 POSIX sh 中不可用
array=(a b c) # 在 sh 中不可用
# SC2164: cd 失败后使用 || exit
cd /tmp # 警告
cd /tmp || exit # 正确
# SC2181: 直接检查退出状态
command
if [ $? -eq 0 ]; then ... fi # 冗余
if command; then ... fi # 推荐
# SC2010: 不要通过 ls | grep 过滤
ls | grep ".txt" # 警告
ls *.txt # 正确
单元测试
Shell 脚本也可以进行单元测试,常用的测试框架有 shunit2 和 bats。
使用 shunit2
#!/bin/bash
# 被测试的函数
add() {
echo $(($1 + $2))
}
is_even() {
local num=$1
(( num % 2 == 0 ))
}
# 测试函数(以 test 开头)
testAdd() {
result=$(add 2 3)
assertEquals "2 + 3 should be 5" 5 "$result"
result=$(add 0 0)
assertEquals "0 + 0 should be 0" 0 "$result"
}
testIsEven() {
is_even 4
assertTrue "4 is even" $?
is_even 3
assertFalse "3 is not even" $?
}
# 加载 shunit2
. shunit2
使用 bats
#!/usr/bin/env bats
# 被测试的函数
add() {
echo $(($1 + $2))
}
@test "add 2 and 3" {
result=$(add 2 3)
[ "$result" -eq 5 ]
}
@test "add 0 and 0" {
result=$(add 0 0)
[ "$result" -eq 0 ]
}
@test "add negative numbers" {
result=$(add -1 -2)
[ "$result" -eq -3 ]
}
简单的手动测试
#!/bin/bash
# 被测试的函数
trim() {
local var="$1"
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
echo "$var"
}
# 测试函数
test_trim() {
local input expected actual
# 测试 1
input=" hello "
expected="hello"
actual=$(trim "$input")
if [[ "$actual" == "$expected" ]]; then
echo "PASS: trim('$input') = '$actual'"
else
echo "FAIL: trim('$input') = '$actual', expected '$expected'"
return 1
fi
# 测试 2
input="world"
expected="world"
actual=$(trim "$input")
if [[ "$actual" == "$expected" ]]; then
echo "PASS: trim('$input') = '$actual'"
else
echo "FAIL: trim('$input') = '$actual', expected '$expected'"
return 1
fi
# 测试 3
input=""
expected=""
actual=$(trim "$input")
if [[ "$actual" == "$expected" ]]; then
echo "PASS: trim('$input') = '$actual'"
else
echo "FAIL: trim('$input') = '$actual', expected '$expected'"
return 1
fi
}
# 运行测试
test_trim
常见错误与解决
变量未加引号
# 错误
file="my document.txt"
ls $file # 词分割:ls my document.txt
# 正确
ls "$file"
忘记 shebang
# 错误:没有 shebang,使用当前 Shell 执行
echo "Hello"
# 正确
#!/bin/bash
echo "Hello"
条件判断错误
# 错误:使用 = 比较数字
if [ $a = $b ]; then ... fi
# 正确:使用 -eq 比较数字
if [ $a -eq $b ]; then ... fi
# 或者使用 (( ))
if (( a == b )); then ... fi
路径中的空格
# 错误
path="/path/with spaces/file.txt"
cat $path
# 正确
cat "$path"
子 Shell 中的变量修改
# 错误:管道在子 Shell 中执行
echo "hello" | read var
echo $var # 输出为空
# 正确:使用进程替换
read var <<< "hello"
echo $var # 输出:hello
# 或者使用 lastpipe
shopt -s lastpipe
echo "hello" | read var
echo $var
循环中的变量作用域
# 错误:循环变量在循环外仍存在
for i in 1 2 3; do
echo $i
done
echo $i # 输出:3
# 正确:使用子 Shell 或清理变量
(
for i in 1 2 3; do
echo $i
done
)
echo $i # 输出为空
调试技巧总结
快速定位问题
#!/bin/bash
# 1. 开启严格模式
set -euo pipefail
# 2. 添加调试输出
debug() {
if [[ "${DEBUG:-}" == "1" ]]; then
echo "[DEBUG] $*" >&2
fi
}
# 3. 使用 trap 捕获错误
trap 'echo "Error at line $LINENO"; exit 1' ERR
# 4. 关键步骤添加日志
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
# 主逻辑
log "Starting..."
debug "Debug mode enabled"
使用 assert 函数
#!/bin/bash
assert() {
local condition=$1
local message=${2:-"Assertion failed"}
if ! eval "$condition"; then
echo "Assertion failed: $message" >&2
echo "Condition: $condition" >&2
exit 1
fi
}
# 使用
x=5
assert "[[ $x -eq 5 ]]" "x should be 5"
assert "(( x > 0 ))" "x should be positive"
性能分析
#!/bin/bash
# 使用 time 命令
time ./script.sh
# 使用 date 计算耗时
start=$(date +%s)
# ... 代码 ...
end=$(date +%s)
echo "Elapsed: $((end - start)) seconds"
# 使用 Bash 内置变量
SECONDS=0
# ... 代码 ...
echo "Elapsed: $SECONDS seconds"
小结
本章介绍了 Shell 脚本的调试和测试方法:
- 调试选项:
set -euxo pipefail、bash -x、bash -n - trap 调试:捕获 DEBUG、ERR 信号
- 错误处理:检查命令结果、自定义错误处理
- 日志记录:日志函数、带颜色的输出
- ShellCheck:静态分析工具
- 单元测试:shunit2、bats
- 常见错误:变量引号、条件判断、作用域问题
良好的调试习惯和完善的错误处理是编写可靠脚本的基础。使用 ShellCheck 进行静态分析,使用测试框架进行单元测试,可以大大提高脚本的质量。