staging.inyokaproject.org

SIGERR vollständiger Ersatz für set -e ?

Status: Gelöst | Ubuntu-Version: Server 20.04 (Focal Fossa)
Antworten |

BillMaier Team-Icon

Supporter

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

Hello,

meine Skripte starte ich in der Regel mit

1
set -e

Jetzt habe ich ein Skript, von dem muss ich mitbekommen, wenn etwas schief läuft. Das Skript wird auch "blind" gestartet, bekommt seinen Trigger also nicht direkt per user-Eingabe.

Im Fehlerfall soll ein curl-Aufruf gestartet werden, der einen alert triggert. (Mir ist klar, dass auch das eine Abhängigkeit (netzwerktechnisch) benötigt, die das Skript nicht abfangen kann. Irgendwas ist immer ☺ )

Ich habe nun folgendes vorgesehen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash

set -x

_error(){
  curl https://alert.example.org \
    --form-string "$1"
}

trap '_error SIGERR' ERR
trap '_error SIGINT' INT
trap '_error SIGTERM' TERM

foo1(){
...
}

foo1(){
...
}

bar(){
...
}

source helper.sh

if foo1 ; then foo2 ; fi

bar

echo bla

Nach meinem Test zufolge darf der Parameter set -e nicht gesetzt sein, da sonst meine error-Funktion nicht mehr ausgeführt wird.

Kann ich nun davon ausgehen, dass damit alle Fehler/Unterbrechungen den curl triggern (abgesehen von sigkill)? Oder hab ich was übersehen?

Danke und Gruß

BillMaier

EDIT: Ich habe in meinem Skript oben

1
2
set -u
set -o pipefail

entfernt, da auch das anscheinend dazu führt, dass das error-handling nicht wie gewünscht funktioniert.

Bearbeitet von rklm:

Syntaxhighlighting

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

Du mischt hier zwei Dinge

  1. Rückgabewerte von Prozessen

  2. Signale

Ich habe Dich so verstanden, dass Du den curl-Request absetzen willst, wenn irgendwo ein Fehler aufgetreten ist. Aber Dein Code reagiert auf Signale. Von daher weiß ich nicht so recht, was Du nun erreichen willst.

Die Behandlung beliebiger Fehler könnte man so machen:

1
2
3
4
5
6
7
(
set -e

foo
bla
baz
) || curl https://alert.example.org -form-string "exit code"

Man könnte es auch so angehen:

1
2
3
4
5
6
7
8
9
unset complete
trap '[ -z "$complete" ] && curl https://alert.example.org -form-string "irgendwas"' 0
set -e

foo
bla
baz

complete=yes

Du müsstest wohl mal definieren, was Du mit "wenn etwas schief läuft" genau meinst. Dann kann man eine passende Lösung bauen.

BillMaier Team-Icon

Supporter
(Themenstarter)

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

Danke für deine Antwort.

rklm schrieb:

Du mischt hier zwei Dinge

  1. Rückgabewerte von Prozessen

  2. Signale

Das habe ich bis gestern Mittag nicht getan, bis ich über das Signal ERR gestolpert bin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash

set -o pipefail
set -u

all(){
false
}

_error(){
echo "error"
}

trap '_error' ERR

all

Das macht anscheinend das, was ich erwarte und brauche. Wenn es hier nicht um Rückgabewerte geht: wo ist der Unterschied für die Praxis?

Du müsstest wohl mal definieren, was Du mit "wenn etwas schief läuft" genau meinst. Dann kann man eine passende Lösung bauen.

Ich probier es mal:

Punkt 1:

Das Skript erhält json-Daten via STDIN (ist so vorgegeben) und verarbeitet sie zu alerts. Dazu habe ich ein helper-Skript gebaut, das diese Daten mit jq aufbereitet und dann entsprechend alerts generiert.

Was ich vermeiden will: Dass in diesem Prozess etwas schief geht und ich es nicht mit bekomme. Beispiel: Die Struktur der json-Daten ändert sich und das parsen funktioniert nicht mehr wie gewohnt. Dann würde ich gar keinen alert bekommen. Solche Fehler will ich abfangen und dann einen "allgemeinen" alert schicken a'la "pass auf, hier ist was kaputt, aber da das Skript nicht wie erwartet durchläuft kann ich dir gerade nicht sagen was genau das ist".

Ich habe auch schon überlegt, das über ein wrapper-Skript zu machen und das darin aufgerufene Skript mit -e zu starten. Dann könnte ich dessen Exitcode abfangen.

Punkt 2:

Dadurch, dass ich auf die Signale gestoßen bin, fiel mir ein dass ich das auch abfangen sollte. Das Skript läuft in einem Container - und sollte der beendet werden, solange das Skript läuft, hätte ich ebenfalls gerne eine Info. Deshalb das 'TERM', damit habe ich schon etwas Erfahrung und das funktioniert in anderen Kontexten wie gewünscht. Zurück zu oben: Da ein ERR zu ergänzen wäre halt super simpel ☺

