J'utilise tail -f pour surveiller un fichier journal en cours d'écriture. Lorsqu'une certaine chaîne est écrite dans le fichier journal, je souhaite quitter la surveillance et poursuivre le reste de mon script.
Actuellement, j'utilise:
tail -f logfile.log | grep -m 1 "Server Started"
Lorsque la chaîne est trouvée, grep se ferme comme prévu, mais je dois trouver un moyen de faire quitter également la commande tail afin que le script puisse continuer.
Voici un simple one-liner. Il n'a pas besoin d'astuces spécifiques à bash ou non-POSIX, ni même de pipe nommée. Tout ce dont vous avez besoin est de dissocier la terminaison de tail
de grep
. De cette façon, une fois que grep
se termine, le script peut continuer même si tail
n'est pas encore terminé. Donc, cette méthode simple vous y mènera:
( tail -f -n0 logfile.log & ) | grep -q "Server Started"
grep
bloquera jusqu'à ce qu'il ait trouvé la chaîne, après quoi il se fermera. En exécutant tail
à partir de son propre sous-shell, nous pouvons le placer en arrière-plan afin qu'il s'exécute indépendamment. Pendant ce temps, le shell principal est libre de poursuivre l'exécution du script dès que grep
est terminé. tail
restera dans son sous-shell jusqu'à ce que la ligne suivante soit écrite dans le fichier journal, puis se ferme (éventuellement même après la fin du script principal). Le point principal est que le pipeline n'attend plus la fin de tail
. Il se ferme donc dès que grep
est terminé.
Quelques modifications mineures:
tail
permet de commencer à lire à partir de la dernière ligne actuelle du fichier journal, au cas où la chaîne existe plus tôt dans le fichier journal.tail
-F plutôt que -f. Ce n'est pas POSIX, mais cela permet à tail
de fonctionner même si le journal est pivoté pendant l'attente.grep
à quitter après la première occurrence, mais sans afficher la ligne de déclenchement. En outre, il s'agit de POSIX, ce qui n'est pas le cas de -m1.La réponse acceptée ne fonctionne pas pour moi. En plus, c'est déroutant et cela change le fichier journal.
J'utilise quelque chose comme ça:
tail -f logfile.log | while read LOGLINE
do
[[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done
Si la ligne de journal correspond au modèle, supprimez la tail
démarrée par ce script.
Remarque: si vous souhaitez également afficher la sortie à l'écran, utilisez | tee /dev/tty
ou faites écho à la ligne avant de tester la boucle while.
Si vous utilisez Bash (du moins, mais il semble que ce ne soit pas défini par POSIX, il est peut-être manquant dans certains shells), vous pouvez utiliser la syntaxe
grep -m 1 "Server Started" <(tail -f logfile.log)
Cela fonctionne à peu près comme les solutions FIFO déjà mentionnées, mais beaucoup plus simple à écrire.
Il y a plusieurs façons d'obtenir tail
pour sortir:
tail
à écrire une autre ligneVous pouvez forcer tail
à écrire une autre ligne de sortie immédiatement après que grep
ait trouvé une correspondance et quitté. Cela va donner à tail
une SIGPIPE
, ce qui la fera sortir. Une façon de procéder consiste à modifier le fichier surveillé par tail
après la sortie de grep
.
Voici un exemple de code:
tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }
Dans cet exemple, cat
ne sortira pas tant que grep
n'aura pas fermé sa sortie standard. Il est donc peu probable que tail
puisse écrire dans le canal avant que grep
ait pu fermer son stdin. cat
est utilisé pour propager la sortie standard de grep
non modifiée.
Cette approche est relativement simple, mais il y a plusieurs inconvénients:
grep
ferme stdout avant stdin, il y aura toujours une condition de concurrence: grep
ferme stdout, en déclenchant cat
pour quitter, en déclenchant echo
et en déclenchant tail
en sortie. Si cette ligne est envoyée à grep
avant que grep
ne puisse fermer stdin, tail
n'obtiendra pas la SIGPIPE
tant qu'elle n'aura pas écrit une autre ligne.tail
— elle ne fonctionnera pas avec d'autres programmes.bash
's PIPESTATUS
). Ce n’est pas un gros problème dans ce cas car grep
retournera toujours 0, mais en général, l’étape intermédiaire peut être remplacée par une commande différente dont le code de retour vous tient à cœur (par exemple, quelque chose qui retourne 0 quand "serveur démarré" est détecté, 1 lorsque "échec du démarrage du serveur" est détecté).Les approches suivantes évitent ces limitations.
Vous pouvez utiliser un FIFO pour éviter complètement le pipeline, permettant ainsi à l'exécution de continuer une fois que grep
est retourné. Par exemple:
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}"
Les lignes marquées du commentaire # optional
peuvent être supprimées et le programme fonctionnera toujours. tail
s'attardera jusqu'à ce qu'il lise une autre ligne d'entrée ou soit tué par un autre processus.
Les avantages de cette approche sont les suivants:
tail
grep
(ou toute autre commande que vous utilisez)L'inconvénient de cette approche est la complexité, en particulier la gestion de la FIFO: vous devez générer de manière sécurisée un nom de fichier temporaire et vous assurer que le FIFOtemporaire est supprimé même si l'utilisateur appuie sur la touche Ctrl -C au milieu du script. Cela peut être fait en utilisant un piège.
tail
Vous pouvez obtenir la fermeture de l’étape de pipeline tail
en lui envoyant un signal du type SIGTERM
. Le défi consiste à connaître de manière fiable deux choses au même endroit dans le code: le PID de tail
et si grep
est terminé.
Avec un pipeline comme tail -f ... | grep ...
, il est facile de modifier la première étape du pipeline pour enregistrer le PID de tail
dans une variable en mettant en arrière-plan tail
et en lisant $!
. Il est également facile de modifier la deuxième étape du pipeline pour qu'elle exécute kill
lorsque grep
se termine. Le problème est que les deux étapes du pipeline s'exécutent dans des "environnements d'exécution" distincts (dans la terminologie de la norme POSIX), de sorte que la deuxième étape de pipeline ne peut lire aucune variable définie par l'étape de pipeline précédente. Sans utiliser de variables Shell, la deuxième étape doit déterminer le PID de tail
de façon à pouvoir tuer tail
lorsque grep
est renvoyé, ou la première étape doit être notifiée lorsque grep
est renvoyée.
La deuxième étape pourrait utiliser pgrep
pour obtenir le PID de tail
, mais cela ne serait pas fiable (vous pourriez correspondre au processus incorrect) et non portable (pgrep
n'est pas spécifié par la norme POSIX).
La première étape pourrait envoyer le PID à la deuxième étape via le tuyau en echo
ing le PID, mais cette chaîne sera mélangée avec la sortie de tail
. Le démultiplexage des deux peut nécessiter un schéma d'échappement complexe, en fonction du résultat de tail
.
Vous pouvez utiliser un FIFO pour que la deuxième étape de pipeline informe la première étape de pipeline lorsque grep
se ferme. Alors la première étape peut tuer tail
. Voici un exemple de code:
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}"
Cette approche présente tous les avantages et les inconvénients de l'approche précédente, à l'exception que ce soit plus compliqué.
POSIX autorise la mise en mémoire tampon complète des flux stdin et stdout, ce qui signifie que la sortie de tail
peut ne pas être traitée par grep
pendant un temps arbitrairement long. Il ne devrait y avoir aucun problème sur les systèmes GNU: GNU grep
utilise read()
, ce qui évite toute mise en mémoire tampon, et GNU tail -f
appelle régulièrement fflush()
lors de l'écriture sur stdout. Les systèmes non-GNU peuvent avoir à faire quelque chose de spécial pour désactiver ou vider régulièrement les tampons.
Permettez-moi de développer la réponse @ 00prometheus (qui est la meilleure).
Peut-être devriez-vous utiliser un délai d'attente au lieu d'attendre indéfiniment.
La fonction bash ci-dessous sera bloquée jusqu'à ce que le terme de recherche donné apparaisse ou qu'un délai d'expiration donné soit atteint.
L'état de sortie sera 0 si la chaîne est trouvée dans le délai imparti.
wait_str() {
local file="$1"; shift
local search_term="$1"; shift
local wait_time="${1:-5m}"; shift # 5 minutes as default timeout
(timeout $wait_time tail -F -n0 "$file" &) | grep -q "$search_term" && return 0
echo "Timeout of $wait_time reached. Unable to find '$search_term' in '$file'"
return 1
}
Peut-être que le fichier journal n'existe pas encore juste après le lancement de votre serveur. Dans ce cas, vous devez attendre son apparition avant de rechercher la chaîne:
wait_server() {
echo "Waiting for server..."
local server_log="$1"; shift
local wait_time="$1"; shift
wait_file "$server_log" 10 || { echo "Server log file missing: '$server_log'"; return 1; }
wait_str "$server_log" "Server Started" "$wait_time"
}
wait_file() {
local file="$1"; shift
local wait_seconds="${1:-10}"; shift # 10 seconds as default timeout
until test $((wait_seconds--)) -eq 0 -o -f "$file" ; do sleep 1; done
((++wait_seconds))
}
Voici comment vous pouvez l'utiliser:
wait_server "/var/log/server.log" 5m && \
echo -e "\n-------------------------- Server READY --------------------------\n"
Donc, après avoir fait quelques tests, j'ai trouvé un moyen rapide, en une ligne, de faire en sorte que cela fonctionne. Il semble que tail -f se ferme quand grep se ferme, mais il y a un piège. Il semble ne se déclencher que si le fichier est ouvert et fermé. Pour ce faire, j'ai ajouté la chaîne vide au fichier lorsque grep a trouvé la correspondance.
tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> logfile \;
Je ne suis pas sûr de savoir pourquoi l'ouverture/la fermeture du fichier amène la queue à se rendre compte que le tuyau est fermé. Je ne compte donc pas sur ce comportement. mais cela semble fonctionner pour le moment.
Raison pour laquelle il ferme, regardez le drapeau -F, par rapport au drapeau -f.
Actuellement, comme indiqué, toutes les solutions tail -f
ici risquent de capturer une ligne "Server Started" précédemment enregistrée (ce qui peut être un problème dans votre cas spécifique, en fonction du nombre de lignes enregistrées et de la rotation du fichier journal/troncature).
Plutôt que de trop compliquer les choses, utilisez simplement une tail
plus intelligente, comme le montre bmike avec un extrait Perl. La solution la plus simple est cette retail
qui intègre le support regex avec start et stop modèles de condition:
retail -f -u "Server Started" server.log > /dev/null
Cela suivra le fichier comme un tail -f
normal jusqu'à ce que la première nouvelle nouvelle instance de cette chaîne apparaisse, puis se ferme. (L'option -u
ne se déclenche pas sur les lignes existantes des 10 dernières lignes du fichier en mode "normal".)
Si vous utilisez GNU tail
(from coreutils ), l'option suivante la plus simple consiste à utiliser --pid
et un FIFO (canal nommé):
mkfifo ${FIFO:=serverlog.fifo.$$}
grep -q -m 1 "Server Started" ${FIFO} &
tail -n 0 -f server.log --pid $! >> ${FIFO}
rm ${FIFO}
Un FIFO est utilisé car les processus doivent être démarrés séparément pour obtenir et transmettre un PID. Un FIFO souffre toujours du même problème de traîner pendant une écriture rapide pour que tail
reçoive un SIGPIPE , utilisez le --pid
Option permettant à tail
de se fermer lorsqu'il constate que grep
est terminé (généralement utilisé pour surveiller le processus d'écriture plutôt que le lecteur , mais que tail
ne le fait pas. soigne vraiment). L'option -n 0
est utilisée avec tail
afin que les anciennes lignes ne déclenchent pas de correspondance.
Enfin, vous pouvez utiliser une queue d'état , qui stockera le décalage de fichier actuel afin que les invocations suivantes ne montrent que les nouvelles lignes (il gère également la rotation de fichier). Cet exemple utilise l'ancien FWTK retail
*:
retail "${LOGFILE:=server.log}" > /dev/null # skip over current content
while true; do
[ "${LOGFILE}" -nt ".${LOGFILE}.off" ] &&
retail "${LOGFILE}" | grep -q "Server Started" && break
sleep 2
done
* Remarque, même nom, programme différent de l'option précédente.
Plutôt que d'avoir une boucle monopolisation d'UC, comparez l'horodatage du fichier avec le fichier d'état (.${LOGFILE}.off
) et mettez en veille. Utilisez "-T
" pour spécifier l'emplacement du fichier d'état, le cas échéant, le répertoire ci-dessus est supposé. N'hésitez pas à ignorer cette condition, ou sous Linux, vous pouvez utiliser la plus efficace inotifywait
à la place:
retail "${LOGFILE:=server.log}" > /dev/null
while true; do
inotifywait -qq "${LOGFILE}" &&
retail "${LOGFILE}" | grep -q "Server Started" && break
done
Ce sera un peu délicat, car vous devrez entrer dans le contrôle et la signalisation du processus. Plus kludgey serait une solution à deux scripts utilisant le suivi PID. Mieux vaut utiliser des pipes nommés comme ceci.
Quel script shell utilisez-vous?
Pour une solution de script rapide et sale, je créerais un script Perl en utilisant Fichier: Tail
use File::Tail;
$file=File::Tail->new(name=>$name, maxinterval=>300, adjustafter=>7);
while (defined($line=$file->read)) {
last if $line =~ /Server started/;
}
Ainsi, plutôt que d'imprimer à l'intérieur de la boucle while, vous pouvez filtrer la correspondance de chaîne et sortir de la boucle while pour laisser votre script se poursuivre.
L'une ou l'autre de ces opérations devrait impliquer un peu d'apprentissage pour mettre en œuvre le contrôle de flux d'observation que vous recherchez.
Je ne peux pas imaginer une solution plus propre que celle-ci:
#!/usr/bin/env bash
# file : untail.sh
# usage: untail.sh logfile.log "Server Started"
(echo $BASHPID; tail -f $1) | while read LINE ; do
if [ -z $TPID ]; then
TPID=$LINE # the first line is used to store the previous subshell PID
else
echo "$LINE"; [[ "$LINE" == *"${*:2}"* ]] && kill -3 $TPID && break
fi
done
ok, peut-être que le nom peut être sujet à des améliorations ...
Avantages:
attendre que le fichier apparaisse
while [ ! -f /path/to/the.file ]
do sleep 2; done
attendre que la chaîne apparaisse dans le fichier
while ! grep "the line you're searching for" /path/to/the.file
do sleep 10; done
Vous n'avez pas nécessairement besoin de queue pour le faire. Je pense que la commande watch est ce que vous recherchez. La commande watch surveille la sortie d'un fichier et peut être terminée avec l'option - g lorsque la sortie est modifiée.
watch -g grep -m 1 "Server Started" logfile.log && Yournextaction
Alex, je pense que celui-ci vous aidera beaucoup.
tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> /dev/null ;
cette commande ne donnera jamais une entrée sur le fichier journal mais grep silencieusement ...
La commande tail
peut être mise en arrière-plan et son pid renvoyé au sous-shell grep
. Dans le sous-shell grep
, un gestionnaire d'interruption sur EXIT peut tuer la commande tail
.
( (sleep 1; exec tail -f logfile.log) & echo $! ; wait ) |
(trap 'kill "$pid"' EXIT; pid="$(head -1)"; grep -m 1 "Server Started")
Voici une solution bien meilleure qui ne vous oblige pas à écrire dans le fichier journal, ce qui est très dangereux, voire impossible dans certains cas.
sh -c 'tail -n +0 -f /tmp/foo | { sed "/EOF/ q" && kill $$ ;}'
Actuellement, il n'a qu'un seul effet secondaire: le processus tail
restera en arrière-plan jusqu'à ce que la ligne suivante soit écrite dans le journal.
Les autres solutions ici ont plusieurs problèmes:
Voici ce que j'ai proposé d'utiliser l'exemple de Tomcat (supprimez les hachages si vous voulez voir le journal pendant son démarrage):
function startTomcat {
loggingProcessStartCommand="${CATALINA_HOME}/bin/startup.sh"
loggingProcessOwner="root"
loggingProcessCommandLinePattern="${Java_HOME}"
logSearchString="org.Apache.catalina.startup.Catalina.start Server startup"
logFile="${CATALINA_BASE}/log/catalina.out"
lineNumber="$(( $(wc -l "${logFile}" | awk '{print $1}') + 1 ))"
${loggingProcessStartCommand}
while [[ -z "$(sed -n "${lineNumber}p" "${logFile}" | grep "${logSearchString}")" ]]; do
[[ -z "$(ps -ef | grep "^${loggingProcessOwner} .* ${loggingProcessCommandLinePattern}" | grep -v grep)" ]] && { echo "[ERROR] Tomcat failed to start"; return 1; }
[[ $(wc -l "${logFile}" | awk '{print $1}') -lt ${lineNumber} ]] && continue
#sed -n "${lineNumber}p" "${logFile}"
let lineNumber++
done
#sed -n "${lineNumber}p" "${logFile}"
echo "[INFO] Tomcat has started"
}
Lisez-les tous. tldr: découpler la fin de tail de grep.
Les deux formes les plus pratiques sont
( tail -f logfile.log & ) | grep -q "Server Started"
et si vous avez bash
grep -m 1 "Server Started" <(tail -f logfile.log)
Mais si cette queue assise à l'arrière-plan vous dérange, il y a une meilleure façon de parler ici ou une autre réponse. Nécessite bash.
coproc grep -m 1 "Server Started"
tail -F /tmp/x --pid $COPROC_PID >&${COPROC[1]}
Ou si ce n'est pas la queue qui produit des choses,
coproc command that outputs
grep -m 1 "Sever Started" ${COPROC[0]}
kill $COPROC_PID
Vous voulez partir dès que la ligne est écrite, mais vous voulez aussi partir après un délai d'attente:
if (timeout 15s tail -F -n0 "stdout.log" &) | grep -q "The string that says the startup is successful" ; then
echo "Application started with success."
else
echo "Startup failed."
tail stderr.log stdout.log
exit 1
fi
Essayez d'utiliser inotify (inotifywait)
Vous configurez inotifywait pour tout changement de fichier, puis vérifiez le fichier avec grep. S'il ne le trouve pas, relancez-le inotifywait. S'il le trouve, quittez la boucle ... Smth like that