tr 替换连字符时报字符范围顺序错误
tr 的字符列表中间出现连字符可能被解析为范围。把字面连字符放到末尾,并明确第二数组的空格重复形式,避免误替换其他字符。
用 tr 把下划线、括号、斜杠和连字符换成空格时,字符列表已经放进单引号,命令却报 range-endpoints 或 reverse collating sequence order。这类错误意味着 tr 把列表中间的 - 解释成了范围连接符,而不是要替换的普通字符。
单引号负责让 shell 原样传入参数,不会关闭 tr 自己对字符数组的解析。下面按 GNU coreutils 的 tr 规则说明;不同实现、版本和 locale 的报错文字与具体解析可能不同,不能只凭一条报错推断系统版本。
把字面连字符放在列表末尾
若需要把 _、[、]、(、)、/、- 都换成空格,可以将 - 放在第一个字符数组的末尾:
LC_ALL=C tr '_][()/-' '[ *]' < ./input.txt
./input.txt 是输入文件占位。[ *] 中间是一个空格,它在第二个字符数组里表示按需要重复空格,使每个待替换字符都有对应的输出字符。GNU tr 默认也会扩展较短的第二数组,但显式写重复形式更清楚,不依赖其他实现对长短不等数组的处理。
这里的方括号是要替换的字符,不是为所有字符包一层正则表达式。tr 的操作数是字符数组,不能直接套用 grep 的字符组规则。把右方括号写在左方括号前面,也避免把这两个字面字符误写成某种特殊方括号形式。
这个命令一次替换一个字符。它不会把连续多个符号自动合成一个空格;原有空格、字母、数字和换行也不会因为不在第一数组中而被删除。
错误消失后,仍要检查有没有扩大替换范围
范围不一定报错。比如:
tr 'a-z' '[ *]' < ./input.txt
这里表示替换从 a 到 z 的范围,并非只替换 a、-、z 三个字符。如果真正想替换这三个字面字符,应写成:
LC_ALL=C tr 'az-' '[ *]' < ./input.txt
因此,验收不能只看退出状态正常。应检查原本不应改变的中间字母是否保留。改变 locale 也不能表达“把连字符当普通字符”的意图;示例固定 LC_ALL=C 是为了限定按单字节处理,字面连字符仍然放在末尾。
本例只操作 ASCII 标点。不要据此把中文字符或多字节字母直接填进 GNU tr 的两组参数,期待得到逐个 Unicode 字符的替换;那需要明确支持相应编码的工具。
用小样本确认空格数量与文件边界
先对不含业务数据的短字符串查看结果:
printf '%s\n' 'a-b_c[d](e)/f' | LC_ALL=C tr '_][()/-' '[ *]'
按上面的逐字符规则,预期结果是:
a b c d e f
d 与 e 之间、e 与 f 之间各有两个空格,因为相邻的两个符号分别被替换。若下游确实只需要一个空格,可以另外考虑 -s,但它也会压缩结果中的连续空格,可能改变原文已有的间距。不要只为让输出整齐就顺手加上。
确认后再将结果写入一个尚不存在的新文件,并保留原输入:
LC_ALL=C tr '_][()/-' '[ *]' < ./input.txt > ./output.new.txt
不要把输出重定向到同一个输入文件,shell 可能在 tr 读取前就截断它。替换会丢失“这个空格原来是哪一种标点”的信息,不能靠反向 tr 恢复。回退应使用保留的原文件;批量使用前还要核对这些符号是否承担路径、字段或其他业务含义,避免把正常结构一起抹掉。


