Successivo: , Precedente: , Su: Programmi vari   [Contenuti][Indice]


11.3.9 Una maniera facile per usare funzioni di libreria

In Come includere altri file nel proprio programma, abbiamo visto come gawk preveda la possibilità di includere file. Tuttavia, questa è un’estensione gawk. Questa sezione evidenzia l’utilità di rendere l’inclusione di file disponibile per awk standard, e mostra come farlo utilizzando una combinazione di programmazione di shell e di awk.

Usare funzioni di libreria in awk può presentare molti vantaggi. Incoraggia il riutilizzo di codice e la scrittura di funzioni di tipo generale. I programmi sono più snelli e quindi più comprensibili. Tuttavia, usare funzioni di libreria è facile solo in fase di scrittura di programmi awk; è invece complicato al momento di eseguirli, rendendo necessario specificare molte opzioni -f. Se gawk non è disponibile, non lo sono neppure la variabile d’ambiente AWKPATH e la possibilità di conservare funzioni awk in una directory di libreria (vedi la sezione Opzioni sulla riga di comando). Sarebbe bello poter scrivere programmi nel modo seguente:

# funzioni di libreria
@include getopt.awk
@include join.awk
…

# programma principale
BEGIN {
    while ((c = getopt(ARGC, ARGV, "a:b:cde")) != -1)
        …
    …
}

Il programma seguente, igawk.sh, fornisce questo servizio. Simula la ricerca da parte di gawk della variabile d’ambiente AWKPATH e permette anche delle inclusioni nidificate (cioè, un file che è stato incluso tramite @include può contenere ulteriori istruzioni @include). igawk tenta di includere ogni file una volta sola, in modo che delle inclusioni nidificate non contengano accidentalmente una funzione di libreria più di una volta.

igawk dovrebbe comportarsi esternamente proprio come gawk. Questo vuol dire che dovrebbe accettare sulla riga di comando tutti gli argomenti di gawk, compresa la capacità di specificare più file sorgenti tramite l’opzione -f e la capacità di mescolare istruzioni da riga di comando e file di sorgenti di libreria.

Il programma è scritto usando il linguaggio della Shell POSIX (sh).85 Il funzionamento è il seguente:

  1. Esegue un ciclo attraverso gli argomenti, salvando tutto ciò che non si presenta come codice sorgente awk, per quando il programma espanso sarà eseguito.
  2. Per ogni argomento che rappresenta del codice awk, mette l’argomento in una variabile di shell che verrà espansa. Ci sono due casi:
    1. Un testo letterale, fornito con l’opzione -e o --source. Questo testo viene aggiunto direttamente in fondo.
    2. nomi-file sorgenti, forniti con l’opzione -f. Usiamo il trucchetto di aggiungere ‘@include nome_file’ in fondo ai contenuti della variabile di shell. Poiché il programma di inclusione dei file funziona allo stesso modo in cui funziona gawk, ne risulta che il file viene incluso nel programma al punto giusto.
  3. Esegue un programma (naturalmente awk) sui contenuti della variabile di shell per espandere le istruzioni @include. Il programma espanso è messo in una seconda variabile di shell.
  4. Esegue il programma espanso richiamando gawk e tutti gli altri argomenti originalmente forniti dall’utente sulla riga di comando (come p.es. dei nomi di file-dati).

Questo programma usa variabili di shell in quantità: per immagazzinare argomenti della riga di comando e il testo del programma awk che espanderà il programma dell’utente, per il programma originale dell’utente e per il programma espanso. Questo modo di procedere risolve potenziali problemi che potrebbero presentarsi se si usassero invece dei file temporanei, ma rende lo script un po’ più complicato.

La parte iniziale del programma attiva il tracciamento della shell se il primo argomento è ‘debug’.

La parte successiva esegue un ciclo che esamina ogni argomento della riga di comando. Ci sono parecchi casi da esaminare:

--

Quest’opzione termina gli argomenti per igawk. Tutto quel che segue dovrebbe essere passato al programma awk dell’utente senza essere preso in considerazione.

-W

Questo indica che l’opzione successiva è propria di gawk. Per facilitare l’elaborazione degli argomenti, l’opzione -W è aggiunta davanti agli argomenti rimanenti, e il ciclo continua. (Questo è un trucco di programmazione della sh. Non è il caso di preoccuparsene se non si ha familiarità con il comando sh.)

-v, -F

Queste opzioni sono conservate e lasciate da gestire a gawk.

-f, --file, --file=, -Wfile=

Il nome-file è aggiunto alla variabile di shell programma, insieme a un’istruzione @include. Il programma di utilità expr è usato per eliminare la parte iniziale dell’argomento (p.es., ‘--file=’). (La sintassi tipica di sh richiederebbe di usare il comando echo e il programma di utilità sed per far questo. Sfortunatamente, alcune versioni di echo valutano le sequenze di protezione contenute nei loro argomenti, e questo potrebbe finire per alterare il testo del programma. L’uso di expr evita questo problema.)

