Exécuter la commande sur tous les fichiers d'un répertoire


290

Quelqu'un pourrait-il fournir le code pour effectuer les opérations suivantes: Supposons qu'il existe un répertoire de fichiers, qui doivent tous être exécutés via un programme. Le programme sort les résultats en sortie standard. J'ai besoin d'un script qui ira dans un répertoire, exécutera la commande sur chaque fichier et concaténera la sortie dans un gros fichier de sortie.

Par exemple, pour exécuter la commande sur 1 fichier:

$ cmd [option] [filename] > results.out

3
Je voudrais ajouter à la question. Peut-on le faire en utilisant xargs? par exemple, ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
Ozair Kafray

2
C'est possible, mais vous ne voudrezls probablement pas l'utiliser pour conduire xargs. Si cmdc'est écrit avec compétence, vous pouvez peut-être simplement le faire cmd <wildcard>.
tripleee

Réponses:


425

Le code bash suivant passera $ file à la commande où $ file représentera chaque fichier dans / dir

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

Exemple

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt

23
Si aucun fichier n'existe /dir/, la boucle s'exécute toujours une fois avec une valeur de '*' pour $file, ce qui peut être indésirable. Pour éviter cela, activez nullglob pendant la durée de la boucle. Ajoutez cette ligne avant la boucle shopt -s nullglobet cette ligne après la boucle shopt -u nullglob #revert nullglob back to it's normal default state.
Stew-au

43
+1, et cela m'a juste coûté toute ma collection de papiers peints. tout le monde après moi, utilisez des guillemets doubles. "$ file"
Behrooz

Si le fichier de sortie est le même à l'intérieur de la boucle, il est beaucoup plus efficace de rediriger en dehors de la boucle done >results.out(et probablement alors vous pouvez écraser au lieu d'ajouter, comme je l'ai supposé ici).
tripleee

Comment obtenir des fichiers de résultats individuels dont le nom est personnalisé dans leurs fichiers d'entrée?
Timothy Swan

1
soyez prudent en utilisant cette commande pour une énorme quantité de fichiers en dir. Utilisez plutôt find -exec.
kolisko

181

Que dis-tu de ça:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1L'argument empêche la recherche de descendre récursivement dans les sous-répertoires. (Si vous souhaitez que ces répertoires imbriqués soient traités, vous pouvez omettre cela.)
  • -type -f spécifie que seuls les fichiers ordinaires seront traités.
  • -exec cmd option {}lui dit de s'exécuter cmdavec le spécifié optionpour chaque fichier trouvé, avec le nom de fichier substitué pour{}
  • \; indique la fin de la commande.
  • Enfin, la sortie de toutes les cmdexécutions individuelles est redirigée vers results.out

Cependant, si vous vous souciez de l'ordre dans lequel les fichiers sont traités, vous feriez mieux d'écrire une boucle. Je pense que findtraite les fichiers dans l'ordre des inodes (bien que je puisse me tromper à ce sujet), ce qui n'est peut-être pas ce que vous voulez.


1
C'est la bonne façon de traiter les fichiers. L'utilisation d'une boucle for est sujette aux erreurs pour de nombreuses raisons. Le tri peut également être effectué en utilisant d'autres commandes telles que statet sort, qui dépendent bien sûr des critères de tri.
tuxdna

1
si je voulais exécuter deux commandes, comment pourrais-je les lier après l' -execoption? Dois-je les mettre entre guillemets simples ou quelque chose?
frei

findest toujours la meilleure option car vous pouvez filtrer par modèle de nom de fichier avec option -nameet vous pouvez le faire en une seule commande.
João Pimentel Ferreira

3
@frei la réponse à votre question est ici: stackoverflow.com/a/6043896/1243247 mais simplement ajouter des -execoptions:find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira

2
comment pouvez-vous référencer le nom de fichier en option?
Toskan

54

Je fais cela sur mon Raspberry Pi à partir de la ligne de commande en exécutant:

for i in *;do omxplayer "$i";done

7

