在Shell世界里,字符串不是“小事”——它几乎决定了脚本的成败。
无论是数据库连接、日志处理,还是配置文件解析,字符串表达式都是最被低估却最常出错的环节。
本文带你全面掌握Shell中的字符串操作,从-z、-n到${}参数扩展,再到正则与模式匹配,用最直观的示例拆解每个用法的正确姿势。
一文吃透Shell字符串处理的全部技能点,让你的脚本稳如磐石!
01
字符串测试与比较
条件测试是判断字符串状态的基础。
这一般在 if 语句中,通过 [] (单括号) 或 [[]] (双括号) 实现。
1
常用字符串测试操作符

注意
- 在 [] 中,= 和 == 是同义的。
- 在 [[]] 中,< 和 > 执行字典序比较。在 [] 中,它们是重定向操作符,不能用于字符串比较。
- 在 [] 中,必须对变量使用双引号,以防止因变量为空或包含空格而导致的解析错误。[[]] 则无此问题。
2
实验:字符串状态与比较
&&、|| 和 ; 等操作符根据前一个命令的退出码,来决定是否执行以及如何执行后一个命令。
02
控制操作符详解
场景
检查一个PostgreSQL数据库连接字符串和用户名的状态。
#!/bin/bash
DB_USER="postgres"
EMPTY_USER=""
CONN_STR_A="host=localhost port=5432"
CONN_STR_B="host=localhost port=5432"
echo "--- 字符串状态测试 ---"
if [[ -z $EMPTY_USER ]]; then
echo "变量 EMPTY_USER 为空。"
fi
if [[ -n $DB_USER ]]; then
echo "变量 DB_USER 不为空,值为: $DB_USER"
fi
echo -e "
--- 字符串相等性比较 ---"
if [[ "$CONN_STR_A" == "$CONN_STR_B" ]]; then
echo "连接字符串A和B相等。"
fi
echo -e "
--- 字典序比较(陷阱演示) ---"
VERSION_A="2.1.10"
VERSION_B="2.1.2"
if [[ "$VERSION_A" < "$VERSION_B" ]]; then
echo "字典序比较结果: '$VERSION_A' 小于 '$VERSION_B' (由于字符'1'小于'2')"
fi
解析
- -z 和 -n 是判断字符串是否为空的可靠方法。
- == 用于准确比较两个字符串的内容。
- 字典序比较的实验揭示了一个常见陷阱:Shell是逐字符比较,因此 “2.1.10” 会被判定为小于 “2.1.2”。这说明不能用简单的字符串比较来处理版本号。
03
字符串参数扩展
参数扩展是Shell中进行字符串操作的核心,它无需调用外部命令(如 sed, awk, cut),效率极高。
1
获取长度: ${#string}
描述: 返回字符串中的字符数。
示例:
FILENAME="backup_prod_db.sql.gz"
LEN=${#FILENAME}
echo "文件名 '$FILENAME' 的长度是: $LEN"
# 输出: 文件名 'backup_prod_db.sql.gz' 的长度是: 23
2
子字符串提取: ${string:offset:length}
描述: 从 offset 位置(从0开始)开始,提取 length 个字符。
如果省略 length,则提取到末尾。
示例: 从日志时间戳中提取日期和时间。
LOG_TIMESTAMP="2025-10-15T14:30:05"
DATE_PART=${LOG_TIMESTAMP:0:10} # 从第0位开始,取10个字符
TIME_PART=${LOG_TIMESTAMP:11} # 从第11位开始,取到末尾
echo "日期: $DATE_PART, 时间: $TIME_PART"
# 输出: 日期: 2025-10-15, 时间: 14:30:05
3
模式匹配与移除
这是处理文件名和路径最强劲的工具。
- ${string#pattern}
从开头删除最短匹配 pattern 的部分。
- ${string##pattern}
从开头删除最长匹配 pattern 的部分。
- ${string%pattern}
从结尾删除最短匹配 pattern 的部分。
- ${string%%pattern}
从结尾删除最长匹配 pattern 的部分。
示例: 解析一个完整的文件路径。
FULL_PATH="/var/lib/postgresql/data/base/16384/12345.fsm"
FILENAME=${FULL_PATH##*/}
echo "文件名: $FILENAME"
DIR_PATH=${FULL_PATH%/*}
echo "目录路径: $DIR_PATH"
BASENAME=${FILENAME%.*}
echo "文件基础名: $BASENAME"
EXTENSION=${FILENAME##*.}
echo "文件扩展名: $EXTENSION"
输出:
文件名: 12345.fsm
目录路径: /var/lib/postgresql/data/base/16384
文件基础名: 12345
文件扩展名: fsm
4
搜索与替换
- ${string/pattern/replacement}
将第一个匹配 pattern 的部分替换为 replacement。
- ${string//pattern/replacement}
将所有匹配 pattern 的部分替换为 replacement。
示例: 格式化一个主机列表。
HOST_LIST="db-node1 db-node2 db-node3"
CSV_HOSTS=${HOST_LIST// /,}
echo "CSV格式的主机列表: $CSV_HOSTS"
FIRST_HOST_RENAMED=${HOST_LIST/db-/database-}
echo "重命名第一个主机后: $FIRST_HOST_RENAMED"
输出:
CSV格式的主机列表: db-node1,db-node2,db-node3
重命名第一个主机后: database-node1 db-node2 db-node3
5
大小写转换 (Bash 4.0+)
- ${string^pattern}
将第一个匹配 pattern 的字符转为大写。
- ${string^^pattern}
将所有匹配 pattern 的字符转为大写。
- ${string,pattern}
将第一个匹配 pattern 的字符转为小写。
- ${string,,pattern}
将所有匹配 pattern 的字符转为小写。
如果省略 pattern,则作用于所有字符。
示例:
STATUS="running"
echo "状态 (大写): ${STATUS^^}"
# 输出: 状态 (大写): RUNNING
6
设置默认值
这对于编写能处理未定义或空变量的健壮脚本至关重大。

示例: 在脚本中处理可选和必需的配置。
#!/bin/bash
# PG_HOST 未被设置
# PG_USER 已被设置
PG_USER="admin"
# 1. 使用 :- 提供一个临时的默认值
# 用于连接命令,但不改变原始变量
echo "正在连接到主机: ${PG_HOST:-"localhost"}..."
# 2. 使用 := 设置一个持久的默认值
# 如果 LOG_LEVEL 未设置,则将其设置为 INFO
echo "原始日志级别: $LOG_LEVEL"
echo "当前日志级别: ${LOG_LEVEL:="INFO"}"
echo "设置后的日志级别: $LOG_LEVEL"
# 3. 使用 :? 强制要求变量必须存在
# 如果 DB_NAME 未设置,脚本将报错并退出
# DB_NAME="" # 撤销此行注释来触发错误
: "${DB_NAME:?"错误:必须提供数据库名称(DB_NAME)。"}"
echo "数据库名称: $DB_NAME" # 如果DB_NAME未设置,这行不会执行
# 4. 使用 :+ 根据变量是否存在来构造参数
# 如果 DEBUG_MODE 变量存在,则在命令后添加 --verbose 标志
DEBUG_MODE="true"
VERBOSE_FLAG="${DEBUG_MODE:+"--verbose"}"
echo "启动命令: my_app_runner ${VERBOSE_FLAG}"
输出 (假设 DB_NAME 被设置为 “testdb”):
正在连接到主机: localhost...
原始日志级别:
当前日志级别: INFO
设置后的日志级别: INFO
数据库名称: testdb
启动命令: my_app_runner --verbose
04
[[]] 中的高级字符串匹配
[[]] 关键字提供了 [] 所不具备的高级匹配功能。
1
通配符模式匹配 (Globbing)
在 [[]] 中,== 和 != 操作符的右侧若不加引号,会被视为一个通配符模式。
示例: 检查一个文件名是否为日志文件。
FILENAME="application-2025-10-15.log"
if [[ "$FILENAME" == *.log ]]; then
echo "'$FILENAME' 是一个日志文件。"
fi
解析
*.log 匹配任何以 .log 结尾的字符串。
2
正则表达式匹配 (=~)
=~ 操作符允许使用扩展正则表达式(ERE)进行匹配。
示例: 验证一个字符串是否为合法的IP地址格式。
IP_ADDR="192.168.1.10"
REGEX="^([0-9]{1,3}.){3}[0-9]{1,3}$"
if [[ "$IP_ADDR" =~ $REGEX ]]; then
echo "'$IP_ADDR' 格式符合IP地址规范。"
fi
注意
为了兼容性,正则表达式本身最好不要加引号。
05
最佳实践与注意事项
1
优先使用内置扩展
参数扩展(如 ${…})是Shell内置功能,其性能远高于调用外部命令如 sed, awk, cut。
在追求效率的脚本中应优先使用。
2
注意引号的使用
- 在 [] 中,对变量使用双引号是强制性的,以防止解析错误。
- 在 [[]] 中,变量一般无需加引号,但如果要进行准确的字面量比较而不是模式匹配,则应将 == 右侧的模式用引号括起来,例如 [[ “$VAR” == “*.log” ]]。
3
理解可移植性
[[]] 和许多高级参数扩展(如大小写转换)是Bash等现代Shell的特性,不被POSIX sh 支持。
如果脚本需要极高的可移植性,应使用 [] 和更基础的参数扩展。
4
区分 = 和 ==
在 [] 中,两者是同义词。
在 [[]] 中,两者也都用于比较,但 == 具有模式匹配的特殊含义,因此在 [[]] 中使用 == 更能体现其意图。
写在最后
字符串处理,是Shell脚本中最常见、也最容易埋坑的领域。
真正的高手,不是记住多少命令,而是能写出无惧空值、兼容多Shell、少一行命令也能跑通的代码。
如果你想让脚本更机智、更优雅,收藏这篇文章,实践每一个例子——你会发现,掌握字符串,才算真正掌控了Shell。
作者介绍

大家好,我是刘峰,安丫科技创始人 & 数据库技术高级讲师,专注于 PostgreSQL、国产数据库运维与迁移、数据库性能优化 等方向。
作为 PG中国分会官方授权讲师、PostgreSQL ACE 讲师认证专家,我长期活跃在一线项目实战中,拥有 10年以上大型数据库管理与优化经验,曾深度参与电信、金融、政务等多个行业的数据库性能调优与迁移项目。
欢迎关注我,一起深入探索数据库的无限可能,技术交流不设限!
觉得有收获的话,记得点赞、收藏、转发支持一下哦,别忘了关注我获取更多数据库干货~
安呀智数据坊|我们能做什么
无论你是业务系统的技术负责人,还是数据部门的第一响应人,我们都能为你提供可靠的支持:
- 数据库类型支持
Oracle / MySQL / PostgreSQL / PG / SQL Server 等主流数据库
- 核心服务内容
性能优化 / 故障处理 / 数据迁移 / 备份恢复 / 版本升级 / 补丁管理
- 系统性支持
深度巡检 / 高可用架构设计 / 应用层兼容评估 / 运维工具集成
- 专项能力补充
定制课程培训 / 甲方团队辅导 / 复杂问题协作排查 / 紧急救援支持
如果你有一张删不掉的表、一个跑不动的查询,或者一场说不清的升级风险,欢迎来找我们聊聊。
















暂无评论内容