Comment accéder à chaque répertoire et exécuter une commande?

143

Comment écrire un script bash qui parcourt chaque répertoire dans un parent_directory et exécute une commande dans chaque répertoire .

La structure des répertoires est la suivante:

parent_directory (le nom peut être n'importe quoi - ne suit pas un modèle)

  • 001 (les noms de répertoire suivent ce modèle)
    • 0001.txt (les noms de fichiers suivent ce modèle)
    • 0002.txt
    • 0003.txt
  • 002
    • 0001.txt
    • 0002.txt
    • 0003.txt
    • 0004.txt
  • 003
    • 0001.txt

le nombre de répertoires est inconnu.

Van de Graff
la source

Réponses:

108

Vous pouvez effectuer les opérations suivantes lorsque votre répertoire actuel est parent_directory:

for d in [0-9][0-9][0-9]
do
    ( cd "$d" && your-command-here )
done

Le (et )créer un sous - shell, de sorte que le répertoire courant ne change pas dans le script principal.

Mark Longair
la source
5
Cela n'a pas d'importance ici car le caractère générique ne correspond à rien d'autre que des nombres, mais dans le cas général, vous voulez toujours mettre le nom du répertoire entre guillemets doubles dans la boucle. cd "$d"serait mieux en ce sens qu'il transfère vers des situations où le caractère générique correspond aux fichiers dont les noms contiennent des espaces et / ou des métacaractères shell.
tripleee
1
(Toutes les réponses ici semblent avoir le même défaut, mais c'est ce qui compte le plus dans la réponse la plus votée.)
tripleee
1
De plus, la grande majorité des commandes ne se soucient pas vraiment du répertoire dans lequel vous les exécutez. for f in foo bar; do cat "$f"/*.txt; done >outputest fonctionnellement équivalent à cat foo/*.txt bar/*.txt >output. Cependant, lsest une commande qui produit une sortie légèrement différente selon la façon dont vous lui passez les arguments; de même, un grepou wcqui génère un nom de fichier relatif sera différent si vous l'exécutez à partir d'un sous-répertoire (mais souvent, vous voulez éviter d'entrer dans un sous-répertoire précisément pour cette raison).
tripleee
158

Cette réponse publiée par Todd m'a aidé.

find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd" \;

Le \( ! -name . \) évite d'exécuter la commande dans le répertoire courant.

Christian Vielma
la source
4
Et si vous ne souhaitez exécuter la commande que dans certains dossiers:find FOLDER* -maxdepth 0 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd" \;
Martin K
3
J'ai dû le faire -exec bash -c 'cd "$0" && pwd' {} \;car certains répertoires contenaient des guillemets simples et des guillemets doubles.
Charlie Gorichanaz
12
Vous pouvez également omettre le répertoire actuel en ajoutant-mindepth 1
mdziob
Comment changer cela en une fonction accepter une commande comme "git remote get-url origin` au lieu de pwddirectement?
roachsinai
disd(){find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c 'cd "{}" && "$@"' \;}ne fonctionne pas.
roachsinai
48

Si vous utilisez GNU find, vous pouvez essayer des -execdirparamètres, par exemple:

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

ou (selon le commentaire @gniourf_gniourf ):

find . -type d -execdir sh -c 'printf "%s/%s\n" "$PWD" "$0"' {} \;

Remarque: vous pouvez utiliser ${0#./}au lieu de $0pour fixer ./à l'avant.

ou un exemple plus pratique:

find . -name .git -type d -execdir git pull -v ';'

Si vous souhaitez inclure le répertoire courant, c'est encore plus simple en utilisant -exec:

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

ou en utilisant xargs:

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

Ou un exemple similaire suggéré par @gniourf_gniourf :

find . -type d -print0 | while IFS= read -r -d '' file; do
# ...
done

Les exemples ci-dessus prennent en charge les répertoires avec des espaces dans leur nom.


Ou en affectant dans un tableau bash:

dirs=($(find . -type d))
for dir in "${dirs[@]}"; do
  cd "$dir"
  echo $PWD
done

Modifiez le nom .de votre dossier spécifique. Si vous n'avez pas besoin d'exécuter récursivement, vous pouvez utiliser: à la dirs=(*)place. L'exemple ci-dessus ne prend pas en charge les répertoires avec des espaces dans le nom.

Ainsi, comme @gniourf_gniourf l'a suggéré, la seule façon correcte de mettre la sortie de find dans un tableau sans utiliser de boucle explicite sera disponible dans Bash 4.4 avec:

mapfile -t -d '' dirs < <(find . -type d -print0)

Ou pas une méthode recommandée (qui implique l' analyse dels ):

ls -d */ | awk '{print $NF}' | xargs -n1 sh -c 'cd $0 && pwd && echo Do stuff'

L'exemple ci-dessus ignorerait le répertoire actuel (comme demandé par OP), mais il cassera sur les noms avec des espaces.

Voir également:

Kenorb
la source
1
La seule manière correcte de mettre la sortie de finddans un tableau sans utiliser de boucle explicite sera disponible dans Bash 4.4 avec mapfile -t -d '' dirs < <(find . -type d -print0). Jusque-là, votre seule option est d'utiliser une boucle (ou de reporter l'opération à find).
gniourf_gniourf
1
En fait, vous n'avez pas vraiment besoin du dirstableau; vous en boucle pourrait sur findla sortie « ( en toute sécurité) comme ceci: find . -type d -print0 | while IFS= read -r -d '' file; do ...; done.
gniourf_gniourf
@gniourf_gniourf Je suis visuel et j'essaie toujours de trouver / rendre les choses simples. Par exemple, j'ai ce script et en utilisant le tableau bash, c'est facile et lisible pour moi (en séparant la logique en plus petits morceaux, au lieu d'utiliser des tuyaux). Présentation de tuyaux supplémentaires, IFS, read, il donne l'impression de complexité supplémentaire. Je vais devoir y réfléchir, il y a peut-être un moyen plus simple de le faire.
kenorb
Si par plus facile vous entendez brisé, alors oui, il existe des moyens plus faciles de faire. Maintenant ne vous inquiétez pas, tout cela IFSet read(comme vous le dites) devient une seconde nature au fur et à mesure que vous vous y habituez ... ils font partie des manières canoniques et idiomatiques d'écrire des scripts.
gniourf_gniourf
Au fait, votre première commande find . -type d -execdir echo $(pwd)/{} ';'ne fait pas ce que vous voulez (elle $(pwd)est développée avant findmême d'être exécutée)…
gniourf_gniourf
29

Vous pouvez y parvenir en raccordant puis en utilisant xargs. Le hic, c'est que vous devez utiliser l' -Iindicateur qui remplacera la sous-chaîne de votre commande bash par la sous-chaîne passée par chacun des xargs.

ls -d */ | xargs -I {} bash -c "cd '{}' && pwd"

Vous voudrez peut-être remplacer pwdpar la commande que vous souhaitez exécuter dans chaque répertoire.

Piyush Singh
la source
2
Je ne sais pas pourquoi cela n'a pas été voté, mais c'est le script le plus simple que j'ai trouvé jusqu'à présent. Merci
Linvi
20

Si le dossier de niveau supérieur est connu, vous pouvez simplement écrire quelque chose comme ceci:

for dir in `ls $YOUR_TOP_LEVEL_FOLDER`;
do
    for subdir in `ls $YOUR_TOP_LEVEL_FOLDER/$dir`;
    do
      $(PLAY AS MUCH AS YOU WANT);
    done
done

Sur le $ (JOUEZ AUSSI QUE VOUS VOULEZ); vous pouvez mettre autant de code que vous le souhaitez.

Notez que je n'ai pas "cd" sur aucun répertoire.

À votre santé,

gforcada
la source
3
Il y a quelques exemples de "Utilisation inutile de ls" ici - vous pouvez simplement le faire à la for dir in $YOUR_TOP_LEVEL_FOLDER/*place.
Mark Longair
1
Bon, c'est peut-être inutile dans un sens général mais cela permet de faire du filtrage directement dans les ls (c'est-à-dire tous les répertoires se terminant par .tmp). C'est pourquoi j'ai utilisé l' ls $direxpression
gforcada
13
for dir in PARENT/*
do
  test -d "$dir" || continue
  # Do something with $dir...
done
Idélique
la source
Et c'est très facile à comprendre!
Rbjz
6

Alors que les doublures sont bonnes pour une utilisation rapide et sale, je préfère une version plus détaillée pour l'écriture de scripts. C'est le modèle que j'utilise qui prend en charge de nombreux cas extrêmes et vous permet d'écrire du code plus complexe à exécuter sur un dossier. Vous pouvez écrire votre code bash dans la fonction dir_command. Ci-dessous, dir_coomand implémente le balisage de chaque référentiel dans git à titre d'exemple. Le reste du script appelle dir_command pour chaque dossier du répertoire. L'exemple d'itération à travers un ensemble de dossiers donné est également inclus.

#!/bin/bash

#Use set -x if you want to echo each command while getting executed
#set -x

#Save current directory so we can restore it later
cur=$PWD
#Save command line arguments so functions can access it
args=("$@")

#Put your code in this function
#To access command line arguments use syntax ${args[1]} etc
function dir_command {
    #This example command implements doing git status for folder
    cd $1
    echo "$(tput setaf 2)$1$(tput sgr 0)"
    git tag -a ${args[0]} -m "${args[1]}"
    git push --tags
    cd ..
}

#This loop will go to each immediate child and execute dir_command
find . -maxdepth 1 -type d \( ! -name . \) | while read dir; do
   dir_command "$dir/"
done

#This example loop only loops through give set of folders    
declare -a dirs=("dir1" "dir2" "dir3")
for dir in "${dirs[@]}"; do
    dir_command "$dir/"
done

#Restore the folder
cd "$cur"
Shital Shah
la source
5

Je ne comprends pas le formatage du fichier, car vous ne voulez parcourir que des dossiers ... Cherchez-vous quelque chose comme ça?

cd parent
find . -type d | while read d; do
   ls $d/
done
Aif
la source
4

vous pouvez utiliser

find .

pour rechercher tous les fichiers / répertoires dans le répertoire courant de manière récurrente

Ensuite, vous pouvez diriger la sortie de la commande xargs comme ceci

find . | xargs 'command here'
Dan Bizdadea
la source
3
Ce n'est pas ce qui a été demandé.
kenorb
0
for p in [0-9][0-9][0-9];do
    (
        cd $p
        for f in [0-9][0-9][0-9][0-9]*.txt;do
            ls $f; # Your operands
        done
    )
done
Fedir RYKHTIK
la source
Vous auriez besoin de modifier à nouveau le répertoire ou de le faire cddans un sous-shell.
Mark Longair
Downvote: la modification a perdu le cddonc ce code ne fait rien d'utile.
tripleee