# Shell 变量

URL: https://caijiao.org/linux/shell/variables
Source: docs/linux/shell/variables.md
Description: 深入了解Shell变量的定义、使用、作用域和高级特性，掌握Shell编程的核心概念

## 1. Shell 变量基础

### 1.1 什么是Shell变量

Shell变量是Shell中存储数据的一种方式，可以将字符串、数字或命令输出等赋值给变量，然后在脚本或命令行中引用这些变量。Shell变量是大小写敏感的，通常使用大写字母表示环境变量，使用小写字母表示局部变量。

### 1.2 变量命名规则

Shell变量的命名需要遵循以下规则：

- 变量名只能包含字母、数字和下划线
- 变量名必须以字母或下划线开头，不能以数字开头
- 变量名不能包含空格或特殊字符
- Shell变量区分大小写

以下是有效的变量名示例：

```bash
name
user_name
USER_NAME
_count
SHELL2
```

以下是无效的变量名示例：

```bash
1name      # 不能以数字开头
user-name  # 不能包含连字符
user name  # 不能包含空格
user@name  # 不能包含特殊字符
```

### 1.3 变量的定义和赋值

在Shell中，可以通过简单的赋值语句来定义变量：

```bash
变量名=值
```

注意，变量名和等号之间不能有空格，等号和值之间也不能有空格。

示例：

```bash
# 定义字符串变量
name="John Doe"
age=30

# 定义命令输出作为变量值
date_today=$(date +"%Y-%m-%d")

# 使用反引号（较旧的语法）
host_name=`hostname`
```

## 2. 变量的使用

### 2.1 引用变量

要在Shell中引用变量的值，可以使用`$变量名`或`${变量名}`语法。使用花括号`{}`可以在变量名后直接添加其他字符，避免歧义。

示例：

```bash
name="John"
echo "Hello, $name!"
echo "Hello, ${name}Doe!"  # 输出: Hello, JohnDoe!
```

### 2.2 修改变量值

可以通过重新赋值来修改已定义变量的值：

```bash
count=10
echo "当前计数: $count"

count=20
echo "更新后的计数: $count"
```

### 2.3 显示所有变量

可以使用以下命令显示所有已定义的变量：

```bash
# 显示所有环境变量和局部变量
env
printenv

# 显示所有变量（包括局部变量和函数）
set

# 使用grep过滤特定变量
env | grep PATH
echo $PATH
```

### 2.4 删除变量

使用`unset`命令可以删除已定义的变量：

```bash
name="John"
echo "$name"  # 输出: John

unset name
echo "$name"  # 输出空行，变量已被删除
```

## 3. 变量类型

### 3.1 局部变量

局部变量是在当前Shell进程中定义的变量，只在定义它的Shell或Shell脚本中可见，对其他Shell进程不可见。

```bash
# 在当前Shell中定义局部变量
local_var="这是一个局部变量"

echo $local_var  # 输出变量值
```

在Shell函数中，可以使用`local`关键字定义只在函数内部可见的变量：

```bash
my_function() {
  local local_var="函数内部变量"
  echo "函数内: $local_var"
}

my_function  # 输出: 函数内: 函数内部变量
echo "函数外: $local_var"  # 输出空行或其他值
```

### 3.2 环境变量

环境变量是对当前Shell及其所有子Shell可见的变量。环境变量通常用于存储系统配置信息，如PATH、HOME等。

使用`export`命令可以将局部变量转换为环境变量：

```bash
# 定义并导出环境变量
export PATH="$PATH:/usr/local/bin"
export LANG="zh_CN.UTF-8"

# 另一种方式：先定义变量，再导出
home_dir="/home/user"
export home_dir
```

### 3.3 位置参数

位置参数是Shell脚本接收的命令行参数，使用`$1`, `$2`, `$3`等表示第一个、第二个、第三个参数，依此类推。`$0`表示脚本本身的名称。

示例脚本`parameters.sh`：

```bash
#!/bin/bash

echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "第三个参数: $3"
```

运行方式：

```bash
chmod +x parameters.sh
./parameters.sh arg1 arg2 arg3
```

