为什么 bash 会删除其他数字?

为什么 bash 会删除其他数字?

关于这一点(这是不是旨在成为一个范围,但却是一个明确的列表):

$ a='0123456789 ٠١٢٣٤٥٦٧٨٩ ۰۱۲۳۴۵۶۷۸۹ ߀߁߂߃߄߅߆߇߈߉ ०१२३४५६७८९'
$ echo "${a//[0123456789]}"
  ۰۱۲۳۴۵۶۷۸۹ ߀߁߂߃߄߅߆߇߈߉ ०१२३४५६७८९

Bash 错误地(IMO)删除了数字٠١٢٣٤٥٦٧٨٩(第二组)。


字符都不同(手工格式化):

$ for c in $(echo "$a" | grep -o .); do printf '\\U%04x ' "'$c"; done; echo
\U0030 \U0031 \U0032 \U0033 \U0034 \U0035 \U0036 \U0037 \U0038 \U0039
\U0660 \U0661 \U0662 \U0663 \U0664 \U0665 \U0666 \U0667 \U0668 \U0669
\U06f0 \U06f1 \U06f2 \U06f3 \U06f4 \U06f5 \U06f6 \U06f7 \U06f8 \U06f9
\U07c0 \U07c1 \U07c2 \U07c3 \U07c4 \U07c5 \U07c6 \U07c7 \U07c8 \U07c9
\U0966 \U0967 \U0968 \U0969 \U096a \U096b \U096c \U096d \U096e \U096f

分别对应:

123456789    # Hindu-Arabic Arabic numerals
٠١٢٣٤٥٦٧٨٩   # ARABIC-INDIC
۰۱۲۳۴۵۶۷۸۹   # EXTENDED ARABIC-INDIC/PERSIAN
߀߁߂߃߄߅߆߇߈߉  # NKO DIGIT
०१२३४५६७८९   # DEVANAGARI

为了确保从该网站粘贴时不会出现问题,还可以a使用 Unicode 转义将此 Unicode 内容生成到变量中:

a=$(echo -e '\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039 \u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669 \u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9 \u07c0\u07c1\u07c2\u07c3\u07c4\u07c5\u07c6\u07c7\u07c8\u07c9 \u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f')

或者直接使用$'...'接受转义的字符串:

a=$'\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039 \u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669 \u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9 \u07c0\u07c1\u07c2\u07c3\u07c4\u07c5\u07c6\u07c7\u07c8\u07c9 \u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'

其他 shell 不能像 bash 那样工作(手动格式化):

$ for sh in zsh ksh lksh mksh bash; do $sh -c 'a="0123456789 ٠١٢٣٤٥٦٧٨٩ ۰۱۲۳۴۵۶۷۸۹ ߀߁߂߃߄߅߆߇߈߉ ०१२३४५६७८९"; echo "$0 : ${a//[0123456789]}" $sh'; done
zsh  :  ٠١٢٣٤٥٦٧٨٩ ۰۱۲۳۴۵۶۷۸۹ ߀߁߂߃߄߅߆߇߈߉ ०१२३४५६७८९
ksh  :  ٠١٢٣٤٥٦٧٨٩ ۰۱۲۳۴۵۶۷۸۹ ߀߁߂߃߄߅߆߇߈߉ ०१२३४५६७८९
lksh :  ٠١٢٣٤٥٦٧٨٩ ۰۱۲۳۴۵۶۷۸۹ ߀߁߂߃߄߅߆߇߈߉ ०१२३४५६७८९
mksh :  ٠١٢٣٤٥٦٧٨٩ ۰۱۲۳۴۵۶۷۸۹ ߀߁߂߃߄߅߆߇߈߉ ०१२३४५६७८९
bash :   ۰۱۲۳۴۵۶۷۸۹ ߀߁߂߃߄߅߆߇߈߉ ०१२३४५६७८९

bash 排序顺序是:

$ mkdir test1; cd test1; IFS=$' \t\n'
$ touch $(echo "$a" | grep -o .)
$ printf '%s' *; echo
߃߇߆߁߂߅߉߄߀߈0٠०۰1١१۱٢2२۲3٣३۳٤4४۴٥5५۵٦6६۶7٧७۷8٨८۸٩9९۹

$ locale
LANG=en_US.utf8
LANGUAGE=
LC_CTYPE="en_US.utf8"
LC_NUMERIC="en_US.utf8"
LC_TIME="en_US.utf8"
LC_COLLATE="en_US.utf8"
LC_MONETARY="en_US.utf8"
LC_MESSAGES="en_US.utf8"
LC_PAPER="en_US.utf8"
LC_NAME="en_US.utf8"
LC_ADDRESS="en_US.utf8"
LC_TELEPHONE="en_US.utf8"
LC_MEASUREMENT="en_US.utf8"
LC_IDENTIFICATION="en_US.utf8"
LC_ALL=

它似乎没有应用排序顺序来删除字符。

无论如何,它不应该(IMO),因为字符被明确列出。

所以为什么?


这里使用 bash 4.4.12。但它也会在 3.0、3.2、4.0、4.1、4.4.23、5.0 上失败,但在 2.0.1 和 2.0.5 上不会失败。看来 3.0 中的更改导致了这个问题。

答案1

我设法在 Ubuntu 17.10 (glibc 2.26) 和 Ubuntu 18.04 (glibc 2.27) 上重现这个问题,但它似乎在 Ubuntu 18.10 (glibc 2.28) 上得到了修复