EDIT: nochmal geändert

BillMaier Team-Icon

Supporter
(Themenstarter)

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

rklm schrieb:

Die Behandlung beliebiger Fehler könnte man so machen:

1
2
3
4
5
6
7
(
set -e

foo
bla
baz
) || curl https://alert.example.org -form-string "exit code"

Ja, das ist gut. Ich hatte das statt in eine Subshell in eine Funktion verpackt, da funktioniert das mit dem set -e aber nicht, wie ich erwartet hatte.

1
2
3
4
5
6
7
8
9
unset complete
trap '[ -z "$complete" ] && curl https://alert.example.org -form-string "irgendwas"' 0
set -e

foo
bla
baz

complete=yes

Die trap-Zeile hier habe ich leider nicht verstanden. Kannst du mir bitte helfen?

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

BillMaier schrieb:

1
trap '[ -z "$complete" ] && curl https://alert.example.org -form-string "irgendwas"' 0

Die trap-Zeile hier habe ich leider nicht verstanden. Kannst du mir bitte helfen?

Die schaut beim Beenden des Skripts (statt 0 könnte man auch EXIT schreiben) nach, ob die Variable complete leer ist (was der Fall wäre, wenn das Skript nicht das Ende erreicht hat ohne wegen set -e abzubrechen) - und wenn das der Fall ist, macht sie den curl-Request.

Muss das ein Shell-Skript sein? Um JSON-Daten bequem mit Fehlern bei Abweichung vom Schema zu parsen würde ich eher an so etwas wie pydantic denken - da hat man explizit die Möglichkeit Felder zu validieren und zu transformieren, zu kennzeichnen, ob sie Optional sind, ob umwandelbare Typen (z.B. string zu number erlaubt sind) usw. - und dank Dataclasses kann man es sich ersparen unnötigen Code dafür zu schreiben.

Mal als Beispiel für https://developers.themoviedb.org/3/tv/get-tv-credits, wenn man die Beispiel-Daten (vgl. Response, Reiter Examples) auf stdin an das Programm weiterreichen können wollte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from pydantic import BaseModel
from typing import List, Optional
import requests

class Cast(BaseModel):
    adult: bool
    gender: Optional[int]
    id: int
    known_for_department: str
    name: str
    original_name: str
    popularity: float
    profile_path: Optional[str]
    character: str
    credit_id: str
    order: int


class Crew(BaseModel):
    adult: bool
    gender: Optional[int]
    id: int
    known_for_department: str
    name: str
    original_name: str
    popularity: float
    profile_path: Optional[str]
    department: str
    credit_id: str
    job: str


class Credits(BaseModel):
    cast: List[Cast]
    crew: List[Crew]

def send_msg(msg):
    requests.post("https://alert.example.org", data = {"msg": msg)

if __name__ == '__main__':
    import json
    import sys
    import signal

    def handler(signum, frame):
        send_msg(f"got {signal.Signals(signum).name} signal, exiting.")
        raise SystemExit

    for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGPIPE):
        signal.signal(signum, handler)

    try:
        data = json.load(sys.stdin)
    except json.JSONDecodeError as err:
        send_msg(f"Error reading json on line {err.lineno}, position {err.colno}: {err.msg}")
    except Exception as err:
        send_msg(f"unexpected error: {err}")
    else:
        try:
            credits = Credits(**data)
        except pydantic.ValidationError as err:
            send_msg(f"Error validating data: {err}")
        except Exception as err:
            send_msg(f"unexpected error while validating data: {err}")
        else:
            if credits.cast:
                print(credits.cast[0])

BillMaier Team-Icon

Supporter
(Themenstarter)

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

seahawk1986 schrieb:

Die schaut beim Beenden des Skripts (statt 0 könnte man auch EXIT schreiben) nach, ob die Variable complete leer ist (was der Fall wäre, wenn das Skript nicht das Ende erreicht hat ohne wegen set -e abzubrechen) - und wenn das der Fall ist, macht sie den curl-Request.

Ah, die 0 konnte ich gerade nicht zuordnen. Stimmt, hatte ich schon mal gelesen. Ja, danke, jetzt ist es klar.

Muss das ein Shell-Skript sein?

Aktuell ja, weil ich eine Bash und jq in dem Container habe, Python nicht. Das Fass will ich im Moment nicht aufmachen. Aber danke für den Tipp - ich brauche das immer wieder und werde bestimmt beim nächsten Anwendungsfall einen Blick drauf werfen.

Wenn mir jemand das Prinzip des ERR Signals noch erklären könnte... ☺

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11278