输出结果：

```
脚本名称: ./parameters.sh
第一个参数: arg1
第二个参数: arg2
第三个参数: arg3
```

### 3.4 特殊变量

Shell提供了一系列特殊变量，用于获取命令行参数数量、退出状态等信息：

| 变量 | 描述 |
|-----|------|
| `$0` | 脚本名称 |
| `$1`, `$2`, ... | 位置参数 |
| `$#` | 命令行参数的个数 |
| `$*` | 所有位置参数，作为一个单词 |
| `$@` | 所有位置参数，每个参数作为独立的单词 |
| `$?` | 上一个命令的退出状态（0表示成功，非0表示失败） |
| `$$` | 当前进程的PID |
| `$!` | 上一个后台进程的PID |
| `$_` | 上一个命令的最后一个参数 |
| `$HOME` | 用户主目录 |
| `$PATH` | 命令搜索路径 |
| `$LANG` | 当前语言环境 |
| `$USER` | 当前用户名 |
| `$HOSTNAME` | 主机名 |

示例脚本`special_vars.sh`：

```bash
#!/bin/bash

echo "脚本名称: $0"
echo "参数数量: $#"
echo "所有参数(\$*): $*"
echo "所有参数(\$@): $@"
echo "当前进程PID: $$"

# 执行一个命令并查看退出状态
echo "测试命令"
command_status=$?
echo "命令退出状态: $command_status"
```

## 4. 变量展开和替换

### 4.1 参数展开

Shell提供了强大的参数展开功能，可以在引用变量时进行各种操作：

```bash
# 基本展开
name="John"
echo "Hello, $name!"

# 默认值：如果变量未设置或为空，使用默认值
user_name=${name:-Guest}
echo "用户名: $user_name"

# 强制赋值：如果变量未设置或为空，设置并使用默认值
user_age=${age:=18}
echo "年龄: $user_age"

# 错误处理：如果变量未设置或为空，显示错误信息并退出
# ${required_var:?变量未设置}

# 替代值：如果变量已设置且非空，使用替代值，否则为空
capital_name=${name:+JOHN}
echo "大写名称: $capital_name"

# 字符串长度
echo "名称长度: ${#name}"
```

### 4.2 字符串操作

可以对变量进行各种字符串操作，如截取、替换等：

```bash
path="/home/user/documents/report.txt"

# 提取子字符串（从索引0开始，长度为5）
echo "前5个字符: ${path:0:5}"

# 从指定索引到末尾
echo "从第6个字符开始: ${path:6}"

# 从字符串开头删除匹配的最短子串
echo "删除/home/: ${path#/home/}"

# 从字符串开头删除匹配的最长子串
echo "删除直到最后一个/: ${path##*/}"

# 从字符串结尾删除匹配的最短子串
echo "删除.txt: ${path%.txt}"

# 从字符串结尾删除匹配的最长子串
echo "删除扩展名: ${path%.*}"

# 替换第一个匹配的子串
echo "替换user为admin: ${path/user/admin}"

# 替换所有匹配的子串
echo "替换所有o为O: ${path//o/O}"
```

## 5. 数组变量

### 5.1 数组定义和访问

在Shell中，可以定义数组变量来存储多个值：

```bash
# 定义数组
fruits=('Apple' 'Banana' 'Orange' 'Grape')

# 另一种定义方式
colors[0]="Red"
colors[1]="Green"
colors[2]="Blue"

# 访问数组元素
echo "第一个水果: ${fruits[0]}"
echo "第三个颜色: ${colors[2]}"

# 访问所有数组元素
echo "所有水果: ${fruits[@]}"
echo "所有颜色: ${colors[*]}"

# 获取数组长度
echo "水果数量: ${#fruits[@]}"
```

### 5.2 数组操作

```bash
# 添加元素到数组末尾
fruits+=('Mango')

# 更新数组元素
fruits[1]='Pineapple'

# 删除数组元素
unset fruits[2]

# 遍历数组
echo "遍历水果数组:"
for fruit in "${fruits[@]}"; do
  echo "- $fruit"
done

# 使用索引遍历数组
echo "使用索引遍历颜色数组:"
for ((i=0; i<${#colors[@]}; i++)); do
  echo "索引 $i: ${colors[i]}"
done
```

