trouver -exec une fonction shell sous Linux?

185

Y a-t-il un moyen d'obtenir find exécuter une fonction que je définis dans le shell? Par exemple:

dosomething () {
  echo "doing something with $1"
}
find . -exec dosomething {} \;

Le résultat est:

find: dosomething: No such file or directory

Y at - il un moyen de find« s -execpour voir dosomething?

alxndr
la source

Réponses:

262

Puisque seul le shell sait exécuter les fonctions du shell, vous devez exécuter un shell pour exécuter une fonction. Vous devez également marquer votre fonction pour l'exportation avec export -f, sinon le sous-shell n'en héritera pas:

export -f dosomething
find . -exec bash -c 'dosomething "$0"' {} \;
Adam Rosenfield
la source
7
Tu me bats. Au fait, vous pouvez mettre les accolades entre les guillemets au lieu d'utiliser $0.
Suspendu jusqu'à nouvel ordre.
20
Si vous l'utilisez, find . -exec bash -c 'dosomething "$0"' {} \;il gérera les espaces (et autres caractères étranges) dans les noms de fichiers ...
Gordon Davisson
3
@alxndr: cela échouera sur les noms de fichiers avec des guillemets doubles, des guillemets, des signes dollar, des combos d'échappement, etc.
Gordon Davisson
4
Notez également que toutes les fonctions que votre fonction pourrait appeler ne seront pas disponibles à moins que vous les exportiez également.
hraban le
3
Le export -fne fonctionnera que dans certaines versions de bash. Ce n'est pas posix, pas crossplatforn, /bin/shaura une erreur avec lui
Роман Коптев
120
find . | while read file; do dosomething "$file"; done
Jac
la source
10
Belle solution. Ne nécessite pas d'exporter la fonction ou de manipuler des arguments d'échappement et est probablement plus efficace car il ne génère pas de sous-shell pour exécuter chaque fonction.
Tom
19
Gardez à l'esprit, cependant, qu'il se cassera sur les noms de fichiers contenant des nouvelles lignes.
chepner
3
C'est plus "shell'ish" car vos variables et fonctions globales seront disponibles, sans créer à chaque fois un shell / environnement entièrement nouveau. J'ai appris cela à la dure après avoir essayé la méthode d'Adam et rencontré toutes sortes de problèmes d'environnement. Cette méthode ne corrompe pas non plus le shell de votre utilisateur actuel avec toutes les exportations et nécessite moins de dicipline.
Ray Foss
TELLEMENT PLUS RAPIDE que la réponse acceptée sans sous-shell! AGRÉABLE! Je vous remercie!
Bill Heller
2
aussi j'ai résolu mon problème en changeant while readpour une boucle for; for item in $(find . ); do some_function "${item}"; done
user5359531
22

La réponse de Jak ci-dessus est excellente mais comporte quelques écueils faciles à surmonter:

find . -print0 | while IFS= read -r -d '' file; do dosomething "$file"; done

Cela utilise null comme délimiteur au lieu d'un saut de ligne, donc les noms de fichiers avec des sauts de ligne fonctionneront. Il utilise également l' -rindicateur qui désactive l'échappement de la barre oblique inverse, sans cela, les barres obliques inverses dans les noms de fichiers ne fonctionneront pas. Il efface également de IFSsorte que les espaces blancs de fin potentiels dans les noms ne soient pas ignorés.

pajamian
la source
1
C'est bon pour /bin/bashmais ne fonctionnera pas /bin/sh. C'est dommage.
Роман Коптев
@ РоманКоптев Quelle chance qu'au moins cela fonctionne dans / bin / bash.
sdenham
21

Ajoutez des citations {}comme indiqué ci-dessous:

export -f dosomething
find . -exec bash -c 'dosomething "{}"' \;

Cela corrige toute erreur due à des caractères spéciaux renvoyés par find, par exemple, des fichiers avec des parenthèses dans leur nom.

