Hay algunas formas tail
de salir:
Enfoque deficiente: obligar tail
a escribir otra línea
Puede forzar la tail
escritura de otra línea de salida inmediatamente después de grep
encontrar una coincidencia y salir. Esto provocará tail
que aparezca un SIGPIPE
, haciendo que salga. Una forma de hacerlo es modificar el archivo que se está monitoreando tail
después de las grep
salidas.
Aquí hay un código de ejemplo:
tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }
En este ejemplo, cat
no saldrá hasta que grep
haya cerrado su stdout, por tail
lo que no es probable que pueda escribir en la tubería antes de grep
haber tenido la oportunidad de cerrar su stdin. cat
se utiliza para propagar la salida estándar de grep
no modificado.
Este enfoque es relativamente simple, pero hay varias desventajas:
- Si
grep
cierra stdout antes de cerrar stdin, siempre habrá una condición de carrera: grep
cierra stdout, disparando cat
para salir, disparando echo
, disparando tail
para generar una línea. Si esta línea se envió grep
antes, grep
ha tenido la oportunidad de cerrar la entrada estándar, tail
no aparecerá SIGPIPE
hasta que escriba otra línea.
- Requiere acceso de escritura al archivo de registro.
- Debe estar de acuerdo con la modificación del archivo de registro.
- Puede corromper el archivo de registro si escribe al mismo tiempo que otro proceso (las escrituras pueden estar intercaladas, haciendo que aparezca una nueva línea en el medio de un mensaje de registro).
- Este enfoque es específico para:
tail
no funcionará con otros programas.
- La tercera etapa de canalización dificulta el acceso al código de retorno de la segunda etapa de canalización (a menos que esté utilizando una extensión POSIX como
bash
la PIPESTATUS
matriz de 's ). Esto no es un gran problema en este caso porque grep
siempre devolverá 0, pero en general la etapa intermedia podría ser reemplazada por un comando diferente cuyo código de retorno le interese (por ejemplo, algo que devuelve 0 cuando se detecta "servidor iniciado", 1 cuando se detecta "no se pudo iniciar el servidor").
Los siguientes enfoques evitan estas limitaciones.
Un mejor enfoque: evitar tuberías
Puede usar un FIFO para evitar la canalización por completo, permitiendo que la ejecución continúe una vez que grep
regrese. Por ejemplo:
fifo=/tmp/tmpfifo.$$
mkfifo "${fifo}" || exit 1
tail -f logfile.log >${fifo} &
tailpid=$! # optional
grep -m 1 "Server Started" "${fifo}"
kill "${tailpid}" # optional
rm "${fifo}"
Las líneas marcadas con el comentario # optional
pueden eliminarse y el programa seguirá funcionando; tail
solo permanecerá hasta que lea otra línea de entrada o sea eliminado por algún otro proceso.
Las ventajas de este enfoque son:
- no necesita modificar el archivo de registro
- el enfoque funciona para otras utilidades además de
tail
- no sufre una condición de carrera
- puede obtener fácilmente el valor de retorno de
grep
(o cualquier comando alternativo que esté usando)
La desventaja de este enfoque es la complejidad, especialmente la gestión de la FIFO: deberá generar de forma segura un nombre de archivo temporal, y deberá asegurarse de que la FIFO temporal se elimine incluso si el usuario presiona Ctrl-C en medio de la secuencia de comandos. Esto se puede hacer usando una trampa.
Enfoque alternativo: envíe un mensaje para matar tail
Puede hacer que salga la tail
etapa de canalización enviándole una señal como SIGTERM
. El desafío es conocer de manera confiable dos cosas en el mismo lugar en el código: tail
el PID y si grep
ha salido.
Con una canalización como tail -f ... | grep ...
, es fácil modificar la primera etapa de canalización para guardar tail
el PID en una variable al fondo tail
y leer $!
. También es fácil modificar la segunda etapa de canalización para que se ejecute kill
cuando grep
salga. El problema es que las dos etapas de la tubería se ejecutan en "entornos de ejecución" separados (en la terminología del estándar POSIX) por lo que la segunda etapa de la tubería no puede leer ninguna variable establecida por la primera etapa de la tubería. Sin usar variables de shell, o bien la segunda etapa debe de alguna manera descubrir tail
el PID para que pueda matar tail
cuando grep
regrese, o la primera etapa debe ser notificada de alguna manera cuando grep
regrese.
La segunda etapa podría usarse pgrep
para obtener tail
el PID, pero eso sería poco confiable (podría coincidir con el proceso incorrecto) y no portátil ( pgrep
no está especificado por el estándar POSIX).
La primera etapa podría enviar el PID a la segunda etapa a través de la tubería mediante echo
el PID, pero esta cadena se mezclará con tail
la salida de. La desmultiplexación de los dos puede requerir un esquema de escape complejo, dependiendo de la salida de tail
.
Puede usar un FIFO para que la segunda etapa de la tubería notifique a la primera etapa de la tubería cuando grep
salga. Entonces la primera etapa puede matar tail
. Aquí hay un código de ejemplo:
fifo=/tmp/notifyfifo.$$
mkfifo "${fifo}" || exit 1
{
# run tail in the background so that the shell can
# kill tail when notified that grep has exited
tail -f logfile.log &
# remember tail's PID
tailpid=$!
# wait for notification that grep has exited
read foo <${fifo}
# grep has exited, time to go
kill "${tailpid}"
} | {
grep -m 1 "Server Started"
# notify the first pipeline stage that grep is done
echo >${fifo}
}
# clean up
rm "${fifo}"
Este enfoque tiene todos los pros y los contras del enfoque anterior, excepto que es más complicado.
Una advertencia sobre el almacenamiento en búfer
POSIX permite que las secuencias stdin y stdout se almacenen completamente en búfer, lo que significa que tail
es posible que el resultado de la salida no se procese grep
durante un tiempo arbitrariamente largo. No debería haber ningún problema en los sistemas GNU: GNU grep
usa read()
, lo que evita todo almacenamiento en búfer, y GNU tail -f
realiza llamadas regulares fflush()
cuando escribe en stdout. Los sistemas que no son GNU pueden tener que hacer algo especial para deshabilitar o limpiar regularmente los búferes.