### 5.3 数组切片

```bash
# 数组切片：获取子数组
numbers=(1 2 3 4 5 6 7 8 9 10)

# 从索引2开始，取3个元素
echo "切片 [2:5]: ${numbers[@]:2:3}"

# 从索引3到末尾
echo "切片 [3:]: ${numbers[@]:3}"
```

## 6. 变量作用域

### 6.1 全局变量

在脚本的顶层定义的变量默认为全局变量，可以在脚本的任何地方访问，包括函数内部：

```bash
global_var="这是全局变量"

display_var() {
  echo "函数内访问全局变量: $global_var"
  # 在函数内修改全局变量
  global_var="修改后的全局变量"
}

echo "函数调用前: $global_var"
display_var
echo "函数调用后: $global_var"
```

### 6.2 局部变量

在函数内部使用`local`关键字定义的变量是局部变量，只在函数内部可见：

```bash
outer_var="外部变量"

demo_function() {
  local inner_var="内部变量"
  echo "函数内访问局部变量: $inner_var"
  echo "函数内访问外部变量: $outer_var"
  
  # 修改外部变量
  outer_var="函数内修改的外部变量"
}

echo "函数调用前: $outer_var"
demo_function
echo "函数调用后: $outer_var"
# 以下行将报错，因为inner_var在函数外部不可见
# echo "函数外部访问局部变量: $inner_var"
```

### 6.3 环境变量作用域

环境变量在当前Shell及其所有子Shell中可见：

```bash
# 创建一个脚本示例 (child_script.sh)
cat > child_script.sh << 'EOF'
#!/bin/bash
echo "子脚本中访问环境变量: $MY_ENV_VAR"
echo "子脚本中访问局部变量: $MY_LOCAL_VAR"
EOF

chmod +x child_script.sh

# 设置局部变量和环境变量
MY_LOCAL_VAR="这是局部变量"
export MY_ENV_VAR="这是环境变量"

# 在当前Shell中访问
echo "当前Shell中访问局部变量: $MY_LOCAL_VAR"
echo "当前Shell中访问环境变量: $MY_ENV_VAR"

# 在子脚本中访问
./child_script.sh
```

## 7. 变量的高级用法

### 7.1 命令替换

可以将命令的输出赋值给变量：

```bash
# 使用 $(command) 语法（推荐）
date_today=$(date +"%Y-%m-%d")
file_count=$(ls -l | wc -l)

# 使用反引号语法（较旧的方式）
current_dir=`pwd`

# 在字符串中嵌入命令替换
message="今天是 $(date +"%Y-%m-%d")，共有 $(ls -l | wc -l) 个文件。"
echo "$message"
```

### 7.2 间接变量引用

间接变量引用允许通过另一个变量的值作为变量名来访问变量：

```bash
# 使用 ${!var} 语法
first_name="John"
last_name="Doe"
full_name="first_name last_name"

# 直接引用
name_var="first_name"
echo "直接引用: ${!name_var}"  # 输出: John

# 组合变量名
prefix="user"
suffix="name"
user_name="Alice"
var_name="${prefix}${suffix}"
echo "组合变量名: ${!var_name}"  # 输出: Alice
```

### 7.3 变量类型转换

Shell变量本质上都是字符串，但可以进行算术运算：

```bash
# 算术运算
a=10
b=20

# 使用 $((expression)) 语法
c=$((a + b))
d=$((a * b))
e=$((b / a))

# 也可以使用 let 命令
let "f = a + 5"

# 浮点数运算需要使用 bc 命令
x=10.5
y=5.2
result=$(echo "$x + $y" | bc)

# 转换为字符串（Shell变量默认就是字符串）
number=42
string="$number"

# 字符串拼接
greeting="Hello, "
name="World"
message="$greeting$name!"
```

## 8. 变量和引号

### 8.1 不同引号的作用

在Shell中，引号对变量的展开有不同的影响：

