18. Documents en ligne

Ici et maintenant, les gars.

-- Aldous Huxley, Islande

Un document en ligne est un bloc de code à usage spécial. Il utilise une forme de redirection d'E/S pour fournir une liste de commande à un programme ou une commande interactif, tel que ftp, cat ou l'éditeur de texte ex.

COMMANDE <<DesEntreesIci
...
DesEntreesIci

Une chaîne de caractères de limite encadre la liste de commandes. Le symbole spécial << désigne la chaîne de caractères de limite. Ceci a pour effet de rediriger la sortie d'un fichier vers le stdin d'un programme ou d'une commande. Ceci est similaire à programme-interactif < fichier-commandes, où fichier-commandes contient

commande n°1
commande n°2
...

L'alternative au document en ligne ressemble à ceci :

#!/bin/bash
programme-interactif <<ChaineLimite
commande #1
commande #2
...
ChaineLimite

Choisissez une chaîne de caractères de limite suffisamment inhabituelle pour qu'elle ne soit pas présente où que ce soit dans la liste de commandes afin qu'aucune confusion ne puisse survenir.

Notez que les documents en ligne peuvent parfois être utilisés correctement avec des utilitaires et des commandes non interactifs, tels que wall.

Exemple 18.1. broadcast : envoie des messages à chaque personne connectée

#!/bin/bash

wall <<zzz23EndOfMessagezzz23
Envoyez par courrier électronique vos demandes de pizzas à votre administrateur système.
     (Ajoutez un euro supplémentaire pour les anchois et les champignons.)
# Un message texte supplémentaire vient ici.
# Note: Les lignes de commentaires sont affichées par 'wall'.
zzz23EndOfMessagezzz23

# Peut se faire plus efficacement avec
#         wall <fichier-message
#  Néanmoins, intégrer un message modèle dans un script
#+ est une solution rapide bien que sale

exit 0

Même de si improbables candidats comme vi tendent eux-même aux documents en ligne.

Exemple 18.2. fichierstupide : Crée un fichier stupide de deux lignes

#!/bin/bash

# Utilisation non interactive de 'vi' pour éditer un fichier.
# Émule 'sed'.

E_MAUVAISARGS=65

if [ -z "$1" ]
then
  echo "Usage: `basename $0` nomfichier"
  exit $E_MAUVAISARGS
fi

FICHIERCIBLE=$1

