Comment parcourir les noms de fichiers renvoyés par find?

x=$(find . -name "*.txt") echo $x 

Si je lance le morceau de code ci-dessus dans le shell Bash, ce que j’obtiens est une chaîne contenant plusieurs noms de fichiers séparés par un blanc, pas une liste.

Bien sûr, je peux les séparer en blanc pour obtenir une liste, mais je suis sûr qu’il existe une meilleure façon de le faire.

Alors, quelle est la meilleure façon de parcourir les résultats d’une commande find ?

TL; DR: Si vous êtes juste là pour la réponse la plus correcte, vous voulez probablement que ma préférence personnelle soit find . -name '*.txt' -exec process {} \; find . -name '*.txt' -exec process {} \; (voir le bas de cet article). Si vous avez le temps, lisez le rest pour voir plusieurs façons différentes et les problèmes avec la plupart d’entre eux.


La réponse complète:

Le meilleur moyen dépend de ce que vous voulez faire, mais voici quelques options. Tant qu’aucun fichier ou dossier du sous-arbre n’a d’espaces dans son nom, vous pouvez simplement faire une boucle sur les fichiers:

 for i in $x; do # Not recommended, will break on whitespace process "$i" done 

Marginalement mieux, découpez la variable temporaire x :

 for i in $(find -name \*.txt); do # Not recommended, will break on whitespace process "$i" done 

C’est beaucoup mieux de globaliser quand vous le pouvez. Espace blanc sécurisé pour les fichiers du répertoire en cours:

 for i in *.txt; do # Whitespace-safe but not recursive. process "$i" done 

En activant l’option globstar , vous pouvez regrouper tous les fichiers correspondants dans ce répertoire et tous les sous-répertoires:

 # Make sure globstar is enabled shopt -s globstar for i in **/*.txt; do # Whitespace-safe and recursive process "$i" done 

Dans certains cas, par exemple si les noms de fichiers sont déjà dans un fichier, vous devrez peut-être utiliser read :

 # IFS= makes sure it doesn't sortingm leading and trailing whitespace # -r prevents interpretation of \ escapes. while IFS= read -r line; do # Whitespace-safe EXCEPT newlines process "$line" done < filename 

read peut être utilisé en toute sécurité en combinaison avec find en définissant le délimiteur de manière appropriée:

 find . -name '*.txt' -print0 | while IFS= read -r -d $'\0' line; do process $line done 

Pour des recherches plus complexes, vous voudrez probablement utiliser find avec son option -exec ou avec -print0 | xargs -0 -print0 | xargs -0 :

 # execute `process` once for each file find . -name \*.txt -exec process {} \; # execute `process` once with all the files as arguments*: find . -name \*.txt -exec process {} + # using xargs* find . -name \*.txt -print0 | xargs -0 process # using xargs with arguments after each filename (implies one run per filename) find . -name \*.txt -print0 | xargs -0 -I{} process {} argument 

find peut également cd dans le répertoire de chaque fichier avant d'exécuter une commande en utilisant -execdir au lieu de -exec , et peut être rendu interactif (invite avant d'exécuter la commande pour chaque fichier) en utilisant -ok au lieu de -exec (ou -okdir au lieu de -execdir ).

*: Techniquement, find et xargs (par défaut) exécuteront la commande avec autant d’arguments qu’ils peuvent contenir sur la ligne de commande, autant de fois que nécessaire pour parcourir tous les fichiers. Dans la pratique, à moins que vous n'ayez un très grand nombre de fichiers, cela n'aura aucune importance, et si vous dépassez la longueur mais que vous en avez besoin sur la même ligne de commande, vous trouvez la solution différente.

 find . -name "*.txt"|while read fname; do echo "$fname" done 

Remarque: cette méthode et la (seconde) méthode indiquée par bmargulies peuvent être utilisées avec un espace blanc dans les noms de fichiers / dossiers.

Pour avoir le cas un peu exotique de nouvelles lignes dans les noms de fichiers / dossiers, vous devrez utiliser le prédicat -exec de find comme ceci:

 find . -name '*.txt' -exec echo "{}" \; 

Le {} est l’espace réservé pour l’élément trouvé et le \; est utilisé pour terminer le prédicat -exec .

Et pour être complet, laissez-moi append une autre variante – vous devez aimer les manières * nix pour leur polyvalence:

 find . -name '*.txt' -print0|xargs -0 -n 1 echo 

Cela séparerait les éléments imprimés avec un caractère \0 qui n’est autorisé dans aucun des systèmes de fichiers dans les noms de fichiers ou de dossiers, à ma connaissance, et devrait donc couvrir toutes les bases. xargs ramasse un par un puis …

