The Daily Shaarli
Je souhaite améliorer un script bash existant. Le principal problème est qu'il utilise n fois une même commande genre mysql -u <login> -p<mdp> -B -N. Il n'y a que le -e <ma_requête_SQL> qui change entre chaque commande.
J'ai justement un argument à changer… dans chaque appel à la commande. Bon, un coup de sed et le travail est fait, mais je me dis que je vais factoriser tout ça.
Je crée une variable MYSQLCMD='/usr/bin/mysql -B -N -u <login> -p<mdp>' et je remplace toutes les commandes de la forme mysql -u <login> […] -e "ma_requête_SQL> par $MYSQLCMD -e ma_requête_SQL>.
Je teste… Ça fonctionne partout… sauf à plusieurs endroits du script. L'erreur est : « mon_script.sh: ligne 138: /usr/bin/mysql -B -N -u root -p<CENSURÉ>: Aucun fichier ou dossier de ce type ». On dirait que bash cherche une commande qui se nomme « /usr/bin/mysql -B -N -u root -p<CENSURÉ> » (espaces incluses). La commande /usr/bin/mysql existe, mais pas une commande /usr/bin/mysql -B -N -u root -p<CENSURÉ> puisqu'il s'agit d'un mélange d'une commande, de ses arguments et des valeurs de ses arguments.
set -x affiche + '/usr/bin/mysql -B -N -u root -p<CENSURÉ>' -e 'SHOW DATABASES;'. On constate que la variable $MYSQLCMD est développée comme un seul tenant… alors que sa valeur est assignée à l'aide de simples quotes, et non de guillemets. Et, surtout, ce comportement se produit uniquement à partir d'un certain endroit dans le script. Dit autrement, un $MYSQLCMD -e ma_requête_SQL> en début ou en milieu de script fonctionne très bien.
Solution : un bout de code traînait. Un bout de code tout simple :
IFS='
'
Ainsi, le séparateur des champs a été modifié. Sa valeur est passé de espace + tab + line feed à juste line feed (saut de ligne). Évidemment, cela a été fait dans une intention bien précise : itérer sur chaque ligne d'un résultat d'une requête SQL avec une boucle for… Illustration :
IFS='
'
for line in $(mysql -B -N -u <login> -p<mdp> -D <base> -e <requête_sql) do
[travail sur $line]
done
Sans la modification du séparateur, la boucle itère sur chaque colonne du résultat au lieu d'itérer sur chaque ligne du résultat.
Évidemment, bidouiller IFS est une mauvaise pratique. Dans ce contexte-là, on préférera utiliser une boucle while read. ;)
Virer cette modification du séparateur résout le problème. \o/
Parfois, j'ai besoin d'écrire des scripts shell banals qui enchaînent des commandes qui dépendent les unes des autres (si l'une échoue, il ne faut pas exécuter les autres) et qui sont convenablement verbeuses pour ne pas justifier un traitement des erreurs. Un unique code d'erreur peut être retourné, quelle que soit la commande qui échoue. En termes d'algorithmique, ça se représente environ comme cela :
Si commande1 termine avec succès
{
Si commande2 termine avec succès
{
Si commande3 termine avec succès
{
Sortir avec le code retour 0
}
}
}
Afficher 'ERREUR ! SORTIE PRÉMATURÉE !'
Sortir avec le code retour 1
Une méthode qui vient assez intuitivement à l'esprit pour traiter cela est : commande1 && command2 && commande3 && exit 0 || exit 1. Cela fonctionne, mais c'est peu pratique quand on enchaîne beaucoup de commandes, que les commandes prennent des tas d'arguments voire des redirections de texte dans leur entrée standard, ou que l'on souhaite documenter un peu ce que fait notre enchaînement de commandes.
Bash propose deux commandes internes bien pratiques :
set -e, qui permet de terminer immédiatement le script dès qu'une commande (ou un ensemble de commandes, voir le manuel) échoue ;trap <ma_commande> <signal>, qui permet d'exécuter une commande lorsqu'un signal est reçu (on dit « capturer un signal », d'où son nom ;) ).set -e lève le signal ERR. Donc, il est possible de le capturer avec trap <ma_commande> ERR. Il devient donc possible d'afficher un message et de positionner un code d'erreur immédiatement lorsqu'une commande parmi un enchaînement échoue.
Appliquons cela à notre algorithme précédent :
#!/bin/bash
set -e
trap "echo 'ERREUR ! SORTIE PRÉMATURÉE !' && exit 1" ERR
commande1
commande2
commande3
exit 0
Sous ce format, il est parfaitement possible d'ajouter des commentaires voire des affichages (echo) pour des groupes de commandes qui le méritent (« Étape 1 : je fais ceci », « Étape 2 : je fais cela », etc.).
Si les commandes sont convenablement verbeuses, alors, en cas d'erreur, on lira « Étape 1 : je fais ceci » suivi de l'affichage de la commande suivi de « ERREUR ! SORTIE PRÉMATURÉE ! ». Difficile de ne pas comprendre qu'une erreur s'est produite dans le traitement de l'étape 1.
Je lis parfois cette mécanique utilisée avec trap EXIT. Je trouve cela moins pratique car un exit 0 déclenche tout autant la commande. Il est donc nécessaire d'arrêter de capturer le signal EXIT à la fin du script avec trap - EXIT, ce qui fait une commande de plus à mémoriser et à saisir.
Attention : comme le souligne ban, trap est inhibée par l'entrée dans une fonction. Le programme s'arrêtera immédiatement en cas d'erreur dans la fonction, mais l'action définie avec trap ne sera pas réalisée, sauf à re-définir trap à l'entrée de chaque fonction. On peut parfaitement écrire une fonction qui appelle trap puis appeler cette fonction au début du programme et de chacune de ses fonctions. Mais c'est du bricolage. Pour ma part, je confine mon utilisation de set -e + trap au cas d'usage présenté dans ce shaarli.
