Up Next

1  Quelques commandes Unix

Il est proposé de réimplanter quelques commandes Unix courantes. Ces quelques exercices illustrent l’utilisation des primitives POSIX relatives au système de fichiers : autorisations d’accès, parcours d’un répertoire, parcours d’un système de fichiers, entrées/sorties.

Vérifier les droits d’accès... et expliquer

La commande Unix access vérifie les droits d’accès (lecture : option -r, écriture : option -w, exécution : option -x) à un fichier donné. L’invocation de la commande comporte une option obligatoire spécifiant le ou les accès à vérifier. (Cette commande est malheureusement en voie de disparition...)

La commande access n’affiche rien mais retourne son résultat sous la forme d’une terminaison sur un succès ou un échec. Ce comportement peut par exemple être mis en valeur dans une session telle la suivante (Il s’agit d’une session C-shell. Pour un shell de la famille de sh tel bash, on utilisera $? en remplacement de $status) :

% access -r /tmp
% echo $status 
0
% access -r /tmp && echo OK
OK
% access -w /etc/passwd
% echo $status 
1
% access -w /etc/passwd && echo OK
% 

Dans le cadre de ces exercices, il s’agit de fournir notre propre version de la commande access qui supporte l’option supplémentaire -v (verbose) expliquant, en cas d’échec, ce pourquoi l’accès est impossible. Les raisons possibles de l’échec sont les suivantes :

  1. Le droit d’accès demandé au fichier n’est pas positionné.
  2. Le fichier n’existe pas.
  3. Une des composantes du nom de fichier n’est pas un répertoire.
  4. Une des composantes du nom du fichier est trop longue.
  5. Le nom du fichier est trop long.
  6. Le nom du fichier comporte trop de liens symboliques.
  7. Autre erreur.

Contraintes POSIX

La norme POSIX fournit un certain nombres de contraintes de taille ou longueur diverses. Ces contraintes définissent des valeurs minimales que doit garantir toute implémentation de la norme. La norme POSIX impose aussi que les valeurs effectivement garanties par l’implémentation soient accessibles aux applications.

Parmi les macro-définitions fournies par les fichiers <limits.h> et <unistd.h> on trouve

NAME_MAX
qui est la longueur maximale d’un nom d’entrée dans le système de fichiers (dont la valeur minimale requise est 14) ;
PATH_MAX
qui est la longueur maximale d’un chemin dans le système de fichiers (dont la valeur minimale requise est 255).

Code source POSIX

Afin de spécifier au compilateur que le code source C que l’on désire compiler est conforme à la norme POSIX, on définira la macro _XOPEN_SOURCE avec une valeur supérieure ou égale à 500.

Un Makefile comportera donc par exemple :

CC      = gcc                              
CFLAGS  = -Wall -Werror -ansi -pedantic    
CFLAGS += -D_XOPEN_SOURCE=500              
CFLAGS += -g

Les curieux pourront consulter le contenu du fichier /usr/include/features.h !


Exercice 1
 (prlimit, information de configuration)   Fournissez une commande prlimit qui affiche les valeurs des constantes NAME_MAX et PATH_MAX sur votre système, voir l’encart.

Exercice 2
 (maccess, ma commande access)   Fournissez une commande maccess qui ajoute l’option -v à la commande access standard qui fournit un des sept motifs d’erreur possibles.

Les permissions d’accès à un fichier peuvent être vérifiées par l’utilisation de l’appel système access(). La page de manuel :

% man 2 access

pourra être consultée.

Copie d’une session

La commande script permet de fournir dans un fichier une copie d’une session shell. Exemple :

