web-dev-qa-db-fra.com

Comprendre l'option -exec de `find`

Je me retrouve constamment à chercher la syntaxe de

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

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

65
Zsolt Szilagy

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

  • Utilisation de base de -exec
  • En utilisant -exec en combinaison avec sh -c
  • En utilisant -exec ... {} +
  • En utilisant -execdir

Utilisation de base de -exec

Le -exec option prend un utilitaire externe avec des arguments optionnels comme argument et l'exécute.

Si la chaîne {} est présent n'importe où dans la commande donnée, chaque instance de celle-ci sera remplacée par le chemin d'accès en cours de traitement (par exemple ./some/path/FILENAME). Dans la plupart des shells, les deux caractères {} n'a pas besoin d'être cité.

La commande doit se terminer par un ; pour find pour savoir où cela se termine (car il peut y avoir d'autres options par la suite). Pour protéger le ; du Shell, il doit être cité comme \; ou ';', sinon le Shell la verra comme la fin de la commande find.

Exemple (le \ à la fin des deux premières lignes sont juste pour les continuations de ligne):

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

Cela trouvera tous les fichiers réguliers (-type f) dont les noms correspondent au modèle *.txt dans ou sous le répertoire actuel. Il testera ensuite si la chaîne hello apparaît dans l'un des fichiers trouvés à l'aide de grep -q (qui ne produit aucune sortie, juste un état de sortie). Pour les fichiers contenant la chaîne, cat sera exécuté pour sortir le contenu du fichier vers le terminal.

Chaque -exec agit également comme un "test" sur les chemins d'accès trouvés par find, tout comme -type et -name Est-ce que. Si la commande renvoie un état de sortie nul (signifiant "succès"), la partie suivante de la commande find est prise en compte, sinon la commande find continue avec le nom de chemin suivant. Ceci est utilisé dans l'exemple ci-dessus pour rechercher des 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. Pour effectuer une sorte d'action sur le chemin d'accès trouvé (généralement, mais pas nécessairement, à la fin de la commande find).

En utilisant -exec en combinaison avec sh -c

La commande qui -exec peut s'exécuter est limité à un utilitaire externe avec des arguments facultatifs. Pour utiliser directement les fonctions intégrées, les fonctions, les conditions, les pipelines, les redirections, etc. de Shell avec -exec n'est pas possible, sauf s'il est enveloppé dans quelque chose comme un sh -c enfant Shell.

Si les fonctionnalités bash sont requises, utilisez bash -c au lieu de sh -c.

sh -c s'exécute /bin/sh avec un script donné sur la ligne de commande, suivi d'arguments de ligne de commande facultatifs pour ce script.

Un exemple simple d'utilisation de sh -c seul, sans find:

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

Cela transmet deux arguments au script enfant Shell. Ceux-ci seront placés dans $0 et $1 pour le script à utiliser.

  1. La chaîne sh. Ce sera disponible en tant que $0 à l'intérieur du script, et si le shell interne génère un message d'erreur, il le préfixe avec cette chaîne.

  2. L'argument apples est disponible sous la forme $1 dans le script, et s'il y avait eu plus d'arguments, ceux-ci auraient été disponibles en tant que $2, $3 etc. Ils seraient également disponibles dans la liste "$@" (à l'exception de $0 qui ne ferait pas partie de "$@").

Ceci est utile en combinaison avec -exec car il nous permet de créer des scripts arbitrairement complexes qui agissent sur les chemins d'accès trouvés par find.

Exemple: recherchez tous les fichiers normaux qui ont un certain suffixe de nom de fichier et remplacez ce suffixe de nom de fichier par un autre suffixe, où les suffixes sont 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" {} ';'

A l'intérieur du script interne, $1 serait la chaîne text, $2 serait la chaîne txt et $3 serait le nom de chemin que find a trouvé pour nous. L'expansion des paramètres ${3%.$1} prendrait le chemin d'accès et supprimerait le suffixe .text à partir de cela.

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 from et to dans le shell enfant sont distinctes des variables portant les mêmes noms dans le script externe.

Ce qui précède est la bonne façon d'appeler un script complexe arbitraire à partir de -exec avec find. Utiliser find dans une boucle comme

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

est sujet aux erreurs et inélégant (opinion personnelle). Il fractionne les noms de fichiers sur les espaces, invoque la globalisation des noms de fichiers et force également le shell à développer le résultat complet de find avant 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 +. Cela provoque find pour exécuter la commande donnée avec autant d'arguments (chemins d'accès trouvés) que possible plutôt qu'une fois pour chaque chemin d'accès trouvé. La chaîne {} doit se produire juste avant le + pour que cela fonctionne .

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

Ici, find collectera les chemins d'accès résultants et exécutera cat sur autant d'entre eux que possible à la fois.

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

De même ici, mv sera exécuté aussi peu de fois que possible. Ce dernier exemple nécessite GNU mv de coreutils (qui prend en charge le -t option).

En utilisant -exec sh -c ... {} + est également un moyen efficace de parcourir un ensemble de noms de chemin avec un script arbitrairement complexe.

Les bases sont les mêmes que lorsque vous utilisez -exec sh -c ... {} ';', mais le script prend maintenant une liste d'arguments beaucoup plus longue. Ceux-ci peuvent être bouclés en bouclant sur "$@" à l'intérieur du script.

Notre exemple de la dernière section qui modifie les suffixes des noms de fichiers:

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 y a aussi -execdir (implémenté par la plupart des variantes de find, mais pas une option standard).

Cela fonctionne comme -exec avec la différence que la commande Shell donnée est exécutée avec le répertoire du chemin trouvé comme répertoire de travail actuel et que {} contiendra le nom de base du chemin trouvé sans son chemin (mais GNU find préfixera toujours le nom de base avec ./, tandis que BSD find ne le fera pas).

Exemple:

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

Cela déplacera chacun des *.txt- fichier dans un fichier done-texts sous-répertoire dans le même répertoire que celui où le fichier a été trouvé . Le fichier sera également renommé en ajoutant le suffixe .done à elle.

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

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

L'opération correspondante utilisant -exec au lieu de -execdir devrait employer un enfant Shell:

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 {} +
102
Kusalananda