Modèles de conception ou meilleures pratiques pour les scripts shell

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.)?

J’ai écrit des scripts shell assez complexes et ma première suggestion est “ne pas”. La raison en est que c’est assez facile de faire une petite erreur qui entrave 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 excessif, mais a tendance à être solide, bien que très verbeux.

Invocation

Faites en sorte que votre script accepte les options longues et courtes. soyez prudent car il y a deux commandes pour parsingr les options, getopt et getopts. Utilisez getopt lorsque vous rencontrez moins de problèmes.

CommandLineOptions__config_file="" CommandLineOptions__debug_level="" getopt_results=`getopt -s bash -oc: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 retourner zéro s’il est terminé avec succès, non nul 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 que renvoyer 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 est correctement exécutée. Vous pouvez également générer des exceptions si vous retournez une valeur différente de zéro, puis définir 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.

Quand vous appelez une fonction, les parameters de la fonction sont assignés aux vars spéciaux $ 0, $ 1 etc. Je vous suggère de les mettre dans des noms plus significatifs. déclarer les variables à l’intérieur de la fonction comme locales:

 function foo { local bar="$0" } 

Situations sujettes aux erreurs

En bash, sauf indication contraire, une variable non définie est utilisée comme 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 sera évaluée comme vide. utilisation

 set -o nounset 

pour empêcher que cela se produise. Attention cependant, car si vous faites cela, le programme abandonnera 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 “de type 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 trait de soulignement comme sharepoint 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 object

En bash, vous ne pouvez pas faire de programmation orientée object, sauf si vous construisez un système assez complexe d’allocation d’objects (j’ai pensé à cela, c’est faisable, mais fou). En pratique, vous pouvez cependant faire de la “programmation orientée singleton”: vous avez une instance de chaque object, et une seule.

Ce que je fais, c’est: je définis un object dans un module (voir l’entrée de modularisation). Puis 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 manutention

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

Conseils 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.

ne même pas envisager de travailler avec tcsh. il ne supporte pas les fonctions, et c’est horrible en général.

J’espère que cela aide, même s’il vous plaît 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 Je devais l’utiliser en raison de facteurs humains et de l’inheritance.

Jetez un coup d’œil au Guide avancé des scripts Bash pour obtenir beaucoup de sagesse sur les scripts shell – et pas seulement Bash.

N’écoutez pas les gens vous dire de regarder d’autres langues, sans doute plus complexes. Si les scripts shell répondent à vos besoins, utilisez-les. Vous voulez la fonctionnalité, pas la fantaisie. De nouveaux langages apportent 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à le 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.

Le script shell 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 généraliste, alors essayez toujours de coller la logique des utilitaires existants plutôt que de recréer une nouvelle logique dans un script shell.

Autre que ce principe général, j’ai collecté des erreurs de script shell courantes .

Il y a eu une excellente session à OSCON cette année (2008) sur ce sujet: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf

Facile: utilisez python au lieu de scripts shell. Vous obtenez une facilité de lecture presque 100 fois plus grande, sans avoir à compliquer quelque chose dont vous n’avez pas besoin, et vous conservez la possibilité de transformer des parties de votre script en fonctions, objects, objects persistants (zodb), objects dissortingbués (pyro) pratiquement sans aucune code supplémentaire

utilisez set -e pour ne pas avancer après les erreurs. Essayez de le rendre compatible sans vous baser sur bash si vous voulez qu’il fonctionne sur non-linux.

Savoir quand l’utiliser Pour des commandes de collage rapides et sales, ça va. Si vous avez besoin de faire plus que quelques décisions non sortingviales, boucles, n’importe quoi, optez pour Python, Perl et modularize .

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 croissance… et vous ne pouvez pas vous en débarrasser car votre projet tout entier en dépend. Bien sûr, cela a commencé à 40 lignes de belle bash.

Pour trouver des “meilleures pratiques”, regardez comment les dissortingbutions 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 parameters de configuration, des fichiers de bibliothèque et du formatage des sources.

Mon style personnel consiste à écrire un script maître qui définit certaines variables par défaut, puis essaie de charger (“source”) un fichier de configuration pouvant contenir de nouvelles valeurs.

J’essaie d’éviter les fonctions car elles ont tendance à compliquer le script. (Perl a été créé à cet effet.)

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

Ou l’ancienne citation similaire à ce que Joao a dit:

“Utilisez perl. Vous voudrez savoir bash mais ne pas l’utiliser.”

Malheureusement, j’ai oublié qui a dit ça.

Et oui ces jours-ci je recommanderais python over perl.