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 $?
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.
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é.
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)
Quelques utilitaires ne fonctionneront pas dans un document intégré.
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'.
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.
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.