# Insère deux lignes dans le fichier et le sauvegarde.
#--------Début document en ligne-----------#
vi $FICHIERCIBLE <<x23LimitStringx23
i
Ceci est la ligne 1 du fichier exemple.
Ceci est la ligne 2 du fichier exemple.
^[
ZZ
x23LimitStringx23
#--------Fin   document en ligne-----------#

#  Notez que ^[ ci-dessus est un échappement littéral, saisi avec
#+ Control-V <Esc>.

#  Bram Moolenaar indique que ceci pourrait ne pas fonctionner avec 'vim',
#+ à cause de problèmes possibles avec l'interaction du terminal.

exit 0

Le script ci-dessus pourrait avoir été implémenté aussi efficacement avec ex, plutôt que vi. Les documents en ligne contenant une liste de commandes ex sont assez courants pour disposer de leur propre catégorie, connue sous le nom de scripts ex.

#!/bin/bash
#  Remplace toutes les instances de "Smith" avec "Jones"
#+ dans les fichiers avec extension ".txt".

ORIGINAL=Smith
REMPLACEMENT=Jones

for mot in $(fgrep -l $ORIGINAL *.txt)
do
  # -------------------------------------
  ex $mot <<EOF
  :%s/$ORIGINAL/$REMPLACEMENT/g
  :wq
EOF
  # :%s est la commande de substitution d'"ex".
  # :wq est un raccourci pour deux commandes : sauvegarde puis quitte.
  # -------------------------------------
done

Les « scripts cat » sont analogues aux scripts ex.

Exemple 18.3. Message multi-lignes en utilisant cat

#!/bin/bash

#  'echo' est bien pour afficher des messages sur une seule ligne
#+ mais il est parfois problématique pour des blocs de message.
#  Un document en ligne style 'cat' permet de surpasser cette limitation.

cat <<Fin-du-message
-------------------------------------
Ceci est la ligne 1 du message.
Ceci est la ligne 2 du message.
Ceci est la ligne 3 du message.
Ceci est la ligne 4 du message.
Ceci est la dernière ligne du message.
-------------------------------------
Fin-du-message

#  le remplacement de la ligne 7, ci-dessus, par
#+   cat > $NouveauFichier <<Fin-du-message
#+       ^^^^^^^^^^
#+ écrit la sortie vers le fichier $NouveauFichier, au lieu de stdout.

exit 0


#--------------------------------------------
# Le code ci-dessous est désactivé à cause du "exit 0" ci-dessus.

# S.C. indique que ce qui suit fonctionne aussi.
echo "-------------------------------------
Ceci est la ligne 1 du message.
Ceci est la ligne 2 du message.
Ceci est la ligne 3 du message.
Ceci est la ligne 4 du message.
Ceci est la dernière ligne du message.
-------------------------------------"
#  Néanmoins, le texte ne pourrait  pas inclure les doubles guillemets sauf
#+ s'ils sont échappés.


L'option - marquant la chaîne de caractères de limite d'un document en ligne (<<-ChaineLimite) supprime les tabulations du début (mais pas les espaces) lors de la sortie. Ceci est utile pour réaliser un script plus lisible.

Exemple 18.4. Message multi-lignes, aves les tabulations supprimées

#!/bin/bash
# Identique à l'exemple précédent, mais...

#  L'option - pour un document en ligne <<-
#  supprime les tabulations du début dans le corps du document,
#+ mais *pas* les espaces.

cat &lt;&lt;-FINDUMESSAGE
        Ceci est la ligne 1 du message.
        Ceci est la ligne 2 du message.
        Ceci est la ligne 3 du message.
        Ceci est la ligne 4 du message.
        Ceci est la dernière ligne du message.
FINDUMESSAGE
# La sortie du script sera poussée vers la gauche.
# Chaque tabulation de chaque ligne ne s'affichera pas.

# Les cinq lignes du "message" sont préfacées par une tabulation, et non des espaces,
# Les espaces ne sont pas affectés par <<-  .

# Notez que cette option n'a aucun effet sur les tabulations *intégrées*.


exit 0

Un document en ligne supporte la substitution de paramètres et de commandes. Il est donc possible de passer différents paramètres dans le corps du document en ligne, en changeant la sortie de façon appropriée.

Exemple 18.5. Document en ligne avec une substitution de paramètre

#!/bin/bash
# Autre document en ligne 'cat' utilisant la substitution de paramètres.

# Essayez-le sans arguments,     ./scriptname
# Essayez-le avec un argument,   ./scriptname Mortimer
# Essayez-le avec deux arguments entre guillemets,
#                                ./scriptname "Mortimer Jones"

CMDLINEPARAM=1     # Attendez au moins un paramètre en ligne de commande.

if [ $# -ge $CMDLINEPARAM ]
then
  NOM=$1          #  Si plus d'un paramètre en ligne de commande, prendre
                  #+ seulement le premier.
else
  NOM="John Doe"  # Par défaut, s'il n'y a pas de paramètres.
fi  

INTERLOCUTEUR="l'auteur de ce joli script"
  

cat <<FinDuMessage

Salut, $NOM.
Bienvenue à toi, $NOM, de la part de $INTERLOCUTEUR.

# Ce commentaire s'affiche dans la sortie (pourquoi ?).

FinDuMessage

# Notez que les lignes blanches s'affichent. Ainsi que le commentaire.

exit 0

Voici un script utile contenant un document intégré avec une substitution de paramètres.

Exemple 18.6. Télécharger un ensemble de fichiers dans le répertoire de récupération Sunsite

#!/bin/bash
# upload.sh

#  Téléchargement de fichiers par paires (Fichier.lsm, Fichier.tar.gz)
#+ pour le répertoire entrant de Sunsite (metalab.unc.edu).
#  Fichier.tar.gz est l'archive tar elle-même.
#  Fichier.lsm est le fichier de description.
#  Sunsite requiert le fichier "lsm", sinon cela retournera les contributions.

E_ERREURSARGS=65

if [ -z "$1" ]
then
  echo "Usage: `basename $0` fichier_à_télécharger"
  exit $E_ERREURSARGS
fi  


NomFichier=`basename $1`              # Supprime le chemin du nom du fichier.

Serveur="ibiblio.org"
Repertoire="/incoming/Linux"
#  Ils n'ont pas besoin d'être codés en dur dans le script,
#+ mais peuvent être changés avec un argument en ligne de commande.

MotDePasse="votre.adresse.courriel"   # A changer suivant vos besoins.

ftp -n $Serveur <<Fin-De-Session
# L'option -n désactive la connexion automatique

user anonymous "$MotDePasse"
binary
bell                # Sonne après chaque transfert de fichiers.
cd $Repertoire
put "$NomFichier.lsm"
put "$NomFichier.tar.gz"
bye
Fin-De-Session

exit 0

Mettre entre guillemets, ou échapper la « chaîne de caractères de limite » au début du document intgr, désactive la substitution de paramètres en son corps.

Exemple 18.7. Substitution de paramètres désactivée

#!/bin/bash
#  Un document en ligne 'cat', mais avec la substitution de paramètres
#+ désactivée.

NOM="John Doe"
INTERLOCUTEUR="l'auteur de ce joli script"

cat <<'FinDuMessage'

Salut, $NOM.
Bienvenue à toi, $NOM, de la part de $INTERLOCUTEUR.

FinDuMessage

#  Remplacement de la ligne 7, ci-dessus, avec
#+   cat > $Nouveaufichier <<Fin-du-message
#+       ^^^^^^^^^^
#+ écrit la sortie dans le fichier $Nouveaufichier, plutôt que sur stdout.

#  Pas de substitution de paramètres lorsque la chaîne de fin est entre
#+ guillemets ou échappée.
#  L'une des deux commandes ci-dessous à l'entête du document en ligne aura le
#+ le même effet.
#  cat <<"FinDuMessage"
#  cat <<\FinDuMessage

exit 0

Désactiver la substitution de paramètres permet d'afficher le texte littéral. Générer des scripts, ou même du code, en est une des utilités principales.

Exemple 18.8. Un script générant un autre script

#!/bin/bash
# generate-script.sh
# Basé sur une idée d'Albert Reiner.

FICHIER_SORTIE=genere.sh         # Nom du fichier à générer.


# -----------------------------------------------------------
# 'Document en ligne contenant le corps du script généré.
(
cat <<'EOF'
#!/bin/bash

echo "Ceci est un script shell généré"
#  Notez que, comme nous sommes dans un sous-shell,
#+ nous ne pouvons pas accéder aux variables du script "externe".
#  Prouvez-le...

echo "Le fichier généré aura pour nom : $FICHIER_SORTIE"
#  La ligne ci-dessus ne fonctionnera pas comme on pourrait s'y attendre
#+ parce que l'expansion des paramètres a été désactivée.
#  A la place, le résultat est une sortie littérale.

a=7
b=3

let "c = $a * $b"
echo "c = $c"

exit 0
EOF
) > $FICHIER_SORTIE
# -----------------------------------------------------------

#  Mettre entre guillemets la chaîne limite empêche l'expansion de la variable
#+ à l'intérieur du corps du document en ligne ci-dessus.
#  Ceci permet de sortir des chaînes littérales dans le fichier de sortie.

if [ -f "$FICHIER_SORTIE" ]
then
  chmod 755 $FICHIER_SORTIE
  # Rend le fichier généré exécutable.
else
        echo "Problème lors de la création du fichier: \"$FICHIER_SORTIE\""
fi

#  Cette méthode est aussi utilisée pour générer des programmes C, Perl, Python,
#+ Makefiles et d'autres.

exit 0

Il est possible d'initialiser une variable à partir de la sortie d'un document en ligne. En fait, il s'agit d'une forme dévié de substitution de commandes.

variable=$(cat <<SETVAR
Cette variable
est sur plusieurs lignes.
SETVAR)

echo "$variable"

Un document en ligne peut donner une entrée à une fonction du même script.

Exemple 18.9. Documents en ligne et fonctions

#!/bin/bash
# here-function.sh

ObtientDonneesPersonnelles ()
{
  read prenom
  read nom
  read adresse
  read ville
  read etat
  read codepostal
} # Ceci ressemble vraiment à une fonction interactive, mais...


# Apporter l'entrée à la fonction ci-dessus.
ObtientDonneesPersonnelles <<ENREG001
Bozo
Bozeman
2726 Nondescript Dr.
Baltimore
MD
21226
RECORD001


echo
echo "$prenom $nom"
echo "$adresse"
echo "$ville, $etat $codepostal"
echo

exit 0

Il est possible d'utiliser : comme commande inactive acceptant une sortie d'un document en ligne. Cela crée un document en ligne « anonyme ».

Exemple 18.10. Document en ligne « anonyme »

#!/bin/bash

: <<TESTVARIABLES
${HOSTNAME?}${USER?}${MAIL?}  #  Affiche un message d'erreur
                              #+ si une des variables n'est pas configurée.
TESTVARIABLES

exit 0

[Astuce]

Astuce

Une variante de la technique ci-dessus permet de « supprimer les commentaires » de blocs de code.

Exemple 18.11. Décommenter un bloc de code

#!/bin/bash
# commentblock.sh

: &lt;&lt;BLOC_COMMENTAIRE
echo "Cette ligne n'est pas un echo."
C'est une ligne de commentaire sans le préfixe "#".
Ceci est une autre ligne sans le préfixe "#".

&*@!!++=
La ligne ci-dessus ne causera aucun message d'erreur,
Parce que l'interpréteur Bash l'ignorera.
BLOC_COMMENTAIRE

echo "La valeur de sortie du \"BLOC_COMMENTAIRE\" ci-dessus est $?."   # 0
# Pas d'erreur.
echo


#  La technique ici-dessus est aussi utile pour mettre en commentaire un bloc
#+ de code fonctionnel pour des raisons de déboguage.
#  Ceci permet d'éviter de placer un "#" au début de chaque ligne, et d'avoir
#+ ensuite à les supprimer.

echo "Juste avant le bloc de code commenté."
#  Les lignes de code entre les lignes de soulignés doubles ne s'exécuteront pas.
#  ==============================================================================
: &lt;&lt;DEBUGXXX
for fichier in *
do
  cat "$fichier"
done
DEBUGXXX
#  ==============================================================================
echo "Juste après le bloc de code commenté."

exit 0



######################################################################
#  Notez, néanmoins, que si une variable entre crochets est contenu
#+ dans un bloc de code commenté, cela pourrait poser problème.
#  Par exemple :


#/!/bin/bash

  : &lt;&lt;BLOC_COMMENTAIRE
  echo "Cette ligne ne s'affichera pas."
  &*@!!++=
  ${foo_bar_bazz?}
  $(rm -rf /tmp/foobar/)
  $(touch mon_repertoire_de_construction/cups/Makefile)
BLOC_COMMENTAIRE


$ sh commented-bad.sh
commented-bad.sh: line 3: foo_bar_bazz: parameter null or not set

#  Le remède pour ceci est de placer le BLOC_COMMENTAIRE
#+ entre guillemets simples à la ligne 48, ci-dessus.

  : &lt;&lt;'COMMENTBLOCK'

# Merci de nous l'avoir indiqué, Kurt Pfeifle.

[Astuce]

Astuce

Encore une autre variante de cette sympathique astuce rendant possibles les scripts « auto-documentés ».

Exemple 18.12. Un script auto-documenté

#!/bin/bash
# self-document.sh : script auto-documenté
# Modification de "colm.sh".

DEMANDE_DOC=70

if [ "$1" = "-h"  -o "$1" = "--help" ]     # Demande de l'aide.
then
  echo; echo "Usage: $0 [nom-repertoire]"; echo
  sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATIONXX$/p' "$0" |
  sed -e '/DOCUMENTATIONXX$/d'; exit $DEMANDE_DOC; fi


: <<DOCUMENTATIONXX
Liste les statistiques d'un répertoire spécifié dans un format de tabulations.
------------------------------------------------------------------------------
Le paramètre en ligne de commande donne le répertoire à lister.
Si aucun répertoire n'est spécifié ou que le répertoire spécifié ne peut être
lu, alors liste le répertoire courant.

DOCUMENTATIONXX

if [ -z "$1" -o ! -r "$1" ]
then
  repertoire=.
else
  repertoire="$1"
fi  

echo "Liste de "$repertoire":"; echo
(printf "PERMISSIONS LIENS PROP GROUPE TAILLE MOIS  JOUR HH:MM NOM-PROG\n" \
; ls -l "$repertoire" | sed 1d) | column -t

exit 0

Utiliser un script cat est une autre façon d'accomplir ceci.

REQUETE_DOC=70

if [ "$1" = "-h"  -o "$1" = "--help" ]     # Demande d'aide.
then                                       # Utilise un "script cat"...
  cat <<DOCUMENTATIONXX
Liste les statistiques d'un répertoire spécifié au format de tableau.
---------------------------------------------------------------------
Le paramètre en ligne de commande indique le répertoire à lister.
Si aucun répertoire n'est spécifié ou si le répertoire spécifié ne
peut pas être lu, alors liste le répertoire courant.

DOCUMENTATIONXX
exit $REQUETE_DOC
fi

Voir aussi l'Exemple A.29, « Identification d'un spammer » pour un excellent exemple de script auto-documenté.

[Note]

Note

Les documents en ligne créent des fichiers temporaires mais ces fichiers sont supprimés après avoir été ouverts et ne sont plus accessibles par aucun autre processus.

bash$ bash -c 'lsof -a -p $$ -d0' << EOF
> EOF
lsof    1213 bozo    0r   REG    3,5    0 30386 /tmp/t1213-0-sh (deleted)
              
[Attention]

Attention

Quelques utilitaires ne fonctionneront pas à l'intérieur d'un document en ligne.

[Avertissement]

Avertissement

La chaîne de limite fermante, à la ligne finale d'un document en ligne, doit commencer à la position du tout premier caractère. Il ne peut pas y avoir d'espace blanc devant. Les espaces de fin après la chaîne de limite cause un comportement inattendu. L'espace blanc empêche la chaîne de limite d'être reconnu.

#!/bin/bash

echo "----------------------------------------------------------------------"

cat <<ChaineLimite
echo "Ligne 1 du document en ligne."
echo "Ligne 2 du document en ligne."
echo "Ligne finale du document en ligne."
     ChaineLimite
#^^^^Chaîne de limite indentée. Erreur! Ce script ne va pas se comporter comme
#+ on s'y attend.

echo "----------------------------------------------------------------------"

#  Ces commentaires sont en dehors du document en ligne et ne devraient pas
#+ s'afficher.

echo "En dehors du document en ligne."

exit 0

echo "Cette ligne s'affiche encore moins."  # Suit une commande 'exit'.

Pour ces tâches trop complexes pour un « document en ligne », considérez l'utilisation du langage de scripts expect, qui est conçu spécifiquement pour alimenter l'entrée de programmes interactifs.

18.1. Chaînes en ligne

Une chaîne en ligne peut être considéré comme une forme minimale du document en ligne. Il consiste simplement en la chaîne COMMANDE <<<$MOT$MOT est étendu et est initialisé via l'entrée standard (stdin) de COMMANDE.

Comme exemple de base, considérez cette alternative à la construction echo-grep.

# Au lieu de :
if echo "$VAR" | grep -q txt   # if [[ $VAR = *txt* ]]
# etc.

# Try:
if grep -q "txt" <<< "$VAR"
then
   echo "$VAR contient la sous-chaîne \"txt\""
fi
# Merci pour la suggestion, Sebastian Kaminski.

Ou en combinaison avec read :

Chaine="Ceci est une chaîne de mots."

read -r -a Mots <<< "$Chaine"
#  L'option -a pour "lire" affecte les valeurs résultants
#+ aux membres d'un tableau.

echo "Le premier mot de Chaine est   :   ${Mots[0]}"   # Ceci
echo "Le deuxième mot de Chaine est  :   ${Mots[1]}"   # est
echo "Le troisième mot de Chaine est :   ${Mots[2]}"   # une
echo "Le quatrième mot de Chaine est :   ${Mots[3]}"   # chaîne
echo "Le cinquième mot de Chaine est :   ${Mots[4]}"   # de
echo "Le sixième mot de Chaine est   :   ${Mots[5]}"   # mots.
echo "Le septième mot de Chaine est  :   ${Mots[6]}"   # (null)
                                                 # On dépasse la fin de $Chaine.

# Merci à Francisco Lobo pour sa suggestion.

Exemple 18.13. Ajouter une ligne au début d'un fichier

#!/bin/bash
# prepend.sh: Ajoute du texte au début d'un fichier.
#
#  Exemple contribué par Kenny Stauffer,
#+ et légèrement modifié par l'auteur du document.


E_FICHIERINEXISTANT=65

read -p "Fichier : " fichier   # argument -p pour que 'read' affiche l'invite.
if [ ! -e "$fichier" ]
then   # Quitte si le fichier n'existe pas.
  echo "Fichier $fichier introuvable."
  exit $E_FICHIERINEXISTANT
fi

read -p "Titre : " titre
cat - $fichier <<<$titre > $fichier.nouveau

echo "Le fichier modifié est $fichier.nouveau"

exit 0

# provenant de 'man bash'
# Chaînes en ligne
#       Une variante des documents en ligne, le format est :
# 
#               <<<mot
# 
#       Le mot est étendu et fourni à la commande sur son entrée standard.

Exemple 18.14. Analyser une boîte mail

#!/bin/bash
#  Script par Francisco Lobo,
#+ et légèrement modifié par l'auteur du guide ABS.
#  Utilisé avec sa permission dans le guide ABS (Merci !).

# Ce script ne fonctionnera pas avec les versions de Bash antérieures à la 3.0.


E_ARGS_MANQUANTS=67
if [ -z "$1" ]
then
  echo "Usage: $0 fichier-mailbox"
  exit $E_ARGS_MANQUANTS
fi

mbox_grep()  # Analyse le fichier mailbox.
{
    declare -i corps=0 correspondance=0
    declare -a date emetteur
    declare mail entete valeur


    while IFS= read -r mail
#         ^^^^                 Réinitialise $IFS.
#  Sinon, "read" supprimera les espaces devant et derrière sa cible.

   do
       if [[ $mail =~ "^From " ]]   # correspondance du champ "From" dans le message.
       then
          (( corps  = 0 ))           # Variables ré-initialisées.
          (( correspondance = 0 ))
          unset date

       elif (( corps ))
       then
            (( correspondance ))
            # echo "$mail"
            #  Décommentez la ligne ci-dessus si vous voulez afficher
            #+ le corps entier du message.

       elif [[ $mail ]]; then
          IFS=: read -r entete valeur <<< "$mail"
          #                          ^^^  "chaîne intégrée"

          case "$entete" in
          [Ff][Rr][Oo][Mm] ) [[ $valeur =~ "$2" ]] && (( correspondance++ )) ;;
          # correspondance de la ligne "From".
          [Dd][Aa][Tt][Ee] ) read -r -a date <<< "$valeur" ;;
          #                                  ^^^
          # correspondance de la ligne "Date".
          [Rr][Ee][Cc][Ee][Ii][Vv][Ee][Dd] ) read -r -a sender <<< "$valeur" ;;
          #                                                    ^^^
          # correspondance de l'adresse IP (pourrait être f).
          esac

       else
          (( corps++ ))
          (( correspondance  )) &&
          echo "MESSAGE ${date:+of: ${date[*]} }"
       #    Tableau entier $date           ^
          echo "IP address of sender: ${sender[1]}"
       #    Second champ de la ligne "Received"^

       fi


    done < "$1" # Redirige le stdout du fichier dans une boucle.
}


mbox_grep "$1"  # Envoie le fichier mailbox.

exit $?

# Exercices :
# ----------
# 1) Cassez la seule fonction, ci-dessus, dans plusieurs fonctions.
# 2) Ajoutez des analyses supplémentaires dans le script, en vérifiant plusieurs mots-clés.



$ mailbox_grep.sh scam_mail
--> MESSAGE of Thu, 5 Jan 2006 08:00:56 -0500 (EST) 
--> IP address of sender: 196.3.62.4

Exercice : trouver d'autres utilisations des chaînes en ligne.