19. Documents intégrés

Ici et maintenant, les gars.

-- Aldous Huxley, Islande

Un document intégré est un bloc de code à usage spécial. Il utilise une forme de redirection d'entrée/sortie pour passer une liste de commandes à un programme ou à une commande interactifs, 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 << précède la chaîne de caractères de limite. Ceci a pour effet de rediriger la sortie d'un bloc de commandes vers le stdin d'un programme ou d'une commande. Un peu comme programme-interactif <fichier-commandes, où fichier-commandes contient

commande n°1 commande n°2
      ...

Un équivalent du document intégré :

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 intégrés peuvent parfois être utilisés correctement avec des utilitaires et des commandes non interactifs, tels que wall.

Exemple 19.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 modèle de message dans un script
#+ est une solution rapide bien que sale

exit 0

Même d'aussi improbables candidats comme vi tendent eux-même aux documents intégrés.

Exemple 19.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 intégrés 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 19.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 intégré (<<-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 19.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 <<-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 intégré supporte la substitution de paramètres et de commandes. Il est donc possible de passer différents paramètres dans le corps du document intégré, en changeant la sortie de façon appropriée.

Exemple 19.5. Document intégré avec paramètres substituables

#!/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 substitution de paramètres.

Exemple 19.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 intégré, désactive la substitution de paramètres en son corps. La raison en est que le fait de citer ou d'échapper produit réellement l'échapper des caractères spéciaux $, ` et \, qui sont dès lors interprétés littéralement. (Merci à Allen Halsey pour cette indication.)

Exemple 19.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 19.8. Un script générant un autre script

#!/bin/bash
# generate-script.sh
# Based on an idea by Albert Reiner.

OUTFILE=generated.sh         # Name of the file to generate.


# -----------------------------------------------------------
# 'Here document containing the body of the generated script.
(
cat <<'EOF'
#!/bin/bash

echo "This is a generated shell script."
#  Note that since we are inside a subshell,
#+ we can't access variables in the "outside" script.

echo "Generated file will be named: $OUTFILE"
#  Above line will not work as normally expected
#+ because parameter expansion has been disabled.
#  Instead, the result is literal output.

a=7
b=3

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

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

#  Quoting the 'limit string' prevents variable expansion
#+ within the body of the above 'here document.'
#  This permits outputting literal strings in the output file.

if [ -f "$OUTFILE" ]
then
  chmod 755 $OUTFILE
  # Make the generated file executable.
else
  echo "Problem in creating file: \"$OUTFILE\""
fi

#  This method can also be used for generating
#+ C programs, Perl programs, Python programs, Makefiles,
#+ and the like.

exit 0

Il est possible d'initialiser une variable à partir de la sortie d'un document intégré. 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 intégré peut donner une entrée à une fonction du même script.

Exemple 19.9. Documents intégrés 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 intégré. Cela crée un document intégré « anonyme ».

Exemple 19.10. document intégré « anonyme »

#!/bin/bash

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

exit $?

[Astuce]

Astuce

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

Exemple 19.11. Décommenter un bloc de code

#!/bin/bash
# commentblock.sh

: <<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.
#  ==============================================================================
: <<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

  : <<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.

  : <<'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 19.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.28, « Identification d'un spammer » , Exemple A.40, « Pétales autour d'une rose », Exemple A.41, « Quacky : un jeu de mots de type Perquackey » et Exemple A.42, « Nim » pour d'autres exemples de scripts auto-documenté.

[Note]

Note

Les documents intégrés 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 dans un document intégré.

[Avertissement]

Avertissement

La chaîne de limite fermante, à la ligne finale d'un document intégré, 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 empêche la chaîne limite d'être reconnue. [99]

#!/bin/bash

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

cat <<ChaineLimite
echo "Ligne 1 du document intégré."
echo "Ligne 2 du document intégré."
echo "Ligne finale du document intégré."
     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 intégré et ne devraient pas
#+ s'afficher.

echo "En dehors du document intégré."

exit 0

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

Attention

Très astucieusement, certaines personnes utilisent un simple ! comme chaîne limite. Toutefois ce n'est pas forcément une bonne idée.

# Ceci fonctionne.
cat <<!
Bonjour !
! Trois points d'exclamation supplémentaires !!!
!


# Mais...
cat <<!
Bonjour !
Ensuite un unique point d'exclamation !
!
!
# Crashes with an error message.


# Alors que les lignes suivantes vont fonctionner.
cat <<EOF
Bonjour !
Ensuite un unique point d'exclamation !
!
EOF
# Il est plus sûr d'utiliser une chaîne limite multi-caractères.

Pour ces tâches, trop complexes pour un document intégré, pensez à utiliser le langage de script expect, conçu spécialement pour alimenter l'entrée de programmes interactifs.

19.1. Chaînes en ligne


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

Comme exemple de base, voyez 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 à Sebastian Kaminski pour la suggestion.

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.

Bien sûr, il est possible d'envoyer la sortie d'une chaîne en ligne vers le stdin d'une boucle.

 # Comme le fait remarquer Seamus...

VarTableau=( element0 element1 element2 {A..D} )

while read element ; do
  echo "$element" 1>&2
done <<< $(echo ${VarTableau[*]})

# element0 element1 element2 A B C D

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

#!/bin/bash
# prepend.sh: Add text at beginning of file.
#
#  Example contributed by Kenny Stauffer,
#+ and slightly modified by document author.


E_NOSUCHFILE=85

read -p "File: " file   # -p arg to 'read' displays prompt.
if [ ! -e "$file" ]
then   # Bail out if no such file.
  echo "File $file not found."
  exit $E_NOSUCHFILE
fi

read -p "Title: " title
cat - $file <<<$title > $file.new

echo "Modified file is $file.new"

exit  # Ends script execution.

  from 'man bash':
  Here Strings
        A variant of here documents, the format is:
  
                <<<word
  
        The word is expanded and supplied to the command on its standard input.


  Of course, the following also works:
   sed -e '1i\
   Title: ' $file

Exemple 19.14. Analyser une boîte mail

#!/bin/bash
#  Script by Francisco Lobo,
#+ and slightly modified and commented by ABS Guide author.
#  Used in ABS Guide with permission. (Thank you!)

# This script will not run under Bash versions < 3.0.


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

mbox_grep()  # Parse mailbox file.
{
    declare -i body=0 match=0
    declare -a date sender
    declare mail header value


    while IFS= read -r mail
#         ^^^^                 Reset $IFS.
#  Otherwise "read" will strip leading & trailing space from its input.

   do
       if [[ $mail =~ "^From " ]]   # Match "From" field in message.
       then
          (( body  = 0 ))           # "Zero out" variables.
          (( match = 0 ))
          unset date

       elif (( body ))
       then
            (( match ))
            # echo "$mail"
            # Uncomment above line if you want entire body of message to display.

       elif [[ $mail ]]; then
          IFS=: read -r header value <<< "$mail"
          #                          ^^^  "here string"

          case "$header" in
          [Ff][Rr][Oo][Mm] ) [[ $value =~ "$2" ]] && (( match++ )) ;;
          # Match "From" line.
          [Dd][Aa][Tt][Ee] ) read -r -a date <<< "$value" ;;
          #                                  ^^^
          # Match "Date" line.
          [Rr][Ee][Cc][Ee][Ii][Vv][Ee][Dd] ) read -r -a sender <<< "$value" ;;
          #                                                    ^^^
          # Match IP Address (may be spoofed).
          esac

       else
          (( body++ ))
          (( match  )) &&
          echo "MESSAGE ${date:+of: ${date[*]} }"
       #    Entire $date array             ^
          echo "IP address of sender: ${sender[1]}"
       #    Second field of "Received" line    ^

       fi


    done < "$1" # Redirect stdout of file into loop.
}


mbox_grep "$1"  # Send mailbox file to function.

exit $?

# Exercises:
# ---------
# 1) Break the single function, above, into multiple functions,
#+   for the sake of readability.
# 2) Add additional parsing to the script, checking for various keywords.



$ 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 pour les chaînes intégrées. Par exemple, alimenter dc.



[99] Sauf, comme le remarque Dennis Benzinger, si on utilise <<- pour supprimer les tabulations.