Constructions « with » en langage… bash ! – Partie 2/2

Mais en réalité, en deuxième paramètre, on n’est pas restreint aux seuls identifiants de signaux : la commande help trap (voir encadré) nous indique quelques possibilités supplémentaires. En particulier, on peut spécifier EXIT pour détecter la sortie du script, et donc exécuter un code adéquat juste avant de sortir.


Pourquoi « help trap » et pas « man trap » ?

Certaines commandes, comme trap, sont internes au shell. La preuve :

Il n’y a pas d’exécutable nommé trap sur le système. C’est juste bash qui l’interprète. Par conséquent, la documentation de la commande trap s’obtient par… man bash ! Le souci, c’est que la page de manuel de bash est un peu longuette. C’est pour ça que je vous proposais d’utiliser l’aide en ligne de bash, en tapant help <cmd>.

Pour corser un peu le tout, sachez qu’il y a aussi des commandes qui sont à la fois disponibles sur le système ET implémentées en interne par bash. C’est le cas de la commande echo par exemple :

Quel intérêt me direz-vous ? La plupart du temps, c’est une question de performance. Si le shell utilise la commande système, alors il lui faut créer un processus fils à chaque fois pour la lancer, ce qui est quand même très coûteux pour une commande aussi basique. Le problème, c’est qu’il peut y avoir quelques différences subtiles entre les deux implémentations. Et au final, si vous tapez man echo, vous ne regardez sûrement pas la bonne documentation ! Il y a d’ailleurs une indication dans cette page de manuel qui devrait vous alerter à ce sujet.


Rajoutons donc le code suivant à la fin de with_bloc.sh :

Voilà, je crois que ce code est assez évident : en sortie du script, on referme les éventuels blocs with restés ouverts. Testons :

Cette fois-ci, ça fonctionne !

2.3 Le test de trop

Il ne reste plus qu’une chose à tester : les bloc with imbriqués. Mais normalement, avec notre gestion basée sur une pile, ça devrait rouler.

Bon voilà, j’ai réécrit la fin de test.sh. Voilà les dernières lignes :

Je vous laisse imaginer le code de ouvrir_tiroir(), defaire_ouvrir_tiroir(), prendre_telephone() et defaire_prendre_telephone(). Voyons ce que ça donne à l’exécution, juste avec 2 clous pour commencer :

?? Qu’est-ce que c’est que ce bazar ? Y aurait-il un souci dans ma gestion de pile ?1

Un bon quart d’heure plus tard

Suis-je idiot ! J’ai juste oublié d’écrire les end_with !! Je pouvais toujours chercher un bug dans ma gestion de pile !

Trois balises end_with rajoutées plus tard…

C’est reparti pour le test :

C’est mieux, beaucoup mieux ! Et avec la vis :

Impeccable, on a juste refermé les with ouverts, avant de quitter à cause de l’exception.

3. Une intégration plus poussée dans le langage

Cette petite mésaventure d’oubli des end_with m’a fait prendre conscience d’une limite importante de mon implémentation. En Python, la fermeture du bloc with est implicite, elle s’effectue automatiquement quand on quitte le bloc indenté. De ce fait, il n’y a pas de mot-clé comme end_with à indiquer (ou à oublier, en l’occurrence). En bash, l’indentation n’influe pas sur la syntaxe, mais on a quand même des vérifications de syntaxe bien utiles sur certaines constructions. Par exemple, dans une boucle while, si jamais on oublie le done final, on aura une erreur de syntaxe, ce qui a l’avantage d’aiguiller directement le programmeur sur le bon diagnostic.

Avec mon implémentation actuelle, il paraît compliqué de déclencher une telle erreur de syntaxe. Mais si on la fait évoluer un peu, c’est peut-être possible. Je crois d’ailleurs que j’ai une petite idée qui se dessine, un genre de détournement de la boucle while

3.1 Une boucle while contrôlée

Commençons par un petit test avec le script suivant, boucle_controlee.sh :

Si on identifie les itérations du while (variable entree_boucle), on peut la « contrôler » : on décide de passer le premier test, donc d’exécuter le contenu du bloc, mais d’arrêter au deuxième test. Voilà ce que ça donne à l’exécution :

3.2 Un alias pour cacher ce que vous n’êtes pas censés voir

Si vous ne voyez pas où je veux en venir, modifions légèrement la chose en ajoutant la définition d’un alias :

Là c’est clair, non ? Il est pas beau ce bloc with…do…done ? Et là, si on oublie de fermer le bloc avec le done, on aura effectivement une erreur de syntaxe !

On peut vérifier, à l’exécution ça marche toujours :

