<chapter id="chap_08">
<title>Ecrire des scripts interactifs</title>
<abstract>
<para>Dans ce chapitre nous expliquerons comment interagir avec les utilisateurs de nos scripts&nbsp;:</para>
<para>
<itemizedlist>
<listitem><para>En affichant des messages et des explications conviviaux à l'intention des utilisateurs</para></listitem>
<listitem><para>Récupérer la saisie utilisateur</para></listitem>
<listitem><para>Demander une entrée utilisateur</para></listitem>
<listitem><para>Utiliser les descripteurs de fichiers pour lire et écrire depuis et sur de multiples fichiers</para></listitem>
</itemizedlist>
</para>
</abstract>
<sect1 id="sect_08_01"><title>Afficher les messages utilisateurs</title>
<sect2 id="sect_08_01_01"><title>Interactif ou pas&nbsp;?</title>
<para>Certains scripts s'exécutent sans aucune interaction avec l'utilisateur.  Les avantages des scripts non interactifs comprennent&nbsp;:</para>
<itemizedlist>
<listitem><para>Le script s'exécute d'une façon prédictible chaque fois.</para></listitem>
<listitem><para>Le script peut s'exécuter en tâche de fond.</para></listitem>
</itemizedlist>
<para>Beaucoup de scripts, cependant, demandent des entrées de la part de l'utilisateur, ou donnent des résultats à l'utilisateur alors que le script tourne.  Les avantages des scripts interactifs sont, parmi d'autres&nbsp;:</para>
<itemizedlist>
<listitem><para>Scripts plus flexibles</para></listitem>
<listitem><para> L'utilisateur peut adapter le script pendant qu'il est lancé ou plutôt faire en sorte qu'il agisse de diverses façons.</para></listitem>
<listitem><para>Le script peut afficher la progression du traitement.</para></listitem>
</itemizedlist>
<para>Quand vous écrivez un script interactif, ne jamais être avare de commentaires.  Un script qui affiche des messages appropriés est bien plus convivial et peut être plus facilement débuggé.  Un script peut effectuer un traitement tout à fait correct, mais vous aurez bien plus d'appels d'utilisateurs si il ne les informe pas de l'état d'avancement.  Donc inclure des messages qui demandent à l'utilisateur d'attendre le résultat parce que le traitement est en cours.  Si possible, essayer d'indiquer combien de temps l'utilisateur aura à attendre.  Si l'attente doit régulièrement durer un bon moment à l'exécution de certaines tâches, vous pouvez considérer l'intérêt d'intégrer la situation du processus dans le résultat du script.</para>
<para>Quand vous sollicitez de l'utilisateur une saisie, le mieux est aussi de donner plutôt trop que pas assez d'informations au sujet du type de données attendues.  Ceci s'applique au contrôle des paramètres et au message mode d'emploi l'accompagnant.</para>
<para>Bash a les commandes <command>echo</command> et <command>printf</command> pour fournir des commentaires aux utilisateurs, et bien que vous devriez être familiarisé maintenant au moins avec  <command>echo</command>, nous verrons d'autres exemples dans les sections suivantes.</para>
</sect2>
<sect2 id="sect_08_01_02"><title>Utiliser la commande intégrée echo</title>
<para>La commande intégrée <command>echo</command> affiche ses arguments, séparés par des espaces, et termine par un saut de ligne.  Le statut renvoyé est toujours zéro.  <command>echo</command> a plusieurs options&nbsp;:</para>

<itemizedlist>
<listitem><para><option>-e</option>&nbsp;: interprète les caractères protégés.</para></listitem>
<listitem><para><option>-n</option>&nbsp;: supprime les sauts de ligne résiduels de la fin.</para></listitem>
</itemizedlist>
<para>Comme exemple d'ajout de commentaires, nous écrirons le <filename>feed.sh</filename> et le <filename>penguin.sh</filename> de la <xref linkend="sect_07_02_01_02"/> de façon améliorée&nbsp;:</para>
<screen>
<prompt>michel ~/test&gt;</prompt> <command>cat <filename>penguin.sh</filename></command>
#!/bin/bash

# Ce script vous laisse présenter divers menus à Tux.  Il ne sera heureux que
# quand il aura du poisson.  Pour s'amuser un peu, nous ajoutons quelques animaux.

if [ "$menu" == "poisson" ]; then
  if [ "$animal" == "pingouin" ]; then
    echo -e "Hmmmmmm poisson... Tux heureux&nbsp;!\n"
  elif [ "$animal" == "dauphin" ]; then
    echo -e "\a\a\aPweetpeettreetppeterdepweet&nbsp;!\a\a\a\n"
  else
    echo -e "*prrrrrrrt*\n"
  fi
else
  if [ "$animal" == "pingouin" ]; then
    echo -e "Tux déteste ça.  Tux veut du poisson&nbsp;!\n"
    exit 1
  elif [ "$animal" == "dauphin" ]; then
    echo -e "\a\a\a\a\a\aPweepwishpeeterdepweet&nbsp;!\a\a\a"
    exit 2
  else
    echo -e "Voulez-vous lire cette affiche&nbsp;?!  Ne pas nourrir les "$animal"s&nbsp;!\n"
    exit 3
  fi
fi

<prompt>michel ~/test&gt;</prompt> <command>cat <filename>feed.sh</filename></command>
#!/bin/bash
# Ce script agit en fonction du statut d'exécution renvoyé par penguin.sh

if [ "$#" != "2" ]; then
  echo -e "Utilisation du script feed:\t$0 nourriture nom-animal \n"
  exit 1
else

  export menu="$1"
  export animal="$2"

  echo -e "Nourrissage $menu to $animal...\n"

  feed="/nethome/anny/testdir/penguin.sh"

  $feed $menu $animal

result="$?"

  echo -e "Nourrissage fait.\n"

