• # Rashell

    Posté par (site web personnel) . En réponse au journal Gufo: un langage de shell moderne!. Évalué à 9.

    Puisque tu programmes en OCaml je te signale l'existence de Rashell, une bibliothèque qui permet de facilement interagir avec des programmes externes à la manière de ce que le shell propose. Tu peux l l'essayer avec opam: opam install rashell.

    https://github.com/michipili/rashell

    En gros je reconnais 3-4 modes d'interaction typiques avec les programmes externes, comme par exemple l'utilitaire qui nous intéresse pour son effet de bord ou sa sortie:

    (** Execute the given command and return its exit status, the content
     of stdout and of stderr. *)
    val exec_utility : t -> string Lwt.t

    La recherche qui produit ses résultats en mode ligne à ligne:

    (** Execute the given command and return a stream reading the output
     of the command and its exit status with error output. *)
    val exec_query : t -> string Lwt_stream.t

    Le test qui nous intéresse par son code de retour

    (** Execute the given command and test its exit status. An exit status
     of [0] indicates success of the test, [1] indicates failure of the
     test and other values indicate a general error. *)
    val exec_test : t -> bool Lwt.t

    et le filtre, qui permet de passer un flot de lignes à travers in filtre UNIX:

    (** Execute the given command and return a function mapping streams,
     and its exit status with error output. *)
    val exec_filter : t -> string Lwt_stream.t -> string Lwt_stream.t

    Avec ces nouvelles primitives, il est facile d'écrire des interfaces bien typées pour plein de commandes Unix, voir le dossier [src](https://github.com/michipili/rashell/tree/master/src) dans le code du projet. Par exemple Rashell_Posix fournit une interface à quelques utilitaires définis par POSIX (find, test, cp, rm, mv, ln, sed, awk, df and du) des fonctions pour utiliser des dossiers ou des fichiers temporaires, ainsi que des interfaces à quelques fonctions de git ou de docker. Par exemple voici le code de Rashell_Git.topleveldir qui retourne le dossier racine d'un dépôt git:

    let topleveldir ?workdir () =
     exec_utility ~chomp:true
     (command ?workdir ("", [| ac_path_git; "rev-parse"; "--show-toplevel" |]))

    et celui de la commande Rashell_Git.branch_current qui donne le nom de la branche visitée, ou signale une exception lorsque la copie de travail est en "detached HEAD state":

    let branch_current ?workdir ?env () =
     let argv = [| ac_path_git; "rev-parse"; "--abbrev-ref"; "HEAD" |] in
     exec_utility ~chomp:true (command ?workdir ?env (ac_path_git, argv))
     >>= function
     | "HEAD" -> Printf.ksprintf Lwt.fail_with "Rashell_Git.branch_current: %S: The repository is in detached HEAD state."
     (match workdir with Some(path) -> path | None -> ".")
     | branch -> Lwt.return branch

    La commande clone de git a de nombreuses options, même en ne tenant compte que d'une fraction d'entre elles on obtient quelque chose de plus touffu:

    let clone ?workdir ?env ?template ?bare ?mirror ?origin ?branch ?(config = []) ?depth ?single_branch ?recursive ?reference ?dissociate ?destdir repository =
     let open Printf in
     let configuration =
     Array.concat(List.map (fun (k,v) -> [| "--config"; sprintf "%s=%s" k v; |]) config)
     in
     let argv =
     Array.concat [
     [| ac_path_git; "clone"; |];
     (option "--template" template);
     (flag "--bare" bare);
     (flag "--mirror" mirror);
     (option "--origin" origin);
     (option "--branch" branch);
     configuration;
     (option "--depth" (Maybe.map string_of_int depth));
     (flag "--single-branch" single_branch);
     (flag "--recursive" recursive);
     (option "--reference" reference);
     (flag "--dissociate" dissociate);
     [| repository |];
     (match destdir with
     | Some(x) -> [| x |]
     | None -> [| |]);
     ]
     in
     exec_utility
     (command ?workdir (ac_path_git, argv))
     >>= fun _ -> Lwt.return_unit

    Le module Rashell_Docker et les projets https://github.com/michipili/anvil ainsi que https://github.com/michipili/dockertk fournissent d'autres exemples d'utilisation de Rashell.

    Pour en revenir à Gufo c'est un projet certainement intéressant comme véhicule d'apprentissage mais il me semble adopter une approche étrange. Si j'ai à coordonner l'exécution de programmes, il me semble voir deux approches naturelles:

    • Utiliser le shell Bourne Cette solution a deux avantages: Prenièrement la présence d'un shell compatible Bourne est garantie sur presque tous les systèmes Unix, soit ni configuration, ni prérequis; deuxièmement le parallélisme et la concurrence y sont facilement utilisables (pour les tâches simples). Les inconvénients sont aussi bien connus que nombreux: langage rustre, difficile d'accès, pas très adapté à la gestion des erreurs ni au traitement de données... etc.

    • Utiliser un langage de haut niveau Cette solution résout en principe tous les problèmes du shell, mais il y a un problème de déploiement à résoudre, éventuellement le besoin d'écrire une interface agréable pour utiliser les programmes extérieurs , et le besoin d'apprendre la gestion du parallélisme dans le langage en question – pas forcément difficile mais a priori bien plus complexe que le modèle du shell.

    Dans cette grille de lecture, j'ai un peu du mal à voir la place que peut occuper Gufo: si j'ai besoin de configurer la machine sur laquelle je dois effectuer une tâche complexe, autant y installer mon langage préféré et tirer pleinement parti d'un langage puissant et complet.