Converta glob em `find`

11

Eu tive repetidamente este problema: Eu tenho um glob, que corresponde exatamente aos arquivos corretos, mas causa Command line too long. Toda vez que o converti em alguma combinação de finde grepisso funciona para uma situação específica, mas que não é 100% equivalente.

Por exemplo:

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

Existe uma ferramenta para converter globs em findexpressões que eu não conheço? Ou existe uma opção para findcorresponder ao globo sem corresponder ao mesmo globo em um subdiretório (por exemplo, foo/*.jpgnão é permitido corresponder bar/foo/*.jpg)?

Ole Tange
fonte
Expanda a chave e você poderá usar as expressões resultantes com -pathou -ipath. find . -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg'deve funcionar - exceto que corresponderá /fooz/blah/bar/quuxA/pic1234d.jpg. Isso será um problema?
Muru
Sim, isso será um problema. Tem que ser 100% equivalente.
precisa saber é o seguinte
O problema é que não temos idéia, qual é exatamente a diferença. Seu padrão está bem.
peterh - Restabelece Monica
Adicionei sua postagem de extensão como resposta à pergunta. Espero que não seja tão ruim.
peterh - Restabelece Monica
Você não pode fazer echo <glob> | cat, assumindo o meu conhecimento do bash, eco é build-in, e, portanto, não tem o limite de comandos max
Ferrybig

Respostas:

15

Se o problema é que você recebe um erro da lista de argumentos muito longo, use um loop ou um shell interno. Enquanto command glob-that-matches-too-muchpode errar, for f in glob-that-matches-too-muchnão, então você pode simplesmente fazer:

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

O loop pode ser terrivelmente lento, mas deve funcionar.

Ou:

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

( printfsendo incorporado na maioria dos shells, o acima funciona em torno da limitação da execve()chamada do sistema)

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

Também funciona com o bash. Não tenho certeza exatamente onde isso está documentado.


Tanto o Vim glob2regpat()quanto o Python fnmatch.translate()podem converter globs em regexes, mas ambos também usam .*para *, correspondendo /.

muru
fonte
Se isso for verdade, a substituição somethingpor echodeveria fazê-lo.
precisa saber é o seguinte
1
@OleTange É por isso que sugeri printf- será mais rápido do que ligar echomilhares de vezes e oferece mais flexibilidade.
muru 18/08/19
4
Há um limite nos argumentos que podem ser passados exec, que se aplica a comandos externos como cat; mas esse limite não se aplica a comandos internos do shell, como printf.
Stephen Kitt
1
@OleTange A linha não é muito longa porque printfé um builtin, e os shells presumivelmente usam o mesmo método para fornecer argumentos a eles que eles usam para enumerar argumentos para for. catnão é um builtin.
Muru
1
Tecnicamente, existem conchas como mkshonde printfnão está embutido e conchas como ksh93onde catestá (ou pode ser) embutido. Veja também zargsem zshtrabalhar em torno dele sem ter de recorrer a xargs.
Stéphane Chazelas
9

find(para os predicados -name/ -pathpadrão) usa padrões curinga como globs (observe que {a,b}não é um operador glob; após a expansão, você obtém dois globs). A principal diferença é o tratamento de barras (e os arquivos e diretórios de ponto não são tratados especialmente em find). *no globs não abrange vários diretórios. */*/*fará com que até 2 níveis de diretórios sejam listados. A adição de a -path './*/*/*'corresponderá a qualquer arquivo com pelo menos três níveis de profundidade e não deixará findde listar o conteúdo de qualquer diretório em qualquer profundidade.

Para esse particular

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

par de globs, é fácil de traduzir, você quer diretórios na profundidade 3, para poder usar:

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 {} +

(ou -depth 3com algumas findimplementações). Ou POSIXly:

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 {} +

O que garantiria isso *e ?não poderia corresponder aos /caracteres.

( find, ao contrário dos globs, leria o conteúdo de diretórios diferentes dos do diretório foo*baratual¹ e não classificaria a lista de arquivos. Mas, se deixarmos de lado o problema de que o que corresponde [A-Z]ou o comportamento de */ ?com relação a caracteres inválidos é não especificado, você obterá a mesma lista de arquivos).

Mas, em qualquer caso, como o @muru mostrou , não há necessidade de recorrer findse for apenas para dividir a lista de arquivos em várias execuções para contornar o limite da execve()chamada do sistema. Algumas conchas como zsh(com zargs) ou ksh93(com command -x) têm suporte embutido para isso.

With zsh(cujos globos também têm o equivalente -type fe a maioria dos outros findpredicados), por exemplo:

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)É um operador glob contrário a {,.bak}, o (.)glob qualificador é o equivalente do find's -type f, adicione oNlá para ignorar a classificação como com find, Dpara incluir dot-files (não se aplica a este glob))


To Para findrastrear a árvore de diretórios como os globs precisariam, seria necessário algo como:

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 \) \)

Isso remove todos os diretórios no nível 1, exceto foo*baros e todos no nível 2, exceto os quux[A-Z]ou quux[A-Z].bak, e então seleciona pic...os no nível 3 (e remove todos os diretórios nesse nível).

Stéphane Chazelas
fonte
3

Você pode escrever um regex para encontrar os requisitos correspondentes:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'
sebasth
fonte
Existe uma ferramenta que faça essa conversão para evitar erros humanos?
precisa saber é o seguinte
Não, mas as únicas mudanças que fiz foram para escapar ., adicione o jogo opcional para .bake mudança *para [^/]*não coincidir com caminhos como / foo / foo / bar etc.
sebasth
Mas mesmo a sua conversão está errada. ? não é alterado para [^ /]. Este é exatamente o tipo de erro humano que quero evitar.
precisa saber é o seguinte
1
Eu acho que com egrep, você pode encurtar [0-9][0-9][0-9][0-9]?a[0-9]{3,4}
wjandrea
1
@OleTange Consulte Criar regex a partir da expressão glob
wjandrea
0

Generalizando a nota da minha outra resposta , como uma resposta mais direta à sua pergunta, você pode usar este shscript POSIX para converter o glob em uma findexpressão:

#! /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 . "$@"

Para ser usado com umsh glob padrão (não os dois globs do seu exemplo que usam expansão de chave ):

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

(isso não ignora arquivos de ponto ou dirs de ponto, exceto .e ..não classifica a lista de arquivos).

Esse funciona apenas com globs em relação ao diretório atual, sem componentes .ou ... Com algum esforço, você pode estendê-lo a qualquer glob, mais do que um glob ... Isso também pode ser otimizado para glob2find 'dir/*'não parecer diro mesmo que seria para um padrão.

Stéphane Chazelas
fonte