Exécution d'une fonction définie par l'utilisateur dans un appel find -exec

25

Je suis sur Solaris 10 et j'ai testé les éléments suivants avec ksh (88), bash (3.00) et zsh (4.2.1).

Le code suivant ne donne aucun résultat:

function foo {
    echo "Hello World"
}

find somedir -exec foo \;

La recherche correspond à plusieurs fichiers (comme indiqué en remplaçant -exec ...par -print), et la fonction fonctionne parfaitement lorsqu'elle est appelée en dehors de l' findappel.

Voici ce que dit la man findpage -exec:

 -exec, commande True si la commande exécutée renvoie un
                     valeur zéro comme état de sortie. La fin de
                     la commande doit être ponctuée par un échappé
                     point-virgule (;). Un argument de commande {} est
                     remplacé par le chemin d'accès actuel. Si la
                     le dernier argument de -exec est {} et vous
                     spécifiez + plutôt que le point-virgule (;),
                     la commande est invoquée moins de fois, avec
                     {} remplacé par des groupes de chemins d'accès. Si
                     toute invocation de la commande renvoie un
                     valeur non nulle comme état de sortie, rechercher
                     renvoie un état de sortie différent de zéro.

Je pourrais probablement m'en tirer en faisant quelque chose comme ça:

for f in $(find somedir); do
    foo
done

Mais j'ai peur de traiter des problèmes de séparateur de champs.

Est-il possible d'appeler une fonction shell (définie dans le même script, ne nous soucions pas des problèmes de portée) à partir d'un find ... -exec ...appel?

Je l' ai essayé avec les deux /usr/bin/findet /bin/findet a obtenu le même résultat.

rahmu
la source
avez-vous essayé d'exporter la fonction après l'avoir déclarée? export -f foo
h3rrmiller
2
Vous devrez faire de la fonction un script externe et l'intégrer PATH. Alternativement, utilisez sh -c '...'et définissez ET exécutez la fonction dans le ...bit. Il peut être utile de comprendre les différences entre les fonctions et les scripts .
jw013

Réponses:

27

A functionest local à un shell, vous devez find -execdonc générer un shell et avoir cette fonction définie dans ce shell avant de pouvoir l'utiliser. Quelque chose comme:

find ... -exec ksh -c '
  function foo {
    echo blan: "$@"
  }
  foo "$@"' ksh {} +

bashpermet d'exporter des fonctions via l'environnement avec export -f, donc vous pouvez faire (en bash):

foo() { ...; }
export -f foo
find ... -exec bash -c 'foo "$@"' bash {} +