--source, --source=, -Wsource=

Il testo sorgente è aggiunto in fondo a programma.

--version, -Wversion

igawk stampa il proprio numero di versione, esegue ‘gawk --version’ per ottenere l’informazione relativa alla versione di gawk, ed esce.

Se nessuno degli argomenti -f, --file, -Wfile, --source, o -Wsource è stato fornito, il primo argomento che non è un’opzione dovrebbe essere il programma awk. Se non ci sono argomenti rimasti sulla riga di comando, igawk stampa un messaggio di errore ed esce. Altrimenti, il primo argomento è aggiunto in fondo a programma. In qualsiasi caso, dopo che gli argomenti sono stati elaborati, la variabile di shell programma contiene il testo completo del programma originale awk.

Il programma è il seguente:

#! /bin/sh
# igawk --- come gawk ma abilita l'uso di @include

if [ "$1" = debug ]
then
    set -x
    shift
fi

# Un ritorno a capo letterale,
# per formattare correttamente il testo del programma
n='
'

# Inizializza delle variabili alla stringa nulla
programma=
opts=

while [ $# -ne 0 ] # ciclo sugli argomenti
do
    case $1 in
    --)     shift
            break ;;

    -W)     shift
            # Il costrutto ${x?'messaggio qui'} stampa un
            # messaggio diagnostico se $x è la stringa nulla
            set -- -W"${@?'manca operando'}"
            continue ;;

    -[vF])  opts="$opts $1 '${2?'manca operando'}'"
            shift ;;

    -[vF]*) opts="$opts '$1'" ;;

    -f)     programma="$programma$n@include ${2?'manca operando'}"
            shift ;;

    -f*)    f=$(expr "$1" : '-f\(.*\)')
            programma="$programma$n@include $f" ;;

    -[W-]file=*)
            f=$(expr "$1" : '-.file=\(.*\)')
            programma="$programma$n@include $f" ;;

    -[W-]file)
            programma="$programma$n@include ${2?'manca operando'}"
            shift ;;

    -[W-]source=*)
            t=$(expr "$1" : '-.source=\(.*\)')
            programma="$programma$n$t" ;;

    -[W-]source)
            programma="$programma$n${2?'manca operando'}"
            shift ;;

    -[W-]version)
            echo igawk: version 3.0 1>&2
            gawk --version
            exit 0 ;;

    -[W-]*) opts="$opts '$1'" ;;

    *)      break ;;
    esac
    shift
done

if [ -z "$programma" ]
then
     programma=${1?'manca programma'}
     shift
fi

# A questo punto, `programma' contiene il programma.

Il programma awk che elabora le direttive @include è immagazzinato nella variabile di shell progr_che_espande. Ciò serve a mantenere leggibile lo script. Questo programma awk legge tutto il programma dell’utente, una riga per volta, usando getline (vedi la sezione Richiedere input usando getline). I nomi-file in input e le istruzioni @include sono gestiti usando una pila. Man mano che viene trovata una @include, il valore corrente di nome-file è “spinto” sulla pila e il file menzionato nella direttiva @include diventa il nome-file corrente. Man mano che un file è finito, la pila viene “disfatta”, e il precedente file in input diventa nuovamente il file in input corrente. Il processo viene iniziato ponendo il file originale come primo file sulla pila.

La funzione percorso() trova qual è il percorso completo di un file. Simula il comportamento di gawk quando utilizza la variabile d’ambiente AWKPATH (vedi la sezione Ricerca di programmi awk in una lista di directory.). Se un nome-file contiene una ‘/’, non viene effettuata la ricerca del percorso. Analogamente, se il nome-file è "-", viene usato senza alcuna modifica. Altrimenti, il nome-file è concatenato col nome di ogni directory nella lista dei percorsi, e vien fatto un tentativo per aprire il nome-file così generato. Il solo modo di controllare se un file è leggibile da awk è di andare avanti e tentare di leggerlo con getline; questo è quel che percorso() fa.86 Se il file può essere letto, viene chiuso e viene restituito il valore di nome-file:

progr_che_espande='

function percorso(file,    i, t, da_buttare)
{
    if (index(file, "/") != 0)
        return file

    if (file == "-")
        return file

    for (i = 1; i <= n_dir; i++) {
        t = (lista_percorsi[i] "/" file)
        if ((getline da_buttare < t) > 0) {
            # found it
            close(t)
            return t
        }
    }
    return ""
}

Il programma principale è contenuto all’interno di una regola BEGIN. La prima cosa che fa è di impostare il vettore lista_percorsi usato dalla funzione percorso(). Dopo aver diviso la lista usando come delimitatore ‘:’, gli elementi nulli sono sostituiti da ".", che rappresenta la directory corrente:

