1. Introduction

Cet article va montrer comment créer un minisystème d'exploitation étape par étape. Celui-ci fonctionnera comme un unix-like.

Pour cet article, je considérerai qu'un niveau minimum de compréhension de la programmation assembleur x86, du langage C et du système Linux est acquis. Si vous ne vous sentez pas à l'aise avec ces technologies, n'hésitez pas à vous référer aux tutoriels suivants :

le tutoriel d'Étienne Sauvage sur l'assembleur x86 ;

Je vous invite aussi à consulter les différentes rubriques dédiées du forum qui sont très bien fournies.

1-1. Convention typographique

Les mots clés de langage de programmation ainsi que les fonctions de la bibliothèque standard du C sont affichés en gras (monospace).

1-2. Les abréviations utilisées dans ce tutoriel

  • OS ou SE : pour système d'exploitation ;
  • x86 : ensemble des processeurs utilisant le jeu d'instructions Intel ;
  • asm : langage assembleur ;
  • gcc : GNU Compiler Collection : le compilateur par défaut de Linux ;
  • Win32 : API 32 bits utilisée par Windows ;
  • libc : bibliothèque standard C ;
  • API : Application Programming Interface (ou interface de programmation) ;
  • ABI : Application Binary Interface (Interface binaire-programme) ;
  • IDE : Integrated Development Environment : (EDI : Environnement de développement intégré).

2. Le fonctionnement d'un système d'exploitation

Un système d'exploitation est un programme ou plutôt un ensemble de programmes qui du point de vue utilisateur gère dans son ensemble l'ordinateur. Du point de vue programmeur, le système permet d'avoir des fonctions universelles pour toute application telle que fopen pour l'ouverture d'un fichier, et fclose pour la fermeture de celui-ci (fonctions utilisées en C). Par exemple, il délègue donc l'ouverture et la création d'un fichier pour la fonction fopen à l'OS. Il fait également l'interface entre les applications et les périphériques par le biais de drivers (aussi appelés pilotes de périphérique) et contrôle les accès aux ressources (gestion de droits, ordonnancements, réservations de ressources). Le cœur du système d'exploitation proprement dit est appelé noyau.

Seuls les pilotes de haut niveau font partie de l'OS (tels que les pilotes ext2/3/4 pour Linux, NTFS pour Windows). Les pilotes de cartes graphiques, de cartes réseau, d'imprimantes peuvent être intégrés au noyau, ou se présenter sous forme de modules.

Il existe aussi des pilotes génériques tels que :

  • USB Mass Storage sous Windows, permettant la gestion de toute clé USB ou disque dur externe USB ;
  • le pilote NE2000 : ancienne norme de carte réseau étant considéré comme un pilote générique ;
  • les pilotes VESA, pilotes génériques pour les cartes vidéo.

Les pilotes génériques permettent d'utiliser les fonctions de base du périphérique qu'ils doivent piloter, mais les périphériques ne pourront être exploités de façon optimale qu'avec leur pilote constructeur. (Exemple : pilote VESA : pas de support des fonctions 3D et résolution limitée, pilote générique d'imprimante : pas de couleur, de format A3, ni d'agrafage.)

Les pilotes de périphérique peuvent être fournis avec l'OS, mais n'en font pas partie, ils sont fournis par les fabricants de ceux-ci.

Un OS est fourni avec des applications, telles que « Bloc-notes »(notepad.exe) ou « l'explorateur de fichiers » (explorer.exe) sous Windows ; les environnements KDE ou GNOME sous Linux (liste non exhaustive). Ces logiciels fournis de base avec leur OS respectif sont considérés par analogie comme faisant partie du système d'exploitation, mais sont réellement des applications proprement dites (même si elles sont développées par l'éditeur de l'OS notamment pour Windows), utilisant celui-ci et appelant eux-mêmes bien entendu les fonctions de l'OS.

Quand on ouvre un fichier, l'OS va appeler le sous-système de gestion de fichiers, qui va lui-même appeler le sous-système de gestion d'un système de fichiers (FAT32, NTFS, Ext2/3/4 selon le cas), qui, à son tour, va appeler le pilote IDE ou SATA (ou USB pour un disque externe ou clé USB), etc. L'application tournant sous l'OS et ayant demandé l'ouverture d'un fichier n'a pas notion de tout cela et n'en a pas besoin : il reçoit juste un pointeur appelé handle lui permettant de gérer ses opérations sur le fichier.

Le point principal de dialogue entre l'utilisateur et l'OS est le gestionnaire de fenêtres pour le mode graphique (exemple : KDE ou Gnome pour Linux, bureau/explorateur de fichiers pour Windows, Finder pour mac OS X) ou l'interpréteur de commande ou shell en mode texte (sh, ksh, bash pour Linux/Mac OS X, cmd.exe ou PowerShell pour Windows).

Les fonctions de l'OS sont accessibles via ce que l'on nomme une API (Application Programming Interface).

Ces fonctions comme en C attendent des paramètres et retournent (ou non) un résultat. Dans l'exemple ci-dessous, la variable texte est passée en paramètre dans la fonction affichage qui appelle elle-même la fonction printf. Cette fonction ne retourne rien (void = vide en français).

 
Sélectionnez
void affichage(char *texte)
{
  printf("%s",texte);
}

En C++, une fonction a obligatoirement un type, contrairement au C. Si une fonction ne retourne rien, on la déclare avec le type void comme pour la fonction affichage ci-dessus. Void est obligatoire en C++, optionnel sur les anciennes normes C. Les normes actuelles du C génèrent un avertissement si aucun type n'est spécifié.

En C, l'API fournie se nomme la libc Vous la trouverez sous Linux sous le nom de libc6 ou glibc2. Il en existe des substituts tels que uClibc, utilisés pour l'informatique embarquée. Celle-ci est limitée aux fonctions de base de la norme C, il vous faudra donc probablement ajouter d'autres bibliothèques (bibliothèque graphique, support SSL, etc. Au niveau OS sous Windows, on trouvera l'API système Win32., sous Linux, il y aura la glibc à laquelle il faut ajouter les bibliothèques graphiques pour la gestion des fenêtres qui, contrairement à Windows, est séparée. Exemple : Xlib ; ou des bibliothèques de plus haut niveau telles que Qt ou GTK, utilisées respectivement par les environnements KDE et GNOME.

3. Les différentes approches

Je vois deux approches possibles pour ce projet :

  • partir d'un système existant et s'en détacher au fur et à mesure ;
  • partir « from scratch », et tout coder.

En partant d'un système existant, on peut bénéficier des outils et API du système en cours d'exécution et on peut s'en abstraire au fur et à mesure de l'avancée du projet. En partant « from scratch », on n'a rien, même pas d'accès disque, pas de compilateur. Il faut tout faire soi-même.

3-1. Langage C vs Assembleur

Le langage C permet de facilement modifier le code créé et d'être compatible avec quasiment tout type de plateforme. Il nécessite un compilateur afin de créer le fichier exécutable sur la plateforme de destination, ce compilateur effectuant la traduction vers le langage machine de la machine de destination. La quasi-totalité des systèmes ont un compilateur C. Un programme C écrit dans les normes sera compilable sur toutes les machines fournissant un compilateur C.

L'assembleur étant très proche de la machine permet des choses que ne peut faire le langage C (exemple : accès direct à l'électronique tel que l'activation du mode protégé). Le source n'est pas portable vers un autre type de CPU. Il faut très bien connaître la machine et ses composants pour faire quoi que ce soit.

Dans la pratique, nous utiliserons le C et y incorporerons du code assembleur quand cela sera nécessaire. Il est à noter que le langage C a été créé pour Unix, et qu'Unix a été réécrit en C.

Il existe d'autres langages tels que le Basic, le Pascal, l'ADA, etc., mais ceux-ci sont inadaptés à la situation.

4. Exemples de Hello World !

4-1. Exemple en C :

 
Sélectionnez
#include <stdio.h>
int main()
{
  printf("Hello World");
}

Ce code utilise simplement la fonction printf (nous verrons cette fonction plus en détail ultérieurement).

La ligne :

 
Sélectionnez
#include <stdio.h>

est nécessaire, car le prototype de printf se situe dans le fichier d'en-têtes stdio.h.

La description des en-têtes de la bibliothèque standard C est disponible ici.

4-2. Exemples en assembleur

4-2-1. Hello World sous Windows

Exemple « Hello World » sous Windows en assembleur :

 
Sélectionnez
.data
        HelloWorld db "Hello World!", 0

.code
start:

        lea eax, HelloWorld
        xor  ebx,ebx
        push ebx
        push eax
        push eax
        push ebx
        call MessageBox
        push ebx
        call ExitProcess

end start

Dans ce code, on peut voir que les paramètres sont poussés sur la pile avant appel de la fonction, c'est la fonction qui nettoiera la pile. On ne voit pas de dépilement.

Il existe sous Windows des applications en mode console ou en mode fenêtre. L'exemple ci-dessus montre un exemple en mode fenêtre (il n'y a pas de fenêtre ouverte, juste une boite de dialogue). Un printf serait plutôt une application en mode console. (Voir dans ce cas le point : 4.2.4 Hello World MS-DOSHello World sous MS-DOS.)

4-2-2. Hello World sous Linux (avec bibliothèque)

 
Sélectionnez
BITS 32

EXTERN  puts

SECTION .data
        chaine db "Hello World !",0

SECTION .text
        GLOBAL main
        
        main :
        push dword chaine
        call puts
        add esp,4
        ret

Pour la compilation/assemblage :

 
Sélectionnez
nasm -f elf hello.asm
gcc hello.o -o hello

Dans cet exemple, on utilise la fonction de la glibc puts. La glibc est linkée à notre code par gcc. L'option -f elf permet de générer du code au format ELF (Executable and Linkable Format), le pendant Linux du format exécutable PE (Portable Executable) utilisé sous Windows.

La ligne GLOBAL main est présente, car gcc attend la fonction main en point d'entrée pour l'édition de liens. La ligne extern permet de déclarer puts comme externe au code.

4-2-3. Hello World sous Linux (sans bibliothèque)

Dans cet exemple, on n'utilise plus la glibc, mais on appelle directement l'appel système write (int 0x80 eax=4) de Linux. Sans bibliothèque standard (libc), puts n'est plus disponible. J'utilise ensuite l'appel système exit (int 0x80 eax=1).

 
Sélectionnez
BITS 32
SECTION .data
        chaine db "Hello World !"
fin_chaine :

SECTION .text
        GLOBAL start :
        start:
        mov eax,4
        mov ebx,1
        mov ecx,chaine
        mov edx,fin_chaine - chaine
        int 0x80

        mov eax,1
        xor ebx,ebx
        int 0x80

Compilation/assemblage :

 
Sélectionnez
nasm -f elf hello.asm
gcc -nostdlib hello.o -o hello

Dans cet exemple, j'utilise directement les appels système int 0x80 eax=4 pour write et int 0x80 eax=1 pour exit.

