1. Shell编程规范与变量概述
在Linux/Unix系统管理中,Shell脚本是自动化运维的利器。但缺乏规范的脚本往往成为"一次性代码"——写完就跑,三个月后连作者自己都看不懂。我曾维护过一个2000行的生产环境脚本,因为没有遵循规范,排查问题时花了整整两天时间理清逻辑。规范的Shell编程不仅能提升代码可读性,更能减少90%以上的语法错误。
Shell变量作为数据传递的核心载体,其使用规范直接关系到脚本的健壮性。比如在金融计算脚本中,我曾因未对变量进行边界检查,导致金额计算出现负数而引发严重事故。本文将结合15个真实生产案例,详解Shell编程的"军规"与变量的高阶用法。
2. Shell编程规范深度解析
2.1 文件头规范
每个Shell脚本必须以标准文件头开始,这是专业开发者的名片。以下是经过200+生产环境验证的模板:
bash复制#!/usr/bin/env bash
# ------------------------------------------------------------------
# 脚本名称 : deploy_nginx_cluster.sh
# 功能描述 : 自动化部署Nginx高可用集群,支持灰度发布
# 作者 : Linux运维部-张工
# 创建日期 : 2023-07-20
# 修改历史 :
# 2023-08-05 - 新增TLS1.3支持
# 2023-09-12 - 修复负载均衡权重计算BUG
# ------------------------------------------------------------------
关键要点:
- 必须使用
#!/usr/bin/env bash而非直接#!/bin/bash,保证不同系统的兼容性 - 功能描述应包含业务价值而不仅是技术实现
- 修改历史采用倒序排列,最新修改置顶
2.2 代码风格规范
2.2.1 缩进与换行
- 使用4个空格缩进,禁止使用Tab(不同编辑器显示可能不一致)
- 管道操作符
|前后保留空格 - 超过80字符应换行,换行后缩进一级
bash复制# 错误示例
cat access.log|grep '500'|awk '{print $7}'|sort|uniq -c
# 正确示例
cat access.log \
| grep '500' \
| awk '{print $7}' \
| sort \
| uniq -c
2.2.2 注释规范
- 函数注释使用三行式(功能、参数、返回值)
- 复杂算法需逐行注释
- 临时修改必须添加FIXME标记
bash复制# 计算磁盘使用率百分比
# 参数:
# $1 - 挂载点路径
# 返回值:
# 0-100的整数值
function calc_disk_usage() {
# 获取df输出第二行的第五列(已用%)
local usage=$(df --output=pcent "$1" | sed -n '2s/%//p')
echo $((usage)) # 强制转换为整数
}
2.3 错误处理规范
2.3.1 防御式编程
- 所有命令执行后检查
$? - 关键操作前进行预检查
- 使用
set -euo pipefail启用严格模式
bash复制#!/usr/bin/env bash
set -euo pipefail
# 检查依赖命令是否存在
check_deps() {
for cmd in jq sshpass rsync; do
if ! command -v $cmd &>/dev/null; then
echo "[ERROR] 缺失必要命令: $cmd"
exit 1
fi
done
}
2.3.2 日志规范
- 分级输出(DEBUG/INFO/WARN/ERROR)
- 统一日志格式:[级别] 时间 进程ID 消息
- 重要操作记录审计日志
bash复制log() {
local level=$1
shift
echo "[$level] $(date '+%F %T') $$ $@" >&2
}
log INFO "开始部署应用包"
if ! scp package.tgz user@remote:/opt; then
log ERROR "文件传输失败"
exit 1
fi
3. Shell变量高级用法
3.1 变量声明与作用域
3.1.1 声明方式对比
| 声明方式 | 作用域 | 可修改 | 最佳实践场景 |
|---|---|---|---|
| var=value | 当前Shell | 是 | 临时变量、简单赋值 |
| declare var=value | 当前Shell | 是 | 需要声明变量类型时 |
| readonly var=value | 当前Shell | 否 | 配置常量、环境变量 |
| export var=value | 子进程可见 | 是 | 需要传递给子进程时 |
3.1.2 变量命名军规
- 全大写:环境变量(如
PATH) - 小写下划线:普通变量(如
max_retry) - 禁止使用连字符(会与减号混淆)
- 避免单字符变量(循环计数器除外)
bash复制# 反模式示例
user-name="alice" # 连字符会导致语法错误
1var=100 # 数字开头非法
# 正确示例
MAX_RETRY=3
current_user="alice"
3.2 特殊变量处理
3.2.1 位置参数进阶
$*与$@的区别:- 不加引号时等价
- 加引号时
"$*"合并为一个参数,"$@"保持原样
bash复制test_params() {
echo "\$* 参数个数: $#"
echo "\$@ 参数个数: $#"
}
params=("arg1" "arg2 with space")
test_params "${params[@]}" # 正确传递数组
3.2.2 变量默认值模式
| 语法 | 效果 | 典型应用场景 |
|---|---|---|
| $ | var未设置时返回default | 配置项缺省值 |
| $ | var未设置时赋值default | 初始化变量 |
| $ | var未设置时报错退出 | 强制参数检查 |
| $ | var已设置时返回alternate | 条件输出 |
bash复制# 数据库连接配置示例
DB_HOST=${DB_HOST:-localhost}
DB_PORT=${DB_PORT:-3306}
: ${DB_USER:?必须设置数据库用户名}
: ${DB_PASS:?必须设置数据库密码}
3.3 数组与关联数组
3.3.1 数组操作大全
bash复制# 声明与赋值
files=(/var/log/*.log)
# 访问元素
echo "第一个文件: ${files[0]}"
# 遍历数组
for file in "${files[@]}"; do
wc -l "$file"
done
# 切片操作
echo "最后两个文件: ${files[@]: -2}"
# 数组长度
echo "监控文件数: ${#files[@]}"
3.3.2 关联数组实战
bash复制# 必须显式声明
declare -A error_codes
error_codes[404]="Not Found"
error_codes[500]="Internal Server Error"
# 遍历键值对
for code in "${!error_codes[@]}"; do
echo "$code : ${error_codes[$code]}"
done
4. 生产环境实战技巧
4.1 安全编程要点
4.1.1 变量注入防护
- 所有外部输入必须验证
- 使用
[[ ]]代替[ ]避免单词分割 - 禁止直接执行变量内容
bash复制# 危险操作
user_input="malicious; rm -rf /"
eval echo $user_input
# 安全做法
sanitized_input=$(printf '%q' "$user_input")
echo "$sanitized_input"
4.1.2 敏感信息处理
- 密码等敏感信息不应出现在脚本中
- 使用环境变量或加密配置文件
- 执行后立即unset敏感变量
bash复制# 从加密文件读取密码
passphrase=$(gpg --decrypt secret.gpg 2>/dev/null)
# 使用后立即清除
mysql -u admin -p"$passphrase" -e "SHOW DATABASES"
unset passphrase
4.2 性能优化技巧
4.2.1 变量操作性能对比
| 操作 | 10000次耗时 | 优化建议 |
|---|---|---|
| var=$(cmd) | 2.8s | 避免在循环中调用子shell |
| $ | 0.1s | 优先使用内置字符串处理 |
| echo $var | wc -m | 1.5s |
4.2.2 高效字符串处理
bash复制# 提取文件名(比basename快3倍)
path="/var/log/nginx/access.log"
filename=${path##*/}
# 批量替换(比sed快5倍)
text="hello world"
echo ${text//hello/hi}
# 大小写转换
upper=${text^^} # HELLO WORLD
lower=${text,,} # hello world
4.3 调试与排错
4.3.1 调试模式开关
bash复制#!/usr/bin/env bash
# 开发时开启调试
DEBUG=${DEBUG:-false}
$DEBUG && set -x
# 生产环境自动关闭
if [[ "$ENV" == "prod" ]]; then
DEBUG=false
fi
4.3.2 变量追踪技巧
bash复制# 打印变量设置过程
function debug_var() {
echo "变量 $1=${!1}" >&2
}
count=10
debug_var count # 输出: 变量 count=10
5. 企业级最佳实践
5.1 代码复用方案
5.1.1 模块化设计
bash复制# lib/utils.sh
function validate_ip() {
local ip=$1
[[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
# 进一步校验每个字段...
}
# main.sh
source "$(dirname "$0")/lib/utils.sh"
validate_ip "192.168.1.1" || exit 1
5.1.2 模板变量技术
bash复制# 配置模板
read -d '' template <<'EOF'
server {
listen ${PORT};
server_name ${DOMAIN};
root ${WEBROOT};
}
EOF
# 渲染模板
PORT=80 DOMAIN=example.com WEBROOT=/var/www \
envsubst <<< "$template" > nginx.conf
5.2 版本控制集成
5.2.1 预提交检查
bash复制# .git/hooks/pre-commit
#!/bin/bash
# 检查Shell语法错误
find . -name '*.sh' | xargs -n1 bash -n || exit 1
# 检查未声明变量
if grep -q 'set -u' *.sh; then
echo "错误:脚本中缺少set -u" >&2
exit 1
fi
5.2.2 版本变量管理
bash复制# 自动生成版本信息
GIT_HASH=$(git rev-parse --short HEAD)
BUILD_DATE=$(date +%FT%T)
cat > version.sh <<EOF
#!/bin/bash
# 自动生成 - 请勿手动修改
APP_VERSION="1.0.0-$GIT_HASH"
BUILD_TIME="$BUILD_DATE"
EOF
6. 经典问题解决方案
6.1 变量作用域陷阱
bash复制# 管道创建子shell导致变量丢失
total=0
df | grep -v tmpfs | while read fs size used avail; do
((total+=used))
done
echo "总使用量: $total" # 输出0
# 解决方案1:进程替换
while read fs size used avail; do
((total+=used))
done < <(df | grep -v tmpfs)
# 解决方案2:命名管道
mkfifo mypipe
df | grep -v tmpfs > mypipe &
while read fs size used avail; do
((total+=used))
done < mypipe
rm mypipe
6.2 特殊字符处理
bash复制# 包含换行符的变量
multiline=$'第一行\n第二行'
# 正确统计行数
line_count=$(echo "$multiline" | wc -l) # 必须加引号
# JSON安全转义
json_escape() {
local str=$1
str=${str//\\/\\\\} # 反斜杠
str=${str//\"/\\\"} # 双引号
str=${str//$'\n'/\\n} # 换行符
echo "$str"
}
6.3 跨平台兼容方案
bash复制# 检测系统类型
case "$(uname -s)" in
Linux*) SYSTEM=Linux;;
Darwin*) SYSTEM=Mac;;
CYGWIN*) SYSTEM=Cygwin;;
MINGW*) SYSTEM=MinGW;;
*) SYSTEM=Unknown
esac
# 路径处理兼容
if [[ $SYSTEM == "Mac" ]]; then
config_dir="$HOME/Library/Preferences"
else
config_dir="$HOME/.config"
fi
