Author: Christian.Queinnec@lip6.fr

## Exercices

Les trois exercices proposés ici manipulent respectivement les descripteurs de fichier, les processus, les tubes de communication, et les signaux. Les deux premiers sont des classiques de la programmation système sous Unix. Il peut être intéressant de comparer le code Objective CAML avec le code C que l'on trouve dans les distributions d'Unix ou de Linux.

### Compter les mots : la commande `wc`

On veut (re)programmer la commande Unix `wc` qui compte le nombre de lignes, mots ou caractères que contient un fichier texte. Les mots sont séparés par un espace, une tabulation ou un retour chariot. On ne compte pas les caractères séparateurs.

1. Écrire une première version (`wc1`) de cette commande qui ne traite qu'un seul fichier dont le nom est passé en argument sur la ligne de commande.
   On définit une liste de séparateurs ainsi que trois variables globales qui serviront aux différents comptages.

       # let seps = " \t" ;;
       val seps : string = " \t"
       # let nl = ref 0 ;;
       val nl : int ref = {contents=0}
       # let nw = ref 0 ;;
       val nw : int ref = {contents=0}
       # let nc = ref 0 ;;
       val nc : int ref = {contents=0}

   La fonction `counts` est chargée du comptage d'une ligne.

       # let counts s =
          let was_sep = ref true in
          let n = String.length s in
            for i=0 to n-1 do
              let is_sep = String.contains seps s.[i] in
              if is_sep && (not !was_sep) then incr nw ;
              was_sep := is_sep
            done ;
            if not !was_sep then incr nw ;
            nc := !nc+n+1;
            incr nl ;;
       val counts : string -> unit = <fun>

   La fonction `count` itère la fonction de comptage d'une ligne sur l'ensemble des lignes d'un fichier.

       # let count f =
          nl := 0; nw := 0; nc := 0;
          let f_in = open_in f in
           try
             while true do
              counts (input_line f_in)
             done
           with
             End_of_file -> close_in f_in ;;
       val count : string -> unit = <fun>

   La fonction principale appelle la fonction de comptage sur le nom de fichier passé en argument et affiche les résultats.

       # let print_count() =
          Printf.printf"\t%d" !nl ;
          Printf.printf"\t%d" !nw ;
          Printf.printf"\t%d\n" !nc ;;
       val print_count : unit -> unit = <fun>

       # let main() =
          try
            if Array.length Sys.argv < 2
            then print_string "wc1: missing file name\n"
            else ( count Sys.argv.(1) ; print_count() )
          with e -> Printf.printf "wc1: %s\n" (Printexc.to_string e) ;;
       val main : unit -> unit = <fun>

2. Écrire une version plus élaborée (`wc2`) qui peut prendre en argument tout ou partie des trois options : `-l`, `-c`, `-w` ainsi que plusieurs noms de fichiers. Les options indiquent, respectivement, si l'on veut voir afficher le nombre de lignes, caractères ou mots. L'affichage de chaque résultat sera précédé du nom du fichier concerné.
   On reprend les variables globales et les fonctions `counts` et `count` de la question précédente.
   On se donne trois variable globales donnant le statut des trois options possibles.

       # let l_opt = ref false
        and w_opt = ref false
        and c_opt = ref false ;;
       val l_opt : bool ref = {contents=false}
       val w_opt : bool ref = {contents=false}
       val c_opt : bool ref = {contents=false}

   On redéfinit l'affichage en fonction du statut des options.

       # let print_count f =
          Printf.printf "%s:" f ;
          if !l_opt then Printf.printf"\t%d" !nl ;
          if !w_opt then Printf.printf"\t%d" !nw ;
          if !c_opt then Printf.printf"\t%d" !nc;
          print_newline() ;;
       val print_count : string -> unit = <fun>

   La ligne de commande est analysée pour mettre à jour le statut des options ainsi que la liste des fichiers à traiter.

       # let f_list = ref ([]:string list) ;;
       val f_list : string list ref = {contents=[]}

       # let read_args () =
          let usage_msg = "wc2 [-l] [-w] [-w] files..." in
          let add_f f = f_list := f::!f_list in
          let spec_list =
            [ ("-l", Arg.Set l_opt, "affichage nombre de lignes") ;
              ("-w", Arg.Set w_opt, "affichage nombre de mots") ;
              ("-c", Arg.Set c_opt, "affichage nombre de caractères") ] in
          Arg.parse spec_list add_f usage_msg ;;
       val read_args : unit -> unit = <fun>

   La fonction principale itère le comptage sur la liste des fichiers.

       # let main() =
         try
           read_args() ;
           List.iter (fun f -> count f; print_count f) !f_list
         with
           e -> Printf.printf "wc2: %s\n" (Printexc.to_string e) ;;
       val main : unit -> unit = <fun>

### Pipeline pour correcteur orthographique

Cet exercice cherche à enchaîner une suite d'actions. Chaque action prend en entrée le résultat de l'action précédente. La communication s'effectue avec des pipes où seule la sortie d'un processus est redirigée vers l'entrée du processus suivant, à la manière du symbole `|` des interprètes de commande Unix.

1. Écrire une fonction `pipe_two_progs` de type string * string list -> string * string list -> unit telle que pipe_two_progs (p1,[a1; ...; an]) (p2,[b1; ...; bp]) lance les programmes p1 a1 ... an et p2 b1 ... bp en redirigeant la sortie standard de p1 sur l'entrée standard de p2. Les ai et bi sont les arguments de la ligne de commande de chaque programme.
   Voici une façon trés << unixienne >> d'implanter cette fonction.

       # let pipe_two_progs (p1, args1) (p2, args2) =
          let in2, out1 = Unix.pipe() in
          match Unix.fork() with
              0 ->
               Unix.close in2 ;
               Unix.close Unix.stdout ;
               ignore(Unix.dup out1) ;
               Unix.close out1 ;
               Unix.execvp p1 (Array.of_list args1)
            | _ ->
               Unix.close out1 ;
               Unix.close Unix.stdin ;
               ignore(Unix.dup in2) ;
                Unix.close in2 ;
                Unix.execvp p2 (Array.of_list args2) ;;
       val pipe_two_progs : string * string list -> string * string list -> unit =
         <fun>

