Successivo: , Su: Cloni   [Contenuti][Indice]


11.2.1 Ritagliare campi e colonne

Il programma di utilità cut seleziona, o “taglia” (cut), caratteri o campi dal suo standard input e li spedisce al suo standard output. I campi sono separati da caratteri TAB per default, ma è possibile fornire un’opzione dalla riga di comando per cambiare il campo delimitatore (cioè, il carattere che separa i campi). La definizione di campo di cut è meno generale di quella di awk.

Un uso comune del comando cut potrebbe essere quello di estrarre i nomi degli utenti correntemente collegati al sistema, a partire dall’output del comando who. Per esempio, la seguente pipeline genera una lista in ordine alfabetico, senza doppioni, degli utenti correntemente collegati al sistema:

who | cut -c1-8 | sort | uniq

Le opzioni per cut sono:

-c lista

Usare lista come lista di caratteri da ritagliare. Elementi all’interno della lista possono essere separati da virgole, e intervalli di caratteri possono essere separated da trattini. La lista ‘1-8,15,22-35’ specifica i caratteri da 1 a 8, 15, e da 22 a 35.

-f lista

Usare lista come lista di campi da ritagliare.

-d delimitatore

Usare delimitatore come carattere che separa i campi invece del carattere TAB.

-s

Evita la stampa di righe che non contengono il delimitatore di campo.

L’implementazione awk del comando cut usa la funzione di libreria getopt() (vedi la sezione Elaborare opzioni specificate sulla riga di comando) e la funzione di libreria join() (vedi la sezione Trasformare un vettore in una sola stringa).

Il programma inizia con un commento che descrive le opzioni, le funzioni di libreria necessarie, e una funzione sintassi() che stampa un messaggio ed esce. sintassi() è chiamato se si specificano degli argomenti non validi:

# cut.awk --- implementa cut in awk

# Opzioni:
#    -f lista    Ritagliare campi
#    -d c        Carattere di delimitazione di campo
#    -c lista    Ritagliare caratteri
#
#    -s          Sopprimere righe che non contengono il delimitatore
#
# Richiede le funzioni di libreria getopt() e join()

function sintassi()
{
    print("sintassi: cut [-f lista] [-d c] [-s] [file...]") > "/dev/stderr"
    print("sintassi: cut [-c lista] [file...]") > "/dev/stderr"
    exit 1
}

Subito dopo c’è una regola BEGIN che analizza le opzioni della riga di comando. Questa regola imposta FS a un solo carattere TAB, perché quello è il separatore di campo di cut per default. La regola poi imposta il separatore di campo in output allo stesso valore del separatore di campo in input. Un ciclo che usa getopt() esamina le opzioni della riga di comando. Una e una sola delle variabili per_campi o per_caratteri è impostata a "vero", per indicare che l’elaborazione sarà fatta per campi o per caratteri, rispettivamente. Quando si ritaglia per caratteri, il separatore di campo in output è impostato alla stringa nulla:

BEGIN {
    FS = "\t"    # default
    OFS = FS
    while ((c = getopt(ARGC, ARGV, "sf:c:d:")) != -1) {
        if (c == "f") {
            per_campi = 1
            lista_campi = Optarg
        } else if (c == "c") {
            per_caratteri = 1
            lista_campi = Optarg
            OFS = ""
        } else if (c == "d") {
            if (length(Optarg) > 1) {
                printf("cut: usa il primo carattere di %s" \
                       " come delimitatore\n", Optarg) > "/dev/stderr"
                Optarg = substr(Optarg, 1, 1)
            }
            fs = FS = Optarg
            OFS = FS
            if (FS == " ")    # mette specifica in formato awk
                FS = "[ ]"
        } else if (c == "s")
            sopprimi = 1
        else
            sintassi()
    }

    # Toglie opzioni da riga di comando
    for (i = 1; i < Optind; i++)
        ARGV[i] = ""

Nella scrittura del codice si deve porre particolare attenzione quando il delimitatore di campo è uno spazio. Usare un semplice spazio (" ") come valore per FS è sbagliato: awk separerebbe i campi con serie di spazi, TAB, e/o ritorni a capo, mentre devono essere separati solo da uno spazio. Per far questo, salviamo il carattere di spazio originale nella variabile fs per un uso futuro; dopo aver impostato FS a "[ ]" non è possibile usarlo direttamente per vedere se il carattere delimitatore di campo è nella stringa.

Si ricordi anche che dopo che si è finito di usare getopt() (come descritto nella Elaborare opzioni specificate sulla riga di comando), è necessario eliminare tutti gli elementi del vettore ARGV da 1 a Optind, in modo che awk non tenti di elaborare le opzioni della riga di comando come nomi-file.

Dopo aver elaborato le opzioni della riga di comando, il programma verifica che le opzioni siano coerenti. Solo una tra le opzioni -c e -f dovrebbe essere presente, ed entrambe richiedono una lista di campi. Poi il programma chiama prepara_lista_campi() oppure prepara_lista_caratteri() per preparare la lista dei campi o dei caratteri:

    if (per_campi && per_caratteri)
        sintassi()

    if (per_campi == 0 && per_caratteri == 0)
        per_campi = 1    # default

    if (lista_campi == "") {
        print "cut: specificare lista per -c o -f" > "/dev/stderr"
        exit 1
    }

    if (per_campi)
        prepara_lista_campi()
    else
        prepara_lista_caratteri()
}