Paramètres attendus par la fonction write (eax=4 int 0x80) :

  • eax : numéro de fonction (4 pour write) ;
  • ebx : descripteur de fichier (1=stdout) ;
  • ecx : adresse chaîne de caractères ;
  • edx : nombre de caractères à écrire (égal dans l'exemple à adresse fin chaîne - adresse chaîne).

La compilation se fait avec l'option -nostdlib (qui ne fera pas de linkage avec la bibliothèque standard). Le point d'entrée main est remplacé par _start.

4-2-4. Hello World sous MS-DOS

 
Sélectionnez
org 0x100

mov ah,9
mov dx,msg
int 0x21                                         ; fonction DOS affichage chaîne

mov ax,0x4c00
int 0x21                                         ; fonction DOS sortie

msg                                              db 10,13,"Hello World ! ",10,13,"$"

Les codes ASCII 10 et 13 sont mis pour générer un retour chariot. Je n'ai pas créé de section, ce n'est pas propre, mais, dans le cadre de ce test, cela fonctionnera. (Les sections étant nécessaires pour séparer le code des données dans des segments différents, ce qui n'est pas le cas sous DOS en format .com, tout est inclus dans le même segment.)

Sous MS-DOS, les chaînes de caractères se terminent par un « $ » au lieu du \0 habituel.

Dans cet exemple, j'utilise la fonction int 0x21 ah=9 « Write string to standard output », suivie de ah=4ch : « Terminate with return code » (le code passé étant 00).

Ce code n'est pas opérationnel en ligne de commande sous Windows 64 bits, mais opérationnel en 32 bits (test fonctionnant sous Windows 8 32 bits également). Ceci n'est pas dû à Windows, mais à l'impossibilité des CPU x86 de lancer une application 16 bits en mode 64 bits. Un code MS-DOS au format .com étant du code 16 bits.

La directive [org 0x100] permet de forcer l'adressage commençant à 0x100, l'appel d'une application en .com sous DOS démarre toujours à l'adresse CS :0x100 (le segment CS étant fixé par MS-DOS et les 256 octets (0x100 en hexadécimal) au début du code contenant le PSP Program Segment Prefix).

5. Le processus que nous allons utiliser

Nous partirons d'un développement à partir d'une distribution Linux Debian 7. Une fois l'abstraction suffisante (plus besoin du noyau Linux), nous pourrons utiliser un émulateur tel que Bochs, voire une machine virtuelle, et enfin booter directement sous notre système, mais nous en sommes pour le moment très loin.

Voici la liste des grandes étapes qui devront être effectuées pour avoir un système créé from scratch complètement autonome :

  • création d'un shell ;
  • création des commandes utilisées par le shell pour interagir avec le système (telles que cp, cat, mv, rm, etc.) ;
  • création d'une bibliothèque standard, base de tout programme ;
  • création d'un noyau et des drivers ou possibilité d'utilisation de drivers existants ;
  • création d'un code d'amorce ou possibilité d'utilisation d'un boot-loader tel que GRUB.

ll y a différents interpréteurs shell disponibles (sh, csh, tcsh, ksh, bash, zsh). Mon implémentation se basera sur le comportement de bash.

5-1. Les outils

Voici les outils que je vais utiliser pour mon développement. Ils seront complétés au fur et à mesure du besoin.

  • geany ;
  • gcc : le compilateur C par défaut de Linux ;
  • gdb : le débogueur ;

Il est possible d'utiliser n'importe quel autre IDE.

6. Premier essai

Pour commencer, je vais créer un petit code lisant la console :

 
Sélectionnez
1.
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.
/*****************************
 * shell 0.1
 * © 2015 - Christophe LOUVET
 ****************************/

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
    char    buffer[50];

    printf("\nInterpreteur de commande\nTaper \"exit\" pour quitter\n");
    while(1)
    {
        printf("Prompt : ");
        scanf("%s",buffer);
        if (strcmp("exit",buffer)==0)
        {
            exit(0);
        }
        else
        {
            printf("Commande inconnue ...\n");
        }
    }
}

Ce code utilise la fonction printf pour afficher les messages et scanf pour lire la chaîne au clavier (sur l'entrée standard stdin en fait). Cette fonction imprime une chaîne formatée (printf : print formated) sur la sortie standard. Je compare la chaîne récupérée avec « exit » via strcmp. Si la réponse est exit, j'appelle la fonction exit(0).

Le code étant très simple, il n'y a pas de Makefile.

Bien entendu, la compilation depuis la ligne de commande se fera par :

 
Sélectionnez
gcc code.c -o code

6-1. Les fonctions utilisées

Les fonctions utilisées sont des fonctions standard de la libc.

La libc est la bibliothèque standard du C, c'est-à-dire la bibliothèque regroupant les fonctions C. Voici les fonctions utilisées jusqu'à présent :

7. Début d'un interpréteur

Un interpréteur de commande ou shell lit la commande qui est entrée sur la console et l'exécute, ou il exécute une liste d'instructions stockée dans un fichier texte appelé script.

Pour plus d'information sur le shell : http://frederic-lang.developpez.com/tutoriels/linux/prog-shell/

Avec cette version de code modifiée, ce qui est lu sur la console est envoyé à la fonction system qui va donc transférer la commande à l'interpréteur réel du système, sauf si l'on tape exit, auquel cas l'application s'arrête.

Je remplace donc la ligne 25 du code précédent par :

 
Sélectionnez
system(buffer);

Documentation sur la fonction system ici.

Cette fonction lance /bin/sh -c suivi de la chaîne. Si la chaîne est NULL, le retour est 0 si le shell est accessible, -1 sinon.

Sous Windows /bin/sh -c est remplacé par cmd.exe /c

8. 1er problème

L'appel à :

 
Sélectionnez
ls

Va fonctionner.

Par contre l'appel à :

 
Sélectionnez
ls *.c

Va lancer ls.

Cela est dû au fait que scanf s'arrête au premier retour chariot ou au premier espace saisi.

Je remplace donc l'usage de scanf par fgets. fgets me permet également d'éviter les buffers overflow en limitant le nombre de caractères lus.

 
Sélectionnez
1.
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.
/*****************************
 * shell 0.3
 * © 2015 - Christophe LOUVET
 ****************************/

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#define TAILLE_BUFFER 150

int main()
{
      char    buffer[TAILLE_BUFFFER];

    
    printf("\nInterpreteur de commande\nTaper \"exit\" pour quitter\n");
    while(1)
    {
        printf("Prompt : ");
        fgets(buffer,150,stdin);
        buffer[strlen(buffer)-1]='\0';
        if (strcmp("exit",buffer)==0)
        {
            exit(0);
        }
        printf("%s\n",buffer);
        system(buffer);
    }
}

Comme vous pouvez le voir ligne 20, je mets l'avant-dernier caractère à 0 de façon à supprimer le retour chariot conservé par fgets (contrairement à scanf). Je calcule l'avant-dernier caractère en calculant la longueur de la chaîne avec strlen. Au niveau du if, je n'utilise plus de else, car si la fonction exit dans la condition if est exécutée, l'application se termine.

Les nouvelles fonctions utilisées :

9. Nouvel interpréteur :

L'utilisation de fork/execve est plus adaptée à un shell que la simple fonction system. La fonction system va appeler l'interpréteur de commande par défaut et y passer en paramètre la chaîne lui ayant été fournie et attendre la fin du processus pour retourner, et va donc bloquer le programme appelant. Avec fork/execve, le programme père reste autonome, il peut ou non attendre le comportement de son fils avec la fonction wait.

Dans cette version, je n'utilise donc plus system. J'utilise la fonction fork pour créer un processus fils, puis la fonction execvp qui remplace le contenu du processus par l'application passée en paramètre. La fonction wait attend la fin du processus lui ayant été passé en paramètre (dans notre le cas le processus fils généré par fork).

À partir de maintenant, ce « shell » utilise des fonctions spécifiques à Linux. fork et execvp n'existent pas sous Windows, le fonctionnement diffère (ce sera l'utilisation de CreateProcess sous Windows). Pour créer ma liste d'arguments, je travaille sur une copie et utilise la fonction strtok (voir infos sur strtok plus bas).

Cet interpréteur reste très minimal. Il ne gère pour l'instant ni les redirections, ni les pipes, ni l'historique, ni la gestion des variables, boucles for, etc., il n'est bien sûr pas exploitable.

Un appel à « ls *.c » va retourner une erreur  : impossible d'accéder à *.c : aucun fichier ou dossier de ce type. Cette erreur vient d'une mauvaise interprétation de « * », qui sera expliquée et gérée ultérieurement.

Algorithme 

boucle sans fin effectuant les opérations suivantes :

  • affichage  :« Prompt : » (printf) ;
  • début boucle ;
  • lecture sur entrée standard (fgets) ;
  • suppression du retour chariot de fin (ligne_cmd[strlen(ligne_cmd)-1]='\0';) ;
  • si ligne_cmd est égale à exit, je sors via la fonction exit(0) ;
  • création d'un tableau list_args, contenant les bouts de ligne entre caractères espace ;
  • création d'un nouveau processus (fork) ;
  • si le retour de fork est différent de 0, il s'agit du numéro de processus père, et je suis dans l'exécution de celui-ci, et me mets en attente de la fin du processus fils (wait) ;
  • si le retour de fork est égal à 0, je suis dans le processus fils, je lance execvp qui remplace le processus actuel par le contenu des paramètres passés et exécute donc le code de celui-ci. Dans ce cas, execve sera l'équivalent d'une fonction infinie, car celle-ci modifie, comme expliqué, le code en cours d'exécution. Si execvp retourne (retour de -1 et code erreur dans errno), c'est qu'une erreur s'est produite et que le code ne s'est pas exécuté. Dans ce cas j'affiche l'erreur (strerror(errno)) ;
  • à ce stade, le processus fils est terminé et le père est réveillé ;
  • libération des chaînes contenues dans list_args ;
  • retour début boucle.

9-1. source

 
Sélectionnez
1.
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.
62.
63.
64.
65.
/*****************************
 * shell 0.4
 * © 2015 - Christophe LOUVET
 ****************************/

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>

#define TAILLE_BUFFER 150

int main()
{
      char    buffer[TAILLE_BUFFER];

    
    while(1)
    {
        printf("Prompt : ");
        fgets(buffer,TAILLE_BUFFER,stdin);
        buffer[strlen(buffer)-1]='\0';
        if (strcmp("exit",buffer)==0)
        {
            exit(0);
        }
        int compteur=0;
        int boucle;
        for (boucle=0;boucle<strlen(buffer);++boucle)
        {
            if (buffer[boucle]==' ') ++compteur;
        }
        char *arg_list[compteur+2];
        char *p=strdup(buffer);
        char *tmp=strtok(p," ");
        int increment=0;
        while (tmp!=NULL)
        {
            arg_list[increment]=strdup(tmp);
            increment++;
            tmp=strtok(NULL," ");
        }
        arg_list[increment]=NULL;
        pid_t process=fork();
        if (process==0)
        {
            int retour=execvp(arg_list[0],arg_list);
            if (retour==-1) printf("%s\n",strerror(errno));
            exit(0);
        }
        else
        {
            wait(&process);
        }
        increment=0;
        while (arg_list[increment]!=NULL)
        {
            free(arg_list[increment]);
            increment++;
        }
        free(p);
    }
}

Nouvelles fonctions utilisées :

Il vaut mieux utiliser la fonction strtok_r qui prend un pointeur en paramètre supplémentaire, au lieu de strtok, carcelle-ci ne fonctionne pas en cas d'utilisation à travers des blocs de code de niveaux différents (appels à travers boucles, fonctions, etc.)

Cette fonction est dangereuse, elle modifie str.

En environnement multithread, il faut utiliser strerror_r.

La mémoire occupée par la chaîne retournée par strdup devra être libérée par free.

10. Prise en compte caractère joker *

Le caractère joker * utilisé dans une commande signifie n'importe quel caractère. La commande ls *.c va donc permettre d'afficher tous les noms de fichier finissant par « .c » en remplaçant le symbole * par toutes les occurrences de ceux-ci.

Le simple appel à « ls *.c » par mon interpréteur retourne l'erreur : « impossible d'accéder à *.c: Aucun fichier ou dossier de ce type ».

La documentation de ls ne parle aucunement des caractères de remplacement tels que l'astérisque. ls attend dans ses paramètres une liste de fichiers. S'il n'y a pas de liste de fichiers, tous les fichiers du répertoire courant sont pris en compte. La gestion de l'astérisque s'effectue donc bien en amont, au niveau de l'interpréteur.

En effectuant des recherches, je suis tombé sur la fonction glob. Celle-ci permet de rechercher les occurrences de fichiers selon le motif lui ayant été passé.

Lors du parcours des sous-chaînes séparées par un espace (boucle while ligne 32), j'utilise la fonction strstr pour voir si le symbole « * » y est contenu. Si le retour de strstr est NULL, c'est qu'il n'y a pas de caractère « * », j'exécute le code comme avant. S'il y a le caractère « * », je passe la sous-chaîne en paramètre à glob, puis parcours la liste retournée (lignes 47 à 50) et copie chaque occurrence dans la arg_list. Ne pouvant plus connaître à l'avance le nombre d'arguments (celui-ci variant selon les retours éventuels de glob), je crée une arg_list de 32 éléments fixes (ligne 28) à la place du calcul selon le nombre de sous-chaînes.

La taille de 32 éléments pour arg_list est arbitraire. Si le nombre d'éléments retournés par glob est supérieur, ce code posera problème. Il faudrait créer un tableau arg_list du nombre d'éléments retournés par GLOB, ou gérer cela par une liste chaînée.

10-1. Source

 
Sélectionnez
1.
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.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
/*****************************
 * shell 0.5
 * © 2015 - Christophe LOUVET
 ****************************/

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <glob.h>

#define TAILLE_BUFFER 150

int main()
{
      char    buffer[TAILLE_BUFFER];

    
    while(1)
    {
        printf("Prompt : ");
        fgets(buffer,TAILLE_BUFFER,stdin);
        buffer[strlen(buffer)-1]='\0';
        if (strcmp("exit",buffer)==0)
        {
            exit(0);
        }
        char *arg_list[32];
        char *p=strdup(buffer);
        char *tmp=strtok(p," ");
        int increment=0;
        while (tmp!=NULL)
        {
            char *p=strstr(tmp,"*");
            if (p==NULL)
            {
                arg_list[increment]=strdup(tmp);
                increment++;
            }
            else
            {
                glob_t     g;
                int retour_glob=glob(tmp,0,NULL,&g);
                if (retour_glob==0)
                {
                    int boucle;
                    for (boucle=0;boucle<g.gl_pathc;++boucle)
                    {
                        arg_list[increment]=strdup(g.gl_pathv[boucle]);
                        increment++;
                    }
                }
                globfree(&g);
            }
            tmp=strtok(NULL," ");
        }
        arg_list[increment]=NULL;
        pid_t process=fork();
        if (process==0)
        {
            int retour=execvp(arg_list[0],arg_list);
            if (retour==-1) printf("%s\n",strerror(errno));
            exit(0);
        }
        else
        {
            wait(&process);
        }
        increment=0;
        while (arg_list[increment]!=NULL)
        {
            free(arg_list[increment]);
            increment++;
        }
        free(p);
    }
}

Nouvelles fonctions utilisées :

glob/globfree ;

strstr.

La fonction glob recherche les chemins d'accès correspondant au motif contenu dans pattern, les flags permettent de changer le comportement de base (non utilisé dans mon cas), l'argument suivant permet de passer en paramètre une fonction de gestion d'erreurs (que je n'utilise pas non plus), et enfin l'adresse de la structure glob_t.

globfree libère la mémoire occupée par un appel précédent à glob.

Un appel à globfree est nécessaire pour ne pas créer une fuite mémoire.

Pour plus d'info (notamment les flags) : man glob.

11. Redirection sortie avec « > »

J'ai ajouté à ce niveau la gestion des redirections en sortie.

11-1. Travaux préliminaires

Dans un premier temps, je vais modifier le code de façon à pouvoir plus simplement intervenir dessus, et parcourir à plusieurs reprises les arguments.

Après lecture de la console, la fonction creation_liste_arguments crée la liste des arguments telle qu'attendue par execvp. La fonction traitement_joker analyse ceux-ci et les remplace par les arguments obtenus par glob, en cas de présence du joker « * ».

11-1-1. Algorithme

Image non disponible

11-2. «>» ou « > » ?

Le point de dissociation des arguments étant l'espace, il me faut un moyen de faire la différence entre «> », «> » et « > », ces trois écritures étant valides dans un shell classique.

Je crée donc une fonction str_replace permettant de remplacer une sous-chaîne par une autre dans une chaîne.

La fonction str_replace n'existe pas en C, il faut donc la créer. Celle-ci existe dans d'autres langages tels que le PHP, et probablement dans des bibliothèques externes à la libc.

 
Sélectionnez
1.
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.
void str_replace(char *chaine,char* recherche,char *remplace)
{
int nbre=0;

    char *p=chaine;
    char *tmp=strstr(p,recherche);
    while (tmp!=NULL)
    {
        ++nbre;
        p=tmp+strlen(recherche);
        tmp=strstr(p,recherche);
    }
    if (nbre>0)
    {
        char *chaine_copie=malloc(strlen(chaine)-(strlen(recherche)*nbre)+(strlen(remplace)*nbre)+1);
        chaine_copie[0]='\0';
        char *p=chaine;
        char *tmp=strstr(p,recherche);
        while (tmp!=NULL)
        {
            strncat(chaine_copie,p,tmp-p);
            strcat(chaine_copie,remplace);
            p=tmp+strlen(recherche);
            tmp=strstr(p,recherche);
        }
        strcat(chaine_copie,p);
        strcpy(chaine,chaine_copie);
        free(chaine_copie);
    }
}

str_replace calcule le nombre d'occurrences de la sous-chaîne à remplacer et s'en sert pour réserver la mémoire pour la chaîne temporaire. La chaîne à traiter est parcourue à nouveau pour recopier dans la chaîne temporaire (c'est à ce stade que le remplacement s'effectue par recopie de la partie hors remplacement, puis la recopie de la chaîne de remplacement). Une fois l'opération terminée, la chaîne temporaire est recopiée dans la chaîne de départ via strcpy.

11-3. Analyse arguments pour redirection

Si « > » est détecté dans les arguments, la fonction scan_redirection() retourne l'argument suivant en tant que nom de fichier, et supprime « > » et l'argument suivant de la liste des arguments.

11-4. Redirection proprement dite

La redirection proprement dite s'effectue juste avant l'appel à execvp. Si la chaîne retournée par scan_redirection() est non nulle, stdout est redirigée vers le fichier via la fonction freopen.

11-4-1. Source

 
Sélectionnez
1.
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.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
/*****************************
 * shell 0.7
 * © 2015 - Christophe LOUVET
 ****************************/

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <glob.h>

#define TAILLE_BUFFER

char *arg_list[32];
char buffer[TAILLE_BUFFER];

void str_replace(char *chaine,char* recherche,char *remplace)
{
int nbre=0;

    char *p=chaine;
    char *tmp=strstr(p,recherche);
    while (tmp!=NULL)
    {
        ++nbre;
        p=tmp+strlen(recherche);
        tmp=strstr(p,recherche);
    }
    if (nbre>0)
    {
        char *chaine_copie=malloc(strlen(chaine)-(strlen(recherche)*nbre)+(strlen(remplace)*nbre)+1);
        chaine_copie[0]='\0';
        char *p=chaine;
        char *tmp=strstr(p,recherche);
        while (tmp!=NULL)
        {
            strncat(chaine_copie,p,tmp-p);
            strcat(chaine_copie,remplace);
            p=tmp+strlen(recherche);
            tmp=strstr(p,recherche);
        }
        strcat(chaine_copie,p);
        strcpy(chaine,chaine_copie);
        free(chaine_copie);
    }
}

void creation_liste_arguments()
{
char *strtokadr;
int  boucle,increment;

    for (boucle=0;boucle<32;++boucle)
    {
        arg_list[boucle]=NULL;
    }
    char *p=strdup(buffer);
    char *tmp=strtok_r(p," ",&strtokadr);
    increment=0;
    while (tmp!=NULL)
    {
        arg_list[increment]=strdup(tmp);
        ++increment;    
        tmp=strtok_r(NULL," ",&strtokadr);    
    }
    free(p);
}

void liberation_arguments()
{
int increment=0;
    while (arg_list[increment]!=NULL)
    {
        free(arg_list[increment]);
        increment++;
    }
}

void traitement_joker()
{
char *arg_list_tmp[32];

    int increment=0;
    int increment_tmp=0;
    while (arg_list[increment]!=NULL)
    {
        char *tmp=strstr(arg_list[increment],"*");
        if (tmp!=NULL)
        {
            glob_t g;
            int retour_glob=glob(tmp,0,NULL,&g);
            if (retour_glob==0)
            {
                int boucle;
                for (boucle=0;boucle<g.gl_pathc;++boucle)
                {
                    arg_list_tmp[increment_tmp]=strdup(g.gl_pathv[boucle]);
                    ++increment_tmp;
                }
                free(arg_list[increment]);
            }
            else
            {
                arg_list_tmp[increment_tmp]=arg_list[increment];
                ++increment_tmp;
            }
            globfree(&g);
        }
        else
        {
            arg_list_tmp[increment_tmp]=arg_list[increment];
            ++increment_tmp;
        }
        ++increment;
    }
    arg_list_tmp[increment_tmp]=NULL;
    increment=0;
    while (arg_list_tmp[increment]!=NULL)
    {
        arg_list[increment]=arg_list_tmp[increment];
        ++increment;
    }
    arg_list[increment]=NULL;
}

