u_handle_files_in_dirs - John COLIBRI. |
- mots clé:utilitaire - récupération de noms de fichiers - parcours arborescence DOS - FindFirst FindNext
- logiciel utilisé: Windows 98, Delphi 5.0
- matériel utilisé: Pentium 500Mhz, 128 M de mémoire
- champ d'application: Delphi 1 à 6 sur Windows, Kylix
- niveau: débutant en Pascal et Delphi
- uses: u_strings, u_c_display, (u_c_basic_object,
u_c_log, u_loaded, u_types_constants, u_c_basic_file,
u_c_file_name, u_dir)
- plan:
1 - Introduction
De nombreux utilitaries destinés aux programmeurs ou aux utilisateurs nécessitent le balayage d'un arborescence DOS afin de récupérer les fichiers satisfaisant certains critères, tels que l'extension, la date etc. Citons en vrac:
- pour les programmeurs
- les pretty printers
- le changement de commentaire (remplacer "(*" par "//" ou par "{")
- suppression des bannières (style ////// en travers de la page ou autre)
- les cross-reférences (liste des variables avec les numéros de ligne)
- la normalisation des fins de ligne (Retour chariot et / ou interligne)
- pour les fichiers ASCII
- les changements de jeu de caractère (Dos, Windows)
- le formattage (découpage des lignes)
- la correction orthographique
- pour les utilisateurs:
- les sauvegardes de tous ordres (le fichiers plus vieux que xxx, ceux ayant certaines extensions...)
- affichage des statistiques d'utilisation disque (les propriétés de Windows obligent à cliquer sur chaque répertoire, sans présenter de
synoptique globale de la consommation disque)
- suppression de fichiers sauvegardés de multiples fois
Le balayage de base se fait à l'aide des antiques primitives Dos FindFirst et FindNext.
Ces primitives avaient été mises au goût du jour par les composants tDirectoryListBox, tFileListBox et tFilterComboBox de Delphi 1. Borland a placé ces composants dans la catégorie des "deprecated", alors que je ne
connaisse rien qui les remplace. J'avais écrit à l'époque un article sur les "Outlook", mais je me souviens que ce fut une certaine galère. Les tTreeView de Delphi 2 offrent une panoplie impressionnante de fonctionalités, mais il
faut les charger. Et finalement même avec les tDirectoryListBox, il faut encore trouver un moyen d'analyser plusieurs répertoires (alors que le composant n'affiche qu'un répertoire courant).
Nous sommes donc à la case départ, en employant FindFirst et FindNext. Il y for longtemps, j'avais écrit quelques routines qui employaient ces primitives, et les avais d'ailleurs publiées dans le livres 'Delphi dBase' pour
permettre l'effacement d'un répertoire (l'utilitaire effaçait récursivement les fichiers des répertoires les plus profonds avant de supprimer les répertoires contenant des fichiers).
Je coupais et collais vaillamment ces procédures d'utilitaire en utilitaire, sans problème majeur. Je les ai même portées en Kylix pour permettre le transfert de répertoires complets par FTP (je n'utilise pas Linux sur Windows,
Samba ou autre. Mon seul lien entre le PC Linux et le PC Windows est le réseau local. D'où la nécessité d'utiliser FTP). Comme les mécaniques de balayages de répertoire seront utilisées dans d'autres
articles (liste des ancres HTML etc), j'ai décidé de plublier ces routines. 2 - Utilisation 2.1 - Un exemple simple Côté utilisateur, le programmeur:
- appelle la procédure handle_files_in_dirs en fournissant le chemin de base et la procédure à appeler pour chaque fichier
- écrit la procédure de traitement qui est passée en paramètre à handle_files_in_dirs
Voici l'exemple qui est utilisé dans le programme de test:
procedure display_file_names(p_level: Integer; p_dir, p_file_name: String;
p_pt_data: Pointer);
// -- the call-back begin
display(f_spaces(2* p_level)+ p_file_name);
end; // display_file_names
procedure TForm1.list_names_Click(Sender: TObject);
var l_path, l_extension: String;
begin l_path:= dir_.Text;
l_extension:= '.PAS';
handle_all_files_recursive(1, l_path, l_extension,
[e_dir_recursive, e_dir_handle_file], display_file_names, Nil);
end; // list_names_Click | 2.2 - L'interface de l'unité L'interface est la suivante:
type t_pr_handle_file= procedure(p_level: Integer; p_dir, p_file_name: String;
p_pt_data: Pointer);
t_dir_handling_type= (e_dir_recursive,
e_dir_display_dir, e_dir_display_file, e_dir_display_all_file, e_dir_display_debug,
e_dir_indent,
e_dir_handle_dir, e_dir_handle_file);
t_dir_handling_types= set of t_dir_handling_type;
procedure handle_all_files_recursive(p_level: Integer; p_strict_path, p_extension: String;
p_dir_handling_types: t_dir_handling_types;
p_pr_handle_file: t_pr_handle_file; p_pt_data: Pointer); |
Pour les données: - t_pr_handle_file est le type procédural de la fonction appelée pour chaque fichier ou répertoire trouvé. Les paramètres sont:
- p_level: permet l'affichage indenté ou l'utilisation de la profondeur dans l'arborescence
- p_dir: le chemin courant (terminé par \)
- p_file_name:
- si un fichier est trouvé, le nom de fichier
- si un répertoire est trouvé, le nom du dernier segment (si dans "c:\a\b\" il y a un sous-répertoire "c" c'est ce segment qui est fourni)
- p_pt_data: un pointeur pour échanger des données entre la procédure
appelante et la procédure de traitement
- t_dir_handling_type: spécifie les types de traitements actuellement prévus:
- e_dir_recursive: analyser les sous-répertoires
- e_dir_display_dir: afficher le répertoire lors du balayage
- e_dir_display_file: afficher le nom du fichier
- e_dir_display_all_file: afficher le nom des fichiers (même ceux ne
correspondant pas à l'extension retenue)
- e_dir_display_debug: affichage de mise au point
- e_dir_indent: les affichages précédents sont indentés
- e_dir_handle_dir: appeler la procédure de traitement pour chaque
répertoire trouvé
- e_dir_handle_file: appeler la procédure de traitement pour chaque fichier ayant l'extension demandée
- t_dir_handling_types: l'ensemble des options de traitement
La procédure de traitement elle-même a les paramètres suivants: - p_level: le niveau dans l'arborescence
- p_strict_path: le chemin de départ (à fournir sans le "\" final)
- p_extension: l'extension à analyser. Par exemple ".Html" ou ".pas". Le point doit être fourni, la casse est sans importance (le test est effectué sur les extensions majuscules)
- p_dir_handling_types: les options retenues. Essentiellement s'il faut récurser ou non, et s'il faut traiter les fichiers et / ou les répertoires
- p_pt_data: le pointeur pour échanger des données
2.3 - Un exemple avec échange de données Voici un exemple nous permettant de calculer la taille de tous les fichiers *.PAS d'une arborescence:
type t_pt_size= pInteger;
procedure add_file_size(p_level: Integer; p_dir, p_file_name: String;
p_pt_data: Pointer);
// -- the call-back
var l_file: File; begin
Assign(l_file, p_dir+ p_file_name);
Reset(l_file, 1);
Inc(t_pt_size(p_pt_data)^, FileSize(l_file));
Close(l_file);
end; // add_file_size
procedure TForm1.sizes_Click(Sender: TObject);
var l_path, l_extension: String;
l_total_size: Integer; begin
l_path:= dir_.Text;
l_extension:= '.PAS';
handle_all_files_recursive(1, l_path, l_extension,
[e_dir_recursive, e_dir_handle_file], add_file_size, @ l_total_size);
display('Total size: '+ IntToStr(l_total_size)+ ' bytes');
end; | Au point de vue traitement: Notons que: - Nous avons défini t_pt_size comme étant un pointeur d'entier. Le surtypage
aurait naturellement été possible en utilisant directement pInteger, mais si les données à échanger sont plus complexes, il faudra utiliser un RECORD pour définir les données, et un pointeur vers ce RECORD pour
accéder aux données. Pour récupérer la taille et le nombre de fichiers, nous pourrions utiliser:
TYPE t_size_and_count= RECORD
m_size, m_count: INTEGER;
END;
t_pt_size_and_count= ^t_size_and_count;
...
WITH t_pt_size_and_count(p_pt_data)^ DO
BEGIN
Inc(m_count);
Inc(m_size, FileSize(l_file));
END; ...
VAR l_size_and_count: t_size_and_count;
handle_files_in_dirs(.... , @ l_size_and_count);
WITH l_size_and_count DO
display(IntToStr(m_count)+ ' '+ IntToStr(m_size));
| - pour réaliser ce cumul de taille nous aurions pu passer la taille de chaque fichier qui est présente dans tSearchRec. Il aurait suffi d'ajouter ce
paramètre à la procédure de traitement, ou même d'exporter tout le tSearchRec. Cette utilisation étant trop rare, cette solution n'a pas été retenue
2.4 - Répertoires L'unité est prévue pour être placée dans:
C: programs colibri_helpers units Vous pouvez naturellement changer cette organisation par Projet | Options | Directories
2.5 - Directives de compilation Les directives de compilation sont: - R+ (vérification des intervalles)
- S+ (vérification de la pile)
- pas d'optimisation
3 - Programmation
3.1 - Programmation L'essentiel du code s'appuie sur les fonctionalités de FindFirst et FindNext, qui est des plus classiques. Le plus délicat a été de définir les paramètres à envoyer à la procédure de
traitement. Pas assez de paramètres nécessitaient une réécriture de handle_files_in_dirs, et trop de paramètres rendent la mémorisation difficile. Notez d'ailleurs que le texte de u_handle_files_in_dirs contient au début, en
commentaire, un exemple d'appel pour faciliter le copier-coller. Les paramètres choisis permettent: - la gestion de la profondeur (pour limiter, par exemple à un certain nombre de niveaux)
- la séparation du chemin et du nom de fichier (ce qui évite ExtractFilePath ou autre dissécation)
- le passage de paramètres de tout type, au prix d'un surtypage. Une solution
utilisable en parallèle est de déclarer des globales initialisées par la procédure d'appel et mises à jour dans la call-back. Plus sûr, mais moins élégant.
4 - Améliorations
Cette version n'est pas objet. Il n'y a en effet pas de données à mémoriser. Le principal problème, du point de vue style de programmation, est la présence de la procédure call-back globale, qui nécessite l'utilisation de types ou
données globales pour échanger des informations entre la procédure appelant le balayage et la procédure réalisant le travail sur chaque fichier. De mon point de vue, il serait bien plus sympathique de pouvoir appeler:
procedure TForm1.sizes_Click(Sender: TObject);
type t_pt_size= pInteger;
procedure add_file_size(p_level: Integer; p_dir, p_file_name: String;
p_pt_data: Pointer);
// -- the call-back
var l_file: File;
begin
Assign(l_file, p_dir+ p_file_name);
Reset(l_file, 1);
Inc(t_pt_size(p_pt_data)^, FileSize(l_file));
Close(l_file);
end; // add_file_size
var l_path, l_extension: String;
l_total_size: Integer;
begin // sizes_Click
l_path:= dir_.Text;
l_extension:= '.PAS';
handle_all_files_recursive(1, l_path, l_extension,
[e_dir_recursive, e_dir_handle_file], add_file_size, @ l_total_size);
display('Total size: '+ IntToStr(l_total_size)+ ' bytes');
end; // sizes_Click | Nous avions publié toute une série d'articles dans Pascalissime permettant
l'utilisation d'itérateurs en Pascal. Ces itérateurs avaient été employés dans les articles sur les graphismes avec facettes (le programme passant son temps à parcourir les objets, eux-mêmes composés de facettes, composées de segments
etc.). Les itérateurs avaient donc été ajoutés aux conteneurs tels que les listes et les arbres. La technique n'était pas très compliquée: il fallait simplement se débrouiller
pour que le code compilé puisse accéder aux données locales. Quelques lignes d'assembleur Inline avaient fait l'affaire (encore que ces quelques lignes, simples conceptuellement, avait coûté quelques heures de mise au point).
Les itérateurs, avec les primitives du style FOREACH avaient d'ailleurs été introduites par Borland par la suite, du temps de Turbo Vision je crois (TP6, TP7 BP7). Ils ont été retirés avec l'arrivée de Delphi. Il est certain qu'en
cherchant un peu sur le Web, on devrait trouver des implémentations de ce type d'itérateurs, mais j'avoue que je n'ai pas cherché. Si quelqu'un a l'info... En fait si le problème est d'éviter l'utilisation d'une procédure call-back
externe, une autre solution est de construire la liste et de retourner la liste complète. Une légère modification de handle_files_in_dirs permet de construire une tStringList. Ou encore, nous pouvons encapsuler les chemins et fichiers
dans une classe complète, ce qui a été fait dans la classe c_files_in_dirs. Il semble me souvenir que Charlie Calvert dans "Delphi Unleashed" avait utilisé FindFirst et FindNext comme exemple pour créer un composant du type
tFileListBox. Une autre solution encore serait de construire un tTreeView: nous ne serions plus très loin d'un explorateur Windows complet. Mentionnons aussi que nous avons choisi volontairement de ne filtrer que sur
les extensions. En fait FindFirst et FindNext permettent de fournir un filtre (par exemple 'gestion2002*.dat') et le type de fichier (faAnyFile dans notre
cas). Par rapport à mon utilisation du balayage des répertoires, cette option ne s'est pas avérée nécessaire (la procédure de traitement peut toujours, elle, filtrer). Cette version de balayage de répertoires est récente, et a été écrite pour
pouvoir publier les articles suivants. Je n'ai pas ainsi vérifié que les traitements de récursivité finale sont possibles (effacer les fichiers puis effacer le répertoire les contenant, ou calculer la taille d'un répertoire).
5 - Télécharger Vous pouvez télécharger:
Avec les mentions d'usage: - j'apprécie tous les commentaires, remarques ou critiques
- signalez-moi les bugs que vous trouverez.
L'auteur
John COLIBRI est passionné par le développement Delphi et les applications de Bases de Données. Il a écrit de nombreux livres et articles, et partage son temps entre le développement de projets (nouveaux projets, maintenance, audit, migration BDE, migration Xe_n, refactoring) pour ses clients, le
conseil (composants, architecture, test) et la
formation. Son site contient des articles
avec code source, ainsi que le programme et le calendrier des stages de formation Delphi, base de données, programmation objet, Services Web, Tcp/Ip et
UML qu'il anime personellement tous les mois, à Paris, en province ou sur site client. |