第2章 Shell 变量与参数传递:3 种定义方式 + 避坑指南

本章导语:变量是 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 变量的定义、引用、作用域管理和参数传递等核心概念,提供了大量的实用示例和避坑指南。

核心要点:

  1. 三种变量定义方式:直接赋值、命令替换、用户输入,各有适用场景
  2. 变量引用技巧:合理使用大括号、参数扩展,避免常见陷阱
  3. 作用域管理:理解全局变量、局部变量、环境变量的区别和应用
  4. 参数传递:掌握位置参数、特殊变量、getopts 解析等方法
  5. 输入验证:建立完善的验证机制,确保数据安全和正确性
  6. 错误预防:遵循最佳实践,避免常见的变量使用错误

实践练习

  1. 创建一个用户注册脚本,要求:

    • 收集用户姓名、年龄、邮箱、电话
    • 对所有输入进行验证
    • 使用默认值处理空输入
    • 确认后保存到文件
  2. 编写一个配置文件解析脚本:

    • 读取配置文件中的键值对
    • 验证配置项的有效性
    • 支持注释和空行
    • 提供配置项的类型检查
  3. 实现一个命令行工具:

    • 支持长短选项
    • 显示帮助信息
    • 参数验证和错误处理
    • 详细模式输出
相关推荐
云和恩墨1 小时前
打造数据库安全堡垒:统一自动化监控平台在DBA运维中的价值解析
运维·数据库·安全·自动化·dba
AiTEN_Robotics1 小时前
智能化时代,汽车制造机器人的发展趋势是什么?
运维·机器人·自动化
looking_for__1 小时前
【Linux】进程概念
linux
北京阿法龙科技有限公司1 小时前
AR眼镜仓储物流分拣技术应用与落地方案
运维·人工智能·ar·xr
悦数图数据库1 小时前
赋能金融风控:悦数图数据库助力互联网金融平台应对全球扩张挑战
大数据·运维·数据库
Eric.Lee20211 小时前
ubuntu系统在bashrc文件中对conda进行启用设置
linux·运维·python·ubuntu·conda
mooyuan天天1 小时前
CobaltStrike横向渗透之Https Beacon实战2(跳板机Linux)
linux·内网渗透·横向移动·cobalt strike
model20051 小时前
Alibaba linux 3安装mapserver
linux·运维·服务器
xiejava10181 小时前
企业级私有docker镜像仓库Harbor的搭建和使用
运维·docker·云原生·容器