Les réponses acceptées / à vote élevé sont excellentes, mais elles manquent de quelques détails concrets. Cette publication couvre les cas sur la façon de mieux gérer lorsque l'expansion du nom de chemin du shell (glob) échoue, lorsque les noms de fichiers contiennent des sauts de ligne / symboles de tiret et le déplacement de la direction de sortie de la commande hors de la boucle for lors de l'écriture des résultats dans un fichier.

Lors de l'exécution de l'expansion du shell shell à l'aide de l'extension, *il est possible que l'expansion échoue s'il n'y a aucun fichier dans le répertoire et une chaîne glob non développée sera transmise à la commande à exécuter, ce qui pourrait avoir des résultats indésirables. Le bashshell fournit une option shell étendue pour cette utilisation nullglob. Ainsi, la boucle devient essentiellement la suivante dans le répertoire contenant vos fichiers

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

Cela vous permet de quitter en toute sécurité la boucle for lorsque l'expression ./*ne renvoie aucun fichier (si le répertoire est vide)

ou d'une manière conforme POSIX ( nullglobest bashspécifique)

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

Cela vous permet d'aller à l'intérieur de la boucle lorsque l'expression échoue pour une fois et la condition [ -f "$file" ]vérifie si la chaîne non développée ./*est un nom de fichier valide dans ce répertoire, ce qui ne le serait pas. Donc, en cas d'échec de cette condition, en utilisant continuenous reprenons la forboucle qui ne s'exécutera pas par la suite.

Notez également l'utilisation de --juste avant de passer l'argument du nom de fichier. Cela est nécessaire car, comme indiqué précédemment, les noms de fichiers du shell peuvent contenir des tirets n'importe où dans le nom de fichier. Certaines des commandes shell l'interprètent et les traitent comme une option de commande lorsque le nom n'est pas cité correctement et exécute la commande en pensant si l'indicateur est fourni.

Le --signale la fin des options de ligne de commande dans ce cas, ce qui signifie que la commande ne doit pas analyser les chaînes au-delà de ce point en tant que drapeaux de commande, mais uniquement en tant que noms de fichiers.


La citation double des noms de fichiers résout correctement les cas où les noms contiennent des caractères globaux ou des espaces blancs. Mais les noms de fichiers * nix peuvent également contenir des retours à la ligne. Nous limitons donc les noms de fichiers avec le seul caractère qui ne peut pas faire partie d'un nom de fichier valide - l'octet nul ( \0). Comme il bashutilise en interne des Cchaînes de style dans lesquelles les octets nuls sont utilisés pour indiquer la fin de la chaîne, c'est le bon candidat pour cela.

Donc, en utilisant l' printfoption shell pour délimiter les fichiers avec cet octet NULL en utilisant l' -doption de readcommande, nous pouvons faire ci-dessous

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

Le nullglobet le printfsont entourés, (..)ce qui signifie qu'ils sont essentiellement exécutés dans un sous-shell (shell enfant), car pour éviter l' nullgloboption de réfléchir sur le shell parent, une fois la commande terminée. L' -d ''option de readcommande n'est pas compatible POSIX, a donc besoin d'un bashshell pour cela. En utilisant la findcommande, cela peut être fait comme

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

Pour les findimplémentations qui ne prennent pas en charge -print0(autres que les implémentations GNU et FreeBSD), cela peut être émulé en utilisantprintf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

Un autre correctif important consiste à déplacer la redirection hors de la boucle for pour réduire un nombre élevé d'E / S de fichiers. Lorsqu'il est utilisé à l'intérieur de la boucle, le shell doit exécuter des appels système deux fois pour chaque itération de la boucle for, une fois pour ouvrir et une fois pour fermer le descripteur de fichier associé au fichier. Cela deviendra un goulot d'étranglement sur vos performances pour l'exécution de grandes itérations. La suggestion recommandée serait de le déplacer hors de la boucle.

En étendant le code ci-dessus avec ces correctifs, vous pouvez le faire

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

qui placera essentiellement le contenu de votre commande pour chaque itération de votre entrée de fichier dans stdout et lorsque la boucle se terminera, ouvrez le fichier cible une fois pour écrire le contenu de la stdout et l'enregistrer. La findversion équivalente de la même serait

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out

