资讯动态

Bash字符串操作全指南:运算符、长度、拆分、提取与拼接

发布时间:2026/10/14 22:18:08 来源:尧图企业网站定制
干过运维和自动化的人应该都有这种感受Bash 脚本写久了一半时间其实都花在跟字符串打交道。从判断用户输入、统计变量长度到拆日志字段、拼告警消息看似零散的小操作组合在一起却能解决一大片实际问题。Bash 字符串运算符、查找字符串长度、拆分字符串、提取子字符串、拼接字符串这五块内容单独看都不难难的是知道在什么场景下用哪一招、哪些写法会踩坑。这一讲我就把它们串起来讲一遍配合真实脚本示例把参数校验、日志解析、消息拼接这些高频场景都覆盖到。这篇内容适合两类人一类是刚接触 Bash 脚本、想系统把字符串操作补齐的初学者另一类是已经写了很久脚本、但偶尔被[ ]和[[ ]]的差异、空变量报错、中文字节数这类问题绊一下的老手。看完之后你至少能随手写出不劈叉的字符串处理代码也清楚遇到奇怪现象时该往哪个方向排查。1. 字符串操作在 Bash 脚本里的生态位1.1 为什么这一讲值得单独拿出来说我自己的统计是一个中等规模的部署脚本里超过三分之一的代码行多多少少涉及字符串处理。不是我们非要跟字符串较劲而是 Shell 脚本的定位决定的——你总要把命令输出读进来、把配置项拼成参数、把日志字段拆开比对这些动作本质上全是字符串操作。Python 或 Go 里一套标准库就解决的问题在 Bash 里需要用参数扩展、外部命令、正则匹配组合着来所以更需要脑子里有一张“操作地图”。打个比方Bash 字符串处理就像维修工的活工具不多但每一样都有专用场景。${#var}是卷尺${var:offset:length}是切割机IFS加read -a是拆解台~配合BASH_REMATCH是一把能自动定位的镊子。工具本身不复杂但你要知道什么时候掏哪一把更要知道同一把工具在不同条件下比如 locale 变了、Bash 版本变了会出什么幺蛾子。1.2 先建立一条清晰的选型主线面对字符串处理需求我建议你总是按这个顺序思考先看能不能用 Bash 内建能力解决比如参数扩展、[[ ]]、read。内建能力不依赖外部命令执行快也不容易被环境差异影响。内建能力搞不定的再上外部命令比如cut、awk、sed、grep。它们功能强但都要起子进程批量循环里性能差别一下就出来了。最后才是考虑换工具链。如果字符串逻辑复杂到状态机级别那真不是 Bash 该干的活早点换成 Python 之类的更省心。这一讲提到的五个主题基本都按这个思路展开。下面我从运算符开始逐个过。2. 字符串运算符本质是“判断表达式”2.1 五个核心运算符的含义和坑Bash 里的字符串运算符最常见的五类是运算符含义典型写法字符串相等[ $a $b ]!字符串不等[ $a ! $b ]-z字符串为空长度为0[[ -z $s ]]-n字符串非空长度非0[[ -n $s ]]按字典序比较[[ $a $b ]]先说说最容易翻车的几个点。第一两边必须有空格。[ $a$b ]在 Bash 看来不是比较而是一个字符串这个表达式永远为真。初学者犯这个错的频率极高我自己早期也栽过。第二单中括号[ ]和双中括号[[ ]]行为不一样。[[ ]]是 Bash 的关键字比较安全[ ]是外部命令test的语法糖里面的变量最好都加双引号尤其是用-n的时候。经典陷阱是[ -n $unquoted ]如果变量为空展开后变成[ -n ]这是判断字符串-n是否非空结果恒为真空值反而被判定为非空。第三[[ $a $b ]]的字典序比较看着直观但结果受LC_COLLATE环境影响不同 locale 下字母排序顺序可能不一样跨环境脚本里不要依赖具体顺序。另外~也是一个字符串运算符——正则匹配。虽然很多人不把它归类在“运算符”里但配合子字符串提取时它是大杀器后面单独讲。2.2 参数校验运算符最典型的应用现场运算符最常见的应用就是脚本入口参数校验。比如某个部署脚本接受一个环境名参数合法值是prod或staging#!/usr/bin/env bash deploy_env${1:-} if [[ -z $deploy_env ]]; then echo 用法: $0 prod|staging|dev exit 1 fi if [[ $deploy_env ! prod $deploy_env ! staging ]]; then echo 环境名必须是 prod 或 staging当前传入: $deploy_env exit 1 fi echo 开始部署到 $deploy_env这里${1:-}是参数扩展的默认值语法作用是当$1未定义时把它当成空字符串避免在set -u开启时报 unbound variable。配合[[ -z ]]判断空值整个脚本在“用户没传参数”和“传了空参数”两种情况下都能正确给出提示不会中途崩掉。按我个人的习惯脚本入口的参数校验一定放在最前面用-z和!组合把非法输入拦在门外。很多脚本问题其实不是逻辑问题而是外部输入没校验干净后面每一步都在为脏数据买单。3. 查找字符串长度${#var} 与它的兄弟们3.1 ${#var} 的正确用法和常见误区计算字符串长度Bash 最正统的写法是${#var}namenginx echo ${#name} # 输出 5注意花括号不能省。写成$#name会变成“参数个数”语法结果完全不是你想的那个意思。这个错误我见过不少回尤其在从其他语言转过来的人身上。${#var}还有一个容易混淆的亲戚${#array[]}是统计数组元素个数${#str}是统计字符串长度。两者语法看起来很像但用途完全不同不要说混了。3.2 多字节字符、locale 和字节数的问题这里有个值得展开的坑${#var}统计的是“字符数”还是“字节数”它取决于当前 locale。在正常的 UTF-8 locale 下Bash 按字符统计但如果把 locale 切到 C/POSIX同一个字符串统计结果可能变成字节数。举个例子s你好Bash echo ${#s} # 在 UTF-8 locale 下输出 7实际上 你好 两个字符加 Bash 四个字符 6等等我算一下“你好”是 2 个字符“Bash”是 4 个字符加起来是 6 个字符。但按字节数是 2×3 4 10 字节。所以不同 locale 下${#s}可能输出 6也可能输出 10。这就导致同一段脚本换个环境结果不一样非常隐蔽。如果你需要严格按字节数操作就不要依赖${#var}改用byte_len$(printf %s $s | wc -c) char_len$(printf %s $s | wc -m)什么时候必须用字节数典型场景是判断文件名长度。很多文件系统的文件名上限是 255 字节你拿${#name}算出来没超实际上存文件时却报错就是因为中文字符按字节算已经超了。命令统计单位适用场景备注${#var}字符数受 locale 影响日常判断长度内建最快printf %s $var | wc -c字节数文件名长度、网络包大小限制外部命令printf %s $var | wc -m字符数校验字符数外部命令expr length $var字符数兼容 POSIX 老环境坑多不建议expr length有一个很讨厌的问题如果变量值以-开头比如expr length -abcexpr会把它解析成选项直接报错。实务中我基本不用expr做长度计算要用也得写成expr length -- $var太啰嗦了。4. 拆分字符串IFS、read -a、cut、awk 怎么选4.1 IFS 加 read -a把字符串切成数组拆分字符串最常用、最“Bash 原生”的方式是修改 IFS内部字段分隔符配合read -a把结果存进数组IFS, read -r -a arr alpha,beta,gamma echo ${arr[0]} # alpha echo ${arr[1]} # beta echo ${arr[2]} # gamma是 here string作用是把后面的字符串作为输入喂给read。-r防止反斜杠转义处理路径或含\的数据时必加。read -a只在 Bash 里可用POSIX sh 没有这个语法跨 shell 脚本要注意。还有一个更轻量的用法——一行拆多变量read -r ip port desc 192.168.1.10 8080 gateway echo $ip # 192.168.1.10 echo $port # 8080这是日志解析里最常用的招把一行按空白切成几个命名字段后面的cut和awk反而没用它常用。用 IFS 切分时连续的分隔符默认会被折叠空字段会被忽略。比如a,,b按逗号切可能得到a和b空的那一位不会进数组。如果你的业务需要保留空字段这条路就不通得换思路。改 IFS 之后一定要恢复现场否则当前 Shell 后面所有依赖 IFS 的操作都会受影响连for循环的分词行为都会变。我一般写成old_ifs$IFS IFS, read -r -a arr $data IFS$old_ifs如果只在当前语句需要也可以放到子 Shell 里执行避免恢复的麻烦(IFS,; read -r -a arr $data; echo ${arr[1]})。4.2 cut 和 awk两种思路完全不同的脾气cut的逻辑是按单字符分隔符取第 N 列echo nginx:mysql:redis | cut -d: -f2 # mysqlcut简单但有两个硬伤。一是分隔符只能是单个字符没法处理“连续空格”这类复杂分隔。二是遇到连续分隔符时会输出空字段比如a b按空格切中间多出的空格会被当成空字段结果字段位置全乱了。另一个经典坑是用cut -d 处理用多个空格对齐的文本时第二列经常取不到因为第一列和第二列之间有好几个空格cut不会自动合并它们。awk的思路完全不一样。它默认按“连续的空白字符”切分多个空格和 Tab 都算一个分隔符所以处理对齐文本时直接awk {print $2}就能取到第二列不需要关心中间隔了多少个空格。指定其他分隔符也灵活-F后面可以跟正则echo a b c | awk {print $2} # b echo a,b,c | awk -F, {print $2} # b echo a|b|c | awk -F[|,] {print $2} # 支持多分隔符实务中我的选择习惯是单分隔符、格式规整的数据用cut代码直觉好读一旦分隔符是空白、或者有几个连续分隔符的情况直接上awk不要跟cut死磕。4.3 按行拆分和 mapfile拆分不一定都是按逗号、按空格按行也是一种高频需求。从命令输出里一行行读进来可以写循环但更优雅的是mapfilemapfile -t lines $multiline_str for line in ${lines[]}; do echo 处理行: $line done-t会自动去掉行尾换行符。这个命令适合把多行配置、多行日志一次性装进数组再统一处理比while read循环少写不少样板代码。5. 提取子字符串${var:offset:length} 和正则配合5.1 按位置提取${var:offset:length}按位置提取子串是 Bash 参数扩展里最直观的语法strABCDEFGHIJ echo ${str:0:5} # ABCDE从第0个字符开始取5个 echo ${str:3} # DEFGHIJ从第3个字符到末尾 echo ${str: -3} # HIJ取最后3个字符注意最后一行负数偏移前必须有空格或者用括号写成${str:-3}会被解析成“变量未定义时给默认值 3”结果完全不是提取。这是一个高频错误我在脚本 review 里见过好多次。提取日期时间字段是这个语法最常见的应用。比如一串20240612103045想拆成年月日时分秒ts20240612103045 year${ts:0:4} # 2024 month${ts:4:2} # 06 day${ts:6:2} # 12 hour${ts:8:2} # 10 minute${ts:10:2} # 30 second${ts:12:2} # 45和长度统计一样${var:offset:length}在 UTF-8 locale 下按“字符”切在 C locale 下可能按“字节”切。处理中文时正常 UTF-8 环境下${s:0:1}取到的是一个完整汉字但如果 locale 不对可能取到半个汉字输出乱码。跨环境脚本里涉及中文子串时千万留意。我实操中还常用它干一件事从完整文件名里批量去掉前缀后缀。虽然一般更推荐用${var#pattern}那套做但固定偏移的场景用offset提取更直白。5.2 按模式提取${var#}、${var##}、${var%}、${var%%}这一组虽然不是严格的“按下标提取”但它们的用途就是削掉部分字符串留下你要的那一段path/var/log/nginx/access.log echo ${path##*/} # access.log从前面开始删到最后一个 /等价于取文件名 echo ${path%/*} # /var/log/nginx从后面开始删掉最后一个 / 之后的内容等价于取目录规律很好记#从前面删%从后面删单符号是“最短匹配”双符号是“最长匹配”。比如${path#*/}是删到第一个/得到var/log/nginx/access.log${path##*/}删到最后一个/得到access.log。提取文件扩展名也常用这招filebackup.tar.gz echo ${file##*.} # gz echo ${file%.*} # backup.tar这套参数扩展完全是 Bash 内建操作不启动外部进程在循环里大量调用时性能优势非常明显。5.3 正则提取[[ ~ ]] 加 BASH_REMATCH位置提取解决“第几位到第几位”模式提取解决“长得像什么”的问题。Bash 的[[ ]]支持~正则匹配匹配结果保存在BASH_REMATCH数组里下标 0 是整个匹配下标 1 以后是括号捕获组logtime2024-06-14T22:31:09Z ip192.168.66.88 status200 latency0.023 if [[ $log ~ latency([0-9.]) ]]; then latency${BASH_REMATCH[1]} fi if [[ $log ~ status([0-9]) ]]; then status${BASH_REMATCH[1]} fi echo 状态: $status, 耗时: $latencyBASH_REMATCH的捕获组提取能力可以省掉一整条sed或grep -o。但有一点要注意~的正则不支持完全跨行匹配默认也不是“全局匹配”它只做一次匹配检查。需要提取多处时要么递归改字符串要么直接用grep -oE配合while read。如果要从一段长文本里抽所有匹配项grep -oE是更好的选择ids$(echo id1001 id1002 id1003 | grep -oE id[0-9]) while read -r id; do echo 找到: $id done $ids5.4 从命令输出中提取sed 和 awk 的兜底能力如果数据不在变量里而在文件或管道流里直接用参数扩展就不好使了。这时sed的替换提取非常顺手echo request_time1.23s status200 | sed -nE s/.*request_time([0-9.])s.*/\1/p-n是关掉默认输出p是只打印替换成功的行。这个写法在解析日志流时很常用配合管道可以一条命令完成提取。6. 拼接字符串除了相加还要小心词拆分6.1 最基础的拼接双引号、花括号和 Bash 拼接字符串没有专门的运算符“放在一起”就是拼接prefixbackup date_suffix20240612 filename${prefix}_${date_suffix}.tar.gz echo $filename # backup_20240612.tar.gz变量后面紧跟其他字符时花括号必须加。$prefix_suffix会被解析成变量prefix_suffix而不是prefix后面跟_suffix。写不加花括号的拼接本质上是在给自己制造隐蔽 bug。运算符对字符串变量是追加但有个经典坑如果变量被当作整数用过或者变量值是纯数字的行为是算术加法而不是拼接n10 n1 echo $n # 输出 11而不是 101因为 10 被当成整数要拼接数字字符串老老实实写n${n}1。想要区分变量类型的读者注意了Bash 变量没有显式类型这个坑只能靠使用习惯避开。6.2 拼接和词拆分引号是最后的防线字符串拼接本身不难难的是拼接后的字符串在传给命令时被拆词。看这个例子prefixhello world suffixbackup file${prefix}_${suffix}.txt echo $file # 正确输出 hello world_backup.txt command ls ${prefix}_${suffix}.txt # 拆分成了 ls hello world_backup.txt 两个参数大概率报错不加引号的变量展开会被 IFS 按空白拆成多个词。这不是拼接的问题是“拼接完怎么用”的问题。所有包含空白的拼接结果传给命令时一定要加双引号不然程序行为和你脑子里想象的不是一回事。另一个相关坑是变量未定义时拼接出“意外空串”。比如${prefix}${suffix}如果某个变量没定义结果会静默少一段。想提供兜底值就用${var:-default}想直接报错就用set -u让未定义变量在引用时直接暴露出来。6.3 批量拼接的性能问题数组一次成型很多人写循环拼接字符串结果发现跑得很慢result for item in ${items[]}; do result$result,$item done每次循环都生成一个新的字符串对象旧字符串被丢弃数据量大了之后明显变慢。更优的做法是先用数组收着最后一次性拼接parts() for item in ${items[]}; do parts($item) done # 用 IFS 控制分隔符一次性连接数组元素 joined$(IFS,; echo ${parts[*]})注意IFS,; echo放在子 Shell 里不会污染当前环境的 IFS。用${parts[*]}拼接时数组元素之间会插入 IFS 的第一个字符比自己在循环里拼省心得多而且不会在开头或结尾多一个分隔符。这在生成 SQL 的 IN 列表、拼 CSV 行时非常好用。7. 完整的日志解析实战把前面所有内容串起来前面讲了不少语法点现在用一个真实感很强的完整例子过一遍。假设我们有一条业务日志格式是keyvalue对空格分隔logtime2024-06-14T22:31:09Z ip192.168.66.88 methodGET path/api/v1/users status200 latency0.023 regioncn-east需求是提取出ip、status、latency、path判断状态码是不是 2xx/3xx判断耗时是否大于 1 秒最后拼一条告警消息。#!/usr/bin/env bash logtime2024-06-14T22:31:09Z ip192.168.66.88 methodGET path/api/v1/users status200 latency0.023 regioncn-east read -ra kv_pairs $log ip status latency path for kv in ${kv_pairs[]}; do key${kv%%*} # 取 左边的 key val${kv#*} # 取 右边的 value case $key in ip) ip$val ;; status) status$val ;; latency) latency$val ;; path) path$val ;; esac done # 判断状态码2xx 或 3xx 算正常其余告警 if [[ $status ~ ^[23][0-9][0-9]$ ]]; then levelINFO else levelALERT fi # Bash 不能直接比较浮点数用 awk 做阈值判断 if awk BEGIN{exit !($latency 1.0)}; then slow慢请求 else slow正常 fi message${level} | ip${ip} status${status} latency${latency}s path${path} [${slow}] echo $message这段代码用到的知识点逐个对应read -ra kv_pairs $log按空白拆分整行字符串对应第 4 节的 IFS 拆分法。${kv%%*}和${kv#*}按模式删除提取 key 和 value对应第 5 节的模式匹配提取。[[ $status ~ ^[23][0-9][0-9]$ ]]正则匹配运算符对应第 2 节和第 5 节的正则提取。message${level} | ip${ip}...拼接字符串对应第 6 节。这个例子把五个主题串成了一条完整链路。实际写脚本时这种“先拆分、再提取、再判断、最后拼接”的流程占比非常高。把一个流程走顺了其他场景大多是换数据、换字段名的事。8. 常见问题与排查技巧实录8.1 高频问题速查表现象根本原因解决方法脚本报unary operator expected[ -n $var ]里变量为空时被展开没了[ ]拿到单参数用[[ -n $var ]]或[ -n $var ]加引号${var: -3}取不到最后3位负号前没空格被解析成默认值语法写${var: -3}或${var:(-3)}环境变量名变了拼接结果少一段变量名和后续字符没加花括号改成${var}xxxcut -d 取不到第二列连续空格被当成多个空字段换awk {print $2}循环拼接一万次字符串特别慢每次循环都新建字符串对象用数组收集最后IFS,; echo ${arr[*]}中文长度统计结果“不对”locale 不是 UTF-8${#var}按字节统计明确用wc -m或统一 locale修改 IFS 后整个脚本行为异常改了 IFS 没恢复影响后续所有分词保存旧 IFS用后恢复或用子 Shell 隔离n1得到 11 而不是 101对整数做算术加数字拼接用n${n}1变量为空导致命令参数少一个未加引号的空变量被展开消失加双引号或用${var:-}兜底8.2 遇到“脚本行为怪异”时的排查顺序我个人的排查习惯是固定的先看是不是变量为空或带空白再看是不是 locale 差异再看是不是 IFS 被改过最后看是不是 Bash 版本特性不一致。其中“变量为空或带空白”占了我遇到问题的一大半——用printf %s\n $var把变量在尖括号里打印出来一眼就能看出有没有隐藏的空格和换行。另一个建议所有新写的脚本都跑一遍 ShellCheck 之类的静态检查工具它能自动告诉我们哪些地方漏了引号、哪些写法在 POSIX sh 和 Bash 之间不兼容。脚本不用一次写对但一定要能快速查错。字符串操作尤其适合用这种“先写后查”的方式练手因为坑的规律性很强查过一次下次自然就记住了。最后再分享一个我自己的习惯凡是需要给其他人维护的脚本字符串处理的每一行都会写清楚“为什么要这么做”。尤其是那些看起来很多余的双引号和花括号注释里注明“防止空变量展开导致参数丢失”后面接手的人就不会自作主张删掉然后踩进同一个坑里。字符串处理本身不复杂真正复杂的是让代码在五花八门的环境和输入下都表现一致。把这五个基本功练到位再配上一套稳定的排查思路Bash 脚本这块基本就稳了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