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

Bash 脚本编写教程 / 03 - 变量深入

03 - 变量深入

3.1 变量的本质

在 Bash 中,所有变量本质上都是字符串。即使你赋值一个数字,它也被存储为字符串。Bash 在需要时会进行隐式类型转换。

a=42
b=10

# 算术上下文:自动转换为数字
echo "$((a + b))"   # 输出: 52

# 字符串上下文:保持字符串
echo "值是: $a"      # 输出: 值是: 42

# declare 可以声明类型(但本质上仍是字符串)
declare -i num=10
num=num+5
echo "$num"          # 输出: 15
num="hello"
echo "$num"          # 输出: 0(无法转换为整数时变成 0)

declare 类型声明

选项类型示例说明
-i整数declare -i x=10自动进行算术运算
-r只读declare -r PI=3.14等同于 readonly
-a索引数组declare -a arr=()Bash 4+
-A关联数组declare -A map=()Bash 4+
-l小写declare -l name="HELLO"自动转小写
-u大写declare -u name="hello"自动转大写
-n引用(nameref)declare -n ref=varBash 4.3+,间接引用
-x导出为环境变量declare -x PATH="/usr/bin"子进程可见
-p打印变量属性declare -p var调试用
declare -l lower="Hello World"
echo "$lower"        # 输出: hello world

declare -u upper="hello world"
echo "$upper"        # 输出: HELLO WORLD

# nameref:间接引用(类似指针)
greet() {
    local -n ref=$1   # $1 是变量名
    ref="Hello, ${ref}!"
}
name="World"
greet name
echo "$name"          # 输出: Hello, World!

3.2 局部变量

#!/bin/bash

# 全局变量(脚本级别)
global_var="我是全局变量"

my_function() {
    # 局部变量:仅在函数内可见
    local local_var="我是局部变量"
    
    # 全局变量在函数内也可以访问
    echo "函数内: $global_var"
    echo "函数内: $local_var"
}

my_function
echo "函数外: $global_var"
# echo "$local_var"  # ❌ 未定义,为空

# local 的注意事项
test_scope() {
    # local 只在函数中有效
    # 如果在函数外使用 local,Bash 会报错
    # local x=10  # 如果在函数外
    
    # 递归中的 local
    local count=${1:-0}
    echo "$count"
    [[ $count -lt 3 ]] && test_scope $((count + 1))
    # 递归中每次调用都有自己独立的 local count
}
test_scope

局部变量的陷阱

#!/bin/bash

# ⚠️ 陷阱一:忘记 local 声明污染全局
bad_function() {
    counter=0           # 这是全局变量!
    counter=$((counter + 1))
}

good_function() {
    local counter=0     # ✅ 正确:局部变量
    counter=$((counter + 1))
}

# ⚠️ 陷阱二:管道中的变量在子 Shell 中
# 管道的每一段都在子 Shell 中执行
echo "hello" | read greeting
echo "$greeting"        # ❌ 为空!

# ✅ 正确方法:进程替换
read greeting < <(echo "hello")
echo "$greeting"        # 输出: hello

# ✅ 正确方法:lastpipe(Bash 4.2+)
shopt -s lastpipe
echo "hello" | read greeting
echo "$greeting"        # 输出: hello

3.3 环境变量

环境变量是从父进程传递给子进程的变量。

常用环境变量

变量说明示例值
PATH可执行文件搜索路径/usr/bin:/bin:/usr/local/bin
HOME当前用户主目录/home/user
USER当前用户名user
SHELL默认 Shell/bin/bash
PWD当前工作目录/home/user/project
OLDPWD上一个工作目录/home/user
LANG语言/编码设置zh_CN.UTF-8
TERM终端类型xterm-256color
HOSTNAME主机名server01
RANDOM随机数 (0-32767)12345
LINENO当前行号42
SECONDS脚本运行秒数30
FUNCNAME当前函数名my_function
BASH_VERSIONBash 版本5.2.15(1)-release
EPOCHSECONDSUnix 时间戳1683720000(Bash 5+)
# 查看所有环境变量
env
printenv

# 查看特定变量
echo "$PATH"
echo "$HOME"

# 导出为环境变量
export MY_VAR="hello"

# 或者先定义再导出
MY_VAR="hello"
export MY_VAR

# 环境变量只影响当前进程及子进程
# 不影响父进程和兄弟进程

PATH 操作

# 查看 PATH
echo "$PATH" | tr ':' '\n'

# 追加到 PATH(不重启永久生效需写入配置文件)
export PATH="$PATH:/opt/myapp/bin"

# 前置到 PATH(优先搜索)
export PATH="/opt/myapp/bin:$PATH"

# 检查命令是否在 PATH 中
command -v bash        # 输出: /bin/bash
which bash             # 输出: /bin/bash
type bash              # 输出: bash is /bin/bash

# 检查命令是否存在
if command -v docker &>/dev/null; then
    echo "Docker 已安装"
else
    echo "Docker 未安装"
fi

配置文件加载顺序

登录 Shell(Login Shell):
  /etc/profile
    → ~/.bash_profile
      → ~/.bash_login
        → ~/.profile

非登录交互 Shell(Non-login Interactive):
  /etc/bash.bashrc
    → ~/.bashrc

非交互 Shell(脚本执行):
  仅继承环境变量,不加载上述配置

3.4 特殊变量

