Mini shell

Le but de ce TP est de programmer un shell minimal en C.

Exercices

Exécution de programmes au premier plan

Notre shell sera une simple boucle d'interaction qui affiche une invite de commande >, lit une ligne entrée au clavier (fgets(3)), l'exécute, et recommence. Le shell quitte quand l'utilisateur tape Contrôle+D à la place d'une commande.

1) Dans un premier temps, on suppose que la commande est simplement un nom de programme à exécuter dans un processus séparé (fork(2) et execlp(2)). Le shell doit attendre que le programme se termine (waitpid(2)) avant de rendre la main à l'utilisateur.

Correction) minishell1.c.

2) On suppose maintenant que la ligne de commande peut contenir un nombre arbitraire d'arguments, séparés par un ou plusieurs espaces.

Afin de ne pas passer trop de temps sur le problème du découpage d'une ligne en mots, je vous propose une solution toute faite.

Correction) minishell2.c.

3) Modifiez votre shell pour que, si une commande est en cours d'exécution, un appui sur Contrôle+C interrompe la commande et redonne la main à l'utilisateur sans toutefois quitter le shell. On utilisera pour cela sigaction(2).

Correction) minishell3.c.

4) Ajoutez à votre shell la redirection de la sortie standard. Si le dernier argument commence par un caractère >, on suppose que le reste du mot est le nom du fichier où rediriger la sortie de la commande à exécuter (voir dup2(2)).

Que se passe-t-il si la commande écrit sur la sortie d'erreur?

Correction) minishell4.c.

Exécution de programmes en tâche de fond

On ajoute maintenant l'exécution de programmes en tâche de fond. Quand un tel programme est lancé, le shell n'attend pas qu'il termine mais redonne tout de suite la main à l'utilisateur. Attention toutefois, le shell devra tout de même détecter la terminaison (asynchrone) des programmes en tâche de fond afin de:

Il faudra pour cela gérer le signal SIGCHLD grâce à un handler (voir sigaction(2)). Notez vous pouvez ne recevoir qu'un seul signal SIGCHLD pour indiquer la terminaison de plusieurs fils. Attention également aux fonctions (e.g., fgets) qui peuvent être interrompues par l'exécution asynchrone du handler.

5) Modifiez votre shell pour que toutes les commandes soient lancées en tâche de fond. Que se passe-t-il si une commande en tâche de fond essaye de lire l'entrée standard (e.g., cat)?

Correction) minishell5.c.

6) Modifiez votre shell pour qu'il gère deux types de commandes: une commande terminée par & sera lancée en tâche de fond tandis qu'une commande non terminée par & sera lancée au premier plan. À un moment donné, il y a au plus une commande au premier plan et un nombre arbitraire de commandes en tâche de fond. (Attention aux interactions entre le handler du signal SIGCHLD et la commande wait(2)...)

Correction) minishell6.c.

Exercices complémentaires

On propose maintenant plusieurs extensions du mini shell. Celles-ci sont assez indépendantes. Vous pouvez en faire quelques unes (en TP si vous avez le temps, ou à la maison pour vous entraîner un peu) ou proposer les vôtres.

Variables d'environnement

On souhaite gérer les variables d'environnement.

Il s'agit, dans un premier temps, d'effectuer des substitutions. $TOTO et ${TOTO} doivent être remplacés par getenv("TOTO") dans la ligne de commande. (Attention à la différence entre $TOTOA et ${TOTO}A!)

Dans un deuxième temps, il faut permettre à l'utilisateur de modifier, voir supprimer ces variables (void setenv(3) et unsetenv(3)). Vous pouvez vérifiez que cela marche en lançant la commande set(p) depuis votre shell.

Répertoires home

On souhaite gérer la substitution des ~ en répertoire home.

Pour un ~ isolé, il suffit de regarder la valeur de la variable d'environnement HOME. Pour ~moi, c'est plus compliqué: il faut retrouver le répertoire home associé à l'utilisateur moi (avec getpwnam(3) par exemple).

Motifs dans les fichiers

On souhaite gérer les caractères spéciaux dans les noms de fichiers (*, ?, etc).

Il est bien sûr possible de parcourir à la main les répertoires pour trouver tous les fichiers correspondant à un motif donné. C'est long à programmer! On risque de plus d'interpréter les motifs d'une manière légèrement différente de celle du shell préféré de l'utilisateur, ce qui ne manquera pas de l'agacer.

Heureusement, il existe une fonction standard glob(3) pour faire ce travail à notre place! (glob(7) sous Linux.)

Édition conviviale

On souhaite rendre l'édition de la ligne de commande un peu plus conviviale: support des flèches pour la navigation et la correction, accès à l'historique des commandes, complétion automatique, etc.

C'est difficile à faire à la main! Heureusement, il existe la bibliothèque readline de GNU pour faire cela.

Une autre solution, beaucoup plus complexe à mettre en œvre, est d'utiliser une bibliothèque de programmation du terminal telle que ncurses ou S-Lang (qui fait beaucoup plus que cela). Elles permettent un contrôle fin de ce qui est affiché à l'écran tant en étant (relativement) haut niveau et indépendantes du type de terminal utilisé (XTerm, console virtuelles de Linux, VT100, etc.)

Enfin, si on souhaite se passer de bibliothèque et travailler au niveau le plus bas, il faut savoir qu'il existe des séquences magiques de caractères à écrire dans la sortie standard pour déplacer le curseur, changer la couleur du texte, etc. Certaines sont assez standard (contrôle+A, soit \001, revient au début de la ligne) tandis que d'autres dépendent du type de terminal (voir terminfo(5) et expérimenter avec infocmp xterm). Il faut également modifier les paramètres de l'entrée standard (avec tcsetattr(3)) pour éviter qu'un caractère tapé au clavier soit automatiquement affiché à l'écran (phénomène d'écho).


Antoine Miné