跳到主要内容

变量与数据类型

变量是 Shell 脚本的基础构建块。本章详细介绍 Shell 变量的定义、使用、作用域,以及数组、特殊变量和环境变量等核心概念。

变量定义与赋值

Shell 变量是弱类型的,不需要声明类型,直接赋值即可创建变量。

基本语法

variable_name=value

重要规则:等号两边不能有空格!

# 正确的赋值
name="John"
age=25
active=true

# 错误的赋值(等号两边有空格)
name = "John" # 语法错误!会被解释为执行 name 命令,参数为 "=" 和 "John"

为什么不能有空格

在 Shell 中,空格是词分隔符。name="John" 是一个赋值语句,而 name = "John" 会被解析为执行名为 name 的命令,带有两个参数 ="John"

变量命名规则

  • 只能包含字母、数字和下划线
  • 必须以字母或下划线开头
  • 区分大小写
  • 不能使用 Shell 保留字(如 if, then, else 等)
# 合法的变量名
name="John"
_name="John"
NAME="John"
var1="John"
my_var="John"

# 不合法的变量名
1var="John" # 不能以数字开头
my-var="John" # 不能包含连字符

变量使用

使用变量时,在变量名前加 $ 符号:

name="John"
echo $name # 输出:John
echo ${name} # 输出:John(推荐使用花括号)

花括号 {} 用于明确变量名的边界,在变量名后面紧跟其他字符时必须使用:

name="John"
echo "Hello, ${name}!" # 输出:Hello, John!
echo "Hello, $name!" # 也输出:Hello, John!

# 必须使用花括号的情况
echo "${name}s" # 输出:Johns
echo "$names" # 输出为空,因为 $names 是另一个变量

只读变量

使用 readonly 命令可以将变量设为只读,之后不能修改:

name="John"
readonly name
name="Mike" # 报错:name: readonly variable

也可以在定义时直接声明为只读:

readonly PI=3.14159

删除变量

使用 unset 命令删除变量:

name="John"
unset name
echo $name # 输出为空

注意:不能删除只读变量。

数据类型

Shell 变量本质上是字符串类型,但根据上下文可以解释为其他类型。

字符串

字符串是 Shell 中最基本的数据类型,可以使用单引号、双引号或不加引号。

# 不加引号(不能包含空格和特殊字符)
name=John

# 单引号(保留字面值)
name='John Doe'

# 双引号(支持变量扩展和转义)
greeting="Hello, $name"

整数

Shell 变量可以存储整数值,使用 declare -i 声明整数变量:

declare -i count
count=10
count="hello" # 字符串会被当作 0
echo $count # 输出:0

# 整数变量可以直接进行算术运算
declare -i x=10
x=x+5
echo $x # 输出:15

数组

Bash 支持一维数组(索引数组和关联数组)。

布尔值

Shell 没有真正的布尔类型,通常使用整数 0 表示真,非 0 表示假,或者使用字符串 "true" 和 "false"。

# 在条件判断中
if true; then
echo "This is true"
fi

# 自定义布尔变量
is_valid=0 # 0 表示真
if [ $is_valid -eq 0 ]; then
echo "Valid"
fi

字符串操作

Shell 提供了丰富的字符串操作功能。

获取字符串长度

str="Hello, World!"
echo ${#str} # 输出:13

# 或者使用 expr
expr length "$str" # 输出:13

字符串拼接

Shell 中字符串拼接非常简单,直接相邻即可:

first="Hello"
last="World"
full="$first, $last!"
echo $full # 输出:Hello, World!

# 直接拼接
str="Hello"
str="${str} World"
echo $str # 输出:Hello World

子字符串

使用 ${string:position:length} 提取子字符串:

str="Hello, World!"

# 从位置 0 开始提取 5 个字符
echo ${str:0:5} # 输出:Hello

# 从位置 7 开始提取到末尾
echo ${str:7} # 输出:World!

# 从右边开始计数(负数索引)
echo ${str: -6} # 输出:World!(注意空格)
echo ${str:0-6:5} # 输出:World

字符串查找

str="Hello, World!"

# 查找子字符串位置(从 1 开始计数,0 表示未找到)
expr index "$str" "W" # 输出:8

# 查找子字符串
case "$str" in
*World*) echo "Found" ;;
*) echo "Not found" ;;
esac

