Конвертировать глобус в `find`

11

У меня снова и снова возникала такая проблема: у меня есть глобус, который точно соответствует правильным файлам, но вызывает Command line too long. Каждый раз, когда я преобразовывал это в некоторую комбинацию, findи grepэто работает для конкретной ситуации, но это не на 100% эквивалентно.

Например:

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

Есть ли инструмент для преобразования глобусов в findвыражения, о которых я не знаю? Или есть вариант для findсопоставления глоба без совпадения с тем же глобусом в поддиректории (например foo/*.jpg, не разрешено совпадать bar/foo/*.jpg)?

Оле Танге
источник
Разверните скобку, и вы сможете использовать результирующие выражения с помощью -pathили -ipath. find . -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg'должно работать - за исключением того, что оно будет соответствовать /fooz/blah/bar/quuxA/pic1234d.jpg. Это будет проблемой?
Муру
Да, это будет проблемой. Это должно быть на 100% эквивалентно.
Оле Танге
Проблема в том, что мы понятия не имеем, в чем именно разница. С твоей моделью все в порядке.
Петер - Восстановить Монику
Я добавил ваш пост в качестве ответа на вопрос. Я надеюсь, что это не так плохо.
Петер - Восстановить Монику
Разве вы не можете сделать это echo <glob> | cat, предполагая мои знания bash, echo является встроенным и, следовательно, не имеет максимального командного лимита
Ferrybig

Ответы:

15

Если проблема заключается в том, что вы получаете ошибку аргумента list-is-too-long, используйте цикл или встроенную оболочку. Хотя command glob-that-matches-too-muchможет выдавать ошибку, for f in glob-that-matches-too-muchнет, так что вы можете просто сделать:

for f in foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg
do
    something "$f"
done

Цикл может быть мучительно медленным, но он должен работать.

Или:

printf "%s\0" foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg |
  xargs -r0 something

( printfбудучи встроенным в большинство оболочек, вышеприведенное работает вокруг ограничения execve()системного вызова)

$ cat /usr/share/**/* > /dev/null
zsh: argument list too long: cat
$ printf "%s\n" /usr/share/**/* | wc -l
165606

Также работает с Bash. Я не уверен точно, где это задокументировано, хотя.


И Vim, glob2regpat()и Python fnmatch.translate()могут конвертировать глобусы в регулярные выражения, но оба также используют .*для *сопоставления по всему /.

Мур
источник
Если это верно, то замена somethingс echoдолжен сделать это.
Оле Танге
1
@OleTange Вот почему я предложил printf- это будет быстрее, чем звонить echoтысячи раз, и предлагает большую гибкость.
Muru
4
Существует ограничение на количество передаваемых аргументов exec, которое применяется к внешним командам, таким как cat; но это ограничение не относится к встроенным командам оболочки, таким как printf.
Стивен Китт
1
@OleTange Строка не слишком длинная, потому что printfона является встроенной, и оболочки, вероятно, используют тот же метод для предоставления ей аргументов, что и для перечисления аргументов for. catне является встроенным
Муру
1
Технически существуют оболочки типа « mkshгде printfне встроено» и оболочки типа « ksh93где» cat(или может быть) встроено. Смотрите также zargsв zshработать вокруг него , без необходимости прибегать к xargs.
Стефан
9

find(для предикатов -name/ -pathstandard) используются шаблоны с подстановочными знаками, аналогично globs (обратите внимание, что {a,b}это не оператор glob; после расширения вы получите два glob). Основным отличием является обработка слешей (и файлов с точками и директорий, которые не обрабатываются специально find). *в шарах не будет охватывать несколько каталогов. */*/*приведет к перечислению до 2 уровней каталогов. Добавление -path './*/*/*'будет соответствовать любым файлам глубиной не менее 3-х уровней и не остановит findперечисление содержимого любого каталога на любой глубине.

Для этого конкретного

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

пара глобусов, их легко перевести, вам нужны каталоги на глубине 3, поэтому вы можете использовать:

find . -mindepth 3 -maxdepth 3 \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

(или -depth 3с некоторыми findреализациями). Или положительно:

find . -path './*/*/*' -prune \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

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

findотличие от globs прочитал бы содержимое каталогов, отличных foo*barот текущего каталога¹, и не отсортировал бы список файлов. Но если мы оставим в стороне проблему, то, что соответствует [A-Z]или поведение */ в ?отношении недопустимых символов, не указано, вы получите тот же список файлов).

