présente...

 

LéaBook , Chapitre :
Développement

par tous les amis de Léa

 

 

Le copyright de chaque article est détenu par son auteur.
Le copyright du livre lui-même est détenu par Léa (Association Loi 1901).
Les articles de ce livre sont diffusés selon la license GPL (GNU General Public License), sauf mention explicite dans l'article.

Copyright (c) 2003 Association Léa.
This documentation is free documentation; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
This documentation is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
For the purpose of applying the GPL to this document, I consider "source code" to refer to the html source.


Table des matières


Linux : plateforme de développement

Cette section contient les chapitres relatifs aux outils de développement sous Linux, et à des langages qui y sont couramment utilisés.

Pour vos questions concernant le développement dont vous ne trouvez pas la réponse ici, allez voir le forum développement...


Haut


Introduction à (g)awk

par Xavier GARREAU

Cet article va vous permettre de vous familiariser avec (g)awk, langage de programmation permettant de faire très rapidement des traitements intéressants sur des données diverses. Après la présentation générale, on réalisera deux filtres inspirés de filtres ayant été réellement développés.

Introduction

Présentation

Vous vous demandez surement pourquoi ce (g) avant awk, non ? Ce n'est pas le g de gnome, ni celui de gtk+ mais celui de gnu. Extrait de la page de manuel (man gawk ou man awk) :
"Gawk est l'implémentation du projet GNU du langage de programmation AWK, lequel se conforme à la définition du langage dans la norme POSIX1003.2 Command Language and Utilities Standard. Cette version est basée, elle, sur la description d'AWK de Aho, Kernighan and Weinberger, plus les spécificités supplémentaires se trouvant dans la version de awk de l'UNIX System V version 4. Gawk ajoute également des extensions plus récentes des Laboratoires Bell ainsi que des extensions spécifiques à GNU."
Je ne mets pas ce pavé pour remplir de l'espace mais pour que les personnes à la base de choses que je ne fais que présenter soient reconnues pour leur travail et que l'on sache de quoi on parle.

Nous allons pouvoir commencer:

Qu'est ce que c'est

awk (j'utiliserai awk par la suite, mais c'est de gawk que l'on parle) est un langage interprété, installé par défaut sur les distributions linux (au moins rh et mandrake) dans le répertoire /bin. Il permet de traiter des fichiers de données structurées.
Un programme awk est une suite de blocs de code compris entre {} (c'est pas très original) qui sont appliqués aux lignes (par défaut) de données du fichier si un "modèle" est vérifié. La page de manuel nous donne les différents types de modèles utilisables :

BEGIN Le bloc qui suit est exécuté au début du programme, une seule fois.
END Le bloc qui suit est exécuté à la fin du programme, une seule fois.
/expression régulière/ Le bloc qui suit est exécuté si les données en cours de traitement correspondent à l'expression régulière. Si vous ne connaissez pas les expressions régulières, lisez la page man egrep.
Un exemple : /^#/ Permet de selectionner les lignes commençant par un #.
expression relationnelle Le bloc qui suit est exécuté si l'expression est vraie.
Un exemple : maVariable==5
modèle && modèle Le bloc qui suit est exécuté si les deux modèles sont vérifiés.
modèle || modèle Le bloc qui suit est exécuté si au moins un des modèles est vérifié.
modèle0 ? modèle1 : modèle2 Le bloc qui suit est exécuté si les modèles 0 et 1 sont vérifiés où si le modèle2 uniquement est vérifié.
(modèle) Permet de grouper les modèles.
Un exemple : (modèle0 || modèle1) && modèle2
! modèle Le bloc qui suit est exécuté si le modèle n'est pas vérifié.
modèle1, modèle2 Le bloc qui suit est exécuté pour la partie des données en cours commençant par modèle1 et finissant par modèle2.
Un exemple : /\/\*/, /\*\// pour afficher les commentaires multilignes

Premiers pas

Les enregistrements

Pour awk, les blocs de données lues sont séparés par un caractère contenu dans la variable RS (Record Separator, retour à la ligne par défaut). On peut accéder à l'intégralité du bloc grâce à la variable $0. C'est ce qu'on appelle un enregistrement.

Un exemple : Tapez awk '{ print $0 }' un_fichier
Passionant, non ? vous venez de réinventer la commande cat ;-) !
Un autre exemple : Tapez awk '/\/\*/, /\*\// { print $0 }' un_fichier_c_avec_des_commentaires
C'est déjà plus sympa, hein ?
Un dernier exemple : Tapez awk '! (/^ *#/ || /^$/) { print $0 }' un_fichier_conf
Vous n'aviez jamais vu votre fichier de config d'Apache comme ça, si ?

Les champs

Ah, la nature ... Courir nus dans les champs en se tenant la main ... NON, je vous arrête tout de suite.
Chaque enregistrement est séparé en champs par un caractère contenu dans la variable FS (Field Separator, un espace par défaut). Les champs sont accessibles par les variables $1, $2, $3, etc ...
Pour afficher le 3ème champ de toutes les lignes de vos données à traiter, vous utilisez print $3.
Un exemple : awk -F ':' '{ print $1 " est " $5 }' /etc/passwd pour afficher la description des utilisateurs de votre machine.

Les variables

Il existe bon nombre de variables prédéfinies en awk. Je vais vous en donner la liste résumée, encore une fois, je n'invente rien et je ne créée rien, je ne fais que vous refaire une lecture de la page de manuel. D'ailleurs pour avoir une liste exhaustive des variables, consultez la.

ARGC Si vous avez fait du c, vous vous en doutez ! Sinon, je vous le dis. Cette variable contient le nombre d'arguments passés au programme. (sans les options passées à awk)
ARGV Un tableau contenant les ARGC paramètres, indéxé de 0 à ARGC-1. Ce sont tous les fichiers à traiter
CONVFMT Le format par défaut pour l'affichage des nombres, "%.6g" par défaut
ENVIRON Un tableau contenant les variables d'environnement. Exemple ENVIRON["PATH"] contient votre path.
FIELDWIDTHS Pour le cas de traitement de données non délimités mais contenant des champs de largeur fixe.
Cette variable est de la forme largeur_champ_1 largeur_champ_2 largeur_champ_3 etc....
FNR Le numéro de l'enregistrement actuellement en cours de traitement.
Un exemple : awk '{print FNR ": " $0}' un_fichier permet d'afficher le fichier avec les numéros de lignes.
Un autre exemple : awk '{print FNR ": " $0}' un_fichier | grep ^45: permet d'afficher la ligne 45 du fichier un_fichier. On fait appel à grep, ce n'est plus du 100% awk mais c'est beau la diversité, non ?
FS Le séparateur de champs. No comment !
IGNORECASE En gros, permet de ne pas prendre en compte la casse lors des comparaisons de chaines de caractères entre elles où avec des expressions régulières. Voir la page de manuel pour les détails.
NF Norme Française ... NON, Number of fields : C'est le nombre de champs de l'enregistrement en cours.
NR Number of Records : Le nombre d'enregistrements traités jusqu'à maintenant.
RS Le séparateur d'enregistrements. No comment !

Bien entendu, vous pouvez également définir les vôtres.

Exemples :
maVar=3 déclare et initialise la variable myVar à la valeur 3.
print maVar affiche le contenu de la variable maVar.
print $maVar affiche le champ n°maVar de l'enregistrement en cours (ici, équivaut à print $3).
print $(maVar-1) affiche le champ n°(maVar-1) de l'enregistrement en cours (ici, équivaut à print $2).
split("toto:tata:titi:tutu", arr, ":") initialise le tableau arr à ("toto","tata",...). On accède ensuite aux valeurs par arr[1], arr[2], ..., arr[4]

Les fonctions

Il y en a de nombreuses :

Les possibilités de formatage importantes à votre disposition sont définies dans la page de manuel de awk.

Et vous pouvez également définir vos fonctions grâce à la syntaxe : function nom_de_fonction(param1, param2, var_locale_1, var_locale_2) {
...
...
}
ou func nom_de_fonction(param1, param2 var_locale_1, var_locale_2) {
...
...
}
La déclaration des variables locales est étrange. Celà vient du fait que awk n'était pas prévu pour permettre la créaton de fonctions. Il a donc été décidé, de les ajouter à la liste des paramètres, mais séparées par des espaces supplémentaires. L'appel de la fonction, lui, ne contient que les paramètres. Exemple : nom_de_fonction(5,2). Les variables locales sont initialisées à chaine vide ou zéro.

Blocs

Merci à Jean-Louis pour m'avoir montré que je n'insistais pas assez sur le fonctionnement des blocs. C'est grâce à lui que cette partie existe

Utilisation des blocs

Prenons un exemple :

#!/usr/bin/awk -f
BEGIN {
print "début du script"
maVariable=5
nbLignes=0
nbCommentaires=0
nbCommentaires2=0
print "maVariable vaut " maVariable
}

( /^#/ || /^$/ ) {
nbCommentaires++
}

{
nbLignes++
printf "%04d:%s\n",FNR,$0
if ( $0 ~ /^#/ || $0 ~ /^$/ ) {
nbCommentaires2++
}
}

END {
print "fin du script maVariable vaut toujours " maVariable
print "Le script a traité " FNR " lignes dont " nbCommentaires "(=" nbCommentaires2 ") lignes vides ou commençant par un #"
}

Explications :

Exécution

# chmod a+x ceScript.awk
# ./ceScript.awk unfichier.shell
début du script
maVariable vaut 5
0001:# Ce script affiche hello world
0002:
0003:echo "hello world"
0004:exit 0
fin du script maVariable vaut toujours 5
Le script a traité 4 lignes dont 2(=2) lignes vides ou commençant par un #

Divers

awk permet, bien sûr l'utilisation de :

Utiliser awk

Par la ligne de commande

Il existe deux façons de se servir de awk en tapant la commande awk suivie d'arguments.
La première consiste à saisir le code à exécuter directement sur la ligne de commande, celà convient parfaitement à de petits scripts. La syntaxe est la suivante : awk -F séparateur_de_champ 'script awk' fichiers_a_traiter, par exemple awk -F ':' '{ print $1 " est " $5 }' /etc/passwd.
La seconde consiste à placer votre script dans un fichier et à l'appeler par la commande awk -f fichier_script fichier_a_traiter vous pouvez définir le séparateur de champ dans votre script, dans la section BEGIN ou le spécifier sur la ligne de commande grâce à l'option -F, comme ci-dessus. L'exemple ci-dessus deviendrait alors awk -f monScript /etc/passwd ou monScript serait : BEGIN { FS = ":"}
{ print $1 " est " $5 }
ou bien awk -F ':' -f monScript2 /etc/passwd avec monScript2 valant { print $1 " est " $5 }
Notez que dans le script on doit saisir FS = ":" et non FS = ':'. Vous êtes prévenus !

Comme d'habitude, pour les autres options de la ligne de commande, je vous renvoie à la page de manuel de awk.

Des scripts exécutables

Cette utilisation a vite quelque chose de rébarbatif quand même, on rend donc les scripts directement exécutables en plaçant en en-tête #!/bin/awk -f (le chemin doit être adapté à l'emplacement de awk). Créons donc un fichier monScript3 dans lequel nous plaçons : #!/bin/awk -f
BEGIN { FS = ":"}
{ print $1 " est " $5 }
Suite à cette saisie, on rend le script exécutable par chmod a+x et on exécute par ./monScript3 /etc/passwd.

A partir de maintenant, j'estime que vous avez les bases nécessaires pour réaliser vos scripts, si awk vous intéresse. Je vais maintenant vous soumettre des exemples qui vous aideront à mieux comprendre les choses qui sont peut être restées obscures lors de cette présentation sommaire. N'oubliez pas que le bon réflexe en cas de problème reste de lire la doc. En cas de gros pépin, postez un message dans le forum de lea-linux, je ne suis jamais loin ou envoyez moi un mail (j'y réponds dès que j'ai le temps).

Des filtres en exemple

Transformer le fichier hosts

Si vous êtes des habitués du site, ce premier exemple vous rapellera peut être quelque chose puisque je l'avais posté dans le forum en réponse à une personne qui souhaitait adapter le format des IP du fichier hosts à ses besoins, xxx.xxx.xxx.xxx.
Voici donc le code du premier filtre exemple :

#!/usr/bin/awk -f
!(/^#/ || /^$/) {
split ($1, array_ip, ".")
printf "%03d.%03d.%03d.%03d\t%s\t%s\n", array_ip[1], array_ip[2], array_ip[3], array_ip[4],$2,$3
}

La première ligne signifie qu'on va chercher awk dans le répertoire /usr/bin/, ce qui est le bon chemin pour ma slack.
La deuxiême ligne signifie qu'on ne s'intéresse qu'aux lignes qui ne commencent pas par un # ni aux lignes vides. En effet en expressions régulières, ^ signifie début de chaîne et $, fin de chaîne.
Par la suite, on découpe le premier paramètre en prenant comme séparateur le ., on stocke le résultat dans un tableau et on affiche les données du tableau en les formattant puis les autres champs (nom_de_machine et nom_de_machine.domaine). Le formatage %03d signifie qu'on affiche des entiers sur trois caractères et que s'il manque des caractères, on précède ceux qui existent avec des 0.
Il faut rendre ce script exécutable : chmod a+x prepare_hosts.awk
Puis, on l'appelle comme ça : ./prepare_hosts.awk /etc/hosts > hosts.prepared pour récupérer le fichier traité dans un fichier hosts.prepared placé dans le répertoire en cours.

Filtre de GéoConcept© vers GRASS(GNU/GPL)

GéoConcept et GRASS sont deux systèmes d'information géographique (je dirais SIG par la suite). GéoConcept permet d'exporter des objets surfaces selon une syntaxe explicitée ci-dessous. Le fichier export de GéoConcept contient tous les objets exportés.

Bien entendu ce format n'est pas du tout celui de GRASS qui est explicité ci-après, j'ai donc du écrire un p'tit filtre. Il n'est pas universel puisqu'on ne sait pas combien de champs se trouvent avant les coordonnées avant de faire l'export dans GéoConcept. Je le donne ici à titre d'exemple et j'y ai fait de grosses découpes, ne l'utilisez pas tel quel. Si vous êtes arrivés ici en faisant une recherche sur les SIG et que ce script vous intéresse, je peux vous l'envoyer par mail.

Format GéoConcept :
//$SYSCOORD {Type:1}
//$UNIT Distance:m
//$FORMAT 1
28878 buffer comm TRITTELING 911561.09 2462669.95 69 0. 1.e-002 ...
28595 buffer comm LONGWY 848138.94 2509824.85 91 45.62 -171.88 ...
...
Soit en clair :

Format GRASS :

14 lignes d'en-tête comprenant des renseignements divers
...
A 69
2462669.95 911561.09
2462669.96 911561.09
...
Soit en clair :

Les bouts intéressants du script BEGIN {
melange = "v.patch input="
exited = 0
getline h1 < "header"
getline h2 < "header"
...
}

!/\/\// {
print h1 > ENVIRON["LOCATION"] "/dig_ascii/" $1
print h2 > ENVIRON["LOCATION"] "/dig_ascii/" $1
...

melange = melange $1 ".awkImport,"
...

beginObj = 5
if ( (beginObj + 2 + $(beginObj+2) * 2) < NF) {
nbTrou = $((beginObj+3) + $(beginObj+2) * 2)
}
else {
nbTrou = 0
}

for ( i=0 ; i < nbTrou+1 ; i++) {
actualx = $beginObj
actualy = $(beginObj+1)
print "A " ($(beginObj+2)+2) > ENVIRON["LOCATION"] "/dig_ascii/" $1
print " " actualy " " actualx > ENVIRON["LOCATION"] "/dig_ascii/" $1

for ( j=(beginObj+3) ; j<(beginObj+3)+$(beginObj+2)*2-1 ; j++ ) {
actualx += $j
actualy += $(++j)
print " " actualy " " actualx > ENVIRON["LOCATION"] "/dig_ascii/" $1
}
print " " $(beginObj+1) " " $beginObj > ENVIRON["LOCATION"] "/dig_ascii/" $1
...
}
...
fflush (ENVIRON["LOCATION"] "/dig_ascii/" $1)
close (ENVIRON["LOCATION"] "/dig_ascii/" $1)
...
if ( system("v.support map=" $1 ".awkImport option=build -s threshold=" thresh " >/dev/null")!=0 )
exit (1)
}

END {
print "Patching all files into composite.awkImport"
if ( system (melange " output=composite.awkImport" ) != 0 )
exit (1)
...
for (i=1;i<=FNR;i++) {
getline < ARGV[1]
print i ":" $4 > ENVIRON["LOCATION"] "/dig_cats/" mapName
}
system (effaceTempos " > /dev/null")
fflush ()
...
}

Nous allons ensemble analyser les morceaux intéressants de ce script.

Tout d'abord, je définis quelques variables dans la section BEGIN, qui restent accessibles jusqu'à la fin de l'exécution. Ensuite j'affecte des valeurs aux variables h1 et h2 en lisant les lignes dans un fichier du répertoire courant nommé header.

Pour chaque objet (tout ce qui ne commence pas par //), j'insère un entête GRASS, composé des variables h1, h2, etc... que je range dans un fichier avec le symbole de redirection > dont le chemin est la concaténation de la variable d'environnement LOCATION (ENVIRON["LOCATION"]), de la chaîne "/dig_ascii/" et de la première colonne de la ligne que je suis en train de lire ($1).

J'ajoute à la variable melange une chaine constituée de ma variable $1 et de la chaine ".awkImport"

Je dis que les coordonnées de l'objet commencent à la colonne 5. Ceci me dit (voir explications du format GC ci-dessus) que si il y a des trous, c'est à dire si la somme du double du nombre de points intermédiaires donné en colonne 7 ($(beginObj+2) équivaut à $7 car beginObj vaut 5)+ les cinq colonnes de départ + 2 pour les 2 premières coordonnées absolues est inférieur au nombre total de colonnes de la ligne contenu dans la variable NF, alors leur nombre me serra donné à la colonne suivant ce nombre de colonnes précalculé, d'où le nbTrous = $((beginObj+3) + $(beginObj+2) * 2).

Ensuite la reconstitution des coordonnées absolues et l'inscription dans le fichier utilisé plus haut donne l'occasion de voir la syntaxe des boucles for.

On flushe le buffer du fichier et on le ferme. Au début j'avais oublié, alors, je me suis retrouvé avec BEAUCOUP TROP de fichiers ouverts. Vous me direz, le fichier je l'ai pas ouvert. Bé, oui, c'est le côté expérimentation hasardeuse qui m'a fait comprendre. M'enfin, vous pourrez pas dire : "euh, ben, je comprends pas pourquoi mon linux me dit que le script il doit s'arrêter à cause que j'ai un problème de ressources".
Ceci dit, je peux me tromper aussi. Si j'ai tort envoyez moi un mail.

La ligne suivante nous donne un exemple d'utilisation de la commande system, avec récupération du code de retour et aussi de la commande exit. RAS.

La suite montre l'utilisation de variables telles que FNR, ARGV ainsi que des appels à la fonction système avec redirection et l'utilisation des variables déclarées dans le begin et que l'on a fait évoluer au cours du script.

Le mot de la fin, enfin ;-)

