本章导语:变量是 Shell 脚本编程的核心概念,掌握变量的定义、使用和传递技巧是编写高效脚本的基础。本章将深入讲解 Shell 变量的三种定义方式、作用域管理、参数传递机制,并通过大量实战案例帮助你避开常见的编程陷阱,让你能够灵活运用变量解决实际问题。
学习目标
完成本章学习后,你将能够:
- 🎯 掌握 Shell 变量的三种定义方式及其适用场景
- 📝 理解变量的作用域和生命周期
- 🔤 熟练运用字符串操作和数组处理
- 📥 处理用户输入和位置参数
- 🛡️ 避免变量使用中的常见陷阱
- 🔧 运用变量处理高级技巧和最佳实践
- 📋 实现数据验证和安全处理
核心概念
- 变量类型:字符串变量、数值变量、数组变量、环境变量
- 作用域:局部变量、全局变量、环境变量
- 参数传递:位置参数、特殊参数、参数验证
- 变量操作:赋值、引用、替换、删除
应用场景
- 配置管理:读取配置文件、环境变量设置
- 数据处理:解析命令行参数、处理用户输入
- 系统集成:与其他程序交换数据、状态传递
- 用户交互:菜单选择、输入验证、反馈显示
2.1 变量的三种定义方式
方式一:直接赋值(最常用)
bash
#!/bin/bash
# 基本赋值
name="张三"
age=25
city="北京"
# 带空格的字符串必须用引号
description="软件工程师,精通多种编程语言"
# 数值运算
result=$((10 + 5)) # 结果为 15
price=99.99 # 注意:Shell 原生不支持浮点数
echo "姓名: $name"
echo "年龄: $age"
echo "城市: $city"
echo "描述: $description"
echo "计算结果: $result"
方式二:命令替换
bash
#!/bin/bash
# 使用 $() 进行命令替换(推荐)
current_date=$(date +%Y-%m-%d)
current_time=$(date +%H:%M:%S)
current_user=$(whoami)
current_dir=$(pwd)
file_count=$(ls -1 | wc -l)
# 使用反引号进行命令替换(旧方式,不推荐)
old_style_date=`date +%Y-%m-%d`
echo "当前日期: $current_date"
echo "当前时间: $current_time"
echo "当前用户: $current_user"
echo "当前目录: $current_dir"
echo "文件数量: $file_count"
echo "旧式日期: $old_style_date"
# 复杂命令替换示例
disk_usage=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
memory_usage=$(free | awk 'NR==2{printf "%.1f", $3*100/$2}')
echo "磁盘使用率: ${disk_usage}%"
echo "内存使用率: ${memory_usage}%"
方式三:read 命令读取用户输入
bash
#!/bin/bash
# 基本输入读取
echo -n "请输入您的姓名: "
read name
# 带提示信息的输入读取
read -p "请输入您的年龄: " age
# 密码输入(不显示内容)
read -s -p "请输入您的密码: " password
echo # 换行
# 多变量输入
read -p "请输入城市和省份(用空格分隔): " city province
# 限制输入时间(5秒超时)
read -t 5 -p "请在5秒内输入您的工作: " job
# 限制输入长度
read -n 3 -p "请输入3位数字: " code
echo
# 数组输入
read -p "请输入您的技能(用空格分隔): " -a skills
echo "=== 信息汇总 ==="
echo "姓名: $name"
echo "年龄: $age"
echo "密码长度: ${#password}"
echo "城市: $city"
echo "省份: $province"
echo "工作: ${job:-未输入}"
echo "代码: $code"
echo "技能: ${skills[*]}"
变量定义的高级技巧
bash
#!/bin/bash
# 使用 declare 定义变量类型
declare -i integer_var=42 # 整数变量
declare -a array_var=("a" "b" "c") # 数组变量
declare -A assoc_var=([name]="张三" [age]=25) # 关联数组
declare -r readonly_var="不可修改" # 只读变量
# 环境变量导出
export GLOBAL_VAR="这是一个全局变量"
declare -x EXPORT_VAR="这也是全局变量"
# 条件赋值
unset_var="默认值"
: ${unset_var:="如果未设置则使用此值"}
# 使用 eval 动态变量名
for i in {1..3}; do
eval "var_$i=\"变量$i的值\""
done
echo "整数变量: $integer_var"
echo "数组变量: ${array_var[@]}"
echo "关联数组: name=${assoc_var[name]}, age=${assoc_var[age]}"
echo "只读变量: $readonly_var"
echo "动态变量1: $var_1"
echo "动态变量2: $var_2"
echo "动态变量3: $var_3"
2.2 变量的引用与作用域
变量引用方式
bash
#!/bin/bash
# 基本引用
name="张三"
echo "姓名: $name"
# 大括号引用(推荐,避免歧义)
echo "姓名: ${name}"
echo "姓名和年龄: ${name}_25" # 避免变量名歧义
# 变量长度
echo "姓名长度: ${#name}"
# 字符串切片
full_name="张三丰"
echo "姓: ${full_name:0:1}"
echo "名: ${full_name:1}"
echo "后两个字符: ${full_name: -2}"
# 字符串替换
text="hello world hello shell"
echo "替换hello为hi: ${text//hello/hi}"
# 默认值处理
unset_var="已设置"
echo "unset_var的值: ${unset_var:-默认值}"
empty_var=""
echo "empty_var的值: ${empty_var:-默认值}"
echo "如果未设置则赋值: ${new_var:=新值}"
echo "new_var现在为: $new_var"
变量作用域
bash
#!/bin/bash
# 全局变量
global_var="全局变量"
function demo_function() {
# 局部变量
local local_var="局部变量"
# 修改全局变量
global_var="修改后的全局变量"
# 函数内部变量
func_var="函数内部变量"
echo "函数内部:"
echo " global_var = $global_var"
echo " local_var = $local_var"
echo " func_var = $func_var"
}
# 子函数
function sub_function() {
echo "子函数中:"
echo " global_var = $global_var"
# echo " local_var = $local_var" # 错误:无法访问局部变量
# echo " func_var = $func_var" # 错误:无法访问
}
# 调用函数
echo "调用函数前:"
echo " global_var = $global_var"
demo_function
sub_function
echo "函数调用后:"
echo " global_var = $global_var"
# echo " local_var = $local_var" # 错误:局部变量不可见
# echo " func_var = $func_var" # 错误:函数变量不可见
环境变量与 Shell 变量
bash
#!/bin/bash
# Shell 变量(仅当前 Shell 可见)
shell_var="Shell 变量"
# 环境变量(子进程可见)
export env_var="环境变量"
# 显示所有环境变量
echo "=== 部分环境变量 ==="
echo "USER: $USER"
echo "HOME: $HOME"
echo "PATH: $PATH"
echo "SHELL: $SHELL"
# 在子脚本中测试环境变量
cat > /tmp/test_env.sh << 'EOF'
#!/bin/bash
echo "子脚本中的变量:"
echo " shell_var: ${shell_var:-未定义}"
echo " env_var: ${env_var:-未定义}"
echo " PARENT: $PARENT"
EOF
chmod +x /tmp/test_env.sh
export PARENT="父进程"
echo "=== 调用子脚本 ==="
./tmp/test_env.sh
# 清理
rm -f /tmp/test_env.sh
2.3 位置参数与特殊变量
位置参数
bash
#!/bin/bash
# 位置参数演示脚本
# 用法: ./script.sh param1 param2 param3
echo "脚本名称: $0"
echo "参数个数: $#"
# 单个参数访问
echo "第1个参数: $1"
echo "第2个参数: $2"
echo "第3个参数: $3"
# 所有参数(不同形式)
echo "所有参数 (\$*): $*"
echo "所有参数 (\$@): $@"
echo "所有参数 (带引号的\$*): \"$*\""
echo "所有参数 (带引号的\$@): \"$@\""
# 遍历所有参数
echo "=== 使用 \$@ 遍历参数 ==="
for arg in "$@"; do
echo "参数: '$arg'"
done
echo "=== 使用 \$* 遍历参数 ==="
for arg in $*; do
echo "参数: '$arg'"
done
# 参数偏移
echo "=== 参数偏移演示 ==="
echo "原始参数: $@"
shift
echo "shift 后: $@"
shift 2
echo "再 shift 2 后: $@"
特殊变量详解
bash
#!/bin/bash
# special_vars.sh - 特殊变量演示
echo "=== 进程相关 ==="
echo "当前进程PID: $$"
echo "后台最后一个进程PID: $!"
echo "最后命令的退出状态: $?"
echo "当前选项标志: $-"
echo "=== 脚本信息 ==="
echo "脚本名称: $0"
echo "脚本第N个参数: ${10:-未提供}"
# 执行一些命令来测试特殊变量
ls /nonexistent_directory 2>/dev/null
echo "ls 命令退出状态: $?"
true
echo "true 命令退出状态: $?"
false
echo "false 命令退出状态: $?"
# 测试后台进程
sleep 1 &
echo "后台 sleep 进程PID: $!"
wait $!
echo "后台进程完成,退出状态: $?"
# 数组形式的特殊变量
echo "=== 特殊变量的数组形式 ==="
echo "所有参数数组: ${@[@]}"
echo "参数数量: ${#@}"
echo "最后参数: ${!#}"
# 函数中的特殊变量
function test_function() {
echo "函数内部:"
echo " 参数数量: $#"
echo " 所有参数: $*"
echo " 函数名: ${FUNCNAME[0]}"
echo " 调用行号: ${BASH_LINENO[0]}"
}
test_function "函数参数1" "函数参数2"
echo "调用函数后的退出状态: $?"
实际应用:参数处理脚本
bash
#!/bin/bash
# param_processor.sh - 参数处理实用脚本
# 显示用法信息
usage() {
echo "用法: $0 [选项] <输入文件> [输出文件]"
echo ""
echo "选项:"
echo " -h, --help 显示帮助信息"
echo " -v, --verbose 详细输出模式"
echo " -o, --output FILE 指定输出文件"
echo " -f, --force 强制覆盖输出文件"
echo " -n, --number NUM 处理行数"
echo ""
echo "示例:"
echo " $0 -v input.txt output.txt"
echo " $0 --force --number 10 input.txt"
}
# 初始化变量
verbose=0
force=0
line_number=-1
input_file=""
output_file=""
# 参数解析
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--verbose)
verbose=1
shift
;;
-f|--force)
force=1
shift
;;
-o|--output)
output_file="$2"
shift 2
;;
-n|--number)
line_number="$2"
if ! [[ "$line_number" =~ ^[0-9]+$ ]]; then
echo "错误: 行数必须是正整数"
exit 1
fi
shift 2
;;
-*)
echo "错误: 未知选项 $1"
usage
exit 1
;;
*)
if [[ -z "$input_file" ]]; then
input_file="$1"
elif [[ -z "$output_file" ]]; then
output_file="$1"
else
echo "错误: 多余的参数 $1"
usage
exit 1
fi
shift
;;
esac
done
# 参数验证
if [[ -z "$input_file" ]]; then
echo "错误: 必须指定输入文件"
usage
exit 1
fi
if [[ ! -f "$input_file" ]]; then
echo "错误: 输入文件不存在: $input_file"
exit 1
fi
# 处理输出文件
if [[ -z "$output_file" ]]; then
output_file="${input_file%.*}_processed.txt"
fi
if [[ -f "$output_file" && $force -eq 0 ]]; then
echo "错误: 输出文件已存在: $output_file"
echo "使用 --force 选项强制覆盖"
exit 1
fi
# 详细输出
if [[ $verbose -eq 1 ]]; then
echo "=== 参数处理结果 ==="
echo "输入文件: $input_file"
echo "输出文件: $output_file"
echo "详细模式: 开启"
echo "强制覆盖: $([ $force -eq 1 ] && echo "是" || echo "否")"
if [[ $line_number -gt 0 ]]; then
echo "处理行数: $line_number"
fi
fi
# 处理文件
echo "开始处理文件..."
if [[ $line_number -gt 0 ]]; then
head -n "$line_number" "$input_file" > "$output_file"
else
cp "$input_file" "$output_file"
fi
# 添加处理信息
echo "" >> "$output_file"
echo "=== 处理信息 ===" >> "$output_file"
echo "处理时间: $(date)" >> "$output_file"
echo "处理脚本: $0" >> "$output_file"
echo "原始文件: $input_file" >> "$output_file"
echo "处理完成,结果保存到: $output_file"
2.4 命令行参数传递
getopts 参数解析
bash
#!/bin/bash
# getopts_demo.sh - 使用 getopts 解析参数
# 显示帮助信息
show_help() {
echo "用法: $0 [-a] [-b VALUE] [-c VALUE] [-h] 文件..."
echo ""
echo "选项:"
echo " -a 启用选项a(无参数)"
echo " -b VAL 设置选项b的值"
echo " -c VAL 设置选项c的值"
echo " -h 显示帮助信息"
echo ""
echo "示例:"
echo " $0 -a -b 123 -c hello input.txt"
}
# 初始化变量
opt_a=0
opt_b=""
opt_c=""
verbose=0
# 使用 getopts 解析参数
while getopts "ab:c:hv" opt; do
case $opt in
a)
opt_a=1
;;
b)
opt_b="$OPTARG"
;;
c)
opt_c="$OPTARG"
;;
h)
show_help
exit 0
;;
v)
verbose=1
;;
\?)
echo "无效选项: -$OPTARG" >&2
show_help
exit 1
;;
:)
echo "选项 -$OPTARG 需要参数" >&2
exit 1
;;
esac
done
# 移除已处理的参数
shift $((OPTIND - 1))
# 显示解析结果
echo "=== getopts 解析结果 ==="
echo "选项a: $opt_a"
echo "选项b: $opt_b"
echo "选项c: $opt_c"
echo "详细模式: $verbose"
echo "剩余参数: $*"
# 处理文件参数
if [[ $# -eq 0 ]]; then
echo "错误: 必须指定至少一个文件"
exit 1
fi
for file in "$@"; do
if [[ -f "$file" ]]; then
echo "处理文件: $file"
# 这里可以添加实际的处理逻辑
else
echo "警告: 文件不存在: $file"
fi
done
手动参数解析(处理长选项)
bash
#!/bin/bash
# manual_parser.sh - 手动解析参数(支持长选项)
# 默认值
input_file=""
output_file="output.txt"
verbose=0
recursive=0
pattern=""
# 显示帮助
show_help() {
cat << EOF
用法: $0 [选项] <输入文件>
选项:
-i, --input FILE 指定输入文件
-o, --output FILE 指定输出文件 (默认: output.txt)
-p, --pattern PATT 搜索模式
-v, --verbose 详细输出
-r, --recursive 递归处理
-h, --help 显示帮助信息
示例:
$0 --input data.txt --output result.txt --pattern "error"
$0 -v -r -i logs/
EOF
}
# 错误处理
error_exit() {
echo "错误: $1" >&2
show_help
exit 1
}
# 解析参数
while [[ $# -gt 0 ]]; do
case "$1" in
-i|--input)
if [[ -z "$2" ]]; then
error_exit "选项 $1 需要参数"
fi
input_file="$2"
shift 2
;;
-o|--output)
if [[ -z "$2" ]]; then
error_exit "选项 $1 需要参数"
fi
output_file="$2"
shift 2
;;
-p|--pattern)
if [[ -z "$2" ]]; then
error_exit "选项 $1 需要参数"
fi
pattern="$2"
shift 2
;;
-v|--verbose)
verbose=1
shift
;;
-r|--recursive)
recursive=1
shift
;;
-h|--help)
show_help
exit 0
;;
--)
shift
break
;;
-*)
error_exit "未知选项: $1"
;;
*)
if [[ -z "$input_file" ]]; then
input_file="$1"
else
error_exit "多余的参数: $1"
fi
shift
;;
esac
done
# 参数验证
if [[ -z "$input_file" ]]; then
error_exit "必须指定输入文件"
fi
# 显示配置
echo "=== 配置信息 ==="
echo "输入文件: $input_file"
echo "输出文件: $output_file"
echo "搜索模式: ${pattern:-无}"
echo "详细模式: $verbose"
echo "递归处理: $recursive"
2.5 变量的读取与验证
输入验证函数库
bash
#!/bin/bash
# input_validation.sh - 输入验证函数库
# 验证非空
validate_not_empty() {
local value="$1"
local name="$2"
if [[ -z "$value" ]]; then
echo "错误: $name 不能为空" >&2
return 1
fi
return 0
}
# 验证数字
validate_number() {
local value="$1"
local name="$2"
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
echo "错误: $name 必须是正整数" >&2
return 1
fi
return 0
}
# 验证邮箱
validate_email() {
local email="$1"
local name="${2:-邮箱}"
local email_regex='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if ! [[ "$email" =~ $email_regex ]]; then
echo "错误: $name 格式不正确" >&2
return 1
fi
return 0
}
# 验证URL
validate_url() {
local url="$1"
local name="${2:-URL}"
local url_regex='^(https?|ftp)://[a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})?(/[^\s]*)?$'
if ! [[ "$url" =~ $url_regex ]]; then
echo "错误: $name 格式不正确" >&2
return 1
fi
return 0
}
# 验证文件路径
validate_file() {
local file="$1"
local name="${2:-文件}"
local must_exist="${3:-true}"
if [[ "$must_exist" == "true" && ! -f "$file" ]]; then
echo "错误: $name 不存在: $file" >&2
return 1
fi
if [[ -f "$file" && ! -r "$file" ]]; then
echo "错误: $name 不可读: $file" >&2
return 1
fi
return 0
}
# 验证目录
validate_directory() {
local dir="$1"
local name="${2:-目录}"
local must_exist="${3:-true}"
if [[ "$must_exist" == "true" && ! -d "$dir" ]]; then
echo "错误: $name 不存在: $dir" >&2
return 1
fi
if [[ -d "$dir" && ! -r "$dir" ]]; then
echo "错误: $name 不可读: $dir" >&2
return 1
fi
return 0
}
# 验证端口范围
validate_port() {
local port="$1"
local name="${2:-端口}"
if ! [[ "$port" =~ ^[0-9]+$ ]]; then
echo "错误: $name 必须是数字" >&2
return 1
fi
if [[ $port -lt 1 || $port -gt 65535 ]]; then
echo "错误: $name 必须在 1-65535 范围内" >&2
return 1
fi
return 0
}
# 验证IP地址
validate_ip() {
local ip="$1"
local name="${2:-IP地址}"
local ip_regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
if ! [[ "$ip" =~ $ip_regex ]]; then
echo "错误: $name 格式不正确" >&2
return 1
fi
# 验证每个段都在0-255范围内
IFS='.' read -ra segments <<< "$ip"
for segment in "${segments[@]}"; do
if [[ $segment -gt 255 ]]; then
echo "错误: $name 的段值不能超过255" >&2
return 1
fi
done
return 0
}
交互式输入验证
bash
#!/bin/bash
# interactive_input.sh - 交互式输入验证
# 导入验证函数
source <(cat << 'EOF'
validate_not_empty() {
if [[ -z "$1" ]]; then echo "错误: $2 不能为空" >&2; return 1; fi
return 0
}
validate_number() {
if ! [[ "$1" =~ ^[0-9]+$ ]]; then echo "错误: $2 必须是正整数" >&2; return 1; fi
return 0
}
validate_email() {
local email_regex='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if ! [[ "$1" =~ $email_regex ]]; then echo "错误: $2 格式不正确" >&2; return 1; fi
return 0
}
EOF
)
# 获取并验证用户输入
get_valid_input() {
local prompt="$1"
local validator="$2"
local var_name="$3"
local default_value="$4"
while true; do
if [[ -n "$default_value" ]]; then
read -p "$prompt [默认: $default_value]: " input
input="${input:-$default_value}"
else
read -p "$prompt: " input
fi
if $validator "$input" "$var_name"; then
echo "$input"
return 0
else
echo "请重新输入..."
fi
done
}
# 获取数字输入(带范围验证)
get_number_in_range() {
local prompt="$1"
local min="$2"
local max="$3"
local default_value="$4"
while true; do
if [[ -n "$default_value" ]]; then
read -p "$prompt [默认: $default_value]: " input
input="${input:-$default_value}"
else
read -p "$prompt: " input
fi
if validate_number "$input" "数字"; then
if [[ $input -ge $min && $input -le $max ]]; then
echo "$input"
return 0
else
echo "错误: 数字必须在 $min 到 $max 之间"
fi
fi
done
}
# 主程序
main() {
echo "=== 用户信息收集 ==="
# 获取姓名
name=$(get_valid_input "请输入您的姓名" 'validate_not_empty' "姓名")
# 获取年龄
age=$(get_number_in_range "请输入您的年龄" 1 120 "25")
# 获取邮箱
email=$(get_valid_input "请输入您的邮箱" 'validate_email' "邮箱")
# 获取电话
phone=""
while true; do
read -p "请输入您的电话号码: " phone
if validate_number "$phone" "电话号码"; then
if [[ ${#phone} -ge 7 && ${#phone} -le 15 ]]; then
break
else
echo "错误: 电话号码长度应在7-15位之间"
fi
fi
done
# 确认信息
echo ""
echo "=== 信息确认 ==="
echo "姓名: $name"
echo "年龄: $age"
echo "邮箱: $email"
echo "电话: $phone"
read -p "信息是否正确?(y/n): " confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
echo "信息已确认,处理完成!"
else
echo "信息收集已取消"
exit 1
fi
}
# 执行主程序
main
2.6 避坑指南:常见变量错误
常见错误类型及解决方案
1. 变量未定义错误
bash
#!/bin/bash
# 错误示例
# echo $undefined_var # 会输出空值
# 正确做法1:使用默认值
echo "未定义变量: ${undefined_var:-默认值}"
# 正确做法2:检查变量是否定义
if [[ -v undefined_var ]]; then
echo "变量已定义: $undefined_var"
else
echo "变量未定义,使用默认值"
undefined_var="新值"
fi
# 正确做法3:严格模式下处理
set -u # 启用严格模式
try_access_undefined() {
# 使用参数扩展避免错误
local safe_value="${undefined_var:-安全默认值}"
echo "安全访问: $safe_value"
# 或者先检查变量是否存在
if [[ -v undefined_var ]]; then
echo "变量值: $undefined_var"
else
echo "变量未定义"
fi
}
try_access_undefined
set +u # 关闭严格模式
2. 引号使用错误
bash
#!/bin/bash
# 错误示例:不带引号处理包含空格的变量
filename="my document.txt"
ls -l $filename # 错误:会被解析为两个参数
# 正确做法:使用双引号
ls -l "$filename" # 正确:作为一个参数
# 错误示例:路径中的空格
path="/home/user/my documents"
cd $path # 错误
# 正确做法
cd "$path"
# 数组使用示例
files=("file1.txt" "file with spaces.txt" "file3.txt")
# 错误:不带引号
for file in $files; do
echo "处理文件: $file" # 错误:会被空格分割
done
# 正确:带引号
for file in "${files[@]}"; do
echo "处理文件: $file" # 正确:保持文件名完整性
done
# 特殊情况:命令替换
files_with_spaces=$(find . -name "*.txt" -type f)
# 错误
for file in $files_with_spaces; do
echo "$file" # 错误:文件名中的空格会被分割
done
# 正确:使用 find 的 -print0 和 read 的 -d ''
find . -name "*.txt" -type f -print0 | while IFS= read -r -d '' file; do
echo "文件: $file" # 正确:完整文件名
done
3. 变量赋值错误
bash
#!/bin/bash
# 错误示例:等号两边有空格
var = "value" # 错误:语法错误
# 正确做法
var="value"
# 错误示例:使用单引号进行变量替换
name="张三"
greeting='你好, $name' # 错误:不会替换变量
# 正确做法:使用双引号
greeting="你好, $name"
# 错误示例:命令替换中的引号问题
current_date=$(date '+%Y-%m-%d %H:%M:%S') # 正确
# current_date=$(date '+%Y-%m-%d %H:%M:%S') # 错误:不需要嵌套引号
# 错误示例:变量名中包含特殊字符
1var="value" # 错误:不能以数字开头
var-able="value" # 错误:不能包含连字符
var able="value" # 错误:不能包含空格
# 正确的变量名
var1="value"
var_able="value"
varABLE="value"
4. 全局变量污染
bash
#!/bin/bash
# 错误示例:函数中意外修改全局变量
global_counter=0
bad_function() {
global_counter=10 # 意外修改全局变量
temp_var="临时值"
# 忘记使用 local 声明,temp_var 成为全局变量
}
# 正确做法:使用局部变量
good_function() {
local local_counter=10 # 局部变量
local temp_var="临时值" # 局部变量
echo "函数内部: local_counter=$local_counter"
# 返回值通过 echo 或全局变量
return 0
}
# 更好的做法:使用函数返回值
calculate_sum() {
local a=$1
local b=$2
echo $((a + b)) # 通过 echo 返回结果
}
result=$(calculate_sum 5 3)
echo "计算结果: $result"
# 错误示例:污染环境变量
modify_global_env() {
PATH="/custom/path" # 危险:修改了全局 PATH
}
# 正确做法:局部修改环境
modify_local_env() {
local old_path="$PATH"
PATH="/custom/path:$PATH"
# 使用修改后的环境
some_command
# 恢复原始环境
PATH="$old_path"
}
5. 数组处理错误
bash
#!/bin/bash
# 错误示例:数组初始化
array=1 2 3 # 错误:只赋值了第一个元素
# 正确做法
array=(1 2 3)
# 错误示例:访问数组元素
echo $array[1] # 错误:输出 array[1] 而不是第一个元素
# 正确做法
echo ${array[0]} # 第一个元素(从0开始)
echo ${array[1]} # 第二个元素
# 错误示例:遍历数组
for item in $array; do # 错误:只遍历第一个元素
echo $item
done
# 正确做法
for item in "${array[@]}"; do
echo $item
done
# 关联数组错误
# 普通数组方式初始化关联数组
wrong_assoc=(key1=value1 key2=value2) # 错误
# 正确做法
declare -A right_assoc
right_assoc[key1]="value1"
right_assoc[key2]="value2"
# 或者直接初始化
declare -A right_assoc=([key1]="value1" [key2]="value2")
错误预防最佳实践
bash
#!/bin/bash
# robust_variables.sh - 健壮的变量使用实践
# 启用严格模式
set -euo pipefail
IFS=$'\n\t'
# 变量定义的最佳实践
define_variable() {
local var_name="$1"
local var_value="${2:-}"
local description="${3:-}"
# 验证变量名
if [[ ! "$var_name" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
echo "错误: 无效的变量名: $var_name" >&2
return 1
fi
# 检查变量是否已定义
if [[ -v "$var_name" ]]; then
echo "警告: 变量 $var_name 已被重新定义" >&2
fi
# 定义变量
declare -g "$var_name"="$var_value"
# 记录变量定义
echo "定义变量: $var_name=\"$var_value\"${description:+ # $description}"
}
# 安全的变量访问
safe_get() {
local var_name="$1"
local default_value="${2:-}"
if [[ -v "$var_name" ]]; then
echo "${!var_name}"
else
echo "$default_value"
fi
}
# 变量类型检查
check_var_type() {
local var_name="$1"
local expected_type="$2"
local var_value="${!var_name}"
case "$expected_type" in
"number")
if ! [[ "$var_value" =~ ^[0-9]+$ ]]; then
echo "错误: 变量 $var_name 应该是数字,实际值: $var_value" >&2
return 1
fi
;;
"email")
if ! [[ "$var_value" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "错误: 变量 $var_name 应该是邮箱格式,实际值: $var_value" >&2
return 1
fi
;;
"file")
if [[ ! -f "$var_value" ]]; then
echo "错误: 变量 $var_name 应该是文件路径,文件不存在: $var_value" >&2
return 1
fi
;;
*)
echo "错误: 未知类型 $expected_type" >&2
return 1
;;
esac
return 0
}
# 变量使用示例
main() {
# 定义变量
define_variable "user_name" "张三" "用户姓名"
define_variable "user_age" "25" "用户年龄"
define_variable "user_email" "zhangsan@example.com" "用户邮箱"
define_variable "config_file" "/etc/config.conf" "配置文件路径"
# 类型检查
check_var_type "user_age" "number" || exit 1
check_var_type "user_email" "email" || exit 1
check_var_type "config_file" "file" || {
echo "警告: 配置文件不存在,使用默认配置"
config_file="/tmp/default_config.conf"
}
# 安全访问变量
safe_user_name=$(safe_get "user_name" "匿名用户")
safe_log_level=$(safe_get "log_level" "INFO")
safe_debug_mode=$(safe_get "debug_mode" "0")
echo "用户信息:"
echo " 姓名: $safe_user_name"
echo " 年龄: $user_age"
echo " 邮箱: $user_email"
echo " 配置文件: $config_file"
echo " 日志级别: $safe_log_level"
echo " 调试模式: $safe_debug_mode"
}
# 执行主函数
main "$@"
本章总结
本章详细讲解了 Shell 变量的定义、引用、作用域管理和参数传递等核心概念,提供了大量的实用示例和避坑指南。
核心要点:
- 三种变量定义方式:直接赋值、命令替换、用户输入,各有适用场景
- 变量引用技巧:合理使用大括号、参数扩展,避免常见陷阱
- 作用域管理:理解全局变量、局部变量、环境变量的区别和应用
- 参数传递:掌握位置参数、特殊变量、getopts 解析等方法
- 输入验证:建立完善的验证机制,确保数据安全和正确性
- 错误预防:遵循最佳实践,避免常见的变量使用错误
实践练习
-
创建一个用户注册脚本,要求:
- 收集用户姓名、年龄、邮箱、电话
- 对所有输入进行验证
- 使用默认值处理空输入
- 确认后保存到文件
-
编写一个配置文件解析脚本:
- 读取配置文件中的键值对
- 验证配置项的有效性
- 支持注释和空行
- 提供配置项的类型检查
-
实现一个命令行工具:
- 支持长短选项
- 显示帮助信息
- 参数验证和错误处理
- 详细模式输出