Ich hätte gerne ein Bash-Skript, mit dem ich eine Funktion aufrufen kann, um zu prüfen, ob eine Datei Daten enthält. Wenn dies nicht der Fall ist, schlägt das Hauptskript fehl, ungefähr wie das folgende, das der Übersichtlichkeit halber vereinfacht wurde.
Dies funktioniert nicht (das Hauptskript wird nicht beendet, wenn die Untershell fehlschlägt).
Wie kann ich die Funktion so schreiben require_line
, dass ich sagen wir mal über 20 davon in einer Datei haben kann, wie hier
VALUE1=$(require_line "myKey1")
VALUE2=$(require_line "myKey2")
...
und nicht bei jedem ein „wenn“ erforderlich ist?
#!/bin/bash
set -eo pipefail
VALUE=$(require_line "myKey")
require_line(){
local KEY=$1
local DATA=$(cat /tmp/myfile)
local INFO=$(echo "$DATA" | grep "$KEY")
if [ ! -z "$INFO" ]
then
echo "Key not found in $DATA, key: $KEY"
exit 1;
fi
echo "$INFO"
}
Antwort1
Das Beenden der Subshell beendet nicht, wie Sie es erlebt haben, das Hauptskript.
Mir fallen 3 (und eine halbe) Lösungen ein:
- so verwenden
set -e
, dass jeder ("ungetestete") fehlgeschlagene Befehl (oder jede Subshell) das Hauptskript sofort beendet (das kann übertrieben sein oder andere Probleme verursachen), - Senden Sie ein Signal von der Funktion und fangen Sie es mit einem
trap
- verwenden Sie
|| exit $?
nach jedemVALUEn=$(...)
wie folgtVALUE=$(require_line "myKey") || exit $?
, - kombiniere (3.) mit einer (nicht ganz so eleganten) Schleife unter Verwendung von
eval
.
Die dritte erfordert nicht unbedingt „ein if um jedes herum“ und wäre meiner Meinung nach immer noch eine ziemlich kompakte Syntax.
Übrigens, diese Linie
echo "Key not found in $DATA, key: $KEY"
...ist eigentlich nutzlos, wenn Sie das ganze Skript direkt danach beenden, da der Satz in der $VALUEn
Variable gespeichert wird, die nicht angezeigt wird.
Ich schlage vor, stderr
so auszudrucken:
echo "my error" 1>&2
Beispiele
Beispiel für Lösung 1
#!/bin/sh
set -e
myfunc(){
echo $1
if [ "$1" != "OK" ] ; then exit 1 ; fi
}
VALUE1=$(myfunc "OK")
echo $VALUE1
VALUE2=$(myfunc "NO WAY")
echo $VALUE2
echo "main script did not exit"
$ ./test.sh
OK
zsh: exit 1 ./test.sh
Wenn ich es jedoch set -e
vom Anfang entferne, erhalte ich:
$ ./test.sh
OK
NO WAY
main script did not exit
Beispiel für Lösung 2
#!/bin/sh
trap "exit $?" USR1
myfunc(){
echo $1
if [ "$1" != "OK" ] ; then kill -USR1 $$ ; fi
}
VALUE1=$(myfunc "OK")
echo $VALUE1
VALUE2=$(myfunc "NO WAY")
echo $VALUE2
echo "main script did not exit"
$ ./test.sh
OK
Beispiel für Lösung 3
#!/bin/sh
myfunc(){
echo $1
if [ "$1" != "OK" ] ; then exit 1 ; fi
}
VALUE1=$(myfunc "OK") || exit $?
echo $VALUE1
VALUE2=$(myfunc "NO WAY") || exit $?
echo $VALUE2
echo "main script did not exit"
$ ./test.sh
OK
zsh: exit 1 ./test.sh
Beispiel für Lösung 4
#!/bin/sh
myfunc(){
echo $1
if [ "$1" != "OK" ] ; then exit 1 ; fi
}
I=1
for key in "OK" "OK" "NO WAY": ; do
eval "VALUE$I=\$(myfunc \"$key\")" || exit $?
eval "echo \$VALUE$I"
I=$(($I+1))
done
echo "main script did not exit"
$ ./test.sh
OK
OK
zsh: exit 1 ./test.sh
Antwort2
require_line
Um Ihnen zu helfen, strukturieren wir Ihr Skript ein wenig um.
Erstens können wir das nutzlose loswerden cat | grep
. Zweitens können wir
grep
das intrinsische Verhalten von verwenden, um den Erfolg oder Misserfolg der Suche nach anzuzeigen
KEY
und den Schlüssel, falls gefunden, an auszugeben stdout
.
require_line(){
local KEY="$1"
local FILE="/tmp/myfile"
if grep "$KEY" "$FILE"
then
return 0
else
printf 'Key not found in:\n\n"%s"\n\nKey: "%s"\n' "$(cat "$FILE")" "$KEY" >&2
return 1
fi
}
Dies nutzt das integrierte Verhalten von grep
. Wenn der Schlüssel gefunden wird, grep
wird die passende Zeile gedruckt und Erfolg zurückgegeben. Andernfalls else
wird der Zweig genommen und eine Meldung ausgegeben, die angibt, dass der Schlüssel nicht gefunden wurde. Außerdem wird im Fall von grep
fehlgeschlagen die Fehlermeldung an ausgegeben, stderr
damit die Fehlermeldung nicht mit einer gültigen Übereinstimmung verwechselt wird, die in gefunden wurde $FILE
.
Sie können die Funktion außerdem so ändern, require_line
dass als Parameter ein Dateiname akzeptiert wird, $2
indem Sie die Zeile zum Lesen ändern local FILE="$2"
und dann bei jedem Aufruf den gewünschten Dateinamen übergeben require_line
.
Nun, da dies erledigt ist …
Müssen Sie wirklich jeden VALUEn für KEYn speichern oder müssen Sie nur sicherstellen, dass sie alle vorhanden sind?
Da Sie nun eine Funktion haben require_line
, die einen Erfolgs- oder Fehlerwert sauber zurückgibt, können Sie AND
alle Ihre Tests einfach zusammenfassen. Sobald einer davon fehlschlägt, schlägt der gesamte Test fehl.
Vorausgesetzt, Sie benötigen die tatsächlichen Übereinstimmungswerte, wäre die umständliche Vorgehensweise:
if value1=$(require_line "key1") &&
value2=$(require_line "key2") &&
value3=$(require_line "key3")
then
printf "%s\n" "$value1" "$value2" "$value3"
else
printf "One or more keys failed.\n" >&2
fi
Das wird mühsam, wenn Sie viele Schlüssel überprüfen müssen. Die Verwendung eines Arrays kann besser sein:
#!/usr/bin/env bash
require_line(){
local KEY="$1"
local FILE="/tmp/myfile" # or perhaps "$2"
if grep "$KEY" "$FILE"
then
return 0
else
printf 'Key not found in:\n\n"%s"\n\nKey: "%s"\n' "$(cat "$FILE")" "$KEY" >&2
return 1
fi
}
declare keys=("this" "is" "a" "test" "\." "keyN")
N=${#keys[@]}
declare values=()
j=0
while [ $j -lt $N ] && values[$j]="$(require_line "${keys[j]}")"
do
j=$(($j+1))
done
if [ $j -lt $N ]
then
printf 'error: found only %d keys out of %d:\n' $j $N
printf ' "%s"\n' "${values[@]}"
fi
Ausführen dieses Codes mit einigen Beispieldaten:
$ cat /tmp/myfile
this is a test.
$ ./test.sh
Key not found in:
"this is a test."
Key: "keyN"
error: found only 5 keys out of 6:
"this is a test."
"this is a test."
"this is a test."
"this is a test."
"this is a test."
""
Wenn Sie schließlich wirklich nur überprüfen müssen, ob alle Schlüssel vorhanden sind, ohne die Übereinstimmungswerte kennen zu müssen, könnte der arrayorientierte Code oben so vereinfacht werden, dass er einfach eine Schleife ausführt, bis alle Schlüssel gefunden sind, oder beim ersten fehlenden Schlüssel abbricht.