Shell脚本最佳实践Skill shell-best-practices

本技能提供编写Shell脚本的现代最佳实践指南,涵盖可移植脚本、Bash模式、错误处理和安全编码,适用于自动化任务、系统管理和DevOps场景。关键词:Shell脚本、Bash编程、错误处理、安全编码、可移植性、自动化、脚本优化、DevOps工具。

DevOps 0 次安装 0 次浏览 更新于 3/25/2026

名称: shell-best-practices 用户可调用: false 描述: 在编写遵循现代最佳实践的Shell脚本时使用。涵盖可移植脚本编写、Bash模式、错误处理和安全编码。 允许工具:

  • 读取
  • 写入
  • 编辑
  • Bash
  • Grep
  • Glob

Shell脚本最佳实践

遵循现代最佳实践编写健壮、可维护和安全的Shell脚本的全面指南。

脚本基础

Shebang选择

根据需求选择合适的shebang:

# 可移植bash(推荐)
#!/usr/bin/env bash

# 直接bash路径(更快,可移植性较差)
#!/bin/bash

# POSIX兼容shell(最可移植)
#!/bin/sh

# 特定shell版本
#!/usr/bin/env bash
# 需要Bash 4.0+

严格模式

始终启用严格的错误处理:

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

# 这些的作用:
# -e: 命令失败时立即退出
# -u: 将未设置变量视为错误
# -o pipefail: 如果任何命令失败,管道失败

调试时添加:

set -x  # 执行时打印命令

脚本头部模板

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

# 脚本: script-name.sh
# 描述: 本脚本功能的简要描述
# 用法: ./script-name.sh [选项] <参数>

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"

变量处理

始终引用变量

防止单词分割和通配符扩展:

# 好
echo "$variable"
cp "$source" "$destination"
if [ -f "$file" ]; then

# 坏 - 空格或特殊字符可能破坏
echo $variable
cp $source $destination
if [ -f $file ]; then

使用有意义的名称

# 好
readonly config_file="/etc/app/config.yml"
local user_input="$1"
declare -a log_files=()

# 坏
readonly f="/etc/app/config.yml"
local x="$1"
declare -a arr=()

默认值

# 如果未设置则使用默认值
name="${NAME:-default_value}"

# 如果未设置或为空则使用默认值
name="${NAME:-}"

# 如果未设置则分配默认值
: "${NAME:=default_value}"

# 如果未设置则报错(带消息)
: "${REQUIRED_VAR:?错误: REQUIRED_VAR 必须设置}"

只读和局部变量

# 常量
readonly MAX_RETRIES=3
readonly CONFIG_DIR="/etc/myapp"

# 函数局部变量
my_function() {
    local input="$1"
    local result=""
    # ...
}

错误处理

退出码

使用有意义的退出码:

# 标准码
readonly EXIT_SUCCESS=0
readonly EXIT_FAILURE=1
readonly EXIT_INVALID_ARGS=2
readonly EXIT_NOT_FOUND=3

# 带码退出
exit "$EXIT_FAILURE"

陷阱用于清理

cleanup() {
    local exit_code=$?
    # 清理临时文件
    rm -f "${temp_file:-}"
    # 如果需要恢复状态
    exit "$exit_code"
}

trap cleanup EXIT

# 脚本继续...
temp_file=$(mktemp)

错误消息

error() {
    echo "错误: $*" >&2
}

warn() {
    echo "警告: $*" >&2
}

die() {
    error "$@"
    exit 1
}

# 用法
[[ -f "$config_file" ]] || die "未找到配置文件: $config_file"

验证输入

validate_args() {
    if [[ $# -lt 1 ]]; then
        die "用法: $SCRIPT_NAME <输入文件>"
    fi

    local input_file="$1"
    [[ -f "$input_file" ]] || die "未找到文件: $input_file"
    [[ -r "$input_file" ]] || die "文件不可读: $input_file"
}

函数

函数定义

# 文档化函数
# 处理日志文件并提取错误
# 参数:
#   $1 - 日志文件路径
#   $2 - 输出目录(可选,默认: ./output)
# 返回:
#   0 成功,1 失败
process_log() {
    local log_file="$1"
    local output_dir="${2:-./output}"

    [[ -f "$log_file" ]] || return 1

    grep -i "error" "$log_file" > "$output_dir/errors.log"
}

返回值

# 返回状态
is_valid() {
    [[ -n "$1" && "$1" =~ ^[0-9]+$ ]]
}

if is_valid "$input"; then
    echo "有效"
fi

# 捕获输出
get_config_value() {
    local key="$1"
    grep "^${key}=" "$config_file" | cut -d= -f2
}

value=$(get_config_value "database_host")

条件语句

使用 [[ ]] 进行测试

# 好 - [[ ]] 更强大且更安全
if [[ -f "$file" ]]; then
if [[ "$string" == "value" ]]; then
if [[ "$string" =~ ^[0-9]+$ ]]; then

# 避免 - [ ] 有局限性
if [ -f "$file" ]; then
if [ "$string" = "value" ]; then

数值比较

# 使用 (( )) 进行算术
if (( count > 10 )); then
if (( a == b )); then
if (( x >= 0 && x <= 100 )); then

# 或在 [[ ]] 中使用 -eq/-lt/-gt
if [[ "$count" -gt 10 ]]; then

字符串比较

# 相等
if [[ "$str" == "value" ]]; then

# 模式匹配
if [[ "$str" == *.txt ]]; then

# 正则表达式匹配
if [[ "$str" =~ ^[a-z]+$ ]]; then

# 空/非空
if [[ -z "$str" ]]; then  # 空
if [[ -n "$str" ]]; then  # 非空

循环

遍历文件

# 好 - 处理文件名中的空格
for file in *.txt; do
    [[ -e "$file" ]] || continue  # 如果没有匹配则跳过
    process "$file"
done

# 使用find进行递归
while IFS= read -r -d '' file; do
    process "$file"
done < <(find . -name "*.txt" -print0)

# 坏 - 空格会破坏
for file in $(ls *.txt); do  # 不要这样做

从文件读取行

# 正确 - 保留空白
while IFS= read -r line; do
    echo "$line"
done < "$filename"

# 使用进程替换
while IFS= read -r line; do
    echo "$line"
done < <(some_command)

带索引迭代

files=("one.txt" "two.txt" "three.txt")

for i in "${!files[@]}"; do
    echo "索引 $i: ${files[i]}"
done

数组

声明和用法

# 索引数组
declare -a files=()
files+=("file1.txt")
files+=("file2.txt")

# 访问所有元素
for f in "${files[@]}"; do
    echo "$f"
done

# 数组长度
echo "${#files[@]}"

# 关联数组 (Bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"

echo "${config[host]}"

数组最佳实践

# 引用扩展
"${array[@]}"   # 所有元素,单词分割
"${array[*]}"   # 所有元素,单个字符串

# 检查是否为空
if [[ ${#array[@]} -eq 0 ]]; then
    echo "空数组"
fi

# 检查键(关联)
if [[ -v config[key] ]]; then
    echo "键存在"
fi

命令执行

检查命令存在

# 首选方法
if command -v docker &>/dev/null; then
    echo "Docker已安装"
fi

# 在条件语句中
require_command() {
    command -v "$1" &>/dev/null || die "未找到必需命令: $1"
}

require_command git
require_command docker

捕获输出和状态

# 捕获输出
output=$(some_command)

# 捕获输出和状态
if output=$(some_command 2>&1); then
    echo "成功: $output"
else
    echo "失败: $output" >&2
fi

# 检查状态而不捕获输出
if some_command &>/dev/null; then
    echo "命令成功"
fi

安全命令替换

# 使用 $() 而不是反引号
result=$(command)      # 好
result=`command`       # 避免

# 嵌套替换
result=$(echo $(date)) # 使用 $() 有效

可移植性

POSIX 与 Bash

功能 POSIX Bash
测试语法 [ ] [[ ]]
数组
$()
${var//pat/rep}
[[ =~ ]] 正则
(( )) 算术

可移植替代

# 代替 [[ ]],使用带引号的 [ ]
if [ -f "$file" ]; then
if [ "$str" = "value" ]; then

# 代替 (( )),使用带 -eq 的 [ ]
if [ "$count" -gt 10 ]; then

# 代替 ${var//pat/rep}
echo "$var" | sed 's/pat/rep/g'

# 代替数组,使用空格分隔的字符串
files="one.txt two.txt three.txt"
for f in $files; do
    echo "$f"
done

安全

避免 Eval

# 坏 - 代码注入风险
eval "$user_input"

# 更好 - 使用数组构建命令
cmd=("grep" "-r" "$pattern" "$directory")
"${cmd[@]}"

净化输入

# 验证预期格式
if [[ ! "$input" =~ ^[a-zA-Z0-9_-]+$ ]]; then
    die "无效输入格式"
fi

# 转义用于命令
escaped=$(printf '%q' "$input")

临时文件

# 安全临时文件创建
temp_file=$(mktemp) || die "创建临时文件失败"
trap 'rm -f "$temp_file"' EXIT

# 安全临时目录
temp_dir=$(mktemp -d) || die "创建临时目录失败"
trap 'rm -rf "$temp_dir"' EXIT

日志记录

基本日志记录

readonly LOG_FILE="/var/log/myapp.log"

log() {
    local level="$1"
    shift
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}

log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@" >&2; }
log_error() { log "ERROR" "$@" >&2; }

# 用法
log_info "开始处理"
log_error "连接失败"

详细模式

VERBOSE="${VERBOSE:-false}"

debug() {
    if [[ "$VERBOSE" == "true" ]]; then
        echo "DEBUG: $*" >&2
    fi
}

# 启用方式: VERBOSE=true ./script.sh

完整脚本模板

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

# =============================================================================
# 脚本: example.sh
# 描述: 演示Shell最佳实践的模板
# 用法: ./example.sh [选项] <输入文件>
# =============================================================================

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"

# 退出码
readonly EXIT_SUCCESS=0
readonly EXIT_FAILURE=1
readonly EXIT_INVALID_ARGS=2

# 日志函数
log_info() { echo "[INFO] $*"; }
log_error() { echo "[ERROR] $*" >&2; }

# 错误处理
die() {
    log_error "$@"
    exit "$EXIT_FAILURE"
}

cleanup() {
    local exit_code=$?
    rm -f "${temp_file:-}"
    exit "$exit_code"
}
trap cleanup EXIT

# 参数解析
usage() {
    cat <<EOF
用法: $SCRIPT_NAME [选项] <输入文件>

选项:
    -h, --help      显示此帮助信息
    -v, --verbose   启用详细输出
    -o, --output    输出目录 (默认: ./output)

示例:
    $SCRIPT_NAME input.txt
    $SCRIPT_NAME -v -o /tmp/output input.txt
EOF
}

parse_args() {
    local OPTIND opt
    while getopts ":hvo:-:" opt; do
        case "$opt" in
            h) usage; exit "$EXIT_SUCCESS" ;;
            v) VERBOSE=true ;;
            o) OUTPUT_DIR="$OPTARG" ;;
            -) case "$OPTARG" in
                   help) usage; exit "$EXIT_SUCCESS" ;;
                   verbose) VERBOSE=true ;;
                   output=*) OUTPUT_DIR="${OPTARG#*=}" ;;
                   *) die "未知选项: --$OPTARG" ;;
               esac ;;
            :) die "选项 -$OPTARG 需要参数" ;;
            \?) die "未知选项: -$OPTARG" ;;
        esac
    done
    shift $((OPTIND - 1))

    if [[ $# -lt 1 ]]; then
        usage
        exit "$EXIT_INVALID_ARGS"
    fi

    INPUT_FILE="$1"
}

# 验证输入
validate() {
    [[ -f "$INPUT_FILE" ]] || die "未找到文件: $INPUT_FILE"
    [[ -r "$INPUT_FILE" ]] || die "文件不可读: $INPUT_FILE"
    mkdir -p "$OUTPUT_DIR" || die "无法创建输出目录"
}

# 主逻辑
main() {
    # 默认值
    VERBOSE="${VERBOSE:-false}"
    OUTPUT_DIR="${OUTPUT_DIR:-./output}"

    parse_args "$@"
    validate

    log_info "处理 $INPUT_FILE"
    # ... 主逻辑在此 ...
    log_info "完成"
}

main "$@"

何时使用此技能

  • 从头编写新Shell脚本
  • 审查Shell脚本问题
  • 重构遗留Shell代码
  • 调试脚本失败
  • 提高脚本安全性
  • 使脚本更可移植
  • 设置适当的错误处理