Cours 3

Htmligure
Version & licenses
Creative Commons License

Un exemple complet.

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Munis de toutes ces fonctions de listes, nous sommes prêt à écrire des programmes complets, intéressants et pourtant simples. Ici, nous prenons l'exemple d'un petit script pour compter le nombre d'octets des fichiers de code source (en C, Java, OCaml et Haskell) d'un répertoire, récursivement sur les sous-répertoires.

Nous utilisons les modules (de Core.Std, les trois premiers sont aussi dans la librairie standard) :

  • Sys pour manipuler le système de fichier,
  • Unix pour interagir avec le système d'exploitation,
  • Filename pour gérer les noms de fichiers, ainsi que les modules List et Option,
  • Command permet de gérer la lecture des arguments en ligne de commande du programme.

On peut copier ce script dans un fichier, et l'exécuter avec l'interprète OCaml (ou bien le rendre exécutable avec #! et chmod) :

  1. ocaml script_size_of_sources.ml directory_name

Voici le script complet :

  1. (* add #!/path/to/ocaml as first line, and do
  2. chmod +x on the file to make it an executable
  3. *)


  4. (* There is an error in OCaml 4.03 and 4.04.0 that
  5. makes so that Ephemeron is not loaded by the interpreter,
  6. but Ephemeron is used by Core so we must load it.

  7. A workaround is to load stdlib.cma, but this has the bad effect of
  8. somehow reinitializing Sys.argv, with arguments:
  9. 0 ocaml
  10. 1 the name of the script
  11. 2 the first argument for the script, and so on.

  12. Only in 4.03 and 4.04.0 we have to take a dummy argument,
  13. the name of the script. On any other version of ocaml, remove
  14. the load stdlib.cma line and the scriptname anonymous argument
  15. in the spec of the command-line (at the end of the script).
  16. *)


  17. #load "stdlib.cma";; (* only on ocaml 4.03.0 and 4.04.0 *)


  18. #use "topfind";;
  19. #thread;;
  20. #camlp4o;;
  21. #require "core";;


  22. open Core.Std

  23. let source_extensions = [ "ml"; "mli"; "java"; "c"; "h"; "hs"; "hi" ]
  24. let ignore_directory = [ "_darcs"; "_build" ]



  25. let is_source_file filename =
  26. let (basename, maybe_extension) = Filename.split_extension filename in
  27. let is_source_extension = List.mem source_extensions in
  28. Option.exists maybe_extension ~f:is_source_extension


  29. let size_of_file filename =
  30. let file_stats = Unix.stat filename in
  31. Unix.(file_stats.st_size)
  32. |> Int64.to_int
  33. |> Option.value ~default:0

  34. let is_valid_file full_name base_name =
  35. Sys.is_file full_name = `Yes
  36. && is_source_file base_name



  37. let is_directory_hidden base_name= String.get base_name 0 = '.'

  38. let is_valid_directory full_name base_name =
  39. Sys.is_directory full_name = `Yes
  40. && not (is_directory_hidden base_name)
  41. && not (List.mem ignore_directory base_name)



  42. let rec size_of_dir_item dir_name item_name =
  43. let full_name = Filename.concat dir_name item_name in
  44. if is_valid_directory full_name item_name then size_of_dir full_name
  45. else if is_valid_file full_name item_name then size_of_file full_name
  46. else 0

  47. and size_of_dir dir_name =
  48. dir_name
  49. |> Sys.ls_dir
  50. |> List.map ~f:(size_of_dir_item dir_name)
  51. |> List.fold_left ~f:(+) ~init:0



  52. let show_dir_size dir_name =
  53. let size = size_of_dir dir_name in
  54. Printf.printf "%s bytes of sources.\n%!" (Int.to_string size)


  55. (* For some old version of Core: *)
  56. let count_sources_command =
  57. let cwd = Sys.getcwd () in
  58. let spec =
  59. Command.Spec.(
  60. empty
  61. +> anon ("scriptname" %: string) (* only on ocaml 4.03.0 and 4.04.0 *)
  62. +> anon (maybe_with_default cwd ("dirname" %: string))
  63. )
  64. in
  65. Command.basic
  66. ~summary:"Count size of source files in a directory recursively"
  67. spec
  68. (fun scriptname dirname () -> show_dir_size dirname)
  69. (* remove scriptname in previous line if not on 4.03 or 4.04.0 *)

  70. let main =
  71. Command.run ~version:"0.1" count_sources_command


  72. (* For a recent version of Core:
  73. let count_sources_command =
  74. let cwd = Sys.getcwd () in
  75. let arg_specification =
  76. let open Command.Param in
  77. anon (maybe_with_default cwd ("dirname" %: string))
  78. |> map ~f:(fun dirname () -> show_dir_size dirname)

  79. in
  80. Command.basic
  81. ~summary:"Count size of source files in a directory recursively"
  82. arg_specification

  83. let main =
  84. Command.run ~version:"0.1" count_sources_command
  85. *)

Expliquons linéairement le code.

  • Aux lignes 5 à 8 , on charge la libraire Core dans l'interpréteur. Core utilise des threads et des extensions syntaxiques, il faut donc prévenir l'interpréteur.
  • Les lignes 13 et 14 définissent les extensions de fichiers sources, et les répertoires qu'il faut ignorer. Ces listes peuvent facilement être modifiées, saans avoir à modifier le reste du programme. On pourrait aussi les lire depuis la ligne de commande ou un fichier de configuration.
  • is_source_file (ligne 18 ) vérifie si un nom de fichier correspond à un fichier de code source. On sépare le nom et l'extension du fichier avec Filename.split_extension. Les fichiers n'ont pas tous une extensions, donc maybe_extension est une option. Une option est un conteneur, comme les listes, les options ont donc beaucoup de fonctions similaires à celles des listes. En particulier exists vérifie qu'au moins une extension (parmi au plus une en fait), sont des extensions de fichiers de code source. Bonus : ce qu'on apprend sur les listes nous servira aussi pour d'autres modules ! (c'est aussi un signe que ces fonctions n'ont pas été conçues par hasard).
  • size_of_file interroge le système pour obtenir les statistiques du fichier, dont son nombre d'octets.
  • is_valid_file est un prédicat pour savoir si un fichier doit être comptés. On utilise Sys.is_file pour savoir si le nom désigne bien un fichier, puis on vérifie l'extension. Sys.is_file retourne `Yes, `No ou `Unknown. Ce sont des valeurs spéciales que nous expliquerons plus tard, on peut les comprendre pour l'instant comme les valeurs d'une énumération. En tout cas, on peut tester l'égalité avec, ce qui nous suffit.
  • Les lignes 38 à 41 permettent de vérifier si un nom de fichier désigne un répertoire à explorer. is_directory_hidden vérifie s'il s'agit d'un répertoire caché, dans ce cas il ne faut pas l'explorer. is_valid_directory teste qu'il s'agit d'un répertoire, non-caché, et non-interdit. À nouveau, List.mem nous permet de vérifier facilement la dernière condition.
  • Les lignes 45 à 56 calculent la taille des sources d'un élément d'un répertoire, qui peut être un sous-répertoire. On utilise deux fonctions mutuellement récursives. Pour tester un élément, on teste sa catégorie et on applique la fonction correspondante, 0 sinon. La fonction intéressante est le test de tout un répertoire : Sys.ls_dir nous fournit la liste des noms d'éléments du répertoire. Pour chaque élément, on calcule sa contribution grâce à List.map. Puis on somme avec List.fold_left .

    Il faut bien se rendre compte que nous venons de réaliser un parcours d'arbre, simplement avec une récursion et deux fonctionnelles de liste. Ce n'est pas un algorithme complètement trivial, pourtant on l'exprime ici très simplement.

  • show_dir_size en ligne 60 est la fonction principale : elle calcule la taille des sources d'un répertoire et affiche le résultat.
  • La suite permet de définir l'utilisation des arguments en ligne de commande. Chaque utilisation possible définit une commande, ici nous avons une seule utilisation donc une seule commande, en ligne 65 . Chaque commande est définie par une spécification précisant les arguments associés, et la fonction exécutant la commande. Ici la spécification est construite en partant d'une spécification vide, et en ajoutant un argument anonyme, optionnel (maybe_with_default), qui est un nom de fichier (ou de répertoire), et valant le chemin de répertoire courant s'il n'est pas donné par l'utilisateur. La chaîne "dirname" permet au script de préciser l'usage de l'argument, lors de l'utilisation de l'argument "-help" . La fonction à exécuter doit prendre un argument vide supplémentaire, on utilise une fonction anonyme (ligne 72). Une fois la commande définie, on crée un exécutable avec Command.run. On peut maintenant facilement ajouter d'autres commandes, d'autres arguments à notre exécutable. Pour une introduction et des exemples d'utilisation de Command, on peut lire le chapitre dédié du livre Real World OCaml.

Retour au sommaire.