Wykonaj polecenie na wszystkich plikach w katalogu

290

Czy ktoś mógłby podać kod do wykonania następujących czynności: Załóżmy, że istnieje katalog plików, z których wszystkie muszą być uruchomione przez program. Program wypisuje wyniki na standardowe wyjście. Potrzebuję skryptu, który przejdzie do katalogu, wykona polecenie dla każdego pliku i połączy dane wyjściowe w jeden duży plik wyjściowy.

Na przykład, aby uruchomić polecenie dla 1 pliku:

$ cmd [option] [filename] > results.out
mistrz
źródło
3
Chciałbym dodać do pytania. Czy można to zrobić za pomocą xargs? np. ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
Ozair Kafray
2
Może, ale prawdopodobnie nie chcesz używaćls do prowadzenia pojazdu xargs. Jeśli cmdw ogóle jest napisany kompetentnie, być może możesz po prostu zrobić cmd <wildcard>.
tripleee

Odpowiedzi:

425

Poniższy kod bash przekaże $ file do polecenia, gdzie $ file będzie reprezentować każdy plik w / dir

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

Przykład

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt
Andrew Logvinov
źródło
23
Jeśli nie ma żadnych plików /dir/, wówczas pętla nadal działa raz z wartością „*” dla $file, co może być niepożądane. Aby tego uniknąć, włącz nullglob na czas trwania pętli. Dodaj tę linię przed pętlą shopt -s nullglobi tę linię po pętli shopt -u nullglob #revert nullglob back to it's normal default state.
Gulasz-au
43
+1, I to tylko kosztowało mnie całą kolekcję tapet. wszyscy za mną używają podwójnych cudzysłowów. „$ file”
Behrooz
Jeśli plik wyjściowy jest taki sam w pętli, przekierowanie poza pętlę jest o wiele bardziej wydajne done >results.out(i prawdopodobnie wtedy możesz zastąpić zamiast dołączać, tak jak tutaj założyłem).
tripleee
Jak uzyskać poszczególne pliki wyników, które są niestandardowo nazwane w swoich plikach wejściowych?
Timothy Swan
1
bądź ostrożny, używając tego polecenia do ogromnej ilości plików w reż. Zamiast tego użyj find -exec.
kolisko
181

Co powiesz na to:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1argument zapobiega rekurencyjnemu zejściu find do jakichkolwiek podkatalogów. (Jeśli chcesz przetworzyć takie zagnieżdżone katalogi, możesz to pominąć).
  • -type -f określa, że ​​będą przetwarzane tylko zwykłe pliki.
  • -exec cmd option {} każe mu działać cmd z podanym optiondla każdego znalezionego pliku, z zastąpioną nazwą pliku{}
  • \; oznacza koniec polecenia.
  • Wreszcie, wynik wszystkich osób cmd wykonań są przekierowywane do results.out

Jeśli jednak zależy Ci na kolejności przetwarzania plików, lepiej zapisz pętlę. Myślę, że findprzetwarza pliki w kolejności i-węzłów (chociaż mogę się mylić), co może nie być tym, czego chcesz.

Jim Lewis
źródło
1
To jest właściwy sposób przetwarzania plików. Korzystanie z pętli for jest podatne na błędy z wielu powodów. Również sortowania można dokonać za pomocą innych poleceń, takich jak stati sort, które oczywiście zależą od kryteriów sortowania.
tuxdna,
1
gdybym chciał uruchomić dwa polecenia, jak bym je połączyć po -execopcji? Czy muszę zawijać je w pojedyncze cudzysłowy czy coś?
frei
findjest zawsze najlepszą opcją, ponieważ możesz filtrować według wzorca nazwy pliku z opcją -namei możesz to zrobić za pomocą jednego polecenia.
João Pimentel Ferreira,
3
@frei odpowiedź na twoje pytanie jest tutaj: stackoverflow.com/a/6043896/1243247, ale w zasadzie po prostu dodaj -execopcje:find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira
2
jak możesz odwoływać się do nazwy pliku jako opcji?
Toskan,
54

Robię to na moim malinowym pi z wiersza poleceń, uruchamiając:

for i in *;do omxplayer "$i";done
robgraves
źródło
7

