Bash 负数截取偏移:冒号后少一个空格,为什么拿到整段字符串
脚本想从编号末尾取三个字符,却拿回完整编号。表达式里的冒号和减号紧挨着,Bash 把它读成默认值展开,而没有进入字符串截取。给负数偏移留出空格,看起来只是排版变化,实际上是在选择另一种语法;因此这类问题不能只检查变量的内容。
先区分负数起点与默认值写法
字符串截取的基本形式是变量名后接冒号、偏移,随后可选长度。起点从零计数,负数起点从字符串尾部向前数。负号若直接挨着第一个冒号,就与冒号减号的默认值运算冲突;本例统一保留一个空格,也示范带括号的算术表达式写法。
下面保存为 demo.sh,用 bash demo.sh 执行。示例固定使用 ASCII 内容与 C 区域设置,只操作自己的变量,不读取用户文件。check 函数逐项比较结果,一旦不符合预期就退出。方括号能让空字符串和保留下来的空格更容易看清。
set -u
export LC_ALL=C
check() {
if [[ "$1" != "$2" ]]; then
printf 'check failed: [%s] != [%s]\n' "$1" "$2" >&2
exit 1
fi
}
text='ABCDEFGH'
check "${text:-3}" 'ABCDEFGH'
check "${text: -3}" 'FGH'
check "${text:(-3)}" 'FGH'
check "${text: -4:2}" 'EF'
check "${text:2:-2}" 'CDEF'
check "${text:2:0}" ''
check "${text: -0}" 'ABCDEFGH'
check "${text: -20}" ''
offset=-3
check "${text:offset}" 'FGH'
short='AB'
check "${short: -3}" ''
need=3
if (( ${#short} < need )); then
tail=$short
else
tail=${short: -need}
fi
check "$tail" 'AB'
spaced='AB C '
check "${spaced: -3}" ' C '
empty=''
check "${empty:-3}" '3'
check "${empty: -3}" ''
printf 'default / tail: [%s] [%s]\n' "${text:-3}" "${text: -3}"
printf 'offset + length: [%s]\n' "${text: -4:2}"
printf 'negative endpoint: [%s]\n' "${text:2:-2}"
printf 'zero / beyond: [%s] [%s]\n' "${text:2:0}" "${text: -20}"
printf 'short policy: [%s]\n' "$tail"
printf 'spaces: [%s]\n' "${spaced: -3}"
printf 'all substring checks passed\n'长度为负时,它表示尾部终点
首行的两个结果分别是完整的八个字符与最后三个字符。下一行从倒数第四个字符开始取两个,得到 EF。随后从正向偏移二开始、到末尾前两个字符之前结束,得到 CDEF;这里的负二不是“向后取负两个”,而是用尾部位置确定截取终点。
AI生成概念示意图:从字符串末端反向确定起点后,再按明确的终点或长度取出片段。
因此,第一处负数负责确定开始位置,第二处负数负责确定结束位置,两者不能凭符号一样就当成同一种数量。终点如果跑到起点前面,可能触发展开错误。业务若允许任意范围,应先检查起点与终点的关系,再进行截取,不要把异常输出当成正常的空结果。
短输入和零也需要独立约定
对只有两个字符的 short 使用负三偏移,实测得到空字符串,并不会自动退到开头把整段返回。如果业务承诺“最多取末尾三个”,应先比较长度,短于所需数量时直接返回原文。代码中的分支正是把这个业务约定补在语言操作之外,结果因此保留 AB。
零长度与负零偏移也不同:前者确实取零个字符,后者仍然是从偏移零开始,会返回整段。需要支持取末尾零个字符的通用函数时,应将零作为独立分支,不能只把传入数量加一个负号。这里保留负零断言,防止以后把简单表达式误当成完整接口。
偏移和长度属于算术表达式,所以示例可以直接使用保存负三的 offset。外部输入不能因此不经检查就塞入表达式:应先按业务接受的整数格式与范围验证,再转换和使用。复杂计算最好提前赋给命名变量,方便记录和检查,而不是全挤在花括号里。
所有展开结果都放在双引号中,尾部的空格才会作为一个完整参数保留下来。不过引号不会把错误的默认值语法改成截取语法,这两件事需要分别检查。本文仅演示普通字符串;数组与位置参数的索引、长度规则还有差别,应单独查阅手册与测试。
处理中文等多字节内容时,还要核对运行环境的区域设置以及任务按字符还是按字节计数。不要直接套用本例固定 ASCII 的验收结果。把空串、短串、刚好足够、过长、零数量和含空格的输入各留一例,才能明确这段脚本真正承诺的截取行为。
资料核对日期:2026年10月2日(北京时间)。示例在 GNU Bash 5.2.37 中独立执行并通过断言。