Wagner
la source
1
Ce n'est pas la bonne façon d'utiliser {}. Cela va casser pour un nom de fichier contenant des guillemets doubles. touch '"; rm -rf .; echo "I deleted all you files, haha. Oups.
gniourf_gniourf
Oui, c'est très mauvais. Il peut être exploité par des injections. Très dangereux. Ne l'utilisez pas!
Dominik
1
@kdubs: utilisez $ 0 (sans guillemets) dans la chaîne de commande et passez le nom de fichier comme premier argument: -exec bash -c 'echo $0' '{}' \;Notez que lors de l'utilisation bash -c, $ 0 est le premier argument, pas le nom du script.
sdenham
10

Traitement des résultats en masse

Pour une efficacité accrue, de nombreuses personnes utilisent xargspour traiter les résultats en vrac, mais c'est très dangereux. Pour cette raison, une autre méthode a été introduite dansfind exécuter les résultats en masse.

Notez cependant que cette méthode peut être accompagnée de quelques mises en garde comme par exemple une exigence dans POSIX- findà avoir {}à la fin de la commande.

export -f dosomething
find . -exec bash -c 'for f; do dosomething "$f"; done' _ {} +

findpassera de nombreux résultats en tant qu'arguments à un seul appel de bashet le for-loop parcourt ces arguments, exécutant la fonction dosomethingsur chacun de ceux-ci.

La solution ci-dessus commence les arguments à $1, c'est pourquoi il y a un _(qui représente $0).

Traitement des résultats un par un

De la même manière, je pense que la première réponse acceptée doit être corrigée pour être

export -f dosomething
find . -exec bash -c 'dosomething "$1"' _ {} \;

Ce n'est pas seulement plus sain, car les arguments doivent toujours commencer à $1, mais l'utilisation $0peut également conduire à un comportement inattendu si le nom de fichier renvoyé par finda une signification particulière pour le shell.

Dominik
la source
9

Faites appeler le script lui-même, en passant chaque élément trouvé en argument:

#!/bin/bash

if [ ! $1 == "" ] ; then
   echo "doing something with $1"
   exit 0
fi

find . -exec $0 {} \;

exit 0

Lorsque vous exécutez le script seul, il trouve ce que vous recherchez et s'appelle lui-même en passant chaque résultat de la recherche comme argument. Lorsque le script est exécuté avec un argument, il exécute les commandes sur l'argument puis se ferme.

Mike Maready
la source
idée sympa mais mauvais style: utilise le même script à deux fins. si vous voulez réduire le nombre de fichiers dans votre bac / alors vous pouvez fusionner tous vos scripts en un seul qui a une clause de casse au début. solution très propre, n'est-ce pas?
user829755
sans oublier que cela échouera avec find: ‘myscript.sh’: No such file or directorysi commencé en tant que bash myscript.sh...
Camusensei
5

Pour ceux d'entre vous qui recherchent une fonction bash qui exécutera une commande donnée sur tous les fichiers du répertoire actuel, j'en ai compilé une à partir des réponses ci-dessus:

toall(){
    find . -type f | while read file; do "$1" "$file"; done
}

Notez qu'il rompt avec les noms de fichiers contenant des espaces (voir ci-dessous).

À titre d'exemple, prenez cette fonction:

world(){
    sed -i 's_hello_world_g' "$1"
}

Disons que je voulais changer toutes les instances de hello en monde dans tous les fichiers du répertoire actuel. Je ferais:

toall world

Pour être sûr de tous les symboles dans les noms de fichiers, utilisez:

toall(){
    find . -type f -print0 | while IFS= read -r -d '' file; do "$1" "$file"; done
}

