![在 ZSH 中,如何取消设置任意关联数组元素?](https://linux22.com/image/185472/%E5%9C%A8%20ZSH%20%E4%B8%AD%EF%BC%8C%E5%A6%82%E4%BD%95%E5%8F%96%E6%B6%88%E8%AE%BE%E7%BD%AE%E4%BB%BB%E6%84%8F%E5%85%B3%E8%81%94%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%EF%BC%9F.png)
我的关联数组具有任意键,包括包含反引号、括号等的键:
$ typeset -A arr
$ key='`'
$ arr[$key]=backquote
$ echo $arr[$key]
backquote
我现在需要取消设置arr[$key]
。通过浏览网络,我尝试了:
$ unset "arr[$key]"
unset: arr[`]: invalid parameter name
$ unset "arr[${(b)key}]"
unset: arr[`]: invalid parameter name
……没有运气。现在,我有点幸运,因为这提供了一条错误消息;在以下情况下,除了结果之外,似乎没有什么会失败:
$ typeset -A arr
$ key='?~>#'
$ arr[$key]=symbols
$ echo "$arr[$key]"
symbols
$ unset "arr[${(b)key}]"
$ echo "$arr[$key]"
symbols
事实上,中的任何符号?~>#
都会触发相同的行为。
对正在发生的事情以及如何获得预期行为有任何澄清吗?
答案1
哇,这真是一团糟。
自从Bart Schaefer 的 2016 年补丁,合并为提交 95663e936596933d529a648ed3d6c707d1a1dffe 中的补丁 37914并首次在 zsh 5.4 中发布,实验性地,unset "arr[$key]"
只要key
不包含以下任何字符即可工作:\`()[]
。这六个字符必须以反斜杠为前缀,其他字符不得以反斜杠为前缀(例如unset 'arr[\*] arr[\;]'
尝试取消设置键\*
and \;
、 not*
和;
)。这不是任何引用的内容参数扩展标志(${(b)key}
及其${(q)key}
变体) 可以。
此外,还有一个额外的问题:我找不到取消设置空键的方法。unset 'arr[]'
是一个错误,任何其他操作都会取消设置非空键。我发现取消设置空键的唯一解决方法是使用下标标志过滤掉不需要的键(正如 Stéphane Chazelas 在2018 zsh-workers 线程)。
以下函数适用于 zsh ≥5.4(在 zsh 5.8 中测试),并且仅在需要删除空键时才复制数组。
# Usage: unset_keys ARRAY [KEY]...
# ARRAY must be the name of an associative array parameter.
# Equivalent to unset 'ARRAY[KEY1]' 'ARRAY[KEY2]' ...
# except that this function works correctly even with keys containing
# special characters or is empty. See
# https://unix.stackexchange.com/questions/626393/in-zsh-how-do-i-unset-an-arbitrary-associative-array-element
function unset_keys {
emulate -LR zsh
if [[ -${(Pt)1}- != *-association-* ]]; then
return 120 # Fail early if $1 is not the name of an associative array
fi
if ((${#@[2,$#]:#?*})); then
if [[ -n ${${(P)1}[${:-}]+y} ]]; then
# Copy all entries with non-empty keys
: "${(AAP)1::=${(@kv)${(P)1}[(I)?*]}}"
fi
set -- $@ # Remove empty keys from the to-do list
fi
if (($# < 2)); then
return 0
fi
set -- "$1" "${@[2,$#]//\\/\\\\}"
set -- "$1" "${@[2,$#]//\`/\\\`}"
set -- "$1" "${@[2,$#]//\(/\\(}"
set -- "$1" "${@[2,$#]//\)/\\)}"
set -- "$1" "${@[2,$#]//\[/\\[}"
set -- "$1" "${@[2,$#]//\]/\\]}"
noglob unset $1[${^@[2,$#]}]
}
这是一个更简单的函数,无论如何都只执行一份副本。
# Usage: unset_keys ARRAY [KEY]...
# ARRAY must be the name of an associative array parameter.
# Equivalent to unset 'ARRAY[KEY1]' 'ARRAY[KEY2]' ...
# except that this function works correctly even with keys containing
# special characters or is empty. See
# https://unix.stackexchange.com/questions/626393/in-zsh-how-do-i-unset-an-arbitrary-associative-array-element
function unset_keys {
emulate -LR zsh
setopt extended_glob
if [[ -${(Pt)1}- != *-association-* ]]; then
return 120 # Fail early if $1 is not the name of an associative array
fi
set -- "$1" "${(j:|:)${(@b)@[2,$#]}}"
# Copy all entries except the specified ones
: "${(AAP)1::=${(@kv)${(P)1}[(I)^($~2)]}}"
}
在 zsh 5.4 之前,这是一个我没有探索过的不同混乱。
这是我使用的测试工具。我认为它提供了合理的覆盖范围,但我没有花任何时间对其进行完善。
set -e
test_keys=(
'()safe' '(r)set' '(R)set' '(k)safe' # look like valid subscript flags
'(a' '(a)' '(n:foo:)a' '(n:1)a' # look like invalid subscript flags
'set' '"set"' \'set\' '\s\e\t'
'safe' '"safe"' \'safe\' '\s\a\f\e'
'\\' '\\\' '\\\\' '""' \'\'
'two words' 'two spaces' ' initial space' 'trailing space '
$'\x80\\' $'\x80\`' $'\x80\~' # broken UTF-8
''
'?~>#'
)
for ((i=0; i<255; i++)); do
printf -v n '\\x%02x' $i
eval "test_keys+=(\$'$n')"
done
function populate_test_array {
for k in "${(@)test_keys}"; do
arr[$k]=set
done
}
function check_expected_keys {
local -a actual_keys
actual_keys=("${(@k)arr}")
actual_keys=("${(@o)actual_keys}") # Sorting in one step seems to misplace the empty string at the end (zsh 5.8 on Ubuntu 20.04), so sort in two steps.
local actual_list="${(j: :)${(@qqqq)actual_keys}}"
local expected_list="${(j: :)${(@qqqq)expected_keys}}"
if [[ "$actual_list" != "$expected_list" ]]; then
<<EOF
Failure: unexpected list of keys after $1
expected: $expected_list
actual : $actual_list
EOF
((++errors))
fi
}
typeset -A arr
errors=0
populate_test_array
expected_keys=("${(@o)test_keys}")
test_keys=("${(@)test_keys:#safe}") # [safe] must stay until the end
for k in "${(@)test_keys}"; do
unset_keys arr "$k"
if (($+arr[$k])); then
printf 'Failure: unset %s did not unset it\n' "${(qq)k}"
((++errors))
else
expected_keys=("${(@)expected_keys:#"$k"}")
fi
check_expected_keys "unset ${(qq)k}"
done
populate_test_array
unset_keys arr "${(@)test_keys}"
expected_keys=(safe)
check_expected_keys "unsetting all"
exit $((!!errors))