it-swarm-eu.dev

Entwurfsmuster oder Best Practices für Shell-Skripte

Kennt jemand Ressourcen, die sich mit Best Practices oder Entwurfsmustern für Shell-Skripte befassen (sh, bash usw.)?

161
user14437

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.

Das heißt, ich habe keine anderen Ressourcen, um Sie weiterzugeben, aber meine persönliche Erfahrung. Hier ist, was ich normalerweise mache, was übertrieben ist, aber dazu neigt, solide zu sein, obwohl sehr wortreich.

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, wenn Sie weniger Probleme haben.

CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "[email protected]"`

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 bei erfolgreichem Abschluss immer Null zurückgeben sollte, und nicht Null, wenn ein Fehler aufgetreten ist.

Funktionsaufrufe

Sie können Funktionen in bash aufrufen. Denken Sie jedoch daran, diese 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 ordnungsgemäß 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 für 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 in 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 falsch eingegebene Variable nicht gemeldet wird und als leer ausgewertet wird. verwenden

set -o nounset

um dies zu verhindern. Seien Sie jedoch vorsichtig, denn in diesem Fall bricht das Programm jedes Mal ab, wenn Sie eine undefinierte Variable auswerten. Aus diesem Grund kann nur Folgendes überprüft werden, 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 im globalen Namespace daran, allen Funktionen und Variablen das richtige Präfix als Präfix zuzuweisen. Andernfalls besteht die Gefahr von Namenskonflikten. 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

Objektorientierte Programmierung

In Bash können Sie keine objektorientierte Programmierung durchführen, es sei denn, Sie erstellen ein recht komplexes System zur Zuordnung 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 von jedem Objekt und nur eine.

Was ich tue ist: Ich definiere ein Objekt in ein Modul (siehe den Eintrag Modularisierung). Dann definiere ich leere Variablen (analog zu Member-Variablen), eine Init-Funktion (Konstruktor) und Member-Funktionen, 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 abfangen 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 "[email protected]"

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 es 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. Verwenden Sie eine andere Sprache. Ich musste es aufgrund menschlicher Faktoren und des Erbes benutzen.

215
Stefano Borini

Schauen Sie sich das Advanced Bash-Scripting Guide an, um mehr über Shell-Skripte zu erfahren - und nicht nur über Bash.

Hören Sie nicht auf Leute, die Ihnen sagen, dass Sie sich andere, möglicherweise komplexere Sprachen ansehen sollen. Wenn Shell-Skripte Ihren Anforderungen entsprechen, verwenden Sie diese. Sie möchten Funktionalität, nicht Fantasie. Neue Sprachen bieten wertvolle neue Fähigkeiten für Ihren Lebenslauf, aber das hilft nichts, wenn Sie noch zu erledigende Aufgaben haben und 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 auch.

24
jtimberman

Shell-Skript ist eine Sprache zum Bearbeiten von Dateien und Prozessen. Obwohl es für diesen Zweck großartig ist, handelt es sich nicht um eine Allzwecksprache. Versuchen Sie daher immer, die Logik von vorhandenen Dienstprogrammen zu übernehmen, anstatt eine neue Logik in einem Shell-Skript zu erstellen.

Abgesehen von diesem allgemeinen Prinzip habe ich einige häufige Shell-Skriptfehler gesammelt.

20
pixelbeat

Es gab eine großartige Sitzung auf der OSCON in diesem Jahr (2008) zu diesem Thema: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf

13
Fhoxh

Einfach: Verwenden Sie python anstelle von Shell-Skripten. Die Lesbarkeit wird nahezu 100-fach gesteigert, ohne dass Sie etwas komplizieren müssen, das Sie nicht benötigen, und Sie können Teile Ihres Skripts weiterentwickeln Funktionen, Objekte, persistente Objekte (zodb), verteilte Objekte (pyro) fast ohne zusätzlichen Code.

9
Joao S O Bueno

Wissen, wann man es benutzt. Für schnelles und schmutziges Zusammenkleben ist es okay. Wenn Sie mehr als nur wenige nicht triviale Entscheidungen treffen müssen, wählen Sie Python, Perl und modularisieren.

Das größte Problem bei Shell ist oft, dass das Endergebnis nur wie eine große Schlammkugel aussieht, 4000 Zeilen Bash und wächst ... und Sie können es nicht loswerden, weil jetzt Ihr gesamtes Projekt davon abhängt. Natürlich es begann bei 40 Zeilen von schönen Bash.

9
Paweł Hajdan

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

8
user10392

Um einige "Best Practices" zu finden, schauen Sie, wie Linux-Distributionen (z. B. Debian) ihre Init-Skripte schreiben (normalerweise in /etc/init.d).

Die meisten von ihnen sind ohne "Bash-Ismen" und haben eine gute Trennung von Konfigurationseinstellungen, Bibliotheksdateien und Quellenformatierung.

Mein persönlicher Stil ist es, ein Master-Shellscript zu schreiben, das einige Standardvariablen definiert, und dann zu versuchen, eine Konfigurationsdatei zu laden ("source"), die möglicherweise neue Werte enthält.

Ich versuche, Funktionen zu vermeiden, da sie das Skript komplizierter machen. (Perl wurde zu diesem Zweck erstellt.)

Um sicherzustellen, dass das Skript portabel ist, testen Sie es nicht nur mit #!/Bin/sh, sondern verwenden Sie auch #!/Bin/ash, #!/Bin/dash usw. Sie werden den Bash-spezifischen Code früh genug erkennen.

7
Willem