# 使用 grep
if echo "$str" | grep -q "World"; then
echo "Found"
fi

字符串替换

使用参数扩展进行字符串替换:

str="Hello, World! World!"

# 替换第一个匹配
echo ${str/World/Shell} # 输出:Hello, Shell! World!

# 替换所有匹配
echo ${str//World/Shell} # 输出:Hello, Shell! Shell!

# 替换开头匹配
echo ${str/#Hello/Hi} # 输出:Hi, World! World!

# 替换结尾匹配
echo ${str/%World!/Bash} # 输出:Hello, World! Bash

删除子字符串

str="Hello, World!"

# 删除最短匹配的前缀
echo ${str#Hello, } # 输出:World!

# 删除最长匹配的前缀
path="/usr/local/bin/script.sh"
echo ${path#*/} # 输出:usr/local/bin/script.sh
echo ${path##*/} # 输出:script.sh

# 删除最短匹配的后缀
echo ${str%World!} # 输出:Hello,

# 删除最长匹配的后缀
echo ${path%/*} # 输出:/usr/local/bin
echo ${path%%/*} # 输出:(空)

大小写转换

str="Hello, World!"

# 转换为小写
echo ${str,,} # 输出:hello, world!

# 转换为大写
echo ${str^^} # 输出:HELLO, WORLD!

# 首字母大写
echo ${str^} # 输出:Hello, world!

# 首字母小写
str2="HELLO"
echo ${str2,} # 输出:hELLO

数组

Bash 支持两种类型的数组:索引数组(以数字为索引)和关联数组(以字符串为索引)。

索引数组

创建数组

# 方式一:使用括号
arr=(one two three four)

# 方式二:逐个赋值
arr[0]=one
arr[1]=two
arr[2]=three

# 方式三:使用 declare
declare -a arr
arr=(one two three)

# 方式四:从命令输出创建
arr=($(ls))

访问数组元素

arr=(one two three four)

# 访问单个元素(索引从 0 开始)
echo ${arr[0]} # 输出:one
echo ${arr[2]} # 输出:three

# 访问所有元素
echo ${arr[@]} # 输出:one two three four
echo ${arr[*]} # 输出:one two three four

# 访问连续元素
echo ${arr[@]:1:2} # 输出:two three(从索引 1 开始,取 2 个)

获取数组信息

arr=(one two three four)

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

# 单个元素长度
echo ${#arr[0]} # 输出:3

# 获取所有索引
echo ${!arr[@]} # 输出:0 1 2 3

修改数组

arr=(one two three)

# 修改元素
arr[1]=TWO

# 添加元素
arr[3]=four
arr+=(five six) # 追加多个元素

# 删除元素
unset arr[1] # 删除索引 1 的元素
unset arr # 删除整个数组

遍历数组

arr=(one two three four)

# 方式一:遍历元素
for item in "${arr[@]}"; do
echo "Item: $item"
done

# 方式二:遍历索引
for i in "${!arr[@]}"; do
echo "Index $i: ${arr[$i]}"
done

# 方式三:C 风格遍历
for ((i=0; i<${#arr[@]}; i++)); do
echo "Index $i: ${arr[$i]}"
done

关联数组

关联数组使用字符串作为索引,类似于其他语言中的字典或哈希表。

创建关联数组

# 必须使用 declare -A 声明
declare -A person

# 赋值
person[name]="John"
person[age]=25
person[city]="New York"

# 或者一次性创建
declare -A person=([name]="John" [age]=25 [city]="New York")

访问关联数组

echo ${person[name]}      # 输出:John
echo ${person[age]} # 输出:25

# 获取所有键
echo ${!person[@]} # 输出:name age city

# 获取所有值
echo ${person[@]} # 输出:John 25 New York

# 获取元素个数
echo ${#person[@]} # 输出:3

遍历关联数组

declare -A person=([name]="John" [age]=25 [city]="New York")

for key in "${!person[@]}"; do
echo "$key: ${person[$key]}"
done

检查键是否存在

if [[ -v person[name] ]]; then
echo "Key 'name' exists"
fi

# 或者
if [[ ${person[name]+x} ]]; then
echo "Key 'name' exists"
fi

特殊变量

Shell 提供了一系列特殊变量,用于获取脚本和命令的各种信息。

位置参数

位置参数是传递给脚本或函数的参数。

#!/bin/bash
# 文件名:script.sh

echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "All arguments: $*"
echo "Number of arguments: $#"

执行 ./script.sh arg1 arg2 arg3 输出:

Script name: ./script.sh
First argument: arg1
Second argument: arg2
All arguments: arg1 arg2 arg3
All arguments: arg1 arg2 arg3
Number of arguments: 3

$@$* 的区别

当使用双引号包围时,两者有重要区别:

#!/bin/bash
# 测试 "$@" 和 "$*" 的区别

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

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

执行 ./test.sh "arg1 arg2" arg3 输出:

Using "$@"
Arg: arg1 arg2
Arg: arg3
Using "$*"
Arg: arg1 arg2 arg3

"$@" 保留每个参数的独立性,而 "$*" 将所有参数合并为一个字符串。

shift 命令

shift 命令将位置参数向左移动,$2 变成 $1$3 变成 $2,以此类推:

#!/bin/bash

while [ $# -gt 0 ]; do
echo "Processing: $1"
shift
done

执行 ./script.sh a b c 输出:

Processing: a
Processing: b
Processing: c

其他特殊变量

变量含义
$?上一个命令的退出状态码
$$当前 Shell 进程的 PID
$!最后一个后台命令的 PID
$-当前 Shell 的选项标志
$_上一个命令的最后一个参数
$RANDOM0-32767 之间的随机数
$LINENO当前行号
$BASH_VERSIONBash 版本
$BASH_SOURCE当前脚本文件名
$FUNCNAME当前函数名
#!/bin/bash

echo "Current PID: $$"
echo "Random number: $RANDOM"
echo "Bash version: $BASH_VERSION"

# 后台执行命令
sleep 100 &
echo "Background PID: $!"

# 检查上一个命令的退出状态
ls /nonexistent 2>/dev/null
echo "Exit status: $?"

环境变量

环境变量是在 Shell 进程中定义的变量,可以被子进程继承。

查看环境变量

# 查看所有环境变量
env
# 或
printenv

# 查看特定环境变量
echo $HOME
echo $PATH
echo $USER

# 使用 printenv
printenv HOME
printenv PATH

常用环境变量

变量含义
HOME用户主目录
PATH命令搜索路径
USER当前用户名
PWD当前工作目录
SHELL当前 Shell
LANG系统语言设置
TERM终端类型
EDITOR默认编辑器
PS1主命令提示符
PS2次命令提示符(续行)

设置环境变量

临时设置(当前 Shell)

export MY_VAR="Hello"
echo $MY_VAR

在脚本中设置

#!/bin/bash
export MY_VAR="Hello"
./child_script.sh # 子脚本可以访问 MY_VAR

在配置文件中设置

将环境变量添加到 ~/.bashrc~/.bash_profile

# 在 ~/.bashrc 中添加
export MY_VAR="Hello"
export PATH="$HOME/bin:$PATH"

然后执行 source ~/.bashrc 使其生效。

export 命令

export 命令将变量导出为环境变量,使其能被子进程继承:

# 定义普通变量
var1="Hello"

# 定义并导出环境变量
export var2="World"

# 先定义后导出
var3="Test"
export var3

# 查看导出的变量
export -p

删除环境变量

unset MY_VAR

变量作用域

理解变量作用域对于编写复杂脚本非常重要。

全局变量

在脚本中定义的变量默认是全局的,在整个脚本中都有效:

#!/bin/bash

var="global"

func() {
echo "In function: $var"
}

func
echo "Outside function: $var"

局部变量

使用 local 关键字在函数中定义局部变量:

#!/bin/bash

var="global"

func() {
local var="local"
echo "In function: $var"
}

func
echo "Outside function: $var"

输出:

In function: local
Outside function: global

子 Shell 中的变量

子 Shell 会继承父 Shell 的变量,但子 Shell 中的修改不会影响父 Shell:

#!/bin/bash

var="parent"

(
var="child"
echo "In subshell: $var"
)

echo "After subshell: $var"

输出:

In subshell: child
After subshell: parent

参数扩展高级用法

参数扩展是 Shell 中处理变量的强大工具。

默认值

# 如果变量未定义或为空,使用默认值
echo ${var:-default} # 如果 var 未定义,输出 default

# 如果变量未定义或为空,赋默认值并返回
echo ${var:=default} # 如果 var 未定义,var 被设为 default 并输出

# 如果变量未定义或为空,显示错误信息
echo ${var:?Error: var is not set} # 如果 var 未定义,输出错误并退出

# 如果变量已定义且非空,使用替代值
echo ${var:+alternative} # 如果 var 已定义,输出 alternative

字符串操作总结

str="Hello, World!"

# 长度
${#str} # 13

# 子字符串
${str:7} # World!
${str:0:5} # Hello
${str: -6} # World!

# 删除前缀
${str#Hello, } # World!
${str##*, } # World!

# 删除后缀
${str%World!} # Hello,
${str%%,*} # Hello

# 替换
${str/World/Shell} # Hello, Shell!
${str//o/0} # Hell0, W0rld!

# 大小写
${str^^} # HELLO, WORLD!
${str,,} # hello, world!

变量间接引用

使用 ${!var} 进行间接引用:

name="John"
var="name"
echo ${!var} # 输出:John

变量名前缀匹配

# 获取以特定前缀开头的所有变量名
prefix="BASH"
echo ${!prefix*} # 输出所有以 BASH 开头的变量名
echo ${!prefix@} # 同上

declare 命令

declare 命令用于声明变量并设置属性。

常用选项

选项含义
-a声明数组
-A声明关联数组
-i声明整数
-r声明只读
-x导出为环境变量
-l转换为小写
-u转换为大写
-n声明名称引用

示例

# 声明整数
declare -i num=10
num="hello" # 自动转换为 0
echo $num # 输出:0

# 声明只读变量
declare -r PI=3.14159

# 声明数组
declare -a arr=(1 2 3)

# 声明关联数组
declare -A dict=([key1]="value1" [key2]="value2")

# 声明并导出
declare -x MY_VAR="Hello"

# 自动转小写
declare -l lower="HELLO"
echo $lower # 输出:hello

# 自动转大写
declare -u upper="hello"
echo $upper # 输出:HELLO

# 名称引用
var="target"
declare -n ref=var
ref="modified"
echo $var # 输出:modified

小结

本章详细介绍了 Shell 变量和数据类型:

  • 变量定义:等号两边不能有空格,使用 $ 引用变量
  • 数据类型:Shell 变量本质是字符串,但可以声明为整数或数组
  • 字符串操作:丰富的参数扩展语法支持字符串截取、替换、删除
  • 数组:支持索引数组和关联数组
  • 特殊变量:位置参数、退出状态、进程 ID 等系统信息
  • 环境变量:使用 export 导出,被子进程继承
  • 变量作用域:全局变量、局部变量、子 Shell 变量
  • 参数扩展:默认值、字符串操作、间接引用等高级用法

掌握变量操作是编写复杂 Shell 脚本的基础。下一章将学习流程控制,包括条件判断和循环语句。