#!/bin/bash
# special_vars.sh —— 展示特殊变量

echo "脚本名称:    $0"
echo "参数个数:    $#"
echo "所有参数:    $*"
echo "所有参数:    $@"
echo "第一个参数:  $1"
echo "第二个参数:  $2"
echo "上一个PID:   $$"
echo "上一个退出码: $?"
echo "Shell 选项:  $-"
变量说明示例
$0脚本名称./deploy.sh
$1-$9第 1-9 个参数$1 = “production”
${10}第 10+ 个参数需要花括号
$#参数个数3
$*所有参数(单字符串)"a b c"
$@所有参数(独立字符串)"a" "b" "c"
$$当前进程 PID12345
$!最近后台进程 PID12346
$?上一条命令退出码0
$-当前 Shell 选项himB

$*$@ 的区别

#!/bin/bash

echo '--- $* ---'
for arg in $*; do
    echo "  arg: '$arg'"
done

echo '--- "$*" ---'
for arg in "$*"; do
    echo "  arg: '$arg'"
done

echo '--- $@ ---'
for arg in $@; do
    echo "  arg: '$arg'"
done

echo '--- "$@" (推荐) ---'
for arg in "$@"; do
    echo "  arg: '$arg'"
done

# 调用: ./script.sh "hello world" "foo bar"
# "$*" 输出两个参数合并为一个: "hello world foo bar"
# "$@" 保持原始参数分隔: "hello world" 和 "foo bar"(推荐)

💡 规则:遍历参数时永远使用 "$@"

3.5 数组(Array)

Bash 支持索引数组和关联数组(Bash 4.0+)。

索引数组

# 声明方式一:直接赋值
fruits=("apple" "banana" "cherry" "date")

# 声明方式二:逐个赋值
colors[0]="red"
colors[1]="green"
colors[2]="blue"

# 声明方式三:declare
declare -a numbers=(10 20 30 40 50)

# 读取元素
echo "${fruits[0]}"     # 输出: apple(索引从 0 开始)
echo "${fruits[2]}"     # 输出: cherry

# 读取所有元素
echo "${fruits[@]}"     # 输出: apple banana cherry date
echo "${fruits[*]}"     # 输出: apple banana cherry date

# 数组长度
echo "${#fruits[@]}"    # 输出: 4

# 添加元素
fruits+=("elderberry")

# 切片
echo "${fruits[@]:1:2}" # 输出: banana cherry(从索引1开始取2个)

3.6 关联数组(Associative Array)

# 必须先声明为关联数组
declare -A user

# 赋值
user[name]="张三"
user[age]=30
user[role]="工程师"
user[email]="[email protected]"

# 读取
echo "${user[name]}"    # 输出: 张三
echo "${user[role]}"    # 输出: 工程师

# 所有键
echo "${!user[@]}"      # 输出: name age role email

# 所有值
echo "${user[@]}"       # 输出: 张三 30 工程师 [email protected]

# 长度
echo "${#user[@]}"      # 输出: 4

# 遍历
for key in "${!user[@]}"; do
    echo "$key = ${user[$key]}"
done

# 判断键是否存在
if [[ -v user[name] ]]; then
    echo "name 存在"
fi

3.7 业务场景:环境检测脚本

#!/bin/bash
# check_env.sh —— 检查部署环境
set -euo pipefail

declare -A required_tools=(
    [git]="版本控制"
    [docker]="容器运行时"
    [curl]="HTTP 客户端"
    [jq]="JSON 处理"
)

declare -A optional_tools=(
    [terraform]="基础设施管理"
    [ansible]="配置管理"
    [helm]="K8s 包管理"
)

check_tool() {
    local tool=$1
    local desc=$2
    local required=$3

    if command -v "$tool" &>/dev/null; then
        local version
        version=$("$tool" --version 2>/dev/null | head -1 || echo "未知版本")
        printf "  ✅ %-15s %s\n" "$tool" "$version"
        return 0
    else
        if [[ "$required" == "yes" ]]; then
            printf "  ❌ %-15s %s (必需!)\n" "$tool" "$desc"
            return 1
        else
            printf "  ⚠️  %-15s %s (可选)\n" "$tool" "$desc"
            return 0
        fi
    fi
}

echo "================================"
echo "  环境依赖检查"
echo "================================"

echo ""
echo "[必需工具]"
errors=0
for tool in "${!required_tools[@]}"; do
    check_tool "$tool" "${required_tools[$tool]}" "yes" || ((errors++))
done

echo ""
echo "[可选工具]"
for tool in "${!optional_tools[@]}"; do
    check_tool "$tool" "${optional_tools[$tool]}" "no"
done

echo ""
if [[ $errors -gt 0 ]]; then
    echo "❌ 发现 $errors 个必需工具缺失,请先安装。"
    exit 1
else
    echo "✅ 环境检查通过!"
fi

3.8 注意事项

陷阱说明解决方案
未加引号的变量路径含空格时出错始终使用 "$var"
管道中的变量赋值子 Shell 中操作不影响父进程使用 < <() 进程替换
空变量展开rm -rf $DIR/*$DIR 为空set -u[[ -n "$DIR" ]]
数组空元素${arr[@]} 跳过空元素使用 ${arr[@]+"${arr[@]}"}
declare -i 转换失败字符串赋值给 -i 变量得 0检查输入合法性

3.9 扩展阅读