% pwd
/users/phm/ens/pds/src
% ls
CVS/     atexit.c  echouser.c  mecho*         mecho.c~    mprintenv.c~
Makefile atexit.c~ maccess*    mecho.c        mprintenv*
atexit*  echouser* maccess.c   mecho.c.~1.1.  mprintenv.c
% script session.txt
Script started, output file is session.txt
% pwd
/users/phm/ens/pds/src
% ls
CVS/     atexit.c  echouser.c  mecho*         mecho.c~    mprintenv.c~
Makefile atexit.c~ maccess     mecho.c        mprintenv*  session.txt
atexit*  echouser* maccess.c   mecho.c.~1.1.~ mprintenv.c
% exit
Script done, output file is session.txt
% ls
CVS/     atexit.c  echouser.c  mecho*         mecho.c~    mprintenv.c~
Makefile atexit.c~ maccess     mecho.c        mprintenv*  session.txt
atexit*  echouser* maccess.c   mecho.c.~1.1.~ mprintenv.c
% cat session.txt
Script started on Sun Dec 19 05:52:24 2004
% pwd
/users/phm/ens/pds/src
% ls
CVS/     atexit.c  echouser.c  mecho*         mecho.c~    mprintenv.c~
Makefile atexit.c~ maccess     mecho.c        mprintenv*  session.txt
atexit*  echouser* maccess.c   mecho.c.~1.1.~ mprintenv.c
% exit
Script done on Sun Dec 19 05:52:30 2004
%  


Exercice 3
 (Trouver les 7 erreurs)   Fournissez un exemple de session shell comportant une suite d’invocations de la commande maccess produisant l’ensemble des sept erreurs possibles. Cette session inclura les commandes de création des fichiers sur lesquels la commande maccess est invoquée.

Voir l’encart sur la manière de réaliser une copie d’une session.

Retrouver une commande

La commande Unix which recherche les commandes dans les chemins de recherche de l’utilisateur.

La variable d’environnement $PATH contient une liste de répertoires séparés par des « : ». Lors de l’invocation d’une commande depuis le shell, un fichier exécutable du nom de la commande est recherchée dans l’ordre dans ces répertoires. C’est ce fichier qui est invoqué pour l’exécution de la commande. Si aucun fichier ne peut être trouvé, le shell le signale par un message.

La commande which affiche le chemin du fichier qui serait trouvé par le shell lors de l’invocation de la commande :

% echo $PATH 
/bin:/usr/local/bin:/usr/X11R6/bin:/users/phm/bin
% which ls 
/bin/ls
% echo $status
0
% which foo 
foo: Command not found.
% echo $status
1

Exercice 4
 (Liste des répertoires de recherche)   Dans un premier temps, proposez une fonction filldirs() qui remplit un tableau dirs contenant la liste des noms des répertoires de $PATH. Ce tableau sera terminé par un pointeur NULL.

Exercice 5
 (Une fonction which())   Écrivez maintenant une fonction which() qui affiche le chemin absolu correspondant à la commande dont le nom est passé en paramètre ou un message d’erreur si la commande ne peut être trouvée dans les répertoires de recherche de $PATH. Cette fonction retourne une valeur booléenne indiquant un succès ou un échec de la recherche.

On pourra utiliser l’appel système

#include <unistd.h>
int access(const char *path, int mode);

qui vérifie les permissions fournies sous la forme d’un « ou » des valeurs R_OK (read, lecture), W_OK (write, écriture), X_OK (execution, exécution), F_OK (existence).


Exercice 6
 (La commande which)   Terminez par écrire votre propre version de la commande which qui se termine par un succès si et seulement si toutes les commandes données en paramètre ont été trouvées dans les répertoires de recherche désignés par $PATH.

Afficher le répertoire courant

La commande pwd (print working directory) affiche le chemin absolu du répertoire de travail courant.

La fonction

char *getcwd(char *buf, size_t size);

de la bibliothèque C copie le chemin absolu du répertoire courant dans le tableau buf de taille size. Si buf est NULL, la bibliothèque alloue dynamiquement un espace nécessaire pour y stocker le résultat et en retourne alors l’adresse.