char *scan_redirection()
{
char *redirection=NULL;
int  increment=0;

    while (arg_list[increment]!=NULL)
    {
        if (strcmp(arg_list[increment],">")==0)
        {
            redirection=arg_list[increment+1];
            free(arg_list[increment]);        
            while (arg_list[increment+2]!=NULL)
            {
                arg_list[increment]=arg_list[increment+2];
                ++increment;
            }
            arg_list[increment]=NULL;
        }
        ++increment;
    }
    return redirection;
}

int main()
{
    while(1)
    {
        printf("Prompt : ");
        fgets(buffer,150,stdin);
        buffer[strlen(buffer)-1]='\0';
        if (strcmp("exit",buffer)==0)
        {
            exit(0);
        }
        str_replace(buffer," >",">");
        str_replace(buffer,"> ",">");
        str_replace(buffer,">"," > ");
        creation_liste_arguments();
        traitement_joker();
        char *fichier_redirection=scan_redirection();
        pid_t process=fork();
        if (process==0)
        {
            if (fichier_redirection!=NULL)
            {
                int handler=(int)freopen(fichier_redirection, "w", stdout);
                if (handler==-1) 
                {
                    fprintf(stderr,"%s\n",strerror(errno));
                    exit(0);
                }
            }
            int retour=execvp(arg_list[0],arg_list);
            if (retour==-1) printf("%s\n",strerror(errno));
            exit(0);
        }
        else
        {
            wait(&process);
        }
        if (fichier_redirection!=NULL) free(fichier_redirection);
        liberation_arguments();
    }
}

Nouvelles fonctions utilisées :

strcat/strncat ;

strcpy/strncpy ;

freopen (dans doc de fopen) ;

malloc/realloc/free ;

fprintf (sur page printf).

L'appel à man free affiche le man de la commande Linux free indiquant la mémoire disponible. man 3 free affichera la page man des fonctions malloc/calloc/realloc/free.

12. Redirection sortie avec « >> »

La redirection par « > » écrase le fichier passé en paramètre si celui-ci existe contrairement à la redirection par « >> »qui va effectuer un ajout à la fin de celui-ci s'il existe, sinon tout comme avec « > », il est créé.

Je rajoute str_replace(buffer," > "," >> "); ceci après le traitement des « > » car « >> » va générer « > > » en cas de double « > ». J'ai ensuite modifié scan_redirection() qui va placer un w devant le nom de fichier en cas d'écrasement de fichier existant ou a en cas d'ajout. Lors du traitement de la redirectionavant l'execvp, j'extrais le premier caractère (qui sera donc a ou w) et le passe en second paramètre à freopen. L'adresse du nom de fichier est décalée d'un caractère en ajoutant 1, de façon à ne pas traiter le w ou le a dans le nom de fichier.

13. Redirection entrée « < »

La redirection entrante « < » fonctionne sur le même principe que la sortante. Le contenu du fichier de redirection est envoyé sur le flux stdin.

Il est tout à fait possible d'utiliser une redirection entrante et une sortante dans une commande.

Exemple :

 
Sélectionnez
Commande < fichier_entrant > fichier_sortant

13-1. Exemple de commande

Pour mes essais, je vais utiliser la commande wc. Celle-ci retourne le nombre d'octets, de mots, et de lignes d'un fichier.

La commande :

 
Sélectionnez
wc fichier.txt

va afficher les informations précitées ainsi que son nom de fichier.

La commande :

 
Sélectionnez
wc < fichier.txt

va quant à elle retourner le même résultat, mais sans le nom de fichier.

La commande :

 
Sélectionnez
wc < fichier.txt > resultat.txt

va donc retourner le résultat de la commande (sans le nom de fichier) dans le fichier resultat.txt.

13-2. Modifications

J'ai renommé la fonction scan_redirection() scan_redirection_sortante(). J'ai créé une nouvelle fonction scan_redirection_entrante() reprenant la version scan_redirection() de la partie 11 (donc sans l'ajout de a ou w au nom de fichier). J'ai ajouté un test sur le retour de cette nouvelle fonction, comme pour une redirection sortante. S'il y a redirection sortante, j'utilise freopen.

14. Gestion des pipes

14-1. Qu'est-ce qu'un pipe ?

Un pipe, ou tube en français, permet de relier la sortie standard d'un programme à l'entrée d'un autre.

Un pipe s'exécute comme ceci :

 
Sélectionnez
Commande 1 | commande 2

Si nous utilisons l'exemple suivant :

 
Sélectionnez
cat fichier.txt | more

Le résultat de la commande « cat fichier.txt » est envoyé en entrée à la commande « more ».

Il est possible d'enchaîner plusieurs pipes :

 
Sélectionnez
Commande_1 | commande_2 | commande_3

14-2. Application

Je sépare la chaîne saisie en deux parties, la première, cmd1, correspondant à la partie gauche de la chaîne et cmd2 à la partie droite, le point de séparation étant le symbole du pipe : « | ». Si la chaîne saisie ne comprend pas de symbole « | », cmd1 sera une copie du buffer saisi, et cmd2 sera NULL.

Je traite ensuite cmd1 et cmd2 si non nulle via les fonctions :

  • creation_liste_arguments();
  • traitement_joker();
  • *char=scan_redirection_sortante();
  • *char=scan_redirection_entrante();

Ces fonctions ont été modifiées pour prendre des paramètres, de façon à les exploiter pour cmd1 et cmd2.

Je ne peux pas avec cette situation enchaîner les pipes. Pour cela, il me faudrait créer une liste chaînée plutôt que d'utiliser cmd1 et cmd2, et y appliquer les procédures d'analyse précitées.

Nouvelles fonctions utilisées :

15. shell : mode script/mode interactif

Le mode interactif est le mode que l'on utilise par défaut, celui-ci attend la saisie d'une commande sur la console et la traite.

Le mode script lit un fichier passé en paramètres et exécute les commandes ligne par ligne. Il est également possible de gérer des variables (passées ou non en paramètres), des structures de type if, etc. Une ligne commençant par # est considérée comme un commentaire.

15-1. shebang

Le « shebang » permet de lancer un script comme un exécutable. Pour cela, il faut insérer en début de celui-ci les caractères #! suivis du nom de l'exécutable appelant le script, par exemple :

 
Sélectionnez
#!/bin/bash
echo Bonjour

Comme le script commence par #, la première ligne est considérée comme commentaire. Par contre, les fonctions de la famille exec (execve, execvp, etc.) vont reconnaître ceci comme un « Magic Number », appeler l'interpréteur inscrit après #! en passant le nom du script en paramètre 0.

15-2. Modifications pour gestion en mode script

De façon à gérer la lecture d'un fichier de scripts, j'ai placé mon code gérant l'analyse de la commande lue sur la console dans une fonction nommée traitement_ligne.

J'ai remplacé :

 
Sélectionnez
int main()

par :

 
Sélectionnez
int main(int argc,char *argv[],char *arge[])

De façon à pouvoir utiliser les arguments passés en paramètres.

Si argc est supérieur à 1, cela signifie qu'un paramètre au moins a été passé à mon code. Dans ce cas, je passe en mode script.

15-2-1. Algorithme

Image non disponible

15-2-2. Code source  :

 
Sélectionnez
1.
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.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
268.
269.
270.
271.
272.
273.
274.
275.
276.
277.
278.
279.
280.
281.
282.
283.
284.
285.
286.
287.
288.
289.
290.
291.
292.
293.
294.
295.
296.
297.
298.
299.
300.
301.
302.
303.
304.
305.
306.
307.
308.
309.
310.
311.
312.
313.
314.
315.
316.
317.
318.
319.
320.
321.
322.
323.
324.
325.
326.
327.
328.
329.
330.
331.
332.
333.
334.
335.
336.
337.
338.
339.
340.
341.
342.
343.
344.
345.
346.
347.
348.
349.
350.
351.
352.
353.
/*****************************
 * shell 0.11
 * © 2015 - Christophe LOUVET
 ****************************/

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <glob.h>

#define TAILLE_BUFFER 250

char *arg_list[32],*arg_list2[32];
char buffer[TAILLE_BUFFER];

void str_replace(char *chaine,char* recherche,char *remplace)
{
int nbre=0;

    char *p=chaine;
    char *tmp=strstr(p,recherche);
    while (tmp!=NULL)
    {
        ++nbre;
        p=tmp+strlen(recherche);
        tmp=strstr(p,recherche);
    }
    if (nbre>0)
    {
        char *chaine_copie=malloc(strlen(chaine)-(strlen(recherche)*nbre)+(strlen(remplace)*nbre)+1);
        chaine_copie[0]='\0';
        char *p=chaine;
        char *tmp=strstr(p,recherche);
        while (tmp!=NULL)
        {
            strncat(chaine_copie,p,tmp-p);
            strcat(chaine_copie,remplace);
            p=tmp+strlen(recherche);
            tmp=strstr(p,recherche);
        }
        strcat(chaine_copie,p);
        strcpy(chaine,chaine_copie);
        free(chaine_copie);
    }
}

void creation_liste_arguments(char *arguments[32],char *commande)
{
char *strtokadr;
int  boucle,increment;

    for (boucle=0;boucle<32;++boucle)
    {
        arguments[boucle]=NULL;
    }
    char *p=strdup(commande);
    char *tmp=strtok_r(p," ",&strtokadr);
    increment=0;
    while (tmp!=NULL)
    {
        arguments[increment]=strdup(tmp);
        ++increment;    
        tmp=strtok_r(NULL," ",&strtokadr);    
    }
    free(p);
}

void liberation_arguments(char *arguments[32])
{
int increment=0;
    while (arguments[increment]!=NULL)
    {
        free(arguments[increment]);
        increment++;
    }
}

void traitement_joker(char *arguments[32])
{
char *arg_list_tmp[32];

    int increment=0;
    int increment_tmp=0;
    while (arguments[increment]!=NULL)
    {
        char *tmp=strstr(arguments[increment],"*");
        if (tmp!=NULL)
        {
            glob_t g;
            int retour_glob=glob(tmp,0,NULL,&g);
            if (retour_glob==0)
            {
                int boucle;
                for (boucle=0;boucle<g.gl_pathc;++boucle)
                {
                    arg_list_tmp[increment_tmp]=strdup(g.gl_pathv[boucle]);
                    ++increment_tmp;
                }
                free(arguments[increment]);
            }
            else
            {
                arg_list_tmp[increment_tmp]=arguments[increment];
                ++increment_tmp;
            }
            globfree(&g);
        }
        else
        {
            arg_list_tmp[increment_tmp]=arguments[increment];
            ++increment_tmp;
        }
        ++increment;
    }
    arg_list_tmp[increment_tmp]=NULL;
    increment=0;
    while (arg_list_tmp[increment]!=NULL)
    {
        arguments[increment]=arg_list_tmp[increment];
        ++increment;
    }
    arguments[increment]=NULL;
}

char *scan_redirection_entrante(char *arguments[32])
{
char *redirection=NULL;
int  increment=0;

    while (arguments[increment]!=NULL)
    {
        if (strcmp(arguments[increment],"<")==0)
        {
            redirection=arguments[increment+1];
            free(arguments[increment]);        
            while (arguments[increment+2]!=NULL)
            {
                arguments[increment]=arguments[increment+2];
                ++increment;
            }
            arguments[increment]=NULL;
        }
        ++increment;
    }
    return redirection;
}

char *scan_redirection_sortante(char *arguments[32])
{
char *redirection=NULL;
int  increment=0;

    while (arguments[increment]!=NULL)
    {
        if (strcmp(arguments[increment],">")==0)
        {
            redirection=malloc(strlen(arguments[increment+1])+1);
            redirection[0]='w';
            redirection[1]='\0';
            strcat(redirection,arguments[increment+1]);
            free(arguments[increment]);    
            free(arguments[increment+1]);    
            while (arguments[increment+2]!=NULL)
            {
                arguments[increment]=arguments[increment+2];
                ++increment;
            }
            arguments[increment]=NULL;
        }
        else if (strcmp(arguments[increment],">>")==0)
        {
            redirection=malloc(strlen(arguments[increment+1])+1);
            redirection[0]='a';
            redirection[1]='\0';
            strcat(redirection,arguments[increment+1]);
            free(arguments[increment]);    
            free(arguments[increment+1]);    
            while (arguments[increment+2]!=NULL)
            {
                arguments[increment]=arguments[increment+2];
                ++increment;
            }
            arguments[increment]=NULL;
        }
        ++increment;
    }
    return redirection;
}

void traitement_ligne()
{
char *cmd1,*cmd2;
char *fichier_redirection_sortante,*fichier_redirection_entrante;
char *fichier_redirection_sortante2,*fichier_redirection_entrante2;
int pipefd[2];

    cmd2=NULL;
    str_replace(buffer,"\n","");
    str_replace(buffer," <","<");
    str_replace(buffer,"< ","<");
    str_replace(buffer,"<"," < ");
    str_replace(buffer," >",">");
    str_replace(buffer,"> ",">");
    str_replace(buffer,">"," > ");
    str_replace(buffer," >  > "," >> ");
    str_replace(buffer,"| ","|");
    str_replace(buffer," |","|");
    str_replace(buffer,"|"," | ");
    char *tmp=strstr(buffer," | ");
    if (tmp!=NULL)
    {
        cmd1=strndup(buffer,strlen(buffer)-strlen(tmp));
        cmd2=strdup(tmp+3);
    }
    else
    {
        cmd1=strdup(buffer);
    }
    creation_liste_arguments(arg_list,cmd1);
    traitement_joker(arg_list);
    fichier_redirection_sortante=scan_redirection_sortante(arg_list);
    fichier_redirection_entrante=scan_redirection_entrante(arg_list);
    if (cmd2!=NULL)
    {
        creation_liste_arguments(arg_list2,cmd2);
        traitement_joker(arg_list2);
        fichier_redirection_sortante2=scan_redirection_sortante(arg_list2);
        fichier_redirection_entrante2=scan_redirection_entrante(arg_list2);
    }        
    pipe(pipefd);
    pid_t process=fork();
    if (process==0)
    {
        if (cmd2!=NULL) 
        {
            dup2(pipefd[1],STDOUT_FILENO);
        }
        if (fichier_redirection_entrante!=NULL)
        {
            int handler=(int)freopen(fichier_redirection_entrante,"r", stdin);
            if (handler==-1) 
            {
                fprintf(stderr,"%s\n",strerror(errno));
                exit(0);
            }
        }
        if (fichier_redirection_sortante!=NULL)
        {
            char* type_redirection=strndup(fichier_redirection_sortante,1);
            int handler=(int)freopen(fichier_redirection_sortante+1,type_redirection, stdout);
            if (handler==-1) 
            {
                fprintf(stderr,"%s\n",strerror(errno));
                exit(0);
            }
            free(type_redirection);
        }
        int retour=execvp(arg_list[0],arg_list);
        if (retour==-1) printf("%s\n",strerror(errno));
        exit(0);
    }
    else
    {
        wait(&process);
    }
    if (cmd2!=NULL)
    {
        pid_t process2=fork();
        if (process2==0)
        {
            dup2(pipefd[0],STDIN_FILENO);
            if (fichier_redirection_entrante2!=NULL)
            {
                int handler=(int)freopen(fichier_redirection_entrante2,"r", stdin);
                if (handler==-1) 
                {
                    fprintf(stderr,"%s\n",strerror(errno));
                    exit(0);
                }
            }
            if (fichier_redirection_sortante2!=NULL)
            {
                char* type_redirection=strndup(fichier_redirection_sortante,1);
                int handler=(int)freopen(fichier_redirection_sortante2+1,type_redirection, stdout);
                if (handler==-1) 
                {
                    fprintf(stderr,"%s\n",strerror(errno));
                    exit(0);
                }
                free(type_redirection);
            }
            int retour=execvp(arg_list2[0],arg_list2);
            if (retour==-1) printf("%s\n",strerror(errno));
            exit(0);
        }
        else
        {
            wait(&process2);
        }
    }
        
    if (cmd2!=NULL)
    {
        if (fichier_redirection_sortante2!=NULL) free(fichier_redirection_sortante2);
        if (fichier_redirection_entrante2!=NULL) free(fichier_redirection_entrante2);
        liberation_arguments(arg_list2);
        free(cmd2);
        cmd2=NULL;
    }
    if (fichier_redirection_entrante!=NULL) free(fichier_redirection_entrante);
    if (fichier_redirection_sortante!=NULL) free(fichier_redirection_sortante);
    liberation_arguments(arg_list);
    free(cmd1);
}

int main(int argc,char *argv[],char *arge[])
{
    if (argc>1)
    {
        printf("traitement mode batch en cours\n");
        FILE *fichier=fopen(argv[1],"r");
        if (fichier==NULL)
        {
            fprintf(stderr,"%s: %s\n",argv[1],strerror(errno));
            exit(EXIT_FAILURE);
        }
        while (fgets(buffer,150,fichier)!=NULL)
        {
            if (buffer[0]!='#')
            {
                traitement_ligne();
            }
        }
        exit(0);
    }
    else  // mode interactif
    {
        while(1)
        {
            printf("Prompt : ");
            fgets(buffer,150,stdin);
            buffer[strlen(buffer)-1]='\0';
            if (strcmp("exit",buffer)==0)
            {
                exit(0);
            }
            traitement_ligne();
        }
    }
}

15-3. Gestion de plusieurs commandes sur une ligne

Une ligne d'un script ou saisie depuis le terminal peut contenir plusieurs commandes. Celles-ci sont alors séparées par « ; ».

Exemple :

 
Sélectionnez
#!/bin/bash
echo test; echo affichage fichiers txt
ls *.txt

Ce code va exécuter la commande « echo test », la commande « echo affichage fichiers txt », puis la commande « ls *,txt ».

Pour ajouter le support de plusieurs commandes sur une même ligne via la séparation par « ; » j'ai renommé traitement_ligne() en traitement_cmd() et ajouté une nouvelle fonction traitement_ligne() qui sépare le contenu de la ligne saisie avec strtok et les passe en paramètres à traitement_cmd().

 
Sélectionnez
void traitement_ligne()
{
 char *cmd=strdup(buffer);
 char *tmp=strtok(cmd,";");
 while (tmp!=NULL)
 {
 traitement_cmd(tmp);
 tmp=strtok(NULL,";");
 }
 free(cmd);
}

void traitement_cmd(char *commande)
{
char *cmd1,*cmd2;
char *fichier_redirection_sortante,*fichier_redirection_entrante;
char *fichier_redirection_sortante2,*fichier_redirection_entrante2;
int pipefd[2];

    cmd2=NULL;
    str_replace(commande,"\n","");
    str_replace(commande," <","<");
    str_replace(commande,"< ","<");
    str_replace(commande,"<"," < ");
    str_replace(commande," >",">");
    str_replace(commande,"> ",">");
    str_replace(commande,">"," > ");
    str_replace(commande," >  > "," >> ");
    str_replace(commande,"| ","|");
    str_replace(commande," |","|");
    str_replace(commande,"|"," | ");
    char *tmp=strstr(commande," | ");
    if (tmp!=NULL)
    {
        cmd1=strndup(commande,strlen(commande)-strlen(tmp));
        cmd2=strdup(tmp+3);
    }
    else
    {
        cmd1=strdup(commande);
    }
    creation_liste_arguments(arg_list,cmd1);
    traitement_joker(arg_list);
    fichier_redirection_sortante=scan_redirection_sortante(arg_list);
    fichier_redirection_entrante=scan_redirection_entrante(arg_list);
    if (cmd2!=NULL)
    {
        creation_liste_arguments(arg_list2,cmd2);
        traitement_joker(arg_list2);
        fichier_redirection_sortante2=scan_redirection_sortante(arg_list2);
        fichier_redirection_entrante2=scan_redirection_entrante(arg_list2);
    }        
    pipe(pipefd);
    pid_t process=fork();
    if (process==0)
    {
        if (cmd2!=NULL) 
        {
            dup2(pipefd[1],STDOUT_FILENO);
        }
        if (fichier_redirection_entrante!=NULL)
        {
            int handler=(int)freopen(fichier_redirection_entrante,"r", stdin);
            if (handler==-1) 
            {
                fprintf(stderr,"%s\n",strerror(errno));
                exit(0);
            }
        }
        if (fichier_redirection_sortante!=NULL)
        {
            char* type_redirection=strndup(fichier_redirection_sortante,1);
            int handler=(int)freopen(fichier_redirection_sortante+1,type_redirection, stdout);
            if (handler==-1) 
            {
                fprintf(stderr,"%s\n",strerror(errno));
                exit(0);
            }
            free(type_redirection);
        }
        int retour=execvp(arg_list[0],arg_list);
        if (retour==-1) printf("%s\n",strerror(errno));
        exit(0);
    }
    else
    {
        wait(&process);
    }
    if (cmd2!=NULL)
    {
        pid_t process2=fork();
        if (process2==0)
        {
            dup2(pipefd[0],STDIN_FILENO);
            if (fichier_redirection_entrante2!=NULL)
            {
                int handler=(int)freopen(fichier_redirection_entrante2,"r", stdin);
                if (handler==-1) 
                {
                    fprintf(stderr,"%s\n",strerror(errno));
                    exit(0);
                }
            }
            if (fichier_redirection_sortante2!=NULL)
            {
                char* type_redirection=strndup(fichier_redirection_sortante,1);
                int handler=(int)freopen(fichier_redirection_sortante2+1,type_redirection, stdout);
                if (handler==-1) 
                {
                    fprintf(stderr,"%s\n",strerror(errno));
                    exit(0);
                }
                free(type_redirection);
            }
            int retour=execvp(arg_list2[0],arg_list2);
            if (retour==-1) printf("%s\n",strerror(errno));
            exit(0);
        }
        else
        {
            wait(&process2);
        }
    }
        
    if (cmd2!=NULL)
    {
        if (fichier_redirection_sortante2!=NULL) free(fichier_redirection_sortante2);
        if (fichier_redirection_entrante2!=NULL) free(fichier_redirection_entrante2);
        liberation_arguments(arg_list2);
        free(cmd2);
        cmd2=NULL;
    }
    if (fichier_redirection_entrante!=NULL) free(fichier_redirection_entrante);
    if (fichier_redirection_sortante!=NULL) free(fichier_redirection_sortante);
    liberation_arguments(arg_list);
    free(cmd1);
}

16. Gestion des variables

Les shells permettent de gérer des variables. Celles-ci peuvent être spécifiques au shell (elles ne seront dans ce cas pas transmises aux processus fils et donc internes au processus), ou être des variables d'environnement (transmises aux processus fils par son père via l'environnement).

