Bei der Arbeit schreibe ich häufig Bash-Skripte. Mein Vorgesetzter hat vorgeschlagen, das gesamte Skript in Funktionen zu unterteilen, ähnlich dem folgenden Beispiel:
#!/bin/bash
# Configure variables
declare_variables() {
noun=geese
count=three
}
# Announce something
i_am_foo() {
echo "I am foo"
sleep 0.5
echo "hear me roar!"
}
# Tell a joke
walk_into_bar() {
echo "So these ${count} ${noun} walk into a bar..."
}
# Emulate a pendulum clock for a bit
do_baz() {
for i in {1..6}; do
expr $i % 2 >/dev/null && echo "tick" || echo "tock"
sleep 1
done
}
# Establish run order
main() {
declare_variables
i_am_foo
walk_into_bar
do_baz
}
main
Gibt es einen anderen Grund, als die "Lesbarkeit", die meiner Meinung nach mit ein paar weiteren Kommentaren und einigem Zeilenabstand gleich gut festgestellt werden könnte?
Lässt sich das Skript effizienter ausführen (ich würde eigentlich eher das Gegenteil erwarten) oder ist es einfacher, den Code über das oben genannte Lesbarkeitspotential hinaus zu ändern? Oder ist es wirklich nur eine stilistische Präferenz?
Bitte beachten Sie, dass , obwohl das Skript nicht gut funktioniert zeigen, die „run Ordnung“ der Funktionen in unserem aktuellen Skripte sehr linear zu sein scheint - walk_into_bar
abhängig von Sachen , die i_am_foo
getan hat, und do_baz
wirkt auf Material eingesetzt durch walk_into_bar
- so seinen Die Laufreihenfolge willkürlich tauschen zu können, ist etwas, was wir normalerweise tun würden. Zum Beispiel, würden Sie nicht plötzlich setzen wollen declare_variables
nach walk_into_bar
, dass sich die Dinge brechen.
Ein Beispiel, wie ich das obige Skript schreiben würde, wäre:
#!/bin/bash
# Configure variables
noun=geese
count=three
# Announce something
echo "I am foo"
sleep 0.5
echo "hear me roar!"
# Tell a joke
echo "So these ${count} ${noun} walk into a bar..."
# Emulate a pendulum clock for a bit
for i in {1..6}; do
expr $i % 2 >/dev/null && echo "tick" || echo "tock"
sleep 1
done
quelle
main()
oben undmain "$@"
unten, um es aufzurufen. Auf diese Weise können Sie die übergeordnete Skriptlogik als Erstes anzeigen, wenn Sie sie öffnen.local
- bietet diese Variable Umfang , die in jedem nicht-trivialen Skript unglaublich wichtig ist.Antworten:
Nachdem ich Kfir Lavis Blogpost "Defensive Bash Programming" gelesen habe, habe ich angefangen, diesen Stil der Bash-Programmierung zu verwenden . Er gibt einige gute Gründe an, aber ich persönlich finde diese die wichtigsten:
Prozeduren werden beschreibend: Es ist viel einfacher herauszufinden, was ein bestimmter Teil des Codes tun soll. Anstelle der Codewand sehen Sie "Oh, die
find_log_errors
Funktion liest diese Protokolldatei auf Fehler". Vergleichen Sie dies damit, dass Sie eine ganze Reihe von awk / grep / sed-Zeilen finden, die genau wissen, welche Art von Regex in der Mitte eines langen Skripts vorkommt.Sie können Funktionen debuggen, indem Sie in
set -x
und einschließenset +x
. Sobald Sie wissen, dass der Rest des Codes in Ordnung ist, können Sie sich mit diesem Trick darauf konzentrieren, nur diese bestimmte Funktion zu debuggen. Sicher, Sie können Teile des Skripts einschließen, aber was ist, wenn es ein langer Teil ist? So etwas ist einfacher:Drucknutzung mit
cat <<- EOF . . . EOF
. Ich habe es einige Male benutzt, um meinen Code viel professioneller zu machen. Darüber hinaus istparse_args()
mitgetopts
Funktion ganz bequem. Auch dies trägt zur Lesbarkeit bei, anstatt alles als riesige Textwand in die Schrift zu schreiben. Es ist auch bequem, diese wiederzuverwenden.Und offensichtlich ist dies für jemanden, der C, Java oder Vala kennt, aber nur über begrenzte Bash-Erfahrung verfügt, viel besser lesbar. Was die Effizienz angeht, können Sie nicht viel tun - bash selbst ist nicht die effizienteste Sprache, und die Leute bevorzugen Perl und Python, wenn es um Geschwindigkeit und Effizienz geht. Sie können jedoch
nice
eine Funktion:Verglichen mit dem Aufruf von nice in jeder Codezeile, verringert sich dadurch der Aufwand für die Eingabe von AND und kann bequem verwendet werden, wenn nur ein Teil Ihres Skripts mit niedrigerer Priorität ausgeführt werden soll.
Das Ausführen von Funktionen im Hintergrund hilft meiner Meinung nach auch, wenn Sie eine ganze Reihe von Anweisungen im Hintergrund ausführen möchten.
Einige Beispiele, in denen ich diesen Stil verwendet habe:
quelle
local
und alles über diemain()
Funktion aufzurufen . Dies macht die Dinge viel leichter handhabbar und Sie können eine möglicherweise unordentliche Situation vermeiden.Lesbarkeit ist eine Sache. Modularisierung ist aber noch viel mehr . ( Semi-Modularisierung ist für Funktionen möglicherweise korrekter.)
In Funktionen können Sie einige Variablen lokal halten, was die Zuverlässigkeit erhöht und die Wahrscheinlichkeit verringert, dass Dinge durcheinander geraten.
Ein weiterer Pluspunkt ist die Wiederverwendbarkeit . Sobald eine Funktion codiert ist, kann sie im Skript mehrmals angewendet werden. Sie können es auch in ein anderes Skript portieren.
Ihr Code ist jetzt möglicherweise linear, aber in Zukunft können Sie in der Bash-Welt in den Bereich des Multi-Threading oder der Multi-Verarbeitung eintreten . Wenn Sie erst einmal gelernt haben, Dinge in Funktionen zu tun, sind Sie für den Schritt in die Parallele gut gerüstet.
Ein weiterer Punkt zum Hinzufügen. Wie Etsitpab Nioliv im Kommentar unten bemerkt, ist es einfach, Funktionen als zusammenhängende Einheit umzuleiten. Aber es gibt noch einen Aspekt bei Umleitungen mit Funktionen. Die Umleitungen können nämlich entlang der Funktionsdefinition festgelegt werden. Z.B.:
Jetzt sind keine expliziten Weiterleitungen durch die Funktionsaufrufe erforderlich.
Dies kann viele Wiederholungen ersparen, was wiederum die Zuverlässigkeit erhöht und hilft, die Dinge in Ordnung zu halten.
Siehe auch
quelle
source
oder verwenden. scriptname.sh
und diese Funktionen so verwenden, als wären sie in Ihrem neuen Skript enthalten).In meinem Kommentar erwähnte ich drei Vorteile von Funktionen:
Sie sind einfacher zu testen und auf Richtigkeit zu überprüfen.
Funktionen können in zukünftigen Skripten problemlos wiederverwendet (bezogen) werden
Ihr Chef mag sie.
Und unterschätzen Sie niemals die Wichtigkeit von Nummer 3.
Ich möchte noch ein Problem ansprechen:
Um den Vorteil einer Aufteilung des Codes in Funktionen zu erzielen, sollten Sie versuchen, die Funktionen so unabhängig wie möglich zu gestalten. Wenn
walk_into_bar
eine Variable erforderlich ist, die an keiner anderen Stelle verwendet wird, sollte diese Variable in definiert und lokal für festgelegt werdenwalk_into_bar
. Das Aufteilen des Codes in Funktionen und das Minimieren ihrer gegenseitigen Abhängigkeiten sollten den Code klarer und einfacher machen.Im Idealfall sollten Funktionen einfach einzeln zu testen sein. Wenn sie aufgrund von Interaktionen nicht einfach zu testen sind, ist dies ein Zeichen dafür, dass sie von einem Refactoring profitieren könnten.
quelle
;-)
Sie unterteilen den Code in Funktionen aus dem gleichen Grund, aus dem Sie dies für C / C ++, Python, Perl, Ruby oder einen beliebigen Programmiersprachencode tun würden. Der tiefere Grund ist die Abstraktion - Sie kapseln Aufgaben auf niedrigerer Ebene in übergeordnete Grundelemente (Funktionen), sodass Sie sich nicht darum kümmern müssen, wie die Dinge ausgeführt werden. Gleichzeitig wird der Code lesbarer (und wartbarer) und die Programmlogik wird klarer.
Wenn ich mir jedoch Ihren Code anschaue, finde ich es ziemlich seltsam, eine Funktion zum Deklarieren von Variablen zu haben. das lässt mich wirklich die Augenbrauen hochziehen.
quelle
main
Funktion / Methode zu deklarieren ?Obwohl ich der Wiederverwendbarkeit , Lesbarkeit und dem zarten Küssen der Chefs vollkommen zustimme , gibt es einen weiteren Vorteil der Funktionen in bash : den variablen Umfang . Wie LDP zeigt :
Ich sehe das nicht sehr oft in echten Shell-Skripten, aber es scheint eine gute Idee für komplexere Skripten zu sein. Durch das Reduzieren der Kohäsion können Sie Fehler vermeiden, bei denen Sie eine Variable blockieren, die in einem anderen Teil des Codes erwartet wird.
Wiederverwendbarkeit bedeutet oft, eine gemeinsame Bibliothek von Funktionen zu erstellen und
source
diese Bibliothek in alle Ihre Skripte einzubinden. Dies wird ihnen nicht helfen, schneller zu laufen, aber es wird Ihnen helfen, sie schneller zu schreiben.quelle
local
, aber ich denke, die meisten Leute, die Skripte schreiben, die in Funktionen unterteilt sind, folgen immer noch dem Designprinzip. Usignlocal
macht es nur schwieriger, Fehler einzuführen.local
Stellt Variablen für function und seine untergeordneten Elemente zur Verfügung. Daher ist es sehr schön, eine Variable zu haben, die von function A übergeben werden kann, für function B jedoch nicht verfügbar ist. Möglicherweise möchten Sie eine Variable mit demselben Namen, aber mit einem anderen Zweck. Das ist also gut, um den Umfang zu definieren, und wie Voo sagte - weniger BugsEin ganz anderer Grund als der, der bereits in anderen Antworten angegeben wurde: Ein Grund, warum diese Technik manchmal verwendet wird, wenn die einzige Nicht-Funktionsdefinitionsanweisung auf oberster Ebene ein Aufruf von ist
main
, ist sicherzustellen, dass das Skript nicht versehentlich etwas Böses tut wenn das Skript abgeschnitten ist. Das Skript wird möglicherweise abgeschnitten, wenn es von Prozess A zu Prozess B (der Shell) weitergeleitet wird und Prozess A aus irgendeinem Grund beendet wird, bevor das gesamte Skript geschrieben wurde. Dies ist besonders wahrscheinlich, wenn Prozess A das Skript von einer Remote-Ressource abruft. Obwohl dies aus Sicherheitsgründen keine gute Idee ist, wird dies getan, und einige Skripte wurden geändert, um das Problem zu antizipieren.quelle
main()
Muster in Python üblich, wo esif __name__ == '__main__': main()
am Ende der Datei verwendet wird.import
das aktuelle Skript ohne Ausführung lassenmain
. Ich nehme an, eine ähnliche Wache könnte in ein Bash-Skript eingefügt werden.Ein Prozess erfordert eine Sequenz. Die meisten Aufgaben sind sequentiell. Es macht keinen Sinn, sich mit der Bestellung herumzuschlagen.
Aber die große Sache beim Programmieren - einschließlich Skripten - ist das Testen. Testen, testen, testen. Mit welchen Testskripten müssen Sie derzeit die Richtigkeit Ihrer Skripten überprüfen?
Ihr Chef versucht, Sie vom Drehbuchkind zum Programmierer zu führen. Das ist eine gute Richtung. Leute, die nach dir kommen, werden dich mögen.
ABER. Denken Sie immer an Ihre prozessorientierten Wurzeln. Wenn es sinnvoll ist, die Funktionen in der Reihenfolge anzuordnen, in der sie normalerweise ausgeführt werden, dann tun Sie dies zumindest im ersten Durchgang.
Später werden Sie feststellen, dass einige Ihrer Funktionen die Eingabe, andere die Ausgabe, andere die Verarbeitung, andere die Modellierung von Daten und andere die Bearbeitung der Daten verarbeiten. Es kann daher sinnvoll sein, ähnliche Methoden zu gruppieren und sie möglicherweise sogar in separate Dateien zu verschieben .
Später werden Sie vielleicht feststellen, dass Sie jetzt Bibliotheken mit kleinen Hilfsfunktionen geschrieben haben, die Sie in vielen Ihrer Skripten verwenden.
quelle
Kommentare und Abstände können nicht annähernd die Lesbarkeit erreichen, die Funktionen können, wie ich demonstrieren werde. Ohne Funktionen kann man den Wald vor lauter Bäumen nicht sehen - große Probleme verbergen sich in vielen Detaillinien. Mit anderen Worten, man kann sich nicht gleichzeitig auf die feinen Details und das Gesamtbild konzentrieren. Das mag in einem kurzen Drehbuch nicht offensichtlich sein; Solange es kurz bleibt, ist es möglicherweise ausreichend lesbar. Software wird jedoch nicht kleiner, sondern größer und gehört mit Sicherheit zum gesamten Softwaresystem Ihres Unternehmens, das mit Sicherheit sehr viel größer ist, wahrscheinlich Millionen von Zeilen.
Überlegen Sie, ob ich Ihnen Anweisungen wie diese gegeben habe:
Bis Sie die Hälfte oder sogar 5% erreicht haben, haben Sie die ersten Schritte vergessen. Die meisten Probleme konnten Sie unmöglich erkennen, da Sie den Wald vor lauter Bäumen nicht sehen konnten. Vergleichen Sie mit Funktionen:
Das ist sicherlich viel verständlicher, egal wie viele Kommentare Sie in die zeilenweise sequentielle Version einfügen. Es macht es auch viel wahrscheinlicher, dass Sie bemerken, dass Sie vergessen haben , Kaffee zu kochen, und dass Sie am Ende wahrscheinlich sit_down () vergessen haben. Wenn Ihr Verstand an die Details von Grep und Awk Regexes denkt, können Sie kein großes Bild denken - "Was ist, wenn kein Kaffee gemacht wird?"
Mit den Funktionen können Sie in erster Linie den Überblick behalten und feststellen, dass Sie vergessen haben, Kaffee zuzubereiten (oder dass jemand Tee bevorzugt). Zu einem anderen Zeitpunkt, in einer anderen Stimmung, sorgen Sie sich um die detaillierte Implementierung.
Es gibt natürlich auch andere Vorteile, die in anderen Antworten erörtert werden. Ein weiterer Vorteil, der in den anderen Antworten nicht klar angegeben ist, besteht darin, dass Funktionen eine Garantie bieten, die wichtig ist, um Fehler zu verhindern und zu beheben. Wenn Sie feststellen, dass eine Variable $ foo in der richtigen Funktion walk_to () falsch war, müssen Sie sich nur die anderen 6 Zeilen dieser Funktion ansehen, um alles zu finden, was von diesem Problem betroffen sein könnte, und alles, was davon betroffen sein könnte habe es falsch gemacht. Ohne (ordnungsgemäße) Funktionen kann alles und jedes im gesamten System dazu führen, dass $ foo falsch ist und alles und jedes von $ foo beeinflusst wird. Daher können Sie $ foo nicht sicher reparieren, ohne jede einzelne Zeile des Programms erneut zu untersuchen. Wenn $ foo für eine Funktion lokal ist,
quelle
bash
Syntax. Es ist allerdings eine Schande; Ich glaube nicht, dass es eine Möglichkeit gibt, Eingaben an solche Funktionen weiterzuleiten. (dhpour();
<coffee
). Es sieht eher aus wiec++
oderphp
(denke ich).Einige relevante Binsenweisheiten zur Programmierung:
Kommentare beginnen als eine Lücke, um Ihre Ideen nicht klar in Code * auszudrücken, und werden durch Änderungen schlimmer (oder einfach falsch). Drücken Sie daher, wenn möglich, Konzepte, Strukturen, Argumentation, Semantik, Ablauf, Fehlerbehandlung und alles andere aus, was für das Verständnis des Codes als Code von Belang ist.
Bash-Funktionen haben jedoch einige Probleme, die in den meisten Sprachen nicht zu finden sind:
local
Schlüsselwort zu verwenden, wird der globale Namespace verschmutzt.local foo="$(bar)"
führt zum Verlust des Exit-Codes vonbar
."$@"
in verschiedenen Kontexten bedeutet.* Es tut mir leid, wenn dies beleidigt, aber nachdem ich Kommentare für einige Jahre verwendet und ohne sie entwickelt habe ** für weitere Jahre ist es ziemlich klar, was überlegen ist.
** Die Verwendung von Kommentaren für die Lizenzierung, API-Dokumentation und dergleichen ist weiterhin erforderlich.
quelle
local foo=""
Dann setze ich sie mit der Befehlsausführung, um auf das Ergebnis zu reagierenfoo="$(bar)" || { echo "bar() failed"; return 1; }
. Dies bringt uns schnell aus der Funktion heraus, wenn ein erforderlicher Wert nicht eingestellt werden kann. Die geschweiften Klammern sind notwendig, um sicherzustellen, dass das Programmreturn 1
nur im Fehlerfall ausgeführt wird.Zeit ist Geld
Es gibt andere gute Antworten , die die technischen Gründe für das modulare Schreiben eines möglicherweise langen Skripts beleuchten , das in einer Arbeitsumgebung entwickelt wurde und von einer Gruppe von Personen verwendet werden kann und nicht nur für den eigenen Gebrauch bestimmt ist.
Ich möchte Fokus auf der einen erwarten: in einer Arbeitsumgebung „Zeit ist Geld“ . So werden das Fehlen von Fehlern und die Leistung Ihres Codes zusammen mit Lesbarkeit , Testbarkeit, Wartbarkeit, Refactorability, Wiederverwendbarkeit ... bewertet.
Das Schreiben eines Codes in "Module" verringert nicht nur die vom Codierer selbst benötigte Lesezeit , sondern auch die von den Testern oder vom Chef verwendete Zeit. Beachten Sie außerdem, dass die Zeit eines Chefs in der Regel mehr als die Zeit eines Programmierers bezahlt wird und dass Ihr Chef die Qualität Ihrer Arbeit beurteilt.
Darüber hinaus können Sie durch das Schreiben eines Codes (sogar eines Bash-Skripts) in unabhängigen "Modulen" "parallel" mit anderen Komponenten Ihres Teams arbeiten. Dies verkürzt die gesamte Produktionszeit und nutzt bestenfalls das Fachwissen der Single, um einen Teil zu überprüfen oder umzuschreiben Keine Nebenwirkung für die anderen, um den Code, den Sie gerade geschrieben haben, so wie er ist , zu recyceln.für ein anderes Programm / Skript, um Bibliotheken (oder Bibliotheken von Snippets) zu erstellen, um die Gesamtgröße und die damit verbundene Fehlerwahrscheinlichkeit zu verringern, um jeden einzelnen Teil gründlich zu debuggen und zu testen ... und natürlich wird Ihr Programm in logischen Abschnitten organisiert / script und verbessern die Lesbarkeit. Alles, was Zeit und damit Geld spart. Der Nachteil ist, dass Sie sich an Standards halten und Ihre Funktionen kommentieren müssen (die Sie jedoch in einer Arbeitsumgebung tun müssen).
Die Einhaltung eines Standards verlangsamt Ihre Arbeit am Anfang, beschleunigt jedoch die Arbeit aller anderen (und auch Ihrer) danach. In der Tat wird dies zu einem unvermeidlichen Bedürfnis, wenn die Anzahl der beteiligten Personen ansteigt. Selbst wenn ich zum Beispiel der Meinung bin, dass die globalen Variablen global und nicht in einer Funktion definiert werden müssen, kann ich einen Standard verstehen, der sie in einer Funktion mit dem
declare_variables()
Namen always in der ersten Zeile dermain()
einen initialisiert.Nicht zuletzt sollten Sie die Möglichkeit in modernen Quellcode-Editoren nicht unterschätzen, einzelne Routinen selektiv ein- oder auszublenden ( Code Folding ). Dies hält den Code kompakt und spart dem Benutzer erneut Zeit.
Hier oben können Sie sehen, wie es nur die Funktion entfaltet
walk_into_bar()
. Selbst wenn die anderen jeweils 1000 Zeilen lang waren, konnten Sie den gesamten Code auf einer einzigen Seite unter Kontrolle halten. Beachten Sie, dass auch der Abschnitt, in dem Sie die Variablen deklarieren / initialisieren, geklappt ist.quelle
Abgesehen von den in anderen Antworten genannten Gründen:
quelle
Ein weiterer Grund, der oft übersehen wird, ist die Syntaxanalyse von bash:
Dieses Skript enthält offensichtlich einen Syntaxfehler und Bash sollte es überhaupt nicht ausführen, oder? Falsch.
Wenn wir den Code in eine Funktion einbinden würden, würde dies nicht passieren:
quelle