Source de cours/gnu.php

<?
  
require ("../page.inc");
  require (
"lessons.inc");
  
  
$currentPage = new LessonPage("gnu");

  
$currentPage->setContent('');

  
$currentPage->addChapter('introduction''Introduction''
<p>
Cet article ne donnera pas toutes les options possibles des outils présentés. Il s\'agit ici de montrer leurs cas d\'utilisation les plus courants pour vos besoins quotidiens. C\'est pour cela que l\'organisation se fait par besoin et non par programme. Pour obtenir plus d\'informations sur toutes les options d\'un de ces programmes, consultez sa <a href="references.php#ref">page de manuel</a>.
</p><p>
Ces programmes peuvent soit travailler sur un fichier passé en paramètre, soit traiter leur entrée standard. On peut ainsi les chaîner les uns aux autres pour réaliser des tâches complexes à l\'aide de <a href="bash.php#redirect">tubes</a>.
</p><p>
Les logiciels présentés ici existent en d\'autres versions sur d\'autres Unix. Les versions abordées ici sont celles <acronym xml:lang="en" lang="en" title="GNU\'s Not Unix">GNU</acronym>, mais la plupart des commandes s\'utilisent avec les autres.
</p>
'
);

  
$currentPage->addChapter('grep''Rechercher un motif''
<p>
Pour simplement rechercher une expression, on peut utiliser <strong>grep</strong>. Cet outil affiche sur sa sortie standard les lignes contenant le motif spécifié. Ce motif est une <a href="regexp.php">expression régulière</a>. La version GNU de grep ne fait pas la distinction entre les expressions simples ou étendues comme le font d\'autres versions.
</p><p>
Le premier argument est l\'expression à rechercher, tous les autres arguments désignent le ou les fichiers dans lesquels effectuer la recherche. Si un seul argument est présent, la recherche se fait sur ce qui provient de l\'entrée standard (généralement la sortie d\'une commande précédente).
</p><p>
Le premier exemple permet d\'afficher la ligne du fichier /etc/passwd correspondant à l\'utilisateur Tian. Ce fichier contient les utilisateurs du système ainsi que diverses informations comme le shell à utiliser.
</p>
<code class="terminal">
&gt; grep "^Tian:" /etc/passwd<br />
Tian:x:500:100:Tian:/home/Tian:/bin/bash<br />
</code>
<p>
L\'exemple suivant exécute tout d\'abord ls -l qui liste les fichiers d\'un répertoire en donnant notamment le propriétaire du fichier. Ensuite grep est utilisé comme filtre pour ne garder que les noms des fichiers appartenant à Tian&nbsp;:
</p>
<code class="terminal">
&gt; ls -l /tmp | grep Tian<br />
drwx------    2 Tian     users        4096 jan 23 20:55 Trash<br />
-rw-r--r--    1 Tian     users       31237 jan 23 20:54 chvar.1532<br />
-rw-r--r--    1 Tian     users       27483 jan 23 20:51 gfind.found<br />
</code>
<p>
En fait les fichiers dont le nom contient Tian dans /tmp seront également affichés. Mais c\'est un moyen rapide pour faire un premier tri.
</p><p>
Une option utile de grep est <strong>-i</strong> qui permet de faire une recherche insensible à la casse. ps -e liste tous les programmes en cours d\'exécution sur le système. Pour voir tous ceux dont le nom commence par x ou X on fera&nbsp;:
</p>
<code class="terminal">
&gt; ps -e | grep -i [[:blank:]]x<br />
  474 ?        00:00:00 xfs<br />
 1241 ?        00:01:09 X<br />
 1493 ?        00:00:02 xconsole<br />
</code>
<p>
Lorsque plusieurs fichiers sont passés en paramètre, chaque ligne en sortie contiendra au début le nom du fichier. L\'option <strong>-h</strong> permet de désactiver cela. <strong>-n</strong> est également d\'usage courant. Avec cette option, l\'affichage contiendra le numéro de la ligne où la correspondance a été trouvée. Pour trouver dans le répertoire courant tous les fichiers contenant la chaîne tmp et savoir à quelle ligne cela se trouve, on utilisera&nbsp;:
</p>
<code class="terminal">
&gt; grep -n tmp * 2&gt;/dev/null<br />
debit:3:DATA_FILE=/tmp/debit.data<br />
gfind:3:FOUND=/tmp/gfind.found<br />
migrate:3:TMP_SED=/tmp/tmp_sed_`basename $0`_$$<br />
verifyPicture:5:export TEMPORARY_DIR=/tmp/<br />
</code>
<p>
Le caractère * sera remplacé par le shell par la liste de tous les fichiers. Le 2&gt;/dev/null à la fin de la ligne permet de ne pas afficher les messages d\'erreurs pour si par exemple l\'utilisateur n\'a pas le droit de lire un fichier ou qu\'un sous-répertoire est présent (car le grep essayera de s\'exécuter sur celui-ci ce qui est impossible). Pour cela le flux d\'erreur standard noté 2 est redirigé vers le périphérique spécial /dev/null qui ignore tout ce qu\'on lui passe agissant comme une poubelle.
</p>
'
);

  
$currentPage->addChapter('sed''Modifier une chaîne''
<p>
Pour effectuer des subtitutions, <strong>sed</strong> est bien adapté. Il s\'agit en fait d\'un éditeur fonctionnant en mode flux et qui permet donc de faire beaucoup plus que cela sur ce qu\'il reçoit en entrée.
</p><p>
Pour l\'utiliser afin de modifier une chaîne, il faut utiliser sa commande <strong>s</strong> (pour substitute) sous la forme suivante&nbsp;:
</p>
<code class="terminal">
&gt; sed s/motif/remplacement/modificateurs
</code>
<p>
Le motif est une <a href="regexp.php">expression régulière</a>. remplacement est une chaîne qui ne sera pas considéré comme une expression régulière donc aucun caractère spécial ne sera pris en compte. En revanche on peut y utiliser les paramètres positionnels (\1 \2 ...) qui correspondent aux différents <a href="regexp.php#sub">sous-motifs</a> trouvés dans la recherche.
</p><p>
Pour les modificateurs, il s\'agit d\'un ensemble de caractères indiquant comment doit être effectué le remplacement. Les plus utiles sont <strong>i</strong> et <strong>g</strong>. La lettre i permet de faire une recherche insensible à la casse (la différence majuscule/minuscule n\'est pas prise en compte). Pour le g, il indique que le remplacement doit être global. Si plusieurs motifs correspondent dans la même ligne, tous seront remplacés. En l\'absence de g, seul le premier le sera.
</p><p>
Traditionnellement, le / est utilisé pour séparer les différents paramètres de la commande de substitution. Mais un autre caractère quelconque peut être utilisé du moment qu\'il n\'apparaît ni dans le motif ni dans le remplacement.
</p><p>
L\'exemple suivant (purement illustratif) remplace tous les o de la chaîne par un a&nbsp;:
</p>
<code class="terminal">
&gt; echo "toto" | sed s/o/a/g<br />
tata<br />
</code>
<p>
Voici comment procéder pour remplacer dans le fichier index.html toutes les occurences du mot tian (en majuscules ou minuscules) par le même mot encadré par &lt;b&gt; et &lt;/b&gt; (ce qui permet en HTML de mettre en gras toutes les fois où le mot apparaît). Le résultat sera copié dans index2.html.
</p>
<code class="terminal">
&gt; sed "sA\(tian\)A&lt;b&gt;\1&lt;/b&gt;Agi" index.html &gt; index2.html
</code>
<p>
Le séparateur utilisé ici est la lettre A (choisi aléatoirement). Cela a été fait pour simplifier le remplacement qui contient un /
</p><p>
Les doubles guillemets autour de la commande passée à sed permettent que le shell n\'interpréte pas &lt; et &gt; comme des <a href="bash.php#redirect">redirections</a>.
</p><p>
sed affiche le résultat sur sa sortie standard. Il est ici ensuite mis dans un fichier à l\'aide de &gt; index2.html qui redirige l\'affichage. On ne peut pas écrire directement dans le même fichier. Il faut donc utiliser un autre fichier avec lequel on peut éventuellement écraser celui d\'origine ensuite.
</p>
'
);

  
$currentPage->addChapter('gawk''Traiter des colonnes''
<p>
Le programme <strong>cut</strong> permet de couper une partie de chaque ligne en entrée. On peut lui indiquer d\'extraire par exemple du quatrième au huitième caractère. Mais nous nous intéresserons plus ici à sa possibilité de considérer des colonnes. On lui spécifie un séparateur de champ et les colonnes à afficher. Voici un exemple pour mieux expliquer cela&nbsp;:
</p>
<code class="terminal">
&gt; cut -d: -f1,6 /etc/passwd<br />
root:/root<br />
Tian:/home/Tian<br />
</code>
<p>
L\'option <strong>-d</strong> permet de spécifier le <strong>séparateur de champ</strong> qui est par défaut la tabulation. Ici on veut utiliser les deux points. Ensuite <strong>-f</strong> permet d\'indiquer une liste de numéros de colonnes séparés par des virgules. On peut aussi indiquer une suite de colonne en séparant les extrémités par un tiret comme ceci&nbsp;:
</p>
<code class="terminal">
&gt; cut -d: -f1-6 /etc/passwd<br />
root:x:0:0:root:/root<br />
Tian:x:400:100:Tian:/home/Tian:/bin/bash<br />
</code>
<p>
Pour de petites tâches comme vu précédemment, cut est idéal. Mais il existe un outil beaucoup plus puissant&nbsp;: <strong>gawk</strong> (version GNU de awk). Il s\'agit d\'un interpréteur complet qui permet de faire des programmes dont la syntaxe est proche du C. Détailler tout ce qu\'il est possible de faire ne rentre pas dans le cadre de cet article, nous ne verrons donc qu\'une petite partie de ses fonctionnalités.
</p><p>
Un programme gawk traite son entrée (passée par un tube ou par un nom de fichier) ligne par ligne. Le séparateur de colonne est défini par l\'option <strong>-F</strong> ou par la variable <strong>FS</strong>. On définit une action (suite d\'instructions) encadrée par des accolades. Chaque action est précédée d\'une expression régulière. L\'action ne sera exécutée que si l\'expression est trouvée dans la ligne. A l\'intérieur d\'une action, les colonnes sont stockées dans les variables <strong>$1</strong>, <strong>$2</strong>, ... La variable <strong>$0</strong> contient la ligne entière.
</p><p>
Voyons tout d\'abord un exemple sans spécification de motif permettant de réaliser le même résultat que le premier exemple de cut&nbsp;:
</p>
<code class="terminal">
&gt; gawk -F: \'{print $1 ":" $6}\' /etc/passwd<br />
root:/root<br />
Tian:/home/Tian<br />
</code>
<p>
On spécifie à l\'aide de -F que le séparateur de colonnes est le caractère : (deux points). Par défaut, il s\'agit de tout caractère d\'espacement. Ensuite on a une seule action à être exécutée sur toutes les lignes. La commande <strong>print</strong> permet de réaliser un affichage. On affiche donc ici la première colonne, une chaîne de caractère fixe ":" (les doubles-guillemets ne sont pas affichés) puis la sixième colonne. Pour utiliser print, on lui spécifie la liste de tout ce qu\'il doit afficher en séparant les éléments par des espaces. Ces éléments peuvent être soit des chaînes fixes encadrés par des doubles guillemets, soit des variables.
</p><p>
On peut faire précéder l\'action par un motif qui est une expression régulière. Par exemple pour n\'afficher ces informations que si l\'utilisateur est Tian&nbsp;:
</p>
<code class="terminal">
&gt; gawk -F: \'/^Tian:/ {print $1 ":" $6}\' /etc/passwd<br />
Tian:/home/Tian<br />
</code>
<p>
Le motif à rechercher est placé entre / / puis entre { } suivent les instructions. Ici il n\'y en a qu\'une seule. On peut en indiquer plusieurs en les séparant par des ; (points-virgules)&nbsp;:
</p>
<code class="terminal">
&gt; gawk -F: \'/^Tian:/ {print "Utilisateur : " $1; print "Répertoire personnel : " $6}\' /etc/passwd<br />
Utilisateur : Tian<br />
Répertoire personnel : /home/Tian<br />
</code>
<p>
Dans un script, on placera les différentes instructions sur des lignes différentes pour plus de lisibilité. L\'exemple suivant montre cela. Il met également en évidence le fait que l\'on peut spécifier plusieurs motifs avec chacun des actions différentes à exécuter&nbsp;:
</p>
<code>
gawk -F: \'<br />
/^Tian:/ {<br />
&nbsp;&nbsp;&nbsp;print "Utilisateur : " $1;<br />
&nbsp;&nbsp;&nbsp;print "Identifiant : " $3;<br />
&nbsp;&nbsp;&nbsp;print "Répertoire personnel : " $6;<br />
}<br />
/^root:/ {<br />
&nbsp;&nbsp;&nbsp;print "Administrateur";<br />
&nbsp;&nbsp;&nbsp;print "Répertoire personnel : " $6;<br />
}<br />
\' /etc/passwd
</code>
<p>
Il faut prendre garde à placer l\'accolade ouvrante sur la même ligne que le motif auquel correspondent les instructions suivantes.
</p><p>
Le message affiché est ici différent selon le nom de l\'utilisateur. Le résultat de l\'exécution de ce programme serait&nbsp;:
</p>
<code class="terminal">
Administrateur<br />
Répertoire personnel : /root<br />
Utilisateur : Tian<br />
Identifiant : 500<br />
Répertoire personnel : /home/Tian<br />
</code>
<p>
Il existe deux motifs spéciaux <strong>BEGIN</strong> et <strong>END</strong>. Ils correspondent à des actions devant être effectuées avant ou après la lecture des lignes. Comme indiqué précédemment, la variable FS permet aussi de spécifier le séparateur d\'enregistrements. Les actions associées à BEGIN permettent d\'initialiser cette variable au lieu d\'utiliser l\'option -F. La variable NR contient le nombre de lignes déjà lues, qu\'elles aient correspondu ou non à un des motifs. En affichant sa valeur à la fin, on peut connaître le nombre d\'enregistrements total. L\'exemple suivant met en pratique ces notions&nbsp;:
</p>
<code>
gawk \'<br />
BEGIN {<br />
&nbsp;&nbsp;&nbsp;FS=":";<br />
&nbsp;&nbsp;&nbsp;print "Les utilisateurs :\n";<br />
}<br />
/^Tian:/ {<br />
&nbsp;&nbsp;&nbsp;print "Utilisateur : " $1;<br />
&nbsp;&nbsp;&nbsp;print "Identifiant : " $3;<br />
&nbsp;&nbsp;&nbsp;print "Répertoire personnel : " $6 "\n";<br />
}<br />
/^root:/ {<br />
&nbsp;&nbsp;&nbsp;print "Administrateur";<br />
&nbsp;&nbsp;&nbsp;print "Répertoire personnel : " $6 "\n";<br />
}<br />
END {<br />
&nbsp;&nbsp;&nbsp;print NR " enregistrements traité(s)"<br />
}<br />
\' /etc/passwd<br />
</code>
<p>
Avec pour résultat&nbsp;:
</p>
<code class="terminal">
Les utilisateurs :<br />
<br />
Administrateur<br />
Répertoire personnel : /root<br />
<br />
Utilisateur : tian<br />
Identifiant : 500<br />
Répertoire personnel : /home/tian<br />
<br />
2 enregistrements traité(s)<br />
</code>
<p>
Dans les affichages est utilisé <strong>\n</strong> qui permet de sauter une ligne. En plus d\'initialiser le séparateur de champ, la partie BEGIN réalise un affichage. Cela afin de bien montrer que ces actions ne sont exécutées qu\'une seule fois au début.
</p>
'
);

  
$currentPage->addChapter('find''Parcourir des répertoires''
<p>
Il existe un outil permettant de lister tous les fichiers et sous-répertoires d\'un répertoire et ce récursivement (pour chaque sous-répertoire, son contenu est également listé). Il s\'agit de <strong>find</strong>. Contrairement aux autres programmes présentés jusque là, il ne lit ni sur son entrée standard, ni à partir d\'un fichier. C\'est donc pour cela qu\'on le trouvera généralement en début d\'une chaîne avec des tubes, sa sortie étant ensuite filtrée par les autres outils.
</p><p>
La manière la plus simple d\'invoquer find est de lui donner un nom de répertoire&nbsp;:
</p>
<code class="terminal">
&gt; find /tmp<br />
/tmp<br />
/tmp/Trash<br />
/tmp/Trash/core<br />
/tmp/Trash/a.out<br />
/tmp/chvar.1532<br />
/tmp/chvar.1754<br />
/tmp/gfind.found<br />
</code>
<p>
Il existe ensuite de nombreuses possibilités de tests pour filtrer les résultats.
</p><p>
On peut demander à ce que ne soient affichés que certains types de résultats avec l\'option <strong>-type</strong> suivie d\'un paramètre. Le plus utile est pour ne garder que les fichiers, car on va généralement leur appliquer ensuite un traitement qui n\'aurait pas de sens pour un répertoire. -type <strong>f</strong> (abréviation de file) est destiné à cela.
</p>
<code class="terminal">
&gt; find /tmp -type f<br />
/tmp/Trash/core<br />
/tmp/Trash/a.out<br />
/tmp/chvar.1532<br />
/tmp/chvar.1754<br />
/tmp/gfind.found<br />
</code>
<p>
Par rapport au résultat précédent, le contenu du répertoire /tmp/Trash est toujours affiché mais pas le répertoire seul. Les possiblités les plus courantes avec -type est de lui passer ensuite <strong>d</strong> pour ne garder que les répertoires et <strong>l</strong> pour les liens symboliques.
</p>
<p>
Un des exemples d\'utilisation de grep présenté plus haut permettait de n\'afficher que les fichiers appartenant à l\'utilisateur Tian. Mais cela ne marchait pas forcément tout le temps. Avec find, on peut spécifier l\'utilisateur à qui doivent appartenir les fichiers au moyen de l\'option <strong>-user</strong>
</p>
<code class="terminal">
&gt; find /tmp -user Tian<br />
/tmp/Trash<br />
/tmp/Trash/core<br />
/tmp/chvar.1532<br />
/tmp/gfind.found<br />
</code>
<p>
On peut aussi préciser quelle profondeur doit parcourir find. Cela permet par exemple de ne pas afficher le contenu des sous-répertoires, ou de le faire seulement jusqu\'à un certain niveau. Avec <strong>-maxdepth</strong> on peut spécifier la valeur maximale de profondeur que find peut atteindre. Pour n\'afficher que le contenu du répertoire courant sans lister ses sous-répertoires, on indiquera une profondeur de 1&nbsp;:
</p>
<code class="terminal">
&gt; find /tmp -maxdepth 1<br />
/tmp<br />
/tmp/Trash<br />
/tmp/chvar.1532<br />
/tmp/chvar.1754<br />
/tmp/gfind.found<br />
</code>
<p>
Il est aussi possible de filtrer selon le nom du fichier. Avec soit <strong>-name</strong> qui permet de donner le nom du fichier uniquement, soit avec <strong>-regex</strong> qui comparera à toute la chaîne affichée (incluant le chemin).
</p><p>
-name fonctionne avec les <a href="bash.php#substi">caractères de substitution du shell</a>. * correspond à une chaîne quelconque et ? à un caractère quelconque.
</p>
<code class="terminal">
&gt; find /tmp -name chvar*<br />
/tmp/chvar.1532<br />
/tmp/chvar.1754<br />
</code>
<p>
Pour -regex, on trouve ensuite une expression régulière. Mais celle-ci sera comparée à toute la chaîne. Cela se passe un peu comme si automatiquement un ^ et un $ étaient ajoutés respectivement en début et en fin de l\'expression régulière. L\'exemple suivant ne trouvera donc rien&nbsp;:
</p>
<code class="terminal">
&gt; find /tmp -regex chvar.*<br />
</code>
<p>
Il faut ajouter au début de l\'expression de quoi englober le chemin complet&nbsp;:
</p>
<code class="terminal">
&gt; find /tmp -regex .*chvar.*<br />
/tmp/chvar.1532<br />
/tmp/chvar.1754<br />
</code>
<p>
Cette expression régulière peut être affinée selon les besoins bien évidemment.
</p><p>
Toutes ces possibilités de filtrage peuvent être combinées en les indiquant les unes à la suite des autres. Il en existe bien plus que celles présentées ici, comme pour ne garder que les fichiers dont le dernier accès remonte à moins qu\'une certaine durée (-atime), ceux appartenant à un groupe donné (-group), ceux avec certaines permissions (-perm), ...
</p><p>
Une fonctionnalité utile de find est sa possibilité d\'exécuter pour chaque fichier trouvé (en prenant en compte les filtres) une commande. Cela se réalise avec l\'option <strong>-exec</strong> suivie de la commande. Cette dernière doit être terminée par un ; (point-virgule). Et à l\'intérieur de cette commande, on peut utiliser {} (deux accolades) qui seront remplacées par chaque élément trouvé par find.
</p>
<code class="terminal">
&gt; find /tmp -exec ls -ld {} \;<br />
drwxrwxrwt   26 root     root         4096 fév  4 20:30 /tmp<br />
drwx------    2 Tian     users        4096 jan 23 20:55 /tmp/Trash<br />
-rwxr--r--    1 root     root        85418 jan 22 14:38 /tmp/Trash/a.out<br />
-rw-r--r--    1 Tian     users       85418 jan 22 13:27 /tmp/Trash/core<br />
-rw-r--r--    1 Tian     users       31237 jan 23 20:54 /tmp/chvar.1532<br />
-rw-r--r--    1 root     root        31237 jan 23 21:08 /tmp/chvar.1754<br />
-rw-r--r--    1 Tian     users       27483 jan 23 20:51 /tmp/gfind.found<br />
</code>
<p>
Cet exemple a parcouru tous les fichiers et répertoires et a exécuté sur chacun d\'eux la commande ls -ld permettant d\'obtenir des informations détaillées (l\'option -d de ls permet que les répertoires soient affichés aussi et non pas leur contenu). Le point-virgule terminant l\'instruction est précédé par un \ (backslash) pour ne pas que celui-ci soit interprété par le shell.
</p>
'
);

  
$currentPage->addChapter('xargs''Passer des arguments''
<p>
A l\'aide de <strong>xargs</strong> on peut passer des arguments à un programme. Une commande est spécifiée à xargs. Il se charge de l\'exécuter avec comme arguments tout ce qu\'il a reçu par son entrée standard.
</p>
<code class="terminal">
&gt; ls /tmp | xargs echo "Fichiers :"<br />
Fichiers : Trash chvar.1532 chvar.1754 gfind.found<br />
</code>
<p>
Cet exemple n\'a aucun intérêt pratique. Il permet simplement de montrer que xargs passe en une seule fois tout ce qu\'il a reçu car avec ls lancé seul, les résultats auraient été affichés sur plusieurs lignes. Mais on peut modifier ce comportement à l\'aide de l\'option <strong>-l</strong> suivie par un nombre. Ce dernier correspond au nombre de lignes à attendre avant de lancer la commande.
</p>
<code class="terminal">
&gt; ls /tmp | xargs -l1 echo "Fichier :"<br />
Fichier : Trash<br />
Fichier : chvar.1532<br />
Fichier : chvar.1754<br />
Fichier : gfind.found<br />
</code>
<p>La valeur était ici de 1. Donc xargs a exécuté une nouvelle fois la commande echo pour chaque ligne en entrée.
</p><p>
Si rien n\'est précisé, les paramètres sont ajoutés à la fin de la commande. Mais on peut spécifier une chaîne de caractères qui devra être remplacée par l\'entrée de xargs avant exécution. Cela se réalise avec l\'option <strong>-i</strong> suivie par la chaîne en question.
</p>
<code class="terminal">
&gt; ls | xargs -l1 -i{} cp {} ~/Trash/<br />
</code>
<p>
Cela va copier (programme cp) tous les fichiers du répertoire courant dans le répertoire ~/Trash. Les {} seront donc remplacées par chaque nom de fichier. En supposant que cette commande ait été lancée à partir du répertoire /tmp/Trash, tout se passe comme si on avait tapé&nbsp;:
</p>
<code class="terminal">
&gt; cp a.out ~/Trash/<br />
&gt; cp core ~/Trash/<br />
</code>
'
);

  
$currentPage->display();
?>