Voilà, j'espère vous avoir fait comprendre ce qu'était awk et à quoi il servait. Bien sûr on peut faire la même chose en Perl, PHP, C, Java. Je sais. Mais utiliser awk plutôt que Perl quand il est adapté c'est un peu comme aller dans un petit hôtel/restaurant pittoresque plutôt que d'aller au Club Med. En plus, awk est très léger et peut être un allié puissant dans les systèmes embarqués.

Nota : Je vous ai généré un pdf du manuel de awk accessible ici (64ko). Petit rappel en passant : la manipulation pour générer des pdf à partir des pages de manuel se trouve sur Léa section Trucs et astuces, rubrique Shell.

a+
Xavier

Haut


PostgreSQL: installation

par serge

Une petite description de l'installation de PostgreSQL.


Introduction

PostgreSQL est un système de gestion de base de données relationelles, c'est a dire un SGDBR. C'est un logiciel libre. Sous linux les deux SGDBR les plus utilisés sont PostgreSQL et MySQL. MySQL est trés utilisé pour de petites applications ou pour générer des pages WEB dynamique (comme les forums de ce site par exemple) mais comporte quelques lacunes par rapport à PostgreSQL. De plus PostgreSQL est mieux adapté pour de plus grosses bases, il est plus robuste en quelques sortes.
Cette rubrique n'est pas un apprentissage de PostgreSQL mais juste un "manuel d'installation".

Création d'un utilisateur "root" pour PostgreSQL

PostgreSQL gére lui aussi des utilisateurs pour attribuer des droits aux bases de données comme linux gére les droits pour la gestion des fichiers et du système. Donc comme pour linux il faut un "root" pour PostgreSQL. Pour des questions de sécurité, utilisez toujours un compte user qui n'a aucun droit d'administration linux pour le compte "root" de PostgreSQL. On vas donc créer cet utilisateur, avec par exemple comme nom postgres.
Tapez en root:

#adduser postgres

Validez toutes les questions, et quand le système vous demande un mot de passe, entrez en un.
Maitenant logguez vous en user postgres pour toutes les commandes ci-dessous, sauf si je vous indique clairement de vous logguer en root. Pour tout le reste du document, je suppose que votre root postgresql est "postgres", à vous d'adapter suivant le nom de ce compte.

Récupération des sources et compilation

Bon tout d'abord récupérez les sources sur le site http://www.postgresql.org/sites.html (à l'heure ou j'écris cet article la derniére version stable est la 7.0.2). Récupérez le tarball postgresql-7.0.2.tar.gz (ou une autre version si une mise à jour est sortie). Une fois les sources récupérées, placez vous dans le répertoire ou ces sources se trouvent et decompressées les par:

$ tar zxvf postgresql-7.0.2.tar.gz

On vas copier les sources dans un repertoire source du système:

mkdir /usr/src/pgsql
cp postgresql-7.0.2/* /usr/src/pgsql -r

Passez en root ,mettre l'utilisateur postgres propriétaire de ces sources:

su root
password:
chown postgres /usr/src/pgsql -R
exit

Puis les compiler:

$ cd /usr/src/pgsql/src
$ ./configure "options"

Avec pour les options les plus utiles:

--prefix=chemin chemin ou vous voulez installer PostgreSQL, par défaut s'installe dans /usr/local/pgsql

--enable-locale Ajoute le support des locales (support multi-langues)

--with-odbc Compile le modules ODBC

--with-perl Ajoute le support de Perl pour PostgreSQL et les modules Perl (utile si vous voulez utiliser Perl pour vos applications avec Postgresql)

--with-tcl Ajoute le support Tcl/Tk

Remarque: Pour les developpeurs qui souhaitent ajouter d'autres supports ou fonctions, tapez un ./configure --help pour voir toutes les options disponibles.

Une fois que le ./configure à finis son travail et n'a renvoyé aucune érreur, compilez les sources:

$make

passez en root, tapez un :

make install

puis toujours en tant que root entrez la commande:

chown postgres /usr/local/pgsql (remplacez /usr/local/pgsql par le répertoire spécifié par --prefix si vous en avez spécifiez un)

Configuration du systéme

Passez en root pour ces commandes.
On vas ajouter dans le PATH les binaires de PostgreSQL, les pages man, les data, et ajouter au système les librairies de PostgreSQL et activer le démarrage de PostgreSQL en automatique. Pour cela:

-Editez /etc/profile et ajoutez les lignes:

PATH=$PATH:/usr/local/pgsql/bin
MANPATH=$MANPATH:/usr/local/pgsql/man
PGLIB=/usr/local/pgsql/lib
PGDATA=/usr/local/pgsql/data
export MANPATH PGLIB PGDATA

-Editez /etc/ld.so.conf et ajoutez la ligne:

/usr/local/pgsql/lib

Lancez alors la commande:

ldconfig

-Puis editez le fichier /etc/rc.local et ajoutez ces lignes:

su postgres -c "/usr/local/pgsql/bin/postmaster -S -D /usr/local/pgsql/data" >\/var/log/postgresql.log

Voila notre système est bien configuré. Pour tester toujours en tant que root lancez le script par:

/etc/rc.d/rc.local

Vérifiez que postmaster est bien lancez par un :

ps -aux | grep postmaster

Si tout est bon passez à la suite, autrement relisez bien les indications ci-dessus, vous avez du rater un truc, ou mailez moi

Passez maintenant en utilisateur "postgres" et lancer la commande:

/usr/local/pgsql/initdb

Test du bon fonctionement

On vas maitenant tester le bon fonctionement de PostgreSQL en lançant le test "regression" fournit avec PostgreSQL.
Assurez vous d'être bien en user postgres, puis:

cd /usr/src/pgsql/src/test/regress
make all
make runtest

Vous devez voir apparaitre des lignes fur et à mesure avec des OK. Si vous avez des erreurs, postez les dans le forums pour qu'on vous aide.
J'ajouterais une rubrique de présentation de "psql", l'outils de console de PostgreSQL prochainement.

Haut


Programmation de Script: Une introduction

par Fred, correction et ajouts de Marc

Comment écrire de petits scripts permettant d'automatiser la réalisation de taches répétitives.


Introduction

Vous aurez envie d'écrire un script (petit programme écrit avec un langage simple : shell, perl ou autre) dès que vous aurez tapé dans un terminal quatre fois la même série de commandes et que vous vous apercevrez que vous êtes ammené à le refaire de nombreuses fois.
Un script est une suite d'instructions élémentaires qui sont éxécutées de façon séquencielle (les unes après les autres) par le langage de script.
Dans cet article nous nous limiterons à l'utilisation du shell comme langage, et en particulier à du shell bash. En guise de première introduction, vous pouvez lire ce qui concerne les commandes du shell dans l'article Le Shell et les Commandes. Attention, n'espérez pas que le présent document constitue un manuel complet de programmation ! C'est une courte introduction qui nous l'espérons, vous permettra d'écrire de petits scripts qui vous rendront de précieux services.

Notions de base

Mon premier script.

Pour commencer, il faut savoir qu'un script est un fichier texte standard pouvant être créé par n'importe quel éditeur : vi, emacs, kedit, gnotepad, ou autre. D'autre part, conventionnellement, un script commence par une ligne de commentaire contenant le nom du langage à utiliser pour interpréter ce script, soit dans notre cas : /bin/sh (on parle alors de "script shell"). Donc un script shell élémentaire pourrait être :
#!/bin/sh
Note : "#!" se prononce "she bang", soit "chi-bang".
Évidemment un tel script ne fait rien ! Changeons cela. La commande qui affiche quelque chose à l'écran est echo. Donc pour créer le script bonjour_monde nous pouvons écrire :
#!/bin/sh
echo "Bonjour, Monde !"
echo "un premier script est né."
Comment on l'éxécute ? C'est simple il suffit de faire :
[user@becane user]$ sh bonjour_monde
Bonjour, Monde !
un premier script est né.
[user@becane user]$ _
C'est pas cool, vous préféreriez taper quelque chose comme :
[user@becane user]$ ./bonjour_monde
Bonjour, Monde !
un premier script est né.
[user@becane user]$ _
C'est possible si vous avez au préalable rendu votre script exécutable par la commande :
[user@becane user]$ chmod +x bonjour_monde
[user@becane user]$ ./bonjour_monde
Bonjour, Monde !
un premier script est né.
[user@becane user]$ _
Résumons : un script shell commence par : #!/bin/sh, il contient des commandes du shell et est rendu exécutable par chmod +x.

Quelques conseils concernant les commentaires

Dans un shell-script, est considéré comme un commentaire tout ce qui suit le caractère # et ce, jusqu'à la fin de la ligne.
Usez et abusez des commentaires : lorsque vous relirez un script 6 mois après l'avoir écrit, vous serez bien content de l'avoir documenté. Un programme n'est jamais trop documenté. Par contre, il peut être mal documenté ! Un commentaire est bon lorsqu'il décrit pourquoi on fait quelque chose, pas quand il décrit ce que l'on fait. Exemple :
#!/bin/sh
# pour i parcourant tous les fichiers,
for i in * ; do
# copier le fichier vers .bak
    cp $i $i.bak
# fin pour
done
Que fait le script ? Les commentaires ne l'expliquent pas ! Ce sont de mauvais commentaire. Par contre :
#!/bin/sh
# on veut faire un copie de tous les fichiers
for i in * ; do
# sous le nom *.bak
   cp $i $i.bak
done
Là, au moins, on sait ce qu'il se passe. (Il n'est pas encore important de connaître les commandes de ces deux fichiers.)

Le passage de paramètres

Un script ne sera, en général, que d'une utilisation marginale si vous ne pouvez pas modifier son comportement d'une manière ou d'une autre. On obtient cet effet en "passant" un (ou plusieurs) paramètre(s) au script via la ligne de commande. Voyons comment faire cela. Soit le script essai01:
#!/bin/sh
echo le paramètre \$1 est \"$1\"
echo le paramètre \$2 est \"$2\"
echo le paramètre \$3 est \"$3\"
Que fait-il ? Il affiche, les uns après les autres les trois premiers paramètres du script, donc si l'on tappe :
$ ./essai01 paramètre un
le paramètre $1 est "paramètre"
le paramètre $2 est "un"
le paramètre $3 est ""
$ _
Donc, les variables $1, $2 ...  $9 contiennent les "mots" numéro 1, 2 ... 9 de la ligne de commande. Attention : par "mot" on entend ensemble de caractères ne contenant pas de caractères de séparations. Les caractères de séparation sont l'espace, la tabulation, le point virgule.

Vous avez sans doute remarqué que j'ai utilisé les caractères : \$ à la place de $ ainsi que \" à la place de " dans le script. Pour quelle raison ? La raison est simple, si l'on tape : echo "essai" on obtient : essai, si l'on veut obtenir "essai" il faut dire à echo que le caractère " n'indique pas le début d'une chaîne de caractère (comme c'est le comportement par défaut) mais que ce caractère fait partie de la chaîne : on dit que l'on "échappe" ou "protège" le caractère " en tapant \". En "échappant" le caractère \ (par \\) on obtient le caractère \ sans signification particulière. On peut dire que le caractère \ devant un autre lui fait perdre sa signification particulière s'il en a une, ne fait rien si le caractère qui suit \ n'en a pas.

Maintenant, essayons de taper :

$ ./essai01 *
le paramètre $1 est "Mail"
le paramètre $2 est "essai01"
le paramètre $3 est "nsmail"
$ _
(Le résultat doit être sensiblement différent sur votre machine). Que s'est-il passé ? Le shell a remplacé le caractère * par la liste de tous les fichiers non cachés présents dans le répertoire actif. En fait, toutes les substitutions du shell sont possibles ! C'est le shell qui "substitue" aux paramètres des valeurs étendues par les caractères spéciaux : * (toute suite de caractères) ? (un caractère grave quelconque), [dze] (l'un des caractères d, z ou e), [d-z] (les caractères de 'd' à 'z')... Autre exemple :
$ ./essai01 \*
le paramètre $1 est "*"
le paramètre $2 est ""
le paramètre $3 est ""
$ _
Hé oui, on a "échappé" le caractère * donc il a perdu sa signification particulière : il est redevenu un simple *.

C'est bien, me direz vous, mais si je veux utiliser plus de dix paramètres ? Il faut utiliser la commande shift ; à titre d'exemple voici le script essai02 :

#!/bin/sh
echo le paramètre 1 est \"$1\"
shift
echo le paramètre 2 est \"$1\"
shift
echo le paramètre 2 est \"$1\"
shift
echo le paramètre 4 est \"$1\"
shift
echo le paramètre 5 est \"$1\"
shift
echo le paramètre 6 est \"$1\"
shift
echo le paramètre 7 est \"$1\"
shift
echo le paramètre 8 est \"$1\"
shift
echo le paramètre 9 est \"$1\"
shift
echo le paramètre 10 est \"$1\"
shift
echo le paramètre 11 est \"$1\"
Si vous tapez :
$ ./essai02 1 2 3 4 5 6 7 8 9 10 11 12 13
le paramètre 1 est "1"
le paramètre 2 est "2"
le paramètre 2 est "3"
le paramètre 4 est "4"
le paramètre 5 est "5"
le paramètre 6 est "6"
le paramètre 7 est "7"
le paramètre 8 est "8"
le paramètre 9 est "9"
le paramètre 10 est "10"
le paramètre 11 est "11"
$ _
A chaque appel de shift les paramètres sont décalés d'un numéro : le paramètre 2 devient le paramètre 1, 3 devient 2, etc... Évidemment le paramètre 1 est perdu par l'appel de shift : vous devez donc vous en servir avant d'appeler shift (ou le sauvegarder dans une variable).

Les variables

Le passage des paramètres nous a montré l'utilisation de "noms" particuliers : $1, $2 etc. Ce sont les substitutions des variables 1, 2, etc. par leur valeurs. Mais vous pouvez définir et utiliser n'importe quel nom. Attention toutefois à ne pas confondre le nom d'une variable (notée par exemple machin) et son contenu (noté dans cas $machin). Vous connaissez peut-être la variable PATH (attention, le shell différencie les majuscules des minuscules) qui contient la liste des répertoires (séparés par des ":") dans lesquels il doit rechercher les programmes. Si dans un script vous tapez :
1:#!/bin/sh
2:PATH=/bin           # PATH contient /bin
3:PATH=PATH:/usr/bin  # PATH contient PATH:/bin
4:PATH=/bin           # PATH contient /bin
5:PATH=$PATH:/usr/bin # PATH contient /bin:/usr/bin
(Les numéros ne sont là que pour repérer les lignes, il ne faut pas les taper)
La ligne 3 est très certainement une erreur, à gauche du signe "=" il faut une variable (donc un nom sans $) mais à droite de ce même signe il faut une valeur, et la valeur que l'on a mis est "PATH:/usr/bin"  : il n'y a aucune substitution à faire.
Par contre la ligne 5 est certainement correcte : à droite du "=" on a mis "$PATH:/usr/bin", la valeur de $PATH étant "/bin", la valeur après substitution par le shell de "$PATH:/usr/bin" est "/bin:/usr/bin". Donc, à la fin de la ligne 5, la valeur de la variable PATH est "/bin:/usr/bin".

Attention : il ne doit y avoir aucun espace de part et d'autre du signe "=".

Résumons : MACHIN est un nom de variable que l'on utilise lorsque l'on a besoin d'un nom de variable (mais pas de son contenu), et $MACHIN est le contenu de la variable MACHIN que l'on utilise lorsque l'on a besoin du contenu de cette variable.

Variables particulières

Il y a un certain nombre de variables particulières, en voici quelques unes : Il y en a d'autres, moins utilisées : allez voir la man page de bash.

Saisir la valeur d'une variable

Les paramètres permettent à l'utilisateur d'agir sur le déroulement du script avant son exécution.  Mais il est aussi souvent intéressant de pouvoir agir sur le déroulement du script lors de son exécution, c'est ce que permet la commande : read nom_variable. Dans cette commande vous pouvez bien sûr remplacer nom_variable par le nom de variable qui vous convient le mieux. Voici un exemple simple.
#!/bin/sh
echo -n "Entrez votre prénom : "
read prenom
echo -n "Entrez votre nom de login : "
read nomlogin
echo "Le nom de login de $prenom est $nomlogin."
Ce script se déroule ainsi :
./essai02bis
Entrez votre prénom : Marc
Entrez votre nom de login : spoutnik
Le nom de login de Marc est spoutnik.
Lors du déroulement du script vous devez valider vos entrées en appuyant sur la touche "Entrée".

Arithmétique

Vous vous doutez bien qu'il est possible de faire des calculs avec le shell. En fait, le shell ne "sait" faire que des calculs sur les nombres entiers (ceux qui n'ont pas de virgules ;-). Pour faire un calcul il faut encadrer celui-ci de : $(( un calcul )) ou $[ un calcul ]. Exemple, le script essai03 :
#!/bin/sh
echo 2+3*5 = $((2+3*5))
MACHIN=12
echo MACHIN*4 = $[$MACHIN*4]
Affichera :
$ sh essai03
2+3*5 = 17
MACHIN*4 = 48
Vous remarquerez que le shell respecte les priorités mathématiques habituelles (il fait les multiplications avant les additions !). L'opérateur puissance est "**" (ie : 2 puissance 5 s'écrit : 2**5). On peut utiliser des parenthèses pour modifier l'ordre des calculs.
 

Les instructions de contrôle de scripts

Les instructions de contrôle du shell permettent de modifier l'exécution purement séquencielle d'un script. Jusqu'à maintenant, les scripts que nous avons créés n'étaient pas très complexes. Ils ne pouvaient de toute façon pas l'être car nous ne pouvions pas modifier l'ordre des instructions, ni en répéter.

L'exécution conditionnelle

Lorsque vous programmerez des scripts, vous voudrez que vos scripts fassent une chose si une certaine condition est remplie et autre chose si elle ne l'est pas. La construction de bash qui permet cela est le fameux test : if then else fi. Sa syntaxe est la suivante (la partie else... en italique est optionnelle) :
if <test> ; then
    <instruction 1>
    <instruction 2>
    ...
    <instruction n>
else
    <instruction n+1>
    ...
    <instruction n+p>
fi
Il faut savoir que tous les programmes renvoient une valeur. Cette valeur est stockée dans la variable ? dont la valeur est, rappelons le : "$?".
Pour le shell une valeur nulle est synonyme de VRAI et une valeur non nulle est synonyme de FAUX (ceci parce que, en général les programmes renvoie zéro quand tout c'est bien passé et un code d'erreur (numéro non nul) quand il s'en est produit une).

Il existe deux programmes particuliers : false et true. true renvoie toujours 0 et false renvoie toujours 1. Sachant cela, voyons ce que fait le programme suivant :

#!/bin/sh
if true ; then
    echo Le premier test est VRAI($?)
else
    echo Le premier test est FAUX($?)
fi

if false ; then
    echo Le second test est VRAI($?)
else
    echo Le second test est FAUX($?)
fi

Affichera :
$ ./test
Le premier test est VRAI(0)
Le second test est FAUX(1)
$ _
On peut donc conclure que l'instruction if ... then ... else ... fi, fonctionne de la manière suivante : si (if en anglais) le test est VRAI(0) alors (then en anglais) le bloc d'instructions compris entre le then et le else (ou le fi en l'absence de else) est exécuté, sinon (else en anglais) le test est FAUX(différent de 0)) et on exécute le bloc d'instructions compris entre le else et le fi si ce bloc existe.

Bon, évidemment, des tests de cet ordre ne paraissent pas très utiles. Voyons maintenant de vrais tests.

Les tests

Un test, nous l'avons vu, n'est rien de plus qu'une commande standard. Une des commandes standard est 'test', sa syntaxe est un peu complexe, je vais la décrire avec des exemples. Pour plus d'information faites : man test.

On peut aussi combiner deux tests par des opérations logiques : 'ou' correspond à -o ('o' comme or), 'et' correspond à -a ('a' comme and) (à nouveau allez voir la man page), exemple :

test -x /bin/sh -a -d /etc
Cette instruction teste l'existence de l'éxécutable /bin/sh (-x /bin/sh) et (-a) la présence d'un répertoire /etc (-d /etc).

On peut remplacer la commande test <un test> par [ <un test> ] qui est plus lisible, exemple :

if [ -x /bin/sh ] ; then # ('x' comme executable)
    echo /bin/sh est exécutable. C\'est bien.
else
    echo /bin/sh n\'est pas exécutable.
    echo Votre système n\'est pas normal.
fi

Toujours avec les crochets de test, si vous n'avez qu'une seule chose à faire en fonction du résultat d'un test, alors vous pouvez utiliser la syntaxe suivante :

[ -x /bin/sh ] && echo /bin/sh est exécutable.
ou encore :
[ -x /bin/sh ] || echo /bin/sh n\'est pas exécutable.

L'affichage du message est effectué, dans le premier cas que si le test est vrai et dans le second cas, que si le test est faux. Dans l'exemple on teste si /bin/sh est un fichier exécutable.
Cela allége le script sans pour autant le rendre illisible, si cette syntaxe est utilisée à bon essient.


Mais il n'y a pas que la commande test qui peut être employée. Par exemple, la commande grep renvoie 0 quand la recherche a réussi et 1 quand la recherche a échoué.
Par exemple :

if grep -E "^frederic:" /etc/passwd > /dev/null ; then
    echo L\'utilisateur frederic existe.
else
    echo L'utilisateur frederic n\'existe pas.
fi
Cette série d'instruction teste la présence de l'utilisateur frederic dans le fichier /etc/passwd. Vous remarquerez que l'on a fait suivre la commande grep d'une redirection vers /dev/null pour que le résultat de cette commande ne soit pas affiché : c'est une utilisation classique. Ceci explique aussi l'expression : "Ils sont tellement intéressants, tes mails, que je les envoie vers /dev/null" ;-).

Faire quelque chose de différent suivant la valeur d'une variable

L'instruction case ... esac permet de modifier le déroulement du script selon la valeur d'un paramètre ou d'une variable. On l'utilise le plus souvent quand les valeurs possibles sont en nombre restreint et peuvent être prévues. Les imprévus peuvent alors être représentés par le signe *. Demandons par exemple à l'utilisateur s'il souhaite afficher ou non les fichiers cachés du répertoire en cours.
#!/bin/sh
# pose la question et récupère la réponse
echo "Le contenu du répertoire courant va être affiché."
echo -n "Souhaitez-vous afficher aussi les fichiers cachés (oui/non) : "
read reponse
# agit selon la réponse
case $reponse in
oui)
clear
ls -a;;
non)
ls;;
*)
echo "Erreur, vous deviez répondre par oui ou par non.";;
esac
Seules les réponses "oui" et "non" sont réellement attendues dans ce script, toute autre réponse engendrera le message d'erreur.  On notera qu'ici l'écran est effacé avant l'affichage dans le cas d'une réponse positive, mais pas dans celui d'une réponse négative. Lorsque vous utilisez l'instruction case ... esac, faites bien attention de ne pas oublier les doubles points-virgules terminant les instructions de chacun des cas envisagés.

Faire la même chose pour tous les éléments d'une liste

Lorsqu'on programme, on est souvent amené à faire la même chose pour tous les élément d'une liste. Dans un shell script, il est bien évidemment possible de ne pas réécrire dix fois la même chose. On dira que l'on fait une boucle. L'instruction qui réalise une boucle est
for <variable> in <liste de valeurs pour la variable> ; do
    <instruction 1>
    ...
    <instruction n>
done
Voyons comment ça fonctionne. Supposons que nous souhaitions renommer tous nos fichiers *.tar.gz en *.tar.gz.old, nous taperons le script suivant :
#!/bin/sh
# I prend chacune des valeurs possibles correspondant
# au motif : *.tar.gz
for I in *.tar.gz ; do
    # tous les fichiers $I sont renommés $I.old
    echo "$I -> $I.old"
    mv $I $I.old
# on finit notre boucle
done
Simple, non ?  Un exemple plus complexe ? Supposons que nous voulions parcourir tous les sous-répertoires du répertoire courant pour faire cette même manipulation. Nous pourrons taper :
 1:#!/bin/sh
 2:for REP in `find -type d` ; do
 3:    for FICH in $REP/*.tar.gz ; do
 4:        if [ -f $FICH ] ; then
 5:            mv $FICH $FICH.old
 6:        else
 7:            echo On ne renomme pas $FICH car ce n\'est pas un répertoire
 8:        fi
 9:    done
10:done
Explications : dans le premier 'for', on a précisé comme liste : `find -type d` (attention au sens des apostrophes, sur un clavier azerty français, on obtient ce symbole en appuyant sur ALTGR+é, ce ne sont pas des simples quotes ').
Lorsque l'on tape une commande entre apostrophes inverses, le shell exécute d'abord cette commande, et remplace l'expression entre apostrophes inverses par la sortie standard de cette commande (ce qu'elle affiche à l'écran).
Donc, dans le cas qui nous intéresse, la liste est le résultat de la commande find -type d, c'est à dire la liste de tous les sous-répertoires du répertoire courant.
Ainsi, en ligne 2, on fait prendre à la variable REP le nom de chacun des sous-répertoires du répertoire courant, puis (en ligne 3) on fait prendre à la variable FICH le nom de chacun des fichiers .tar.gz de $REP (un des sous-répertoires), puis si $FICH est un fichier, on le renomme, sinon on affiche un avertissement.

Remarque : ce n'est pas le même fonctionnement que la boucle for d'autres langage (le pascal, le C ou le basic par exemple).

Faire une même chose tant qu'un certaine condition est remplie

Pour faire une certaine chose tant qu'une condition est remplie, on utilise un autre type de boucle :
while <un test> ; do
    <instruction 1>
    ...
    <instruction n>
done
Supposons, par exemple que vous souhaitiez afficher les 100 premiers nombres (pour une obscure raison), alors vous taperez :
i=0
while [ i -lt 100 ] ; do
    echo $i
    i=$[$i+1]
done
Remarque : -lt signifie "lesser than" ou "plus petit que" (et -gt signifie "plus grand", ou "greater than").

Ici, on va afficher le contenu de i et lui ajouter 1 tant que i sera (-lt) plus petit que 100. Remarquez que 100 ne s'affiche pas, car -lt est "plus petit", mais pas "plus petit ou égal" (dans ce cas, utilisez -le et -ge pour "plus grand ou égal").

Refaire à un autre endroit la même chose

Souvent, vous voudrez refaire ce que vous venez de taper autre part dans votre script. Dans ce cas il est inutile de retaper la même chose, préférez utiliser l'instruction function qui permet de réutiliser une portion de script (on dit : une "fonction"). Voyons un exemple :
#!/bin/sh
function addpath ()
{
    if echo $PATH | grep -v $1 >/dev/null; then
        PATH=$PATH:$1;
    fi;
    PATH=`echo $PATH|sed s/::/:/g`
}

addpath /opt/apps/bin
addpath /opt/office52/program
addpath /opt/gnome/bin

Au début, nous avons défini une fonction nommée addpath dont le but est d'ajouter le premier argument ($1) de la fonction addpath à la varaible PATH si ce premier argument n'est pas déjà présent (grep -v $1) dans la variable PATH, ainsi que supprimer les chemins vides (sed s/::/:/g) de PATH.
Ensuite, nous exécutons cette fonction pour trois arguments : /opt/apps/bin, /opt/office52/bin et /opt/gnome/bin.

En fait, une fonction est seulement un script écrit à l'intérieur d'un script. Les fonctions permettent surtout de ne pas multiplier les petits scripts, ainsi que de partager des variables sans se préoccuper de la clause export mais cela constitue une utilisation avancée du shell, nous n'irons pas plus loin dans cet article.

Remarque : le mot function peut être omis, mais son utilisation facilite la lecture du script.

Autres types de répétitions.

Il existe d'autres types de répétitions, mais nous ne nous en occuperons pas dans cet article, je vous conseille la lecture, forcément profitable, de la "man page" de bash (man bash).

À vous de jouer !

Haut


SUID Scripts

par Xavier GARREAU

Cet article fait suite à une question qui avait été débattue sur la liste lea_aide@club.voila.fr au premier semestre 2001. Comme ce sujet m'avait passionné et qu'il pourrait intéresser d'autres personnes, j'ai écrit cet article.
Afin d'éviter une polémique, je dis tout de suite que la réponse apportée ci-dessous m'avait été inspirée par la lecture de l'article "Eviter les failles dès le développement de vos applications" paru dans le Linuxmagazine-france de Décembre 2000. Ceci dit, le problème est traité ici dans une optique différente qui est de permettre à un administrateur de permettre aux utilisateurs de lancer quelques scripts choisis en temps que root.

Introduction

Qu'est ce que j'appelle un SUID Script ? C'est un script que vous souhaiteriez pouvoir exécuter en temps que simple utilisateur mais qui ferait des choses uniquement permises au root.
J'en vois qui sourient en me prenant pour un débile se jeter sur leur shell pour me prouver que je suis un tocard qui ne connait pas chmod 4755. Lisez donc encore quelques lignes avant de m'envoyer un mail d'insultes.

Un échec

Voyons si cet article s'adresse à vous !
Imaginons que vous soyez root vous voulez permettre à un simple utilisateur de lire le contenu de votre répertoire personnel, dans un souci de transparence, pour prouver qu'ils n'y trouveront pas de photos pornographiques. Vous écrivez donc un script qui permet d'afficher le contenu de votre répertoire personnel ( je présuppose chez vous des bases de shell ;-) ) :

[root@ZAZOU /root]# cat > voir_rep_root
#!/bin/sh
echo "Contenu du répertoire de" `whoami`
# ou echo "Contenu du répertoire de" $(whoami)
ls -a /root
< Ctrl+D >
et vous le rendez suid root et vous le placez dans /usr/bin pour que tous les utilisateurs puissent l'exécuter en temps que vous ! :
[root@ZAZOU /root]# chmod 4755 voir_rep_root
[root@ZAZOU /root]# mv voir_rep_root /usr/bin/
(Les plus assidus d'entre vous constaterons que mon PC ne s'appelle plus Rooty mais là n'est pas la question)
Vous testez que ça marche pour vous puis en temps qu'utilisateur normal (l'utilisateur xavier par exemple)
[root@ZAZOU /root]# voir_rep_root
Contenu du répertoire de root
.            .bash_history  .cshrc          .toprc  Mail
..           .bash_logout   .mysql_history  .vimrc
.Xauthority  .bash_profile  .parsecrc       .wmrc
.Xdefaults   .bashrc        .tcshrc         .zshrc
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ voir_rep_root
Contenu du répertoire de xavier
ls: /root: Permission non accordée

Vous avez compris le problème ça y est ? Votre script est exécutable, appartient au root, a le SUID bit à 1 mais n'en tient pas compte.

Scènette de fin de partie
"- La solution c'est chmod 4755 /bin/bash
- Qui a dit ça ? Bill ? A la porte, tout de suite !"

Bill ? Porte ? Je vous laisse méditer là dessus et passe à l'étape suivante.

En C, ça ne marche pas non plus (au début) !

Vous vous dites alors : "Je suis très intelligent, je vais l'avoir en finesse...". Hé hé hé ! Je rigole parce que c'est ce que je me suis dit moi aussi ...
Si sur les programmes ça marche, on est tentés de se dire qu'on va écrire un programme qui appelle le script. Allons y, créons lanceur_de_script.c :

#include <unistd.h>
#include <errno.h>
#include <stdio.h>

extern char **environ;
extern int errno;

int main (int argc, char **argv) {
  execve("/usr/bin/voir_rep_root", NULL, environ);
  printf ("Error : %d\n", errno);
  return errno;
}
( Parenthèse : A ceux qui seraient tentés de dire : "Oui mais pourquoi on met return errno ?", je réponds d'aller lire le man execve où ils pourront constater que si execve ne rencontre pas d'erreur, alors il ne renvoie rien puisque l'image du programme est TOTALEMENT remplacée par celle du programme appelé (ce programme étant l'interpréteur dans le cas d'un script). )
On est content d'avoir fait ce beau programme. Alors on le compile, on le rend exécutable par tout le monde, on met le SUID bit à 1, on le déplace dans /usr/bin et on refait le test de tout à l'heure.
[root@ZAZOU /root]# gcc -o lanceur_de_script lanceur_de_script.c
[root@ZAZOU /root]# chmod 4111 lanceur_de_script
[root@ZAZOU /root]# mv -f lanceur_de_script /usr/bin/
[root@ZAZOU /root]# lanceur_de_script
Contenu du répertoire de root
.            .bash_history  .cshrc          .toprc  Mail
..           .bash_logout   .mysql_history  .vimrc  lanceur_de_script.c
.Xauthority  .bash_profile  .parsecrc       .wmrc
.Xdefaults   .bashrc        .tcshrc         .zshrc
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ lanceur_de_script
Contenu du répertoire de xavier
ls: /root: Permission non accordée

Le début de la compréhension

Oui, pour comprendre, rendez vous à la fin de l'article ;-) ... Pour suivre le raisonnement, ajoutez dans le programme une ligne :
printf ("UID %d - EUID %d\n", getuid(), geteuid()); comme ci-dessous :

#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>

extern char **environ;
extern int errno;

int main (int argc, char **argv) {
  printf ("UID %d - EUID %d\n", getuid(), geteuid());
  execve("/usr/bin/voir_rep_root", NULL, environ);
  printf ("Error : %d\n", errno);
  return errno;
}
Celà vous donne un début de réponse lors de l'exécution (non ?) :
[root@ZAZOU /root]# chmod 4111 lanceur_de_script
[root@ZAZOU /root]# mv -f lanceur_de_script /usr/bin/
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ lanceur_de_script
UID 501 - EUID 0
Contenu du répertoire de xavier
ls: /root: Permission non accordée
Oui, la réponse se situe dans la première ligne de sortie du programme. L'EUID du programme, celle fixée par le SUID bit est bien 0 (root) mais l'uid (celle utilisée pour l'appel du script) est 501. Or, 501 correspond d'après mon fichier /etc/passwd à l'utilisateur xavier. D'où un problème dans le comportement attendu ! En fait le script est appelé avec l'UID et non l'EUID. Il faut donc faire un pas supplémentaire et dire au programme de lancer le script en temps que root, c'est à dire faire en sorte que l'UID devienne l'EUID.

Ça marche mais c'est dangereux

Celà n'est rendu possible que parce que le programme est SUID root. Un programme lancé par le root peut prendre l'UID de n'importe qui, par contre un programme lancé par un utilisateur classique ne peut prendre comme UID que son EUID. C'est à dire l'UID de l'utilisateur qui l'a créé, sous réserve qu'il ait placé le SUID bit à 1.
Bref ! Ce changement se fait grâce à la fonction setreuid. Les fonctions seteuid, setuid et setreuid servent à manipuler les UID et EUID d'un programme. setuid modifie prend un paramètre qu'il affecte à l'UID et l'EUID. seteuid prend un paramètre qu'il affecte à l'EUID. setreuid en prend deux, affect le premier à l'UID et le second à l'EUID. Si un de ces paramètres vaut -1 la valeur correspondante n'est pas changée.
DONC setuid(ID) est équivalent à setreuid(ID, ID) et seteuid(ID) est équivalent à setreuid(-1, ID).

Voici l'illustration de cette partie dans le fichier lanceur_de_script_2.c :

#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>

extern char **environ;
extern int errno;

int main (int argc, char **argv) {
  uid_t uid, euid;
  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
  setreuid (euid, euid);
  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
  execve("/usr/bin/voir_rep_root", NULL, environ);
  printf ("Error : %d\n", errno);
  return errno;
}
Une fois le code mis à jour :
[root@ZAZOU /root]# gcc -o lanceur_de_script_2 lanceur_de_script_2.c
[root@ZAZOU /root]# chmod 4111 lanceur_de_script_2
[root@ZAZOU /root]# mv lanceur_de_script_2 /usr/bin/
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ lanceur_de_script_2
UID 501 - EUID 0
UID 0 - EUID 0
Contenu du répertoire de root
.            .bash_history  .cshrc          .toprc  Mail
..           .bash_logout   .mysql_history  .vimrc  lanceur_de_script.c
.Xauthority  .bash_profile  .parsecrc       .wmrc   lanceur_de_script_2.c
.Xdefaults   .bashrc        .tcshrc         .zshrc

Vous voyez qu'après l'appel à setreuid, nous sommes non seulement UID root mais aussi EUID root et que de par le fait, tous les scripts que nous appelons s'exécutent avec l'identité root... Donc, ça marche ! Fin de l'article.
Mais non, ce n'est pas la fin de l'article car vous vous rendez sûrement compte à ce niveau que le script n'a pas besoin d'être SUID root pour que celà fonctionne...
Imaginons, un instant que l'on remplace l'appel statique au script par un appel à un script passé en paramètre... Ca devient super pratique mais aussi super dangereux, n'importe quel script pouvant être exécuté en temps que root...

Nicking ze danger !

Ouaip, ça marche ! Mais c'est dangeureux et cet article est là pour vous en faire prendre conscience... D'autre part, un autre point à comprendre est que setreuid vous permet certes de gagner l'accés au privilèges du root dans un programme SUID mais qu'il vous permet aussi de les abandonner lorsque vous n'en avez plus besoin.
Celà demande des exemples, allons y, créons le fichier createur_de_fichier.c en temps que root pour prendre conscience du côté éphémère de la toute puissance :

#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>

extern char **environ;
extern int errno;

int main (int argc, char **argv) {
  FILE * fd;
  uid_t uid, euid;

  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
  if (!(fd = fopen("/root/test", "w")))
    printf ("Je n'ai pas pu créer le fichier /root/test avant setreuid\n");
  else {
    printf ("J'ai pu créer le fichier /root/test avant setreuid\n");
    fclose(fd);
  }
  setreuid (euid, uid);
  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
  if (!(fd = fopen("/root/test", "w")))
    printf ("Je n'ai pas pu créer le fichier /root/test après setreuid\n");
  else {
    printf ("J'ai pu créer le fichier /root/test après setreuid\n");
	fclose(fd);
  }
}
Dans le fichier ci-dessus on tente de créer un fichier en c dans un programme SUID puis après avoir inversé l'uid et l'euid. On se rend alors compte que contrairement au cas précédent, c'est l'uid qui compte lors des actions du programme.
[root@ZAZOU /root]# gcc -o createur_de_fichier createur_de_fichier.c
[root@ZAZOU /root]# chmod 4111 createur_de_fichier
[root@ZAZOU /root]# mv -f createur_de_fichier /usr/bin/
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ createur_de_fichier
UID 501 - EUID 0
J'ai pu créer le fichier /root/test avant setreuid
UID 0 - EUID 501
Je n'ai pas pu créer le fichier /root/test après setreuid
[xavier@ZAZOU /root]$

Pour ce qui est du danger, il suffit de créer un programme en c permmettant d'exécuter un script passé en paramètre mais qui vérifie si ce script fait partie d'une liste de scripts autorisés stockée chez le root, dans le répertoire /root/.authoscripts/, dans le fichier liste. D'autre part, les scripts sont cherchés uniquement dans ce répertoire, ainsi, il n'y a que le root qui puisse en ajouter. Il y a quelques instructions à taper :

[root@ZAZOU /root]# mkdir .authoscripts
[root@ZAZOU /root]# cd .authoscripts/
[root@ZAZOU .authoscripts]# touch liste
[root@ZAZOU .authoscripts]# cat >> liste
voir_rep_root
< Ctrl+D >
[root@ZAZOU .authoscripts]#
[root@ZAZOU .authoscripts]# mv /usr/bin/voir_rep_root ./
Créons maintenant la prise en charge des SUID Scripts. On ajoute aux contraintes qu'il doit être possible de passer des paramètres aux scripts.
Tout celà nous donne ce code ci :
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>

extern char **environ;
extern int errno;

int main (int argc, char **argv) {
  FILE * fd;
  uid_t uid, euid;
  int isOK = 0;
  char tmpBuff[256];

  if (argc<2) {
    fprintf(stderr, "USAGE : suscript nom_du_script param_1 param_2 ...\n");
    return 1;
  }
  if (!(fd=fopen("/root/.authoscripts/liste", "r"))) {
    fprintf(stderr, "ERREUR : Impossible d'ouvrir le fichier liste dans\
 /root/.authoscripts.\n");
    return 2;
  }
  while (!feof(fd)) {
    fscanf(fd, "%s", tmpBuff);
    if (!strcmp(argv[1], tmpBuff)) {
      isOK++;
      break;
    }
  }
  fclose (fd);
  if (!isOK) {
    fprintf(stderr, "ERREUR : %s n'est pas un script autorisé...\n", argv[1]);
    return 3;
  }
  uid=getuid();
  euid=geteuid();
  setreuid (euid, euid);
  sprintf (tmpBuff, "/root/.authoscripts/%s", argv[1]);
  execve (tmpBuff, &argv[1], environ);
  printf ("Erreur : %d - %s\n", errno, strerror(errno));
  return errno;
}
Que l'on compile et range ainsi :
[root@ZAZOU /root]# gcc -o suscript suscript.c
[root@ZAZOU /root]# chmod 4111 suscript
[root@ZAZOU /root]# mv suscript /usr/bin
Dont on vérifie le bon foncionnement grâce à la série de commandes qui suit.
[root@ZAZOU /root]# cd .authoscripts/
[root@ZAZOU .authoscripts]# cat > script_test
#!/bin/sh
echo id: `whoami`
echo "Params :" $*
< Ctrl+D >
[root@ZAZOU .authoscripts]# chmod a+x script_test
[root@ZAZOU .authoscripts]# su xavier
[xavier@ZAZOU .authoscripts]$ cd
[xavier@ZAZOU xavier]$ suscript script_test 1 2 3 4 5 6 7 8 9 0
id: root
Params : 1 2 3 4 5 6 7 8 9 0

En savoir plus

man execve et entre autres, la section NOTES :

NOTES
       SUID and SGID processes can not be ptrace()d SUID or SGID.

       A maximum line length of 127 characters is allowed for the
       first line in a #! executable shell script.

       Linux ignores the SUID and SGID bits on scripts.

Vous avez compris le pourquoi de cet article ?

man getuid

man setuid

man setreuid

man getlogin

man cuserid

man chmod

Tshaw

C'est tout pour cette fois !
N'hésitez pas à m'envoyer vos commentaires par mail, il reste sûrement des améliorations à faire.
Retrouvez l'article remis en forme, les sources, archives et binaires sur mon site perso..., http://perso.club-internet.fr/xgarreau/
Vous pouvez également me contacter dans le forum Développement de Léa, je le regarde (très) souvent.
A bientôt,
Xavier GARREAU
Ingénieur de recherche PRIM'TIME TECHNOLOGY : http://www.prim-time.com/
Membre fondateur et président (2002-20??) du ROCHELUG : http://lug.larochelle.tuxfamily.org/
Pigiste ;-) (Léa-Linux et LinuxMag-France)

Haut


Les Librairies C (linux)

par Xavier GARREAU (alaide)

Dans cette page, je vais évoquer la rédaction, la compilation et l'utilisation des librairies en C. Je traiterai avant tout de développement sous linux bien que la majorité des explications soient applicables à d'autres systèmes d'exploitation, y compris ceux qu'on voudrait voir passer par la fenêtre ;-).


Introduction

Intérêt ?

Imaginez : Vous avez développé une série de fonctions très utiles pour un projet, prenons l'exemple d'un tri par bulles. Un jour, vous devez effectuer une nouvelle série de tris. Vous ouvrez le projet et passez la journée à faire du copier coller ?!?! NON !!! car vous aviez pensé à créer un librairie contenant la fonction principale du tri, n'est ce pas ?
Les librairies permettent de se constituer des bibliothèques de fonctions réutilisables et distribuables.

Vous voulez un autre avantage des librairies ?
On les compile à part ! Si vous avez comme moi un pc qui se fait vieux, vous savez que la compilation peut prendre pas mal de temps. La bonne nouvelle c'est qu'une fois les librairies compilées, vous ne les recompilez pas à chaque fois que vous recompilez votre projet. Tout au plus, vous les liez, s'il s'agit de bibliothèques statiques.

Qu'est ce qu'il y a dedans ?

Une librairie est composée d'un fichier d'en têtes (headers en anglais, d'ou le .h), ce sont les fichiers .h qui doivent vous dire quelque chose, non ? Et d'un partie binaire.
Le fichier d'en têtes, permet de "déclarer" les fonctions présentes dans la librairie, le type de valeurs qu'elles renvoient et les paramètres à leur passer.
La partie binaire est constituée d'un ou plusieurs fichiers c compilés. Ces fichiers c contiennent l'implémentation des prototypes de fonctions déclarées dans le fichier d'en têtes.
Vous pouvez modifier le code de la librarie autant que vous le désirez, du moment que vous respectez les prototypes définis dans le fichier .h.

Statique, Dynamique ?

Ouais, entrons dans le vif du sujet. Qu'est ce qu'une librairie statique ? et une librairie dynamique ? Quelles sont les avantages et inconvénients de chacune ?

Plus d'infos sur cette partie ?

Consultez les pages man de dlopen et dlsym et celles qui y sont conseillées et tapant dans la console
	man dlopen
	man dlsym

Rédaction : remarques

Quelques généralités

Si vous voulez que votre librairie soit réutilisable et efficace, vous devez respecter quelques notions fondamentales de la programmation.

Les "chemins"

La directive #include : pour inclure un fichier dans un autre en c, on utilise la directive #include.
On spécifie le chemin relatif entre le fichier incluant et le fichier inclus, par exemple :
#include "mesH/monTest.h"
ou bien on place le fichier .h dans le répertoire ou le compilateur s'attend à trouver les fichiers .h. (regardez dans /usr/include par exemple) et on donne le chemin relatif par rapport à ce répertoire, par exemple :
#include <orb/orbit.h>
Nota Bene : On peut également spécifier d'autres chemins de recherche grâce à l'option -I de gcc, par exemple, pour y inclure le répertoire courant :
gcc -I. etc...

Pour devenir incollables sur ces amusantes petites choses, apprenez par coeur les man pages de gcc et ld. ;-)

Plus d'infos sur cette partie ?

Consultez les pages man des fonctions d'allocation dynamique de mémoire, du compilateur, du linker et celles qui y sont conseillées et tapant dans la console :
	man malloc
	man realloc
	man calloc
	man free
	man gcc
	man ld

On rentre enfin dans le vif du sujet avec cette partie. On va en effet maintenant suivre un exemple pas à pas pour construire une librarie, la compiler, l'utiliser dans un exécutable sous ses formes statiques et dynamiques.
On va prendre l'exemple d'un librairie comportant une fonction permettant de trier un tableau contenant des entiers longs. La méthode de tri sera le tri par bulles. Je ne suis pas vraiment sûr que ce soit cet algorithme là mais ça marche alors ... Roule !



Rédaction : le fichier d'en têtes

Bien entrons dans le dedans du vif du sujet ! ouvrez un éditeur de texte et tapez ça dans un fichier tri_a_bulles.h :

tri_a_bulles.h
/*
 * tri_a_bulles.h
 *
 * Quelques fonctions pour opérations basiques sur un tableau de longs.
 *
 * auteur: Xavier GARREAU : xgarreau@club-internet.fr
 *
 * web : http://perso.club-internet.fr/xgarreau/
 *
 * dmodif: 13.03.2000
 *
 */
