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

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

si j'exécute 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 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 une meilleure façon de le faire.

Alors, quelle est la meilleure façon de parcourir les résultats d'une findcommande?

Haiyuan Zhang
la source
3
La meilleure façon de parcourir les noms de fichiers dépend en grande partie de ce que vous voulez réellement en faire, mais à moins que vous ne puissiez garantir qu'aucun fichier ne comporte d'espaces dans leur nom, ce n'est pas une excellente façon de le faire. Alors, que voulez-vous faire en bouclant sur les fichiers?
Kevin
1
Concernant la prime : l'idée principale ici est d'obtenir une réponse canonique qui couvre tous les cas possibles (noms de fichiers avec de nouvelles lignes, caractères problématiques ...). L'idée est alors d'utiliser ces noms de fichiers pour faire des trucs (appeler une autre commande, effectuer un renommage ...). Merci!
fedorqui 'SO arrête de nuire'
N'oubliez pas qu'un fichier ou un nom de dossier peut contenir ".txt" suivi d'un espace et d'une autre chaîne, par exemple "quelque chose.txt quelque chose" ou "quelque
chose.txt
Utilisez un tableau, pas var x=( $(find . -name "*.txt") ); echo "${x[@]}"Ensuite, vous pouvez parcourirfor item in "${x[@]}"; { echo "$item"; }
Ivan

Réponses:

392

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 le bas de cet article). Si vous avez le temps, lisez le reste pour voir plusieurs façons différentes et les problèmes avec la plupart d'entre elles.


La réponse complète:

La meilleure façon dépend de ce que vous voulez faire, mais voici quelques options. Tant qu'aucun fichier ou dossier dans la sous-arborescence n'a d'espace 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

Il est beaucoup mieux glob quand vous le pouvez. Sécuritaire pour les espaces blancs, pour les fichiers du répertoire courant:

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

En activant l' globstaroption, vous pouvez glober 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 trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readpeut être utilisé en toute sécurité en combinaison avec finden définissant 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 -execoption, 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

findpeut également se placer dans le répertoire de chaque fichier avant d'exécuter une commande en utilisant -execdirau lieu de -exec, et peut être rendu interactif (demander avant d'exécuter la commande pour chaque fichier) en utilisant -okau lieu de -exec(ou -okdirau lieu de -execdir).

*: Techniquement, les deux findet xargs(par défaut) exécuteront la commande avec autant d'arguments qu'ils peuvent tenir sur la ligne de commande, autant de fois qu'il faut pour parcourir tous les fichiers. En pratique, à moins que vous n'ayez un très grand nombre de fichiers, cela n'aura pas d'importance, et si vous dépassez la longueur mais que vous en avez tous besoin sur la même ligne de commande, vous êtes SOL trouver une manière différente.

