为什么[AZ]在bash中匹配小写字母?

为什么[AZ]在bash中匹配小写字母?

在我知道的所有 shell 中,rm [A-Z]*都会删除所有以大写字母开头的文件,但在 bash 中,这会删除所有以字母开头的文件。

由于此问题存在于使用 bash-3 和 bash-4 的 Linux 和 Solaris 上,因此它不可能是由 libc 中有缺陷的模式匹配器或错误配置的区域设置定义引起的错误。

这种奇怪且危险的行为是有意为之还是只是多年来存在未修复的错误?

答案1

请注意,当使用范围表达式(如 [az])时,可能会包含其他大小写的字母,具体取决于 LC_COLLATE 的设置。

LC_COLLATE是一个变量,它确定对路径名扩展结果进行排序时使用的排序规则,并确定路径名扩展和模式匹配中范围表达式、等价类和排序序列的行为。


考虑以下:

$ touch a A b B c C x X y Y z Z
$ ls
a  A  b  B  c  C  x  X  y  Y  z  Z
$ echo [a-z] # Note the missing uppercase "Z"
a A b B c C x X y Y z
$ echo [A-Z] # Note the missing lowercase "a"
A b B c C x X y Y z Z

请注意,调用该命令时echo [a-z],预期输出将是所有带有小写字符的文件。此外,对于echo [A-Z], 文件也应该包含大写字符。


与区域设置的标准排序规则en_US具有以下顺序:

aAbBcC...xXyYzZ
  • az(in )之间[a-z]均为大写字母,除了Z
  • AZ(in )之间[A-Z]都是小写字母,除了a

看:

     aAbBcC[...]xXyYzZ
     |              |
from a      to      z

     aAbBcC[...]xXyYzZ
      |              |
from  A     to       Z

如果您将LC_COLLATE变量更改为C它,它看起来如预期的那样:

$ export LC_COLLATE=C
$ echo [a-z]
a b c x y z
$ echo [A-Z]
A B C X Y Z

所以就是不是一个错误, 它是整理问题


您可以使用 POSIX 定义的范围表达式来代替范围表达式字符类,例如upperlower。他们还与不同的LC_COLLATE配置,甚至可以使用重音字符:

$ echo [[:lower:]]
a b c x y z à è é
$ echo [[:upper:]]
A B C X Y Z

答案2

[A-Z]in匹配所有排序后和排序前bash的排序元素(字符,但也称为字符序列,如匈牙利语言环境中的字符) 。在您所在的区域设置中,可能介于 B 和 C 之间。DszAZc

$ printf '%s\n' A a á b B c C Ç z Z Ẑ | sort
a
A
á
b
B
c
C
Ç
z
Z

所以corz会被 匹配[A-Z],但or不会被匹配a

$ printf '%s\n' A a á b B c C Ç z Z Ẑ |
pipe>  bash -c 'while IFS= read -r x; do case $x in [A-Z]) echo "$x"; esac; done'
A
á
b
B
c
C
Ç
z
Z

在 C 语言环境中,顺序为:

$ printf '%s\n' A a á b B c C Ç z Z Ẑ | LC_COLLATE=C sort
A
B
C
Z
a
b
c
z
Ç
á

因此[A-Z]会匹配A, B, C, Z, 但不匹配Ç,仍然不匹配

如果您想匹配大写字母(在任何脚本中),您可以使用[[:upper:]]。没有内置的方法bash可以只匹配大写字母拉丁脚本(除非单独列出它们)。

如果你想匹配AZ 英语没有变音符号的字母,您可以使用[A-Z][[:upper:]]但在C(假设数据不是以 BIG5 或 GB18030 等字符集编码的,其中有几个字符的编码包含这些字母的编码)或单独列出它们([ABCDEFGHIJKLMNOPQRSTUVWXYZ])。

请注意,外壳之间存在一些差异。

对于zsh, bash -O globasciiranges(bash-4.3 中引入的奇怪命名选项)schily-shyash,[A-Z]匹配代码点介于A和 之间的字符Z,因此相当于bashC 语言环境中 的行为。

