Bash 脚本编写技巧:变量、函数与错误处理

FreeGuideOnline 最新 2026-06-13

Bash 脚本编写技巧:变量、函数与错误处理

本教程将带你掌握 Bash 脚本中三个核心构建块:变量的灵活运用、函数的模块化封装,以及错误处理的稳健实践。无论你是刚接触脚本编写,还是想提升代码质量,这些技巧都将让你的脚本更可靠、更易维护。

变量:脚本的灵活记忆

变量是存储数据的容器。在 Bash 中用好变量,能避免硬编码,让脚本适应不同场景。

声明与赋值

#!/bin/bash
# 基本赋值(等号两端不要有空格)
name="FreeCourse"
count=42

# 命令替换:将命令的输出赋给变量
current_date=$(date +%Y-%m-%d)
file_list=$(ls /var/log)

技巧:优先使用 $() 进行命令替换,它比反引号 ` 更清晰,且支持嵌套。

安全引用:双引号的重要性

未使用引号包裹的变量会导致单词拆分和通配符展开,是脚本中的常见陷阱。

# 错误:$filename 中包含空格时会被拆分为多个参数
rm $filename

# 正确:始终用双引号包裹变量
rm "$filename"

规则:只要变量值中可能包含空格或特殊字符,就用双引号括起来:"$var"

默认值与参数扩展

利用参数扩展可以优雅地处理未定义或空变量。

# 设置默认值:若 var 未定义或为空,则使用 "default"
echo "${var:-default}"

# 赋默认值:若 var 未定义或为空,则将其赋值为 "default" 并返回该值
: "${var:=default}"

# 使用示例
backup_dir="${1:-/tmp/backup}"   # 从命令行第一个参数获取,未提供则使用默认值
db_name="${DB_NAME:?DB_NAME is not set}"  # 若未定义则退出并报错

变量作用域

  • 默认全局:Bash 中变量默认是全局的,但在函数内可以用 local 声明局部变量。
  • 最佳实践:在函数内始终使用 local,防止污染全局命名空间。
function test_scope() {
    local inside="只能在函数内访问"
    echo "$inside"
}
echo "$inside"  # 此行将输出空,变量不可见

函数:让代码模块化

函数将重复的任务封装起来,提高可读性和可维护性。

定义与调用

# 两种主流定义方式
function greet {
    echo "Hello, $1!"   # $1 是第一个参数
}

say_goodbye() {
    echo "Goodbye, ${@}!"  # $@ 代表所有参数
}

# 调用
greet "Alice"
say_goodbye "Bob" "Charlie"

在函数内部,$1$2... 是位置参数,$# 是参数个数,$@ 是所有参数的列表(单独单词)。

返回值与输出

Bash 函数的返回机制有两个层:shell 命令的标准输出(stdout)和退出状态码(return value)。

  • 使用 echoprintf 输出数据,供调用者捕获。
  • 使用 return 返回状态码(0 表示成功,非0表示失败)。
# 通过 stdout 返回计算结果
get_sum() {
    local total=$(( $1 + $2 ))
    echo "$total"
}

result=$(get_sum 3 5)   # result=8

# 通过退出状态码返回执行状态
file_exists() {
    [[ -f "$1" ]] && return 0 || return 1
}

if file_exists "/etc/passwd"; then
    echo "文件存在"
fi

关键技巧:复杂的函数尽量通过状态码表示成败,数据通过 stdout 返回,并在调用处用 $(...) 捕获。

函数库与包含

将常用函数放入独立文件,然后在脚本中用 source(或 .)引入。

# lib.sh 文件
#!/bin/bash
log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}

# 主脚本
#!/bin/bash
source ./lib.sh
log "脚本开始执行"

这样你可以构建自己的 Bash 工具集,提高复用性。

错误处理:打造健壮的脚本

默认情况下,Bash 遇到错误会继续执行,这往往导致雪崩式故障。通过设置合适的选项和结构,让脚本在第一时间退出并给出清晰提示。

关键 Shell 选项

在脚本开头显式设置以下选项:

#!/bin/bash
set -euo pipefail
  • -e (errexit):任何命令返回非0退出状态时立刻终止脚本。
  • -u (nounset):使用未定义变量时视为错误并退出。
  • -o pipefail:管道中任何一个命令失败,整个管道的返回值为失败。配合 -e 使用。

例外处理:有时命令返回非0是正常情况(如 grep 没找到匹配),可以这样避免脚本退出:

# 临时关闭 -e
set +e
grep "pattern" file.txt
result=$?
set -e

# 或使用 || true 强制命令总是成功
grep "pattern" file.txt || true

捕获错误和处理函数

使用 trap 可以在脚本退出或发生错误时执行清理或通知。

#!/bin/bash
set -euo pipefail

cleanup() {
    echo "清理临时文件..."
    rm -f "$temp_file"
}

error_handler() {
    local line_no=$1
    local err_code=$2
    echo "错误:第 $line_no 行,退出码 $err_code" >&2
    exit "$err_code"
}

trap cleanup EXIT
trap 'error_handler ${LINENO} $?' ERR

temp_file=$(mktemp)
# 假设这里执行一些操作
echo "处理中..."
# 故意制造一个错误(用于演示)
false  # 该命令会触发 ERR trap

trap 'error_handler ...' ERR 会在任何因 -e 导致退出的命令上触发,便于打印出错位置。

输入验证与防御性编程

永远不要假设用户输入是干净的。在脚本开头检查必要的参数和环境。

#!/bin/bash
set -euo pipefail

# 检查参数数量
if [[ $# -ne 2 ]]; then
    echo "用法: $0 <源目录> <目标目录>" >&2
    exit 1
fi

src_dir="$1"
dest_dir="$2"

# 验证源目录存在且可读
if [[ ! -d "$src_dir" ]]; then
    echo "错误:源目录 $src_dir 不存在" >&2
    exit 1
fi

if [[ ! -r "$src_dir" ]]; then
    echo "错误:源目录 $src_dir 不可读" >&2
    exit 1
fi

函数内的错误传播

在函数内部如果使用了 set -e,错误会导致整个脚本退出;但如果你想让调用者决定如何处理错误,可以在函数内使用局部错误处理。

#!/bin/bash
set -euo pipefail

# 一个可能失败的操作,以状态码返回
do_dangerous_work() {
    local output
    # 临时关闭 errexit,用 if 捕获错误
    set +e
    output=$(some_command 2>/dev/null)
    local rc=$?
    set -e

    if [[ $rc -ne 0 ]]; then
        echo "操作失败,内部错误码 $rc" >&2
        return 1
    fi
    echo "$output"
}

# 调用者可以检查函数返回值
if result=$(do_dangerous_work); then
    echo "成功:$result"
else
    echo "调用失败,脚本继续运行或根据业务处理"
    exit 1
fi

综合示例:备份脚本

将上述技巧结合,编写一个带参数检查、函数封装和错误处理的备份脚本骨架。

#!/bin/bash
set -euo pipefail

# 配置区
readonly BACKUP_ROOT="/var/backups"
readonly LOG_FILE="/var/log/backup.log"

# 函数库:日志
log() {
    local level="$1"; shift
    echo "[$(date +'%F %T')] [$level] $*" >> "$LOG_FILE"
}

# 错误处理 trap
cleanup() {
    log INFO "脚本结束,清理操作(如有)"
}
trap cleanup EXIT
trap 'log ERROR "第 $LINENO 行出错,退出码 $?"' ERR

# 参数检查
if [[ $# -lt 1 ]]; then
    echo "用法: $0 <项目名> [保留天数]" >&2
    exit 1
fi

project="$1"
retention_days="${2:-7}"   # 默认保留7天

src_dir="./data/$project"
backup_file="${BACKUP_ROOT}/${project}_$(date +%Y%m%d_%H%M%S).tar.gz"

# 前置条件验证
[[ -d "$src_dir" ]] || { echo "错误:源目录 $src_dir 不存在" >&2; exit 1; }

# 核心功能函数
create_backup() {
    log INFO "开始备份 $src_dir -> $backup_file"
    tar -czf "$backup_file" -C "$(dirname "$src_dir")" "$(basename "$src_dir")"
    log INFO "备份完成,大小 $(stat -c%s "$backup_file") 字节"
}

clean_old_backups() {
    log INFO "清理 ${retention_days} 天前的备份"
    find "$BACKUP_ROOT" -name "${project}_*.tar.gz" -mtime +"$retention_days" -delete
    log INFO "清理完成"
}

# 执行主流程
create_backup
clean_old_backups

echo "备份成功: $backup_file"
exit 0

总结

掌握 Bash 变量、函数与错误处理技巧,能让你的脚本从一次性“打火机”蜕变为可靠的自动化工具。牢记以下要点:

  • 变量:始终用双引号保护,善用参数扩展设定默认值。
  • 函数:使用 local 限定作用域;数据通过 stdout 返回,状态通过 return 码。
  • 错误处理set -euo pipefail 作为起点,配合 trap 和输入检查打造防御式脚本。

将这些模式融入日常编写,你会发现自己脚本的稳定性显著提升,debug 时间大幅缩短。现在,动手重构你的下一个 Bash 脚本吧!