Comprendre l'option -exec de `find`

53

Je me trouve constamment à la recherche de la syntaxe de

find . -name "FILENAME"  -exec rm {} \;

principalement parce que je ne vois pas comment fonctionne exactement la -execpièce. Quelle est la signification des accolades, de la barre oblique inverse et du point-virgule? Existe-t-il d'autres cas d'utilisation de cette syntaxe?

Zsolt Szilagy
la source
11
@Philippos: Je vois ce que tu veux dire. N'oubliez pas que les pages de manuel sont une référence, c'est-à-dire utiles pour ceux qui comprennent le sujet, à rechercher la syntaxe. Pour quelqu'un de nouveau sur le sujet, ils sont souvent trop cryptés et formels pour être utiles. Vous constaterez que la réponse acceptée est environ 10 fois plus longue que celle de la page de manuel, et cela pour une raison.
Zsolt Szilagy
6
Même la vieille manpage POSIX se lit comme suit: Un nom_utilitaire ou un argument ne contenant que les deux caractères "{}" doit être remplacé par le chemin actuel , ce qui me semble suffisant. De plus, il a un exemple avec -exec rm {} \;, tout comme dans votre question. À l'époque, il n'existait guère d'autres ressources que le "grand mur gris", des livres de manpages imprimées (le papier coûtait moins cher que le stockage). Donc, je sais que cela suffit pour quelqu'un de nouveau sur le sujet. Votre dernière question est cependant juste de poser ici. Malheureusement, ni @Kusalananda ni moi-même n’avons une réponse à cela.
Philippos
1
Comeon @Philippos. Voulez-vous vraiment dire à Kusalananda qu'il ne s'est pas amélioré sur la page de manuel? :-)
Zsolt Szilagy
1
@allo Bien que cela xargssoit parfois pratique, vous findpouvez transmettre plusieurs arguments de chemin pour commander sans lui. -exec command... {} +(avec +au lieu de \;) passe autant de chemins command...que nécessaire (chaque système d’exploitation a sa propre limite quant à la longueur d’une ligne de commande). Et comme xargs, la +forme de mots terminée findpar » -execl'action sera également exécuté command...plusieurs fois dans les rares cas où il y a trop de chemins pour se loger dans la limite.
Eliah Kagan
2
@ZsoltSzilagy Je n'ai ni dit cela ni voulu dire cela. Il vous a très bien nourri, je pense juste que vous êtes assez vieux pour manger tout seul. (-;
Philippos

Réponses:

90

Cette réponse se présente dans les parties suivantes:

  • Utilisation basique de -exec
  • Utilisation -execen combinaison avecsh -c
  • En utilisant -exec ... {} +
  • En utilisant -execdir

Utilisation basique de -exec

L' -execoption prend un utilitaire externe avec des arguments facultatifs comme arguments et l'exécute.

Si la chaîne {}est présente n'importe où dans la commande donnée, chaque instance sera remplacée par le chemin d'accès en cours de traitement (par exemple ./some/path/FILENAME). Dans la plupart des coquillages, il {}n'est pas nécessaire de citer les deux caractères .