Przyjęte / wysoko głosowane odpowiedzi są świetne, ale brakuje im kilku drobiazgowych szczegółów. Ten post omawia przypadki, w których lepiej radzić sobie, gdy rozszerzenie nazwy ścieżki powłoki (glob) kończy się niepowodzeniem, gdy nazwy plików zawierają osadzone symbole nowego wiersza / myślnika i przeniesienie wyjścia polecenia z pętli for podczas zapisywania wyników do plik.

Podczas uruchamiania rozszerzenia globu powłoki za pomocą *istnieje możliwość niepowodzenia rozszerzenia, jeśli w katalogu nie ma żadnych plików, a nierozwinięty ciąg globu zostanie przekazany do polecenia do uruchomienia, co może mieć niepożądane skutki. bashPowłoka zapewnia rozszerzoną opcję powłoki dla tego użyciem nullglob. Pętla zasadniczo wygląda następująco w katalogu zawierającym pliki

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

Pozwala to bezpiecznie wyjść z pętli for, gdy wyrażenie ./*nie zwraca żadnych plików (jeśli katalog jest pusty)

lub w sposób zgodny z POSIX ( nullglobjest bashspecyficzny)

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

Pozwala to wejść do pętli, gdy wyrażenie nie powiedzie się raz i warunek [ -f "$file" ]sprawdzi, czy nierozwinięty ciąg ./*jest prawidłową nazwą pliku w tym katalogu, co nie byłoby. Tak więc w tym przypadku błąd, przy użyciu continuewznawiamy z powrotem do forpętli, która nie będzie działać później.

Zwróć także uwagę na użycie --tuż przed przekazaniem argumentu nazwy pliku. Jest to konieczne, ponieważ, jak wspomniano wcześniej, nazwy plików powłoki mogą zawierać myślniki w dowolnym miejscu w nazwie pliku. Niektóre polecenia powłoki interpretują to i traktują je jako opcję polecenia, gdy nazwa nie jest poprawnie cytowana, i wykonują polecenie, zastanawiając się, czy flaga jest podana.

W takim przypadku --sygnalizuje koniec opcji wiersza poleceń, co oznacza, że ​​polecenie nie powinno analizować żadnych ciągów poza tym punktem jako flag poleceń, a jedynie jako nazwy plików.


Podwójne cytowanie nazw plików prawidłowo rozwiązuje przypadki, gdy nazwy zawierają znaki globalne lub białe znaki. Ale nazwy plików * nix mogą również zawierać w nich znaki nowej linii. Dlatego ograniczamy nazwy plików za pomocą jedynego znaku, który nie może być częścią prawidłowej nazwy pliku - null byte ( \0). Ponieważ bashwewnętrznie używa Cciągów stylów, w których do wskazania końca łańcucha używane są bajty zerowe, jest to odpowiedni kandydat na to.

Tak więc używając printfopcji powłoki do rozdzielenia plików tym bajtem NULL za pomocą -dopcji readpolecenia, możemy to zrobić poniżej

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

nullglobI printfsą owinięte wokół (..)co oznacza, że są w zasadzie prowadzone w sub-shell (powłoka dziecko), ponieważ aby uniknąć nullglobmożliwości zastanowienia się na powłoce macierzystej, raz wyjść sterujących. -d ''Opcja readpolecenia jest nie POSIX zgodne, więc potrzebuje bashskorupę, aby to zrobić. Za pomocą findpolecenia można to zrobić jako

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

W przypadku findimplementacji, które nie obsługują -print0(innych niż implementacje GNU i FreeBSD), można to emulować za pomocąprintf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

Inną ważną poprawką jest przeniesienie zmiany kierunku poza pętlę for, aby zmniejszyć dużą liczbę operacji we / wy pliku. Gdy używana jest w pętli, powłoka musi wykonywać wywołania systemowe dwa razy dla każdej iteracji pętli for, raz dla otwarcia i raz dla zamknięcia deskryptora pliku skojarzonego z plikiem. Stanie się to wąskim gardłem w wydajności podczas wykonywania dużych iteracji. Zalecaną sugestią byłoby przeniesienie go poza pętlę.

Rozszerzając powyższy kod o te poprawki, możesz to zrobić

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

który po prostu umieści zawartość polecenia dla każdej iteracji wejścia pliku na standardowe wyjście, a gdy pętla się zakończy, otwórz plik docelowy jeden raz, aby zapisać zawartość standardowego wejścia i zapisać go. Równoważna findwersja tego samego byłaby

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out
Inian
źródło
1
+1 za sprawdzenie, czy plik istnieje. Jeśli wyszukiwanie w nieistniejącym katalogu, $ plik zawiera ciąg wyrażenia regularnego „/ invald_dir / *”, nie jest prawidłową nazwą pliku.
cdalxndr
3

Jednym szybkim i brudnym sposobem, który czasami wykonuje zadanie, jest:

find directory/ | xargs  Command 

Na przykład, aby znaleźć liczbę wierszy we wszystkich plikach w bieżącym katalogu, możesz:

find . | xargs wc -l
Rahul
źródło
8
@Hubert Dlaczego w nazwach plików masz znaki nowej linii ?!
musicin3d
2
to nie jest pytanie „dlaczego”, to kwestia poprawności - nazwy plików nie muszą zawierać znaków do wydrukowania, nie muszą nawet być prawidłowymi sekwencjami UTF-8. Ponadto to, co jest nową linią, jest bardzo zależne od kodowania, jedno kodowanie ♀ jest nową linią innego. Zobacz stronę kodową 437
Hubert Kario,
2
cmon, naprawdę? to działa 99,9% czasu, a on powiedział „szybki i brudny”
Edoardo
Nie jestem fanem „szybkich i brudnych” (AKA „zepsutych”) skryptów Bash. Wcześniej czy później kończy się na czymś w rodzaju słynnego „Moved ~/.local/share/steam. Ran steam. Skasował wszystko w systemie należącym do użytkownika”. Zgłoszenie błędu.
ograniczenie aktywności
To również nie będzie działać z plikami ze spacjami w nazwie.
Shamas S - Przywróć Monikę
2

Musiałem skopiować wszystkie pliki .md z jednego katalogu do drugiego, więc oto co zrobiłem.

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

Co jest dość trudne do odczytania, więc podzielmy to.

najpierw cd do katalogu z plikami,

for i in **/*.md; dla każdego pliku we wzorze

