Получить список подкаталогов, которые содержат файл, имя которого содержит строку

45

Как я могу получить список подкаталогов, которые содержат файл, имя которого соответствует определенному шаблону?

Более конкретно, я ищу каталоги, которые содержат файл с буквой 'f', встречающийся в имени файла.

В идеале список не должен иметь дубликатов и содержать только путь без имени файла.

Muhd
источник

Ответы:

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

Вышеприведенное находит все файлы в текущей директории ( .), которые являются обычными файлами ( -type f) и имеют fгде-то свое имя ( -name '*f*'). Далее sedудаляет имя файла, оставляя только имя каталога. Затем список каталогов сортируется ( sort) и удаляются дубликаты ( uniq).

Команда sedсостоит из единственной замены. Он ищет совпадения с регулярным выражением /[^/]+$и заменяет все, что соответствует этому, ничем. Знак доллара означает конец строки. [^/]+'означает один или несколько символов, которые не являются косыми чертами. Таким образом, /[^/]+$означает все символы от последней косой черты до конца строки. Другими словами, это соответствует имени файла в конце полного пути. Таким образом, команда sed удаляет имя файла, оставляя неизменным имя каталога, в котором находился файл.

Упрощения

Многие современные sortкоманды поддерживают -uфлаг, который делает uniqненужным. Для GNU sed:

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

И, для MacOS SED:

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

Кроме того, если ваша findкоманда поддерживает это, можно findраспечатать имена каталогов напрямую. Это позволяет избежать необходимости sed:

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

Более надежная версия (требуются инструменты GNU)

Вышеуказанные версии будут сбиты с толку именами файлов, которые включают переводы строк. Более надежное решение - выполнить сортировку по NUL-завершенным строкам:

find . -type f -name '*f*' -printf '%h\0' | sort -zu | sed -z 's/$/\n/'
John1024
источник
У меня много файлов, что делает их сортировку слишком дорогой. Бросать uniqв микс очень помогает, удаляя повторяющиеся строки, которые уже находятся рядом друг с другом. find . -type f -name '*f*' -printf '%h\0' | uniq -z | sort -zu | tr '\0' '\n', Или, если ваши инструменты немного старше, то uniq может не иметь опции -z. find . -type f -name '*f*' -printf '%h\n' | uniq | sort -u
jbo5112
1
Пользователи MacOS: флаг sed не -r. По некоторым причинам это -E
Дэвид
@ Дэвид Очень верно. Ответ обновлен, чтобы показать -Eдля MacOS.
John1024
23

Почему бы не попробовать это:

find / -name '*f*' -printf "%h\n" | sort -u
Патрик Тейлор
источник
Лучший ответ. Полностью POSIX-совместимый, в отличие от некоторых ответов выше, выше, а также получает специальный приз The Shortest Pipeline :).
км
Я хотел бы, чтобы кто-то показал время этого против других выше, потому что я чувствую, что это самый быстрый.
Дламблин
4
@kkm Я согласен, что это лучшее решение, но спецификации POSIXfind довольно скудны - -printfоператор не указан. Это не работает с BSD find. Таким образом, не "полностью POSIX-совместимый". (Хотя sort -u это в POSIX .)
Wildcard
8

Есть по существу два метода, которые вы можете использовать для этого. Один будет анализировать строку, в то время как другой будет работать с каждым файлом. Для разбора строки используйте такой инструмент, как grep, sedили awk, очевидно, он будет быстрее, но вот пример, показывающий оба, а также то, как вы можете «профилировать» 2 метода.

Пример данных

Для примеров ниже мы будем использовать следующие данные

$ 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}

Удалить некоторые *f*файлы из dir1/*:

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

Подход № 1 - Разбор через строки

Здесь мы будем использовать следующие инструменты, find, grep, и sort.

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

Подход № 2 - Разбор с использованием файлов

Та же цепочка инструментов, что и раньше, но на этот раз мы будем использовать dirnameвместо grep.

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

ПРИМЕЧАНИЕ. Приведенные выше примеры используются head -5для того, чтобы просто ограничить объем вывода, с которым мы имеем дело в этих примерах. Обычно они удаляются, чтобы получить полный список!

Сравнивая результаты

Мы можем использовать, timeчтобы взглянуть на 2 подхода.

имя_директории

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

Grep

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

Поэтому всегда лучше разбираться со строками, если это возможно.

Альтернативные методы анализа строк

grep & PCRE

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

СЭД

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

AWK

$ find . -type f -name '*f*' | awk -F'/[^/]*$' '{print $1}' | sort -u
ОДС
источник
+1 Потому что это работает, но интересно, что это занимает много раз дольше, чем ответ @ John1024
Muhd
@Muhd - да, вызовы dirname медленные. Я работаю над альтернативой.
SLM
2

Вот один, который я считаю полезным:

find . -type f -name "*somefile*" | xargs dirname | sort | uniq
Мартин Тапп
источник
1

Этот ответ бесстыдно основан на ответе SLM. Это был интересный подход, но он имеет ограничение, если имена файлов и / или каталогов имели специальные символы (пробел, полуколонка ...). Хорошей привычкой является использование find /somewhere -print0 | xargs -0 someprogam.

Пример данных

Для примеров ниже мы будем использовать следующие данные

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}

Удалить некоторые *f*файлы из dir1/*/:

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

Подход № 1 - Разбор с использованием файлов

$ 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

ПРИМЕЧАНИЕ . Приведенные выше примеры используются head -5для того, чтобы просто ограничить объем вывода, с которым мы имеем дело в этих примерах. Обычно они удаляются, чтобы получить полный список! также замените echoкакую команду вы хотите использовать.

Франклин Пиат
источник
1

С zsh:

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

printf '%s\n' $dirs
Стефан Шазелас
источник