Shell 脚本编写最佳实践

Shell 脚本编写最佳实践

  1. 脚本头部规范
#!/bin/bash  # 明确的 shebang
# -*- coding: utf-8 -*-  # 编码声明
# 作者:Your Name
# 描述:脚本功能说明
# 日期:2024-01-01
# 版本:1.0
  1. 严格的错误处理
# 遇到错误立即退出
set -e
# 未定义变量时报错
set -u
# 管道命令中有错误也退出
set -o pipefail

# 或者在脚本开始统一设置
set -euo pipefail
  1. 变量使用规范
# 使用有意义的变量名
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"
  1. 函数化编程
# 每个功能独立成函数
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 "$@"
  1. 参数处理
# 使用 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))
  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
  1. 日志记录
# 日志级别
LOG_LEVEL=${LOG_LEVEL:-"INFO"}

debug() {
    [ "$LOG_LEVEL" = "DEBUG" ] && echo "[DEBUG] $*"
}

info() {
    echo "[INFO] $*"
}

error() {
    echo "[ERROR] $*" >&2
}

# 记录到文件
exec >> "$log_file" 2>&1
  1. 临时文件处理
# 创建临时文件
temp_file=$(mktemp) || {
    echo "Error: Cannot create temp file" >&2
    exit 1
}

# 确保退出时清理
trap 'rm -f "$temp_file"' EXIT

# 使用临时文件
echo "data" > "$temp_file"
  1. 安全考虑
# 避免使用 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"
  1. 代码风格
# 统一的缩进(通常用 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容