web-dev-qa-db-fra.com

Comment utiliser la commande exec dans une boucle for après une commande find?

J'essaie de faire fonctionner ça depuis un moment maintenant. Je souhaite rechercher une liste de fichiers, puis les copier tous dans un certain chemin.

Si j'exécute la commande séparément, cela fonctionne comme suit:

find <path> -name 'file1' -exec cp '{}' <path2> \;

Mais j'ai été incapable de l'exécuter dans une boucle for.

#this works
for f in file1 file2 file3...; do find <path> -name "$f" \; done;
#none of these work (this code tries to find and copy the files named file and list from the path)
for f in file1 file2 file3...; do find <path> -name "$f" -exec cp '{}' <path2> \; done;
for f in file1 file2 file3...; do find <path> -name "$f" -exec cp {} <path2> \; done;

J'ai essayé quelques autres trucs qui ne fonctionneraient probablement pas. La première ligne de la citation de code reste bloquée et les autres ne copient rien même s'ils ne sont pas bloqués.

Après une recherche, je n'ai rien pu faire avec exec dans une boucle for et à ce stade, je ne sais pas trop quoi faire.

J'ai résolu le problème immédiat en recherchant les fichiers, en enregistrant les résultats dans un autre fichier, puis en exécutant une copie dans une boucle for séparément, mais j'aimerais quand même savoir comment procéder.

4
John Hamilton

Bien que les autres réponses soient correctes, je souhaite proposer une approche différente ne nécessitant pas plusieurs appels de find pour analyser la même structure de répertoires à plusieurs reprises. L'idée de base est d'utiliser find pour

  1. générer une liste de fichiers correspondant aux critères communs,
  2. appliquer un filtre personnalisé à cette liste, et
  3. effectuer une action, e. g. cp, sur les entrées de la liste filtrée.

Mise en œuvre 1

(nécessite que Bash lise des enregistrements délimités par des octets nuls)

find /some/path -type f -print0 |
while read -rd '' f; do
  case "${f##*/}" in
    file1|file2|file3)
      printf '%s\0' "$f";;
  esac
done |
xargs -r0 -- cp -vt /some/other/path --

Chaque tuyau correspond au début de la prochaine étape des trois étapes décrites ci-dessus.

L'instruction case a l'avantage d'autoriser les correspondances globbing . Sinon, vous pouvez utiliser Bash expressions conditionnelles :

if [[ "${f##*/}" == file1 || "${f##*/}" == file2 || "${f##*/}" == file3 ]]; then
  printf '%s\0' "$f"
fi

Mise en œuvre 2

Si la liste des noms de fichiers à comparer est un peu plus longue et ne peut pas être remplacée par un petit ensemble de modèles globbing , ou si la liste des noms de fichiers à associer n'est pas connue au moment de l'écriture, vous peut avoir recours à un tablea qui contient la liste des noms de fichiers d’intérêt:

FILE_NAMES=( "file1" "file2" "file3" ... )

find /some/path -type f -print0 |
while read -rd '' f; do
  for needle in "${FILE_NAMES[@]}"; do
    if [ "${f##*/}" = "$needle" ]; then
      printf '%s\0' "$f"
    fi
  done
done |
xargs -r0 -- cp -vt /some/other/path --

Mise en œuvre 3

En variante, nous pouvons utiliser un tableau associatif qui, espérons-le, a des temps de recherche plus rapides que les tableaux "list":

declare -A FILE_NAMES=( ["file1"]= ["file2"]= ["file3"]= ... )  # Note the superscripts with []=

find /some/path -type f -print0 |
while read -rd '' f; do
  if [ -v FILE_NAMES["${f##*/}"] ]; then
    printf '%s\0' "$f"
  fi
done |
xargs -r0 -- cp -vt /some/other/path --
4
David Foerster

Deux problèmes:

  1. for f in some_file n'itère pas le contenu de some_file, il suffit d'itérer ou de prendre la chaîne littérale some_file. Pour surmonter cela, oubliez la boucle for pour parcourir le contenu du fichier, utilisez une construction while correctement implémentée.

  2. Les variables ne seront pas développées si elles sont placées entre guillemets simples, '$f' dans ce cas. Pour que votre idée originale fonctionne, utilisez des guillemets doubles.