1
+1 pour vérifier que le fichier existe. Si vous recherchez dans un répertoire inexistant, $ file contient la chaîne d'expression régulière "/ rép_invald / *" qui n'est pas un nom de fichier valide.
cdalxndr

3

Un moyen rapide et sale qui fait parfois le travail est:

find directory/ | xargs  Command 

Par exemple, pour trouver le nombre de lignes dans tous les fichiers du répertoire courant, vous pouvez faire:

find . | xargs wc -l

8
@Hubert Pourquoi avez-vous des nouvelles lignes dans vos noms de fichiers?!
musicin3d

2
ce n'est pas une question de "pourquoi", c'est une question d'exactitude - les noms de fichiers n'ont pas à inclure de caractères imprimables, ils n'ont même pas à être des séquences UTF-8 valides. De plus, ce qui est une nouvelle ligne dépend beaucoup du codage, un codage ♀ est la nouvelle ligne d'un autre. Voir page de codes 437
Hubert Kario

2
cmon, vraiment? cela fonctionne 99,9% du temps, et il a dit "rapide et sale"
Edoardo

Je ne suis pas fan des scripts Bash "rapides et sales" (AKA "cassés"). Tôt ou tard, cela se termine par des choses comme le fameux "Moved ~/.local/share/steam. Ran Steam. Il a supprimé tout sur le système appartenant à l'utilisateur." rapport d'erreur.
réduction de l'activité le

Cela ne fonctionnera pas non plus avec les fichiers qui ont des espaces dans le nom.
Shamas S

2

J'avais besoin de copier tous les fichiers .md d'un répertoire dans un autre, alors voici ce que j'ai fait.

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

Ce qui est assez difficile à lire, alors décomposons-le.

premier cd dans le répertoire avec vos fichiers,

for i in **/*.md; pour chaque fichier de votre modèle

mkdir -p ../docs/"$i"créez ce répertoire dans un dossier docs en dehors du dossier contenant vos fichiers. Ce qui crée un dossier supplémentaire avec le même nom que ce fichier.

rm -r ../docs/"$i" supprimer le dossier supplémentaire créé à la suite de mkdir -p

cp "$i" "../docs/$i" Copiez le fichier réel

echo "$i -> ../docs/$i" Faites écho à ce que vous avez fait

; done Vivre heureux pour toujours


Remarque: pour **fonctionner, l' globstaroption shell doit être définie:shopt -s globstar
Hubert Kario

2

Vous pouvez utiliser xarg

ls | xargs -L 1 -d '\n' your-desired-command

-L 1 fait passer 1 élément à la fois

-d '\n'make output of lsest divisé en fonction de la nouvelle ligne.


1

Basé sur l'approche de @Jim Lewis:

Voici une solution rapide utilisant findet triant également les fichiers par date de modification:

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

Pour le tri, voir:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time


cela ne fonctionnera pas si les fichiers ont des nouvelles lignes dans leurs noms
Hubert Kario

1
@HubertKario Vous pouvez en savoir plus sur -print0pour findet -0pour xargsqui utilisent le caractère nul au lieu de les espaces blancs (y compris les nouvelles lignes).
tuxdna

oui, l'utilisation -print0est quelque chose qui aide, mais l'ensemble du pipeline doit utiliser quelque chose comme ça, et ce sortn'est pas le cas
Hubert Kario

1

je pense que la solution simple est:

sh /dir/* > ./result.txt

2
Avez-vous bien compris la question? Cela essaiera simplement d'exécuter chaque fichier du répertoire via le shell - comme s'il s'agissait d'un script.
rdas

1

Profondeur max

J'ai trouvé que cela fonctionne bien avec la réponse de Jim Lewis, ajoutez simplement un peu comme ceci:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

Ordre de tri

Si vous souhaitez exécuter dans l'ordre de tri, modifiez-le comme ceci:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

Juste pour un exemple, cela s'exécutera dans l'ordre suivant:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

Profondeur illimitée

Si vous souhaitez exécuter en profondeur illimitée par certaines conditions, vous pouvez utiliser ceci:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

puis placez au-dessus de chaque fichier dans les répertoires enfants comme ceci:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

et quelque part dans le corps du fichier parent:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.