OCaml étant un langage interprétable, on peut l'utiliser en remplacement de tout langage de script, par exemple à la place de Bash. Il existe même des libraires OCaml permettant de faire facilement les opérations liés au système de fichiers ou au système d'exploitation ( shcaml par exemple).
Pour ce TP, on reprend le script vu en cours, et on va lui apporter des fonctionnalités supplémentaires.
Commencez par créer un nouveau répertoire, mettez-y le script. Ajoutez en première ligne :
en remplacement PATH_TO_OCAML par le résultat de which ocaml.
En ligne de commande, rendez le script executable :
Puis testez le script, par exemple sur le répertoire courant.
Il doit vous afficher un nombre d'octets.
Relisez bien tout le script et assurez-vous de bien comprendre comment il fonctionne, puisque vous allez ensuite devoir le modifier. Le script utilise la librairie Core. Créez donc un fichier .merlin pour référencer l'usage de Core et profiter de l'auto-complétion et des indications de types. La documentation pour Core 0.10 est ici, celle pour Core 0.9 est là. En particulier on utilise les modules suivantes (ouvrez-les dans différents onglets pour y avoir accès facilement pendant la durée du TP) :
Pour s'échauffer, on modifie le script de sorte à retourner, non pas le nombre d'octets des fichiers sources, mais le nombre de lignes de code. Pour cela il faut ouvrir le fichier en lecture, extraire les lignes, et fermer le fichier. On utilise ces trois fonctions :
Le type in_channel représente les sources de caractères : fichiers ouverts en lecture ou sortie d'un tube.
Écrivez une fonction nbr_of_lines_of_file : string -> int, qui au nom d'un fichier retourne le nombre de lignes du fichier (utilisez la fonction déjà définie pour trouver la longueur de la liste).
Puis, modifiez le script pour qu'il calcule le nombre total de lignes de code des fichiers d'un répertoire et de ses sous-répertoires.
Vérifiez votre modification en calculant le nombre de lignes de code du répertoire courant (qui devrait être le nombre de lignes du script).
Pourquoi choisir entre nombre d'octets et nombre de lignes de codes ? Autant calculer les deux à la fois. On pourrait utiliser un couple pour encoder les deux informations, mais on voudra peut-être en ajouter d'autres par la suite. Donc nous allons utiliser un enregistrement.
On introduit le nouveau type :
Nous mettons ce nouveau type dans un module pour garder le code organisé. Idéalement, on en ferait un nouveau fichier, mais comme nous ne comptons pas le compiler, merlin n'en tiendra pas compte et ce ne sera pas pratique (on pourrait néanmoins le faire, il faudrait juste utiliser #use stats.ml au début du script).
Toujours pour organiser, déplacez les définitions de nbr_of_lines_of_file et size_of_file dans le module Stats.
Créez ensuite une fonction stats_of_file qui étant donné un nom de fichier, retourne ses statistiques. La syntaxe pour créer un enregistrement est :
Pour intégrer cela à la suite du script, il nous faut deux ingrédients. Le premier, c'est le fait qu'avec seulement un entier, il suffisait de faire des additions pour tenir compte des multiples fichiers. Du coup, il nous faut une fonction d'addition sur les statistiques, et une valeur zéro. Définissez les deux fonctions suivantes au module Stats :
Un type muni d'un zéro et d'une fonction d'addition associative (et ayant zéro pour neutre) s'appelle un monoïde. On en trouve partout en informatique, c'est même une des structures fondamentales en programmation fonctionnelle. Quels autres monoïdes connaissez-vous ?
Ajoutez maintenant une fonction qui retourne les statistiques d'un fichier dont le nom est donné en argument. Puis une fonction donnant une représentation par chaîne de caractères d'une statistique en utilisant le module Printf (toujours dans le module Stats).
Modifiez maintenant le reste du script pour utiliser le type Stats.t. Les modifications devraient être mineures ! Testez le script modifié.
Nous souhaitons maintenant enregistrer indépendamment les fichiers de chaque extension possible. On commence par créer un type pour distinguer les différentes sortes de fichiers. Un fichier a une extension et un langage associé, cela nous donne :
Déplacez la liste des extensions dans ce module Kind. Modifiez la liste, de sorte que chaque extension soit associée à son langage : on aura donc une liste de paire de chaînes de caractères.
Il nous faut maintenant une fonction qui a un nom de fichier associe sa sorte de type Kind.t. Pour cela, créez d'abord une fonction qui a une extension associe sa sorte. Bien sûr, il peut ne pas y avoir de sorte associée, donc la fonction retourne une option. Pour trouver la bonne extension dans la liste, utilisez List.Assoc.find. Il faudra ensuite transformer le contenu de l'option, quelle fonction du module Option utiliser ?
Ensuite, ajoutez une fonction of_file utilisant Filename pour retrouver l'extension du nom de fichier. À nouveau, cette fonction peut retourner une option. Il vous faudra une autre fonction de Option, de quelle type doit-elle être ?
Enfin, ajoutez une fonction pour comparer les sortes. Pour l'instant, on compare les langages par ordre alphabétique (l'ordre naturel des string), en cas d'égalité, on compare les extensions.
Ceci termine le module Kind.
Il nous faut maintenant un type capable de contenir toutes les informations pour les différentes sortes de fichiers. Idéalement il nous faut un dictionnaire associant à chaque sorte sa comptabilité. Nous apprendrons à utiliser les dictionnaires de la librairie standard dans un prochain cours (ils utilisent une fonctionnalité que nous introduirons plus tard). En attendant, nous utilisons le dictionnaire du pauvre : une liste associative, c'est-à-dire une liste de paires (clé,valeur). Les fonctions associées sont dans le module List.Assoc (qui manque cruellement de documentation, mais ça devrait être compréhensible néanmoins).
Créez un nouveau module Digest à la suite de Kind.
Nous voulons en faire un monoïde bien sûr. Ajoutez une valeur empty, qui sera le zéro.
L'addition sera la fonction merge fusionnant deux listes associatives. Avant de l'écrire, il nous faut une fonction pour ajouter un nouveau fichier dans une liste associative. Si la sorte du fichier est déjà référencée, il faut augmenter les statistiques associées. Sinon il faut simplement ajouter un nouvel élément. Écrivez donc la fonction :
Utilisez les fonctions mem,find,add de List.Assoc et les fonctions de Option.
Ensuite, en utilisant une seule fonction du module List, ajoutez :
Il reste à écrire une fonction pour créer un Digest.t depuis un nom de fichier, puis des fonctions de conversions en chaînes de caractères (utilisez String.concat) :
Modifiez alors le reste du script : de nouveau il n'y a pas grand chose à changer.
On se propose maintenant d'améliorer un peu la sortie, de sorte à afficher un joli tableau :
Pour cela, ajoutons encore un module, qui gère l'affichage de tableaux, donnés comme la liste des entête de colonnes, plus la liste des lignes (chaque ligne est une liste de chaînes de caractères).
Ajoutez une fonction compute_widths_of_line qui calcule la largeur nécessaire à chaque colonne d'une ligne donnée. Puis il nous faut une fonction prenant deux listes de même longueur, et calculant la liste des maximums coordonnée par coordonnée (si le nombre de colonnes était déterminé, on pourrait ajouter un zéro et avoir un autre monoïde, mais ce n'est pas le cas). Ensuite, utilisez ces deux fonctions pour faire une fonction qui, étant donnée une liste de lignes, calcule pour chaque colonne la largeur nécessaire. Utilisez les fonctions de List pour cela.
Il nous faut ensuite une fonction pad qui ajoute des espaces à une chaîne pour obtenir une chaîne de longueur voulue, une fonction format prenant la largeur de chaque colonne, le contenu de chaque colonne, et retourne la chaîne de caractère contenant toute la ligne.
il reste à écrire la fonction principale, ce qui devrait être facile maintenant (utilisez Printf.printf "%s\n% pour afficher chaque ligne) :
Ceci termine le module ArrayDisplay.
Ajoutez maintenant aux modules Stats, Kind et Digest des fonctions header retournant les entêtes des informations du module, et to_strings retournant les lignes pour chaque valeur.
Puis modifiez la fonction principale show_dir_size pour terminer l'ajout de cette fonctionnalité.
Quelques idées d'améliorations possibles :