Antworten:
Ich habe ziemlich komplexe Shell-Skripte geschrieben und mein erster Vorschlag ist "nicht". Der Grund ist, dass es ziemlich einfach ist, einen kleinen Fehler zu machen, der Ihr Skript behindert oder es sogar gefährlich macht.
Trotzdem habe ich keine anderen Ressourcen, um Sie weiterzugeben, als meine persönliche Erfahrung. Hier ist, was ich normalerweise mache, was übertrieben ist, aber dazu neigt, solide zu sein, obwohl es sehr ausführlich ist.
Aufruf
Lassen Sie Ihr Skript lange und kurze Optionen akzeptieren. Seien Sie vorsichtig, da es zwei Befehle zum Parsen von Optionen gibt: getopt und getopts. Verwenden Sie getopt, da Sie weniger Probleme haben.
CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""
getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"`
if test $? != 0
then
echo "unrecognized option"
exit 1
fi
eval set -- "$getopt_results"
while true
do
case "$1" in
--config_file)
CommandLineOptions__config_file="$2";
shift 2;
;;
--debug_level)
CommandLineOptions__debug_level="$2";
shift 2;
;;
--)
shift
break
;;
*)
echo "$0: unparseable option $1"
EXCEPTION=$Main__ParameterException
EXCEPTION_MSG="unparseable option $1"
exit 1
;;
esac
done
if test "x$CommandLineOptions__config_file" == "x"
then
echo "$0: missing config_file parameter"
EXCEPTION=$Main__ParameterException
EXCEPTION_MSG="missing config_file parameter"
exit 1
fi
Ein weiterer wichtiger Punkt ist, dass ein Programm immer Null zurückgeben sollte, wenn es erfolgreich abgeschlossen wurde, ungleich Null, wenn etwas schief gelaufen ist.
Funktionsaufrufe
Sie können Funktionen in bash aufrufen. Denken Sie daran, sie vor dem Aufruf zu definieren. Funktionen sind wie Skripte, sie können nur numerische Werte zurückgeben. Dies bedeutet, dass Sie eine andere Strategie erfinden müssen, um Zeichenfolgenwerte zurückzugeben. Meine Strategie besteht darin, eine Variable namens RESULT zu verwenden, um das Ergebnis zu speichern, und 0 zurückzugeben, wenn die Funktion sauber abgeschlossen wurde. Sie können auch Ausnahmen auslösen, wenn Sie einen anderen Wert als Null zurückgeben, und dann zwei "Ausnahmevariablen" festlegen (meine: EXCEPTION und EXCEPTION_MSG), wobei die erste den Ausnahmetyp und die zweite eine von Menschen lesbare Nachricht enthält.
Wenn Sie eine Funktion aufrufen, werden die Parameter der Funktion den speziellen Variablen $ 0, $ 1 usw. zugewiesen. Ich empfehle Ihnen, sie in aussagekräftigere Namen zu setzen. Deklarieren Sie die Variablen innerhalb der Funktion als lokal:
function foo {
local bar="$0"
}
Fehleranfällige Situationen
In bash wird, sofern Sie nichts anderes deklarieren, eine nicht gesetzte Variable als leere Zeichenfolge verwendet. Dies ist im Falle eines Tippfehlers sehr gefährlich, da die schlecht typisierte Variable nicht gemeldet wird und als leer ausgewertet wird. verwenden
set -o nounset
um dies zu verhindern. Seien Sie jedoch vorsichtig, denn wenn Sie dies tun, wird das Programm jedes Mal abgebrochen, wenn Sie eine undefinierte Variable auswerten. Aus diesem Grund können Sie nur dann überprüfen, ob eine Variable nicht definiert ist:
if test "x${foo:-notset}" == "xnotset"
then
echo "foo not set"
fi
Sie können Variablen als schreibgeschützt deklarieren:
readonly readonly_var="foo"
Modularisierung
Sie können eine "Python-ähnliche" Modularisierung erreichen, wenn Sie den folgenden Code verwenden:
set -o nounset
function getScriptAbsoluteDir {
# @description used to get the script path
# @param $1 the script $0 parameter
local script_invoke_path="$1"
local cwd=`pwd`
# absolute path ? if so, the first character is a /
if test "x${script_invoke_path:0:1}" = 'x/'
then
RESULT=`dirname "$script_invoke_path"`
else
RESULT=`dirname "$cwd/$script_invoke_path"`
fi
}
script_invoke_path="$0"
script_name=`basename "$0"`
getScriptAbsoluteDir "$script_invoke_path"
script_absolute_dir=$RESULT
function import() {
# @description importer routine to get external functionality.
# @description the first location searched is the script directory.
# @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable
# @param $1 the .shinc file to import, without .shinc extension
module=$1
if test "x$module" == "x"
then
echo "$script_name : Unable to import unspecified module. Dying."
exit 1
fi
if test "x${script_absolute_dir:-notset}" == "xnotset"
then
echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying."
exit 1
fi
if test "x$script_absolute_dir" == "x"
then
echo "$script_name : empty script path. Dying."
exit 1
fi
if test -e "$script_absolute_dir/$module.shinc"
then
# import from script directory
. "$script_absolute_dir/$module.shinc"
elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset"
then
# import from the shell script library path
# save the separator and use the ':' instead
local saved_IFS="$IFS"
IFS=':'
for path in $SHELL_LIBRARY_PATH
do
if test -e "$path/$module.shinc"
then
. "$path/$module.shinc"
return
fi
done
# restore the standard separator
IFS="$saved_IFS"
fi
echo "$script_name : Unable to find module $module."
exit 1
}
Sie können dann Dateien mit der Erweiterung .shinc mit der folgenden Syntax importieren
"AModule / ModuleFile" importieren
Welches wird in SHELL_LIBRARY_PATH gesucht. Denken Sie beim Importieren in den globalen Namespace daran, allen Funktionen und Variablen ein korrektes Präfix voranzustellen, da sonst die Gefahr von Namenskonflikten besteht. Ich benutze doppelten Unterstrich als Python-Punkt.
Fügen Sie dies auch als erstes in Ihr Modul ein
# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
Objekt orientierte Programmierung
In Bash können Sie keine objektorientierte Programmierung durchführen, es sei denn, Sie erstellen ein recht komplexes System zur Zuweisung von Objekten (ich habe darüber nachgedacht. Es ist machbar, aber verrückt). In der Praxis können Sie jedoch "Singleton-orientierte Programmierung" durchführen: Sie haben eine Instanz jedes Objekts und nur eine.
Was ich mache ist: Ich definiere ein Objekt in ein Modul (siehe den Modularisierungseintrag). Dann definiere ich leere Variablen (analog zu Mitgliedsvariablen), eine Init-Funktion (Konstruktor) und Mitgliedsfunktionen, wie in diesem Beispielcode
# avoid double inclusion
if test "${Table__imported+defined}" == "defined"
then
return 0
fi
Table__imported=1
readonly Table__NoException=""
readonly Table__ParameterException="Table__ParameterException"
readonly Table__MySqlException="Table__MySqlException"
readonly Table__NotInitializedException="Table__NotInitializedException"
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException"
# an example for module enum constants, used in the mysql table, in this case
readonly Table__GENDER_MALE="GENDER_MALE"
readonly Table__GENDER_FEMALE="GENDER_FEMALE"
# private: prefixed with p_ (a bash variable cannot start with _)
p_Table__mysql_exec="" # will contain the executed mysql command
p_Table__initialized=0
function Table__init {
# @description init the module with the database parameters
# @param $1 the mysql config file
# @exception Table__NoException, Table__ParameterException
EXCEPTION=""
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
RESULT=""
if test $p_Table__initialized -ne 0
then
EXCEPTION=$Table__AlreadyInitializedException
EXCEPTION_MSG="module already initialized"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
local config_file="$1"
# yes, I am aware that I could put default parameters and other niceties, but I am lazy today
if test "x$config_file" = "x"; then
EXCEPTION=$Table__ParameterException
EXCEPTION_MSG="missing parameter config file"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e "
# mark the module as initialized
p_Table__initialized=1
EXCEPTION=$Table__NoException
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
return 0
}
function Table__getName() {
# @description gets the name of the person
# @param $1 the row identifier
# @result the name
EXCEPTION=""
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
RESULT=""
if test $p_Table__initialized -eq 0
then
EXCEPTION=$Table__NotInitializedException
EXCEPTION_MSG="module not initialized"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
id=$1
if test "x$id" = "x"; then
EXCEPTION=$Table__ParameterException
EXCEPTION_MSG="missing parameter identifier"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = '$id'"`
if test $? != 0 ; then
EXCEPTION=$Table__MySqlException
EXCEPTION_MSG="unable to perform select"
EXCEPTION_FUNC="$FUNCNAME"
return 1
fi
RESULT=$name
EXCEPTION=$Table__NoException
EXCEPTION_MSG=""
EXCEPTION_FUNC=""
return 0
}
Signale einfangen und handhaben
Ich fand das nützlich, um Ausnahmen zu fangen und zu behandeln.
function Main__interruptHandler() {
# @description signal handler for SIGINT
echo "SIGINT caught"
exit
}
function Main__terminationHandler() {
# @description signal handler for SIGTERM
echo "SIGTERM caught"
exit
}
function Main__exitHandler() {
# @description signal handler for end of the program (clean or unclean).
# probably redundant call, we already call the cleanup in main.
exit
}
trap Main__interruptHandler INT
trap Main__terminationHandler TERM
trap Main__exitHandler EXIT
function Main__main() {
# body
}
# catch signals and exit
trap exit INT TERM EXIT
Main__main "$@"
Hinweise und Tipps
Wenn etwas aus irgendeinem Grund nicht funktioniert, versuchen Sie, den Code neu zu ordnen. Ordnung ist wichtig und nicht immer intuitiv.
Denken Sie nicht einmal daran, mit tcsh zu arbeiten. Es unterstützt keine Funktionen und ist im Allgemeinen schrecklich.
Hoffe es hilft, obwohl bitte beachten. Wenn Sie die Art von Dingen verwenden müssen, die ich hier geschrieben habe, bedeutet dies, dass Ihr Problem zu komplex ist, um mit Shell gelöst zu werden. benutze eine andere Sprache. Ich musste es aufgrund menschlicher Faktoren und Vermächtnisse benutzen.
getopt
vs getopts
? getopts
ist portabler und funktioniert in jeder POSIX-Shell. Insbesondere da es sich bei der Frage um Best Practices für Shell handelt, anstatt um Best Practices für Bashs, würde ich die POSIX-Konformität unterstützen, um nach Möglichkeit mehrere Shells zu unterstützen.
Im Advanced Bash-Scripting Guide finden Sie viele Informationen zum Thema Shell-Scripting - nicht nur zu Bash.
Hören Sie nicht auf Leute, die Ihnen sagen, Sie sollen sich andere, wohl komplexere Sprachen ansehen. Wenn Shell-Skripte Ihren Anforderungen entsprechen, verwenden Sie diese. Sie wollen Funktionalität, keine Phantasie. Neue Sprachen bieten wertvolle neue Fähigkeiten für Ihren Lebenslauf, aber das hilft nicht, wenn Sie Arbeit haben, die erledigt werden muss, und Sie Shell bereits kennen.
Wie bereits erwähnt, gibt es nicht viele "Best Practices" oder "Entwurfsmuster" für Shell-Skripte. Unterschiedliche Verwendungen haben unterschiedliche Richtlinien und Vorurteile - wie jede andere Programmiersprache.
Shell-Skript ist eine Sprache zum Bearbeiten von Dateien und Prozessen. Das ist zwar großartig, aber keine Allzwecksprache. Versuchen Sie daher immer, Logik aus vorhandenen Dienstprogrammen zu kleben, anstatt neue Logik in Shell-Skripten neu zu erstellen.
Abgesehen von diesem allgemeinen Prinzip habe ich einige häufige Shell- Skriptfehler gesammelt .
Es gab dieses Jahr (2008) eine großartige Sitzung auf der OSCON zu diesem Thema: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
Wissen, wann man es benutzt. Für schnelles und schmutziges Zusammenkleben ist es in Ordnung. Wenn Sie mehr als nur wenige nicht triviale Entscheidungen treffen müssen, wählen Sie Python, Perl und modularisieren Sie .
Das größte Problem mit Shell ist oft, dass das Endergebnis wie ein großer Schlammball aussieht, 4000 Zeilen Bash und Wachstum ... und Sie können es nicht loswerden, weil jetzt Ihr gesamtes Projekt davon abhängt. Natürlich begann es bei 40 Zeilen wunderschöner Bash.
Einfach: Verwenden Sie Python anstelle von Shell-Skripten. Sie erhalten eine nahezu 100-fache Verbesserung der Lesbarkeit, ohne dass Sie etwas komplizieren müssen, das Sie nicht benötigen, und behalten die Fähigkeit, Teile Ihres Skripts in Funktionen, Objekte, persistente Objekte (zodb) und verteilte Objekte (pyro) zu verwandeln, fast ohne zusätzlicher Code.
Um einige "Best Practices" zu finden, schauen Sie, wie Linux-Distributionen (z. B. Debian) ihre Init-Skripte schreiben (normalerweise in /etc/init.d zu finden).
Die meisten von ihnen sind ohne "Bash-Ismen" und haben eine gute Trennung von Konfigurationseinstellungen, Bibliotheksdateien und Quellformatierung.
Mein persönlicher Stil ist es, ein Master-Shellscript zu schreiben, das einige Standardvariablen definiert, und dann zu versuchen, eine Konfigurationsdatei zu laden ("Quelle"), die möglicherweise neue Werte enthält.
Ich versuche, Funktionen zu vermeiden, da sie dazu neigen, das Skript komplizierter zu machen. (Perl wurde zu diesem Zweck erstellt.)
Um sicherzustellen, dass das Skript portabel ist, testen Sie nicht nur mit #! / Bin / sh, sondern verwenden Sie auch #! / Bin / ash, #! / Bin / dash usw. Sie werden den Bash-spezifischen Code früh genug erkennen.