find (1): comment le caractère générique star est-il implémenté pour qu'il échoue sur certains noms de fichiers?

31

Dans un système de fichiers où les noms de fichiers sont en UTF-8, j'ai un fichier avec un nom erroné; il est affiché comme D�sinstaller:, nom réel selon zsh D$'\351'sinstaller:, Latin1 pour Désinstaller, lui-même une barbarie française pour "désinstaller". Zsh ne le ferait pas correspondre [[ $file =~ '^.*$' ]]mais le ferait avec un globbing *- c'est le comportement que j'attends.

Maintenant, je m'attends toujours à le trouver lors de l'exécution find . -name '*'- en fait, je ne m'attendrais jamais à ce qu'un nom de fichier échoue à ce test. Cependant, avec LANG=en_US.utf8, le fichier ne pas apparaître, et je dois ensemble LANG=C(ou en_US, ou '') pour que cela fonctionne.

Question: Quelle est la mise en œuvre derrière et comment aurais-je pu prédire ce résultat?

Infos: Arch Linux 3.14.37-1-lts, find (GNU findutils) 4.4.2

Michael
la source
1
avez-vous envisagé convmvde convertir les noms de fichiers en utf-8?
ctrl-alt-delor
@richard: En fait, j'ai l'habitude de [[ $file =~ '^.*$' ]]ne pas utiliser recodele nom de fichier, mais je vais maintenant vérifier convmvsi besoin est. Merci.
Michaël

Réponses:

25

C'est vraiment une belle prise. D'un rapide coup d'œil au code source de GNU find, je dirais que cela se résume à la façon dont fnmatchse comporte les séquences d'octets invalides ( pred_name_commonen pred.c):

b = fnmatch (str, base, flags) == 0;
(...)
return b;

Ce code teste la valeur de retour de fnmatchpour l'égalité avec 0, mais ne vérifie pas les erreurs; cela se traduit par des erreurs signalées comme "ne correspond pas".

Il a été suggéré, il y a de nombreuses années, de changer le comportement de cette fonction libc pour toujours retourner vrai sur le *modèle, même sur les noms de fichiers cassés, mais d'après ce que je peux dire, l'idée doit avoir été rejetée (voir le fil commençant à https : //sourceware.org/ml/libc-hacker/2002-11/msg00071.html ):

Lorsque fnmatch détecte un caractère codé sur plusieurs octets, il doit revenir à la correspondance sur un seul octet, afin que "*" ait une chance de correspondre à une telle chaîne.

Et pourquoi est-ce mieux ou plus correct? Existe-t-il une pratique existante?

Comme mentionné par Stéphane Chazelas dans un commentaire, et également dans le même fil de 2002, cela est incompatible avec l'expansion globale effectuée par les shells, qui ne s'étouffent pas avec les caractères invalides. Peut-être encore plus déroutant est le fait que l'inversion du test ne correspondra qu'aux fichiers qui ont des noms cassés (créez des fichiers en bash avec touch $'D\351marrer' $'Touch\303\251' $'\346\227\245\346\234\254\350\252\236'):

$ find -name '*'
.
./Touché
./日本語

$ find -not -name '*'
./D?marrer

Donc, pour répondre à votre question, vous auriez pu prédire cela en connaissant le comportement de votre fnmatchdans ce cas, et en sachant comment findgère la valeur de retour de cette fonction; vous n'auriez probablement pas pu le découvrir uniquement en lisant la documentation.

dhag
la source
Je suppose que pourquoi il n'y a pas de solution, *c'est que ce serait incompatible avec D*staller.
ctrl-alt-delor
7
@richard, l'idée serait de D*stallercorrespondre $'D\351sinstaller'aussi bien que dans le glob de tous les shells que j'ai testés. Étant donné que le comportement GNU fnmatch n'est pas cohérent avec celui du shell GNU, je dirais que c'est un bug.
Stéphane Chazelas
1
Grande réponse approfondie, dhag; très appréciée. Pourriez-vous signaler la spécification standard à laquelle fnmatch est conforme? Je peux trouver la spécification d'expression régulière POSIX spécifiant qui .ne doit correspondre qu'à des caractères valides dans l'encodage - d'où mon attente qui .*ne correspond pas à des chaînes non valides - mais je ne trouve pas de spécification correspondante pour l'étoile globulante.
Michaël
1
La spécification la plus proche que je peux trouver en ligne se trouve sur cette page OpenGroup . Il indique que la correspondance doit être basée sur la configuration binaire utilisée pour coder le caractère, et non sur la représentation graphique du caractère. et le <asterisk> est un modèle qui doit correspondre à n'importe quelle chaîne, y compris la chaîne nulle. Cela peut sans doute être interprété comme la suggestion de @ StéphaneChazelas. 13 ans plus tard, il est peut-être temps de cingler en amont :-)
Michaël
@ Michaël, je n'ai rien trouvé de mieux non plus. Peut-être, à titre de comparaison, que GNU trouve sur Mac OS se comporte d'une manière cohérente avec le globbing du shell (c'est-à-dire, -name '*'correspond à tous les fichiers, noms brisés inclus), donc vraisemblablement la version de BSD de fnmatch, qui ne revendique pas la cnoformance POSIX.2, contrairement à la version GNU, a une interprétation différente, et sans doute plus saine, de ce qui devrait être fait sur les caractères invalides.
dhag
13

L' -name option find utilise la notation de correspondance de modèle de shell pour effectuer la correspondance du nom de fichier. *est un modèle correspondant à plusieurs caractères , doit correspondre à une chaîne de zéro ou plusieurs caractères.

findutilise fnmatch pour vérifier la correspondance des modèles, vous pouvez donc utiliser ltrace pour vérifier le résultat:

$ touch $'\U1212'aa
$ touch D$'\351'sinstaller
$ LC_ALL=en_US.utf8 ltrace -e fnmatch find -name '*'          
find->fnmatch("foo", "foo", 0)                   = 0
find->fnmatch("Foo", "foo", 0)                   = 1
find->fnmatch("Foo", "foo", 16)                  = 0
find->fnmatch("*", ".", 0)                       = 0
.
find->fnmatch("*", "D\351sinstaller", 0)         = -1
find->fnmatch("*", "\341\210\222aa", 0)          = 0
./ሒaa
+++ exited (status 0) +++

Avec D\351sinstaller, fnmatchretour -1, a indiqué qu'il ne correspondait pas. Un caractère valide comme ሒaasera mis en correspondance.

Dans votre cas, avec UTF-8locale, \351est un caractère non valide, provoquant l'échec de la correspondance de modèle.

cuonglm
la source
3
À tout le moins, +1 pour l'utilisation de ltrace. Je le savais strace, mais ltracec'est nouveau pour moi. Charmant!
Michaël