Effectuer une action dans chaque sous-répertoire à l'aide de Bash


194

Je travaille sur un script qui doit effectuer une action dans chaque sous-répertoire d'un dossier spécifique.

Quelle est la façon la plus efficace d'écrire cela?


1
Veuillez envisager de revenir en arrière et de réévaluer les réponses pour vous assurer qu'elles sont correctes - vous avez une réponse acceptée obtenant beaucoup de vues malgré les bogues majeurs (f / e, l'exécuter dans un répertoire où quelqu'un a précédemment exécuté mkdir 'foo * bar'causera fooet barsera réitéré même si ils n'existent pas et *seront remplacés par une liste de tous les noms de fichiers, même ceux qui ne sont pas des répertoires).
— Charles Duffy

1
... pire encore mkdir -p '/tmp/ /etc/passwd /', si quelqu'un s'exécute - si quelqu'un exécute un script suivant cette pratique /tmppour, par exemple, trouver des répertoires à supprimer, il pourrait finir par les supprimer /etc/passwd.
— Charles Duffy

Réponses:


177
for D in `find . -type d`
do
    //Do whatever you need with D
done

81
cela cassera les espaces blancs
— ghostdog74

2
Notez également que vous devez modifier les findparamètres si vous souhaitez un comportement récursif ou non récursif.
— Chris Tonkinson

32
La réponse ci-dessus m'a également donné le répertoire personnel, donc ce qui suit a fonctionné un peu mieux pour moi: find . -mindepth 1 -type d
— jzheaux

1
@JoshC, les deux variantes décomposeront le répertoire créé par mkdir 'directory name with spaces'en quatre mots distincts.
— Charles Duffy

4
J'avais besoin d'ajouter -mindepth 1 -maxdepth 1ou c'est allé trop profondément.
— ScrappyDev

283

Une version qui évite de créer un sous-processus:

for D in *; do
    if [ -d "${D}" ]; then
        echo "${D}"   # your processing here
    fi
done

Ou, si votre action est une commande unique, c'est plus concis:

for D in *; do [ -d "${D}" ] && my_command; done

Ou une version encore plus concise ( merci @enzotib ). Notez que dans cette version, chaque valeur de Daura une barre oblique de fin:

for D in */; do my_command; done