prepara_lista_campi() pone la lista campi, usando la virgola come separatore, in un vettore. Poi, per ogni elemento del vettore, controlla che esso non sia un intervallo. Se è un intervallo, lo fa diventare un elenco. La funzione controlla l’intervallo specificato, per assicurarsi che il primo numero sia minore del secondo. Ogni numero nella lista è aggiunto al vettore lista_c, che semplicemente elenca i campi che saranno stampati. Viene usata la normale separazione in campi di awk. Il programma lascia ad awk il compito di separare i campi:

function prepara_lista_campi(        n, m, i, j, k, f, g)
{
    n = split(lista_campi, f, ",")
    j = 1    # indice in lista_c
    for (i = 1; i <= n; i++) {
        if (index(f[i], "-") != 0) { # un intervallo
            m = split(f[i], g, "-")
            if (m != 2 || g[1] >= g[2]) {
                printf("cut: lista campi errata: %s\n",
                                  f[i]) > "/dev/stderr"
                exit 1
            }
            for (k = g[1]; k <= g[2]; k++)
                lista_c[j++] = k
        } else
            lista_c[j++] = f[i]
    }
    ncampi = j - 1
}

La funzione prepara_lista_caratteri() è più complicata di prepara_lista_campi(). L’idea qui è di usare la variabile di gawk FIELDWIDTHS (vedi la sezione Leggere campi di larghezza costante), che descrive input a larghezza costante. Quando si usa una lista di caratteri questo è proprio il nostro caso.

Impostare FIELDWIDTHS è più complicato che semplicemente elencare i campi da stampare. Si deve tener traccia dei campi da stampare e anche dei caratteri che li separano, che vanno saltati. Per esempio, supponiamo che si vogliano i caratteri da 1 a 8, 15, e da 22 a 35. Per questo si specifica ‘-c 1-8,15,22-35’. Il valore che corrisponde a questo nella variabile FIELDWIDTHS è "8 6 1 6 14". Questi sono cinque campi, e quelli da stampare sono $1, $3, e $5. I campi intermedi sono riempitivo (filler), ossia è ciò che separa i dati che si desidera estrarre. lista_c lista i campi da stampare, e t traccia l’elenco completo dei campi, inclusi i riempitivi:

function prepara_lista_caratteri(    campo, i, j, f, g, n, m, t,
                          filler, ultimo, lungo)
{
    campo = 1   # contatore totale campi
    n = split(lista_campi, f, ",")
    j = 1       # indice in lista_c
    for (i = 1; i <= n; i++) {
        if (index(f[i], "-") != 0) { # intervallo
            m = split(f[i], g, "-")
            if (m != 2 || g[1] >= g[2]) {
                printf("cut: lista caratteri errata: %s\n",
                               f[i]) > "/dev/stderr"
                exit 1
            }
            lungo = g[2] - g[1] + 1
            if (g[1] > 1)  # calcola lunghezza del riempitivo
                filler = g[1] - ultimo - 1
            else
                filler = 0
            if (filler)
                t[campo++] = filler
            t[campo++] = lungo  # lunghezza del campo
            ultimo = g[2]
            lista_c[j++] = campo - 1
        } else {
            if (f[i] > 1)
                filler = f[i] - ultimo - 1
            else
                filler = 0
            if (filler)
                t[campo++] = filler
            t[campo++] = 1
            ultimo = f[i]
            lista_c[j++] = campo - 1
        }
    }
    FIELDWIDTHS = join(t, 1, campo - 1)
    ncampi = j - 1
}

Poi viene la regola che elabora i dati. Se l’opzione -s è stata specificata, il flag sopprimi è vero. La prima istruzione if accerta che il record in input abbia il separatore di campo. Se cut sta elaborando dei campi, e sopprimi è vero, e il carattere di separazione dei campi non è presente nel record, il record è ignorato.

Se il record è valido, gawk ha già separato i dati in campi, usando il carattere in FS o usando campi a lunghezza fissa e FIELDWIDTHS. Il ciclo scorre attraverso la lista di campi che si dovrebbero stampare. Il campo corrispondente è stampato se contiene dati. Se il campo successivo contiene pure dei dati, il carattere di separazione è scritto tra i due campi:

{
    if (per_campi && sopprimi && index($0, fs) == 0)
        next

    for (i = 1; i <= ncampi; i++) {
        if ($lista_c[i] != "") {
            printf "%s", $lista_c[i]
            if (i < ncampi && $lista_c[i+1] != "")
                printf "%s", OFS
        }
    }
    print ""
}

Questa versione di cut utilizza la variabile FIELDWIDTHS di gawk per ritagliare in base alla posizione dei caratteri. È possibile, in altre implementazioni di awk usare substr() (vedi la sezione Funzioni di manipolazione di stringhe), ma la cosa è molto più complessa. La variabile FIELDWIDTHS fornisce una soluzione elegante al problema di suddividere la riga in input in singoli caratteri.


Successivo: , Su: Cloni   [Contenuti][Indice]