web-dev-qa-db-fra.com

Pourquoi [A-Z] fait-il correspondre les lettres minuscules dans bash?

Dans tous les obus que je connais, rm [A-Z]* supprime tous les fichiers commençant par une lettre majuscule, mais avec bash cela supprime tous les fichiers commençant par une lettre.

Comme ce problème existe sous Linux et Solaris avec bash-3 et bash-4, il ne peut pas être un bogue provoqué par un correcteur de modèle de bogue dans libc ou une définition de paramètres régionaux mal configurée.

Ce comportement étrange et risqué est-il prévu ou s'agit-il simplement d'un bug qui existe sans correction depuis de nombreuses années?

43
schily

Notez que lorsque vous utilisez des expressions de plage comme [a-z], les lettres de l'autre cas peuvent être incluses, selon le paramètre de LC_COLLATE.

LC_COLLATE est une variable qui détermine l'ordre de classement utilisé lors du tri des résultats de l'expansion du nom de chemin et détermine le comportement des expressions de plage, des classes d'équivalence et des séquences de classement dans l'expansion du nom de chemin et la correspondance de modèle.


Considérer ce qui suit:

$ 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

Remarquez quand la commande echo [a-z] est appelé, la sortie attendue serait tous les fichiers avec des minuscules. De plus, avec echo [A-Z], des fichiers avec des majuscules sont attendus.


Collations standard avec des paramètres régionaux tels que en_US ont l'ordre suivant:

aAbBcC...xXyYzZ
  • Entre a et z (dans [a-z]) sont TOUTES les lettres majuscules, à l'exception de Z.
  • Entre A et Z (dans [A-Z]) sont TOUTES les lettres minuscules, à l'exception de a.

Voir:

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

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

Si vous modifiez le LC_COLLATE variable à C il ressemble comme prévu:

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

Donc, ce n'est pas un bug , c'est un problème de classement .


Au lieu d'expressions de plage, vous pouvez utiliser des POSIX définis classes de caractères , tels que upper ou lower. Ils travaillent également avec différents LC_COLLATE configurations et même avec caractères accentués :

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

[A-Z] Dans bash correspond à tous les éléments de classement (caractères mais appelez également une séquence de caractères comme Dsz dans les paramètres régionaux hongrois) qui trient après A et trient avant Z. Dans votre région, c trie probablement entre B et C.

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

Ainsi, c ou z correspondrait à [A-Z], Mais pas Ni 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

Dans l'environnement local C, l'ordre serait le suivant:

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

Donc [A-Z] Correspondrait à A, B, C, Z, mais pas Ç Et toujours pas .

Si vous souhaitez faire correspondre les lettres majuscules (dans n'importe quel script), vous pouvez utiliser [[:upper:]] À la place. Il n'y a aucun moyen intégré dans bash pour faire correspondre uniquement les lettres majuscules dans le script latin (sauf en les listant individuellement).

Si vous souhaitez faire correspondre les lettres A à Z English sans signes diacritiques, vous pouvez utiliser [A-Z] Ou [[:upper:]] mais dans la locale C (en supposant que les données ne sont pas encodées dans des jeux de caractères comme BIG5 ou GB18030 qui a plusieurs caractères dont l'encodage contient l'encodage de ces lettres) ou les lister individuellement ([ABCDEFGHIJKLMNOPQRSTUVWXYZ]).

Notez qu'il existe une certaine variation entre les coques.

Pour zsh, bash -O globasciiranges (Option étrangement nommée introduite dans bash-4.3), schily-sh Et yash, [A-Z] Correspond aux caractères dont le point de code se situe entre celui de A et celui de Z, il serait donc équivalent au comportement de bash dans l'environnement local C.

Pour les cendres, les mksh et les coquilles anciennes, comme zsh ci-dessus, mais limité aux jeux de caractères à un octet. Autrement dit, dans un environnement local UTF-8 par exemple, [É-Ź] Ne correspondrait pas à Ó, Mais comme c'est [<c3><89>-<c5><b9>], Cela correspondrait aux valeurs d'octet 0x89 à 0xc5!

ksh93 Se comporte comme bash sauf qu'il traite comme des plages de cas spéciaux dont les extrémités commencent par des lettres minuscules ou des lettres majuscules. Dans ce cas, il ne correspond qu'aux éléments de classement triés entre ces extrémités, mais qui sont (ou leur premier caractère pour les éléments de classement à plusieurs caractères) aussi minuscules (ou majuscules respectivement). Ainsi, [A-Z] Il y aurait correspondance sur É, Mais pas sur e car e trie entre A et Z mais n'est pas en majuscule comme A et Z.

Pour les modèles fnmatch() (comme dans find -name '[A-Z]') Ou les expressions régulières du système (comme dans grep '[A-Z]'), Cela dépend du système et des paramètres régionaux. Par exemple, sur un système GNU ici, [A-Z] Ne correspond pas à x dans les paramètres régionaux en_GB.UTF-8, Mais il le fait dans le th_TH.UTF-8 Un. Je ne sais pas exactement quelles informations il utilise pour le déterminer, mais il est apparemment basé sur une table de recherche dérivée des données locales LC_COLLATE ).

