Come andare in ciascuna directory ed eseguire un comando?


143

Come faccio a scrivere uno script bash che passa attraverso ogni directory all'interno di una directory_procedura ed esegue un comando in ciascuna directory .

La struttura delle directory è la seguente:

parent_directory (il nome potrebbe essere qualsiasi cosa - non segue uno schema)

  • 001 (i nomi delle directory seguono questo modello)
    • 0001.txt (i nomi dei file seguono questo modello)
    • 0002.txt
    • 0003.txt
  • 002
    • 0001.txt
    • 0002.txt
    • 0003.txt
    • 0004.txt
  • 003
    • 0001.txt

il numero di directory è sconosciuto.

Risposte:


108

È possibile effettuare le seguenti operazioni, quando la directory corrente è parent_directory:

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

L' (e )creare una subshell, quindi la directory corrente non viene modificata nello script principale.


5
Non importa qui perché il carattere jolly non corrisponde a nient'altro che ai numeri, ma nel caso generale, in pratica, si desidera sempre mettere il nome della directory tra virgolette doppie all'interno del ciclo. cd "$d"sarebbe meglio in quanto trasferisce a situazioni in cui il carattere jolly corrisponde a file i cui nomi contengono spazi bianchi e / o metacaratteri di shell.
Tripleee,

1
(Tutte le risposte qui sembrano avere lo stesso difetto, ma conta di più nella risposta più votata.)
Tripleee

