Uzyskaj listę podkatalogów zawierających plik, którego nazwa zawiera ciąg znaków

45

Jak mogę uzyskać listę podkatalogów zawierających plik, którego nazwa pasuje do określonego wzorca?

Mówiąc dokładniej, szukam katalogów zawierających plik z literą „f”, który występuje w nazwie pliku.

Najlepiej byłoby, gdyby lista nie zawierała duplikatów i zawierała tylko ścieżkę bez nazwy pliku.

Muhd
źródło

Odpowiedzi:

43
find . -type f -name '*f*' | sed -r 's|/[^/]+$||' |sort |uniq

Powyżej znajduje wszystkie pliki poniżej bieżącego katalogu ( .), które są zwykłymi plikami ( -type f) i mają fgdzieś swoją nazwę ( -name '*f*'). Następnie sedusuwa nazwę pliku, pozostawiając tylko nazwę katalogu. Następnie lista katalogów jest sortowana ( sort), a duplikaty usuwane ( uniq).

sedKomenda składa się z pojedynczego substytut. Wyszukuje dopasowania do wyrażenia regularnego /[^/]+$i zastępuje wszystko pasujące do niego niczym. Znak dolara oznacza koniec linii. [^/]+'oznacza jeden lub więcej znaków, które nie są ukośnikami. Oznacza /[^/]+$to , że wszystkie znaki od ostatniego ukośnika do końca linii. Innymi słowy, odpowiada to nazwie pliku na końcu pełnej ścieżki. Zatem polecenie sed usuwa nazwę pliku, pozostawiając niezmienioną nazwę katalogu, w którym plik się znajdował.

Uproszczenia

Wiele nowoczesnych sortpoleceń obsługuje -uflagę, która czyni ją uniqniepotrzebną. W przypadku GNU sed:

find . -type f -name '*f*' | sed -r 's|/[^/]+$||' |sort -u

I dla MacOS sed:

find . -type f -name '*f*' | sed -E 's|/[^/]+$||' |sort -u

Ponadto, jeśli findpolecenie to obsługuje, możliwe jest findbezpośrednie wydrukowanie nazw katalogów. Pozwala to uniknąć potrzeby sed:

find . -type f -name '*f*' -printf '%h\n' | sort -u

Bardziej niezawodna wersja (wymaga narzędzi GNU)

Powyższe wersje będą mylone z nazwami plików zawierającymi znaki nowej linii. Bardziej niezawodnym rozwiązaniem jest sortowanie ciągów zakończonych NUL:

find . -type f -name '*f*' -printf '%h\0' | sort -zu | sed -z 's/$/\n/'
John1024
źródło
Mam wiele plików, przez co sortowanie ich jest zbyt kosztowne. Wrzucanie uniqdo miksu bardzo pomaga, usuwając powtarzające się linie, które są już obok siebie. find . -type f -name '*f*' -printf '%h\0' | uniq -z | sort -zu | tr '\0' '\n'. Lub jeśli twoje narzędzia są trochę starsze, to uniq może nie mieć opcji -z. find . -type f -name '*f*' -printf '%h\n' | uniq | sort -u
jbo5112
1
Użytkownicy systemu MacOS: Flaga sed nie jest -r. Z jakiegoś powodu jego -E
David
@David Bardzo prawda. Odpowiedź zaktualizowana w celu wyświetlenia -Ew systemie MacOS.
John1024
22

Dlaczego nie spróbować tego:

find / -name '*f*' -printf "%h\n" | sort -u
Patrick Taylor
źródło
Najlepsza odpowiedź. Całkowicie kompatybilny z POSIX, w przeciwieństwie do niektórych odpowiedzi powyżej, powyżej, a także zdobywa specjalną nagrodę The Shortest Pipeline :).
kkm,
Chciałbym zobaczyć, jak ktoś pokazuje czas w porównaniu z innymi powyżej, ponieważ mam wrażenie, że jest to zdecydowanie najszybszy.
dlamblin
4
@kkm Zgadzam się, że to najlepsze rozwiązanie, ale specyfikacje POSIXfind są w rzeczywistości dość rzadkie - -printfoperator nie jest określony. To nie działa z BSD find. Zatem nie „w pełni kompatybilny z POSIX”. (Chociaż sort -u jest w POSIX .)
Wildcard
8

