Shell 脚本编写最佳实践
- 脚本头部规范
#!/bin/bash # 明确的 shebang
# -*- coding: utf-8 -*- # 编码声明
# 作者:Your Name
# 描述:脚本功能说明
# 日期:2024-01-01
# 版本:1.0
- 严格的错误处理
# 遇到错误立即退出
set -e
# 未定义变量时报错
set -u
# 管道命令中有错误也退出
set -o pipefail
# 或者在脚本开始统一设置
set -euo pipefail
- 变量使用规范
# 使用有意义的变量名
readonly MAX_RETRY=3 # 只读变量
local temp_file="/tmp/temp.$$" # 函数内使用 local
# 变量引用用花括号
echo "${user_name}_file"
# 默认值设置
name=${1:-"default_name"}
# 使用大写表示环境变量/全局常量
readonly CONFIG_FILE="/etc/myapp/config.conf"
export APP_HOME="/opt/myapp"
- 函数化编程
# 每个功能独立成函数
usage() {
echo "Usage: $0 [options] <argument>"
echo "Options:"
echo " -h Show this help"
echo " -v Verbose mode"
}
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $*"
}
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $*" >&2
}
main() {
# 主逻辑
log_info "Script started"
# ...
log_info "Script finished"
}
# 执行主函数
main "$@"
- 参数处理
# 使用 getopts 处理命令行参数
while getopts "hvf:" opt; do
case $opt in
h)
usage
exit 0
;;
v)
verbose=true
;;
f)
config_file="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND-1))
- 输入验证
# 检查参数数量
if [ $# -lt 2 ]; then
echo "Error: Missing arguments" >&2
usage
exit 1
fi
# 验证文件是否存在
if [ ! -f "$config_file" ]; then
echo "Error: Config file not found: $config_file" >&2
exit 1
fi
# 验证目录是否存在,不存在则创建
mkdir -p "$log_dir" || {
echo "Error: Cannot create directory $log_dir" >&2
exit 1
}
# 验证输入格式
if ! [[ "$1" =~ ^[0-9]+$ ]]; then
echo "Error: Argument must be a number" >&2
exit 1
fi
- 日志记录
# 日志级别
LOG_LEVEL=${LOG_LEVEL:-"INFO"}
debug() {
[ "$LOG_LEVEL" = "DEBUG" ] && echo "[DEBUG] $*"
}
info() {
echo "[INFO] $*"
}
error() {
echo "[ERROR] $*" >&2
}
# 记录到文件
exec >> "$log_file" 2>&1
- 临时文件处理
# 创建临时文件
temp_file=$(mktemp) || {
echo "Error: Cannot create temp file" >&2
exit 1
}
# 确保退出时清理
trap 'rm -f "$temp_file"' EXIT
# 使用临时文件
echo "data" > "$temp_file"
- 安全考虑
# 避免使用 eval
# 不好的做法
eval "echo \$$var_name"
# 好的做法
echo "${!var_name}"
# 路径安全
cd "$(dirname "$0")" || exit 1 # 切换到脚本所在目录
# 避免命令注入
# 不好的做法
grep "$user_input" file.txt # 如果 user_input 包含特殊字符
# 好的做法
grep -- "$user_input" file.txt
# 使用 printf 代替 echo 处理特殊字符
printf '%s\n' "$variable"
- 代码风格
# 统一的缩进(通常用 2 或 4 个空格)
if condition; then
command1
command2
fi
# 长的管道链换行
command1 \
| command2 \
| command3 \
> output.txt
# 复杂的条件分行写
if [ "$condition1" = "true" ] \
&& [ "$condition2" = "true" ] \
&& [ "$condition3" = "true" ]; then
do_something
fi
调试支持
# 调试开关
DEBUG=${DEBUG:-false}
if [ "$DEBUG" = true ]; then
set -x # 显示执行的命令
PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
fi
# 检查依赖
check_dependencies() {
local deps=("curl" "jq" "awk")
for dep in "${deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then
echo "Error: $dep is not installed" >&2
exit 1
fi
done
}
退出码规范
# 使用有意义的退出码
EXIT_SUCCESS=0
EXIT_INVALID_ARGS=1
EXIT_FILE_NOT_FOUND=2
EXIT_PERMISSION_DENIED=3
# 返回错误码
if [ ! -f "$file" ]; then
echo "File not found: $file" >&2
exit $EXIT_FILE_NOT_FOUND
fi
完整示例模板
#!/bin/bash
set -euo pipefail
# 常量定义
readonly SCRIPT_NAME=$(basename "$0")
readonly VERSION="1.0.0"
readonly CONFIG_FILE="/etc/myapp/config.conf"
# 颜色定义(可选)
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly NC='\033[0m' # No Color
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
error() {
echo -e "${RED}[ERROR] $*${NC}" >&2
}
success() {
echo -e "${GREEN}[SUCCESS] $*${NC}"
}
# 使用说明
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] <input-file>
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-o, --output Output file (default: stdout)
--version Show version information
Example:
$SCRIPT_NAME -o output.txt input.txt
EOF
}
# 清理函数
cleanup() {
log "Cleaning up..."
rm -f "$temp_file"
}
# 主函数
main() {
local input_file=""
local output_file=""
local verbose=false
# 参数解析
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--verbose)
verbose=true
shift
;;
-o|--output)
output_file="$2"
shift 2
;;
--version)
echo "$SCRIPT_NAME version $VERSION"
exit 0
;;
-*)
error "Unknown option: $1"
usage
exit 1
;;
*)
input_file="$1"
shift
;;
esac
done
# 验证输入
if [ -z "$input_file" ]; then
error "Input file is required"
usage
exit 1
fi
if [ ! -f "$input_file" ]; then
error "File not found: $input_file"
exit 1
fi
# 设置信号处理
trap cleanup EXIT
# 创建临时文件
temp_file=$(mktemp)
# 主要逻辑
log "Processing $input_file..."
if [ "$verbose" = true ]; then
set -x
fi
# 你的业务逻辑在这里
# ...
success "Done!"
}
# 执行主函数(只有直接运行时)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi