强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

20 - 最佳实践

20 - 最佳实践

20.1 脚本模板

每个新脚本都应包含以下基础结构:

#!/usr/bin/env bash
# ============================================================
# 脚本名称: script.sh
# 描述:     脚本用途说明
# 用法:     ./script.sh [选项] <参数>
# 作者:     作者名
# 日期:     2026-05-10
# ============================================================

set -euo pipefail
IFS=$'\n\t'

readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly SCRIPT_VERSION="1.0.0"

# ---- 日志函数 ----
log()   { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2; }
warn()  { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️  $*" >&2; }
error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ❌ $*" >&2; }
die()   { error "$@"; exit 1; }

# ---- 清理函数 ----
cleanup() {
    local exit_code=$?
    # 清理临时资源
    [[ -n "${TMP_DIR:-}" && -d "$TMP_DIR" ]] && rm -rf "$TMP_DIR"
    exit "$exit_code"
}
trap cleanup EXIT INT TERM

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

选项:
  -h, --help     显示帮助
  -v, --version  显示版本
  -q, --quiet    静默模式
EOF
}

QUIET=false

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help)    usage; exit 0 ;;
            -v|--version) echo "$SCRIPT_VERSION"; exit 0 ;;
            -q|--quiet)   QUIET=true; shift ;;
            --)           shift; break ;;
            -*)           die "未知选项: $1" ;;
            *)            break ;;
        esac
    done
    ARGS=("$@")
}

# ---- 主逻辑 ----
main() {
    parse_args "$@"
    
    TMP_DIR=$(mktemp -d)
    
    # 业务逻辑
    [[ "$QUIET" != true ]] && log "开始执行..."
    # ...
    [[ "$QUIET" != true ]] && log "执行完成"
}

main "$@"

20.2 代码规范

命名规范

元素风格示例
常量全大写+下划线MAX_RETRY, CONFIG_FILE
全局变量小写+下划线log_file, target_dir
局部变量小写+下划线local count=0
函数小写+下划线validate_input()
私有函数前缀 __internal_helper()
模块函数模块::函数log::info(), file::exists()

格式规范

# ✅ 良好的格式
process_file() {
    local input_file="$1"
    local output_file="$2"
    local verbose="${3:-false}"
    
    if [[ ! -f "$input_file" ]]; then
        error "文件不存在: $input_file"
        return 1
    fi
    
    if [[ "$verbose" == true ]]; then
        log "处理文件: $input_file -> $output_file"
    fi
    
    # 业务逻辑
    sed 's/old/new/g' "$input_file" > "$output_file"
}

# ❌ 不良的格式
process_file(){
local input_file=$1
local output_file=$2
if [[ ! -f $input_file ]];then
error "文件不存在: $input_file"
return 1
fi
sed 's/old/new/g' $input_file > $output_file
}

代码组织

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

# 1. 常量定义
readonly VERSION="1.0.0"

# 2. 全局变量
VERBOSE=false

# 3. 工具函数
log() { echo "[$(date '+%H:%M:%S')] $*" >&2; }

# 4. 业务函数
validate() { :; }
process() { :; }

# 5. 参数解析
parse_args() { :; }

# 6. 主入口
main() { :; }
main "$@"

20.3 ShellCheck 规则精要

必须修复的规则

# SC2086: 变量未加引号 —— 高危!
# ❌ 错误
rm $file
files=$(find . -name "*.txt")
cd $dir

# ✅ 正确
rm "$file"
files=$(find . -name "*.txt")
cd "$dir"

# SC2046: 命令替换未加引号
# ❌ 错误
rm $(find /tmp -name "*.log")

# ✅ 正确
find /tmp -name "*.log" -exec rm {} +

# SC2155: declare 和赋值应分开
# ❌ 错误
local var=$(some_command)

# ✅ 正确
local var
var=$(some_command)

# SC2164: cd 缺少错误处理
# ❌ 错误
cd /some/dir

# ✅ 正确
cd /some/dir || exit 1

# SC2034: 变量定义但未使用
# 可能是拼写错误或应删除
unused_var="hello"  # ShellCheck 会警告

# SC2162: read 未使用 -r
# ❌ 错误
read line

# ✅ 正确
read -r line

ShellCheck 配置文件

# .shellcheckrc 文件
# 全局启用
enable=avoid-nullary-conditions

# 全局禁用
disable=SC2034  # unused variables (common in sourced files)

# 指定 Shell 方言
shell=bash

# 外部源文件路径
source-path=lib/

20.4 性能优化

避免不必要的子 Shell

# ❌ 慢:每次循环都创建子 Shell
while read -r line; do
    echo "$line"
done < <(command)

# ❌ 更慢:管道创建两个子 Shell
cat file.txt | grep "pattern"

# ✅ 快:直接重定向
while IFS= read -r line; do
    echo "$line"
done < file.txt

# ✅ 更快:直接 grep
grep "pattern" file.txt

内建命令优于外部命令

操作慢(外部命令)快(内建命令)
字符串长度echo "$str" | wc -c${#str}
大小写转换echo "$str" | tr 'a-z' 'A-Z'${str^^}
子串提取echo "$str" | cut -c1-5${str:0:5}
路径提取echo "$path" | xargs dirname${path%/*}
去除前缀echo "$str" | sed 's/^prefix//'${str#prefix}
算术运算expr $a + $b$((a + b))
默认值if [ -z "$var" ]; then var="default"; fi${var:-default}

减少循环中的外部命令

# ❌ 慢:循环中调用外部命令
for file in *.txt; do
    count=$(wc -l < "$file")
    echo "$file: $count lines"
done

# ✅ 快:一次处理
while IFS= read -r file; do
    wc -l "$file"
done < <(find . -name "*.txt" -type f)

# ✅ 更快:使用 find -exec
find . -name "*.txt" -type f -exec wc -l {} +

# ✅ 并行处理
find . -name "*.txt" -type f | xargs -P "$(nproc)" wc -l

缓存重复计算

# ❌ 每次调用都重新计算
get_config() {
    grep "^$1=" /etc/myapp/config.ini | cut -d= -f2
}
host=$(get_config host)  # 读文件
port=$(get_config port)  # 又读文件

# ✅ 一次性加载到关联数组
declare -A CONFIG
load_config() {
    while IFS='=' read -r key value; do
        [[ -n "$key" && "$key" != \#* ]] && CONFIG["$key"]="$value"
    done < /etc/myapp/config.ini
}
load_config
host="${CONFIG[host]}"
port="${CONFIG[port]}"

20.5 安全最佳实践

输入验证

# 永远不要信任用户输入
validate_input() {
    local input="$1"
    
    # 检查是否为空
    [[ -z "$input" ]] && { echo "输入为空" >&2; return 1; }
    
    # 检查长度
    [[ ${#input} -gt 255 ]] && { echo "输入过长" >&2; return 1; }
    
    # 检查非法字符(根据场景调整)
    if [[ "$input" =~ [[:cntrl:]] ]]; then
        echo "包含非法控制字符" >&2
        return 1
    fi
    
    # 白名单验证(推荐)
    if [[ "$input" =~ ^[a-zA-Z0-9._-]+$ ]]; then
        return 0
    else
        echo "输入包含不允许的字符" >&2
        return 1
    fi
}

安全的临时文件

# ❌ 不安全:可预测的文件名
tmpfile="/tmp/myapp_$$.tmp"

# ✅ 安全:使用 mktemp
tmpfile=$(mktemp /tmp/myapp.XXXXXX)
tmpdir=$(mktemp -d /tmp/myapp.XXXXXX)

# 设置安全的临时目录
export TMPDIR=/tmp
umask 077  # 只有 owner 可读写

避免命令注入

# ❌ 危险:直接拼接用户输入
filename="$1"
eval "cat $filename"     # 如果 filename="; rm -rf /"

# ❌ 危险:未加引号
rm $filename             # 如果 filename="a b"会删除两个文件

# ✅ 安全:加引号
cat "$filename"
rm "$filename"

# ✅ 安全:使用数组传递参数
args=("-name" "*.txt" "-type" "f")
find . "${args[@]}"

# ✅ 安全:避免 eval
# ❌ eval "$command"
# ✅ 直接调用函数

敏感信息处理

# ❌ 硬编码密码
DB_PASSWORD="secret123"

# ✅ 从环境变量读取
DB_PASSWORD="${DB_PASSWORD:?数据库密码未设置}"

# ✅ 从文件读取(权限 600)
DB_PASSWORD=$(cat /etc/myapp/db_password)

# ✅ 使用 secret 管理工具
DB_PASSWORD=$(vault kv get -field=password secret/myapp/db)

# 不要在日志中输出敏感信息
log() {
    local msg="$*"
    # 脱敏处理
    msg=$(echo "$msg" | sed -E 's/password=[^ ]*/password=****/g')
    echo "$msg" >&2
}

20.6 可移植性

POSIX 兼容性

# 如果需要兼容 sh/dash,避免以下 Bash 特性:

# ❌ [[ ]] —— 使用 [ ]
[ -f "$file" ] && echo "存在"

# ❌ ${var,,} —— 使用 tr
echo "$var" | tr '[:upper:]' '[:lower:]'

# ❌ 数组 —— 使用位置参数
set -- a b c
for item; do echo "$item"; done

# ❌ $(( )) 算术 —— 使用 expr 或 test
result=$(expr $a + $b)

# ❌ function 关键字 —— 使用 name() 语法
my_func() { echo "hello"; }

# ❌ <<< Here String —— 使用 echo | 
echo "hello" | read -r var

# ❌ process substitution <()
diff <(ls /tmp) <(ls /var)

跨平台检测

# 检测操作系统
detect_os() {
    case "$(uname -s)" in
        Linux*)     echo "linux" ;;
        Darwin*)    echo "macos" ;;
        CYGWIN*)    echo "cygwin" ;;
        MINGW*)     echo "mingw" ;;
        FreeBSD*)   echo "freebsd" ;;
        *)          echo "unknown" ;;
    esac
}