BillMaier schrieb:

Wenn mir jemand das Prinzip des ERR Signals noch erklären könnte... ☺

Das ist eine Bash-Besonderheit, da wird ein zusätzlicher Signal-Handler definiert, der aber nur getriggert wird wenn die Shell selbst (aber nicht eine Subshell wie z.B. eine Funktion) intern einen Fehler wirft. In der Hinsicht ersetzt ERR die Funktion von set -e nicht 1:1, man muss bei der Verwendung von ERR in jeder Subshell extra auf Fehler prüfen - vgl. z.B.: https://medium.com/@dirk.avery/the-bash-trap-trap-ce6083f36700

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

Danke für den Hinweis mit trap ... ERR! Das kannte ich auch noch nicht. Die Lösung mit trap ... ERR ist auf jeden Fall viel eleganter als meine mit der Variable "complete".

seahawk1986 schrieb:

BillMaier schrieb:

Wenn mir jemand das Prinzip des ERR Signals noch erklären könnte... ☺

Das ist eine Bash-Besonderheit, da wird ein zusätzlicher Signal-Handler definiert, der aber nur getriggert wird wenn die Shell selbst (aber nicht eine Subshell wie z.B. eine Funktion) intern einen Fehler wirft. In der Hinsicht ersetzt ERR die Funktion von set -e nicht 1:1,

Genau genommen ist weder set -e ein Ersatz für trap ... ERR noch umgekehrt. set -e beeinflusst den Kontrollfluss (führt zum sofortigen Abbruch der aktuellen Shell) und trap ... ERR ist ein Hook, der immer aufgerufen wird, wenn ein Prozess mit einen Rückgabewert ungleich 0 endet, der nicht in einer Bedingung (also if, while, until, && etc.) ausgewertet wird. Also, auf Deutsch, er wird für jeden unbehandelten Fehler ausgeführt.

man muss bei der Verwendung von ERR in jeder Subshell extra auf Fehler prüfen - vgl. z.B.: https://medium.com/@dirk.avery/the-bash-trap-trap-ce6083f36700

Wenn man in der Top-Level-Shell set -e benutzt, dann sprudelt der Fehler auch aus Subshells nach oben und dort wird dann der Trap ausgeführt, wenn er gesetzt ist. Man kann übrigens mit set -E auch das trap ... ERR vererben, wie die Manpage der bash weiß.

BillMaier Team-Icon

Supporter
(Themenstarter)

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

hui, vielen Dank, das klingt ja vielversprechend.

Dann werde ich mich da mal durchwühlen. Ich denke, dass ich meine fertige Struktur hier nochmal posten werde, wir können sicher noch weiter voneinander lernen ☺

Ich markiere mal als gelöst.

Gruß BillMaier

BillMaier Team-Icon

Supporter
(Themenstarter)

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

Mir scheint, die drei Dinge sollten tatsächlich kombiniert werden:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

set -eE

stuff(){
  echo stuff
  false
}

trap 'echo error' ERR

true
stuff

echo 'soll nicht ausgegeben werden.'

macht genau, was es soll. Dagegen ergibt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

set -E

stuff(){
  echo stuff
  false
}

trap 'echo error' ERR

true
stuff

echo 'soll nicht ausgegeben werden.'

das hier:

stuff
error
error
soll nicht ausgegeben werden.

Hier wird das error-Signal also zweimal ausgewertet. (Könnte für das Erstellen von Skripten ganz hilfreich sein.)

Eine Alternative wäre, den trap-Befehl mit einem exit 1 abzuschließen.

Jetzt wäre noch interessant, ob und wie sich das ganze mit set -u und set -o pipefail kombinieren lässt, das ist mir nämlich noch nicht klar.

BillMaier Team-Icon

Supporter
(Themenstarter)

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

seahawk1986 schrieb:

https://medium.com/@dirk.avery/the-bash-trap-trap-ce6083f36700

Der Artikel ist mega. In der Tat lassen sich damit dann auch die set -u Fehler abfangen. Genial, sowas hab ich schon lange vermisst.

Bei mir sieht das jetzt so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

#!/bin/bash

set -Ee
set -o pipefail
set -u

stuff(){
  echo $UNBOUND_VARIABLE
  echo "end of stuff, should not be displayed"
  return 4
}


_exit(){
  echo "catching!"
  if [[ $1 -ne 0 ]]; then
    # error handling goes here
    echo "Error $1 occurred"
  fi
}

trap '_exit $?' EXIT

stuff

echo "end of script, should not be displayed"

Weitere Ideen/Optimierungen gerne gesehen!

EDIT: https://stackoverflow.com/questions/6109225/echoing-the-last-command-run-in-bash könnte das noch pimpen, indem man den letzten Befehl ausgibt. Auf die Schnelle bekomm ich das hier aber nicht integriert.

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

