Est-ce que quelqu'un connaît des ressources qui parlent de meilleures pratiques ou de 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 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 excessif, mais tend à être solide, bien que très verbeux.
Invocation
faites votre script accepter les options longues et courtes. faites attention car il y a deux commandes pour analyser 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 -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 est terminé avec succès, différent de zéro en cas de problème.
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 s'est terminée correctement. En outre, vous pouvez générer des exceptions si vous renvoyez une valeur différente de zéro, puis définir deux "variables d'exception" (mine: 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 affectés aux vars spéciaux $ 0, $ 1, etc. Je vous suggère de les attribuer à des noms plus significatifs. déclarer les variables à l'intérieur de la fonction en tant que locales:
function foo {
local bar="$0"
}
situations sujettes aux erreurs
Dans bash, sauf indication contraire de votre part, 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 étant vide. utilisation
set -o nounset
pour empêcher que cela se produise. Soyez prudent cependant, car si vous faites cela, le programme sera annulé chaque fois que vous évaluerez une variable non définie. Pour cette raison, le seul moyen de vérifier si une variable n'est pas définie est le suivant:
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"
Modularisation
Vous pouvez obtenir une modularisation "python like" 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"
Ce 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 risqueriez des conflits de noms. J'utilise un double trait de soulignement comme python dot.
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 cinglé). 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: i définit un objet dans un module (voir l'entrée de modularisation). Ensuite, je définis des vars vides (analogues à des 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
}
Détection et traitement des signaux
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’envisage même pas 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'héritage.
Jetez un coup d'œil au Guide de script de Bash avancé pour plus de sagesse concernant les scripts Shell - et pas seulement Bash.
N'écoutez pas les gens vous dire de regarder d'autres langages, sans doute plus complexes. Si les scripts Shell répondent à vos besoins, utilisez-les. Vous voulez des fonctionnalités, pas de fantaisie. Les nouvelles langues apportent de nouvelles compétences utiles à votre CV, mais cela n’aide en rien si vous avez un 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 différentes consignes et préjugés - 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 très utile pour cela, il ne s’agit pas d’un langage à usage général, essayez donc toujours de coller la logique à partir d’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é quelques erreurs de script Shell courantes .
Il y a eu une excellente session à OSCON cette année (2008) sur ce sujet uniquement: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
Facile: utilisez python à la place des scripts Shell. Vous obtenez une lisibilité multipliée par 100, sans avoir à compliquer tout ce dont vous n’avez pas besoin, tout en préservant la possibilité de faire évoluer certaines parties de votre script. fonctions, objets, objets persistants (zodb), objets distribués (pyro) presque sans code supplémentaire.
Sachez quand l'utiliser. Pour les commandes de collage rapides et sales, tout va bien. Si vous avez besoin de prendre plus que quelques décisions non triviales, des boucles, n’importe quoi, choisissez Python, Perl et modularise.
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 tout votre projet en dépend maintenant. Bien sûr, ça a commencé à 40 lignes de belle bash.
utilisez set -e pour ne pas avancer après des erreurs. Essayez de le rendre compatible sans faire confiance à bash si vous voulez que cela fonctionne sur non-linux.
Pour trouver certaines "pratiques recommandées", observez comment les distributeurs Linux (Debian, par exemple) écrivent leurs scripts d'initialisation (généralement situé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 de la source.
Mon style personnel est d'écrire un script maître-shell qui définit certaines variables par défaut, puis de charger ("source") un fichier de configuration pouvant contenir de nouvelles valeurs.
J'essaie d'éviter les fonctions car elles ont tendance à rendre le script plus compliqué. (Perl a été créé à cet effet.)
Pour vous assurer que le script est portable, testez non seulement avec #!/Bin/sh, mais utilisez également #!/Bin/ash, #!/Bin/dash, etc. Vous détecterez assez tôt le code spécifique à Bash.