Istnieją zasadniczo 2 metody, których można użyć do tego celu. Jeden parsuje ciąg, a drugi będzie działał na każdym pliku. Podczas analizowania ciągu użyj narzędzia takiego jak grep, sedlub, awkoczywiście, będzie on szybszy, ale oto przykład pokazujący oba, a także sposób „profilowania” 2 metod.

Przykładowe dane

W poniższych przykładach wykorzystamy następujące dane

$ touch dir{1..3}/dir{100..112}/file{1..5}
$ touch dir{1..3}/dir{100..112}/nile{1..5}
$ touch dir{1..3}/dir{100..112}/knife{1..5}

Usuń niektóre *f*pliki z dir1/*:

$ rm dir1/dir10{0..2}/*f*

Podejście nr 1 - Analiza składni za pomocą łańcuchów

Tutaj mamy zamiar wykorzystywać następujące narzędzia, find, grep, i sort.

$ find . -type f -name '*f*' | grep -o "\(.*\)/" | sort -u | head -5
./dir1/dir103/
./dir1/dir104/
./dir1/dir105/
./dir1/dir106/
./dir1/dir107/

Podejście nr 2 - Analiza przy użyciu plików

Ten sam łańcuch narzędzi jak poprzednio, tyle że tym razem będziemy używać dirnamezamiast grep.

$ find . -type f -name '*f*' -exec dirname {} \; | sort -u | head -5
./dir1/dir103
./dir1/dir104
./dir1/dir105
./dir1/dir106
./dir1/dir107

UWAGA: Powyższe przykłady służą head -5jedynie do ograniczenia ilości danych wyjściowych, z którymi mamy do czynienia w tych przykładach. Zwykle byłyby usuwane, aby uzyskać pełną listę!

Porównywanie wyników

Możemy użyć, timeaby spojrzeć na 2 podejścia.

dirname

real        0m0.372s
user        0m0.028s
sys         0m0.106s

grep

real        0m0.012s
user        0m0.009s
sys         0m0.007s

Dlatego zawsze najlepiej radzić sobie z ciągami znaków, jeśli to możliwe.

Alternatywne metody analizy ciągów

grep i PCRE

$ find . -type f -name '*f*' | grep  -oP '^.*(?=/)' | sort -u

sed

$ find . -type f -name '*f*' | sed 's#/[^/]*$##' | sort -u

awk

$ find . -type f -name '*f*' | awk -F'/[^/]*$' '{print $1}' | sort -u
slm
źródło
+1 Ponieważ to działa, ale co ciekawe, zajmuje to wiele razy więcej niż odpowiedź @ John1024
Muhd
@Muhd - tak, połączenia do dirname są powolne. Pracuję nad alternatywą.
slm
2

Oto jeden uważam za użyteczny:

find . -type f -name "*somefile*" | xargs dirname | sort | uniq
Martin Tapp
źródło
1

Ta odpowiedź bezwstydnie oparta jest na odpowiedzi SLM. To było ciekawe podejście, ale ma ograniczenia, jeśli nazwy plików i / lub katalogów mają specjalne znaki (spacja, półkolumna ...). Dobrym nawykiem jest używanie find /somewhere -print0 | xargs -0 someprogam.

Przykładowe dane

W poniższych przykładach wykorzystamy następujące dane

mkdir -p dir{1..3}/dir\ {100..112}
touch dir{1..3}/dir\ {100..112}/nile{1..5}
touch dir{1..3}/dir\ {100..112}/file{1..5}
touch dir{1..3}/dir\ {100..112}/kni\ fe{1..5}

Usuń niektóre *f*pliki z dir1/*/:

rm dir1/dir\ 10{0..2}/*f*

Podejście nr 1 - Analiza przy użyciu plików

$ find -type f -name '*f*' -print0 | sed -e 's#/[^/]*\x00#\x00#g' | sort -zu | xargs -0 -n1 echo | head -n5
./dir1/dir 103
./dir1/dir 104
./dir1/dir 105
./dir1/dir 106
./dir1/dir 107

UWAGA : Powyższe przykłady służą head -5jedynie do ograniczenia ilości danych wyjściowych, z którymi mamy do czynienia w tych przykładach. Zwykle byłyby usuwane, aby uzyskać pełną listę! również zamień echodowolne polecenie, którego chcesz użyć.

Franklin Piat
źródło
1

Z zsh:

typeset -aU dirs # array with unique values
dirs=(**/*f*(D:h))

printf '%s\n' $dirs
Stéphane Chazelas
źródło