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:
find
doit être terminée. 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:
nullglob
peut être utilisée pour éviter cette ligne supplémentaire. 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) 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 extglob
, nocaseglob
, variable GLOBIGNORE
& shell GLOBIGNORE
sur le motif 2:
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.
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. 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
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.