case "$result" in

  1)
    echo -e "Gaffe&nbsp;: \"Vous feriez mieux de lui donner du poisson, Sinon il s'énerve...\"\n"
    ;;
  2)
    echo -e "Gaffe&nbsp;: \"Pas étonnant qu'il fuit notre planète...\"\n"
    ;;
  3)
    echo -e "Gaffe&nbsp;: \"Achetez la nourriture fournie par le zoo à l'entrée, i***\"\n"
    echo -e "Gaffe&nbsp;: \"Vous voulez les empoisonner&nbsp;?\"\n"
    ;;
  *)
    echo -e "Gaffe&nbsp;: \"N'oubliez pas le guide&nbsp;!\"\n"
    ;;
  esac

fi

echo "Fin..."
echo -e "\a\a\aMerci de votre visite. En espérant vous revoir bientôt&nbsp;!\n"

<prompt>michel ~/test&gt;</prompt> <command>feed.sh <parameter>apple camel</parameter></command>
Nourrir le chameau avec des pommes...

Avez-vous vu l'affiche&nbsp;?!  Ne pas nourrir les chameaux&nbsp;!

Nourrissage fait.

Gaffe&nbsp;: "Achetez la nourriture que le zoo fournie à l'entrée, i ***"

Gaffe&nbsp;: "Vous voulez les empoisonner&nbsp;?"

Fin...
Merci de votre visite. En espérant vous revoir bientôt&nbsp;!

<prompt>michel ~/test&gt;</prompt> <command>feed.sh <parameter>apple</parameter></command>
Utilisation du script feed&nbsp;:       ./feed.sh menu nom-animal

</screen>
<para>Plus d'informations sur les caractères d'échappement à la <xref linkend="sect_03_03_02" />.  Le tableau suivant donne un aperçu des séquences reconnues par la commande <command>echo</command>&nbsp;:</para>

<table id="tab_08_01" frame="all"><title>Séquences d'échappement reconnues par la commande echo</title>
<tgroup cols="2" align="left" colsep="1" rowsep="1">
<thead>
<row><entry>Séquence</entry><entry>sens</entry></row>
</thead>
<tbody>
<row><entry>\a</entry><entry>Alerte (sonnerie).</entry></row>
<row><entry>\b</entry><entry>Retour arrière.</entry></row>
<row><entry>\c</entry><entry>Supprime les saut de lignes résiduel à la fin.</entry></row>
<row><entry>\e</entry><entry>Escape.</entry></row>
<row><entry>\f</entry><entry>Saut de page.</entry></row>
<row><entry>\n</entry><entry>Saut de ligne.</entry></row>
<row><entry>\r</entry><entry>Retour chariot.</entry></row>
<row><entry>\t</entry><entry>Tabulation horizontale.</entry></row>
<row><entry>\v</entry><entry>Tabulation verticale.</entry></row>
<row><entry>\\</entry><entry>Slash inversé.</entry></row>
<row><entry>\ONNN</entry><entry>Le caractère sur 8 bits dont la valeur en base octal est NNN (de 0 à 3 chiffres en octal).</entry></row>
<row><entry>\NNN</entry><entry>Le caractère sur 8 bits dont la valeur en base octal est NNN (de 1 à 3 chiffres en octal).</entry></row>
<row><entry>\xHH</entry><entry>Le caractère sur 8 bits avec la valeur en base hexadécimale (de 1 à 2 chiffres en hexadécimal).</entry></row>
</tbody>
</tgroup>
</table>
<para>Pour plus d'information sur la commande <command>printf</command> et la façon dont elle permet de formater les résultats, voir les pages info de Bash.</para>
</sect2>

</sect1>
<sect1 id="sect_08_02"><title>Récupérer la saisie utilisateur</title>
<sect2 id="sect_08_02_01"><title>L'emploi de la commande intégrée read</title>
<para>La commande intégrée <command>read</command> est la contrepartie de  <command>echo</command> et <command>printf</command>.  La syntaxe de la commande <command>read</command> est la suivante&nbsp;:</para>
<cmdsynopsis><command>read <option>[options]</option> <varname>NAME1 NAME2 ... NAMEN</varname></command></cmdsynopsis>
<para>Une ligne est lue depuis l'entrée standard, ou depuis le fichier dont le descripteur est fourni en argument à l'option  <option>-u</option> option.  Le premier mot de la ligne est affecté au premier nom  <varname>NAME1</varname>, le second mot au second nom, et ainsi de suite, avec les mots résiduels et leurs séparateurs affectés au dernier nom <varname>NAMEN</varname>.  Si il y a moins de mots lus sur le flot d'entrée qu'il y a de noms, les noms résiduels sont valorisés à vide.</para>
<para>Les caractères de la valeur de la variable <varname>IFS</varname> sont employés pour découper l'entrée en mots ou jetons&nbsp;; voir la <xref linkend="sect_03_04_07" />.  Le caractère slash inversé peut être utilisé pour inhiber le sens particulier du caractère lu suivant et pour la continuation de la ligne.</para>
<para>Si aucun nom n'est fourni, la ligne lue est affectée à la variable  <varname>REPLY</varname>.</para>
<para>La commande <command>read</command> renvoie un code à zéro, sauf si un caractère de fin de fichier est rencontré, si <command>read</command> dépasse son temps imparti ou si un descripteur de fichier invalide est fourni en argument à l'option  <option>-u</option> option.</para>
<para>Les options suivantes sont supportées par l'intégrée Bash <command>read</command>&nbsp;:</para>