Il reste deux points à éclaircir. Le premier, c’est l’emploi bizarre de la variable entree_boucle, initialisée avant la boucle while (dans l’alias) et ensuite modifiée dans la fonction boucle_controlee(). En fait, on est tenté d’implémenter ce contrôle de la boucle dans la fonction uniquement, en alternant entre deux états, pour gérer respectivement l’entrée et la sortie du bloc. En réalité, ce serait une erreur, car l’entrée et la sortie correspondent à deux appels distincts de cette fonction. Or, en cas d’imbrication de blocs with, on casse cette alternance entre entrée et sortie de bloc. Au final, je me suis donc contenté de simplement repérer l’entrée dans la boucle.

Le deuxième point concerne l’activation de l’option expand_aliases. Il faut savoir que, par défaut, bash n’interprète les alias que lors d’une session interactive. Ici, s’agissant de l’exécution d’un script, ce n’est pas le cas. Il faut donc activer cette option pour que l’alias soit pris en compte.

3.3 Implémentation finale

Notre implémentation finale de with_bloc.sh va reprendre les éléments de notre première implémentation, ainsi que l’idée développée ci-dessus.

La fonction with() de notre première implémentation a été renommée en start_with(). Excepté ce renommage, elle est inchangée. La fonction end_with() est également inchangée, tout comme le code qui ferme les blocs restés ouverts en cas d’exception (on_exit() et commande trap). La fonction boucle_controlee() a été très légèrement modifiée pour appeler les fonctions start_with() et end_with(). Enfin, la définition de l’alias reprend ce qu’on a écrit ci-dessus.

Pour tester cette version, on pourra adapter notre script de test :

En testant, avec ou sans la vis, on retrouve des résultats identiques à notre première implémentation. Par contre, si on oublie de fermer un des blocs avec done, l’interpréteur détecte le souci de syntaxe :

Pour tout vous dire, c’est bien la première fois que je suis content de voir s’afficher une erreur de syntaxe. 🙂

Épilogue

Deux ans plus tard…

Après avoir implémenté cette gestion dans debootstick [6], j’ai pu débuguer plus rapidement. Parce qu’un outil qui met le bazar sur votre machine à chaque plantage, c’est plutôt pénible à débuguer ! D’ailleurs, phase de débugage ou pas, cela reste un outil qui enchaîne les opérations bas niveau sur votre système. Ce « filet de sécurité » est donc plutôt bienvenu.

Peu de temps après, j’ai donc pu fournir une première version, capable de prendre en charge les scénarios décrits au début de l’article. Et quelques autres joyeusetés. L’outil est maintenant disponible dans l’OS Debian (testing), depuis l’été 2015. Vous pourrez donc le trouver dans la prochaine version stable de Debian [7], « stretch », qui sera peut-être sortie quand vous lirez ces lignes.

Pour la petite histoire, quand on construit un paquet pour Debian on doit faire tourner lintian, un outil qui vérifie les sources. Comme je l’ai laissé entendre au tout début de l’article, cette construction bizarre n’est alors pas passée inaperçue ! J’ai dû ajouter ce qu’on appelle un « lintian override », une annotation indiquant à l’outil que ce qu’il a détecté n’est pas un souci.

Merci à Henry-Joseph pour la relecture !

Notes et références

[1] Vous avez pu entrevoir dans un article récent [5] cette suite d’opérations à effectuer. Notons toutefois que debootstick vise à créer des systèmes live pérennes sur le long terme, la structure de l’OS est donc plus simple (pas de squashfs), ce qui permet des mises à jour complètes (bootloader & noyau compris). D’autre part, debootstick ne travaille pas directement sur votre clé USB, il construit une image ; vous pourrez donc la tester au préalable avec kvm. En effet, flasher un OS sur une clé USB n’est pas un acte anodin vis-à-vis de la durée de vie de celle-ci.
[2] Dans la plupart des langages, pour implémenter une pile, on se base sur un tableau. Mais à mon avis, en bash, l’usage des tableaux est plutôt cryptique, surtout pour des opérations comme « supprimer le dernier élément du tableau ». Alors je ne vais pas souiller votre magazine préféré avec ce genre d’incantations.
[3] Si c’était un « article dont vous êtes le héros », ici il y aurait une première fin possible 😉
[4] L’avis de mon relecteur est plutôt que le script a développé une intelligence propre et déduit qu’avec deux mains un humain peut très bien prendre le téléphone sans lâcher le marteau…
[5] ENDRES F., « Live-System from Scratch », GNU/Linux Magazine n°202, mars 2017, p. 54 à 61.
[6] Page GitHub de debootstick : https://github.com/drakkar-lig/debootstick
[7] Package debootstick dans Debian Stretch : https://packages.debian.org/stretch/debootstick

Etienne Dublé
[Ingénieur de Recherche CNRS au LIG]

Relu par H.-J. Audéoud

Retrouvez cet article (et bien d’autres) dans GNU/Linux Magazine n°204, disponible sur la boutique et sur la plateforme de lecture en ligne Connect !

Laisser un commentaire