Tous les comportements sont autorisés par POSIX car POSIX laisse le comportement des plages non spécifié dans les paramètres régionaux autres que les paramètres régionaux C. Nous pouvons maintenant discuter des avantages de chaque approche.

L'approche de bash a beaucoup de sens car avec [C-G], Nous voulons les caractères entre C et G. Et utiliser l'ordre de tri de l'utilisateur pour déterminer ce qui détermine ce qui est entre les deux est l'approche la plus logique.

Maintenant, le problème est qu'il brise les attentes de beaucoup de gens, en particulier ceux qui sont habitués au comportement traditionnel des jours pré-Unicode, même pré-internationalisation. Alors que pour un utilisateur normal, il peut sembler que [C-I] Inclut h car la lettre h se situe entre C et I et que [A-g] N'inclut pas Z, c'est une question différente pour les personnes ayant traité avec ASCII seulement pendant des décennies.

Ce comportement bash est également différent de la plage [A-Z] Correspondant dans d'autres outils GNU comme dans GNU expressions régulières (comme dans grep/sed ...) ou fnmatch() comme dans find -name.

Cela signifie également que ce que [A-Z] Correspond varie en fonction de l'environnement, du système d'exploitation et de la version du système d'exploitation. Le fait que [A-Z] Corresponde à Á mais pas à Ź est également sous-optimal.

Pour zsh/yash, nous utilisons un ordre de tri différent. Au lieu de s'appuyer sur la notion de l'ordre des caractères de l'utilisateur, nous utilisons les valeurs de code de point de caractère. Cela a l'avantage d'être facile à comprendre, mais d'un point de vue pratique de peu, en dehors de l'ASCII, ce n'est pas très utile. [A-Z] Correspond aux 26 lettres majuscules anglais américain, [0-9] Correspond aux chiffres décimaux. Il y a des points de code dans Unicode qui suivent l'ordre de certains alphabets mais ce n'est pas généralisé et ne peut pas être généralisé car de toute façon différentes personnes utilisant un même script ne sont pas nécessairement d'accord sur l'ordre des lettres.

Pour les shells traditionnels et mksh, dash, c'est cassé (maintenant que la plupart des gens utilisent des caractères multi-octets), mais principalement parce qu'ils n'ont pas encore de support multi-octets. L'ajout d'une prise en charge multi-octets à des shells comme bash et zsh a été un énorme effort et est toujours en cours. yash (un shell japonais) a été initialement conçu avec une prise en charge multi-octets dès le départ.

l'approche de ksh93 a l'avantage d'être cohérente avec les expressions régulières du système ou fnmatch () (ou au moins semble au moins sur les systèmes GNU). Là, cela ne brise pas les attentes de certaines personnes car [A-Z] N'inclut pas de lettres minuscules, [A-Z] Inclut É (Et Á, mais pas Ź). Ce n'est pas cohérent avec sort ou généralement strcoll() ordre.

25
Stéphane Chazelas

Il est prévu et documenté dans la documentation bash, section de correspondance de modèles . L'expression de plage [X-Y] sera inclus tous les caractères entre X et Y en utilisant la séquence de classement et le jeu de caractères des paramètres régionaux actuels:

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

Vous pouvez voir, b trié entre A et Z dans en_US.utf8 locale.

Vous avez le choix pour éviter ce comportement:

# 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:]]*'

ou activez globasciiranges (avec bash 4.3 et supérieur):

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