Но в любом случае, как показало @muru , нет необходимости прибегать к помощи, findесли это просто для разделения списка файлов на несколько прогонов, чтобы обойти ограничение execve()системного вызова. Некоторые оболочки, такие как zshzargs) или ksh93command -x), даже имеют встроенную поддержку для этого.

С zsh(чьи глобусы также имеют эквивалент -type fи большинство других findпредикатов), например:

autoload zargs # if not already in ~/.zshrc
zargs ./foo*bar/quux[A-Z](|.bak)/pic[0-9][0-9][0-9][0-9]?.jpg(.) -- cmd

( (|.bak)оператор glob противоречит {,.bak}, (.)квалификатор glob является эквивалентом find's' -type f, добавьте oNтуда, чтобы пропустить сортировку, например find, Dс включением точек-файлов (не относится к этому глобу))


¹ Для того, findчтобы сканировать дерево каталогов, как глобусы, вам нужно что-то вроде:

find . ! -name . \( \
  \( -path './*/*' -o -name 'foo*bar' -o -prune \) \
  -path './*/*/*' -prune -name 'pic[0-9][0-9][0-9][0-9]?.jpg' -exec cmd {} + -o \
  \( ! -path './*/*' -o -name 'quux[A-Z]' -o -name 'quux[A-Z].bak' -o -prune \) \)

Это означает удаление всех каталогов на уровне 1, за исключением foo*barтех, и все на уровне 2, за исключением quux[A-Z]или quux[A-Z].bak, и затем выберите pic...те на уровне 3 (и удалите все каталоги на этом уровне).

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

Вы можете написать регулярное выражение для поиска соответствия вашим требованиям:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'
sebasth
источник
Есть ли инструмент, который делает это преобразование, чтобы избежать человеческих ошибок?
Оле Танге
Нет, но только изменения я сделал было бежать ., добавить дополнительный матч за .bakи изменения *к [^/]*не совпадают пути , как / Foo / Foo / бар и т.д.
sebasth
Но даже ваше обращение неверно. ? не изменяется на [^ /]. Это именно та человеческая ошибка, которую я хочу избежать.
Оле Танге
1
Я думаю, с egrep, вы можете сократить [0-9][0-9][0-9][0-9]?до[0-9]{3,4}
wjandrea
0

Обобщая примечание к моему другому ответу , в качестве более прямого ответа на ваш вопрос, вы можете использовать этот shскрипт POSIX для преобразования глобуса в findвыражение:

#! /bin/sh -
glob=${1#./}
shift
n=$#
p='./*'

while true; do
  case $glob in
    (*/*)
      set -- "$@" \( ! -path "$p" -o -path "$p/*" -o -name "${glob%%/*}" -o -prune \)
      glob=${glob#*/} p=$p/*;;
    (*)
      set -- "$@" -path "$p" -prune -name "$glob"
      while [ "$n" -gt 0 ]; do
        set -- "$@" "$1"
        shift
        n=$((n - 1))
      done
      break;;
  esac
done
find . "$@"

Для использования с одним стандартным shглобаном (не для двух глобусов вашего примера, в которых используется расширение скобок ):

glob2find './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' \
  -type f -exec cmd {} +

(это не игнорирует точечные файлы или точечные каталоги, кроме как .и ..не сортирует список файлов).

Он работает только с глобусами относительно текущего каталога, без .или с ..компонентами. Приложив некоторые усилия, вы можете распространить его на любой глобус, более чем глобус ... Это также можно оптимизировать, чтобы glob2find 'dir/*'он не выглядел так dirже, как для шаблона.

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