对于 ash、mksh 和古老的 shell,与zsh上面相同,但仅限于单字节字符集。也就是说,例如,在 UTF-8 语言环境中,[É-Ź]不会匹配Ó,但因为那是[<c3><89>-<c5><b9>],所以它将匹配字节值 0x89 到 0xc5!

ksh93表现得像bash,只是它将视为结尾均以小写字母或大写字母开头的特殊情况范围。在这种情况下,它仅匹配在这些末端之间排序的整理元素,但它们(或多字符整理元素的第一个字符)小写(或分别为大写)。因此,[A-Z]会匹配 on É,但不会匹配 on ,e就像在 ande之间排序一样AZ但不是像Aand一样大写Z

对于fnmatch()模式(如find -name '[A-Z]')或系统正则表达式(如grep '[A-Z]'),它取决于系统和区域设置。例如,在此处的 GNU 系统上,在语言环境中[A-Z]与 on 不匹配,但在xen_GB.UTF-8th_TH.UTF-8。我不清楚它使用什么信息来确定这一点,但是它显然基于从 LC_COLLATE 语言环境数据派生的查找表)。

POSIX 允许所有行为,因为 POSIX 保留除 C 语言环境之外的语言环境中未指定范围的行为。现在我们可以争论每种方法的好处。

bash的方法很有意义,因为我们想要和[C-G]之间的字符。并使用用户的排序顺序来决定什么CG中间是最合乎逻辑的方法。

现在的问题是,它打破了很多人的期望,尤其是那些习惯了 Unicode 之前、甚至国际化之前的传统行为的人。虽然对于普通用户来说,[C-I]包含可能是有意义的,h因为h字母位于C和之间,I而不[A-g]包含Z,但对于仅处理 ASCII 数十年的人来说这是另一回事。

bash行为也不同于其他 GNU 工具中的范围匹配,[A-Z]例如 GNU 正则表达式(如grep/ sed...)或fnmatch().find -name

这也意味着[A-Z]匹配的内容会因环境、操作系统和操作系统版本而异。匹配 Á 但不匹配 Ź的事实[A-Z]也是次优的。

对于zsh/ yash,我们使用不同的排序顺序。我们不依赖用户的字符顺序概念,而是使用字符点代码值。这样做的好处是易于理解,但从很少的实际角度来看,除了 ASCII 之外,它并不是很有用。[A-Z]匹配 26 个美国英语大写字母,[0-9]匹配十进制数字。 Unicode 中有一些代码点遵循某些字母的顺序,但这不是通用的,也不能通用,因为使用相同脚本的不同人不一定就字母顺序达成一致。

对于传统的 shell 和 mksh、dash,它已经被破坏了(现在大多数人都使用多字节字符),但主要是因为它们还没有多字节支持。向bash和等 shell 添加多字节支持zsh已经付出了巨大的努力,并且仍在进行中。yash(日语 shell)最初设计时从一开始就支持多字节。

ksh93 的方法的优点是与系统的正则表达式或 fnmatch() 保持一致(或者至少在 GNU 系统上看起来如此)。在那里,它并没有打破一些人的期望,因为[A-Z]不包含小写字母,[A-Z]包含É(和 Á,但不包含 Ź)。它不符合sort或一般strcoll()顺序。

答案3

它的意图并记录在bash文档中,模式匹配部分。范围表达式[X-Y]将包含介于当前区域设置的整理序列和字符集之间XY使用当前区域设置的整理序列和字符集的任何字符:

LC_ALL=en_US.utf8 bash -c 'case b in [A-Z]) echo yes; esac' 
yes

您可以看到,在区域设置之间和区域设置中b排序。AZen_US.utf8

您可以通过一些选择来防止这种行为:

# Setting LC_ALL or LC_COLLATE to C
LC_ALL=C bash -c 'echo [A-Z]*'

# Or using POSIX character class
LC_ALL=C bash -c 'echo [[:upper:]]*'

或启用globasciiranges(使用 bash 4.3 及更高版本):

bash -O globasciiranges -c 'echo [A-Z]*'

答案4

区域设置可以更改 匹配的字符[A-Z]。使用

(LC_ALL=C; rm [A-Z]*)

以消除影响。 (我使用子 shell 来本地化更改)。

相关内容