<table id="tab_08_02" frame="all"><title>Options de l'intégrée read</title>
<tgroup cols="2" align="left" colsep="1" rowsep="1">
<thead>
<row><entry>Option</entry><entry>sens</entry></row>
</thead>
<tbody>
<row><entry>-a <varname>ANAME</varname></entry><entry>Les mots sont affectés séquentiellement aux éléments de la variable tableau <varname>ANAME</varname>, en commençant par l'index 0.  Tous les éléments sont supprimés de <varname>ANAME</varname> avant l'affectation.  Les autres arguments <varname>NAME</varname> sont ignorés.</entry></row>
<row><entry>-d <varname>DELIM</varname></entry><entry>Le premier caractère de <varname>DELIM</varname> est utilisé pour terminer la ligne entrée, plutôt que le saut de ligne.</entry></row>
<row><entry>-e</entry><entry><command>readline</command> est utilisé pour obtenir la ligne.</entry></row>
<row><entry>-n <varname>NCHARS</varname></entry><entry><command>read</command> s'arrête après avoir lu <varname>NCHARS</varname> caractères plutôt que d'attendre une ligne entrée complète.</entry></row>
<row><entry>-p <varname>PROMPT</varname></entry><entry>Affiche le <varname>PROMPT</varname>, sans saut de ligne final, avant de tenter de lire tout autre entrée.  L'invite est affichée seulement si l'entrée vient d'un terminal.</entry></row>
<row><entry>-r</entry><entry>Si cette option est donnée, le slash 
inversé n'agit pas comme caractère d'échappement.  Le slash inversé est 
considéré comme faisant parti de la ligne.  En particulier, un couple 
<quote>saut de ligne-slash inversé</quote> ne devrait pas être utilisé 
comme 
symbole de continuation de ligne.</entry></row>
<row><entry>-s</entry><entry>Mode Silencieux.  Si l'entrée provient d'un terminal, les caractères n'y sont pas renvoyés.</entry></row>
<row><entry>-t <varname>TIMEOUT</varname></entry><entry>Donne un temps imparti à <command>read</command> et renvoie une erreur si une ligne complète n'a pas été lue avant <varname>TIMEOUT</varname> secondes.  Cette option n'a pas d'effet si <command>read</command> n'a pas son entrée depuis un terminal ou un tube.</entry></row>
<row><entry>-u <varname>FD</varname></entry><entry>Obtenir les lignes entrées depuis le fichier de descripteur <varname>FD</varname>.</entry></row>
</tbody>
</tgroup>
</table>
<para>Ceci est un rapide exemple, améliorant le script <filename>leaptest.sh</filename> du chapitre précédent&nbsp;:</para>
<screen>
<prompt>michel ~/test&gt;</prompt> <command>cat <filename>leaptest.sh</filename></command>
#!/bin/bash
# Ce script teste si vous avez saisi une année bissextile ou pas.

echo "Saisissez une année que vous voulez tester  (4 chiffres), puis appuyer sur [ENTREE]&nbsp;:"

read year

if (( ("$year" % 400) == "0" )) || (( ("$year" % 4 == "0") &amp;&amp; ("$year" % 100 !=
"0") )); then
  echo "$année bissextile."
else
  echo "année non bissextile."
fi

<prompt>michel ~/test&gt;</prompt> <command>leaptest.sh</command>
Saisissez une année que vous voulez tester  (4 chiffres), puis appuyer sur [ENTREE]&nbsp;:
2000
2000 année bissextile.
</screen>
</sect2>
<sect2 id="sect_08_02_02"><title>Demander une entrée utilisateur</title>
<para>L'exemple suivant montre comment vous pouvez vous servir de l'invite pour expliquer ce que l'utilisateur devrait saisir.</para>
<screen>
<prompt>michel ~/test&gt;</prompt> <command>cat <filename>friends.sh</filename></command>
#!/bin/bash

# Ce programme garde votre carnet d'adresse à jour.

friends="/var/tmp/michel/friends"

echo "Bonjour, "$USER".  Ce script vous enregistrera dans la base de données des amis de Michel."

echo -n "Saisir votre nom et appuyer sur  [ENTREE]&nbsp;: "
read name
echo -n "Saisir votre sexe et appuyer sur [ENTREE]&nbsp;: "
read -n 1 gender
echo

grep -i "$name" "$friends"

if  [ $? == 0 ]; then
  echo "Vous êtes déjà enregistré, terminé."
  exit 1
elif [ "$gender" == "m" ]; then
  echo "Vous êtes ajouté à la liste des amis de Michel."
  exit 1
else
  echo -n "Quel âge avez-vous&nbsp;? "
  read age
  if [ $age -lt 25 ]; then
    echo -n "De quelle couleur sont vos cheveux&nbsp;? "
    read colour
    echo "$name $age $colour" &gt;&gt; "$friends" 
    echo "Vous êtes ajouté à la liste des amis de Michel.  Merci beaucoup&nbsp;!"
  else
    echo "Vous êtes ajouté à la liste des amis de Michel."
    exit 1
  fi
fi

<prompt>michel ~/test&gt;</prompt> <command>cp <filename>friends.sh /var/tmp</filename>; cd <filename>/var/tmp</filename></command>

<prompt>michel ~/test&gt;</prompt> <command>touch <filename>friends</filename>; chmod <option>a+w</option> <filename>friends</filename></command>

<prompt>michel ~/test&gt;</prompt> <command>friends.sh</command>
Bonjour, michel.  Ce script vous enregistrera dans la base de données des amis de Michel.
Saisir votre nom et appuyer sur  [ENTREE]&nbsp;: michel
Saisir votre sexe et appuyer sur [ENTREE]&nbsp;: m
Vous êtes ajouté à la liste des amis de Michel.

<prompt>michel ~/test&gt;</prompt> <command>cat <filename>friends</filename></command>