```bash
name="John Doe"

# 双引号：变量会被展开
echo "双引号: Hello, $name!"
# 输出: 双引号: Hello, John Doe!

# 单引号：变量不会被展开
echo '单引号: Hello, $name!'
# 输出: 单引号: Hello, $name!

# 反引号：执行命令并替换结果
echo "反引号: 今天是 `date +"%Y-%m-%d"`"
# 输出: 反引号: 今天是 2023-07-15（取决于当前日期）
```

### 8.2 转义字符

使用反斜杠`\`可以转义特殊字符，使其被当作普通字符处理：

```bash
# 转义美元符号，使其不被当作变量
price=100
echo "价格是 \$${price}"
# 输出: 价格是 $100

# 转义引号
echo "他说\"Hello, World!\""
# 输出: 他说"Hello, World!"

# 转义反斜杠
echo "路径是 C:\\Users\\Documents"
# 输出: 路径是 C:\Users\Documents
```

## 9. 环境变量配置文件

### 9.1 常用配置文件

Linux系统中有多个配置文件用于设置环境变量，这些文件在不同的登录情况下会被加载：

| 配置文件 | 描述 | 加载时机 |
|---------|------|----------|
| `/etc/profile` | 系统级别的全局配置 | 所有用户登录时加载 |
| `/etc/bash.bashrc` | 系统级别的Bash配置 | 每个新的Bash shell启动时加载 |
| `~/.profile` | 用户级别的配置 | 用户登录时加载 |
| `~/.bash_profile` | 用户级别的Bash配置 | Bash登录时加载 |
| `~/.bashrc` | 用户级别的Bash配置 | 每个新的Bash shell启动时加载 |
| `~/.bash_login` | 用户级别的登录配置 | 用户登录时加载（如果没有~/.bash_profile） |

### 9.2 永久设置环境变量

要永久设置环境变量，可以将变量定义添加到适当的配置文件中：

```bash
# 编辑个人Bash配置文件
nano ~/.bashrc

# 添加环境变量定义
export PATH="$PATH:/usr/local/bin"
export JAVA_HOME="/usr/lib/jvm/java-11-openjdk"
export EDITOR="vim"

# 保存退出后，重新加载配置文件
source ~/.bashrc
# 或者
. ~/.bashrc
```

## 10. 变量使用最佳实践

### 10.1 命名约定

- **环境变量**：使用全大写字母，单词之间用下划线分隔（如`JAVA_HOME`）
- **局部变量**：使用小写字母，单词之间用下划线分隔（如`user_name`）
- **常量**：使用全大写字母（如`MAX_SIZE=100`）
- **临时变量**：可以使用简短的名称（如`i`, `j`等循环变量）

### 10.2 安全使用变量

- 始终使用双引号包围变量引用，避免因变量值中的空格或特殊字符导致的问题
- 使用`${变量名}`而不是`$变量名`，特别是在变量名后直接跟其他字符时
- 验证输入变量，避免注入攻击

```bash
# 不安全的做法
file=$1
cat $file  # 如果file包含恶意输入如 "/etc/passwd; rm -rf /" 会非常危险

# 安全的做法
file="$1"
# 验证文件是否存在且为普通文件
if [ -f "$file" ]; then
  cat "$file"
else
  echo "错误: 文件不存在或不是普通文件"
  exit 1
fi
```

### 10.3 变量调试技巧

```bash
# 使用set -x启用调试模式，会显示执行的每条命令及其展开后的形式
set -x

variable="test"
echo "$variable"

# 使用set +x关闭调试模式
set +x

# 打印所有变量和函数
declare -p

# 只打印数组
declare -a

# 只打印函数
declare -f
```

## 11. 变量相关的内置命令

### 11.1 declare/typeset 命令

`declare`命令用于声明变量类型和属性：

```bash
# 声明只读变量
declare -r readonly_var="这是只读变量"
# 或者使用 readonly 命令
readonly readonly_var2="另一个只读变量"

# 声明整数变量
declare -i int_var=10
int_var=$int_var+5  # 自动进行算术运算，结果为15