#ifndef _TRI_A_BULLES_H
#define _TRI_A_BULLES_H 1

/*
 * test_case_tableau : Dans le tableau pointé par addr_tableau,
 *   Vérifie que la case case case_tableau est bien placée par rapport à celle qui la précède
 *   Si ce n'est pas le cas, permute les cases et se rappelle sur la case précédente.
 *   La récurrence s'arrête quand
 *     la case case case_tableau est bien placée par rapport à celle qui la précède
 *     ou bien si case_tableau vaut prem_case_tableau.
 *   Ne renvoie rien.
 *
 */
void test_case_tableau ( long * addr_tableau,
                         int taille_tableau,
                         int case_tableau,
                         int prem_case_tableau );

/*
 * permut_cases :
 *   permute les valeurs des cases case_1 et case_2 du tableau pointé par addr_tableau
 *   Ne renvoie rien.
 *
 */
void permut_cases ( long * addr_tableau,
                    int case_1,
                    int case_2 );

/*
 * test_tableau :
 *   renvoie le tableau pointé par addr_tableau trié (ordre croissant)
 *     de la case prem_case_tableau
 *     à la case taille_tableau-1
 *
 */
long * test_tableau ( long * addr_tableau,
                      int prem_case_tableau,
                      int taille_tableau );

/*
 * nb_cases_tableau :
 *   Renvoie le nombre de cases du tableau pointé par addr_tableau
 *     càd nombre de cases allouées
 *     ou première case contenant (long)NULL si le tableau en contient une.
 *
 */
int nb_cases_tableau ( long * addr_tableau );

#endif

Si vous êtes familier avec les fichiers .h, pas de problème. Sinon, disons qu'on se contente de définir les "prototypes" des fonctions, on écrira leur corps dans un fichier .c.
Les fichiers .h permettent au compilateur de connaitre les prototypes des fonctions qu'il rencontre dans les différents fichiers.c qui les utilisent.

Je m'explique ! Les fonctions définies dans un fichier .c peuvent être utilisées dans un autre, ça vous le savez ! (hein ? vous le savez ?) Il suffit de préciser au compilateur tous les fichiers à compiler (gcc fic1.c fic2.c ... ficn.c). Si par hasard vous commettez une erreur en appelant une fonction, vous verrez que la compilation se passera sans problème, MAIS, lors de l'exécution vous obtiendrez des résultats inattendus ou pire, une erreur. Brrrrrrrrrrrr ... Flippant non ?

Bon, si on programmait ? Un petit peu de récursivité maintenant ? GO ! BANZAI !

Rédaction : le fichier c

Ouvrez un éditeur de texte et tapez ça dans un fichier tri_a_bulles.c :

tri_a_bulles.c
/*
 * tri_a_bulles.c
 *
 * Quelques fonctions pour opérations basiques sur un tableau de longs.
 *
 * auteur: Xavier GARREAU : xgarreau@club-internet.fr
 *
 * web : http://perso.club-internet.fr/xgarreau/
 *
 * dmodif: 13.03.2000
 *
 */
#include "tri_a_bulles.h"

void test_case_tableau ( long * addr_tableau,
                         int taille_tableau,
                         int case_tableau,
                         int prem_case_tableau ) {
  if ( case_tableau > prem_case_tableau )
    if (addr_tableau[case_tableau] < addr_tableau[case_tableau-1]) {
      permut_cases ( addr_tableau,
                     case_tableau,
                     case_tableau-1 );
      test_case_tableau ( addr_tableau,
                          taille_tableau,
                          case_tableau-1,
                          prem_case_tableau );
    }
}

void permut_cases ( long * addr_tableau,
                    int case_1,
                    int case_2 ) {
  long tempo;
	
  tempo = addr_tableau[case_1];
  addr_tableau[case_1] = addr_tableau[case_2];
  addr_tableau[case_2] = tempo;
}

long * test_tableau ( long * addr_tableau,
                      int prem_case_tableau,
                      int taille_tableau ) {
  int i;
	
  for (i=prem_case_tableau ; i<taille_tableau ; i++)
    test_case_tableau (addr_tableau,
                       taille_tableau,
                       i,
                       prem_case_tableau);
 	
  return addr_tableau;
}

int nb_cases_tableau ( long * addr_tableau ) {
  int nb_cases;
	
  nb_cases=0;
  while (addr_tableau[nb_cases]) nb_cases++;
  return (nb_cases);
}

Finalement ça s'est bien passé non ? Pas si compliqué !
Bon si on veut utiliser ça il va falloir créer une application qui en a besoin ! On y va ?

Rédaction : le fichier de l'application

Maintenant que nous avons les sources de notre librairie, prêtes à être compilées et liées, il va falloir penser à construire une application pour utiliser les fonctions que l'on y a mis ! Ce sera bientôt chose faite si vous voulez bien vous prêter encore un peu au jeu de cette dernière fastidieuse saisie.

Ouvrez un éditeur de texte et tapez ça dans un fichier test.c :