</screen>
<para>Remarquez qu'aucun affichage n'est omis ici.  Le script enregistre seulement les informations sur les gens qui intéressent Michel, mais il avertira toujours que vous avez été ajouté à la liste, sauf si vous y êtes déjà.</para>
<para>D'autres gens peuvent maintenant lancer le script&nbsp;:</para>
<screen>
<prompt>[anny@octarine tmp]$</prompt> <command>friends.sh</command>
Bonjour, anny.  Ce script vous enregistrera dans la base de données des amis de Michel.
Saisir votre nom et appuyer sur  [ENTREE]&nbsp;: anny
Saisir votre sexe et appuyer sur [ENTREE]&nbsp;:f
Quel âge avez-vous&nbsp;? 22
De quelle couleur sont vos cheveux&nbsp;? noir
Vous êtes ajouté à la liste des amis de Michel.
</screen>
<para>Finalement, la liste <filename>friends</filename> ressemble à ceci&nbsp;:</para>
<screen>
tille 24 noir
anny 22 noir
katya 22 blonde
maria 21 noir
--output omitted--
</screen>
<para>Bien sûr, cette situation n'est pas idéale, parce que chacun peut renseigner (mais pas se retirer) du fichier de Michel.  Vous pouvez palier ce défaut en utilisant des accès privilégiés au fichier de script, voir <ulink url="http://www.tldp.org/LDP/intro-linux/html/sect_04_01.html#sect_04_01_06">SUID et SGID</ulink> dans le guide d'introduction à Linux.</para>
</sect2>
<sect2 id="sect_08_02_03"><title>Redirection et descripteurs de fichiers</title>
<sect3 id="sect_08_02_03_01"><title>Généralité</title>
<para>Comme vous avez pu le réaliser à la suite d'une utilisation rudimentaire du Shell, l'entrée et la sortie d'une commande peuvent être redirigées avant son exécution, grâce à la notation appropriée - les opérateurs de redirection - interprétée par le Shell.  La redirection peut aussi être employée pour ouvrir et fermer des fichiers dans l'environnement d'exécution du Shell.</para>  
<para>La redirection peut aussi se produire dans le script, de sorte qu'il puisse recevoir son entrée depuis un fichier, par exemple, ou qu'il puisse envoyer les résultats vers un fichier.  Ultérieurement, l'utilisateur peut visualiser le fichier de résultat, ou il peut être exploité en entrée par un autre script.</para>
<para>Les fichiers d'entrée et de sortie sont désignés par des entiers indicateurs qui repèrent tout les fichiers ouverts d'un processus donné.  Ces valeurs numériques sont connues sous le nom de descripteurs de fichiers.  Les descripteurs les plus courant sont <emphasis>stdin</emphasis>, <emphasis>stdout</emphasis> et <emphasis>stderr</emphasis>, avec les numéros 0, 1 et 2, respectivement.  Ces numéros et leur entité respective sont réservés.  Bash peut aussi considérer des ports TCP ou UDP sur les hôtes du réseau en tant que descripteur.</para>
<para>L'affichage ci-dessous montre comment les descripteurs réservés pointent sur des entités concrètes&nbsp;:</para>
<screen>
<prompt>michel ~&gt;</prompt> <command>ls <option>-l</option> <filename>/dev/std*</filename></command>
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stderr -&gt; ../proc/self/fd/2
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stdin -&gt; ../proc/self/fd/0
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stdout -&gt; ../proc/self/fd/1

<prompt>michel ~&gt;</prompt> <command>ls <option>-l</option> <filename>/proc/self/fd/[0-2]</filename></command>
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/0 -&gt; /dev/pts/6
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/1 -&gt; /dev/pts/6
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/2 -&gt; /dev/pts/6
</screen>
<para>Notez que chaque process a sa propre vue des fichiers sous  <filename>/proc/self</filename>, puisque c'est un lien symbolique vers  <filename>/proc/&lt;process_ID&gt;</filename>.</para>
<para>Vous pouvez consulter <command>info MAKEDEV</command> et <command>info proc</command> pour plus de détails sur le sous-répertoire <filename>/proc</filename> et la façon dont votre système gère les descripteurs standards de chaque processus lancé.</para>
<para>Quand vous exécutez n'importe quelle commande, les étapes suivantes sont déroulées, dans l'ordre&nbsp;: </para>
<itemizedlist>
<listitem><para>Si la sortie standard de la commande précédente a été dirigée sur l'entrée standard de la commande en cours, alors <filename>/proc/&lt;current_process_ID&gt;/fd/0</filename> est modifié pour cibler le même tube anonyme que  <filename>/proc/&lt;previous_process_ID/fd/1</filename>.</para></listitem>
<listitem><para>Si la sortie standard de la commande en cours a été dirigée sur l'entrée standard de la commande à suivre, alors  <filename>/proc/&lt;current_process_ID&gt;/fd/1</filename> est modifié pour cibler un autre tube anonyme.</para></listitem>
<listitem><para>La redirection pour la commande en cours est traitée de gauche à droite.</para></listitem>
<listitem><para>La redirection de <quote>N&gt;&amp;M</quote> ou <quote>N&lt;&amp;M</quote> après une commande a pour effet de créer ou modifier le lien symbolique <filename>/proc/self/fd/N</filename> avec la même cible que le lien symbolique <filename>/proc/self/fd/M</filename>.</para></listitem>
<listitem><para>Les redirections de <quote>N&gt; file</quote> et <quote>N&lt; file</quote> ont pour effet de créer ou modifier le lien symbolique <filename>/proc/self/fd/N</filename> avec le fichier cible.</para></listitem>
<listitem><para>La fermeture du descripteur de fichier <quote>N&gt;&amp;-</quote> a pour effet de supprimer le lien symbolique <filename>/proc/self/fd/N</filename>.</para></listitem>
<listitem><para>Seulement maintenant la commande courante est exécutée.</para></listitem>
</itemizedlist>
<para>Quand vous lancez un script depuis la ligne de commande, rien ne change tellement parce que le processus Shell enfant utilisera les mêmes descripteurs que son parent.   Quand le parent n'existe pas, par exemple quand vous lancez un script par l'outil <emphasis>cron</emphasis> les descripteurs standards sont des tubes et autres fichiers (temporaires), à moins qu'un autre moyen de redirection soit employé.  Ceci est démontré dans l'exemple ci-dessous, lequel produit le résultat avec un simple script <command>at</command>&nbsp;:</para>
<screen>
<prompt>michel ~&gt;</prompt> <command>date</command>
Fri Jan 24 11:05:50 CET 2003

<prompt>michel ~&gt;</prompt> <command>at <parameter>1107</parameter></command>
avertissement&nbsp;: les commandes seront exécutées avec (par ordre) 
a) $SHELL b) login shell c)/bin/sh
<prompt>at&gt;</prompt> <command>ls <option>-l</option> <filename>/proc/self/fd/</filename> &gt; <filename>/var/tmp/fdtest.at</filename></command>
<prompt>at&gt;</prompt> <command>&lt;EOT&gt;</command>
job 10 at 2003-01-24 11:07

