Поиск всех файлов с заданным расширением, базовое имя которых является именем родительского каталога

9

Я хочу рекурсивно искать каждый *.pdfфайл в каталоге ~/foo, базовое имя которого совпадает с именем родительского каталога файла.

Например, предположим, что структура каталогов ~/fooвыглядит следующим образом

foo
├── dir1
│   ├── dir1.pdf
│   └── dir1.txt
├── dir2
│   ├── dir2.tex
│   └── spam
│       └── spam.pdf
└── dir3
    ├── dir3.pdf
    └── eggs
        └── eggs.pdf

Запуск моей желаемой команды вернется

~/foo/dir1/dir1.pdf
~/foo/dir2/spam/spam.pdf
~/foo/dir3/dir3.pdf
~/foo/dir3/eggs/eggs.pdf

Возможно ли это с помощью findкакой-либо другой основной утилиты? Я предполагаю, что это выполнимо, используя -regexопцию, findно я не уверен, как написать правильный шаблон.

Брайан Фицпатрик
источник
Да, я сейчас приведу пример.
Брайан Фитцпатрик
1
@Inian Добавил пример. Это помогает?
Брайан Фитцпатрик

Ответы:

16

С GNU find:

find . -regextype egrep -regex '.*/([^/]+)/\1\.pdf'
  • -regextype egrep используйте регулярное выражение в стиле egrep.
  • .*/ соответствует директориям прародителя.
  • ([^/]+)/ соответствует родительский каталог в группе.
  • \1\.pdfиспользовать backreferenceдля сопоставления имени файла в качестве родительского каталога.

Обновить

Один (я для одного) может подумать, что .*это достаточно жадный, его не нужно исключать /из родительского соответствия:

find . -regextype egrep -regex '.*/(.+)/\1\.pdf'

Выше команда не будет работать хорошо, потому что это выглядит так ./a/b/a/b.pdf:

  • .*/ Матчи ./
  • (.+)/ Матчи a/b/
  • \1.pdf Матчи a/b.pdf
dedowsdi
источник
Очень круто. Жаль, что я не смог бы это хорошо проверить.
Брайан Фитцпатрик
Или find . -regex '.*/\([^/]*\)/\1\.pdf'тогда это будет даже работать с BSD find.
Стефан
7

Традиционный вариант цикла find .. -exec sh -c ''использования конструкций оболочки для соответствия базовому имени и непосредственному пути выше должен быть сделан ниже.

find foo/ -name '*.pdf' -exec sh -c '
    for file; do 
        base="${file##*/}"
        path="${file%/*}"
        if [ "${path##*/}" =  "${base%.*}" ]; then
            printf "%s\n" "$file" 
        fi
    done' sh {} +

Для разбивки отдельных расширений параметров

  • fileсодержит полный путь к .pdfфайлу, возвращенному из findкоманды
  • "${file##*/}"содержит только часть после последней, /т.е. только базовое имя файла
  • "${file%/*}"содержит путь до финала, /т.е. за исключением части базового имени результата
  • "${path##*/}"содержит часть после последней /из pathпеременной, то есть непосредственный путь к папке над базовым именем файла
  • "${base%.*}"содержит часть базового имени с .pdfудаленным расширением

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

Inian
источник
7

Обратный ответ Inian , то есть поиск каталогов, а затем посмотреть, содержат ли они файл с определенным именем.

Далее выводятся пути к найденным файлам относительно каталога foo:

find foo -type d -exec sh -c '
    for dirpath do
        pathname="$dirpath/${dirpath##*/}.pdf"
        if [ -f "$pathname" ]; then
            printf "%s\n" "$pathname"
        fi
    done' sh {} +

