Convertir glob en `find`

11

J'ai toujours eu ce problème: j'ai un glob, qui correspond exactement aux bons fichiers, mais qui cause Command line too long. Chaque fois que je l'ai converti en une combinaison de findet grepcela fonctionne pour la situation particulière, mais qui n'est pas 100% équivalent.

Par exemple:

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

Existe-t-il un outil pour convertir des globes en findexpressions que je ne connais pas? Ou existe-t-il une option pour findfaire correspondre le glob sans faire correspondre le même glob dans un sous-répertoire (par exemple, foo/*.jpgn'est pas autorisé à faire correspondre bar/foo/*.jpg)?

Ole Tange
la source
Développez l'accolade et vous devriez pouvoir utiliser les expressions résultantes avec -pathou -ipath. find . -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg'devrait fonctionner - sauf qu'il correspondra /fooz/blah/bar/quuxA/pic1234d.jpg. Sera-ce un problème?
muru
Oui, ce sera un problème. Il doit être équivalent à 100%.
Ole Tange
Le problème est que nous n'avons aucune idée, quelle est exactement la différence. Votre modèle est assez bien.
peterh
J'ai ajouté votre poste d'extension comme réponse à la question. J'espère que ce n'est pas si mal.
peterh
Ne pouvez-vous pas le faire echo <glob> | cat, en supposant que ma connaissance de bash, echo est
intégrée

Réponses:

15

Si le problème est que vous obtenez une erreur argument-list-is-too-long, utilisez une boucle ou un shell intégré. Bien que l' command glob-that-matches-too-mucherreur puisse survenir, for f in glob-that-matches-too-muchnon, vous pouvez donc simplement faire:

for f in foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg
do
    something "$f"
done

La boucle peut être atrocement lente, mais cela devrait fonctionner.

Ou:

printf "%s\0" foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg |
  xargs -r0 something

( printfétant intégré dans la plupart des shells, ce qui précède contourne la limitation de l' execve()appel système)

$ cat /usr/share/**/* > /dev/null
zsh: argument list too long: cat
$ printf "%s\n" /usr/share/**/* | wc -l
165606

Fonctionne également avec bash. Je ne sais pas exactement où cela est documenté.


Les deux Vim glob2regpat()et Python fnmatch.translate()peuvent convertir des globes en expressions régulières, mais les deux utilisent également .*pour *, la correspondance entre /.

muru
la source
Si c'est vrai, le remplacer somethingpar echodevrait le faire.
Ole Tange
1
@OleTange C'est pourquoi j'ai suggéré printf- ce sera plus rapide que d'appeler des echomilliers de fois et offre plus de flexibilité.
muru
4
Il y a une limite sur les arguments qui peuvent être passés exec, qui s'applique aux commandes externes telles que cat; mais cette limite ne s'applique pas aux commandes internes du shell telles que printf.
Stephen Kitt
1
@OleTange La ligne n'est pas trop longue car elle printfest intégrée, et les shells utilisent vraisemblablement la même méthode pour lui fournir des arguments qu'ils utilisent pour énumérer les arguments for. catn'est pas une fonction intégrée.
muru
1
Techniquement, il y a des coquilles comme mkshprintfn'est pas intégré et des coquilles comme ksh93catest (ou peut être) intégré. Voir aussi zargsdans zshpour contourner ce problème sans avoir à y recourir xargs.
Stéphane Chazelas
9

find(pour les prédicats -name/ -pathstandard) utilise des modèles génériques comme les globs (notez que ce {a,b}n'est pas un opérateur glob; après expansion, vous obtenez deux globs). La principale différence réside dans la gestion des barres obliques (et des fichiers de points et des répertoires qui ne sont pas traités spécialement dans find). *dans globs ne s'étendra pas sur plusieurs répertoires. */*/*entraînera la liste de 2 niveaux de répertoires. L'ajout d'un -path './*/*/*'correspondra à tous les fichiers qui ont au moins 3 niveaux de profondeur et ne s'arrêtera pas findde lister le contenu d'un répertoire à n'importe quelle profondeur.

Pour ce particulier

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

quelques globes, c'est facile à traduire, vous voulez des répertoires en profondeur 3, donc vous pouvez utiliser:

find . -mindepth 3 -maxdepth 3 \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

(ou -depth 3avec certaines findimplémentations). Ou POSIX:

find . -path './*/*/*' -prune \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

Ce qui garantirait que ceux-ci *et ?ne pourraient pas correspondre aux /caractères.

( find, contrairement à globs lirait le contenu de répertoires autres que foo*barceux du répertoire courant¹, et ne trierait pas la liste des fichiers. Mais si nous laissons de côté le problème de ce à quoi correspond [A-Z]ou le comportement de */ ?en ce qui concerne les caractères invalides non spécifié, vous obtiendrez la même liste de fichiers).

Mais dans tous les cas, comme l' a montré @muru , il n'est pas nécessaire d'y recourir finds'il s'agit simplement de diviser la liste de fichiers en plusieurs exécutions pour contourner la limite de l' execve()appel système. Certains shells comme zsh(avec zargs) ou ksh93(avec command -x) ont même un support intégré pour cela.

Avec zsh(dont les globes ont également l'équivalent de -type fet la plupart des autres findprédicats), par exemple:

autoload zargs # if not already in ~/.zshrc
zargs ./foo*bar/quux[A-Z](|.bak)/pic[0-9][0-9][0-9][0-9]?.jpg(.) -- cmd

( (|.bak)est un opérateur glob contrairement à {,.bak}, le (.)qualificatif glob est l'équivalent de find's' -type f, ajoutez- oNy pour ignorer le tri comme avec find, Dpour inclure les fichiers de points (ne s'applique pas à ce glob))


¹ Pour findexplorer l'arborescence de répertoires comme le feraient les globes, vous auriez besoin de quelque chose comme:

find . ! -name . \( \
  \( -path './*/*' -o -name 'foo*bar' -o -prune \) \
  -path './*/*/*' -prune -name 'pic[0-9][0-9][0-9][0-9]?.jpg' -exec cmd {} + -o \
  \( ! -path './*/*' -o -name 'quux[A-Z]' -o -name 'quux[A-Z].bak' -o -prune \) \)

C'est-à-dire élaguer tous les répertoires au niveau 1 à l'exception de foo*barceux et tous au niveau 2 à l'exception de ceux quux[A-Z]ou quux[A-Z].bak, puis sélectionner pic...ceux au niveau 3 (et élaguer tous les répertoires à ce niveau).

Stéphane Chazelas
la source
3

Vous pouvez écrire une expression régulière pour trouver correspondant à vos besoins:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'
sebasth
la source
Existe-t-il un outil qui effectue cette conversion pour éviter les erreurs humaines?
Ole Tange
Non, mais le seul changement que j'ai fait était d'échapper ., ajouter le match en option pour .baket le changement *à la [^/]*non - correspondance des chemins comme / foo / foo / bar , etc.
sebasth
Mais même votre conversion est erronée. ? n'est pas changé en [^ /]. C'est exactement le genre d'erreur humaine que je veux éviter.
Ole Tange
1
Je pense qu'avec egrep, vous pouvez raccourcir [0-9][0-9][0-9][0-9]?à[0-9]{3,4}
wjandrea
1
@OleTange Voir Créer une expression
régulière à
0

En généralisant la note de mon autre réponse , comme réponse plus directe à votre question, vous pouvez utiliser ce shscript POSIX pour convertir le glob en une findexpression:

#! /bin/sh -
glob=${1#./}
shift
n=$#
p='./*'

while true; do
  case $glob in
    (*/*)
      set -- "$@" \( ! -path "$p" -o -path "$p/*" -o -name "${glob%%/*}" -o -prune \)
      glob=${glob#*/} p=$p/*;;
    (*)
      set -- "$@" -path "$p" -prune -name "$glob"
      while [ "$n" -gt 0 ]; do
        set -- "$@" "$1"
        shift
        n=$((n - 1))
      done
      break;;
  esac
done
find . "$@"

Pour être utilisé avec unsh glob standard (donc pas les deux glob de votre exemple qui utilise l' expansion d'accolade ):

glob2find './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' \
  -type f -exec cmd {} +

(qui n'ignore pas les fichiers-point ou point-dirs sauf .et ..et ne trie pas la liste des fichiers).

Celui-ci ne fonctionne qu'avec des globes par rapport au répertoire courant, sans .ou ..composants. Avec un peu d'effort, vous pouvez l'étendre à n'importe quel glob, plus qu'un glob ... Cela pourrait également être optimisé afin que la recherche glob2find 'dir/*'ne soit pas dirla même que celle d'un modèle.

Stéphane Chazelas
la source