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

sch*_*ily 45 shell bash wildcards locale

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

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

这种奇怪且冒险的行为是有意为之,还是只是一个多年未修复的错误?

cha*_*aos 71

请注意,当使用 [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
Run Code Online (Sandbox Code Playgroud)

请注意,echo [a-z]调用该命令时,预期输出将是所有具有小写字符的文件。此外,使用echo [A-Z], 需要大写字符的文件。


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

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

看:

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

     aAbBcC[...]xXyYzZ
      |              |
from  A     to       Z
Run Code Online (Sandbox Code Playgroud)

如果您将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
Run Code Online (Sandbox Code Playgroud)

所以,这不是错误,而是整理问题


您可以使用 POSIX 定义的字符类来代替范围表达式,例如upperor lower。它们也适用于不同的LC_COLLATE配置,甚至可以使用重音字符

$ echo [[:lower:]]
a b c x y z à è é
$ echo [[:upper:]]
A B C X Y Z
Run Code Online (Sandbox Code Playgroud)


Sté*_*las 25

[A-Z]inbash匹配Dsz排序之后A和排序之前的所有整理元素(字符,但调用也可以是像匈牙利语言环境中的字符序列)Z。在您的语言环境中,c可能介于 B 和 C 之间。

$ printf '%s\n' A a á b B c C Ç z Z ? | sort
a
A
á
b
B
c
C
Ç
z
Z
?
Run Code Online (Sandbox Code Playgroud)

所以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
Run Code Online (Sandbox Code Playgroud)

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

$ printf '%s\n' A a á b B c C Ç z Z ? | LC_COLLATE=C sort
A
B
C
Z
a
b
c
z
Ç
á
?
Run Code Online (Sandbox Code Playgroud)

所以[A-Z]会匹配A, B, C, Z, 但不是Ç,仍然不是?

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

如果你想匹配的A,以Z 英文字母没有变音符号,您可以使用[A-Z][[:upper:]],但在C区域(假设数据中的字符集一样BIG5或GB18030其中有几个字符,其编码未编码包含或列表中的那些字母的编码)他们分别([ABCDEFGHIJKLMNOPQRSTUVWXYZ])。

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

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

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

ksh93行为类似于,bash除了它将其视为以小写字母或大写字母开头的特殊情况范围。在这种情况下,它只匹配在这些结尾之间排序的整理元素,但它们(或它们的第一个字符用于多字符整理元素)也是小写(或分别为大写)。因此,[A-Z]将有匹配É,但不是e作为e之间的排序不AZ,但不会像大写AZ

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

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

bash的方法很有意义,因为[C-G]我们想要介于C和之间的字符G。使用用户的排序顺序来确定介于两者之间的内容是最合乎逻辑的方法。

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

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

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

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

对于传统的 shell 和 mksh、dash,它已经坏了(现在大多数人使用多字节字符),但主要是因为它们还没有多字节支持。添加到像炮弹多字节的支持bash,并zsh得到了巨大的努力,目前仍在进行。yash(日语外壳)最初设计时从一开始就支持多字节。

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

  • @schily,我提到`sort`,因为`bash` globs 基于字符排序顺序。我目前无法访问如此旧版本的 `bash`,但我可以稍后查看。那时候不一样吗? (2认同)
  • @schily,请注意 `\xFF` 是 _byte_ 0xFF,而不是字符 U+00FF(`ÿ` 本身编码为 0xC3 0xBF)。`\xFF` 本身不能形成有效字符,所以我不明白为什么它应该与 `[É-Ź]` 匹配。 (2认同)

cuo*_*glm 9

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

LC_ALL=en_US.utf8 bash -c 'case b in [A-Z]) echo yes; esac' 
yes
Run Code Online (Sandbox Code Playgroud)

你可以看到,在locale和localeb之间排序。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:]]*'
Run Code Online (Sandbox Code Playgroud)

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

bash -O globasciiranges -c 'echo [A-Z]*'
Run Code Online (Sandbox Code Playgroud)


bis*_*hop 6