(mais vous avez besoin d'un findqui gère -print0par exemple GNU find).

Jason Basanese
la source
3

Je trouve le moyen le plus simple comme suit, en répétant deux commandes en une seule opération

func_one () {
  echo "first thing with $1"
}

func_two () {
  echo "second thing with $1"
}

find . -type f | while read file; do func_one $file; func_two $file; done
edib
la source
3

Pour référence, j'évite ce scénario en utilisant:

for i in $(find $dir -type f -name "$name" -exec ls {} \;); do
  _script_function_call $i;
done;

Obtenez la sortie de la recherche dans le fichier de script actuel et parcourez la sortie comme vous le souhaitez. Je suis d'accord avec la réponse acceptée, mais je ne veux pas exposer la fonction en dehors de mon fichier de script.

dinesh saini
la source
cela a des limites de taille
Richard
2

Il n'est pas possible d'exécuter une fonction de cette façon.

Pour surmonter cela, vous pouvez placer votre fonction dans un script shell et l'appeler depuis find

# dosomething.sh
dosomething () {
  echo "doing something with $1"
}
dosomething $1

Maintenant, utilisez-le dans find comme:

find . -exec dosomething.sh {} \;
codaddict
la source
J'essayais d'éviter plus de fichiers dans mon ~ / bin. Merci quand même!
alxndr
J'ai envisagé de voter à la baisse mais la solution en soi n'est pas mauvaise. Veuillez simplement utiliser les guillemets corrects: dosomething $1=> dosomething "$1"et démarrer correctement votre fichier avecfind . -exec bash dosomething.sh {} \;
Camusensei
2

Mettez la fonction dans un fichier séparé et obtenez find exécutez-le.

Les fonctions du shell sont internes au shell dans lequel elles sont définies; findne pourra jamais les voir.

Angus
la source
Je t'ai eu; logique. J'essayais cependant d'éviter plus de fichiers dans mon ~ / bin.
alxndr
2

Pour apporter des ajouts et des clarifications à certaines des autres réponses, si vous utilisez l'option bulk pour execou execdir( -exec command {} +) et que vous souhaitez récupérer tous les arguments de position, vous devez envisager la gestion de $0with bash -c. Plus concrètement, considérez la commande ci-dessous, qui utilise bash -ccomme suggéré ci-dessus, et fait simplement écho aux chemins de fichiers se terminant par '.wav' de chaque répertoire qu'il trouve:

find "$1" -name '*.wav' -execdir bash -c 'echo $@' _ {} +

Le manuel bash dit:

If the -c option is present, then commands are read from the first non-option argument command_string.  If there are arguments after the command_string, they  are  assigned  to  the
                 positional parameters, starting with $0.

Voici 'check $@'la chaîne de commande et _ {}les arguments après la chaîne de commande. Notez qu'il $@s'agit d'un paramètre de position spécial dans bash qui s'étend à tous les paramètres de position à partir de 1 . Notez également qu'avec l' -coption, le premier argument est affecté au paramètre positionnel $0. Cela signifie que si vous essayez d'accéder à tous les paramètres de position avec $@, vous n'obtiendrez que les paramètres commençant par $1et plus. C'est la raison pour laquelle la réponse de Dominik a le _, qui est un argument factice pour remplir le paramètre $0, donc tous les arguments que nous voulons sont disponibles plus tard si nous utilisons l' $@expansion des paramètres par exemple, ou la boucle for comme dans cette réponse.

Bien sûr, similaire à la réponse acceptée, bash -c 'shell_function $0 $@'cela fonctionnerait également en passant explicitement $0, mais encore une fois, vous devez garder à l'esprit que $@cela ne fonctionnera pas comme prévu.

pompier
la source
0

Pas directement, non. Find s'exécute dans un processus séparé, pas dans votre shell.

Créez un script shell qui fait le même travail que votre fonction et trouvez -exec-le.

Laurence Gonsalves
la source
J'essayais d'éviter plus de fichiers dans mon ~ / bin. Merci quand même!
alxndr
-2

J'éviterais -execcomplètement d' utiliser . Pourquoi ne pas l'utiliser xargs?

find . -name <script/command you're searching for> | xargs bash -c
Barry
la source
À l'époque, l'IIRC cherchait à réduire la quantité de ressources utilisées. Pensez à trouver des millions de fichiers vides et à les supprimer.
alxndr