Exercice 7
 (Utilisation de getcwd())   Il est immédiat de fournir une implantation de la commande pwd basée sur une utilisation de getcwd().

L’implantation de cette fonction getcwd() ne repose pas sur un appel système unique mais exploite la structuration des répertoires et les liens . et .. pour construire ce chemin absolu.

Pour un processus donné, le système ne conserve que le numéro d’inœud courant, à partir duquel il lui est possible d’accéder au répertoire courant. La navigation à partir de ce répertoire est utilisé pour tous les chemins relatifs, en particulier ceux vers des répertoires parents, tels .. ou ../.., etc.

Le système conserve aussi l’inœud du répertoire racine / qui lui permet d’accéder à tous les chemins absolus.

Il s’agit maintenant de proposer une implantation de la commande pwd ne reposant pas sur la bibliothèque C. Le principe en est le suivant :

Informations sur une entrée

Deux valeurs peuvent être utilisées pour rechercher des informations sur une entrée dans un système de fichiers :

Ces structures comportent les champs suivants :

struct dirent { 
    ino_t     d_ino;      /* File number of entry */ 
    char      d_name[];   /* Name of entry */

struct stat {
    dev_t     st_dev;     /* Device ID of device containing file */
    ino_t     st_ino;     /* File serial number */
    mode_t    st_mode;    /* Mode of file */
    nlink_t   st_nlink;   /* Number of hard links to the file */
    uid_t     st_uid;     /* User ID of file */ 
    gid_t     st_gid;     /* Group ID of file */
    dev_t     st_rdev;    /* Device ID (if file is character or block
                             special) */
    off_t     st_size;    /* For regular files, the file size in bytes.
                             For symbolic links, the length in bytes of
                             the pathname contained in the symbolic 
                             link */
    time_t    st_atime;   /* Time of last access */
    time_t    st_mtime;   /* Time of last data modification */
    time_t    st_ctime    /* Time of last status change */
    blksize_t st_blksize; /* A file system-specific preferred I/O block 
                             size for this file */
    blkcnt_t  st_blocks;  /* Number of blocks allocated for this file */

Notez qu’il s’agit là des champs définis par la norme POSIX, une implantation particulière peut fournir des informations supplémentaires que l’on évitera d’utiliser.

Les macros suivantes peuvent être utilisées sur le champ st_mode :

S_ISREG(m)
Test for a regular file.
S_ISDIR(m)
Test for a directory.
S_ISLNK(m)
Test for a symbolic link.
S_ISFIFO(m)
Test for a pipe or FIFO special file.
S_ISSOCK(m)
Test for a socket.
S_ISBLK(m)
Test for a block special file.
S_ISCHR(m)
Test for a character special file.


Exercice 8
 (Racine du système de fichiers)   Donnez le code d’une fonction qui détermine si un chemin donné en paramètre correspond à la racine du système de fichiers.

Exercice 9
 (Mon nom dans le répertoire parent)   Fournissez une fonction print_name_in_parent() qui affiche sur la sortie standard le nom du lien d’un nœud donné dans son répertoire père.

Exercice 10
 (Construction du chemin absolu)   L’affichage du chemin absolu d’un répertoire pouvant être produit par l’affichage du chemin absolu du répertoire père suivit du nom du répertoire courant dans le répertoire père, proposez une définition de la fonction récursive print_node_dirname() qui affiche le chemin absolu du répertoire passé en paramètre.

Exercice 11
 (Fonction pwd())   Terminez par une fonction pwd() qui affiche le chemin absolu du répertoire courant.

Parcours d’une hiérarchie

La commande du (disk usage) rapporte la taille disque utilisée par un répertoire et l’ensemble de ses fichiers (y compris ses sous-répertoires).

Deux tailles peuvent être prises en compte pour un fichier :

Les champs st_size et st_blocks d’une structure de type struct stat retournée par l’appel système stat() contiennent respectivement la taille apparente et la taille réelle d’un fichier ; se référer à l’encart.

Comme pour toutes les commandes réalisant un parcours d’une hiérarchie, il faut préciser le traitement réalisé sur les liens rencontrés : doivent-ils être suivis ou non ?

La commande du comporte donc deux options

-L
indiquant de suivre les liens symboliques ; ce qui n’est pas le cas par défaut ;
-b
indiquant de rapporter les tailles apparentes ; ce sont les tailles réelles qui sont rapportées sinon.

Une possible implantation peut définir deux variables globales pour mémoriser l’état de ces options :

static int opt_follow_links = 0; 
static int opt_apparent_size = 0; 

Dans un premier temps on suppose que l’option opt_follow_links est définie à faux.


Exercice 12
 (Filtrer les entrées)   Proposez une implantation d’une fonction
int valid_name(const char *name);
qui indique, si le chemin name doit être considéré par la commande du ou non. Afin d’assurer la terminaison de l’exécution, les entrées de nom . et .. ne seront pas considérées.

Exercice 13
 (Taille d’un fichier)   Proposez une implantation d’une fonction récursive
int du_file(const char *pathname);
qui retourne la taille occupée par le fichier désigné et ses éventuels sous-répertoires.

Comme dans un premier temps on ne suit pas les liens symboliques, la taille d’un lien symbolique est la taille occupée par le lien, et non par le fichier visé.


Exercice 14
 (Comptage multiple ?)   Un nœud qui serait référencé plusieurs fois dans une hiérarchie dont on veut afficher la taille serait comptabilisé plusieurs fois. En quoi est-ce gênant. Comment s’en affranchir ?

Exercice 15
 (Suivre les liens symboliques)   Comment modifier notre implantation actuelle pour suivre les liens symboliques.

Afficher la fin d’un fichier

La commande Unix tail affiche les dernières lignes des fichiers désignés par les paramètres de la ligne de commande. L’option -n indique d’afficher les n dernières lignes. Par défaut, les 10 dernières lignes sont produites.


Exercice 16
 (Version simpliste de tail)   Une version simpliste de la commande détermine le nombre de lignes du fichier puis parcourt le fichier pour débuter l’affichage n lignes avant la fin.

Malgré l’inconvénient majeur de cette approche, proposez une telle implantation.


Exercice 17
 (Version utile de tail)   Une première version utile de la commande lit le fichier depuis le début en conservant dans un tampon circulaire les n dernières lignes lues. Ce tampon est affiché quand la fin du fichier est atteinte.

Pourquoi une telle implantation peut-elle être qualifiée d’utile ?

Une solution plus efficace consiste à lire le fichier depuis la fin. On lit un tampon de caractères d’une taille arbitraire (mais judicieusement choisie !). On y compte le nombre de retours à la ligne. S’il est supérieur à n, on peut afficher les n dernières lignes. Sinon, on lit le tampon précédent...


Exercice 18
 (Fin d’un tampon)   Écrivez une fonction
int index_tail_buffer(const char *buffer, int bufsize, 
                      int ntail, int *nlines);
qui retourne l’index du début des ntail dernières lignes. Cet index est relatif au tampon de taille bufsize. Si le tampon comporte moins de ntail lignes, une valeur négative est retournée et nlines est positionné avec le nombre de lignes du buffer.

Exercice 19
 (Fonction tail() relative)   Écrivez une fonction récursive
tail_before_pos(int fd, unsigned int pos, int ntail);
qui considère le contenu du fichier référencé par le descripteur fd jusqu’à sa position pos. Cette position pos est comprise comme un déplacement depuis la fin du fichier. La fonction récursive tail_before_pos() affiche les ntail dernières lignes du fichier.

Exercice 20
 (Version efficace de tail)   Écrivez enfin une fonction
tail(const char *path, int ntail);
qui affiche les ntail dernières lignes du fichier désigné.

Up Next