Wie schreibe ich eine Wiederholungslogik in ein Skript, damit es bis zu fünfmal wiederholt wird?

112

Ich möchte eine Logik in ein Shell-Skript schreiben, das versucht, es nach 15 Sekunden bis zu 5 Mal erneut auszuführen, basierend auf "status code = FAIL", wenn es aufgrund eines Problems fehlschlägt.

Sandeep Singh
quelle

Antworten:

90

Dieses Skript verwendet einen Zähler n, um die Versuche, den Befehl auszuführen, auf fünf zu beschränken. Wenn der Befehl erfolgreich ist, $?wird Null gehalten und die Ausführung wird von der Schleife abgebrochen.

n=0
until [ $n -ge 5 ]
do
   command && break  # substitute your command here
   n=$[$n+1]
   sleep 15
done
Verdächtiger
quelle
1
Sie sollten hinzufügen, breakwenn der Befehl erfolgreich ist, dann wird er die Schleife
unterbrechen
Eigentlich ist das die richtige Art zu schreiben, if command; then break; fioder genauer command && break
gesagt
1
"Befehl" ist nur der Name des Befehls, dessen Status Sie überprüfen möchten.
Verdächtiger
3
Beachten Sie, dass Sie testen können, ob n am Ende gleich fünf ist, um festzustellen, ob der Befehl erfolgreich war oder nicht.
Mattdm
4
Gute Lösung - aber im Falle von nAusfällen wird vor dem Beenden unnötigerweise ein weiteres Mal geschlafen.
Ron Rothman
126
for i in 1 2 3 4 5; do command && break || sleep 15; done

Ersetzen Sie "command" durch Ihren Befehl. Dies setzt voraus, dass "Statuscode = FAIL" einen beliebigen Rückkehrcode ungleich Null bedeutet.


Variationen:

Verwenden der {..}Syntax. Funktioniert in den meisten Shells, aber nicht in BusyBox sh:

for i in {1..5}; do command && break || sleep 15; done

Verwendung seqund Weitergabe entlang der Beendigungscode des ausgefallenen Befehl:

for i in $(seq 1 5); do command && s=0 && break || s=$? && sleep 15; done; (exit $s)

Wie oben, aber sleep 15nach dem endgültigen Fehlschlag überspringen . Da es besser ist, die maximale Anzahl von Schleifen nur einmal zu definieren, wird dies erreicht, indem am Anfang der Schleife geschlafen wird, wenn i > 1:

for i in $(seq 1 5); do [ $i -gt 1 ] && sleep 15; command && s=0 && break || s=$?; done; (exit $s)
Alexander
quelle
25
+1 - prägnant und klar. Ein Vorschlag: Ich würde ersetzen for i in 1 2 3 4 5mit , for i in {1..5}weil es leichter zu pflegen.
Paddy Landau
5
Nur ein Hinweis, das funktioniert , weil das &&vor dem ausgewertet wird ||wegen Operatorpräzedenz
gene_wood
6
Ein weiterer Hinweis, dies wird Code 0 zurückgeben, auch wenn dies commandfehlschlägt.
Henrique Zambon
3
@HenriqueZambon Es wurde eine Version hinzugefügt, die sich auch darum kümmert.
Alexander
2
Schläft das nicht nach dem endgültigen Scheitern? Scheint, als würde man unnötig 15 Sekunden warten. Ich denke, Sie können [[ i -eq 5]]vor dem Schlafengehen eine OP- Prüfung durchführen , um dies zu vermeiden.
Dave Lugg
32
function fail {
  echo $1 >&2
  exit 1
}

function retry {
  local n=1
  local max=5
  local delay=15
  while true; do
    "$@" && break || {
      if [[ $n -lt $max ]]; then
        ((n++))
        echo "Command failed. Attempt $n/$max:"
        sleep $delay;
      else
        fail "The command has failed after $n attempts."
      fi
    }
  done
}

Beispiel:

retry ping invalidserver

erzeugt diese Ausgabe:

ping: unknown host invalidserver
Command failed. Attempt 2/5:
ping: unknown host invalidserver
Command failed. Attempt 3/5:
ping: unknown host invalidserver
Command failed. Attempt 4/5:
ping: unknown host invalidserver
Command failed. Attempt 5/5:
ping: unknown host invalidserver
The command 'ping invalidserver' failed after 5 attempts

Ein reales Beispiel für die Arbeit mit komplexen Befehlen finden Sie in diesem Skript .

Fernando Correia
quelle
3
Dies ist eine großartige Lösung. Ich mag, dass es mit einem Exit-Status ungleich Null beendet wird, nachdem etwas auch mehrmals fehlgeschlagen ist.
Ben Liyanage
11

Hier ist die Funktion für einen erneuten Versuch

function retry()
{
        local n=0
        local try=$1
        local cmd="${@: 2}"
        [[ $# -le 1 ]] && {
        echo "Usage $0 <retry_number> <Command>"; }

        until [[ $n -ge $try ]]
        do
                $cmd && break || {
                        echo "Command Fail.."
                        ((n++))
                        echo "retry $n ::"
                        sleep 1;
                        }

        done
}

retry $*

Ausgabe :

[test@Nagios ~]$ ./retry.sh 3 ping -c1 localhost
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.207 ms

--- localhost ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.207/0.207/0.207/0.000 ms

[test@Nagios ~]$ ./retry.sh 3 ping -c1 localhostlasjflasd
ping: unknown host localhostlasjflasd
Command Fail..
retry 1 ::
ping: unknown host localhostlasjflasd
Command Fail..
retry 2 ::
ping: unknown host localhostlasjflasd
Command Fail..
retry 3 ::
Rahul Patil
quelle
Ich habe Ihren Code in eine neue Datei namens retry.sh eingefügt und oben eine Zeile #! / Bin / bash hinzugefügt. Während ich mit den angegebenen Befehlen in der Erklärung arbeite, sehe ich nichts, nur dass die Eingabeaufforderung erneut erscheint.
Java_enthu
Haben Sie es versuchtbash retry.sh 3 ping -c1 localhost
Rahul Patil
Ja Rahul, ich habe es versucht.
Java_enthu
Sorry, ich war bizy .., ich habe noch einmal getestet, es funktioniert, überprüfen Sie die Ausgabe paste.ubuntu.com/6002711
Rahul Patil
Dies ist die eleganteste Antwort hier bisher - wenn Sie etwas Nicht-Triviales tun. Danke, dass Sie sich die Zeit genommen haben.
Jerry Andrews
10

GNU Parallel hat --retries:

parallel --retries 5 --delay 15s ::: ./do_thing.sh
Ole Tange
quelle
5

Hier ist mein Lieblings-Alias ​​/ Skript für eine Zeile

    alias retry='while [ $? -ne 0 ] ; do fc -s ; done'

Dann kannst du Sachen machen wie:

     $ ps -ef | grep "Next Process"
     $ retry

und es wird den vorherigen Befehl so lange ausführen, bis es "Nächster Prozess" findet

Jeff
quelle
1
Verwenden Sie in zsh fc -e "#"anstelle von fc -s.
Ricardo Stuven
2

Ich verwende dieses Skript, das die Wiederholungen eines bestimmten Befehls durchführt. Der Vorteil dieses Skripts besteht darin, dass der Exit-Code erhalten bleibt, wenn alle Wiederholungen fehlschlagen.

#!/usr/bin/env bash

if [ $# -ne 3 ]; then
    echo 'usage: retry <num retries> <wait retry secs> "<command>"'
    exit 1
fi

retries=$1
wait_retry=$2
command=$3

for i in `seq 1 $retries`; do
    echo "$command"
    $command
    ret_value=$?
    [ $ret_value -eq 0 ] && break
    echo "> failed with $ret_value, waiting to retry..."
    sleep $wait_retry
done

exit $ret_value

Wahrscheinlich kann es einfacher werden

Padilo
quelle
Mir gefällt, wie flexibel diese Version ist und wie ausführlich und lesbar der Code ist!
yo.ian.g
Um dem fehlgeschlagenen Echo zu entsprechen, können Sie sogar ein erfolgreiches Echo mit dem Wert [$ ret_value -eq 0] hinzufügen oder anschließend den Wert $ ret_value testen
yo.ian.g
Diese Version hat den Vorteil, dass sie nach dem letzten Fehlschlagen des Befehls nicht in den Ruhezustand wechselt.
Alexander
1

Siehe unten Beispiel:

n=0
while :
do
        nc -vzw1 localhost 3859
        [[ $? = 0 ]] && break || ((n++))
        (( n >= 5 )) && break

done

Ich versuche, Port 3389 auf localhost zu verbinden. Der Versuch wird wiederholt, bis fünfmal ein Fehler auftritt. Bei Erfolg wird die Schleife unterbrochen.

$? Der Status des Befehls ist vorhanden, wenn Null bedeutet, dass der Befehl erfolgreich ausgeführt wurde. Wenn nicht Null, bedeutet dies, dass der Befehl fai ausgeführt wurde

Scheint ein bisschen kompliziert, kann jemand es besser machen.

Rahul Patil
quelle
Vielen Dank rahul .. wird es immer wieder versuchen, das Skript auszuführen?
Sandeep Singh
Bitte überprüfen Sie jetzt, ich habe aktualisiert
Rahul Patil
$?es ist Status des Befehls gegeben , wenn es Null bedeutet , erfolgreich Befehl ausführen, wenn andere als Null bedeutet Befehl fehl
Rahul Patil
Ist es erforderlich, Host- und Port-Adresse anzugeben? Können wir das tun, indem wir nur das Skriptverzeichnis angeben?
Sandeep Singh
durch einen Befehl ersetzen, der den Exit-Statuscode $?
Rahul Patil
1

Sie können den hierloop verfügbaren Befehl folgendermaßen verwenden :

$ loop './do_thing.sh' --every 15s --until-success --num 5 

Das macht dein Ding alle 15 Sekunden, bis es erfolgreich ist, maximal fünf Mal.

Rich Jones
quelle
0

Hier ist eine rekursive retryFunktion für Puristen der funktionalen Programmierung:

retry() {
  cmd=$1
  try=${2:-15}       # 15 by default
  sleep_time=${3:-3} # 3 seconds by default

  # Show help if a command to retry is not specified.
  [ -z "$1" ] && echo 'Usage: retry cmd [try=15 sleep_time=3]' && return 1

  # The unsuccessful recursion termination condition (if no retries left)
  [ $try -lt 1 ] && echo 'All retries failed.' && return 1

  # The successful recursion termination condition (if the function succeeded)
  $cmd && return 0

  echo "Execution of '$cmd' failed."

  # Inform that all is not lost if at least one more retry is available.
  # $attempts include current try, so tries left is $attempts-1.
  if [ $((try-1)) -gt 0 ]; then
    echo "There are still $((try-1)) retrie(s) left."
    echo "Waiting for $sleep_time seconds..." && sleep $sleep_time
  fi

  # Recurse
  retry $cmd $((try-1)) $sleep_time
}

Übergeben Sie einen Befehl (oder einen Funktionsnamen) und optional eine Anzahl von Wiederholungen und eine Ruhezeit zwischen den Wiederholungen, wie folgt:

retry some_command_or_fn 5 15 # 5 tries, sleep 15 seconds between each
Mikhail Vasin
quelle
Dies funktioniert nicht für Befehle, die länger als ein Wort sind: cmd = "echo blah blah" ... Zeile 10: [: blah: Integer-Ausdruck erwartet ... Funktioniert auch nicht für Pipes usw.
Mercury00