Bash 脚本编写技巧:变量、函数与错误处理
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)。
- 使用
echo或printf输出数据,供调用者捕获。 - 使用
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 脚本吧!