Quiero escribir un /bin/sh
script de shell que maneje cualquier archivo que coincida con un comodín. Es fácil manejar 1 o más archivos coincidentes. Sin embargo, me resulta incómodo manejar el caso de 0 archivos coincidentes.
La construcción obvia es:
#!/bin/sh
for f in *.ext; do
handle "$f"
done
donde *.ext
podría haber una o más expresiones que el shell compara con rutas de archivos, y handle
es un comando de shell que se ejecuta correctamente si se le proporciona una ruta a un archivo existente, pero falla si se proporciona una ruta que no se asigna a un archivo. (En el caso que provoque esta pregunta, son *.flac
y ffmpeg
, pero creo que eso no importa).
Si hay archivos coincidentes foo.ext
, bar.ext
entonces este script realiza
handle "foo.ext"
handle "bar.ext"
como se esperaba. Sin embargo, si haynocualquier archivo coincidente, este script muestra un mensaje de error como,
handle: *.ext: No such file or directory
Creo que entiendo por qué sucede esto: elintentopágina de manual(que supongo que /bin/sh
también es válido) dice que "la lista de palabras siguientes in
se expande, generando una lista de elementos... Si la expansión de los elementos siguientes da como resultado una lista vacía, no se ejecuta ningún comando y el estado de retorno es 0." Aparentemente cuando *.ext
"se expande", la lista de resultados incluye *.ext
, en lugar de una lista vacía. Pero eso no explica cómo evitar que esto suceda.
(Actualización: hay unsh (1)
página de manual del Proyecto Heirloomque describe el Bourne Shell sh
, no el posterior Bourne Again Shell bash
. BajoGeneración de nombre de archivo, dice claramente: "Si no se encuentra ningún nombre de archivo que coincida con el patrón, la palabra no se modifica". Explica por qué sh
deja un *.ext
patrón en la lista).
¿Cuál es una forma compacta e idiomática de escribir este bucle para que se ejecute cero veces sin errores si no hay archivos coincidentes, pero se ejecute una vez para cada archivo coincidente? Mejor aún, ¿qué funcionará con múltiples patrones como:
for f in *.ext1 special.ext1 *.ext2; do ...
Prefiero usar una sintaxis compatible con /bin/sh
, para mayor portabilidad. Resulta que estoy usando Mac OS X 10.11.6, pero espero una sintaxis que funcione en cualquier sistema operativo tipo Unix.
Se me ocurrieron un par de formas torpes y no idiomáticas de escribir ese bucle. Si no obtengo buenas respuestas de inmediato, las contribuiré como respuestas, para que conste.
Respuesta1
La forma portátil más sencilla de hacer esto es omitir el ciclo si la expansión no produce algo que realmente exista:
for f in *.ext1 *.ext2; do
[ -e "$f" ] || continue
handle "$f"
done
Explicación: supongamos que hay varios archivos .ext2, pero ningún archivo .ext1. En ese caso, los comodines se expandirán a *.ext1
filea.ext2
fileb.ext2
filec.ext2
. Entonces [ -e "*.ext1" ]
falla y se ejecuta continue
, lo que omite el resto de la iteración del ciclo. Luego [ -e "filea.ext2" ]
, etc. tienen éxito y esas iteraciones del bucle se ejecutan normalmente.
Por cierto, puede modificar esto, por ejemplo, [ -f "$f" ] || continue
para omitir cualquier cosa que no sea un archivo simple (si desea omitir directorios, etc.).
Por supuesto, si está ejecutando bash, puede ejecutar shopt -s nullglob
primero para no dejar patrones no coincidentes en la lista. Y shopt -u nullglob
luego, para evitar efectos secundarios inesperados (por ejemplo, grep pattern *.nonexistent
intentará leer desde la entrada estándar si nullglob
está configurada).
Respuesta2
Cambie a Bourne Again Shell ( /bin/bash
) desde Bourne Shell ( /bin/sh
) y será posible encontrar una solución sencilla.
Elbash(1)
página de manualmenciona elglobo nuloopción:
Si está configurado, bash permite patrones que no coinciden con ningún archivo (consulteExpansión de nombre de rutaarriba) para expandirse a una cadena nula, en lugar de ellos mismos.
ElExpansión de nombre de rutasección dice:
Después de dividir palabras,… bash escanea cada palabra en busca de caracteres*,?, y[. Si aparece uno de estos caracteres, la palabra se considera un patrón y se reemplaza con una lista ordenada alfabéticamente de nombres de archivos que coinciden con el patrón. Si no se encuentran nombres de archivos coincidentes y la opción de shellglobo nulono está habilitado, la palabra no se modifica. Si elglobo nuloSe establece la opción y no se encuentran coincidencias, se elimina la palabra.…
Entonces, estableciendo elglobo nuloLa opción proporciona el comportamiento deseado para los patrones. Si ningún archivo coincide con el patrón, el bucle no se ejecuta.
#!/bin/bash
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
Pero elglobo nuloLa opción puede interferir con el comportamiento deseado de otros comandos. Por lo tanto, probablemente le resulte conveniente guardar la configuración existente deglobo nuloy restaurarlo después. Afortunadamente, el shopt -p
comando incorporado emite una salida en un formato que puede reutilizarse como entrada. Pero emite resultados para todas las opciones, así que utilícelo grep
para seleccionar elobjeto nuloconfiguración. Consulte el registro a continuación (donde $
indica el símbolo del sistema de bash):
$ shopt -p | grep nullglob
shopt -u nullglob
$ shopt -s nullglob
$ shopt -p | grep nullglob
shopt -s nullglob
Entonces, la sintaxis final de bash para manejar cero o más archivos que coincidan con un patrón comodín se ve así:
#!/bin/bash
SAVED_NULLGLOB=$(shopt -p | grep nullglob)
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
eval "$SAVED_NULLGLOB"
Además, la pregunta menciona múltiples patrones, como:
for f in *.ext1 special.ext1 *.ext2; do ...
Elglobo nuloLa opción no afecta una palabra en la lista que no sea un "patrón", por lo que special.ext1
aún así se pasará al bucle. La única solución para esto es@GordonDavissoncontinue
La expresión de [ -e "$f" ] || continue
.
Reconocimiento: ambos@Ignacio Vázquez-Abramsy@GordonDavissonaludido bash(1)
y suglobo nuloopción. Gracias. Como no contribuyeron de inmediato con una respuesta con un ejemplo completamente elaborado, lo estoy haciendo yo mismo.