Prueba si un globo tiene alguna coincidencia en bash

223

Si quiero verificar la existencia de un solo archivo, puedo probarlo usando test -e filenameo [ -e filename ].

Supongamos que tengo un glob y quiero saber si existen archivos cuyos nombres coincidan con el glob. El globo puede coincidir con 0 archivos (en cuyo caso no necesito hacer nada), o puede coincidir con 1 o más archivos (en cuyo caso necesito hacer algo). ¿Cómo puedo probar si un globo tiene alguna coincidencia? (No me importa cuántas coincidencias hay, y sería mejor si pudiera hacer esto con una ifdeclaración y sin bucles (simplemente porque me parece más legible).

( test -e glob*falla si el globo coincide con más de un archivo).

Ken Bloom
fuente
3
Sospecho que mi respuesta a continuación es 'claramente correcta' de una manera que todos los demás se burlan. Es una carcasa de una sola línea que ha existido desde siempre y parece ser 'la herramienta prevista para este trabajo en particular'. Me preocupa que los usuarios hagan referencia por error a la respuesta aceptada aquí. Alguien, por favor, siéntase libre de corregirme y retiraré mi comentario aquí, estoy más que feliz de estar equivocado y aprender de ello. Si la diferencia no pareciera tan drástica, no plantearía este problema.
Brian Chrisman
1
Mis soluciones favoritas para esta pregunta son el comando find que funciona en cualquier shell (incluso los shells que no son Bourne) pero requiere GNU find, y el comando compgen que es claramente un Bashismo. Lástima que no puedo aceptar ambas respuestas.
Ken Bloom
Nota: esta pregunta se ha editado desde que se hizo. El título original era "Prueba si un globo tiene alguna coincidencia en bash". El shell específico, 'bash', se eliminó de la pregunta después de que publiqué mi respuesta. La edición del título de la pregunta hace que mi respuesta parezca errónea. Espero que alguien pueda enmendar o al menos abordar este cambio.
Brian Chrisman

Respuestas:

178

Solución específica de Bash :

compgen -G "<glob-pattern>"

Escápese del patrón o se expandirá previamente en coincidencias.

El estado de salida es:

  • 1 para no partido,
  • 0 para 'uno o más partidos'

stdoutes una lista de archivos que coinciden con el glob .
Creo que esta es la mejor opción en términos de concisión y minimizar los posibles efectos secundarios.

ACTUALIZACIÓN : Ejemplo de uso solicitado.

if compgen -G "/tmp/someFiles*" > /dev/null; then
    echo "Some files exist."
fi
Brian Chrisman
fuente
99
Tenga en cuenta que compgenes un comando incorporado específico de bash y que no forma parte de los comandos integrados estándar de POSIX Unix shell. pubs.opengroup.org/onlinepubs/9699919799 pubs.opengroup.org/onlinepubs/9699919799/utilities/… Por lo tanto, evite usarlo en scripts donde la portabilidad a otros shells es una preocupación.
Diomidis Spinellis
1
Me parece que un efecto similar sin bash builtins sería usar cualquier otro comando que actúe en un globo y falla si no coinciden los archivos, como ls: if ls /tmp/*Files 2>&1 >/dev/null; then echo exists; fi¿quizás útil para el golf de código? Falla si hay un archivo con el mismo nombre que el glob, que el glob no debería haber coincidido, pero si ese es el caso, probablemente tenga mayores problemas.
Dewi Morgan
44
@DewiMorgan Esto es más simple:if ls /tmp/*Files &> /dev/null; then echo exists; fi
Clay Bridges
Para más detalles sobre compgen, ver man basho conhelp compgen
el-teedee
2
sí, cítelo o el comodín del nombre de archivo se expandirá previamente. compgen "dir / *. ext"
Brian Chrisman
169

La opción de shell nullglob es de hecho un bashism.

Para evitar la necesidad de guardar y restaurar tediosamente el estado de globo nulo, solo lo configuro dentro de la subshell que expande el globo:

if test -n "$(shopt -s nullglob; echo glob*)"
then
    echo found
else
    echo not found
fi

Para una mejor portabilidad y un globbing más flexible, use find:

if test -n "$(find . -maxdepth 1 -name 'glob*' -print -quit)"
then
    echo found
else
    echo not found
fi

Las acciones explícitas -print -quit se utilizan para buscar en lugar de la acción implícita -print implícita , de modo que find se cerrará tan pronto como encuentre el primer archivo que coincida con los criterios de búsqueda. Cuando coinciden muchos archivos, esto debería ejecutarse mucho más rápido que echo glob*o ls glob*y también evita la posibilidad de sobrecargar la línea de comando expandida (algunos shells tienen un límite de longitud 4K).

Si find se siente como una exageración y el número de archivos que probablemente coincidan es pequeño, use stat:

if stat -t glob* >/dev/null 2>&1
then
    echo found
else
    echo not found
fi
Flabdablet
fuente
10
findParece ser exactamente correcto. No tiene casos de esquina, ya que el shell no se está expandiendo (y no pasa un globo expandido a otro comando), es portátil entre los shells (aunque aparentemente no todas las opciones que usa están especificadas por POSIX), y es más rápido que ls -d glob*(la respuesta aceptada anterior) porque se detiene cuando llega al primer partido.
Ken Bloom
1
Tenga en cuenta que esta respuesta puede requerir una, shopt -u failglobya que estas opciones parecen entrar en conflicto de alguna manera.
Calimo
La findsolución también coincidirá con un nombre de archivo sin caracteres globales. En este caso, eso es lo que quería. Sin embargo, es algo a tener en cuenta.
Todos somos Mónica
1
Como alguien más decidió editar mi respuesta para que dijera eso, aparentemente.
flabdablet
1
unix.stackexchange.com/questions/275637/… discute cómo reemplazar la -maxdepthopción para un hallazgo POSIX.
Ken Bloom
25
#!/usr/bin/env bash

# If it is set, then an unmatched glob is swept away entirely -- 
# replaced with a set of zero words -- 
# instead of remaining in place as a single word.
shopt -s nullglob

M=(*px)

if [ "${#M[*]}" -ge 1 ]; then
    echo "${#M[*]} matches."
else
    echo "No such files."
fi
miku
fuente
2
Para evitar un posible conjunto falso de "no coincide" en nullgloblugar de verificar si un solo resultado es igual al patrón en sí. Algunos patrones pueden coincidir con nombres que son exactamente iguales al patrón en sí (por ejemplo a*b, pero no por ejemplo a?bo [a]).
Chris Johnsen
Supongo que esto falla en la posibilidad altamente improbable de que realmente haya un archivo llamado como el glob. (por ejemplo, alguien corrió touch '*py'), pero esto me señala en otra buena dirección.
Ken Bloom
Me gusta esta como la versión más general.
Ken Bloom
Y también el más corto. Si solo espera una coincidencia, puede usarla "$M"como abreviatura para "${M[0]}". De lo contrario, bueno, ya tiene la expansión global en una variable de matriz, por lo que debe pasarla a otras cosas como una lista, en lugar de hacer que vuelvan a expandir la globalización.
Peter Cordes
Agradable. Puede probar M más rápidamente (menos bytes y sin generar un [proceso) conif [[ $M ]]; then ...
Tobia
22

me gusta

exists() {
    [ -e "$1" ]
}

if exists glob*; then
    echo found
else
    echo not found
fi

Esto es legible y eficiente (a menos que haya una gran cantidad de archivos).
El principal inconveniente es que es mucho más sutil de lo que parece, y a veces me siento obligado a agregar un comentario largo.
Si hay una coincidencia, "glob*"se expande por el shell y se pasan todas las coincidencias exists(), que comprueba la primera e ignora el resto.
Si no hay coincidencia, "glob*"se pasa exists()y tampoco se encuentra allí.

Editar: puede haber un falso positivo, ver comentario

Dan Bloch
fuente
13
Se puede devolver un falso positivo si el pegote es algo así como *.[cC](puede haber no co Carchivo, sino un archivo llamado *.[cC]) o falso negativo si el primer archivo expandido desde que es, por ejemplo, un enlace a un archivo unexistent o en un archivo en una directorio al que no tiene acceso (usted quiere agregar un || [ -L "$1" ]).
Stephane Chazelas
Interesante. Shellcheck informa que solo funciona el globbing -ecuando hay 0 o 1 coincidencias. No funciona para múltiples coincidencias, porque eso se convertiría [ -e file1 file2 ]y fallaría. Consulte también github.com/koalaman/shellcheck/wiki/SC2144 para conocer los fundamentos y las soluciones sugeridas.
Thomas Praxl
10

Si tienes un conjunto de globfail puedes usar este loco (que realmente no deberías)

shopt -s failglob # exit if * does not match 
( : * ) && echo 0 || echo 1

o

q=( * ) && echo 0 || echo 1
James
fuente
2
Un uso fantástico de un noop fallando. Nunca debe ser usado ... pero realmente hermoso. :)
Brian Chrisman
Puedes poner el shopt dentro de los parens. De esa manera solo afecta la prueba:(shopt -s failglob; : *) 2>/dev/null && echo exists
flabdablet
8

test -e tiene la desafortunada advertencia de que considera que los enlaces simbólicos rotos no existen. Por lo tanto, es posible que también desee verificarlos.

function globexists {
  test -e "$1" -o -L "$1"
}

if globexists glob*; then
    echo found
else
    echo not found
fi
NerdMachine
fuente
44
Eso todavía no soluciona el falso positivo en los nombres de archivo que tienen caracteres especiales globales, como Stephane Chazelas señala para la respuesta de Dan Bloch. (a menos que seas mono con nullglob).
Peter Cordes
3
Debe evitar -oy -aen test/ [. Por ejemplo, aquí, falla si $1es =con la mayoría de las implementaciones. Usar en su [ -e "$1" ] || [ -L "$1" ]lugar.
Stephane Chazelas
4

Para simplificar un poco la respuesta de MYYN, basada en su idea:

M=(*py)
if [ -e ${M[0]} ]; then
  echo Found
else
  echo Not Found
fi
Ken Bloom
fuente
44
Cerrar, pero ¿qué pasa si está haciendo coincidir [a], tiene un archivo con nombre [a], pero no tiene nombre a? Todavía me gusta nullglobpor esto. Algunos podrían ver esto como pedante, pero también podríamos ser tan correctos como sea razonable.
Chris Johnsen
@ sondra.kinsey Eso está mal; el glob [a]solo debe coincidir a, no el nombre literal del archivo [a].
tripleee
4

Basado en la respuesta de flabdablet , para mí parece que lo más fácil (no necesariamente el más rápido) es usar find , mientras deja la expansión global en el shell, como:

find /some/{p,long-p}ath/with/*globs* -quit &> /dev/null && echo "MATCH"

O en ifcomo:

if find $yourGlob -quit &> /dev/null; then
    echo "MATCH"
else
    echo "NOT-FOUND"
fi
queria
fuente
Esto funciona exactamente como la versión que ya presenté usando stat; No estoy seguro de cómo encontrar es "más fácil" que stat.
flabdablet
3
Tenga en cuenta que la redirección &> es un bashism, y silenciosamente hará lo incorrecto en otros shells.
flabdablet
Esto parece ser mejor que la findrespuesta de flabdablet porque acepta rutas en el globo y es más conciso (no requiere, -maxdepthetc.). También parece mejor que su statrespuesta porque no continúa haciendo el extra staten cada partido adicional de glob. Agradecería si alguien pudiera contribuir casos de esquina donde esto no funciona.
drwatsoncode
1
Después de una consideración adicional, agregaría -maxdepth 0porque permite más flexibilidad para agregar condiciones. por ejemplo, suponga que quiero restringir el resultado solo a archivos coincidentes. Podría intentarlo find $glob -type f -quit, pero eso sería verdadero si el glob NO coincidiera con un archivo, pero sí con un directorio que contuviera un archivo (incluso de forma recursiva). Por el contrario find $glob -maxdepth 0 -type f -quit, solo sería verdadero si el glob en sí coincidiera con al menos un archivo. Tenga en cuenta que maxdepthno impide que el glob tenga un componente de directorio. (FYI 2>es suficiente. No es necesario &>)
drwatsoncode
2
El punto de usar finden primer lugar es evitar que el shell genere y clasifique una lista potencialmente enorme de coincidencias globales; find -name ... -quitcoincidirá como máximo con un nombre de archivo. Si un script se basa en pasar una lista generada por shell de coincidencias globales find, la invocación findno logra más que una sobrecarga innecesaria de inicio del proceso. Simplemente probar la lista resultante directamente para no vacío será más rápido y más claro.
flabdablet
4

Tengo otra solución más:

if [ "$(echo glob*)" != 'glob*' ]

Esto funciona muy bien para mí. ¿Hay algunos casos de esquina que echo de menos?

SaschaZorn
fuente
2
Funciona excepto si el archivo se llama realmente 'glob *'.
Ian Kelling
funciona para pasar el glob como variable: da un error de "demasiados argumentos" cuando hay más de una coincidencia. "$ (echo $ GLOB)" no devuelve una sola cadena o al menos no se interpreta como una sola, por lo tanto, el error de demasiados argumentos
DKebler
@DKebler: debe interpretarse como una sola cadena, ya que está entre comillas dobles.
user1934428
3

En Bash, puede pasar a una matriz; Si el glob no coincide, su matriz contendrá una sola entrada que no corresponde a un archivo existente:

#!/bin/bash

shellglob='*.sh'

scripts=($shellglob)

if [ -e "${scripts[0]}" ]
then stat "${scripts[@]}"
fi

Nota: si ha nullglobconfigurado, scriptsserá una matriz vacía, y debe probar con [ "${scripts[*]}" ]o con [ "${#scripts[*]}" != 0 ]. Si está escribiendo una biblioteca que debe funcionar con o sin nullglob, querrá

if [ "${scripts[*]}" ] && [ -e "${scripts[0]}" ]

Una ventaja de este enfoque es que tiene la lista de archivos con los que desea trabajar, en lugar de tener que repetir la operación global.

Toby Speight
fuente
¿Por qué, con el conjunto de nullglob, y la matriz posiblemente vacía, aún no puedes probar if [ -e "${scripts[0]}" ]...? ¿También está permitiendo la posibilidad del conjunto de sustantivos de opción de shell ?
johnraff
@johnraff, sí, normalmente asumo que nounsetestá activo. Además, puede ser (ligeramente) más barato probar que la cadena no está vacía que verificar la presencia de un archivo. Sin embargo, es poco probable, dado que acabamos de realizar un glob, lo que significa que el contenido del directorio debe estar fresco en la memoria caché del sistema operativo.
Toby Speight
1

Esta abominación parece funcionar:

#!/usr/bin/env bash
shopt -s nullglob
if [ "`echo *py`" != "" ]; then
    echo "Glob matched"
else
    echo "Glob did not match"
fi

Probablemente requiere bash, no sh.

Esto funciona porque la opción nullglob hace que el glob se evalúe como una cadena vacía si no hay coincidencias. Por lo tanto, cualquier salida no vacía del comando echo indica que el globo coincide con algo.

Ryan C. Thompson
fuente
Deberías usarif [ "`echo *py`" != "*py"]
yegle
1
Eso no funcionaría correctamente si hubiera un archivo llamado *py.
Ryan C. Thompson
Si no hay un archivo final con py, `echo *py`se evaluará a *py.
yegle
1
Sí, pero también lo hará si hay un solo archivo llamado *py, que es el resultado incorrecto.
Ryan C. Thompson
Corrígeme si me equivoco, pero si no hay ningún archivo que coincida *py, ¿tu script hará eco de "Glob coincidente"?
yegle
1

No vi esta respuesta, así que pensé en ponerla ahí:

set -- glob*
[ -f "$1" ] && echo "found $@"
Brad Howes
fuente
1
set -- glob*
if [ -f "$1" ]; then
  echo "It matched"
fi

Explicación

Cuando no hay una coincidencia para glob*, entonces $1contendrá 'glob*'. La prueba -f "$1"no será verdadera porque el glob*archivo no existe.

¿Por qué esto es mejor que las alternativas?

Esto funciona con sh y derivados: ksh y bash. No crea ningún sub-shell. $(..)y los `...`comandos crean un sub-shell; bifurcan un proceso y, por lo tanto, son más lentos que esta solución.

joseyluis
fuente
1

Me gusta esto en (archivos de prueba que contienen pattern):

shopt -s nullglob
compgen -W *pattern* &>/dev/null
case $? in
    0) echo "only one file match" ;;
    1) echo "more than one file match" ;;
    2) echo "no file match" ;;
esac

Es mucho mejor que compgen -G: porque podemos discriminar más casos y con mayor precisión.

Puede funcionar con un solo comodín *

Gilles Quenot
fuente
0
if ls -d $glob > /dev/null 2>&1; then
  echo Found.
else
  echo Not found.
fi

Tenga en cuenta que esto puede llevar mucho tiempo si hay muchas coincidencias o si el acceso al archivo es lento.

Florian Diesch
fuente
1
Esto dará la respuesta incorrecta si [a]se usa un patrón como cuando el archivo [a]está presente y el archivo aestá ausente. Dirá "encontrado" a pesar de que el único archivo con el que debe coincidir ano está realmente presente.
Chris Johnsen
Esta versión debería funcionar en un POSIX / bin / sh ordinario (sin bashisms), y en el caso en que lo necesite, el glob no tiene corchetes de todos modos, y no necesito preocuparme por los casos que son terriblemente patológico Pero supongo que no hay una buena manera de probar si algún archivo coincide con un globo.
Ken Bloom
0
#!/bin/bash
set nullglob
touch /tmp/foo1 /tmp/foo2 /tmp/foo3
FOUND=0
for FILE in /tmp/foo*
do
    FOUND=$((${FOUND} + 1))
done
if [ ${FOUND} -gt 0 ]; then
    echo "I found ${FOUND} matches"
else
    echo "No matches found"
fi
Peter Lyons
fuente
2
Esta versión falla cuando coincide exactamente un archivo, pero puede evitar el FOUND = -1 kludge utilizando la nullglobopción de shell.
Ken Bloom
@ Ken: Hmm, no llamaría a nullglobun kludge. Comparar un solo resultado con el patrón original es un error (y propenso a resultados falsos), el uso nullglobno lo es.
Chris Johnsen
@ Chris: Creo que leíste mal. No llamé a nullglobun kludge.
Ken Bloom
1
@ Ken: De hecho, leí mal. Por favor, acepte mis disculpas por mi crítica no válida.
Chris Johnsen
-1
(ls glob* &>/dev/null && echo Files found) || echo No file found
Damodharan R
fuente
55
También devolvería falso si hay directorios que coinciden glob*y, por ejemplo, no tiene la escritura para enumerar esos directorios.
Stephane Chazelas
-1

ls | grep -q "glob.*"

No es la solución más eficiente (si hay una tonelada de archivos en el directorio, puede ser lento), pero es simple, fácil de leer y también tiene la ventaja de que las expresiones regulares son más potentes que los patrones simples de globo de bash.

jesjimher
fuente
-2
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true
otocan
fuente
1
Para una mejor respuesta, intente agregar alguna explicación a su código.
Masoud Rahimi