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=var | Bash 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_VERSION | Bash 版本 | 5.2.15(1)-release |
EPOCHSECONDS | Unix 时间戳 | 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" |
$$ | 当前进程 PID | 12345 |
$! | 最近后台进程 PID | 12346 |
$? | 上一条命令退出码 | 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]="zhangsan@example.com"
# 读取
echo "${user[name]}" # 输出: 张三
echo "${user[role]}" # 输出: 工程师
# 所有键
echo "${!user[@]}" # 输出: name age role email
# 所有值
echo "${user[@]}" # 输出: 张三 30 工程师 zhangsan@example.com
# 长度
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 | 检查输入合法性 |