test.c
/*
 * test.c
 *
 * Une application qui utilise les fonctions de la librairie tri_a_bulles
 *
 * auteur: Xavier GARREAU : xgarreau@club-internet.fr
 *
 * web : http://perso.club-internet.fr/xgarreau/
 *
 * dmodif: 14.03.2000
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include "tri_a_bulles.h"

int main (int argc, char * argv[]) {
  long * tab;
  int i;

  /* créée un tableau de 10 longs */
  tab = (long *)malloc( 10 * sizeof (long) );

  /* Initialise le générateur de nombre aléatoires avec ...
   * L'heure de lancement ...
   * voir man random ou man rand pour le pourquoi de la chose !!!
   * voir man time pour le comment !!!
   */
  srandom((int)time((time_t *)NULL));

  /* remplit le tableau avec une suite de nombres pseudo-aléatoires
   * et affiche le contenu.
   */
  for (i=0; i<10; i++) {
    tab[i] = random();
    printf ("tableau[%d] = %ld\n", i, tab[i]);
  }

  /* Affiche le nombre de cases su tableau retourné par la librairie */
  printf ("\nTaille du tableau : %d\n", nb_cases_tableau (tab));

  /* Permute 2 cases et affiche le tableau résultant */
  permut_cases (tab, 2, 8);
  printf ("\nTableau après permutation des cases 2 et 8.\n");
  for (i=0; i<10; i++)
    printf ("tableau[%d] = %ld\n", i, tab[i]);

  /* Trie le tableau et affiche le résultat */
  printf ("\nTableau trié par la fonction de la librairie\n");
  test_tableau ( tab, 0, nb_cases_tableau (tab) );
  for (i=0; i<10; i++)
    printf ("tableau[%d] = %ld\n", i, tab[i]);

  return 0;
}

Bien ! Maintenant qu'on a tous les bouts, on va pouvoir compiler, lier, exécuter, etc...

Compilation de tous les binious

Bon, et bien, nous y voilà, on a tout ! Il ne nous reste plus qu'à tout mettre nesemble selon différentes méthodes. On va commencer par la méthode du projet unique, sans librairies.
Après ça, on va se faire une petite librairie statique, ensuite, on va mettre en place une librarie dynamique (ou shared object, .so chez les pingouins, .dll chez les défenestrés !)
On n'abordera toutefois pas le cas de la construction des dll car franchement, ce serait perdre du temps pour rien. Je suis pour laisser les gens qui n'ont que ça pour occupper leurs tristes journées générer et utiliser (de façon HYPER galère) les librairies dynamiques en envirronnement ouinouin. Ici, je pense que nous sommes entre gens sérieux, on développe donc sous linux, FreeBSD, solaris ou autres unix. Franchement, je m'excuse de sembler aussi méchant vis à vis de wintruc mais quand vous aurez comparé les qualités des deux systèmes (au moins en matière de support de développement), je suis presque sûr que vous penserez comme moi.

A la fin de cette série d'infos, je vous renverrai sur les bons coins pour utiliser les makefiles, ainsi que les merveilleux outils GNU que sont autoconf, autoscan, automake et leurs copains.

Sans librairies

Placez vous dans le répertoire du projet puis tapez ça dans la console :
gcc -o test test.c tri_a_bulles.c
Ceci génère un exécutable test que l'on lance, toujours en étant placé dans le répertoire de projet, en tapant :
./test

Avec librairies statiques

Placez vous dans le répertoire du projet puis tapez ça dans la console :
gcc -c test.c
Vous obtenez un objet binaire test.o. C'est une compilation sans édition de liens. Compilez de même la librairie en tapant :
gcc -c tri_a_bulles.c
Ce qui donne tri_a_bulles.o. Comme je l'ai déjà dit, si plusieurs fichiers composent la bibliothèque, on aurait tapé
gcc -c fic1.c fic2.c ...
Il faut ensuite créer la bibliothèque. Pour celà, tapez :
ar -q tri_a_bulles.a tri_a_bulles.o
S'il y avait eu plusieurs fichiers ... référez vous au pages man de ar. Regardez surtout les options a, q et c !

Ok, maintenant on lie le tout :

gcc -o test test.c tri_a_bulles.a
Petite précision, normalement, l'éditeur de liens GNU de linux c'est ld. Ceci dit, ça marche avec gcc parce qu'il "l'appelle" alors, on ne vient pas se plaindre. Toutefois, dans le doute et pour en savoir plus, tapez man ld

On ne se laisse pas décourager pour autant et on exécute :

./test
C'est bizzare non ? Ca marche pareil. Bienvenue dans le monde du codage efficace ...

Avec librairies dynamiques

Placez vous dans le répertoire du projet puis tapez ça dans la console :
gcc -c test.c
Puis :
gcc -c tri_a_bulles.c
Jusque là, vous n'êtes pas perdus, c'est pareil ! Oui, mais, maintenant on génère la librairie dynamique :
gcc -o tri_a_bulles.so -shared tri_a_bulles.o
Puis on génère l'exécutable en lui disant qu'il fera appel à la librarie dynamique tri_a_bulles.so :
gcc -o test test.o tri_a_bulles.so
Puis content qu'on est, on exécute :
./test
./test: error in loading shared libraries:
tri_a_bulles.so:
cannot open shared object file:
No such file or directory
Et oui, le bon des librairies partagées c'est qu'elles sont liées au moment de l'exécution. Or, les librairies partagées, le système va les chercher dans un répertoire contenu dans la variable d'environnement LD_LIBRARY_PATH. Et bien, par défaut le répertoire courant n'en fait pas partie ... C'est con ?
Non, pas tant que ça ! Et il y a des outils, là encore pour faciliter les choses ...
Tapez :
ldd ./test
tri_a_bulles.so => not found
libc.so.6 => /lib/libc.so.6 (0x4001a000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
Visiblement, il y a une couille !
Alors, soit vous copiez votre librairie dans les répertoires qui contiennent des librairies dynamiques (/usr/lib, /usr/i486-linux-libc5/lib ou /usr/X11R6/lib chez moi), vous trouverez ça dans le fichier /etc/ld.so.conf.
Ou alors ajouter votre répertoire à la variable en tapant
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
Dans les deux cas, vous obtiendrez le résultat attendu en tapant :
./test

En conclusion


Avec les libraries statiques :


Avec les libraries dynamiques :

Notons que :

Le résultat

J'allais oublier !!!
Normalement, le programme affiche un truc comme ça :
xavier@Rooty
Rep: tabLong
$ ./test
tableau[0] = 1613489603
tableau[1] = 866884903
tableau[2] = 295298324
tableau[3] = 1614953842
tableau[4] = 1111079167
tableau[5] = 950260573
tableau[6] = 901366332
tableau[7] = 745370511
tableau[8] = 2063084132
tableau[9] = 374329280

Taille du tableau : 10

Tableau après permutation des cases 2 et 8.
tableau[0] = 1613489603
tableau[1] = 866884903
tableau[2] = 2063084132
tableau[3] = 1614953842
tableau[4] = 1111079167
tableau[5] = 950260573
tableau[6] = 901366332
tableau[7] = 745370511
tableau[8] = 295298324
tableau[9] = 374329280

Tableau trié par la fonction de la librairie
tableau[0] = 295298324
tableau[1] = 374329280
tableau[2] = 745370511
tableau[3] = 866884903
tableau[4] = 901366332
tableau[5] = 950260573
tableau[6] = 1111079167
tableau[7] = 1613489603
tableau[8] = 1614953842
tableau[9] = 2063084132
En scoop, vous apprenez que mon pc s'appelle Rooty (ce qui vient des Régulateurs de Bachman), que je me connecte sous le nom de xavier et que ce projet se trouve dans le répertoire tabLong. En outre le symbole $ précise que je ne suis qu'un simple utilisateur, le root ayant droit à un superbe #. Voilà !

Pour ce qui est du renvoi sur la doc sur autoscan, autoconf, automake et leurs copains ce que j'ai trouvé de plus sympa, ce sont les infos pages du gnome-help-browser. Si vous ne l'avez pas installé, vous pouvez les consulter dans la console en tapant info automake ou info autoscan, etc ... !

Pour trouver moults docs de developpement, voyez http://developer.gnome.org/. Il existe la même chose avec les libs kde sur http://developer.kde.org/. Il existe bien d'autres sources d'infos mais ce sont celles que je préfère.

Un petit conseil, si vous aimez le c, installez les librairies gtk+/gdk/glib/imlib/ORBit C'est le top.
Vous cherchez un environnement de développement ? Choisissez gIDE et glade si vous avez des affinités avec le projet et les librairies du projet gnome ou kdevelop pour affinités avec KDE.
Comme débogueur je dois dire que kdbg est génial, d'autant qu'il s'intègre via DCOP dans kdevelop. Mais xemacs est pas mal non plus et gdb tout seul aussi, le tout c'est de connaitre !
En bref, prenons ce qui existe de meilleur partout. Bienvenue dans linux et à bientôt pour de nouvelles aventures ...

Haut


Utiliser la librairie gd

par Xavier GARREAU (alaide)

Vous avez sûrement déjà vu ou utilisé la librairie gd. En effet, si vous créez les statistiques de votre serveur web avec webalizer ou si vous avez déjà généré des images avec php, vous avez déjà bénéficié des bienfaits de cette librairie. Il s'agit d'une librairie permettant de générer des images dans un programme. Initialement gd est écrite pour le C mais il existe des portages pour perl, tcl, pascal, haskell. Bien évidemment, gd est utilisable depuis php.

Introduction

Présentation

Traduction de la présentation de gd1.8.3 sur le site officiel.

" gd1.8.3 permet la création d'images aux formats png, jpeg et wbmp, mais pas gif. C'est une bonne chose, png est un format plus compact, et la "full compression" est permise. le format JPEG est bien pour les images photographiques et est plus compatible avec les browsers actuels que le format png. le format WBMP sert aux appareils sans fils (ndt: applications wap) mais pas aux browsers classiques. Le code existant basé sur d'anciennes versions de gd qui prenaient en charge le format gif devra être modifié. Il faudra utiliser gdImagePng ou gdImageJpeg en lieu et place des appels à gdImageGif. Ne nous demandez pas de vous fournir d'anciennes versions de gd. Unisys possède une licence sur l'algorithme de compression LZW qui est utilisé pour générer les "images GIF compressées". La meilleure solution est d'adopter des formats modernes, libres, bien compressés tels que PNG et JPEG autant que possible et le plus tôt sera le mieux. "
Nota : Cet article est basé sur la version 1.8.3 de la librairie gd.

Il vous faut

Pour installer la version minimale de gd1.8.3 vous devez avoir sur votre système (les librairies + les .h):

Si vous désirez inclure le support du format jpeg, vous devez également avoir la librairie jpeg6b ou supérieure.
Si vous désirez inclure le support des TrueTypeFonts, vous devez également avoir la librairie Freetype et quelques polices ttf.
Si vous désirez inclure le support de chargement d'images XPM, vous devez avoir X et la librairie XPM sur votre système.

Préparatifs

Trouver le tout

Sur ma machine, j'avais déjà toutes les librairies et fichiers .h requis (sauf gd naturellement et freetype) mais ça ne sera peut être pas le cas pour tout le monde. J'ai donc regroupé ci-dessous les emplacements des choses dont vous pourriez avoir besoin, y compris gd bien sûr.

Compilation et installation

Vous avez à ce stade compilé et installé les pré-requis. Nous sommes donc prêts à aller plus avant.
Commencez par vous placer dans le répertoire où vous avez placé l'archive de gd puis tapez tar xvfz gd-1.8.3.tar.gz. Cela vous créé un répertoire gd-1.8.3. Débarrassez vous de l'archive éventuellement en tapant rm -f gd-1.8.3.tar.gz.
Placez vous dans le répertoire en tapant cd gd-1.8.3. Editez le makefile en fonction des librairies dont vous disposez et de leurs emplacements.

1. Mettez un '#' devant les lignes commençant par LIBS puis tapez la vôtre comme suit :
LIBS=-lm -lgd -lpng -lz [-ljpeg] [-lttf] [-lXpm -lX11]
Expliquons ce que les options sont :

En clair, la ligne libs minimale pour réaliser des images png est LIBS=-lm -lgd -lpng -lz. C'est ce qui est saisi par défaut donc si cela vous va, ne changez rien à cette ligne.

2. Editez les lignes INCLUDEDIRS et LIBDIRS si vos librairies ne se trouvent pas dans les répertoires saisis par défaut.

3. Editez les lignes INSTALL_LIB, INSTALL_INCLUDE et INSTALL_BIN si les valeurs par défaut ne vous conviennent pas (respectivement /usr/local/lib, /usr/local/include et /usr/local/bin). Il est à noter que, généralement, ces répertoires ne sont pas parcourus par défaut. Donc, si vous êtes root et que vous voulez rendre accessible gd à tous sans vous poser de questions, remplacez ces lignes par :
INSTALL_LIB=/usr/lib
INSTALL_INCLUDE=/usr/include
INSTALL_BIN=/usr/bin

4. Mettez un '#' devant les lignes commençant par CFLAGS puis tapez la vôtre comme suit:
CFLAGS=-O [-DHAVE_XPM] [-DHAVE_JPEG] [-DHAVE_LIBTTF]
Expliquons :

Comme il est dit dans le Makefile, les lignes du dessous ne vous regardent pas. Vous pouvez quand même les modifier si vous voulez, notamment pour corriger le numéro de version ;-).

A ce point vous pouvez taper make. Tout devrait bien se passer. Continuer en tapant make install si vous êtes root ou si vous avez les droits en écriture dans les répertoires spécifiés dans l'étape 3. ci-dessus. Sinon, ce n'est pas grave, il vous suffira d'indiquer au compilateur où trouver les librairies et fichiers d'en-tête quand vous compilerez vos applications.

Utilisation

Je considère ici que vous avez installé les librairies dans des répertoires habituels, où le compilateur et le linker peuvent trouver les fichiers nécessaires.
Dans le cas contraire, créez un répertoire de travail où vous saisirez les exemples, copiez y le répertoire gd-1.8.3 et lors de la compilation, ajoutez après gcc -o expl_gd_x les options -I./gd-1.8.3 -L./gd-1.8.3.

Préambule

Avec gd vous pouvez créer vos images entièrement, à partir de rien, où vous servir d'images existantes. Les exemples simplistes ci-dessous vous montrent comment faire. Si vous avez déjà utilisé php pour réaliser des images, vous ne serez pas dépaysés par la syntaxe ;-).

Exemple1

Dans cet exemple nous allons créer une image simple, il s'agira d'une croix rouge sur fond blanc. Notez qu'il faut, dans l'ordre :

/*
 * Fichier expl_1.c
 */
#include <stdlib.h> /* Y'en aura bien besoin (pour le exit) */

#include <gd.h> /* On va utiliser gd */
/* Théoriquement il faut inclure stdio.h
   MAIS c'est fait dans gd.h donc ... */

int main(void) {
  gdImagePtr image; /* Pointeur vers notre image */
  FILE *image_png; /* Fichier image PNG */
  int rouge, blanc; /* Deux couleurs */

  /* On créée un image de 100 par 100 */
  image = gdImageCreate(100, 100);

  /*
   * On "alloue" deux couleurs.
   * Notez que la première sera la couleur de fond
   *
   * En paramètres : - l'image
   *                 - les valeurs de rouge, vert et bleu (de 0 à 255)
   */
  blanc = gdImageColorAllocate(image, 255, 255, 255);
  rouge = gdImageColorAllocate(image, 255, 0, 0);

  /*
   * On trace la croix, deux rectangles pleins qui se croisent
   * le premier de x=20,y=45 à x=80,y=55
   * le second de x=45,y=20 à x=55,y=80
   * tous les deux de couleurs rouge.
   */
  gdImageFilledRectangle(image, 20, 45, 80, 55, rouge);
  gdImageFilledRectangle(image, 45, 20, 55, 80, rouge);

  /* Ouvrir le fichier image en écriture. */
  image_png = fopen("expl1.png", "w");

  /* Créer l'image au format PNG. */
  gdImagePng(image, image_png);

  /* On ferme le fichier de l'image. */
  fclose(image_png);

  /* On détruit l'image. */
  gdImageDestroy(image);

  exit (0);
}

On compile en tapant
gcc -o expl_gd_1 expl_1.c -lgd -lpng
ou
gcc -o expl_gd_1 expl_1.c -I./gd-1.8.3 -L./gd-1.8.3 -lgd -lpng (Voir plus haut)
Après exécution par ./expl_gd_1, vous obtenez dans le répertoire courant une image expl1.png. résultat du code...

