1. Introduction▲
Cet article est la suite de La création d'un minisystème : création d'un Shell.
Cette partie va évoquer l'intégration des commandes de base nommées également Core Utilities.
Voici un exemple de ces commandes :
- cat ;
- cp ;
- rm mv ;
- mkdir/rmdir ;
- etc.
Vous aurez la possibilité d'avoir un système minimal avec le shell déjà créé et les outils de base, à partir d'un dossier chrooté d'un Linux existant.
2. Autonomie de mon code▲
Le code généré par la partie précédente ne comporte pour le moment qu'un shell. Dans le processus destiné à rendre mon « système » autonome par rapport à l'hôte Linux, la prochaine étape sera d'implémenter les commandes de base. Je vais effectuer les préparatifs d'isolation en utilisant chroot.
3. Isolation par chroot▲
chroot (pour CHange ROOT) permet de lancer un processus dans une partie isolée du système. Une fois cette commande lancée, la racine « / » vue par le processus correspondra au dossier fourni en paramètre à la commande. Ce système permet d'isoler une partie du système du reste. En cas de problème, seule la zone « chrootée » sera impactée.
Appel de chroot :
chroot dossier commande
Ce qui donnera par exemple :
choot dossier_chroot /bin/bash
Cette commande positionnera / sur dossier_chroot et n'aura accès qu'à cette sous-zone, et ira lancer l'interpréteur de commande /bin/bash (placé dans le dossier _chroot/bin/bash).
3-1. Retour d'expérience▲
J'ai donc créé un dossier pour mon chroot (que j'ai nommé tout simplement chroot) et créé dedans un sous-dossier /bin dans lequel j'ai copié le fichier bash. Je me suis retrouvé avec l'erreur :
«Failed to run command '/bin/bash'
 : no such file or directory »
