Modèles de conception ou bonnes pratiques pour les scripts shell [fermé]

167

Est-ce que quelqu'un connaît des ressources qui parlent des meilleures pratiques ou des modèles de conception pour les scripts shell (sh, bash, etc.)?

user14437
la source
2
Je viens d'écrire un petit article sur le modèle de modèle dans BASH hier soir. Voyez ce que vous pensez.
quickshift du

Réponses:

222

J'ai écrit des scripts shell assez complexes et ma première suggestion est "ne pas". La raison en est qu'il est assez facile de faire une petite erreur qui gêne votre script, voire le rend dangereux.

Cela dit, je n'ai pas d'autres ressources pour vous transmettre mais mon expérience personnelle. Voici ce que je fais normalement, ce qui est exagéré, mais qui a tendance à être solide, bien que très verbeux.

Invocation

faites accepter à votre script des options longues et courtes. soyez prudent car il existe deux commandes pour analyser les options, getopt et getopts. Utilisez getopt car vous rencontrez moins de problèmes.

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

Un autre point important est qu'un programme doit toujours renvoyer zéro s'il se termine avec succès, différent de zéro si quelque chose ne va pas.

Appels de fonction

Vous pouvez appeler des fonctions dans bash, n'oubliez pas de les définir avant l'appel. Les fonctions sont comme des scripts, elles ne peuvent renvoyer que des valeurs numériques. Cela signifie que vous devez inventer une stratégie différente pour renvoyer des valeurs de chaîne. Ma stratégie consiste à utiliser une variable appelée RESULT pour stocker le résultat et à renvoyer 0 si la fonction s'est terminée proprement. En outre, vous pouvez déclencher des exceptions si vous renvoyez une valeur différente de zéro, puis définissez deux "variables d'exception" (la mienne: EXCEPTION et EXCEPTION_MSG), la première contenant le type d'exception et la seconde un message lisible par l'homme.

Lorsque vous appelez une fonction, les paramètres de la fonction sont assignés aux variables spéciales $ 0, $ 1 etc. Je vous suggère de les mettre dans des noms plus significatifs. déclarez les variables à l'intérieur de la fonction comme locales:

function foo {
   local bar="$0"
}

Situations sujettes aux erreurs

Dans bash, sauf si vous déclarez le contraire, une variable non définie est utilisée comme une chaîne vide. Ceci est très dangereux en cas de faute de frappe, car la variable mal typée ne sera pas signalée et elle sera évaluée comme vide. utilisation

set -o nounset

pour éviter que cela se produise. Attention cependant, car si vous faites cela, le programme s'interrompra chaque fois que vous évaluerez une variable non définie. Pour cette raison, la seule façon de vérifier si une variable n'est pas définie est la suivante:

if test "x${foo:-notset}" == "xnotset"
then
    echo "foo not set"
fi

Vous pouvez déclarer des variables en lecture seule:

readonly readonly_var="foo"

La modularisation

Vous pouvez réaliser une modularisation «comme python» si vous utilisez le code suivant:

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
} 

vous pouvez ensuite importer des fichiers avec l'extension .shinc avec la syntaxe suivante

importer "AModule / ModuleFile"

Qui sera recherché dans SHELL_LIBRARY_PATH. Comme vous importez toujours dans l'espace de noms global, n'oubliez pas de préfixer toutes vos fonctions et variables avec un préfixe approprié, sinon vous risquez des conflits de noms. J'utilise le double soulignement comme point python.

Aussi, mettez ceci comme première chose dans votre module

# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
    return 0
fi
BashInclude__imported=1

Programmation orientée objet

En bash, vous ne pouvez pas faire de programmation orientée objet, à moins de construire un système assez complexe d'allocation d'objets (j'y ai pensé. C'est faisable, mais insensé). En pratique, vous pouvez cependant faire de la "programmation orientée Singleton": vous avez une instance de chaque objet, et une seule.

