Jak przejść do każdego katalogu i wykonać polecenie?

143

Jak napisać skrypt bash, który przechodzi przez każdego katalogu wewnątrz parent_directory i wykonuje się polecenia w każdym katalogu .

Struktura katalogów jest następująca:

katalog_nadrzędny (nazwa może być dowolna - nie podąża za wzorcem)

  • 001 (nazwy katalogów są zgodne z tym wzorcem)
    • 0001.txt (nazwy plików są zgodne z tym wzorcem)
    • 0002.txt
    • 0003.txt
  • 002
    • 0001.txt
    • 0002.txt
    • 0003.txt
    • 0004.txt
  • 003
    • 0001.txt

liczba katalogów jest nieznana.

Van de Graff
źródło

Odpowiedzi:

108

Możesz wykonać następujące czynności, gdy Twój bieżący katalog to parent_directory:

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

(I )tworzyć podpowłoce, więc katalog bieżący nie jest zmieniany w głównym skrypcie.

Mark Longair
źródło
5
Nie ma to znaczenia, ponieważ symbol wieloznaczny nie pasuje do niczego innego niż liczby, ale w ogólnym przypadku w zasadzie zawsze chcesz umieścić nazwę katalogu w podwójnych cudzysłowach wewnątrz pętli. cd "$d"byłoby lepsze, ponieważ przenosi się do sytuacji, w których symbol wieloznaczny pasuje do plików, których nazwy zawierają białe spacje i / lub metaznaki powłoki.
tripleee
1
(Wszystkie odpowiedzi tutaj wydają się mieć tę samą wadę, ale ma to największe znaczenie w przypadku odpowiedzi z największą liczbą głosów).
tripleee
1
Ponadto zdecydowanej większości poleceń tak naprawdę nie obchodzi, w którym katalogu je wykonujesz. for f in foo bar; do cat "$f"/*.txt; done >outputjest funkcjonalnie równoważny z cat foo/*.txt bar/*.txt >output. Jednak lsjest jedno polecenie, które daje nieco inny wynik w zależności od tego, jak przekazujesz mu argumenty; podobnie, a greplub, wcktóry wypisuje względną nazwę pliku, będzie inny, jeśli uruchomisz go z podkatalogu (ale często chcesz uniknąć wchodzenia do podkatalogu właśnie z tego powodu).
tripleee
158

Ta odpowiedź wysłana przez Todda pomogła mi.

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

W \( ! -name . \) unika wykonywania polecenia w bieżącym katalogu.

Christian Vielma
źródło
4
A jeśli chcesz uruchomić polecenie tylko w określonych folderach:find FOLDER* -maxdepth 0 -type d \( ! -name . \) -exec bash -c "cd '{}' && pwd" \;
Martin K
3
Musiałem to zrobić, -exec bash -c 'cd "$0" && pwd' {} \;ponieważ niektóre katalogi zawierały pojedyncze cudzysłowy, a niektóre podwójne cudzysłowy.
Charlie Gorichanaz
12
Możesz również pominąć bieżący katalog, dodając-mindepth 1
mdziob
Jak zmienić to na funkcję akceptującą polecenie, takie jak "git remote get-url origin` zamiast pwdbezpośrednio?
roachsinai
disd(){find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c 'cd "{}" && "$@"' \;}nie działa.
roachsinai
48

Jeśli używasz GNU find, możesz wypróbować -execdirparametr, np:

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

lub (zgodnie z komentarzem @gniourf_gniourf ):

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

Uwaga: można użyć ${0#./}zamiast $0mocowania ./z przodu.

lub bardziej praktyczny przykład:

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

Jeśli chcesz dołączyć bieżący katalog, jest to jeszcze prostsze, używając -exec:

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

lub używając xargs:

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

Lub podobny przykład sugerowany przez @gniourf_gniourf :

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

Powyższe przykłady obsługują katalogi ze spacjami w nazwie.


Lub przypisując do tablicy bash:

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

Zmień .na określoną nazwę folderu. Jeśli nie potrzebujesz uruchamiać rekurencyjnie, możesz użyć: dirs=(*)zamiast. Powyższy przykład nie obsługuje katalogów ze spacjami w nazwie.

Tak więc, jak zasugerował @gniourf_gniourf , jedyny właściwy sposób umieszczenia wyniku find w tablicy bez użycia jawnej pętli będzie dostępny w Bash 4.4 z:

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

Lub nie jest to zalecany sposób (który obejmuje analizęls ):

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

Powyższy przykład zignorowałby bieżący katalog (zgodnie z żądaniem OP), ale zepsuje nazwy ze spacjami.

Zobacz też:

kenorb
źródło
1
Jedyny poprawny sposób umieszczenia wyniku findw tablicy bez użycia jawnej pętli będzie dostępny w Bash 4.4 z mapfile -t -d '' dirs < <(find . -type d -print0). Do tego czasu jedyną opcją jest użycie pętli (lub odroczenie operacji find).
gniourf_gniourf
1
Tak naprawdę dirstablica nie jest potrzebna ; mógłbyś pętla na find„wyjście S (bezpiecznie) tak: find . -type d -print0 | while IFS= read -r -d '' file; do ...; done.
gniourf_gniourf
@gniourf_gniourf Jestem wizualny i zawsze staram się znaleźć / uczynić rzeczy, które wyglądają na proste. Np. Mam ten skrypt i dzięki zastosowaniu tablicy bash jest dla mnie łatwe i czytelne (poprzez rozdzielenie logiki na mniejsze części, zamiast używania potoków). Wprowadzenie dodatkowych rur, IFS, read, daje wrażenie dodatkowej złożoności. Muszę się nad tym zastanowić, może jest na to prostszy sposób.
kenorb
Jeśli przez łatwiejszy masz na myśli zepsuty, to tak, są łatwiejsze sposoby. A teraz nie martw się, wszystko to IFSi read(jak mówisz) stają się drugą naturą, gdy się do nich przyzwyczaisz… są częścią kanonicznych i idiomatycznych sposobów pisania scenariuszy.
gniourf_gniourf
Nawiasem mówiąc, twoje pierwsze polecenie find . -type d -execdir echo $(pwd)/{} ';'nie robi tego, co chcesz ( $(pwd)jest rozwijane, zanim findzostanie nawet wykonane)…
gniourf_gniourf
29

Możesz to osiągnąć przez orurowanie, a następnie za pomocą xargs. Chwyt polega na tym, że musisz użyć -Iflagi, która zamieni podciąg w twoim poleceniu bash na podciąg przekazany przez każdy z xargs.

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

Możesz zamienić na pwddowolne polecenie, które chcesz wykonać w każdym katalogu.

Piyush Singh
źródło
2
Nie wiem, dlaczego nie zostało to przyjęte, ale najprostszy skrypt, jaki do tej pory znalazłem. Dzięki
Linvi,
20

Jeśli znany jest folder najwyższego poziomu, możesz po prostu napisać coś takiego:

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

Na $ (GRAJ JAK TYLKO CHCESZ); możesz umieścić tyle kodu, ile chcesz.

Zauważ, że nie "cd" w żadnym katalogu.

Twoje zdrowie,

gforcada
źródło
3
Jest tutaj kilka przypadków „bezużytecznego wykorzystania ls - możesz po prostu to zrobić for dir in $YOUR_TOP_LEVEL_FOLDER/*.
Mark Longair,
1
No cóż, może jest to ogólnie bezużyteczne, ale pozwala na filtrowanie bezpośrednio w ls (czyli wszystkie katalogi zakończone .tmp). Dlatego użyłem ls $dirwyrażenia
gforcada
13
for dir in PARENT/*
do
  test -d "$dir" || continue
  # Do something with $dir...
done
Ideliczne
źródło
I to jest bardzo łatwe do zrozumienia!
Rbjz,
6

Podczas gdy jeden liner jest dobry do szybkiego i brudnego użycia, wolę poniżej bardziej rozwlekłą wersję do pisania skryptów. To jest szablon, którego używam, który zajmuje się wieloma skrajnymi przypadkami i umożliwia pisanie bardziej złożonego kodu do wykonania w folderze. Możesz napisać swój kod basha w funkcji dir_command. Poniżej dir_coomand implementuje tagowanie każdego repozytorium w git jako przykład. Reszta skryptu wywołuje dir_command dla każdego folderu w katalogu. Dołączono również przykład iteracji po tylko podanym zestawie folderów.

#!/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
źródło
5

Nie rozumiem, o co chodzi z formatowaniem pliku, ponieważ chcesz tylko iterować po folderach ... Szukasz czegoś takiego?

cd parent
find . -type d | while read d; do
   ls $d/
done
Aif
źródło
4

możesz użyć

find .

aby przeszukać wszystkie pliki / katalogi w bieżącym katalogu cyklicznie

Następnie możesz potokować wyjście polecenie xargs w ten sposób

find . | xargs 'command here'
Dan Bizdadea
źródło
3
Nie o to pytano.
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
źródło
Będziesz musiał ponownie zmienić kopię zapasową katalogu lub zrobić to cdw podpowłoce.
Mark Longair,
Głos przeciw: edycja przegrała, cdwięc ten kod nie robi nic pożytecznego.
tripleee