Introduction

Cet e-bouquin a pour objectif de préparer les étudiants à l'épreuve d'informatique communément appelée "Piscine", qu'importe l'école.

Il utilise des connaissances également présentes sur cette chaine Youtube

Je pars du fait que je peux te tutoyer et que ton épreuve demande d'apprendre le langage C sur un système UNIX (genre MacOS) ou UNIX-like (genre GNU/Linux).

Pour être sûr de n'oublier personne, je repars de zéro ici. J'assume que tu ne sais rien. Ne t'étonne pas si j'ai l'air de raconter des banalités, tout le monde n'a pas tes connaissances. J'ai l'habitude de prendre mes étudiants pour des cons. Ca fonctionne plutôt bien quant à leur apprentissage.

Le but de l'e-bouquin n'est pas te de rendre bon sur le champs, mais de t'aider à appréhender les notions difficiles qui t'attendent et d'avoir des outils pour les travailler. Tu gagneras en compétences dans le temps. Cependant, je présente un nombre de notions assez important.

Je ne peux pas présenter tous les exercices des piscines ; ce serait de la triche. Mais je peux présenter les exercices communs à chaque école, des tous petits exercices qui sont des classiques.

Je présente également des outils que j'utilise au quotidien (awesomewm, vim, ...), qui ne sont pas toujours présentés dans les piscines. Ca te servira à avoir des références en plus pour travailler efficacement.

Tu sortiras de ce cours avec plus qu'il n'en faut pour attaquer la Piscine d'EPITA, Epitech, 42, ou une autre école qui a également repris le concept. Pour l'instant, seule l'école 2600 échappe à l'objectif de ce document, car elle a de l'assembleur dans son bootcamp d'entrée que je ne présente pas encore.

J'essaie d'être le plus simple et pratique possible. Pour les professionnels du métier, il y a des parties laissées de côté pour ne pas alourdir la compréhension des notions aux nouveaux étudiants. Je préfère mettre en avant l'exemple et la compréhension par la pratique avec des petits codes de test plutôt que des longs chapitres magistraux. Il manque également des parties pour compléter des notions, mais ce sont des notions que les étudiants aborderont pendant leur Piscine, elles ne sont pas réellement manquantes pour un Pédiluve.

Vous n'avez pas les bases

\[ chiffre * base ^{indice} \]

Avant d'attaquer la pratique, il faut revoir quelques préliminaires mathématiques.

En informatique, plusieurs bases sont utilisées pour travailler avec des données. Les bases 2, 16 et 8. Cette dernière est plus rarement utilisée et ne sera pas abordée dans ce cours. Mais avec les connaissances que tu auras en travaillant les bases 2 et 16, tu sauras travailler avec la base 8 simplement.

Avant de travailler les autres bases, tu vas revoir la base 10 que tu utilises tous les jours.

La base 10 possède 10 chiffres : 0, 1, 2, 3, 4, 5, 6, 7, 8 et 9.

La base 10 utilise les puissances de 10. Prends ce tableau en référence :

Indice43210
Notation\[ 10^4\]\[ 10^3\]\[ 10^2\]\[ 10^1\]\[ 10^0\]
Valeur100001000100101

Tu peux voir que \[ 10^3 = 1000\]

La base 10 ne fait que se multiplier par 10 pour passer à l'indice suivant.

Ce tableau est à utiliser pour décomposer n'importe quel nombre.

Prenons un nombre pris au hasard : 4269.

Pour décomposer ton nombre tu vas d'abord reporter le nombre sous le tableau en partant de la droite, ce qui donne :

Indice43210
Notation\[ 10^4\]\[ 10^3\]\[ 10^2\]\[ 10^1\]\[ 10^0\]
Valeur100001000100101
Nombre4269

Tu peux voir que 4 a comme indice 3, que 2 a comme indice 2, que 6 a comme indice 1 et que 9 a comme indice 0.

Décompose le nombre ainsi : \[ 4 * 10^3 + 2 * 10^2 + 6 * 10^1 + 9 * 10^0 \]

Calcule les puissances : \[ 4 * 1000 + 2 * 100 + 6 * 10 + 9 * 1 \]

Calcule les multiplications : \[ 4000 + 200 + 60 + 9 \]

Tu as décomposé ton nombre en utilisant la base 10.

Pourquoi tous ces efforts ?

Pour te préparer aux bases 2 et 16.

Franklin savait compter deux par deux

De la base 10 à la base 2

Cours vidéo

Maintenant que tu as compris comment décomposer un nombre en base 10, tu vas apprendre à transformer un nombre écrit en base 10 en base 2 et inversement.

Pour rappel, la base 2 a deux chiffres : 0 et 1.

Prends ce tableau en référence

Indice109876543210
Notation\[ 2^{10} \]\[ 2^9\]\[ 2^8\]\[ 2^7\]\[ 2^6\]\[ 2^5\]\[ 2^4\]\[ 2^3\]\[ 2^2\]\[ 2^1\]\[ 2^0\]
Valeur10245122561286432168421

La base 2 ne fait que se multiplier par 2 à chaque nouvel indice.

Le tableau va jusqu'à 1024 car c'est le strict minimum à connaître par coeur.

Prends un nombre, complètement au hasard : 651.

Tu vas le soustraire par la puissance de 2 inférieure ou égale la plus proche.

Reprends le tableau. La puissance de 2 inférieure ou égale la plus proche de 651 est... 512.

\[ 651 - 512 = 139\]

Tu as 1 fois 512 dans ton nombre de départ, reporte le chiffre 1 sous 512.

Indice109876543210
Notation\[ 2^{10} \]\[ 2^9\]\[ 2^8\]\[ 2^7\]\[ 2^6\]\[ 2^5\]\[ 2^4\]\[ 2^3\]\[ 2^2\]\[ 2^1\]\[ 2^0\]
Valeur10245122561286432168421
Nombre1

Recommence avec le nouveau nombre qui est 139. La puissance de 2 inférieure ou égale la plus proche de 139 est... 128. Tu as 1 fois 128 dans ton nouveau nombre, reporte le chiffre 1 sous 128.

\[ 139 - 128 = 11 \]

Indice109876543210
Notation\[ 2^{10} \]\[ 2^9\]\[ 2^8\]\[ 2^7\]\[ 2^6\]\[ 2^5\]\[ 2^4\]\[ 2^3\]\[ 2^2\]\[ 2^1\]\[ 2^0\]
Valeur10245122561286432168421
Nombre11

Recommence avec le nouveau nombre qui est 11. La puissance de 2 inférieure ou égale la plus proche de 11 est... 8. Tu as 1 fois 8 dans ton nouveau nombre, reporte le chiffre 1 sous 8.

\[ 11 - 8 = 3 \]

Indice109876543210
Notation\[ 2^{10} \]\[ 2^9\]\[ 2^8\]\[ 2^7\]\[ 2^6\]\[ 2^5\]\[ 2^4\]\[ 2^3\]\[ 2^2\]\[ 2^1\]\[ 2^0\]
Valeur10245122561286432168421
Nombre111

Recommence avec le nouveau nombre qui est 3. La puissance de 2 inférieure ou égale la plus proche de 3 est... 2. Tu as 1 fois 2 dans ton nouveau nombre, reporte le chiffre 1 sous 2.

\[ 3 - 2 = 1 \]

Indice109876543210
Notation\[ 2^{10} \]\[ 2^9\]\[ 2^8\]\[ 2^7\]\[ 2^6\]\[ 2^5\]\[ 2^4\]\[ 2^3\]\[ 2^2\]\[ 2^1\]\[ 2^0\]
Valeur10245122561286432168421
Nombre1111

Recommence avec le nouveau nombre qui est 1. La puissance de 2 inférieure ou égale la plus proche de 1 est... 1. Tu as 1 fois 1 dans ton nouveau nombre, reporte le chiffre 1 sous 1.

\[ 1 - 1 = 0 \]

Indice109876543210
Notation\[ 2^{10} \]\[ 2^9\]\[ 2^8\]\[ 2^7\]\[ 2^6\]\[ 2^5\]\[ 2^4\]\[ 2^3\]\[ 2^2\]\[ 2^1\]\[ 2^0\]
Valeur10245122561286432168421
Nombre11111

Il te reste 0, tu as terminé. Reprends le tableau et ajoute 0 aux cases où tu n'as pas écrit 1.

Indice109876543210
Notation\[ 2^{10} \]\[ 2^9\]\[ 2^8\]\[ 2^7\]\[ 2^6\]\[ 2^5\]\[ 2^4\]\[ 2^3\]\[ 2^2\]\[ 2^1\]\[ 2^0\]
Valeur10245122561286432168421
Nombre1010001011

Ce sont les puissances de 2 que tu n'as pu soustraire à chaque nombre que tu avais.

Tu peux donc dire que 651 en base 10 s'écrit 1010001011 en base 2.

De la base 2 à la base 10

Tu vas maintenant faire l'inverse, passer de la base 2 à la base 10. Toujours avec le tableau. Tu vas voir ça va être encore plus rapide.

Prenons un nombre en base 2 complètement au hasard : 1010011010.

Tu vas inscrire ce nombre dans le tableau le plus à droite possible.

Indice109876543210
Notation\[ 2^{10} \]\[ 2^9\]\[ 2^8\]\[ 2^7\]\[ 2^6\]\[ 2^5\]\[ 2^4\]\[ 2^3\]\[ 2^2\]\[ 2^1\]\[ 2^0\]
Valeur10245122561286432168421
Nombre1010011010

Maintenant, tu n'as qu'à reprendre les puissances de 2 avec un 1 en dessous et les sommer ensemble.

\[ 512 + 128 + 16 + 8 + 2 = 666 \]

Dans le cas de grands nombres

Si tu dois passer de la base 10 à la base 2 mais avec un nombre plus grand que le tableau, il existe une technique pour généraliser la transformation.

Prenons un nombre vraiment plus grand que 1024 : 6789.

Tu vas diviser par 2 ton nombre et garder à chaque fois le reste de côté. Rappelle toi de tes cours de primaire, c'est une opération euclidienne.

L'opération mathématique pour avoir le reste est %.

Ainsi si j'écris \[ x \% 2 = 0 \] c'est qu'il reste 0 à la division x / 2. Tu peux aussi dire que x est pair.

Et si j'écris \[ x \% 2 = 1 \] c'est qu'il reste 1 à la division x / 2. Tu peux aussi dire que x est impair.

Tu vas faire apparaître le résultat petit à petit avec cet affichage :

Résultat =

Il se remplira à chaque itération de tes calculs en y inscrivant le reste que tu viens de calculer sur la gauche.

\[ 6789 / 2 = 3394 \] \[ 6789 \% 2 = 1 \] Résultat = 1

\[ 3394 / 2 = 1697 \] \[ 3394 \% 2 = 0 \] Résultat = 01

\[ 1697 / 2 = 848 \] \[ 1697 \% 2 = 1 \] Résultat = 101

\[ 848 / 2 = 424 \] \[ 848 \% 2 = 0 \] Résultat = 0101

\[ 424 / 2 = 212 \] \[ 424 \% 2 = 0 \] Résultat = 00101

\[ 212 / 2 = 106 \] \[ 212 \% 2 = 0 \] Résultat = 000101

\[ 106 / 2 = 53 \] \[ 106 \% 2 = 0 \] Résultat = 0000101

\[ 53 / 2 = 26 \] \[ 53 \% 2 = 1 \] Résultat = 10000101

\[ 26 / 2 = 13 \] \[ 26 \% 2 = 0 \] Résultat = 010000101

\[ 13 / 2 = 6 \] \[ 13 \% 2 = 1 \] Résultat = 1010000101

\[ 6 / 2 = 3 \] \[ 6 \% 2 = 0 \] Résultat = 01010000101

\[ 3 / 2 = 1 \] \[ 3 \% 2 = 1 \] Résultat = 101010000101

\[ 1 / 2 = 0 \] \[ 1 \% 2 = 1 \] Résultat = 1101010000101

Ta dernière division donne 0. Tu as terminé.

Tu sais maintenant que 6789 s'écrit 1101010000101 en base 2.

C'est exact

La notation en base 16 est appellée hexadécimale.

Les plus avertis se demanderont comment écrire un nombre dans une base plus grande que 10. La base 2 utilise 0 et 1, la base 10 va de 0 et 9... mais qu'utiliser quand on va au delà de 9 ?

Les lettres de l'alphabet !

Ainsi la base 16 va de 0 à F. Voici les chiffres de la base 16 : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E et F.

Voici un tableau de référence :

Base 2Base 10Base 16
000
111
1022
1133
10044
10155
11066
11177
100088
100199
101010A
101111B
110012C
110113D
111014E
111115F
100001610
100011711
100101812
100112913
101002014
101012115
101102216
101112317
110002418
110012519
11010261A
11011271B
11100281C
11101291D
11110301E
11111311F
1000003220

On dira ainsi qu'en base 16, A est un chiffre et le nombre 10 vaut 16 en base 10.

Tu peux réutiliser les notions vues en base 2 pour la base 16 pour passer de l'un à l'autre. Pas besoin d'alourdir le cours avec des calculs supplémentaires.

Voici quelques valeurs à connaître par coeur.

Base 2Base 10Base 16
101010A
111115F
100001610
110002418
1000003220
1100004830
10000006440
1000000012880
11111111255FF
100000000256100
100000000001024400

Bit et octet

Bit

Le bit est la quantité minimale d'information utilisable en informatique. Elle n'a que deux valeurs : 0 et 1.

Si tu as mille bits, tu as un kilobit, que tu peux écrire avec un petit 'b' : 1 kb.

Si tu as un million de bits, tu écris un mégabit ou 1 Mb.

Si tu as un milliard de bits, tu écris un gigabit ou 1 Gb.

Octet

L'octet est composé de 8 bits. Sa traduction en Anglais est le byte.

Tu peux utiliser la même notation qu'avec le bit mais en utilisant un 'o' minuscule ou un 'B' majuscule en Anglais.

Si tu as mille octets, tu as un kilooctet ou un kilobyte, que tu peux écrire 1 ko ou 1 kB.

Si tu as un million d'octets, tu écris un mégaoctet ou 1 megabyte, que tu peux écrire 1 Mo ou 1 MB.

Si tu as un milliard d'octets, tu écris un gigaoctet ou 1 gigabyte, que tu peux écrire 1 Go ou 1 GB.

Portes logiques

Enfant, tu as appris les opérations mathématiques élémentaires : l'addition, la soustraction, la multiplication et la division.

En informatique, il existe d'autres opérations élémentaires qu'on appelle des portes logiques.

Tu vas appliquer ces portes logiques sur des bits, c'est à dire 0 et 1. Tu peux remplacer 0 par Faux et 1 par Vrai comme valeurs logiques.

NOT

La porte NOT est un inverseur. Elle prend une entrée et une sortie. Son rôle est d'inverser la valeur en entrée.

EntréeSortie
01
10

AND

La porte AND a deux entrées. Pour que le signal de sortie soit Vrai, il faut que les deux signaux aux entrées le soient également.

Entrée 1Entrée 2Sortie
000
010
100
111

Si tu as besoin d'une phrase logique pour mieux comprendre, imagine que tu fais des courses pour faire un gâteau (miam). Dans une ville, il existe deux magasins, un spécialisé en lait et l'autre en farine.

Il faut que les deux magasins possèdent les bons produits pour que tu puisses faire ton gâteau.

OR

La porte OR a deux entrées. Pour que le signal de sortie soit Vrai, il faut qu'au moins une des entrées soit à Vrai.

Entrée 1Entrée 2Sortie
000
011
101
111

Si tu as besoin d'une phrase logique pour mieux comprendre, imagine que tu fais des courses pour acheter un gâteau (c'est plus rapide que de le préparer). Dans une ville, il existe deux magasins spécialisés en gâteaux.

Il faut qu'au moins un des deux magasins soit ouvert pour que tu puisses avoir ton gâteau.

XOR

La porte XOR est un eXclusive OR, ou un OU eXclusif. Elle agit comme la porte OR à condition d'avoir une exclusivité en entrée.

Entrée 1Entrée 2Sortie
000
011
101
110

Si tu as besoin d'une phrase logique pour mieux comprendre, imagine que tu fais des courses pour le dernier disque de musique de ton artiste préféré. Dans une ville, il existe deux magasins spécialisés en musique.

Il faut qu'au moins un des deux magasins possède les droits exclusifs de vente pour qu'il existe une exclusivité. Si les deux magasins possèdent les droits de vente, alors il n'y a pas d'exclusivité.

Autres portes

Il existe d'autres portes logiques qui ne sont pas présentées ici. Elles ne te seront pas utilises pour valider la piscine, mais elles pourraient l'êtres plus tard. Je te laisse voir la page Wikipédia à leur sujet.

Choix du système

Le choix du système n'est pas déterminant pour une piscine. Je conseille Ubuntu qui est la distribution Linux désignée pour les novices. Cependant, je présente également d'autres distributions qui ont leurs avantages et inconvénients. Les pages qui suivent ont un lien pour télécharger la distribution et une vidéo pour l'installer et la configurer.

Si tu es débutant complet, préfère Ubuntu. Les paquets (programmes) ne seront pas toujours à jour, mais c'est une distribution d'une stabilité sans précédent quand on ne sait pas mettre à jour ses paquets.

Si tu veux une distribution Linux toujours à jour et simple d'installation, préfère Manjaro ou Fedora.

Si tu veux une distribution Linux toujours à jour et à installer à la main, préfère Arch Linux.

Si tu veux une distribution qui isole tes paquets, qui t'assure une stabilité, mais qui est bien plus compliquée à prendre en main quand on n'a pas l'habitude, préfère NixOS.

Ubuntu

ISO : https://www.ubuntu-fr.org/download

Arch Linux

ISO : https://archlinux.org/download/

Manjaro

ISO : https://manjaro.org/products/download/x86

Fedora

ISO : https://fedoraproject.org/fr/workstation/download

NixOS

ISO : https://nixos.org/download

Shell

Le Shell est un langage de programmation qui te permet de dialoguer avec ton système d'exploitation. Tu donnes des commandes à effectuer, et le système exécutera tes ordres.

Le terminal graphique est ce qu'on appelle un émulateur de terminal, celui-ci va appeler un interpréteur shell (sh, bash, zsh, dash, ...) qui interprétera tes commandes.

Tous les interpréteurs comprennent le Shell, mais certains ont des extensions qui te permettent une meilleure interactivité, avec de l'autocomplétion plus intelligente, une correction des fautes, une plus grande souplesse d'utilisation, etc.

J'utilise zsh, mais tout ce que je vais présenter est valide avec d'autres interpréteurs.

Les prochains chapitres t'apprendront à utiliser ton terminal.

Utilisation

J'admets ici que c'est la première fois que tu ouvres un shell qui a sa configuration par défaut.

Une fois sur ton shell, voici ce qui se présente à toi.

username@hostname:~$

Décortiquons tout ça.

username est ton nom d'utiisateur.

Le arobase '@' veut dire que tu es connecté sur une machine.

hostname est le nom de cette machine.

Le deux-points ':' est un simple séparateur pour la suite de la ligne.

Le tilde '~' signifie que tu te trouves dans ton HOME. C'est ton répertoire personnel avec tes fichiers personnels.

Le dollar '$' signifie que toutes les commandes que tu rentres sont exécutées comme simple utilisateur.

Si tu avais un signe dièse '#' à la place, ça signifierait que les commandes seront exécutées en tant que root (admin).

Pour simplifier la lecture des codes que je vais présenter, mon shell sera présenté ainsi :

$

Pour essayer, entre la commande pwd, et vois la différence de résultat avec la mienne.

$ pwd
/home/gelules
$

La commande pwd te dit où te trouves. C'est ton Path Working Directory.

Shutdown et reboot

Si tu veux arrêter ta machine, exécute la commande

$ shutdown now

Si tu veux rebooter ta machine, exécute la commande

$ reboot

Se déplacer

Introduction

Maintenant que tu sais lire ce que t'affiche ton shell par défaut, tu vas apprendre quelques manoeuvres de base.

Pour comprendre ce qu'il se passe, je te conseille d'ouvrir ton explorateur graphique (le programme qui te permet de voir tes documents graphiquement) d'un côté et ton shell de l'autre côté. Ca te permettra de faire le lien entre ce que tu tapes et ce qu'il se passe dans ton système à chaque commande.

Ton explorateur graphique ne ressemblera peut-être pas au mien qui est Thunar. Et j'aurai probablement à certains endroits des fichiers que tu n'as pas. Ce n'est pas grave.

Dans tous les cas tu verras des dossiers et des fichiers et tu devras double-cliquer pour te déplacer. Qui a dit que Windows avait l'exclusivité du clique ?

Je te conseille de faire les mêmes commandes que moi, pour que ça rentre ! Ne fais pas que lire. L'apprentissage vient surtout par la pratique, même si c'est jouer au miroir.

Explorateur et shell

Tu vois que dans mon exemple, mon shell se trouve dans mon HOME et Thunar aussi. La différence c'est que Thunar me liste directement tous mes fichiers.

Lister un répertoire

Pour lister ton répertoire courant, utilises la commande ls.

$ ls
Desktop  Development  Documents  Downloads  Formation  mount_vbox_share.sh  Music  Pictures  Public  School  Templates  Videos
$

Tu peux donner un ou plusieurs noms de fichiers et répertoires à ls pour lister leurs contenus.

Depuis ton HOME, exécute

ls Desktop Documents Downloads

Si ces répertoires n'existent pas chez toi, attends de savoir en créer pour tester ls avec des noms de fichiers et répertoires donnés en paramètres.

Créer un répertoire

Pour créer un répertoire, utilise la commande mkdir.

mkdir Test

mkdir Test

Comme tu peux le voir, mon explorateur de fichiers s'est actualisé pour afficher le nouveau répertoire. Utilise la commande ls dans ton terminal pour valider que ton terminal le voit également.

Sache que tu peux créer plusieurs répertoires d'un coup en les donnant à mkdir.

$ mkdir Je suis un seul appel a mkdir

Les commandes shell ont pour habitude de ne rien afficher si tout se passe bien. En cas d'erreur, un message s'affichera. Lis toujours tes messages, il ne faut pas avoir peur. C'est justement une aide pour comprendre ce qu'il se passe.

mkdir Test deux fois

Ici, j'ai voulu créer le répertoire Test alors qu'il existe déjà.

Se déplacer dans un répertoire

Pour te déplacer dans un répertoire graphiquement, c'est facile, tu doubles-cliques dessus.

Pour te déplacer dans un répertoire en terminal, c'est facile, tu utilises la commande cd.

$ cd Test

Déplace toi dans le répertoire Test.

cd Test

Ton shell affiche maintenant ~/Test. Ca veut dire qu'il se situe dans le répertoire Test lui-même situé dans ton HOME.

Tu peux créer une arborescence complète en donnant le paramètre -p à mkdir.

$ mkdir -p je/suis/une/longue/arborescence
$

mkdir path

Créer des fichiers

Tu ne vas pas encore voir comment éditer des fichiers. Tu vas commencer par créer des fichiers vides.

Pour se faire, utilise la commande touch.

$ touch je_suis_un_fichier
$ ls

Listing long format

Jusqu'ici tu as dû voir les différences entre fichier et répertoire aux couleurs affichées sur le terminal avec ls. Cette coloration est dûe à une configuration de ls.

Dans mon cas, si j'exécuter which ls pour en savoir plus sur la commande ls, j'ai ceci :

$ which ls
ls: aliased to ls --color=auto
$

Ca veut dire que ls utilise par défaut le paramètre --color=auto qui colore les noms des fichiers et répertoires dans ton terminal.

Exécute maintenant ceci, je fais exprès de cacher certains informations pour te forcer à exécuter les commandes :

$ ls
$ mkdir repertoire
$ touch fichier
$ /bin/ls
repertoire fichier
$

Tu vois maintenant que tout s'affiche en blanc. Ce que tu as fait, c'est exécuter le programme ls directement depuis là où il est installé, sans la configuration de coloration automatique de ton shell.

Imagine tomber un jour sur une distribution Linux qui n'affiche pas les couleurs par défaut, comment savoir si ce que tu listes et un fichier ou un répertoire ?

Tu vas utiliser le paramètre -l à ls pour afficher le long listing format, pour afficher plein d'informations sur le répertoire où tu te trouves.

Exécute

$ ls -l
total 0
-rw-r--r-- 1 gelules wheel  0 Jul 20 18:56 fichier
drwxr-xr-x 2 gelules wheel 40 Jul 20 18:55 repertoire
$

La première ligne indique combien de place sur le disque prend ce que tu listes. Pour l'instant c'est 0 car c'est vide.

Mainenant, pour chaque ligne, regarder le tout premier caractère. Si c'est un tirer '-', c'est que tu listes un fichier, si c'est un 'd' minuscule, c'est que c'est un répertoire.

Tu apprendras le reste des informations listées dans un prochain chapitre.

Lister récursivement

Pour atteindre graphiquement la fin de l'arborescence, ça demande quelques clics. Alors que pour le shell, il suffit de donner le paramètre -R à ls pour lui demander de lister récursivement les répertoires.

ls récursif

Pour information, le '.' situé au début de chaque résultat représente ton dossier courant (le working directory). Ca veut dire que si tu fais la commande

$ cd .

Tu te déplaceras... où tu te trouves.

J'ai un exemple pratique qui sera présenté plus tard.

Supprimer des fichiers et répertoires

Pour supprimer des répertoires, il existe deux commandes. *rmdir et rm.

