Sommaire
Une des difficultés principales dans la programmation shell est la gestion des erreurs dans l'utilisation des tubes (pipes) qui sont pourtant au cœur de la programmation shell, et la plupart des interfaces fournies dans nos langages de programmation préférés ne font rien pour améliorer la situation: la règle générale est que soit les erreurs fans les sous-processus sont ignorées, soit il faut mettre en place une infrastructure assez lourde pour récupérer ces erreurs.
La solution à ce problème que j'explore avec Rashell est l'emploi judicieux des monades pour avoir les bénéfices et d'une gestion fine des erreurs et une interface aussi légère que possible.
La monade Success
Bien que les monades puissent être utilisées dans presque tous les langages modernes, pour peu qu'il aient des fonctions d'ordre supérieur et des fermetures ou des fonctions locales, le concept est souvent peu familier aux programmeurs qui ne pratiquent pas la programmation fonctionnelle. Rappelons-donc rapidement ce dont il s'agit.
Tout d'abord les monades sont un design pattern – ce ne sont donc pas des objets du programme comme le sont les listes ou les fonctions – ce qui, conjugué à un vocabulaire fleuri qui parle de “valeur monadique” ou “calcule dans une monade” et à la pédanterie de quelques uns, a probablement contribué à leur réputation, largement injustifiée, d'être un sujet très difficile. Le plus simple consiste à regarder un exemple, disons success – une monade que les pessimistes appellent error.
Comme toutes les monades , la monade success étend un type de départ en en type dit monadique. Dans le cas particulier de success cette extension se fait en ajoutant une condition d'erreur, ici représentée par un message d'erreur:
type 'a t = (* 'a dénote le type de base *)
| Success of 'a
| Error of string
L'extension est concrétisée par l'opérateur return définit par let return x = Success(x)
et pour compléter la monade on a encore besoin d'un opérateur dit bind définit ainsi
let bind m f = match m with
| Success(x) -> f x
| Error(_) -> m
Le type a t
se comprend comme une sorte de tube dans lequel on peut lire une valeur de type 'a
(information in band) mais qui peut aussi signaler une erreur si le calcul de la valeur rate et porte donc une information secondaire (information out of band). L'opérateur bind est simplement un opérateur d'application de fonction ajusté à ce contexte: si notre calcul a fonctionné, nous pouvons passer à l'étape suivante, sinon nous devons nous abstenir de calculer et simplement faire remonter l'erreur.
La monade success fait à peu près la même chose que les exceptions, qui peuvent aussi porter une erreur dans la partie out of band de la communication au sein du programme. La différence essentielle – et qui contribue à rendre les monades si intéressantes – est dans la gestion des erreurs: si on n'y prend garde la gestion des erreurs par exceptions ne produit que du code spaghetti – après tout, une exception est une sorte d'exception, tandis que dans la monade success on examine la valeur finale du calcul qui est soit Success(x)
pour un calcul réussi soit Error(mesg)
pour un calcul raté: la gestion des deux conditions se fait au même endroit, il n'y a pas d'effort spécial à faire pour écrire du code maintenable. J'ai écrit une implémentation de la monade Success dans la bibliothèque Lemonade.
Les monades utilisés par Rashell
Rashell s'appuie sur la monade Lwt (pour lightweight cooperative threads) qui définit deux structures importantes:
Les valeurs monadiques
'a Lwt.t
elles-mêmes, qui représentent un fil d'exécution calculant une valeur de type'a
– comme dans la monade success on a aussi une information out of band indiquant le succès du calcul.Les flux
'a Lwt_stream.t
dont l'opérateur de lecture livre une valeur monadique'a Lwt.t
.
Ces structures permettent de définir les opérations suivantes, qui encapsulent l'appel à un sous-processus – dans ce qui suite le type t
représente une commande, grosso modo un argv:
val exec_utility : t -> string Lwt.t
pour une commande qui renvoie une information unique, commeuname
ou biencurl
.exec_test : t -> bool Lwt.t
pour les commandes dont le code de retour est interprété comme un prédicat, commegrep
par exemple.exec_query : t -> string Lwt_stream.t
pour les commandes dont le résultat est à interpréter ligne à ligne, par exemple la commandefind
, ou biensed
, etc.exec_filter : t -> string Lwt_stream.t -> string Lwt_stream.t
pour les commandes utilisées pour réécrire un flux, i.e. comme un filtre Unix.
Lorsqu'une commande rate, le fil d'éxécution correspondant retourne une erreur, indiquant la commande qui a échoué et son statut d'erreur.
Un exemple
À titre d'exemple voici l'implémentation de la fonction tags dans les wrappers pour docker. Cette fonction examine le dépôt local et calcule une liste associative dont les clefs sont des images et les valeurs la liste des tags qu'elles portent.
let tags () =
let triple_of_alist alist =
let get field = List.assoc field alist in
try (get "IMAGE ID", (get "REPOSITORY", get "TAG"))
with Not_found -> failwith(__MODULE__^": images: Protocol mismatch.")
in
let pack lst =
let images =
Pool.elements(List.fold_right Pool.add (List.map fst lst) Pool.empty)
in
List.map
(fun x -> (x, List.map snd (List.filter (fun (k,_) -> k = x) lst)))
images
in
Lwt_stream.to_list
(exec_query
(command ("", [| ac_path_docker; "images"; "--all=true"; "--no-trunc=true"; |])))
>>= to_alist "images" image_keyword
>>= Lwt.wrap1 (List.map triple_of_alist)
>|= List.filter
(fun (_,(container, tag)) -> container <> "<none>" && tag <> "<none>")
>|= pack
Par exemple dans mon toplevel, j'obtiens:
# Lwt_main.run (Rashell_Docker.tags());;
- : (string * (string * string) list) list =
[("0ecdc1a8a4c9eb53830ec59072a7f5dd7bf69c6077f60215cf4a99cd351dd5a1",
[("redis", "3.0.2")]);
("272056a49fd13d5a711dbab6629c715ef9178aeec90f5d634134964e3cf38f2a",
[("michipili/ubuntu-precise", "latest")]);
("5c9464760d54612edf1df762d13207117aa4480b2174d9c23962c44afaa4d808",
[("mongo", "3.0"); ("mongo", "3.0.6"); ("mongo", "latest"); ("mongo", "3")]);
("63e3c10217b8ff32018e44ddd9e92dc317dc7fff204e149fac1efb6620490e7a",
[("ubuntu", "14.04.2"); ("ubuntu", "trusty-20150730")]);
("66b43e3cae49068cb0f2bc768f76adca5e3dfd3269b608771a6f139d3f568073",
[("mongo", "3.0.4")]);
("6d4946999d4fb403f40e151ecbd13cb866da125431eb1df0cdfd4dc72674e3c6",
[("ubuntu", "trusty-20150612")]);
("78cef618c77e86749226ad7855e5e884a7bdbd85fa1c9361b8653931b4adaec5",
[("anvil/test", "latest"); ("ubuntu", "precise");
("ubuntu", "precise-20150612"); ("ubuntu", "12.04.5");
("ubuntu", "12.04")]);
("91e54dfb11794fad694460162bf0cb0a4fa710cfa3f60979c177d920813e267c",
[("ubuntu", "trusty"); ("ubuntu", "trusty-20150814"); ("ubuntu", "14.04");
("ubuntu", "14.04.3"); ("ubuntu", "latest")]);
("9216d5a4eec8459d0bdcc4e13ef45b5e6e6cf3affae59bb3a8673525cbc36118",
[("redis", "latest"); ("redis", "3"); ("redis", "3.0"); ("redis", "3.0.3")]);
("ab57dbafeeeabc3c108245c3582391c5f1c50630e9fd253d499f66c68fde9d50",
[("ubuntu", "utopic"); ("ubuntu", "14.10"); ("ubuntu", "utopic-20150612")]);
("bf84c1d84a8fbea92675f0e8ff61d5b7f484462c4c44fd59f0fdda8093620024",
[("debian", "jessie"); ("debian", "latest"); ("debian", "8");
("debian", "8.1")]);
("c6f67f622b2aa9753a2a97adac1906320a62e9f529b80c4367ed0b11f505d660",
[("mongo", "3.0.5")]);
("f38e3980050a2f5e2550b94fd2787931bb316671d89f62b29e9612ba80999e29",
[("anvil/ubuntu-precise", "latest")])]
Idées de projets
Voici quelques idées de projets, du moins ambitieux au plus ambitieux, pour ceux qui aimeraient s'initier à la programmation shell avec OCaml et Rashell! Rashell peut-être très facilement installé avec opam et le pinning, c'est décrit dans le fichier README du projet.
- Écrire une ramasse-miette docker qui élimine tous les vieux containers stoppés.
- Écrire plus de tests dans la test-suite de Rashell
- Traduire quelques-uns de vos scripts maisons en Rashell
- Écrire un wrapper pour GNU Make et pour BSD Make.
- Écrire des wrappers GNU Tar pour Rashell
- Écrire des wrappers pour curl
- Écrire des wrappers pour GPG
- Écrire des wrappers pour apt dpkg debuild et git buildpackage (pour les Debianistes)
- Écrire des wrappers pour pkg pw, fetch, dump, restore et jail (pour les FreeBSD istes)
- Écrire un wrapper git pour Rashell (j'ai déjà commencé une branche).
- Écrire des wrappers pour TeX et METAPOST (déjà commencé aussi)
- Écrire des wrappers pour yum.
- Écrire des wrappers pour svn
- Écrire des wrappers pour noweb l'outil de programmation lettrée de Norman Ramsey
- Écrire des wrappers pour GraphicsMagick
- Écrire des wrappers pour pdftk
- Écrire des wrappers pour sox
- Écrire des wrappers pour pov
- Écrire des wrappers pour gcc ou clang
- Écrire des wrappers pour les outils de préparation des fichiers djvu
- Écrire des wrappers pour VirtualBox, aws, gcloud, ou autre (déjà commencé aussi)
L'écriture de wrappers est en principe assez facile et permet de réfléchir à la préparation d'interface agréables.
# Commentaire supprimé
Posté par Anonyme . Évalué à 10.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Exagération
Posté par Michaël (site web personnel) . Évalué à 3.
Dans la pratique, quand on écrit de relativement gros (2kloc) programmes en shell, la gestion des erreurs pose beaucoup de problèmes, non seulement à cause de pipes mais aussi des sous-shells, etc. Le shell est à mes yeux essentiellement un langage de prototypage.
Dans le cas particulier que je cite, je ne peux de toutes façons pas utiliser bash parceque sa gestion du job control est trop bugguée.
[^] # Re: Exagération
Posté par claudex . Évalué à 10.
Le problème est peut-être là.
« Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche
[^] # Re: Exagération
Posté par Ontologia (site web personnel) . Évalué à 2.
En effet, de ce que j'ai pu voir des pratiques en informatiques de gestion, on a deux cas :
Note : Je fais du OCaml régulièrement, dès le second paragraphe, ma tête m'a fait "oulàlà, trop réfléchir pour comprendre".
« Il n’y a pas de choix démocratiques contre les Traités européens » - Jean-Claude Junker
[^] # Re: Exagération
Posté par Dabowl_75 . Évalué à 7.
C'est marrant cette propension qu'ont les gens à croire que tout Unix en dehors de Linux est forcément vieux et obsolète, alors que rien n'est plus faux.
Tu es probablement tombé sur des contextes particuliers où par manque de compétences en interne, des machines étaient laissées à l'abandon…
Sous AIX, on fait aussi du python, du ruby, on a aussi des outils modernes…ansible, chef, etc etc…
Quant à OCaml sous AIX, il y a des gens qui s'y essayent mais personnellement je n'en ai pas l'utilité donc je ne saurai te répondre.
Ne généralisons pas….
[^] # Commentaire supprimé
Posté par Anonyme . Évalué à 5.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Exagération
Posté par Michaël (site web personnel) . Évalué à 1.
Cela veut dire que les programmes écrits pout le shell sont – sauf rare exception – des prototypes qui ont vocation å être transposé dans un autre langage pour être pérennisés.
[^] # Re: Exagération
Posté par lolop (site web personnel) . Évalué à 6.
Perso quand je parts vers un script shell, c'est pour un besoin identifié où celui-ci sera bien adapté, typiquement des séquences de commandes de manipulations de fichiers ou de process avec de le logique autour. Et une fois écrit et fonctionnel, on laisse tourner.
Si j'ai à faire un proto d'un soft plus conséquent, je parts plutôt vers un langage de script… et là c'est pas obligatoirement pour changer de langage si celui utilisé va bien.
Votez les 30 juin et 7 juillet, en connaissance de cause. http://www.pointal.net/VotesDeputesRN
[^] # Re: Exagération
Posté par Michaël (site web personnel) . Évalué à 1.
Pour beaucoup de tâches d'administration le shell est un choix naturel pour écrire un prototype, car il permet d'obtenir des résultats très rapidement (la fameuse loi du 80/20 qui dit qu'on développe 80% des fonctionnalités en 20% du temps de développement). Ensuite lorsqu'il s'agit de maintenir, améliorer, etc. ce prototype, cela peut-être utile de passer à un langage plus évolué.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.