URL: https://linuxfr.org/users/chat_de_sorciere/journaux/rashell-bibliotheque-de-programmation-shell-resiliante-pour-ocaml Title: Rashell, bibliothèque de programmation shell résiliante pour OCaml Authors: Michaël Date: 2015年09月26日T03:29:36+02:00 License: CC By-SA Tags: rashell, ocaml, monad et shell Score: 18 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](https://github.com/michipili/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: ```ocaml 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 ```ocaml 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](https://github.com/michipili/rashell). ## Les monades utilisés par Rashell **Rashell** s'appuie sur la monade **Lwt** (pour *lightweight cooperative threads*) qui définit deux structures importantes: 1. 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. 2. 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*: 1. `val exec_utility : t -> string Lwt.t` pour une commande qui renvoie une information unique, comme `uname` ou bien `curl`. 2. `exec_test : t -> bool Lwt.t` pour les commandes dont le code de retour est interprété comme un prédicat, comme `grep` par exemple. 3. `exec_query : t -> string Lwt_stream.t` pour les commandes dont le résultat est à interpréter ligne à ligne, par exemple la commande `find`, ou bien `sed`, etc. 4. `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. ```ocaml 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 "" && tag "") >|= pack ``` Par exemple dans mon toplevel, j'obtiens: ```ocaml # 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.

AltStyle によって変換されたページ (->オリジナル) /