Nous avons vu ici la fonction permettant de créer des rectangles pleins. Mais, nous pouvons également avec gd créer des rectangles vides, des lignes continues où non (tirets, style défini par l'utilisateur), des polygones pleins ou non, des arcs et ce en utilisant des lignes basiques ou en créant des "pinceaux" (brushes) ou bien encore ajouter du texte ...

Exemple2

Nous allons maintenant écrire, en noir, en utilisant les polices par défaut de gd, contenues dans les fichiers fournis dans l'archive. Ce sont les fichiers gdfont*.h. A ces fichiers correspondent les polices gdFontTiny, gdFontSmall, gdFontMediumBold, gdFontLarge et gdFontGiant
Dans cet exemple, on va générer une image de la taille requise par les textes, ni plus ni moins. La hauteur et la largeur d'un caractère étant données par gdFontxxx->h et gdFontxxx->w.
Si on souhaite écrire verticalement, on utilisera la fontion gdImageStringUp.
Si on souhaite utiliser les polices TrueType on pourra se référer à la fonction gdImageStringTTF. Attention, pour les polices TT, les prototypes de fonctions sont différents et la récupération de l'espace occupé ne se fait pas de la même manière. D'autre part, les polices TT ne sont pas fournies dans l'archive gd, faites un find / -name *.ttf pour rechercher les polices sur votre système.

/*
 * Fichier expl_2.c
 */
#include <stdlib.h> /* Y'en aura bien besoin (pour le exit) */

#include <gd.h> /* On va utiliser gd */
/* Théoriquement il faut inclure stdio.h 
   MAIS c'est fait dans gd.h donc ... */

#include <gdfontt.h> /*on va utiliser la police gdFontTiny */
#include <gdfonts.h> /*on va utiliser la police gdFontSmall */
#include <gdfontmb.h> /*on va utiliser la police gdFontMediumBold */
#include <gdfontl.h> /*on va utiliser la police gdFontLarge */
#include <gdfontg.h> /*on va utiliser la police gdFontGiant */

int main(void) {
  gdImagePtr image; /* Pointeur vers notre image */
  gdFontPtr mesPolices[5]; /* tableau des polices */
  FILE *image_png; /* Fichier image PNG */
  int blanc, bleu; /* Nos deux couleurs */
  char *message = "Hello World !"; /* Un message original */
  int long_message; /* La taille du message */
  int tmp_larg, larg = 0, haut = 40; /* Dimension de l'image */
  int posx = 10, posy = 10; /* Position du texte */
  int i; /* Ca ressemble à une variable pour une boucle for ;-) */

  /* on remplit le tableau */
  mesPolices[0] = gdFontTiny;
  mesPolices[1] = gdFontSmall;
  mesPolices[2] = gdFontMediumBold;
  mesPolices[3] = gdFontLarge;
  mesPolices[4] = gdFontGiant;

  /* On calcule la longueur du message */
  long_message = strlen(message);

  /* On calcule les dimensions de l'image */
  for (i=0 ; i<5 ; i++) {
  	haut += mesPolices[i]->h;
  	larg = ( larg < ( 
  	 tmp_larg = long_message*mesPolices[i]->w + 20 ) 
  	                ) ? tmp_larg : larg;
  }

  /* On créée un image de larg par haut */
  image = gdImageCreate(larg, haut);

  /* On alloue deux couleurs. */
  blanc = gdImageColorAllocate(image, 255, 255, 255);
  bleu = gdImageColorAllocate(image, 0, 0, 88);

  /*
   * On écrit le texte avec les cinq polices en bleu.
   * On remet à jour la hauteur
   */
  for (i=0 ; i<5 ; i++) {
  	posy += mesPolices[i]->h;
  	gdImageString(image, mesPolices[i], posx, posy, message, bleu);
  }

  /* Ouvrir le fichier image en écriture. */
  image_png = fopen("expl2.png", "w");

  /* Créer l'image au format PNG. */
  gdImagePng(image, image_png);

  /* On ferme le fichier de l'image. */
  fclose(image_png);

  /* On détruit l'image. */
  gdImageDestroy(image);

  exit (0);
}

On compile en tapant
gcc -o expl_gd_2 expl_2.c -lgd -lpng
Après exécution par ./expl_gd_2, vous obtenez dans le répertoire courant une image expl2.png. résultat du code...

Exemple3

Nous savons à présent créer des images mais il peut être utile d'en utiliser une existante qui servira de base à notre création afin de créer un histogramme sur un fond dégradé, pour rendre une couleur d'image transparente où créer des boutons personnalisés sur une page web.

Prenons ce cas de figure comme exemple :
Vous avez une page qui permet de contacter les 10 personnes les plus actives d'un forum en cliquant sur un bouton contenant leur pseudo. Vous ne pouvez pas à l'avance savoir qui sera dans les 10 personnes ou non, vous avez donc un script qui vous donne les dix personnes et un lien vers eux. Examinons ce qu'il faut faire pour générer un bouton contenant leur nom.

Jusqu'à maintenant on a créé les images avec gdImageCreate. Il existe aussi :

Ces fonctions permettent de récupérer, pour traitement, une image existante. Nous allons travailler à partir d'un fichier image PNG contenant le bouton, vide, bout_in.png. résultat du code...
Je sais, ce n'est pas beau mais le but est de montrer ce que l'on peut faire, pas de faire beau. On pourra améliorer cela en partant d'un plus beau bouton et en utilisant une police TrueType pour le texte.
On va créer un cgi, pour changer. Il enverra l'image sur le flux standard, précédée de l'entête adéquate, ici Content-type: image/png suivi de deux retours à la ligne, comme le veux la norme.
/*
 * Fichier expl_3.c
 */
#include <stdlib.h> /* Y'en aura bien besoin (pour le exit) */

#include <stdlib.h> /* Pour le getenv */

#include <gd.h> /* On va utiliser gd */
/* Théoriquement il faut inclure stdio.h 
   MAIS c'est fait dans gd.h donc ... */

#include <gdfontmb.h> /* On va utiliser la police gdFontMediumBold */

int main(void) {
  gdImagePtr image; /* Pointeur vers notre image */
  FILE *image_png_in; /* Fichier image PNG */
  int jaune; /* Notre couleur */
  char *texte;
  int long_texte; /* La taille du texte */

  /* récupère le texte */
  if ( !(texte = getenv("QUERY_STRING")) ) {
  	texte = "Erreur !";
	}
  long_texte = strlen(texte);

  /* On ouvre l'image initiale */
  if ( !(image_png_in = fopen ("bout_in.png", "rb")) ) {
  	fprintf (stderr, "Impossible de trouver l'image bout_in.png.\n\n");
  	exit (1);
  }
  image = gdImageCreateFromPng(image_png_in);

  /* On ferme le fichier de l'image. */
  fclose(image_png_in);

  /* On alloue une couleur. */
  jaune = gdImageColorResolve(image, 0xff, 0xff, 0);

  /*
   * On écrit le texte
   */
  gdImageString(image, gdFontMediumBold,
                (image->sx-long_texte*gdFontMediumBold->w)/2,
                (image->sy-gdFontMediumBold->h)/2,
                texte, jaune);

  /* Envoyer l'image au format PNG. */
  printf ("Content-type: image/png\n\n");
  gdImagePng(image, stdout);

  /* On détruit l'image. */
  gdImageDestroy(image);
  exit (0);
}
On compile en tapant
gcc -o expl_gd_3 expl_3.c -lgd -lpng
On place le binaire obtenu et un fichier contenant le bouton vide (bout_in.png) dans le répertoire cgi-bin de son serveur web.
Pour utiliser ce que l'on vient de faire, il suffit d'inclure dans un fichier html (ce fichier peut être généré par un cgi, vous me suivez ?), un tag img, en passant le texte au script en "query_string". Et Hop !
Exemple :
<html>
<body bgcolor="#ffffff" text="#000088">
<p><b>Mes boutons : </b></p>
<p><img src="/cgi-bin/expl_gd_3?Zazou"></p>
<p><img src="/cgi-bin/expl_gd_3?Xavier"></p>
<p><img src="/cgi-bin/expl_gd_3?alaide"></p>
</body>
</html>

On obtient alors une page semblable à celle-ci :
résultat du code...

Aller plus loin

J'ai présenté ici quelques fonctionnalités de la librairie gd. Cela vous aura, j'espère, donné envie de creuser plus avant par vous mêmes. D'autres fonctionnalités dont je n'ai pas parlé sont la copie de portions d'images, l'ouverture de portions d'images uniquement avec changement d'échelle à la volée, fonctionnalités très utiles pour se lancer dans la génération d'extraits de grosses images (photos aériennes ou satellitaires ou je ne sais quoi d'autre). Pour plus d'informations et pour la liste complète des fonctions de lalibrairie, consultez le fichier index.html de l'archive de gd ou la page officielle : http://www.boutell.com/gd/

J'ai placé les images, sources et toutes les archives nécessaires à la réalisation de ces exemples ainsi que les 3 binaires obtenus sur cette page. Attention, les versions des archives seront sans doute vite obsolètes mais ce sont celles utilisées pour cet article.

Pour info, si vous voulez tester les binaires, il vous faudra (en plus de la libc et ld-linux que vous avez) :

Comme d'habitude, j'ai "appris" ça en tapant ldd expl_gd_1, ldd expl_gd_2 et ldd expl_gd_3 dans ma console préférée.

N'hésitez pas à envoyer vos commentaires par mail en cliquant sur mon nom en haut de la page.
A bientôt, Xavier GARREAU (alaide)

Haut


Polices Freetype et gd-2.0

par Xavier GARREAU

Voici le deuxième article concernant gd. Il traite de l'utilisation de Freetype pour inclure du texte dans vos images et des nouveautés apportées à gd par la version 2.

Introduction

L'inclusion de textes dans les images est quelque chose de très utile, pour ne pas dire indispensable et cette facette de gd méritait bien un article à elle toute seule. Je parlerai donc ici de l'utilisation de la librairie freetype-2.x pour la génération d'images avec gd-2.0.1, en langage c. En effet, à l'heure où j'écris ces lignes la librairie gd-2 n'est pas encore stable mais bon, elle le sera bientôt et les seules nouveautés d'ici là ne seront que corrections d'éventuels bugs. Pour ce qui est du choix de ne traiter que la librairie freetype-2, il vient du fait que la version 2 de gd fait pareil. Les anciennes fonction liées à freetype-1 (celles qui se terminent par TTF) ne font que renvoyer vers celles liées à freetype-2 (celles qui se terminent par FT).
A l'issue de cette première partie de l'article, vous pourrez utiliser vos polices de windows à partir de programmes c sous linux, ainsi que toutes les autres polices truetype, freetype, ... (voir http://www.freetype.org pour plus d'infos)
La deuxième partie de cet article examinera les nouveautés de gd dans sa deuxième version.

Préparatifs

Compilation et installation de freetype-2

Téléchargez la dernière version de Freetype-2. Lors de l'écriture de l'article, il s'agissait de la version 2.0.3.
Voici comment on l'installe :
Placez vous dans le répertoire dans lequel vous avez téléchargé freetype puis tapez :
tar xvfy freetype-2.0.3.tar.bz2
cd freetype-2.0.3
make setup
make
su
make install
Cela installe freetype sous /usr/local. Vous pouvez ensuite redevenir "simple utilisateur".

Compilation et installation de gd

Pour avoir plus d'informations sur l'installation de gd, consultez le premier article sur gd, sur léa.

Placez vous dans le répertoire dans lequel vous avez téléchargé gd puis tapez

tar xvfz gd-2.0.1.tar.gz
cd gd-2.0.1
Editez le début du Makefile comme suit :
COMPILER=gcc
AR=ar
CFLAGS=-g -DHAVE_LIBPNG -DHAVE_LIBJPEG -DHAVE_LIBFREETYPE
LIBS=-lgd -lpng -lz -ljpeg -lfreetype -lm
INCLUDEDIRS=-I. -I/usr/local/include/freetype2
LIBDIRS=
INSTALL_LIB=/usr/local/lib
INSTALL_INCLUDE=/usr/local/include
INSTALL_BIN=/usr/local/bin
Puis tapez:
su
make install
Il ne s'agit pas d'une erreur de ma part, il faut bien taper directement make install, c'est comme ça !
Si vous obtenez une erreur du type : unresolved symbol (sur gdImageCreateFromPng par exemple) :

Utilisation

Pour les notions de base concernant gd, reportez vous à l'article précédent, celui-ci constitue une suite.

Exemple1

Dans cet exemple nous allons recréer l'image simple du premier exercice du premier article mais ajouterons dans la croix la mention santé écrite en Comics, par exemple, et ce, en jaune.
Il faut : La declaration de la fonction permettant d'écrire du texte en utilisant FreeType est la suivante :
char *gdImageStringFT(gdImagePtr im,
                            int *brect,
                            int color,
                            char *fontname,
                            double ptsize,
                            double angle,
                            int x, int y,
                            char *chaine)
où :


Le code :

/*
 * Fichier expl_gd2_1.c
 *
 * Pour les explications de base,
 * se reporter à l'article sur gd.
 */
#include <stdlib.h>
#include <gd.h>

// Deux macros qui simplifient la vie
#define brect_largeur (brect[4]-brect[0])
#define brect_hauteur (brect[1]-brect[5])

int main(void) {
  gdImagePtr image;
  FILE *image_png;
  char *err;
  int rouge, blanc, jaune, noir;
  char *chaine = "santé"; // La chaîne à écrire
  char *font = "/usr/share/ttf/comic.ttf"; // La police
  double taille = 20; // La taille de la police
  int brect[8]; // Les coordonnées du rectangle
  // entourant le texte entier.
  /* brect[0] X bas gauche
   * brect[1] Y bas gauche
   * brect[2] X bas droit
   * brect[3] Y bas droit
   * brect[4] X haut droit
   * brect[5] Y haut droit
   * brect[6] X haut gauche
   * brect[7] Y haut gauche
   */

  image = gdImageCreate(100, 100);
  blanc = gdImageColorAllocate(image, 255, 255, 255);
  rouge = gdImageColorAllocate(image, 255, 0, 0);
  jaune = gdImageColorAllocate(image, 255, 255, 0);
  noir = gdImageColorAllocate(image, 0, 0, 0);

  gdImageFilledRectangle(image, 20, 40, 80, 60, rouge);
  gdImageFilledRectangle(image, 40, 20, 60, 80, rouge);

  /* Les nouveautés commencent ici */
  /* On a droit à 60x20 pour placer notre chaîne
   * soit brect[4]-brect[0]<60 ET brect[1]-brect[5]<20
   * --> Pour récupérer brect sans écrire
   * le texte, on place im à NULL
   * Si on a un dépassement on réduit la 
   * taille de la police et on recommence
   */
  do {
    err = gdImageStringFT (NULL, brect,
                           jaune, font,
                           taille--,
                           0,
                           0,
                           0,
                           chaine);
    if (err) fprintf(stderr, "%s\n", err);
    fprintf (stderr, "Essai taille : %.0f\n", taille+1);
    fprintf (stderr,
             "* bas gauche ( %d, %d ), haut droite ( %d, %d )\n",
             brect[0], brect[1],
             brect[4], brect[5]);
    fprintf (stderr,
             "* largeur x hauteur : %dx%d\n",
             brect_largeur,
             brect_hauteur);
  } while ( ( brect_hauteur >= 20 ) || ( brect_largeur >= 60 ) );

  /* A décommenter pour voir le brect
  gdImageRectangle (image,
                    50-brect_largeur/2,
                    50-brect_hauteur/2,
                    50+brect_largeur/2,
                    50+brect_hauteur/2,
                    noir);
  */

  /* Une fois ici on a la bonne taille moins un.
   * Le milieu de l'image la moitié de la
   * largeur nous donne le x gauche.
   * Le milieu de l'image + la moitié de
   * la hauteur nous donne le y bas.
   * On retracnche la moitié des brect[0] et brect[1]
   * car on a vu qu'il ne valaient pas 
   * nécessairement 0 d'où un décalage
   */
  err = gdImageStringFT (image,
                         brect,
                         // mettre -jaune pour
                         // supprimer l'anti-aliasing
                         jaune,
                         font,
                         ++taille,
                         0,
                         50-(brect_largeur-brect[0])/2,
                         50+(brect_hauteur-brect[1])/2,
                         chaine);
  if (err) fprintf(stderr, "%s\n", err);

  /* Les nouveautés s'arrêtent ici */

  image_png = fopen("gd2_expl1.png", "w");
  gdImagePng(image, image_png);
  fclose(image_png);

  gdImageDestroy(image);

  exit (0);
}
On compile en tapant
gcc -o expl_gd2_1 expl_gd2_1.c -lgd -ljpeg -lpng -lfreetype
Après exécution par ./expl_gd2_1, vous obtenez dans le répertoire courant une image gd2_expl1.png, qui sera une des 4 présentées ci-dessous selon les modifications que vous aurez apportées au code :
 
Image normale Sans anti-aliasing
(-jaune)
résultat du code... résultat du code...
Avec dessin
du brect
Avec brect
Sans anti-aliasing
résultat du code... résultat du code...
Remarque : Dans une application réelle, une fois connue la bonne taille de police, vous enleveriez la boucle permettant de la trouver, vous ne traceriez pas le brect, etc ... Je l'ai fait ici dans un but pédagogique.

Exemple2 : Selon un angle ?

Nous allons maintenant écrire, selon un angle. Pas de gros changements ... On en profitera toutefois au passage pour faire connaissance avec le type gdPoint, utilisé pour tracer des polygones de façon aisée. Il nous servira pour tracer le brect de ce texte "en pente", grâce à la fonction gdImagePolygon.
On note qu'un type gdPoint a deux champs, x et y. C'est pas plus compliqué que ça !
Le code :
/*
 * Fichier expl_gd2_2.c
 *
 * Pour les explications de base,
 * se reporter à l'article sur gd.
 */
#include <stdlib.h>
#include <stdio.h>
#include <gd.h>
#include <math.h>

// Une macro pour transformer des degrés en radians
// NB: M_PI est définie dans math.h
#define en_radians(ndeg) (M_PI*(ndeg)/180)

// Deux macros qui simplifient la vie
#define brect_largeur (brect[4]-brect[0])
#define brect_hauteur (brect[1]-brect[5])

int main(void) {
  gdImagePtr image;
  FILE *image_png;
  char *err;
  int rouge, blanc, jaune, noir;
  char *chaine = "santé"; // La chaîne à écrire
  char *font = "/home/xavier/Docs/contribs_lea/c4/comic.ttf";
  double taille = 20; // La taille de la police
  int brect[8]; // Les coordonnées du rectangle
  // entourant le texte entier.
  gdPoint brect_points[4]; // Le tableau de points
  // pour tracer le polygone

  image = gdImageCreate(100, 100);
  blanc = gdImageColorAllocate(image, 255, 255, 255);
  rouge = gdImageColorAllocate(image, 255, 0, 0);
  jaune = gdImageColorAllocate(image, 255, 255, 0);
  noir = gdImageColorAllocate(image, 0, 0, 0);

  gdImageFilledRectangle(image, 20, 40, 80, 60, rouge);
  gdImageFilledRectangle(image, 40, 20, 60, 80, rouge);

  err = gdImageStringFT (NULL, brect,
                         jaune, font,
                         taille,
                         en_radians(45),
                         0,
                         0,
                         chaine);
  /* on stocke les points dans le tableau de gdPoints */
  brect_points[0].x = brect[0]+50-(brect_largeur-brect[0])/2;
  brect_points[0].y = brect[1]+50+(brect_hauteur-brect[1])/2;
  brect_points[1].x = brect[2]+50-(brect_largeur-brect[0])/2;
  brect_points[1].y = brect[3]+50+(brect_hauteur-brect[1])/2;
  brect_points[2].x = brect[4]+50-(brect_largeur-brect[0])/2;
  brect_points[2].y = brect[5]+50+(brect_hauteur-brect[1])/2;
  brect_points[3].x = brect[6]+50-(brect_largeur-brect[0])/2;
  brect_points[3].y = brect[7]+50+(brect_hauteur-brect[1])/2;

  /* On trace le polygone */
  gdImagePolygon (image, brect_points, 4, noir);

  /* On ajoute la chaîne de caractères
     comme avant, mais avec un angle */
  err = gdImageStringFT (image,
                         brect,
                         jaune,
                         font,
                         taille,
                         en_radians(45),
                         50-(brect_largeur-brect[0])/2,
                         50+(brect_hauteur-brect[1])/2,
                         chaine);
  if (err) fprintf(stderr, "%s\n", err);

  image_png = fopen("gd2_expl2.png", "w");
  gdImagePng(image, image_png);
  fclose(image_png);

  gdImageDestroy(image);

  exit (0);
}
On compile en tapant
gcc -o expl_gd2_2 expl_gd2_2.c -lgd -ljpeg -lpng -lfreetype
Après exécution par ./expl_gd2_2, vous obtenez dans le répertoire courant une image gd2_expl2.png.
résultat du code...

Quoi de neuf dans gd-2 ?

Ben oui !!! Vous devez bien vous le demander ! Alors voici ce que dit (en résumé) la section "what's new ?" de la page officielle pour la version 2.0.1 par rapport à la 1.8.3 du précédent article : Plus des améliorations et corrections que je vous conseille de lire directement sur la page, http://www.boutell.com/gd/
Notez que ce lien vous emmène pour l'instant sur la page de la version stable, qui, à l'heure où ces lignes sont écrites, est encore la 1.8.4. Il y a toutefois un (gros) lien vers la page des versions 2.0.x.

Exemple3 : Transparence

/*
 * Fichier expl_gd2_3.c
 *
 * Pour les explications de base,
 * se reporter à l'article sur gd.
 */
#include <stdlib.h>
#include <gd.h>

int main(void) {
  FILE *image_png;
  gdImagePtr image, image_d_avant;
  int orange, bleu;

  // On crée une image TrueColor
  image = gdImageCreateTrueColor(100, 100);
  // On alloue une couleur
  orange = gdTrueColor (255, 128, 0);
  // On trace un rectangleorange
  gdImageFilledRectangle(image, 0, 0, 100, 100, orange);

  // On ouvre l'image du premier exercice
  // et on en crée une image
  image_png = fopen("expl1.png", "r");
  image_d_avant = gdImageCreateFromPng (image_png);
  fclose (image_png);

  // on copie avec remise à l'échelle de 100x100 pixels
  // en partant de (0,0) de l'ancienne image
  // on place le résultat de taille 90x90 à
  // (5,5) dans la nouvelle
  gdImageCopyResampled(image, image_d_avant, 
    5, 5, 0, 0, 90, 90, 100, 100);
  // on détruit l'ancienne image en mémoire.
  gdImageDestroy (image_d_avant);

  // On alloue une couleur bleue semi transparente
  bleu = gdTrueColorAlpha(0, 0, 255, 
    gdAlphaTransparent / 2);
  // on se met en mode écrasement
  // La où on dessinera il y aura 
  // du bleu semi-transparent, c'est tout
  gdImageAlphaBlending(image, 0);
  // ontrca eun rectangle
  gdImageFilledRectangle(image, 10, 10, 90, 30, bleu);

  // On se met en mode "mélange"
  // Ce qu'on dessine, se mélange avec
  // ce qu'il y a dessous
  // 50% de bleu et 50% du dessous
  gdImageAlphaBlending(image, 1);
  gdImageFilledRectangle(image, 40, 10, 90, 90, bleu);

  image_png = fopen("gd2_expl3.png", "w");
  gdImagePng(image, image_png);
  fclose(image_png);

  gdImageDestroy(image);

  exit (0);
}
On compile en tapant
gcc -o expl_gd2_3 expl_gd2_3.c -lgd -ljpeg -lpng -lfreetype
On obtient après exécution, dans gimp :
résultat du code...
Notez : C'est tout pour cette fois !
N'hésitez pas à envoyer vos commentaires par mail en cliquant sur mon nom en haut de la page.
Retrouvez prochainement l'article remis en forme, les sources, archives et binaires sur mon site perso..., http://perso.club-internet.fr/xgarreau.
Vous pouvez également me contacter dans le forum Développement de Léa, je le regarde (très) souvent.
A bientôt.

Haut


Perl

par Jice et Jonesy

Installation et introduction à Perl.



Avant propos

Ceci n'est qu'une introduction au Perl, car nous pourrions écrire 4 ou 5 livres, voir plus, sur ce sujet ! ;-) Donc ne vous attendez pas à faire le tour de la question...
Et je précise aussi, que ce n'est pas non plus un cours de programmation : cet article s'adresse à des personnes possédant des bases de programmation et voulant se mettre à Perl.