Les variables se positionnent de la façon suivante :

 
Sélectionnez
variable=test

Pour ensuite utiliser le contenu de celles-ci, il faut mettre le symbole $ devant.

Exemple :

 
Sélectionnez
echo $variable

va afficher la valeur de « variable » : (dans notre cas : test). Avant l'appel de echo, $variable est remplacé par test.

Dans la fonction traitement_ligne, si « = » est détecté, la fonction ajout_environnement est appelée après séparation de la partie gauche et droite (respectivement le nom de la variable et sa valeur). L'appel à $essai remplace $essai par test. Pour cela, j'ai créé une fonction gestion_variables. Cette fonction parcourt tous les arguments, et dans le cas où un argument commence par « $ », l'argument est remplacé par la valeur de la variable du même nom.

La liste des variables déclarées est stockée dans une liste chaînée contenant leur nom et valeur.

Les variables arguments ( $0, $1, etc.) seront traitées au chapitre 17Les arguments.

Il me faudra également charger les variables d'environnement préexistantes depuis arge

16-1. Source nouvelles fonctions

Fonction ajout_environnement :

 
Sélectionnez
void ajout_environnement(char *nom_variable,char *valeur_variable)
{
 Environnement *liste=var_environnement;
 
 int test=0;
 if (liste!=NULL)
 {
 while (liste->next!=NULL)
 {
 if (strcmp(nom_variable,liste->nom)==0)
 {
 free(liste->valeur);
 liste->valeur=strdup(valeur_variable);
 test=1;
 }
 liste=liste->next;
 }
 }
 
 if (test==0)
 {
 Environnement *new_env=malloc(sizeof(Environnement));
 new_env->nom=strdup(nom_variable);
 new_env->valeur=strdup(valeur_variable);
 new_env->next=NULL;
 liste=var_environnement;
 if (liste!=NULL)
 {
 while(liste->next!=NULL)
 {
 liste=liste->next;
 } 
 liste->next=new_env;
 }
 else
 {
            var_environnement=new_env;
        }
    }
}

Fonction gestion_variables :

 
Sélectionnez
void gestion_variables(char* arguments[32])
{
int increment=0;

    while (arguments[increment]!=NULL)
    {
        char *chaine_a_scanner=arguments[increment];
        if (chaine_a_scanner[0]=='$') 
        {
            Environnement *liste=var_environnement;
            while (liste!=NULL)
            {
                if (strcmp(liste->nom,chaine_a_scanner+1)==0)
                {
                    free(arguments[increment]);
                    arguments[increment]=strdup(liste->valeur);
                }
                liste=liste->next;
            }
        }
        ++increment;
    }
}

16-2. Ajout commande set

La commande interne set liste les variables du shell. Son traitement s'effectue dans la fonction traitement_ligne.

J'ai également modifié le code pour gérer une variable non existante qui retournera l'équivalent de "". Ceci se produit dans la fonction gestion_variables. Lors du parcours des arguments, la variable détection est mise à 1 si le nom de variable fourni est détecté dans la liste des variables, et dans ce cas le nom de la variable est remplacé dans l'argument par la valeur de la variable. Dans le cas contraire (détection=0), l'argument (contenant $ suivi du nom de la variable inexistante) est remplacé par "".

16-3. Ajout export

Par défaut, une variable créée dans un shell n'est accessible que de celui-ci. Pour la rendre accessible aux processus fils, il faut utiliser la commande interne « export ». Celle-ci prend en paramètre le nom de la variable. Dans mon code, export va appeler setenv avec le nom de la variable en paramètre. setenv va comme son nom l'indique ajouter (ou modifier sa valeur si elle existe déjà) à l'environnement.

Nouvelles fonctions utilisées :

  • setenv (unsetenv vu ci-dessous sur la même page).

16-4. sizeof

sizeof est un mot clé du langage, et non une fonction de la libc. sizeof retourne la taille d'une variable (y compris des structures ou allocations mémoire) en nombre d'octets.

16-5. Ajout unset

La commande unset supprime la variable. J'appelle unsetenv poursupprimer celle-ci de l'environnement. Au cas où la variable n'a pas été exportée avec export (et n'est donc pas dans l'environnement, mais uniquement dans la liste des variables shell), unsetvenv n'aura aucun effet, je n'ai donc pas besoin de m'assurer que la variable à supprimer est dans l'environnement.

Documentation unsetenv.

17. Les arguments

Les arguments sont transmis depuis le processus appelant de la même façon que l'environnement : une liste de pointeurs vers des chaînes de caractères terminées par NULL et appelée argv. Le nombre d'arguments transmis sous forme d'un entier se nomme argc.

Le 1er argument est toujours la ligne de commande appelante.

Exemple :

 
Sélectionnez
cat /etc/resolv.conf

Lors de l'appel de la commande ci-dessus, voici les arguments :

  • Argument 0 : cat ;
  • Argument 1 : /etc/resolv.conf.

Pour appeler les variables correspondant aux arguments dans un shell, il faut juste les préfixer du symbole « $ ».

Exemple :

 
Sélectionnez
echo $0

Cette commande va afficher l'argument 0 (comme vu précédemment).

Pour utiliser les arguments au-dessus du nombre 9, il faut les protéger par {} exemple : echo ${10}. Mon code ne traite pas ceci, contrairement au fonctionnement standard d'un shell.

De façon à gérer ceci, j'ai modifié ma fonction gestion_variables(). Celle-ci recherche la présence d'un $ en premier caractère de l'argument et considère qu'il s'agit alors d'une variable. (comme vu au paragraphe précédent). J'utilise ensuite la fonction strtol qui retourne un entier correspondant à la chaîne fournie en paramètre.

Documentation strtol.

Dans mon cas, j'analyse le retour de **endptr de strtol. Si celui-ci n'est pas NULL, c'est qu'il y a des symboles dans la chaîne qui ne correspondent pas à une valeur numérique, je considère donc la chaîne comme ne contenant pas un nombre valide.

18. Interlude

Avant de passer à l'étape suivante, j'ai modifié le code de main gérant le mode interactif/script. La lecture par fgets se fait maintenant dans les deux cas sur le handle nommé fichier. Dans le cas du mode interactif, fichier est positionné à stdin, dans le cas contraire, fichier est positionné sur le nom du fichier passé en paramètre.

 
Sélectionnez
int main(int argc,char *argv[],char *arge[])
{

    global_argc=argc;
    int increment=0;
    while (arge[increment]!=NULL)
    {
        char *valeur=strstr(arge[increment],"=");
        char *nom=strndup(arge[increment],strlen(arge[increment])-strlen(valeur));
        ajout_environnement(nom,valeur+1);
        free(nom);
        ++increment;
    }
    if (argc>1)
    {
        fichier=fopen(argv[1],"r");
        if (fichier==NULL)
        {
            fprintf(stderr,"%s: %s\n",argv[1],strerror(errno));
            exit(EXIT_FAILURE);
        }
    }
    else 
    {
        fichier=stdin;
    }
    while(!feof(fichier))
    {
        if (argc==1) printf("Prompt : ");
        char *lecture=fgets(buffer,150,fichier);
        if (lecture==NULL)
        {
            exit(EXIT_SUCCESS);
        }
        if (buffer[strlen(buffer)-1]=='\n') buffer[strlen(buffer)-1]='\0';
        traitement_ligne(argv);
    }
    return 0;
}

19. Boucle conditionnelle if/then/else

Tout comme dans un langage de programmation, le shell est capable d'exécuter du code conditionnel. Le paragraphe en cours va traiter de la boucle if/then/else.

Celle-ci se présente comme ceci :

 
Sélectionnez
Commande 1
if condition
then
  commande 2
  commande 3
else
  commande 4
  commande 5
fi
commande 6

if-then marque le début du code conditionnel. fi marque la fin de la boucle conditionnelle. Si le script ne contient pas then et/ou fi, une erreur sera générée. La condition else est optionnelle.

Dans l'exemple ci-dessus, si la condition est reconnue, le fil d'exécution sera l'exécution des commandes 1,2,3,6 sinon ce sera 1,4,5,6.

19-1. script de test

Voici le script de test qui sera utilisé :

 
Sélectionnez
#!/bin/bash
if [ "$2" = "2" ]
then 
   #partie test ok
   echo ok
else
   echo pas ok
fi
echo apres boucle if
echo fin script

Ce script est appelé avec la commande :

 
Sélectionnez
./code script 1

ou la commande :

 
Sélectionnez
./code script 2

Dans cet exemple, le script compare l'argument $2 (soit 1 ou 2) et dans le cas où le script test est appelé avec comme dernier argument 2, affichera « ok », sinon « pas ok ».

La première ligne #!/bin/bash ne sert à rien quand le script est lancé par mon code (la ligne sera considérée comme commentaire), mais sert en cas d'appel depuis le shell (Shebang). En cas d'appel depuis le shell, il faut remplacer $2 par $1 pour que l'argument soit analysable par la boucle if.

19-2. Fonctionnement de ma boucle if/then/else

En cas de présence de « if », il me faut détecter le bloc if/then/else, tester la condition, et la traiter.