Ce que je fais est: je définis un objet dans un module (voir l'entrée de modularisation). Ensuite, je définis des vars vides (analogues aux variables membres) une fonction init (constructeur) et des fonctions membres, comme dans cet exemple de code

# 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
}

Signaux de piégeage et de manipulation

J'ai trouvé cela utile pour attraper et gérer les exceptions.

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 "$@"

Trucs et astuces

Si quelque chose ne fonctionne pas pour une raison quelconque, essayez de réorganiser le code. L'ordre est important et pas toujours intuitif.

n'envisagez même pas de travailler avec tcsh. il ne prend pas en charge les fonctions, et c'est horrible en général.

J'espère que cela aide, bien que veuillez noter. Si vous devez utiliser le genre de choses que j'ai écrites ici, cela signifie que votre problème est trop complexe pour être résolu avec shell. utiliser une autre langue. J'ai dû l'utiliser en raison de facteurs humains et de l'héritage.

Stefano Borini
la source
7
Wow, et je pensais que j'allais exagérer dans bash ... J'ai tendance à utiliser des fonctions isolées et à abuser des sous-coquilles (donc je souffre lorsque la vitesse est de quelque façon pertinente). Aucune variable globale jamais, ni dedans ni dehors (pour préserver les restes de raison). Tous les retours via stdout ou sortie de fichier. set -u / set -e (trop mauvais set -e devient inutile dès le premier if, et la plupart de mon code y est souvent). Arguments de fonction pris avec [local something = "$ 1"; shift] (permet une réorganisation facile lors de la refactorisation). Après un script bash de 3000 lignes, j'ai tendance à écrire même les plus petits scripts de cette façon ...
Eugene
petites corrections pour la modularisation: 1 il faut un retour après. "$ script_absolute_dir / $ module.shinc" pour éviter de manquer un avertissement. 2 vous devez définir IFS = "$ saved_IFS" avant votre retour sur le module de recherche dans $ SHELL_LIBRARY_PATH
Duff
les «facteurs humains» sont les pires. Les machines ne vous combattent pas lorsque vous leur donnez quelque chose de mieux.
jeremyjjbrown
1
Pourquoi getoptvs getopts? getoptsest plus portable et fonctionne dans n'importe quel shell POSIX. D'autant que la question concerne les meilleures pratiques du shell au lieu des meilleures pratiques de bash spécifiquement, je soutiendrais la conformité POSIX pour prendre en charge plusieurs shells lorsque cela est possible.
Wimateeka
1
merci d'avoir offert tous les conseils pour le script shell même si vous êtes honnête: "J'espère que cela aide, bien que veuillez noter. Si vous devez utiliser le genre de choses que j'ai écrites ici, cela signifie que votre problème est trop complexe pour être résolu avec shell. utiliser une autre langue. J'ai dû l'utiliser en raison de facteurs humains et de l'héritage. "
dieHellste
25

Jetez un œil au Advanced Bash-Scripting Guide pour beaucoup de sagesse sur les scripts shell - pas seulement Bash, non plus.

N'écoutez pas les gens vous dire de regarder d'autres langues, sans doute plus complexes. Si le script shell répond à vos besoins, utilisez-le. Vous voulez la fonctionnalité, pas la fantaisie. Les nouvelles langues fournissent de nouvelles compétences précieuses pour votre CV, mais cela n'aide pas si vous avez du travail à faire et que vous connaissez déjà Shell.

Comme indiqué, il n'y a pas beaucoup de «meilleures pratiques» ou de «modèles de conception» pour les scripts shell. Différentes utilisations ont des directives et des biais différents - comme tout autre langage de programmation.

jtimberman
la source
9
Notez que pour les scripts de complexité même légère, ce n'est PAS une bonne pratique. Le codage ne consiste pas simplement à faire fonctionner quelque chose. Il s'agit de le construire rapidement, facilement et d'être fiable, réutilisable et facile à lire et à entretenir (en particulier pour les autres). Les scripts shell ne s'adaptent à aucun niveau. Les langages plus robustes sont beaucoup plus simples pour les projets avec n'importe quelle logique.
drifter
20