2. On reprend la fonction `orthographe` de l'exercice de la page ?? pour écrire un premier programme. La modifier pour que la liste des mots incorrects soit envoyée, sans traitement préalable, sous forme d'une ligne par mot sur la sortie standard .

       # let orthographe dico nom =
          let f = open_in nom in
            try
              while true do
                let s = input_line f in
                let ls = mots s in
                 List.iter (Printf.printf"%s\n") (verifie dico ls)
               done ;
               failwith "cas impossible"
             with
               End_of_file -> close_in f
             | x -> close_in f ; raise x  ;;
       val orthographe : arbre_lex -> string -> unit = <fun>

3. Le deuxième programme prend une suite de chaînes de caractères sur son entrée standard, et la trie selon l'ordre lexicographique. On pourra utiliser la fonction `Sort.list` qui trie une liste selon un prédicat donné. La liste triée est ensuite affichée sur la sortie standard.

       # let trie () =
          let l = ref [] in
          try
            while true do l := Sort.list (<) ((input_line stdin)::!l) done
          with
            End_of_file -> List.iter (Printf.printf"%s\n") !l ;;
       val trie : unit -> unit = <fun>

4. Tester la fonction `pipe_two_progs` avec ces deux programmes.

       # pipe_two_progs ("orthographe",["";Sys.argv.(1)]) ("tri",[]) ;;

5. Écrire une fonction `pipe_n_progs` pour enchaîner une liste de programmes.
   Pour alléger un peu l'écriture on se donne deux fonctions de redirection de l'entrée et de la sortie standard.

       # let dup_stdin in_descr =
          if in_descr<>Unix.stdin then Unix.dup2 in_descr Unix.stdin ;
          Unix.close in_descr ;;
       val dup_stdin : Unix.file_descr -> unit = <fun>

       # let dup_stdout out_descr =
          if out_descr<>Unix.stdout then Unix.dup2 out_descr Unix.stdout ;
          Unix.close out_descr ;;
       val dup_stdout : Unix.file_descr -> unit = <fun>

   Pour itérer le pipeline, on définit une fonction récursive dont le premier argument donne le canal d'entrée du premier processus à chaîner.

       # let rec pipe_n_progs_loop in_descr = function
            [p,args] ->
              dup_stdin in_descr ;
              Unix.execvp p (Array.of_list args)
          | (p,args)::ps ->
              let in2, out1 = Unix.pipe() in
              ( match Unix.fork() with
                    0 ->
                      Unix.close in2 ;
                      dup_stdin in_descr ;
                      dup_stdout out1 ;
                      Unix.execvp p (Array.of_list args)
                  | _ ->
                      Unix.close out1 ;
                      pipe_n_progs_loop in2 ps )
           | _ -> () ;;
       val pipe_n_progs_loop :
         Unix.file_descr -> (string * string list) list -> unit = <fun>

       # let pipe_n_progs ps = pipe_n_progs_loop Unix.stdin ps ;;
       val pipe_n_progs : (string * string list) list -> unit = <fun>

6. Écrire un programme qui supprime les occurrences multiples des éléments d'une liste.

       # let rmdup () =
         let l = ref [] in
          try
            while true do
              let x = input_line stdin in
                if not (List.mem x !l) then l := x::!l
            done
          with End_of_file
           -> List.iter (Printf.printf"%s\n") !l ;;
       val rmdup : unit -> unit = <fun>

7. Tester la fonction `pipe_n_progs` avec ces trois programmes.

       # pipe_n_progs [ ("orthographe",["";Sys.argv.(1)]); ("tri",[]); ("rmdup",[]) ] ;;

### Trace interactive

Lors d'un calcul complexe, il peut être utile d'interagir avec le programme pour vérifier la progression de ce calcul. On reprend pour cela l'exercice de la page ?? sur le calcul des nombres premiers contenus dans un intervalle.

1. Modifier le programme pour qu'une variable globale `result` contienne à tout instant le dernier des nombres premiers déjà trouvés.

       # let result = ref 0 ;;
       val result : int ref = {contents=0}
       # let rec eras = function
            [] -> []
          | p::q ->
             result := p ;
              p :: (eras (List.filter (fun x -> x mod p <> 0) q)) ;;
       val eras : int list -> int list = <fun>

2. Écrire une fonction `sigint_handle` pour le traitement du signal `sigint` qui affiche le contenu de `result`.

       # let sigint_handle (_ : int) =
         Printf.printf"Current prime number : %d\n" !result;
         flush stdout ;;
       val sigint_handle : int -> unit = <fun>

3. Modifier le traitement par défaut du signal `sigint` en lui associant la fonction de la question précédente `sigint_handle`.

       # Sys.set_signal Sys.sigint (Sys.Signal_handle sigint_handle) ;;
       - : unit = ()

4. Compiler le programme, puis lancer l'exécutable avec une borne supérieure importante pour l'intervalle de recherche. Pendant cette exécution, envoyer le signal `sigint` au processus, soit en utilisant la commande Unix `kill`, soit en tapant le caractère `CTRL-C`.

       $ ocamlc premiers.ml
       $ premiers 15000
       Current prime number : 2539
       Current prime number : 8263
       2 3 5 7 11 13 17 19 23 29 31 37 41 43 ............