Ce message d'erreur est dû au fait que la commande /bin/bash chrootée ne trouve pas ses bibliothèques dynamiques (pour rappel, le dossier /lib n'est pas visible depuis le chroot). J'ai donc copié les bibliothèques nécessaires. Pour connaître celles-ci, j'ai utilisé la commande ldd. Sur mon poste, voici les bibliothèques retournées par ldd /bin/bash :
- /lib/i386-linux-gnu/libtinfo.so.5Â ;
- /lib/i386-linux-gnu/i686/cmov/libdl.so.2Â ;
- /lib/i386-linux-gnu/i686/cmov/libc.so.6Â ;
- /lib/ld-linux.so.2.
Sur votre distribution, le nom et le chemin d'accès des bibliothèques peuvent différer. Il est important de les copier dans les mêmes sous-dossiers de /lib.
Exemple : pour la bibliothèque libdl.so.2, il m'a fallu la placer dans le sous-dossier /lib/i386-linux-gnu/i686/cmov.
Une fois les bibliothèques copiées, que je vais par la suite appeler dépendances, le chroot fonctionnera.
Aucune commande telle que « ls » ne fonctionnera, sauf si vous les copiez dans le dossier chroot avant de chrooter.
Afin de pouvoir tester, j'applique le même principe pour la commande « ls », en recopiant celle-ci dans le dossier bin du chroot sans oublier les bibliothèques supplémentaires nécessaires à ls.
3-2. Test avec mon shell▲
Mon shell dépend de libreadline.so.6, utilisé pour la gestion de l'historique, en plus des bibliothèques déjà nécessaires. Je copie donc cette bibliothèque ainsi que mon code lui-même dans le dossier de chroot. L'appel à ls depuis mon shell fonctionne.
4. Création d'un live-cd▲
Pour pouvoir tester mon code, je vais créer un live-cd personnalisé. Pour cela, je vais modifier un live-cd existant Debian 7. Ceci sera effectué sous environnement virtualisé VirtualBox, ce qui me permettra de facilement effectuer les modifications nécessaires.
4-1. Fonctionnement d'un live-cd▲
4-1-1. UnionFS▲
Un live-cd va démarrer sur un filesystem spécial. Celui-ci est un « unionFS » avec une partie en lecture seule, et l'autre en lecture-écriture en RAM (ou ailleurs pour les UnionFS utilisés hors live-cd). Le nom unionFS vient du principe d'union en C permettant de stocker dans un même espace mémoire deux variables différentes.
Rappel :
union
mon_union
{
int
entier ;
double
reel ;
}
Le système réservera pour cette union l'espace mémoire de la plus grande possibilité soit 8 octets (car un int prend 4 octets sur une machine 32 bits, et un double 8 octets).
En appelant mon_union.entier, le contenu sera traité comme un entier, en appelant mon_union.reel, le contenu sera traité comme un réel double précision. Appeler l'un puis l'autre n'aura aucun sens ; la valeur sera erronée, c'est soit l'un ou soit l'autre, les deux pseudovariables occupant le même espace mémoire.
Ceci appliqué à l'unionFS.
Deux points de montage distincts sont reliés de façon à être vus comme un seul. Ils se superposent. Dans le cas d'un Live-cd, une partie est en lecture seule, et une seconde en lecture-écriture, en RAM.
Tant qu'aucune modification n'est effectuée, c'est la partie lecture seule qui est utilisée. Si un fichier est modifié, celui-ci sera écrit dans le point de montage dédié à l'écriture et c'est la nouvelle version qui sera « vue » au niveau système (la version d'origine toujours présente dans la partie en lecture seule est alors masquée par la nouvelle version dans la zone lecture-écriture). La suppression d'un fichier est gérée, le fichier dans la partie lecture seule est alors caché.
La partie read-write d'un live-cd est montée en ramdisk, elle est donc volatile. Il est possible de conserver les modifications en utilisant la fonctionnalité « persistance ».
Pour plus d'information : https://fr.wikipedia.org/wiki/Union_File_System
4-1-2. SquashFS▲
SquashFS est un système de fichiers qui est compressé et en lecture seule. La décompression s'effectue à la volée.
Pour plus d'information : https://fr.wikipedia.org/wiki/SquashFS.
Les live-cd récents couplent un squashFS avec AuFS, qui fonctionne comme UnionFS.
4-2. Préparation du live-cd▲
Je prépare donc une VM avec un disque virtuel de 20 Go, et démarre depuis le live-cd. Je crée une partition et un système de fichiers sur le disque virtuel, que je monte dans /mnt. Je monte le cd sur lequel je viens de booter dans /media.
Je copie ensuite le contenu de celui-ci dans /mnt/iso.
Je monte ensuite le contenu du squashFS, le fichier se trouvant dans le dossier live du cd sous le nom filesystem.squashfs dans /mnt/squash avec la commande suivante :
mkdir /mnt2
mount -t squashfs -o loop /mnt/iso/live/filesystem.squashfs /mnt2
Un message d'avertissement me prévient que le système de fichiers est en lecture seule, comme déjà précisé, un squashFS est en lecture seule.
Je copie ensuite le contenu du squashFS dans un dossier créé pour le besoin :
mkdir /mnt/squash
cp -r -v /mnt2 /mnt/squash
Les options -r et -v de cp permettent respectivement de faire une copie récursive et de voir les noms de fichiers sur la console (mode verbeux).
Je recopie ensuite le fichier /etc/resolv.conf au même endroit dans le dossier squash de façon à être sûr d'avoir accès à Internet.
La commande :
du -hs /mnt/squash
permet d'obtenir la taille du système de fichiers squash décompressé : 3,5 Go.
Je prépare le chroot.
Je copie le fichier /etc/mtab pour éviter les messages d'erreur.
J'effectue ensuite les montages suivants :
mount --bind proc /mnt/squash/proc
mount --bind sys /mnt/squash/sys
mount -t devpts none /mnt/squashfs/dev/pts
Enfin le chroot proprement dit :
chroot /mnt/squash
J'ai décidé de faire un CD minimal sans interface graphique, libre à vous de faire votre propre configuration, mais cela est un autre sujet.
Je commence par reconfigurer les locales pour éviter les messages d'erreur de type :
Cannot set LC_ALL to default locale
Pour reconfigurer :
dpkg-reconfigure locales
Je sélectionne toutes les locales, et FR_UTF-8 par défaut.
J'exécute les commandes suivantes :
apt-get update
apt-get remove --purge gnome*
apt-get autoremove --purge
Ceci va désinstaller beaucoup de paquets dans le chroot, ceci ne touchera pas au système hôte (le live-cd Debian standard).
Pour lister les paquets installés :
dpkg -l|
more
Ceci permettra de voir la liste des paquets installés et donc d'enlever ceux qui vous sont inutiles. Leur description est assez explicite.
Pour affiner l'affichage, on peut effectuer un filtre avec grep. Exemple :
dpkg -l|
grep x11|
more
J'ai eu des soucis notamment lors de la désinstallation des paquets bluez. Pour résoudre le problème, j'ai effacé les fichiers commençant par bluez dans /var/lib/dpkg/info, j'ai pu ensuite continuer le processus.
Dans le doute, laissez les paquets inconnus. Attention à la suppression des bibliothèques.
Suppression des paquets orphelins avec deborphan :
apt-get install deborphan
apt get remove -purge $
(
deborphan)
apt-get purge deborphan
apt-get clean
Suppression des locales inutiles avec le paquet localepurge :
apt-get install localepurge
localepurge
apt-get remove localepurge --purge
Suppression des mans inutiles
Dans le dossier /usr/share/man, je supprime tous les dossiers sauf fr et man1,2,3 etc.
Nettoyage du cache et des logs :
rm /var/cache/* /var/log/* -r -f
Je crée ensuite un utilisateur (avec adduser), définis le mot de passe root, modifie le fichier etc/sudoers pour donner tous les droits à cet utilisateur (nom utilisateur ALL=(ALL) NOPASSWD:ALL) puis, après voir copié les fichiers, j'ajoute dans le fichier .profile la commande :
sudo chroot /chroot /shell
Cette pratique déroge aux règles de sécurité, mais permettra un fonctionnement complet et autonome en live-cd, c'est d'ailleurs le fonctionnement de ceux-ci, l'utilisateur sur lequel démarre la session est dans les sudoers.
J'efface etc/resolv.conf et etc/mtab du dossier squashfs, sors ensuite du chroot via exit, puis redémarre.
Après nettoyage, le dossier /mnt/squash pèse 950 Mo au lieu de 3,5 Go, mais on peut faire mieux.
Avant de régénérer le squashFS, je copie mon dossier « chroot » dans le dossier squashfs.
Avant de régénérer le squashFS, il me faut le paquet squashfs-tools :
apt-get update
apt-get install squashfs-tools
Je génère ensuite le nouveau SquashFS :
cd /mnt/squash
mksquashfs . /mnt/filesystem.squashfs
Je remplace ensuite le fichier iso/livbefilesystem,squashfs par celui venant d'être généré.
Il me reste ensuite à régénérer le fichier ISO. Avant cela, je modifie le menu du cd en modifiant les fichiers isolinux/live.cfg, et isolinux/menu.cfg.
Génération du cd proprement dit :
apt-get install genisoimage
cd /mnt/iso
genisoimage -o /mnt/cdrom.iso -b isolinux/isolinux.bin -c isolinux/boot.cat --no-emul-boot --boot-load-size 4
--boot-info-table -r -J ./
Le fichier .iso pèse 520 Mo contre 1,23 Go pour le cd d'origine. Il me reste ensuite à récupérer l'ISO, par le réseau par exemple, pour le sortir de ma VM.
4-3. Tests du CD et correction des problèmes▲
Lors du boot, j'ai eu le message d'erreur suivant :
sudo : effective uid is not 0
, is sudo installed setuid root ?
J'ai corrigé le problème en refaisant un chroot dans le dossier squash, et en changeant les droits comme ceci :
chmod 4755
usr/bin/sudo
Après cela, mon shell démarre correctement. Une fois les commandes créées, je pourrai les tester depuis un live-cd.
5. Mon application ls▲
Pour ma première version, j'utilise la fonction readdir pour lire les enregistrements dirent après ouverture du dossier « . » représentant le dossier courant avec opendir. Je teste si le nom de fichier commence par « . » et dans ce cas ne l'affiche pas, car les fichiers commençant par « . » sont des fichiers invisibles. Ils sont affichables avec l'option -a que je ne gère pas pour le moment. Par ailleurs, il n'y a pas de tri, contrairement au ls normal et les fichiers ne sont pas affichés en colonne. D'autre part, il n'y a pas de gestion de paramètres.
Mon application ls ne se comporte donc pas actuellement comme la fonction ls standard, et devra donc être reprise.
J'ai créé une fonction nommée erreur, que j'utilise en cas de problème d'ouverture du dossier ainsi qu'en cas de problème avec readdir.
5-1. Nouvelles fonctions▲
5-2. Source▲
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
void
erreur
(
)
{
fprintf
(
stderr,"
Erreur : %s
\n
"
,strerror
(
errno));
exit
(
EXIT_FAILURE);
}
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
DIR *
repertoire;
struct
dirent *
lecture;
repertoire=
opendir
(
"
.
"
);
if
(
repertoire==
NULL
) erreur
(
);
while
((
lecture=
readdir
(
repertoire)))
{
if
(
lecture==
NULL
) erreur
(
);
if
(
lecture->
d_name[0
]!=
'
.
'
) printf
(
"
%s
\n
"
,lecture->
d_name);
}
closedir
(
repertoire);
return
EXIT_SUCCESS;
}
6. Amélioration de ls▲
Ma nouvelle version gère le passage en paramètres de type *.txt. Toujours pas de gestion des arguments « -l » ou « -a ».
Pour cela, si le nombre d'arguments (argc) est supérieur à 1, je teste pour chaque entrée, si celle-ci est dans le tableau des arguments (et dans le cas d'une concordance, je l'affiche). Si argc est égal à 1 (donc présence uniquement de la commande d'appel), j'affiche l'entrée.
6-1. Source▲
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
void
erreur
(
)
{
fprintf
(
stderr,"
Erreur : %s
\n
"
,strerror
(
errno));
exit
(
EXIT_FAILURE);
}
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
DIR *
repertoire;
struct
dirent *
lecture;
int
boucle;
repertoire=
opendir
(
"
.
"
);
if
(
repertoire==
NULL
) erreur
(
);
while
((
lecture=
readdir
(
repertoire)))
{
if
(
lecture==
NULL
) erreur
(
);
if
(
lecture->
d_name[0
]!=
'
.
'
)
{
if
(
argc>
1
)
{
for
(
boucle=
1
;boucle<
argc;++
boucle)
{
if
(
strcmp
(
argv[boucle],lecture->
d_name)==
0
)
{
printf
(
"
%s
\n
"
,lecture->
d_name);
}
}
}
else
{
printf
(
"
%s
\n
"
,lecture->
d_name);
}
}
}
closedir
(
repertoire);
return
EXIT_SUCCESS;
}
7. Gestion des arguments▲
Pour gérer les arguments, j'utilise scandir en remplacement d'opendir et closedir. Si aucun argument n'est passé (en dehors de l'exécutable : argc=1), un scandir est effectué avec l'option de tri alphasort. Une boucle utilisant printf affiche ensuite tous les éléments de la structure obtenue de scandir.
Dans le cas de plusieurs arguments, j'utilise la fonction stat sur chacun d'eux. Je teste s'il s'agit d'un dossier avec S_ISDIR. Si ce n'est pas un dossier, je fais un simple printf, sinon j'applique un scandir sur l'argument de façon à afficher les éléments.
7-1. Nouvelles fonctions▲
7-2. S_ISDIR▲
S_ISDIR n'est pas une fonction, mais une macro. Celle-ci permet de tester la valeur du champ st_mode du bloc dirent, si celle-ci comporte l'attribut répertoire (S_IFDIR - 0x0040000)
7-3. Source▲
/**
***************************
* ls 0.5
* © 2015 - Christophe LOUVET
***************************
*/
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <dirent.h>
// refaction dernier code
// puis gestion arguments -l et -a - Ã commencer
typedef
struct
liste_chainee{
char
*
valeur;
struct
liste_chainee *
next;
}
liste_chainee;
struct
dirent **
lecture;
void
traitement
(
char
*
chaine)
{
if
(
strcmp
(
chaine,"
..
"
)!=
0
)
{
struct
stat buffer;
int
retour=
stat
(
chaine,&
buffer);
if
(
retour!=
0
)
{
fprintf
(
stderr,"
Accès impossible à %s : Aucun fichier ou dossier de ce nom
\n
"
,chaine);
}
else
{
if
(
S_ISDIR
(
buffer.st_mode)==
1
)
{
int
boucle2;
printf
(
"
\n
%s/:
\n
"
,chaine);
int
nbre=
scandir
(
chaine,&
lecture,0
,alphasort);
for
(
boucle2=
0
;boucle2<
nbre;++
boucle2)
{
struct
dirent *
entree;
entree =
lecture[boucle2];
if
(
entree->
d_name[0
]!=
'
.
'
) printf
(
"
%s
\n
"
,entree->
d_name);
}
printf
(
"
\n
"
);
}
else
{
printf
(
"
%s
\n
"
,chaine);
}
}
}
}
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
int
boucle;
liste_chainee *
liste=
NULL
;
if
(
argc==
1
)
{
int
nbre=
scandir
(
"
.
"
,&
lecture,0
,alphasort);
for
(
boucle=
0
;boucle<
nbre;++
boucle)
{
struct
dirent *
entree;
entree =
lecture[boucle];
if
(
entree->
d_name[0
]!=
'
.
'
) printf
(
"
%s
\n
"
,entree->
d_name);
}
exit
(
EXIT_SUCCESS);
}
for
(
boucle=
1
;boucle<
argc;++
boucle)
{
liste_chainee *
new_element=
malloc
(
sizeof
(
liste_chainee));
new_element->
valeur=
argv[boucle];
new_element->
next=
NULL
;
if
(
liste==
NULL
)
{
liste=
new_element;
}
else
{
liste_chainee *
liste_temp=
liste;
while
(
liste_temp->
next!=
NULL
)
{
liste_temp=
liste_temp->
next;
}
liste_temp->
next=
new_element;
}
}
liste_chainee *
liste_temp=
liste;
while
(
liste_temp->
next!=
NULL
)
{
traitement
(
liste_temp->
valeur);
liste_temp=
liste_temp->
next;
}
traitement
(
liste_temp->
valeur);
return
EXIT_SUCCESS;
}
7-4. Ajout du support de l'option -a▲
J'ai modifié le code pour gérer le paramètre -a. Je fais une boucle sur les arguments pour créer la liste chaînée de ceux-ci. Ceci me permettra d'avoir une liste des noms de fichier pour effectuer d'autres traitements dessus (exemple tri, affichage ou non selon critère, etc.) Si, dans la boucle, je détecte « -a », je positionne la variable fichiers_caches à 1, ce qui me servira lors de l'affichage. Si le fichier commence par « . », je ne l'affiche que si fichiers_caches est positionné à 1.
7-4-1. source▲
/**
***************************
* ls 0.6
* © 2015 - Christophe LOUVET
***************************
*/
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <dirent.h>
typedef
struct
liste_chainee{
char
*
valeur;
struct
liste_chainee *
next;
}
liste_chainee;
struct
dirent **
lecture;
int
fichiers_caches=
0
;
void
traitement
(
char
*
chaine)
{
struct
stat buffer;
int
retour=
stat
(
chaine,&
buffer);
if
(
retour!=
0
)
{
fprintf
(
stderr,"
Accès impossible à %s : Aucun fichier ou dossier de ce nom
\n
"
,chaine);
}
else
{
if
(
S_ISDIR
(
buffer.st_mode)==
1
)
{
int
nbre=
scandir
(
chaine,&
lecture,0
,alphasort);
int
boucle2;
printf
(
"
\n
%s/:
\n\n
"
,chaine);
for
(
boucle2=
0
;boucle2<
nbre;++
boucle2)
{
struct
dirent *
entree;
entree =
lecture[boucle2];
if
(
entree->
d_name[0
]!=
'
.
'
)
{
printf
(
"
%s
\n
"
,entree->
d_name);
}
else
{
if
(
fichiers_caches==
1
) printf
(
"
%s
\n
"
,entree->
d_name);
}
}
}
else
{
printf
(
"
%s
\n
"
,chaine);
}
}
}
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
int
boucle;
liste_chainee *
liste=
NULL
;
for
(
boucle=
1
;boucle<
argc;++
boucle)
{
if
(
strcmp
(
argv[boucle],"
-a
"
)==
0
)
{
fichiers_caches=
1
;
}
else
{
liste_chainee *
new_element=
malloc
(
sizeof
(
liste_chainee));
new_element->
valeur=
argv[boucle];
new_element->
next=
NULL
;
if
(
liste==
NULL
)
{
liste=
new_element;
}
else
{
liste_chainee *
liste_temp=
liste;
while
(
liste_temp->
next!=
NULL
)
{
liste_temp=
liste_temp->
next;
}
liste_temp->
next=
new_element;
}
}
}
if
(
liste==
NULL
)
{
liste=
malloc
(
sizeof
(
liste_chainee));
liste->
valeur=
strdup
(
"
.
"
);
liste->
next=
NULL
;
}
liste_chainee *
liste_temp=
liste;
while
(
liste_temp->
next!=
NULL
)
{
traitement
(
liste_temp->
valeur);
liste_temp=
liste_temp->
next;
}
traitement
(
liste_temp->
valeur);
return
EXIT_SUCCESS;
}
7-5. Option -l▲
L'option -l de ls permet l'affichage de détails sur les fichiers.
Pour gérer l'option -l, j'utilise le même système de détection que pour le paramètre -a. Pour avoir les informations étendues, je suis obligé d'utiliser stat sur tous les fichiers. Pour pouvoir utiliser stat dans les sous-dossiers, je stocke dans ma liste chaînée le nom de fichier avec le chemin. Je calcule la taille du path où je me trouve (« ./ » pour dossier en cours), et je crée une chaîne contenant le path, suivi de « / » et du nom de fichier. Lors de l'affichage, je retire le chemin en récupérant la position de la chaîne ne contenant pas « / » , la chaîne complète étant utilisée pour stat. Je crée une chaîne de « ------- » pour l'affichage des droits, traitée par récupération des valeurs retournées par stat. Pour la date du fichier, j'utilise la fonction strftime pour transformer le champ st_mtime du buffer utilisé par stat pour afficher la date de dernière modification.
Il n'y a pas de fonction libc retournant la date de création. Cette valeur est présente dans le système de fichiers si celui-ci le gère, mais non utilisable par la libc.
7-5-1. Nouvelles fonctions utilisées▲
7-5-2. Source▲
/**
***************************
* ls 0.7
* © 2015 - Christophe LOUVET
***************************
*/
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <dirent.h>
#include <time.h>
typedef
struct
liste_chainee{
 char
*
valeur;
struct
liste_chainee *
next;
}
liste_chainee;
struct
dirent **
lecture;
int
fichiers_caches=
0
;
int
fichiers_detail=
0
;
void
traitement_detail
(
char
*
chaine)
{
struct
stat buffer;
if
(
fichiers_detail==
1
)
{
int
entier=
stat
(
chaine,&
buffer);
char
*
temp=
chaine;
if
(
temp[0
]==
'
/
'
) ++
temp;
entier=
strcspn
(
temp,"
/
"
);
temp=
temp+
entier;
++
temp;
char
chaine_droits[11
]=
"
----------
"
;
if
(
S_ISLNK
(
buffer.st_mode)==
1
) chaine_droits[0
]=
'
l
'
;
if
(
S_ISDIR
(
buffer.st_mode)==
1
) chaine_droits[0
]=
'
d
'
;
if
((
buffer.st_mode &
S_IRUSR)==
S_IRUSR) chaine_droits[1
]=
'
r
'
;
if
((
buffer.st_mode &
S_IWUSR)==
S_IWUSR) chaine_droits[2
]=
'
w
'
;
if
((
buffer.st_mode &
S_IXUSR)==
S_IXUSR) chaine_droits[3
]=
'
x
'
;
if
((
buffer.st_mode &
S_IRGRP)==
S_IRGRP) chaine_droits[4
]=
'
r
'
;
if
((
buffer.st_mode &
S_IWGRP)==
S_IWGRP) chaine_droits[5
]=
'
w
'
;
if
((
buffer.st_mode &
S_IXGRP)==
S_IXGRP) chaine_droits[6
]=
'
x
'
;
if
((
buffer.st_mode &
S_IROTH)==
S_IROTH) chaine_droits[7
]=
'
r
'
;
if
((
buffer.st_mode &
S_IWOTH)==
S_IWOTH) chaine_droits[8
]=
'
w
'
;
if
((
buffer.st_mode &
S_IXOTH)==
S_IXOTH) chaine_droits[9
]=
'
x
'
;
char
buffer_time[80
];
strftime
(
buffer_time, 80
, "
%d.%m.%Y %H:%M
"
,localtime
(&
buffer.st_mtime));
printf
(
"
%s %d %10ld %s %s
\n
"
,chaine_droits,buffer.st_nlink,buffer.st_size,buffer_time,temp);
}
else
{
char
*
temp=
chaine;
if
(
temp[0
]==
'
/
'
) ++
temp;
int
entier=
strcspn
(
temp,"
/
"
);
temp=
temp+
entier;
++
temp;
printf
(
"
%s
\n
"
,temp);
}
}
void
traitement
(
char
*
chaine)
{
struct
stat buffer;
int
retour=
stat
(
chaine,&
buffer);
if
(
retour!=
0
)
{
fprintf
(
stderr,"
Accès impossible à %s : Aucun fichier ou dossier de ce nom
\n
"
,chaine);
}
else
{
if
(
S_ISDIR
(
buffer.st_mode)==
1
)
{
int
nbre=
scandir
(
chaine,&
lecture,0
,alphasort);
int
boucle2;
printf
(
"
\n
%s/:
\n\n
"
,chaine);
for
(
boucle2=
0
;boucle2<
nbre;++
boucle2)
{
struct
dirent *
entree;
entree =
lecture[boucle2];
if
(
entree->
d_name[0
]!=
'
.
'
)
{
char
*
nom_fichier=
malloc
(
strlen
(
chaine)+
1
+
strlen
(
entree->
d_name)+
1
);
strcpy
(
nom_fichier,chaine);
strcat
(
nom_fichier,"
/
"
);
strcat
(
nom_fichier,entree->
d_name);
traitement_detail
(
nom_fichier);
free
(
nom_fichier);
}
else
{
char
*
nom_fichier=
malloc
(
2
+
strlen
(
entree->
d_name)+
1
);
strcpy
(
nom_fichier,"
./
"
);
strcat
(
nom_fichier,entree->
d_name);
if
(
fichiers_caches==
1
) traitement_detail
(
nom_fichier);
free
(
nom_fichier);
}
}
printf
(
"
\n
"
);
}
else
{
char
*
nom_fichier=
malloc
(
2
+
strlen
(
chaine)+
1
);
strcpy
(
nom_fichier,"
./
"
);
strcat
(
nom_fichier,chaine);
traitement_detail
(
nom_fichier);
free
(
nom_fichier);
}
}
}
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
int
boucle;
liste_chainee *
liste=
NULL
;
for
(
boucle=
1
;boucle<
argc;++
boucle)
{
if
(
strcmp
(
argv[boucle],"
-a
"
)==
0
)
{
fichiers_caches=
1
;
}
else
if
(
strcmp
(
argv[boucle],"
-l
"
)==
0
)
{
fichiers_detail=
1
;
}
else
{
liste_chainee *
new_element=
malloc
(
sizeof
(
liste_chainee));
new_element->
valeur=
argv[boucle];
new_element->
next=
NULL
;
if
(
liste==
NULL
)
{
liste=
new_element;
}
else
{
liste_chainee *
liste_temp=
liste;
while
(
liste_temp->
next!=
NULL
)
{
liste_temp=
liste_temp->
next;
}
liste_temp->
next=
new_element;
}
}
}
if
(
liste==
NULL
)
{
liste=
malloc
(
sizeof
(
liste_chainee));
liste->
valeur=
strdup
(
"
.
"
);
liste->
next=
NULL
;
}
liste_chainee *
liste_temp=
liste;
while
(
liste_temp->
next!=
NULL
)
{
traitement
(
liste_temp->
valeur);
liste_temp=
liste_temp->
next;
}
traitement
(
liste_temp->
valeur);
return
EXIT_SUCCESS;
}
7-6. Gestion de plusieurs options telles que -al▲
Pour gérer cela, j'ai modifié le code dans la boucle « for » d'analyse des arguments. Dans celle-ci, je teste si le 1er caractère est un tiret. Si ce n'est pas le cas, j'insère l'argument dans la liste chaînée, sinon je parcours tous les caractères suivant le tiret jusqu'à « \0 » déterminant la fin de la chaîne d'arguments. Si le caractère est a, j'active l'affichage des fichiers cachés (fichiers_caches=1), si le caractère est l, j'active l'affichage détaillé (fichiers_detail=1).
7-7. Affichage en mode colonne▲
J'ai modifié le code de façon à afficher les noms de fichier en colonnes. Cet affichage ne se déclenche que depuis un terminal. En cas de redirection, les fichiers sont affichés en mode listing.
Je commence par parcourir la liste chaînée de façon à déterminer le nombre de fichiers à afficher. Je crée ensuite un tableau de pointeurs du nombre d'entrées trouvées et y recopie les pointeurs de la liste chaînée via une boucle for.
Je récupère ensuite via ioctl la taille d'une ligne du terminal.
struct
winsize w;
ioctl
(
STDOUT_FILENO, TIOCGWINSZ, &
w);
À partir de là w.ws_col contiendra le nombre de caractères par ligne du terminal.
Je recherche ensuite la chaîne la plus longue dans ma liste de pointeurs. De là , je détermine le nombre de colonnes possible en divisant w.ws_col par la longueur déterminée juste avant.
J'effectue ensuite deux boucles imbriquées :
- la première correspondant au nombre de lignes (nombre total de chaînes divisé par le nombre de colonnes) ;
- la seconde pour le nombre de colonnes affichant les chaînes.
À la fin de la première boucle, j'effectue un retour chariot. Les chaînes sont séparées par des espaces, leur nombre étant calculé avec la formule :
taille colonne (nombre de caractères par ligne/taille max de chaîne) - longueur de la chaîne.
7-7-1. Extrait du code concernant l'affichage en colonne▲
if
(
isatty
(
STDOUT_FILENO))
{
int
nbre_entrees=
0
;
liste_chainee *
noms_tmp=
noms;
while
(
noms_tmp->
next!=
NULL
)
{
++
nbre_entrees;
noms_tmp=
noms_tmp->
next;
}
char
*
tableau[nbre_entrees];
noms_tmp=
noms;
noms_tmp=
noms_tmp->
next;
for
(
boucle=
0
;boucle<
nbre_entrees;++
boucle)
{
tableau[boucle]=
noms_tmp->
valeur;
noms_tmp=
noms_tmp->
next;
}
int
longueur_chaine=
0
;
noms_tmp=
noms;
for
(
boucle=
0
;boucle<
nbre_entrees;++
boucle)
{
if
(
longueur_chaine<
strlen
(
noms_tmp->
valeur)) longueur_chaine=
strlen
(
noms_tmp->
valeur);
noms_tmp=
noms_tmp->
next;
}
struct
winsize w;
ioctl
(
STDOUT_FILENO, TIOCGWINSZ, &
w);
int
colonnes=
w.ws_col/
longueur_chaine;
for
(
boucle=
0
;boucle<
nbre_entrees;boucle+=
colonnes)
{
int
i;
for
(
i=
boucle;i<(
boucle+
colonnes);++
i)
{
if
(
i<
nbre_entrees)
{
printf
(
"
%s
"
,tableau[i]);
int
j;
for
(
j=
strlen
(
tableau[i]);j<
longueur_chaine+
1
;++
j)
{
printf
(
"
"
);
}
}
}
printf
(
"
\n
"
);
}
}
else
{
liste_chainee *
noms_tmp=
noms;
while
(
noms_tmp->
next!=
NULL
)
{
printf
(
"
%s
\n
"
,noms_tmp->
valeur);
noms_tmp=
noms_tmp->
next;
}
printf
(
"
%s
\n
"
,noms_tmp->
valeur);
}
7-7-2. Fonctions IOCTL▲
Prototype :
#include <sys/ioctl.h>
int
ioctl
(
int
d, int
requête, ...);
IOCTL permet de contrôler des périphériques en envoyant des codes requêtes à celui-ci. Le descripteur de fichier du 1er paramètre doit correspondre à un fichier ouvert. Comme sous Unix/Linux tout est fichier, IOCTL peut permettre de passer des paramètres à la carte son, la carte réseau par exemple. Les codes requête sont spécifiques au périphérique.
man ioctl_list permet d'avoir un peu plus d'informations.
8. cat▲
L'implémentation de cat n'a pas été spécialement difficile. J'effectue une boucle sur les arguments au cas où plusieurs fichiers ont été passés en paramètres. Dans cette boucle, j'ouvre le fichier, le lis, et affiche ce qui a été lu jusqu'à la fin du fichier sur stdout. Au tout début, je vérifie s'il y a au moins deux arguments.
Contrairement à mon code, la fonction cat sans paramètre va effectuer un echo ligne par ligne de ce qui est saisi au clavier.
8-1. Source▲
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
int
boucle;
FILE *
fichier;
char
buffer[32768
];
int
lecture;
if
(
argc<
2
) exit (
0
);
for
(
boucle=
1
;boucle<
argc;++
boucle)
{
fichier=
fopen
(
argv[boucle],"
r
"
);
if
(
fichier!=
NULL
)
{
lecture=-
1
;
while
(
lecture!=
0
)
{
lecture=
fread
(
buffer,1
,32768
,fichier);
if
(
lecture!=
0
) fwrite
(
buffer,1
,lecture,stdout);
}
if
(
feof
(
fichier)==
0
) fprintf
(
stderr,"
cat: %s: %s
\n
"
,argv[boucle],strerror
(
errno));
fclose
(
fichier);
}
else
{
fprintf
(
stderr,"
cat: %s: %s
\n
"
,argv[boucle],strerror
(
errno));
}
}
return
0
;
}
9. Automatisation de la compilation▲
Étant donné que j'ai maintenant plusieurs commandes, j'ai créé un petit script. J'utiliserai plus tard un makefile.
9-1. Script de compilation▲
#!/bin/bash
gcc shell.c -lreadline -o shell
gcc ls.c -o ls
gcc cat.c -o cat
9-2. Script de préparation de chroot▲
#!/bin/bash
if
[ -z "
$1
"
]
then
echo "Usage: install nom_dossier"
else
echo "Installation dans
$1
"
mkdir -p $1
/bin
# cp /bin/bash $1/bin # pour avoir le shell normal
cp ./shell $1
/bin/bash
cp ./ls $1
/bin
cp ./cat $1
/bin
mkdir -p $1
/lib/i386-linux-gnu/i686/cmov
cp /lib/ld-linux.so.2
$1
/lib
cp /lib/i386-linux-gnu/libreadline.so.6
$1
/lib/i386-linux-gnu
cp /lib/i386-linux-gnu/i686/cmov/libc.so.6
$1
/lib/i386-linux-gnu/i686/cmov
cp /lib/i386-linux-gnu/libtinfo.so.5
$1
/lib/i386-linux-gnu
fi
Ce script copie tous les fichiers nécessaires dans le dossier donné en paramètre. Le chroot fonctionnera et donnera accès à mon shell et à mes commandes ls et cat.
10. pwd▲
La fonction pwd permet d'afficher le nom du dossier courant. C'est la fonction getcwd qui permet de récupérer celui-ci dans une chaîne de caractères.
#include <unistd.h>
#include <stdio.h>
int
main
(
)
{
char
dossier_en_cours[4096
];
getcwd
(
dossier_en_cours,4096
);
printf
(
"
%s
\n
"
,dossier_en_cours);
return
0
;
}
11. cd ▲
cd n'est pas une commande Unix/Linux, mais une commande interne à l'interpréteur de commandes.
Pour intégrer cd, j'ai commencé par réaliser ce code :
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
char
*
ancien_chemin=
getenv
(
"
PWD
"
);
int
retour=
chdir
(
argv[1
]);
if
(
retour!=
0
)
{
fprintf
(
stderr,"
cd : %s
"
,strerror
(
errno));
exit
(
EXIT_FAILURE);
}
setenv
(
"
PWD
"
,argv[1
],1
);
setenv
(
"
OLDPWD
"
,ancien_chemin,1
);
printf
(
"
valeur PWD : %s
\n
"
,getenv
(
"
PWD
"
));
char
buffer[512
];
printf
(
"
retour getcwd : %s
\n
"
,getcwd
(
buffer,512
));
return
EXIT_SUCCESS;
}
Ce code n'est pas effectif, le changement de répertoire n'étant valable que pour le processus en cours. Le répertoire de base du processus père appelant n'est pas modifié.
Les printf sont là pour démontrer le changement de dossier par défaut (dans le processus ./cd).
11-1. Intégration de cd dans le shell▲
Mon code change le dossier en cours via chdir, teste la validité de celui-ci avec le retour de la fonction. Si celui-ci est valide, je récupère le contenu de la variable d'environnement PWD, copie son contenu dans la variable d'environnement OLDPWD. J'affecte ensuite à PWD la valeur du nouveau dossier en cours.
J'ai changé le prompt. Celui-ci se compose maintenant du nom du dossier courant (récupéré via la valeur de PWD), suivi de « $ ». Si le chemin en cours se trouve dans le dossier HOME, je remplace le début de chemin « /home/user/xxx » par « ~/xxx ».
Le prompt utilisé par le shell correspond à la chaîne contenue dans la variable d'environnement « PS1 ».
Celle-ci gère codes spéciaux d'échappement suivant :
\u |
Nom de l'utilisateur |
\h |
Nom de la machine |
\w |
Répertoire courant |
\W |
Partie terminale répertoire courant (~ /) |
Le prompt est en général défini dans le fichier .bashrc, fichier lancé au démarrage du shell depuis son dossier home et/ou /etc/profile pour la configuration « tout utilisateur ».
12. cp▲
La commande cp permet la copie de un ou plusieurs fichiers. Si plus de deux arguments sont passés, le dernier doit être un répertoire.
Voici mon algorithme :
Algorithme pour la partie recopie :
13. Busybox : le couteau suisse des commandes de base▲
Pour la suite de l'implémentation des commandes de base, j'ai choisi d'appliquer le principe utilisé par Busybox. Celui-ci est un logiciel intégrant la majorité des commandes de base dans une seule application. L'appel de la commande cp intégrée à Busybox s'effectuera comme ceci :
busybox cp source destination
Busybox est inclus dans l'initramfs à des fins de dépannage. Dans ce cas, les commandes cat, cp, etc. sont des liens symboliques vers busybox. Busybox lira le premier argument argv pour connaître la commande à exécuter (ou affichera un prompt si aucun argument ne lui a été passé).
Busybox a été conçu à la base pour être utilisé dans des disquettes de dépannage. Il est maintenant répandu notamment dans les systèmes embarqués.
Les fonctions intégrées à Busybox ont moins d'options que les fonctions standard. Elles sont à usage de dépannage.
Plus d'informations sur BusyBox.
13-1. Mon implémentation▲
Dans mon implémentation, si aucun paramètre n'est passé, j'affiche la liste des commandes intégrées (donc pour le moment uniquement cat). L'appel à cat lance ma fonction cat (argc,argv) qui est en fait la copie du main de mon ancien cat.c.
Nouvelle fonction :Basename: fonction retournant le nom du fichier depuis un path (nécessite libgen.h), utilisée pour tester le nom de l'exécutable appelé (dans ma comparaison avec « cat »).
13-1-1. Code source▲
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <libgen.h>
void
cat
(
int
argc,char
**
argv)
{
int
boucle;
FILE *
fichier;
char
buffer[32768
];
int
lecture;
if
(
argc<
2
) exit
(
0
);
for
(
boucle=
1
;boucle<
argc;++
boucle)
{
fichier=
fopen
(
argv[boucle],"
r
"
);
if
(
fichier!=
NULL
)
{
lecture=-
1
;
while
(
lecture!=
0
)
{
lecture=
fread
(
buffer,1
,32768
,fichier);
if
(
lecture!=
0
) fwrite
(
buffer,1
,lecture,stdout);
}
if
(
feof
(
fichier)==
0
) fprintf
(
stderr,"
cat: %s: %s
\n
"
,argv[boucle],strerror
(
errno));
fclose
(
fichier);
}
else
{
fprintf
(
stderr,"
cat: %s: %s
\n
"
,argv[boucle],strerror
(
errno));
}
}
}
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
if
(
argc==
1
)
{
printf
(
"
\n
Usage mybusybox [fonction] [arguments] ...
\n\n
"
);
printf
(
"
Commandes actuellement disponibles :
\n
"
);
printf
(
"
\t
cat
\n
"
);
exit
(
EXIT_FAILURE);
}
else
{
if
(
strcmp
(
basename
(
argv[0
]),"
busybox
"
)==
0
)
{
argc--
;
argv++
;
}
if
(
strcmp
(
argv[0
],"
cat
"
)==
0
) cat
(
argc,argv);
}
return
0
;
}
13-2. Intégration de mes commandes dans busybox▲
Busybox appellera les commandes qui seront stockées dans des fichiers .c externes (un par commande). Mon fichier main comportant un ou des fichiers .h en conséquence.
Exemple :
extern
void
ls
(
int
argc,char
**
argv);
extern
void
cat
(
int
argc,char
**
argv);
J'ai utilisé pour cela Cmake de façon à gérer facilement un Makefile. La première chose que fait ma busybox, c'est contrôler si le premier paramètre correspond à « busybox ». Si c'est le cas, je retire celui-ci des arguments (en diminuant argc et en augmentant l'adresse de argv pour aller sur le second argument).
Fichier CMakeList.txt pour générer le MakeFile :
project
(
busybox C)
set
(
SRCS main.c busybox_cat.c busybox_cp.c)
set (
HEADERS busybox_cat.h busybox_cp.h)
add_executable
(
busybox ${SRCS}
${HEADERS}
)
Le Makefile sera généré par la commande suivante :
cmake -G "Unix Makefile"
Puis la compilation via :
make
En cas d'ajout de source, ne pas oublier de modifier le fichier CMakeLists.txt. L'appel de « make » prendra automatiquement ces modifications en compte.
J'ai ensuite procédé de la même façon pour intégrer pwd et cp.
13-3. Ajout de la commande la rm▲
J'ai intégré la commande rm dans ma busybox. Pour la commande rm, j'utilise la fonction remove qui utilise unlink pour les fichiers et rmdir pour les dossiers. Pour pouvoir utiliser l'option -r (suppression récursive) j'ai dû créer une fonction réentrante. Dans le main la fonction stat contrôle si les paramètres d'appel sont des dossiers et appelle dans ce cas ma fonction réentrante, sinon elle appelle remove. Ma fonction réentrante (attendant en paramètre un nom de dossier) ouvre celui-ci avec opendir, puis une boucle while s'effectue pour toutes les entrées du dossier. À ces entrées, je rajoute le nom du dossier et un slash (dans une chaîne temporaire) avant de faire un stat dessus. En cas de dossier, je rappelle ma fonction réentrante, sinon j'effectue un appel à remove. J'effectue ensuite un autre appel à remove après la boucle while pour supprimer l'entrée s'il s'agit d'un dossier qui est censé être maintenant vide (remove ne pouvant supprimer un dossier contenant des entrées). Je ferme ensuite le dossier avec la fonction closedir.
13-3-1. Fonctions utilisées ▲
14. Ajout des commandes chdir et rmdir▲
Les fonctions de la bibliothèque portent le même nom que les commandes. J'ai donc ajouté le traitement sur la même base que le code précédent.
14-1. Fonctions utilisées▲
À la place de rmdir, j'aurais pu aussi utiliser remove (cf rmAjout de la commande mv).
15. Ajout de la commande mv▲
mv déplace ou renomme le fichier selon le cas via la fonction système rename.
16. ln : Liens symboliques et hardlinks▲
16-1. Lien symbolique ▲
Un lien symbolique, c'est un fichier (ou enregistrement dans un inode) qui va pointer vers un autre fichier. On peut le reconnaître par le caractère « l » au niveau des droits POSIX ainsi que par le fichier de destination sur lequel pointe le lien.
ls -l
total 716
-rwxr-xr-x 2
totof totof 0
déc. 5
14
:40
busybox
drwxr-xr-x 3
totof totof 4096
déc. 2
09
:08
busybox_source
lrwxrwxrwx 1
totof totof 9
nov. 6
19
:25
cat ->
./busybox
Un lien symbolique se crée par la commande suivante :
ln -s fichier_source nom_lien_destination
16-2. Hardlink ▲
Un hardlink est différent d'un lien symbolique dans le sens où il crée une entrée dans le système de fichiers pointant sur la même inode que le fichier source. L'appel au fichier peut se faire par l'un ou l'autre de ses noms, mais cela reste le même contenu. Lors de la suppression d'un des noms, le fichier est toujours accessible depuis l'autre. Le fichier ne sera détruit que lorsqu'il n'y aura plus aucune entrée pointant sur l'inode.
Création d'un hardlink :
ln fichier_source fichier destination
16-3. Implémentation▲
17. Date▲
Pour cette commande, sans paramètre, la date en cours est affichée. Je commence par appeler la fonction time(), laquelle retourne le nombre de secondes écoulées depuis le 01/01/1970 00:00:00 GMT (Epoch). Cette valeur est ensuite passée à une structure tm_struct via la fonction localtime().
struct
tm {
int
tm_sec; /* secondes */
int
tm_min; /* minutes */
int
tm_hour; /* heures */
int
tm_mday; /* quantième du mois */
int
tm_mon; /* mois (0 Ã 11 !) */
int
tm_year; /* année */
int
tm_wday; /* jour de la semaine */
int
tm_yday; /* jour de l'année */
int
tm_isdst; /* décalage horaire */
}
;
La transformation de variable time_t vers une structure tm prend en compte le décalage horaire entre l'heure locale et l'heure GMT (aussi appelée UTC), la valeur time_t étant exprimée par rapport à GMT. Il est aussi possible de faire l'inverse, la transformation de structure tm en time_t via notamment mktime(), qui sera utilisé pour changer la date.
J'utilise ensuite la fonction strftime() avec la structure tm pour afficher la date. Cette fonction prend, en paramètre, une chaîne de caractères décrivant la façon dont la date va être affichée (voir la page de man pour les détails).
Pour modifier la date, je contrôle son format :
- format normal : MMDDhhmmYY.ss (mois, jour, heure, minute, année, seconde - exemple pour 18/12/2015 9:40:00 : 1218094015,00) ; contrôle de longueur de chaîne (13 caractères, présence du point à la position 10, vérification des valeurs numériques via la fonction strtol()) ;
- format date abrégé ne changeant que la date : JJ/MM/AAAA ; contrôle de longueur de chaîne (10 caractères), de présence des « / » aux positions 2 et 5, vérifications valeurs numériques ;
- format heure abrégé ne changeant que l'heure : HH:MN:SS ; contrôle de longueur de chaîne (10 caractères), de présence des « : » aux positions 2 et 5, vérification des valeurs numériques.
Après contrôle, j'affecte les valeurs adéquates correspondant à ma nouvelle date dans une structure tm, puis utilise la fonction mktime() de façon à convertir la structure en time_t, qui sera ensuite utilisée avec la fonction stime() pour changer la date courante.
17-1. Nouvelles fonctions utilisées ▲
18. Ajout d'un éditeur de texte▲
Une commande de base importante nécessaire à un système est un éditeur de texte. C'est la prochaine commande que je vais implémenter.
18-1. vi VS nano▲
vi et nano sont les éditeurs de texte les plus répandus sur les Unix-like. J'ai personnellement une préférence pour nano que je trouve plus simple à utiliser, mais vous recommande d'avoir un minimum de compétences sur vi, que vous trouverez partout, ce qui n'est pas forcément le cas de nano.
Il me serait possible d'utiliser un tronc commun et d'implémenter par-dessus une interface vi et une interface nano pour le même noyau d'éditeur.
Je vais utiliser la bibliothèque ncurses, permettant de gérer l'interface du terminal. Cette bibliothèque (comme abordé dans l'article précédent) est connue pour gérer les menus de paramétrage. Mon premier objectif sera de charger un fichier texte, et d'afficher son contenu page par page comme le fait vi ou nano. Nano utilise ncurses (mais pas vi). De ce fait, ma version sera différente du vi original.
vi étant intégré à BusyBox, je vais donc me caler sur celui-ci.
18-1-1. Mode commande/mode insertion de vi:▲
vi comporte un mode commande et un mode insertion. Au démarrage, vi est en mode commande. Pour passer du mode commande au mode insertion, il suffit d'appuyer sur la touche « i ». Pour repasser en mode commande, il faut appuyer sur la touche « echap ». Dans le mode commande, l'appui de touches du clavier déclenche des fonctionnalités : exemple, l'appui sur les touches « dd » effacera la ligne courante.
Plus d'infos sur les commandes de vi : https://doc.ubuntu-fr.org/vim
Pourquoi Vim et pas Vi ? Car Vim est une évolution de vi (Vi Improved) qui a tendance à remplacer vi dans les distributions Linux.
Les aficionados de la mythique machine Amiga seront ravis d'apprendre que Vim a tout d'abord été développé en 1988 sur celle-ci, le portage Unix ayant été effectué en 1992 (je parle de Vim, pas de vi).
L'appui sur la touche « : » en mode commande va permettre de saisir une chaîne de caractères (qui sera affichée tout en bas de l'écran) permettant de saisir certaines commandes (exemple : q pour quitter, wq pour quitter et sauvegarder, etc.).
18-2. NCurses▲
Ncurses est une bibliothèque permettant de gérer l'affichage dans un terminal. Vous connaissez tous cette bibliothèque à travers les menus de configuration en mode texte tels que celui-ci :
|
18-2-1. Premier test avec Ncurses▲
Pour utiliser Ncurses, il faut tout d'abord initialiser la bibliothèque avec la fonction initscr(). Avant de quitter votre programme, il faudra lancer endwin() qui aura pour effet de rétablir les réglages du terminal et de réafficher l'écran d'origine.
Une fois initscr() appelée, il n'est plus possible d'utiliser correctement les fonctions accédant à la console telles que printf. Il faut utiliser leur équivalent Ncurses. Exemple :
- printw à la place de printf ;
- getinstr à la place de gets ;
- getch à la place de getchar.
Documentation de Ncurses : ici et ici.
Exemple de « Hello World ! » :
#include <ncurses.h>
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
initscr
(
);
printw
(
"
Hello World !
"
);
getch
(
);
endwin
(
);
return
0
;
}
Il m'a été nécessaire d'insérer l'option -lncurses à gcc pour intégrer la bibliothèque dans mon exécutable.
18-3. Chargement de fichier▲
Pour charger le fichier, je lis son contenu ligne par ligne via la fonction fgets. fgets qui permet de lire un fichier ligne par ligne, s'arrête après lecture d'un nombre de caractères donné (idéal pour limiter à la taille de l'écran) ou s'arrête suite à un retour chariot. L'affichage se fera ensuite tout simplement avec printw.
FILE *
handle=
fopen
(
"
/home/test/test.txt
"
,"
r
"
);
char
chaine[250
];
if
(
handle!=
NULL
)
{
while
(!
feof
(
handle))
{
char
*
retour=
fgets
(
chaine,COLS,handle);
if
(
retour!=
NULL
)
{
printw
(
chaine);
}
else
{
if
(
errno!=
0
)
{
printw
(
"
\n
Erreur : %s
\n
Appuyer sur une touche pour continuer ...
"
,strerror
(
errno));
}
}
}
}
else
{
printw
(
"
\n
Erreur ouverture : %snAppuyer sur une touche pour continuer ...
"
,strerror
(
errno));
}
getch
(
);
Ceci ne permet pas l'affichage correct d'un document de plus d'une page. L'ajout de la fonction Ncurses scrollok() permet le scrolling, c'est-à -dire le décalage vers le haut de toutes les lignes une fois le bas de page atteint. Les lignes effacées par le scrolling sont perdues.
18-4. Gestion du fenêtrage▲
La bibliothèque Ncurses utilise la notion de fenêtre et/ou sous-fenêtre pour afficher le texte. Ceci est mal adapté à la situation, une fenêtre ne représentant que la « première page » du fichier. Je me suis tourné vers la notion de pad, le pad étant une fenêtre virtuelle pouvant être plus grande que l'écran. La notion de « fenêtre affichée » correspondra donc à la partie affichée de cette fenêtre virtuelle. Il faut utiliser les fonctions de rafraîchissement spécifiques aux pads, telles que prefresh() à la place des fonctions refresh(). prefresh() prendra en paramètre les coordonnés du rectangle correspondant à la partie devant être affichée.
Les pads sont créés de la même façon que les fenêtres et utilisent la même structure *WINDOW. La fonction de création d'un pad est newpad() à laquelle il faut fournir le nombre de lignes et colonnes.
À cette fin, je parcours le fichier une première fois de façon à obtenir le nombre de lignes nécessaires.
Pour simplifier l'utilisation, j'utilise la fonction copywin pour afficher page par page le contenu du pad.
#include <ncurses.h>
#include <string.h>
#include <errno.h>
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
WINDOW *
my_pad;
int
nbre_lignes=
0
;
int
boucle;
initscr
(
);
FILE *
handle=
fopen
(
"
/home/totof/hdparm.conf
"
,"
r
"
);
char
chaine[250
];
if
(
handle!=
NULL
)
{
while
(!
feof
(
handle))
{
char
*
retour=
fgets
(
chaine,COLS,handle);
if
(
retour!=
NULL
)
{
++
nbre_lignes;
}
else
{
if
(
errno!=
0
) printw
(
"
\n
Erreur : %s
\n
Appuyer sur une touche pour continuer ...
"
,strerror
(
errno));
}
}
rewind
(
handle);
my_pad=
newpad
(
nbre_lignes,COLS);
for
(
boucle=
0
;boucle<
nbre_lignes;++
boucle)
{
char
*
retour=
fgets
(
chaine,COLS,handle);
if
(
retour!=
NULL
)
{
wprintw
(
my_pad,chaine);
}
else
{
if
(
errno!=
0
) printw
(
"
\n
Erreur : %s
\n
Appuyer sur une touche pour continuer ...
"
,strerror
(
errno));
}
}
fclose
(
handle);
}
else
{
printw
(
"
\n
Erreur ouverture : %snAppuyer sur une touche pour continuer ...
"
,strerror
(
errno));
}
for
(
boucle=
0
;boucle<
nbre_lignes/
LINES+
1
;++
boucle)
{
int
limite=
boucle*
LINES+
LINES-
1
;
if
(
limite>
nbre_lignes-
1
) limite=
nbre_lignes-
1
;
clear
(
);
copywin
(
my_pad,stdscr,boucle*
LINES,0
,0
,0
,limite-
boucle*
LINES,COLS-
1
,FALSE);
refresh
(
);
getch
(
);
}
endwin
(
);
return
0
;
}
18-5. Ajout d'une fonction de sauvegarde▲
Pour la sauvegarde, je récupère ligne par ligne le texte dans le pad via la fonction mvinstr(), récupérant la chaîne dans la fenêtre spécifiée à l'emplacement spécifié. Je retraite ensuite cette chaîne avant écriture dans le fichier de destination afin de supprimer les caractères de padding (espaces) mis par Ncurses à la fin de chaque ligne. Je place un retour chariot à l'emplacement du premier espace en fin de ligne, et clôture la chaîne avec un caractère 0. Ceci aura pour conséquence, une différence entre le fichier source et le fichier destination en cas de présence d'espace avant retour chariot dans le fichier source. Je laisse cela de façon délibérée, car il n'y a aucun intérêt à avoir un espace avant un retour chariot.
Code de sauvegarde :
handle=
fopen
(
"
/home/totof/Documents/code/test.txt
"
,"
w
"
);
if
(
handle!=
NULL
)
{
for
(
boucle=
0
;boucle<
nbre_lignes;++
boucle)
{
mvwinstr
(
my_pad,boucle,0
,chaine);
int
j=
strlen
(
chaine)-
1
;
do
{
--
j;
}
while
(
chaine[j]==
'
'
);
if
(
chaine[j++
]==
'
'
) ++
j;
chaine[++
j]=
'
\n
'
;
chaine[++
j]=
'
\0
'
;
int
retour=
fputs
(
chaine,handle);
if
(
retour==
EOF)
{
clear
(
);
printw
(
"
%snAppuyer sur une touche pour continuer ...
"
,strerror
(
errno));
}
}
fclose
(
handle);
}
else
{
printw
(
"
\n
Erreur sauvegarde fichier : %snAppuyer sur une touche pour continuer ...
"
,strerror
(
errno));
getch
(
);
}
18-6. Boucle « événementielle »▲
Les codes de chargement et de sauvegarde ont été placés dans des fonctions distinctes. J'ai créé une boucle sans fin pour gérer l'appui sur une touche. En cas d'appui sur la touche echap, la sauvegarde est déclenchée puis endwin et exit sont appelés.
int
main
(
int
argc,char
*
argv[],char
*
arge[])
{
initscr
(
);
noecho
(
);
keypad
(
stdscr,1
); // permet l'interception des touches spéciales
chargement
(
);
copywin
(
my_pad,stdscr,0
,0
,0
,0
,LINES-
1
,COLS-
1
,FALSE);
while
(
1
)
{
int
carac=
getch
(
);
if
(
carac==
ESC)
{
sauvegarde
(
);
endwin
(
);
exit
(
0
);
}
}
}
J'ai dû ajouter keypad(stdscr,1); juste après la création du pad, dans la fonction chargement. Ce code permet la gestion correcte des séquences escape CSI. Cela permet aussi de dissocier par exemple l'appui sur la touche echap et l'appui sur la touche Page Down ; les touches spéciales déclenchant une séquence CSI, commençant par echap.
Vous pourrez remarquer que l'appui sur la touche Echap ne déclenche pas une sortie immédiate. Ceci est dû au traitement des séquences d'échappement (déjà évoqué). NCurses attend une éventuelle complétion de chaîne et considère celle-ci comme complète au bout de quelques secondes. Il est possible d'intervenir sur ce délai via la fonction set_escdelay.
J'ai également dû ajouter la fonction noecho(); pour que le texte tapé au clavier ne soit pas affiché.
Le code de sortie a été placé dans la boucle if détectant l'appui de la touche echap.
18-7. Gestion du déplacement dans le fichier▲
Lors de l'appui sur les touches Page UP et Page DOWN, j'incrémente ou décrémente une variable nommée page_en_cours. Je contrôle les bornes : une valeur de page_en_cours négative remet celle-ci à 0 et le réaffichage n'est pas déclenché. Je contrôle également la limite vers le bas afin de ne pas dépasser la taille du PAD.
While (
1
)
{
…
else
if
(
carac==
KEY_NPAGE) // page down
{
++
page_en_cours;
if
(
page_en_cours>
nbre_lignes/
LINES)
{
--
page_en_cours;
}
else
{
int
limite=
page_en_cours*
LINES+
LINES-
1
;
if
(
limite>
nbre_lignes-
1
) limite=
nbre_lignes-
1
;
clear
(
);
copywin
(
my_pad,stdscr,page_en_cours*
LINES,0
,0
,0
,limite-
page_en_cours*
LINES,COLS-
1
,FALSE);
refresh
(
);
move
(
0
,0
);
}
}
…
}
Pour les flèches gauche, droite, haut, bas, je récupère la position actuelle via la fonction Ncurses getyx, incrémente ou décrémente la ligne pour les déplacements haut/bas, incrémente ou décrémente la colonne pour les déplacements droite/gauche. Les limites sont gérées par Ncurses : en cas de positionnement invalide (hors « fenêtre »), rien ne se passe.
Pour empêcher le positionnement au-delà de la fin d'une ligne, j'ai ajouté une fonction contrôle_position() appelée après changement de ligne (lors de l'appui sur les touches flèches haut, bas, droite, et la touche END), laquelle recherche, depuis la position courante, le premier caractère différent d'un espace. Cette position est ensuite comparée à la position courante de façon à se placer en fin de ligne si la position liée au changement de ligne fait que le curseur se retrouve après le texte :
void
controle_position
(
)
{
int
carac=
0
;
int
ligne_orig,colonne_orig,colonne;
getyx
(
stdscr,ligne_orig,colonne_orig);
colonne=
COLS-
1
;
do
{
carac=
mvinch
(
ligne_orig,colonne);
--
colonne;
}
while
(
carac==
'
'
);
if
(
colonne_orig<
colonne)
{
move
(
ligne_orig,colonne_orig);
}
else
{
move
(
ligne_orig,colonne+
2
);
}
}
18-8. Mode insertion▲
Le mode insertion est activé par l'appui sur la touche « i ». La mise à TRUE de la variable mode_insertion stocke cette information.
Contrairement au C++, il n'y a pas de booléen en C. Il faut le gérer soi-même. Dans mon cas TRUE et FALSE fonctionnent, car implémentés dans Ncurses (utilisé avec un int).
Il y a deux fonctions gérant l'ajout de caractère :
- addch : écrase le caractère courant par celui fourni et incrémente la position ;
- insch : insère le caractère à la position courante, mais n'incrémente pas la position.
J'utilise donc insch et incrémente la position avec move.
Ma boucle while va donc avoir la forme suivante :
while
(
1
)
{
int
carac=
getch
(
);
if
(
carac==
KEY_RIGHT)
{
…
}
if
(
carac==
KEY_LEFT)
{
…
}
…
else
{
…
}
}
Dans vi, la touche ESC désactive le mode insertion. Ceci sera géré plus tard, la touche ESC provoquant actuellement la sauvegarde et la sortie du programme.
Les modifications effectuées à l'écran sont enregistrées via la fonction maj_page. Celle-ci se déclenche lors de l'appui sur les touches de changement de page par page up et page down et lors de l'appel de sauvegarde. Cette fonction copie la page en cours à l'écran dans le pad via la fonction copywin si la variable page_modifiee est à TRUE. Cette variable est mise à TRUE par l'appui d'une touche en mode insertion.
18-9. Gestion de la suppression de caractère▲
Pour la suppression d'un caractère, je fais appel à la fonction Ncurses delch().
18-10. Gestion de la touche enter▲
En mode commande, la touche entrée place le curseur en début de ligne suivante. En mode insertion, un retour chariot est ajouté à la position en cours, déclenchant un retour chariot avec une mise à la ligne de la partie droite du texte présent à droite du curseur.
Voici le code traitant le retour chariot en mode insertion :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
char
chaine[80
];
int
ligne,colonne;
getyx
(
stdscr,ligne,colonne);
mvinstr
(
lignes_max-
1
,0
,chaine);
++
nbre_lignes;
wresize
(
my_pad,nbre_lignes,COLS);
wmove
(
my_pad,page_en_cours*
lignes_max+
lignes_max-
1
,0
);
winsertln
(
my_pad);
move
(
ligne,colonne);
instr
(
chaine);
addch
(
'
\n
'
);
insertln
(
);
printw
(
"
%s
"
,chaine);
move
(
LINES-
1
,0
);
deleteln
(
);
move
(
ligne+
1
,0
);
page_modifiee=
TRUE;
Explications :
- Ligne 4 : lecture de la dernière ligne de l'écran et copie dans la variable chaine (le curseur est déplacé en bas) ;
- Ligne 5 : incrément du nombre de lignes du pad, et donc du document ;
- ligne 6 : redimensionnement du pad ;
- ligne 7 : positionnement dans le pad là où il faut ajouter une ligne ;
- ligne 8 : insertion de la ligne ;
- ligne 9 : on se replace là où se situait le curseur ;
- ligne 10 : on lit la chaîne après le curseur ;
- ligne 11 : ajout de '\n' ce qui efface le texte de la ligne après le curseur ;
- ligne 12 : insertion d'une ligne ;
- ligne 13 : écriture du texte copié dans la chaîne ligne 10 ;
- ligne 14 : positionnement sur la dernière ligne de l'écran ;
- ligne 15 : effacement de celle-ci ;
- ligne 16 : positionnement au début de la ligne créée par la coupure.
18-11. Ajout gestion commande▲
Dans vi, l'appui sur la touche ' :' en mode commande déclenche l'affichage du symbole « : » en bas à gauche de l'écran :
Ce symbole sert de prompt à la saisie de commande.
Pour intégrer cette fonctionnalité, j'ai créé une nouvelle fonction traitement_commandes().
Cette fonction sauvegarde la position actuelle, se place sur la dernière ligne (qui ne contient normalement rien), l'efface, affiche le prompt « : », réactive l'écho (avec la fonction echo()), récupère la saisie via la fonction getstr(). La chaîne récupérée est ensuite comparée avec les commandes reconnues (dans un premier temps uniquement « q »).
void
traitement_commandes
(
)
{
char
chaine[80
];
int
ligne,colonne;
getyx
(
stdscr,ligne,colonne);
move
(
LINES-
1
,0
);
deleteln
(
);
printw
(
"
:
"
);
echo
(
);
getnstr
(
chaine,79
);
noecho
(
);
if
(
strcmp
(
chaine,"
q
"
)==
0
)
{
sauvegarde
(
);
endwin
(
);
exit
(
0
);
}
else
{
mvprintw
(
LINES-
1
,0
,"
Commande inconnue : %s
"
,chaine);
}
move
(
ligne,colonne);
}
18-11-1. Contrôle de modification avant sortie▲
Pour contrôler la sauvegarde avant sortie, j'ai ajouté les commandes « w », « wq », « q! » et modifié la commande « q », commandes faisant partie de celles gérées par vi.
- w : enregistre le fichier - appel de la fonction sauvegarde() ;
- wq : enregistre le fichier et quitte - appel de la fonction sauvegarde, puis quitte après l'appel de endwin() ;
- q! : quitte sans sauvegarder les éventuelles modifications ;
- q : quitte si aucune modification n'a été effectuée depuis la dernière sauvegarde, sinon affiche une erreur.
18-12. Gestion de chargement par les arguments de la ligne de commande▲
La fonction chargement charge le fichier passé en argument, et sauvegarde celui-ci en lieu et place du nom de fichier codé en dur pour les essais des versions précédentes. Si aucun fichier n'est passé en paramètre, il y a création d'un tampon correspondant à une page d'affichage (création d'un pad correspondant à une page). Les fonctions chargement et sauvegarde gèrent les erreurs. Les commandes « q », « q! », « wq », et « w » sont gérées. w suivi d'un nom de fichier écrit un nouveau fichier. Pour cela, en début de programme, argv[1] est copié dans une variable qui servira de nom de fichier. Les fonctions chargement et sauvegarde attendent maintenant le nom du fichier en argument.
18-13. Écrasement du fichier en cours▲
Les commandes « e » et « e! » ont été ajoutées. Celles-ci créent un nouveau document ayant pour nom le fichier passé en paramètre. L'option « ! » permet d'écraser les modifications en cours si celles-ci n'ont pas été enregistrées depuis la dernière modification.
18-14. Ajout d'une fonction de recherche▲
Dans vi, la recherche est déclenchée en mode commande via « / ». Il suffit de rentrer la chaîne à rechercher depuis le prompt « / » pour déclencher celle-ci. La recherche sera effectuée depuis la position courante.
La chaîne à rechercher est récupérée via la fonction getnstr() après activation de la fonction echo() pour que la saisie s'affiche à l'écran (noecho() est ensuite appelé pour supprimer l'affichage automatique).
Une boucle est ensuite appelée pour parcourir toutes les lignes du pad à partir de la position actuelle jusqu'à la fin de celui-ci. Cette boucle effectue les opérations suivantes :
- lecture de la ligne correspondant à la position de la boucle dans le pad (via mvinstr(), décalé pour la première ligne du nombre de caractères correspondant à la colonne du curseur avant appel de la recherche ) ;
- recherche du texte dans cette chaîne via strstr, variable booléenne test mise à TRUE en cas de texte trouvé, et interruption de la boucle.
À la fin de la boucle, le curseur est placé au niveau du texte. Si la recherche n'a pas abouti, le curseur garde sa position d'avant recherche. Pour cela, la valeur de la variable boucle est divisée par le nombre de lignes affichées par page, donnant donc la nouvelle page en cours, affichée comme pour les touches page up et page down via copywin. Le reste de la division représente la ligne de la page où se trouve le texte. Pour se positionner au niveau colonne, il suffit de soustraire la taille de la chaîne retournée par mvinstr par la taille du pointeur de retour de strstr, ce qui donne la position du texte trouvé dans cette chaîne.
void
recherche_texte
(
)
{
int
ligne,colonne,temp,boucle,test;
char
chaine[81
];
char
texte_a_chercher[81
];
char
*
chaine_trouvee;
getyx
(
stdscr,ligne,colonne);
move
(
LINES-
1
,0
);
deleteln
(
);
printw
(
"
/
"
);
echo
(
);
getnstr
(
texte_a_chercher,79
);
noecho
(
);
temp=
colonne;
test=
FALSE;
for
(
boucle=
page_en_cours*
lignes_max+
ligne;boucle<
nbre_lignes;++
boucle)
{
mvwinstr
(
my_pad,boucle,temp,chaine);
chaine_trouvee=
strstr
(
chaine,texte_a_chercher);
if
(
chaine_trouvee!=
NULL
)
{
test=
TRUE;
break
;
}
temp=
0
;
}
if
(
test==
TRUE)
{
page_en_cours=
boucle/
lignes_max;
copywin
(
my_pad,stdscr,page_en_cours*
lignes_max,0
,0
,0
,LINES-
1
,COLS-
1
,FALSE);
ligne=
boucle%
lignes_max;
colonne=
strlen
(
chaine)-
strlen
(
chaine_trouvee);
}
move
(
lignes_max,0
);
deleteln
(
);
if
(
test==
FALSE)
{
printw
(
"
\"
%s
\"
non trouvé
"
,texte_a_chercher);
}
move
(
ligne,colonne);
}
18-15. Rechercher/remplacer▲
Cette recherche est appelée par la commande s/[chaine a rechercher]/[chaine de remplacement]. Ma fonction prend également en paramètre la limite de recherche. Pour remplacer le texte, j'utilise la fonction recherche prenant en paramètre la position de départ de la recherche et retournant la position ou le texte a été trouvé ou -1 si rien n'a été trouvé. En cas de chaîne trouvée, je me positionne à l'endroit et supprime celle-ci via une boucle appelant la fonction NCurses wdelch() autant de fois qu'il y a de caractères dans la chaîne. Le remplacement se fait via la fonction Ncurses winsch() avec une boucle inversée, winsch() plaçant les caractères avant le texte de la position courante.
/* prototype :
pos=position ou débuter la recherche,
chaine a rechercher,
chaine de remplacement,
limite de position de recherche
*/
int
rechercher_remplacer
(
int
pos,char
*
chaine_a_remplacer,char
*
chaine_remplacement,int
pos_limite_recherche)
{
int
new_pos=
0
;
int
trouve=
FALSE;
int
boucle,limite,ligne,colonne;
while
(
new_pos!=-
1
)
{
new_pos=
recherche
(
chaine_a_remplacer,pos);
if
(
new_pos>
pos_limite_recherche) new_pos=-
1
;
if
(
new_pos!=-
1
)
{
for
(
boucle=
0
;boucle<
strlen
(
chaine_a_remplacer);++
boucle)
{
mvwdelch
(
my_pad,new_pos/
COLS,new_pos%
COLS);
}
if
(
strcmp
(
chaine_remplacement,""
)!=
0
)
{
for
(
boucle=
strlen
(
chaine_remplacement);boucle>
0
;--
boucle)
{
winsch
(
my_pad,chaine_remplacement[boucle-
1
]);
}
}
pos=
new_pos+
strlen
(
chaine_a_remplacer);
trouve=
TRUE;
doc_modifie=
TRUE;
}
}
if
(
trouve==
FALSE)
{
move
(
LINES-
1
,0
);
deleteln
(
);
printw
(
"
\"
%s
\"
non trouvé
"
,chaine_a_remplacer);
move
(
pos/
COLS,pos%
COLS);
}
else
{
move
(
LINES-
1
,0
);
deleteln
(
);
pos=
pos-
strlen
(
chaine_a_remplacer);
page_en_cours=
pos/
COLS/
lignes_max;
limite=
page_en_cours*
lignes_max+
lignes_max-
1
;
if
(
limite>
nbre_lignes-
1
) limite=
nbre_lignes-
1
;
copywin
(
my_pad,stdscr,page_en_cours*
lignes_max,0
,0
,0
,limite-
page_en_cours*
lignes_max,COLS-
1
,FALSE);
colonne=
pos%
COLS;
ligne=
pos/
COLS;
ligne=
ligne%
lignes_max;
move
(
ligne,colonne);
}
return
0
;
}
18-16. Rechercher/remplacer : seconde partie▲
La méthode de recherche/remplacement a été modifiée de façon à prendre en compte les recherches possibles avec vi. Voici les solutions de recherche possibles en mode commande :
- s/[chaîne à chercher]/[chaîne de remplacement] ;
- s/[chaîne à chercher]/[chaîne de remplacement]/g ;
- %s/[chaîne à chercher]/[chaîne de remplacement].
La première option va effectuer une recherche et un remplacement unique.
La seconde option va effectuer une recherche et un remplacement depuis la position actuelle et jusqu'Ã la fin de ligne.
La troisième option va effectuer une recherche et un remplacement depuis la position actuelle jusqu'à la fin du document.
Extrait de la fonction traitement_commande :
…
else
if
(
strncmp
(
chaine,"
%s/
"
,3
)==
0
)
{
if
(
strlen
(
chaine)>=
3
)
{
recherche_remplacement
(
ligne,colonne,chaine+
3
,TRUE);
}
else
{
mvprintw
(
LINES-
1
,0
,"
Aucun élément à rechercher indiqué
"
);
move
(
ligne,colonne);
}
}
else
if
(
strncmp
(
chaine,"
s/
"
,2
)==
0
)
{
if
(
strlen
(
chaine)>=
2
)
{
recherche_remplacement
(
ligne,colonne,chaine+
2
,FALSE);
}
else
{
mvprintw
(
LINES-
1
,0
,"
Aucun élément à rechercher indiqué
"
);
move
(
ligne,colonne);
}
}
L'appel aux commandes « %s/ » ou « s/ » va utiliser ma fonction recherche_remplacement dont voici le prototype :
void
recherche_remplacement
(
int
ligne, int
colonne,char
*
chaine,int
recherche_en_boucle)
Les variables ligne et colonne permettent de récupérer la position actuelle, ces variables n'étant pas globales, la variable chaîne contenant la commande (« s/ » ou « %s/ ») ayant été retirée en ajoutant le nombre de caractères (2 ou 3 selon le cas), La variable recherche_en_boucle contenant TRUE ou FALSE selon le cas.
Rappel : Le type booléen n'existe pas en C. Dans notre cas TRUE/FALSE sont utilisables, car présents dans ncurses.h.
Ma fonction recherche_remplacement va servir à créer les paramètres corrects pour la fonction rechercher_remplacer, attendant en paramètres la chaîne à rechercher, la valeur de remplacement (pouvant être « » dans le cas où l'on souhaite effacer la chaîne à chercher), la position de départ, et la limite de recherche. Si la limite est égale à -1, cela signifie qu'un seul remplacement doit être effectué (choix de fonctionnement de ma part). Cette fonction retourne la dernière position où le texte a été remplacé, servant à recherche_remplacement pour se positionner sur le dernier élément remplacé.
Source fonction recherche_remplacement :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
void
recherche_remplacement
(
int
ligne, int
colonne,char
*
chaine,int
recherche_en_boucle) //ligne=ligne début, colonne=colonne début,chaine=chaine recherche/remplacement/TRUE recherches multiples/False recherche unique ou sur la ligne
{
char
*
chaine_a_remplacer,*
chaine_remplacement;
int
pos,retour;
pos=
page_en_cours*
lignes_max;
pos=
pos+
ligne;
pos=
pos*
COLS;
pos=
pos+
colonne;
chaine_remplacement=
strstr
(
chaine,"
/
"
);
if
(
chaine_remplacement!=
NULL
)
{
chaine_remplacement=
strdup
(
chaine_remplacement+
1
);
}
else
{
chaine_remplacement=
strdup
(
""
);
}
if
(
strcmp
(
chaine_remplacement,""
)==
0
)
{
chaine_a_remplacer=
strdup
(
chaine);
}
else
{
chaine_a_remplacer=
strndup
(
chaine,strlen
(
chaine)-
strlen
(
chaine_remplacement)-
1
);
}
if
(
recherche_en_boucle==
TRUE)
{
retour=
rechercher_remplacer
(
chaine_a_remplacer,chaine_remplacement,pos,nbre_lignes*
COLS);
}
else
{
if
(
strcmp
(
chaine_remplacement+
strlen
(
chaine_remplacement)-
2
,"
/g
"
)==
0
)
{
chaine_remplacement[strlen
(
chaine_remplacement)-
2
]=
'
\0
'
;
retour=
rechercher_remplacer
(
chaine_a_remplacer,chaine_remplacement,pos,pos/
80
*
80
+
80
);
}
else
retour=
rechercher_remplacer
(
chaine_a_remplacer,chaine_remplacement,pos,-
1
);
}
if
(
retour!=-
1
)
{
page_en_cours=
retour/
COLS/
lignes_max;
int
limite=
page_en_cours*
lignes_max+
lignes_max-
1
;
if
(
limite>
nbre_lignes-
1
) limite=
nbre_lignes-
1
;
clear
(
);
copywin
(
my_pad,stdscr,page_en_cours*
lignes_max,0
,0
,0
,limite-
page_en_cours*
lignes_max,COLS-
1
,FALSE);
colonne=
retour/
COLS/
lignes_max*
lignes_max;
ligne=
retour/
COLS-
colonne;
colonne=
retour%
COLS;
colonne=
colonne-
strlen
(
chaine_a_remplacer);
move
(
ligne,colonne);
}
else
{
mvprintw
(
LINES-
1
,0
,"
\"
%s
\"
non trouvé !
"
,chaine_a_remplacer);
move
(
ligne,colonne);
}
free
(
chaine_a_remplacer);
free
(
chaine_remplacement);
}
Lignes 6 à 9 : calcul de la position courante, égale à page_en_cours * nbre_lignes auquel est ajouté la ligne en cours, le tout multiplié par le nombre de caractères par colonne (COLS) et auquel est ajouté la colonne actuelle.
Lignes 10 à 19 : création de chaine_remplacement. Si « / » est trouvé dans chaine, le contenu suivant est dupliqué dans chaine_remplacement, sinon chaine_remplacement est positionné à "".
Lignes 19 à 27 : si chaine_remplacement est vide (égal à ""), le contenu de chaine est copié dans chaine_a_remplacer. Sinon, chaine est copié via la fonction strndup, le nombre de caractères à copier correspondant à la longueur de chaine - la longueur de chaine_remplacement -1 (on ne copie pas le caractère « / »).
Lignes 27 à 40 : la fonction rechercher_remplacer est appelée selon le cas. S'il s'agit d'une recherche globale, rechercher_remplacer est appelé avec la variable limite fixée à nbre_lignes*COLS (ligne 29) ; sinon une recherche de « /g » est effectuée à la fin de chaine_remplacement. Si la valeur est trouvée, elle est retirée de chaine_remplacement en la remplaçant par « \0 », ce qui correspond au caractère de fin de chaîne, et la limite est calculée par rapport à la position actuelle (lignes 35 et 36), sinon variable limite est mise à -1 (ligne 38).
Lignes 41 à 59 :le curseur est placé sur la dernière position de recherche remplacement retournée par la fonction éponyme. Sinon un message d'erreur est affiché sur la ligne du bas et le curseur est positionné sur la position avant recherche.
Voici le code de la fonction rechercher_remplacer :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
int
rechercher_remplacer
(
char
*
chaine_a_remplacer,char
*
chaine_remplacement,int
pos,int
limite) //chaine a remplacer,chaine remplacement, pos debut recherche, limite max de recherche
{
int
recherche_unique=
FALSE;
int
trouve=
FALSE;
int
new_pos=
0
;
int
boucle;
maj_page
(
);
if
(
limite==-
1
)
{
recherche_unique=
TRUE;
limite=
pos/
COLS*
COLS+
COLS;
}
while
(
new_pos!=-
1
)
{
new_pos=
recherche
(
chaine_a_remplacer,pos);
if
(
new_pos>
limite) new_pos=-
1
;
if
(
recherche_unique==
TRUE &&
trouve==
TRUE) new_pos=-
1
;
if
(
new_pos!=-
1
)
{
pos=
new_pos+
strlen
(
chaine_a_remplacer);
trouve=
TRUE;
for
(
boucle=
0
;boucle<
strlen
(
chaine_a_remplacer);++
boucle)
{
mvwdelch
(
my_pad,new_pos/
COLS,new_pos%
COLS);
}
if
(
strcmp
(
chaine_remplacement,""
)!=
0
)
{
for
(
boucle=
strlen
(
chaine_remplacement);boucle>
0
;--
boucle)
{
winsch
(
my_pad,chaine_remplacement[boucle-
1
]);
}
}
doc_modifie=
TRUE;
}
}
if
(
trouve==
TRUE)
{
return
pos;
}
else
return
-
1
;
}
18-17. Effacement de ligne avec dd▲
La commande dd supprime la ligne sur laquelle se trouve le curseur et remonte les lignes en dessous de celle-ci. Il nous faut dans un premier temps intercepter la saisie de dd depuis le mode commande. Pour cela, dans la boucle main, si la touche saisie ne correspond pas aux touches déjà gérées, j'insère celle-ci dans une chaîne de caractères nommée chaine_commande :
int
ligne=
strlen
(
chaine_commande);
chaine_commande[ligne]=
carac;
chaine_commande[ligne+
1
]=
'
\0
'
;
Cette chaîne sera ensuite analysée dans une fonction nommée traitement_ligne_commande() et appelée à la suite de ce code.
Cette fonction va donc stocker tous les caractères saisis n'étant pas gérés les uns à la suite des autres. Elle ne sert actuellement qu'à gérer « dd », mais gérera d'éventuelles autres commandes plus tard.
La fonction vérifie si les deux derniers caractères correspondent à dd.
-
Les caractères correspondent :
- mise à 0 de chaine_commande en mettant le caractère '\0' en début de chaîne ;
- positionnement par wmove en début de ligne dans le pad (ajout du nombre page_en_cours multiplié par lignes_max à ligne) ;
- effacement de la ligne via wdeleteln ;
- réaffichage de la page en cours ;
- contrôle de la position par appel à controle_position, les lignes du dessous étant remontées, le curseur peut se trouver dans une zone au-delà du texte affiché par la ligne en dessous de celle supprimée ;
- soustraction de 1 à la variable nbre_lignes ;
- mise à TRUE de la variable doc_modifie.
-
Les caractères ne correspondent pas :
- mise à zéro de chaine_commande si celle-ci atteint 78 caractères, ceux-ci ne correspondant à aucune commande.
longueur_ligne_commande=
strlen
(
chaine_commande);
if
(
longueur_ligne_commande>=
2
&&
strcmp
(
chaine_commande+
longueur_ligne_commande-
2
,"
dd
"
)==
0
)
{
maj_page
(
);
chaine_commande[0
]=
'
\0
'
;
wmove
(
my_pad,page_en_cours*
lignes_max+
ligne,0
);
wdeleteln
(
my_pad);
int
limite=
page_en_cours*
lignes_max+
lignes_max-
1
;
if
(
limite>
nbre_lignes-
1
) limite=
nbre_lignes-
1
;
copywin
(
my_pad,stdscr,page_en_cours*
lignes_max,0
,0
,0
,limite-
page_en_cours*
lignes_max,COLS-
1
,FALSE);
controle_position
(
);
--
nbre_lignes;
doc_modifie=
TRUE;
}
else
{
if
(
longueur_ligne_commande>=
78
) chaine_commande[0
]=
'
\0
'
;
}
18-18. Copier/couper/coller▲
Avant de commencer, j'ai créé une nouvelle version de la fonction traitement_ligne_commande() qui aura la charge de gérer ce qui est tapé en mode commande, et qui n'est pas encore traité dans ma boucle while (touches flèches, PageUP/PageDown, retour chariot, « i », « / »). Cette fonction aura la charge d'analyser les commandes de plusieurs caractères saisis. Cette fonction servira plus tard, après modification, à traiter les commandes de type « 2dd » intégrant en préfixe le nombre de fois que la commande doit être exécutée.
Si aucune commande n'est reconnue, je mets la chaîne à 0.
La fonction traitement_ligne_commande() va donc traiter les commandes dd, yy, et p.
dd va se placer à la position actuelle dans le pad via wmove (position actuelle de la ligne et de la colonne intégrant la page où l'on se trouve), et copier la chaîne sur cette ligne dans la variable presse_papier avec winstr, puis supprimer la ligne avec wdelete. La variable nbre_ligne est décrémentée, doc_modifie mise à TRUE, et l'écran est rafraîchi.
yy utilise le même principe pour copier la ligne en cours dans le presse-papier.
p va copier le contenu du presse-papier sur la ligne en dessous de la position du curseur. Pour cela, j'incrémente la variable nbre_ligne, utilise wresize pour mettre à jour la taille du pad en conséquence, insère une ligne à la bonne position (wmove puis winsert), puis copie le contenu du presse-papier avec printw. Je rafraîchis enfin l'écran et fixe doc_modifie à TRUE.
void
traitement_ligne_commande
(
)
{
int
longueur_ligne_commande;
longueur_ligne_commande=
strlen
(
chaine_commande);
if
(
longueur_ligne_commande>=
2
&&
strcmp
(
chaine_commande+
longueur_ligne_commande-
2
,"
dd
"
)==
0
)
{
maj_page
(
);
chaine_commande[0
]=
'
\0
'
;
wmove
(
my_pad,page_en_cours*
lignes_max+
ligne,0
);
presse_papier[0
]=
'
\0
'
;
winstr
(
my_pad,presse_papier);
wdeleteln
(
my_pad);
int
limite=
page_en_cours*
lignes_max+
lignes_max-
1
;
if
(
limite>
nbre_lignes-
1
) limite=
nbre_lignes-
1
;
copywin
(
my_pad,stdscr,page_en_cours*
lignes_max,0
,0
,0
,limite-
page_en_cours*
lignes_max,COLS-
1
,FALSE);
controle_position
(
);
--
nbre_lignes;
doc_modifie=
TRUE;
}
else
if
(
longueur_ligne_commande>=
2
&&
strcmp
(
chaine_commande+
longueur_ligne_commande-
2
,"
yy
"
)==
0
)
{
chaine_commande[0
]=
'
\0
'
;
presse_papier[0
]=
'
\0
'
;
wmove
(
my_pad,page_en_cours*
lignes_max+
ligne,0
);
winstr
(
my_pad,presse_papier);
}
else
if
(
longueur_ligne_commande>=
1
&&
strcmp
(
chaine_commande+
longueur_ligne_commande-
1
,"
p
"
)==
0
)
{
if
(
presse_papier[0
]!=
'
\0
'
)
{
maj_page
(
);
++
nbre_lignes;
wresize
(
my_pad,nbre_lignes,COLS);
wmove
(
my_pad,page_en_cours*
lignes_max+
ligne+
1
,0
);
winsertln
(
my_pad);
wprintw
(
my_pad,presse_papier);
int
limite=
page_en_cours*
lignes_max+
lignes_max-
1
;
if
(
limite>
nbre_lignes-
1
) limite=
nbre_lignes-
1
;
copywin
(
my_pad,stdscr,page_en_cours*
lignes_max,0
,0
,0
,limite-
page_en_cours*
lignes_max,COLS-
1
,FALSE);
move
(++
ligne,0
);
doc_modifie=
TRUE;
}
}
else
chaine_commande[0
]=
'
\0
'
;
}
18-18-1. Copier/couper/coller sur plusieurs lignes▲
vi intègre pour la plupart de ses commandes la possibilité d'utiliser un nombre en préfixe, ce préfixe permettant d'exécuter la commande le suivant du nombre de fois indiqué dans celui-ci.
Exemple :
2dd
va exécuter deux fois la commande dd, c'est-à -dire supprimer deux lignes en les mettant dans le presse-papier.
Pour séparer cet éventuel préfixe de la commande, je parcours la chaîne saisie via une boucle appelant isdigit. Si un chiffre est détecté, la boucle est interrompue.
J'utilise ensuite la fonction strtol depuis le début de la chaîne à laquelle j'ai additionné le premier chiffre détecté. strtol retourne la valeur de la chaîne convertie et retourne également dans le pointeur fourni (endptr) l'adresse du premier caractère de la chaîne n'étant pas un chiffre, ou la chaîne si aucun caractère n'est un chiffre. C'est ce retour de chaîne qui sera utilisé pour rechercher les commandes à traiter. Si le retour de strtol est 0, je le force à 1 en modifiant la variable de retour de strtol de façon à ce que les boucles de copie de lignes pour dd et yy soient exécutées au moins une fois.
Pour les commandes dd et yy, je commence par vider le presse-papier s'il contient quelque chose (free sur celui-ci et mise à 0 de la variable lignes_presse_papier). Je mets ensuite lignes_presse_papier à la valeur de retour de strtol. Après malloc pour le presse-papier et positionnement dans le pad, une boucle va copier le nombre de lignes dans le presse-papier via winstr. Dans le cas de dd, la ligne est effacée via wdelete après copie dans le presse-papier.
Pour la commande coller, il suffit de copier le presse-papier via printw après mise à jour de la taille du pad (nbre_lignes=nbre_lignes+lignes_presse_papier; suivi d'un wresize).
18-19. Gestion de version précédente▲
J'ai modifié le code de sauvegarde. Lors de la sauvegarde d'un fichier, la version en cours est conservée. Pour cela, le nom du fichier est copié dans un tampon, puis le symbole tilde est ajouté à la fin. J'utilise ensuite la fonction rename pour renommer le fichier. Le fopen suivant créera donc un nouveau fichier avec le nom d'origine. Si le fichier n'existe pas encore (création d'un nouveau fichier), la fonction rename échouera.
Pour créer le nom du fichier de copie, j'utilise les fonctions basename et dirname sur une copie du nom d'origine, ces fonctions pouvant modifier cette chaîne. Le nom du fichier de copie sera donc le nom d'origine avec un tilde (~) devant.
18-20. Affichage nom du fichier à l'ouverture et à la sauvegarde▲
Lors du chargement et de la sauvegarde du fichier, son nom, son nombre de lignes et sa taille sont affichés.
La taille du fichier est obtenue par la fonction fseek avec SEEK_END en paramètre, celle-ci plaçant le handle de fichier à la fin de celui-ci. L'appel à ftell retournera ensuite la position, et donc comme nous sommes à la fin du fichier, sa taille.
Nous aurions également pu utiliser les fonctions de la famille stat, retournant des informations plus complètes sur un fichier.
19. Conclusion▲
Nous arrivons au terme de cette seconde partie de la création d'un minisystème. À ce stade nous avons un shell opérationnel (vu dans le tutoriel précédent), bien qu'incomplet par rapport à bash, ainsi que les commandes de base utilisées sous Linux.
19-1. Remerciements▲
Je remercie philippe-dpt35 pour sa relecture technique et orthographique.
Je remercie LittleWhite pour sa relecture technique.
Remerciements supplémentaires :
Senaku-seishin et fœtus pour leur explication sur les liens symboliques
Matt_houston, BufferBob pour leurs informations sur le control passthrough.