# 声明关联数组（类似字典）
declare -A colors
colors["red"]="#FF0000"
colors["green"]="#00FF00"
colors["blue"]="#0000FF"
echo "红色: ${colors[red]}"

# 声明局部变量（在函数中使用）
declare -l lower_var="HELLO"  # 自动转换为小写
```

### 11.2 unset 命令

```bash
# 删除单个变量
my_var="test"
unset my_var

# 删除数组的单个元素
my_array=(1 2 3 4 5)
unset my_array[2]

# 删除整个数组
unset my_array

# 删除函数
my_function() {
  echo "Hello"
}
unset -f my_function
```

### 11.3 export 命令

```bash
# 导出单个变量
export PATH

# 定义并导出变量
export DB_HOST="localhost"

# 导出多个变量
export USER_NAME="admin" PASSWORD="secret"

# 查看所有导出的变量
export -p
```

## 12. 变量实例与应用

### 12.1 基本变量应用示例

```bash
#!/bin/bash

# 定义变量
SCRIPT_NAME="系统信息收集器"
SCRIPT_VERSION="1.0"
HOST_NAME=$(hostname)
CURRENT_DATE=$(date +"%Y-%m-%d %H:%M:%S")

# 显示欢迎信息
echo "========================================"
echo "$SCRIPT_NAME v$SCRIPT_VERSION"
echo "主机: $HOST_NAME"
echo "日期: $CURRENT_DATE"
echo "========================================"

# 收集系统信息
CPU_INFO=$(cat /proc/cpuinfo | grep 'model name' | head -1 | awk -F: '{print $2}' | sed 's/^[ \t]*//')
MEMORY_INFO=$(free -h | grep Mem | awk '{print $2}')
DISK_INFO=$(df -h / | grep / | awk '{print $2, $5}')

# 显示系统信息
echo "\n系统信息:"
echo "CPU: $CPU_INFO"
echo "内存: $MEMORY_INFO"
echo "磁盘: $DISK_INFO"

echo "\n脚本执行完成！"
```

### 12.2 数组变量应用示例

```bash
#!/bin/bash

# 定义应用列表
APPLICATIONS=("Apache" "MySQL" "PHP" "Node.js" "Docker")
VERSIONS=("2.4.51" "8.0.26" "7.4.24" "16.11.0" "20.10.8")

# 显示应用列表
echo "已安装应用列表:"
echo "=================="

# 遍历数组显示应用及其版本
for ((i=0; i<${#APPLICATIONS[@]}; i++)); do
  echo "${APPLICATIONS[$i]}: ${VERSIONS[$i]}"
done

# 添加新应用
APPLICATIONS+=("Redis")
VERSIONS+=("6.2.6")

echo "\n更新后的应用列表:"
echo "=================="
for ((i=0; i<${#APPLICATIONS[@]}; i++)); do
  echo "${APPLICATIONS[$i]}: ${VERSIONS[$i]}"
done
```

### 12.3 环境变量应用示例

```bash
#!/bin/bash

# 检查并设置必要的环境变量
if [ -z "$JAVA_HOME" ]; then
  echo "警告: JAVA_HOME 环境变量未设置"
  # 尝试自动检测
  if [ -d "/usr/lib/jvm/java-11-openjdk" ]; then
    export JAVA_HOME="/usr/lib/jvm/java-11-openjdk"
    echo "已自动设置 JAVA_HOME=$JAVA_HOME"
  fi
fi

# 将Java bin目录添加到PATH（如果尚未添加）
if [[ "$PATH" != *"$JAVA_HOME/bin"* ]]; then
  export PATH="$JAVA_HOME/bin:$PATH"
  echo "已将Java添加到PATH"
fi

# 创建应用配置目录
APP_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/myapp"
mkdir -p "$APP_CONFIG_DIR"

echo "配置目录: $APP_CONFIG_DIR"
echo "Java版本: $(java -version 2>&1 | head -1)"
```

通过本章的学习，您应该掌握了Shell变量的定义、使用、作用域和高级特性，可以在Shell脚本中灵活应用这些知识来编写更加高效和健壮的脚本程序。