<prompt>michel ~&gt;</prompt> <command>cat <filename>/var/tmp/fdtest.at</filename></command>
total 0
lr-x------    1 michel michel  64 Jan 24 11:07 0 -&gt; /var/spool/at/!0000c010959eb (deleted)
l-wx------    1 michel michel  64 Jan 24 11:07 1 -&gt; /var/tmp/fdtest.at
l-wx------    1 michel michel  64 Jan 24 11:07 2 -&gt; /var/spool/at/spool/a0000c010959eb
lr-x------    1 michel michel  64 Jan 24 11:07 3 -&gt; /proc/21949/fd
</screen>
<para>Et un avec <command>cron</command>&nbsp;:</para>
<screen>
<prompt>michel ~&gt;</prompt> <command>crontab <option>-l</option></command>
# NE PAS EDITER CE FICHIER - éditer le modèle et le réinstaller.
# (/tmp/crontab.21968 installed on Fri Jan 24 11:30:41 2003)
# (Cron version -- $Id: chap8.xml,v 1.3 2007-05-01 21:11:11 fevrier Exp $)
32 11 * * * ls -l /proc/self/fd/ &gt; /var/tmp/fdtest.cron

<prompt>michel ~&gt;</prompt> <command>cat <filename>/var/tmp/fdtest.cron</filename></command>
total 0
lr-x------    1 michel michel  64 Jan 24 11:32 0 -&gt; pipe:[124440]
l-wx------    1 michel michel  64 Jan 24 11:32 1 -&gt; /var/tmp/fdtest.cron
l-wx------    1 michel michel  64 Jan 24 11:32 2 -&gt; pipe:[124441]
lr-x------    1 michel michel  64 Jan 24 11:32 3 -&gt; /proc/21974/fd
</screen>
</sect3>
<sect3 id="sect_08_02_03_02"><title>Redirection des erreurs</title>
<para>Dans l'exemple précédent il apparaît clairement que vous pouvez fournir les fichiers d'entrée et de sortie à un script (voir <xref linkend="sect_08_02_04" /> pour plus de détails), mais certains oublient de rediriger les erreurs - un affichage dont peut dépendre la suite.  Aussi, si vous êtes chanceux, les erreurs vous seront adressées par mail et d'éventuels dysfonctionnements pourront vous apparaître.  Si vous n'êtes pas chanceux, les erreurs feront planter votre script et ne seront ni capturées ni adressées nulle part, par conséquent vous ne pourrez débugger.</para>
<para>Quand vous redirigez les erreurs, faites attention à l'ordre de préséance.  Par exemple, cette commande lancée dans <filename>/var/spool</filename></para>
<screen>
<command>ls <option>-l</option> <filename>*</filename> <parameter>2</parameter>&gt; <filename>/var/tmp/unaccessible-in-spool</filename></command>
</screen>
<para>va rediriger la sortie standard de la commande <command>ls</command> vers le fichier <filename>unaccessible-in-spool</filename> dans <filename>/var/tmp</filename>.  La commande</para>
<screen>
<command>ls <option>-l</option> <filename>*</filename> &gt; <filename>/var/tmp/spoollist</filename> <parameter>2</parameter>&gt;&amp;<parameter>1</parameter></command>
</screen>
<para>redirigera et le standard d'entrée et le standard d'erreur vers le fichier  <filename>spoollist</filename>.  La commande</para>
<screen>
<command>ls <option>-l</option> <filename>*</filename> <parameter>2</parameter> &gt;&amp; <parameter>1</parameter> &gt; <filename>/var/tmp/spoollist</filename></command>
</screen>
<para>dirige seulement le standard de sortie vers le fichier de destination, parce que le standard d'erreurs est copié vers le standard de sortie avant que le standard de sortie soit redirigé.</para>
<para>Par commodité, les erreurs sont souvent redirigées vers  <filename>/dev/null</filename>, si il est sûr qu'elles n'ont pas d'intérêt.  Des centaines d'exemples peuvent être trouvés dans les scripts de lancement de votre système.</para>
<para>Bash autorise à la fois le standard de sortie et le standard d'erreurs à être redirigés vers le fichier dont le nom est le résultat de l'expansion de <filename>FILE</filename> avec cette forme&nbsp;:</para>
<cmdsynopsis><command>&amp;&gt; <filename>FILE</filename></command></cmdsynopsis>
<para>C'est l'équivalent de <command>&gt; <filename>FILE</filename> 2&gt;&amp;1</command>, la forme employée dans les exemples précédents.  C'est aussi combiné souvent avec la redirection vers <filename>/dev/null</filename>, par exemple quand vous voulez juste qu'une commande s'exécute, quelque soit le résultat ou le statut qu'elle donne.</para>
</sect3>
</sect2>
<sect2 id="sect_08_02_04"><title>Fichier d'entrée et fichier de sortie</title>
<sect3 id="sect_08_02_04_01"><title>Avec /dev/fd</title>
<para>Le répertoire <filename>/dev/fd</filename> contient des entrées nommées <filename>0</filename>, <filename>1</filename>, <filename>2</filename>, etc.  Ouvrir le fichier <filename>/dev/fd/N</filename> est équivalent à dupliquer le descripteur <emphasis>N</emphasis>.  Si votre système possède <filename>/dev/stdin</filename>, <filename>/dev/stdout</filename> et <filename>/dev/stderr</filename>, vous verrez qu'ils sont équivalents à <filename>/dev/fd/0</filename>, <filename>/dev/fd/1</filename> et <filename>/dev/fd/2</filename>, respectivement.</para>
<para>La principale utilisation des fichiers <filename>/dev/fd</filename> est faites par le Shell.  Ce mécanisme permet aux programmes qui utilisent des chemins en paramètre de voir le standard d'entrée et le standard de sortie de la même façon que d'autres chemins.  Si <filename>/dev/fd</filename> n'est pas disponible sur votre système, vous devrez trouver un moyen pour résoudre ce problème. Ce qui peut être fait, par exemple, avec un tiret (<emphasis>-</emphasis>) qui indique que le programme doit lire depuis un tube.  Un exemple&nbsp;:</para>
<screen>
<prompt>michel ~&gt;</prompt> <command>filter <filename>body.txt.gz</filename> | cat <filename>header.txt</filename> - <filename>footer.txt</filename></command>
Ce texte est affiché au début de chaque travail d'affichage et merci à l'administrateur d'avoir mis en place une si bonne infrastructure d'affichage.

