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?

mikewilliamson
la source
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
Mike Clark
la source
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
Kanaka
la source
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
  • ...
d0x
la source
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

Kenorb
la source
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
Sriram Murali
la source
10

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

Paul Tomblin
la source
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.

En pause jusqu'à nouvel ordre.
la source
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 .)

Henry Dobson
la source
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 
ghostdog74
la source
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