Capitolo 18. Here document

 

Here and now, boys.

 Aldous Huxley, "Island"

Un here document è un blocco di codice con una funzione specifica. Utilizza una particolare forma di redirezione I/O per fornire un elenco di comandi a un programma o a un comando interattivi, come ftp, cat o l'editor di testo ex.

COMANDO <<InputArrivaDaQui(HERE)
 ...
InputArrivaDaQui(HERE)

Una stringa limite delimita (incornicia) l'elenco dei comandi. Il simbolo speciale << indica la stringa limite. Questo ha come effetto la redirezione dell'output di un file nello stdin del programma o del comando. È simile a programma-interattivo < file-comandi, dove file-comandi contiene

comando nr.1
comando nr.2
...

L'alternativa rappresentata da un here document è la seguente:

#!/bin/bash
programma-interattivo <<StringaLimite
comando nr.1
comando nr.2
...
StringaLimite

Si scelga, per la stringa limite, un nome abbastanza insolito in modo che non ci sia la possibilità che lo stesso nome compaia accidentalmente nell'elenco dei comandi presenti nel here document e causare confusione.

Si noti che gli here document possono, talvolta, essere usati efficacemente anche con utility e comandi non interattivi come, ad esempio, wall.

Esempio 18-1. broadcast: invia un messaggio a tutti gli utenti connessi

#!/bin/bash

wall <<zzz23FineMessaggiozzz23
Inviate per e-mail gli ordini per la pizza di mezzogiorno 
all'amministratore di  sistema.
    (Aggiungete un euro extra se la volete con acciughe o funghi.)
# Il testo aggiuntivo del messaggio va inserito qui.
# Nota: 'wall' visualizza le righe di commento.
zzz23FIneMessaggiozzz23

# Si sarebbe potuto fare in modo più efficiente con
#         wall <file-messaggio
#  Comunque, inesrire un messaggio campione in uno script è una soluzione 
#+ rapida-e-grezza definitiva.

exit 0

Persino candidati improbabili come vi si prestano ad essere impiegati negli here document.

Esempio 18-2. File di prova: crea un file di prova di due righe

#!/bin/bash

# Uso non interattivo di 'vi' per scrivere un file.
# Simula 'sed'.

E_ERR_ARG=65

if [ -z "$1" ]
then
  echo "Utilizzo: `basename $0` nomefile"
  exit $E_ERR_ARG
fi

FILE=$1