# 跨平台兼容的命令
case "$(detect_os)" in
    linux)
        SED_INPLACE=(sed -i)
        OPEN_URL=(xdg-open)
        ;;
    macos)
        SED_INPLACE=(sed -i '')
        OPEN_URL=(open)
        ;;
esac

"${SED_INPLACE[@]}" 's/old/new/g' file.txt

20.7 代码审查清单

类别检查项优先级
安全变量是否加引号🔴 必须
安全是否有命令注入风险🔴 必须
安全敏感信息是否硬编码🔴 必须
安全临时文件是否安全🟡 建议
健壮性是否设置 set -euo pipefail🔴 必须
健壮性是否有错误处理🔴 必须
健壮性是否有清理逻辑 (trap)🟡 建议
健壮性输入是否验证🟡 建议
可读性变量名是否有意义🟡 建议
可读性是否有注释说明🟡 建议
可读性函数是否单一职责🟡 建议
可维护性ShellCheck 是否通过🔴 必须
可维护性是否有测试🟡 建议
性能循环中是否有不必要的外部命令🟢 优化
性能是否缓存重复计算🟢 优化
可移植性是否使用了 Bash 特有特性🟡 视情况

20.8 快速参考卡

常用一行命令

# 查找并替换文件内容
find . -name "*.txt" -exec sed -i 's/old/new/g' {} +

# 统计代码行数
find . -name "*.sh" -exec cat {} + | wc -l

# 批量重命名
for f in *.JPG; do mv "$f" "${f%.JPG}.jpg"; done

# 查找大文件
find / -type f -size +100M -exec ls -lh {} \; 2>/dev/null

# 监控文件变化
inotifywait -m -r /path/to/watch

# 提取 URL
grep -oE 'https?://[^ ]+' file.txt

# 去重保留顺序
awk '!seen[$0]++' file.txt

# CSV 列求和
awk -F',' '{sum+=$3} END {print sum}' data.csv

# JSON 解析(无 jq 时)
grep -o '"key": *"[^"]*"' file.json | cut -d'"' -f4

# 并行压缩
find . -name "*.log" | xargs -P 4 gzip

20.9 扩展阅读


🎉 恭喜! 你已完成全部 20 章的 Bash 脚本编写教程。

记住:实践是最好的老师。将这些知识应用到你的日常工作中,从写一个小工具开始,逐步构建更复杂的脚本。