Présentation

Perl est un langage de programmation extrêmement efficace pour traiter les fichiers et les chaînes de caractères. Venant du monde Unix, il intègre toutes les fonctions que l'on peut retrouver en shell avec des commandes comme grep, sed, awk, ... Sa syntaxe générale ressemble beaucoup à celle du C et du shell. De plus, il est très apprécié des administrateurs système afin de gérer les fichiers de log.

Note de Jice : Perl est un langage très polyvalent. LE jeu, j'ai nommé Frozen Bubble, est même programmé en Perl !

Les particularités de Perl


L'installation

Dans cette partie, je n'aborde que l'installation de Perl sur un système Linux, et ce quelque soit la distribution. Enfin, j'espère... ;-)
 

Vérifions si Perl est présent

Il y a des chances pour que Perl soit déjà installé avec notre distribution ou que celle-ci fournisse un package Perl. Pour vérifier si Perl est déjà sur notre système faisons, en console :
perl -v
Ceci doit nous retourner un message du genre:
This is perl, v5.6.1 built for i386-linux

Copyright 1987-2001, Larry Wall

Perl may be copied only under the terms of either the Artistic licence or the GNU General Public licence, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on this system using `man perl' or `perldoc perl'. If you have access to the Internet, point your browser at http://www.perl.com/, the Perl Home Page.

Si c'est le cas, nous pouvons sauter le paragraphe suivant.

Installation de Perl

Pas de chance... Donc allons voir si notre distribution fournit un package et si oui, installons-le. Sinon...
Les binaires sont accessibles ici : http://www.activestate.com/Products/Download/Get.plex?id=ActivePerl&_x=1
Note : je viens de remarquer que maintenant il demande e-mail, prénom et nom pour télécharger Perl.
Les sources sont accessibles ici : http://www.perl.com/pub/a/langage/info/software.html#sourcecode

Maintenant détarons le package
tar -xvzf <votre package>
Allons sous le répertoire créé.

Si nous souhaitons choisir nos paramètres en répondant à des questions :
./Configure -Dprefix=/usr
make
En root (su)
make install

Sinon, pour une installation par défaut :
./Configure -Dprefix=/usr -d -e
make
En root (su)
make install

Vérifions que Perl est bien installé
perl -v


Introduction au langage Perl

Les principales règles du Perl

Définir l'interpréteur

Le fait de définir l'interpréteur dans le script permet de l'exécuter directement. Pour ce faire il faut placer la ligne suivante au tout début du script :
#!/usr/bin/perl
Ce qui permettra d'exécuter le script directement par :
./script.pl
Sans cette ligne il faudrait exécuter le script comme ceci :
perl script.pl
En ajoutant l'option -W, nous obtenons la liste des toutes les erreurs et de tous les avertissements lors de l'interprétation du script.
 

Les commentaires