Texte à filtrer.

Ce texte est à afficher à la fin de chaque travail d'affichage.
</screen>
<para>La commande <command>cat</command> d'abord lit le fichier  <filename>header.txt</filename>, puis son standard d'entrée lequel est le standard de sortie de la commande <command>filter</command>, et finalement le fichier <filename>footer.txt</filename>.  Le sens spécial du tiret en tant que paramètre de ligne de commande pour se référer au standard d'entrée ou de sortie est une confusion qui a perduré dans beaucoup de programmes. Il peut aussi y avoir des problèmes quand vous spécifiez le tiret en tant que premier paramètre, puisqu'il peut être interprété comme une option de la commande précédente.  L'usage de <filename>/dev/fd</filename> permet l'uniformité et évite la confusion&nbsp;:</para>
<screen>
<prompt>michel ~&gt;</prompt> <command>filter <filename>body.txt</filename> | cat <filename>header.txt /dev/fd/0 footer.txt</filename> | lp</command>
</screen>
<para>Dans cet exemple propre, toutes les sorties sont cumulées dans l'entonnoir <command>lp</command> pour les envoyer vers l'imprimante par défaut.</para>
</sect3>
<sect3 id="sect_08_02_04_02"><title>Read et exec</title>
<sect4 id="sect_08_02_04_02_01"><title>Assigner des descripteurs de fichiers</title>
<para>Une autre façon de considérer les descripteurs de fichiers est d'y penser comme un indicateur numérique assigné à un fichier.  Au lieu d'employer le nom de fichier, vous pouvez employer le numéro de descripteur.  L'intégrée <command>exec</command> peut être utilisée pour remplacer le Shell du process actif ou pour réassigner des descripteurs de fichiers à ce Shell.  Par exemple, elle peut être employée pour assigner un descripteur de fichier à un fichier.  Employez</para>
<cmdsynopsis><command>exec fdN&gt; <filename>file</filename></command></cmdsynopsis>
<para>pour assigner le descripteur N au fichier <filename>file</filename> en sortie, et</para>
<cmdsynopsis><command>exec fdN&lt; <filename>file</filename></command></cmdsynopsis>
<para>pour assigne le descripteur N au fichier <filename>file</filename> en entrée.  Après qu'un descripteur ait été assigné à un fichier, il peut être employé avec les opérateurs Shell de redirection, comme le montre l'exemple suivant&nbsp;:</para>
<screen>
<prompt>michel ~&gt;</prompt> <command>exec <filename>4</filename>&gt; <filename>result.txt</filename></command>

<prompt>michel ~&gt;</prompt> <command>filter <filename>body.txt</filename> | cat <filename>header.txt /dev/fd/0 footer.txt</filename> &gt;&amp; <filename>4</filename></command>

<prompt>michel ~&gt;</prompt> <command>cat <filename>result.txt</filename></command>
Ce texte est affiché au début de chaque travail d'impression et remercie l'administrateur d'avoir mis en place pour nous une infrastructure d'impression si efficace.

Texte à filtrer.

Ce texte est à afficher à la fin de chaque travail d'affichage.
</screen>
<note><title>Le descripteur 5</title>
<para>L'emploi de ce descripteur de fichier peut être cause de soucis, voir <ulink url="http://www.tldp.org/LDP/abs/html/io-redirection.html">the Advanced Bash-Scripting Guide</ulink>, chapitre 16.  Il vous est sérieusement recommandé de ne pas l'employer.</para>
</note>
</sect4>
<sect4 id="sect_08_02_04_02_02"><title>Read dans un script</title>
<para>Ce qui suit est un exemple qui montre comment vous pouvez permuter l'entrée depuis un fichier sur l'entrée de la ligne de commande et vice-versa&nbsp;:</para>
<screen>
<prompt>michel ~/testdir&gt;</prompt> <command>cat <filename>sysnotes.sh</filename></command>
#!/bin/bash

# Ce script fait un index des fichiers de configuration importants, les sauvegarde tous dans
# un fichier et autorise l'ajout de commentaires à chaque fichier.

CONFIG=/var/tmp/sysconfig.out
rm "$CONFIG" 2&gt;/dev/null

echo "Le résultat sera mémorisé dans $CONFIG."

# Crée fd 7 avec la cible de fd 0 (save stdin "value")
exec 7&lt;&amp;0

# update fd 0 to target file /etc/passwd
exec &lt; /etc/passwd

# Read the first line of /etc/passwd
read rootpasswd

echo "Sauvegarde des info de root..."
echo "Les infos du compte root&nbsp;:" &gt;&gt; "$CONFIG"
echo $rootpasswd &gt;&gt; "$CONFIG"

# Modifie fd 0 pour cibler fd 7  (old fd 0 target); supprime fd 7
exec 0&lt;&amp;7 7&lt;&amp;-

echo -n "Entrez un commentaire ou [ENTER] sans commentaire&nbsp;: "
read comment; echo $comment &gt;&gt; "$CONFIG"

echo "Mémorise les infos de l'hôte..."

# D'abord préparer un fichier hôte sans commentaires
TEMP="/var/tmp/hosts.tmp"
cat /etc/hosts | grep -v "^#" &gt; "$TEMP"

exec 7&lt;&amp;0
exec &lt; "$TEMP"

read ip1 name1 alias1
read ip2 name2 alias2

echo "La configuration de l'hôte local&nbsp;:" &gt;&gt; "$CONFIG"

echo "$ip1 $name1 $alias1" &gt;&gt; "$CONFIG"
echo "$ip2 $name2 $alias2" &gt;&gt; "$CONFIG"

exec 0&lt;&amp;7 7&lt;&amp;-

