web-dev-qa-db-fra.com

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

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

si je lance le code ci-dessus dans Bash Shell, je reçois une chaîne contenant plusieurs noms de fichiers séparés par des blancs, pas une liste.

Bien sûr, je peux les séparer davantage en blanc pour obtenir une liste, mais je suis sûr qu’il existe un meilleur moyen de le faire.

Quel est donc le meilleur moyen de parcourir les résultats d’une commande find?

178
Haiyuan Zhang

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


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 de la sous-arborescence n'a d'espaces blancs dans son nom, vous pouvez simplement parcourir les fichiers:

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

Un peu mieux, supprimez la variable temporaire x:

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

C'est beaucoup mieux à Glob quand vous le pouvez. Espace-blanc sécurisé, pour les fichiers du répertoire actuel:

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

En activant l'option globstar, vous pouvez placer 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 fichier sont déjà dans un fichier, vous devrez peut-être utiliser read:

# IFS= makes sure it doesn't trim 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 réglant le délimiteur de manière appropriée:

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

Pour des recherches plus complexes, vous voudrez probablement utiliser find, soit avec son option -exec, soit avec -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 entrer 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 (Demander 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 que possible sur la ligne de commande, autant de fois qu'il faudra pour parcourir tous les fichiers. En pratique, à moins que vous n'ayez un très grand nombre de fichiers, cela n'a pas d'importance, et si vous dépassez la longueur mais que vous avez besoin de tous les fichiers sur la même ligne de commande, vous êtes SOL trouver un moyen différent.

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

Remarque: cette méthode and la (seconde) méthode indiquée par bmargulies peut être utilisée en toute sécurité avec des espaces dans les noms de fichiers/dossiers.

Afin de couvrir également le cas - quelque peu exotique - de sauts de ligne dans les noms de fichier/dossier, vous devrez recourir au 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 mettre fin au prédicat -exec.

Et par souci d'exhaustivité, permettez-moi d'ajouter 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 les ramasse un à un puis ...

94
0xC0000022L

Quoi que vous fassiez, n'utilisez pas de boucle for:

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

Trois raisons:

  • Pour que la boucle for puisse même démarrer, la find doit être exécutée complètement.
  • Si un nom de fichier contient des espaces (y compris des espaces, des tabulations ou des nouvelles lignes), il sera traité comme deux noms distincts.
  • Bien que cela soit maintenant peu probable, vous pouvez saturer votre tampon de ligne de commande. Imaginez si votre tampon de ligne de commande contient 32 Ko et que votre boucle for renvoie 40 Ko de texte. Les 8 derniers Ko seront déposés directement de 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 s'exécutera 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 le _-d $'\0'_ utilisera NULL comme séparateur lors de la lecture.

92
David W.

Les noms de fichiers peuvent inclure des espaces et même des caractères de contrôle. Les espaces sont des délimiteurs (par défaut) pour l'expansion du shell dans bash. Par conséquent, x=$(find . -name "*.txt") de la question n'est pas recommandé du tout. 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 modifiant le délimiteur (bash IFS Variable), par exemple. à \r\n, mais les noms de fichiers peuvent inclure des caractères de contrôle - ce n'est donc pas une méthode (totalement) sûre.

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

1. Utiliser pour l'expansion 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 la substitution recherche-lecture-en-cours et 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 Pattern 1:

  1. bash renvoie le modèle de recherche ("* .txt") si aucun fichier correspondant n'est trouvé. La ligne supplémentaire "continuer, si le fichier n'existe pas" est nécessaire. voir Manuel Bash, Extension du nom de fichier
  2. L'option shell nullglob peut être utilisée pour éviter cette ligne supplémentaire.
  3. "Si l'option failglob Shell 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 Bash ci-dessus)
  4. Option shell globstar: "Si défini, le modèle '**' utilisé dans un contexte d'extension de nom de fichier correspond à 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 correspond aux sous-répertoires. " voir Manuel Bash, Shopt Builtin
  5. autres options pour l’extension de nom de fichier: extglob, nocaseglob, dotglob & Variable shell GLOBIGNORE

sur le Pattern 2:

  1. les noms de fichiers peuvent contenir des espaces, des tabulations, des espaces, des nouvelles lignes, ... pour traiter les noms de fichiers de manière sécurisée, find avec -print0 est utilisé: le nom de fichier est imprimé avec tous les caractères de contrôle et se termine par NUL. voir aussi page de manuel Gnu Findutils, Traitement des noms de fichier non sécurisés , Traitement sûr des noms de fichier , caractères inhabituels dans les noms de fichier . Voir David A. Wheeler ci-dessous pour une discussion détaillée de ce sujet.

  2. Il existe certains 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 cette partie de code, vous verrez que cela ne fonctionne pas: files_found est toujours "true" et le code renvoie toujours "aucun fichier trouvé". La raison est la suivante: 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 distinct) ne change 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, plus général.
    Voir Je règle des variables dans une boucle en pipeline. Pourquoi elles disparaissent ... (extrait de la FAQ de Greg's Bash) pour une discussion détaillée sur ce sujet.

Références et sources supplémentaires:

12
Michael Brux
# 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
5
bmargulies

(Mise à jour pour inclure l'excellente amélioration de la vitesse de @ Socowi)

Avec tout $Shell qui le supporte (dash/zsh/bash ...):

find . -name "*.txt" -exec $Shell -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Terminé.


Réponse originale (plus courte mais plus lente):

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

Vous pouvez stocker votre sortie find dans un tableau si vous souhaitez utiliser la sortie ultérieurement comme:

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

Maintenant, pour imprimer chaque élément de la nouvelle ligne, vous pouvez soit utiliser la boucle for en effectuant une itération 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

Ceci imprimera chaque nom de fichier dans une nouvelle ligne

Pour n'imprimer que la sortie 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 donnera uniquement le nom de fichier en sortie dans la nouvelle ligne.

Si vous souhaitez utiliser les noms de fichiers, les stocker dans un tableau est une bonne chose, sinon vous n'avez pas besoin de consommer cet espace et vous pouvez directement imprimer le résultat de find.

4
Rakholiya Jenish

Si vous pouvez supposer que les noms de fichiers ne contiennent pas de nouvelles lignes, vous pouvez lire le résultat de find dans un tableau Bash à l'aide de la commande suivante:

readarray -t x < <(find . -name '*.txt')

Remarque:

  • -t provoque readarray pour supprimer les nouvelles lignes.
  • Cela ne fonctionnera pas si readarray est dans un tuyau, d’où le processus de substitution.
  • readarray est disponible depuis Bash 4.

Bash 4.4 et versions ultérieures prennent également en charge le paramètre -d pour la spécification du délimiteur. L'utilisation du caractère null, au lieu de nouvelle ligne, pour délimiter les noms de fichier fonctionne également dans les rares cas où les noms de fichier contiennent des nouvelles lignes:

readarray -d '' x < <(find . -name '*.txt' -print0)

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

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

3
Seppo Enarvi

J'aime utiliser find qui est d'abord affecté à la variable et IFS basculé sur 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 voudriez répéter plus d’actions sur le même ensemble de DATA et que la recherche est très lente sur votre serveur (utilisation élevée I/0)

2
Paco

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

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

Maintenant, vous pouvez simplement parcourir le tableau pour accéder à des éléments individuels et en faire ce que vous voulez.

Note: C'est un espace blanc en sécurité.

1
Jahid

sur la base d'autres réponses et commentaires de @phk, en utilisant le fd # 3:
(qui permet encore d'utiliser stdin à l'intérieur de la boucle)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")
0
Florian

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

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

0
chetangb