Un commentaire débute par un caractère dièse (#). Tout ce qui suit ce caractère jusqu'à la fin de la ligne sera considéré comme du commentaire.
Par exemple :
# Ceci est un exemple de commentaire
print "test"; # un 2ème commentaire
 

Utiliser une libraire particulière

L'extension des fichiers librairies (ou module) est pm. pm signifie Perl Module.
Pour appeler une librairie, on ajoute la directive :
use la_librairie;
Pour indiquer à perl où chercher nos librairies personnelles, on utilise la directive :
use lib "le chemin d'accès";

Remarque : les chemins de recherche des librairies sont définit dans le tableau @INC
 

Déclarer une variable

Variable globale :
Cela se fait automatiquement lors de la première utilisation de la variable. Cependant, lorsqu'on utilise use strict, il faut déclarer les variables globales par la directive suivante : use vars qw($var1 $var2 @tab1 %hash1);

Remarque : le mot clef local s'applique à une variable globale (local $variable;). Il permet à l'intérieur d'un bloc de cacher une variable globale par une autre valeur. La variable reste globale, c'est à dire que dans les procédures appelées dans le bloc, la nouvelle valeur est utilisée ; à la fin du bloc, l'ancienne valeur est restaurée.

Variable lexicale :
Une variable lexicale n'existe qu'à l'intérieur d'un bloc. On parle aussi de variable locale.
On la déclare par my $variable ou my @tableau...

Pour déclarer plusieurs variables et un tableau en une seule commande, faire :
my ($var1, $var2, @tab);
 

Les opérateurs

Voici les opérateurs les plus couramment utilisés. Pour plus d'info sur les opérateurs : man perlop
Opérateurs unaires
! : négation logique. Si $a vaut vrai, !$a vaudra faux.
- : négation arithmétique. Si $a vaut 2, -$a vaudra -2.
~ : négation binaire ou complément à 1.
++ : incrémentation ; si ++ est placé avant la variable, il la décrémente avant de retourner la valeur, s'il est placé après, il retourne la valeur puis l'incrémente. Exemple : $A++
-- : décrémentation ; idem.
Opérateurs binaires
+ - * / % ** : opérations arithmétiques (% est le modulo, ** est l'exponentiation).
. : concatène deux chaînes : "aze"."rty" vaut "azerty".
x : "multiplie" une chaîne : "to" x 2 vaut "toto".
& | ^ << >> : opérations sur les bits : et, ou, ou exclusif (xor), décalage à gauche, à droite.
&& || : et logique, ou logique.
Opérateur ternaire
?:  : cet opérateur particulier est nommé "opérateur conditionnel". Il fonctionne à la manière d'un si alors sinon. Si l'expression à gauche du ? est vraie, l'argument avant le : est retourné, sinon l'argument après le : est retourné. Mieux vaut un exemple :
print ($n>1)?'s':'';
affichera 's' si $n est plus grand que 1, et n'affichera rien ('') sinon.
Opérateurs de comparaison numériques
< <= > >= : plus petit, plus petit ou égal, plus grand, plus grand ou égal
== != : égal, différent
Opérateurs de comparaison pour les chaînes de caractères
lt gt le ge : plus petit, plus petit ou égal, plus grand, plus grand ou égal
eq ne : égal, différent
=~ !~ : teste si une chaîne de caractères correspond à une expression régulière. Voir le paragraphe expressions régulières.

L'affectation d'une variable

L'opérateur d'affectation "normal" est '='. Par exemple :
$variable = mavaleur;
affecte la valeur mavaleur à la variable $variable.

D'autres opérateurs d'affectation sont un raccourci pour les opérations binaires.
Par exemple $a += 2 est un raccourci pour $a = $a + 2.

Les opérateurs d'affectation les plus couramment utilisés sont :
  = : affectation "standard"
 +=  -=  *=  /=   %= : pour les opérations arithmétiques (+, -, *, / et modulo)
&&= ||=  != : pour les opérations logiques (et logique, ou logique, non logique)
 &=  |=  ^=  ~=  <<=  >>= : pour les opérations logiques sur les bits (et, ou, ou exclusif, complément à 1, décalages gauche et droite)
 .= : pour les opérations sur les chaînes de caractères (concaténation)
 =~  !~ : pour les expressions régulières (voir plus bas).

Quelques variables particulières prédéfinies

use English; permet d'utiliser les noms des variables au lieu des noms standards des variables spéciales (ex : $ARG au lieu de $_).

@_ ou @ARG : Tableau d'arguments. Sert pour le passage de paramètres.
$_ ou $ARG : Variable par défaut des fonctions unaires entrées/sorties, recherche...
$/ ou $RS ou $INPUT_RECORD_SEPARATOR : fin de ligne lorsqu'on lit un fichier, \n par défaut sous unix et \r\n sous windows.
$! ou $ERRNO : suivant le contexte d'utilisation, retourne le numéro de la dernière erreur système, ou le message d'erreur système.
$0 ou $PROGRAM_NAME : Nom du fichier qui contient le script Perl.
$^0 ou $OSNAME : Nom du système d'exploitation.
Utiliser une variable d'environnement
$variable = $ENV{"nom de la variable d'environnement"}; Remarque, ce n'est que l'utilisation du hachage %ENV.

Pour plus d'information sur ces variables : man perlvar
 

Branchements conditionnels

Ce test classique peut s'écrire de la manière suivante :
if (test) {bloc}
if (test) {bloc} else {bloc}
if (test) {bloc} elsif (test) {bloc} ... else {bloc}

test étant toute expression qui retourne une valeur.
exemple :
if (!open(F,$FICHIER)) { die "impossible d'ouvrir $FICHIER: $!"; }

Il y a d'autres manières de faire la même chose, vous pouvez utiliser par exemple :
 
instruction if test; exécute l'instruction si le test est vrai.
exemple : die "impossible d'ouvrir $FICHIER: $!" if !open(F,$FICHIER);
instruction unless test; exécute l'instruction si le test est faux.
exemple : die "impossible d'ouvrir $FICHIER: $!" unless open(F,$FICHIER);
commande1 or commande2; exécute la commande1 ; si elle retourne faux, alors la commande2 sera exécutée. Sinon on passe à la suite.
exemple : open(F,$FICHIER) or die "impossible d'ouvrir $FICHIER: $!";
test?commande1:commande2; exécute la commande1 si la condition du test est remplie, sinon il exécute la commande2.

Note : Pour ceux qui connaissent, il n'y a pas d'équivalent au 'case' ou 'switch'. Il y a plusieurs façons de faire, notamment utiliser if elsif ... else.
man perlsyn vous donnera plus d'informations.
 

Les boucles

For et foreach
Il y a deux types de boucles for, le for 'style C', et le for/foreach Perl.
for/foreach Perl
Dans ce cas, for et foreach sont synonymes. On peut utiliser l'un ou l'autre pour améliorer la lecture, par habitude, etc.

Pour aller de 1 à 9 :
for $cpt (1 .. 9) {bloc} ou foreach $cpt (1 .. 9) {bloc}
(dans {bloc}, $cpt prend successivement les valeurs de 1 à 9)
ou
for (1 .. 9) {bloc}
ici, à chaque itération, la variable par défaut $_ prend la valeur suivante.

Pour parcourir chaque éléments d'un tableau :
foreach $variable (@tableau) {bloc}
A chaque itération, $variable prendra la valeur suivante de @tableau.
Si on omet $variable, alors la valeur de l'élément sera stocké dans la variable $_ :
foreach (@tableau) {bloc}

Remarque : l'écriture 1..9 définit en réalité un tableau de 9 éléments (1,2,3,4,5,6,7,8,9).

for 'style C'
La construction 'classique' à la C est possible, mais tellement moins Perl...
for ($cpt=1 ; $cpt < 10 ; $cpt++) {bloc}
Cette construction est néanmoins très puissante, et permet de faire beaucoup plus de choses que de simples itérations... A vous de voir !

A noter que dans les exemples ci-dessus, les variables $cpt ou $variable sont locales à la boucle. Si elles doivent être déclarées, vous pouvez les faire précéder du mot clef my :
for my $cpt (1 .. 9) {bloc}
for (my $cpt=1 ; $cpt < 10 ; $cpt++) {bloc}

While
Les boucles while se construisent comme suit :
while (test) {bloc1}
while (test) {bloc1} continue {bloc2}

Le bloc1 puis le bloc2 sera exécuté tant que le test est vrai.

L'exécution des boucles peut être pilotée à l'intérieur des blocs par les commandes next, last et redo.

Par exemple, le code suivant lit l'entrée standard, et ignore les commentaires (lignes commençant par #)
while (<STDIN>) {
    next if /^#/;
    ...
}
Autre exemple, une itération de 1 à 9
$cpt = 1;
while ($cpt < 10) {
  ...
  $cpt++;
}
ou bien :
$cpt = 1;
while ($cpt < 10) {
  ...
}
continue {
  $cpt++;
}
Pour plus d'information sur ces constructions : man perlsyn.
 

Les fonctions et procédures

Déclaration des fonctions
Une fonction peut être déclarée n'importe où dans le script Perl, mais pour des questions de lisibilité, il est préférable de déclarer les fonctions regroupées soit en début, soit en fin de script.
On peut pré-déclarer les fonctions par :
sub mafonction;
ou les déclarer directement par :
sub mafonction {bloc}
Passage de paramètres
Les paramètres sont passés comme une liste de valeurs scalaires (nombre, chaîne...) à plat.
Par exemple :
ma_fonction($variable, "chaîne");

Pour lire valeur des paramètres passés dans la fonction, il suffit de lire la variable @_ qui est le tableau contenant les paramètres.
Une façon simple de les lire est d'écrire en début de fonction :
my ($variable, $chaine) = @_;
On peut aussi récupérer les arguments en dépilant les éléments du tableau @_ avec la commande shift.
my $variable = shift @_;
qui peut s'abréger en
my $variable = shift;
car le tableau @_ est l'argument par défaut de shift.

Les paramètres sont passés par valeur. Pour pouvoir modifier le contenu d'une variable passée en paramètre à une fonction, il faut passer à cette fonction une référence sur la variable (on parle de passage par référence). Une référence est un type scalaire particulier qui permet de se référer à une variable. La notation consiste à faire précéder la variable par un caractère '\'. Pour récupérer la variable cachée derrière la référence (ou déréférencer), il faut faire précéder la référence du caractère de type correspondant, et optionnellement entourer la référence d'accolades.
Exemple :
$a = 1;
$b = \$a;       # $b est une référence à $a
$c = $$b + 1;   # $c = 2
$c = ${$b} + 1; # synonyme

Démonstration :
sub test1 {
    my $a=shift; # récupération du paramètre
    $a += 1;     # on ajoute un
}
sub test2 {
    my $ref_a=shift;
    $$ref_a += 1; # le premier $ déréférence la référence
}
$a = 1;
print $a;   # affiche 1
test1($a);
print $a;   # affiche 1
test2(\$a); # \$a est une référence sur $a
print $a;   # affiche 2

Le passage par référence a aussi pour d'intérêt de pouvoir passer des tableaux ou des hachages en paramètres à une fonction. En effet, on a vu que les paramètres d'une fonction sont passés comme une liste de valeurs scalaires à plat. Si on désire passer 2 tableaux @tab1=(1,1,1) et @tab2=(2,2) à une fonction, et que l'on écrit :
mafonction (@tab1, @tab2);
alors perl traduira cela comme :
mafonction (1,1,1,2,2);
car il met toutes les valeurs à plat dans une liste. Une fois dans la fonction on sera bien embêté pour savoir distinguer les éléments du premier tableau de ceux du deuxième.
On va donc passer une référence sur chaque tableau :
mafonction (\@tab1, \@tab2);

Exemple :
sub liste_gt {
    my ($l1ref, $l2ref) = @_;
    $n1 = @$l1ref; # @$l1ref représente le tableau référencé par $l1ref
    $n2 = @$l2ref;
    # (affecter un tableau dans un scalaire retourne le nombre d'éléments)
    if ($n1 > $n2) { return 1; }
    else { return 0; }
}
@tab1 = (1,2,3);
@tab2 = ("a",2,"r",5);
if (liste_gt(\@tab1, \@tab2)) {
    print "tab1 a plus d'éléments que tab2\n";
} else {
    print "tab2 a plus d'éléments que tab1\n";
}

Tout cela s'applique de la même façon aux hachages.
Pour plus d'informations sur les références, voir man perlreftut (tutoriel) et man perlref (références).

Retour de valeurs
Une fonction peut renvoyer une ou des valeurs, grâce à la commande return :
sub ecrire
{
  ...
  return $valeur;
  ...      # renvoie une valeur unique
  return $valeur1, $valeur2;
  ...      # renvoie une liste de valeurs
}
Cette commande provoque la sortie immédiate du sous-programme pour retourner dans le programme appelant.

return permet de renvoyer une valeur ou une liste de valeurs. Cela permet donc à une fonction de retourner soit un tableau :
@resultat = ma_fonction;
soit plusieurs valeurs :
($a, $b) = ma_fonction;

De même que pour le passage de paramètres, la liste de valeur est une liste "à plat", ce qui signifie que pour retourner plusieurs tableaux, il faut également passer par les références :
($ref_a, $ref_b) = ma_fonction;
$ref_a est une référence sur le tableau @$ref_a et où ma_fonction retourne une liste de deux références.
 

Pour plus d'information sur les fonctions de Perl, voir man perlsub.
 
 

Les expressions régulières, ou regex

Perl est un langage très pratique pour manipuler du texte. Un des éléments qui le rend si adapté à cet usage est la présence des expressions régulières, qui permettent de déterminer si une chaîne de caractère correspond à un masque prédéfini, d'extraire des éléments d'une chaîne de caractères suivant un masque, de remplacer des parties d'une chaîne suivant un masque, etc. C'est ce masque, défini sous forme d'une chaîne de caractères, que l'on appelle expression régulière.

Par exemple, $a =~ /lea/ teste si la chaîne "lea" est dans la chaîne $a, $a =~ /^lea/ si $a commence par "lea", $a =~ /l[eé]a/ si la chaîne $a contient "lea" ou "léa"...

De prime abord, ce sont des sortes de formules qui semblent ne rien vouloir dire, mais une fois que nous les maîtrisons, elles se révèlent extrêmement pratiques et puissantes.

Les éléments composant une expression régulière
Les éléments les plus utilisés sont :
Les différentes utilisations des expressions régulières
m/regex/ ou /regex/ (sans le m) teste si une chaîne correspond (match) à une regex donnée.
s/regex/substitution/ remplace la partie de la chaîne correspondant à la regex par la substitution.

Pour appliquer un test m// ou une substitution s/// à une chaîne de caractères, on utilise l'opérateur =~ ou !~.
Par exemple :
if ($s =~ /win(dows)?/) { kickban(); }
teste si la chaîne $s contient "win" ou "windows".
Si on ne précise pas de chaîne, le test se fait sur la variable par défaut $_ :
if (/win(dows)?/) { kickban(); }
teste si $_ contient "win" ou "windows".
L'opérateur !~ correspond quant à lui à la négation : $s !~ /coin/ teste si la chaîne $s ne contient pas "coin".

options
Les expressions régulières peuvent de plus être suivies de modifieurs ou options, qui en modifient le comportement, notamment : Par exemple :
"Bonjour Léa" =~ m/jour/; # correspond
"Bonjour Léa" =~ m/Jour/; # ne correspond pas
"Bonjour Léa" =~ m/Jour/i; # correspond
groupement et capture
Si une partie d'une regex est entre parenthèses, alors la partie de la chaîne qui correspond à cette parenthèse sera affectée à la variable $1 (on dit "capturée"). Si une deuxième parenthèse est présente dans la regex, la variable $2 sera utilisée, et ainsi de suite. Si la parenthèse commence par ?: alors il n'y aura pas d'affectation à une variable $1, $2, etc.
Ainsi :
if (/win(?:dows)?\s(..)/i) {
  if ($1 eq "xp") { kickban(); }
  else { kick(); }
}
teste si $_ contient "win" ou "windows", suivi d'un espace, puis de 2 caractères, que l'on va capturer dans $1. La première parenthèse (?:dows) permet de regrouper les quatre caractères "dows" afin de leur appliquer le quantifieur '?' (0 ou 1).

Les captures $1, $2... peuvent être utilisées lors d'une substitution :
$s =~ /win(?:dows)?\s(..)/winblows $1/
 

Tester si une chaîne correspond à un masque donné : m// ou //
Par exemple, pour tester si une chaîne contient le mot "lea" (le i de la fin permet de ne pas différencier majuscules et minuscules) :
if (/lea/i) ...

Pour tester si une chaîne commence par la lettre 'a', on peut faire :
if (/^a/) ...

Pour tester si une chaîne se termine par 'z' :
if (/z$/) ...

Enfin pour tester si une chaîne contient un quadruplet héxadécimal (i.e. 4 chiffres de 0 à 9 ou A à F) :
if (/[0-9A-F]{4}/) ...

Ici, par défaut, c'est la valeur contenu dans $_ qui est utilisée pour le test. Et bien entendu, nous pouvons compliquer à loisir l'expression régulière, et utiliser le groupement (voir plus haut) afin de récupérer les valeurs trouvées dans les chaînes pour s'en servir par la suite.
 

Substituer une partie d'une chaîne à une autre : s///
Rappel : la syntaxe est s/regex décrivant l'élément à remplacer/élément remplaçant/options

Exemples :
$variable =~ s/a/b/;
Cette commande va remplacer le premier 'a' de la ligne par 'b'. Pour remplacer tous les 'a' faire :
$variable =~ s/a/b/g;
Si en plus nous voulons que cela ne fasse pas la différence entre les majuscules et les minuscules (case sensitive), faire
$variable =~ s/a/b/gi;
Pour remplacer tous les ensembles de caractères commençant par 'a', finissant par 'e' et avec au moins un caractère au milieu sans être sensitif par 'toto', faire :
$variable =~ s/a.*e/toto/gi;
 

La traduction : tr///
Cette construction "à la regex" permet de remplacer des caractères par d'autres dans une chaîne, à la manière de la commande unix 'tr'.
La syntaxe est tr/éléments à traduire/éléments traduits/
Ou y/éléments à traduire/éléments traduits/

Exemple :
$variable =~ tr/a-b/A-B/;
Cela va transformer tous les 'a' et 'b' minuscules en majuscules.

Plus d'info sur :
man perlre
man perlretut (le tutoriel sur les regex, une bonne lecture !)
 

Structures de données complexes

Tableaux et tables de hachage multidimensionnels
On a vu plus haut que les tableaux Perl ne possèdent qu'une seule dimension (il ne peuvent contenir que des éléments de type scalaire). On a aussi vu plus haut l'existence de références, type scalaire particulier qui permet de se référer à une variable, donc en l'occurence à un tableau.
Ainsi pour représenter la matrice
 0 1 2
 3 4 5
 6 7 8
Nous allons pouvoir faire :
@ligne1 = (0, 1, 2);
@ligne2 = (3, 4, 5);
@ligne3 = (6, 7, 8);
@matrice = (\@ligne1, \@ligne2, \@ligne3);
Perl introduit une nouvelle notation pour ce faire : au lieu d'entourer la liste avec des parenthèses, nous allons le faire avec des crochets, ce qui aura pour effet de retourner une référence :
$ligne1_ref = [0, 1, 2];
etc...
ou plus simplement :
@matrice = (
 [0, 1, 2],
 [3, 4, 5],
 [6, 7, 8],
);
Dans tous ces cas, @matrice est un tableau de trois éléments, qui sont chacun une référence vers un autre tableau. $matrice[1] est une référence sur la 2ème ligne de la matrice.

Afin d'accéder aux éléments de la matrice, nous pouvons utiliser une nouvelle notation : la flèche '->'.
En effet, afin d'accéder au 3ème élément de $ligne1_ref définie plus haut, nous utiliserons $ligne1_ref->[2] (ce qui est synonyme de ${$ligne1_ref}[2].)
Ainsi, afin d'accéder au '5' dans notre matrice, nous ferons : $matrice[1]->[2].
Mais... Perl est gentil avec nous, car entre deux indices de tableau, la flèche est optionnelle ! Donc nous pouvons écrire : $matrice[1][2], ce qui ressemble maintenant à un tableau bi-dimensionnel.

De même, si $hash_ref est une référence vers un hachage, $hash_ref->{'nom'} renverra l'élément du hachage indexé par 'nom'.

L'ensemble tableaux, hachages et références peut bien sûr être mixé dans l'ordre qui vous plaira. Par exemple, pour réaliser un tableau de personnes, nous écrirons :
@staff = (
 [ prenom => "jice", nom => "cardot" ],
 [ prenom => "fred", nom => "bonnaud" ],
 [ prenom => "serge", nom => "tchesmeli" ],
);
et pour accéder au prénom de la première personne, nous ferons : $staff[0]->{'prenom'}(meuh non je ne suis pas mégalo ;) ou $staff[0]{'prenom'} car ici aussi la flèche est optionnelle.

Notes :

Les références symboliques

On a vu ci-dessus une utilisation des références, pour le passage et le retour de valeurs dans les fonctions. On vient de voir également leur utilité pour travailler avec des structures de données complexes.

Les références peuvent aussi servir dans un autre contexte : si vous déréférencez une variable contenant une valeur scalaire plutôt qu'un référence vers une autre variable, elle sera considérée comme une référence symbolique, c'est à dire que la valeur scalaire sera utilisée en tant que nom de la variable.
Exemple :
$test = "toto";
$$test = 144;     # affecte 144 à $toto
${$test} = 12;    # affecte 12 à $toto
$test->[1] = 12;  # affecte 12 à $toto[1]
@$test = (1,2,3); # affecte le tableau @toto

Cela peut aller beaucoup plus loin, car vous pouvez mettre une chaîne de caractères entre les accolades :
${$test x 2} = 12;   # affecte 12 à $totototo
$titi = 2;
${"$toto$titi"} = 2; # affecte 2 à $toto2
etc.

Remarque : l'utilisation des références symboliques avec use strict; (tel que préconisé au début de l'article) n'est pas possible. Pour contourner ce problème, il faut appeler l'instruction no strict 'refs'; dans le bloc où on veut utiliser les références symboliques.

Autre exemple d'utilisation :
$min = 12;
$max = 144;
while (<FICHIER>) {
  if (/^([A-Z]+)\ (\d+)\ (\d+)\ (\d+)$/) {
    # les masques ont affecté $1, $2, $3 et $4 (voir groupement/capture)
    print "$1: ";
    # on va effectuer une même opération sur $2, $3 et $4 :
    for $i (2 .. 4) {
      no strict 'refs';
      # on utilise les références symboliques : si $i = 2, alors $$i est $2
      if ($$i > $max) print "sup ";
      elseif ($$i < $min) print "inf ";
      else print "ok ";
    }
  }
}

Manipulation de fichiers

Comme toujours en Perl, il y a plusieurs moyens de faire une même opération.
Ouverture "à la shell" :
Cette façon de faire offrant un moyen simple d'ouvrir un fichier en lecture / écriture est empruntée du shell.
open(FILE, "< nom du fichier"); # ouvre en lecture
open(FILE, "  nom du fichier"); # idem
open(FILE, "> nom du fichier'); # ouvre en écriture
open(FILE, ">>nom du fichier'); # ouvre en ajout

Remarque : le fichier "moins" correspond à l'entrée standard stdin en lecture et à la sortie standard stdout en écriture. Par exemple  open(SORTIE,">-") ouvre la sortie standard.

Ouverture "à la C" :
La fonction sysopen de Perl correspond à l'appel système open(2) du C. Cela permet une plus grande maîtrise sur l'ouverture des fichiers, au prix d'une plus grande complexité.
Synoptique : sysopen DESCRIPTEUR, CHEMIN, FLAGS, [MASQUE]
MASQUE correspond au masque octal des permissions du fichier (utile pour la création d'un fichier), combiné avec l'umask du processus perl. voir l'article sur les permissions.
Je vous renvoie vers un cours de C pour la signification des exemples suivants, et vous donne la correspondance avec l'open "à la shell".
 
à la Shell à la C
open(F, "< $chemin"); sysopen(F, $chemin, O_RDONLY);
open(F, "> $chemin"); sysopen(F, $chemin, O_WRONLY | O_TRUNC | O_CREAT);
open(F, ">> $chemin"); sysopen(F, $chemin, O_WRONLY | O_APPEND | O_CREAT);

Bien sûr, sysopen permet de faire plus de choses que le simple open(), en jouant sur les flags ci-dessus et O_RDWR, O_RDONLY, O_EXCL, O_NONBLOCK, O_BINARY. Pour plus d'informations, voir man perlopentut ou man 2 open.

Fermer un fichier
close(FILE);
FILE étant le descripteur du fichier ouvert.
Descripteurs de fichier spéciaux
STDIN et STDOUT sont définis dès le lancement de votre programme. Ils correspondent respectivement à l'entrée et à la sortie standard. Rien ne vous empêche de les redéfinir, en les ouvrant sur un autre fichier, par exemple : open(STDIN, "mon_fichier");
Lire dans un fichier (l'opérateur diamant)
À chaque appel, <DESCRIPTEUR DE FICHIER> (on appelle <> l'opérateur "diamant") retourne la ligne suivante du fichier (en fait jusqu'à la fin de ligne définie par $/, voir le paragraphe sur les variables prédéfinies.), ou renvoie faux en fin de fichier.
exemple :
open (F, "mon_fichier");
$premiere_ligne = <F>;
while (<F>) {
  ... traitement en fonction de $_ ...
}
close(F);

exemple : pour lire tout un fichier dans une variable :
open (F, "mon_fichier");
undef $/;
$contenu_fichier = <F>;
close(F);

La lecture sur l'entrée standard est extrêmement simplifiée, car STDIN est déjà ouvert, et <STDIN> et <> sont synonymes. Ainsi, il suffit de faire :
while (<>) {
  ... traitement de la ligne en cours ...
}

Écrire dans un fichier
Il suffit de spécifier à la commande print ou printf le descripteur de fichier à utiliser.
Exemple :
open(FLOG, ">> fichier.log");
print FLOG "nouveau log\n";
close(FLOG);
 

Quelques autres commandes utiles

voir man perlfunc pour la liste exhaustive des commandes intégrées de Perl.

split

split correspond en partie à la fonction cut en shell. Elle permet de découper une chaîne de caractères.
Exemple :
($variable, @tableau) = split (":", "prenom:nom:age:adresse:ville:telephone");
Renvoie "prenom" dans $variable et le reste dans des cases distinctes du tableau @tableau.

systeme ou exec

exec permet de lancer un programme extérieur.
system de même que exec, mais produit un fork, processus fils.
Exemple :
@args = ("ls", "-lrt", "toto*");
system(@args);
Cela va lancer la commande :
ls -lrt toto*
Attention, l'utilisation de ces commandes rend votre script beaucoup moins portable ! En général, il y a moyen de tout faire en Perl, il faut éviter de recourir à ces fonctions.

chomp

chomp supprime tous les caractères de fin de ligne d'une chaîne de caractères et renvoie le nombre de caractères supprimés.
Exemple :
$nb_car = chomp($chaine);
Contrairement à chop qui enlève seulement le dernier caractère de fin de ligne.
 

Exemple d'un script Perl

Le script va se lire lui-même et sortir à l'écran son contenu modifié :
#!/usr/bin/perl -W
# Fichier script.pl
use strict qw(subs vars refs);

my $file = "$0";

open(FILE, "<$file");

#Lecture du fichier ligne par ligne jusqu'à la fin du fichier.
while (<FILE>) {
  #Remplace tous les mots 'file' par 'fichier'.
  $_ =~ s/file/fichier/ig;
  #Transforme les lettres minuscules de 'a' à 'e' en majuscules.
  $_ =~ tr/a-e/A-E/;
  #Affiche le résultat des transformations à l'écran.
  print $_;
}

close(FILE);
#Fin script.pl
Ce script est totalement inutile mais je crois que cela montre la facilité d'utilisation.
Oui, j'en conviens, en shell cela tiens en une ligne : cat script.pl | sed -e 's/file/fichier/ig' | sed y/a-e/A-E/. Bizarre, cela ressemble au Perl !? ;-)


Obtenir de l'aide ou en savoir plus

Sur notre machine

Et oui ! Sur notre propre machine il y a plein de sources de documentation sans même le savoir.
La première qui viens à l'esprit naturellement c'est LE man.
man perl vous donne quelques généralités, ainsi que la liste de toutes les sections du manuel de perl (perlsyn, perlre, perlvar, perlop, perlfun...).

Ensuite, il y a le man propre à Perl, qui est perldoc. Le meilleur moyen de voir comment cela marche, c'est d'essayer :
perldoc perldoc

Bon, l'inconvénient, c'est que tout ceci est en anglais...

Sur Internet

Sur Internet, nous trouverons plein de sites parlant de Perl. En général, ils parlent des scripts CGI (scripts Perl pour serveur web). Mais néanmoins, il y a Sinon, si on a les moyens les livres O'Reilly sur Perl sont très bien écrits et sont la référence en la matière.
 

Le mot de la fin

Surtout n'hésitez pas à m'envoyer vos remarques, vos corrections et vos idées, merci.

Haut


Fin du chapitre