GNU tr 删除后再压缩:-d 与 -s 同时启用时,两组字符各管什么
清理设备输出时,常见需求是删掉某种分隔符,再把重复字符压成一个。tr -ds看上去只是两个简单开关,但处理顺序会改变最终结果:删除动作可能让原本分开的字符成为相邻一段,而挤压随后会把这段一起缩短。若把同样两个步骤反过来写,输出未必相同。下面通过固定ASCII样本,把每一步的输入和输出完整保留下来。
本文在Linux、GNU Coreutils 9.7、Bash 5.2.37上实跑,LC_ALL=C。示例只处理ASCII字符并走标准流,不覆盖任何文件。GNU tr的字符集合及多字节处理有实现边界,本文不把ASCII字节清洗方案扩展成通用Unicode文本规范化方案。
AI生成的概念示意图:橡皮擦除符号的处理室与压紧重复卡片的处理室依次相连,强调先删除再挤压的执行顺序;不是真实软件界面或运行截图。
完整程序与实际输出
保存为demo.sh,执行bash demo.sh。程序只使用固定测试输入,代码和本次输出分别列出。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
sample() { printf 'aa---aaa bb\n'; }
printf 'input: '; sample
printf 'squeeze-a: '; sample | tr -s 'a'
printf 'delete-hyphen: '; sample | tr -d '-'
printf 'delete-then-squeeze: '; sample | tr -ds '-' 'a'
printf 'reverse-stages: '; sample | tr -s 'a' | tr -d '-'
printf 'translate-then-squeeze: '; printf 'a--a\n' | tr -s '-' 'a'
printf 'spaces-and-tabs: '; printf 'x \t y\n' | tr -s '[:blank:]' | od -An -tx1本次实际标准输出:
input: aa---aaa bb squeeze-a: a---a bb delete-hyphen: aaaaa bb delete-then-squeeze: a bb reverse-stages: aa bb translate-then-squeeze: a spaces-and-tabs: 78 20 09 20 79 0a
先区分删除和挤压
输入是两个a、三个连字符、三个a、两个空格、两个b。只运行tr -s a时,每段连续的a会缩成一个,得到a---a,后面的两个空格和两个b保持原状。-s并不是把整行所有重复内容都去重,也不会压缩没有列进集合的字符。它只关注指定字符各自连续重复的片段。
只运行tr -d连字符集合时,三个连字符全部移除,前后两段a随之贴到一起,形成五个a。这一步没有“最多保留一个”的动作。删除和挤压必须分开理解:前者可以让指定字符完全消失,后者为一段至少保留一个字符。把删除误当作替换为空格,会改变词边界甚至把两个标识符拼成一个。
示例把连字符写在引号中作为集合参数,以明确它是交给tr解释的内容。更复杂的集合需要考虑选项解析、范围写法和反斜杠转义,不应凭视觉猜测。实际脚本里要保留参数边界,并把接收的文本放在标准输入,不要临时拼成命令源码。
两个集合在-ds中分工不同
同时启用-d和-s时,第一个集合用于删除,第二个集合用于挤压。因此本例先删连字符,再压缩剩余结果里的a,最终只剩一个a,空格与b依旧保持两份。第二个集合不是第一个集合的替换内容;这里根本没有字符翻译。先写清两个集合的职责,可以避免把-ds理解成“把连字符换成a以后再压缩”。
反向对照先用tr -s a,再用tr -d删除连字符。第一步已经把两段a分别缩成一个,但第二步只是删除中间屏障,不再执行新的挤压,所以输出里留下两个相邻a。这是本例最有价值的差异:相同操作集合不表示相同函数组合,处理顺序本身就是数据契约的一部分。
在日志处理链里,这类顺序差异经常被隐藏在多个管道段之间。若先去掉标点再识别重复,与先识别重复再删标点可能得出不同结果。验证时应该输出中间态,找出相邻关系在哪一步发生变化,而不是只比较最终文件大小;两个结果长度接近,也可能包含不同词义或标识符。
带翻译的-s又遵循另一套入口
不使用-d、同时提供两个集合时,tr可以先翻译,再对第二个集合中的重复字符挤压。本例把a--a里的连字符翻译为a,随后得到四个连续a,再收缩成一个。原本已经存在的a也属于最终结果中的重复片段,不会因为它不是翻译产生的字符就得到特殊保护。
这意味着字符翻译可能制造新的连续片段,和删除制造相邻关系一样,都要在挤压前考虑。若只是想压缩多个连字符而保留字母,直接指定连字符挤压集合更清楚;若想把多种分隔符统一成一种,再压缩结果,则要明确翻译目标。不要为压缩重复误引入会改变正文字符的映射。
字符集合不是字符串替换词典。把多个字符列在同一个集合里,并不表示只匹配那串完整子串。需要删除完整单词、根据上下文处理标点或保留引用区间时,应选择能够表达语法边界的工具。tr适合简单字符级变换,越过这个边界后,即使命令运行成功,也不代表结果符合业务语义。
空白集合不等于单一空格
最后一段包含空格、制表符、两个空格。tr -s [:blank:]会分别挤压相同空白字符的重复段,但不会自动把空格与制表符当作一种字符。十六进制结果仍包含20、09、20三个空白位置。若凭终端显示判断,可能以为它们只是一个拉长的间隙,因此程序用od把实际字节显式打印出来。
若目标是统一空白,需要先明确是否包含换行,再设计翻译和压缩。把所有空白都变成空格,可能会把多行记录连接为一行;只处理水平空白,则保留换行但仍可能破坏需要对齐的表格、代码缩进或定宽字段。不能把“清洗得更整齐”当成足够的安全条件,必须知道空白在格式里承担什么意义。
用于生产前,至少保留无目标字符、全是目标字符、交替空白、首尾空白和分隔符两侧同字符几组样本。输出先写入新文件,核对字节和业务记录数,再决定替换原件。对结构化数据,应尽量在解析后修改字段,而不是在原始字节流上做跨边界删除;这个小例子的价值正是让这种风险变得可见。
删除操作还有一个结构风险:两个本来合法且独立的片段,删除分隔符后可能变成另一个合法标识符,因此语法检查也不一定报错。例如字母数字编号之间的连字符若具有分段意义,删掉以后长度仍满足规则,却已经失去原来的层级信息。回归测试除了检查是否成功运行,还应核对业务字段是否仍可恢复。只要变换不可逆,就应该保留原始输入和明确的转换说明。
参考资料与验证记录
官方资料核验于2026年10月3日。本次完整程序退出码为0,标准错误为空;例子验证的是上述固定输入与运行环境,不表示所有平台和版本的输出细节完全一致。