# Inserisce 2 righe nel file, quindi lo salva.
#--------Inizio here document----------------#
vi $FILE <<x23StringaLimitex23
i
Questa è la riga 1 del file d'esempio.
Questa è la riga 2 del file d'esempio.
^[
ZZ
x23StringaLimitex23
#----------Fine here document----------------#

#  Notate che i caratteri ^[ corrispondono alla
#+ digitazione di Control-V <Esc>.

#  Bram Moolenaar fa rilevare che questo potrebbe non funzionare con 'vim',
#+ a causa di possibili problemi di interazione con il terminale.

exit 0

Lo script precedente si potrebbe, semplicemente ed efficacemente, implementare con ex, invece che con vi. Gli here document che contengono una lista di comandi ex sono abbastanza comuni e formano una specifica categoria a parte, conosciuta come ex script.

#!/bin/bash
#  Sostituisce tutte le occorrenze d "Smith" con "Jones"
#+ nei file i cui nomi abbiano il suffisso ".txt". 
										
ORIGINALE=Smith
SOSTITUTO=Jones
										
for parola in $(fgrep -l $ORIGINALE *.txt)
do
  # ---------------------------------------------
  ex $parola <<EOF
  :%s/$ORIGINALE/$SOSTITUTO/g
  :wq
EOF
  # :%s è la sostituzione di comando di "ex".
  # :wq significa write-and-quit (scrivi ed esci).
  # ----------------------------------------------
done

Analoghi agli "script ex" sono gli script cat.

Esempio 18-3. Messaggio di più righe usando cat

#!/bin/bash

# 'echo' è ottimo per visualizzare messaggi di una sola riga,
#+ ma diventa problematico per messaggi più lunghi.
#  Un here document 'cat' supera questa limitazione.

cat <<Fine-messaggio
-------------------------------------
Questa è la riga 1 del messaggio.
Questa è la riga 2 del messaggio.
Questa è la riga 3 del messaggio.
Questa è la riga 4 del messaggio.
Questa è l'ultima riga del messaggio.
-------------------------------------
Fine-messaggio

#  Sostituendo la precedente riga 7 con
#+   cat > $Nuovofile <<Fine-messaggio
#+       ^^^^^^^^^^^^
#+ l'output viene scritto nel file $Nuovofile invece che allo stdout.

exit 0


#--------------------------------------------
# Il codice che segue non viene eseguito per l'"exit 0" precedente.

# S.C. sottolinea che anche la forma seguente funziona.
echo "-------------------------------------
Questa è la riga 1 del messaggio.
Questa è la riga 2 del messaggio.
Questa è la riga 3 del messaggio.
Questa è la riga 4 del messaggio.
Questa è l'ultima riga del messaggio.
--------------------------------------------"
#  Tuttavia, il testo non dovrebbe contenere doppi apici privi
#+ del carattere di escape.

L'opzione - alla stringa limite del here document (<<-StringaLimite) elimina, nell'output, i caratteri di tabulazione iniziali (ma non gli spazi). Può essere utile per rendere lo script più leggibile.

Esempio 18-4. Messaggio di più righe con cancellazione dei caratteri di tabulazione

#!/bin/bash
# Uguale all'esempio precedente, ma...

#  L'opzione - al here document <<-
#+ sopprime le tabulazioni iniziali nel corpo del documento, 
#+ ma *non* gli spazi.

cat <<-FINEMESSAGGIO
        Questa è la riga 1 del messaggio.
        Questa è la riga 2 del messaggio.
        Questa è la riga 3 del messaggio.
        Questa è la riga 4 del messaggio.
        Questa è l'ultima riga del messaggio.
FINEMESSAGGIO
# L'output dello script viene spostato a sinistra.
# Le tabulazioni iniziali di ogni riga non vengono mostrate.

#  Le precedenti 5 righe del "messaggio" sono precedute da
#+ tabulazioni, non da spazi.
#  Gli spazi non sono interessati da  <<-.

#  Notate che quest'opzione non ha alcun effetto sulle tabulazioni *incorporate*

exit 0

Un here document supporta la sostituzione di comando e di parametro. È quindi possibile passare diversi parametri al corpo del here document e modificare, conseguentemente, il suo output.

Esempio 18-5. Here document con sostituzione di parametro

#!/bin/bash
# Un altro here document 'cat' che usa la sostituzione di parametro.

# Provatelo senza nessun parametro da riga di comando,   ./nomescript
# Provatelo con un parametro da riga di comando,   ./nomescript Mortimer
# Provatelo con un parametro di due parole racchiuse tra doppi apici,
#                           ./nomescript "Mortimer Jones"

RIGACMDPARAM=1     #  Si aspetta almeno un parametro da riga di comando.

if [ $# -ge $RIGACMDPARAM ]
then
  NOME=$1          #  Se vi è più di un parametro,
                   #+ tiene conto solo del primo.
else
  NOME="John Doe"  #  È il nome predefinito, se non si passa alcun parametro.
fi

RISPONDENTE="l'autore di questo bello script"
  

cat <<Finemessaggio

Ciao, sono $NOME.
Salute a te $NOME, $RISPONDENTE.

# Questo commento viene visualizzato nell'output (perché?).

Finemessaggio

# Notate che vengono visualizzate nell'output anche le righe vuote.
# Così si fa un "commento".

exit 0

Quello che segue è un utile script contenente un here document con sostituzione di parametro.

Esempio 18-6. Caricare due file nella directory incoming di Sunsite

#!/bin/bash
# upload.sh

#  Carica due file (Nomefile.lsm, Nomefile.tar.gz)
#+ nella directory incoming di Sunsite/UNC (ibiblio.org).
#  Nomefile.tar.gz è l'archivio vero e proprio.
#  Nomefile.lsm è il file di descrizione.
#  Sunsite richiede il file "lsm", altrimenti l'upload viene rifiutato.


E_ERR_ARG=65

if [ -z "$1" ]
then
  echo "Utilizzo: `basename $0` nomefile-da-caricare"
  exit $E_ERR_ARG
fi


Nomefile=`basename $1`              # Toglie il percorso dal nome del file.

Server="ibiblio.org"
Directory="/incoming/Linux"
#  Questi dati non dovrebbero essere codificati nello script,
#+ ma si dovrebbe avere la possibilità di cambiarli fornendoli come 
#+ argomenti da riga di comando.

Password="vostro.indirizzo.e-mail"  # Sostituitelo con quello appropriato.

ftp -n $Server <<Fine-Sessione
# l'opzione -n disabilita l'auto-logon

user anonymous "$Password"
binary
bell                                #  Emette un 'segnale acustico' dopo ogni
                                    #+ trasferimento di file
cd $Directory
put "$Nomefile.lsm"
put "$Nomefile.tar.gz"
bye
Fine-Sessione

exit 0

L'uso del quoting o dell'escaping sulla "stringa limite" del here document disabilita la sostituzione di parametro al suo interno.

Esempio 18-7. Sostituzione di parametro disabilitata

#!/bin/bash
#  Un here document 'cat' con la sostituzione di parametro disabilitata.

NOME="John Doe"
RISPONDENTE="L'autore dello script"

cat <<'Finemessaggio'

Ciao, sono $NOME.
Salute a te $NOME, $RISPONDENTE.

Finemessaggio

#  Non c'è sostituzione di parametro quando si usa il quoting o l'escaping
#+ sulla "stringa limite".
#  Le seguenti notazioni avrebbero avuto, entrambe, lo stesso effetto.
#  cat <"Finemessaggio"
#  cat <\Finemessaggio

exit 0

Disabilitare la sostituzione di parametro permette la produzione di un testo letterale. Questo può essere sfruttato per generare degli script o perfino il codice di un programma.

Esempio 18-8. Uno script che genera un altro script

#!/bin/bash
# generate-script.sh
# Basato su un'idea di Albert Reiner.

OUTFILE=generato.sh          # Nome del file da generare.


# -----------------------------------------------------------
# 'Here document contenente il corpo dello script generato.
(
cat <<'EOF'
#!/bin/bash

echo "Questo è uno script di shell creato da un altro script."
#  È da notare che, dal momento che ci troviamo all'interno di una subshell,
#+ non possiamo accedere alle variabili dello script "esterno".
#
echo "Il file prodotto si chiamerà: $OUTFILE"
#  La riga precedente non funziona come ci si potrebbe normalmente attendere
#+ perché è stata disabilitata l'espansione di parametro.
#  Come risultato avremo, invece, una visualizzazione letterale.

a=7
b=3

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

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

#  Il quoting della 'stringa limite' evita l'espansione di variabile
#+ all'interno del corpo del precedente 'here document.'
#  Questo consente di conservare le stringhe letterali nel file prodotto.

if [ -f "$OUTFILE" ]
then
  chmod 755 $OUTFILE
  # Rende eseguibile il file generato.
else
  echo "Problema nella creazione del file: \"$OUTFILE\""
fi

#  Questo metodo può anche essere usato per generare
#+ programmi C, Perl, Python, Makefiles e simili.

exit 0

È possibile impostare una variabile all'output di un here document.

variabile=$(cat <<IMPVAR
Questa variabile
si estende su più righe.
IMPVAR)
										
echo "$variabile"

Un here document può fornire l'input ad una funzione del medesimo script.

Esempio 18-9. Here document e funzioni

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

AcquisisceDatiPersonali ()
{
  read nome
  read cognome
  read indirizzo
  read città
  read cap
  read nazione
} # Può certamente apparire come una funzione interattiva, ma...


# Forniamo l'input alla precedente funzione.
AcquisisceDatiPersonali <<RECORD001
Ferdinando
Rossi
Via XX Settembre, 69
Milano
20100
ITALIA
RECORD001


echo
echo "$nome $cognome"
echo "$indirizzo"
echo "$città,  $cap, $nazione"
echo


exit 0

È possibile usare i : come comando fittizio per ottenere l'output di un here document. Si crea, così, un here document "anonimo".

Esempio 18-10. Here document "anonimo"

#!/bin/bash
										
: <<VERIFICAVARIABILI
${HOSTNAME?}${USER?}${MAIL?}  #  Visualizza un messaggio d'errore se una 
			      #+ delle variabili non è impostata.
VERIFICAVARIABILI
										
exit 0

Suggerimento

Una variazione della precedente tecnica consente di "commentare" blocchi di codice.

Esempio 18-11. Commentare un blocco di codice

#!/bin/bash
# commentblock.sh

: <<BLOCCOCOMMENTO
echo "Questa riga non viene visualizzata."
Questa è una riga di commento senza il carattere "#"
Questa è un'altra riga di commento senza il carattere "#"

&*@!!++=
La riga precedente non causa alcun messaggio d'errore, 
perché l'interprete Bash la ignora.
BLOCCOCOMMENTO

echo "Il valore di uscita del precedente \"BLOCCOCOMMENTO\" è $?."   # 0
# Non viene visualizzato alcun errore.


#  La tecnica appena mostrata diventa utile anche per commentare
#+ un blocco di codice a scopo di debugging.
#  Questo evita di dover mettere il "#" all'inizio di ogni riga,
#+ e quindi di dovere, più tardi, ricominciare da capo e cancellare tutti i "#"

: <<DEBUGXXX
for file in *
do
 cat "$file"
done
DEBUGXXX

exit 0

Suggerimento

Un'altra variazione di questo efficace espediente rende possibile l'"auto-documentazione" degli script.

Esempio 18-12. Uno script che si auto-documenta

#!/bin/bash
# self-document.sh: script autoesplicativo
# È una modifica di "colm.sh".

RICHIESTA_DOCUMENTAZIONE=70

if [ "$1" = "-h"  -o "$1" = "--help" ]     # Richiesta d'aiuto.
then
  echo; echo "Utilizzo: $0 [nome-directory]"; echo
  sed --silent -e '/DOCUMENTAZIONEXX$/,/^DOCUMENTAZIONEXX$/p' "$0" |
  sed -e '/DOCUMENTAZIONEXX$/d'; exit $RICHIESTA_DOCUMENTAZIONE; fi


: <<DOCUMENTAZIONEXX
Elenca le statistiche di una directory specificata in formato tabellare.
------------------------------------------------------------------------------
Il parametro da riga di comando specifica la directory  di cui si desiderano
le statistiche. Se non è specificata alcuna directory o quella indicata non
può essere letta, allora vengono visualizzate le statistiche della directory
di lavoro corrente.

DOCUMENTAZIONEXX

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

echo "Statistiche di "$directory":"; echo
(printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
; ls -l "$directory" | sed 1d) | column -t

exit 0

Un modo alternativo per ottenere lo stesso risultato è quello di usare uno script cat.

RICHIESTA_DOCUMENTAZIONE=70
										
if [ "$1" = "-h"  -o "$1" = "--help" ]     # Richiesta d'aiuto.
then                                       # Usa uno "script cat" . . .
  cat <<DOCUMENTAZIONEXX
Elenca le statistiche della directory specificata in formato tabellare.
---------------------------------------------------------------------------
Il parametro da riga di comando specifica la directory di cui si desiderano
le statistiche. Se non è specificata alcuna directory o quella indicata non
può essere letta, allora vengono visualizzate le statistiche della directory
di lavoro corrente.
										
DOCUMENTAZIONEXX
exit $RICHIESTA_DOCUMENTAZIONE
fi

Vedi anche Esempio A-28 come eccellente dimostrazione di script autoesplicativo.

Nota

Gli here document creano file temporanei che vengono cancellati subito dopo la loro apertura e non sono accessibili da nessun altro processo.

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

Attenzione

Alcune utility non funzionano se inserite in un here document.

Avvertimento

La stringa limite di chiusura, dopo l'ultima riga di un here document, deve iniziare esattamente dalla prima posizione della riga. Non deve esserci nessuno spazio iniziale. Allo stesso modo uno spazio posto dopo la stringa limite provoca comportamenti imprevisti. Lo spazio impedisce il riconoscimento della stringa limite.

#!/bin/bash
										
echo "------------------------------------------------------------------------"
							
cat <<StringaLimite
echo "Questa è la riga 1 del messaggio contenuto nel here document."
echo "Questa è la riga 2 del messaggio contenuto nel here document."
echo "Questa è la riga finale del messaggio contenuto nel here document."
    StringaLimite
#^^^^Stringa limite indentata. 
#    Errore! Lo script non si comporta come speravamo.
										
echo "------------------------------------------------------------------------"
										
#  Questi commenti si trovano esternamente al 'here document'
#+ e non vengono visualizzati.
										
echo "Fuori dal here document."
										
exit 0
										
echo "Questa riga sarebbe meglio evitarla."  # Viene dopo il comando 'exit'.

Per quei compiti che risultassero troppo complessi per un "here document", si prenda in considerazione l'impiego del linguaggio di scripting expect che è particolarmente adatto a fornire gli input ai programmi interattivi.

18.1. Here String

Una here string può essere considerata un here document ridotto ai minimi termini. È formata semplicemente da COMANDO <<<$PAROLA, dove $PAROLA viene espansa per diventare lo stdin di COMANDO.

Come semplice esempio, la si consideri una alternativa al costrutto echo-grep.

# Invece di:
if echo "$VAR" | grep -q txt   # if [[ $VAR = *txt* ]]
# ecc.
			
# Provate:
if grep -q "txt" <<< "$VAR"
then
   echo "$VAR contiene la sottostringa \"txt\""
fi
# Grazie a Sebastian Kaminski per il suggerimento.

O in combinazione con read:

Stringa="Questa è una stringa di parole."
										
read -r -a Parole <<< "$Stringa"
#  L'opzione -a di "read"
#+ assegna consecutivamente i valori risultanti agli elementi di un array.
										
echo "La prima parola in Stringa è:    ${Parole[0]}"   # Questa
echo "La second parola in Stringa è:   ${Parole[1]}"   # è
echo "La terza parola in Stringa è:    ${Parole[2]}"   # una
echo "La quarta parola in Stringa è:   ${Parole[3]}"   # stringa
echo "La quinta parola in Stringa è:   ${Parole[4]}"   # di
echo "La sesta parola in Stringa è:    ${Parole[5]}"   # parole.
echo "La settima parola in Stringa è:  ${Parole[6]}"   # (null)
							      #  Oltre la 
							      #+ dimensione di $Stringa.
										
# Grazie a Francisco Lobo per il suggerimento.

Esempio 18-13. Anteporre una riga in un file

#!/bin/bash
# prepend.sh: Aggiunge del testo all'inizio di un file.
#
#  Esempio fornito da Kenny Stauffer,
#+ leggermente modificato dall'autore del libro.


E_FILE_ERRATO=65

read -p "File: " file   # L'opzione -p di 'read' visualizza il prompt.
if [ ! -e "$file" ]
then   # Termina l'esecuzione se il file non esiste.
  echo "File $file non trovato."
  exit $E_FILE_ERRATO
fi

read -p "Titolo: " titolo
cat - $file <<<$titolo > $file.nuovo

echo "Il file modificato è $file.nuovo"

exit 0

# da '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.

Esempio 18-14. Analizzare un file mailbox

#!/bin/bash
#  Script di Francisco Lobo,
#+ commentato e leggermente modificato dall'autore de Guida ASB.
#  Usato in Guida ASB con il permesso dell'autore dello script . (Grazie!)

# Lo script non funziona con versioni Bash < 3.0.


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

mbox_grep()  # Analizza il file mailbox.
{
    declare -i corpo=0 corrispondenza=0
    declare -a data mittente
    declare mail intestazione valore


    while IFS= read -r mail
#         ^^^^                 Reimposta $IFS.
#  Altrimenti "read" elimina lo spazio iniziale e finale dal proprio input.

   do
       if [[ $mail =~ "^From " ]]   # Verifica il campo "From" del messaggio.
       then
          (( corpo  = 0 ))          # "Azzera" le variabili.
          (( corrispondenza = 0 ))
          unset data

       elif (( corpo ))
       then
            (( corrispondenza ))
            # echo "$mail"
            #  Decommentate la riga precedente se volete visualizzare l'intero 
            #+ corpo del messaggio.

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

          case "$intestazione" in
          [Ff][Rr][Oo][Mm] ) [[ $valore =~ "$2" ]] && (( corrispondernza++ )) ;;
          # Verifica della riga "From".
          [Dd][Aa][Tt][Ee] ) read -r -a data <<< "$valore" ;;
          #                                  ^^^
          # Verifica della riga "Date".
          [Rr][Ee][Cc][Ee][Ii][Vv][Ee][Dd] ) read -r -a mittente <<< "$valore" ;;
          #                                                      ^^^
          # Verifica dell'indirizzo IP (c'è stato dello spoofing?).
          esac

       else
          (( corpo++ ))
          (( corrispondenza  )) &&
          echo "MESSAGGIO ${data:+di: ${data[*]} }"
       #    L'intero array $data             ^
          echo "Indirizzo IP del mittente: ${mittente[1]}"
       #    Secondo campo della riga "Received"       ^

       fi


    done < "$1" # Redirezione dello stdout del file nel ciclo.
}


mbox_grep "$1"  # Invio del file mailbox alla funzione.

exit $?

# Esercizi:
# ---------
# 1) Suddividete la precedente funzione in più funzioni,
#+   per rendere lo script maggiormente leggibile.
# 2) Aggiungete altre analisi che verifichino diverse parole chiave.



$ mailbox_grep.sh scam_mail
--> MESSAGGIO di Thu, 5 Jan 2006 08:00:56 -0500 (EST) 
--> Indirizzo IP del mittente: 196.3.62.4

Esercizio: scoprite altri impieghi per le here string.