Le traitement de if est intégré dans la fonction traitement_ligne (au même niveau que le test de commentaire, le test de présence de « exit »,« set »,« unset »,« export » ). Voici son cheminement :

  • teste si la ligne contenant if est immédiatement suivie de « [ », si ce n'est pas le cas, fin de code avec affichage de message d'erreur ;
  • teste si la ligne if se termine par « ] » ;
  • suppression de if, des crochets, des guillemets et des espaces superflus (par appels de str_replace) ;
  • teste la présence de « = » dans la formule ;
  • séparation partie avant et après « = » et mise dans tableau arg_list_temp (en vue de l'appel à gestion_variable) ;
  • appel à gestion_variables pour remplacer les variables dans la formule par leur valeur (dans notre exemple, $2 est remplacé par l'argument de la ligne de commande) ;
  • boucle while lisant le fichier (ou stdin) jusqu'à sa fin ou la présence de « fi ». Toutes les lignes sont stockées dans une liste chaînée (réutilisation structure Environnement contenant normalement les variables d'environnement ) ;
  • teste si le dernier élément de la liste chaînée est « fi », sinon génération d'une erreur et sortie ;
  • vérification de la présence de « then » sur la ligne suivant le « if », sinon : erreur ;
  • boucle exécutant chaque ligne stockée dans la liste chaînée via traitement_cmd. Le code n'est exécuté que si le test de la ligne 464 est valide (comparaison retournant 0) ou qu'il suit une ligne « else ».

Mes fonctions ont été modifiées pour pouvoir utiliser les arguments passés en paramètres, car argc et argv sont passés à la fonction main(). Pour qu'elle soit accessible aux autres fonctions, je copie argv dans une variable globale.

Nouvelles fonctions utilisées :

strcspn ;

memmove.

19-3. Source gestion if/then/else

Voici l'extrait de la fonction traitement_ligne() gérant « if »

 
Sélectionnez
    else if (strncmp(buffer,"if",2)==0)
    {
        char *lecture=buffer;
        int boucle=0;
        Environnement *var_if=malloc(sizeof(Environnement));
        var_if->nom=malloc(4);
        sprintf(var_if->nom,"%d",boucle);
        var_if->valeur=strdup(buffer);
        var_if->next=NULL;
        if (strncmp(buffer,"if [",4)!=0)
        {
            fprintf(stderr,"%s : Erreur de syntaxe\n",buffer);
            exit(EXIT_FAILURE);
        }
        char *test_fin=strstr(buffer,"]");
        if (strcmp(test_fin,"]")!=0 && strcmp(test_fin,"] ")!=0)
        {
            fprintf(stderr,"%s : Erreur de syntaxe\n",buffer);
            exit(EXIT_FAILURE);
        }
        char *formule=strdup(buffer);
        str_replace(formule,"if [","");
        str_replace(formule,"]","");
        str_replace(formule," ","");
        str_replace(formule,"\"","");    
        int retour=strcspn(formule,"=");
        if (retour==strlen(formule))
        {
            fprintf(stderr,"%s : Erreur de syntaxe\n",buffer);
            exit(EXIT_FAILURE);            
        }
        char *arg_list_temp[32];
        arg_list_temp[0]=strndup(formule,retour);
        arg_list_temp[1]=strdup(formule+retour+1);
        arg_list_temp[2]=NULL;
        gestion_variables(arg_list_temp,argv);
        int test=strcmp(arg_list_temp[0],arg_list_temp[1]);
        free(arg_list_temp[1]);
        free(arg_list_temp[0]);
        free(formule);
        Environnement *liste_var_if=var_if;
        Environnement *liste=liste_var_if;
        while (lecture!=NULL)
        {
            lecture=fgets(buffer,150,fichier);
            traitement_espaces_debut(buffer);
            if (lecture==NULL) break;
            if (buffer[strlen(buffer)-1]=='\n') buffer[strlen(buffer)-1]='\0';
            if (strncmp(buffer,"fi",2)==0)
            {
                ++boucle;
                var_if=malloc(sizeof(Environnement));
                var_if->nom=malloc(4);
                sprintf(var_if->nom,"%d",boucle);
                var_if->valeur=strdup(buffer);
                var_if->next=NULL;
                liste->next=var_if;
                liste=var_if;
                lecture=NULL;                
            }
            else
            {
                ++boucle;
                var_if=malloc(sizeof(Environnement));
                var_if->nom=malloc(4);
                sprintf(var_if->nom,"%d",boucle);
                var_if->valeur=strdup(buffer);
                var_if->next=NULL;
                liste->next=var_if;
                liste=var_if;
            }
        }
        if (strcmp(liste->valeur,"fi")!=0)
        {
            fprintf(stderr,"%s : Erreur de syntaxe\n",buffer);
            exit(EXIT_FAILURE);                        
        }
        liste=liste_var_if;
        liste=liste->next;
        str_replace(liste->valeur," ","");
        if (strcmp(liste->valeur,"then")!=0)
        {
            fprintf(stderr,"*%s* : Erreur de syntaxe\n",liste->valeur);
            exit(EXIT_FAILURE);                        
        }
        liste=liste_var_if;
        liste=liste->next;
        liste=liste->next;
        int detection_else=0;
        while (liste!=NULL)
        {
            if (strcmp(liste->valeur,"else")==0)
            {
                detection_else=1;
            }
            else if (strcmp(liste->valeur,"fi")==0)
            {
            }
            else if (liste->valeur[0]=='#')
            {
            }
            else
            {
                if (test==0)
                {
                    if (detection_else==0)
                    {
                        traitement_cmd(liste->valeur,argv);
                    }
                }
                else
                {
                    if (detection_else==1)
                    {
                        traitement_cmd(liste->valeur,argv);
                    }
                }
            }
            liste=liste->next;
        }        
    }

20. Boucle for

Le fonctionnement d'une boucle for en shell diffère un peu d'une boucle for en C.

Là où on écrirait en C :

 
Sélectionnez
for (boucle=0;boucle<9;++boucle)
{}

on écrirait en shell :

 
Sélectionnez
for variable in valeur1 valeur2 valeur3...
dodone

Il existe en bash une notation ressemblant au C, moins répandue, je la présente ci-dessous, mais sans l'utiliser.

 
Sélectionnez
for ((i = 0; i <10; i += 1))
do
  echo $i
done

Les mots clés dodone délimitent le contenu de la boucle comme les accolades en C. Les éléments après le mot clé in indiquent les différentes valeurs pour la boucle.

Exemple :

 
Sélectionnez
#!/bin/bash
echo debut script test boucle for
for i in 1 3 5 7 8 
do
 echo dans boucle
 echo i = $i
done 
echo derniere ligne script

Dans cet exemple, la boucle va afficher la valeur de i avec 1,3,5,7, et 8.

J'ai ajouté une fonction nommée traitement_espaces_fin supprimant les espaces inutiles en fin de ligne.

Le nombre d'arguments de for est limité à 32. Cette limitation étant due à l'utilisation de creation_liste_arguments et non pas aux limites système.

20-1. Source gestion for

Extrait de la fonction traitement_ligne s'occupant de la gestion de for :

 
Sélectionnez
    else if (strncmp(buffer,"for",3)==0)
    {
        int boucle=0;
        Environnement *var_if=malloc(sizeof(Environnement));
        var_if->nom=malloc(4);
        sprintf(var_if->nom,"%d",boucle);
        var_if->valeur=strdup(buffer);
        var_if->next=NULL;
        ++boucle;
        Environnement *liste_var_if=var_if;
        Environnement *liste=liste_var_if;
        char *lecture=buffer;
        while (1)
        {
            lecture=fgets(buffer,TAILLE_BUFFER,fichier);
            if (lecture==NULL) break;
            if (buffer[strlen(buffer)-1]=='\n') buffer[strlen(buffer)-1]='\0';
            traitement_espaces_debut(buffer);
            traitement_espaces_fin(buffer);
            str_replace(buffer,"\n","");
            var_if=malloc(sizeof(Environnement));
            var_if->nom=malloc(4);
            sprintf(var_if->nom,"%d",boucle);
            var_if->valeur=strdup(buffer);
            var_if->next=NULL;
            liste->next=var_if;
            liste=var_if;    
            ++boucle;
            if (strcmp(var_if->valeur,"done")==0) break;
        }
        if (strcmp(liste->valeur,"done")!=0)
        {
            fprintf(stderr,"Erreur de syntaxe : fin de fichier prématurée\n");
            exit(EXIT_FAILURE);                                    
        }
        liste=liste_var_if;
        liste=liste->next;
        if (strcmp(liste->valeur,"do")!=0)
        {
            fprintf(stderr,"Erreur de syntaxe : \"do\" non trouvé dans structure for\n");
            exit(EXIT_FAILURE);                                    
        }    
        liste=liste_var_if;
        char *args_for[32];
        creation_liste_arguments(args_for,liste->valeur);            
        boucle=3;
        ajout_environnement(args_for[1],"0");
        ajout_environnement(args_for[1],"0");
        while (args_for[boucle]!=NULL)
        {
            ajout_environnement(args_for[1],args_for[boucle]);
            liste=liste_var_if;
            liste=liste->next;
            liste=liste->next;
            while (liste!=NULL)
            {
                if (strcmp(liste->valeur,"done")==0) break;
                traitement_cmd(liste->valeur,argv);
                liste=liste->next;
            }
            ++boucle;
        }        
    }

21. Traitement des guillemets

Les arguments sont normalement séparés par des espaces. Mais quid en cas d'arguments tels qu'un nom de fichier contenant des espaces ? L'argument mis entre guillemets règle le problème.

Il est aussi possible d'échapper les espaces en plaçant un antislash devant, mais il est plutôt d'usage d'utiliser les guillemets.

Vous pourrez par contre constater l'affichage d'antislash après avoir appuyé sur la touche tabulation pour utiliser la complétion dans le shell en cas de présence de fichiers avec espaces dans leur nom.

Pour cela, j'ai modifié ma fonction creation_liste_arguments(). Celle-ci extrait le 1er argument en calculant la longueur de texte sans espace ni guillemet par strcspn, puis continue à parcourir la chaîne jusqu'à trouver un espace si pas de guillemet trouvé en début de chaîne ou un guillemet (de fermeture) en cas de présence d'un guillemet.

21-1. Code modifié (fonction creation_liste_arguments)

 
Sélectionnez
void creation_liste_arguments(char *arguments[32],char *commande)
{
int  boucle,increment,longueur;

    for (boucle=0;boucle<32;++boucle)
    {
        arguments[boucle]=NULL;
    }
    longueur=strcspn(commande," \"");
    arguments[0]=strndup(commande,longueur);
    commande=commande+longueur;
    increment=1;
    while (strlen(commande)>0)
    {    
        if (commande[0]==' ') ++commande;
        char *separateur=" ";
        if (commande[0]=='"') separateur="\"";
        if (strcmp(separateur,"\"")==0) ++commande;
        longueur=strcspn(commande,separateur);
        arguments[increment]=strndup(commande,longueur);
        commande=commande+longueur;
        if (strcmp(separateur,"\"")==0) ++commande;
        ++increment;
    }
}

22. Gestion historique

L'historique dans le shell permet, comme son nom l'indique, de garder trace des précédentes commandes saisies. La dernière commande appelée s'affiche par l'appui de la touche flèche du haut. La liste des commandes est affichable par la commande history, les commandes étant précédées de leur numéro. La commande « history -c » efface l'historique (le fichier historique sera donc remis à 0). Le nombre de commandes d'historique conservées est stocké dans la variable $HISTSIZE. Si vous utilisez la commande set, vous constaterez la présence de $HISTSIZE et de $HISTFILESIZE $HISTSIZE correspondant au nombre de commandes d'historique potentiellement en mémoire, tandis qu'$HISTFILESIZE correspond au nombre stocké dans le fichier d'historique. Dans le cas de bash, l'historique est stocké par défaut dans le fichier .bash_history du dossier de l'utilisateur (la variable $HISTFILE contient le nom du fichier stockant l'historique - le point au début du nom de fichier signifiant bien entendu que le fichier est un fichier caché).

Le symbole « ! » Suivi d'un numéro permet de rappeler la commande correspondant à ce numéro dans l'historique. La commande « !-2 » va rappeler l'avant-dernière commande (référence à la position actuelle moins le nombre négatif).

Exemple :

 
Sélectionnez
ls
echo bonjour

La commande :

 
Sélectionnez
!-2

va rappeler la commande ls.

22-1. Implémentation