问题在于 localedata,更具体地说是 en_US.utf8 的 LC_COLLATE 数据(实际上,该排序规则数据来自大多数语言环境中包含的 ISO 14651 文件,因此它可能也会影响所有其他 utf8 语言环境。)

localeddata 来自 glibc,并且该错误似乎存在于该处(尽管发行版对该数据的自定义程度相当高,因此 glibc <2.28 的其他发行版可能不会出现此问题。)

事实上,glibc 2.28 公告开始列出新功能:

ISO 14651 的本地化数据已更新,以匹配该标准的 2016 年第 4 版版本,这与 Unicode 9.0.0 提供的数据匹配。此更新对 Unicode 字符的排序规则进行了重大改进。

查看提交,这是对本地数据的巨大修改,所以这可能就是修复错误的原因!

简而言之,这两个符号(U0030,即“0”,U0660,即阿拉伯-印度语零“٠”)的排序问题在于,当使用strcoll(3),可以通过这个简短的测试来演示(在幕后sort使用):strcoll

ubuntu-18.04$ { echo 0; echo -e '\u0660'; echo 0; } | sort
0
٠
0

在 glibc 2.28 上:

ubuntu-18.10$ { echo 0; echo -e '\u0660'; echo 0; } | sort
0
0
٠

正如您所看到的,在较旧的 glibc 上,它不会对阿拉伯-印度语零“٠”进行重新排序,无论是在“0”之前还是之后,这证明它们的校对相同。

查看 glibc 源代码,我们可以理解为什么会出现问题。

在里面ISO 14651 的 glibc 2.27 源,可以找到如下定义:

<U0030> <0>;<BAS>;<MIN>;IGNORE # 171 0
<U0660> <0>;<BAS>;<MIN>;IGNORE
<U06F0> <0>;<PCL>;<MIN>;IGNORE
<U0966> <0>;"<BAS><NUM>";"<MIN><MIN>";IGNORE

因此,'0' ( \u0030) 和 '٠' ( \u0660) 都扩展为完全相同的序列 ( <0>;<BAS>;<MIN>;IGNORE),这意味着strcoll将以相同方式对待它们。 (这也解释了为什么其他字符如\u06f0\u0966不受影响,因为它们的扩展是不同的。)

看着ISO 14651 的 glibc 2.28 源,现在找到以下定义:

<U0030> <S0030>;<BASE>;<MIN>;<U0030> % DIGIT ZERO
<U0660> <S0030>;<BASE>;<MIN>;<U0660> % ARABIC-INDIC DIGIT ZERO
<U06F0> <S0030>;<BASE>;<MIN>;<U06F0> % EXTENDED ARABIC-INDIC DIGIT ZERO
<U07C0> <S0030>;<BASE>;<MIN>;<U07C0> % NKO DIGIT ZERO
<U0966> <S0030>;<BASE>;<MIN>;<U0966> % DEVANAGARI DIGIT ZERO

第四个字段现在始终填充代码点本身,这意味着即使前几个字段匹配,它们也将具有定义的排序顺序。虽然<U0660>没有引入更改这个特定的提交,它的描述解释了这个想法:

[...] 将字符的代码点放入第四级而不是“忽略”。如果没有这种更改,所有这些字符将比较相等,这将使 wcscoll 测试用例失败。即使对于这样的字符,最好也有一个明确定义的排序顺序,因此最好使用代码点作为平局。

  • localedata/locales/iso14651_t1_common:对于在所有 4 个级别上都有 IGNORE 的所有条目,使用第四个排序规则级别中字符的代码点,而不是 IGNORE。

希望这能解释 glibc <2.28 中 localedata 的错误以及 glibc 2.28 中的修复。


关于 bash,如果你看一下源代码,您将看到它处理0方括号表达式 ( ) 中的单个字符 ( ) ,就像处理以该字符作为开始和结束 ( )[0]的范围一样:[0-0]

cstart = cend = FOLD (cstart);

然后将当前字符与该范围进行比较使用 RANGECMP:

if (RANGECMP (test, cstart, forcecoll) >= 0 && RANGECMP (test, cend, forcecoll) <= 0)
  goto matched;

然后是 RANGECMP(定义为rangecmp_wc多字节模式)使用 wcscoll(3)(这是 strcoll 的多字节版本):

return (wcscoll (s1, s2));

事实上,bash 对单个字符使用范围比较(作为一种快捷方式,共享一些处理范围的代码),因此它接受所有排序相同的字符以及原始字符。

其他 shell 可能没有这个问题,因为如果不涉及范围,它们会进行直接比较。

这个问题开始出现在 bash 3.0 上的原因是 bash 3.0 引入了对多字节 (Unicode) 的支持,最终重构了所有这些代码,并可能使用了与该问题相关的区域设置感知比较。

更新:这个问题是报告为错误到 bash 项目@艾萨克


解决方法:如果升级到使用 glibc 2.28 的发行版不可行,则可能的解决方法是使用LC_COLLATE=C.utf8POSIX.utf8定义一个“简单”排序顺序,其中没有代码点会进行相同的排序。考虑到问题在于排序规则,LC_COLLATE仅设置就足够了。在 Ubuntu 17.10 和 18.04 上测试此解决方法表明它足以解决此问题。

相关内容