shell script est un langage conçu pour manipuler des fichiers et des processus. Bien que ce soit génial pour cela, ce n'est pas un langage à usage général, alors essayez toujours de coller la logique à partir des utilitaires existants plutôt que de recréer une nouvelle logique dans un script shell.

En dehors de ce principe général, j'ai rassemblé quelques erreurs courantes de script shell .

pixelbeat
la source
11

Sachez quand l'utiliser. Pour des commandes de collage rapides et sales, c'est correct. Si vous avez besoin de prendre plus que quelques décisions non triviales, des boucles, quoi que ce soit, optez pour Python, Perl et modularisez .

Le plus gros problème avec Shell est souvent que le résultat final ressemble à une grosse boule de boue, 4000 lignes de bash et de plus en plus ... et vous ne pouvez pas vous en débarrasser car maintenant tout votre projet en dépend. Bien sûr, cela a commencé à 40 lignes de belle bash.

Paweł Hajdan
la source
9

Facile: utilisez python au lieu des scripts shell. Vous obtenez une augmentation de près de 100 fois la lisibilité, sans avoir à compliquer quoi que ce soit dont vous n'avez pas besoin, et en préservant la possibilité de faire évoluer des parties de votre script en fonctions, objets, objets persistants (zodb), objets distribués (pyro) presque sans aucun code supplémentaire.


la source
7
vous vous contredisez en disant «sans avoir à vous compliquer», puis en énumérant les diverses complexités qui, selon vous, ajoutent de la valeur, alors que dans la plupart des cas, elles sont transformées en monstres laids au lieu d'être utilisées pour simplifier les problèmes et la mise en œuvre.
Evgeny
3
cela implique un gros inconvénient, vos scripts ne seront pas portables sur les systèmes où python n'est pas présent
astropanique
1
Je me rends compte que cela a été répondu en '08 (il est maintenant deux jours avant '12); cependant, pour ceux qui regardent ces années plus tard, je mettrais en garde quiconque de ne pas tourner le dos à des langages comme Python ou Ruby car il est plus probable qu'il soit disponible et sinon, c'est une commande (ou quelques clics) loin d'être installée . Si vous avez besoin de plus de portabilité, pensez à écrire votre programme en Java car vous aurez du mal à trouver une machine qui n'a pas de JVM disponible.
Wil Moore III du
@astropanic à peu près tous les ports Linux avec Python de nos jours
Pithikos
@Pithikos, bien sûr, et tripotez les tracas de python2 contre python3. Aujourd'hui, j'écris tous mes outils avec go, et je ne peux pas être plus heureux.
2017 astropanique
9

utilisez set -e pour ne pas avancer après des erreurs. Essayez de le rendre compatible sans vous fier à bash si vous voulez qu'il fonctionne sur non-Linux.

utilisateur10392
la source
7

Pour trouver quelques «meilleures pratiques», regardez comment les distributions Linux (par exemple Debian) écrivent leurs scripts d'initialisation (généralement trouvés dans /etc/init.d)

La plupart d'entre eux sont sans "bash-isms" et ont une bonne séparation des paramètres de configuration, des fichiers de bibliothèque et du formatage source.

Mon style personnel est d'écrire un master-shellscript qui définit certaines variables par défaut, puis essaie de charger ("source") un fichier de configuration qui peut contenir de nouvelles valeurs.

J'essaie d'éviter les fonctions car elles ont tendance à rendre le script plus compliqué. (Perl a été créé dans ce but.)

Pour vous assurer que le script est portable, testez non seulement avec #! / Bin / sh, mais aussi utilisez #! / Bin / ash, #! / Bin / dash, etc. Vous repérerez assez tôt le code spécifique à Bash.

Willem
la source
-1

Ou la citation plus ancienne similaire à ce que Joao a dit:

"Utilisez perl. Vous voudrez connaître bash mais pas l'utiliser."

Malheureusement, j'ai oublié qui a dit cela.

Et oui ces jours-ci, je recommanderais python plutôt que perl.

Sarien
la source