ksh88doit typeset -fxexporter la fonction (pas via l'environnement), mais qui ne peut être utilisée que par des scripts she-bang moins exécutés par ksh, donc pas avec ksh -c.

Une autre option consiste à faire:

find ... -exec ksh -c "
  $(typeset -f foo)"'
  foo "$@"' ksh {} +

Autrement dit, utilisez typeset -fpour vider la définition de la foofonction à l'intérieur du script en ligne. Notez que si vous fooutilisez d'autres fonctions, vous devrez également les vider.

Stéphane Chazelas
la source
2
Pouvez-vous expliquer pourquoi il y a deux occurrences de kshou bashdans la -execcommande? Je comprends le premier, mais pas le deuxième.
daniel kullmann
6
@danielkullmann In bash -c 'some-code' a b c, $0is a, $1is b..., donc si vous voulez l' $@être, a, b, cvous devez insérer quelque chose avant. Parce que $0est également utilisé lors de l'affichage des messages d'erreur, c'est une bonne idée d'utiliser le nom du shell, ou quelque chose qui a du sens dans ce contexte.
Stéphane Chazelas
@ StéphaneChazelas, merci pour cette jolie réponse.
User9102d82
5

Ce n'est pas toujours applicable, mais quand c'est le cas, c'est une solution simple. Définissez l' globstaroption ( set -o globstardans ksh93, shopt -s globstardans bash ≥4; elle est activée par défaut dans zsh). Utilisez ensuite **/pour faire correspondre le répertoire en cours et ses sous-répertoires de manière récursive.

Par exemple, au lieu de find . -name '*.txt' -exec somecommand {} \;, vous pouvez exécuter

for x in **/*.txt; do somecommand "$x"; done

Au lieu de find . -type d -exec somecommand {} \;, vous pouvez exécuter

for d in **/*/; do somecommand "$d"; done

Au lieu de find . -newer somefile -exec somecommand {} \;, vous pouvez exécuter

for x in **/*; do
  [[ $x -nt somefile ]] || continue
  somecommand "$x"
done

Lorsque **/ne fonctionne pas pour vous (parce que votre shell ne l'a pas ou parce que vous avez besoin d'une findoption qui n'a pas d'analogue de shell), définissez la fonction dans l' find -execargument .

Gilles 'SO- arrête d'être méchant'
la source
Je n'arrive pas à trouver une globstaroption sur la version de ksh ( ksh88) que j'utilise.
rahmu
@rahmu En effet, c'est nouveau dans ksh93. Solaris 10 n'a-t-il pas un ksh93 quelque part?
Gilles 'SO- arrête d'être méchant'
Selon ce billet de blog, ksh93 a été introduit dans Solaris 11 pour remplacer à la fois le Bourne Shell et ksh88 ...
rahmu
@rahmu. Solaris a eu ksh93 pendant un certain temps sous la forme dtksh(ksh93 avec quelques extensions X11), mais une ancienne version et peut-être une partie d'un package facultatif où CDE est facultatif.
Stéphane Chazelas
Il convient de noter que le remplacement récursif diffère du findfait qu'il exclut les fichiers de points et ne descend pas dans les répertoires de points et qu'il trie la liste des fichiers (qui peuvent tous deux être des adresses en zsh via des qualificatifs de remplacement). Aussi, par **/*opposition à ./**/*, les noms de fichiers peuvent commencer par -.
Stéphane Chazelas
4

Utilisez \ 0 comme délimiteur et lisez les noms de fichiers dans le processus en cours à partir d'une commande générée, comme ceci:

foo() {
  printf "Hello {%s}\n" "$1"
}

while read -d '' filename; do
  foo "${filename}" </dev/null
done < <(find . -maxdepth 2 -type f -print0)

Que se passe t-il ici:

  • read -d '' lit jusqu'au prochain \ 0 octet, vous n'avez donc pas à vous soucier des caractères étranges dans les noms de fichiers.
  • de même, -print0utilise \ 0 pour terminer chaque nom de fichier généré au lieu de \ n.
  • cmd2 < <(cmd1)est le même que cmd1 | cmd2sauf que cmd2 est exécuté dans le shell principal et non dans un sous-shell.
  • l'appel à foo est redirigé depuis /dev/nullpour s'assurer qu'il ne lit pas accidentellement depuis le tube.
  • $filename est cité afin que le shell n'essaye pas de diviser un nom de fichier contenant des espaces.

Maintenant, read -det <(...)sont en zsh, bash et ksh 93u, mais je ne suis pas sûr des versions précédentes de ksh.

aecolley
la source
4

si vous voulez qu'un processus enfant, généré à partir de votre script, utilise une fonction shell prédéfinie, vous devez l'exporter avec export -f <function>

REMARQUE: export -fest spécifique à bash

car seul un shell peut exécuter des fonctions shell :

find / -exec /bin/bash -c 'function "$1"' bash {} \;

EDIT: essentiellement votre script devrait ressembler à ceci:

#!/bin/bash
function foo() { do something; }
export -f foo
find somedir -exec /bin/bash -c 'foo "$0"' {} \;
h3rrmiller
la source
1
export -fest une erreur de syntaxe dans ksh, et imprime la définition de la fonction à l'écran dans zsh. Dans tous ksh, zshet bashcela ne résout pas le problème.
rahmu
1
find / -exec /bin/bash -c 'function' \;
h3rrmiller
Maintenant ça marche, mais seulement avec bash. Merci!
rahmu
4
Ne jamais intégrer {}dans le code shell! Cela signifie que le nom du fichier est interprété comme un code shell, il est donc très dangereux
Stéphane Chazelas