为什么此参数扩展替换在 bash 4.2 中失败但在 5.1 中有效?

Why does this parameter expansion replacement fail in bash 4.2 but work in 5.1?

我正在尝试将一些代码从 bash 5.1 移植到 4.2.46。一个试图从特定格式的字符串中去除颜色代码的函数停止工作。

这是这种格式的示例字符串 text。我为此打开了扩展的 globbing。

text="$(printf -- "%b%s%b" "\[\e[31m\]" "hello" "\[\e[0m\]")"
shopt -s extglob

在bash 5.1 中,此参数扩展用于删除所有颜色代码和转义字符

bash-5.1$ echo "${text//$'\[\e'\[/}"
31m\]hello0m\]
bash-5.1$ echo "${text//$'\[\e'\[+([0-9])/}"
m\]hellom\]
bash-5.1$ echo "${text//$'\[\e'\[+([0-9])m$'\]'/}"
hello

在 bash 4.2.46 中,我在构建参数扩展时开始出现不同的行为。

bash-4.2.46$ echo "${text//$'\[\e'\[/}"
m\]hello[=13=]m\]
bash-4.2.46$ echo "${text//$'\[\e'\[+([0-9])/}"
\[\]hello\[\]  ## no longer matches because `+([0-9])` doesn't follow `\[`

差异来自这一行:echo "${text//$'\[\e'\[/}"

bash-5.1:    31m\]hello0m\]
bash-4.2.46: m\]hello[=14=]m\]

这是 printf "%q" "${text//$'\[\e'\[/}" 显示的内容:

bash-5.1:    31m\\]hello0m\\]
bash-4.2.46: \31m\\]hello\0m\\]

4.2.26 中额外的 \ 来自哪里?

即使我尝试删除它,模式也停止匹配:

bash-4.2.46$ echo "${text//$'\[\e'\[\/}"
\[\]hello\[\]  ## no longer matches because `\` doesn't follow `\[`

我猜可能存在与参数扩展、反斜杠转义和扩展通配相关的错误。

我的目标是编写适用于 bash 4.0 之后的代码,因此我主要是在寻找解决方法。不过,解释(错误报告等)为什么会发生行为差异会很好。

似乎是 bash 中的错误。通过平分 available versions,我发现 4.2.53(1)-release 是最后一个有这个错误的版本。版本 4.3.0(1)-release 修复了这个问题。

list of changes 提到了这方面的一些错误修复。可能是以下错误修正之一:

This document details the changes between this version, bash-4.3-alpha, and the previous version, bash-4.2-release.
[...]
zz. When using the pattern substitution word expansion, bash now runs the replacement string through quote removal, since it allows quotes in that string to act as escape characters. This is not backwards compatible, so it can be disabled by setting the bash compatibility mode to 4.2.
[...]
eee. Fixed a logic bug that caused extended globbing in a multibyte locale to cause failures when using the pattern substititution word expansions.

解决方法

不使用 extglobs 的参数扩展,而是使用 bash 与实际正则表达式匹配的模式(在 bash 3.0.0 及更高版本中可用):

text=$'\[\e[31m\]hello\[\e[0m\]'
while [[ "$text" =~ (.*)$'\[\e['[0-9]*'m\]'(.*) ]]; do
  text="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
done
echo "$text"

或依赖外部(但 posix 标准化)工具,如 sed:

text=$'\[\e[31m\]hello\[\e[0m\]'
text=$(sed $'s#\\[\e[[0-9]*m\\]##g' <<< "$text")
echo "$text"

当在 " 引号内时,问题似乎是在 ${test//<here>} 内解析 $'...'

$ test='f() { "${text//\[$'\''\e'\''\[+([0-9])/}"; }; printf "%q\n" "$(declare -f f)"'; echo -n 'bash4.1 '; docker run bash:4.1 bash -c "$test" ; echo -n 'bash5.1 '; bash -c "$test"
bash4.1 $'f () \n{ \n    "${text//\[\E\[+([0-9])/}"\n}'
bash5.1 $'f () \n{ \n    "${text//\[\'\E\'\[+([0-9])/}"\n}'

只需使用一个变量。

esc=$'\e'
echo "${text//\\[$esc\[+([0-9])/}"