echo -n "Entrez un commentaire ou [ENTER] sans commentaire&nbsp;: "
read comment; echo $comment &gt;&gt; "$CONFIG"
rm "$TEMP"

<prompt>michel ~/testdir&gt;</prompt> <command>sysnotes.sh</command>
Le résultat sera mémorisé dans /var/tmp/sysconfig.out.
Sauvegarde des info de root...
Entrez des commentaires [ENTER] for no comment&nbsp;: pense-bête pour mot de passe&nbsp;: vacance
Sauvegarde des informations système...
Entrez des commentaires [ENTER] for no comment&nbsp;: dans le DNS central

<prompt>michel ~/testdir&gt;</prompt> <command>cat <filename>/var/tmp/sysconfig.out</filename></command>
Les infos du compte root&nbsp;:
root:x:0:0:root:/root:/bin/bash
pense-bête pour mot de passe&nbsp;: vacance
Votre configuration sur la machine locale&nbsp;:
127.0.0.1 localhost.localdomain localhost
192.168.42.1 tintagel.kingarthur.com tintagel
dans le DNS central
</screen>
</sect4>
</sect3>
<sect3 id="sect_08_02_04_03"><title>Fermer les descripteurs de fichiers</title>
<para>Parce que les processus enfants héritent des descripteurs ouverts, c'est une bonne pratique que de fermer les descripteurs quand on en a plus besoin.  Ceci  est fait avec la syntaxe suivante </para>
<cmdsynopsis><command>exec fd&lt;&amp;-</command></cmdsynopsis>
<para>syntax.  Dans l'exemple ci-dessus, le descripteur 7, qui a été assigné au standard d'entrée, est fermé chaque fois que l'utilisateur a besoin d'utiliser le périphérique d'entrée standard, habituellement le clavier.</para>
<para>Suit un exemple simple de redirection du standard d'erreurs sur un tube&nbsp;:</para>
<screen>
<prompt>michel ~&gt;</prompt> <command>cat <filename>listdirs.sh</filename></command>
#!/bin/bash

# Ce script garde le standard de sortie, tandis qu'il redirige le standard d'erreurs 
# afin d'être traité par awk.

INPUTDIR="$1"

# fd 6 targets fd 1 target (console out) in current shell
exec 6&gt;&amp;1