Quoi que vous fassiez, n’utilisez pas for boucle for :

 # Don't do this for file in $(find . -name "*.txt") do …code using "$file" done 

Trois raisons:

  • Pour que la boucle for démarre, la find doit être terminée.
  • Si un nom de fichier contient des espaces (y compris un espace, une tabulation ou une nouvelle ligne), il sera traité comme deux noms distincts.
  • Bien que cela soit peu probable, vous pouvez surcharger votre tampon de ligne de commande. Imaginez que votre tampon de ligne de commande contienne 32 Ko, et que votre boucle for renvoie 40 Ko de texte. Ce dernier 8 Ko sera déposé sur votre boucle for et vous ne le saurez jamais.

Utilisez toujours une construction while read :

 find . -name "*.txt" -print0 | while read -d $'\0' file do …code using "$file" done 

La boucle sera exécutée pendant l’exécution de la commande find . De plus, cette commande fonctionnera même si un nom de fichier est renvoyé avec des espaces. Et, vous ne déborderez pas votre tampon de ligne de commande.

-print0 utilisera la valeur NULL comme séparateur de fichier au lieu d’une nouvelle ligne et -d $'\0' utilisera NULL comme séparateur lors de la lecture.

Les noms de fichiers peuvent inclure des espaces et même des caractères de contrôle. Les espaces sont (par défaut) des délimiteurs pour l’extension de shell dans bash et par conséquent, x=$(find . -name "*.txt") de la question n’est pas du tout recommandé. Si find obtient un nom de fichier avec des espaces, par exemple "the file.txt" vous obtiendrez 2 chaînes séparées pour le traitement, si vous traitez x dans une boucle. Vous pouvez améliorer cela en changeant le délimiteur (bash IFS Variable) par exemple en \r\n , mais les noms de fichiers peuvent inclure des caractères de contrôle – ce n’est donc pas une méthode (complètement) sûre.

De mon sharepoint vue, il existe 2 modèles recommandés (et sûrs) pour le traitement des fichiers:

1. Utilisez pour l’extension de la boucle et du nom de fichier:

 for file in ./*.txt; do [[ ! -e $file ]] && continue # continue, if file does not exist # single filename is in $file echo "$file" # your code here done 

2. Utilisez find-read-while et la substitution de processus

 while IFS= read -r -d '' file; do # single filename is in $file echo "$file" # your code here done < <(find . -name "*.txt" -print0) 

Remarques

sur le motif 1:

  1. bash renvoie le motif de recherche ("* .txt") si aucun fichier correspondant n'est trouvé - la ligne supplémentaire "continue si le fichier n'existe pas" est nécessaire. voir le manuel Bash, extension de nom de fichier
  2. l'option shell nullglob peut être utilisée pour éviter cette ligne supplémentaire.
  3. "Si l'option shell failglob est définie et qu'aucune correspondance n'est trouvée, un message d'erreur est imprimé et la commande n'est pas exécutée." (extrait du manuel de Bash ci-dessus)
  4. option shell globstar : "Si défini, le motif '**' utilisé dans un contexte d'extension de nom de fichier correspondra à tous les fichiers et à zéro ou plusieurs répertoires et sous-répertoires. Si le modèle est suivi d'un '/', seuls les répertoires et sous-répertoires correspondent." voir Bash Manual, Shopt Builtin
  5. autres options pour l'extension du nom de fichier: extglob , nocaseglob , variable GLOBIGNORE & shell GLOBIGNORE

sur le motif 2:

  1. les noms de fichiers peuvent contenir des espaces, des tabulations, des espaces, des nouvelles lignes, ... pour traiter les noms de fichiers en toute sécurité, find -print0 est utilisé: filename est imprimé avec tous les caractères de contrôle et terminé par NUL. voir aussi la page de manuel Gnu Findutils, la gestion des noms de fichiers non sécurisés , la gestion sécurisée des noms de fichiers , les caractères inhabituels dans les noms de fichiers . Voir David A. Wheeler ci-dessous pour une discussion détaillée de ce sujet.

  2. Il existe des modèles possibles pour traiter les résultats de recherche dans une boucle while. D'autres (kevin, David W.) ont montré comment faire cela en utilisant des tuyaux:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"

    Lorsque vous essayez ce morceau de code, vous verrez que cela ne fonctionne pas: files_found est toujours "true" et le code fera toujours écho "aucun fichier trouvé". La raison en est que chaque commande d'un pipeline est exécutée dans un sous-shell distinct, de sorte que la variable modifiée à l'intérieur de la boucle (sous-shell séparé) ne modifie pas la variable dans le script shell principal. C'est pourquoi je recommande d'utiliser la substitution de processus comme modèle "meilleur", plus utile et plus général.
    Voir Je règle les variables dans une boucle qui est dans un pipeline. Pourquoi disparaissent-ils ... (extrait de la FAQ de Greg Bash) pour une discussion détaillée sur ce sujet.

Références et sources supplémentaires:

  • Manuel Gnu Bash, Correspondance des modèles

  • Noms de fichiers et noms de chemin dans Shell: comment le faire correctement, David A. Wheeler

  • Pourquoi tu ne lis pas les lignes avec "for", le Wiki de Greg

  • Pourquoi vous ne devriez pas parsingr le résultat de ls (1), le Wiki de Greg

  • Manuel Gnu Bash, Substitution de processus

 # Doesn't handle whitespace for x in `find . -name "*.txt" -print`; do process_one $x done or # Handles whitespace and newlines find . -name "*.txt" -print0 | xargs -0 -n 1 process_one 

Vous pouvez stocker votre sortie find dans le tableau si vous souhaitez utiliser la sortie plus tard en tant que:

 array=($(find . -name "*.txt")) 

Maintenant, pour imprimer chaque élément dans une nouvelle ligne, vous pouvez soit utiliser for boucle itérative sur tous les éléments du tableau, soit utiliser l’instruction printf.

 for i in ${array[@]};do echo $i; done 

ou

 printf '%s\n' "${array[@]}" 

Vous pouvez aussi utiliser:

 for file in "`find . -name "*.txt"`"; do echo "$file"; done 

Cela va imprimer chaque nom de fichier en ligne nouvelle

Pour imprimer uniquement la sortie de find sous forme de liste, vous pouvez utiliser l’une des méthodes suivantes:

 find . -name "*.txt" -print 2>/dev/null 

ou

 find . -name "*.txt" -print | grep -v 'Permission denied' 

Cela supprimera les messages d’erreur et indiquera uniquement le nom du fichier comme sortie dans une nouvelle ligne.

Si vous voulez faire quelque chose avec les noms de fichiers, le stockage dans le tableau est bon, sinon il n’y a pas besoin de consumr cet espace et vous pouvez directement imprimer le résultat à partir de find .

Avec n’importe quel $SHELL qui le supporte (sh / bash / zsh / …):

 find . -name "*.txt" -exec $SHELL -c ' echo "$0" ' {} \; 

Terminé.

Si vous pouvez supposer que les noms de fichiers ne contiennent pas de nouvelles lignes, vous pouvez lire la sortie de find dans un tableau Bash en utilisant la commande readarray :

 readarray -tx < <(find . -name '*.txt') 

Remarque:

  • -t provoque le readarray pour readarray nouvelles lignes.
  • Cela ne fonctionnera pas si readarray trouve dans un tube, d'où la substitution de processus.
  • readarray est disponible depuis Bash 4.

readarray peut également être readarray tant que mapfile avec les mêmes options.

Référence: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream

En supposant que vous n’avez pas de noms de fichiers avec des nouvelles lignes intégrées, vous pouvez obtenir une liste comme ceci:

 list=($(find . -name '*.txt')) printf '%s\n' "${list[@]}" 

Comme d’autres personnes l’ont souligné, le fait que cela soit utile dépend du contexte.

find -xdev -type f -name *.txt -exec ls -l {} \;

Cela listera les fichiers et donnera des détails sur les atsortingbuts.

basé sur d’autres réponses et commentaires de @phk, en utilisant fd # 3:
(qui permet toujours d’utiliser stdin dans la boucle)

 while IFS= read -rf < &3; do echo "$f" done 3< <(find . -iname "*filename*") 

Vous pouvez mettre les noms de fichiers renvoyés par find dans un tableau comme celui-ci:

 array=() while IFS= read -r -d $'\0'; do array+=("$REPLY") done < <(find . -name '*.txt' -print0) 

Maintenant, vous pouvez simplement parcourir le tableau pour accéder aux éléments individuels et faire ce que vous voulez avec eux.

Note: C'est un espace blanc sécurisé.

J’aime utiliser find qui est d’abord assigné à la variable et IFS est passé à la nouvelle ligne comme suit:

 FilesFound=$(find . -name "*.txt") IFSbkp="$IFS" IFS=$'\n' counter=1; for file in $FilesFound; do echo "${counter}: ${file}" let counter++; done IFS="$IFSbkp" 

Juste au cas où vous souhaiteriez répéter plusieurs actions sur le même jeu de données et que la recherche est très lente sur votre serveur (utilisation élevée I / 0)

Que diriez-vous si vous utilisez grep au lieu de trouver?

 ls | grep .txt$ > out.txt 

Maintenant, vous pouvez lire ce fichier et les noms de fichiers sont sous la forme d’une liste.