Entwurfsmuster oder Best Practices für Shell-Skripte [geschlossen]

167

Kennt jemand Ressourcen, die über Best Practices oder Entwurfsmuster für Shell-Skripte (sh, bash usw.) sprechen?

user14437
quelle
2
Ich habe gestern Abend einen kleinen Artikel über Vorlagenmuster in BASH geschrieben . Sehen Sie, was Sie denken.
Quickshiftin

Antworten:

222

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.

Stefano Borini
quelle
7
Wow, und ich dachte, ich würde in Bash Overkill machen ... Ich neige dazu, isolierte Funktionen zu verwenden und Subshells zu missbrauchen (daher leide ich, wenn Geschwindigkeit in irgendeiner Weise relevant ist). Keine globalen Variablen, weder rein noch raus (um Überreste der Vernunft zu bewahren). Alle Rückgaben über stdout oder Dateiausgabe. set -u / set -e (schade, dass set -e sofort nutzlos wird, wenn zuerst, und der größte Teil meines Codes ist oft drin). Funktionsargumente mit [local Something = "$ 1"; Shift] (ermöglicht eine einfache Neuordnung beim Refactoring). Nach einem Bash-Skript mit 3000 Zeilen neige ich dazu, selbst kleinste Skripte auf diese Weise zu schreiben ...
Eugene
kleine Korrekturen für die Modularisierung: 1 Sie benötigen eine Rückgabe nach. "$ script_absolute_dir / $ module.shinc", um fehlende Warnungen zu vermeiden. 2 Sie müssen IFS = "$ saved_IFS" setzen, bevor Sie das Modul in $ SHELL_LIBRARY_PATH
Duff
"menschliche Faktoren" sind die schlimmsten. Maschinen kämpfen nicht gegen dich, wenn du ihnen etwas Besseres gibst.
Jeremyjjbrown
1
Warum getoptvs getopts? getoptsist 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.
Wimateeka
1
Vielen Dank, dass Sie alle Ratschläge für Shell-Skripte gegeben haben, obwohl Sie ehrlich sind: "Ich hoffe, es hilft, obwohl Sie dies 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 damit gelöst zu werden Shell. Verwenden Sie eine andere Sprache. Ich musste es aufgrund menschlicher Faktoren und Vermächtnis verwenden. "
dieHellste
25

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.

jtimberman
quelle
9
Beachten Sie, dass dies für Skripte mit nur geringer Komplexität KEINE bewährte Methode ist. Beim Codieren geht es nicht nur darum, etwas zum Laufen zu bringen. Es geht darum, es schnell, einfach und zuverlässig, wiederverwendbar und einfach zu lesen und zu warten (insbesondere für andere) zu erstellen. Shell-Skripte lassen sich auf keiner Ebene gut skalieren. Robustere Sprachen sind für Projekte mit beliebiger Logik viel einfacher.
Drifter
20

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 .

Pixelbeat
quelle
11

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.

Paweł Hajdan
quelle
9

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.


quelle
7
Sie widersprechen sich selbst, indem Sie "ohne Komplikationen" sagen und dann die verschiedenen Komplexitäten auflisten, von denen Sie glauben, dass sie einen Mehrwert bieten, während Sie in den meisten Fällen zu hässlichen Monstern missbraucht werden, anstatt zur Vereinfachung von Problemen und zur Implementierung verwendet zu werden.
Evgeny
3
Dies bedeutet einen großen Nachteil: Ihre Skripte können nicht auf Systemen portiert werden, auf denen Python nicht vorhanden ist
Astropanic
1
Mir ist klar, dass dies in '08 beantwortet wurde (es ist jetzt zwei Tage vor '12); Für diejenigen, die sich dies Jahre später ansehen, würde ich jedoch jeden davor warnen, Sprachen wie Python oder Ruby den Rücken zu kehren, da es wahrscheinlicher ist, dass es verfügbar ist, und wenn nicht, ist es ein Befehl (oder ein paar Klicks) von der Installation entfernt . Wenn Sie weitere Portabilität benötigen, sollten Sie Ihr Programm in Java schreiben, da Sie kaum einen Computer finden können, auf dem keine JVM verfügbar ist.
Wil Moore III
@astropanic so ziemlich alle Linux-Ports mit Python heutzutage
Pithikos
@Pithikos, klar, und spielen Sie mit dem Ärger von Python2 gegen Python3. Heutzutage schreibe ich alle meine Werkzeuge mit go und kann nicht glücklicher sein.
Astropanic
9

Verwenden Sie set -e, damit Sie nach Fehlern nicht vorwärts pflügen. Versuchen Sie, es sh-kompatibel zu machen, ohne sich auf bash zu verlassen, wenn Sie möchten, dass es unter Nicht-Linux ausgeführt wird.

user10392
quelle
7

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.

Willem
quelle
-1

Oder das ältere Zitat ähnlich dem, was Joao gesagt hat:

"Benutze Perl. Du wirst Bash wissen wollen, aber nicht benutzen."

Leider habe ich vergessen, wer das gesagt hat.

Und ja, heutzutage würde ich Python über Perl empfehlen.

Sarien
quelle