rmdir va supprimer un répertoire vide et rm avec les bons paramètres va supprimer un répertoire et toute son arborescence récursivement.

Essaie le commandes suivantes, je n'affiche pas le résultat pour te forcer à visualiser par toi-même ce qu'il se passe.

$ ls
je je_suis_un_fichier
$ rmdir je_suis_un_fichier
??? # Mais diantre, que se passe t-il ?
$ rm je_suis_un_fichier
$ ls
je
$ ls -R
.:
je

./je:
suis

./je/suis:
une

./je/suis/une:
longue

./je/suis/une/longue:
arborescence

./je/suis/une/longue/arborescence:
$ rmdir je
??? # Mais diantre, que se passe t-il ?
$ rmdir je/suis/une/longue/arborescence
$ ls -R
???
$ rm -r je
$ ls -R
$

Tu l'as compris, le paramètre -r de rm supprime toute une arborescence.

rm peut aussi être utilisé pour supprimer des fichiers, mais nous n'avons pas encore vu ce cas.

Tu peux également donner plusieurs répertoires à rmdir et rm pour en supprimer plusieurs. Attention, rm doit toujours avoir son paramètre -r si tu supprimes des répertoires. Dans le cas de fichiers, tu n'as pas besoin de l'utiliser.

$ rmdir Je suis un seul appel a mkdir

ou

$ rm -r Je suis un seul appel a mkdir

Copier des fichiers et répertoires

Pour copier des fichiers et des répertoires, tu peux utiliser la commande cp.

$ ls
$ touch fichier_1
$ cp fichier_1 fichier_2
$ mkdir repertoire_1
$ cp fichier_1 fichier_2 repertoire_1
$ cp -r repertoire_1 repertoire_2
$ ls
fichier_1  fichier_2  repertoire_1  repertoire_2
$ ls repertoire_1 repertoire_2
repertoire_1:
fichier_1  fichier_2

repertoire_2:
fichier_1  fichier_2
$ mkdir repertoire_3
$ cp -r repertoire_1 repertoire_2 repertoire_3
$ ls repertoire_3
repertoire_1  repertoire_2
$

Comme tu peux le constater, si tu veux copier un répertoire, il faut le faire récursivement avec l'option -r.

Renommer des fichiers et répertoires

Pour renommer ou déplacer un répertoire ou un fichier, utilise la commande mv, pour MoVe.

$ touch gato
$ ls
gato
$ mv gato gateau
$ ls
gateau
$ mkdir ventre
$ mv gateau ventre
$ ls
ventre
$ ls ventre
gateau
$

En réalité quand tu renommes un fichier, tu le déplaces dans le même répertoire avec un nouveau nom.

cd en arrière

Tu sais avancer dans des répertoires, mais comment revenir en arrière ?

Il faut donner .. à cd pour revenir en arrière. Chaque .. part en arrière d'un niveau.

Un exemple sera plus parlant.

$ mkdir -p toto/titi/tutu
$ cd toto/titi/tutu
$ cd ..
$ ls
tutu
$ cd ../..
$ ls
toto
$

Retourner au dernier répertoire visité avec cd

Si tu veux basculer vers le dernier répertoire visité, donne - en paramètre à cd.

Je te laisse faire les commandes pour apprécier, j'ai caché exprès les parties intéressantes.

$ mkdir -p je/suis/un/humain tu/es/un/gateau
$ cd je/suis/un/humain
$ cd ../../../../tu/es/un/gateau
$ pwd
???
$ cd -
$ pwd
???
$ cd -
$ pwd
???
$

Retourner dans le HOME avec cd

Si tu veux retourner rapidement dans ton HOME, tu peux le faire en appellant simplement cd sans paramètre.

Je te laisse faire les commandes pour apprécier, j'ai caché exprès les parties intéressantes.

$ cd je/suis/un/humain
$ pwd
???
$ cd
$ pwd
???
$

Cas particuliers

Crée le répertoire canare et cd y toi dedans.

~ $ mkdir canare
~ $ cd canare
~/canare $

Tu te rends compte que tu as fait une faute de frappe.

Renomme le répertoire dans lequel tu te trouves ainsi :

~/canare $ mv ../canare ../canard
~/canare $

Horreur et damnation, ton shell affiche encore canare. Il faut que tu exécutes cd . pour que ton shell se mette à jour.

~/canare $ cd .
~/canard $

Maintenant plus drôle. Tu vas créer ce qu'on appelle un undefined behavior ou comportement indéfini.

Voici quelques commandes :

$ mkdir toto
$ cd toto
$ rmdir ../toto
$ cd .
$ pwd

Que se passe t-il ? Tout dépend de ton shell, certains feront avec à leur manière, d'autres afficheront une erreur. C'est un comportement indéfini, le langage shell n'a pas défini quoi faire dans ce cas, chaque interpréteur a un comportement différent.

Le système de fichier

Tu l'as peut-être vu en exécutant la commande pwd, ton chemin commence toujours par un slash '/'.

Ton HOME se trouve dans un autre répertoire appelé home qui se trouve lui-même dans un répertoire nommé / (slash).

Exécute la commande ls / pour voir ce qui se trouve à la racine de ton système de fichier (filesystem).

$ ls /
bin  boot  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

Tu n'as peut-être pas exactement le même résultat. Ce n'est pas grave. Tu devrais quand même y voir des similarités.

Pour les utilisateurs Windows, le répertoire racine est un peu équivalent au disque C qui contient tous les répertoires de configuration. C'est faux sur beaucoup d'aspects techniques que tu n'as pas encore besoin de connaître. Je veux juste que tu saches qu'il existe un répertoire racine depuis lequel tu peux te déplacer et lister ce qu'il s'y trouve.

Si jamais tu dois faire des tests temporaires, je te renvoie au répertoire /tmp qui est, dans beaucoup de distributions Linux, remis à zéro à chaque démarrage. Il peut être configuré pour être remis à zéro tous les jours, semaines, mois...

$ cd /tmp
$ touch je_vais_disparaitre
$ ls
je_vais_disparaitre
$ reboot
...
$ cd /tmp
$ ls

Cache-cache

Il y a une dernière chose dont je ne t'ai pas parlé. Les fichiers cachés.

Je vais exprès de cacher certains informations pour te forcer à pratiquer.

Exécute ces commandes depuis ton HOME :

$ mkdir Test
$ cd Test
$ touch .cache affiche
$ ls
???
$

Où est le fichier .cache ? Il est caché. Les fichiers et répertoires dont le nom commencent par un point '.' ne sont pas affichés, car il s'agit en général de fichier de configuration personnelle pour tes programmes et c'est plus élégant de ne pas toujours les afficher.

Si tu veux les afficher, exécute :

$ ls -a
. .. .cache affiche

Mais que sont '.' et '..' ? Rappelle toi, le '.' représente le répertoire dans lequel tu te trouves et '..' représente le répertoire parent.

Si tu veux afficher tout ce qui est caché sauf '.' et '..', exécute :

$ ls -A
.cache affiche

Je te laisse maintenant exécuter cette dernière commande dans ton HOME. Tu verras que même ton explorateur graphique ne t'affiche pas tout par défaut (mais c'est configurable).

Fichiers cachés dans Thunar

RTFM : Read The Fucking Manual

Pour en apprendre d'avantages quant aux commandes que tu ne maitrises pas, il faut que tu fasses confiance à ton meilleur ami : Google le manuel.

Pour l'invoquer, utilise la commande man.

$ man ls

Le manuel s'affiche. Pour t'y déplacer, utilise les touches fléchées. Pour le quitter, appuie sur q.

Chaque commande a son manuel, mais aussi certaines notions de Linux.

Par exemple, si tu veux en apprendre plus sur ton système de fichier, exécute la commande

$ man hier

hier pour hierarchy.

Et si tu veux apprendre à utiliser le manuel, alors consulte le manuel du manuel.

$ man man

Chose intéressante dedans, les sections.

The table below shows the section numbers of the manual followed by the types of pages they contain.

1   Executable programs or shell commands
2   System calls (functions provided by the kernel)
3   Library calls (functions within program libraries)
4   Special files (usually found in /dev)
5   File formats and conventions, e.g. /etc/passwd
6   Games
7   Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7), man-pages(7)
8   System administration commands (usually only for root)
9   Kernel routines [Non standard]

Pour ta picine, les 3 premières sections seront à consulter.

La 1ere contient celle des binaires (des programmes) que tu utilises depuis ton shell.

La 2eme et la 3eme contiennet des fonctions que tu peux appeler quand tu fais du langage C. La 2eme est celle des fonctions que ton Kernel te propose, la 3eme sont les fonctions de la libc (bibliothèque C).

Un exemple. Si tu veux afficher le manuel de la fonction C printf (tu ne sais peut-être pas encore ce que c'est, ce n'est pas grave), tu serais tenté d'exécuter.

$ man printf

Si tu le fais, tu verras en haut à gauche printf(1), le '1' entre parenthèses signifie que tu es dans la 1ère section du manuel. Ce que tu veux c'est la 3ème, pour les appels de la bibliothèque C (library calls).

mauvais printf

Il faut alors utiliser une de ces deux notations :

$ man 3 printf
$ man printf.3

bon printf

A partir de maintenant, pour toutes les commandes que tu as vues et verras, je t'invite très fortement à lire le manuel à chaque fois.

Lire et écrire dans des fichiers

Tu as vu comment créer des fichiers vides, mais tu n'as pas encore vu comment écrire dedans et les lire à la ligne de commande.

Ecrire dans un fichier

Pour écrire dans un fichier, tu peux utiliser l'éditeur de texte nano qui se veut extrêmement simple d'utilisation et surtout s'exécute dans le terminal.

Mettons que tu sois dans ton HOME, nous allons faire les tests d'écriture et lecture dans le répertoire Test.

$ mkdir Test
$ cd Test

Tu peux appeler nano sans nom de fichier derrière, tu pourras donner un nom après.

$ nano
  GNU nano 8.1                                                   New Buffer

































                                           [ Welcome to nano.  For basic help, type Ctrl+G. ]
^G Help          ^O Write Out     ^F Where Is      ^K Cut           ^T Execute       ^C Location      M-U Undo         M-A Set Mark
^X Exit          ^R Read File     ^\ Replace       ^U Paste         ^J Justify       ^/ Go To Line    M-E Redo         M-6 Copy

Tu peux directement écrire ton texte et te déplacer dans le fichier avec les touches fléchées.

Tu peux voir en bas des commandes notées avec un circonflexe '^'. C'est un alias pour la touche ctrl. Si tu veux sauvegarder ton fichier, appuie sur ctrl+o pour Write Out.

Après cette combinaison, nano te demande le nom du fichier avec cette ligne affichée en bas à gauche :

File Name to Write:

Donne un nom de fichier. Tu peux même mettre un chemin entier pour le sauvegarder ailleurs. Appelle ton fichier fichier_test.

Si tu veux quitter nano, exécute ctrl+x. Si tu as modifié ton fichier entretemps, il te demandera si tu veux sauvegarder avant de quitter.

De retour sur le shell, tu peux rouvrir ton fichier avec nano en lui donnant son nom en paramètre.

$ nano fichier_test

Lire un fichier

Pour lire un fichier, tu peux utiliser la redirection gauche '<' en mettant le nom de ton fichier.

$ < fichier_test
# Contenu du fichier
$

Tu peux aussi utiliser la commande cat

$ cat fichier_test
# Contenu du fichier
$

cat te sera très utile pour vérifier que tes fichiers respectent une partie de la norme. La norme en piscine est le style de code (la coding style) à suivre pour que tes fichiers soient considérés comme valides.

Rouvre ton fichier avec nano et ajoute des espaces à la fin d'une ou plusieurs lignes de texte. Exécute maintenant

$ cat -e fichier_test
# Contenu du fichier avec des '$' en fin de ligne
$

Comme tu peux le voir, il y a des dollars en fin de ligne. Mais avant certains dollars, il y a tes espaces. Ce n'est pas bien ! Un fichier de code ne doit pas avoir d'espace qui donne ensuite sur une fin de ligne. Ce sont des caractères inutiles présents dans tes fichiers. Ca prend de la place pour rien et c'est moche, bouh !

Rouvre ton fichier avec nano et supprime ces espaces en trop.

$ cat -e fichier_test
Je suis un fichier$
avec     $
des $
espaces              $
en trop$
BERK                           $
$ # Bouh c'est moche
$ nano fichier_test
...
$ cat -e fichier_test
Je suis un fichier$
avec$
des$
espaces$
en trop$
BERK$
$ # Que c'est beau, je suis amoureux 

Permissions

Comprendre les permissions

Tu te rappelles du long listing format de ls avec toutes ces informations imbitables ? C'est le moment de les biter.

Reprenons dans répertoire Test vide.

$ touch fichier
$ mkdir repertoire
$ cp /bin/ls . # Si tu as une erreur, essaie avec /usr/bin/ls
$ ls -l
total 136
-rw-r--r-- 1 gelules wheel      0 Jul 20 21:29 fichier
-rwxr-xr-x 1 gelules wheel 129728 Jul 20 21:29 ls
drwxr-xr-x 2 gelules wheel   4096 Jul 20 21:29 repertoire
$

La commande cp copie le binaire /bin/ls dans '.', c'est à dire le répertoire où tu te trouves.

Il est très probable que tu n'aies pas la même sortie. Déjà au niveau des dates et des heures, mais aussi sur la colonne avec "wheel". Ne t'inquiète pas, tout va bien.

Décortiquons tout ça.

La première colonne contient les informations sur le type de fichier et les permissions du fichier. Rappelle toi, '-' signifie que c'est un simple fichier, et 'd' signifie que c'est un répertoire (directory).

Ensuite, le reste de la colonne, prenons le fichier ls :

rwxr-xr-x

Il faut diviser ces 9 caractères en 3 groupes de 3 caractères.

UserGroupOther
rwxr-xr-x

Avant d'expliquer les lettres, je dois expliquer les colonnes du tableau.

User donne des informations sur l'utilisateur qui possède le fichier. Si on relit la sortie de ls -l plus haut, on peut voir que l'utilisateur qui possède le fichier est gelules.

Group donne des informations sur le groupe qui possède le fichier. Si on relit la sortie de ls -l plus haut, on peut voir que le groupe qui possède le fichier est wheel.

Other est le reste du monde qui n'est ni l'utilisateur gelules ni les utilisateurs dans le groupe wheel.

Tu peux voir dans quel groupe tu es avec la commande group.

$ group

Revenons aux lettres.

r signifie read, pour lecture.

w signifie write, pour écriture.

x signifie execute, pour exécution.

L'utilisateur a le droit de lire, modifier et exécuter le binaire.

Le groupe a le droit de lire et exécuter le binaire.

Tous les autres ont le droit de lire et exécuter le binaire.

Changer les permissions

Pour modifier les permissions, tu peux utiliser la commande chmod.

Mais avant ça, il va falloir revoir un peu la base 2.

rwx
101

Tu peux voir ici, je demande à ce que les permissions du fichier donnent le droit de lecture et d'exécution et interdisent le droit de modification. La valeur de 101 en base 10 est... 5.

Il faut faire 3 fois cette gymnastique pour avoir 3 chiffres. Le 1er sera le droit pour l'utilisateur, le 2eme les droits pour le groupe et le 3eme les droits pour les autres.

Par exemple, je souhaite donner les permissions suivantes au fichier : l'utilisateur peut lire, modifier et exécuter le fichier, le groupe peut lire et modifier le fichier et les autres ne peuvent qu'exécuter le fichier.

rwxrwxrwx
111110001

Il faut traduire par paquet de 3.

On a donc :

111 = 7

110 = 6

001 = 1

Le droit à appliquer sera 761.

$ chmod 761 ls
$ ls -l ls
-rwxrw---x 1 gelules wheel 129728 Jul 20 21:29 ls
$

Comme tu peux le voir, le fichier ls a vu ses permissions être modifiées.

Sache qu'un répertoire a des droits d'exécution. Le droit d'exécution d'un répertoire permet de s'y cd.

Faisons un autre exemple.

$ chmod 300 repertoire

Maintenant je n'ai que les droits de mofidication et d'exécution sur le répertoire repertoire.

$ nano repertoire/test
...
$ ls repertoire
ls: cannot open directory 'repertoire': Permission denied
$ cat repertoire/test
...
$

Tu ne peux pas afficher le contenu, mais tu peux quand même t'y déplacer et modifier son contenu.

Tips and tricks

Tout au long des derniers chapitres, tu t'es écorché les doigts à taper des commandes et sûrement mal taper le nom d'une commande, ce qui t'a valu de recommencer.

Qu'à cela ne tienne, tu vas découvrir ici quelques raccourcis claviers.

Touches fléchées

Déjà, les touches fléchées te permettent de déplacer ton cursus à gauche et à droite si tu dois modifier ta commande avant de l'exécuter.

Ensuite, les touches fléchées haut et bas te permettent de te déplacer dans ton historique de commandes et en rappeller plus rapidement qu'en réécrivant une commande.

Autocomplétion

Ton shell est capable d'autocompléter tes commandes. Appuie sur sur la touche de tabulation pour lui demander d'autocompléter.

$ touch travail ventre ventricule
$ ls t<TAB>

Tu verras que le fichier travail va s'autocompléter tout seul.

$ ls v<TAB>
     ventr

Là il va faire face à un problème. Le shell ne peut pas deviner à ta place si tu veux ventre ou ventricule. En appuyant une deuxième fois sur TAB, il t'affichera quels fichiers correspondent.

$ ls v<TAB><TAB>
     ventre ventricule

A toi de régler l'ambiguïté en ajoute e ou i.

Arrêt d'un programme

Si jamais tu as lancé un programme ou un script depuis ton terminal et que tu veux arrêter son exécution, exécute la combinaison de touches ctrl+c pour envoyer un signal d'arrêt et reprendre la main sur ton terminal.

Nettoyer le terminal

Si tu veux nettoyer tout ce qui est affiché sur ton terminal pour repartir sur une vue propre, exécute la combinaison de touches ctrl+l.

Recherche arrière

Avec ctrl+r, tu peux recherche une commande. Par exemple ctrl+r cp - te montrera la dernière commande qui contient cp -r, si tu rappuies sur ctrl+r, la commande précédent s'affichera, jusqu'à afficher la toute première commande dans ton historique qui contient cp -r.

Déplacement rapide du curseur

Si tu utilises ctrl+fleche gauche ou droite, tu peux déplacer le curseur de mot en mot.

Tu peux aussi faire alt+f pour forward (avant) et alt+b pour backward (arrière) pour faire pareil.

Tu peux aussi faire la combinaison ctrl+a pour envoyer le cursus au tout début de la ligne, et ctrl+e pour l'envoyer à la fin de la ligne.

Suppression avant / après le curseur

Si ton curseur est au milieu de la commande, et que tu veux supprimer tout ce qu'il y a à droite, tu peux faire la combinaison ctrl+k. Si tu veux supprimer tout ce qu'il y a à gauche, effectuer la combinaison ctrl+u.

Editeurs de texte

Tu as utilisé nano pour modifier des fichiers. Maintenant je vais te présenter 2 éditeurs qui te permettront de coder très efficacement.

Il en existe d'autres tout aussi bien, mais je ne les maitrise pas, donc je préfère te laisser avec ce que je connais.

Si tu en as marre de la console, et que tu veux éditer ton code dans un éditeur grapghique, tu trouveras ton bonheur dans la prochaine section, vscode.

Si tu veux apprendre à modifier des fichiers toujours dans le terminal, vim sera ton fidèle allié.

Aux utilisateurs d'Emacs : désolé ! Je ne connais pas bien votre éditeur et surtout je n'ai pas 6 doigts à chaque main pour ça. Mais je sais que votre éditeur est super cool.

Aux utilisateurs de Neovim : Oui oui ton éditeur est super moderne. En attendant ça fait des années que je suis sur Vim et que je suis bien heureux avec, rien ne me donne envie pour le moment de changer.

VSCode

Pour installer vscode, installer le paquet code

$ sudo apt install code # Ubuntu
$ sudo pacman -S code # Arch et Manjaro
$ sudo dnf install code # Fedora
$ nix-env -i nixos.vscode # NixOS

Tu peux l'exécuter avec la commande code.

La première fenêtre te demander de configurer le thème graphique.

Ensuite quelles extensions tu veux. Je ne vais pas en conseiller.

Maintenant sur la page d'accueil, tu peux ouvrir un fichier ou un répertoire qui contient des fichiers.

Tu peux également appeler vscode avec des fichiers en paramètres.

code fichier_1 fichier_2

Il se peut qu'il te dise que les fichiers ne sont pas "trustés". C'est parce que tu n'as pas crée de projet vscode. Tu peux dire "Oui" sans problème.

C'est tout ce que je vais présenter de vscode, je ne l'utilise pas plus que ça. Mais je voulais quand même ajouter un éditeur graphique si tu es vraiment allergique à la ligne de commande.

Vim

Alors là trop facile, j'ai fais plusieurs vidéos pour apprendre à l'utiliser.

Tu trouveras la playlist complète ici.

Run Command

Tu ne le sais peut-être pas, ton shell est configuré par un fichier Run Command. C'est un fichier caché dans ton HOME.

Si tu es sur bash, ton fichier de configuration est

$ ~/.bashrc

Si tu es sur zsh, ton fichier de configuration est

$ ~/.zshrc

Si tu es sur un autre shell... regarde le man de ton shell pour savoir où se trouve ton fichier de configuration.

mkcd

On va agrémenter ton fichier de configuration avec une petite fonction bien sympathique que j'aime bien.

Ouvre ton fichier de configuration et ajoute à la fin ces lignes :

mkcd ()
{
    [ ! -d "$1" ] && mkdir -p $1
    cd $1
}

Pour mettre à jour la configuration du shell où tu te trouves, exécute

$ . ~/.zshrc

Accorde la commande avec ton fichier de configuration.

La commande '.' exécute le fichier que tu donnes. Ton shell va donc exécuter son fichier de Run Command pour se mettre à jour.

Tu peux aussi utiliser la commande source, c'est la même chose.

Pour les prochains shells, tu n'auras pas à faire cette commande. Ca sera appliqué automatiquement.

Mais que fais cette commande ?

Décortiquons tout ça.

mkcd ()
{

}

Ca, c'est une fonction en shell. Une fonction c'est du code que tu pourras appeler depuis ton shell.

[ ! -d "$1" ] && mkdir -p $1

Cette ligne est compliquée à lire. Mais tu dois reconnaître la commande mkdir -p.

[ ! -d "$1" ]

Ce morceau de code va vérifier si le 1er argument n'existe pas comme répertoire.

&& mkdir -p $1

&& est une porte logique. Dans le contexte du shell, il s'agit de la porte AND et exécutera la partie à droite seulement si la partie de gauche renvoie Vrai.

La partie de droite créee une arborescence de répertoires.

cd $1

cd va te déplacer dans le répertoire envoyé en 1er argument.

La fonction mkcd va donc créer un répertoire ou même toute une arborescence de répertoire et t'y cd automatiquement.

$ pwd
/home/gelules
$ ls /tmp/tests
ls: cannot access '/tmp/tests': No such file or directory
$ mkcd /tmp/tests/pediluve/shell
$ pwd
/tmp/tests/pediluve/shell
$

Window Manager

Un Window Manager est l'environnement graphique de ta session. Selon ta distribution, tu vas probablement utiliser Gnome, Plasma, XFCE, etc..

J'utilise Awesome comme window manager, la prochaine section te montrera comment je l'utilise.

Awesome

Hop là c'est facile, il y a une vidéo !

Programmation Shell

Tu y es ! Le début de ta grande aventure dans la programmation !

Ne t'inquiète pas, je reste dans les prochains chapitres le plus clair et simple possible, avec des exemples pour appréhender simplement chaque notion.

Tu verras pendant ta piscine et le reste de ton cursus que ce que je présente va beaucoup plus loin en terme de complexité, mais le but du Pédiluve reste de t'aider à appréhender une piscine, pas de devenir une brute en informatique.

Flux systèmes

Sortie standard

La sortie standard (ou stdout) est le flux de texte affiché sur ton terminal quand tout se passe bien.

Par exemple, quand tu exécutes

$ ls -l fichier_existant # assure toi que le fichier existe

Tout ce qui est affiché passe par stdout.

Tu peux rediriger le flux dans un fichier en utilisant un chevron.

$ ls -l > fichier_stdout
$ cat fichier_stdout

Comme tu peux le voir, ls -l n'a pas affiché de résultat, tout a été redirigé dans un fichier qui n'existait pas.

Recommence l'opération.

$ ls -l > fichier_stdout
$ cat fichier_stdout

Le fichier n'a pas gardé l'information précédente. Il a été tronqué.

Si tu veux garder le contenu du fichier à chaque redirection, tu peux utiliser la double redirection.

$ ls -l >> fichier_stdout
$ cat fichier_stdout

Et là, le contenu déjà présent reste dans le fichier.

Si tu veux rediriger stdout dans le vide, c'est à dire ne pas l'afficher et ne pas le rediriger dans un fichier, tu peux utiliser le fichier /dev/null comme fichier de redirection.

$ ls -l > /dev/null

C'est un fichier spécial utilisé uniquement pour rediriger des flux dans le vide.

Erreur standard

Le flux standard des erreurs (ou stderr) s'affiche aussi sur ton terminal mais en empruntant un autre flux. Il ne s'affiche qu'en cas d'erreur.

Prenons la situtation suivante :