# fd 1 targets pipe, fd 2 targets fd 1 target (pipe),
# fd 1 targets fd 6 target (console out), fd 6 closed, execute ls
ls "$INPUTDIR"/* 2&gt;&amp;1 &gt;&amp;6 6&gt;&amp;- \
                                # Closes fd 6 for awk, but not for ls.

| awk 'BEGIN { FS=":" } { print "YOU HAVE NO ACCESS TO" $2 }' 6&gt;&amp;-

# fd 6 closed for current shell
exec 6&gt;&amp;-
</screen>
</sect3>
<sect3 id="sect_08_02_04_04"><title>Les documents <emphasis>intégrés</emphasis> (NdT&nbsp;: here documents, que l'on appele aussi 'document lié')</title>
<para>Fréquemment votre script peut avoir besoin d'appeler un autre programme ou script qui nécessite une entrée.  Le document <emphasis>intégré</emphasis> fournit un moyen d'enjoindre au Shell de lire l'entrée de la source actuelle jusqu'à une ligne contenant seulement la chaîne ad hoc (pas de blancs résiduels).  Toutes les lignes lues jusqu'à celle-là sont envoyées comme entrée standard de la commande.</para>
<para>Le résultat est que vous n'avez pas besoin de faire appel à différents fichiers&nbsp;; vous pouvez utiliser les caractères spéciaux du Shell, et c'est plus lisible qu'un flot d'<command>echo</command>&nbsp;:</para>
<screen>
<prompt>michel ~&gt;</prompt> <command>cat <filename>startsurf.sh</filename></command>
#!/bin/bash

# Ce script fournit aux usagers un moyen facile de choisir entre plusieurs navigateurs.

echo "Voici les navigateurs WEB de ce système&nbsp;:"
 
# Début du document 'intégré'
cat &lt;&lt; BROWSERS
mozilla
links
lynx
konqueror
opera
netscape
BROWSERS
# Fin du document 'intégré'

echo -n "Lequel préférez-vous&nbsp;? "
read browser

echo "Démarrage de $browser, Merci de patienter..."
$browser &amp;

<prompt>michel ~&gt;</prompt> <command>startsurf.sh</command>
Voici les navigateurs WEB de ce système&nbsp;:
mozilla
links
lynx
konqueror
opera
netscape
<prompt>Lequel préférez-vous&nbsp;?</prompt> <command>opera</command>
Démarrage de opera, Merci de patienter...
</screen>
<para>Bien que nous parlions de <emphasis>document intégré</emphasis>, il doit être un bloc dans le script.  Voici un exemple qui installe un paquetage automatiquement, même si vous devriez normalement confirmer&nbsp;:</para>
<screen>
#!/bin/bash
 
# Ce script installe un paquetage automatiquement avec yum.
 
if [ $# -lt 1 ]; then
        echo "Utilisation&nbsp;: $0 package."
        exit 1
fi
 
yum install $1 &lt;&lt; CONFIRM
y
CONFIRM
</screen>
<para>Et voici comment le script tourne.  Quand la question <quote>Is this ok [y/N]</quote> apparaît, le script répond <quote>y</quote> automatiquement&nbsp;:</para>
<screen>
<prompt>[root@picon bin]#</prompt> <command>./install.sh <parameter>tuxracer</parameter></command>
Gathering header information file(s) from server(s)
Server: Fedora Linux 2 - i386 - core
Server: Fedora Linux 2 - i386 - freshrpms
Server: JPackage 1.5 for Fedora Core 2
Server: JPackage 1.5, generic
Server: Fedora Linux 2 - i386 - updates
Finding updated packages
Downloading needed headers
Resolving dependencies
Dependencies resolved
I will do the following:
[install: tuxracer 0.61-26.i386]
<prompt>Is this ok [y/N]:</prompt> <command><keycap>Enter</keycap></command>Downloading Packages
Running test transaction:
Test transaction complete, Success!
tuxracer 100 % done 1/1
Installed:  tuxracer 0.61-26.i386
Transaction(s) Complete
</screen>

</sect3>
</sect2>

</sect1>
<sect1 id="sect_08_03"><title>Résumé</title>
<para>Dans ce chapitre nous avons appris comment afficher des commentaires et comment solliciter une saisie de la part de l'utilisateur.  Ce qui est effectué habituellement par la combinaison de  <command>echo</command>/<command>read</command>.  Nous avons aussi abordé comment les fichiers peuvent être employés en entrée et en sortie par le biais des descripteurs de fichiers et la redirection, et comment cela peut être combiné avec la saisie utilisateur.</para>
<para>Nous avons insisté sur l'importance des messages à destination des utilisateurs dans vos scripts.  Comme d'habitude quand d'autres se servent de vos scripts, mieux faut donner trop d'informations que pas assez.  Le document <emphasis>intégré</emphasis> est un type de construction Shell qui permet la création de liste, contenant des choix pour les utilisateurs.  Cette construction peut aussi être employée pour exécuter d'autres sortes de tâches interactives en tâche de fond, sans intervention.</para>

</sect1>
<sect1 id="sect_08_04"><title>Exercices</title>
<para>Ces exercices sont des applications pratiques des constructions abordées dans ce chapitre.  Quand vous écrivez un script, il est préférable de tester dans un répertoire qui ne contienne pas trop de données.  Ecrire chaque étape, puis tester cette portion de code, plutôt que d'écrire tout d'un seul coup.</para>
<orderedlist>
<listitem><para>Ecrire un script qui demande l'âge de l'usager.  Si il est égal ou supérieur à 16, afficher un message indiquant que l'usager est autorisé à boire de l'alcool.  Si l'usager est en dessous de 16 ans, afficher un message disant combien d'années l'usager devra attendre avant d'être autorisé légalement à boire de l'alcool.</para>
<para>En plus, calculer combien de bière un usager de plus de 18 ans a bu statistiquement (100 litres/an) et donner cette information à l'usager.</para>
</listitem>
<listitem><para>Ecrire un script qui prenne un fichier en paramètre.  Servez-vous d'un document <emphasis>intégré</emphasis> qui présente à l'usager différents choix pour compresser ce fichier.  Les choix possibles peuvent être <command>gzip</command>, <command>bzip2</command>, <command>compress</command> et <command>zip</command>. </para></listitem>
<listitem><para>Ecrire un script appelé <filename>homebackup</filename> qui automatise  <command>tar</command> afin que la personne qui lance le script ait toujours les options désirées (<option>cvp</option>) et sauvegarder le répertoire de destination (<filename>/var/backups</filename>) afin de faire une sauvegarde du répertoire racine de l'usager.  Ajouter les fonctionnalités suivantes&nbsp;:</para>

<itemizedlist>
<listitem><para>Tester le nombre de paramètres.  Le script ne devrait pas en avoir.  Si des paramètres sont présents, sortir après avoir affiché le message d'utilisation du script.</para></listitem>
<listitem><para>Déterminer si le répertoire <filename>backups</filename> a assez d'espace libre pour contenir la sauvegarde.</para></listitem>
<listitem><para>Demander à l'usager si il veut une sauvegarde complète ou incrémentale.  Si l'usager n'a pas eu encore de sauvegarde complète, afficher un message disant qu'une complète sera faite.  Dans le cas d'une sauvegarde incrémentale, ne la faire que si la complète n'est pas plus vieille que d'une semaine.</para></listitem>
<listitem><para>Comprimer la sauvegarde avec un outil de compression quelconque.  Informer l'usager que le script va compresser, parce que ça peut prendre du temps, et l'usager pourrait s'inquiéter si aucun résultat n'apparaît à l'écran.</para></listitem>
<listitem><para>Afficher un message indiquant à l'usager la taille de la sauvegarde compressée.</para></listitem>
</itemizedlist>
<para>Voir <command>info tar</command> ou <ulink url="http://tille.xalasys.com/training/tldp/c4540.html#sect_09_01_01">Introduction à Linux</ulink>, chapitre 9&nbsp;: <quote>Preparing your data</quote> for background information.</para>
</listitem>
<listitem><para>Ecrire un script appelé <filename>simple-useradd.sh</filename> qui ajoute un utilisateur au système local. Le script devrait&nbsp;:</para>
<itemizedlist>
<listitem><para>Ne prendre qu'un seul paramètre, ou sinon sortir avec un message d'utilisation.</para></listitem>
<listitem><para>Contrôler <filename>/etc/passwd</filename> et sélectionner le premier identifiant non affecté.  Afficher un message contenant l'identifiant.</para></listitem>
<listitem><para>Créer un groupe privé pour cet utilisateur, en contrôlant le fichier  <filename>/etc/group</filename>.  Afficher un message contenant l'identifiant du groupe.</para></listitem>
<listitem><para>Rassembler des informations sur l'utilisateur&nbsp;: un commentaire décrivant cet utilisateur, choix dans une liste de Shell (tester sa validité, si non sortir avec un message), la date d'expiration du compte, les autres groupes auxquels ce nouvel utilisateur peut appartenir.</para></listitem>
<listitem><para>Avec les informations obtenues, ajouter une ligne à  
<filename>/etc/passwd</filename>, <filename>/etc/group</filename> et 
<filename>/etc/shadow</filename>&nbsp;; créer le répertoire racine de 
l'utilisateur (avec les autorisations correctes&nbsp;! Ajouter 
l'utilisateur aux groupes secondaires désirés.</para></listitem>
<listitem><para>Définir le mot de passe de cet utilisateur à une chaîne connue.</para></listitem>
</itemizedlist>
</listitem>
<listitem><para>Réécrire le script de la <xref linkend="sect_07_02_01_04" /> afin qu'il lise son entrée depuis la saisie utilisateur plutôt que du premier paramètre.</para>
</listitem>
</orderedlist>

</sect1>
</chapter>