J’ai eu besoin il y a quelques jours de comparer deux versions d’un même fichier C après qu’il a subi de nombreuses modifications. La structure du fichier et le contenu ayant beaucoup bougé, les outils habituels (éditeurs, diff
, meld
, etc.) ne pouvaient me venir en aide. J’avais besoin d’extraire chaque fonction du programme pour qu’elle se trouve dans un fichier séparé. Cela m’a amené à écrire un petit script shell qui pourra peut-être s’avérer utile à d’autres…
Le fichier en question provenait d’un projet client ancien, dont le code avait été écrit de manière incrémentale, avait servi pour du débogage, de la mise au point d’algorithmes, etc. La première version du fichier devenait illisible et impossible à maintenir sereinement. Il a donc été décidé de procéder à un réusinage pour le rendre plus conforme aux standards logiciels actuels.
Cette phase de refactoring a consisté en plusieurs opérations. Entre autres
- suppression de code en commentaire ou dans des portions inaccessibles (comme
#ifdef 0
/#endif
) - suppression de code mort (routines jamais appelées)
- suppression des variables globales (plusieurs centaines !) inutilisées (plusieurs dizaines !)
- réintégration du maximum de variables globales en variables statiques ou automatiques
- renommage de variables et de constantes
- réorganisation globale du fichier pour contenir des blocs cohérents (définitions des macros, des types, des fonctions privées, des variables globales, implémentations des fonctions publiques puis des fonctions privées)
- réécriture du code pour correspondre à des normes de lisibilité actuelles. Personnellement j’ai adopté le standard de Busybox (voir la description dans son fichier
docs/style-guide.txt
).
Durant les opérations manuelles de ce refactoring (qui comprenait plusieurs dizaines de fichiers, chacun de plusieurs milliers de lignes de code), une erreur a été commise dont le débogage s’avérait complexe. J’ai donc décidé de comparer visuellement toutes les modifications, fonction par fonction du module incriminé.
Il existe un outil très agréable pour comparer deux versions d’un même fichier : il s’agit de Meld.Toutefois, lorsque la structure complète du fichier a été modifiée, il devient difficilement utilisable. La solution la plus simple pour moi consistait à exploser mon programme source en autant de fichiers qu’il y avait de fonctions (une petite centaine). Et de les comparer une-à-une (d’abord avec diff
en console pour voir si quelque chose me sautait aux yeux, puis avec Meld pour les fonctions les plus longues).
Il existe un utilitaire nommé csplit
qui permet de découper en fichier en utilisant des expressions régulières pour décrire le contexte (le « c
» de csplit
signifie context, pas « langage C » !). Malheureusement, dans les blocs de code commentés, il se trouvait des déclarations de fonctions avortées, et du code invalide. La description sous forme d’expression régulière semblait impossible.
J’ai commencé par demander à ctags
d’extraire la liste des fonctions présentes, et de me la présenter de manière « humainement lisible ».
Je prends en exemple le fichier main.c
se trouvant dans le répertoire init/
des sources du noyau Linux.
$ ctags -x -f - --c-kinds=f main.c boot_cpu_init function 431 main.c static void __init boot_cpu_init(void) debug_kernel function 195 main.c static int __init debug_kernel(char *str) do_basic_setup function 777 main.c static void __init do_basic_setup(void) do_ctors function 647 main.c static void __init do_ctors(void) do_early_param function 389 main.c static int __init do_early_param(char *param, char *val, const char *unused) do_initcall_level function 746 main.c static void __init do_initcall_level(int level) do_initcalls function 762 main.c static void __init do_initcalls(void) do_one_initcall function 680 main.c int __init_or_module do_one_initcall(initcall_t fn) do_one_initcall_debug function 662 main.c static int __init_or_module do_one_initcall_debug(initcall_t fn) do_pre_smp_initcalls function 789 main.c static void __init do_pre_smp_initcalls(void) init_setup function 291 main.c static int __init init_setup(char *str) kernel_init function 807 main.c static int __ref kernel_init(void *unused) kernel_init_freeable function 848 main.c static noinline void __init kernel_init_freeable(void) loglevel function 210 main.c static int __init loglevel(char *str) mark_rodata_ro function 92 main.c static inline void mark_rodata_ro(void) { } mm_init function 454 main.c static void __init mm_init(void) obsolete_checksetup function 158 main.c static int __init obsolete_checksetup(char *line) parse_early_options function 407 main.c void __init parse_early_options(char *cmdline) parse_early_param function 413 main.c void __init parse_early_param(void) quiet_kernel function 201 main.c static int __init quiet_kernel(char *str) rdinit_setup function 308 main.c static int __init rdinit_setup(char *str) repair_env_string function 230 main.c static int __init repair_env_string(char *param, char *val, const char *unused) rest_init function 360 main.c static noinline void __init_refok rest_init(void) run_init_process function 797 main.c static int run_init_process(const char *init_filename) set_reset_devices function 144 main.c static int __init set_reset_devices(char *str) setup_command_line function 341 main.c static void __init setup_command_line(char *command_line) setup_nr_cpu_ids function 331 main.c static inline void setup_nr_cpu_ids(void) { } smp_init function 323 main.c static void __init smp_init(void) smp_prepare_cpus function 332 main.c static inline void smp_prepare_cpus(unsigned int maxcpus) { } smp_setup_processor_id function 441 main.c void __init __weak smp_setup_processor_id(void) start_kernel function 468 main.c asmlinkage void __init start_kernel(void) thread_info_cache_init function 446 main.c void __init __weak thread_info_cache_init(void) unknown_bootoption function 250 main.c static int __init unknown_bootoption(char *param, char *val, const char *unused)
Nous voyons que chaque ligne correspond à une fonction du fichier. Le premier champ est le nom de la fonction, il est suivi par le type d’item (il s’agit toujours de « function
» car c’est ce que j’ai demandé à ctags
). Le troisième champ est le numéro de la première ligne, le quatrième le nom du fichier et enfin nous trouvons le prototype de la fonction.
Je souhaite extraire chaque fonction séparément, il me suffit donc de parcourir cette liste, et de créer un fichier à chaque ligne. L’implémentation de chaque fonction commence – dans le fichier original – à la ligne dont le numéro est indiqué en troisième colonne. Comme je ne sais pas où la fonction se termine, il me suffit de prendre tout le contenu jusqu’à la fonction suivante.
Pour cela il faut ordonner la liste ci-dessus. Utilisons sort
.
$ ctags -x -f - --c-kinds=f main.c | sort -nk 3 mark_rodata_ro function 92 main.c static inline void mark_rodata_ro(void) { } set_reset_devices function 144 main.c static int __init set_reset_devices(char *str) obsolete_checksetup function 158 main.c static int __init obsolete_checksetup(char *line) debug_kernel function 195 main.c static int __init debug_kernel(char *str) quiet_kernel function 201 main.c static int __init quiet_kernel(char *str) loglevel function 210 main.c static int __init loglevel(char *str) repair_env_string function 230 main.c static int __init repair_env_string(char *param, char *val, const char *unused) unknown_bootoption function 250 main.c static int __init unknown_bootoption(char *param, char *val, const char *unused) init_setup function 291 main.c static int __init init_setup(char *str) rdinit_setup function 308 main.c static int __init rdinit_setup(char *str) smp_init function 323 main.c static void __init smp_init(void) setup_nr_cpu_ids function 331 main.c static inline void setup_nr_cpu_ids(void) { } smp_prepare_cpus function 332 main.c static inline void smp_prepare_cpus(unsigned int maxcpus) { } setup_command_line function 341 main.c static void __init setup_command_line(char *command_line) rest_init function 360 main.c static noinline void __init_refok rest_init(void) do_early_param function 389 main.c static int __init do_early_param(char *param, char *val, const char *unused) parse_early_options function 407 main.c void __init parse_early_options(char *cmdline) parse_early_param function 413 main.c void __init parse_early_param(void) boot_cpu_init function 431 main.c static void __init boot_cpu_init(void) smp_setup_processor_id function 441 main.c void __init __weak smp_setup_processor_id(void) thread_info_cache_init function 446 main.c void __init __weak thread_info_cache_init(void) mm_init function 454 main.c static void __init mm_init(void) start_kernel function 468 main.c asmlinkage void __init start_kernel(void) do_ctors function 647 main.c static void __init do_ctors(void) do_one_initcall_debug function 662 main.c static int __init_or_module do_one_initcall_debug(initcall_t fn) do_one_initcall function 680 main.c int __init_or_module do_one_initcall(initcall_t fn) do_initcall_level function 746 main.c static void __init do_initcall_level(int level) do_initcalls function 762 main.c static void __init do_initcalls(void) do_basic_setup function 777 main.c static void __init do_basic_setup(void) do_pre_smp_initcalls function 789 main.c static void __init do_pre_smp_initcalls(void) run_init_process function 797 main.c static int run_init_process(const char *init_filename) kernel_init function 807 main.c static int __ref kernel_init(void *unused) kernel_init_freeable function 848 main.c static noinline void __init kernel_init_freeable(void)
Une simple boucle while read
du shell nous donnera :
$ ctags -x -f - --c-kinds=f main.c | sort -nk 3 | while read name unused line rest; do echo "${name}: ${line}"; done mark_rodata_ro: 92 set_reset_devices: 144 obsolete_checksetup: 158 debug_kernel: 195 quiet_kernel: 201 loglevel: 210 repair_env_string: 230 unknown_bootoption: 250 init_setup: 291 rdinit_setup: 308 smp_init: 323 setup_nr_cpu_ids: 331 smp_prepare_cpus: 332 setup_command_line: 341 rest_init: 360 do_early_param: 389 parse_early_options: 407 parse_early_param: 413 boot_cpu_init: 431 smp_setup_processor_id: 441 thread_info_cache_init: 446 mm_init: 454 start_kernel: 468 do_ctors: 647 do_one_initcall_debug: 662 do_one_initcall: 680 do_initcall_level: 746 do_initcalls: 762 do_basic_setup: 777 do_pre_smp_initcalls: 789 run_init_process: 797 kernel_init: 807 kernel_init_freeable: 848
Il nous faut maintenant extraire du fichier original la portion contenue entre le numéro indiqué sur chaque ligne et celui de la ligne suivante.
Une solution simple est d’utiliser sed
. Une commande comme
$ sed -ne '123,456p' < fic-entree > fic-sortie
copiera (commande p
pour print) dans le fichier de sortie le contenu du fichier d’entrée depuis la ligne 123 jusqu’à la ligne 456. De même
$ sed -ne '2014,$p' < fic-entree > fic-sortie
fera la copie depuis la ligne 2014 jusqu’à la fin du fichier. Attention à bien mettre des quotes simples pour encadrer la commande afin que le shell n’interprète pas le $p
comme un nom de variable.
Prenons les dernières lignes affichées ci-dessus. Lorsque nous traitons la ligne
kernel_init: 807
nous ne savons pas encore où la fonction kernel_init
se terminera. Il nous faut donc mémoriser son nom et le numéro 807, pour qu’une fois lue la ligne
kernel_init_freeable: 848
nous puissions extraire la portion 807 – (848-1) du fichier original pour le stocker dans un nouveau fichier nommé function-kernel_init
par exemple.
Pour pouvoir traiter la dernière fonction de la liste, il faut que notre boucle while read
soit appelée une fois de plus, aussi devons nous ajouter une ligne supplémentaire ainsi :
$ (ctags -x -f - --c-kinds=f main.c | sort -nk 3 ; echo) | while read name unused line rest; do echo "${name}: ${line}"; done mark_rodata_ro: 92 set_reset_devices: 144 obsolete_checksetup: 158 debug_kernel: 195 quiet_kernel: 201 loglevel: 210 repair_env_string: 230 unknown_bootoption: 250 init_setup: 291 rdinit_setup: 308 smp_init: 323 setup_nr_cpu_ids: 331 smp_prepare_cpus: 332 setup_command_line: 341 rest_init: 360 do_early_param: 389 parse_early_options: 407 parse_early_param: 413 boot_cpu_init: 431 smp_setup_processor_id: 441 thread_info_cache_init: 446 mm_init: 454 start_kernel: 468 do_ctors: 647 do_one_initcall_debug: 662 do_one_initcall: 680 do_initcall_level: 746 do_initcalls: 762 do_basic_setup: 777 do_pre_smp_initcalls: 789 run_init_process: 797 kernel_init: 807 kernel_init_freeable: 848 :
lors de la dernière itération tous les champs sont vides, ce qui conduit à la ligne contenant seulement le caractère deux-points.
Nous pouvons maintenant regrouper tout ceci dans un petit script qui prendra autant de fichiers d’entrée qu’on le souhaite.
extract-functions.sh: #! /bin/sh if [ $# -eq 0 ] then echo "usage: $0 C-file..." >&2 exit 1 fi for file in "$@" do prev_line="" prev_name="" (ctags -x -f - --c-kinds=f "${file}" | \ sort -n -k 3 ; echo) | \ while read name unused line rest do if [ "$prev_name" != "" ] # avoid first line then if [ "${name}" != "" ] then # normal line sed -ne "${prev_line},$((line - 1))p" < "${file}" > "function-${prev_name}.c" else # last line sed -ne "${prev_line},\$p" < "${file}" > "function-${prev_name}.c" fi fi prev_line="${line}" prev_name="${name}" done done
En voici un exemple d’utilisation.
$ ls main.c $ extract-functions.sh main.c $ ls function-boot_cpu_init.c function-do_one_initcall.c function-mark_rodata_ro.c function-repair_env_string.c function-smp_prepare_cpus.c function-debug_kernel.c function-do_one_initcall_debug.c function-mm_init.c function-rest_init.c function-smp_setup_processor_id.c function-do_basic_setup.c function-do_pre_smp_initcalls.c function-obsolete_checksetup.c function-run_init_process.c function-start_kernel.c function-do_ctors.c function-init_setup.c function-parse_early_options.c function-set_reset_devices.c function-thread_info_cache_init.c function-do_early_param.c function-kernel_init.c function-parse_early_param.c function-setup_command_line.c function-unknown_bootoption.c function-do_initcall_level.c function-kernel_init_freeable.c function-quiet_kernel.c function-setup_nr_cpu_ids.c main.c function-do_initcalls.c function-loglevel.c function-rdinit_setup.c function-smp_init.c $
En conclusion, ce petit script est loin d’être parfait, il n’évite pas les lignes blanches en fin de fichier par exemple, mais sa simplicité le rend facile à modifier pour l’adapter à d’autres situations où il pourra être utile.