八、Linux Shell 脚本:变量与字符串
作者:IvanCodes
日期:2025年8月3日
专栏:Linux教程
在Shell脚本编程中,变量是存储和操作数据的基石。理解如何定义、使用、传递变量以及如何处理字符串,是编写高效、灵活脚本的第一步,也是最关键的一步。
思维导图
一、变量的定义与使用
1.1 定义变量
- 基本格式:
variable_name=value
- 关键点:赋值号
=
的两边绝对不能有空格!这是初学者最常犯的错误之一,务必留意! - 命名规则: 变量名通常由字母、数字、下划线构成,且不能以数字开头。习惯上,全大写用于环境变量或常量,小写或驼峰式用于本地变量。
- 值的“类型”: Shell 不严格区分数据类型,几乎所有值都可视为字符串。即使是数字,在进行算术运算前,本质上也是字符串。
引号的使用规则:
如果值不包含空格或特殊字符,可以省略引号:
myvar=hello
如果值包含空格,必须使用单引号' '
或双引号" "
包围:message='Hello World'
单引号 (’ '):强引用或“字面”引用。其内部所有内容都按原样处理,变量不会被解析,特殊字符也失去其特殊含义。
双引号 (" "):弱引用或“解析”引用。其内部的变量引用 ($var
) 会被替换为变量的值,某些特殊字符 (如$
、\
、`
) 依然有效。
# 正确的变量定义
name="Alice"
age=30
city="Beijing"
greeting1='你好, ${name}!' # 单引号内 ${name} 不会被替换
greeting2="你好, ${name}!" # 双引号内 ${name} 会被替换成 Alice# 错误的变量定义 (等号两边有空格)
# wrong_var = "error"
1.2 引用变量
要获取或使用变量的值,在变量名前加上 $ 符号。
- 基础用法:
$variable_name
- 推荐用法:
${variable_name}
(使用花括号{}
包围)
为什么推荐使用 ${}
?
明确边界: 当变量名后紧跟其他字符时,花括号能清晰地界定变量名的范围,避免歧义。例如,
${user}_id
可以正确解析,而$user_id
会试图查找一个名为user_id
的变量。
高级功能: 所有高级字符串操作 (如截取、替换) 都必须在花括号内进行。
#!/bin/bash
user="Bob"
action="studying"
echo "用户是: ${user}"
echo "${user}正在${action}Shell。"
1.3 只读变量与删除变量
- 只读变量 (
readonly
):
使用readonly
命令可以将一个变量锁定,使其变为只读。一旦设置,其值不能被修改,也无法被unset
删除。非常适合定义脚本中的常量。
#!/bin/bash
readonly APP_VERSION="1.0.2"
echo "当前应用版本: ${APP_VERSION}"# 尝试修改将导致错误
# APP_VERSION="1.0.3"# 尝试删除也将导致错误
# unset APP_VERSION
- 删除变量 (
unset
):
使用unset
命令可以彻底删除一个变量 (包括其名称和值)。注意:readonly
的变量无法被删除。
#!/bin/bash
tmp_dir="/tmp/my_app_temp"
echo "临时目录: ${tmp_dir}"unset tmp_direcho "删除后的临时目录: ${tmp_dir}" # 此处将输出空行
二、变量的作用域
变量的有效范围 (作用域) 是脚本编程中的核心概念
2.1 本地变量
- 定义方式: 默认通过
variable_name=value
定义的都是本地变量。 - 作用范围: 仅在创建它的当前 Shell 进程中有效。
- 继承性: 无法被当前 Shell 启动的子进程 (如执行另一个脚本) 自动继承。
#!/bin/bash
# 定义本地变量
my_secret="这是我的小秘密"
echo "父脚本中的秘密: ${my_secret}"# 启动一个子Shell
bash -c 'echo "子Shell中能看到秘密吗? ${my_secret}"' # 此处 ${my_secret} 为空
2.2 环境变量 与 export
命令
- 作用范围: 在当前 Shell 及其启动的所有子进程中都有效。
- 继承性: 可以被子进程继承。
- export 命令: 该命令用于将一个本地变量 “发布” 或 “导出” 为环境变量,使其能够被子进程访问。
export
的两种用法:
- 先定义,后导出:
my_local_var="初始值"
export my_local_var
- 定义并同时导出 (更常用):
export SHARED_CONFIG_PATH="/etc/my_app/config"
- 验证继承性:
#!/bin/bash
local_info="只在父脚本可见"
export shared_info="父子都能看到"echo "父脚本中的本地信息: ${local_info}"
echo "父脚本中的共享信息: ${shared_info}"# 启动子Shell来验证
bash -c 'echo "子Shell中的本地信息: ${local_info}"; echo "子Shell中的共享信息: ${shared_info}"'
# 输出显示:子Shell中local_info为空,但shared_info有值!
- 查看环境变量: 使用
env
或printenv
命令可以列出当前所有的环境变量。
2.3 让环境变量“永久生效”
通过 export
设置的环境变量是临时的,随当前 Shell 会话关闭而失效。要使其永久生效,需要将其写入Shell的配置文件。
常用配置文件 (Bash):
~/.bashrc
: 常用。每次打开新的交互式终端时加载。适合个人常用的环境变量。
~/.bash_profile
: 用户登录时加载一次。
/etc/profile
: 最常用。系统全局配置,影响所有用户。
配置步骤:
1. 用文本编辑器打开配置文件 (如 vim /etc/profile
)
2. 在文件末尾添加 export
命令:
export JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"
export PATH=$PATH:$JAVA_HOME/bin
3.保存并退出
4. 使配置立即生效,执行 source /etc/profile
或重新打开一个终端
2.4 作用域总结
特性 | 本地变量 | 环境变量 |
---|---|---|
定义方式 | variable_name=value (默认) | 使用 export 命令 (如 export var=val ) |
作用域 | 仅限当前 Shell 进程 | 当前 Shell 进程 及 其所有子进程 |
继承性 | 不被 子进程继承 | 可被 子进程继承 |
持久性 | 临时,随 Shell 关闭消失 | 临时(若仅 export ),可配置为永久(修改配置文件) |
关键命令 | (无) | export , env , printenv |
用途 | 脚本内部临时存储、计数器 | PATH 设置、跨脚本共享配置、系统级参数 |
三、预定义与特殊变量
Shell 预先定义了一些特殊变量,它们在特定上下文中有固定含义。
变量 | 描述 |
---|---|
$0 | 当前脚本的名称 |
$1 - $9 | 第一个到第九个位置参数 |
$# | 传递给脚本的参数总个数 |
$@ | 所有位置参数列表,加引号 "$@" 后每个参数为独立字符串 |
$* | 所有位置参数列表,加引号 "$*" 后所有参数合并为单个字符串 |
$$ | 当前脚本的进程ID (PID) |
$! | 最近一个放入后台的作业的进程ID (PID) |
$? | 上一个命令的退出状态码 (0表示成功,非0表示失败) |
$_ | 上一个命令的最后一个参数 |
$IFS | 内部字段分隔符 (默认为空格、制表符、换行符) |
- 位置参数变量:
#!/bin/bash
# 文件名: show_params.sh
echo "脚本名 (\$0): $0"
echo "第一个参数 (\$1): $1"
echo "第二个参数 (\$2): $2"
# 执行: ./show_params.sh apple banana
- 特殊含义变量:
#!/bin/bash
# 文件名: special_vars.sh
echo "收到参数个数 (\$#): $#"
ls /no_such_dir
echo "上个命令的退出状态码 (\$?): $?"
echo "本脚本的进程号 (\$\$): $$"
四、字符串操作
Shell 提供了内置的、强大的字符串处理能力。
4.1 获取字符串长度
使用 ${#variable_name}
语法。
#!/bin/bash
sentence="学习 Shell 很有趣!"
len=${#sentence}
echo "字符串 '${sentence}' 的长度是: ${len}"
4.2 提取子字符串
使用 ${variable_name:offset:length}
语法。
#!/bin/bash
full_url="https://example.com/products/item123"
protocol=${full_url:0:5}
echo "协议是: ${protocol}"
product_id=${full_url:26}
echo "产品ID是: ${product_id}"
4.3 字符串替换
${variable/pattern/replacement}
: 只替换第一个匹配。${variable//pattern/replacement}
: 替换所有匹配。
#!/bin/bash
raw_text="path is /home/user/data dir"
fixed_text=${raw_text/path is/directory is}
echo "修正后的文本: ${fixed_text}"
no_spaces=${raw_text// /_}
echo "无空格版本: ${no_spaces}"
4.4 字符串删除 (裁剪)
${variable#pattern}
: 从开头删除最短匹配。${variable##pattern}
: 从开头删除最长匹配。${variable%pattern}
: 从结尾删除最短匹配。${variable%%pattern}
: 从结尾删除最长匹配。
pattern
可以使用*
通配符。
#!/bin/bash
filepath="/var/log/httpd/access.log"
# 从开头删除最短的 */
filename=${filepath#*/} # var/log/httpd/access.log
# 从开头删除最长的 */
basename=${filepath##*/} # access.log
# 从结尾删除最短的 .*
name_no_ext=${basename%.*} # access
# 从结尾删除最长的 /
dirname=${filepath%/*} # /var/log/httpd
echo "文件名: ${basename}"
echo "目录名: ${dirname}"
echo "无后缀名: ${name_no_ext}"
练习题
题目:
- 以下哪个 Shell 变量定义是错误的,为什么?
A.my_var=hello
B._value="some text"
C.count = 10
D.message='Error code: $?'
- 假设有变量
filename="data.csv"
,如何安全地将其与字符串_backup
拼接成data.csv_backup
? - 本地变量和环境变量在被子进程继承方面有什么关键区别?哪个命令用于将本地变量变为环境变量?
- 一个脚本
run.sh
被这样调用:./run.sh first "second arg" third
。在run.sh
内部,变量$#
的值是什么? - 执行一个不存在的命令
non_exist_cmd
后,$?
的值通常是什么 (一个非零值)? - 当脚本的参数是
"文件 1.txt"
和"文件 2.txt"
时,for i in "$*"
和for i in "$@"
遍历的结果有何不同? - 如何获取变量
address="北京市海淀区"
的长度? - 给定
version_str="app-1.0.5-release"
,如何提取出中间的版本数字1.0.5
? - 如何将字符串
path_var="/usr/bin:/usr/local/bin:/bin"
中所有的冒号:
替换为空格 readonly my_const="cannot_change"
执行后,再执行unset my_const
会发生什么?my_var="value"
和unset my_var
之后,echo ${my_var}
的输出有何不同?- 如何将一个名为
API_TOKEN
的本地变量传递给一个用python my_script.py
启动的子进程? echo $$
命令会输出什么?- 给定
full_path="/home/user/documents/report.docx"
,如何只提取出文件名report.docx
? - 给定
full_path="/home/user/documents/report.docx"
,如何只提取出目录路径/home/user/documents
?
答案与解析:
- 答案: C.
count = 10
是错误的。
解析: Shell 变量赋值时,等号=
两边绝对不能有空格。正确写法应为count=10
。 - 答案:
${filename}_backup
解析: 推荐使用花括号{}
来明确界定变量名,防止 Shell 将_backup
视为变量名的一部分。 - 答案: 本地变量 不会被子进程继承,而环境变量 可以被子进程继承。
export
命令用于将本地变量变为环境变量。 - 答案:
$#
的值是3
。
解析:"second arg"
因为被双引号包围,被视为一个独立的、完整的参数。 - 答案:
$?
的值会是一个非零值 (通常是127,表示 “command not found”)。
解析:$?
记录上一个命令的退出状态码。0
代表成功,任何非零值都表示某种形式的失败。 - 答案:
for i in "$*"
: 循环只执行一次,变量i
的值是整个字符串"文件 1.txt 文件 2.txt"
。
for i in "$@"
: 循环会执行两次。第一次i
是"文件 1.txt"
,第二次i
是"文件 2.txt"
。"$@"
更适合逐个处理带空格的参数。 - 答案:
${#address}
- 解析:
${#variable}
是获取字符串长度的标准语法。
- 解析:
- 答案:
${version_str:4:5}
解析: 从索引4 (第5个字符) 开始,提取5个字符。 - 答案:
${path_var//:/ }
解析: 双斜杠//
表示全局替换 (替换所有匹配项)。 - 答案: 会报错,提示变量是只读的,无法被
unset
。
解析:readonly
属性保护变量不被修改或删除。 - 答案:
my_var="value"
后echo
输出value
。unset my_var
后echo
输出一个空行。
解析:unset
彻底移除了变量,而不仅仅是将其值设为空。 - 答案:
export API_TOKEN="your_token_here"
python my_script.py
- 解析: 必须先使用
export
将API_TOKEN
提升为环境变量,这样python
这个子进程才能继承并访问它。
- 答案: 会输出当前正在执行
echo
命令的那个 Shell 进程的进程ID (PID)。 - 答案:
${full_path##*/}
解析:##*/
表示从字符串开头 (##
) 删除最长匹配*/
(任何字符直到最后一个斜杠) 的部分。 - 答案:
${full_path%/*}
解析:%/*
表示从字符串结尾 (%
) 删除最短匹配/*
(一个斜杠及后面的所有字符) 的部分。