在 Linux 系统运维、自动化脚本编写与日常文本处理中,字符串操作是出现频率最高、最为核心的技能之一。与专门的高级语言(如 Python)相比,Shell 环境下不仅有 Bash 自身极其轻量且原生的参数扩展语法,还紧密协同着 Linux 文本处理工具链(grep、sed、awk、tr、cut、expr 等)。
本文系统梳理 Shell 编程中字符串的属性校验、终端显示、数组存储、切片提取、模式替换与综合实战,助你构建完整的 Shell 字符串处理知识体系。
一、方案速查与核心语法矩阵
在日常开发中,优先使用 Bash 原生参数扩展(性能最高),复杂场景配合外部工具。下表整理了最常用的核心语法速查:
| 操作类型 | 原生语法 / 工具 | 典型示例 | 执行效果与说明 |
|---|---|---|---|
| 计算长度 | ${#var} |
${#var} |
极快,直接返回字符个数(代替 expr length) |
| 按位置切片 | ${var:offset:length} |
${var:0:5} |
提取从第 0 位开始的 5 个字符 |
| 反向切片 | ${var:(-offset)} |
${var:(-3)} |
提取末尾 3 个字符(负号需加括号或空格) |
| 从左最短剔除 | ${var#pattern} |
${var#*/} |
删除左侧第一个匹配项及之前的内容(非贪婪) |
| 从左最长剔除 | ${var##pattern} |
${var##*/} |
提取文件名:删除左侧最后一个匹配项之前的所有内容(贪婪) |
| 从右最短剔除 | ${var%pattern} |
${var%.*} |
剔除文件后缀:删除右侧第一个匹配项之后的内容 |
| 从右最长剔除 | ${var%%pattern} |
${var%%.*} |
删除右侧最后一个匹配项之后的所有内容 |
| 单次替换 | ${var/old/new} |
${var/ /_} |
将第一个匹配项替换为目标值 |
| 全局替换 | ${var//old/new} |
${var// /_} |
全局替换所有匹配项 |
| 子串删除 | ${var//old/} |
${var// /} |
替换目标为空,即删除所有指定字符 |
[!TIP] 网中人键盘记忆法(巧记
#与%): 查看电脑键盘上方的数字键:#(数字 3)在左侧,%(数字 5)在右侧。 -#在左边 ➡️ 从左端开始匹配并删除; -%在右边 ➡️ 从右端开始匹配并删除; - 单个符号(#或%)为最短匹配(非贪婪); - 双个符号(##或%%)为最长匹配(贪婪)。
二、字符串属性校验与长度统计
1. 字符类型与正则模式匹配
通过 grep -q(静默模式)结合正则表达式,可以优雅地验证变量的内容合法性(退出状态码 $? 为 0 表示匹配成功):
# 1. 验证是否为纯数字
num="94234832472"
if echo "$num" | grep -q '^[0-9]\+$'; then
echo "合法:纯整数"
fi
# 2. 验证是否为字母组合 (支持大小写)
letters="DevOps"
if echo "$letters" | grep -q '^[a-zA-Z]\+$'; then
echo "合法:纯字母"
fi
# 3. 验证是否为字母与数字混合
id_code="x7lab2026"
echo "$id_code" | grep -q '^[0-9a-zA-Z]\+$' && echo "合法:字母数字混合"
# 4. 验证是否包含空白字符 (空格或 Tab)
# POSIX 字符集 [[:space:]] 能同时匹配空格、制表符 \t
str="hello \t world"
echo -e "$str" | grep -q '[[:space:]]' && echo "包含空白字符"
# 5. 校验邮箱与 URL 地址
echo "admin@x7lab.cn" | grep -q '^[a-zA-Z0-9._%+-]\+@[a-zA-Z0-9.-]\+\.[a-zA-Z]\{2,\}$' && echo "合法邮箱"
2. 计算字符串长度与字符频次统计
计算字符串长度有多种手段,但在性能敏感场景推荐 Bash 原生写法:
var="get the length of me"
# 方式 1: Bash 原生内置语法 (强烈推荐,速度极快)
echo ${#var} # 输出 20
# 方式 2: expr 命令行工具
expr length "$var" # 输出 20 (注意必须加双引号防空格拆分)
# 方式 3: awk 内置 length 函数
echo "$var" | awk '{print length($0)}' # 输出 20
# 方式 4: wc -c 字节统计 (注意使用 -n 排除结尾换行符)
echo -n "$var" | wc -c # 输出 20
统计特定字符在字符串中出现的频次:
# 统计字母 'g' 出现的次数 (使用 tr -cd 仅保留匹配字符并统计)
echo "$var" | tr -cd 'g' | wc -c # 输出 2
# 统计单词总数
echo "$var" | wc -w # 输出 5
三、终端彩色显示与控制字符处理
1. ANSI 转义序列与彩色输出
在 Linux 终端中输出带有颜色和样式的提示信息,可以通过 ANSI 转义序列实现(格式为 \033[背景色;前景色m):
# 输出红字黑底
echo -e "\033[31;40m 错误: 服务连接超时! \033[0m"
# 输出高亮绿字 (适合成功提示)
echo -e "\033[1;32m [SUCCESS] 部署已完成! \033[0m"
常用颜色代码速查:
- 前景色:30 (黑)、31 (红)、32 (绿)、33 (黄)、34 (蓝)、35 (紫)、36 (青/Cyan)、37 (白);
- 重置样式:\033[0m。
2. 光标绝对定位与终端动态时钟
利用 \033[行号;列号H 可以在终端任意指定坐标定位光标,结合循环可实现仪表盘或悬浮时钟:
# 在屏幕第 5 行、第 20 列动态刷新当前时间
while true; do
echo -ne "\033[5;20H\033[1;36m当前系统时间: $(date '+%Y-%m-%d %H:%M:%S')\033[0m"
sleep 1
done
3. 使用 col 过滤终端控制字符
当使用 script 或 screen 命令录制终端日志时,日志文件中往往充斥着诸如退格、换行和 ANSI 彩色控制符。可以使用 col -b 清洗保留纯文本内容:
# 过滤掉终端录制文件中的控制转义字符,还原纯净可读日志
cat screenlog.0 | col -b > clean_log.txt
四、字符串结构化存储与数组操作
1. Bash 原生索引数组
Bash 支持通过括号直接将按空格分割的字符串转换为空格分隔的数组:
var="get the length of me"
# 1. 字符串转数组 (默认按 IFS 空格/换行拆分)
var_arr=($var)
# 2. 访问指定下标元素 (从 0 开始)
echo ${var_arr[0]} # 输出: get
echo ${var_arr[2]} # 输出: length
# 3. 输出全部元素与数组长度
echo "${var_arr[@]}" # 输出: get the length of me
echo "数组总长度: ${#var_arr[@]}" # 输出: 5
# 4. 追加新元素
var_arr[5]="appended"
echo "更新后长度: ${#var_arr[@]}" # 输出: 6
2. awk 分割与关联数组实战(System.map 符号地址解析)
awk 不仅支持根据任意分隔符拆分字符串(split),还支持以字符串为 Key 的关联数组(Hash Map)。
实战场景:将内核符号名根据符号表替换为实际内存地址
假设我们有一组待查询的内核符号文件 symbol.txt:
sys_exit
sys_read
sys_close
以及系统内核符号映射文件 /boot/System.map(部分内容):
c0129a80 T sys_exit
c0177310 T sys_read
c0175d80 T sys_close
通过 awk 的关联数组双文件流转,单行即可实现高效率的字典映射替换:
awk '{
# 当处理第一个文件 System.map 时,建立 map[符号名] = 内存地址
if (FILENAME ~ "System.map") {
map[$3] = $1
} else {
# 当处理第二个文件 symbol.txt 时,查表输出对应地址
if ($1 in map) {
printf("%-12s => %s\n", $1, map[$1])
}
}
}' /boot/System.map symbol.txt
输出结果:
sys_exit => c0129a80
sys_read => c0177310
sys_close => c0175d80
五、字符串核心常规操作实战
1. 取子串与区间切片
在截取字符串子集时,不同工具有不同的下标约定:
var="get the length of me"
# 1. Bash 原生语法: ${var:起始索引:截取长度} (索引从 0 开始)
echo ${var:0:3} # 输出: get
echo ${var:8:6} # 输出: length
# 2. 从尾部倒数截取: ${var:(-长度)} (括号不可省略)
echo ${var:(-2)} # 输出: me
# 3. expr 工具: expr substr "$var" 起始位置 长度 (注意: 索引从 1 开始!)
expr substr "$var" 5 3 # 输出: the
# 4. awk 工具: substr(str, 起始位置, 长度) (索引从 1 开始)
echo "$var" | awk '{print substr($0, 9, 6)}' # 输出: length
# 5. cut 工具: -d 指定分隔符, -f 指定域序号
echo "$var" | cut -d ' ' -f 1 # 输出: get
echo "$var" | cut -d ' ' -f 5 # 输出: me
2. 模式匹配掐头去尾 (# / ## 与 % / %%)
这是日常 Shell 编写中最高效的字符串修剪技巧:
var="get the length of me"
# 1. 从左端匹配并删除 (# 最短, ## 最长)
echo "${var#* }" # 删掉第一个空格及左边内容 -> "the length of me"
echo "${var##* }" # 删掉最后一个空格及左边内容 -> "me"
# 2. 从右端匹配并删除 (% 最短, %% 最长)
echo "${var% *}" # 删掉最后一个空格及右边内容 -> "get the length of"
echo "${var%% *}" # 删掉第一个空格及右边内容 -> "get"
文件路径提取经典应用:
filepath="/var/log/nginx/access_2026.log"
# 提取所在目录名 (相当于 dirname)
echo "${filepath%/*}" # 输出: /var/log/nginx
# 提取完整文件名 (相当于 basename)
echo "${filepath##*/}" # 输出: access_2026.log
# 提取文件基础名 (去掉后缀)
filename="${filepath##*/}"
echo "${filename%.*}" # 输出: access_2026
# 提取扩展名
echo "${filepath##*.}" # 输出: log
3. 子串查询与位置定位
var="get the length of me"
# 1. expr index: 返回匹配字符集中任意字符首次出现的位置 (从 1 开始)
expr index "$var" "t" # 输出: 3 (第 3 位为 't')
# 2. awk match: 支持正则表达式,返回子串起始偏移量
echo "$var" | awk '{print match($0, "the")}' # 输出: 5
4. 子串替换、插入与删除
var="get the length of me"
# 1. 变量单次替换: ${var/old/new}
echo "${var/ /_}" # 输出: get_the length of me
# 2. 变量全局替换: ${var//old/new}
echo "${var// /_}" # 输出: get_the_length_of_me
# 3. 变量删除: 替换为空即是删除
echo "${var// /}" # 输出: getthelengthofme
# 4. 在匹配项前后插入内容 (利用替换模式)
echo "${var/ /_ }" # 在第一个空格前插入下划线 -> get_ the length of me
echo "${var/ / _}" # 在第一个空格后插入下划线 -> get _the length of me
# 5. 使用 sed 进行正则反向引用替换
echo "$var" | sed -e 's/\([a-z]*\) \([a-z]*\)/\2 \1/'
# 将 "get the" 调换位置 -> "the get length of me"
# 6. 使用 tr 进行字符映射 (如大小写批量转换)
echo "$var" | tr '[a-z]' '[A-Z]' # 输出: GET THE LENGTH OF ME
5. 字符排序与去重
将字符串按单词拆分后结合 sort 与 uniq 进行排序分析:
var="get the length of me"
# 1. 将空格转换为换行后按字母升序排列
echo "$var" | tr ' ' '\n' | sort
# 2. 降序排列
echo "$var" | tr ' ' '\n' | sort -r
六、综合实战:URL 地址深度解析与提取
在处理网络请求、镜像同步脚本时,常常需要对复合 URL 字符串进行精细化拆解:
假设输入 URL 如下:
url="ftp://anonymous:ftp@mirror.lzu.edu.cn:21/software/scim-1.4.7.tar.gz"
纯靠 Bash 原生参数扩展即可零外部进程消耗完成全字段拆解:
#!/bin/bash
# parse_url.sh - 纯 Bash 提取复合 URL 各组成部分
url="ftp://anonymous:ftp@mirror.lzu.edu.cn:21/software/scim-1.4.7.tar.gz"
# 1. 提取协议 Protocol (从右侧删除第一个 : 之后的所有内容)
proto="${url%%:*}"
# 2. 提取去除协议后的剩余部分
no_proto="${url#*://}" # "anonymous:ftp@mirror.lzu.edu.cn:21/software/scim-1.4.7.tar.gz"
# 3. 提取认证身份凭证 User & Password
auth="${no_proto%%@*}" # "anonymous:ftp"
user="${auth%%:*}"
pass="${auth#*:}"
# 4. 提取主机与端口 Host & Port
host_port_path="${no_proto#*@}" # "mirror.lzu.edu.cn:21/software/scim-1.4.7.tar.gz"
host_port="${host_port_path%%/*}" # "mirror.lzu.edu.cn:21"
host="${host_port%%:*}" # "mirror.lzu.edu.cn"
port="${host_port#*:}" # "21"
# 5. 提取路径 Path 与 文件名 Filename
path="/${host_port_path#*/}" # "/software/scim-1.4.7.tar.gz"
filename="${url##*/}" # "scim-1.4.7.tar.gz"
file_ext="${filename##*.}" # "gz"
# 输出解析报告
echo "============= URL 解析报告 ============="
echo "协议类型: $proto"
echo "认证账号: $user"
echo "认证密码: $pass"
echo "目标主机: $host"
echo "目标端口: $port"
echo "请求路径: $path"
echo "文件名称: $filename"
echo "扩展后缀: $file_ext"
运行输出:
============= URL 解析报告 =============
协议类型: ftp
认证账号: anonymous
认证密码: ftp
目标主机: mirror.lzu.edu.cn
目标端口: 21
请求路径: /software/scim-1.4.7.tar.gz
文件名称: scim-1.4.7.tar.gz
扩展后缀: gz
七、总结与工程选型建议
在编写生产级 Shell 脚本时,面对字符串处理请牢记以下法则:
- 单变量简单处理首选 Bash 原生参数扩展:
- 长度统计
${#var}、子串截取${var:offset:length}、掐头去尾${var##*/}与局部替换${var//old/new}; - 原生扩展在 Shell 进程内存中即时完成,耗时为微秒级,完全免除子进程 fork 开销。
- 单行多列或流式文本处理首选
awk/cut: - 字段列切分用
cut -d ' ' -f N或awk '{print $N}'; - 跨行或多文件关联字典映射首选
awk关联数组。 - 复杂全文模式替换首选
sed: - 支持强大的分组正则捕获与反向引用(
\1,\2)。