mkdir -p ../docs/"$i"umieść ten katalog w folderze dokumentów poza folderem zawierającym pliki. Który tworzy dodatkowy folder o takiej samej nazwie jak ten plik.

rm -r ../docs/"$i" usuń dodatkowy folder utworzony w wyniku mkdir -p

cp "$i" "../docs/$i" Skopiuj aktualny plik

echo "$i -> ../docs/$i" Echo tego, co zrobiłeś

; done Żyj długo i szczęśliwie

Eric Wooley
źródło
Uwaga: **aby działać, globstarnależy ustawić opcję powłoki:shopt -s globstar
Hubert Kario
2

Możesz użyć xarg

ls | xargs -L 1 -d '\n' your-desired-command

-L 1 powoduje przejście 1 przedmiotu na raz

-d '\n'make wyjście lsjest podzielone na podstawie nowej linii.

Al Mamun
źródło
1

W oparciu o podejście @Jima Lewisa:

Oto szybkie rozwiązanie wykorzystujące, finda także sortujące pliki według daty modyfikacji:

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

Do sortowania patrz:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time

tuxdna
źródło
to nie zadziała, jeśli pliki mają nowe wiersze w swoich nazwach
Hubert Kario
1
@HubertKario Możesz przeczytać więcej informacji -print0na temat dla findi -0dla xargsktórych należy używać znaku null zamiast jakichkolwiek białych znaków (w tym znaków nowej linii).
tuxdna
tak, używanie -print0jest czymś, co pomaga, ale cały rurociąg musi używać czegoś takiego, a sortnie jest
Hubert Kario
1

myślę, że proste rozwiązanie to:

sh /dir/* > ./result.txt
yovie
źródło
2
Czy dobrze zrozumiałeś pytanie? Spowoduje to jedynie uruchomienie każdego pliku w katalogu przez powłokę - jakby to był skrypt.
rdas 16.04.19
1

Maksymalna głębokość

Przekonałem się, że działa to dobrze z odpowiedzią Jima Lewisa, wystarczy dodać coś takiego:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

Porządek sortowania

Jeśli chcesz wykonać w kolejności sortowania, zmodyfikuj go w następujący sposób:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

Na przykład będzie to wykonywane w następującej kolejności:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

Nieograniczona głębokość

Jeśli chcesz wykonać na nieograniczonej głębokości pod pewnymi warunkami, możesz użyć tego:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

następnie umieść na wierzchu każdego pliku w katalogach potomnych w następujący sposób:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

i gdzieś w treści pliku nadrzędnego:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
Chetabahana
źródło