目录
[二、环境变量 vs 局部变量:作用域的秘密](#二、环境变量 vs 局部变量:作用域的秘密)
[2.1 用实验理解差别](#2.1 用实验理解差别)
[2.2 什么时候用哪种?](#2.2 什么时候用哪种?)
[2.3 查看当前所有环境变量](#2.3 查看当前所有环境变量)
[2.4 持久化环境变量](#2.4 持久化环境变量)
[3.1 readonly:让变量变成"常量"](#3.1 readonly:让变量变成“常量”)
[3.2 什么时候用只读变量?](#3.2 什么时候用只读变量?)
[3.3 查看所有只读变量](#3.3 查看所有只读变量)
[4.1 字符串拼接](#4.1 字符串拼接)
[4.2 获取字符串长度](#4.2 获取字符串长度)
[4.3 截取子字符串](#4.3 截取子字符串)
[4.4 从首尾删除模式匹配](#4.4 从首尾删除模式匹配)
[4.5 字符串替换](#4.5 字符串替换)
[4.6 其他常用判断](#4.6 其他常用判断)
[5.1 整数运算:(()) 和 []](#5.1 整数运算:(()) 和 [])
[5.2 常用的数学场景](#5.2 常用的数学场景)
一、引言:变量不只是"存个值"
写脚本久了你会发现,真正花时间的不是"怎么定义变量",而是怎么处理变量里的内容:
-
从日志文件中提取出IP地址
-
批量修改文件名后缀(
.jpg→.png) -
检查路径末尾有没有多余的斜杠
-
拼接出带时间戳的备份文件名
Shell是一种"弱类型"语言------变量不需要声明类型,同一个变量可以存数字、存字符串、甚至存命令输出。这种灵活性是便利也是坑:你必须清楚知道"现在这个变量里到底装着什么"。今天的目标就是掌握这些处理技巧。
二、环境变量 vs 局部变量:作用域的秘密
2.1 用实验理解差别
先做一个小实验。打开两个终端,在第一个终端中:
bash
# 终端1
my_var="hello"
echo $my_var # 输出:hello
bash # 启动一个子Shell
echo $my_var # 输出:空!(子Shell访问不到父Shell的局部变量)
exit # 退出子Shell
现在改用export:
bash
# 终端1
export my_var="hello"
bash # 启动一个子Shell
echo $my_var # 输出:hello(子Shell能访问到环境变量了)
exit
这个实验揭示了局部变量 和环境变量的核心区别:前者只在当前Shell进程内可见,后者会传递给所有子进程。
2.2 什么时候用哪种?
局部变量(默认):
bash
# 脚本内部的临时变量,用局部变量
temp_file="/tmp/backup_$(date +%s).tar.gz"
counter=0
环境变量(需要export):
bash
# 需要让子进程(如Python脚本、其他Shell)读取的配置
export DATABASE_URL="mysql://localhost:3306/mydb"
export APP_ENV="production"
2.3 查看当前所有环境变量
bash
env # 查看所有环境变量
printenv # 同上
printenv HOME # 查看特定环境变量
常见预定义环境变量:
| 变量 | 含义 | 示例值 |
|---|---|---|
$HOME |
当前用户家目录 | /home/zhangsan |
$PATH |
命令搜索路径 | /usr/local/bin:/usr/bin:/bin |
$USER |
当前用户名 | zhangsan |
$PWD |
当前工作目录 | /home/zhangsan/project |
$SHELL |
当前Shell | /bin/bash |
$OLDPWD |
上一个工作目录 | /tmp |
$RANDOM |
0-32767的随机数 | 12345 |
$$ |
当前Shell进程的PID | 5678 |
2.4 持久化环境变量
在脚本中用export定义的环境变量只在当前会话生效。要让环境变量永久生效,需要写入Shell配置文件:
bash
# 添加到 ~/.bashrc(每个交互式Shell都会加载)
vim ~/.bashrc
export MY_PROJECT_HOME="/opt/myproject"
# 让配置立即生效
source ~/.bashrc
注意 :
~/.bashrc每次打开终端都会执行,~/.bash_profile只执行一次。一般把环境变量定义在~/.bashrc中,然后在~/.bash_profile里source它。
三、只读变量与常量保护
3.1 readonly:让变量变成"常量"
bash
#!/bin/bash
readonly PI=3.14159
PI=3.14 # 报错:PI: readonly variable
3.2 什么时候用只读变量?
bash
#!/bin/bash
# 配置文件路径——不希望在脚本中被意外修改
readonly CONFIG_FILE="/etc/myapp/config.yaml"
readonly LOG_DIR="/var/log/myapp"
readonly MAX_RETRIES=3
# 后续代码中 CONFIG_FILE 绝不会被意外覆盖
3.3 查看所有只读变量
bash
readonly # 列出所有只读变量(包括系统预定义的)
readonly -p # 同上
你会发现$HOME、$USER等也是只读的------系统不允许你修改这些关键信息。
四、字符串处理:Shell脚本的核心技艺
Shell中一切都是字符串。你输入的IP、文件名、路径、日志内容------全是字符串。掌握字符串处理,就掌握了Shell脚本的半壁江山。
4.1 字符串拼接
Shell的拼接非常直接------把变量挨着写就行:
bash
name="zhangsan"
greeting="Hello, ${name}!"
echo $greeting # Hello, zhangsan!
# 多个变量拼接
src="/var/log"
app="nginx"
filename="error.log"
full_path="${src}/${app}/${filename}"
echo $full_path # /var/log/nginx/error.log
避免歧义的拼接:
bash
prefix="super"
echo "${prefix}man" # superman(推荐)
echo "$prefix"man # superman(也可,但不够清晰)
echo $prefix"man" # superman
echo $prefix man # super man(空格被保留了,不是拼接!)
4.2 获取字符串长度
bash
str="Hello World"
echo ${#str} # 11
实用场景:判断用户输入是否为空:
bash
if [ ${#input} -eq 0 ]; then
echo "输入不能为空!"
exit 1
fi
4.3 截取子字符串
格式:${变量:起始位置:长度}(起始位置从0开始)
bash
str="Hello World"
echo ${str:0:5} # Hello (位置0开始,取5个字符)
echo ${str:6} # World (位置6开始,取到末尾)
echo ${str:6:3} # Wor (位置6开始,取3个)
echo ${str: -5} # World (从末尾倒数5个,注意冒号后有空格)
实用场景:
bash
# 从日期时间戳中提取年月日
timestamp="20260426_153000"
date_part=${timestamp:0:8} # 20260426
time_part=${timestamp:9} # 153000
# 去掉文件扩展名
filename="report.tar.gz"
base=${filename:0:10} # 依赖硬编码位置,不够通用
# 更好的方法见下一节的模式匹配
4.4 从首尾删除模式匹配
这是Shell字符串处理中最强大的功能,有四种模式:
| 语法 | 含义 | 记忆 |
|---|---|---|
${var#pattern} |
从开头 删除最短匹配 | #像一个井号,开头的标记 |
${var##pattern} |
从开头 删除最长匹配 | ##多删 |
${var%pattern} |
从末尾 删除最短匹配 | %像百分号,结尾的标记 |
${var%%pattern} |
从末尾 删除最长匹配 | %%多删 |
实战:处理文件路径
bash
path="/home/zhangsan/project/report.tar.gz"
# 获取文件名(删除最后一个/之前的所有内容)
filename=${path##*/}
echo $filename # report.tar.gz
# 获取目录名(删除文件名部分)
dirname=${path%/*}
echo $dirname # /home/zhangsan/project
# 获取文件扩展名(删除最后一个.之前的所有内容)
ext=${path##*.}
echo $ext # gz(注意不是 tar.gz)
# 获取不带扩展名的文件名
base=${filename%%.*}
echo $base # report(删除了 .tar.gz)
实战:批量处理日志文件
bash
# 把所有 .log 文件重命名为 .log.bak
for file in *.log; do
base=${file%.log} # 去掉 .log 后缀
mv "$file" "${base}.log.bak"
done
4.5 字符串替换
格式:${变量/旧内容/新内容}
bash
url="http://example.com/index.html"
# 替换第一个匹配
echo ${url/http/https} # https://example.com/index.html
# 替换所有匹配(加一个/)
path="/usr/local/bin:/usr/bin:/bin"
echo ${path//\/usr/\/opt} # /opt/local/bin:/opt/bin:/bin
单斜杠与双斜杠的区别:
bash
text="aaaa"
echo ${text/a/A} # Aaaa(只替换第一个)
echo ${text//a/A} # AAAA(替换所有)
4.6 其他常用判断
| 语法 | 含义 | 示例 |
|---|---|---|
${var:-默认值} |
如果var为空,返回默认值 | ${user:-root} |
${var:=默认值} |
如果var为空,赋值为默认值并返回 | ${count:=1} |
${var:+替代值} |
如果var不为空,返回替代值 | ${debug:+verbose} |
${var:?错误信息} |
如果var为空,打印错误并退出 | ${config:?未设置} |
bash
#!/bin/bash
# 使用默认值,避免变量未定义
DB_HOST=${DB_HOST:-"localhost"}
DB_PORT=${DB_PORT:-3306}
echo "连接数据库 ${DB_HOST}:${DB_PORT}"
五、数字运算:Shell的"计算器"
Shell默认把变量值当字符串处理,要做数学运算需要特殊语法。
5.1 整数运算:(()) 和 []
bash
a=10
b=3
echo $((a + b)) # 13
echo $((a - b)) # 7
echo $((a * b)) # 30
echo $((a / b)) # 3(整数除法,向下取整)
echo $((a % b)) # 1(取余)
echo $((a ** b)) # 1000(指数)
也可以写成:
bash
echo $[a + b] # 老式写法,仍然可用但不推荐
let "c = a + b" # 使用let命令
5.2 常用的数学场景
bash
# 计数器(循环中常用)
count=0
count=$((count + 1))
# 计算百分比
total=100
used=35
percent=$((used * 100 / total)) # 35
echo "使用率:${percent}%"
# 随机数生成
random_num=$((RANDOM % 100)) # 0-99的随机数
echo $random_num
六、综合实战:Nginx日志解析器
把今天学的字符串处理技巧整合到一个实用脚本中:
bash
#!/bin/bash
# 练习脚本:简单Nginx访问日志解析
# 模拟一条Nginx日志(实际使用时应从文件读取)
log_line='192.168.1.100 - - [26/Apr/2026:15:30:00 +0800] "GET /index.html HTTP/1.1" 200 1024 "http://example.com" "Mozilla/5.0"'
echo "原始日志:"
echo "${log_line}"
echo ""
# 1. 提取IP地址(第一个字段)
ip=${log_line%% *}
echo "IP地址:${ip}"
# 2. 提取请求时间([]之间的内容)
# 先删除 [ 之前的所有内容
time_bracket=${log_line#*[}
# 再删除 ] 之后的所有内容
time=${time_bracket%%]*}
echo "请求时间:${time}"
# 3. 提取请求方法和路径(引号之间的内容)
request_part=${log_line#*\"}
request=${request_part%%\"*}
echo "请求内容:${request}"
# 4. 提取HTTP状态码(请求行后面的三个数字)
# 先找到"开始的第一次出现,去掉它
after_request=${log_line#*\"}
# 再去掉中间的 HTTP版本"部分
status_part=${after_request#*\" }
# 取第一个字段(状态码)
status_code=${status_part%% *}
echo "状态码:${status_code}"
# 5. 判断状态码类型
if [ ${#status_code} -eq 3 ]; then
case ${status_code:0:1} in
2) echo " → 请求成功" ;;
3) echo " → 重定向" ;;
4) echo " → 客户端错误" ;;
5) echo " → 服务器错误" ;;
esac
fi
# 6. 统计:请求路径中包含home的请求次数(实际使用时应循环处理所有行)
path_part=${request#* }
path=${path_part%% *}
if [ "${path:0:5}" = "/home" ]; then
echo " → 这是首页请求"
fi
该练习展示了:日志提取字段 + 字符串截取 + 长度判断 + 条件分支的组合使用。实际生产场景中只需把输入改为while read line; do ... done < access.log循环即可。
七、本篇小结
环境变量 vs 局部变量:
-
局部变量只在当前Shell内可见
-
export让变量传递给所有子进程 -
永久生效写到
~/.bashrc
只读变量:
-
readonly定义不可修改的常量 -
适用于配置文件路径、限制参数等
字符串处理核心技巧:
| 操作 | 语法 | 示例 |
|---|---|---|
| 长度 | ${#var} |
echo ${#name} |
| 子串截取 | ${var:pos:len} |
${str:0:5} |
| 开头删最短 | ${var#pattern} |
${path#*/} |
| 开头删最长 | ${var##pattern} |
${path##*/} |
| 结尾删最短 | ${var%pattern} |
${path%.*} |
| 结尾删最长 | ${var%%pattern} |
${path%%.*} |
| 替换首个 | ${var/old/new} |
${url/http/https} |
| 替换所有 | ${var//old/new} |
${text//a/A} |
| 默认值 | ${var:-默认值} |
${user:-root} |
记忆口诀:
#从左删,%从右删;一个删最短,两个删最长;
/替换,//全换;:-给默认,:=设默认。
动手练习
bash
#!/bin/bash
# 练习:文件路径处理
# 给定文件路径
file="/var/log/nginx/access.log"
echo "原始路径:${file}"
# 1. 提取文件名
echo "文件名:${file##*/}"
# 2. 提取目录路径
echo "目录:${file%/*}"
# 3. 提取不带扩展名的文件名
basename="${file##*/}"
echo "文件名(无扩展名):${basename%.*}"
# 4. 修改扩展名为 .bak
echo "备份文件名:${file%.log}.bak"
# 5. 练习字符串默认值
read -p "请输入你的名字(直接回车使用默认值):" name
echo "你好,${name:-访客}!"
八、下篇预告
掌握了字符串和变量,脚本还缺少一项关键能力:判断。没有判断的脚本只能顺序执行,遇到不同情况无法做出不同反应。
下一篇我们将进入条件判断的世界,学习:
-
test命令与[ ]、[[ ]]的用法和区别 -
文件测试(是否存在、是否是目录、是否可写)
-
字符串比较与数字比较
这是构建脚本逻辑能力的第一个台阶。学会条件判断,你的脚本就拥有了"大脑"。
延伸思考 :有些运维脚本里你会看到${var:=default}这种写法。它和${var:-default}有什么区别?提示:前者不仅返回默认值,还会把默认值赋给变量。试试看,这是很多生产环境脚本用来自动初始化配置变量的技巧。