48
Vous pouvez éviter le ifou [avec:for D in */; do
— enzotib

2
+1 parce que les noms de répertoire ne commencent pas par ./ ​​par opposition à la réponse acceptée
— Hayri Uğur Koltuk

3
Celui-ci est correct même jusqu'à des espaces dans les noms de répertoire +1
— Alex Reinking

5
Il y a un problème avec la dernière commande: si vous êtes dans un répertoire sans sous-répertoires; il renverra "* /". Il vaut donc mieux utiliser la deuxième commande for D in *; do [ -d "${D}" ] && my_command; doneou une combinaison des deux dernières:for D in */; do [ -d $D ] && my_command; done
— Chris Maes

5
Notez que cette réponse ignore les répertoires cachés. Pour inclure des répertoires cachés, utilisez for D in .* *; doplutôt for D in *; do.
— patryk.beza

105

La manière non récursive la plus simple est:

for d in */; do
    echo "$d"
done

À /la fin indique, utilisez uniquement les répertoires.

Il n'y a pas besoin de

  • trouver
  • awk
  • ...

11
Remarque: cela n'inclura pas les répertoires de points (ce qui peut être une bonne chose, mais il est important de le savoir).
— wisbucky

Il est utile de noter que vous pouvez utiliser shopt -s dotglobpour inclure des fichiers / dotdirs lors de l'extension des caractères génériques. Voir aussi: gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html
— Steen Schütt

Je pense que vous vouliez dire au /*lieu de */avec /représentant le chemin que vous souhaitez utiliser.
— Brōtsyorfuzthrāx

2
@Shule /*serait pour le chemin absolu alors */qu'il inclurait les sous-répertoires de l'emplacement actuel
— Dan G

3
astuce utile: si vous avez besoin de couper le '/' de fin de $ d, utilisez${d%/*}
— Hafthor

18

Utilisez la findcommande.

Dans GNU find, vous pouvez utiliser le -execdirparamètre:

find . -type d -execdir realpath "{}" ';'

ou en utilisant le -execparamètre:

find . -type d -exec sh -c 'cd -P "$0" && pwd -P' {} \;

ou avec xargscommande:

find . -type d -print0 | xargs -0 -L1 sh -c 'cd "$0" && pwd && echo Do stuff'

Ou en utilisant pour la boucle:

for d in */; { echo "$d"; }

Pour la récursivité, essayez **/plutôt le globbing étendu ( ) (activer par :) shopt -s extglob.


Pour plus d'exemples, voir: Comment accéder à chaque répertoire et exécuter une commande? à SO


1
-exec {} +est spécifié par POSIX, -exec sh -c 'owd=$PWD; for arg; do cd -- "$arg" && pwd -P; cd -- "$owd"; done' _ {} +est une autre option légale et appelle moins de shells que -exec sh -c '...' {} \;.
— Charles Duffy

13

Doublures pratiques

for D in *; do echo "$D"; done
for D in *; do find "$D" -type d; done ### Option A

find * -type d ### Option B

L'option A est correcte pour les dossiers avec des espaces entre les deux. En outre, généralement plus rapide car il n'imprime pas chaque mot d'un nom de dossier en tant qu'entité distincte.

# Option A
$ time for D in ./big_dir/*; do find "$D" -type d > /dev/null; done
real    0m0.327s
user    0m0.084s
sys     0m0.236s

# Option B
$ time for D in `find ./big_dir/* -type d`; do echo "$D" > /dev/null; done
real    0m0.787s
user    0m0.484s
sys     0m0.308s

10

find . -type d -print0 | xargs -0 -n 1 my_command


puis-je introduire la valeur de retour de cette découverte dans une boucle for? Cela fait partie d'un script plus large ...
— mikewilliamson

@Mike, peu probable. $? vous obtiendra probablement le statut de la commande find ou xargs, plutôt que my_command.
— Paul Tomblin

7

Cela créera un sous-shell (ce qui signifie que les valeurs des variables seront perdues à la fin de la whileboucle):

find . -type d | while read -r dir
do
    something
done

Cela ne va pas:

while read -r dir
do
    something
done < <(find . -type d)

L'un ou l'autre fonctionnera s'il y a des espaces dans les noms de répertoire.


Pour une gestion encore meilleure des noms de fichiers étranges (y compris les noms qui se terminent par des espaces et / ou incluent des sauts de ligne), utilisez find ... -print0etwhile IFS="" read -r -d $'\000' dir
— Gordon Davisson

@GordonDavisson, ... en effet, je dirais même que -d ''c'est moins trompeur sur la syntaxe et les capacités de bash, car cela -d $'\000'implique (faussement) qui $'\000'est en quelque sorte différent de ''- en effet, on pourrait facilement (et encore, faussement) déduire de il que bash prend en charge les chaînes de style Pascal (longueur spécifiée, capable de contenir des littéraux NUL) plutôt que les chaînes C (délimitées NUL, incapable de contenir des NUL).
— Charles Duffy

5

Tu pourrais essayer:

#!/bin/bash
### $1 == the first args to this script
### usage: script.sh /path/to/dir/

for f in `find . -maxdepth 1 -mindepth 1 -type d`; do
  cd "$f"
  <your job here>
done

ou similaire...

Explication:

find . -maxdepth 1 -mindepth 1 -type d: Trouver uniquement les répertoires avec une profondeur récursive maximale de 1 (uniquement les sous-répertoires de 1 $) et une profondeur minimale de 1 (exclut le dossier actuel .)


C'est bogué - essayez avec un nom de répertoire avec des espaces. Voir BashPitfalls # 1 et DontReadLinesWithFor .
— Charles Duffy

Le nom du répertoire avec des espaces est placé entre guillemets et fonctionne donc et OP n'essaie pas de lire les lignes du fichier.
— Henry Dobson

cela fonctionne dans le cd "$f". Cela ne fonctionne pas lorsque la sortie de findest divisée en chaînes, vous aurez donc les pièces séparées du nom en tant que valeurs distinctes dans $f, ce qui améliore la façon dont vous faites ou ne citez pas $fl'expansion de moot.
— Charles Duffy

Je ne dis pas qu'ils ont essayé de lire les lignes d'un fichier. findLa sortie de est orientée ligne (un nom sur une ligne, en théorie - mais voir ci-dessous) avec l' -printaction par défaut .
— Charles Duffy

La sortie orientée ligne, à partir de, find -printn'est pas un moyen sûr de passer des noms de fichiers arbitraires, car on peut exécuter quelque chose mkdir -p $'foo\n/etc/passwd\nbar'et obtenir un répertoire qui a /etc/passwdune ligne distincte dans son nom. Gérer les noms des fichiers /uploadou des /tmprépertoires sans précaution est un excellent moyen d'obtenir des attaques par élévation de privilèges.
— Charles Duffy

4

la réponse acceptée se brisera sur les espaces blancs si les noms de répertoire les ont, et la syntaxe préférée est $()pour bash / ksh. Utilisez l' GNU find -exec option avec +;par exemple

find .... -exec mycommand +; #this is same as passing to xargs

ou utilisez une boucle while

find .... |  while read -r D
do
  ...
done 

quel paramètre contiendra le nom du répertoire? par exemple, chmod + x $ DIR_NAME (oui, je sais qu'il existe une option chmod pour les répertoires uniquement)
— Mike Graf

Il y a une différence subtile entre find -execet passant à xargs: findignorera la valeur de sortie de la commande en cours d'exécution, tandis xargsqu'échouera sur une sortie différente de zéro. Les deux peuvent être corrects, selon vos besoins.
— Jason Wodicka

find ... -print0 | while IFS= read -r dest plus sûr - prend en charge les noms qui commencent ou se terminent par des espaces, et les noms qui contiennent des littéraux de nouvelle ligne.
— Charles Duffy
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.