我在一个新的 Amazon EC2 实例上观察到了这种行为。由于 OP 没有提供MCVE,我将发布一个:

$ cd $(mktemp -d)
$ touch foo
$ echo [A-Z]*     # prepare for a surprise!
foo

$ echo $BASH_VERSION
4.1.2(1)-release
$ uname -a
Linux spinup-tmp12 3.14.27-25.47.amzn1.x86_64 #1 SMP Wed Dec 17 18:36:15 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ env | grep LC_  # no locale, let's set one
$ LC_ALL=C
$ echo [A-Z]*
[A-Z]*

$ unset LC_ALL    # ok, good. what if we go back to no locale?
$ echo [A-Z]*
foo
Run Code Online (Sandbox Code Playgroud)

因此,没有我的LC_*设置会导致 Linux 上的 bash 4.1.2(1)-release 产生明显奇怪的行为。我可以通过设置和取消设置相应的语言环境变量来可靠地切换奇怪的行为。不出所料,通过导出,这种行为似乎是一致的:

$ export LC_ALL=C
$ bash
$ echo [A-Z]*
[A-Z]*
$ exit
$ echo $SHLVL
1
$ unset LC_ALL
$ bash
$ echo [A-Z]*
foo
Run Code Online (Sandbox Code Playgroud)

虽然我看到 bash 表现得像 Stéphane "Shellshock" Chazelas回答的那样,但我认为关于模式匹配bash 文档有问题

例如,默认的 C语言环境中,'[a-dx-z]' 等价于 '[abcdxyz]'

我将这句话(强调我的)读为“如果未设置相关的语言环境变量,那么 bash 将默认为 C 语言环境”。Bash 似乎没有这样做。相反,它似乎默认为字符按字典顺序排序的语言环境,并带有变音符号折叠:

$ echo [A-E]*
[A-E]*
$ echo [A-F]*
foo
$ touch "évocateur"
$ echo [A-F]*
foo évocateur
Run Code Online (Sandbox Code Playgroud)

我认为 bash 最好记录下LC_*(特别是LC_CTYPELC_COLLATE)未定义时的行为。但与此同时,我将分享一些智慧

...您必须非常小心[字符范围],因为除非正确配置,否则它们不会产生预期的结果。现在,您应该避免使用它们并改用字符类。

如果你真的很合适,和/或正在为多语言环境编写脚本,最好确保在匹配文件时知道你的语言环境变量是什么,或者确保你在编码完全通用的方式。


更新基于@G-Man 评论,让我们更深入地了解正在发生的事情:

$ env | grep LANG
LANG=en_US.UTF-8
Run Code Online (Sandbox Code Playgroud)

啊哈!这解释了之前看到的排序规则。让我们删除所有语言环境变量:

$ unset LANG LANGUAGE LC_ALL
$ env | grep 'LC_|LANG'
$ echo [A-Z]*
[A-Z]*
Run Code Online (Sandbox Code Playgroud)

我们走了。现在 bash 就该 Linux 系统上的文档而言始终如一地运行。如果任何语言环境变量的设定(LANGUAGELANGLC_COLLATELC_CTYPELC_ALL,等等),然后击使用根据其手册那些。否则,bash 会退回到 C。

Wooledge bash的常见问题有这样一段话:

在最新的 GNU 系统上,变量按此顺序使用。如果设置了 LANGUAGE,则使用它,除非 LANG 设置为 C,在这种情况下 LANGUAGE 将被忽略。此外,有些程序根本不使用 LANGUAGE。否则,如果设置了 LC_ALL,则使用它。否则,如果设置了涵盖此用法的特定 LC_* 变量,请使用该变量。(例如,LC_MESSAGES 包含错误消息。)否则,使用 LANG。

因此,可以通过查看所有语言环境驱动变量的总和来解释操作和文档中的明显问题。


cho*_*oba 3

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

(LC_ALL=C; rm [A-Z]*)
Run Code Online (Sandbox Code Playgroud)

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

  • 这不起作用,因为 glob 是在 rm 执行之前完成的。首先尝试“export LC_ALL=C”。 (7认同)