Bash 删除数组中间项:元素只剩两个,为什么索引仍然是零和二

前天 3阅读

脚本把任务名放进数组,删除已经取消的第二项,再按数组长度从零循环到末尾。输出却多了一项空内容,真正的最后任务没被处理。Bash 索引数组允许不连续的下标,unset 只移除指定元素,不会把后面的任务向前搬;元素数量因而不再等于最大下标加一。

下面从 alpha、beta、gamma 三项开始,删除索引一,逐步检查键、值、追加位置与重新压紧后的结果。保存为 demo.sh,用 bash demo.sh 运行,代码只操作当前脚本里的数组,不访问业务文件。每个关键步骤都有断言,任何预期不符都会返回非零状态。

按元素数量构造下标,可能访问错位置

Bash 删除数组中间项:元素只剩两个,为什么索引仍然是零和二

AI概念示意图:中间格子清空后,后方物件仍占原格,新物件放在尾端。图片不是运行截图。

set -u
items=(alpha beta gamma)
unset 'items[1]'
[[ ${#items[@]} == 2 ]] || exit 1
keys=("${!items[@]}")
[[ ${keys[*]} == '0 2' ]] || exit 2
wrong=()
for ((i=0; i<${#items[@]}; i++)); do
  wrong+=("${items[i]-MISSING}")
done
[[ ${wrong[*]} == 'alpha MISSING' ]] || exit 3
printf 'count-based loop: %s\n' "${wrong[*]}"

correct=()
for index in "${!items[@]}"; do
  correct+=("$index:${items[index]}")
done
[[ ${correct[*]} == '0:alpha 2:gamma' ]] || exit 4
printf 'actual entries: %s\n' "${correct[*]}"

items+=(delta)
[[ ${items[3]} == delta && ${items[1]+set} == '' ]] || exit 5
items[7]=''
[[ ${#items[@]} == 4 && ${items[7]+set} == set ]] || exit 6
keys=("${!items[@]}")
[[ ${keys[*]} == '0 2 3 7' ]] || exit 7
printf 'sparse keys: %s\n' "${keys[*]}"

dense=("${items[@]}")
dense_keys=("${!dense[@]}")
[[ ${dense_keys[*]} == '0 1 2 3' ]] || exit 8
[[ ${dense[1]} == gamma && ${dense[3]} == '' ]] || exit 9
[[ ${#dense[@]} == 4 ]] || exit 10
keys=("${!items[@]}")
[[ ${keys[*]} == '0 2 3 7' ]] || exit 11
printf 'dense keys: %s\n' "${dense_keys[*]}"
printf 'empty final element retained: yes\n'

删除后元素数为二,现存键却是零和二。错误循环访问零与一,因此得到 alpha 和 MISSING,根本没有走到保存 gamma 的索引二。这里用显式文字表示不存在的项,是为了把误读表现出来;生产代码随手补空字符串,会让问题更难发现。

如果只需要每个值,可以直接遍历双引号包围的数组全体值展开;如果还需要原下标,则遍历 ${!items[@]} 得到实际存在的键,再读取对应值。示例采用第二种方式,结果为 0:alpha 和 2:gamma,既没有虚构空项,也保留了原位置关系。

追加会跟在最大现存下标后面

items+=(delta) 将新值放到索引三,没有填补中间的一。接着给索引七赋一个空字符串,元素数变成四,而最大索引已经是七。空字符串是一个真实存在的数组元素,它与从未设置或已删除的位置不同,不能只靠字符串是否为空判断键是否存在。

脚本同时验证索引七已经设置、索引一没有设置,并打印当前所有键。这样的观察能解释为什么一个看起来只有几个可见词的数组,却含有更远的位置。对于任务编号或槽位本身有意义的集合,保留这些间隙可能正是正确行为,不能一律当成需要修复的数据。

需要连续列表时,再明确丢弃旧索引

dense=("${items[@]}") 取出当前各个值,按新的连续位置重建数组,结果索引变成零、一、二、三。gamma 从旧索引二移到了新索引一,末尾空字符串也被保留下来。压紧是一次有含义的数据变换,会改变位置身份,不只是调整显示格式。

若另一份数组或映射仍使用旧索引关联任务,单独压紧其中一份会打破对应关系。此时应把数据放在同一记录结构里,或同步建立旧键到新键的映射,而不是在每个数组上各自过滤后期待它们仍然对齐。本文的单数组示例没有维护外部引用。

引号保护边界,但不会改变索引语义

删除语句使用 unset 后接带单引号的数组引用,避免方括号先被路径名匹配解释。读取与复制全体值时使用双引号,保留包含空格或空字符串的元素边界。引号负责 shell 展开边界,却不会让稀疏下标自动变连续,这两层问题应分别验收。

最后检查原数组仍有零、二、三、七四个键,证明创建 dense 没有修改来源。脚本面向索引数组,不讨论关联数组的键顺序。给数组逻辑写回归时,应包括删除中间项、只剩高位项、空字符串和追加操作,才能暴露“长度就是下一个可用下标”的错误假设。

资料核对日期:2026年10月2日(北京时间)。示例在 GNU Bash 5.2.37 中独立运行。

参考资料

GNU Bash 官方手册:索引数组与 unset

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。