BEGIN {
    percorsi = ENVIRON["AWKPATH"]
    n_dir = split(percorsi, lista_percorsi, ":")
    for (i = 1; i <= n_dir; i++) {
        if (lista_percorsi[i] == "")
            lista_percorsi[i] = "."
    }

La pila è inizializzata con ARGV[1], che sarà "/dev/stdin". Il ciclo principale viene subito dopo. Le righe in input sono lette una dopo l’altra. Righe che non iniziano con @include sono stampate così come sono. Se la riga inizia con @include, il nome-file è in $2. La funzione percorso() è chiamata per generare il percorso completo. Se questo non riesce, il programma stampa un messaggio di errore e continua.

Subito dopo occorre controllare se il file sia già stato incluso. Il vettore gia_fatto è indicizzato dal nome completo di ogni nome-file incluso e tiene traccia per noi di questa informazione. Se un file viene visto più volte, viene stampato un messaggio di avvertimento. Altrimenti il nuovo nome-file è aggiunto alla pila e l’elaborazione continua.

Infine, quando getline giunge alla fine del file in input, il file viene chiuso, e la pila viene elaborata. Quando indice_pila è minore di zero, il programma è terminato:

    indice_pila = 0
    input[indice_pila] = ARGV[1] # ARGV[1] è il primo file

    for (; indice_pila >= 0; indice_pila--) {
        while ((getline < input[indice_pila]) > 0) {
            if (tolower($1) != "@include") {
                print
                continue
            }
            cammino = percorso($2)
            if (cammino == "") {
                printf("igawk: %s:%d: non riesco a trovare %s\n",
                    input[indice_pila], FNR, $2) > "/dev/stderr"
                continue
            }
            if (! (cammino in gia_fatto)) {
                gia_fatto[cammino] = input[indice_pila]
                input[++indice_pila] = cammino  # aggiungilo alla pila
            } else
                print $2, "incluso in", input[indice_pila],
                    "era già incluso in",
                    gia_fatto[cammino] > "/dev/stderr"
        }
        close(input[indice_pila])
    }
}'  # l'apice chiude la variabile `progr_che_espande'

programma_elaborato=$(gawk -- "$progr_che_espande" /dev/stdin << EOF
$programma
EOF
)

Il costrutto di shell ‘comando << marcatore’ è chiamato here document (documento sul posto). Ogni riga presente nello script di shell fino al marcatore è passato in input a comando. La shell elabora i contenuti dell’here document sostituendo, dove serve, variabili e comandi (ed eventualmente altre cose, a seconda della shell in uso).

Il costrutto di shell ‘$(…)’ è chiamato sostituzione di comando. L’output del comando posto all’interno delle parentesi è sostituito nella riga di comando. Poiché il risultato è usato in un assegnamento di variabile, viene salvato come un’unica stringa di caratteri, anche se il risultato contiene degli spazi bianchi.

Il programma espanso è salvato nella variabile programma_elaborato. Il tutto avviene secondo le fasi seguenti:

  1. Si esegue gawk con il programma che gestisce le @include (il valore della variabile di shell progr_che_espande) leggendo lo standard input.
  2. Lo standard input contiene il programma dell’utente, nella variabile di shell programma. L’input è passato a gawk tramite un here document.
  3. I risultati di questo processo sono salvati nella variabile di shell programma_elaborato usando la sostituzione di comando.

L’ultima fase è la chiamata a gawk con il programma espanso, insieme alle opzioni originali e agli argomenti della riga di comando che l’utente aveva fornito:

eval gawk $opts -- '"$programma_elaborato"' '"$@"'

Il comando eval è una struttura della shell che riesegue l’elaborazione dei parametri della riga di comando. Gli apici proteggono le parti restanti.

Questa versione di igawk è la quinta versione di questo programma. Ci sono quattro semplificazioni migliorative:

Inoltre, questo programma dimostra come spesso valga la pena di utilizzare insieme la programmazione della sh e quella di awk. Solitamente, si può fare parecchio senza dover ricorrere alla programmazione di basso livello in C o C++, ed è spesso più facile fare certi tipi di manipolazioni di stringhe e argomenti usando la shell, piuttosto che awk.

Infine, igawk dimostra che non è sempre necessario aggiungere nuove funzionalità a un programma; queste possono spesso essere aggiunte in cima.87


Note a piè di pagina

(85)

Una spiegazione dettagliata del linguaggio della sh non rientra negli intenti di questo libro. Qualche spiegazione sommaria viene fornita, ma se si desidera una comprensione più dettagliata, si dovrebbe consultare un buon libro sulla programmazione della shell.

(86)

In alcune versioni molto datate di awk, il test ‘getline da_buttare < t’ può ripetersi in un ciclo infinito se il file esiste ma è vuoto.

(87)

gawk è in grado di elaborare istruzioni @include al suo stesso interno, per permettere l’uso di programmi awk come script Web CGI.


Successivo: , Precedente: , Su: Programmi vari   [Contenuti][Indice]