En les assemblant, en supposant que les noms de fichiers sont séparés par une nouvelle ligne dans le fichier file_list:

while IFS= read -r f; do 
    find /path1 -name "$f" -exec cp '{}' /path2 \;
done <file_list

Ou si vous savez que les fichiers se trouveront dans le répertoire /path1, et non dans un sous-répertoire de celui-ci, vous pouvez simplement utiliser un tableau pour obtenir les noms de fichiers et utiliser directement (GNU) cp, en supposant que newline noms de fichiers séparés à l'intérieur de file_list:

( 
 IFS=$'\n'
 files=( $(<file_list) )
 cp -t /path2 "${files[@]}"
)

En cas de très grand nombre de fichiers, mieux vaut effectuer une itération individuelle et cp plutôt que de les placer dans un tableau:

while IFS= read -r f; do cp -- "$f" /path2; done <file_list

Si vous avez une liste de fichiers comme par exemple file1 file2 file3 ... directement dans la construction for, alors simplement utiliser des guillemets doubles ferait:

for f in file1 file2 file3; do 
    find /path1 -name "$f" -exec cp '{}' /path2 \;
done

Maintenant, vous pouvez utiliser cp directement ici si tous les fichiers ne sont dans aucun sous-répertoire:

cp -t /path2 file1 file2 file3

Vous pouvez également donner des chemins absolus ou relatifs statiques au cas où vous voudriez utiliser cp uniquement pour traiter les fichiers de tous les sous-répertoires.

7
heemayl

Vous avez précisé que votre liste de fichiers n'est pas un fichier texte, mais une série de noms de fichiers que vous souhaitez rechercher avec find. Vous avez mentionné que cela fonctionne pour vous:

for f in file1 file2 file3 ... ; do find <path> -name "$f" \; done;

vous voulez probablement dire que vous obtenez la liste de fichiers attendue à partir de cette commande.

Vous dites alors que cela ne fonctionne pas:

for f in file1 file2 file3 ... ; do find <path> -name "$f" -exec cp '{}' <path2> \; done

vous voulez probablement dire que les fichiers listés précédemment ne sont pas copiés dans <path2>. Je ne suis pas sûr de l'erreur que vous obtenez, mais comme il est écrit, je m'attendrais à ce que la commande soit bloquée comme ceci:

$for f in file1 file2 file3 ... ; do find <path> -name "$f" -exec cp '{}' <path2> \; done
>

en attente du ; manquant. En supposant que vous corrigiez ce problème, je ne peux penser à aucune autre raison évidente pour laquelle votre commande échouerait définitivement. Vous devriez être capable de gérer avec

for f in file1 file2 file3 ... ; do find <path> -name "$f" -exec cp '{}' <path2> \; ; done

Cependant, je suggère d'utiliser l'indicateur -t pour spécifier le répertoire. Je dois remercier David Foerster pour ce commentaire qui, à mon avis, présente la meilleure forme pour une invocation cp ou mv à l'aide de find . Il refusera également d'écraser les fichiers en double.

for f in file1 file2 file3; do find /path/to/search -name "$f" -exec cp -vt /path/to/destination -- {} + ; done

Remarques

  • -v be verbose - dites-nous ce qui se fait
  • -t utilise le répertoire spécifié comme destination
  • -- n'accepte aucune autre option
  • + s'il y a plusieurs correspondances (dans votre cas, c'est peu probable car for ne sera exécuté qu'une fois pour chaque élément de la liste des fichiers, mais il peut y avoir plusieurs fichiers correspondants pour chaque nom), puis construisez un argument liste pour cp au lieu d'exécuter cp plusieurs fois
  • ; sépare les commandes dans le shell. La forme d'une boucle for est

    for var in things; do command "$var"; done
    

    S'il ne voit pas le ; avant done, bash attendra ; ou un signal d'interruption.

5
Zanna