La commande doit être terminée avec un ;for findpour savoir où elle se termine (car il peut y avoir d'autres options par la suite). Pour protéger le ;shell, il doit être cité comme \;ou ';', sinon le shell le verra comme la fin de la findcommande.

Exemple (la \fin des deux premières lignes ne concerne que les continuations de lignes):

find . -type f -name '*.txt'      \
   -exec grep -q 'hello' {} ';'   \
   -exec cat {} ';'

Ceci trouvera tous les fichiers normaux ( -type f) dont les noms correspondent au modèle *.txtdans ou sous le répertoire actuel. Il vérifiera ensuite si la chaîne helloapparaît dans l'un des fichiers trouvés à l'aide de grep -q(ce qui ne produit aucune sortie, mais uniquement un état de sortie). Pour les fichiers contenant la chaîne, catsera exécuté pour afficher le contenu du fichier sur le terminal.

Chacun -execagit également comme un "test" sur les noms de chemins trouvés par find, tout comme -typeet le -namefait. Si la commande renvoie un état de sortie nul (signifiant "succès"), la partie suivante de la findcommande est considérée, sinon la findcommande continue avec le chemin suivant. Ceci est utilisé dans l'exemple ci-dessus pour rechercher les fichiers contenant la chaîne hello, mais pour ignorer tous les autres fichiers.

L'exemple ci-dessus illustre les deux cas d'utilisation les plus courants de -exec:

  1. Comme test pour restreindre davantage la recherche.
  2. Effectuer une sorte d'action sur le chemin trouvé (généralement, mais pas nécessairement, à la fin de la findcommande).

Utilisation -execen combinaison avecsh -c

La commande -execpouvant être exécutée est limitée à un utilitaire externe avec des arguments facultatifs. Utiliser directement les fonctions intégrées au shell, les fonctions, les conditions, les pipelines, les redirections, etc. -execn'est pas possible, à moins d'être enveloppé dans un environnement similaire à un sh -cshell enfant.

Si des bashfonctionnalités sont requises, utilisez bash -cà la place de sh -c.

sh -cs’exécute /bin/shavec un script donné sur la ligne de commande, suivi d’arguments facultatifs en ligne de commande pour ce script.

Un exemple simple d'utilisation sh -cpar lui-même, sans find:

sh -c 'echo  "You gave me $1, thanks!"' sh "apples"

Cela passe deux arguments au script shell enfant:

  1. La ficelle sh. Cela sera disponible comme $0dans le script, et si le shell interne génère un message d'erreur, il le préfixera avec cette chaîne.

  2. L'argument applesest disponible comme $1dans le script, et s'il y avait eu plus d'arguments, ceux-ci auraient été disponibles comme $2, $3etc. Ils seraient également disponibles dans la liste "$@"(sauf ceux $0qui ne feraient pas partie de "$@").

Ceci est utile en combinaison avec -execcar cela nous permet de faire des scripts arbitrairement complexes qui agissent sur les noms de chemins trouvés par find.

Exemple: Recherchez tous les fichiers normaux portant un suffixe de nom de fichier donné et remplacez ce suffixe par un autre suffixe, les suffixes étant conservés dans des variables:

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c 'mv "$3" "${3%.$1}.$2"' sh "$from" "$to" {} ';'

Dans le script interne, $1serait la chaîne text, $2serait la chaîne txtet $3serait tout chemin que chemin finda trouvé pour nous. Le paramètre expansion ${3%.$1}prendrait le chemin et en supprimait le suffixe .text.

Ou en utilisant dirname/ basename:

find . -type f -name "*.$from" -exec sh -c '
    mv "$3" "$(dirname "$3")/$(basename "$3" ".$1").$2"' sh "$from" "$to" {} ';'

ou, avec des variables ajoutées dans le script interne:

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2; pathname=$3
    mv "$pathname" "$(dirname "$pathname")/$(basename "$pathname" ".$from").$to"' sh "$from" "$to" {} ';'

Notez que dans cette dernière variante, les variables fromet todans le shell enfant sont distinctes des variables portant le même nom dans le script externe.

Ce qui précède est la manière correcte d’appeler un script complexe arbitraire -execavec find. Utiliser finden boucle comme

for pathname in $( find ... ); do

est sujet aux erreurs et inélégant (opinion personnelle). Il divise les noms de fichiers sur des espaces, appelle la suppression du nom de fichier et oblige également le shell à développer le résultat complet findavant même d'exécuter la première itération de la boucle.

Voir également:


En utilisant -exec ... {} +

Le ;à la fin peut être remplacé par +. Ceci a findpour effet d'exécuter la commande donnée avec autant d'arguments (noms de chemins d'accès trouvés) que possible plutôt qu'une fois pour chaque chemin d'accès trouvé. La chaîne {} doit apparaître juste avant le +pour que cela fonctionne .

find . -type f -name '*.txt' \
   -exec grep -q 'hello' {} ';' \
   -exec cat {} +

Ici, findcollectera les noms de chemin résultants et exécutera catle plus grand nombre possible à la fois.

find . -type f -name "*.txt" \
   -exec grep -q "hello" {} ';' \
   -exec mv -t /tmp/files_with_hello/ {} +

De même ici, mvsera exécuté aussi peu de fois que possible. Ce dernier exemple nécessite GNU mvde coreutils (qui supporte l’ -toption).

Utiliser -exec sh -c ... {} +est également un moyen efficace de parcourir un ensemble de chemins avec un script arbitrairement complexe.

Les bases sont les mêmes que lors de l'utilisation -exec sh -c ... {} ';', mais le script nécessite maintenant une liste d'arguments beaucoup plus longue. Ceux-ci peuvent être bouclés en boucle "$@"dans le script.

Notre exemple de la dernière section qui modifie les suffixes de nom de fichier:

from=text  #  Find files that have names like something.text
to=txt     #  Change the .text suffix to .txt

find . -type f -name "*.$from" -exec sh -c '
    from=$1; to=$2
    shift 2  # remove the first two arguments from the list
             # because in this case these are *not* pathnames
             # given to us by find
    for pathname do  # or:  for pathname in "$@"; do
        mv "$pathname" "${pathname%.$from}.$to"
    done' sh "$from" "$to" {} +

En utilisant -execdir

Il existe également -execdir(mis en œuvre par la plupart des findvariantes, mais pas une option standard).

Cela fonctionne -execà la différence que la commande shell donnée est exécutée avec le répertoire du chemin trouvé en tant que répertoire de travail actuel et qu'il {}contiendra le nom de base du chemin trouvé sans son chemin (mais GNU findpréfixera toujours le nom de base avec ./, alors que BSD findne fera pas ça).

Exemple:

find . -type f -name '*.txt' \
    -execdir mv {} done-texts/{}.done \;

Cela déplacera chaque *.txtfichier- trouvé dans un done-textssous - répertoire préexistant dans le même répertoire que celui où le fichier a été trouvé . Le fichier sera également renommé en y ajoutant le suffixe .done.

Ce serait un peu plus délicat à faire -execcar il faudrait extraire le nom de base du fichier trouvé {}pour former le nouveau nom du fichier. Nous avons également besoin du nom de répertoire de {}pour localiser le done-textsrépertoire correctement.

Avec -execdir, certaines choses comme celles-ci deviennent plus faciles.

L'opération correspondante utilisant à la -execplace de -execdirdevrait utiliser un shell enfant:

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "$( dirname "$name" )/done-texts/$( basename "$name" ).done"
    done' sh {} +

ou,

find . -type f -name '*.txt' -exec sh -c '
    for name do
        mv "$name" "${name%/*}/done-texts/${name##*/}.done"
    done' sh {} +
Kusalananda
la source
7
-execprend un programme et des arguments et l'exécute; Certaines commandes shell ne comprennent qu'un programme et des arguments, mais d'autres non. Une commande shell peut inclure la redirection et la tuyauterie; -execne peut pas (bien que le tout findpuisse être redirigé). Une commande shell peut utiliser ; && ifetc; -execne peut pas, bien que -a -opeut en faire. Une commande shell peut être un alias ou une fonction shell, ou intégrée; -execne peux pas. Une commande shell peut développer des vars; -execne peut pas (bien que la coque externe qui exécute la findboîte). Une commande shell peut se substituer $(command)différemment à chaque fois; -execne peux pas. ...
dave_thompson_085
... Une commande shell peut glob, -execne peut pas - bien qu'elle findpuisse parcourir les fichiers de la même manière que la plupart des globs, ce qui est rarement voulu.
dave_thompson_085
@ dave_thompson_085 Bien sûr, la commande shell peut être shelle-même, ce qui est tout à fait capable de faire tout cela
Tavian Barnes
2
Dire que c'est une commande shell est incorrecte ici, find -exec cmd arg \;n'appelle pas un shell pour interpréter une ligne de commande, cela ne s'exécute pas execlp("cmd", "arg")directement execlp("sh", "-c", "cmd arg")(pour lequel le shell finirait par faire l'équivalent de execlp("cmd", "arg")if cmdn'était pas intégré).
Stéphane Chazelas
2
Vous pouvez préciser que tous les findarguments après -execet jusqu'à ;ou +composent la commande à exécuter avec ses arguments, chaque instance d'un {}argument étant remplacée par le fichier en cours (avec ;) et, {}en dernier lieu, +par une liste de fichiers. comme arguments séparés (dans le {} +cas). IOW -execprend plusieurs arguments, terminés par un ;ou {} +.
Stéphane Chazelas