1
Inoltre, la stragrande maggioranza dei comandi in realtà non importa in quale directory li esegui. for f in foo bar; do cat "$f"/*.txt; done >outputè funzionalmente equivalente a cat foo/*.txt bar/*.txt >output. Tuttavia, lsè un comando che produce un output leggermente diverso a seconda di come gli argomenti vengono passati; allo stesso modo, un grepo wcche genera un nome di file relativo sarà diverso se lo si esegue da una sottodirectory (ma spesso, si desidera evitare di entrare in una sottodirectory proprio per quel motivo).
Tripleee,

158

Questa risposta inviata da Todd mi ha aiutato.

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

I \( ! -name . \) evita di eseguire il comando nella directory corrente.


4
E se vuoi solo eseguire il comando in determinate cartelle:find FOLDER* -maxdepth 0 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd" \;
Martin K

3
Ho dovuto farlo -exec bash -c 'cd "$0" && pwd' {} \;poiché alcune directory contenevano virgolette singole e alcune virgolette doppie.
Charlie Gorichanaz,

12
Puoi anche omettere la directory corrente aggiungendo-mindepth 1
mdziob il

Come cambiarlo in una funzione accettare un comando come "git remote get-url origin` invece che pwddirettamente?
roachsinai

disd(){find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c 'cd "{}" && "$@"' \;}non funziona.
Roachsinai,

48

Se stai usando GNU find, puoi provare il -execdirparametro, ad esempio:

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

oppure (come da commento su @gniourf_gniourf ):

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

Nota: è possibile utilizzare ${0#./}invece di $0risolvere ./nella parte anteriore.

o esempio più pratico:

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

Se vuoi includere la directory corrente, è ancora più semplice usando -exec:

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

o usando xargs:

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

O esempio simile suggerito da @gniourf_gniourf :

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

Gli esempi sopra supportano le directory con spazi nel loro nome.


O assegnando in array bash:

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

Passare .al nome della cartella specifica. Se non è necessario eseguire ricorsivamente, è possibile utilizzare: dirs=(*)invece. L'esempio sopra non supporta le directory con spazi nel nome.

Quindi, come suggerito da @gniourf_gniourf , l'unico modo corretto per mettere l'output di find in un array senza usare un ciclo esplicito sarà disponibile in Bash 4.4 con:

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

O non un modo raccomandato (che prevede l' analisi dils ):

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

L'esempio sopra ignorerebbe la directory corrente (come richiesto dall'OP), ma si spezzerebbe sui nomi con gli spazi.

Guarda anche:


1
L'unico modo corretto per mettere l'output di findin un array senza usare un ciclo esplicito sarà disponibile in Bash 4.4 con mapfile -t -d '' dirs < <(find . -type d -print0). Fino ad allora, l'unica opzione è utilizzare un loop (o rinviare l'operazione a find).
gniourf_gniourf

1
In effetti, non è davvero necessario l' dirsarray; si potrebbe loop su finduscita 's (in modo sicuro) in questo modo: find . -type d -print0 | while IFS= read -r -d '' file; do ...; done.
gniourf_gniourf

@gniourf_gniourf Sono visivo e cerco sempre di trovare / rendere le cose che sembrano semplici. Ad esempio ho questo script e usando bash array è facile e leggibile per me (separando la logica in pezzi più piccoli, invece di usare pipe). L'introduzione di tubi aggiuntivi, IFS, read, dà l'impressione di complessità. Ci dovrò pensare, forse c'è un modo più semplice per farlo.
Kenorb,

Se per facile intendi rotto allora sì, ci sono modi più facili di fare. Ora non ti preoccupare, tutti questi IFSe read(come dici tu) diventano una seconda natura quando ti abitui a loro ... fanno parte dei modi canonici e idiomatici di scrivere sceneggiature.
gniourf_gniourf

A proposito, il tuo primo comando find . -type d -execdir echo $(pwd)/{} ';'non fa quello che vuoi (l' $(pwd)espansione prima findancora viene eseguita) ...
gniourf_gniourf

29

È possibile ottenere questo tramite tubazioni e quindi utilizzando xargs. Il trucco è che è necessario utilizzare la -Ibandiera che sostituirà la sottostringa nel comando bash con la sottostringa passata da ciascuno dei xargs.

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

È possibile che si desideri sostituire pwdcon qualsiasi comando si desideri eseguire in ciascuna directory.


2
Non so perché questo non sia stato votato, ma lo script più semplice che ho trovato finora. Grazie
Linvi,

20

Se la cartella di livello superiore è nota, puoi semplicemente scrivere qualcosa del genere:

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

$ (GIOCA QUANTO VUOI); puoi mettere tutto il codice che vuoi.

Nota che non ho "cd" su nessuna directory.

Saluti,


3
Ci sono un paio di esempi di "Uso inutile di ls" qui - invece potresti semplicemente fare for dir in $YOUR_TOP_LEVEL_FOLDER/*.
Mark Longair,

1
Bene, forse è inutile in senso generale, ma permette di fare il filtro direttamente in ls (cioè tutte le directory sono finite con .tmp). Ecco perché ho usato l' ls $direspressione
gforcada,

13
for dir in PARENT/*
do
  test -d "$dir" || continue
  # Do something with $dir...
done

E questo è molto facile da capire!
Rbjz,

6

Mentre una linea è buona per un utilizzo rapido e sporco, preferisco di seguito una versione più dettagliata per scrivere script. Questo è il modello che uso che si occupa di molti casi limite e consente di scrivere codice più complesso da eseguire su una cartella. Puoi scrivere il tuo codice bash nella funzione dir_command. Di seguito, dir_coomand implementa la codifica di ciascun repository in git come esempio. Il resto dello script chiama dir_command per ogni cartella nella directory. È incluso anche l'esempio di iterazione attraverso solo un determinato set di cartelle.

#!/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"

5

Non capisco il punto con la formattazione del file, poiché vuoi solo scorrere le cartelle ... Stai cercando qualcosa del genere?

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

4

Puoi usare

find .

per ricorrere a tutti i file / directory nella directory corrente

Quindi puoi inviare l'output al comando xargs in questo modo

find . | xargs 'command here'

3
Non è quello che è stato chiesto.
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

Dovresti cambiare di nuovo il backup della directory, oppure eseguire la cdsottostruttura.
Mark Longair,

Downvote: la modifica ha perso il cdquindi questo codice in realtà non fa nulla di utile.
Tripleee,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.