Chapitre 4 • arguments du main, read, write
| Champ | Valeur |
|---|---|
| Auteur·e | Élise |
| Édition | 2023-10-27 |
| Durée | 3 sessions |
| Taille des équipes | 1 personne |
| Rendu | via git, dépôt $YEAR_ep_c4, droits en lecture à delivery_collector |
Barème
| Critère | Points |
|---|---|
print_args |
3 |
has_opt |
3 |
write_hello |
3 |
file_contains |
4 |
file_contains2 |
4 |
has_opt_value |
4 |
| Total des critères | 20 |
| Malus par ligne en faute de norme | -0,5 |
| Malus Dépôt Sale | -5 |
Règlement
La réalisation de cet exercice est assujettie aux règles en vigueur dans l’école en matière de triche et de normalisation. Tricher vous expose à de graves sanctions.
Vous devez respecter les règles de normalisation suivantes : https://git.ecole-89.com/eriizu/coding_style/src/branch/main/norm.md
Le non-respect de la norme vous fera perdre une partie ou la totalité des points qui auraient pu être acquis, sur l’ensemble du rendu.
Instructions de rendu
Respectez à la lettre les noms de fichiers et leur emplacement dans le dépôt de rendu. S’il vous est demandé de rendre un fichier nommé hello.c sans qu’un nom de dossier soit précisé : rendez un fichier hello.c à la racine de votre dépôt.
La présence d’une étoile * signifie que le nom du fichier peut être n’importe lequel dès lors qu’il porte l’extension ou l’affixe (préfixe/suffixe) demandée. Elle signifie aussi qu’il est possible de rendre plusieurs fichiers.
La présence d’une double étoile ** signifie que les fichiers peuvent être placés n’importe où dans un dossier, y compris dans des sous-dossiers.
Pensez à faire des commits et des push fréquemment. Autrement nous ne pouvons pas vous aider à retrouver vos fichiers perdus.
Fonctions autorisées et interdites
Par défaut, toute fonction système (comme write) ou fonction des bibliothèques (comme printf ou puts) sont interdites.
À chaque exercice vous sera donné une liste de fonctions autorisées, le cas échéant.
main de test
Chaque fois que vous devez rendre une fonction, vous devez aussi rendre un fichier *.main.c. Il s’agit du fichier qui doit contenir votre main de test.
Vous devez écrire des tests pour voir si votre fonction à le bon comportement.
Correction du code source en C
Tout le code source sera compilé avec les drapeaux suivants :
-Wall -Wextra -Werror
Testez votre code avec ces drapeaux pour que le compilateur vous informe de tout ce qui peut poser problème avec votre programme avant de le rendre.
Pour tester qu’un fichier sans main compile, vous devez le compiler contre un second fichier qui lui contient un main.
gcc -Wall -Wextra -Werror example.c example.main.c
1. Arguments
1.a. Théorie : les arguments de la fonction main
Jusqu’à maintenant, nous avons écrit des fonctions main qui n’avaient pas de paramètres. En réalité, les fonctions main reçoivent bel et bien des paramètres, nous les avons juste ignorés. Les deux premiers paramètres de la fonction main sont :
int main(int argc, char **argv);
Vous les verrez parfois notés ac et av, mais ils peuvent techniquement s’appeler n’importe comment.
argc signifie argument count et argv signifie argument values. La valeur d’argc est très importante : elle nous permet de savoir combien de valeurs peuvent être lues en toute sécurité depuis argv. Si on atteint argc lorsqu’on itère sur argv : alors nous sommes trop loin et risquons un segmentation fault.
Chacun des arguments est un char * accessible en utilisant l’opérateur [] sur argv.
Le premier argument correspond systématiquement au nom de la commande qui a permis de lancer le programme. Essayez d’afficher sa valeur ainsi :
int main(int ac, char **av)
{
if (ac >= 1) {
puts(av[0]);
}
}
1.b. Exercice : print_args : afficher les arguments d’un programme
Fichier·s à rendre : print_args.c
Fonction·s autorisée·s : write
Écrivez un programme qui imprime tous les arguments du main.
1.c. Exercice : has_opt : avez vous vu cette option ?
Fichier·s à rendre : has_opt.c, has_opt.main.c
Fonction·s autorisée·s : aucune
Écrivez la fonction has_opt. Elle prend en paramètre argc et argv avec les mêmes types que la fonction main. Elle prend aussi un troisième paramètre de type char. Son type de renvoi est int.
Votre fonction doit pour chaque argument d’argv qui commence par un tiret, rechercher s’il contient le caractère passé en troisième paramètre. Si oui elle renvoie immédiatement 1. Autrement elle renvoie 0.
Considérant le main de test suivant :
int main(int ac, char **av)
{
if (has_opt(ac, av, 'f')) {
puts("opt f found");
} else {
puts("opt f not found");
}
return (0);
}
On aurait le comportement suivant :
[sh] $ gcc has_opt.main.c has_opt.c
[sh] $ ./a.out -f
opt f found
[sh] $ ./a.out f
opt f not found
[sh] $ ./a.out
opt f not found
[sh] $ ./a.out -xf
opt f found
[sh] $ ./a.out -abc -tfo patate.txt
opt f found
2. Fichiers et descripteurs
2.a. Théorie : ouvrir un fichier
Chemins
Ouvrir un fichier en C est une tâche assez simple. Dès lors que vous connaissez le chemin vers votre fichier, vous pouvez demander au système d’exploitation de vous l’ouvrir avec l’appel système open.
open prend le chemin vers le fichier en premier paramètre et des drapeaux (des options) en deuxième paramètre. Les drapeaux permettent de spécifier s’il on veut :
- ouvrir un fichier en lecture et/ou en écriture ; et
- s’il on veut créer le fichier.
Options
Selon le manuel d’open.2 :
The argument flags must include one of the following access modes :
O_RDONLY,O_WRONLY, orO_RDWR. These request opening the file read-only, write-only, or read/write, respectively.
Appel, valeur de retour, descripteur de fichier
Pour ouvrir un fichier en lecture seule, on ferait :
int fd = open("toto.txt", O_RDONLY);
La valeur de retour de la fonction open est :
- soit en cas d’erreur (le fichier demandé n’existe pas, la création n’est pas possible, etc.) ;
- soit un descripteur de fichier “file descriptor”, qui correspond à votre fichier ouvert.
Un descripteur de fichier est un nombre que vous donne le noyau (le kernel, le système d’exploitation) sur lequel il est possible de lire et d’écrire. Un FD peut correspondre à un fichier, une connexion réseau, un pipe, un terminal, etc. N’importe truc sur lequel on pourrait vouloir lire et écrire.
Chaque processus a sa propre liste de descripteurs de fichier.
Dans le cas d’un descripteur de ficher, quand on parle de lire et d’écrire, il s’agit d’utiliser :
- l’appel système
readet - l’appel système
write.
Que ce soit pour read ou write c’est en premier argument qu’il faut donner le descripteur de fichier sur lequel on veut lire ou écrire.
Jusqu’alors on mettait 1 en premier argument à write. Cela correspond à la sortie standard d’un programme. La sortie que l’on utilise pour afficher du texte dans le terminal. En mettant un descripteur de fichier renvoyé par open en premier argument à write, on peut écrire dans un fichier plutôt que sur le terminal.
Fermeture
Lorsqu’on en a fini avec un descripteur de fichier qu’open nous avait donné, il faut le fermer avec close.
2.b. Théorie : création de fichiers et permissions
Bien que les trois options que l’on vient de voir pour open soient mutuellement exclusives, elles peuvent tout de même être combinées à d’autres options en utilisant un | (qui fait office de OU binaire).
Une des options que l’on peut combiner avec O_WRONLY ou O_RDWR, c’est O_CREATE. Cette option permet de créer le fichier s’il n’existe pas.
Exemple :
int fd = open("tata.txt", O_WRONLY | O_CREATE, 0644);
Dans le cas où l’on souhaite créer le fichier s’il n’existe pas, il faut aussi préciser avec quelles permissions le créer. C’est à ça que correspond 0644. C’est un mode de fichier.
Le mode de fichier se découpe ainsi :
0en préfixe, permet d’annoncer que le nombre suivant est en octal (il est absolument nécessaire)- un chiffre de 0 à 7 permission accordée au propriétaire du fichier (vous) ;
- un chiffre de 0 à 7 permission accordée au groupe propriétaire du fichier (le votre “students”) ;
- un chiffre de 0 à 7 permission accordée aux autres utilisateurs.
Les valeurs possibles pour les chiffres de permission sont :
- 7 : on peut lire, écrire et exécuter ;
- 6 : on peut lire et écrire ;
- 5 : ont peut lire et exécuter ;
- 4: on peut lire ;
- 3 : on peut écrire et exécuter ;
- 2 : on peut écrire ;
- 1 : on peut exécuter ;
- 0 : on ne peut rien faire.
En général, par défaut on crée les fichiers avec le mode 0644 pour que :
- vous puissiez lire et écrire votre fichier ;
- votre groupe puisse lire le fichier (s’il a accès au dossier) ;
- les autres puissent lire le fichier (s’ils ont accès au dossier).
2.c. Théorie : écrire à la suite d’un fichier
Une autre option que l’on peut combiner à O_WRONLY et à O_RDWR c’est : O_APPEND. Elle permet d’écrire à la suite de ce que contient déjà le fichier.
int fd = open("test.txt", O_WRONLY | O_APPEND);
2.d. Exercice : write_hello: écrire dans un fichier
Fichier·s à rendre : write_hello.c
Fonction·s autorisée·s : open, close, write
Écrivez un programme qui prend en argument le nom d’un fichier.
Votre programme doit ouvrir le fichier, y écrire “hello” et un retour à la ligne puis le fermer.
Si le fichier n’existe pas : créez-le.
En cas d’erreur affichez ARGV_0: open error ou ARGV_0: write error en remplaçant ARGV_0 par le tout premier argument de votre programme.
[sh] $ gcc -Wall -Wextra write_hello.c -o write_hello
[sh] $ ./write_hello hello.txt
[sh] $ cat hello.txt
hello
[sh] $ chmod 444 hello.txt
[sh] $ ./write_hello hello.txt
./write_hello: open error
2.e. Théorie : lire sur un descripteur de fichier
read est un appel système très similaire à write. Il a la même signature (mêmes paramètres, même valeur de retour), mais a un rôle différent. Au lieu d’écrire sur un descripteur de fichier, read permet de lire depuis un descripteur de fichier (donc depuis un terminal, un fichier, une connexion, etc.).
Paramètres de read :
fdle descripteur de fichier depuis lequel il faut lire ;bufferla zone mémoire dans laquelle enregistrer ce qui a été lu ;nla quantité d’octets à lire au maximum (pour ne pas dépasser la taille debufferet risquer un plantage.)
Exemple de lecture et de restitution de 10 octets :
int fd;
char *buffer;
int size_read;
fd = open("tata.txt", O_RDONLY);
if (fd == -1) {
return;
}
buffer = malloc(sizeof (char) * 10);
if (!buffer) {
close(fd);
return;
}
size_read = read(fd, buffer, 10);
if (size_read == -1) {
free(buffer);
close(fd);
return;
}
write(1, buffer, size_read);
free(buffer);
close(fd);
Si vous voulez vous servir de buffer comme d’une chaîne de caractères, vous pouvez prévoir un octet de battant et placer le terminateur nul juste derrière ce qui a été lu.
int fd;
char *buffer;
int size_read;
fd = open("tata.txt", O_RDONLY);
if (fd == -1) {
return;
}
// Notez l'octet alloué en plus ...
buffer = malloc(sizeof (char) * 11);
if (!buffer) {
close(fd);
return;
}
// ... et le maxmimum à lire inchangé.
size_read = read(fd, buffer, 10);
if (size_read == -1) {
free(buffer);
close(fd);
return;
}
// on place le terminateur nul après ce qui a été lu
buffer[size_read] = '\0';
// on a le droit de puts buffer maintenant que c'est une chaîne valide
puts(buffer);
free(buffer);
close(fd);
2.f. Exercice : file_contains : vérifier le contenu d’un petit fichier
Fichier·s à rendre : file_contains.c
Fonction·s autorisée·s : open, close, read, write, malloc, free
Écrivez un programme qui prend en argument un nom de fichier et une chaîne de caractère arbitraire. Le programme doit afficher match found s’il trouve la chaîne arbitraire dans le fichier. Si il ne la trouve pas, il doit afficher not found. Dans les deux cas le programme doit terminer sa sortie d’un retour à la ligne.
Votre programme doit fonctionner sur des fichiers de 99 octets (caractères) ou moins.
2.g. Exercice : file_contains2 : vérifier le contenu d’un fichier
Fichier·s à rendre : file_contains2.c
Fonction·s autorisée·s : open, close, read, write, malloc, free
Écrivez un programme qui prend en argument un nom de fichier et une chaîne de caractère arbitraire. Le programme doit afficher match found s’il trouve la chaîne arbitraire dans le fichier. Si il ne la trouve pas, il doit afficher not found. Dans les deux cas le programme doit terminer sa sortie d’un retour à la ligne.
Votre programme doit fonctionner sur des fichiers de n’importe quelle taille.
3. Exercice : has_opt_value : valeur d’une option
Fichier·s à rendre : has_opt_value.c, has_opt_value.main.c
Fonction·s autorisée·s : aucune
Écrivez la fonction has_opt_value. Elle prend en paramètre argc, argv et un caractère représentant l’option à trouver.
Elle renvoie un const char *.
Votre objectif est de faire renvoyer à la fonction la valeur associée à une option de votre programme. À défaut de trouver l’option, ou une valeur, vous devez renvoyer NULL.
Exemple de main :
int main(int ac, char **av)
{
const char *opt_val;
opt_val = has_opt_value(ac, av, 'f');
if (opt_val) {
printf("found opt and value %s\n", opt_val);
} else {
puts("opt f not found or no value associated");
}
return (0);
}
[sh] $ gcc has_opt_value.c has_opt_value.main.c -Wall -Wextra
[sh] $ ./a.out -f hello.txt
found opt and value hello.txt
[sh] $ ./a.out -f
opt f not found or no value associated
[sh] $ ./a.out -abcdef hello.txt
found opt and value hello.txt
[sh] $ ./a.out -abfcde hello.txt
opt f not found or no value associated
[sh] $ ./a.out -abcf hello.txt -de
found opt and value hello.txt
[sh] $ ./a.out -abc -def hello.txt
found opt and value hello.txt