$ ls
fichier_existant
$ ls -l fichier_existant fichier_inexistant
ls: cannot access 'fichier_inexistant': No such file or directory
-rw-r--r-- 1 gelules wheel 0 Jul 20 21:29 fichier_existant
$

La première ligne avec le message d'erreur emprunte le flux d'erreur, et la seconde emprunte stdout.

Tu peux les séparer dans des fichiers différents ainsi :

$ ls -l fichier_existant fichier_inexistant >stdout.txt 2>stderr.txt
$ cat stdout.txt stderr.txt
...

Le 2 est le chiffre de référence pour stderr. stdout utilise 1 mais tu n'es pas obligé de l'écrire, sauf à une condition.

Si tu veux rediriger stderr vers stdout, tu dois le faire de cette façon.

$ ls -l fichier_existant fichier_inexistant 1>stdout.txt 2>&1

Le caractère esperluette '&' indique au shell que 1 n'est pas un nom de fichier mais le flux numéro 1.

Le premier 1 devant stdout.txt n'est pas obligatoire, mais je voulais te montrer que tu peux l'écrire quand même.

Pipe

Les pipes servent à rediriger le stdout d'un programme dans le stdin d'un autre programme.

stdin est l'entrée standard. C'est un flux où tu fais en temps normal de l'entrée clavier. Ce que fais ton shell à chaque fois que tu appuies sur Entrée, il lit son entrée standard pour recevoir ta commande.

Faisons quelques exemples plus parlant :

$ ls
$ ls -l / > racine.txt
$ wc -l racine.txt

wc -l te permet de compter le nombre de lignes dans un fichier. Mais tu aurais pu faire ça en une seule commande grâce au pipe (tuyau).

$ ls -l / | wc -l

Le pipe '|' te permet de passer la sortie de ls dans l'entrée de wc.

Beaucoup d'outils sur Linux fonctionne ainsi.

Prenons un enchaînement de commandes plus drôle :

$ ls -l / | cut -d ' ' -f 1 | tee permissions | wc -l

Voici deux nouvelles commandes.

cut va couper des colonnes selon un délimiteur.

Prenons la sortie de ls -l /.

$ ls -l /
total 56
lrwxrwxrwx   1 root root       7 Apr  7 20:02 bin -> usr/bin
drwxr-xr-x   4 root root    4096 Jul 19 01:59 boot
drwxr-xr-x  19 root root    3880 Jul 20 16:52 dev
drwxr-xr-x  95 root root    4096 Jul 20 22:14 etc
drwxr-xr-x   3 root root    4096 Sep 13  2022 home
lrwxrwxrwx   1 root root       7 Apr  7 20:02 lib -> usr/lib
lrwxrwxrwx   1 root root       7 Apr  7 20:02 lib64 -> usr/lib
drwx------   2 root root   16384 Sep 13  2022 lost+found
drwxrwx---   1 root vboxsf  4096 Jul  1 02:05 media
drwxr-xr-x   2 root root    4096 Dec  7  2021 mnt
drwxr-xr-x   2 root root    4096 Dec  7  2021 opt
dr-xr-xr-x 227 root root       0 Jul 20 16:52 proc
drwxr-x---  14 root root    4096 Sep 23  2023 root
drwxr-xr-x  29 root root     680 Jul 20 16:55 run
lrwxrwxrwx   1 root root       7 Apr  7 20:02 sbin -> usr/bin
drwxr-xr-x   4 root root    4096 Sep 13  2022 srv
dr-xr-xr-x  13 root root       0 Jul 20 21:28 sys
drwxrwxrwt  15 root root     360 Jul 21 00:00 tmp
drwxr-xr-x  10 root root    4096 Jul 20 22:14 usr
drwxr-xr-x  12 root root    4096 Jul 20 16:52 var

La commande cut que j'utilise délimite chaque field (champ) en utilisant le délimiteur ESPACE ' '.

On n'aura donc que la première colonne avec cut. On n'aura donc que la première colonne avec cut -d ' ' -f 1

$ ls -l / | cut -d ' ' -f1
total
lrwxrwxrwx
drwxr-xr-x
drwxr-xr-x
drwxr-xr-x
drwxr-xr-x
lrwxrwxrwx
lrwxrwxrwx
drwx------
drwxrwx---
drwxr-xr-x
drwxr-xr-x
dr-xr-xr-x
drwxr-x---
drwxr-xr-x
lrwxrwxrwx
drwxr-xr-x
dr-xr-xr-x
drwxrwxrwt
drwxr-xr-x
drwxr-xr-x

Ensuite la commande tee va rediriger sa sortie dans deux flux, un fichier et stdout. Imagine la lettre 'T' (tee en pronociation Anglaise), la barre de gauche est son entrée standard, la barre qui descend est le fichier, la barre de droite est stdout.

$ ls -l / | cut -d ' ' -f1 | tee permissions
total
lrwxrwxrwx
drwxr-xr-x
drwxr-xr-x
drwxr-xr-x
drwxr-xr-x
lrwxrwxrwx
lrwxrwxrwx
drwx------
drwxrwx---
drwxr-xr-x
drwxr-xr-x
dr-xr-xr-x
drwxr-x---
drwxr-xr-x
lrwxrwxrwx
drwxr-xr-x
dr-xr-xr-x
drwxrwxrwt
drwxr-xr-x
drwxr-xr-x
$ cat permissions
...

tee a bel et bien redirigé ce que tu vois sur stdout dans un fichier nommé permissions.

Et enfin, wc -l affiche le nombre de ligne reçu depuis stdout.

Voilà la puissance de Linux. Utiliser la sortie d'un programme comme donnée pour l'entrée d'un autre programme.

Job control

Le job control te permet de mettre en pause et remettre en route les programmes que tu lances depuis ton shell.

Dans cet exemple, il faut que je seul programme lancé soit ton shell, rien d'autre.

firefox
...

Maintenant que firefox est lancé, tu n'as plus la main sur ton shell. Firefox l'utilise pour afficher son stdout et stderr.

Exécute la combinaison ctrl+z pour mettre Firefox en pause et regagner la main sur ton shell.

...
ctrl+z
zsh: suspended  firefox
$

Selon le shell que tu utilises, tu peux avoir un message sensiblement différent.

Tu vas maintenant démarrer vlc et gimp et pareil, exécuter ctrl+z pour reprendre la main à chaque fois.

$ vlc
...
ctrl+z
zsh: suspended  vlc
$ gimp
...
ctrl+z
zsh: suspended gimp
$

Exécute la commande jobs pour voir l'état des jobs (processus) démarrés par ton shell.

$ jobs
[1]    suspended  firefox
[2]  - suspended  vlc
[3]  + suspended  gimp
$

Tu as ta liste de jobs avec des identifiants. Le signe '-' est l'avant-dernier jobs que tu as suspendu, et le signe '+' et le dernier jobs que tu as suspendu.

Si tu veux remettre un job en foreground, c'est à dire que tu relances son exécution en lui laisant la main sur ton shell, utilise fg %IDENTIFIANT en remplaçant IDENTIFIANT par le numéro du job.

$ fg %1 # firefox est réexécuté et on perd la main sur le shell
...
ctrl+z

Si tu veux mettre un job en background, c'est à dire que tu relances son exécution tout en gardant la main sur ton shell, utilise bg %IDENTIFIANT en remplaçant IDENTIFIANT par le numéro du job.

$ fg %1
$ # firefox reprend son exécution et on garde la main sur le shell

Pour arrêter un job, utilise la commande kill %IDENTIFIANT en remplaçant IDENTIFIANT par le numéro du job.

kill %1
[1]  + terminated  firefox
$

Enfin, si tu veux lancer un processus en arrière-plan directement depuis ton shell, ajoute le caractère esperluette '&' à la fin de ta commande.

$ vlc  &
[1] 22258
$

Le nombre affiché est le numéro du processus dans le système, il est plus que probable que tu n'aies pas le même. Tu peux retrouver le nom d'un processus avec son identidiant et la commande ps.

$ ps -p 22258
    PID TTY          TIME CMD
  22258 pts/0    00:00:00 vlc

Scripts

Voici un gros chapitre, l'introduction aux scripts shell !

On va voir ici comment créer un fichier script, comment l'exécuter et afficher les paramètres qu'on lui donne. Les prochaines chapitres étofferont petit à petit ce que tu peux faire par le script.

En définition, un script shell, c'est juste un fichier avec des commandes shells. En exécutant le fichier, tu exécuteras les commandes dedans.

En réalité, quand tu exécutes ton shell au clavier, il ne fait pas de différence avec un fichier script. Il est juste en attente de la prochaine ligne à lire et attend la fin de fichier (que tu peux envoyer avec la combinaison ctrl+d, ce qui fermera ton shell).

Ce qui fait que tout ce que tu vas voir à partir d'ici est possible à exécuter dans ton terminal.

Voici le minimum d'un script shell :

#!/bin/sh

C'est un commentaire particulier qui indique quel interpréteur utiliser. Par habtiude on utilise /bin/sh car c'est un interpréteur présent sur tous les systèmes Linux. Plus rarement tu verras /bin/bash car ce dernier a des extensions que n'a pas sh. Evite de mettre zsh à moins de réellement utiliser une fonctionnalité qui n'est pas présente sur sh, car tous les systèmes n'ont pas zsh installés.

Pour rendre exécutable ton script, tu peux calculer les valeurs qui vont bien avec chmod, ou aller plus vite en exécutant cette commande :

$ chmod +x script.sh

Et maintenant pour l'exécuter :

$ ./script.sh

Voilà. Ton script ne fait pas encore grand chose.

Pour information, je vais écrire un énorme script, mais ne t'inquiète pas, il est commenté pour que tu comprennes ce que fait le script. Je donnerai ensuite des exemples d'exécution pour que tu voies les différences.

Avant de montrer le script, je dois te présenter une nouvelle commande : echo. Elle affiche ce que tu lui donnes en paramètre sur stdout.

$ echo Coucou les amis !
Coucou les amis !
$

Ce ne te semble pas utile pour le moment, mais ça va vite changer.

#!/bin/sh

# Afficher la façon dont on exécute le script
echo $0

# Afficher le nombre d'argument
echo $#

# Afficher le 1er argument
echo $1

# Afficher le 2eme argument
echo $2

# Afficher le 9eme argument
echo $9

# Afficher le 10ème argument
echo ${10}

# Afficher la liste des arguments avec *
echo $*

# Afficher la liste des arguments avec @
echo $@

Ce que tu vois ici sont des variables spéciales. Elles sont en lien avec le script et auront des valeurs différentes selon comment tu exécutes le script.

Exécute le script avec ces paramètres et regarde ce qu'il se passe.

$ ./script.sh
...
$ ./script.sh toto
...
$ ./script.sh toto tata titi tutu
...
$ ./script.sh toto 'tata titi' tutu
...
$ ./script.sh toto "tata titi" tutu
...
$ ./script.sh je vais toucher le 10eme argument juste pour voir qu il s affiche bien
...
$

Tu comprendras la différence entre les deux dernières variables plus tard.

Exit status

Tu peux appeler n'importe quel programme dans ton script, comme dans ton terminal.

#!/bin/sh

ls $1
mkdir ventre
touch ventre/gateau
ls -l ventre

Tu te rappelles des flux standards de sortie et d'erreur ? En plus de ça, il y a un autre moyen de voir si un script s'est bien exécuté ou non. En utilisant la variable exit status. C'est une variable spéciale qui recevra le code de sortie d'un programme.

Habituellement, on dit en informatique que 0 est Faux et que 1 est Vrai. En shell c'est l'inverse. 0 est Vrai et ce qui n'est pas 0 est Faux. Un programme peut renvoyer 256 codes d'erreurs différents, allant de 0 à 255.

Donc, on peut dire que Vrai est compris dans l'intervalle [0] et que Faux est compris dans l'intervalle [1;255].

Pour voir le code de retour d'un programme, utilise la variable $?. Elle aussi le code de retour de la dernière commande exécutée.

#!/bin/sh

touch fichier_existant
ls fichier_existant
echo exit_status: $?

ls fichier_nonexistant
echo exit_status: $?
$ ./script.sh
fichier_existant
exit_status: 0
ls: cannot access 'fichier_nonexistant': No such file or directory
exit_status: 2

Tu peux voir que ls retourne 2 quand il ne trouve pas un fichier.

Je t'invite à lire le man 1 ls dans la section Exit status, tout est documenté dedans.

Je t'invite à faire ça pour chaque programme que tu connais.

Variables

Les variables sont des mots qui vont avoir une valeur.

Tu peux définir une variable ainsi :

#!/bin/sh

variable=valeur

Attention, il faut absolument que le signe égal '=' n'ait pas d'espace autour de lui. Tout doit être collé.

Pour utiliser sa valeur, tu vas coller le signe dollar '$' devant le nom de cette variable.

Pour afficher une variable, tu peux utiliser echo.

#!/bin/sh

variable=valeur
nombre=42
echo Ma variable variable a comme valeur $variable
echo Mon nombre est $nombre

Si tu veux concaténer deux variables ensembles, tu peux utiliser les accolades pour bien les séparer. Tu peux même en construire de nouvelles comme ça.

#!/bin/sh

prenom=gel
nom_famille=ules
login=${prenom}${nom_famille}
domain=pediluve.info
mail=${login}@${domain}

echo Mon mail est $mail # L'adresse n'existe pas, pas la peine d'essayer

Mathématiques

C'est parti pour faire un peu de mathématique.

Maintenant que tu sais utiliser des variables, tu vas pouvoir effectuer des opérations mathématiques avec.

Tu peux faire toute opération mathématique dans des doubles parenthèses précédées d'un signe dollar '$'.

#!/bin/sh

echo $((11 + 21))

a=42
b=666

echo $((a * b))

c=$((a + b * 11))
echo $c

Si tu mets une variale qui n'est pas un nombre, elle sera remplacée par 0.

Variables d'environnement

Les variables d'environnement sont des variables initialisées au lancement de ton shell.

Tu peux les avoir avec cette commande :

$ env

Voici les variables que tu utiliseras le plus :

Tu peux connaitre le shell exécuté avec la variable $SHELL

$ echo $SHELL

Tu peux connaitre le nom d'utilisateur avec la variable $USER

$ echo $USER

Tu peux connaitre le chemin vers le HOME de l'utilisateur avec la variable HOME

$ echo $HOME

Tu peux connaitre le répertoire courant avec la variable $PWD et le répertoire courant précédent avec la variable $OLDPWD

echo $PWD
...
echo $OLDPWD
...

PATH

La variable d'environnement PATH est une variable particulière. C'est la variable qui permet à ton shell de trouver les programmes à exécuter.

Si je prends en exemple mon PATH

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/home/gelules/.local/bin/:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/home/gelules/.local/bin

Je peux voir que le 1er répertoire vérifié est /usr/local/bin, puis /usr/bin, et ainsi de suite.

On va faire un petit jeu.

Crée le répertoire /tmp/test/ et cd dedans.

$ mkdir /tmp/test
$ cd /tmp/test

Crée le fichier avec ton éditeur et ajoute la ligne suivante dedans :

$ cat firefox
#!/bin/sh

echo Et non, je ne suis pas Firefox
$

Rends le exécutable.

$ chmod +x firefox

On va mettre la variable d'environnement PATH à jour. Elle ne le sera que pour ton shell actuel, les futurs shells ne seront pas impactés.

$ PATH=/tmp/test:$PATH
$ firefox
Et non, je ne suis pas Firefox
$

Tu peux voir que ton shell trouve d'abord le fichier firefox que tu viens de créer.

Tu peux voir quel programme ton shell trouvera avec la commande which.

$ which firefox
/tmp/test/firefox
$

Quotting

Le quotting est la façon de formater du texte. Tu te rappelles de la commande echo qui affiche du texte sur stdout mais qui peut aussi afficher des variables.

Simple quote

La simple quote te permet d'afficher du texte sans qu'il ne soit évalué. Ca veut dire que tu peux afficher le nom d'une variable sans qu'elle soit remplacée par sa valeur.

$ cat script.sh
#!/bin/sh

nom=gelules
echo 'Mon nom est $nom'
$ ./script.sh
Mon nom est $nom
$

Comme tu peux le voir, mettre du texte entre simple quote affiche le texte tel qu'écrit dans le script, sans être évalué par le shell.

Double quote

Le double quote te permet de visuellement savoir où commence et où termine ton texte, le shell qui sera dedans sera évalué. Avec echo, c'est comme ne pas mettre de quotes, mais tu te repères plus facilement à la lecture de ton script.

$ cat script.sh
#!/bin/sh

nom=gelules
echo "Mon nom est $nom"
$ ./script.sh
Mon nom est gelules
$

Escape

Une question t'a peut-être traversé l'esprit : Comment afficher une simple quote ou une double quote sans qu'elle ne soit comprise comme un début ou fin de texte ? Ou même afficher le signe dollar '$' sans utiliser de simple quote ?

Tu peux échapper un caractère spécial avec l'antislash.

$ cat script.sh
#!/bin/sh

echo Voici un dollar : \$
echo Voici un simple quote : \'
echo Voici un double quote : \"
echo Voici un antislash: \\
$ ./script.sh
Voici un dollar : $
Voici un simple quote : '
Voici un double quote : "
Voici un antislash: \
$

Entre simple quote, tu peux afficher sans problème un double quote. Pareil inversement, tu peux entre double quote afficher un simple quote.

$ cat script.sh
#!/bin/sh

echo "Voici un double quote entre double quote => \" <= pas mal hein ?"
echo 'Voici un double quote entre simple quote => " <= pas mal hein ?'
echo "Voici un simple quote entre double quote => ' <= pas mal hein ?"
$ ./script.sh
Voici un double quote entre double quote => " <= pas mal hein ?
Voici un double quote entre simple quote => " <= pas mal hein ?
Voici un simple quote entre double quote => ' <= pas mal hein ?
$

Tu ne peux en revanche rien échapper entre simple quote. Donc pas de simple quote entre simple quote. Mais tu peux arrêter ton simple quote en plein milieu de ton appel à echo, échapper la simple quote et reprendre en simple quote juste après.

$ cat script.sh
#!/bin/sh

echo 'Je fais presque tout en simple quote, en voici une =>' \' '<= pas mal hein ?'
$ ./script.sh
Je fais presque tout en simple quote, en voici une => ' <= pas mal hein ?
$

Tests et conditions

Tu sais récupérer les variables envoyées en paramètres à ton script, tu sais en créer, tu sais faire un peu de maths et afficher toutes ces variables. C'est bien. Mais que dirais-tu de rendre tes scripts un peu plus vivants ? D'avoir des exécutions différentes selon ce que tu envoies en paramètres ? C'est déjà plus intéressant.

Conditions

Une condition en shell est un test qui va renvoyer Vrai ou Faux. Si c'est Vrai, alors le script va emprunter un chemin d'exécution, si c'est Faux, alors le script va emprunter un autre chemin d'exécution.

Test

Tu peux tester plusieurs choses en shell : les nombres, les chaînes de caractères, l'existence de fichier, les exécutions d'autres programmes.

Je t'invite à lire le man 1 test qui liste tous les tests possibles.

Comme tu peux le vois, on peut combiner les tests avec les portes logiques AND et OR.

Par exemple pour tester une chaîne de caractères, tu utilises le test

[ chaine1 = chaine2 ]

Ce test renverra Faux.

Attention à bien laisser des ESPACES ' ' entre le signe '$' et à côté des crochets '[' et ']'. Si tu ne laisses pas d'espaces, le shell ne comprendra pas que c'est un test et boguera.

Protection

Avant de montrer des exemples, je dois te montrer une technique de protection des scripts pour éviter des crashs embêtants.

#!/bin/sh

mot_de_passe=$1

if [ $mot_de_passe = "pediluve" ]
then
    echo "Ouverture de la porte secrète"
else
    echo "Mauvais mot de passe"
fi

Exécute ce script sans lui envoyer de paramètre.

