Shell调试的核心技巧

Shell调试的核心是:打开执行跟踪、检查命令返回值、定位变量问题、捕获错误、缩小问题范围。


1.常用调试命令速查

命令 作用
bash -x script.sh 打印执行的命令及展开后的参数
bash -n script.sh 只检查语法,不执行脚本
bash -v script.sh 打印读取到的脚本原始内容
echo "$?" 查看上一条命令的退出码
set -x 开启执行跟踪
set +x 关闭执行跟踪
type command 查看命令是别名、函数还是可执行文件
command -v command 查找命令的位置或类型
declare -p var 查看变量的值和属性
trap '...' ERR 在发生未捕获的错误时执行处理逻辑

2.最常用的5种调试技巧

2.1使用bash -x跟踪执行过程

例如有一个脚本test.sh:

#!/usr/bin/env bash

name="Alice"
dir="/tmp/demo"

mkdir -p "$dir"
echo "Hello, $name"
ls "$dir/not_exist"
echo "Finished"

执行:

bash -x test.sh

输出类似:

+ name=Alice
+ dir=/tmp/demo
+ mkdir -p /tmp/demo
+ echo 'Hello, Alice'
Hello, Alice
+ ls /tmp/demo/not_exist
ls: cannot access '/tmp/demo/not_exist': No such file or directory
+ echo Finished
Finished

可以快速看出:

  • 每一步执行了什么命令。
  • 变量展开后的实际内容是什么。
  • 错误发生在哪一步。
  • 命令失败后脚本是否继续执行。

如果只想跟踪某一段代码:

set -x
# 需要调试的代码
process_data "$input"
set +x

注意:set -x可能输出密码、Token等敏感信息,调试生产环境时应避免直接打印敏感变量。

2.2检查退出码:$?

Shell通常通过退出码表示命令执行结果:

  • 0:成功。
  • 非0:失败,具体含义取决于命令。
grep "ERROR" app.log
rc=$?

if [ "$rc" -ne 0 ]; then
    echo "grep failed or no match, rc=$rc"
fi

2.3调试变量:检查空值、空格和引号

Shell中大量问题来自变量展开和字符串处理。

name="hello world"

echo $name
echo "$name"

两者看起来相似,但行为不同:

  • echo $name:变量展开后还会发生单词分割和路径名展开。
  • echo "$name":保留变量内容中的空格,通常更安全。

调试变量时推荐:

printf 'name=<%s>\n' "$name"
declare -p name

例如:

file="my file.txt"
ls $file

2.4使用set -euo pipefail捕获常见错误

脚本开头可以使用:

#!/usr/bin/env bash
set -euo pipefail

各选项的含义:

选项 含义
-e 某些未处理的命令失败时退出
-u 使用未定义变量时触发错误
-o pipefail 管道中任意命令失败时,管道整体返回失败

例如:

#!/usr/bin/env bash
set -euo pipefail

input="data.txt"
cat "$input" | grep "ERROR" | wc -l

echo "Done"

需要注意,set -e存在条件判断、函数、管道等方面的例外行为,不能替代显式错误处理。实际开发中,关键操作最好明确检查:

if ! cp source.txt target.txt; then
    echo "Copy failed" >&2
    exit 1
fi

2.5使用trap定位错误行

对于较长的脚本,可以在开头添加:

#!/usr/bin/env bash
set -Eeuo pipefail

trap 'rc=$?; printf "ERROR: line=%s rc=%s command=%s\n" \
    "$LINENO" "$rc" "$BASH_COMMAND" >&2' ERR

当符合ERR触发条件的命令失败时,可以输出:

  • 出错行号。
  • 命令退出码。
  • 发生错误的命令。

ERR陷阱和set -e一样存在特定的触发规则,并非所有失败都会触发。复杂脚本中还可以使用DEBUG和RETURN陷阱,但它们会显著增加输出量。