J'ai observé ce comportement sur une nouvelle instance Amazon EC2. Puisque l'OP n'a pas offert de MCVE , j'en posterai un:

$ 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

Donc, ne pas avoir mon LC_* set leads bash 4.1.2 (1) -release sur Linux pour produire un comportement apparemment étrange. Je peux basculer de manière fiable le comportement étrange en définissant et en désactivant les variables locales respectives. Sans surprise, ce comportement semble cohérent lors de l'exportation:

$ export LC_ALL=C
$ bash
$ echo [A-Z]*
[A-Z]*
$ exit
$ echo $SHLVL
1
$ unset LC_ALL
$ bash
$ echo [A-Z]*
foo

Pendant que je vois bash se comporter comme Stéphane "Shellshock" Chazelas réponse , je pense que documentation bash sur la correspondance de motifs est bogué:

Par exemple, dans la région C par défaut, "[a-dx-z]" est équivalent à "[abcdxyz]"

J'ai lu cette phrase (soulignement le mien) comme "si les variables locales pertinentes ne sont pas définies, alors bash sera par défaut la locale C". Bash ne semble pas faire ça. Au lieu de cela, il semble être par défaut à un lieu où les caractères sont triés dans l'ordre du dictionnaire avec un pliage diacritique:

$ echo [A-E]*
[A-E]*
$ echo [A-F]*
foo
$ touch "évocateur"
$ echo [A-F]*
foo évocateur

Je pense qu'il serait bon pour bash de documenter comment il se comportera lorsque LC_* (Plus précisément LC_CTYPE et LC_COLLATE) ne sont pas définis. Mais en attendant, je vais partager quelques sagesse :

... vous devez être très prudent avec les [plages de caractères] car elles ne produiront les résultats attendus que si elles sont correctement configurées. Pour l'instant, vous devez éviter de les utiliser et utiliser des classes de caractères à la place.

et

Si vous êtes vraiment correct et/ou que vous créez un script pour un environnement multi-paramètres régionaux, il est probablement préférable de vous assurer que vous savez quelles sont vos variables locales lorsque vous faites correspondre des fichiers, ou pour être sûr que vous codez dans un manière complètement générique.


pdate Sur la base du commentaire de @ G-Man, regardons plus en profondeur ce qui se passe:

$ env | grep LANG
LANG=en_US.UTF-8

Ah, ha! Cela explique la collation vue précédemment. Supprimons toutes les variables locales:

$ unset LANG LANGUAGE LC_ALL
$ env | grep 'LC_|LANG'
$ echo [A-Z]*
[A-Z]*

Et voilà. Maintenant, bash fonctionne de manière cohérente en ce qui concerne la documentation sur ce système Linux. Si l'une des variables de paramètres régionaux est définie (LANGUAGE, LANG, LC_COLLATE, LC_CTYPE, LC_ALL, etc.) puis Bash les utilise conformément à son manuel. Sinon, bash retombe en C.

La Wooledge bash FAQ a ceci à dire:

Sur les systèmes récents GNU, les variables sont utilisées dans cet ordre. Si LANGUAGE est défini, utilisez-le, sauf si LANG est défini sur C, auquel cas LANGUAGE est ignoré. En outre, certains programmes n'utilisez pas du tout LANGUAGE. Sinon, si LC_ALL est défini, utilisez-le. Sinon, si la variable LC_ * spécifique qui couvre cette utilisation est définie, utilisez-la. (Par exemple, LC_MESSAGES couvre les messages d'erreur.) Sinon, utilisez LANG.

Ainsi, le problème apparent, à la fois dans le fonctionnement et la documentation, peut être expliqué en regardant la somme totale de toutes les variables de conduite locales.

6
bishop

Les paramètres régionaux peuvent modifier les caractères correspondants par [A-Z]. Utilisation

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

pour éliminer l'influence. (J'ai utilisé un sous-shell pour localiser le changement).

3
choroba

Comme cela a déjà été dit, il s'agit d'un problème d '"ordre de classement".

La plage a-z peut contenir des lettres majuscules dans certains paramètres régionaux:

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

La solution correcte depuis bash 4.3 est de définir l'option globasciiranges:

shopt -s globasciiranges

pour que bash se comporte comme si LC_COLLATE=C a été défini dans les plages glob ing.

2
user79743