$ ./script.sh
./script.sh: line 5: [: =: unary operator expected
Mauvais mot de passe
$

Que s'est-il passé ?

La variable n'a pas de valeur, le shell a donc lu la ligne avec le test littéralement de cette manière :

if [ = "pediluve" ]

Ceci n'est pas du shell valide. Il faut une valeur avant le signe '='.

Tu peux protéger ta variable en lui mettant des double quotes.

#!/bin/sh

mot_de_passe=$1

if [ "$mot_de_passe" = "pediluve" ]
then
    echo "Ouverture de la porte secrète"
else
    echo "Mauvais mot de passe"
fi

Si tu exécutes le script sans paramètre, voici ce que le shell lit.

if [ "" = "pediluve" ]

Le shell comprend alors qu'il teste une chaîne de caractères vide avec "pediluve".

Exemples

if elif else

Avant de te montrer de réels exemples, tu vas apprendre à enchainer les tests.

if sert à tester une ou plusieurs conditions. Si tout est Vrai, alors le code qui suit sera exécuté.

elif est la concaténation de else if. Il faut aussi mettre une condition, et si la condition est Vraie, alors le code qui suit sera exécuté. Tu peux enchainer autant de elif que tu veux. Tu n'es pas obligé d'utiliser elif quand tu fais des tests.

else contient du code qui sera exécuté si tous les tests précédents étaient Faux. Il ne faut pas lui mettre de test.

#!/bin/sh

# Si le code secret n'est pas bon
# on écrit sur stderr que le code est faux
# et on quitte le script avec l'exit status 1
if [ "$1" != "super_mot_de_passe" ]
then
    echo "Mauvais mot de passe" >&2
    exit 1
fi

if [ "$2" = "gelules" ]
then
    echo "Bonjour maitre"
elif [ "$2" = "pediluvien" ]
then
    echo "Bonjour jeune apprenti"
elif [ "$2" = "piscinien" ]
then
    echo "Bonjour, tu viens te rafraichir la mémoire ?"
else
    echo "Tu m'es inconnu"
fi

Petit jeu vidéo

Pour apprendre à faire des tests, au lieu de t'énumérer tous les tests possibles, je préfère te montrer plein d'exemples différents. A toi ensuite de lire le man 1 test pour compléter tes connaissances.

Imaginons que tu codes un petit jeu vidéo en shell.

#!/bin/sh

# Si le nombre d'arguments n'est pas 2
if [ $# -ne 2 ]
then
    echo "Usage: $0 LOGIN SALLE_DE_TP" >&2
    exit 1
fi

login=$1
tp=$2
save_file=sauvegarde.txt

# Si le fichier de sauvegarde n'existe pas et que c'est bien un pédiluvien qui
# joue au jeu, on crée le fichier de sauvegarde
if [ ! -f "$save_file" ] -a [ "$login" != "gelules" ]
then
    touch "$save_file"
fi

# Exception pour le maitre de jeu
if [ "$login" = "gelules" ]
then
    echo "TP gelules dans 'salle secrète'" >> "gelules_sauvegarde.txt"
    exit
fi

if [ "$tp" = "donjon" ]
then
    echo "TP $login dans 'donjon'" >> "$save_file"
    exit
elif [ "$tp" = "tour" ]
then
    echo "TP $login dans 'tour'" >> "$save_file"
    exit
elif [ "$tp" = "chambre" ]
then
    echo "TP $login dans 'chambre'" >> "$save_file"
    exit
else
    echo "Mauvaise TP" >&2
    exit 1
fi

Je te laisse essayer le script.

Il n'est pas optimisé du tout. Comme tu peux le voir, il y a du code qui se répète. On retrouve beaucoup de fois

echo "TP $login dans 'LIEU'" >> "$save_file"

mais c'est un problème pour plus tard. Pour l'instant il faut que tu apprennes à jouer avec les tests.

Je t'invite à regarder plus en détail comment tester les nombres. Dans mon exemple je teste si le nombre d'argument est Not Equal à 2. Mais tu peux tester une égalité, si le nombre est strictement inférieur ou inférieur ou égal, et pareil avec la supériorité.

Boucles

Les boucles sont un moyen de répéter une séquence de code autant de fois que nécessaire.

Imagine que tu veuilles créer les fichiers day_0.txt day_1.txt day_2.txt etc. jusqu'à day_10.txt. Tu ne vas quand même pas écrire les noms de ces 11 fichiers dans ton script ?

while

while a une iou plusieurs conditions, et le code de la condition sera répété tant que la condition est Vraie.

#!/bin/sh

i=0
while [ i -le 10 ]
do
    touch day_${i}.txt
    i=$((i + 1))
done

Ce code va créer la liste des fichiers day_0.txt jusqu'à day_10.txt.

Essaie maintenant de modifier le script pour le rendre plus modulable. Tu dois utiliser des arguments pour :

  • Avoir un nom de fichier différent que day
  • Partir d'un nombre de départ différent que 0
  • Finir à un nombre différent que 10. Attention, le nombre de fin doit être strictement supérieur que le nombre de départ
  • Avoir une extension différent que txt

Voici un exemple de corrigé :

#!/bin/sh

if [ $# -ne 4 ]
then
    echo "Usage: $0 FILENAME START STOP EXTENSION" >&2
    exit 1
fi

if [ "$3" -le "$2" ]
then
    echo "Le nombre de départ doit être strictement supérieur que le nombre de fin" >&2
    exit 1
fi

filename=$1
start=$2
stop=$3
extension=$4

while [ "$start" -le "$stop" ]
do
    touch "${filename}_${start}.${extension}"
    start=$((start + 1))
done

On peut tester l'environnement du système. Par exemple, tant qu'un répertoire n'existe pas, on boucle sur un message d'erreur.

#!/bin/sh

while [ ! -d ventre ]
do
    echo "Le répertoire 'ventre' n'existe pas"
done

touch ventre/gateau_chocolat
touch ventre/gateau_caramel
touch ventre/gateau_vanille
touch ventre/gateau_pistache

echo "Burp"

Il faut utiliser un deuxième shell pour créer le répertoire.

Lire un fichier ligne par ligne

Il est possible de lire un fichier ligne par ligne avec une boucle while.

#!/bin/sh

if [ ! -f input_file ]
then
    echo "input_file n'existe pas" >&2
    exit 1
fi

while read line
do
    echo "La ligne est: $line"
done < input_file

Evidemment, tu peux remplacer input_file par un paramètre.

until

until est exactement la même chose que while, à la différence que le code exécuté est répété tant que la condition est Fausse. Plus exactement, jusqu'à ce qu'elle soit Vraie.

#!/bin/sh

i=0

# Tant que i n'égale pas 10, on exécute le code de la boucle
until [ $i -eq 10 ]
do
    echo $i
    i=$((i + 1))
done

for

for sert à boucler sur une séquence. Avec un point de départ, un point de fin.

Tu peux boucler sur un ensemble d'éléments, ou une séquence de nombres.

#!/bin/sh

for element in je suis un sequence
do
    echo $element
done

Tu peux boucler sur des séquences générées en pure shell.

#!/bin/sh

for element in {a..z}
do
    echo $element
done

Pour les nombres il y a une différence. Je te laisse tester toutes ces différences :

#!/bin/sh

# De 1 à 20 par pas de 1
for nombre in {1..20}
do
    echo $nombre
done

# De 1 à 20 par pas de 4
for nombre in {1..20..4}
do
    echo $nombre
done

# De 20 à 1 par pas de 1
for nombre in {20..1}
do
    echo $nombre
done

# De 20 à 1 par pas de 6
for nombre in {20..1..6}
do
    echo $nombre
done

Tu peux aussi boucler sur les paramètres. Tu vas ici comprendre la différence entre $@ et $*

Teste les scripts avec au moins deux paramètres.

#!/bin/sh

for arg in $@
do
    echo $arg
done
#!/bin/sh

for arg in $*
do
    echo $arg
done

Jusqu'ici, aucune différence. Ajoute maintenant les variables entre double quotes.

#!/bin/sh

for arg in "$@"
do
    echo $arg
done
#!/bin/sh

for arg in "$*"
do
    echo $arg
done

Tu peux maintenant voir la différence. $@ reste une liste alors que $* devient un seul argument.

Git

Git est un outil de versionnage. Il te permet de sauvegarder un état d'un projet à n'importe quel moment, et de revenir à cet état quand tu le souhaites.

Voici un petit glossaire au vocabulaire de git.

  • repository (repo) : un dépôt local de travail dirigé par git
  • remote : un dépôt distant de travail dirigé par git
  • commit : acte de sauvegarder l'état du projet
  • revision : un point de sauvegarde (qui a été commit)
  • push : pousser le projet du repo local à la remote
  • pull : tirer le projet de la remote au repo local
  • HEAD : nom donné à la dernière révision du projet
  • changeset (ou SHA-1) : idenfiant unique d'une révision

Commandes de bases

Tu vas effectuer quelques commandes de bases pour mieux appréhender Git. Rien de spectaculaire mais tu sauras faire le minimum.

init

Avant de jouer avec git, il faut initier le dépôt local.

gitconfig

Avant d'initier un dépôt local, il faut configurer git avec au moins deux informations : ton nom et ton mail.

Pour ça, tu vas créer un fichier .gitconfig dans ton HOME et le remplir ainsi :

$ cat ~/.gitconfig
[user]
    name = Gélules
    email = gelules@gelules.gelules
$

Evidemment, change les informations avec les tiennes.

Ces informations seront utiles pour savoir qui a créé quel commit.

$ mkdir projet
$ cd projet
$ git init

Voilà, le répertoire projet est prêt à utiliser git.

Comment ça fonctionne ? Git crée en secret un répertoire caché qu'il utilise pour suivre l'état du projet.

$ ls -l .git
...
$

status

git status est une commande fondamentale. Elle te permet de savoir où tu en es dans ton travail.

Si tu le fais dans un répertoire vide, la commande te répondera qu'il n'y a rien à commit, et qu'il faut utiliser git add pour commencer à suivre les fichiers.

add

Tu vas créer deux fichiers, README et file_creator.sh. Pour l'instant ils seront vides.

$ touch README file_creator.sh
$ git status
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	README
	file_creator.sh
$ git add README file_creator.sh
$ git status
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   README
	new file:   file_creator.sh
$

Tes fichiers sont prêts à être commités.

commit

Pour créer un commit, il faut penser d'abord réfléchir à un message de commit intelligent. Le tout premier est en général un simple initial commit. Les prochains seront plus réfléchis selon le projet que tu effectues.

$ git commit -m "initial commit"
[master (root-commit) 8a87f72] initial commit
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README
 create mode 100644 file_creator.sh
$

Tu as crées ton premier commit. Bravo !

Tu peux lire ici que c'est le root-commit, le tout premier du projet, et que son changeset est 8a87f72, et que 2 fichiers ont été créés avec les permissions 644.

Il est plus que probable que tu n'aies pas le même SHA-1. C'est normal.

A savoir : Git ne traque que la permission d'exécution. Si un fichier est exécutable en local, il le sera aussi sur la remote.

log

Maintenant que tu as crée ton premier commit, tu peux voir les logs du projet évoluer avec la commande git log.

$ git log
commit 8a87f728e30904f1cd837fd3ca2d4f17d11c0e58 (HEAD -> master)
Author: Gélules <gelules@gelules.gelules>
Date:   Sun Jul 21 22:15:07 2024 +0200

    initial commit
$

On continue le projet sur Github

Dans la prochaine section, tu vas apprendre à créer un dépôt sur Github, à créer des clés SSH pour garantir la sécurité de push et pull entre ton dépôt local et la remote de GitHub et tu vas travailler ton projet que tu pusheras sur GitHub pour voir l'évolution.

Cas pratique avec GitHub

Le but de git, c'est de sauvegarder chaque nouvelle fonctionnalité de ton projet dans un commit. Des que tu ajoutes quelque chose, que tu l'as testé et qu'il fonctionne, alors tu fais un commit.

Et si tu utilises une remote, alors tu veux en plus pousser tes sauvegardes dessus.

Tu vas ici apprendre à utiliser git avec GitHub.

Création du compte

Tout d'abord, crée un compte sur GitHub

Clés SSH

Pour que tu puisses utiliser ton dépôt, tu dois créer des clés SSH. Tu vas créer deux clés, une clé privée, qui comme son nom l'indique doit rester privée, et une clé publique, que tu peux montrer à tout le monde.

Quand tu installeras ta clé publique sur GitHub, il y aura une opération cryptographique entre ta clé privée et la clé publique pour assurer que tu as les droits de pousser ton travail sur GitHub.

Ca semble compliqué dit comme ça, ne t'inquiète pas, tout est transparant pour toi, tu n'auras rien à faire une fois que tout est installé.

Va dans ton HOME et crée le répertoire .ssh. Dedans, crée les clés SSH.

$ cd
$ mkdir .ssh
$ cd .ssh
$ ssh-keygen -t ed25519

Là, un prompt te demande comment tu veux appeler ta clé. Appelle la github.

Ensuite, un prompt te demande si tu veux mettre un mot de passe à ta clé. C'est une sécurité en plus. A chaque fois que tu feras appel à cette clé pour faire une connexion avec la remote de GitHub, tu devras rentrer ce mot de passe.

Met un mot de passe si tu veux.

ATTENTION, si tu veux absolument mettre un mot de passe, alors je te conseille de changer la génération de tes clés avec cette commande

$ ssh-keygen -t ed25519 -a 100

La différence est qu'il y aura une centaine de passages sur ta clé avec un chiffrement particulier. Par défaut la valeur est 16. 100 est une assez bonne valeur pour augmenter la robustesse de la clé si quelqu'un te la vole et tente de casser le mot de passe avec une attaque bruteforce sans que ça ralentisse trop les vérifications de mot de passe à chaque utilisation de la clé.

Maintenant que tu as crées les clés, tu vas créer dans le répertoire ~/.ssh/ le fichier config que tu vas configurer pour la connexion à GitHub.

$ cat ~/.ssh/config
Host github.com
    Hostname github.com
    User git
    IdentityFile ~/.ssh/github
$

Laisse git dans la partie User.

Tu as deux clés. github et github.pub. La première est la clé privée, la seconde la clé publique.

Dans GitHub, dirige toi dans Settings > Access > SSH and GPG keys

Clique sur New SSH key. Donne lui un nom, le nom de ton PC par exemple, et colle la clé publique dans le champ prévu à cet effet. Sauvegarde en cliquant sur Add SSH key.

Tu vas maintenant tester la connexion.

$ ssh -T git@github.com
The authenticity of host 'github.com (IP ADDRESS)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
Are you sure you want to continue connecting (yes/no)?

Ecris yes et appuie sur Entrée.

Hi Gelules! You've successfully authenticated, but GitHub does not provide shell access.

Evidemment tu devrais voir ton login à la place du mien.

Création du projet

Retourne sur l'accueil de GitHub.

En haut à gauche, il y a un bouton vert New. Clique dessus pour créer un nouveau distant. Appelle le file_creator. Tu peux le rendre publique ou privée au choix.

Création du projet sur GitHub

Crée le projet.

Clone du projet

Te voilà maintenant sur le projet, vide.

Tu as au milieu une partie Quick setup avec deux boutons, HTTPS et SSH. Clique sur SSH. Copie le lien qui est inscrit. Le mien ressemble à ceci : git@github.com:Gelules/file_creator.git

Retourne dans ton HOME et exécute la commande suivante :

$ git clone git@github.com:Gelules/file_creator.git
...
$ cd file_creator

Remplace le lien par le tien, évidemment.

Retour au projet

Le projet, créer un générateur de fichiers. Le même que tu as fait dans les chapitres de shell, mais on va ici le construire petit à petit et faire un commit à chaque nouvelle fonctionnalité.

Tu vas recréer le fichier file_creator.sh et README, vides. Les ajouter et les commit avec le message initial commit.

push

Maintenant que tu es retourné à l'état du chapitre précédent, tu vas pousser tes fichiers vides.

$ git push
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 233 bytes | 233.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To github.com:Gelules/file_creator.git
 * [new branch]      main -> main
$

Retourne sur GitHub, et rafraichis la page. Surprise ! Tes fichiers y sont. Toujours vides.

Avancement

Comme je te l'avais dit, git suit ce qu'on appelle le execution bit, le bit qui indique qu'un fichier est exécutable ou non.

Pour le voir par l'exemple, tu vas rendre file_creator.sh exécutable, exécuter git status, tu verras qu'il t'indique d'ajouter le fichier.

Rends le exécutable, ajoute le, crée un commit pour indiquer que le fichier doit être exécutable et pousse ton travail.

$ chmod +x file_creator.sh
$ git status
$ git add file_creator.sh
$ git commit -m "file_creator must be executable"
$ git push
$ git log

Dans les logs, tu peux voir que les commits montent à chaque nouvelle sauvegarde.

Tu peux voir à la fin du premier commit que la HEAD est sur la branche main, qui est la branche par défaut sur git, et que la branche main de la remote origin est aussi sur le dernier commit : tu es à jour sur ton dépôt local et sur la remote.

Voilà, tu as ici l'utilisation moyenne de git. Tu modifies, tu vérifies que tout aille bien

Tu vas commencer par un script qui genère des fichiers qui suivent ce pattern : test_NUMERO.txt allant de 0 à 10.

#!/bin/sh

i=0
while [ $i -le 10 ]
do
    touch test_${i}.txt
done

En temps normal, tu dois tester ta nouvelle fonctionnalité. Quand tous tes tests sont bons, alors tu peux commit ton travail.

Que manque t-il ici ? L'incrémentation de i à chaque création de fichier. Je te laisse faire le changement nécessaire.

Ajoute dans le README un message pour indiquer comment utiliser ton projet.

$ cat README
file_generator is a super project to generate a lot of files with different
extensions. For now it's only able to generate 10 test files.
$ git add file_generator.sh README
$ git commit -m "generate 10 tests file"
$ git log

Tu peux le voir, ta remote est maintenant en retard d'un commit. Pousse ton travail pour que tout soit à jour.

$ git push

Nom de fichiers

Tu vas maintenant modifier le script pour qu'il gère un nom de fichier différent selon le paramètre que tu envoies.

#!/bin/sh

if [ $# -ne 1 ]
then
    echo "Usage: $0 FILENAME" >&2
    exit 1
fi

filename=$1

i=0
while [ $i -le 10 ]
do
    touch ${filename}_${i}.txt
    $i=$((i + 1))
done

Modifie le README en conséquence.

Ajoute tes modifications à git, commit et pousse.

N'oublie pas d'utiliser git status et git log pour voir l'évolution.

Nombre de fichiers

Nouvelle évolution : Tu vas maintenant avoir un script qui peut générer un nombre de fichiers différents, qui part d'un nombre donné en paramètre et qui termine sur un autre nombre donné en paramètre.

#!/bin/sh

if [ $# -ne 3 ]
then
    echo "Usage: $0 FILENAME START STOP" >&2
    exit 1
fi

if [ "$stop" -le "$start" ]
then
    echo "START number must be greater than STOP number" 2>
    exit 1
fi

filename=$1
start=$2
stop=$3

while [ "$start" -le "$stop" ]
do
    touch ${filename}_${start}.txt
    start=$((start + 1))
done

Modifie le README en conséquence.

Ajoute tes modifications à git, commit et pousse.

N'oublie pas d'utiliser git status et git log pour voir l'évolution.

reclone

J'ai besoin que tu connaisses la commande git pull. Pour ça tu vas cloner une deuxième fois ton dépôt ailleurs. Dans le répertoire /tmp par exemple.

Ouvre un deuxième terminal et effectue ces commandes.

$ cd /tmp
$ git clone git@github.com:Gelules/file_creator.git
...
$ cd file_creator

Tu viens de cloner ton dépôt dans le dernier état auquel tu avais poussé ton travail.

Garde ce terminal de côté et retourne sur le premier.

Extension

Tu vas ajouter la dernière modification. Tu vas demander à l'utilisateur de donner une extension aux fichiers générés.

#!/bin/sh

if [ $# -ne 4 ]
then
    echo "Usage: $0 FILENAME START STOP EXTENSION" >&2
    exit 1
fi

if [ "$stop" -le "$start" ]
then
    echo "START number must be greater than STOP number" 2>
    exit 1
fi

filename=$1
start=$2
stop=$3
extension=$4

while [ "$start" -le "$stop" ]
do
    touch ${filename}_${start}.${extension}
    start=$((start + 1))
done

Modifie le README en conséquence.

Ajoute tes modifications à git, commit et pousse.

N'oublie pas d'utiliser git status et git log pour voir l'évolution.

pull

Tu te souviens du deuxième terminal qui avait clone la remote ?

Ce répertoire n'est plus à jour avec la nouvelle modification ! Tu vas y remédier.

Sur ton deuxième terminal :

$ git pull
...
$

Et voilà, tu as pull ta mise à jour.

Evidemment sur le même ordinateur ce n'est pas utile.

Mais si tu travailles sur plusieurs machines ou que tu es un groupe à plusieurs personnes sur un projet, alors chacun pourra mettre à jour le projet ainsi.

Evidemment il peut y avoir des conflits si tout le monde touche au même fichier en même temps, mais c'est un problème que tu apprendras à résoudre pendant tes études.

Bravo ! Tu as terminé ton pédiluve sur git !

Langage C

Le langage C n'est pas le langage de programmation le plus simple à apprendre. Si on veut le maitriser, il faut aller en profondeur dans les entrailles du système, ce qui n'est pas évident au début.

La bonne nouvelle, c'est qu'on peut s'en servir pour de simples applications sans trop s'inquiéter de ces fameuses entrailles. C'est ce que tu vas voir ici.

Tu vas apprendre à faire un peu de C. Assez pour comprendre à utiliser le langage et ne pas être perdu au début de ta piscine.

Je donne quand même quelques petites astuces qui ne sont pas souvent données dans les cours en ligne pour garder de bons réflexes et être sûr de ne pas faire n'importe quoi.

Abécédaire

Les chapitres de l'abécédaire vont te permettre de souffler après autant de Shell. Ils sont écrits en sorte à ce que tu puisses t'amuser à faire des petits programmes sans comprendre les magouilles techniques qui se cachent pendant l'exécution de ton programme.

Compilation de test

Le langage C est un langage compilé. A l'inverse du shell, tu ne vas pas exécuter du C comme tu l'écris. Tu vas utiliser un compilateur pour transformer ton code course en langage machine.

Tu vas pour ce cours faire tes codes de test dans un seul et unique fichier que tu vas appeler test.c. Ton compilateur est gcc. Installe le si tu ne l'as pas.

$ which gcc
/usr/bin/gcc
$

Voici le fichier le plus petit que tu peux utiliser pour faire du C.

int main(void)
{
    return 0;
}

Tu peux écrire ton code entre les accolades pour commencer.

Pour compiler et essayer ton programme, voici la commande à utiliser :

$ gcc test.c -o mon_super_programme
$ ./mon_super_programme
$

Sache que tu peux commenter ton code pour le documenter et décrire que tu fais. Il existe deux types de commentaire. Les singie lines et les multilines.

/* Cette fonction main
ne prend rien en entrée et ne fait que
retourner 0 */

int main(void)
{
    // retourne 0 en exit status;
    return 0;
}

Voilà, tu es prêt pour la suite.

La fonction main et l'environnement de développement

La fonction main est le point d'entrée des programmes en C.

Une fonction est une zone qui contient du code.

Tu pourras créer tes propres fonctions plus tard, que tu pourras appeler depuis la fonction main.

Pour l'instant, voici ce que tu as :

int main(void)
{
    return 0;
}

Décortiquons tout ça.

int signifie que la fonction retourne un integer, c'est à dire un entier. C'est effectivement le cas avec le return 0 dans le code. Tu retournes la valeur 0 dans le shell comme exit status.

void veut dire que la fonction ne prend pas de paramètres. Donc tu ne peux pas envoyer de paramètre à l'exécution de ton programme, à l'inverse de programmes que tu as déjà utilisés comme ls, echo, mkdir, etc. Tu verras juste après comment faire.

Change le 0 par un 42, compile ton programme, et exécute le. Tu verras que tu auras un exit status à 42.

int main(void)
{
    return 42;
}
$ gcc test.c -o mon_super_programme
$ ./mon_super_programme
$ echo $?
42
$

Tu comprends mieux maintenant comment les programmes que tu sais utiliser te renvoient des exit status maintenant.

Argument

La fonction main peut prendre des arguments. Voici comment :

int main(int argc, char *argv[])
{
    return 0;
}

Oh là là, en voilà une nouvelle écriture compliquée.

Décortiquons tout ça.

int argc signifie que argc est de type int, c'est un entier. Il s'agit ici du nombre d'arguments (arguments counter) que ton programme reçoit à son exécution. En shell, c'est l'équivalent de la variable $#.

char *argv[] ; celui là est un peu plus difficile. char signifie caractère. Donc il y a des caractères en jeu ici, du texte.

L'étoile * signifie pointeur, tu sauras ce que c'est plus tard.o

[] signifie tableau (array).

argv est un tableau de chaines de caractères. Il contient autant de chaines de caractères que la valeur de argc.

Son nom signifie arguments values

Voici un bout de code pour voir les paramètres que tu envoies en entrée à ton programme.

#include <stdio.h>

int main(int argc, char *argv[])
{
    for (int i = 0; i < argc; i = i + 1)
    {
        printf("argv[%d] = %s\n", i, argv[i]);
    }

    return 0;
}

Que de nouvelles complications !

Décortiquons tout ça.

#include <stdio.h> veut dire qu'on va inclure stdio.h. C'est un fichier quelque part dans les répertoires systèmes. C'est ce qu'on appelle une bibliothèque (et pas une librairie, une librairie c'est un endroit avec des livres. En revanche la traduction de bibliothèque en Anglais et library, d'où cet abus de langage). Cette bibliothèque contient du code pour indiquer à gcc qu'il existe quelque part une fonctione appelée printf.

Ensuite il y a une boucle for. La boucle for initialise une variable i de type int à 0. Elle va continuer son exécution tant que i est plus petit que argc, et à chaque itération, i s'incrémente de 1. Tu peux voir que toutes les étapes de la boucle sont séparées par des point-virgules ';'.

Ensuite, entre les accolades de la boucle, il y a un appel à la fonction printf. Le nom de la fonction signifie print format. Tu vas formater du texte avec des arguments.

Dans argv[%d], %d attend un entier. i qui est envoyé en paramètre juste après sera remplacé et aura sa valeur affichée entre les crochets.

%s est un code qui attend une chaine de caractères. argv est le tableau qui contient des chaines de caractères.

Enfin, \n signifie saut à la ligne. Plus exactement linefeed.

Compile et exécute ton programme ainsi :

$ gcc test.c -o mon_super_programme
$ ./mon_super_programme coucou les loulous 0 1 10
argv[0] = ./mon_super_programme
argv[1] = coucou
argv[2] = les
argv[3] = loulous
argv[4] = 0
argv[5] = 1
argv[6] = 10
$

Voilà à quoi ressemble argv au moment de l'exécution argv = [ ./mon_super_programme coucou les loulous 0 1 10 ]

Tu peux voir que i va aller de ligne en ligne pour afficher chaque valeur.

Tu te poses peut-être la question de pourquoi i commence à 0 et pas 1 ? C'est comme ça que beaucoup de langages de programmation gèrent les accès mémoires, en commençant par 0.

Attention, en shell tu aurais pu utiliser les valeurs 0, 1 et 10 comme entiers. Ce n'est pas le cas ici, les valeurs sont des caractères, tu ne peux pas les utiliser pour faire des maths comme tu as l'habitude de faire en shell.

Pour transformer une chaine de caractères en entier, tu peux utiliser la fonction atoi qui signifie ascii to integer.

ASCII est un standard pour représenter des caractères.

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

int main(int argc, char *argv[])
{
    int age = 0;

    if (argc != 2)
        return 1;

    age = atoi(argv[1]);

    printf("J'ai %d ans\n", age);

    return 0;
}

Tout d'abord, la bibliothèque stdlib.h est inclue. Je te montrerai juste après comment savoir quelle bibliothèque inclure selon ce que tu veux utiliser.

Je crée la variable age à 0 en tant qu'entier. C'est une bonne pratique, toujours créer ses variables au tout début, avec des valeurs définies à 0, sauf si tu as absolument besoin d'une autre valeur d'initialisation.

Ici, je teste si argc est différent de 2. Si c'est le cas, je quitte le programme avec un exit status à 1. Tu peux voir que ce n'est pas comme en shell. Je teste les valeurs avec des signes d'égalités, pas avec -ne comme tu as pu le voir en shell.

Ensuite je dis que age est égal à l'appel de atoi sur le 1er argument. Puis je l'affiche.

Je t'invite à compiler ton programme et le tester ainsi :

$ gcc test.c -o mon_super_programme
$ ./mon_super_programme
$ echo $?
1
$ ./mon_super_programme 42
J'ai 42 ans
$ echo $?
0
$./mon_super_programme coucou
J'ai 0 ans
$ ./mon_super_programme 42coucou
J'ai 42 ans
$ ./mon_super_programme coucou42
J'ai 0 ans
$

atoi transforme ton paramètre en nombre seulement s'il trouve au début un nombre, sinon il renvoie 0.

man et bibliothèque

Pour savoir quel bibliothèque utiliser, tu vas utiliser ton meilleur ami, le man.

$ man atoi
...

Tu peux voir dans le man de atoi qu'il faut inclure stdlib.h.

Tu peux également vérifier dans le main quelle est la bibliothèque à inclure pour printf. Il s'agit bien de stdio.h.

$ man 3 printf
...

Attention à bien mettre 3 pour bien dire que c'est le printf du langage C que tu veux utiliser, pas le binaire installé sur le système.

Plus tard dans ton cursus, tu utiliseras des syscalls, des appels systèmes. Ce sont des fonctions fournies par le kernel Linux. La différence est qu'il faut demander la 2eme section du manuel.

$ man 2 write

Tu sauras avec le temps si tu cherches un syscall ou une fonction.

Recherche dans le man

Si tu veux lister les sections pour un mot en particulier dans le man, tu peux utiliser la commande suivante :

$ man -k printf
...

Tu verras que ça va générer BEAUCOUP de lignes. Tu peux filter en rajoutant un circonflexe '^' au début du mot et un dollar '$' à a fin, le tout entre simple quote.

$ man -k '^printf$'
...

Ce sont des caractères spéciaux. ^ signifie début de ligne et $ signifie fin de ligne.

$ man -k '^read$'
...

Tu peux voir que read existe dans la section 1p, 2 et 3p. Si tu n'avais pas limité la recherche avec ^ et $, tu aurais eu énormément de résultats pas très intéressants.

Types prédéfinis

Types

Tu as déjà joué avec deux types depuis le début du chapitre en C : les entiers et les pointeurs sur caractères.

Voici une liste de types que tu peux créer, je vais aussi te montrer les codes à utiliser pour les afficher avec printf.

Tu peux retrouver les informations des codes à utiliser dans man 3 printf.

Entiers

Les entiers se reconnaissent avec l'utilisation du type int. Mais tu peux ajouter des qualificateurs pour changer la façon de les utiliser.

#include <stdio.h>

int main(void)
{
    // Peut être négatif et positif
    int entier_simple = -42;
    printf("entier_simple = %d\n", entier_simple);

    // Ne peut pas être négatif
    unsigned int entier_non_signe = 333;
    printf("entier_simple = %u\n", entier_non_signe);

    // Ne peut pas avoir une grande taille
    short int petit = 32767;
    printf("petit = %d\n", petit);
    petit = petit + 1;
    printf("petit = %d\n", petit);

    long int large = 2000000000; // 2 milliards
    printf("large = %ld\n", large);

    long long int tres_large = 30000000000; // 30 milliards
    printf("tres_large = %lld\n", tres_large);

    return 0;
}

Je te laisse essayer ce code. Il n'est pas exhaustif sur les combinaisons possibes sur les entiers qu'on peut créer.

Décortiquons tout ça.

La variable entier_simple est un entier tout ce qu'il y a de plus simple. Elle peut aller dans le négatif et le positif.

La variable entier_non_signe ne peut pas avoir de signe négatif. Elle sera toujours strictement positive. La différence avec entier_simple est qu'elle peut avoir une valeur positive deux fois plus grande, mais en contrepartie elle ne peut peut pas être négative.

Que s'est-il passé avec la variable petit ? Le qualificateur short informe que la variable a une petite taille en mémoire, elle peut aller de -32768 à 32767. Si elle dépasse 32767, elle revient à la valeur la plus petite qu'elle peut prendre. On appelle ça un overflow.

Les variables large et tres_large peuvent avoir des valeurs énormes.

Tu peux ajouter unsigned avec tous les qualificateurs que tu viens de voir. Ils pourront avoir une valeur positive deux fois plus grande que les variables implicitement signed mais ne pourront jamais avoir une valeur négative.

Voici un tableau des types d'entiers que tu peux avoir, la place qu'ils prennent en mémoire et leurs limites.

NomOctetsValeurs
short int2-32768 à 32767
unsigned short int20 à 65535
int4-2147483648 à 2147483647
unsigned int40 à 4294967295
long int8-9223372036854775808 à 9223372036854775807
unsigned long int80 à 18446744073709551615
long long int8-9223372036854775808 à 9223372036854775807
unsigned long long int80 à 18446744073709551615

S'il n'y a pas de différence entre long int et long long int, c'est parce que les architectures actuelles sont des architectures 64 bits. Juste avant notre ère actuelle, les ordinateurs étaient sur des architectures 32 bits, et les types en C étaient bien différents.

L'architecture que j'utilise est de l'Intel 64 bits. Mais par exemple Apple qui a une autre architecture aura peut-être des valeurs différentes.

Pour calculer combien de nombres tu peux avoir avec un type, prends sa taille en mémoire (sous la colonne Octets du tableau), multiplie la par 8. Pourquoi ? Rappelle toi du chapitre bits et octets. Un octet et constitué de 8 bits.

Maintenant que tu as multiplié par 8, pose cette valeur en puissance de 2.

Par exemple, le short int prend 2 octets en mémoire, donc 16 bits.

\[ 2 ^{16} = 65536 \]

Tu as donc 65536 nombres possibles avec un short int. C'est pour ça que signé tu as la moitié de cette valeur disponible en négatif et pareil en positif.

Pour le formatage avec printf, tu dois utiliser %d pour les entiers signés et %u pour les entiers non signés.

Tu dois ajouter l pour un long et ll pour un long long. Selon le système sur lequel tu te trouves, vérifie toujours la taille en octet des types prédéfinis. Je te montre à la fin de ce chapitre comment faire ça.

Décimales

Tu peux également utiliser des variables à décimales. Tu en as deux : float et double.

Sur mon architecture, float prend 4 octets en mémoire et double prend 8 octets. On dit que double a une précision double.

#include <stdio.h>

int main(void)
{
    float f =  1.234567891011121314;
    double d = 1.234567891011121314;
    long double ld = 1.234567891011121314;

    printf("f  = %.15f\n", f);
    printf("d  = %.15lf\n", d);
    printf("ld = %.15Lf\n", ld);

    return 0;
}

Ici, je veux te faire comprendre la différence entre float et double.

float a une précision de 7 chiffres après la virgule, là où double a une précision de 15 chiffres après la virgule.

Dans le printf, j'utilise la notation %.15f pour demander à printf de m'afficher 15 chiffres après la virgule, même à float.

Comme tu peux le voir, au bout de quelques chiffres, la valeur affichée n'est pas du tout la même que celle affectée dans le code.

Voici un tableau récapitulatif sur l'utilisation de float et double.

NomOctets
float4
double8
long double16

Si long double peut prendre 128 bis même sur une architecture 64 bits, c'est parce qu'une partie est utilisée pour la précision, c'est à dire le nombre de chiffres possible derrière la virgule.

Caractère

Il existe un type pour les caractères : char.

Tu peux l'utiliser comme un entier pour avancer dans l'alphabet.

#include <stdio.h>

int main(void)
{
    char alpha = 'a'; // Des simple quotes, surtout pas des double quotes !
    printf("alpha = %c\n", alpha);
    printf("alpha = %d\n", alpha);
    alpha = alpha + 1;
    printf("alpha = %c\n", alpha);
    printf("alpha = %d\n", alpha);

    return 0;
}

Je vais devoir expliquer ce qu'il se passe ici un peu plus en détail.

$ gcc test.c -o mon_super_programme
$ ./mon_super_programme
alpha = a
alpha = 97
alpha = b
alpha = 98
$

le caractère 'a' est codé sur la valeur 97. Où trouver cette information ?

Dans le man ascii.

Oct   Dec   Hex   Char                        Oct   Dec   Hex   Char
────────────────────────────────────────────────────────────────────────
000   0     00    NUL '\0' (null character)   100   64    40    @
001   1     01    SOH (start of heading)      101   65    41    A
002   2     02    STX (start of text)         102   66    42    B
003   3     03    ETX (end of text)           103   67    43    C
004   4     04    EOT (end of transmission)   104   68    44    D
005   5     05    ENQ (enquiry)               105   69    45    E
006   6     06    ACK (acknowledge)           106   70    46    F
007   7     07    BEL '\a' (bell)             107   71    47    G
010   8     08    BS  '\b' (backspace)        110   72    48    H
011   9     09    HT  '\t' (horizontal tab)   111   73    49    I
012   10    0A    LF  '\n' (new line)         112   74    4A    J
013   11    0B    VT  '\v' (vertical tab)     113   75    4B    K
014   12    0C    FF  '\f' (form feed)        114   76    4C    L
015   13    0D    CR  '\r' (carriage ret)     115   77    4D    M
016   14    0E    SO  (shift out)             116   78    4E    N
017   15    0F    SI  (shift in)              117   79    4F    O
020   16    10    DLE (data link escape)      120   80    50    P
021   17    11    DC1 (device control 1)      121   81    51    Q
022   18    12    DC2 (device control 2)      122   82    52    R
023   19    13    DC3 (device control 3)      123   83    53    S
024   20    14    DC4 (device control 4)      124   84    54    T
025   21    15    NAK (negative ack.)         125   85    55    U
026   22    16    SYN (synchronous idle)      126   86    56    V
027   23    17    ETB (end of trans. blk)     127   87    57    W
030   24    18    CAN (cancel)                130   88    58    X
031   25    19    EM  (end of medium)         131   89    59    Y
032   26    1A    SUB (substitute)            132   90    5A    Z
033   27    1B    ESC (escape)                133   91    5B    [
034   28    1C    FS  (file separator)        134   92    5C    \  '\\'
035   29    1D    GS  (group separator)       135   93    5D    ]
036   30    1E    RS  (record separator)      136   94    5E    ^
037   31    1F    US  (unit separator)        137   95    5F    _
040   32    20    SPACE                       140   96    60    `
041   33    21    !                           141   97    61    a
042   34    22    "                           142   98    62    b
043   35    23    #                           143   99    63    c
044   36    24    $                           144   100   64    d
045   37    25    %                           145   101   65    e
046   38    26    &                           146   102   66    f
047   39    27    '                           147   103   67    g
050   40    28    (                           150   104   68    h
051   41    29    )                           151   105   69    i
052   42    2A    *                           152   106   6A    j
053   43    2B    +                           153   107   6B    k
054   44    2C    ,                           154   108   6C    l
055   45    2D    -                           155   109   6D    m
056   46    2E    .                           156   110   6E    n
057   47    2F    /                           157   111   6F    o
060   48    30    0                           160   112   70    p
061   49    31    1                           161   113   71    q
062   50    32    2                           162   114   72    r
063   51    33    3                           163   115   73    s
064   52    34    4                           164   116   74    t
065   53    35    5                           165   117   75    u
066   54    36    6                           166   118   76    v
067   55    37    7                           167   119   77    w
070   56    38    8                           170   120   78    x
071   57    39    9                           171   121   79    y
072   58    3A    :                           172   122   7A    z
073   59    3B    ;                           173   123   7B    {
074   60    3C    <                           174   124   7C    |
075   61    3D    =                           175   125   7D    }
076   62    3E    >                           176   126   7E    ~
077   63    3F    ?                         │ 177   127   7F    DEL

La première colonne est la valeur en octal (base 8), la deuxième en base 10, la troisième en base 16 et enfin la dernière le caractère codé derrière ces valeurs.

Tu peux retrouver le 'a' minuscule à la valeur 97.

Retiens bien ça, ça sera très important pour la suite de ton cursus.

NomOctetsValeurs
char1-128 à 127
unsigned char10 à 255

Void

void est un type particulier qui signifie pas de type. Il n'a pas de taille.

Tu verras plus tard comment il est utilisé.

sizeof

Pour avoir la taille d'un type, tu peux utiliser la macro sizeof. Elle te donne en octet la taille prise par le type donné en paramètre. Pour information, sizeof renvoie un long unsigned int.

#include <stdio.h>

int main(void)
{
    printf("sizeof (char): %lu\n", sizeof (char));
    printf("sizeof (char*): %lu\n", sizeof (char*));

    printf("sizeof (int): %lu\n", sizeof (int));
    printf("sizeof (int*): %lu\n", sizeof (int*));
    printf("sizeof (short int): %lu\n", sizeof (short int));
    printf("sizeof (long int): %lu\n", sizeof (long int));
    printf("sizeof (long long int): %lu\n", sizeof (long long int));
    printf("sizeof (long long int*): %lu\n", sizeof (long long int*));

    printf("sizeof (float): %lu\n", sizeof (float));
    printf("sizeof (float*): %lu\n", sizeof (float*));

    printf("sizeof (double): %lu\n", sizeof (double));
    printf("sizeof (long double): %lu\n", sizeof (long double));
    printf("sizeof (long double*): %lu\n", sizeof (long double*));

    printf("sizeof (void*): %lu\n", sizeof (void*));

    return 0;
}
$ gcc test.c -o mon_super_programme
$ ./mon_super_programme
sizeof (char): 1
sizeof (char*): 8
sizeof (int): 4
sizeof (int*): 8
sizeof (short int): 2
sizeof (long int): 8
sizeof (long long int): 8
sizeof (long long int*): 8
sizeof (float): 4
sizeof (float*): 8
sizeof (double): 8
sizeof (long double): 16
sizeof (long double*): 8
sizeof (void*): 8
$

Les valeurs avec des étoiles '*' sont ce qu'on appelle des pointeurs. Tu verras ce que c'est plus tard. Sache qu'un pointeur fait toujours 8 octets.

Chaines de caractères

Je ne vais pas encore détailler ce qu'est une chaine de caractères, mais je vais te montrer comment en créer que tu puisses jouer avec.

char *string = "My name Lules. Gé Lules";

printf("string: %s\n", string);

Comme tu peux le voir, ça utilise le mot-clé char de caractère, mais avec l'utilisation d'un pointeur en plus. Et tu dois définir ta chaine de caractères entre guillements.

Sache juste que tu ne peux pas modifier les chaines de caractères définies de cette manière. Tu verras comment réellement jouer avec plus tard.

Fonctions

Une fonction est un bout de code que tu peux appeler quand tu veux.

Une fonction possède un identifiant (un nom), un type de retour et une entrée pour des paramètres. C'est ce qu'on appelle la signature.

Voici comment définir une fonction et sa portée (la zone qui délimite le début et la fin du code qui sera exécuté).

Les variables déclarées et définies dans une fonction ne sont vivantes que pendant l'exécution de cette fonction. Au retour d'une fonction, toutes les variables disparaissent.

type identifiant(type_param1 param1, type_param2 param2)
{
}

Tu n'es pas obligé de mettre des paramètres ou de valeurs de retour. Tu peux utiliser le type void pour ça.


// Ne retourne rien et ne prend rien en entrée
void fonction_1(void)
{
}

// Retourne un float et ne prend rien en entrée
float fonction_2(void)
{
}

// Ne retourne rien et prend un entier en entrée
void fonction_3(int entier)
{
}

// Retourne un caractère et prend 1 float et 1 caractère en entrée
char fonction_4(float flottant, char caractere)
{
}

Comme avec la fonction main, il ne te reste plus qu'à mettre le code entre les accolades.

Attention, si tu veux créer une fonction, pour le moment écrit la au dessus de la fonction main.

Le compilateur est un outil puissant mais pas très intelligent. Si ta fonction main appelle la fonction fonction_1, elle doit être définie au dessus de la fonction main pour qu'il sache qu'elle existe déjà. Si elle est en dessous, il te dira qu'il ne trouve pas ta fonction. Le compilateur lit un code source comme nous : de gauche à droite de haut en bas.

Crée une fonction qui prend deux entiers en entrée et retourne l'addition des deux.

#include <stdio.h>

int addition(int a, int b)
{
    return a + b;
}

int main(void)
{
    int val_1 = 42;
    int val_2 = 51;
    int result = addition(val_1, val_2);

    printf("result = %d\n", result);

    return 0;
}

Bravo ! Tu viens de créer ta première fonction. Quelle fierté. Tant d'autres t'attendent déjà.

Le positionnement des variables est important. val_1 enverra sa valeur à a et val_2 enverra sa valeur à b.

Il faut que les variables que tu envoies soient du même type. Si j'avais défini val_1 comme étant un float, mon code aurait été faux.

Petit exercice pratique : Crée 4 fonctions pour les 4 opérations mathémathiques que tu as appris plus jeune : addition, soustraction, multiplication et division.

Voici une correction... sera t-il parfaite ?

int addition(int a, int b)
{
    return a + b;
}

int soustraction(int a, int b)
{
    return a - b;
}

int multiplication(int a, int b)
{
    return a * b;
}

int division(int a, int b)
{
    return a / b;
}

Penses-tu que tout fonctionne correctement avec ce que tu connais des mathématiques ?

Il y a une petite erreur : si tu envoies 0 en deuxième paramètre à la fonction division, il y aura une division par 0.

Voici une correction possible : si b vaut 0, alors on retourne 0.

int division(int a, int b)
{
    if (b == 0)
    {
        return 0;
    }

    return a / b;
}

Pour tester une égalité, tu dois utiliser la double égalité '=='. Avant que tu n'essaies, non, ça ne fonctionne pas avec les chaines de caractères. Mais ça fonctionne sur les caractères uniques.

Tu verras plus tard comment agrémenter ton code de tests et conditions.

Quitter une fonction qui ne renvoie rien

Si tu fois quitter une fonction qui ne renvoie rien (void), tu peux utiliser le mot-clé return seul.

void positive_printer(int n)
{
    if (n < 0)
    {
        return;
    }

    printf("n: %d\n");
}

Mathématiques

Tu viens de voir précédemment comment faire des opérations arithématiques simples avec l'addition, la soustraction, la multiplication et la division. Mais il reste quelques particularité à découvrir.

Modulo

Tu vas vu la division, mais tu n'as pas encore vu le reste de la division.

Tu dois utiliser le caractère %.

int valeur = 666;
int diviseur = 111;
int reste = valeur % diviseur;

printf("reste de %d par %d = %d%d\n", valeur, diviseur, reste);

Parité

Si tu veux savoir si un nombre est pair ou impair, tu peux vérifier le reste de la division par 2. S'il reste 0 alors le nombre est pair, s'il reste 1, alors le nombre est impair.

int pair = 10;
int impair = 11;

int reste = pair % 2;
printf("%d\n", reste);

reste = impair % 2;
printf("%d\n", reste);

Sucre syntaxique

Pour ajouter une valeur à une variable en C, tu serais très tenté de l'écrire de cette façon :

int i = 0;

// Du code...

i = i + 1;

Mais tu peux l'écrire plus rapidement de cette façon :

int i = 0;

// Du code...

i += 1; // => i = i + 1

Ca revient à la même chose. Tu peux évidemment mettre un autre signe que le signe '+' et n'importe quelle expression à droite du égal.

Si tu veux par exemple diviser une valeur par 10 :

int valeur = 5467;

// Du code

valeur /= 10; // => valeur = valeur / 10

C'est ce qu'on appelle du sucre syntaxique. La syntaxe est adoucit pour la rendre plus agréable.

pré-incrémentation et post-incrémentation

Tu peux écrire encore plus rapidement une incrémentation en C avec l'opérateur ++.

int i = 0;

// Du code

i++; // => i = i + 1

// Du code

++i; // => i = i + 1

Il y a cependant une différence entre les deux.

La première forme, i++, est une pré-incrémentation. Une autre variable peut récupérer la valeur de i avant que i ne s'incrémente.

La deuxième forme, ++i, est une post-incrémentation. i va s'incrémenter et après ça, une autre variable peut récupérer la nouvelle valeur de i.

Voici un exemple :

int i = 10;

printf("i: %d\n", i);

int j = i++; // => j = 10 ; i = 11

printf("i: %d | j = %d\n", i, j);

int k = ++i; // => k = 12 ; i = 12

printf("i: %d | k = %d\n", i, k);

Tu peux faire la même logique avec l'opérateur de soustraction '-'.

Bitwise

Logic bit

En plus de faire des opérations sur les variables, tu peux faire des opérations directement sur leurs bits.

Voici les portes logiques que tu connais en C :

  • ~ : NOT
  • & : AND
  • | : OR
  • ^ : XOR
int maximum = ~0; // => NOT(00000000000000000000000000000000) =  11111111111111111111111111111111 = 4294967295

int op_and = 42 & 51; //       101010
                      // AND
                      //       110011
                      //       ======
                      //       100010 = 34
printf("op_and = %d\n", op_and);

int op_or = 42 | 51; //        101010
                     // OR
                     //        110011
                     //        ======
                     //        111011 = 51
printf("op_or = %d\n", op_or);

int op_xor = 42 ^ 51; //       101010
                      // XOR
                      //       110011
                      //       ======
                      //       011001 = 25
printf("op_xor = %d\n", op_xor);

Shifting

Tu peux aussi décaler des bits sur la gauche et sur la droite.

Prenons la valeur 42 en binaire, codée sur un int (4 octets, donc 32 bits).

00000000000000000000000000101010

Avec les opérateurs << et >>, je peux décaler les bits sur la gauche et sur la droite. Attention si un bit disparaît d'un côté, il ne réapparait pas de l'autre.

int valeur = 42;      // valeur = 00000000000000000000000000101010 = 42

valeur = valeur << 1; // valeur = 00000000000000000000000001010100 = 84

valeur = valeur << 2; // valeur = 00000000000000000000000101010000 = 336

valeur = valeur >> 5; // valeur = 00000000000000000000000000001010 = 10

valeur = valeur << 1; // valeur = 00000000000000000000000000010100 = 20

Tu peux aussi écrire ces instructions de ces façons :

valeur <<= 2;
valeur >>= 2;

L'inverseur logique

Il existe un deuxième opérateur d'inversion en C qui est le point d'exclamation '!'. Celui-ci n'inverse pas les valeurs des bits mais la véracité d'une variable.

En C, la valeur 0 vaut Faux et n'importe quelle autre valeur vaut Vrai. C'est exactement l'inverse du shell.

Voici comment l'utiliser :

int zero = 0;
int one = 1;
int two = 2;

zero = !zero; // => zero = 1
one = !one;   // => one = 0
two = !two;   // => two = 0

zero = !zero; // => zero = 0
one = !one;   // => one = 1
two = !two;   // => two = 1

Structures de contrôles

Faire du code, c'est bien. Faire du code qui n'exécute pas toujours la même chose, c'est mieux.

Tu vas apprendre ici à créer des conditions en C.

if else

Le C n'a que deux mot-clés pour créer des conditions, là où le shell en a trois.

Le mot-clé if prend en compte une condition, le mot-clé else n'en prend pas sauf s'il est suivi d'un if, ce qui donne la construction else if.

Tu ne peux pas facilement faire des comparaisons de chaines de caractères comme en shell, en C tout se fait avec des entiers. Mais tu peux tester des chaines de caractères avec des fonctions de la bibliothèque C qui vont retourner des entiers pour dire Vrai ou Faux.

Entiers

Pour tester des entiers, tu peux utiliser les opérateurs :

  • < : strictement inférieur
  • <= : inférieur ou égal
  • == : égal
  • != : différent
  • >= : supérieur ou égal
  • > : strictement supérieur
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int secret = 42;
    int guess = atoi(argv[1]);

    if (guess < secret)
    {
        printf("Plus grand\n");
    }
    else if (guess > secret)
    {
        printf("Plus petit");
    }
    else // Si ce n'est ni plus petit ni plus grand, alors c'est égal
    {
        printf("Bien deviné !");
    }

    return !(guess == secret);
}

La dernière ligne doit être décortiquée.

(guess == secret)

retourne 1 si c'est Vrai et 0 si c'est Faux.

L'opérateur d'inversion '!' va inverser la valeur logique.

// Dans le cas où guess == secret

!(guess == secret) // => !(1)
!(1) // => 0

// Dans le cas où guess != secret
!(guess != secret) // => !(0)
!(0) // => 1

Pourquoi faire ça ?

Parce qu'en shell, 0 est Vrai et autre chose que 0 est Faux. Donc pour garder une cohérence avec le shell qui va recevoir la valeur de retour de ton programme, j'ai besoin que l'exit status soit à 0 si le nombre deviné est bon et que l'exit status soit à 1 si le nombre deviné n'est pas le bon.

Chaines de caractères

Si tu veux tester deux chaines de caractères, tu peux utiliser la fonction strcmp. Je t'invite à lire le man de cette fonction man 3 strcmp, spécialement la fin du manuel ;)

Elle prend deux chaines de caractères en paramètres, retourne 0 si les deux sont égales, une valeur négative si la première chaine de caractères est alphabétiquement plus petite que la deuxième, une valeur positive dans le cas inverse.

L'ordre alphabétique est basée sur la table ASCII que tu as vu précédemment.

Ainsi

char *s1 = "a";
char *s2 = "B";

s2 est plus petit que s1, car dans la table ASCII, 'B' vient avant 'a'.

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

int main(int argc, char *argv[])
{
    char *secret = "pediluve";
    char *guess = argv[1];
    int result = strmpc(guess, secret);

    if (result != 0)
    {
        printf("Mauvais mot de passe\n");
    }
    else
    {
        printf("Bon mot de passe\n");
    }

    return result;
}

Attention, rappelle toi quand même qu'en shell, l'exit status va de 0 à 255, si tu retournes une valeur négative, elle sera transformée en une autre valeur.

switch

Pour éviter de faire des if else à rallonge, tu peux tester une variable sur un switch et faire un code beaucoup plus élégant et agréable à lire.

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

// Ici un ensemble de fonctions

int main(int argc, char *argv[])
{
    int choice = atoi(argv[1]);

    switch (choice)
    {
        case -1:
            printf("En route pour le donjon :\n");
            dungeon();
            break;
        case 0:
            printf("Vous êtes déjà dans le lobby\n");
            break;
        case 1:
            printf("En route pour la tour !\n");
            tower();
            break;
        case 666:
            printf("En route pour le boss\n");
            boss();
            break;
        default:
            printf("Je ne peux accéder à votre requête\n");
    }

    return 0;
}

Tu peux voir que c'est beaucoup plus lisible qu'un enchaînement de if else.

Le mot-clé break sert à partir du switch si un des cas était Vrai et pour éviter de tester les autres cas suivants.

Le mot-clé default agit comme un else, si tous les cas précédents étaient faux, alors le code de default sera exécuté.

Tableaux

Un tableau est une collection de valeurs du même type. On peut accéder aux éléments par des index.

Un tableau se crée ainsi :

type nom_tableau[nombre_elements] = { valeur_1, valeur_2, valeur_3 };

On peut mettre autant d'éléments que l'on veut.

Tableaux de types simples

Pour créer un tableau d'entiers, de floats ou de char, il suffit de remplir le tableau avec les bonnes valeurs.

int variable = 666;
int levels[6] = {0, 1, -1, 2600, variable, 42 };

char alpha[3] = {'x', 'y', 'z'};

Pour accéder à un élément du tableau, il suffit d'utiliser cette notation :

element = array[index];

A savoir : l'index commence toujours à 0.

Ainsi si je reprends le tableaux levels, il faut le lire avec les index suivants :

Index012345
Valeur01-1260066642

Ainsi, pour accéder à un élémeent d'un tableau de type int :

int levels[6] = {0, 1, -1, 2600, variable, 42 };

int level = levels[2]; // => level = -1

levels[0] = 21; // array = 21 1 -1 2600 666 42

Attention à ne pas utiliser un index plus grand que le nombre d'éléments dans le tableau. Si tu dépasses, tu récupéreras une valeur ailleurs en mémoire, que tu ne contrôles pas.

Tableau à zéro

Si tu veux remplir une partie du tableau et laisser le reste à 0 ou même l'initialiser complètement à 0, tu n'es pas obligé de tout définir, le compilateur va pour toi mettre la partie non définie à 0.

int array_1[10] = { 1, 1, 1 }; // => 1 1 1 0 0 0 0 0 0 0
int array_2[10] = { 0 }; // => 0 0 0 0 0 0 0 0 0 0

Nombre d'éléments calculé à la compilation

Si tu ne sais pas combien d'éléments ton tableau va recevoir, tu n'es pas obligé de mettre une taille, le compilateur comptera le nombre d'éléments que tu as écrit entre les accolades.

int array[] = { 1, 2, 3, 4, 5 }; // => 5 éléments

Taille d'un tableau

Pour avoir la taille utilisée par un tableau en mémoire, tu peux utiliser l'opérateur que tu connais, sizeof.

A ton avis, combien d'octets prend en mémoire un tableau de type int à 10 éléments ?

Si un int utilise 4 octets, alors 10 int vont en utiliser 10 fois plus, donc 40 octets.

int array[10] = { 0 };
int size = sizeof (array);

printf("size of array: %lu\n", size);

Nombre d'éléments dans un tableau

Pour savoir le nombre d'éléments dans un tableau sans compter un par un, on peut utiliser sizeof.

int array[] = { beaucoup mais alors vraiment beaucoup de valeurs };

int nbr_elements = sizeof (array) / sizeof (array[0]);

Que fait-on exactement ?

sizeof (array) donne la taille du tableau en octets

sizeof (array[0]) donne la taille d'un élément du tableau en octets

Si on divise la taille totale par la taille d'un élément, nous avons bien le nombre d'éléments dans un tableau.

On utilise array[0] car on ne sait pas combien d'éléments a le tableau, mais étant donné qu'il doit en avoir au moins 1, on utilise tout simpelemnt le premier qui est obligatoirement existant.

Tableaux pour chaines de caractères

Je t'avais dis que pour faire une chaîne de caractères, tu devais écrire :

char *text = "salut les loulous";

et que cette chaîne de caractères est constante et que tu ne peux pas la modifier.

Tu peux utiliser la notation de tableau pour avoir une chaîne de caractères que tu peux modifier.

char text[] = "salut les poutous";
printf("%s\n", text);
text[10] = 'l';
text[13] = 'l';
printf("%s\n", text);

Chaîne de caractères

Il y a une chose que je n'ai pas mentionnée quant aux chaînes de caractères, c'est comment elles sont réellement construites par le compilateur.

Quand printf affiche ta chaîne, il ne peut pas calculer la taille avec la technique de sizeof.

Pour ça, le compilateur va ajouter une valeur très spéciale à la fin de la chaîne de caractères, la valeur \0.

Ainsi en parcourant ta chaîne de caractères, printf sait où s'arrêter : une fois qu'il a atteind \0.

Voici comment est en mémoire une chaîne de caractères.

Pour la chaîne de caractères suivante :

char text[] = "coucou";
Index0123456
Valeurcoucou\0

Tu peux voir dans le man 1 ascii que le caractère \0 a la valeur 0.

Pointeurs

Il est dit par certains que la notion de pointeur est ce qu'il y a de plus difficile à comprendre en C. Aussi, tu vas répéter avec moi ces quelques phrases.

Un pointeur est une adresse.

Un pointeur est une adresse.

Un pointeur est une adresse.

Et pour être un peu plus correct.

Un pointeur est une adresse sur un type.

Un pointeur est une adresse sur un type.

Un pointeur est une adresse sur un type.

Voilà. Maintenant tu vas voir, les pointeurs, c'est pas si terrible.

Mémoire

Il faut comprendre que quand tu exécutes ton programme, il est copié dans la RAM de ton PC. Tout est dans une mémoire virtuelle. Qui va de 0 à 264.

La mémoire fonctionne comme un tableau. Il y a l'adresse 0, puis 1, puis 2, ...

Et dans ces cases mémoires, se trouvent tout ton programme, avec les valeurs de tes variables.

Initialiser un pointeur à zéro

Avant d'utiliser un pointeur, tu vas voir comment initialiser unpointeur à zéro.

Pour cela, tu vas lui affecter la valeur NULL. C'est un alias sur un pointeur sur void à la valeur 0. C'est à dire (void *)0.

int *ptr = NULL;

Voilà, si tu ne sais pas encore où pointera ton pointeur, par sécurité, met le à NULL.

Afficher l'adresse d'une variable

Pour afficher l'adresse d'une variable, on va utiliser l'opérateur &.

#include <stdio.h>

int main(void)
{
    int value = 42;
    void *addr_value = &value;

    printf("Adresse de value = %p\n", addr_value);

    return 0;
}
$ ./mon_super_programme
Adresse de value = 0x7ffd2cd0df1c
$

Comme tu peux le voir, le pointeur addr_value a comme valeur une adresse.

Un pointeur est une adresse.

Une valeur qui commence par 0x est de l'hexadécimal.

Tu auras très probablement une autre valeur affichée. D'ailleurs chaque exécution devrait t'afficher une valeur différente (c'est une sécurité mise en place par Linux).

Déréférencement

C'est là où ça devient difficile.

Rappelle toi : Un pointeur est une adresse sur un type.

Reprenons le code précédent, je vais le modifier un peu. Le code précédent servait à avoir l'adresse de value, ici je vais avoir un pointeur sur le type int pour récupérer sa valeur autrement que par value.

#include <stdio.h>

int main(void)
{
    int value = 42;
    int *ptr_value = &value;

    return 0;
}

value est une variable qui vaut 42. Cette variable a une valeur enregistrée dans une adresse.

addr_value est un pointeur. Sa valeur est une adresse, celle où se trouve la valeur de value.

Pour pouvoir accéder à la case mémoire qui contient 42 depuis un pointeur, il faut le déréférencer.

Rappelle toi, la mémoire fonctionne comme un tableau.

Un pointeur sur un int, tu peux le voir comme un tableau de int à un élément.

Tu peux donc utiliser ptr_value[0] pour accéder à la valeur 42.

#include <stdio.h>

int main(void)
{
    int value = 42;
    int *ptr_value = &value;

    printf("Valeur de value = %d\n", ptr_value[0]);

    return 0;
}
$ ./mon_super_programme
Valeur de value = 42
$

Encore du sucre syntaxique

Tu verras dans d'autres cours que pour déréférencer un pointeur, il faut utiliser, avec mon exemple, l'écriture suivant : *ptr_value

#include <stdio.h>

int main(void)
{
    int value = 42;
    int *ptr_value = &value;

    printf("Valeur de value = %d\n", *ptr_value);

    return 0;
}

Le code est exactement le même. Parce que l'utilisation des crochets pour l'accès tableau est du sucre syntaxique.

En réalite, quand tu écris array[0], la mémoire utilise cette notation : *(array + 0)) qui est la même chose que *array

Et quand tu écris array[1], la mémoire utilise cette notation : *(array + 1))

Et quand tu écris array[2], la mémoire utilise cette notation : *(array + 2))

etc.

L'opérateur * sert à déréférencer un pointeur pour accéder à la case mémoire vers laquelle il pointe.

On peut ainsi modifier la valeur d'une variable depuis un pointeur.

#include <stdio.h>

int main(void)
{
    int value = 42;
    int *ptr_value = &value;

    *ptr_value = 2600; // Equivalent à "ptr_value[0] = 2600;"

    printf("Valeur de value = %d\n", *ptr_value);

    return 0;
}
$ ./mon_super_programme
Valeur de value = 2600
$

Bon c'est bien, mais c'est quoi l'utilité réelle des pointeurs ?

L'utilité des pointeurs

Tu peux utiliser les pointeurs pour changer la valeur d'une ou plusieurs variables dans une autre fonction.

Quand tu envoies une variable en paramètre d'une fonction, tu envoies en réalité une copie locale. Si tu modifies la variable dans la fonction appelée, elle reviendra à sa valeur d'origine dans la fonction appelante.

#include <stdio.h>

void example(int value)
{
    value = 2600;
    printf("Valeur de value = %d\n", value);
}

int main(void)
{
    int value = 42;
    printf("Valeur de value = %d\n", value);

    example(value);

    printf("Valeur de value = %d\n", value);

    return 0;
}
$ ./mon_super_programme
Valeur de value = 42
Valeur de value = 2600
Valeur de value = 42
$

Utilise la notion de pointeur pour modifier la valeur de value dans la fonction example.

#include <stdio.h>

void example(int *value)
{
    *value = 2600;
    printf("Valeur de value = %d\n", *value);
}

int main(void)
{
    int value = 42;
    printf("Valeur de value = %d\n", value);

    example(&value);

    printf("Valeur de value = %d\n", value);

    return 0;
}
$ ./mon_super_programme
Valeur de value = 42
Valeur de value = 2600
Valeur de value = 2600
$

Pointeurs sur caractères

Voilà donc ce qu'il se cache derrière les pointeurs sur caractères. Au lieu de n'avoir qu'une valeur en mémoire de type char, il y a plusieurs valeurs de type char en mémoire les unes à la suite des autres, accessibles avec les crochets du tableau.

char *ptr_char = "vive le c";
char array_char[] = "vive linux";

Le premier v de chaque chaînes de caractères est enregistré quelque part en mémoire, et les autres lettres qui suivent sont à la suite dans la mémoire. On utilise la notion de pointeurs pour accéder à ses valeurs.

La différence, c'est qu'un tableau possède une taille fixe en mémoire, alors qu'un pointeur fait 8 octets, car il ne pointe que sur une valeur. Il peut accéder aux prochaines valeurs comme un tableau mais est défini autrement en mémoire.

Arithmétique des pointeurs, 1ere partie

Tu peux changer la valeur d'un pointeur et te déplacer vers la suite de la chaine.

Pour ça, il suffit d'incrémenter le pointeur.

char *str = "Encore des pointeurs";

printf("%c\n", *str); // => 'E'

str = str + 1;

printf("%c\n", *str); // => 'n'

Boucles

Les boucles sont un moyen de répéter du code autant de fois que nécessaire.

while

La boucle while exécute du code tant que la condition est Vraie.

#include <stdio.h>

int main(void)
{
    int i = 0;

    while (i < 10)
    {
        printf("%d\n", i);
        ++i;
    }

    return 0;
}
$ ./mon_super_programme
0
1
2
3
4
5
6
7
8
9
$

Si la condition est tout de suite Fausse, alors la boucle ne sera jamais exécutée.

#include <stdio.h>

int main(void)
{
    int i = 10;

    while (i < 10)
    {
        printf("%d\n", i);
        ++i;
    }

    return 0;
}
$ ./mon_super_programme
$

do while

do while est une boucle qui va exécuter au moins une fois le code de la boucle.

#include <stdio.h>

int main(void)
{
    int i = 0;

    do
    {
        printf("%d\n", i);
        ++i;
    } while (i < 10);

    return 0;
}

Note le point-virgule ';' à la toute fin de la ligne avec le while.

$ ./mon_super_programme
0
1
2
3
4
5
6
7
8
9
$

Le code sera toujours exécuté au moins une fois.

#include <stdio.h>

int main(void)
{
    int i = 10;

    do
    {
        printf("%d\n", i);
        ++i;
    } while (i < 10);

    return 0;
}
$ ./mon_super_programme
10
$

for

Une boucle for est séparée en trois instructions.

  • INITIALISATION qui est exécutée une seule fois au tout début de la boucle
  • CONDITION qui est testée à chaque itération
  • INSTRUCTION qui est exécutée à chaque itération après le test de la condition, sauf la première fois
for (INITIALISATION; CONDITION; INSTRUCTION)
{
    // Le code
}

Note qu'il y a une séparation avec des point-virgules ';'.

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 10; ++i)
    {
        printf("%d\n", i);
    }

    return 0;
}
$ ./mon_super_programme
0
1
2
3
4
5
6
7
8
9
$

Tu peux aussi t'en servir pour parcourir les chaînes de caractères.

#include <stdio.h>

int my_strlen(char *str)
{
    int result = 0;
    for (int i = 0; str[i] != '\0'; ++i)
    {
        ++result;
    }

    return result;
}

int main(void)
{
    char *ptr = "coucou les loulous";
    int ptr_size = my_strlen(ptr);

    printf("Longueur de '%s' = %d\n", ptr, ptr_size);

    return 0;
}
$ ./mon_super_programme
Longueur de 'coucou les loulous' = 18
$

Ici tu recodes la fonction strlen qui retourne le nombre de caractères dans une chaîne de caractères.

Je t'invite à lire man 3 strlen.

Exercices

Voici une liste d'exercices pour t'entrainer sur tout ce que tu as vu.

Les corrections sont au prochain chapitre.

Fonctions simples

Ecris le code des fonctions suivantes pour additionner, soustraire, multiplier et diviser des entiers ensemble.

Attention, si tu viens à diviser par 0, retourne 0.

int addition(int a, int b);

int substraction(int a, int b);

int multiplication(int a, int b);

int divide(int a, int b);

Ecris le code des fonctions and, or, xor, not qui retournent les opérations bitwises correspondantes.

int and(int a, int b);

int or(int a, int b);

int xor(int a, int b);

int not(int a);

Ecris le code qui retourne le double, le carré et le cube d'un entier.

int double_int(int a);

int square(int a);

int cube(int a);

Ecris le code qui retourne la valeur de 2n. Indice pour être efficace : utilise le shifting.

int power_of_two(unsigned int n);

Ecris le code de la fonction int_len qui retourne le nombre de caractères utilisés pour afficher un entier.

  • Si n vaut 0 : retourne 1
  • Si n vaut -1 : retourne 2
  • Si n vaut 1 : retourne 1
  • Si n vaut 2600 : retourne 4
  • Si n vaut -42 : retourne 3
  • etc.
int int_len(n);

Ecris le code des fonctions

is_lowercase qui retourne 1 si le caractère envoyé est une lettre de l'alphabet en minuscule, 0 sinon.

is_uppercase qui retourne 1 si le caractère envoyé est une lettre de l'alphabet en majuscule, 0 sinon.

is_num qui retourne 1 si le caractère envoyé est un caractère qui représente un chiffre compris entre 0 et 9, 0 sinon.

Regarde bien le man ascii et réfléchis à comment tester si c'est bien dans la partie minuscule, majuscule ou numérique.

int is_lowercase(char c);
int is_uppercase(char c);
int is_num(char c);

Ecris le code de la fonction is_alphanum qui retourne 1 si le caractère en paramètre correspond à une lettre de l'alphabet en minuscule, ou un chiffre compris entre 0 et 9, 0 sinon.

Tu peux tout à fait appeller les 3 dernières fonctions depuis la fonction is_alphanum, pense à bien les écrire au dessus de la fonction is_alphanum.

int is_lowercase(char c)
{
    // Le code
}

int is_uppercase(char c)
{
    // Le code
}

int is_num(char c)
{
    // Le code
}

int is_alphanum(char c)
{
    // Le code
}

Ecris la fonction graduate qui :

  • Affiche "Mauvais" si la note est compris dans l'intervalle [0;8]
  • Affiche "Moyen" si la note est compris dans l'intervalle [9;12]
  • Affiche "Bien" si la note est compris dans l'intervalle [13;16]
  • Affiche "Très bien" si la note est compris dans l'intervalle [17;19]
  • Affiche "Parfait" si la note est à 20
  • Affiche "Quoi ?" si la note n'est pas compris dans l'intervalle [0;20]
void graduate(int n);

Notions de pointeurs

Ecris le code de la fonction moyenne qui renvoie la moyenne de 3 floats en les remettant à 0 dans la fonction.

Une moyenne sur 42 éléments se calcule ainsi : (valeur_1 + valeur 2 + ... + valeur_42) / 42

float moyenne(float *a, float *b, float *c);

Ecris le code de la fonction odd_even qui affiche :

  • C'est pair : Si la valeur pointée est paire
  • C'est impair : Si la valeur pointée est impair
void odd_even(int *value);

Ecris le code swap qui échange les valeurs pointées de deux pointeurs.

Si tu y arrives, fais le sans créer de variable dans la fonction. Sinon, ce n'est pas grace, ça sera déjà bien !

void swap(int *a, int *b)
{
    // Le code
}

int main(void)
{
    int a = 42;
    int b = 2600;

    printf("a = %d | b =  %d\n"); // a = 42 | b = 2600
    swap(&a, &b);
    printf("a = %d | b =  %d\n"); // a = 2600 | b = 42

    return 0;
}

Chaînes de caractères

Ecris le code de la fonction strlen qui renvoie le nombre de caractères dans une chaîne de caractères

unsigned int strlen(char *s);

Ecris le code de la fonction upper qui prend un tableau de caractères en entrée et qui change les caractères minuscules en caractères majuscules.

Ecris le code de la fonction lower qui prend un tableau de caractères en entrée et qui change les caractères majuscules en caractères minuscules.

Tu peux tout à fait utiliser les fonctions uppercase et lowercase.

int uppercase(char c)
{
    // Le code
}

int lowercase(char c)
{
    // Le code
}

void upper(char *str)
{
    // Le code
}

void lower(char *str)
{
    // Le code
}

int main(void)
{
    char str[] = "J'aImE b3AuC0uP l3 PeDiLuvE";

    printf("%s\n", str); // "J'aImE b3AuC0uP l3 PeDiLuvE";

    upper(str);

    printf("%s\n", str); // "J'AIME B3AUC0UP L3 PEDILUVE";

    lower(str);

    printf("%s\n", str); // "j'aime b3auc0up l3 pediluve";

    return 0;
}

Ecris le code de la fonction vowels qui retourne le nombre de voyelles présentes dans une chaîne de caractères.

int vowels(char *str);

Ecris le code de la fonction miniatoi qui retourne l'entier représenté sous chaînes de caractères. Dès que tu rencontres un caractère qui n'est pas numérique, retourne ce que tu as déjà calculé.

  • Si str vaut "0", retourne 0
  • Si str vaut "2", retourne 2
  • Si str vaut "-42", retourne -42
  • Si str vaut "2600", retourne 2600
  • Si str vaut "pediluve", retourne 0
  • Si str vaut "pediluve666", retourne 0
  • Si str vaut "69pediluve777", retourne 69
  • Si str vaut "-pediluve7", retourne 0

Tu peux utiliser la fonction is_num que tu as écris plus tôt.

int miniatoi(char *str);

Avec ça tu as déjà quelques exercices pour apprendre à faire un peu de C et être à l'aise pour la suite des évènements.

Corrections

Fonctions simples

int addition(int a, int b)
{
    return a + b;
}

int substraction(int a, int b)
{
    return a - b;
}

int multiplication(int a, int b)
{
    return a * b;
}

int divide(int a, int b)
{
    if (b == 0)
    {
        return 0;
    }

    return a / b;
}

int and(int a, int b)
{
    return a & b;
}

int or(int a, int b)
{
    return a | b;
}

int xor(int a, int b)
{
    return a ^ b;
}

int not(int a)
{
    return ~a;
}

int double_int(int a)
{
    return a + a;
}

int square(int a)
{
    return a * a;
}

int cube(int a)
{
    return a * a * a;
}

int power_of_two(unsigned int n)
{
    return 1 << n;
}
int int_len(n)
{
    int result = 0;
    int sign = (n < 0);

    while (n != 0)
    {
        n = n / 10;
        ++result;
    }

    return result + sign;
}
int is_lowercase(char c)
{
    return ('a' <= c && c <= 'z');
}

int is_uppercase(char c);
{
    return ('A' <= c && c <= 'Z');
}

int is_num(char c);
{
    return ('0' <= c && c <= '9');
}

int is_alphanum(char c)
{
    return (is_lowercase(c) || is_uppercase(c) || is_num(c));
}
void graduate(int n)
{
    if (0 <= n && n <= 8)
    {
        printf("Mauvais\n");
    }
    else if (9 <= n && n <= 12)
    {
        printf("Moyen\n");
    }
    else if (13 <= n && n <= 16)
    {
        printf("Bien\n");
    }
    else if (17 <= n && n <= 19)
    {
        printf("Très bien\n");
    }
    else if (n == 20)
    {
        printf("Parfait\n")
    }
    else
    {
        printf("Quoi ?\n");
    }
}

Notions de pointeurs

float moyenne(float *a, float *b, float *c)
{
    float result = (*a + *b + *c) / 3

    *a = 0;
    *b = 0;
    *c = 0;

    return result;
}
void odd_even(int *value)
{
    if (*value % 2 == 0)
    {
        printf("C'est pair\n");
    }
    else
    {
        printf("C'est impair");
    }
}
void swap(int *a, int *b)
{
    int tempo = *a;
    *a = *b;
    *b = tempo;
}

// Ou alors en mode bitwise expert sans avoir à créer une variable temporaire

void swap(int *a, int *b)
{
    *a = *a ^ *b;
    *b = *a ^ *b;
    *a = *a ^ *b;
}

Chaînes de caractères

unsigned int strlen(char *s)
{
    unsigned int result = 0;
    for (int i = 0; s[i] != '\0'; ++i)
    {
        ++result;
    }

    return result;
}
void upper(char *str)
{
    for (int i = 0; str[i] != '\0'; ++i)
    {
        if (is_uppercase(str[i])
        {
            str[i] = str[i] + 32;
        }
    }
}

void lower(char *str)
{
    for (int i = 0; str[i] != '\0'; ++i)
    {
        if (is_lowercase(str[i])
        {
            str[i] = str[i] - 32;
        }
    }

}

Pourquoi addition ou soustraire par 32 ?

Si tu regardes le man ascii, tu peux voir que la lettre majuscule 'A' a la valeur 65 et que la lettre minuscule 'a' a la valeur 97. La différence entre 65 et 97 est 32. En additionant ou soustrayant par 32, on peut passer de l'intervalle minuscule à majuscule et inversement.

int vowels(char *str)
{
    int result = 0;}

    for (int i = 0; str[i] != '\0'; ++i)
    {
        if (str[i] == 'a' || str[i] == 'e' || str[i] == 'i' || str[i] == 'o' || str[i] == 'u' || str[i] == 'y')
        {
            ++result;
        }
    }

    return result;
}
int is_num(char c);
{
    return ('0' <= c && c <= '9');
}

int miniatoi(char *str)
{
    int result = 0;
    int sign = 1;

    if (str[0] == '-')
    {
        sign = -1;
        ++str;
    }

    for (int i = 0; str[i] != '\0'; ++i)
    {
        if (is_num(str[i]))
        {
            result = result * 10;
            result = str[i] - '0';
        }
        else
        {
            break;
        }
    }

    return result * sign;
}

Décortiquons.

La variable result est créée en étant multipliée par 10 petit à petit puis on retourne le résultat multiplié par 1 ou -1.

Si j'envoie "1234", la variable result sera égale aux valeurs suivantes :

  • 0 // ligne 13 et ligne 27
  • 1 // ligne 28
  • 10 // ligne 27
  • 12 // ligne 28
  • 120 // ligne 27
  • 123 // ligne 28
  • 1230 // ligne 27
  • 1234 // ligne 28

Enfin, on retourne le résultat multiplié par 1 ou -1.

\[ 1234 * 1 = 1234 \]

Si j'envoie "-1234", on retient que le signe est négatif et on avance le pointeur après le signe '-'. On refait les mêmes caluls et on retourne 1234 multiplié par -1.

\[ 1234 * -1 = -1234 \]

Structures de données

Tu as vu comment utiliser les types prédéfinis par le langage C, mais le C te permet de créer tes propres structures et quelque part tes propres types.

Il existe différents moyens de créer ses propres types, tels que les struct, les enum et les unions.

Pour le Pédiluve, tu ne vas que voir les struct.

Une structure est une collection de différents types. Ca ne fonctionne pas tout à fait comme un tableau car tu peux accéder aux attributs par leurs noms.

#include <stdio.h>

struct player
{
    char *name;
    int level;
    float mana;
};

int main(void)
{
    struct player p;

    p.name = "gelules";
    p.level = 42;
    p.mana = 2600,6951;

    printf("Player:\n");
    printf("Name: %s\nLevel: %d\nMana: %f\n", p.name, p.level, p.mana);

    return 0;
}

Et voilà, tu peux définir des structures qui collectionnent des types différents.

Par pointeurs

L'accès aux structures par pointeurs est différent des accès aux tableaux. Quand c'est une variable locale comme dans le code ci-dessus, tu accès aux attributs par un point '.'. Quand c'est par pointeur, il faut une flèche '->'.

On va coder les fonctions player_create et player_print.

player_create prend un pointeur vers une structure player pour la remplir avec les paramètres restants.

player_print affiche les attributs.

#include <stdio.h>

struct player
{
    char *name;
    int level;
    float mana;
};

void player_create(struct player *p, char *name, int level, float mana)
{
    p->name = name;
    p->level = level;
    p->mana = mana;
}

void player_print(struct player *p)
{
    printf("Name: %s\n", p->name);
    printf("Level: %d\n", p->level);
    printf("Mana: %f\n", p->mana);
}

int main(void)
{
    struct player p;

    player_create(&p, "gelules", 42, 2600.6951);

    player_print(&p);

    return 0;
}

Tu peux aussi créer une structure en une seule instruction ainsi :

#include <stdio.h>

struct player
{
    char *name;
    int level;
    float mana;
};

void player_print(struct player *p)
{
    printf("Name: %s\n", p->name);
    printf("Level: %d\n", p->level);
    printf("Mana: %f\n", p->mana);
}

int main(void)
{
    struct player p = {.name = "gelules",
                       .level = 42,
                       .mana = 2600.6951};

    player_print(&p);

    return 0;
}

Il n'y a même pas besoin de suivre l'ordre des attributs.

Tu peux voir la taille qu'utilise une structure en mémoire avec sizeof.

struct player
{
    char *name;
    int level;
    float mana;
};

unsigned long int size = sizeof (struct player);

printf("sizeof (struct player): %lu\n", size);

Avancé

Tu as terminé le minimum dont tu as besoin pour être à l'aise pendant ta prochaine Piscine. Hourra !

Les chapitres suivants sont des notions un peu plus avancées pour être encore plus à l'aise, mais ce n'est pas grave si tu préfères attendre ta Piscine.

Je ne t'en voudrais pas. (Pas beaucoup).

Flags de compilation

Depuis le début, tu compiles ton code en appelant gcc sur ton fichier source et le nom du binaire qu'il doit crée, sans aucune autre information.

Tu peux donner des paramètres à gcc pour orienter la compilation.

Voici les paramètres que tu devrais utiliser pour valider avec brio les codes que tu envoies êtres testés :

  • -std=c99 : Utilise le C version 99. Si tu ne suis pas la norme, tu auras un message d'avertissement (warning).
  • -pedantic : Utilise la norme ISO du C. Si tu ne suis pas la norme, tu auras un message d'avertissement.
  • -Wall : Active beaucoup de warnings.
  • -Wextra : Active encore plus de warnings.
  • -Werror : Transforme les warnings en erreurs.
  • -Wvla : Interdit les tableaux à taille variable.

Un warning permet la compilation, le compilateur t'informe qu'il peut y avoir un comportement indéfini à l'exécution.

Une erreur interdit la compilation. Ainsi si tu as ne serait-ce que le moindre petit warning, la compilation échouera.

Voici comment compiler avec les flags de compilation présentés.

$ gcc -std=c99 -pedantic -Wall -Wextra -Werror -Wvla test.c -o mon_super_programme

Voici un code qui comporte des warnings et erreurs avec les flags de compilation.

#include <stdlib.h>

void mystery(int *i)
{
    int j = *i * 42;
    *i += j;
}

int main(int argc, char *argv[])
{
    int i;
    int j = i + 1;
    int k = atoi(argv[1]);
    int array[k];
    printf("Coucou\n");

    mystery(&i);

    printf("%u\n", i);
}
$ gcc -Wall -Wextra -Wvla -Werror -std=c99 -pedantic test.c -o mon_super_programme
test.c: In function ‘main’:
test.c:14:5: error: ISO C90 forbids variable length array ‘array’ [-Werror=vla]
   14 |     int array[k];
      |     ^~~
test.c:15:5: error: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
   15 |     printf("Coucou\n");
      |     ^~~~~~
test.c:2:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
    1 | #include <stdlib.h>
  +++ |+#include <stdio.h>
    2 |
test.c:15:5: error: incompatible implicit declaration of built-in function ‘printf’ [-Werror=builtin-declaration-mismatch]
   15 |     printf("Coucou\n");
      |     ^~~~~~
test.c:15:5: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
test.c:14:9: error: unused variable ‘array’ [-Werror=unused-variable]
   14 |     int array[k];
      |         ^~~~~
test.c:12:9: error: unused variable ‘j’ [-Werror=unused-variable]
   12 |     int j = i + 1;
      |         ^
test.c:9:14: error: unused parameter ‘argc’ [-Werror=unused-parameter]
    9 | int main(int argc, char *argv[])
      |          ~~~~^~~~
cc1: all warnings being treated as errors
$

Ca fait peur hein ? Il suffit pourtant de lire les messages d'erreurs.

test.c:14:5: error: ISO C90 forbids variable length array ‘array’ [-Werror=vla]
   14 |     int array[k];
      |     ^~~

Tu crées une tableau dont la taille ne peut être calculé à la compilation. Comment savoir quelle place prendre en mémoire ? C'est interdit, tu dois définir une taille fixe pour la compilation, pas l'exécution.

test.c:15:5: error: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration]
   15 |     printf("Coucou\n");
      |     ^~~~~~
test.c:2:1: note: include ‘<stdio.h>’ or provide a declaration of ‘printf’
    1 | #include <stdlib.h>
  +++ |+#include <stdio.h>
    2 |

Tu appelles printf sans inclure stdio.h. Lis bien le message, il te dit même à quelle ligne inclure le fichier.

test.c:12:9: error: unused variable ‘j’ [-Werror=unused-variable]
   12 |     int j = i + 1;
      |         ^

Tu crées une variable sans l'utiliser, quelle inutilité !

Si tu es entrain de créer du code que tu veux tester avec des variables pas encore utilisées, tu peux faire ça :

int i = 0;
int j = i + 1;

(void) j;

fonction(&i);

L'instruction ligne 4 ne fait strictement rien avec j. Mais ne rien faire, c'est déjà faire quelque chose.

Pareil avec argc.

Plusieurs fichiers

Depuis le début, tu compiles un seul et unique fichier. Mais un programme ce n'est pas qu'un seul fichier. C'est une multitude de fichiers compilés ensemble.

Pour la ligne de commande, c'est simple, il suffit d'ajouter le nom de test fichiers.

Tu as créer un fichier test.c avec le main et un fichier math.c avec les fonctions addition et soustraction que tu connais si bien maintenant.

// test.c

#include <stdio.h>

int main(void)
{
    int a = 42;
    int b = 2600;
    int result = addition(a, b);

    printf("result: %d\n", result);

    return 0;
}
/// math.c

int addition(int a, int b)
{
    return a + b;
}

int soustraction(int a, int b)
{
    return a - b;
}

Compile.

$ gcc test.c math.c -o mon_super_programme
test.c: In function ‘main’:
test.c:9:18: error: implicit declaration of function ‘addition’ [-Wimplicit-function-declaration]
    9 |     int result = addition(a, b);
      |

Bigre, la fonction main ne trouve pas la fonction addition. Pourtant le fichier est bien dans la compilation.

C'est parce que le compilateur ne fonctionne pas comme ça. C'est moche, mais tu ne vas pas voir ça maintenant, mais pendant ta Piscine.

Il faut indiquer à la fonction main que tes fonctions de maths existent, et ça dans le fichier test.c

Tu vas déclarer les fonctions.

Déclarer signifie que tu indiques au compilateur qu'il existe des fonctions avec un identifiant, des paramètres et un type de sortie.

Définir signifie créer le code des fonctions déclarées.

Ca fonctionne aussi pour des variables. Tu peux définir i ainsi :

int i;

et le définir avec le symbole d'égalité.

i = 0;

// ou directement le définir en le déclarant

int i = 0;

Tu vas d'abord créer ton premier fichier header, que tu vas appeler math.h.

#ifndef MATH_H
#define MATH_H

int addition(int a, int b);
int soustraction(int a, int b);

#endif /* !MATH_H */

Ne fais pas attention aux code avec ifndef, define et endif. Ils sont là pour éviter des redéfinitions de code. Tu sauras aussi ce que c'est pendant ta Piscine.

Ce que tu fais ici, c'est annoncer qu'il existe, quelque part, des fonctions addition et soustraction. addition prend 2 int en paramètres et retourne 1 int. Pareil pour soustraction.

Tu vas maintenant informer test.c que ces fonctions existent.

// test.c

#include <stdio.h>

#include "math.h"

int main(void)
{
    int a = 42;
    int b = 2600;
    int result = addition(a, b);

    printf("result: %d\n", result);

    return 0;
}

Vois-tu la différence entre une bibliothèque installée dans le système et une bibliothèque locale ?

Celle dans le système est inclue avec des chevrons '<' et '>' tandis que la bibliothèque locale est inclue par des guillements '"'.

Egalement, ce sont dans ces fichiers que tu dois initialiser tes structures.

Recompile.

$ gcc test.c math.c -o mon_super_programme
$

Et voilà, à partir de maintenant, tu peux créer des programmes qui utilisent plusieurs fichiers.

Makefile

Un cours complet sur les Makefiles serait bien disproportionné pour un Pédiluve.

Aussi je t'invite à voir ces deux vidéos pour un cours plus complet.

Makefile, introduction

Makefile, Wvla

Pour la suite, je vais te proposer un Makefile fonctionnel, préconstruit. Je te montrerai comment l'utiliser et comment le modifier en cas d'ajout de fichier.

CFLAGS = -std=c99 -pedantic -Wall -Wextra -Wvla -Werror
OBJ = test.o math.o
BIN = test
TARGET = mon_super_programme

all: $(BIN)
        @mv $< $(TARGET)

$(BIN): $(OBJ)

clean:
        $(RM) $(OBJ) $(BIN) $(TARGET)

Pour utiliser ce Makefile, tu dois tout simplement appeler make.

$ make
cc -std=c99 -pedantic -Wall -Wextra -Wvla -Werror   -c -o test.o test.c
cc -std=c99 -pedantic -Wall -Wextra -Wvla -Werror   -c -o math.o math.c
cc   test.o math.o   -o test
$ ls
Makefile  math.c  math.h  math.o  mon_super_programme  test.c  test.o
$

Et voilà, tu as automatisé ta compilation !

Décortiquons tout ça.

Il y a 4 variables dans ce Makefile, CFLAGS, OBJ, BIN et TARGET.

  • CFLAGS a les flags de compilation.
  • OBJ est le nom de tes fichiers C compilés en fichiers objets, remplace tout simplement l'extension .c en .o
  • BIN est un de tes fichiers .c sans extension
  • TARGET est le nom du binaire que tu veux avoir comme programme

Si tu veux ajouter des fichiers C dans tes projets, ajoute les noms des fichiers dans OBJ en remplaçant .c par .o.

Pour faciliter la compilation, il faut que BIN soit le nom d'un fichier .o sans l'extension.

TARGET est le nom du programme que tu veux avoir à la fin

Enfin, dans la recette all, on move le nom du binaire généré par celui de TARGET. On utilise un arobase '@' devant l'instruction pour que ça ne soit pas affiché pendant l'appel à make.

Un fichier objet est un fichier C compilé en langage machine.

Si tu veux supprimer les fichiers générés (le binaire, les fichiers objets, ...), appelle la target clean dans la ligne de commande.

$ make clean
rm -f test.o math.o test mon_super_programme
$

Je t'invite à regarder les vidéos pour avoir plus de détails sur les Makefiles.

Récursivité

Avais-tu pensé à appeler la fonction dans laquelle tu étais ?

Voici un exemple :

#include <stdio.h>

unsigned int mystery(unsigned int n)
{
    if (n == 0)
    {
        return 0;
    }

    return n + mystery(n - 1);
}

int main(void)
{
    unsigned int value = 3;
    unsigned int result = mystery(value);
    printf("mystery(%u) = %u\n", value, result);

    return 0;
}

Arrives-tu à déterminer ce que fait cette fonction ?

Elle somme l'ensemble des nombres dans l'intervalle [0;n].

En effet, la fonction va sans cesse se rappeler en décrémentant n de 1 jusqu'à arriver à 0.

Voilà ce qu'il se passe en mémoire avec n = 3.

n = 3

\[ 3 + 2 + 1 + 0 = 6 \]

Prenons un autre exemple extrêmement simple :

#include <stdio.h>

void counter(unsigned int n)
{
    printf("%u\n", n);

    if (n == 0)
    {
        return;
    }

    counter(n - 1);
}

int main(void)
{
    counter(10);
    return 0;
}

Avec n = 10, ça crée un compteur qui va de 10 à 0.

$ gcc test.c -o mon_super_programme
$ ./mon_super_programme
10
9
8
7
6
5
4
3
2
1
0
$

Il faut faire attention à une chose avec la récursivité : la condition d'arrêt.

Dans les deux exemples ci-dessus, on vérifie toujours le cas où n == 0. Si la condition est vraie, on quitte la fonction. Dans le cas contraire, on rappelle la fonction avec n qui décrémente de 1.

Si tu ne mets pas de condition d'arrêt, tu vas partir en récursion infinie, et avoir ce qu'on appelle un stack overflow.

Je te laisse essayer ce bout ce code qui n'a pas de condition d'arrêt :

#include <stdio.h>

void counter(unsigned int n)
{
    printf("%u\n", n);

    counter(n - 1);
}

int main(void)
{
    counter(10);
    return 0;
}

A chaque appel de la fonction counter, la mémoire recrée une zone mémoire pour la nouvelle fonction appelée, qui est une stack. Ta mémoire n'étant pas infinie, quand elle n'a pas de place pour une nouvelle stack, le programme crash avec un stack overflow.

Tu verras pendant ta Piscine d'autres cas d'utilisation de la récursivité. Sache que c'est utile pour résoudre des gros problèmes en découpant un problème en sous-problème et en itérant dessus.

La récursivité a l'avantage d'être plus élégante mais a le défaut d'être plus lente qu'une boucle à l'exécution car à chaque rappel de la fonction, se créé en mémoire tout le contexte nécessaire pour créer une fonction, et à la fin de la récursion, il faut remonter tous les appels effectués pour revenir au point de départ. Si tu fais des récursions sur des milliers et milliers de nombres, tu perdras rapidement en efficacité.

Voici par exemple, la fonction d'Ackermann :

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

int ackermann(int m, int n)
{
    if (m == 0)
    {
        return n + 1;
    }

    if (m > 0 && n == 0)
    {
        return ackermann(m - 1, 1);
    }

    if (m > 0 && n > 0)
    {
        return ackermann(m - 1, ackermann(m, n - 1));
    }
}


int main(int argc, char *argv[])
{
    if (argc != 3)
        return 1;

    int m = atoi(argv[1]);
    int n = atoi(argv[2]);
    int a = ackermann(m, n);

    printf("ackermann(%d, %d) = %d\n", m, n, a);

    return 0;
}
$ gcc test.c -o mon_super_programme
$ time ./mon_super_programme 3 14
ackermann(3, 14) = 131069
./ackermann 3 14  69.22s user 0.56s system 98% cpu 1:10.59 total
$

La fonction met 69 secondes à s'exécuter sur ma machine.

Pointeurs avancés

Arithmétique des pointeurs, 2ème partie

Plus tôt, je t'ai dit qu'on pouvait incrémenter un pointeur pour avancer dans un tableau. Mais comment penses-tu que cela se passe en mémoire ?

Prends deux tableaux de types différents.

char ptr_c[] = "abcdefghijklmnopqrstuvwxyz";
double ptr_d[26] = { 0 };

Maintenant tu vas créer les deux pointeurs pour avancer en mémoire, et les deux pointeurs sur void correspondants pour avoir leurs adresses.

char array_c[] = "abcdefghijklmnopqrstuvwxyz";
double array_d[26] = { 0 };
char *ptr_c = array_c;
double *ptr_d = array_d;
void *addr_c = ptr_c;
void *addr_d = ptr_d;

Pas besoin d'utiliser l'esperluette '&' pour avoir l'adresse, un tableau agit déjà comme un pointeur, sa valeur est donc déjà une adresse.

Fais quelques affichages.

char array_c[] = "abcdefghijklmnopqrstuvwxyz";
double array_d[26] = { 0 };
char *ptr_c = array_c;
double *ptr_d = array_d;
void *addr_c = ptr_c;
void *addr_d = ptr_d;

printf("addr ptr_c: %p with value %c\n", addr_c, *ptr_c);
printf("addr ptr_d: %p with value %lf\n", addr_d, *ptr_d);

ptr_c = ptr_c + 1;
++ptr_d;
addr_c = ptr_c;
addr_d = ptr_d;

printf("addr ptr_c: %p with value %c\n", addr_c, *ptr_c);
printf("addr ptr_d: %p with value %lf\n", addr_d, *ptr_d);

++ptr_c;
++ptr_d;
addr_c = ptr_c;
addr_d = ptr_d;

printf("addr ptr_c: %p with value %c\n", addr_c, *ptr_c);
printf("addr ptr_d: %p with value %lf\n", addr_d, *ptr_d);
$ ./mon_super_programme
addr ptr_c: 0x7ffccc171530 with value a
addr ptr_d: 0x7ffccc171460 with value 0.000000
addr ptr_c: 0x7ffccc171531 with value b
addr ptr_d: 0x7ffccc171468 with value 0.000000
addr ptr_c: 0x7ffccc171532 with value c
addr ptr_d: 0x7ffccc171470 with value 0.000000
$

Comme tu peux le voir, le pointeur vers caractère avance de 1 en 1, tandis que le pointeur vers double avance de 8 en 8 (rappelle toi on est en hexadécimal, 0x8 + 0x8 = 0x10)

Pourquoi ? Parce qu'un caractère prend 1 octet en mémoire et un double prend 8 octets en mémoire, le pointeur va donc avancer d'autant d'octets.

Imagine avoir un tableau de struct player assez large.

#include <stdio.h>

struct player
{
    char *name;
    int level;
    double mana;
    int x;
    int y;
    int z;
    char *weapon;
};

int main(void)
{
    unsigned long int size = sizeof (struct player);
    struct player players[16];
    struct player *p = players;
    void *addr_p = p;

    printf("sizeof (struct player): %lu\n", size);

    for (int i = 0; i < 16; ++i)
    {
        addr_p = p + i;
        printf("addr of p[%d]: %p\n", i, addr_p);
    }
}
$ ./mon_super_programme
sizeof (struct player): 48
addr of p[0]: 0x7fffebaf2210
addr of p[1]: 0x7fffebaf2240
addr of p[2]: 0x7fffebaf2270
addr of p[3]: 0x7fffebaf22a0
addr of p[4]: 0x7fffebaf22d0
addr of p[5]: 0x7fffebaf2300
addr of p[6]: 0x7fffebaf2330
addr of p[7]: 0x7fffebaf2360
addr of p[8]: 0x7fffebaf2390
addr of p[9]: 0x7fffebaf23c0
addr of p[10]: 0x7fffebaf23f0
addr of p[11]: 0x7fffebaf2420
addr of p[12]: 0x7fffebaf2450
addr of p[13]: 0x7fffebaf2480
addr of p[14]: 0x7fffebaf24b0
addr of p[15]: 0x7fffebaf24e0
$

Comme tu peux le voir, le tableau de struct avance de 0x30 octets en 0x30 octets, donc de 48 octets en 48 octets en base 10.

C'est parce que le pointeur n'avance pas un par un comme on pourrait le croire avec la boucle, mais sizeof (element) par sizeof (element). Ici, l'élément est la struct player qui fait 48 octets.

Mémoire

Pour l'instant, tu n'as fait que jouer avec des variables locales aux fonctions que tu codes. Rappelle toi que ces variables ne sont vivantes que le temps d'exécution de la fonction.

Comment créer une zone mémoire qui survivrait à la fonction ? Comment créer une tableau à taille variable ?

Dis bonjour à malloc et free.

Malloc

Malloc (memory allocator) alloue une zone mémoire dynamique appelée la heap dans laquelle tu peux faire ce que bon te semble. Tu peux y accéder comme un tableau.

Je t'invite à lire le man 3 malloc.

void *malloc(size_t size);

Malloc renvoie un pointeur sur void. Ca veut dire que tu peux créer un pointeur du type que tu veux. Mais il peut aussi renvoyer NULL, qui est un alias sur (void *) 0. C'est un pointeur sur l'adresse 0. Cette adresse est particulière, tu n'as pas le droit de la déréférencer, sinon ton programme crashera et t'affichera un segfault.

Il prend en entrée un size_t. C'est un alias sur unsigned int. cf. man 3type size_t. C'est aussi ce que retourne strlen, profitons-en.

C'est le kernel (noyau) de Linux qui s'occupe de te trouver une zone dans ta mémoire vive qui est libre.

Mettons que tu veuilles créer une zone mémoire pour accueillir une chaîne de caractères de la taille que tu veux pour copier le contenu de argv[1] (le premier argument de ton programme).

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

char *arg_copy(char *arg_to_copy)
{
    size_t size = strlen(arg_to_copy);
    char *arg = malloc(size + 1);

    if (arg == NULL)
    {
        return NULL;
    }

    for (size_t i = 0; i < size; ++i)
    {
        arg[i] = arg_to_copy[i];
    }

    arg[size] = '\0';

    return arg;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        printf("Usage: %s ARG\n", argv[0]);
        return 1;
    }

    char *text = arg_copy(argv[1]);
    if (text == NULL)
    {
        printf("malloc failed\n");
        return 2;
    }

    printf("%s\n", text);

    return 0;
}

Décortiquons tout ça.

text est un pointeur sur char, il faut que ma fonction arg_copy retourne un pointeur sur char. J'envoie en paramètre argv[1].

Dans la fonction arg_copy, je crée le pointeur sur char arg. Je demande à malloc de lui allouer la taille de l'entrée + 1. J'ai besoin de ce caractère en plus pour écrire le caractère de fin de chaîne de caractères : \0.

Admettons que j'envoie coucou en entrée. size a la valeur de retour de strlen("coucou");, soit 6.

Index0123456
Emplacement1234567
Valeurcoucou\0

Dans ce tableau, la taille size est de 6, et le caractère \0 est au 7ème emplacement pour indiquer la fin de la chaîne de caractère. D'où l'utilisation du + 1.

Maintenant que j'ai ma zone mémoire, je vais copier le contenu de arg_to_copy dans cette zone avec une boucle.

L'avant-dernière ligne de la fonction peut être difficile à comprendre. Etant donné que size égale 6, alors arg[size] = arg[6]. Si tu reprends le tableau, l'index 6 est l'emplacement où doit se trouver le caractère \0.

Etant donné que ma boucle s'arrête juste avant de caractère, je l'écris après les itérations.

J'aurai aussi pu changer la condition de ma boucle ainsi :

for (size_t i = 0; i <= size; ++i)

Ce qui fait que i aurai atteint la valeur de size.

Mais je voulais réappuyer sur l'utilisation des index sur les chaînes de caractères.

Segfault

Pour te montrer pourquoi il est important de tester les retours de malloc, tu vas faire un code qui segfault.

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

void *my_malloc(size_t length)
{
    (void) length;
    return NULL;
}

int main(void)
{
    char *text = my_malloc(10);

    text[0] = 'B';

    return 0;
}
$ ./mon_super_programme
Segmentation fault (core dumped)
$

Voilà pourquoi il est essentiel de toujours vérifier le retour de malloc. Imagine que tu livres un programme qui crash en production pour une erreur aussi simple. Quelle honte !

Free

Une fois que tu n'as plus besoin de ta zone mémoire, il faut la libérer.

La mémoire vive de ton PC est limitée. Si tu demandes sans cesse de la mémoire sans la libérer, tu utiliseras de la mémoire inutilement que d'autres programmes pourraient utiliser. C'est ce qu'on appelle une fuite mémoire (momory leak).

Imagine qu'à chaque fois que tu ouvres un onglet sur ton navigateur Internet, celui-ci demande 100 Mo au kernel, mais qu'une fois l'onglet fermé, ton navigateur ne libère pas les 100 Mo alloués. Si tu cumules les onglets, tu vas vite te retrouver sans beaucoup de mémoire vive libre. Tu seras obligé de fermer ton navigateur pour tout libérer.

Pour libérer la mémoire, tu peux faire appel à free. Je t'invite à lire le man 3 free.

char *text = malloc(10);

// du code...

free(text);

Après l'appel de free, tu ne dois surtout pas réutiliser la variable libérée. Tu utiliserais une zone mémoire que ton programme considèrerait comme libre de ta portée et qui pourrait contenir des valeurs allouées par ton programme et pas par toi. Tes résultats se retrouveraient faussés.

Si on reprend le premier code de cette page, il faut donc ajouter free juste après le printf de la fonction main qui est le dernier endroit où la zone mémoire allouée est utilisée.

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

char *arg_copy(char *arg_to_copy)
{
    size_t size = strlen(arg_to_copy);
    char *arg = malloc(size + 1);

    if (arg == NULL)
    {
        return NULL;
    }

    for (size_t i = 0; i < size; ++i)
    {
        arg[i] = arg_to_copy[i];
    }

    arg[size] = '\0';

    return arg;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        printf("Usage: %s ARG\n", argv[0]);
        return 1;
    }

    char *text = arg_copy(argv[1]);
    if (text == NULL)
    {
        printf("malloc failed\n");
        return 2;
    }

    printf("%s\n", text);

    free(text);

    return 0;
}

Vive la mémoire libre !

Sécurité avec free

Pour faire preuve de sécurité quand tu utilises free, tu peux utiliser une fonction qui va libérer la zone mémoire et mettre l'adresse du pointeur sur NULL.

free ne laisse ton pointeur sur laquelle il pointe, donc si tu tt'en sers encore après, tu liras des valeurs qui ne t'appartiennent plus.

char *text = malloc(10);

// du code...

free(text);

printf("%s\n", text);
text[0] = 'g';
printf("%s\n", text);

Ce code compile et peut s'exécuter en donnant un comportement indéfini.

Pour ne pas rencontrer ce genre de bug, tu vas créer une fonction qui va libérer la zone mémoire et mettre le pointeur sur NULL pour de bon. Ainsi si tu utilises ton pointeur, tu auras un segfault. C'est très bien pour des phases de tests.

#include <stdlib.h>

void ptr_char_destructor(char **ptr)
{
    free(*ptr);
    *ptr = NULL;
}

int main(void)
{
    char *text = malloc(2600);

    // du code

    ptr_char_destructor(&text);

    // du code

    return 0;
}

Oulah oulah. Quelle est cette écriture ?

Etant donné que tu dois mettre pour de bon le pointeur à NULL, tu vas coder une fonction qui va utiliser un pointeur de pointeur. Un double pointeur.

Si un pointeur est une adresse qui pointe un type, alors un pointeur de pointeur est une adresse qui pointe sur... une adresse.

multi pointeur

Donc si tu modifies l'adresse pointée en la mettant à NULL, au retour de la fonction ptr_char_destructor, la valeur du pointeur sera toujours à NULL.

Si tu l'utilises sans faire exprès, tu auras un segfault à l'exécution.

Par exemple, ce code segfault :

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

void ptr_char_destructor(char **ptr)
{
    free(*ptr);
    *ptr = NULL;
}

int main(void)
{
    char *text = malloc(2600);

    // du code

    ptr_char_destructor(&text);

    // du code

    text[0] = 'g';

    return 0;
}

Valgrind

Valgrind (qu'on prononce "Val grine de") est un outil pour déboguer et profiler tes programmes. Pour l'occasion, tu vas l'utiliser pour vérifier que tu as bien libérer ta mémoire.

Prenons un simple et incorrect code.

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

char *arg_copy(char *arg_to_copy)
{
    size_t size = strlen(arg_to_copy);
    char *arg = malloc(size + 1);

    if (arg == NULL)
    {
        return NULL;
    }

    for (size_t i = 0; i < size; ++i)
    {
        arg[i] = arg_to_copy[i];
    }

    arg[size] = '\0';

    return arg;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        printf("Usage: %s ARG\n", argv[0]);
        return 1;
    }

    char *text = arg_copy(argv[1]);
    if (text == NULL)
    {
        printf("malloc failed\n");
        return 2;
    }

    printf("%s\n", text);

    // free(text);

    return 0;
}
$ valgrind ./mon_super_programme coucou
==4902== Memcheck, a memory error detector
==4902== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==4902== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==4902== Command: ./mon_super_programme coucou
==4902==
coucou
==4902==
==4902== HEAP SUMMARY:
==4902==     in use at exit: 7 bytes in 1 blocks
==4902==   total heap usage: 2 allocs, 1 frees, 1,031 bytes allocated
==4902==
==4902== LEAK SUMMARY:
==4902==    definitely lost: 7 bytes in 1 blocks
==4902==    indirectly lost: 0 bytes in 0 blocks
==4902==      possibly lost: 0 bytes in 0 blocks
==4902==    still reachable: 0 bytes in 0 blocks
==4902==         suppressed: 0 bytes in 0 blocks
==4902== Rerun with --leak-check=full to see details of leaked memory
==4902==
==4902== For lists of detected and suppressed errors, rerun with: -s
==4902== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Décortiquons tout ça.

Tu peux voir au début la commande exécutée.

==4902== Command: ./mon_super_programme coucou

Tu peux voir l'effet du printf au milieu des lignes.

Ensuite il y a deux parties qui nous résument l'utilisation mémoire.

==4902== HEAP SUMMARY:
==4902==     in use at exit: 7 bytes in 1 blocks
==4902==   total heap usage: 2 allocs, 1 frees, 1,031 bytes allocated

in use at exit: 7 bytes in 1 blocks : c'est notre coucou\0 qui n'a pas été libéré.

total heap usage: 2 allocs, 1 frees, 1,031 bytes allocated : la heap est la zone mémoire dynamique. Comment peut-il y avoir 2 allocations alors que nous ne faisons appel à malloc qu'une seule fois ?

Parce que nous ne faisons pas appel à malloc qu'une seule fois.

printf fait aussi appel à malloc pour afficher le texte. Mais à l'inverse de notre code, printf fait appel à free, c'est pour ça qu'on peut lire ensuite qu'il y a un appel à free.

A la fin des logs, on nous indique qu'on peut relancer Valgrind avec l'argument suivant :

==4902== Rerun with --leak-check=full to see details of leaked memory
$ valgrind --leak-check=full ./mon_super_programme coucou
==5307== Memcheck, a memory error detector
==5307== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==5307== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==5307== Command: ./mon_super_programme coucou
==5307==
coucou
==5307==
==5307== HEAP SUMMARY:
==5307==     in use at exit: 7 bytes in 1 blocks
==5307==   total heap usage: 2 allocs, 1 frees, 1,031 bytes allocated
==5307==
==5307== 7 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5307==    at 0x48447A8: malloc (vg_replace_malloc.c:446)
==5307==    by 0x109194: arg_copy (in /tmp/tests/pediluve/c/mon_super_programme)
==5307==    by 0x10923B: main (in /tmp/tests/pediluve/c/mon_super_programme)
==5307==
==5307== LEAK SUMMARY:
==5307==    definitely lost: 7 bytes in 1 blocks
==5307==    indirectly lost: 0 bytes in 0 blocks
==5307==      possibly lost: 0 bytes in 0 blocks
==5307==    still reachable: 0 bytes in 0 blocks
==5307==         suppressed: 0 bytes in 0 blocks
==5307==
==5307== For lists of detected and suppressed errors, rerun with: -s
==5307== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Le nouveau bloc de texte intéressant se trouve en plein milieu :

==5307== 7 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5307==    at 0x48447A8: malloc (vg_replace_malloc.c:446)
==5307==    by 0x109194: arg_copy (in /tmp/tests/pediluve/c/mon_super_programme)
==5307==    by 0x10923B: main (in /tmp/tests/pediluve/c/mon_super_programme)

Ce bloc se lit de bas en haut.

On peut lire que depuis la fonction main, on fait appel à la fonction arg_copy qui elle-meme fait appel à la fonction malloc et que 7 octets sont définitivement perdus depuis cet enchainement d'exécution.

Mais les emplacements ne sont pas humainement lisibles, on a des adresses mémoire en hexadécimal. Nous allons ajouter un argument à la compilation pour avoir des symboles de débug.

$ gcc -g test.c -o mon_super_programme

L'option -g permet d'ajouter des symboles de débug à ton programme. Ca te permet de déboguer plus efficacement tes programmes. Mais attention à ne pas livrer en production ce que tu produis avec des informations. Des gens mal intentionnés pourraient s'en servir.

$ valgrind --leak-check=full ./mon_super_programme coucou
...
==5550== 7 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5550==    at 0x48447A8: malloc (vg_replace_malloc.c:446)
==5550==    by 0x109194: arg_copy (test.c:8)
==5550==    by 0x10923B: main (test.c:33)
...
$

On peut lire ici que le premier appel à arg_copy se fait dans la fonction main à la ligne 33, et que arg_copy fait appel à malloc ligne 8.

On sait où se trouve notre memory leak !

Recoder malloc et free

Nous n'allons pas faire ça ici, mais dans les écoles d'informatiques, il y a un exercice assez répandu qui est de recoder et remplacer malloc et free. C'est un exercice ma foi fort sympathique.

Listes chainées

Pour voir un ensemble d'exercices avancés, tu vas apprendre à utiliser les listes chaînées. C'est une structure de données avancée (par rapport aux tableaux, par exemple).

Une liste chaînée est juste une structure qui a en attribut un pointeur sur sa propre structure.

Voici un bout de code pour mieux se rendre compte.

struct node
{
    int value;
    struct node *next;
};

Ainsi notre noeud peut aller dans une autre zone mémoire en pointant sur un autre noeud qui aura d'autres valeurs, et ainsi de suite.

L'intérêt est que tu vas pouvoir ajouter un noeud quand bon te semble dynamiquement.

Voici une représentation d'une liste chaînée.

liste chaînée

Chaque rectangle coloré en vert et rose est un noeud. Le premier est appelé head car il est en tête de la liste. On sait que le dernier est en fin de liste car son attribut next est à NULL.

Pour passer au noeud d'après, on utilise l'attribut next.

Commençons par créer une fonction pour créer un noeud avec une valeur envoyée en paramètre. Maintenant que tu sais utiliser malloc, tu ne seras pas surpris de le voir utilisé dans la fonction.

#include <stdlib.h>

struct node
{
    int value;
    struct node *next;
};

struct node *node_constructor(int value)
{
    struct node *new = malloc(sizeof (struct node));
    if (new == NULL)
    {
        return NULL;
    }

    new->value = value;
    new->next = NULL;

    return new;
}

Fais bien attention à mettre next à NULL. On va s'en servir pour parcourir la liste chaînée. On saura que nous sommes à la fin une fois que le dernier noeud aura son next à NULL.

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

struct node
{
    int value;
    struct node *next;
};

struct node *node_constructor(int value)
{
    struct node *new = malloc(sizeof (struct node));
    if (new == NULL)
    {
        return NULL;
    }

    new->value = value;
    new->next = NULL;

    return new;
}

int main(void)
{
    struct node *head = node_constructor(69);
    void *ptr_next = head->next;

    printf("head value: %d\n", head->value);
    printf("head next : %p\n", next);

    return 0;
}

La variable ptr_next m'est juste utile pour afficher dans printf ce que vaut head->next. En temps normal tu n'as pas besoin de faire ça.

$ ./mon_super_programme
head value: 69
head next : (nil)
$

(nil) est une écriture particulière de printf pour signifier que la valeur de l'adresse à affcher est NULL.

Tu peux maintenant enchaîner les appels.

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

struct node
{
    int value;
    struct node *next;
};

struct node *node_constructor(int value)
{
    struct node *new = malloc(sizeof (struct node));
    if (new == NULL)
    {
        return NULL;
    }

    new->value = value;
    new->next = NULL;

    return new;
}

int main(void)
{
    struct node *head = node_constructor(69);

    head->next = node_constructor(51);
    head->next->next = node_constructor(2600);
    head->next->next->next = node_constructor(42);

    printf("1: %d\n", head->value);
    printf("2: %d\n", head->next->value);
    printf("3: %d\n", head->next->next->value);
    printf("4: %d\n", head->next->next->next->value);

    return 0;
}
$ ./mon_super_programme
1: 69
2: 51
3: 2600
4: 42
$

Et si tu faisais plutôt une fonction pour parcourir et afficher ta liste chaînée ?

Tu as deux moyen pour ça. Une fonction itérative qui utilise une boucle, et une fonction récursive qui se rappelle elle-même.

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

struct node
{
    int value;
    struct node *next;
};

struct node *node_constructor(int value)
{
    struct node *new = malloc(sizeof (struct node));
    if (new == NULL)
    {
        return NULL;
    }

    new->value = value;
    new->next = NULL;

    return new;
}

void node_printer_rec(struct node *n)
{
    if (n == NULL)
    {
        return;
    }

    printf("%d\n", n->value);

    node_printer_rec(n->next);
}

void node_printer_loop(struct node *n)
{
    while (n != NULL)
    {
        printf("%d\n", n->value);
        n = n->next;
    }
}

int main(void)
{
    struct node *head = node_constructor(69);

    head->next = node_constructor(51);
    head->next->next = node_constructor(2600);
    head->next->next->next = node_constructor(42);

    printf("Recursif:\n");
    node_printer_rec(head);

    printf("Loop:\n");
    node_printer_loop(head);

    return 0;
}
$ ./mon_super_programme
Recursif:
69
51
2600
42
Loop:
69
51
2600
42
$

On va garder la façon de faire avec la boucle. Elle est plus efficace et rapide que la récursive qui doit se rappeler à chaque fois.

En se basant sur cette technique, code une fonction node_append qui prend en paramètre la head de la liste chaînée et la nouvelle value et ajoute à la toute fin le nouveau neud avec la valeur.

ps: Tu peux tout à faire utiliser node_constructor pour ça.

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

struct node
{
    int value;
    struct node *next;
};

struct node *node_constructor(int value)
{
    struct node *new = malloc(sizeof (struct node));
    if (new == NULL)
    {
        return NULL;
    }

    new->value = value;
    new->next = NULL;

    return new;
}

void node_append(struct node *head, int value)
{
    if (head == NULL)
    {
        return;
    }

    while (head->next != NULL)
    {
        head = head->next;
    }

    head->next = node_constructor(value);
}

void node_printer(struct node *n)
{
    while (n != NULL)
    {
        printf("%d\n", n->value);
        n = n->next;
    }
}

int main(void)
{
    struct node *head = node_constructor(69);

    node_append(head, 51);
    node_append(head, 2600);
    node_append(head, 42);

    node_printer(head);

    return 0;
}

Comme dans le node_printer, je vais parcourir la liste. Une fois arrivé au dernier noeud qui n'a pas de next, je dis que son next est construit en utilisant la fonction node_constructor.

Au cas où quelqu'un fait n'importe quoi avec ma fonction ou si je suis fatigué, je vérifie quand même qu'on ne m'envoie pas une fonction inexistante, c'est à dire avec la head à NULL, car je ne peux pas ajouter un noeud à rien.

$ ./mon_super_programme
69
51
2600
42
$

C'est bien. Il ne manque qu'une chose... LIBERER LA MEMOIRE !

$ valgrind ./mon_super_programme
...
==9266== HEAP SUMMARY:
==9266==     in use at exit: 64 bytes in 4 blocks
==9266==   total heap usage: 5 allocs, 1 frees, 1,088 bytes allocated
==9266==
==9266== LEAK SUMMARY:
==9266==    definitely lost: 16 bytes in 1 blocks
==9266==    indirectly lost: 48 bytes in 3 blocks
==9266==      possibly lost: 0 bytes in 0 blocks
==9266==    still reachable: 0 bytes in 0 blocks
==9266==         suppressed: 0 bytes in 0 blocks
...
$

Tu vas créer une fonction node_destructor qui prend en paramètre un double pointeur sur la head de type struct node, libère chaque noeud et met la valeur de head à NULL pour de bon.

Attention à ne pas écrire quelque chose comme ceci :

while (node)
{
    free(node);
    node = node->next;
}

Si tu veux accéder à l'attribut d'une struct en ayant un double pointeur, utilise cette notation :

(*node)->value;

Les parenthèses te servent à bien déréférencer node et pas node-value.

Une fois que tu as libérer une zone mémoire, tu ne dois plus l'utiliser.

Réfléchis à utiliser une deuxième variable.

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

struct node
{
    int value;
    struct node *next;
};

struct node *node_constructor(int value)
{
    struct node *new = malloc(sizeof (struct node));
    if (new == NULL)
    {
        return NULL;
    }

    new->value = value;
    new->next = NULL;

    return new;
}

void node_append(struct node *head, int value)
{
    if (head == NULL)
    {
        return;
    }

    while (head->next != NULL)
    {
        head = head->next;
    }

    head->next = node_constructor(value);
}

void node_destructor(struct node **head)
{
    if (head == NULL || *head == NULL)
    {
        return;
    }

    struct node *sentinel = *head->next;
    struct node *to_free = NULL;

    while (sentinel != NULL)
    {
        to_free = sentinel;
        sentinel = sentinel->next;
        free(to_free);
    }

    free(*head);
    *head = NULL;
}

void node_printer(struct node *n)
{
    while (n != NULL)
    {
        printf("%d\n", n->value);
        n = n->next;
    }
}

int main(void)
{
    struct node *head = node_constructor(69);

    node_append(head, 51);
    node_append(head, 2600);
    node_append(head, 42);

    node_printer(head);

    node_destructor(&head);

    return 0;
}
$ ./mon_super_programme
69
51
2600
42
$
$ valgrind ./mon_super_programme
==9462== HEAP SUMMARY:
==9462==     in use at exit: 0 bytes in 0 blocks
==9462==   total heap usage: 5 allocs, 5 frees, 1,088 bytes allocated
==9462==
==9462== All heap blocks were freed -- no leaks are possible
$

Super ! Tu sais maintenant créer une liste chaînée et faire les actions les plus basiques !

Réfléchis maintenant à faire les fonctions suivantes :

Je ne donne par les corrections car il s'agit d'exercices très souvent présents aux piscines (et qui sont notés) auxquels tu dois réfléchir par toi-même.

struct node
{
    int value;
    struct node *next;
};

/*
    Ajoute un noeud en remplaçant la tête.
    Retourne la nouvelle tête qui vient d'être ajoutée.

     head : [51 -> 42 -> NULL]

    node_insert(head, 69);

     head : [69 -> 51 -> 42 -> NULL]
*/
    struct node *node_prepend(struct node *head, int value);

/*
    Ajoute un nouveau noeud avec la valeur 'value' à l'indexième position

    Index :  0     1     2
     head : [69 -> 51 -> 42 -> NULL]

    node_insert(head, 2600, 42);

    Index :  0     1     2       3
     head : [69 -> 51 -> 2600 -> 42 -> NULL]
*/
    void node_insert(struct node *head, int value, unsigned int index);

/*
    Renvoie l'index dans la liste du premier noeud qui contient la valeur
    'value'
*/

    unsigned int node_find(struct node *head, int value);

/*
    Supprime le premier noeud dont on rencontre la valeur 'value'
    Raccroche le noeud précédent avec le noeud suivant

    head : [69 -> 51 -> 42 -> 51 -> NULL]

    node_remove(head, 51);

    head : [69 -> 42 -> 51 -> NULL]
*/
    void node_remove(struct node *head, int value);

/*
    Ajoute la deuxième liste à la suite de la première liste

    head1 : [69 -> 51 -> NULL]
    head2 : [42 -> 2600 -> 51 -> NULL]

    node_concat(head1, head2);

    head1 : [69 -> 51 -> 42 -> 2600 -> 51 -> NULL]
    head2 : [42 -> 2600 -> 51 -> NULL]
*/
    void node_concat(struct node *head1, struct node *head2);

/*
    Trie la liste dans l'ordre croissant

    head : [69 -> 51 -> 42 -> 2600 -> 51 -> NULL]

    node_sort(head);

    head : [42 -> 51 -> 51 -> 69 -> 2600 -> NULL]

*/
    void node_sort(struct node *head);

/*
    Inverse l'ordre des éléments de la liste et retourne la nouvelle tête

    head : [69 -> 51 -> 42 -> 2600 -> 51 -> NULL]

    node_reverse(head);

    head : [51 -> 2600 -> 42 -> 51 -> 69 -> NULL]
*/
    struct node *node_reverse(struct node *head);

/*
    Coupe la liste en deux à partir de l'indexième position
    La première liste aura NULL à partir de la coupure

    Retourne la tête de la seconde liste

    Index :  0     1     2       3
     head : [69 -> 51 -> 2600 -> 42 -> NULL]

    struct node *head2 = node_split(head, 1);

    head1 : [69 -> 51 -> NULL]
    head2 : [2600 -> 42 -> NULL]
*/

    struct node *node_split(struct list *head, unsigned int index);

GDB

GDB signifie Gnu DeBugger. C'est un débogueur qui te permet de voir plus en profondeur comment s'exécute ton programme, remonter la trace des bugs, etc.

Je ne vais pas te montrer tout l'envers du décort avec gdb, ça serait beaucoup trop long et ça mériterait un livre complet. Je vais plutôt te montrer quelques commandes de bases qui te seront déjà bien utiles.

Symbole de débug

Si tu veux utiliser GDB, je te conseille de toujours inclure les symboles de débug.

Exemples

Voici le code que l'on va déboguer.

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

void upperchar(char *str, unsigned int index)
{
    str[index] -= 32; 
}

void uppercase(char *str)
{
    size_t str_len = strlen(str);
    for (size_t i = 0; i < str_len; ++i)
    {
        if ('a' <= str[i] && str[i] <= 'z')
        {
            upperchar(str, i);
        }
    }
}

int main(void)
{
    char *text = "je suis en majuscule";

    printf("%s\n", text);

    uppercase(text);

    printf("%s\n", text);

    return 0;
}
$ ./mon_super_programme
je suis en majuscule
segmentation fault (core dumped)  ./mon_super_programme
$

Exécution

$ gcc test.c -g -o mon_super_programme
$

Pour déboguer ton programme, appelle gdb avec le nom de ton exécutable.

$ gdb mon_super_programme
GNU gdb (GDB) 15.1
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
/home/gelules/.config/gdb/gdbinit:1: Error in sourced command file:
No symbol table is loaded.  Use the "file" command.
Reading symbols from mon_super_programme...
(gdb)

Tu peux lire que GDB a bien charger ton programme, et tu as maintenant un prompt.

Si tu veux quitter GDB, exécuter exit ou effecute la combinaison de touches ctrl+d. Si tu es en débug, il se peut que tu dois faire cette manipulation deux fois d'affilées.

Afficher le code source

Ceci fonctionne avec les codes compilés avec les symboles de débug.

(gdb) tui enable

gdb affiche le code source

Si tu utilises les touches fléchées, tu parcouras le code. Mais GDB te permet avec les touches fléchées de parcourir ton historique de commandes.

Pour te refocaliser sur la fenêtre des commandes, exécute

(gdb) foc cmd

Pour refocaliser le code source, exécute

(gdb) foc src

Exécuter le programme

Pour exécuter le programme normalement dans GDB, exécute run ou r pour aller plus vite.

Si GDB te demande de télécharger et activer debuginfo, répond yes. Ca ne te sera pas utile maintenant, mais ça te fait des informations de débug supplémentaires. Ce n'est pas négligeable.

(gdb) run
...
je suis en majuscule

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555170 in upperchar (str=0x555555556004 "je suis en majuscule", index=0) at test.c:6
6	    str[index] -= 32;
(gdb)

gdb affiche le segfault

GDB t'indique que le segfault a lieu dans la fonction upperchar à la ligne 6, et te met en surbrillance la ligne exécutée qui segfault. Elle t'affiche aussi les paramètres de la fonction.

Backtrace

Tu peux utiliser la commande backtrace ou bt pour aller plus vite pour voir où tu es et d'où tu viens.

(gdb) backtrace
#0  0x0000555555555170 in upperchar (str=0x555555556004 "je suis en majuscule", index=0) at test.c:6
#1  0x00005555555551d3 in uppercase (str=0x555555556004 "je suis en majuscule") at test.c:16
#2  0x0000555555555211 in main () at test.c:27
(gdb)

Ca se lit de bas en haut.

Tu étais dans le main, à la ligne 27 tu as appelé uppercase et à la ligne 16 tu as appelé upperchar avec l'index à 0.

str n'est pas à NULL (0x0), elle est à l'adresse 0x555555556004 et on voit qu'elle pointe vers une chaîne de caractères qui contient notre texte.

Tu devines que tu n'as pas le droit de mofidier str.

Retournons dans le main.

    char *text = "je suis en majuscule";

Rappelle toi, tu n'as pas le droit de modifier un pointeur sur un string literal, tu dois créer un tableau pour ça.

Fixons ça avec :

    char text[] = "je suis en majuscule";
$ ./mon_super_programme
je suis en majuscule
JE SUIS EN MAJUSCULE
$

Hourra ! Ca fonctionne !

Je vais quand même te montrer quelques commandes qui pourraient s'avérer utiles.

start

La commande start crée un breakpoint, un point d'arrêt, au tout début de ton programme. Ca te permet de commencer à déboguer ton programme en restant au début pour faire de la vérification avec de continuer l'exécution

continue

Quand tu es dans un breakpoint, le programme est à l'arrêt. Utilise la commande continue ou c pour aller plus vite pour reprendre le cours d'exécution.

breakpoints

Tu peux créer des breakpoints avec la commande break ou break pour aller plus vite, suivi d'une adresse mémoire, d'un nom de fonction ou d'un numéro de ligne.

Sache que GDB autorise l'autocomplétion, si tu mets un début de nom de fonction, il peut autocompléter en appuyant sur la touche de tabulation.

Dans le code que j'ai donné au début. Si je fais

(gdb) break upperchar
Breakpoint 1 at 0x1164: file test.c, line 6.
(gdb)

J'ai bien un breakpoint qui se fera à la fonction upperchar.

Pour lister les breakpoints, tu peux exécuter la commande info breakpoints.

(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000001164 in upperchar at test.c:6
(gdb)

Pour supprimer un breakpoint, exécute delete breakpoints NUMERO_DU_BREAKPOINT.

next et step

Si tu veux avancer pas à pas, tu peux utiliser les commandes next ou n pour aller plus vite, et step ou s pour aller plus vite.

next ne rentre pas dans une fonction pendant le débug, gdb exécutera tout le code qui doit s'y exécuter en gardant le focus sur la fonction dans laquelle tu es.

step rentre dans la fonction

Si tu appuies sur Entrée alors que le prompt est vide, GDB rééexuctera ta dernière commande.

La ligne de code que GDB affiche sera la ligne qui sera exécutée une fois que tu entreras next ou step.

(gdb) start
Temporary breakpoint 1 at 0x5555555551fe: file test.c, line 22.
Starting program: /tmp/tests/pediluve/c/mon_super_programme
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".

Temporary breakpoint 1, main () at test.c:22
22	{
(gdb) n
23	    char text[] = "je suis en majuscule";
(gdb)
25	    printf("%s\n", text);
(gdb)
je suis en majuscule
27	    uppercase(text);
(gdb) s
uppercase (str=0x7fffffffe610 "je suis en majuscule") at test.c:11
11	    size_t str_len = strlen(str);
(gdb) n
12	    for (size_t i = 0; i < str_len; ++i)
(gdb)
14	        if ('a' <= str[i] && str[i] <= 'z')
(gdb)
16	            upperchar(str, i);
(gdb) s
upperchar (str=0x7fffffffe610 "je suis en majuscule", index=0) at test.c:6
6	    str[index] -= 32;
(gdb)
7	}
(gdb)

next et step dans GDB

Exécuter son programme avec des arguments sur argv

Si tu dois exécuter ton programme avec des arguments, exécute gdb normalement en donnant le nom du programme. Une fois dans le prompt de GDB, exécute

(gdb) start argument_1 argument_2 argument_3

ou

(gdb) run argument_1 argument_2 argument_3

pour lancer le débug avec des arguments.

Assembleur

Auteurs

Gélules