Kevin
la source
4
Il est à noter que dans le cas avec done < filenameet le suivant avec le tuyau, le stdin ne peut plus être utilisé (→ plus de trucs interactifs à l'intérieur de la boucle), mais dans les cas où cela est nécessaire, on peut utiliser à la 3<place de <et ajouter <&3ou -u3à la readpartie, en utilisant essentiellement un descripteur de fichier distinct. De plus, je pense que read -d ''c'est la même chose read -d $'\0'mais je ne trouve aucune documentation officielle à ce sujet pour le moment.
phk
1
pour i dans * .txt; ne fonctionne pas, si aucun fichier ne correspond. Un test xtra, par exemple [[-e $ i]] est nécessaire
Michael Brux
2
Je suis perdu avec cette partie: -exec process {} \;et je suppose que c'est une toute autre question - qu'est-ce que cela signifie et comment puis-je la manipuler? Où est un bon Q / A ou doc. dessus?
Alex Hall
1
@AlexHall, vous pouvez toujours consulter les pages de manuel ( man find). Dans ce cas, -execindique findd'exécuter la commande suivante, terminée par ;(ou +), dans laquelle {}sera remplacé par le nom du fichier qu'il traite (ou, s'il +est utilisé, tous les fichiers qui ont atteint cette condition).
Kevin
3
@phk -d ''est meilleur que -d $'\0'. Ce dernier est non seulement plus long, mais suggère également que vous pouvez passer des arguments contenant des octets nuls, mais vous ne le pouvez pas. Le premier octet nul marque la fin de la chaîne. En bash $'a\0bc'est le même que aet $'\0'est identique $'\0abc'ou juste la chaîne vide ''. help readdéclare que " Le premier caractère de délimitation est utilisé pour terminer l'entrée ", donc l'utilisation ''comme délimiteur est un peu un hack. Le premier caractère de la chaîne vide est l'octet nul qui marque toujours la fin de la chaîne (même si vous ne l'écrivez pas explicitement).
Socowi
114

Quoi que vous fassiez, n'utilisez pas de forboucle :

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

Trois raisons:

  • Pour que la boucle for démarre même, l' findexécution doit se terminer.
  • Si un nom de fichier contient un espace (y compris un espace, une tabulation ou une nouvelle ligne), il sera traité comme deux noms distincts.
  • Bien que cela soit désormais peu probable, vous pouvez saturer votre mémoire tampon de ligne de commande. Imaginez si votre tampon de ligne de commande contient 32 Ko et que votre forboucle retourne 40 Ko de texte. Ces 8 derniers Ko seront supprimés de votre forboucle et vous ne le saurez jamais.

Utilisez toujours une while readconstruction:

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 findcommande. De plus, cette commande fonctionnera même si un nom de fichier est retourné avec des espaces. Et, vous ne dépasserez pas votre mémoire tampon de ligne de commande.

Le -print0va utiliser le NULL comme séparateur de fichier au lieu d'une nouvelle ligne et le -d $'\0'va utiliser NULL comme séparateur lors de la lecture.

David W.
la source
3
Cela ne fonctionnera pas avec les nouvelles lignes dans les noms de fichiers. Utilisez -execplutôt les trouvailles .
utilisateur inconnu
2
@userunknown - Vous avez raison à ce sujet. -execest le plus sûr car il n'utilise pas du tout le shell. Cependant, NL dans les noms de fichiers est assez rare. Les espaces dans les noms de fichiers sont assez courants. Le point principal n'est pas d'utiliser une forboucle recommandée par de nombreuses affiches.
David W.
1
@userunknown - Ici. J'ai corrigé cela, donc il prendra désormais en charge les fichiers avec de nouvelles lignes, onglets et tout autre espace blanc. L'intérêt de la publication est de dire au PO de ne pas utiliser le à for file $(find)cause des problèmes associés à cela.
David W.
4
Si vous pouvez utiliser -exec, c'est mieux, mais il y a des moments où vous avez vraiment besoin du nom donné au shell. Par exemple, si vous souhaitez supprimer des extensions de fichier.
Ben Reser
5
Vous devez utiliser l' -roption pour read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood
102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Remarque: cette méthode et la (seconde) méthode montrées par bmargulies sont sûres à utiliser avec des espaces blancs dans les noms de fichiers / dossiers.

Afin de couvrir également le cas - quelque peu exotique - de nouvelles lignes dans les noms de fichiers / dossiers, vous devrez recourir au -execprédicat findcomme ceci:

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

L' {}est l'espace réservé pour l'élément trouvé et l' \;est utilisé pour mettre fin à la-exec prédicat.

Et par souci d'exhaustivité, permettez-moi d'ajouter une autre variante - vous devez aimer les façons * nix pour leur polyvalence:

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

Cela séparerait les éléments imprimés avec un \0caractère 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. xargsles ramasse un par un puis ...

0xC0000022L
la source
3
Échoue en cas de nouvelle ligne dans le nom de fichier.
utilisateur inconnu
2
@user unknown: vous avez raison, c'est un cas auquel je n'avais pas du tout pensé et qui, je pense, est très exotique. Mais j'ai ajusté ma réponse en conséquence.
0xC0000022L
5
Il vaut probablement la peine de le souligner find -print0et ce xargs -0sont à la fois des extensions GNU et non des arguments portables (POSIX). Incroyablement utile sur les systèmes qui en ont, cependant!
Toby Speight
1
Cela échoue également avec les noms de fichiers contenant des barres obliques inverses (qui read -rcorrigeraient) ou les noms de fichiers se terminant par des espaces (qui IFS= readcorrigeraient). D'où BashFAQ # 1 suggérantwhile IFS= read -r filename; do ...
Charles Duffy
1
Un autre problème avec cela est qu'il semble que le corps de la boucle s'exécute dans le même shell, mais ce n'est pas le cas, donc par exemple exitne fonctionnera pas comme prévu et les variables définies dans le corps de la boucle ne seront pas disponibles après la boucle.
EM0
17

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'expansion du shell dans bash et à la suite de cela 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 xen boucle. Vous pouvez améliorer cela en changeant le délimiteur ( IFSvariable bash ) 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 point de vue, il existe 2 modèles recommandés (et sûrs) pour le traitement des fichiers:

1. Utilisez pour l'extension de boucle et de 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 recherche par lecture 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 modèle de recherche ("* .txt") si aucun fichier correspondant n'est trouvé - donc la ligne supplémentaire "continuer, si le fichier n'existe pas" est nécessaire. voir Bash Manual, Filename Expansion
  2. L'option shell nullglobpeut être utilisée pour éviter cette ligne supplémentaire.
  3. "Si l' failgloboption 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: "S'il est défini, le modèle '**' 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. d' autres options pour l' expansion du nom de fichier: extglob, nocaseglob,dotglob & variable shellGLOBIGNORE

sur le motif 2:

  1. les noms de fichiers peuvent contenir des blancs, des tabulations, des espaces, des retours à la ligne, ... pour traiter les noms de fichiers de manière sûre, findavec -print0est utilisé: le nom de fichier est imprimé avec tous les caractères de contrôle et terminé avec NUL. voir aussi la page de manuel de Gnu Findutils, la gestion non sécurisée des noms de fichiers , la gestion sûre 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 certains modèles possibles pour traiter les résultats de la 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 qu'il ne fonctionne pas: files_foundest toujours "vrai" et le code fera toujours écho "aucun fichier trouvé". La raison est: chaque commande d'un pipeline est exécutée dans un sous-shell séparé, donc la variable modifiée à l'intérieur de la boucle (sous-shell séparé) 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 et plus général.
    Voir Je place des variables dans une boucle qui est dans un pipeline. Pourquoi disparaissent-ils ... (de la FAQ de Greg's Bash) pour une discussion détaillée sur ce sujet.

Références et sources supplémentaires:

Michael Brux
la source
8

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

Avec tous ceux $SHELLqui le supportent (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"
' {} \;
user569825
la source
1
Lent comme la mélasse (car il lance un shell pour chaque fichier) mais cela fonctionne. 1
Dawg
1
Au lieu de cela, \;vous pouvez utiliser +pour passer autant de fichiers que possible à un seul exec. Utilisez ensuite "$@"à l'intérieur du script shell pour traiter tous ces paramètres.
Socowi
3
Il y a un bug dans ce code. La boucle manque le premier résultat. C'est parce que l' $@omet car c'est généralement le nom du script. Nous avons juste besoin d'ajouter dummyentre 'et {}il peut prendre la place du nom de script, assurant tous les matches sont traités par la boucle.
BCartolo
Que faire si j'ai besoin d'autres variables extérieures au shell nouvellement créé?
Jodo
OTHERVAR=foo find . -na.....devrait vous permettre d'accéder à $OTHERVARpartir de ce shell nouvellement créé.
user569825
6
# 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
bmargulies
la source
3
for x in $(find ...)se cassera pour tout nom de fichier avec des espaces en elle. Même chose à find ... | xargsmoins que vous n'utilisiez -print0et-0
glenn jackman
1
Utilisez find . -name "*.txt -exec process_one {} ";"plutôt. Pourquoi devrions-nous utiliser xargs pour collecter des résultats, nous l'avons déjà?
utilisateur inconnu
@userunknown Eh bien, tout dépend de ce qui process_oneest. S'il s'agit d'un espace réservé pour une commande réelle , assurez-vous que cela fonctionnerait (si vous corrigez une faute de frappe et ajoutez des guillemets de fermeture après "*.txt). Mais s'il process_ones'agit d'une fonction définie par l'utilisateur, votre code ne fonctionnera pas.
toxalot
@toxalot: Oui, mais ce ne serait pas un problème d'écrire la fonction dans un script à appeler.
utilisateur inconnu
4

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

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

Maintenant, pour imprimer chaque élément dans une nouvelle ligne, vous pouvez soit utiliser l' foritération en boucle pour 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 affichera chaque nom de fichier en nouvelle ligne

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

Si vous souhaitez faire quelque chose avec les noms de fichiers, le stocker dans un tableau est bon, sinon il n'est pas nécessaire de consommer cet espace et vous pouvez directement imprimer la sortie à partir de find .

Rakholiya Jenish
la source
1
Le bouclage sur le tableau échoue avec des espaces dans les noms de fichiers.
EM0
Vous devez supprimer cette réponse. Il ne fonctionne pas avec les espaces dans les noms de fichiers ou les noms de répertoire.
jww
4

Si vous pouvez supposer que les noms de fichiers ne contiennent pas de retours à la ligne, vous pouvez lire la sortie de finddans un tableau Bash à l'aide de la commande suivante:

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

Remarque:

  • -tprovoque la readarraysuppression des sauts de ligne.
  • Cela ne fonctionnera pas si readarrayest dans un tube, d'où la substitution de processus.
  • readarray est disponible depuis Bash 4.

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

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

readarraypeut également être invoqué comme mapfileavec les mêmes options.

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

Seppo Enarvi
la source
C'est la meilleure réponse! Fonctionne avec: * Espaces dans les noms de fichiers * Aucun fichier correspondant * exitlors du bouclage sur les résultats
EM0
Ne fonctionne pas avec tous les noms de fichiers possibles, cependant - pour cela, vous devez utiliserreadarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy
3

J'aime utiliser find qui est d'abord affecté à une variable et IFS est passé à une 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 DONNÉES et trouver est très lent sur votre serveur (utilisation I / 0 élevée)

Paco
la source
2

Vous pouvez mettre les noms de fichiers renvoyés par finddans 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 aux éléments individuels et faire ce que vous voulez avec eux.

Remarque: Il est sûr pour les espaces blancs.

Jahid
la source
1
Avec bash 4.4 ou plus , vous pouvez utiliser une seule commande au lieu d'une boucle: mapfile -t -d '' array < <(find ...). Le réglage IFSn'est pas nécessaire pour mapfile.
Socowi
1

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

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

done 3< <(find . -iname "*filename*")
Florian
la source
-1

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

Cela répertorie les fichiers et donne des détails sur les attributs.

chetangb
la source
-5

Et si vous utilisiez grep au lieu de trouver?

ls | grep .txt$ > out.txt

Vous pouvez maintenant lire ce fichier et les noms de fichiers se présentent sous la forme d'une liste.

Dhruv Raj Singh Rathore
la source
6
Non, ne fais pas ça. Pourquoi vous ne devriez pas analyser la sortie de ls . C'est fragile, très fragile.
fedorqui 'SO arrête de nuire'