BillMaier schrieb: [...]

Wir kennen ja nun das richtige Skript nicht, aber mir sieht das zu kompliziert aus. Ich würde das einfach halten:

1
2
3
4
set -eo pipefail
trap 'echo "Error $? occurred in $0:$LINENO"' ERR

irgendwas

BillMaier Team-Icon

Supporter
(Themenstarter)

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

Naja, einiges ist ja als dummy im Skript. Verglichen mit deinem bleibt ja nur das hier übrig:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
set -Eeuo pipefail

_exit(){
  if [[ $1 -ne 0 ]]; then
    echo "Error $1 occurred"
  fi
}

trap '_exit $?' EXIT

irgendwas

Der Vorteil ist das Abfangen von undefinierten Variablen. Ob pipefail mit ERR funktioniert habe ich gar nicht mehr getestet.

rklm Team-Icon

Projektleitung

Anmeldungsdatum:
16. Oktober 2011

Beiträge: 13242

BillMaier schrieb:

Naja, einiges ist ja als dummy im Skript. Verglichen mit deinem bleibt ja nur das hier übrig:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
set -Eeuo pipefail

_exit(){
  if [[ $1 -ne 0 ]]; then
    echo "Error $1 occurred"
  fi
}

trap '_exit $?' EXIT

irgendwas

Warum machst Du das? Du willst doch nur Fehler berichten. Da ist der trap auf ERR zielgerichteter. Und set -E führt dann zusammen mit set -e dazu, dass Fehler in Funktionen und Subshells doppelt berichtet werden. Für Deine Zwecke reicht es doch, wenn sie nur einmal auf der obersten Ebene berichtet werden, denn set -e stellt ja sicher, dass unbehandelte Fehler auf jeden Fall zum Abbruch führen.

Der Vorteil ist das Abfangen von undefinierten Variablen.

Ich habe das noch nie gebraucht. Im Gegenteil: ich benutze oft das Muster, dass ich eine Variable mit unset lösche und nur unter bestimmten Bedingungen setze. Wenn ich dann die Variable auf Leerheit teste, schlägt gleich der Fehler an, aber das will ich nichtn denn es ist eine völlig legale Situation.

Ob pipefail mit ERR funktioniert habe ich gar nicht mehr getestet.

Davon ist auszugehen. Testen schadet natürlich nie.

BillMaier Team-Icon

Supporter
(Themenstarter)

Anmeldungsdatum:
4. Dezember 2008

Beiträge: 6497

rklm schrieb:

Warum machst Du das?

Weil ich die verschiedenen Fehlerfälle angefangen habe zu testen und schon beim zweiten (nämlich der undefinierten Variable) blieb das ERR Signal aus. Dann habe ich mich mit dem Link von seahawk1986 befasst und festgestellt, dass ich damit alle mir bekannten Fehler abfangen kann. Entspricht ja ungefähr der Idee

BillMaier schrieb:

Ich habe auch schon überlegt, das über ein wrapper-Skript zu machen und das darin aufgerufene Skript mit -e zu starten. Dann könnte ich dessen Exitcode abfangen.

rklm schrieb:

Der Vorteil ist das Abfangen von undefinierten Variablen.

Ich habe das noch nie gebraucht. Im Gegenteil: ich benutze oft das Muster, dass ich eine Variable mit unset lösche und nur unter bestimmten Bedingungen setze. Wenn ich dann die Variable auf Leerheit teste, schlägt gleich der Fehler an, aber das will ich nichtn denn es ist eine völlig legale Situation.

Wenn du den Test mit if oder && oder sowas machst, wird das mit set -u nicht abgebrochen. Aber egal. (stimmt so nicht)

Wir verwenden Variablen wohl sehr unterschiedlich. Mir passiert es oft, dass ich eine Variable benötige, die ich aber zunächst nur unter einer if oder else Bedingung gesetzt hatte. Das Testen auf unset zeigt mir den Fehler sofort an. Ich arbeite auch oft mit den bool-Variablen true und false, das macht die Abfrage via if sehr leicht (und einigermaßen gut lesbar) und ich kann das als env-Variablen in Docker/Kubernetes prima setzen und v.a. ändern. true und false ist von "außen" betrachtet (z.B. beim Konfigurieren eines Containers) immer explizit, ich muss dazu also nicht mal ins Skript schauen.

Ich bin auch kein Entwickler, vielleicht ist das eher die Admin-Denke? Ich weiß es nicht.

Ob pipefail mit ERR funktioniert habe ich gar nicht mehr getestet.

Davon ist auszugehen. Testen schadet natürlich nie.

Ich bin auch bei set -u davon ausgegangen und war dann sehr überrascht 😊 Aber pipefail wird abgefangen.

Edit: Korrigiert.

Antworten |