Convierta glob a `find`

11

Una y otra vez tuve este problema: tengo un problema, que coincide exactamente con los archivos correctos, pero causa Command line too long. Cada vez que lo he convertido en una combinación de findy grepque funciona para la situación particular, pero que no es 100% equivalente.

Por ejemplo:

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

¿Existe una herramienta para convertir globos en findexpresiones que no conozco? ¿O hay una opción para findhacer coincidir el glob sin hacer coincidir el mismo glob en un subdirectorio (por ejemplo, foo/*.jpgno está permitido hacer coincidir bar/foo/*.jpg)?

Ole Tange
fuente
Expande la llave y deberías poder usar las expresiones resultantes con -patho -ipath. find . -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg'debería funcionar, excepto que coincidirá /fooz/blah/bar/quuxA/pic1234d.jpg. será eso un problema?
muru
Sí, eso será un problema. Tiene que ser 100% equivalente.
Ole Tange
El problema es que no tenemos idea, cuál es exactamente la diferencia. Tu patrón está bastante bien.
peterh - Restablece a Mónica el
Agregué tu publicación de extensión como respuesta a la pregunta. Espero que no sea tan malo.
peterh - Restablece a Mónica el
No puedes hacerlo echo <glob> | cat, suponiendo que yo sepa bash, echo está
integrado

Respuestas:

15

Si el problema es que obtiene un error argumento-lista-es-demasiado-largo, use un bucle o un shell incorporado. Si bien command glob-that-matches-too-muchpuede producir un error, for f in glob-that-matches-too-muchno lo hace, por lo que puede hacer lo siguiente:

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

El bucle puede ser extremadamente lento, pero debería funcionar.

O:

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

( printfestando integrado en la mayoría de los shells, lo anterior funciona alrededor de la limitación de la execve()llamada al sistema)

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

También funciona con bash. Sin embargo, no estoy seguro exactamente dónde está documentado esto.


Tanto Vim glob2regpat()como Python fnmatch.translate()pueden convertir globos en expresiones regulares, pero ambos también se usan .*para *, coincidiendo /.

muru
fuente
Si eso es cierto, entonces reemplazar somethingcon echodebería hacerlo.
Ole Tange
1
@OleTange Por eso sugerí printf: será más rápido que llamar echomiles de veces y ofrece más flexibilidad.
muru
44
Hay un límite en los argumentos que se pueden pasar exec, que se aplica a comandos externos como cat; pero ese límite no se aplica a comandos integrados de shell como printf.
Stephen Kitt
1
@OleTange La línea no es demasiado larga porque printfestá integrada, y los shells presumiblemente usan el mismo método para proporcionarle los argumentos que usan para enumerar los argumentos for. catNo es una construcción.
muru
1
Técnicamente, hay conchas como mkshdonde printfno está incorporado y conchas como ksh93donde catestá (o puede estar) incorporado. Ver también zargsen zshel trabajo alrededor de ella sin tener que recurrir a xargs.
Stéphane Chazelas
9

find(para los predicados -name/ -pathestándar) utiliza patrones comodín al igual que los globos (tenga en cuenta que {a,b}no es un operador de globo; después de la expansión, obtiene dos globos). La principal diferencia es el manejo de barras (y los archivos de puntos y directorios que no se tratan especialmente en find). *en globos no abarcará varios directorios. */*/*hará que se enumeren hasta 2 niveles de directorios. Agregar un -path './*/*/*'archivo coincidirá con cualquier archivo que tenga al menos 3 niveles de profundidad y no dejará findde enumerar el contenido de ningún directorio a ninguna profundidad.

Para ese particular

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

un par de globos, es fácil de traducir, quieres directorios en profundidad 3, por lo que puedes 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 {} +

(o -depth 3con algunas findimplementaciones). O POSIXY:

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

Lo que garantizaría que esos *y ?no podían coincidir con los /personajes

( find, a diferencia de los globos, leería el contenido de directorios distintos de foo*barlos del directorio actual¹, y no ordenaría la lista de archivos. Pero si dejamos de lado el problema de que lo que coincide [A-Z]o el comportamiento de */ ?con respecto a los caracteres no válidos es sin especificar, obtendría la misma lista de archivos).

Pero en cualquier caso, como ha demostrado @muru , no hay necesidad de recurrir findsi es solo para dividir la lista de archivos en varias ejecuciones para evitar el límite de la execve()llamada del sistema. Algunos proyectiles como zsh(con zargs) o ksh93(con command -x) incluso tienen soporte incorporado para eso.

Con zsh(cuyos globos también tienen el equivalente -type fy la mayoría de los otros findpredicados), por ejemplo:

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)es un operador glob contrario a {,.bak}, el (.)calificador glob es el equivalente de find's -type f, agregue oNallí para omitir la clasificación como con find, Dpara incluir archivos de puntos (no se aplica a este glob))


¹ Para findrastrear el árbol de directorios como lo harían los globos, necesitarías 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 \) \)

Es decir, pode todos los directorios en el nivel 1 excepto foo*barlos y todos en el nivel 2 excepto los quux[A-Z]o quux[A-Z].bak, y luego seleccione pic...los del nivel 3 (y pode todos los directorios en ese nivel).

Stéphane Chazelas
fuente
3

Puede escribir una expresión regular para encontrar que coincida con sus requisitos:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'
sebasth
fuente
¿Existe alguna herramienta que haga esta conversión para evitar errores humanos?
Ole Tange
No, pero los únicos cambios que hice fueron a escapar ., añadir el partido opcional para .baky el cambio *a [^/]*que no coincide con los caminos como / foo / foo / bar, etc.
sebasth
Pero incluso tu conversión es incorrecta. ? no se cambia a [^ /]. Este es exactamente el tipo de error humano que quiero evitar.
Ole Tange
1
Creo que con egrep, puedes acortar [0-9][0-9][0-9][0-9]?a[0-9]{3,4}
wjandrea
0

Generalizando en la nota de mi otra respuesta , como una respuesta más directa a su pregunta, podría usar este shscript POSIX para convertir el globo a una findexpresión:

#! /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 usarse con unsh globo estándar (por lo tanto, no con los dos globos de su ejemplo que usan expansión de llaves ):

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

(eso no ignora los archivos de puntos o los directorios de puntos excepto .y ..y no clasifica la lista de archivos).

Que sólo se trabaja con pegotes relativos al directorio actual, sin .o ..componentes. Con un poco de esfuerzo, podría extenderlo a cualquier globo, más que a un globo ... Eso también podría optimizarse para que glob2find 'dir/*'no se vea dirigual que para un patrón.

Stéphane Chazelas
fuente