Au début de main(), je crée une variable HISTSIZE fixée à 500. J'ouvre ensuite le fichier .myshell_history. Je lis ensuite toutes les lignes du fichier (via fgets) jusqu'à sa fin pour compter celles-ci. Si le nombre de lignes est supérieur à HISTSIZE, j'en déduis le nombre de lignes à supprimer (différence entre le nombre de lignes lues et HISTSIZE). Je « rembobine » le fichier (fonction rewind) et me place sur la première ligne à garder (avec boucle for). J'ouvre ensuite un fichier temporaire et copie ligne par ligne le contenu de .myshell_history depuis cette position (par lecture et recopie ligne par ligne avec fgets depuis le fichier source et printf vers le fichier destination. Je supprime ensuite .myshell_history, puis renomme le fichier temporaire en .myshel_history.

L'écriture dans le fichier historique après chaque saisie se fait dans la boucle while de main juste avant appel à traitement_ligne.

Dans traitement_ligne, si « ! » est tapé, je convertis la valeur derrière en nombre. Ce nombre est utilisé comme numéro d'enregistrement dans l'historique tel qu'expliqué au-dessus.

La limitation du nombre de lignes d'historique conservé par la variable HISTSIZE ne s'effectue qu'au lancement de mon shell.

L'appui sur la touche flèche haut qui réaffiche normalement la dernière commande n'est pas géré (ce point sera vu ultérieurement).

J'ai été confronté à un problème : l'ouverture de fichier avec comme paramètre « a+ » écrit toujours à la fin de celui-ci, même si on change la position dans le fichier avec fseek. Ce comportement est documenté, mais il m'avait échappé. Je n'utiliserai donc plus ce paramètre.

Nouvelles fonctions utilisées :

fseek/rewind ;

remove ;

rename.

22-2. Source

 
Sélectionnez
1.
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.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
268.
269.
270.
271.
272.
273.
274.
275.
276.
277.
278.
279.
280.
281.
282.
283.
284.
285.
286.
287.
288.
289.
290.
291.
292.
293.
294.
295.
296.
297.
298.
299.
300.
301.
302.
303.
304.
305.
306.
307.
308.
309.
310.
311.
312.
313.
314.
315.
316.
317.
318.
319.
320.
321.
322.
323.
324.
325.
326.
327.
328.
329.
330.
331.
332.
333.
334.
335.
336.
337.
338.
339.
340.
341.
342.
343.
344.
345.
346.
347.
348.
349.
350.
351.
352.
353.
354.
355.
356.
357.
358.
359.
360.
361.
362.
363.
364.
365.
366.
367.
368.
369.
370.
371.
372.
373.
374.
375.
376.
377.
378.
379.
380.
381.
382.
383.
384.
385.
386.
387.
388.
389.
390.
391.
392.
393.
394.
395.
396.
397.
398.
399.
400.
401.
402.
403.
404.
405.
406.
407.
408.
409.
410.
411.
412.
413.
414.
415.
416.
417.
418.
419.
420.
421.
422.
423.
424.
425.
426.
427.
428.
429.
430.
431.
432.
433.
434.
435.
436.
437.
438.
439.
440.
441.
442.
443.
444.
445.
446.
447.
448.
449.
450.
451.
452.
453.
454.
455.
456.
457.
458.
459.
460.
461.
462.
463.
464.
465.
466.
467.
468.
469.
470.
471.
472.
473.
474.
475.
476.
477.
478.
479.
480.
481.
482.
483.
484.
485.
486.
487.
488.
489.
490.
491.
492.
493.
494.
495.
496.
497.
498.
499.
500.
501.
502.
503.
504.
505.
506.
507.
508.
509.
510.
511.
512.
513.
514.
515.
516.
517.
518.
519.
520.
521.
522.
523.
524.
525.
526.
527.
528.
529.
530.
531.
532.
533.
534.
535.
536.
537.
538.
539.
540.
541.
542.
543.
544.
545.
546.
547.
548.
549.
550.
551.
552.
553.
554.
555.
556.
557.
558.
559.
560.
561.
562.
563.
564.
565.
566.
567.
568.
569.
570.
571.
572.
573.
574.
575.
576.
577.
578.
579.
580.
581.
582.
583.
584.
585.
586.
587.
588.
589.
590.
591.
592.
593.
594.
595.
596.
597.
598.
599.
600.
601.
602.
603.
604.
605.
606.
607.
608.
609.
610.
611.
612.
613.
614.
615.
616.
617.
618.
619.
620.
621.
622.
623.
624.
625.
626.
627.
628.
629.
630.
631.
632.
633.
634.
635.
636.
637.
638.
639.
640.
641.
642.
643.
644.
645.
646.
647.
648.
649.
650.
651.
652.
653.
654.
655.
656.
657.
658.
659.
660.
661.
662.
663.
664.
665.
666.
667.
668.
669.
670.
671.
672.
673.
674.
675.
676.
677.
678.
679.
680.
681.
682.
683.
684.
685.
686.
687.
688.
689.
690.
691.
692.
693.
694.
695.
696.
697.
698.
699.
700.
701.
702.
703.
704.
705.
706.
707.
708.
709.
710.
711.
712.
713.
714.
715.
716.
717.
718.
719.
720.
721.
722.
723.
724.
725.
726.
727.
728.
729.
730.
731.
732.
733.
734.
735.
736.
737.
738.
739.
740.
741.
742.
743.
744.
745.
746.
747.
748.
749.
750.
751.
752.
753.
754.
755.
756.
757.
758.
759.
760.
761.
762.
763.
764.
765.
766.
767.
768.
769.
770.
771.
772.
773.
774.
775.
776.
777.
778.
779.
780.
781.
782.
783.
784.
785.
786.
787.
788.
789.
790.
791.
792.
793.
794.
795.
796.
797.
798.
799.
800.
801.
802.
803.
804.
805.
806.
807.
808.
809.
810.
811.
812.
813.
814.
815.
816.
817.
818.
819.
820.
821.
822.
823.
824.
825.
826.
827.
828.
829.
830.
831.
832.
833.
834.
835.
836.
837.
838.
839.
840.
841.
842.
843.
844.
845.
846.
847.
848.
849.
850.
851.
852.
853.
/*****************************
 * shell 0.21
 * © 2015 - Christophe LOUVET
 ****************************/

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <glob.h>

#define TAILLE_HISTO 500

typedef struct Environnement
{
    char *nom;
    char *valeur;
    struct Environnement *next;
} Environnement ;

Environnement *var_environnement=NULL;

char *arg_list[32],*arg_list2[32];
char buffer[250];
int     global_argc, histocount;
FILE *fichier,*fichier_historique;

void ajout_environnement(char *nom_variable,char *valeur_variable)
{
    Environnement *liste=var_environnement;
    
    int test=0;
    if (liste!=NULL)
    {
        while (liste->next!=NULL)
        {
            if (strcmp(nom_variable,liste->nom)==0)
            {
                free(liste->valeur);
                liste->valeur=strdup(valeur_variable);
                test=1;
            }
            liste=liste->next;
        }
    }
    
    if (test==0)
    {
        Environnement *new_env=malloc(sizeof(Environnement));
        new_env->nom=strdup(nom_variable);
        new_env->valeur=strdup(valeur_variable);
        new_env->next=NULL;
        liste=var_environnement;
        if (liste!=NULL)
        {
            while(liste->next!=NULL)
            {
                liste=liste->next;
            }    
            liste->next=new_env;
        }
        else
        {
            var_environnement=new_env;
        }
    }
}

void str_replace(char *chaine,char* recherche,char *remplace)
{
int nbre=0;

    char *p=chaine;
    char *tmp=strstr(p,recherche);
    while (tmp!=NULL)
    {
        ++nbre;
        p=tmp+strlen(recherche);
        tmp=strstr(p,recherche);
    }
    if (nbre>0)
    {
        char *chaine_copie=malloc(strlen(chaine)-(strlen(recherche)*nbre)+(strlen(remplace)*nbre)+1);
        chaine_copie[0]='\0';
        char *p=chaine;
        char *tmp=strstr(p,recherche);
        while (tmp!=NULL)
        {
            strncat(chaine_copie,p,tmp-p);
            strcat(chaine_copie,remplace);
            p=tmp+strlen(recherche);
            tmp=strstr(p,recherche);
        }
        strcat(chaine_copie,p);
        strcpy(chaine,chaine_copie);
        free(chaine_copie);
    }
}

void creation_liste_arguments(char *arguments[32],char *commande)
{
int  boucle,increment,longueur;

    for (boucle=0;boucle<32;++boucle)
    {
        arguments[boucle]=NULL;
    }
    longueur=strcspn(commande," \"");
    arguments[0]=strndup(commande,longueur);
    commande=commande+longueur;
    increment=1;
    while (strlen(commande)>0)
    {    
        if (commande[0]==' ') ++commande;
        char *separateur=" ";
        if (commande[0]=='"') separateur="\"";
        if (strcmp(separateur,"\"")==0) ++commande;
        longueur=strcspn(commande,separateur);
        arguments[increment]=strndup(commande,longueur);
        commande=commande+longueur;
        if (strcmp(separateur,"\"")==0) ++commande;
        ++increment;
    }
}

void gestion_variables(char* arguments[32],char **argv)
{
int increment=0;

    while (arguments[increment]!=NULL)
    {
        char *chaine_a_scanner=arguments[increment];
        if (chaine_a_scanner[0]=='$') 
        {
            int detection=0;
            char *endptr=NULL;
            int entier=strtol(arguments[increment]+1,&endptr,10);
            if (strcmp(endptr,"")!=0) entier=-1;
            if (entier!=-1)
            {
                if (entier+1<=global_argc)
                {
                    free(arguments[increment]);
                    arguments[increment]=strdup(argv[entier]);
                    detection=1;
                }
            }
            else
            {
                Environnement *liste=var_environnement;
                while (liste!=NULL)
                {
                    if (strcmp(liste->nom,chaine_a_scanner+1)==0)
                    {
                        free(arguments[increment]);
                        arguments[increment]=strdup(liste->valeur);
                        detection=1;
                    }
                    liste=liste->next;
                }
            }
            if (detection==0)
            {
                free(arguments[increment]);
                arguments[increment]=strdup("");
            }
        }
        ++increment;
    }
}

void liberation_arguments(char *arguments[32])
{
int increment=0;
    while (arguments[increment]!=NULL)
    {
        free(arguments[increment]);
        increment++;
    }
}

void traitement_joker(char *arguments[32])
{
char *arg_list_tmp[32];

    int increment=0;
    int increment_tmp=0;
    while (arguments[increment]!=NULL)
    {
        char *tmp=strstr(arguments[increment],"*");
        if (tmp!=NULL)
        {
            glob_t g;
            int retour_glob=glob(tmp,0,NULL,&g);
            if (retour_glob==0)
            {
                int boucle;
                for (boucle=0;boucle<g.gl_pathc;++boucle)
                {
                    arg_list_tmp[increment_tmp]=strdup(g.gl_pathv[boucle]);
                    ++increment_tmp;
                }
                free(arguments[increment]);
            }
            else
            {
                arg_list_tmp[increment_tmp]=arguments[increment];
                ++increment_tmp;
            }
            globfree(&g);
        }
        else
        {
            arg_list_tmp[increment_tmp]=arguments[increment];
            ++increment_tmp;
        }
        ++increment;
    }
    arg_list_tmp[increment_tmp]=NULL;
    increment=0;
    while (arg_list_tmp[increment]!=NULL)
    {
        arguments[increment]=arg_list_tmp[increment];
        ++increment;
    }
    arguments[increment]=NULL;
}

char *scan_redirection_entrante(char *arguments[32])
{
char *redirection=NULL;
int  increment=0;

    while (arguments[increment]!=NULL)
    {
        if (strcmp(arguments[increment],"<")==0)
        {
            redirection=arguments[increment+1];
            free(arguments[increment]);        
            while (arguments[increment+2]!=NULL)
            {
                arguments[increment]=arguments[increment+2];
                ++increment;
            }
            arguments[increment]=NULL;
        }
        ++increment;
    }
    return redirection;
}

char *scan_redirection_sortante(char *arguments[32])
{
char *redirection=NULL;
int  increment=0;

    while (arguments[increment]!=NULL)
    {
        if (strcmp(arguments[increment],">")==0)
        {
            redirection=malloc(strlen(arguments[increment+1])+1);
            redirection[0]='w';
            redirection[1]='\0';
            strcat(redirection,arguments[increment+1]);
            free(arguments[increment]);    
            free(arguments[increment+1]);    
            while (arguments[increment+2]!=NULL)
            {
                arguments[increment]=arguments[increment+2];
                ++increment;
            }
            arguments[increment]=NULL;
        }
        else if (strcmp(arguments[increment],">>")==0)
        {
            redirection=malloc(strlen(arguments[increment+1])+1);
            redirection[0]='a';
            redirection[1]='\0';
            strcat(redirection,arguments[increment+1]);
            free(arguments[increment]);    
            free(arguments[increment+1]);    
            while (arguments[increment+2]!=NULL)
            {
                arguments[increment]=arguments[increment+2];
                ++increment;
            }
            arguments[increment]=NULL;
        }
        ++increment;
    }
    return redirection;
}

void traitement_cmd(char *commande,char **argv)
{
char *cmd1,*cmd2;
char *fichier_redirection_sortante,*fichier_redirection_entrante;
char *fichier_redirection_sortante2,*fichier_redirection_entrante2;
int pipefd[2];

    cmd2=NULL;
    str_replace(commande,"\n","");
    str_replace(commande," <","<");
    str_replace(commande,"< ","<");
    str_replace(commande,"<"," < ");
    str_replace(commande," >",">");
    str_replace(commande,"> ",">");
    str_replace(commande,">"," > ");
    str_replace(commande," >  > "," >> ");
    str_replace(commande,"| ","|");
    str_replace(commande," |","|");
    str_replace(commande,"|"," | ");
    char *tmp=strstr(commande," | ");
    if (tmp!=NULL)
    {
        cmd1=strndup(commande,strlen(commande)-strlen(tmp));
        cmd2=strdup(tmp+3);
    }
    else
    {
        cmd1=strdup(commande);
    }
    creation_liste_arguments(arg_list,cmd1);
    gestion_variables(arg_list,argv);
    traitement_joker(arg_list);
    fichier_redirection_sortante=scan_redirection_sortante(arg_list);
    fichier_redirection_entrante=scan_redirection_entrante(arg_list);
    if (cmd2!=NULL)
    {
        creation_liste_arguments(arg_list2,cmd2);
        gestion_variables(arg_list2,argv);
        traitement_joker(arg_list2);
        fichier_redirection_sortante2=scan_redirection_sortante(arg_list2);
        fichier_redirection_entrante2=scan_redirection_entrante(arg_list2);
    }        
    pipe(pipefd);
    pid_t process=fork();
    if (process==0)
    {
        if (cmd2!=NULL) 
        {
            dup2(pipefd[1],STDOUT_FILENO);
        }
        if (fichier_redirection_entrante!=NULL)
        {
            int handler=(int)freopen(fichier_redirection_entrante,"r", stdin);
            if (handler==-1) 
            {
                fprintf(stderr,"%s\n",strerror(errno));
                exit(0);
            }
        }
        if (fichier_redirection_sortante!=NULL)
        {
            char* type_redirection=strndup(fichier_redirection_sortante,1);
            int handler=(int)freopen(fichier_redirection_sortante+1,type_redirection, stdout);
            if (handler==-1) 
            {
                fprintf(stderr,"%s\n",strerror(errno));
                exit(0);
            }
            free(type_redirection);
        }
        int retour=execvp(arg_list[0],arg_list);
        if (retour==-1) fprintf(stderr,"%s\n",strerror(errno));
        exit(0);
    }
    else
    {
        wait(&process);
    }
    if (cmd2!=NULL)
    {
        pid_t process2=fork();
        if (process2==0)
        {
            dup2(pipefd[0],STDIN_FILENO);
            if (fichier_redirection_entrante2!=NULL)
            {
                int handler=(int)freopen(fichier_redirection_entrante2,"r", stdin);
                if (handler==-1) 
                {
                    fprintf(stderr,"%s\n",strerror(errno));
                    exit(0);
                }
            }
            if (fichier_redirection_sortante2!=NULL)
            {
                char* type_redirection=strndup(fichier_redirection_sortante,1);
                int handler=(int)freopen(fichier_redirection_sortante2+1,type_redirection, stdout);
                if (handler==-1) 
                {
                    fprintf(stderr,"%s\n",strerror(errno));
                    exit(0);
                }
                free(type_redirection);
            }
            int retour=execvp(arg_list2[0],arg_list2);
            if (retour==-1) fprintf(stderr,"%s\n",strerror(errno));
            exit(0);
        }
        else
        {
            wait(&process2);
        }
    }
        
    if (cmd2!=NULL)
    {
        if (fichier_redirection_sortante2!=NULL) free(fichier_redirection_sortante2);
        if (fichier_redirection_entrante2!=NULL) free(fichier_redirection_entrante2);
        liberation_arguments(arg_list2);
        free(cmd2);
        cmd2=NULL;
    }
    if (fichier_redirection_entrante!=NULL) free(fichier_redirection_entrante);
    if (fichier_redirection_sortante!=NULL) free(fichier_redirection_sortante);
    liberation_arguments(arg_list);
    free(cmd1);
}

void traitement_espaces_debut(char *chaine_a_traiter)
{
    char *nouvelle_chaine=chaine_a_traiter;
    while (nouvelle_chaine[0]==' ')
    {
        ++nouvelle_chaine;
    }
    memmove(chaine_a_traiter,nouvelle_chaine,strlen(nouvelle_chaine)+1);
}

void traitement_espaces_fin(char *chaine_a_traiter)
{
    while (chaine_a_traiter[strlen(chaine_a_traiter)-1]==' ')
    {
        chaine_a_traiter[strlen(chaine_a_traiter)-1]='\0';
    }
}

void traitement_ligne(char **argv)
{
    traitement_espaces_debut(buffer);
    traitement_espaces_fin(buffer);
    if (buffer[0]=='#')
    {    
    }
    else if (strncmp(buffer,"if",2)==0)
    {
        char *lecture=buffer;
        int boucle=0;
        Environnement *var_if=malloc(sizeof(Environnement));
        var_if->nom=malloc(4);
        sprintf(var_if->nom,"%d",boucle);
        var_if->valeur=strdup(buffer);
        var_if->next=NULL;
        if (strncmp(buffer,"if [",4)!=0)
        {
            fprintf(stderr,"%s : Erreur de syntaxe\n",buffer);
            exit(EXIT_FAILURE);
        }
        char *test_fin=strstr(buffer,"]");
        if (strcmp(test_fin,"]")!=0 && strcmp(test_fin,"] ")!=0)
        {
            fprintf(stderr,"%s : Erreur de syntaxe\n",buffer);
            exit(EXIT_FAILURE);
        }
        char *formule=strdup(buffer);
        str_replace(formule,"if [","");
        str_replace(formule,"]","");
        str_replace(formule," ","");
        str_replace(formule,"\"","");    
        int retour=strcspn(formule,"=");
        if (retour==strlen(formule))
        {
            fprintf(stderr,"%s : Erreur de syntaxe\n",buffer);
            exit(EXIT_FAILURE);            
        }
        char *arg_list_temp[32];
        arg_list_temp[0]=strndup(formule,retour);
        arg_list_temp[1]=strdup(formule+retour+1);
        arg_list_temp[2]=NULL;
        gestion_variables(arg_list_temp,argv);
        int test=strcmp(arg_list_temp[0],arg_list_temp[1]);
        free(arg_list_temp[1]);
        free(arg_list_temp[0]);
        free(formule);
        Environnement *liste_var_if=var_if;
        Environnement *liste=liste_var_if;
        while (lecture!=NULL)
        {
            lecture=fgets(buffer,150,fichier);
            traitement_espaces_debut(buffer);
            if (lecture==NULL) break;
            if (buffer[strlen(buffer)-1]=='\n') buffer[strlen(buffer)-1]='\0';
            if (strncmp(buffer,"fi",2)==0)
            {
                ++boucle;
                var_if=malloc(sizeof(Environnement));
                var_if->nom=malloc(4);
                sprintf(var_if->nom,"%d",boucle);
                var_if->valeur=strdup(buffer);
                var_if->next=NULL;
                liste->next=var_if;
                liste=var_if;
                lecture=NULL;                
            }
            else
            {
                ++boucle;
                var_if=malloc(sizeof(Environnement));
                var_if->nom=malloc(4);
                sprintf(var_if->nom,"%d",boucle);
                var_if->valeur=strdup(buffer);
                var_if->next=NULL;
                liste->next=var_if;
                liste=var_if;
            }
        }
        if (strcmp(liste->valeur,"fi")!=0)
        {
            fprintf(stderr,"%s : Erreur de syntaxe\n",buffer);
            exit(EXIT_FAILURE);                        
        }
        liste=liste_var_if;
        liste=liste->next;
        str_replace(liste->valeur," ","");
        if (strcmp(liste->valeur,"then")!=0)
        {
            fprintf(stderr,"*%s* : Erreur de syntaxe\n",liste->valeur);
            exit(EXIT_FAILURE);                        
        }
        liste=liste_var_if;
        liste=liste->next;
        liste=liste->next;
        int detection_else=0;
        while (liste!=NULL)
        {
            if (strcmp(liste->valeur,"else")==0)
            {
                detection_else=1;
            }
            else if (strcmp(liste->valeur,"fi")==0)
            {
            }
            else if (liste->valeur[0]=='#')
            {
            }
            else
            {
                if (test==0)
                {
                    if (detection_else==0)
                    {
                        traitement_cmd(liste->valeur,argv);
                    }
                }
                else
                {
                    if (detection_else==1)
                    {
                        traitement_cmd(liste->valeur,argv);
                    }
                }
            }
            liste=liste->next;
        }        
    }
    else if (strncmp(buffer,"for",3)==0)
    {
        int boucle=0;
        Environnement *var_if=malloc(sizeof(Environnement));
        var_if->nom=malloc(4);
        sprintf(var_if->nom,"%d",boucle);
        var_if->valeur=strdup(buffer);
        var_if->next=NULL;
        ++boucle;
        Environnement *liste_var_if=var_if;
        Environnement *liste=liste_var_if;
        char *lecture=buffer;
        while (1)
        {
            lecture=fgets(buffer,150,fichier);
            if (lecture==NULL) break;
            if (buffer[strlen(buffer)-1]=='\n') buffer[strlen(buffer)-1]='\0';
            traitement_espaces_debut(buffer);
            traitement_espaces_fin(buffer);
            var_if=malloc(sizeof(Environnement));
            var_if->nom=malloc(4);
            sprintf(var_if->nom,"%d",boucle);
            var_if->valeur=strdup(buffer);
            var_if->next=NULL;
            liste->next=var_if;
            liste=var_if;    
            ++boucle;
            if (strcmp(var_if->valeur,"done")==0) break;
        }
        if (strcmp(liste->valeur,"done")!=0)
        {
            fprintf(stderr,"Erreur de syntaxe : fin de fichier prématurée\n");
            exit(EXIT_FAILURE);                                    
        }
        liste=liste_var_if;
        liste=liste->next;
        if (strcmp(liste->valeur,"do")!=0)
        {
            fprintf(stderr,"Erreur de syntaxe : \"do\" non trouvé dans structure for\n");
            exit(EXIT_FAILURE);                                    
        }    
        liste=liste_var_if;
        char *args_for[32];
        creation_liste_arguments(args_for,liste->valeur);            
        boucle=3;
        ajout_environnement(args_for[1],"0");
        ajout_environnement(args_for[1],"0");
        while (args_for[boucle]!=NULL)
        {
            ajout_environnement(args_for[1],args_for[boucle]);
            liste=liste_var_if;
            liste=liste->next;
            liste=liste->next;
            while (liste!=NULL)
            {
                if (strcmp(liste->valeur,"done")==0) break;
                traitement_cmd(liste->valeur,argv);
                liste=liste->next;
            }
            ++boucle;
        }        
    }
    else if (strcmp(buffer,"exit")==0)
    {
        exit(EXIT_SUCCESS);
    }
    else if (strcmp(buffer,"set")==0)
    {
        Environnement *liste=var_environnement;
        while (liste!=NULL)
        {
            printf("%s=%s\n",liste->nom,liste->valeur);
            liste=liste->next;
        }
    }
    else if (strncmp(buffer,"unset",5)==0)
    {
        char *nom_variable=strstr(buffer," ");
        unsetenv(nom_variable+1);
        Environnement *liste=var_environnement;
        Environnement *precedent=NULL;
        while (liste!=NULL)
        {
            if (strcmp(nom_variable+1,liste->nom)==0)
            {
                free(liste->valeur);
                free(liste->nom);
                precedent->next=liste->next;
                free(liste);
                liste=NULL;
            }
            precedent=liste;
            if (liste!=NULL) liste=liste->next;
        }
    }
    else if (strncmp(buffer,"export",6)==0)
    {
        char *nom_variable=strstr(buffer," ");
        if (nom_variable!=NULL)
        {
            char *valeur_variable=NULL;
            Environnement *liste=var_environnement;
            while (liste!=NULL)
            {
                if (strcmp(nom_variable+1,liste->nom)==0)
                {
                    valeur_variable=liste->valeur;
                    liste=NULL;
                    setenv(nom_variable+1,valeur_variable,1);
                }
                if (liste!=NULL) liste=liste->next;
            }
        }
    }
    else if (strncmp(buffer,"!",1)==0)
    {
        if (strcmp(buffer,"!")==0) {}
        else 
        {
            char *endptr=NULL;
            int entier=strtol(buffer+1,&endptr,10);
            if (entier==0)
            {
                fprintf(stderr,"%s : element non trouvé\n",buffer);
            }
            else
            {
                if (entier<0) entier=histocount+entier;
                if (entier>histocount-1)
                {
                    fprintf(stderr,"%s : element non trouvé\n",buffer);
                }
                else
                {
                    int pos=ftell(fichier_historique);
                    rewind(fichier_historique);
                    char buffer_tmp[250];
                    int boucle=0;
                    do 
                    {
                        fgets(buffer_tmp,150,fichier_historique);
                        ++boucle;
                    } while (boucle!=entier);
                    printf("%s\n",buffer_tmp);
                    pos=pos-strlen(buffer)-strlen("\n");
                    fseek(fichier_historique,pos,SEEK_SET);
                    fprintf(fichier_historique,"%s",buffer_tmp);
                    strcpy(buffer,buffer_tmp);
                    if (buffer[strlen(buffer)-1]=='\n') buffer[strlen(buffer)-1]='\0';
                    traitement_ligne(argv);
                }
            }
        }
    }
    else if (strcmp(buffer,"history")==0||strcmp(buffer,"history -c")==0)
    {
        if (strcmp(buffer,"history -c")==0)
        {
            fclose(fichier_historique);
            fichier_historique=fopen(".myshel_history","w+");
            histocount=1;
        }
        else if (strcmp(buffer,"history")==0)
        {
            rewind(fichier_historique);
            char buffer_tmp[150];
            histocount=1;
            char *retour_lecture=buffer_tmp;
            while (!feof(fichier_historique)||retour_lecture==NULL) 
            {
                char *retour_lecture=fgets(buffer_tmp,150,fichier_historique);
                if (retour_lecture!=NULL)
                {
                    printf("%4d %s",histocount,buffer_tmp);        
                    ++histocount;
                }
            } 
        }
    }
    else if (strncmp(buffer,"exit",4)==0)
    {
        exit(EXIT_SUCCESS);
    }
    else
    {
        char *cmd=strdup(buffer);
        char *tmp=strtok(cmd,";");
        while (tmp!=NULL)
        {
            char *valeur_var=strstr(tmp,"=");
            if (valeur_var!=NULL)
            {
                char *nom_var=strndup(tmp,strlen(tmp)-strlen(valeur_var));
                ajout_environnement(nom_var,valeur_var+1);    
                free(nom_var);
            }
            else traitement_cmd(tmp,argv);
            tmp=strtok(NULL,";");
        }
        free(cmd);
    }
}

int main(int argc,char *argv[],char *arge[])
{

    ajout_environnement("HISTSIZE","500");
    fichier_historique=fopen(".myshel_history","r+");
    if (fichier_historique==NULL) fichier_historique=fopen(".myshel_history","w+");
    histocount=1;
    char *retour_lecture=buffer;
    while (!feof(fichier_historique)||retour_lecture==NULL) 
    {
        char *retour_lecture=fgets(buffer,150,fichier_historique);
        if (retour_lecture!=NULL)
        {
            ++histocount;
        }
    }
    --histocount;
    if (histocount>TAILLE_HISTO)
    {
        int nbre_a_supprimer=histocount-500;
        int boucle;
        rewind(fichier_historique);
        for (boucle=0;boucle<nbre_a_supprimer;++boucle)
        {
            fgets(buffer,150,fichier_historique);
        }
        FILE *fichier_historique2=fopen(".myshel_history_temp","w+");
        histocount=1;
        while (!feof(fichier_historique)||retour_lecture==NULL) 
        {
            char *retour_lecture=fgets(buffer,150,fichier_historique);
            fprintf(fichier_historique2,"%s",buffer);
            if (retour_lecture!=NULL)
            {
                ++histocount;
            }
        }
        fclose(fichier_historique2);
        fclose(fichier_historique);
        remove(".myshel_history");
        rename(".myshel_history_temp",".myshel_history");
        fichier_historique=fopen(".myshel_history","r+");
        fseek(fichier_historique,0,SEEK_END);
    }
    global_argc=argc;
    int increment=0;
    while (arge[increment]!=NULL)
    {
        char *valeur=strstr(arge[increment],"=");
        char *nom=strndup(arge[increment],strlen(arge[increment])-strlen(valeur));
        ajout_environnement(nom,valeur+1);
        free(nom);
        ++increment;
    }
    if (argc>1)
    {
        fichier=fopen(argv[1],"r");
        if (fichier==NULL)
        {
            fprintf(stderr,"%s: %s\n",argv[1],strerror(errno));
            exit(EXIT_FAILURE);
        }
    }
    else 
    {
        fichier=stdin;
    }
    while(!feof(fichier))
    {
        if (argc==1) printf("Prompt : ");
        char *lecture=fgets(buffer,150,fichier);
        if (lecture==NULL)
        {
            exit(EXIT_SUCCESS);
        }
        if (argc==1) fprintf(fichier_historique,"%s",buffer);
        if (buffer[strlen(buffer)-1]=='\n') buffer[strlen(buffer)-1]='\0';
        traitement_ligne(argv);
    }
    return 0;
}

23. Historique - Rappel de la dernière commande

Pour rappeler la dernière commande, il suffit de taper sur la flèche haute du clavier, celle-ci va réafficher la dernière commande au niveau du prompt. Il est également possible de rappeler la dernière commande via « !! » (sur le même principe que !-1 - voir paragraphe précédent).

23-1. 1re approche : fonction getchar

J'ai commencé par tenter de remplacer la lecture sur la console avec gets par getchar, en faisant une boucle jusqu'à appui sur retour chariot et stockage de chaque caractère lu dans le buffer. En dehors de ne plus avoir de support de la touche backspace (qu'il me faudra gérer moi-même), je me suis retrouvé avec stockage de plusieurs caractères dans stdin lors de l'appui sur la touche flèche gauche.

Voici la chaîne obtenue lors de l'appui sur la touche flèche du haut durant l'appel getchar() : « ^[[A » Par ailleurs, pour quitter la fonction, il faut appuyer sur la touche retour chariot, la fonction ne s'arrêtant qu'après l'appui sur celle-ci.

Cette suite s'appelle un code CSI (pour Control Sequence Introducer - voir aussi le standard ECMA-48) plus connu sous le nom de séquence d'échappement. Une séquence d'échappement commence par le code ASCII 27 (qui correspond à la touche echap).

Vous connaissez ce principe avec les fonctions printf/scanf avec :

  • \t : tabulation ;
  • \n : retour chariot ;
  • \a : beep :
  • etc.

Ces séquences ne sont pas des codes CSI, mais leur fonctionnement est similaire.

Le principe des séquences d'échappement vient historiquement de la gestion des terminaux et télétypes qui étaient utilisés avant l'avènement de l'informatique moderne et des écrans (voir VT100). On ne travaillait pas directement sur l'ordinateur, mais sur un terminal raccordé en liaison série (pour Linux, on travaille toujours avec un terminal, celui-ci étant virtuel).

Les séquences d'échappement permettaient aussi de passer des commandes aux imprimantes. Telle séquence d'échappement permettait de passer en mode gras sur tel modèle d'imprimante, sous MS-DOS par exemple, il fallait régler ces séquences selon le modèle, ces réglages manuels faisant office de drivers et devant être gérés dans chaque logiciel. Ceci a été remplacé par l'avènement des langages dédiés aux imprimantes : PostScript et PCL et l'arrivée du système de pilotes sous Windows.

Il est à noter qu'un dialogue PCL commence par un code ASCII 27 (code d'échappement).

23-2. 2e approche : getch

Une fonction connue sous Windows pourrait répondre à mon besoin : la fonction getch. Cette fonction n'attend pas l'appui sur retour chariot, mais n'existe pas sous Linux.

23-3. 3e approche : ncurses

ncurses est une bibliothèque très connue. Vous la connaissez notamment par les menus d'installation depuis la console. Cette bibliothèque contient une fonction getch. Cette fonction ne fonctionne cependant qu'après appel de la fonction initscr, qu'il faut appeler avant usage de la bibliothèque, et qui initialise les variables internes ; efface l'écran (celui-ci sera restauré à la fin de l'utilisation de ncurses), ncurses travaillant dans un concept de fenêtre. Ceci ne me convenant pas, j'ai cherché une autre approche.

23-4. Ma propre fonction my_getch

Pour créer ma propre fonction getch, je me suis tourné vers termios, permettant de contrôler le comportement du terminal, notamment l'écho (affichage du caractère saisi) et le mode canonique (appui sur la touche retour chariot pour quitter la fonction).

Ma fonction my_getch commence par paramétrer le terminal. Pour ceci, je récupère les réglages actuels de celui-ci via tcgetattr dans la structure termios retournée par tcgetattr. Je modifie ensuite les flags ECHO et ICANON après les avoir sauvegardés en modifiant la structure récupérée et en l'appliquant avec la fonction tcsetattr. Je lis ensuite le prochain caractère via getchar. Vu la modification des réglages du terminal, le caractère ne s'affiche pas (c'est le comportement que je voulais). Ensuite si le caractère est différent de 27 (code d'échappement), je retourne sa valeur dans un int. Si le caractère est le code ASCII 27, je considère qu'il y a une séquence d'échappement, je lis le caractère suivant qui doit être « [ », si ce n'est pas le cas, la fonction s'arrête en retournant 0 après avoir remis le caractère dans le flux avec ungetc, 0 signifiant caractère invalide. Si le caractère est bien « [ », je lis tous les caractères suivants avec getchar et je les stocke dans une chaîne jusqu'à trouver un caractère avec code ASCII inférieur ou égal à 0x40 signifiant la fin de la séquence CSI. J'analyse ensuite cette chaîne. Si elle est égale à « [A », la touche flèche haute a été tapée, si elle est égale à « [21~ » c'est la touche F10 qui a été saisie. Je retourne alors les valeurs des touches trouvées dans le fichier termios.h. Si la séquence n'est pas reconnue, je retourne 0. À la fin de la fonction, je remets les réglages du terminal comme avant l'appel.

Attention, cette fonction n'est qu'une ébauche, elle ne gère que les touches flèche haute et F10 (en dehors des touches de l'alphabet ) et ne gère pas l'appui sur la touche echap.

23-4-1. Source test my_getch

 
Sélectionnez
#include <termios.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int my_getch()
{
struct     termios info;
char     sequence[10];
int        svg,pos,entier;

    tcgetattr(STDIN_FILENO,&info);
    svg=info.c_lflag;
    info.c_lflag = ~ECHO & ~ICANON; // ~ECHO=noecho ICANON=mode canonique (pas d'attente de entrée)
    tcsetattr(STDIN_FILENO, TCSANOW, &info);
    pos=0;
    entier=getchar();
    if (entier==27)
    {
        entier=getchar();
        sequence[pos]=(char)entier;
        ++pos;
        if (entier=='[')
        {
            do
            {
                entier=getchar();
                sequence[pos]=(char)entier;
                ++pos;
            } while (entier<=0x40);
            sequence[pos]='\0';
            if (strcmp(sequence,"[A")==0) entier=0403;             // fleche haute=27,[A 
            else if (strcmp(sequence,"[21~")==0) entier=0420;     // touche F10=27,[21~
            else entier=0;
        }
        else
        {
            ungetc(entier,stdin);
            entier=0;
        }
    }
    info.c_lflag = svg;
    tcsetattr(STDIN_FILENO, TCSANOW, &info);
    return entier;
}

int main()
{
    printf("test - Appuyer sur une touche : ");
    int retour=my_getch();
    printf("\nvaleur entier de retour : %d\n",retour);
    return 0;    
}

man termios pour plus d'info sur la personnalisation du terminal.

23-5. readline

Je me suis finalement tourné vers readline. La principale fonction de readline se comporte de la façon suivante :

 
Sélectionnez
#include <readline/readline.h>

char *ligne_saisie=readline(prompt);

Prompt correspond au prompt qui sera affiché avant lecture, si celui-ci est NULL, il n'y aura pas de prompt (donc rien d'affiché avant la saisie).

Une autre fonction intéressante est la gestion d'historique qui ne sera pas exploitée pour le moment, mon propre système étant opérationnel.

L'intérêt supplémentaire de readline est de pouvoir faire des hooks selon ce qui est saisi (ce qui va me servir notamment pour l'appui sur la flèche du haut). Il y a plusieurs fonctions de modifications de la chaîne saisie à la volée.

La documentation est disponible ici ou depuis le man.

Pour utiliser readline, il m'a fallu installer le paquet libreadlmine-dev via :

 
Sélectionnez
apt-get install readline-dev

J'ai dû également ajouter « -lreadline » à la chaîne de compilation de Geany.

Make permet de gérer les directives de compilation ainsi que la compilation de plusieurs fichiers C. Si un seul fichier C est modifié, seul celui-ci est recompilé, puis l'édition de liens est réeffectuée.

Mon code ne se composant pour le moment que d'un seul fichier source C, make n'a pas d'intérêt.

Un tutoriel sur make

Un autre sur CMake

J'aurais pu mettre mes fonctions dans des fichiers C à part (par thème par exemple : un fichier C pour les fonctions dédiées à readline, un fichier C pour les fonctions dédiées au traitement de la ligne saisie, etc.). Dans ce cas, l'usage de make aurait été indispensable. Il aurait aussi fallu créer les fichiers d'en-tête.h. Cette méthode permet d'avoir un fichier source moins gros, car éclaté en plusieurs parties et donc plus lisible, et dans le cas de gros projets de ne recompiler que le fichier C modifié.

Ci-dessous les fonctions de la bibliothèque readline qui vont être utilisées :

23-5-1. Fonction rl_bind_keyseq

 
Sélectionnez
int rl_bind_keyseq PARAMS((const char *, rl_command_func_t *));

Cette fonction permet de placer un hook lors de la saisie de la séquence contenue dans le premier paramètre et déclenche l'appel de la fonction passée en paramètre 2. Je l'utilise pour intercepter l'appui sur la touche flèche haute via sa chaîne CSI.

23-5-2. Fonction rl_replace_line

 
Sélectionnez
rl_replace_line PARAMS((const char *, int));

Cette fonction permet de remplacer le contenu de la chaîne de saisie de readline par la chaîne du premier paramètre. L'int du second paramètre permet gérer le clear_undo (pile readline pour gérer l'annulation) s'il est utilisé (voir la doc). Je l'utilise pour remplacer la chaîne en cours de saisie par la dernière commande saisie.

23-5-3. Fonction rl_end_of_line

 
Sélectionnez
int rl_end_of_line PARAMS((int, int));

Cette fonction permet de placer le curseur en fin de ligne de la saisie readline en cours.

Les fonctions de hook créées doivent retourner un int.

23-6. Application avec readline

Dans main, avant l'appel de la boucle while, j'exécute ceci :

 
Sélectionnez
rl_bind_keyseq("\e[A",touche_fleche_haute);

Ceci va me créer un hook déclenchant la fonction touche_fleche_haute quand la séquence \27[A (c'est-à-dire l'appui sur la touche flèche haute) est détectée dans la saisie.

Voici le contenu de cette fonction :

 
Sélectionnez
int touche_fleche_haute()
{
    rl_replace_line(last_command,0);
    rl_end_of_line(0,0);
    return 0;
}

rl_replace_line va remplacer le contenu de la chaîne readline par le contenu de ma variable last_command, last_command contenant une copie du dernier retour de readline, ce qui m'évite de devoir reparcourir l'historique.

J'utilise la fonction isatty pour fixer le prompt : si stdin est un tty (et qu'il n'y a pas de redirection) la chaîne prompt correspondra au prompt utilisé par readline. Sinon, le prompt sera à NULL et il n'y aura pas d'affichage de celui-ci (car si le prompt passé en paramètre à readline est NULL, pas d'affichage de celui-ci). Ceci me permet de ne pas avoir d'affichage de prompt en mode script.

23-6-1. isatty

Prototype :

 
Sélectionnez
#include <unistd.h>

int isatty(int fd);

Si le handle fd se réfère à un terminal, 1 est retourné. Pour plus d'informations : man isatty.

23-6-2. Simplification de code :

La partie de code suivante dans main a été retirée, car inutile.

 
Sélectionnez
      if (argc>1)
    {
        fichier=fopen(argv[1],"r");
        if (fichier==NULL)
        {
            fprintf(stderr,"%s: %s\n",argv[1],strerror(errno));
            exit(EXIT_FAILURE);
        }
    }
    else 
    {
        fichier=stdin;
      }

24. Autocomplétion

L'autocomplétion permet par exemple d'éviter de saisir complètement un nom de fichier. Il suffit pour cela de taper sur la touche tabulation dans l'interpréteur de commande. C'est le même principe dans un IDE. Exemple avec toto.txt : L'appui sur la touche « t » va afficher celui-ci sous réserve qu'il n'y ait qu'un fichier commençant par t. En cas de présence de plusieurs fichiers, il faut affiner en tapant la seconde lettre, puis éventuellement la troisième, etc.

Pour cela, j'utilise la fonction rl_bind_key incluse dans readline. rl_bind_key est l'équivalent de rl_bind_seq_key pour une seule touche et dans mon code me sert à lier « \t » (la touche tabulation) avec ma fonction touche_tab().

Dans ma fonction touche_tab(), j'utilise glob (vu iciPrise en compte caractère joker *). Si je trouve une seule concordance, je recopie le nom de fichier dans la chaîne readline, sinon rien ne se passe.

24-1. Source fonction touche_tab()

 
Sélectionnez
int touche_tab()
{
    char *buffer=strdup(rl_line_buffer);
    char *buffer_reallocation=realloc(buffer,strlen(buffer)+2);
    if (buffer_reallocation==NULL) return 0; else buffer=buffer_reallocation;
    char *tmp=strstr(buffer," ");
    if (tmp[0]==' ') ++tmp;
    strcat(tmp,"*");
    glob_t g;
    int retour_glob=glob(tmp,0,NULL,&g);
    if (retour_glob==0)
    {
        if (g.gl_pathc==1)
        {
            char *new_buffer=malloc(strlen(buffer)+strlen(g.gl_pathv[0])+2);
            new_buffer[0]='\0';
            strncat(new_buffer,buffer,strlen(buffer)-strlen(tmp));
            strcat(new_buffer,g.gl_pathv[0]);
            rl_replace_line(new_buffer,0);
            rl_end_of_line(0,0);
            free(new_buffer);
        }
    }
    globfree(&g);
    free(buffer);
    return 0;
}

25. Double tabulation et autocomplétion

Une double tabulation en autocomplétion permet d'afficher la liste des fichiers correspondant au motif de recherche correspondant à ce qui a été saisi.

J'ai pour cela créé une nouvelle fonction appelée double_touche_tab(), copie de touche_tab() que j'ai ensuite modifiée.

La modification se situe au niveau de la condition if. Dans touche_tab(), il y a deux tests avant exécution : le premier testant le retour sans erreur de glob, le second testant la présence d'un seul élément dans la recherche. Dans double_touche_tab, je teste le retour sans erreur du glob et que le nombre d'éléments trouvés par celui-ci soit supérieur ou égal à 1. Dans ce cas, j'appelle « ls » (via la fonction system) suivi du schéma de recherche, puis réaffiche la chaîne déjà saisie avec la fonction rl_on_new_line, celle-ci réaffichant le prompt avant le texte déjà saisi.

25-1. Source double_touche_tab()

 
Sélectionnez
int double_touche_tab()
{
    char *buffer=strdup(rl_line_buffer);
    char *buffer_reallocation=realloc(buffer,strlen(buffer)+2);
    if (buffer_reallocation==NULL) return 0; else buffer=buffer_reallocation;
    char *tmp=strstr(buffer," ");
    if (tmp==NULL) tmp=buffer;
    if (tmp[0]==' ') ++tmp;
    strcat(tmp,"*");
    glob_t g;
    int retour_glob=glob(tmp,0,NULL,&g);
    if (retour_glob==0 && g.gl_pathc>=1)
    {
        char *new_buffer=malloc(strlen(buffer)+3+2);
        new_buffer[0]='\0';
        strcat(new_buffer,"ls ");
        strcat(new_buffer,tmp);
        system(new_buffer);
        rl_on_new_line();
        free(new_buffer);
    }
    globfree(&g);
    free(buffer);
    return 0;
}

26. Correction problème readline avec lecture fichier en paramètre

Lors du lancement de l'interpréteur avec un nom de fichier en paramètre, readline attendait la saisie de quelque chose au clavier.

J'ai donc modifié le code.

Plutôt que travailler avec stdin, je travaille sur une variable « fichier », celle-ci étant égale à stdin, en cas de mode interactif, et au handle du fichier passé en paramètre (ouvert avec fopen sur le handle fichier).

Je lis les entrées avec une nouvelle fonction lecture(). Celle-ci utilise readline en mode interactif, et utilise fgets après un malloc en mode script.

 
Sélectionnez
char *lecture()
{
char *tmp=NULL;
char *lu=NULL;

    if (global_argc>1)
    {
        tmp=malloc(152);
        lu=fgets(tmp,150,fichier);
        if (lu==NULL) exit(EXIT_SUCCESS);
        if (tmp[strlen(tmp)-1]=='\n') tmp[strlen(tmp)-1]='\0';
    }
    else
    {
        tmp=readline("Prompt : ");
    }
    return tmp;
}

27. Gestion de l'historique par la bibliothèque readline

Vu l'utilisation de readline, je décide de remplacer mon code de gestion de l'historique par les fonctions de la bibliothèque readline.

Créer ma gestion d'historique a été un excellent exercice. Mais l'utilisation de readline m'a permis d'ajouter la complétion de façon simple et rapide.

Je commence avant tout par commenter tout le code concernant la gestion de l'historique après avoir lu la documentation de gestion de l'historique par readline.

Avant de pouvoir utiliser les fonctions history de readline, il faut utiliser la fonction using_history() de façon à initialiser correctement la bibliothèque.

J'utilise ensuite la fonction read_history() de façon à lire l'historique précédemment sauvegardé, en passant le nom du fichier d'historique en paramètre (si NULL est passé, le fichier ,~/.history est utilisé).

Si le fichier n'existe pas, l'historique ne pourra pas être lu et sauvegardé correctement. Je teste donc la présence de celui-ci en l'ouvrant. Si l'ouverture échoue, je crée le fichier avec fopen.

J'appelle ensuite stifle_history(int) pour limiter le nombre d'entrées.

L'historique est donc nettoyé au démarrage de mon shell.

Les ajouts à l'historique sont effectués via add_history() que je fais suivre par append_history() de façon à écrire tout de suite dans le fichier, en cas de plantage, cela limite les pertes.

J'ai aussi modifié la fonction de lecture flèche haute de façon à appeler la dernière entrée historique.

27-1. Extrait Source

Nouvelle boucle while de main :

 
Sélectionnez
while(1)
    {
        char *ligne_saisie=lecture();
        if (ligne_saisie!=NULL)
        {
            strcpy(buffer,ligne_saisie);
            free(ligne_saisie);
            if (isatty(fileno(fichier))) 
            {
                add_history(buffer);
                append_history(1,".myshel_history");
            }
            traitement_ligne(argv);    
        }
        else exit(0);
    }

On peut voir que l'historique n'est mis à jour que si l'exécution se fait depuis un terminal. L'appel en mode script générant une redirection de stdin, add_history et append_history ne sont pas appelés en mode script.

28. Correction de bogue

Le fait qu'il n'y ait qu'une correction ne signifie pas que le code est exempt de bogues.

28-1. Mauvaise gestion astérisque

Un appel à ls *.c va bien afficher les fichiers avec extension .c. Par contre l'appel à ls c* va se comporter comme un appel à ls *.

Je recherche logiquement la source du problème dans la fonction traitement_joker(). Le problème se situe au niveau de lafonction strsstr, celle-ci ne gérant pas ce qui est en amont. Pour pallier cela, j'utilise la fonction strcspn, me retournant la longueur de chaîne ne contenant pas la sous-chaîne passée en paramètre. De là je soustrais cette longueur au pointeur tmp.

Ancien code :

 
Sélectionnez
void traitement_joker(char *arguments[32])
{
char *arg_list_tmp[32];

    int increment=0;
    int increment_tmp=0;
    while (arguments[increment]!=NULL)
    {
        char *tmp=strstr(arguments[increment],"*");
        if (tmp!=NULL)
        {
            glob_t g;
            int retour_glob=glob(tmp,0,NULL,&g);
            if (retour_glob==0)
            {
                int boucle;
                for (boucle=0;boucle<g.gl_pathc;++boucle)
                {
                    arg_list_tmp[increment_tmp]=strdup(g.gl_pathv[boucle]);
                    ++increment_tmp;
                }
                free(arguments[increment]);
            }
            else
            {
                arg_list_tmp[increment_tmp]=arguments[increment];
                ++increment_tmp;
            }
            globfree(&g);
        }
        else
        {
            arg_list_tmp[increment_tmp]=arguments[increment];
            ++increment_tmp;
        }
        ++increment;
    }
    arg_list_tmp[increment_tmp]=NULL;
    increment=0;
    while (arg_list_tmp[increment]!=NULL)
    {
        arguments[increment]=arg_list_tmp[increment];
        ++increment;
    }
    arguments[increment]=NULL;
}

Nouveau code :

 
Sélectionnez
void traitement_joker(char *arguments[32])
{
char *arg_list_tmp[32];

    int increment=0;
    int increment_tmp=0;
    while (arguments[increment]!=NULL)
    {
        int longueur_sans_asterisque=strcspn(arguments[increment],"*");
        char *tmp=strstr(arguments[increment],"*");
        if (tmp!=NULL)
        {
            if (longueur_sans_asterisque!=0) tmp=tmp-longueur_sans_asterisque;
            glob_t g;
            int retour_glob=glob(tmp,0,NULL,&g);
            if (retour_glob==0)
            {
                int boucle;
                for (boucle=0;boucle<g.gl_pathc;++boucle)
                {
                    arg_list_tmp[increment_tmp]=strdup(g.gl_pathv[boucle]);
                    ++increment_tmp;
                }
                free(arguments[increment]);
            }
            else
            {
                arg_list_tmp[increment_tmp]=arguments[increment];
                ++increment_tmp;
            }
            globfree(&g);
        }
        else
        {
            arg_list_tmp[increment_tmp]=arguments[increment];
            ++increment_tmp;
        }
        ++increment;
    }
    arg_list_tmp[increment_tmp]=NULL;
    increment=0;
    while (arg_list_tmp[increment]!=NULL)
    {
        arguments[increment]=arg_list_tmp[increment];
        ++increment;
    }
    arguments[increment]=NULL;
}

29. Conclusion

Nous arrivons au terme de cette première partie de la création d'un minisystème. À ce stade nous avons un shell opérationnel, bien qu'incomplet par rapport à bash.

Des points sont à revoir dans ce code, notamment la gestion des pipes limités à deux commandes. Ce sera l'occasion de gérer les arguments non plus par un tableau limité à 32 valeurs, mais par liste chaînée.

La dernière version du code source est disponible ici.

29-1. Remerciements

Je remercie LittleWhite pour sa relecture technique et orthographique.

Je remercie Lolo78 pour sa relecture technique et orthographique.

Je remercie Claude Leloup pour sa relecture orthographique.