${dirpath##*/}будет заменен частью имени файла в пути к каталогу и может быть заменен на $(basename "$dirpath").

Для людей, которым нравится синтаксис короткого замыкания:

find foo -type d -exec sh -c '
    for dirpath do
        pathname="$dirpath/${dirpath##*/}.pdf"
        [ -f "$pathname" ] && printf "%s\n" "$pathname"
    done' sh {} +

Преимущество этого состоит в том, что у вас может быть больше файлов PDF, чем каталогов. Количество задействованных тестов уменьшается, если ограничить запрос меньшим числом (количеством каталогов).

Например, если один каталог содержит 100 файлов PDF, он будет пытаться обнаружить только один из них, а не проверять имена всех 100 файлов по сравнению с именем каталога.

Кусалананда
источник
3

с zsh:

printf '%s\n' **/*/*.pdf(e@'[[ $REPLY:t = $REPLY:h:t.pdf ]]'@)

Остерегайтесь того, что пока **/не будете следовать символическим ссылкам, */будете.

Стефан Шазелас
источник
2

Это не было указано, но вот решение без регулярных выражений, если кто-то заинтересован.

Мы можем использовать, find . -type fчтобы просто получить файлы, затем использовать dirnameи basenameнаписать условное. Утилиты имеют следующее поведение:

$ find . -type f
./dir2/spam/spam.pdf
./dir2/dir2.tex
./dir3/dir3.pdf
./dir3/eggs/eggs.pdf
./dir1/dir1.pdf
./dir1/dir1.txt

basenameвозвращает только имя файла после последнего /:

$ for file in $(find . -type f); do basename $file; done
spam.pdf
dir2.tex
dir3.pdf
eggs.pdf
dir1.pdf
dir1.txt

dirnameдает весь путь до финала /:

$ for file in $(find . -type f); do dirname $file; done
./dir2/spam
./dir2
./dir3
./dir3/eggs
./dir1
./dir1

Следовательно, basename $(dirname $file)дает родительский каталог файла.

$ for file in $(find . -type f); do basename $(dirname $file) ; done
spam
dir2
dir3
eggs
dir1
dir1

Решение

Объедините вышеперечисленное, чтобы сформировать условное выражение "$(basename $file)" = "$(basename $(dirname $file))".pdf, а затем выводите каждый результат только в том findслучае, если это условное выражение возвращает значение true.

$ while read file; do if [ "$(basename "$file")" = "$(basename "$(dirname "$file")")".pdf ]; then echo $file; fi done < <(find . -type f)
./dir2/spam/spam.pdf
./dir3/dir3.pdf
./dir3/eggs/eggs.pdf
./dir1/dir1.pdf
./Final Thesis/grits/grits.pdf
./Final Thesis/Final Thesis.pdf

В приведенном выше примере мы добавили каталог / файл с пробелами в имени, чтобы обработать этот случай (спасибо @Kusalananda в комментариях)

user1717828
источник
К сожалению, это будет нарушать имена файлов, например Final Thesis.pdf(с пробелом).
Кусалананда
@Kusalananda Исправлено.
user1717828
0

Я использую bash globbing, простые циклические тесты строк в любой день в программе Find . Назовите меня иррациональным, и, хотя он может быть неоптимальным, такой простой код делает свое дело для меня: читаемый и многократно используемый, удовлетворяющий даже! Поэтому позвольте мне предложить комбинацию из:

• Баш globstar : for f in ** ; do ... ** перебирает каждые файлы в текущем каталоге и во всех вложенных папках .. проверить состояние globstar в текущем сеансе: shopt -p globstar. Чтобы активировать globstar: shopt -s globstar.

• «file» utlity : if [[ $(file "$f") =~ pdf ]]; then ... проверить фактический формат файла для pdf - более надежный, чем тестирование только на расширение файла

• basename, dirname : сравнить имя файла с именем каталога непосредственно над ним. basenameвозвращает имя файла - dirnameвозвращает полный путь к каталогу - объединяет две функции, чтобы вернуть только один каталог, содержащий соответствующий файл. Я помещаю каждый из них в переменную ( _mydir и _myf ), чтобы затем выполнить простой тест, используя = ~ для сопоставления строк.

Одна подпрограмма: удалите любую «точку» в имени файла, чтобы избежать совпадения имени файла с текущим каталогом, ярлык которого также «.» - Я использовал прямую подстановку строк для переменной _myf : ${_myf//./}- не очень элегантно, но это работает. Положительные матчи будут возвращать путь каждого файла - вместе с полным путем к текущей папке, предваряя выход с: $(pwd)/.

Код

for f in ** ; do
  if [[ $(file "$f") =~ PDF ]]; then
    _mydir="$(basename $(dirname $f))" ; 
    _myf="$(basename $f)" ; 
    [[ "${_myf//./}" =~ "$_mydir" ]] && echo -e "$(pwd)/$f" ; 
  fi ; 
done
docgyneco69
источник